diff --git a/.editorconfig b/.editorconfig index 1792072116d..a142caa1d25 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,9 +8,19 @@ indent_style = tab indent_size = 4 trim_trailing_whitespace = true +[composer.json] +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + + [*.md] trim_trailing_whitespace = false [*.js] indent_style = tab indent_size = 4 + +[*.yml] +indent_style = space +indent_size = 2 diff --git a/.env.example b/.env.example index 077d4a876d2..009a225a232 100644 --- a/.env.example +++ b/.env.example @@ -2,12 +2,37 @@ APP_NAME=Lychee APP_ENV=production APP_KEY= APP_DEBUG=false +# This MUST contain the host name up to the Top Level Domain (tld) e.g. .com, .org etc. APP_URL=http://localhost +APP_FORCE_HTTPS=false + +# If using Lychee in a sub folder, specify the path after the tld here. +# For example for https://lychee.test/path/to/lychee +# Set APP_URL=https://lychee.test +# and APP_DIR=/path/to/lychee +# We (LycheeOrg) do not recommend the use of APP_DIR. +# APP_DIR= # enable or disable debug bar. By default it is disabled. +# Do note that this disable CSP!! DEBUGBAR_ENABLED=false -LOG_CHANNEL=stack +# enable or disable the v6 layout. +VUEJS_ENABLED=true + +# enable or disable log viewer. By default it is enabled. +LOG_VIEWER_ENABLED=true + +# enable or disable clockwork. By default it is disabled (and not provided on non-dev build). +CLOCKWORK_ENABLE=false + +# enable s3 bucket (required in addition to needing AWS_ACCESS_KEY_ID) +# S3_ENABLED=true + +# If you spread old links of to your albums in your Lychee instance starting with +# https://lychee.text/#albumID/PhotoId +# Set this value to true to enable redirection. +LEGACY_V4_REDIRECT=false ############################################################################## # IMPORTANT: To migrate from Lychee v3 you *MUST* use the same MySQL/MariaDB # @@ -28,24 +53,59 @@ DB_PORT= DB_USERNAME= DB_PASSWORD= DB_LOG_SQL=false +DB_LOG_SQL_EXPLAIN=false #only for MySQL + +# List foreign keys in diagnostic page +DB_LIST_FOREIGN_KEYS=false + +# Application timezone. If not specified, the server's default timezone is used. +# Requires a named timezone identifier. +# See https://www.php.net/manual/en/timezones.php for the list of supported timezones. +# Don't use a timezone offset (like +01:00) or a timezone abbreviation (like CEST) +# TIMEZONE=Europe/Paris -TIMEZONE=UTC +# Visibility of directories and (media) files in LYCHEE_UPLOADS +# Possible values are: +# +# - private: world group has neither read nor write access +# - public: world group has read access but no write access (the default) +# - world: world group has read and write access +# +# The default should suffice for most installations. +# For improved security, change this setting to "private". +# Some rare setups may require directories and files to be world writeable. +# In this case, use "world" here. +# USE WITH PRECAUTIONS: world writeable files and folders may be a SECURITY RISK. +# LYCHEE_IMAGE_VISIBILITY=public # folders in which the files will be stored -# LYCHEE_DIST="/var/www/html/Lychee-Laravel/public/dist/" # LYCHEE_UPLOADS="/var/www/html/Lychee-Laravel/public/uploads/" - +# LYCHEE_DIST="/var/www/html/Lychee-Laravel/public/dist/" +# LYCHEE_SYM="/var/www/html/Lychee-Laravel/public/sym/" # url to access those files -# LYCHEE_DIST_URL="dist/" # LYCHEE_UPLOADS_URL="uploads/" +# LYCHEE_DIST_URL="dist/" +# LYCHEE_SYM_URL="sym/" + +# Support for token based authentication used by API requests. Enabled by default. +# ENABLE_TOKEN_AUTH=true -BROADCAST_DRIVER=log CACHE_DRIVER=file SESSION_DRIVER=file SESSION_LIFETIME=120 -QUEUE_DRIVER=sync +# `sync` if jobs needs to be executed live (default) or `database` if they can be defered. +QUEUE_CONNECTION=sync SECURITY_HEADER_HSTS_ENABLE=false +SECURITY_HEADER_CSP_CONNECT_SRC= +SECURITY_HEADER_SCRIPT_SRC_ALLOW= +SECURITY_HEADER_CSP_CHILD_SRC= +SECURITY_HEADER_CSP_FONT_SRC= +SECURITY_HEADER_CSP_FORM_ACTION= +SECURITY_HEADER_CSP_FRAME_SRC= +SECURITY_HEADER_CSP_IMG_SRC= +SECURITY_HEADER_CSP_MEDIA_SRC= +SESSION_SECURE_COOKIE=false REDIS_HOST=127.0.0.1 REDIS_PASSWORD=null @@ -60,10 +120,87 @@ MAIL_ENCRYPTION= MAIL_FROM_NAME= MAIL_FROM_ADDRESS= -PUSHER_APP_ID= -PUSHER_APP_KEY= -PUSHER_APP_SECRET= -PUSHER_APP_CLUSTER=mt1 +# The trusted proxies if Lychee is behind a reverse proxy +# Accepted values: +# - `null`: no proxy +# - `*`: any proxy +# - [,]: a comma-seperated list of IP addresses +TRUSTED_PROXIES=null + +# Comma-separated list of class names of diagnostics checks that should be skipped. +#SKIP_DIAGNOSTICS_CHECKS= + +VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}" +VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" + +# Oauth token data +# XXX_REDIRECT_URI should be left as default unless you know exactly what you do. + +# AMAZON_SIGNIN_CLIENT_ID= +# AMAZON_SIGNIN_SECRET= +# AMAZON_SIGNIN_REDIRECT_URI=/auth/amazon/redirect + +# https://developer.okta.com/blog/2019/06/04/what-the-heck-is-sign-in-with-apple +# Note: the client secret used for "Sign In with Apple" is a JWT token that can have a maximum lifetime of 6 months. +# The article above explains how to generate the client secret on demand and you'll need to update this every 6 months. +# To generate the client secret for each request, see Generating A Client Secret For Sign In With Apple On Each Request. +# https://bannister.me/blog/generating-a-client-secret-for-sign-in-with-apple-on-each-request +# APPLE_CLIENT_ID= +# APPLE_CLIENT_SECRET= +# APPLE_REDIRECT_URI=/auth/apple/redirect + +# FACEBOOK_CLIENT_ID= +# FACEBOOK_CLIENT_SECRET= +# FACEBOOK_REDIRECT_URI=/auth/facebook/redirect + +# GITHUB_CLIENT_ID= +# GITHUB_CLIENT_SECRET= +# GITHUB_REDIRECT_URI=/auth/github/redirect + +# GOOGLE_CLIENT_ID= +# GOOGLE_CLIENT_SECRET= +# GOOGLE_REDIRECT_URI=/auth/google/redirect + +# MASTODON_DOMAIN=https://mastodon.social +# MASTODON_ID= +# MASTODON_SECRET= +# MASTODON_REDIRECT_URI=/auth/mastodon/redirect + +# MICROSOFT_CLIENT_ID= +# MICROSOFT_CLIENT_SECRET= +# MICROSOFT_REDIRECT_URI=/auth/microsoft/redirect + +# NEXTCLOUD_CLIENT_ID= +# NEXTCLOUD_CLIENT_SECRET= +# NEXTCLOUD_REDIRECT_URI=/auth/nextcloud/redirect +# NEXTCLOUD_BASE_URI= + +# KEYCLOAK_CLIENT_ID= +# KEYCLOAK_CLIENT_SECRET= +# KEYCLOAK_REDIRECT_URI=/auth/keycloak/redirect +# KEYCLOAK_BASE_URL= +# KEYCLOAK_REALM= + +# AUTHENTIK_BASE_URL= +# AUTHENTIK_CLIENT_ID= +# AUTHENTIK_CLIENT_SECRET= +# AUTHENTIK_REDIRECT_URI=/auth/authentik/redirect + +# AWS support data + +# AWS_ACCESS_KEY_ID= +# AWS_SECRET_ACCESS_KEY= +# AWS_DEFAULT_REGION= +# AWS_BUCKET= +# AWS_URL= +# AWS_ENDPOINT= +# AWS_IMAGE_VISIBILITY= +# AWS_USE_PATH_STYLE_ENDPOINT= -MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}" -MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" +################################################################### +# Vite local development without running a server. # +# set VITE_LOCAL_DEV to true # +# set VITE_HTTP_PROXY_TARGET to the rediction for the API calls. # +################################################################### +# VITE_LOCAL_DEV=true +# VITE_HTTP_PROXY_TARGET=http://localhost:8000 diff --git a/.env.homestead b/.env.homestead deleted file mode 100644 index a10ca51e101..00000000000 --- a/.env.homestead +++ /dev/null @@ -1,56 +0,0 @@ -APP_NAME=Laravel -APP_ENV=local -APP_KEY= -APP_DEBUG=false -APP_URL=http://localhost - -LOG_CHANNEL=stack - -DB_CONNECTION=mysql -DB_HOST=127.0.0.1 -DB_PORT=3306 -DB_DATABASE=homestead -DB_USERNAME=homestead -DB_PASSWORD=secret -DB_LOG_SQL=false - -# leave empty or delete the line for default (empty string). -DB_OLD_LYCHEE_PREFIX= -# DB_OLD_LYCHEE_PREFIX=someprefix_ - -TIMEZONE=UTC - -# folders in which the files will be stored -# LYCHEE_DIST="/var/www/html/Lychee-Laravel/public/dist/" -# LYCHEE_UPLOADS="/var/www/html/Lychee-Laravel/public/uploads/" - -# url to access those files -# LYCHEE_DIST_URL="dist/" -# LYCHEE_UPLOADS_URL="uploads/" - -BROADCAST_DRIVER=log -CACHE_DRIVER=file -SESSION_DRIVER=file -SESSION_LIFETIME=120 -QUEUE_DRIVER=sync - -SECURITY_HEADER_HSTS_ENABLE=false - -REDIS_HOST=127.0.0.1 -REDIS_PASSWORD=null -REDIS_PORT=6379 - -MAIL_DRIVER=smtp -MAIL_HOST=smtp.mailtrap.io -MAIL_PORT=2525 -MAIL_USERNAME=null -MAIL_PASSWORD=null -MAIL_ENCRYPTION=null - -PUSHER_APP_ID= -PUSHER_APP_KEY= -PUSHER_APP_SECRET= -PUSHER_APP_CLUSTER=mt1 - -MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}" -MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" diff --git a/.gitattributes b/.gitattributes index cd914cdd5c1..29123fc9b91 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,4 @@ -* text=auto +* text=auto eol=lf *.css linguist-vendored *.scss linguist-vendored *.js linguist-vendored diff --git a/.github/DISCUSSION_TEMPLATE/bugs.yml b/.github/DISCUSSION_TEMPLATE/bugs.yml new file mode 100644 index 00000000000..a8422f32ff4 --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/bugs.yml @@ -0,0 +1,68 @@ +body: + - type: markdown + attributes: + value: | + Create a bug report to help us improve, do not ignore the [REQUIRED] sections. + We understand this can be frustrating, take your time and relax. We are usually + pretty quick to answer. :) + Valid bug report will be converted into proper issues to track their advancement. + - type: input + id: lychee-version + attributes: + label: Lychee version + description: Which version of Lychee are you using? Please provide the full version, e.g. v3.2.6. + placeholder: v5.1.2 + validations: + required: true + + - type: dropdown + id: php-version + attributes: + label: Which PHP version are you using? + options: + - PHP 8.4 + - PHP 8.3 + validations: + required: true + + - type: textarea + attributes: + label: Detailed description of the problem + description: A clear and concise description of what the bug is. + validations: + required: true + + - type: textarea + attributes: + label: Steps to reproduce the issue + description: | + Which steps do we need to take to reproduce the problem you are having? + placeholder: | + 1. Go to '...' + 2. Click on '....' + 3. Scroll down to '....' + 4. See error + + - type: textarea + attributes: + label: Diagnostics [REQUIRED] + description: | + Paste here is the content of your diagnostics. + *(Settings => Diagnostics or https://example.com/Diagnostics or `php artisan lychee:diagnostics`)* + placeholder: | + ``` + Paste result here + ``` + + - type: textarea + attributes: + label: Browser & System [REQUIRED] + + - type: checkboxes + attributes: + label: Please confirm (incomplete submissions will not be addressed) + options: + - label: I have provided easy and step-by-step instructions to reproduce the bug. + required: true + - label: I understand my bug report will be removed if I haven't met the criteria above. + required: true \ No newline at end of file diff --git a/.github/DISCUSSION_TEMPLATE/ideas.yml b/.github/DISCUSSION_TEMPLATE/ideas.yml new file mode 100644 index 00000000000..9c47bf9b846 --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/ideas.yml @@ -0,0 +1,14 @@ +body: + - type: markdown + attributes: + value: | + Suggest an idea for this project. :) + + Valid ideas will be converted into proper issues to track their advancement. + + - type: textarea + attributes: + label: Enhancement + description: Explain in a few words which functionality or improvements you would like to see in Lychee. + validations: + required: true \ No newline at end of file diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000000..0a843289459 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,13 @@ +# These are supported funding model platforms + +github: LycheeOrg # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +# patreon: # Replace with a single Patreon username +open_collective: LycheeOrg +# ko_fi: # Replace with a single Ko-fi username +# tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +# community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +# liberapay: # Replace with a single Liberapay username +# issuehunt: # Replace with a single IssueHunt username +# otechie: # Replace with a single Otechie username +# lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +# custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md deleted file mode 100644 index cfe77bae515..00000000000 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -name: Bug report -about: 'Create a report to help us improve, do not ignore the [REQUIRED] sections. - We understand this can be frustrating, take your time and relax. We are usually - pretty quick to answer. :) ' -title: '' -labels: '' -assignees: '' - ---- - -### Detailed description of the problem [REQUIRED] -*A clear and concise description of what the bug is.* - -### Steps to reproduce the issue -**Steps to reproduce the behavior:** -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error* - -**Screenshots** -*If applicable, add screenshots to help explain your problem.* - -### Output of the diagnostics [REQUIRED] -*(Settings => Diagnostics or https://example.com/Diagnostics or `php artisan lychee:diagnostics`)* - -### Browser and system diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000000..cdbf0a20ad9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: false +contact_links: + - name: I think I found a bug! + url: https://github.com/LycheeOrg/Lychee/discussions/new?category=bugs + about: Please submit your a bug here. + - name: I have an idea/suggestion. + url: https://github.com/LycheeOrg/Lychee/discussions/new?category=ideas + about: Please submit your ideas here. + - name: I have a question. + url: https://github.com/LycheeOrg/Lychee/discussions/new?category=q-a + about: Please ask and answer questions here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 41949180f0b..00000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: "[Enhancement]" -labels: '' -assignees: '' - ---- - -*Explain in a few words which functionality or improvements you would like to see in Lychee.* diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000000..29f8e890172 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,25 @@ + \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000000..4e0dfed5fd2 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,26 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + + - package-ecosystem: npm + directory: / + schedule: + interval: weekly + groups: + production-dependencies: + dependency-type: "production" + development-dependencies: + dependency-type: "development" + + - package-ecosystem: composer + directory: / + schedule: + interval: weekly + groups: + production-dependencies: + dependency-type: "production" + development-dependencies: + dependency-type: "development" \ No newline at end of file diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 00000000000..b06aac4921b --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,17 @@ +changelog: + exclude: + labels: + - ignore-for-release + authors: + - octocat + - dependabot + categories: + - title: 🏕 Features + labels: + - '*' + exclude: + labels: + - dependencies + - title: 👒 Dependencies + labels: + - dependencies diff --git a/.github/workflows/.env.legacy.mariadb b/.github/workflows/.env.legacy.mariadb new file mode 100644 index 00000000000..d5a1c3812fe --- /dev/null +++ b/.github/workflows/.env.legacy.mariadb @@ -0,0 +1,21 @@ +APP_NAME=Lychee +APP_URL=https://localhost +APP_ENV=dev +APP_KEY=SomeRandomString +APP_DEBUG=true +VUEJS_ENABLED=false +LEGACY_API_ENABLED=true + +DB_CONNECTION=mysql +DB_HOST=localhost +DB_PORT=3306 +DB_DATABASE=homestead_test +DB_USERNAME=root +DB_PASSWORD=root +DB_LIST_FOREIGN_KEYS=true + +CACHE_DRIVER=array +SESSION_DRIVER=array +QUEUE_CONNECTION=sync + +PHOTO_PIPES=true diff --git a/.github/workflows/.env.legacy.postgresql b/.github/workflows/.env.legacy.postgresql new file mode 100644 index 00000000000..2b83015fc72 --- /dev/null +++ b/.github/workflows/.env.legacy.postgresql @@ -0,0 +1,18 @@ +APP_NAME=Lychee +APP_URL=https://localhost +APP_ENV=dev +APP_KEY=SomeRandomString +APP_DEBUG=true +VUEJS_ENABLED=false +LEGACY_API_ENABLED=true + +DB_CONNECTION=pgsql +DB_HOST=localhost +DB_PORT=5432 +DB_DATABASE=postgres +DB_USERNAME=postgres +DB_PASSWORD=postgres + +CACHE_DRIVER=array +SESSION_DRIVER=array +QUEUE_CONNECTION=sync diff --git a/.github/workflows/.env.legacy.sqlite b/.github/workflows/.env.legacy.sqlite new file mode 100644 index 00000000000..ad87fb4867b --- /dev/null +++ b/.github/workflows/.env.legacy.sqlite @@ -0,0 +1,14 @@ +APP_NAME=Lychee +APP_URL=https://localhost +APP_ENV=dev +APP_KEY=SomeRandomString +APP_DEBUG=true +VUEJS_ENABLED=false +LEGACY_API_ENABLED=true + +DB_CONNECTION=sqlite +DB_LIST_FOREIGN_KEYS=true + +CACHE_DRIVER=array +SESSION_DRIVER=array +QUEUE_CONNECTION=sync diff --git a/.github/workflows/.env.mariadb b/.github/workflows/.env.mariadb index 2a62441e680..bdff1d00018 100644 --- a/.github/workflows/.env.mariadb +++ b/.github/workflows/.env.mariadb @@ -1,8 +1,10 @@ APP_NAME=Lychee APP_URL=https://localhost -APP_ENV=testing +APP_ENV=dev APP_KEY=SomeRandomString APP_DEBUG=true +VUEJS_ENABLED=true +LEGACY_API_ENABLED=false DB_CONNECTION=mysql DB_HOST=localhost @@ -10,7 +12,10 @@ DB_PORT=3306 DB_DATABASE=homestead_test DB_USERNAME=root DB_PASSWORD=root +DB_LIST_FOREIGN_KEYS=true CACHE_DRIVER=array SESSION_DRIVER=array -QUEUE_DRIVER=sync +QUEUE_CONNECTION=sync + +PHOTO_PIPES=true diff --git a/.github/workflows/.env.postgresql b/.github/workflows/.env.postgresql index f81b051b5be..2d6e00da19d 100644 --- a/.github/workflows/.env.postgresql +++ b/.github/workflows/.env.postgresql @@ -1,8 +1,10 @@ APP_NAME=Lychee APP_URL=https://localhost -APP_ENV=testing +APP_ENV=dev APP_KEY=SomeRandomString APP_DEBUG=true +VUEJS_ENABLED=true +LEGACY_API_ENABLED=false DB_CONNECTION=pgsql DB_HOST=localhost @@ -13,4 +15,4 @@ DB_PASSWORD=postgres CACHE_DRIVER=array SESSION_DRIVER=array -QUEUE_DRIVER=sync +QUEUE_CONNECTION=sync diff --git a/.github/workflows/.env.sqlite b/.github/workflows/.env.sqlite index 6f1cfc86318..d199e593699 100644 --- a/.github/workflows/.env.sqlite +++ b/.github/workflows/.env.sqlite @@ -1,11 +1,14 @@ APP_NAME=Lychee APP_URL=https://localhost -APP_ENV=testing +APP_ENV=dev APP_KEY=SomeRandomString APP_DEBUG=true +VUEJS_ENABLED=true +LEGACY_API_ENABLED=false DB_CONNECTION=sqlite +DB_LIST_FOREIGN_KEYS=true CACHE_DRIVER=array SESSION_DRIVER=array -QUEUE_DRIVER=sync +QUEUE_CONNECTION=sync diff --git a/.github/workflows/Build-Dist.yml b/.github/workflows/Build-Dist.yml deleted file mode 100644 index b0513a02cb3..00000000000 --- a/.github/workflows/Build-Dist.yml +++ /dev/null @@ -1,139 +0,0 @@ -name: Build-Dist - -# Run this workflow every time a new commit pushed to your repository -on: - push: - paths-ignore: - - '**/*.md' - pull_request: - paths-ignore: - - '**/*.md' - -jobs: - tests: - - runs-on: ${{ matrix.operating-system }} - # We want to run on external PRs, but not on our own internal PRs as they'll be run by the push to the branch. - if: (github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository) - - # Service containers to run with `container-job` - services: - # Label used to access the service container - postgres: - # Docker Hub image - image: postgres - # Provide the password for postgres - env: - POSTGRES_PASSWORD: postgres - # Set health checks to wait until postgres has started - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - ports: - # Maps tcp port 5432 on service container to the host - - 5432:5432 - - strategy: - matrix: - operating-system: [ubuntu-20.04] - php-versions: ['7.4', '8.0'] - sql-versions: ['mysql', 'postgresql', 'sqlite'] - - name: PHP ${{ matrix.php-versions }} - ${{ matrix.sql-versions }} - - env: - extensions: bcmath, curl, dom, gd, imagick, json, libxml, mbstring, pcntl, pdo, pdo_sqlite, pdo_mysql, pdo_pgsql, pgsql, sqlite3, zip, intl - key: cache-v1 # can be any string, change to clear the extension cache. - - steps: - - name: Cancel Previous Runs - uses: styfle/cancel-workflow-action@0.7.0 - with: - access_token: ${{ github.token }} - - # Checks out a copy of your repository on the ubuntu machine - - name: Checkout code - uses: actions/checkout@v2 - - - name: Setup cache environment - id: extcache - uses: shivammathur/cache-extensions@v1 - with: - php-version: ${{ matrix.php-versions }} - extensions: ${{ env.extensions }} - key: ${{ env.key }} - - - name: Cache PHP Extensions - uses: actions/cache@v2 - with: - path: ${{ steps.extcache.outputs.dir }} - key: ${{ steps.extcache.outputs.key }} - restore-keys: ${{ steps.extcache.outputs.key }} - - - name: Cache Composer Dependencies - uses: actions/cache@v1 - with: - path: ~/.composer/cache/files - key: dependencies-composer-${{ hashFiles('composer.json') }} - - - name: Setup PHP Action - uses: shivammathur/setup-php@2.8.0 - with: - php-version: ${{ matrix.php-versions }} - extensions: ${{ env.extensions }} - tools: pecl, composer - - - name: Set Up imagick (php8) - if: ${{ matrix.php-versions != '7.4' }} - run: sh scripts/install_imagick.sh - - - name: Install Exiftools - run: sudo apt-get -y install libimage-exiftool-perl - - - name: Get composer cache directory - id: composer-cache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" - - - name: Cache dependencies - uses: actions/cache@v2 - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} - restore-keys: ${{ runner.os }}-composer- - - - name: Install Composer dependencies - run: composer install --prefer-dist --no-interaction --no-dev - - - name: Build Dist - run: | - make dist-clean - - - name: setup MySQL Database - if: ${{ matrix.sql-versions == 'mysql' }} - run: | - sudo service mysql start - mysql -uroot -proot -e 'create database homestead_test;' - mysql -uroot -proot homestead_test < .github/workflows/v3.sql - cp .github/workflows/.env.mariadb Lychee/.env - - - name: setup PostGre Database - if: ${{ matrix.sql-versions == 'postgresql' }} - run: | - cp .github/workflows/.env.postgresql Lychee/.env - - - name: setup SQLite Database - if: ${{ matrix.sql-versions == 'sqlite' }} - run: | - touch database/database.sqlite - cp .github/workflows/.env.sqlite Lychee/.env - - - name: Generate secure key & Optimize application & Migrate & go backward - run: | - cd Lychee - pwd - php artisan key:generate - php artisan optimize - php artisan migrate - php artisan migrate:rollback \ No newline at end of file diff --git a/.github/workflows/Build-Full-SQL.yml b/.github/workflows/Build-Full-SQL.yml deleted file mode 100644 index 88781e23d22..00000000000 --- a/.github/workflows/Build-Full-SQL.yml +++ /dev/null @@ -1,157 +0,0 @@ -name: Build-Full-SQL - -# Run this workflow every time a new commit pushed to your repository -on: - push: - paths-ignore: - - '**/*.md' - pull_request: - paths-ignore: - - '**/*.md' - -jobs: - tests: - - runs-on: ${{ matrix.operating-system }} - # We want to run on external PRs, but not on our own internal PRs as they'll be run by the push to the branch. - if: (github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository) - - # Service containers to run with `container-job` - services: - # Label used to access the service container - postgres: - # Docker Hub image - image: postgres - # Provide the password for postgres - env: - POSTGRES_PASSWORD: postgres - # Set health checks to wait until postgres has started - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - ports: - # Maps tcp port 5432 on service container to the host - - 5432:5432 - - strategy: - matrix: - operating-system: [ubuntu-20.04] - php-versions: ['7.4', '8.0'] - sql-versions: ['mysql', 'postgresql', 'sqlite'] - - name: PHP ${{ matrix.php-versions }} - ${{ matrix.sql-versions }} - - env: - extensions: bcmath, curl, dom, gd, imagick, json, libxml, mbstring, pcntl, pdo, pdo_sqlite, pdo_mysql, pdo_pgsql, pgsql, sqlite3, zip, intl - key: cache-v1 # can be any string, change to clear the extension cache. - - steps: - - name: Cancel Previous Runs - uses: styfle/cancel-workflow-action@0.7.0 - with: - access_token: ${{ github.token }} - - # Checks out a copy of your repository on the ubuntu machine - - name: Checkout code - uses: actions/checkout@v2 - - - name: Setup cache environment - id: extcache - uses: shivammathur/cache-extensions@v1 - with: - php-version: ${{ matrix.php-versions }} - extensions: ${{ env.extensions }} - key: ${{ env.key }} - - - name: Cache PHP Extensions - uses: actions/cache@v2 - with: - path: ${{ steps.extcache.outputs.dir }} - key: ${{ steps.extcache.outputs.key }} - restore-keys: ${{ steps.extcache.outputs.key }} - - - name: Cache Composer Dependencies - uses: actions/cache@v1 - with: - path: ~/.composer/cache/files - key: dependencies-composer-${{ hashFiles('composer.json') }} - - - name: Setup PHP Action - uses: shivammathur/setup-php@2.8.0 - with: - php-version: ${{ matrix.php-versions }} - extensions: ${{ env.extensions }} - coverage: xdebug - tools: pecl, composer - - - name: Set Up imagick (php8) - if: ${{ matrix.php-versions != '7.4' }} - run: sh scripts/install_imagick.sh - # run: | - # git clone https://github.com/Imagick/imagick.git - # cd imagick && phpize && ./configure && make - # DEST=$(php -i | grep 'extension_dir => /') - # php -i | grep 'extension_dir => /' - # php -i | grep 'extension_dir' - # echo $DEST - # DEST2=$(echo "${DEST##* }") - # echo 'Copying imagick.so to ' $DEST2 - # sudo cp modules/imagick.so $DEST2 - # echo 'Update php.ini file at ' $(echo $(php --ini | grep 'Loaded Configuration File') | awk 'NF>1{print $NF}') - # sudo echo 'extension="imagick.so"' >> $(echo $(php --ini | grep 'Loaded Configuration File') | awk 'NF>1{print $NF}') - # cd .. - - - name: Install Exiftools - run: sudo apt-get -y install libimage-exiftool-perl - - - name: Get composer cache directory - id: composer-cache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" - - - name: Cache dependencies - uses: actions/cache@v2 - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} - restore-keys: ${{ runner.os }}-composer- - - - name: Install Composer dependencies - run: composer install --prefer-dist --no-interaction - - - name: Check Syntax - run: vendor/bin/php-cs-fixer fix --config=.php_cs --verbose --diff --dry-run - - - name: setup MySQL Database - if: ${{ matrix.sql-versions == 'mysql' }} - run: | - sudo service mysql start - mysql -uroot -proot -e 'create database homestead_test;' - cp .github/workflows/.env.mariadb .env - - - name: setup PostGre Database - if: ${{ matrix.sql-versions == 'postgresql' }} - run: | - cp .github/workflows/.env.postgresql .env - - - name: setup SQLite Database - if: ${{ matrix.sql-versions == 'sqlite' }} - run: | - touch database/database.sqlite - cp .github/workflows/.env.sqlite .env - - - name: Generate secure key & Optimize application & Migrate - run: | - php artisan key:generate - php artisan optimize - php artisan migrate - - - name: Apply tests - run: XDEBUG_MODE=coverage vendor/bin/phpunit --verbose - - - name: Codecov - uses: codecov/codecov-action@v1 - - - name: Make sure we can go backward - run: php artisan migrate:rollback \ No newline at end of file diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml new file mode 100644 index 00000000000..714e9c9834f --- /dev/null +++ b/.github/workflows/CICD.yml @@ -0,0 +1,271 @@ +name: Integrate + +# Run this workflow every time a new commit pushed to your repository +on: + push: + paths-ignore: + - '**/*.md' + - 'public/dist/*.js' + - 'public/dist/**/*.js' + - 'public/Lychee-front' + pull_request: + paths-ignore: + - '**/*.md' + - 'public/dist/*.js' + - 'public/dist/**/*.js' + - 'public/Lychee-front' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Declare default permissions as read only. +permissions: read-all + +jobs: + kill_previous: + name: 0️⃣ Kill previous runs + runs-on: ubuntu-latest + # We want to run on external PRs, but not on our own internal PRs as they'll be run by the push to the branch. + if: (github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository) + steps: + - name: Harden Runner + uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3 + with: + egress-policy: audit + + - name: Cancel Previous Runs + uses: styfle/cancel-workflow-action@85880fa0301c86cca9da44039ee3bb12d3bedbfa # 0.12.1 + with: + access_token: ${{ github.token }} + + php_syntax_errors: + name: 1️⃣ PHP 8.3 - Syntax errors + runs-on: ubuntu-latest + needs: + - kill_previous + steps: + - name: Harden Runner + uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3 + with: + egress-policy: audit + + - name: Setup PHP Action + uses: shivammathur/setup-php@9e72090525849c5e82e596468b86eb55e9cc5401 # 2.32.0 + with: + php-version: 8.3 + + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Install dependencies + uses: ramsey/composer-install@57532f8be5bda426838819c5ee9afb8af389d51a # 3.0.0 + + - name: Check source code for syntax errors + run: vendor/bin/parallel-lint --exclude .git --exclude vendor . + + code_style_errors: + name: 2️⃣ PHP 8.3 - Code Style errors + runs-on: ubuntu-latest + needs: + - php_syntax_errors + steps: + - name: Harden Runner + uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3 + with: + egress-policy: audit + + - name: Set up PHP + uses: shivammathur/setup-php@9e72090525849c5e82e596468b86eb55e9cc5401 # 2.32.0 + with: + php-version: 8.3 + + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Install dependencies + uses: ramsey/composer-install@57532f8be5bda426838819c5ee9afb8af389d51a # 3.0.0 + + - name: Check source code for code style errors + run: PHP_CS_FIXER_IGNORE_ENV=1 vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.php --verbose --diff --dry-run + + check_js: + name: 2️⃣ JS front-end + uses: ./.github/workflows/js_check.yml + needs: + - php_syntax_errors + + phpstan: + name: 2️⃣ PHP 8.3 - PHPStan + runs-on: ubuntu-latest + needs: + - php_syntax_errors + steps: + - name: Harden Runner + uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3 + with: + egress-policy: audit + + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Setup PHP + uses: shivammathur/setup-php@9e72090525849c5e82e596468b86eb55e9cc5401 # 2.32.0 + with: + php-version: 8.3 + coverage: none + + - name: Install Composer dependencies + uses: ramsey/composer-install@57532f8be5bda426838819c5ee9afb8af389d51a # 3.0.0 + + - name: Run PHPStan + run: vendor/bin/phpstan analyze + + tests_legacy: + name: 2️⃣ PHP tests legacy + needs: + - php_syntax_errors + uses: ./.github/workflows/php_tests.yml + with: + test-suite: 'Feature_v1' + env-file: '.env.legacy' + secrets: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + tests: + name: 2️⃣ PHP tests + needs: + - php_syntax_errors + uses: ./.github/workflows/php_tests.yml + with: + test-suite: 'Unit,Feature_v2' + env-file: '.env' + secrets: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + dist: + name: 3️⃣ PHP dist + needs: + - code_style_errors + uses: ./.github/workflows/php_dist.yml + + createArtifact: + name: 4️⃣ Build Artifact + if: github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/') + needs: + - phpstan + - dist + - tests + - tests_legacy + - check_js + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + attestations: write + env: + extensions: bcmath, curl, dom, gd, imagick, json, libxml, mbstring, pcntl, pdo, pdo_sqlite, pdo_mysql, pdo_pgsql, pgsql, sqlite3, zip + + steps: + - name: Harden Runner + uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3 + with: + egress-policy: audit + + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Setup PHP + uses: shivammathur/setup-php@9e72090525849c5e82e596468b86eb55e9cc5401 # 2.32.0 + with: + php-version: 8.3 + extensions: ${{ env.extensions }} + coverage: none + + - name: Use Node.js 20 + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 + with: + node-version: 20 + + - name: Build Dist + run: | + make clean dist + + - name: Upload build artifact + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + with: + name: Lychee.zip + path: Lychee.zip + if-no-files-found: error # 'warn' or 'ignore' are also available, defaults to `warn` + + - name: Attest + uses: actions/attest-build-provenance@7668571508540a607bdfd90a87a560489fe372eb # v2.1.0 + with: + # Path to the artifact serving as the subject of the attestation. Must + # specify exactly one of "subject-path" or "subject-digest". May contain a + # glob pattern or list of paths (total subject count cannot exceed 2500). + subject-path: '${{ github.workspace }}/Lychee.zip' + + # SHA256 digest of the subject for the attestation. Must be in the form + # "sha256:hex_digest" (e.g. "sha256:abc123..."). Must specify exactly one + # of "subject-path" or "subject-digest". + # subject-digest: + + # Subject name as it should appear in the attestation. Required unless + # "subject-path" is specified, in which case it will be inferred from the + # path. + # subject-name: Lychee + + # Whether to push the attestation to the image registry. Requires that the + # "subject-name" parameter specify the fully-qualified image name and that + # the "subject-digest" parameter be specified. Defaults to false. + # push-to-registry: + + # Whether to attach a list of generated attestations to the workflow run + # summary page. Defaults to true. + # show-summary: + + # The GitHub token used to make authenticated API requests. Default is + # ${{ github.token }} + github-token: ${{ github.token }} + + + release: + name: 5️⃣ Release + if: startsWith(github.ref, 'refs/tags/') + needs: + - createArtifact + runs-on: ubuntu-latest + permissions: + contents: write + id-token: write + env: + extensions: bcmath, curl, dom, gd, imagick, json, libxml, mbstring, pcntl, pdo, pdo_sqlite, pdo_mysql, pdo_pgsql, pgsql, sqlite3, zip + + steps: + - name: Install Cosign + uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0 + + - name: Download generated artifact + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + name: Lychee.zip + + # https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable + - name: Sign release with a key + run: | + cosign sign-blob --yes --key env://COSIGN_PRIVATE_KEY --output-signature Lychee.zip.asc Lychee.zip + env: + COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }} + COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }} + + - name: Create release + uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda # v2.2.1 + with: + files: | + Lychee.zip.asc + Lychee.zip + token: ${{ secrets.GITHUB_TOKEN }} + generate_release_notes: true + make_latest: true + \ No newline at end of file diff --git a/.github/workflows/CICD_no_legacy.yml b/.github/workflows/CICD_no_legacy.yml new file mode 100644 index 00000000000..8537dd6cba8 --- /dev/null +++ b/.github/workflows/CICD_no_legacy.yml @@ -0,0 +1,198 @@ +name: Integrate without legacy support + +# Run this workflow every time a new commit pushed to your repository +on: + push: + paths-ignore: + - '**/*.md' + - 'public/dist/*.js' + - 'public/dist/**/*.js' + - 'public/Lychee-front' + pull_request: + paths-ignore: + - '**/*.md' + - 'public/dist/*.js' + - 'public/dist/**/*.js' + - 'public/Lychee-front' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Declare default permissions as read only. +permissions: read-all + +jobs: + kill_previous: + name: 0️⃣ Kill previous runs + runs-on: ubuntu-latest + # We want to run on external PRs, but not on our own internal PRs as they'll be run by the push to the branch. + if: (github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository) + steps: + - name: Harden Runner + uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3 + with: + egress-policy: audit + + - name: Cancel Previous Runs + uses: styfle/cancel-workflow-action@85880fa0301c86cca9da44039ee3bb12d3bedbfa # 0.12.1 + with: + access_token: ${{ github.token }} + + php_syntax_errors: + name: 1️⃣ PHP 8.4 - Syntax errors + runs-on: ubuntu-latest + needs: + - kill_previous + steps: + - name: Harden Runner + uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3 + with: + egress-policy: audit + + - name: Setup PHP Action + uses: shivammathur/setup-php@9e72090525849c5e82e596468b86eb55e9cc5401 # 2.32.0 + with: + php-version: 8.4 + + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Removing legacy files + run: | + sh scripts/delete_legacy.sh + + - name: Install dependencies + uses: ramsey/composer-install@57532f8be5bda426838819c5ee9afb8af389d51a # 3.0.0 + + - name: Check source code for syntax errors + run: vendor/bin/parallel-lint --exclude .git --exclude vendor . + + code_style_errors: + name: 2️⃣ PHP 8.4 - Code Style errors + runs-on: ubuntu-latest + needs: + - php_syntax_errors + steps: + - name: Harden Runner + uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3 + with: + egress-policy: audit + + - name: Set up PHP + uses: shivammathur/setup-php@9e72090525849c5e82e596468b86eb55e9cc5401 # 2.32.0 + with: + php-version: 8.4 + + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Removing legacy files + run: | + sh scripts/delete_legacy.sh + + - name: Install dependencies + uses: ramsey/composer-install@57532f8be5bda426838819c5ee9afb8af389d51a # 3.0.0 + + - name: Check source code for code style errors + run: PHP_CS_FIXER_IGNORE_ENV=1 vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.php --verbose --diff --dry-run + + check_js: + uses: ./.github/workflows/js_check.yml + needs: + - php_syntax_errors + name: 2️⃣ JS front-end + + phpstan: + name: 2️⃣ PHP 8.4 - PHPStan + runs-on: ubuntu-latest + needs: + - php_syntax_errors + steps: + - name: Harden Runner + uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3 + with: + egress-policy: audit + + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Removing legacy files + run: | + sh scripts/delete_legacy.sh + + - name: Setup PHP + uses: shivammathur/setup-php@9e72090525849c5e82e596468b86eb55e9cc5401 # 2.32.0 + with: + php-version: 8.4 + coverage: none + + - name: Install Composer dependencies + uses: ramsey/composer-install@57532f8be5bda426838819c5ee9afb8af389d51a # 3.0.0 + + - name: Run PHPStan + run: vendor/bin/phpstan analyze + + tests: + name: 2️⃣ PHP tests + needs: + - php_syntax_errors + uses: ./.github/workflows/php_no_legacy_tests.yml + with: + test-suite: 'Unit,Feature_v2' + env-file: '.env' + secrets: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + dist: + name: 3️⃣ PHP dist + needs: + - code_style_errors + uses: ./.github/workflows/php_no_legacy_dist.yml + + createArtifact: + name: 4️⃣ Build Artifact + if: github.ref == 'refs/heads/master' + needs: + - phpstan + - dist + - tests + - check_js + runs-on: ubuntu-latest + env: + extensions: bcmath, curl, dom, gd, imagick, json, libxml, mbstring, pcntl, pdo, pdo_sqlite, pdo_mysql, pdo_pgsql, pgsql, sqlite3, zip + + steps: + - name: Harden Runner + uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3 + with: + egress-policy: audit + + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Removing legacy files + run: | + sh scripts/delete_legacy.sh + + - name: Setup PHP + uses: shivammathur/setup-php@9e72090525849c5e82e596468b86eb55e9cc5401 # 2.32.0 + with: + php-version: 8.4 + extensions: ${{ env.extensions }} + coverage: none + + - name: Use Node.js 20 + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 + with: + node-version: 20 + + - name: Build Dist + run: | + make clean dist + + - name: Upload a Build Artifact + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + with: + name: Lychee-No-Legacy.zip + path: Lychee.zip + if-no-files-found: error # 'warn' or 'ignore' are also available, defaults to `warn` diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000000..37aad02ded8 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,78 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: ["master"] + pull_request: + # The branches below must be a subset of the branches above + branches: ["master"] + schedule: + - cron: "0 0 * * 1" + +permissions: + contents: read + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: ["typescript"] + # CodeQL supports [ $supported-codeql-languages ] + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Harden Runner + uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3 + with: + egress-policy: audit + + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 00000000000..922916c5101 --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,27 @@ +# Dependency Review Action +# +# This Action will scan dependency manifest files that change as part of a Pull Request, +# surfacing known-vulnerable versions of the packages declared or updated in the PR. +# Once installed, if the workflow run is marked as required, +# PRs introducing known-vulnerable packages will be blocked from merging. +# +# Source repository: https://github.com/actions/dependency-review-action +name: 'Dependency Review' +on: [pull_request] + +permissions: + contents: read + +jobs: + dependency-review: + runs-on: ubuntu-latest + steps: + - name: Harden Runner + uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3 + with: + egress-policy: audit + + - name: 'Checkout Repository' + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: 'Dependency Review' + uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4.5.0 diff --git a/.github/workflows/js_check.yml b/.github/workflows/js_check.yml new file mode 100644 index 00000000000..b3da904b6bd --- /dev/null +++ b/.github/workflows/js_check.yml @@ -0,0 +1,41 @@ +name: Check JS + +on: + workflow_call: + +permissions: + contents: read + +jobs: + check_js: + name: Node ${{ matrix.node-version }} + runs-on: ubuntu-latest + strategy: + matrix: + node-version: + - 20 + steps: + - name: Harden Runner + uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3 + with: + egress-policy: audit + + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 + with: + node-version: ${{ matrix.node-version }} + + - name: Install + run: npm ci -D + + - name: Check Style + run: npm run check-formatting + + - name: Check TypeScript + run: npm run check + + - name: Compile Front-end + run: npm run build diff --git a/.github/workflows/php-cs-fixer.yml b/.github/workflows/php-cs-fixer.yml deleted file mode 100644 index 32c3549fa24..00000000000 --- a/.github/workflows/php-cs-fixer.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: php-cs-fixer -on: [push, pull_request] -env: - PR_NUMBER: "${{ github.event.number }}" - SOURCE_BRANCH: "$GITHUB_HEAD_REF" - FIXER_BRANCH: "auto-fixed/$GITHUB_HEAD_REF" - TITLE: "Apply fixes from PHP-CS-Fixer" - DESCRIPTION: "This merge request applies PHP code style fixes from an analysis carried out through GitHub Actions." -jobs: - php-cs-fixer: - if: github.event_name == 'pull_request' && ! startsWith(github.ref, 'refs/heads/auto-fixed/') - runs-on: ubuntu-20.04 - name: Run PHP CS Fixer - steps: - - name: Checkout Code - uses: actions/checkout@v2 - - name: Setup PHP - uses: shivammathur/setup-php@2.7.0 - with: - php-version: 7.4 - extensions: json, dom, curl, libxml, mbstring - coverage: none - - name: Install PHP-CS-Fixer - run: | - curl -L https://github.com/FriendsOfPHP/PHP-CS-Fixer/releases/download/v2.18.6/php-cs-fixer.phar -o .github/build/php-cs-fixer - chmod a+x .github/build/php-cs-fixer - - name: Prepare Git User - run: | - git config --global user.name "github-actions[bot]" - git config --global user.email "action@github.com" - git checkout -B "${{ env.FIXER_BRANCH }}" - - name: Apply auto-fixers - run: php .github/build/php-cs-fixer fix --config=.php_cs - - name: Create Fixer PR - run: | - if [[ -z $(git status --porcelain) ]]; then - echo "Nothing to fix.. Exiting." - exit 0 - fi - OPEN_PRS=`curl --silent -H "Accept: application/vnd.github.v3+json" -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" "https://api.github.com/repos/$GITHUB_REPOSITORY/pulls?state=open"` - OPEN_FIXER_PRS=`echo ${OPEN_PRS} | grep -o "\"ref\": \"${{ env.FIXER_BRANCH }}\"" | wc -l` - git commit -am "${{ env.TITLE }}" - git push origin "${{ env.FIXER_BRANCH }}" --force - if [ ${OPEN_FIXER_PRS} -eq "0" ]; then - curl -X POST \ - -H "Accept: application/vnd.github.v3+json" \ - -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ - "https://api.github.com/repos/$GITHUB_REPOSITORY/pulls" \ - -d "{ \"head\":\"${{ env.FIXER_BRANCH }}\", \"base\":\"${{ env.SOURCE_BRANCH }}\", \"title\":\"${{ env.TITLE }}\", \"body\":\"${{ env.DESCRIPTION }}\n\nTriggered by #${{ env.PR_NUMBER }}\" }" - fi - exit 1 \ No newline at end of file diff --git a/.github/workflows/php_dist.yml b/.github/workflows/php_dist.yml new file mode 100644 index 00000000000..4fb9fedb785 --- /dev/null +++ b/.github/workflows/php_dist.yml @@ -0,0 +1,88 @@ +name: PHP Dist + +on: + workflow_call: + +permissions: + contents: read + +jobs: + php_dist: + name: ${{ matrix.php-version }} - ${{ matrix.sql-versions }} + runs-on: ubuntu-latest + strategy: + matrix: + php-version: + - 8.3 + sql-versions: + - mariadb + - postgresql + - sqlite + # Service containers to run with `container-job` + services: + # Label used to access the service container + postgres: + # Docker Hub image + image: postgres + # Provide the password for postgres + env: + POSTGRES_PASSWORD: postgres + # Set health checks to wait until postgres has started + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + # Maps tcp port 5432 on service container to the host + - 5432:5432 + env: + extensions: bcmath, curl, dom, gd, imagick, json, libxml, mbstring, pcntl, pdo, pdo_sqlite, pdo_mysql, pdo_pgsql, pgsql, sqlite3, zip + + steps: + - name: Harden Runner + uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3 + with: + egress-policy: audit + + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Set Up Imagick, FFmpeg & Exiftools + run: | + sudo apt-get update + sudo apt-get --fix-broken install + sudo apt-get -y install ffmpeg libimage-exiftool-perl + + - name: setup Databases + run: | + sudo service mysql start + touch database/database.sqlite + mysql -uroot -proot -e 'create database homestead_test;' + + - name: Setup PHP Action + uses: shivammathur/setup-php@9e72090525849c5e82e596468b86eb55e9cc5401 # 2.32.0 + with: + php-version: ${{ matrix.php-version }} + extensions: ${{ env.extensions }} + tools: pecl, composer + + - name: Build Dist + run: | + make dist-clean + + - name: copy Env + run: | + cp .github/workflows/.env.${{ matrix.sql-versions }} Lychee/.env + + - name: setup MySQL Database with v3 + run: | + mysql -uroot -proot homestead_test < .github/workflows/v3.sql + + - name: Generate secure key & Migrate & go backward + run: | + cd Lychee + pwd + php artisan key:generate + php artisan migrate + php artisan migrate:rollback diff --git a/.github/workflows/php_no_legacy_dist.yml b/.github/workflows/php_no_legacy_dist.yml new file mode 100644 index 00000000000..93dde1ac622 --- /dev/null +++ b/.github/workflows/php_no_legacy_dist.yml @@ -0,0 +1,92 @@ +name: Check JS + +on: + workflow_call: + +permissions: + contents: read + +jobs: + php_dist: + name: ${{ matrix.php-version }} - ${{ matrix.sql-versions }} + runs-on: ubuntu-latest + strategy: + matrix: + php-version: + - 8.4 + sql-versions: + - mariadb + - postgresql + - sqlite + # Service containers to run with `container-job` + services: + # Label used to access the service container + postgres: + # Docker Hub image + image: postgres + # Provide the password for postgres + env: + POSTGRES_PASSWORD: postgres + # Set health checks to wait until postgres has started + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + # Maps tcp port 5432 on service container to the host + - 5432:5432 + env: + extensions: bcmath, curl, dom, gd, imagick, json, libxml, mbstring, pcntl, pdo, pdo_sqlite, pdo_mysql, pdo_pgsql, pgsql, sqlite3, zip + + steps: + - name: Harden Runner + uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3 + with: + egress-policy: audit + + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Removing legacy files + run: | + sh scripts/delete_legacy.sh + + - name: Set Up Imagick, FFmpeg & Exiftools + run: | + sudo apt-get update + sudo apt-get --fix-broken install + sudo apt-get -y install ffmpeg libimage-exiftool-perl + + - name: setup Databases + run: | + sudo service mysql start + touch database/database.sqlite + mysql -uroot -proot -e 'create database homestead_test;' + + - name: Setup PHP Action + uses: shivammathur/setup-php@9e72090525849c5e82e596468b86eb55e9cc5401 # 2.32.0 + with: + php-version: ${{ matrix.php-version }} + extensions: ${{ env.extensions }} + tools: pecl, composer + + - name: Build Dist + run: | + make dist-clean + + - name: copy Env + run: | + cp .github/workflows/.env.${{ matrix.sql-versions }} Lychee/.env + + - name: setup MySQL Database with v3 + run: | + mysql -uroot -proot homestead_test < .github/workflows/v3.sql + + - name: Generate secure key & Migrate & go backward + run: | + cd Lychee + pwd + php artisan key:generate + php artisan migrate + php artisan migrate:rollback diff --git a/.github/workflows/php_no_legacy_tests.yml b/.github/workflows/php_no_legacy_tests.yml new file mode 100644 index 00000000000..dcf9efba8e9 --- /dev/null +++ b/.github/workflows/php_no_legacy_tests.yml @@ -0,0 +1,123 @@ +name: Check JS + +on: + workflow_call: + inputs: + test-suite: + required: true + type: string + description: 'The test suite to run' + env-file: + required: true + type: string + description: 'The env files to use' + secrets: + SONAR_TOKEN: + required: true + description: 'sonar token secret' + CODECOV_TOKEN: + required: true + description: 'codecov token secret' + +permissions: + contents: read + +jobs: + tests: + permissions: + contents: read # for actions/checkout to fetch code + pull-requests: read # for SonarSource/sonarqube-scan-action to determine which PR to decorate + name: ${{ matrix.php-version }} - ${{ matrix.sql-versions }} -- ${{ inputs.test-suite }} + runs-on: ubuntu-latest + strategy: + matrix: + php-version: + - 8.4 + sql-versions: + - mariadb + - postgresql + - sqlite + # Service containers to run with `container-job` + services: + # Label used to access the service container + postgres: + # Docker Hub image + image: postgres + # Provide the password for postgres + env: + POSTGRES_PASSWORD: postgres + # Set health checks to wait until postgres has started + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + # Maps tcp port 5432 on service container to the host + - 5432:5432 + env: + extensions: bcmath, curl, dom, gd, imagick, json, libxml, mbstring, pcntl, pdo, pdo_sqlite, pdo_mysql, pdo_pgsql, pgsql, sqlite3, zip + + steps: + - name: Harden Runner + uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3 + with: + egress-policy: audit + + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Removing legacy files + run: | + sh scripts/delete_legacy.sh + + - name: Set Up Imagick, FFmpeg & Exiftools + run: | + sudo apt-get update + sudo apt-get --fix-broken install + sudo apt-get -y install ffmpeg libimage-exiftool-perl + + - name: setup Databases + run: | + sudo service mysql start + touch database/database.sqlite + mysql -uroot -proot -e 'create database homestead_test;' + + - name: Setup PHP Action + uses: shivammathur/setup-php@9e72090525849c5e82e596468b86eb55e9cc5401 # 2.32.0 + with: + php-version: ${{ matrix.php-version }} + extensions: ${{ env.extensions }} + coverage: xdebug + tools: pecl, composer + + - name: Install Composer dependencies + uses: ramsey/composer-install@57532f8be5bda426838819c5ee9afb8af389d51a # 3.0.0 + + - name: copy Env + run: | + cp .github/workflows/${{ inputs.env-file }}.${{ matrix.sql-versions }} .env + + - name: Generate secure key & Optimize application & Migrate + run: | + php artisan key:generate + php artisan optimize + php artisan migrate + + - name: Apply tests ${{ inputs.test-suite }} + run: XDEBUG_MODE=coverage vendor/bin/phpunit --configuration phpunit.ci.xml --testsuite ${{ inputs.test-suite }} + + - name: Make sure we can go backward + run: php artisan migrate:rollback + + - name: Codecov + uses: codecov/codecov-action@7f8b4b4bde536c465e797be725718b88c5d95e0e # v5.1.1 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + - name: SonarCloud Scan + if: ${{ env.SONAR_TOKEN }} + uses: SonarSource/sonarqube-scan-action@bfd4e558cda28cda6b5defafb9232d191be8c203 # v4.2.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/php_tests.yml b/.github/workflows/php_tests.yml new file mode 100644 index 00000000000..6653116f2da --- /dev/null +++ b/.github/workflows/php_tests.yml @@ -0,0 +1,120 @@ +name: PHP Tests + +on: + workflow_call: + inputs: + test-suite: + required: true + type: string + description: 'The test suite to run' + env-file: + required: true + type: string + description: 'The env files to use' + secrets: + SONAR_TOKEN: + required: true + description: 'sonar token secret' + CODECOV_TOKEN: + required: true + description: 'codecov token secret' + +permissions: + contents: read + +jobs: + tests: + permissions: + contents: read # for actions/checkout to fetch code + pull-requests: read # for SonarSource/sonarcloud-github-action to determine which PR to decorate + name: ${{ matrix.php-version }} - ${{ matrix.sql-versions }} -- ${{ inputs.test-suite }} + runs-on: ubuntu-latest + strategy: + matrix: + php-version: + # - 8.4 + - 8.3 + sql-versions: + - mariadb + - postgresql + - sqlite + # Service containers to run with `container-job` + services: + # Label used to access the service container + postgres: + # Docker Hub image + image: postgres + # Provide the password for postgres + env: + POSTGRES_PASSWORD: postgres + # Set health checks to wait until postgres has started + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + # Maps tcp port 5432 on service container to the host + - 5432:5432 + env: + extensions: bcmath, curl, dom, gd, imagick, json, libxml, mbstring, pcntl, pdo, pdo_sqlite, pdo_mysql, pdo_pgsql, pgsql, sqlite3, zip + + steps: + - name: Harden Runner + uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3 + with: + egress-policy: audit + + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Set Up Imagick, FFmpeg & Exiftools + run: | + sudo apt-get update + sudo apt-get --fix-broken install + sudo apt-get -y install ffmpeg libimage-exiftool-perl + + - name: setup Databases + run: | + sudo service mysql start + touch database/database.sqlite + mysql -uroot -proot -e 'create database homestead_test;' + + - name: Setup PHP Action + uses: shivammathur/setup-php@9e72090525849c5e82e596468b86eb55e9cc5401 # 2.32.0 + with: + php-version: ${{ matrix.php-version }} + extensions: ${{ env.extensions }} + coverage: xdebug + tools: pecl, composer + + - name: Install Composer dependencies + uses: ramsey/composer-install@57532f8be5bda426838819c5ee9afb8af389d51a # 3.0.0 + + - name: copy Env + run: | + cp .github/workflows/${{ inputs.env-file }}.${{ matrix.sql-versions }} .env + + - name: Generate secure key & Optimize application & Migrate + run: | + php artisan key:generate + php artisan optimize + php artisan migrate + + - name: Apply tests ${{ inputs.test-suite }} + run: XDEBUG_MODE=coverage vendor/bin/phpunit --configuration phpunit.ci.xml --testsuite ${{ inputs.test-suite }} + + - name: Make sure we can go backward + run: php artisan migrate:rollback + + - name: Codecov + uses: codecov/codecov-action@7f8b4b4bde536c465e797be725718b88c5d95e0e # v5.1.1 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + - name: SonarCloud Scan + if: ${{ env.SONAR_TOKEN }} + uses: SonarSource/sonarqube-scan-action@bfd4e558cda28cda6b5defafb9232d191be8c203 # v4.2.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 00000000000..4a1dde34216 --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,77 @@ +# This workflow uses actions that are not certified by GitHub. They are provided +# by a third-party and are governed by separate terms of service, privacy +# policy, and support documentation. + +name: Scorecard supply-chain security +on: + # For Branch-Protection check. Only the default branch is supported. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection + branch_protection_rule: + # To guarantee Maintained check is occasionally updated. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained + schedule: + - cron: '00 02 * * 1' + push: + branches: [ "master" ] + +# Declare default permissions as read only. +permissions: read-all + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + permissions: + # Needed to upload the results to code-scanning dashboard. + security-events: write + # Needed to publish results and get a badge (see publish_results below). + id-token: write + # Uncomment the permissions below if installing in a private repository. + # contents: read + # actions: read + + steps: + - name: Harden Runner + uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3 + with: + egress-policy: audit + + - name: "Checkout code" + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + + - name: "Run analysis" + uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0 + with: + results_file: results.sarif + results_format: sarif + # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: + # - you want to enable the Branch-Protection check on a *public* repository, or + # - you are installing Scorecard on a *private* repository + # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. + # repo_token: ${{ secrets.SCORECARD_TOKEN }} + + # Public repositories: + # - Publish results to OpenSSF REST API for easy access by consumers + # - Allows the repository to include the Scorecard badge. + # - See https://github.com/ossf/scorecard-action#publishing-results. + # For private repositories: + # - `publish_results` will always be set to `false`, regardless + # of the value entered here. + publish_results: true + + # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF + # format to the repository Actions tab. + - name: "Upload artifact" + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + # Upload the results to GitHub's code scanning dashboard. + - name: "Upload to code-scanning" + uses: github/codeql-action/upload-sarif@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v2.16.4 + with: + sarif_file: results.sarif diff --git a/.gitignore b/.gitignore index 454d8fc5928..147191aeceb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,53 +1,57 @@ -/node_modules +# We don't care about those +node_modules +build/* -public/Lychee-front/node_modules/ -public/Lychee-front/package-lock.json - -public/Lychee-front/node_modules/ -public/Lychee-front/bower_components/ -public/Lychee-front/package-lock.json - -public/uploads/big/* -public/uploads/import/* -public/uploads/medium/* -public/uploads/raw/* -public/uploads/small/* -public/uploads/thumb/* +# Those are personal +public/dist/user.css +public/dist/custom.js +public/build/* -!public/uploads/big/index.html -!public/uploads/import/index.html -!public/uploads/medium/index.html -!public/uploads/raw/index.html -!public/uploads/small/index.html -!public/uploads/thumb/index.html +# Pictures we do not commit +public/uploads/** +public/uploads-*/** +# Storage stuff: useless /storage/*.key /storage/clockwork/ + +# Those are generated /vendor + +# IDE & Cache stuff /.idea /.vscode -/.vagrant -Homestead.json -Homestead.yaml npm-debug.log -yarn-error.log -.env -.env.bck - -public/dist/user.css -aliases - -Lychee/* .phpunit* _ide* - .php_cs.cache - +.php-cs-fixer.cache *.log -package-lock.json clover.xml *.swp -installed.log .DS_Store -.NO_SECURE_KEY -.NO_AUTO_COMPOSER_MIGRATE \ No newline at end of file +.NO_AUTO_COMPOSER_MIGRATE +storage/bootstrap/cache/* +storage/image-jobs/* + +# used by Vite +public/hot +lang/php_*.json + +sync/* + +# Local DB for easy deployment +backup.sql +*-bck + +# Make sure we don't commit secrets +.env +.env.* + +aliases +pgdata +docker-compose.yml + +# Building stuff for releaseses +Lychee/* +report_* \ No newline at end of file diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php new file mode 100644 index 00000000000..b3987867f1d --- /dev/null +++ b/.php-cs-fixer.php @@ -0,0 +1,60 @@ +in($dir); + }, + PhpCsFixer\Finder::create()->ignoreUnreadableDirs() +)->notName('*.blade.php'); +$rules = [ + '@Symfony' => true, + 'nullable_type_declaration_for_default_null_value' => true, + 'align_multiline_comment' => true, + 'array_indentation' => true, + 'fully_qualified_strict_types' => false, + 'backtick_to_shell_exec' => true, + 'new_with_parentheses' => true, + 'increment_style' => ['style' => 'post'], + 'indentation_type' => true, + 'multiline_comment_opening_closing' => true, + 'no_php4_constructor' => true, + 'nullable_type_declaration' => false, + 'phpdoc_no_empty_return' => false, + 'single_blank_line_at_eof' => false, + 'yoda_style' => false, + 'concat_space' => ['spacing' => 'one'], + 'no_superfluous_phpdoc_tags' => false, + 'phpdoc_to_comment' => false, // required until https://github.com/phpstan/phpstan/issues/7486 got fixed + 'blank_line_between_import_groups' => false, // not PSR-12 compatible, but preserves old behaviour + 'ordered_imports' => [ + 'sort_algorithm' => 'alpha', + 'imports_order' => null, // for PSR-12 compatability, this need to be `['class', 'function', 'const']`, but no grouping preserves old behaviour + ], + 'no_unneeded_control_parentheses' => [ + 'statements' => ['break', 'clone', 'continue', 'echo_print', 'switch_case', 'yield'], + ], + 'operator_linebreak' => [ + 'only_booleans' => true, + 'position' => 'end', + ], + // 'header_comment' => ['header' => "SPDX-License-Identifier: MIT\nCopyright (c) 2017-2018 Tobias Reich\nCopyright (c) 2018-2025 LycheeOrg", 'comment_type' => 'PHPDoc', 'location' => 'after_open', 'separate' => 'bottom'], +]; +$config = new PhpCsFixer\Config(); + +$config->setRiskyAllowed(true); +$config->setRules($rules); +$config->setIndent("\t"); +$config->setLineEnding("\n"); +$config->setFinder($finder); + +return $config; diff --git a/.php_cs b/.php_cs deleted file mode 100644 index 288aa8c10a3..00000000000 --- a/.php_cs +++ /dev/null @@ -1,35 +0,0 @@ -in($dir); - }, - PhpCsFixer\Finder::create()->ignoreUnreadableDirs() -)->notName('*.blade.php'); -$rules = [ - '@Symfony' => true, - 'align_multiline_comment' => true, - 'array_indentation' => true, - 'backtick_to_shell_exec' => true, - 'increment_style' => ['style' => 'post'], - 'indentation_type' => true, - 'multiline_comment_opening_closing' => true, - 'no_php4_constructor' => true, - 'phpdoc_no_empty_return' => false, - 'single_blank_line_at_eof' => false, - 'yoda_style' => false, - 'concat_space' => ['spacing' => 'one'], - 'no_superfluous_phpdoc_tags' => false, -]; -return PhpCsFixer\Config::create() - ->setRiskyAllowed(true) - ->setRules($rules) - ->setIndent("\t") - ->setLineEnding("\n") - ->setFinder($finder); diff --git a/.phpstorm.meta.php b/.phpstorm.meta.php index ddba515010b..ede375a8f8a 100644 --- a/.phpstorm.meta.php +++ b/.phpstorm.meta.php @@ -1,2365 +1,1239 @@ - * @see https://github.com/barryvdh/laravel-ide-helper - */ - override(new \Illuminate\Contracts\Container\Container(), map([ - '' => '@', - 'AccessControl' => \App\ModelFunctions\SessionFunctions::class, - 'App\Actions\Albums\Extensions\PublicIds' => \App\Actions\Albums\Extensions\PublicIds::class, - 'App\Actions\Update\Apply' => \App\Actions\Update\Apply::class, - 'App\Actions\Update\Check' => \App\Actions\Update\Check::class, - 'App\Assets\Helpers' => \App\Assets\Helpers::class, - 'App\Image\ImageHandlerInterface' => \App\Image\ImageHandler::class, - 'App\Metadata\GitHubFunctions' => \App\Metadata\GitHubFunctions::class, - 'App\Metadata\GitRequest' => \App\Metadata\GitRequest::class, - 'App\Metadata\LycheeVersion' => \App\Metadata\LycheeVersion::class, - 'App\ModelFunctions\ConfigFunctions' => \App\ModelFunctions\ConfigFunctions::class, - 'App\ModelFunctions\SessionFunctions' => \App\ModelFunctions\SessionFunctions::class, - 'App\ModelFunctions\SymLinkFunctions' => \App\ModelFunctions\SymLinkFunctions::class, - 'Barryvdh\Debugbar\LaravelDebugbar' => \Barryvdh\Debugbar\LaravelDebugbar::class, - 'Cose\Algorithm\Manager' => \Cose\Algorithm\Manager::class, - 'DarkGhostHunter\Larapass\WebAuthn\PublicKeyCredentialParametersCollection' => \DarkGhostHunter\Larapass\WebAuthn\PublicKeyCredentialParametersCollection::class, - 'DarkGhostHunter\Larapass\WebAuthn\WebAuthnAssertValidator' => \DarkGhostHunter\Larapass\WebAuthn\WebAuthnAssertValidator::class, - 'DarkGhostHunter\Larapass\WebAuthn\WebAuthnAttestCreator' => \DarkGhostHunter\Larapass\WebAuthn\WebAuthnAttestCreator::class, - 'DarkGhostHunter\Larapass\WebAuthn\WebAuthnAttestValidator' => \DarkGhostHunter\Larapass\WebAuthn\WebAuthnAttestValidator::class, - 'Helpers' => \App\Assets\Helpers::class, - 'Illuminate\Auth\Middleware\RequirePassword' => \Illuminate\Auth\Middleware\RequirePassword::class, - 'Illuminate\Broadcasting\BroadcastManager' => \Illuminate\Broadcasting\BroadcastManager::class, - 'Illuminate\Bus\BatchRepository' => \Illuminate\Bus\DatabaseBatchRepository::class, - 'Illuminate\Bus\DatabaseBatchRepository' => \Illuminate\Bus\DatabaseBatchRepository::class, - 'Illuminate\Bus\Dispatcher' => \Illuminate\Bus\Dispatcher::class, - 'Illuminate\Cache\RateLimiter' => \Illuminate\Cache\RateLimiter::class, - 'Illuminate\Console\Scheduling\Schedule' => \Illuminate\Console\Scheduling\Schedule::class, - 'Illuminate\Console\Scheduling\ScheduleFinishCommand' => \Illuminate\Console\Scheduling\ScheduleFinishCommand::class, - 'Illuminate\Console\Scheduling\ScheduleListCommand' => \Illuminate\Console\Scheduling\ScheduleListCommand::class, - 'Illuminate\Console\Scheduling\ScheduleRunCommand' => \Illuminate\Console\Scheduling\ScheduleRunCommand::class, - 'Illuminate\Console\Scheduling\ScheduleTestCommand' => \Illuminate\Console\Scheduling\ScheduleTestCommand::class, - 'Illuminate\Console\Scheduling\ScheduleWorkCommand' => \Illuminate\Console\Scheduling\ScheduleWorkCommand::class, - 'Illuminate\Contracts\Auth\Access\Gate' => \Illuminate\Auth\Access\Gate::class, - 'Illuminate\Contracts\Broadcasting\Broadcaster' => \Illuminate\Broadcasting\Broadcasters\LogBroadcaster::class, - 'Illuminate\Contracts\Console\Kernel' => \App\Console\Kernel::class, - 'Illuminate\Contracts\Debug\ExceptionHandler' => \App\Exceptions\Handler::class, - 'Illuminate\Contracts\Http\Kernel' => \App\Http\Kernel::class, - 'Illuminate\Contracts\Pipeline\Hub' => \Illuminate\Pipeline\Hub::class, - 'Illuminate\Contracts\Queue\EntityResolver' => \Illuminate\Database\Eloquent\QueueEntityResolver::class, - 'Illuminate\Contracts\Routing\ResponseFactory' => \Illuminate\Routing\ResponseFactory::class, - 'Illuminate\Contracts\Validation\UncompromisedVerifier' => \Illuminate\Validation\NotPwnedVerifier::class, - 'Illuminate\Database\Console\DbCommand' => \Illuminate\Database\Console\DbCommand::class, - 'Illuminate\Foundation\Mix' => \Illuminate\Foundation\Mix::class, - 'Illuminate\Foundation\PackageManifest' => \Illuminate\Foundation\PackageManifest::class, - 'Illuminate\Mail\Markdown' => \Illuminate\Mail\Markdown::class, - 'Illuminate\Notifications\ChannelManager' => \Illuminate\Notifications\ChannelManager::class, - 'Illuminate\Routing\Contracts\ControllerDispatcher' => \Illuminate\Routing\ControllerDispatcher::class, - 'Illuminate\Session\Middleware\StartSession' => \Illuminate\Session\Middleware\StartSession::class, - 'Illuminate\Testing\ParallelTesting' => \Illuminate\Testing\ParallelTesting::class, - 'Livewire\LivewireComponentsFinder' => \Livewire\LivewireComponentsFinder::class, - 'Livewire\LivewireManager' => \Livewire\LivewireManager::class, - 'Psr\Http\Message\ResponseInterface' => \Nyholm\Psr7\Response::class, - 'Psr\Http\Message\ServerRequestInterface' => \Nyholm\Psr7\ServerRequest::class, - 'Spatie\ImageOptimizer\OptimizerChain' => \Spatie\ImageOptimizer\OptimizerChain::class, - 'Webauthn\AttestationStatement\AttestationObjectLoader' => \Webauthn\AttestationStatement\AttestationObjectLoader::class, - 'Webauthn\AttestationStatement\AttestationStatementSupportManager' => \Webauthn\AttestationStatement\AttestationStatementSupportManager::class, - 'Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs' => \Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs::class, - 'Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler' => \Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler::class, - 'Webauthn\AuthenticatorAssertionResponseValidator' => \Webauthn\AuthenticatorAssertionResponseValidator::class, - 'Webauthn\AuthenticatorAttestationResponseValidator' => \Webauthn\AuthenticatorAttestationResponseValidator::class, - 'Webauthn\AuthenticatorSelectionCriteria' => \DarkGhostHunter\Larapass\WebAuthn\AuthenticatorSelectionCriteria::class, - 'Webauthn\Counter\CounterChecker' => \Webauthn\Counter\ThrowExceptionIfInvalid::class, - 'Webauthn\PublicKeyCredentialLoader' => \Webauthn\PublicKeyCredentialLoader::class, - 'Webauthn\PublicKeyCredentialRpEntity' => \Webauthn\PublicKeyCredentialRpEntity::class, - 'Webauthn\PublicKeyCredentialSourceRepository' => \DarkGhostHunter\Larapass\Eloquent\WebAuthnCredential::class, - 'Webauthn\TokenBinding\TokenBindingHandler' => \Webauthn\TokenBinding\IgnoreTokenBindingHandler::class, - 'auth' => \Illuminate\Auth\AuthManager::class, - 'auth.driver' => \Illuminate\Auth\SessionGuard::class, - 'blade.compiler' => \Illuminate\View\Compilers\BladeCompiler::class, - 'cache' => \Illuminate\Cache\CacheManager::class, - 'cache.psr6' => \Symfony\Component\Cache\Adapter\Psr16Adapter::class, - 'cache.store' => \Illuminate\Cache\Repository::class, - 'clockwork' => \Clockwork\Clockwork::class, - 'clockwork.authenticator' => \Clockwork\Authentication\NullAuthenticator::class, - 'clockwork.cache' => \Clockwork\DataSource\LaravelCacheDataSource::class, - 'clockwork.eloquent' => \Clockwork\DataSource\EloquentDataSource::class, - 'clockwork.events' => \Clockwork\DataSource\LaravelEventsDataSource::class, - 'clockwork.laravel' => \Clockwork\DataSource\LaravelDataSource::class, - 'clockwork.notifications' => \Clockwork\DataSource\LaravelNotificationsDataSource::class, - 'clockwork.queue' => \Clockwork\DataSource\LaravelQueueDataSource::class, - 'clockwork.redis' => \Clockwork\DataSource\LaravelRedisDataSource::class, - 'clockwork.request' => \Clockwork\Request\Request::class, - 'clockwork.storage' => \Clockwork\Storage\FileStorage::class, - 'clockwork.support' => \Clockwork\Support\Laravel\ClockworkSupport::class, - 'clockwork.swift' => \Clockwork\DataSource\SwiftDataSource::class, - 'clockwork.views' => \Clockwork\DataSource\LaravelViewsDataSource::class, - 'clockwork.xdebug' => \Clockwork\DataSource\XdebugDataSource::class, - 'command.auth.resets.clear' => \Illuminate\Auth\Console\ClearResetsCommand::class, - 'command.cache.clear' => \Illuminate\Cache\Console\ClearCommand::class, - 'command.cache.forget' => \Illuminate\Cache\Console\ForgetCommand::class, - 'command.cache.table' => \Illuminate\Cache\Console\CacheTableCommand::class, - 'command.cast.make' => \Illuminate\Foundation\Console\CastMakeCommand::class, - 'command.channel.make' => \Illuminate\Foundation\Console\ChannelMakeCommand::class, - 'command.clear-compiled' => \Illuminate\Foundation\Console\ClearCompiledCommand::class, - 'command.component.make' => \Illuminate\Foundation\Console\ComponentMakeCommand::class, - 'command.config.cache' => \Illuminate\Foundation\Console\ConfigCacheCommand::class, - 'command.config.clear' => \Illuminate\Foundation\Console\ConfigClearCommand::class, - 'command.console.make' => \Illuminate\Foundation\Console\ConsoleMakeCommand::class, - 'command.controller.make' => \Illuminate\Routing\Console\ControllerMakeCommand::class, - 'command.db.wipe' => \Illuminate\Database\Console\WipeCommand::class, - 'command.debugbar.clear' => \Barryvdh\Debugbar\Console\ClearCommand::class, - 'command.down' => \Illuminate\Foundation\Console\DownCommand::class, - 'command.environment' => \Illuminate\Foundation\Console\EnvironmentCommand::class, - 'command.event.cache' => \Illuminate\Foundation\Console\EventCacheCommand::class, - 'command.event.clear' => \Illuminate\Foundation\Console\EventClearCommand::class, - 'command.event.generate' => \Illuminate\Foundation\Console\EventGenerateCommand::class, - 'command.event.list' => \Illuminate\Foundation\Console\EventListCommand::class, - 'command.event.make' => \Illuminate\Foundation\Console\EventMakeCommand::class, - 'command.exception.make' => \Illuminate\Foundation\Console\ExceptionMakeCommand::class, - 'command.factory.make' => \Illuminate\Database\Console\Factories\FactoryMakeCommand::class, - 'command.ide-helper.eloquent' => \Barryvdh\LaravelIdeHelper\Console\EloquentCommand::class, - 'command.ide-helper.generate' => \Barryvdh\LaravelIdeHelper\Console\GeneratorCommand::class, - 'command.ide-helper.meta' => \Barryvdh\LaravelIdeHelper\Console\MetaCommand::class, - 'command.ide-helper.models' => \Barryvdh\LaravelIdeHelper\Console\ModelsCommand::class, - 'command.job.make' => \Illuminate\Foundation\Console\JobMakeCommand::class, - 'command.key.generate' => \Illuminate\Foundation\Console\KeyGenerateCommand::class, - 'command.listener.make' => \Illuminate\Foundation\Console\ListenerMakeCommand::class, - 'command.mail.make' => \Illuminate\Foundation\Console\MailMakeCommand::class, - 'command.middleware.make' => \Illuminate\Routing\Console\MiddlewareMakeCommand::class, - 'command.migrate' => \Illuminate\Database\Console\Migrations\MigrateCommand::class, - 'command.migrate.fresh' => \Illuminate\Database\Console\Migrations\FreshCommand::class, - 'command.migrate.install' => \Illuminate\Database\Console\Migrations\InstallCommand::class, - 'command.migrate.make' => \Illuminate\Database\Console\Migrations\MigrateMakeCommand::class, - 'command.migrate.refresh' => \Illuminate\Database\Console\Migrations\RefreshCommand::class, - 'command.migrate.reset' => \Illuminate\Database\Console\Migrations\ResetCommand::class, - 'command.migrate.rollback' => \Illuminate\Database\Console\Migrations\RollbackCommand::class, - 'command.migrate.status' => \Illuminate\Database\Console\Migrations\StatusCommand::class, - 'command.model.make' => \Illuminate\Foundation\Console\ModelMakeCommand::class, - 'command.notification.make' => \Illuminate\Foundation\Console\NotificationMakeCommand::class, - 'command.notification.table' => \Illuminate\Notifications\Console\NotificationTableCommand::class, - 'command.observer.make' => \Illuminate\Foundation\Console\ObserverMakeCommand::class, - 'command.optimize' => \Illuminate\Foundation\Console\OptimizeCommand::class, - 'command.optimize.clear' => \Illuminate\Foundation\Console\OptimizeClearCommand::class, - 'command.package.discover' => \Illuminate\Foundation\Console\PackageDiscoverCommand::class, - 'command.policy.make' => \Illuminate\Foundation\Console\PolicyMakeCommand::class, - 'command.provider.make' => \Illuminate\Foundation\Console\ProviderMakeCommand::class, - 'command.queue.batches-table' => \Illuminate\Queue\Console\BatchesTableCommand::class, - 'command.queue.clear' => \Illuminate\Queue\Console\ClearCommand::class, - 'command.queue.failed' => \Illuminate\Queue\Console\ListFailedCommand::class, - 'command.queue.failed-table' => \Illuminate\Queue\Console\FailedTableCommand::class, - 'command.queue.flush' => \Illuminate\Queue\Console\FlushFailedCommand::class, - 'command.queue.forget' => \Illuminate\Queue\Console\ForgetFailedCommand::class, - 'command.queue.listen' => \Illuminate\Queue\Console\ListenCommand::class, - 'command.queue.prune-batches' => \Illuminate\Queue\Console\PruneBatchesCommand::class, - 'command.queue.restart' => \Illuminate\Queue\Console\RestartCommand::class, - 'command.queue.retry' => \Illuminate\Queue\Console\RetryCommand::class, - 'command.queue.retry-batch' => \Illuminate\Queue\Console\RetryBatchCommand::class, - 'command.queue.table' => \Illuminate\Queue\Console\TableCommand::class, - 'command.queue.work' => \Illuminate\Queue\Console\WorkCommand::class, - 'command.request.make' => \Illuminate\Foundation\Console\RequestMakeCommand::class, - 'command.resource.make' => \Illuminate\Foundation\Console\ResourceMakeCommand::class, - 'command.route.cache' => \Illuminate\Foundation\Console\RouteCacheCommand::class, - 'command.route.clear' => \Illuminate\Foundation\Console\RouteClearCommand::class, - 'command.route.list' => \Illuminate\Foundation\Console\RouteListCommand::class, - 'command.rule.make' => \Illuminate\Foundation\Console\RuleMakeCommand::class, - 'command.schema.dump' => \Illuminate\Database\Console\DumpCommand::class, - 'command.seed' => \Illuminate\Database\Console\Seeds\SeedCommand::class, - 'command.seeder.make' => \Illuminate\Database\Console\Seeds\SeederMakeCommand::class, - 'command.serve' => \Illuminate\Foundation\Console\ServeCommand::class, - 'command.session.table' => \Illuminate\Session\Console\SessionTableCommand::class, - 'command.storage.link' => \Illuminate\Foundation\Console\StorageLinkCommand::class, - 'command.stub.publish' => \Illuminate\Foundation\Console\StubPublishCommand::class, - 'command.test.make' => \Illuminate\Foundation\Console\TestMakeCommand::class, - 'command.up' => \Illuminate\Foundation\Console\UpCommand::class, - 'command.vendor.publish' => \Illuminate\Foundation\Console\VendorPublishCommand::class, - 'command.view.cache' => \Illuminate\Foundation\Console\ViewCacheCommand::class, - 'command.view.clear' => \Illuminate\Foundation\Console\ViewClearCommand::class, - 'composer' => \Illuminate\Support\Composer::class, - 'cookie' => \Illuminate\Cookie\CookieJar::class, - 'db' => \Illuminate\Database\DatabaseManager::class, - 'db.connection' => \Illuminate\Database\SQLiteConnection::class, - 'db.factory' => \Illuminate\Database\Connectors\ConnectionFactory::class, - 'db.transactions' => \Illuminate\Database\DatabaseTransactionsManager::class, - 'encrypter' => \Illuminate\Encryption\Encrypter::class, - 'events' => \Illuminate\Events\Dispatcher::class, - 'files' => \Illuminate\Filesystem\Filesystem::class, - 'filesystem' => \Illuminate\Filesystem\FilesystemManager::class, - 'filesystem.disk' => \Illuminate\Filesystem\FilesystemAdapter::class, - 'hash' => \Illuminate\Hashing\HashManager::class, - 'hash.driver' => \Illuminate\Hashing\BcryptHasher::class, - 'image-optimizer' => \Spatie\ImageOptimizer\OptimizerChain::class, - 'log' => \Illuminate\Log\LogManager::class, - 'mail.manager' => \Illuminate\Mail\MailManager::class, - 'mailer' => \Illuminate\Mail\Mailer::class, - 'markdown' => \League\CommonMark\CommonMarkConverter::class, - 'markdown.compiler' => \GrahamCampbell\Markdown\View\Compiler\MarkdownCompiler::class, - 'markdown.directive' => \GrahamCampbell\Markdown\View\Directive\MarkdownDirective::class, - 'markdown.environment' => \League\CommonMark\Environment::class, - 'memcached.connector' => \Illuminate\Cache\MemcachedConnector::class, - 'migration.creator' => \Illuminate\Database\Migrations\MigrationCreator::class, - 'migration.repository' => \Illuminate\Database\Migrations\DatabaseMigrationRepository::class, - 'migrator' => \Illuminate\Database\Migrations\Migrator::class, - 'queue' => \Illuminate\Queue\QueueManager::class, - 'queue.connection' => \Illuminate\Queue\SyncQueue::class, - 'queue.failer' => \Illuminate\Queue\Failed\DatabaseUuidFailedJobProvider::class, - 'queue.listener' => \Illuminate\Queue\Listener::class, - 'queue.worker' => \Illuminate\Queue\Worker::class, - 'redirect' => \Illuminate\Routing\Redirector::class, - 'redis' => \Illuminate\Redis\RedisManager::class, - 'router' => \Illuminate\Routing\Router::class, - 'session' => \Illuminate\Session\SessionManager::class, - 'session.store' => \Illuminate\Session\Store::class, - 'translation.loader' => \Illuminate\Translation\FileLoader::class, - 'translator' => \Illuminate\Translation\Translator::class, - 'url' => \Illuminate\Routing\UrlGenerator::class, - 'validation.presence' => \Illuminate\Validation\DatabasePresenceVerifier::class, - 'view' => \Illuminate\View\Factory::class, - 'view.engine.resolver' => \Illuminate\View\Engines\EngineResolver::class, - 'view.finder' => \Illuminate\View\FileViewFinder::class, - ])); - override(\Illuminate\Container\Container::makeWith(0), map([ - '' => '@', - 'AccessControl' => \App\ModelFunctions\SessionFunctions::class, - 'App\Actions\Albums\Extensions\PublicIds' => \App\Actions\Albums\Extensions\PublicIds::class, - 'App\Actions\Update\Apply' => \App\Actions\Update\Apply::class, - 'App\Actions\Update\Check' => \App\Actions\Update\Check::class, - 'App\Assets\Helpers' => \App\Assets\Helpers::class, - 'App\Image\ImageHandlerInterface' => \App\Image\ImageHandler::class, - 'App\Metadata\GitHubFunctions' => \App\Metadata\GitHubFunctions::class, - 'App\Metadata\GitRequest' => \App\Metadata\GitRequest::class, - 'App\Metadata\LycheeVersion' => \App\Metadata\LycheeVersion::class, - 'App\ModelFunctions\ConfigFunctions' => \App\ModelFunctions\ConfigFunctions::class, - 'App\ModelFunctions\SessionFunctions' => \App\ModelFunctions\SessionFunctions::class, - 'App\ModelFunctions\SymLinkFunctions' => \App\ModelFunctions\SymLinkFunctions::class, - 'Barryvdh\Debugbar\LaravelDebugbar' => \Barryvdh\Debugbar\LaravelDebugbar::class, - 'Cose\Algorithm\Manager' => \Cose\Algorithm\Manager::class, - 'DarkGhostHunter\Larapass\WebAuthn\PublicKeyCredentialParametersCollection' => \DarkGhostHunter\Larapass\WebAuthn\PublicKeyCredentialParametersCollection::class, - 'DarkGhostHunter\Larapass\WebAuthn\WebAuthnAssertValidator' => \DarkGhostHunter\Larapass\WebAuthn\WebAuthnAssertValidator::class, - 'DarkGhostHunter\Larapass\WebAuthn\WebAuthnAttestCreator' => \DarkGhostHunter\Larapass\WebAuthn\WebAuthnAttestCreator::class, - 'DarkGhostHunter\Larapass\WebAuthn\WebAuthnAttestValidator' => \DarkGhostHunter\Larapass\WebAuthn\WebAuthnAttestValidator::class, - 'Helpers' => \App\Assets\Helpers::class, - 'Illuminate\Auth\Middleware\RequirePassword' => \Illuminate\Auth\Middleware\RequirePassword::class, - 'Illuminate\Broadcasting\BroadcastManager' => \Illuminate\Broadcasting\BroadcastManager::class, - 'Illuminate\Bus\BatchRepository' => \Illuminate\Bus\DatabaseBatchRepository::class, - 'Illuminate\Bus\DatabaseBatchRepository' => \Illuminate\Bus\DatabaseBatchRepository::class, - 'Illuminate\Bus\Dispatcher' => \Illuminate\Bus\Dispatcher::class, - 'Illuminate\Cache\RateLimiter' => \Illuminate\Cache\RateLimiter::class, - 'Illuminate\Console\Scheduling\Schedule' => \Illuminate\Console\Scheduling\Schedule::class, - 'Illuminate\Console\Scheduling\ScheduleFinishCommand' => \Illuminate\Console\Scheduling\ScheduleFinishCommand::class, - 'Illuminate\Console\Scheduling\ScheduleListCommand' => \Illuminate\Console\Scheduling\ScheduleListCommand::class, - 'Illuminate\Console\Scheduling\ScheduleRunCommand' => \Illuminate\Console\Scheduling\ScheduleRunCommand::class, - 'Illuminate\Console\Scheduling\ScheduleTestCommand' => \Illuminate\Console\Scheduling\ScheduleTestCommand::class, - 'Illuminate\Console\Scheduling\ScheduleWorkCommand' => \Illuminate\Console\Scheduling\ScheduleWorkCommand::class, - 'Illuminate\Contracts\Auth\Access\Gate' => \Illuminate\Auth\Access\Gate::class, - 'Illuminate\Contracts\Broadcasting\Broadcaster' => \Illuminate\Broadcasting\Broadcasters\LogBroadcaster::class, - 'Illuminate\Contracts\Console\Kernel' => \App\Console\Kernel::class, - 'Illuminate\Contracts\Debug\ExceptionHandler' => \App\Exceptions\Handler::class, - 'Illuminate\Contracts\Http\Kernel' => \App\Http\Kernel::class, - 'Illuminate\Contracts\Pipeline\Hub' => \Illuminate\Pipeline\Hub::class, - 'Illuminate\Contracts\Queue\EntityResolver' => \Illuminate\Database\Eloquent\QueueEntityResolver::class, - 'Illuminate\Contracts\Routing\ResponseFactory' => \Illuminate\Routing\ResponseFactory::class, - 'Illuminate\Contracts\Validation\UncompromisedVerifier' => \Illuminate\Validation\NotPwnedVerifier::class, - 'Illuminate\Database\Console\DbCommand' => \Illuminate\Database\Console\DbCommand::class, - 'Illuminate\Foundation\Mix' => \Illuminate\Foundation\Mix::class, - 'Illuminate\Foundation\PackageManifest' => \Illuminate\Foundation\PackageManifest::class, - 'Illuminate\Mail\Markdown' => \Illuminate\Mail\Markdown::class, - 'Illuminate\Notifications\ChannelManager' => \Illuminate\Notifications\ChannelManager::class, - 'Illuminate\Routing\Contracts\ControllerDispatcher' => \Illuminate\Routing\ControllerDispatcher::class, - 'Illuminate\Session\Middleware\StartSession' => \Illuminate\Session\Middleware\StartSession::class, - 'Illuminate\Testing\ParallelTesting' => \Illuminate\Testing\ParallelTesting::class, - 'Livewire\LivewireComponentsFinder' => \Livewire\LivewireComponentsFinder::class, - 'Livewire\LivewireManager' => \Livewire\LivewireManager::class, - 'Psr\Http\Message\ResponseInterface' => \Nyholm\Psr7\Response::class, - 'Psr\Http\Message\ServerRequestInterface' => \Nyholm\Psr7\ServerRequest::class, - 'Spatie\ImageOptimizer\OptimizerChain' => \Spatie\ImageOptimizer\OptimizerChain::class, - 'Webauthn\AttestationStatement\AttestationObjectLoader' => \Webauthn\AttestationStatement\AttestationObjectLoader::class, - 'Webauthn\AttestationStatement\AttestationStatementSupportManager' => \Webauthn\AttestationStatement\AttestationStatementSupportManager::class, - 'Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs' => \Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs::class, - 'Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler' => \Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler::class, - 'Webauthn\AuthenticatorAssertionResponseValidator' => \Webauthn\AuthenticatorAssertionResponseValidator::class, - 'Webauthn\AuthenticatorAttestationResponseValidator' => \Webauthn\AuthenticatorAttestationResponseValidator::class, - 'Webauthn\AuthenticatorSelectionCriteria' => \DarkGhostHunter\Larapass\WebAuthn\AuthenticatorSelectionCriteria::class, - 'Webauthn\Counter\CounterChecker' => \Webauthn\Counter\ThrowExceptionIfInvalid::class, - 'Webauthn\PublicKeyCredentialLoader' => \Webauthn\PublicKeyCredentialLoader::class, - 'Webauthn\PublicKeyCredentialRpEntity' => \Webauthn\PublicKeyCredentialRpEntity::class, - 'Webauthn\PublicKeyCredentialSourceRepository' => \DarkGhostHunter\Larapass\Eloquent\WebAuthnCredential::class, - 'Webauthn\TokenBinding\TokenBindingHandler' => \Webauthn\TokenBinding\IgnoreTokenBindingHandler::class, - 'auth' => \Illuminate\Auth\AuthManager::class, - 'auth.driver' => \Illuminate\Auth\SessionGuard::class, - 'blade.compiler' => \Illuminate\View\Compilers\BladeCompiler::class, - 'cache' => \Illuminate\Cache\CacheManager::class, - 'cache.psr6' => \Symfony\Component\Cache\Adapter\Psr16Adapter::class, - 'cache.store' => \Illuminate\Cache\Repository::class, - 'clockwork' => \Clockwork\Clockwork::class, - 'clockwork.authenticator' => \Clockwork\Authentication\NullAuthenticator::class, - 'clockwork.cache' => \Clockwork\DataSource\LaravelCacheDataSource::class, - 'clockwork.eloquent' => \Clockwork\DataSource\EloquentDataSource::class, - 'clockwork.events' => \Clockwork\DataSource\LaravelEventsDataSource::class, - 'clockwork.laravel' => \Clockwork\DataSource\LaravelDataSource::class, - 'clockwork.notifications' => \Clockwork\DataSource\LaravelNotificationsDataSource::class, - 'clockwork.queue' => \Clockwork\DataSource\LaravelQueueDataSource::class, - 'clockwork.redis' => \Clockwork\DataSource\LaravelRedisDataSource::class, - 'clockwork.request' => \Clockwork\Request\Request::class, - 'clockwork.storage' => \Clockwork\Storage\FileStorage::class, - 'clockwork.support' => \Clockwork\Support\Laravel\ClockworkSupport::class, - 'clockwork.swift' => \Clockwork\DataSource\SwiftDataSource::class, - 'clockwork.views' => \Clockwork\DataSource\LaravelViewsDataSource::class, - 'clockwork.xdebug' => \Clockwork\DataSource\XdebugDataSource::class, - 'command.auth.resets.clear' => \Illuminate\Auth\Console\ClearResetsCommand::class, - 'command.cache.clear' => \Illuminate\Cache\Console\ClearCommand::class, - 'command.cache.forget' => \Illuminate\Cache\Console\ForgetCommand::class, - 'command.cache.table' => \Illuminate\Cache\Console\CacheTableCommand::class, - 'command.cast.make' => \Illuminate\Foundation\Console\CastMakeCommand::class, - 'command.channel.make' => \Illuminate\Foundation\Console\ChannelMakeCommand::class, - 'command.clear-compiled' => \Illuminate\Foundation\Console\ClearCompiledCommand::class, - 'command.component.make' => \Illuminate\Foundation\Console\ComponentMakeCommand::class, - 'command.config.cache' => \Illuminate\Foundation\Console\ConfigCacheCommand::class, - 'command.config.clear' => \Illuminate\Foundation\Console\ConfigClearCommand::class, - 'command.console.make' => \Illuminate\Foundation\Console\ConsoleMakeCommand::class, - 'command.controller.make' => \Illuminate\Routing\Console\ControllerMakeCommand::class, - 'command.db.wipe' => \Illuminate\Database\Console\WipeCommand::class, - 'command.debugbar.clear' => \Barryvdh\Debugbar\Console\ClearCommand::class, - 'command.down' => \Illuminate\Foundation\Console\DownCommand::class, - 'command.environment' => \Illuminate\Foundation\Console\EnvironmentCommand::class, - 'command.event.cache' => \Illuminate\Foundation\Console\EventCacheCommand::class, - 'command.event.clear' => \Illuminate\Foundation\Console\EventClearCommand::class, - 'command.event.generate' => \Illuminate\Foundation\Console\EventGenerateCommand::class, - 'command.event.list' => \Illuminate\Foundation\Console\EventListCommand::class, - 'command.event.make' => \Illuminate\Foundation\Console\EventMakeCommand::class, - 'command.exception.make' => \Illuminate\Foundation\Console\ExceptionMakeCommand::class, - 'command.factory.make' => \Illuminate\Database\Console\Factories\FactoryMakeCommand::class, - 'command.ide-helper.eloquent' => \Barryvdh\LaravelIdeHelper\Console\EloquentCommand::class, - 'command.ide-helper.generate' => \Barryvdh\LaravelIdeHelper\Console\GeneratorCommand::class, - 'command.ide-helper.meta' => \Barryvdh\LaravelIdeHelper\Console\MetaCommand::class, - 'command.ide-helper.models' => \Barryvdh\LaravelIdeHelper\Console\ModelsCommand::class, - 'command.job.make' => \Illuminate\Foundation\Console\JobMakeCommand::class, - 'command.key.generate' => \Illuminate\Foundation\Console\KeyGenerateCommand::class, - 'command.listener.make' => \Illuminate\Foundation\Console\ListenerMakeCommand::class, - 'command.mail.make' => \Illuminate\Foundation\Console\MailMakeCommand::class, - 'command.middleware.make' => \Illuminate\Routing\Console\MiddlewareMakeCommand::class, - 'command.migrate' => \Illuminate\Database\Console\Migrations\MigrateCommand::class, - 'command.migrate.fresh' => \Illuminate\Database\Console\Migrations\FreshCommand::class, - 'command.migrate.install' => \Illuminate\Database\Console\Migrations\InstallCommand::class, - 'command.migrate.make' => \Illuminate\Database\Console\Migrations\MigrateMakeCommand::class, - 'command.migrate.refresh' => \Illuminate\Database\Console\Migrations\RefreshCommand::class, - 'command.migrate.reset' => \Illuminate\Database\Console\Migrations\ResetCommand::class, - 'command.migrate.rollback' => \Illuminate\Database\Console\Migrations\RollbackCommand::class, - 'command.migrate.status' => \Illuminate\Database\Console\Migrations\StatusCommand::class, - 'command.model.make' => \Illuminate\Foundation\Console\ModelMakeCommand::class, - 'command.notification.make' => \Illuminate\Foundation\Console\NotificationMakeCommand::class, - 'command.notification.table' => \Illuminate\Notifications\Console\NotificationTableCommand::class, - 'command.observer.make' => \Illuminate\Foundation\Console\ObserverMakeCommand::class, - 'command.optimize' => \Illuminate\Foundation\Console\OptimizeCommand::class, - 'command.optimize.clear' => \Illuminate\Foundation\Console\OptimizeClearCommand::class, - 'command.package.discover' => \Illuminate\Foundation\Console\PackageDiscoverCommand::class, - 'command.policy.make' => \Illuminate\Foundation\Console\PolicyMakeCommand::class, - 'command.provider.make' => \Illuminate\Foundation\Console\ProviderMakeCommand::class, - 'command.queue.batches-table' => \Illuminate\Queue\Console\BatchesTableCommand::class, - 'command.queue.clear' => \Illuminate\Queue\Console\ClearCommand::class, - 'command.queue.failed' => \Illuminate\Queue\Console\ListFailedCommand::class, - 'command.queue.failed-table' => \Illuminate\Queue\Console\FailedTableCommand::class, - 'command.queue.flush' => \Illuminate\Queue\Console\FlushFailedCommand::class, - 'command.queue.forget' => \Illuminate\Queue\Console\ForgetFailedCommand::class, - 'command.queue.listen' => \Illuminate\Queue\Console\ListenCommand::class, - 'command.queue.prune-batches' => \Illuminate\Queue\Console\PruneBatchesCommand::class, - 'command.queue.restart' => \Illuminate\Queue\Console\RestartCommand::class, - 'command.queue.retry' => \Illuminate\Queue\Console\RetryCommand::class, - 'command.queue.retry-batch' => \Illuminate\Queue\Console\RetryBatchCommand::class, - 'command.queue.table' => \Illuminate\Queue\Console\TableCommand::class, - 'command.queue.work' => \Illuminate\Queue\Console\WorkCommand::class, - 'command.request.make' => \Illuminate\Foundation\Console\RequestMakeCommand::class, - 'command.resource.make' => \Illuminate\Foundation\Console\ResourceMakeCommand::class, - 'command.route.cache' => \Illuminate\Foundation\Console\RouteCacheCommand::class, - 'command.route.clear' => \Illuminate\Foundation\Console\RouteClearCommand::class, - 'command.route.list' => \Illuminate\Foundation\Console\RouteListCommand::class, - 'command.rule.make' => \Illuminate\Foundation\Console\RuleMakeCommand::class, - 'command.schema.dump' => \Illuminate\Database\Console\DumpCommand::class, - 'command.seed' => \Illuminate\Database\Console\Seeds\SeedCommand::class, - 'command.seeder.make' => \Illuminate\Database\Console\Seeds\SeederMakeCommand::class, - 'command.serve' => \Illuminate\Foundation\Console\ServeCommand::class, - 'command.session.table' => \Illuminate\Session\Console\SessionTableCommand::class, - 'command.storage.link' => \Illuminate\Foundation\Console\StorageLinkCommand::class, - 'command.stub.publish' => \Illuminate\Foundation\Console\StubPublishCommand::class, - 'command.test.make' => \Illuminate\Foundation\Console\TestMakeCommand::class, - 'command.up' => \Illuminate\Foundation\Console\UpCommand::class, - 'command.vendor.publish' => \Illuminate\Foundation\Console\VendorPublishCommand::class, - 'command.view.cache' => \Illuminate\Foundation\Console\ViewCacheCommand::class, - 'command.view.clear' => \Illuminate\Foundation\Console\ViewClearCommand::class, - 'composer' => \Illuminate\Support\Composer::class, - 'cookie' => \Illuminate\Cookie\CookieJar::class, - 'db' => \Illuminate\Database\DatabaseManager::class, - 'db.connection' => \Illuminate\Database\SQLiteConnection::class, - 'db.factory' => \Illuminate\Database\Connectors\ConnectionFactory::class, - 'db.transactions' => \Illuminate\Database\DatabaseTransactionsManager::class, - 'encrypter' => \Illuminate\Encryption\Encrypter::class, - 'events' => \Illuminate\Events\Dispatcher::class, - 'files' => \Illuminate\Filesystem\Filesystem::class, - 'filesystem' => \Illuminate\Filesystem\FilesystemManager::class, - 'filesystem.disk' => \Illuminate\Filesystem\FilesystemAdapter::class, - 'hash' => \Illuminate\Hashing\HashManager::class, - 'hash.driver' => \Illuminate\Hashing\BcryptHasher::class, - 'image-optimizer' => \Spatie\ImageOptimizer\OptimizerChain::class, - 'log' => \Illuminate\Log\LogManager::class, - 'mail.manager' => \Illuminate\Mail\MailManager::class, - 'mailer' => \Illuminate\Mail\Mailer::class, - 'markdown' => \League\CommonMark\CommonMarkConverter::class, - 'markdown.compiler' => \GrahamCampbell\Markdown\View\Compiler\MarkdownCompiler::class, - 'markdown.directive' => \GrahamCampbell\Markdown\View\Directive\MarkdownDirective::class, - 'markdown.environment' => \League\CommonMark\Environment::class, - 'memcached.connector' => \Illuminate\Cache\MemcachedConnector::class, - 'migration.creator' => \Illuminate\Database\Migrations\MigrationCreator::class, - 'migration.repository' => \Illuminate\Database\Migrations\DatabaseMigrationRepository::class, - 'migrator' => \Illuminate\Database\Migrations\Migrator::class, - 'queue' => \Illuminate\Queue\QueueManager::class, - 'queue.connection' => \Illuminate\Queue\SyncQueue::class, - 'queue.failer' => \Illuminate\Queue\Failed\DatabaseUuidFailedJobProvider::class, - 'queue.listener' => \Illuminate\Queue\Listener::class, - 'queue.worker' => \Illuminate\Queue\Worker::class, - 'redirect' => \Illuminate\Routing\Redirector::class, - 'redis' => \Illuminate\Redis\RedisManager::class, - 'router' => \Illuminate\Routing\Router::class, - 'session' => \Illuminate\Session\SessionManager::class, - 'session.store' => \Illuminate\Session\Store::class, - 'translation.loader' => \Illuminate\Translation\FileLoader::class, - 'translator' => \Illuminate\Translation\Translator::class, - 'url' => \Illuminate\Routing\UrlGenerator::class, - 'validation.presence' => \Illuminate\Validation\DatabasePresenceVerifier::class, - 'view' => \Illuminate\View\Factory::class, - 'view.engine.resolver' => \Illuminate\View\Engines\EngineResolver::class, - 'view.finder' => \Illuminate\View\FileViewFinder::class, - ])); - override(\Illuminate\Contracts\Container\Container::get(0), map([ - '' => '@', - 'AccessControl' => \App\ModelFunctions\SessionFunctions::class, - 'App\Actions\Albums\Extensions\PublicIds' => \App\Actions\Albums\Extensions\PublicIds::class, - 'App\Actions\Update\Apply' => \App\Actions\Update\Apply::class, - 'App\Actions\Update\Check' => \App\Actions\Update\Check::class, - 'App\Assets\Helpers' => \App\Assets\Helpers::class, - 'App\Image\ImageHandlerInterface' => \App\Image\ImageHandler::class, - 'App\Metadata\GitHubFunctions' => \App\Metadata\GitHubFunctions::class, - 'App\Metadata\GitRequest' => \App\Metadata\GitRequest::class, - 'App\Metadata\LycheeVersion' => \App\Metadata\LycheeVersion::class, - 'App\ModelFunctions\ConfigFunctions' => \App\ModelFunctions\ConfigFunctions::class, - 'App\ModelFunctions\SessionFunctions' => \App\ModelFunctions\SessionFunctions::class, - 'App\ModelFunctions\SymLinkFunctions' => \App\ModelFunctions\SymLinkFunctions::class, - 'Barryvdh\Debugbar\LaravelDebugbar' => \Barryvdh\Debugbar\LaravelDebugbar::class, - 'Cose\Algorithm\Manager' => \Cose\Algorithm\Manager::class, - 'DarkGhostHunter\Larapass\WebAuthn\PublicKeyCredentialParametersCollection' => \DarkGhostHunter\Larapass\WebAuthn\PublicKeyCredentialParametersCollection::class, - 'DarkGhostHunter\Larapass\WebAuthn\WebAuthnAssertValidator' => \DarkGhostHunter\Larapass\WebAuthn\WebAuthnAssertValidator::class, - 'DarkGhostHunter\Larapass\WebAuthn\WebAuthnAttestCreator' => \DarkGhostHunter\Larapass\WebAuthn\WebAuthnAttestCreator::class, - 'DarkGhostHunter\Larapass\WebAuthn\WebAuthnAttestValidator' => \DarkGhostHunter\Larapass\WebAuthn\WebAuthnAttestValidator::class, - 'Helpers' => \App\Assets\Helpers::class, - 'Illuminate\Auth\Middleware\RequirePassword' => \Illuminate\Auth\Middleware\RequirePassword::class, - 'Illuminate\Broadcasting\BroadcastManager' => \Illuminate\Broadcasting\BroadcastManager::class, - 'Illuminate\Bus\BatchRepository' => \Illuminate\Bus\DatabaseBatchRepository::class, - 'Illuminate\Bus\DatabaseBatchRepository' => \Illuminate\Bus\DatabaseBatchRepository::class, - 'Illuminate\Bus\Dispatcher' => \Illuminate\Bus\Dispatcher::class, - 'Illuminate\Cache\RateLimiter' => \Illuminate\Cache\RateLimiter::class, - 'Illuminate\Console\Scheduling\Schedule' => \Illuminate\Console\Scheduling\Schedule::class, - 'Illuminate\Console\Scheduling\ScheduleFinishCommand' => \Illuminate\Console\Scheduling\ScheduleFinishCommand::class, - 'Illuminate\Console\Scheduling\ScheduleListCommand' => \Illuminate\Console\Scheduling\ScheduleListCommand::class, - 'Illuminate\Console\Scheduling\ScheduleRunCommand' => \Illuminate\Console\Scheduling\ScheduleRunCommand::class, - 'Illuminate\Console\Scheduling\ScheduleTestCommand' => \Illuminate\Console\Scheduling\ScheduleTestCommand::class, - 'Illuminate\Console\Scheduling\ScheduleWorkCommand' => \Illuminate\Console\Scheduling\ScheduleWorkCommand::class, - 'Illuminate\Contracts\Auth\Access\Gate' => \Illuminate\Auth\Access\Gate::class, - 'Illuminate\Contracts\Broadcasting\Broadcaster' => \Illuminate\Broadcasting\Broadcasters\LogBroadcaster::class, - 'Illuminate\Contracts\Console\Kernel' => \App\Console\Kernel::class, - 'Illuminate\Contracts\Debug\ExceptionHandler' => \App\Exceptions\Handler::class, - 'Illuminate\Contracts\Http\Kernel' => \App\Http\Kernel::class, - 'Illuminate\Contracts\Pipeline\Hub' => \Illuminate\Pipeline\Hub::class, - 'Illuminate\Contracts\Queue\EntityResolver' => \Illuminate\Database\Eloquent\QueueEntityResolver::class, - 'Illuminate\Contracts\Routing\ResponseFactory' => \Illuminate\Routing\ResponseFactory::class, - 'Illuminate\Contracts\Validation\UncompromisedVerifier' => \Illuminate\Validation\NotPwnedVerifier::class, - 'Illuminate\Database\Console\DbCommand' => \Illuminate\Database\Console\DbCommand::class, - 'Illuminate\Foundation\Mix' => \Illuminate\Foundation\Mix::class, - 'Illuminate\Foundation\PackageManifest' => \Illuminate\Foundation\PackageManifest::class, - 'Illuminate\Mail\Markdown' => \Illuminate\Mail\Markdown::class, - 'Illuminate\Notifications\ChannelManager' => \Illuminate\Notifications\ChannelManager::class, - 'Illuminate\Routing\Contracts\ControllerDispatcher' => \Illuminate\Routing\ControllerDispatcher::class, - 'Illuminate\Session\Middleware\StartSession' => \Illuminate\Session\Middleware\StartSession::class, - 'Illuminate\Testing\ParallelTesting' => \Illuminate\Testing\ParallelTesting::class, - 'Livewire\LivewireComponentsFinder' => \Livewire\LivewireComponentsFinder::class, - 'Livewire\LivewireManager' => \Livewire\LivewireManager::class, - 'Psr\Http\Message\ResponseInterface' => \Nyholm\Psr7\Response::class, - 'Psr\Http\Message\ServerRequestInterface' => \Nyholm\Psr7\ServerRequest::class, - 'Spatie\ImageOptimizer\OptimizerChain' => \Spatie\ImageOptimizer\OptimizerChain::class, - 'Webauthn\AttestationStatement\AttestationObjectLoader' => \Webauthn\AttestationStatement\AttestationObjectLoader::class, - 'Webauthn\AttestationStatement\AttestationStatementSupportManager' => \Webauthn\AttestationStatement\AttestationStatementSupportManager::class, - 'Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs' => \Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs::class, - 'Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler' => \Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler::class, - 'Webauthn\AuthenticatorAssertionResponseValidator' => \Webauthn\AuthenticatorAssertionResponseValidator::class, - 'Webauthn\AuthenticatorAttestationResponseValidator' => \Webauthn\AuthenticatorAttestationResponseValidator::class, - 'Webauthn\AuthenticatorSelectionCriteria' => \DarkGhostHunter\Larapass\WebAuthn\AuthenticatorSelectionCriteria::class, - 'Webauthn\Counter\CounterChecker' => \Webauthn\Counter\ThrowExceptionIfInvalid::class, - 'Webauthn\PublicKeyCredentialLoader' => \Webauthn\PublicKeyCredentialLoader::class, - 'Webauthn\PublicKeyCredentialRpEntity' => \Webauthn\PublicKeyCredentialRpEntity::class, - 'Webauthn\PublicKeyCredentialSourceRepository' => \DarkGhostHunter\Larapass\Eloquent\WebAuthnCredential::class, - 'Webauthn\TokenBinding\TokenBindingHandler' => \Webauthn\TokenBinding\IgnoreTokenBindingHandler::class, - 'auth' => \Illuminate\Auth\AuthManager::class, - 'auth.driver' => \Illuminate\Auth\SessionGuard::class, - 'blade.compiler' => \Illuminate\View\Compilers\BladeCompiler::class, - 'cache' => \Illuminate\Cache\CacheManager::class, - 'cache.psr6' => \Symfony\Component\Cache\Adapter\Psr16Adapter::class, - 'cache.store' => \Illuminate\Cache\Repository::class, - 'clockwork' => \Clockwork\Clockwork::class, - 'clockwork.authenticator' => \Clockwork\Authentication\NullAuthenticator::class, - 'clockwork.cache' => \Clockwork\DataSource\LaravelCacheDataSource::class, - 'clockwork.eloquent' => \Clockwork\DataSource\EloquentDataSource::class, - 'clockwork.events' => \Clockwork\DataSource\LaravelEventsDataSource::class, - 'clockwork.laravel' => \Clockwork\DataSource\LaravelDataSource::class, - 'clockwork.notifications' => \Clockwork\DataSource\LaravelNotificationsDataSource::class, - 'clockwork.queue' => \Clockwork\DataSource\LaravelQueueDataSource::class, - 'clockwork.redis' => \Clockwork\DataSource\LaravelRedisDataSource::class, - 'clockwork.request' => \Clockwork\Request\Request::class, - 'clockwork.storage' => \Clockwork\Storage\FileStorage::class, - 'clockwork.support' => \Clockwork\Support\Laravel\ClockworkSupport::class, - 'clockwork.swift' => \Clockwork\DataSource\SwiftDataSource::class, - 'clockwork.views' => \Clockwork\DataSource\LaravelViewsDataSource::class, - 'clockwork.xdebug' => \Clockwork\DataSource\XdebugDataSource::class, - 'command.auth.resets.clear' => \Illuminate\Auth\Console\ClearResetsCommand::class, - 'command.cache.clear' => \Illuminate\Cache\Console\ClearCommand::class, - 'command.cache.forget' => \Illuminate\Cache\Console\ForgetCommand::class, - 'command.cache.table' => \Illuminate\Cache\Console\CacheTableCommand::class, - 'command.cast.make' => \Illuminate\Foundation\Console\CastMakeCommand::class, - 'command.channel.make' => \Illuminate\Foundation\Console\ChannelMakeCommand::class, - 'command.clear-compiled' => \Illuminate\Foundation\Console\ClearCompiledCommand::class, - 'command.component.make' => \Illuminate\Foundation\Console\ComponentMakeCommand::class, - 'command.config.cache' => \Illuminate\Foundation\Console\ConfigCacheCommand::class, - 'command.config.clear' => \Illuminate\Foundation\Console\ConfigClearCommand::class, - 'command.console.make' => \Illuminate\Foundation\Console\ConsoleMakeCommand::class, - 'command.controller.make' => \Illuminate\Routing\Console\ControllerMakeCommand::class, - 'command.db.wipe' => \Illuminate\Database\Console\WipeCommand::class, - 'command.debugbar.clear' => \Barryvdh\Debugbar\Console\ClearCommand::class, - 'command.down' => \Illuminate\Foundation\Console\DownCommand::class, - 'command.environment' => \Illuminate\Foundation\Console\EnvironmentCommand::class, - 'command.event.cache' => \Illuminate\Foundation\Console\EventCacheCommand::class, - 'command.event.clear' => \Illuminate\Foundation\Console\EventClearCommand::class, - 'command.event.generate' => \Illuminate\Foundation\Console\EventGenerateCommand::class, - 'command.event.list' => \Illuminate\Foundation\Console\EventListCommand::class, - 'command.event.make' => \Illuminate\Foundation\Console\EventMakeCommand::class, - 'command.exception.make' => \Illuminate\Foundation\Console\ExceptionMakeCommand::class, - 'command.factory.make' => \Illuminate\Database\Console\Factories\FactoryMakeCommand::class, - 'command.ide-helper.eloquent' => \Barryvdh\LaravelIdeHelper\Console\EloquentCommand::class, - 'command.ide-helper.generate' => \Barryvdh\LaravelIdeHelper\Console\GeneratorCommand::class, - 'command.ide-helper.meta' => \Barryvdh\LaravelIdeHelper\Console\MetaCommand::class, - 'command.ide-helper.models' => \Barryvdh\LaravelIdeHelper\Console\ModelsCommand::class, - 'command.job.make' => \Illuminate\Foundation\Console\JobMakeCommand::class, - 'command.key.generate' => \Illuminate\Foundation\Console\KeyGenerateCommand::class, - 'command.listener.make' => \Illuminate\Foundation\Console\ListenerMakeCommand::class, - 'command.mail.make' => \Illuminate\Foundation\Console\MailMakeCommand::class, - 'command.middleware.make' => \Illuminate\Routing\Console\MiddlewareMakeCommand::class, - 'command.migrate' => \Illuminate\Database\Console\Migrations\MigrateCommand::class, - 'command.migrate.fresh' => \Illuminate\Database\Console\Migrations\FreshCommand::class, - 'command.migrate.install' => \Illuminate\Database\Console\Migrations\InstallCommand::class, - 'command.migrate.make' => \Illuminate\Database\Console\Migrations\MigrateMakeCommand::class, - 'command.migrate.refresh' => \Illuminate\Database\Console\Migrations\RefreshCommand::class, - 'command.migrate.reset' => \Illuminate\Database\Console\Migrations\ResetCommand::class, - 'command.migrate.rollback' => \Illuminate\Database\Console\Migrations\RollbackCommand::class, - 'command.migrate.status' => \Illuminate\Database\Console\Migrations\StatusCommand::class, - 'command.model.make' => \Illuminate\Foundation\Console\ModelMakeCommand::class, - 'command.notification.make' => \Illuminate\Foundation\Console\NotificationMakeCommand::class, - 'command.notification.table' => \Illuminate\Notifications\Console\NotificationTableCommand::class, - 'command.observer.make' => \Illuminate\Foundation\Console\ObserverMakeCommand::class, - 'command.optimize' => \Illuminate\Foundation\Console\OptimizeCommand::class, - 'command.optimize.clear' => \Illuminate\Foundation\Console\OptimizeClearCommand::class, - 'command.package.discover' => \Illuminate\Foundation\Console\PackageDiscoverCommand::class, - 'command.policy.make' => \Illuminate\Foundation\Console\PolicyMakeCommand::class, - 'command.provider.make' => \Illuminate\Foundation\Console\ProviderMakeCommand::class, - 'command.queue.batches-table' => \Illuminate\Queue\Console\BatchesTableCommand::class, - 'command.queue.clear' => \Illuminate\Queue\Console\ClearCommand::class, - 'command.queue.failed' => \Illuminate\Queue\Console\ListFailedCommand::class, - 'command.queue.failed-table' => \Illuminate\Queue\Console\FailedTableCommand::class, - 'command.queue.flush' => \Illuminate\Queue\Console\FlushFailedCommand::class, - 'command.queue.forget' => \Illuminate\Queue\Console\ForgetFailedCommand::class, - 'command.queue.listen' => \Illuminate\Queue\Console\ListenCommand::class, - 'command.queue.prune-batches' => \Illuminate\Queue\Console\PruneBatchesCommand::class, - 'command.queue.restart' => \Illuminate\Queue\Console\RestartCommand::class, - 'command.queue.retry' => \Illuminate\Queue\Console\RetryCommand::class, - 'command.queue.retry-batch' => \Illuminate\Queue\Console\RetryBatchCommand::class, - 'command.queue.table' => \Illuminate\Queue\Console\TableCommand::class, - 'command.queue.work' => \Illuminate\Queue\Console\WorkCommand::class, - 'command.request.make' => \Illuminate\Foundation\Console\RequestMakeCommand::class, - 'command.resource.make' => \Illuminate\Foundation\Console\ResourceMakeCommand::class, - 'command.route.cache' => \Illuminate\Foundation\Console\RouteCacheCommand::class, - 'command.route.clear' => \Illuminate\Foundation\Console\RouteClearCommand::class, - 'command.route.list' => \Illuminate\Foundation\Console\RouteListCommand::class, - 'command.rule.make' => \Illuminate\Foundation\Console\RuleMakeCommand::class, - 'command.schema.dump' => \Illuminate\Database\Console\DumpCommand::class, - 'command.seed' => \Illuminate\Database\Console\Seeds\SeedCommand::class, - 'command.seeder.make' => \Illuminate\Database\Console\Seeds\SeederMakeCommand::class, - 'command.serve' => \Illuminate\Foundation\Console\ServeCommand::class, - 'command.session.table' => \Illuminate\Session\Console\SessionTableCommand::class, - 'command.storage.link' => \Illuminate\Foundation\Console\StorageLinkCommand::class, - 'command.stub.publish' => \Illuminate\Foundation\Console\StubPublishCommand::class, - 'command.test.make' => \Illuminate\Foundation\Console\TestMakeCommand::class, - 'command.up' => \Illuminate\Foundation\Console\UpCommand::class, - 'command.vendor.publish' => \Illuminate\Foundation\Console\VendorPublishCommand::class, - 'command.view.cache' => \Illuminate\Foundation\Console\ViewCacheCommand::class, - 'command.view.clear' => \Illuminate\Foundation\Console\ViewClearCommand::class, - 'composer' => \Illuminate\Support\Composer::class, - 'cookie' => \Illuminate\Cookie\CookieJar::class, - 'db' => \Illuminate\Database\DatabaseManager::class, - 'db.connection' => \Illuminate\Database\SQLiteConnection::class, - 'db.factory' => \Illuminate\Database\Connectors\ConnectionFactory::class, - 'db.transactions' => \Illuminate\Database\DatabaseTransactionsManager::class, - 'encrypter' => \Illuminate\Encryption\Encrypter::class, - 'events' => \Illuminate\Events\Dispatcher::class, - 'files' => \Illuminate\Filesystem\Filesystem::class, - 'filesystem' => \Illuminate\Filesystem\FilesystemManager::class, - 'filesystem.disk' => \Illuminate\Filesystem\FilesystemAdapter::class, - 'hash' => \Illuminate\Hashing\HashManager::class, - 'hash.driver' => \Illuminate\Hashing\BcryptHasher::class, - 'image-optimizer' => \Spatie\ImageOptimizer\OptimizerChain::class, - 'log' => \Illuminate\Log\LogManager::class, - 'mail.manager' => \Illuminate\Mail\MailManager::class, - 'mailer' => \Illuminate\Mail\Mailer::class, - 'markdown' => \League\CommonMark\CommonMarkConverter::class, - 'markdown.compiler' => \GrahamCampbell\Markdown\View\Compiler\MarkdownCompiler::class, - 'markdown.directive' => \GrahamCampbell\Markdown\View\Directive\MarkdownDirective::class, - 'markdown.environment' => \League\CommonMark\Environment::class, - 'memcached.connector' => \Illuminate\Cache\MemcachedConnector::class, - 'migration.creator' => \Illuminate\Database\Migrations\MigrationCreator::class, - 'migration.repository' => \Illuminate\Database\Migrations\DatabaseMigrationRepository::class, - 'migrator' => \Illuminate\Database\Migrations\Migrator::class, - 'queue' => \Illuminate\Queue\QueueManager::class, - 'queue.connection' => \Illuminate\Queue\SyncQueue::class, - 'queue.failer' => \Illuminate\Queue\Failed\DatabaseUuidFailedJobProvider::class, - 'queue.listener' => \Illuminate\Queue\Listener::class, - 'queue.worker' => \Illuminate\Queue\Worker::class, - 'redirect' => \Illuminate\Routing\Redirector::class, - 'redis' => \Illuminate\Redis\RedisManager::class, - 'router' => \Illuminate\Routing\Router::class, - 'session' => \Illuminate\Session\SessionManager::class, - 'session.store' => \Illuminate\Session\Store::class, - 'translation.loader' => \Illuminate\Translation\FileLoader::class, - 'translator' => \Illuminate\Translation\Translator::class, - 'url' => \Illuminate\Routing\UrlGenerator::class, - 'validation.presence' => \Illuminate\Validation\DatabasePresenceVerifier::class, - 'view' => \Illuminate\View\Factory::class, - 'view.engine.resolver' => \Illuminate\View\Engines\EngineResolver::class, - 'view.finder' => \Illuminate\View\FileViewFinder::class, - ])); - override(\Illuminate\Contracts\Container\Container::make(0), map([ - '' => '@', - 'AccessControl' => \App\ModelFunctions\SessionFunctions::class, - 'App\Actions\Albums\Extensions\PublicIds' => \App\Actions\Albums\Extensions\PublicIds::class, - 'App\Actions\Update\Apply' => \App\Actions\Update\Apply::class, - 'App\Actions\Update\Check' => \App\Actions\Update\Check::class, - 'App\Assets\Helpers' => \App\Assets\Helpers::class, - 'App\Image\ImageHandlerInterface' => \App\Image\ImageHandler::class, - 'App\Metadata\GitHubFunctions' => \App\Metadata\GitHubFunctions::class, - 'App\Metadata\GitRequest' => \App\Metadata\GitRequest::class, - 'App\Metadata\LycheeVersion' => \App\Metadata\LycheeVersion::class, - 'App\ModelFunctions\ConfigFunctions' => \App\ModelFunctions\ConfigFunctions::class, - 'App\ModelFunctions\SessionFunctions' => \App\ModelFunctions\SessionFunctions::class, - 'App\ModelFunctions\SymLinkFunctions' => \App\ModelFunctions\SymLinkFunctions::class, - 'Barryvdh\Debugbar\LaravelDebugbar' => \Barryvdh\Debugbar\LaravelDebugbar::class, - 'Cose\Algorithm\Manager' => \Cose\Algorithm\Manager::class, - 'DarkGhostHunter\Larapass\WebAuthn\PublicKeyCredentialParametersCollection' => \DarkGhostHunter\Larapass\WebAuthn\PublicKeyCredentialParametersCollection::class, - 'DarkGhostHunter\Larapass\WebAuthn\WebAuthnAssertValidator' => \DarkGhostHunter\Larapass\WebAuthn\WebAuthnAssertValidator::class, - 'DarkGhostHunter\Larapass\WebAuthn\WebAuthnAttestCreator' => \DarkGhostHunter\Larapass\WebAuthn\WebAuthnAttestCreator::class, - 'DarkGhostHunter\Larapass\WebAuthn\WebAuthnAttestValidator' => \DarkGhostHunter\Larapass\WebAuthn\WebAuthnAttestValidator::class, - 'Helpers' => \App\Assets\Helpers::class, - 'Illuminate\Auth\Middleware\RequirePassword' => \Illuminate\Auth\Middleware\RequirePassword::class, - 'Illuminate\Broadcasting\BroadcastManager' => \Illuminate\Broadcasting\BroadcastManager::class, - 'Illuminate\Bus\BatchRepository' => \Illuminate\Bus\DatabaseBatchRepository::class, - 'Illuminate\Bus\DatabaseBatchRepository' => \Illuminate\Bus\DatabaseBatchRepository::class, - 'Illuminate\Bus\Dispatcher' => \Illuminate\Bus\Dispatcher::class, - 'Illuminate\Cache\RateLimiter' => \Illuminate\Cache\RateLimiter::class, - 'Illuminate\Console\Scheduling\Schedule' => \Illuminate\Console\Scheduling\Schedule::class, - 'Illuminate\Console\Scheduling\ScheduleFinishCommand' => \Illuminate\Console\Scheduling\ScheduleFinishCommand::class, - 'Illuminate\Console\Scheduling\ScheduleListCommand' => \Illuminate\Console\Scheduling\ScheduleListCommand::class, - 'Illuminate\Console\Scheduling\ScheduleRunCommand' => \Illuminate\Console\Scheduling\ScheduleRunCommand::class, - 'Illuminate\Console\Scheduling\ScheduleTestCommand' => \Illuminate\Console\Scheduling\ScheduleTestCommand::class, - 'Illuminate\Console\Scheduling\ScheduleWorkCommand' => \Illuminate\Console\Scheduling\ScheduleWorkCommand::class, - 'Illuminate\Contracts\Auth\Access\Gate' => \Illuminate\Auth\Access\Gate::class, - 'Illuminate\Contracts\Broadcasting\Broadcaster' => \Illuminate\Broadcasting\Broadcasters\LogBroadcaster::class, - 'Illuminate\Contracts\Console\Kernel' => \App\Console\Kernel::class, - 'Illuminate\Contracts\Debug\ExceptionHandler' => \App\Exceptions\Handler::class, - 'Illuminate\Contracts\Http\Kernel' => \App\Http\Kernel::class, - 'Illuminate\Contracts\Pipeline\Hub' => \Illuminate\Pipeline\Hub::class, - 'Illuminate\Contracts\Queue\EntityResolver' => \Illuminate\Database\Eloquent\QueueEntityResolver::class, - 'Illuminate\Contracts\Routing\ResponseFactory' => \Illuminate\Routing\ResponseFactory::class, - 'Illuminate\Contracts\Validation\UncompromisedVerifier' => \Illuminate\Validation\NotPwnedVerifier::class, - 'Illuminate\Database\Console\DbCommand' => \Illuminate\Database\Console\DbCommand::class, - 'Illuminate\Foundation\Mix' => \Illuminate\Foundation\Mix::class, - 'Illuminate\Foundation\PackageManifest' => \Illuminate\Foundation\PackageManifest::class, - 'Illuminate\Mail\Markdown' => \Illuminate\Mail\Markdown::class, - 'Illuminate\Notifications\ChannelManager' => \Illuminate\Notifications\ChannelManager::class, - 'Illuminate\Routing\Contracts\ControllerDispatcher' => \Illuminate\Routing\ControllerDispatcher::class, - 'Illuminate\Session\Middleware\StartSession' => \Illuminate\Session\Middleware\StartSession::class, - 'Illuminate\Testing\ParallelTesting' => \Illuminate\Testing\ParallelTesting::class, - 'Livewire\LivewireComponentsFinder' => \Livewire\LivewireComponentsFinder::class, - 'Livewire\LivewireManager' => \Livewire\LivewireManager::class, - 'Psr\Http\Message\ResponseInterface' => \Nyholm\Psr7\Response::class, - 'Psr\Http\Message\ServerRequestInterface' => \Nyholm\Psr7\ServerRequest::class, - 'Spatie\ImageOptimizer\OptimizerChain' => \Spatie\ImageOptimizer\OptimizerChain::class, - 'Webauthn\AttestationStatement\AttestationObjectLoader' => \Webauthn\AttestationStatement\AttestationObjectLoader::class, - 'Webauthn\AttestationStatement\AttestationStatementSupportManager' => \Webauthn\AttestationStatement\AttestationStatementSupportManager::class, - 'Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs' => \Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs::class, - 'Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler' => \Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler::class, - 'Webauthn\AuthenticatorAssertionResponseValidator' => \Webauthn\AuthenticatorAssertionResponseValidator::class, - 'Webauthn\AuthenticatorAttestationResponseValidator' => \Webauthn\AuthenticatorAttestationResponseValidator::class, - 'Webauthn\AuthenticatorSelectionCriteria' => \DarkGhostHunter\Larapass\WebAuthn\AuthenticatorSelectionCriteria::class, - 'Webauthn\Counter\CounterChecker' => \Webauthn\Counter\ThrowExceptionIfInvalid::class, - 'Webauthn\PublicKeyCredentialLoader' => \Webauthn\PublicKeyCredentialLoader::class, - 'Webauthn\PublicKeyCredentialRpEntity' => \Webauthn\PublicKeyCredentialRpEntity::class, - 'Webauthn\PublicKeyCredentialSourceRepository' => \DarkGhostHunter\Larapass\Eloquent\WebAuthnCredential::class, - 'Webauthn\TokenBinding\TokenBindingHandler' => \Webauthn\TokenBinding\IgnoreTokenBindingHandler::class, - 'auth' => \Illuminate\Auth\AuthManager::class, - 'auth.driver' => \Illuminate\Auth\SessionGuard::class, - 'blade.compiler' => \Illuminate\View\Compilers\BladeCompiler::class, - 'cache' => \Illuminate\Cache\CacheManager::class, - 'cache.psr6' => \Symfony\Component\Cache\Adapter\Psr16Adapter::class, - 'cache.store' => \Illuminate\Cache\Repository::class, - 'clockwork' => \Clockwork\Clockwork::class, - 'clockwork.authenticator' => \Clockwork\Authentication\NullAuthenticator::class, - 'clockwork.cache' => \Clockwork\DataSource\LaravelCacheDataSource::class, - 'clockwork.eloquent' => \Clockwork\DataSource\EloquentDataSource::class, - 'clockwork.events' => \Clockwork\DataSource\LaravelEventsDataSource::class, - 'clockwork.laravel' => \Clockwork\DataSource\LaravelDataSource::class, - 'clockwork.notifications' => \Clockwork\DataSource\LaravelNotificationsDataSource::class, - 'clockwork.queue' => \Clockwork\DataSource\LaravelQueueDataSource::class, - 'clockwork.redis' => \Clockwork\DataSource\LaravelRedisDataSource::class, - 'clockwork.request' => \Clockwork\Request\Request::class, - 'clockwork.storage' => \Clockwork\Storage\FileStorage::class, - 'clockwork.support' => \Clockwork\Support\Laravel\ClockworkSupport::class, - 'clockwork.swift' => \Clockwork\DataSource\SwiftDataSource::class, - 'clockwork.views' => \Clockwork\DataSource\LaravelViewsDataSource::class, - 'clockwork.xdebug' => \Clockwork\DataSource\XdebugDataSource::class, - 'command.auth.resets.clear' => \Illuminate\Auth\Console\ClearResetsCommand::class, - 'command.cache.clear' => \Illuminate\Cache\Console\ClearCommand::class, - 'command.cache.forget' => \Illuminate\Cache\Console\ForgetCommand::class, - 'command.cache.table' => \Illuminate\Cache\Console\CacheTableCommand::class, - 'command.cast.make' => \Illuminate\Foundation\Console\CastMakeCommand::class, - 'command.channel.make' => \Illuminate\Foundation\Console\ChannelMakeCommand::class, - 'command.clear-compiled' => \Illuminate\Foundation\Console\ClearCompiledCommand::class, - 'command.component.make' => \Illuminate\Foundation\Console\ComponentMakeCommand::class, - 'command.config.cache' => \Illuminate\Foundation\Console\ConfigCacheCommand::class, - 'command.config.clear' => \Illuminate\Foundation\Console\ConfigClearCommand::class, - 'command.console.make' => \Illuminate\Foundation\Console\ConsoleMakeCommand::class, - 'command.controller.make' => \Illuminate\Routing\Console\ControllerMakeCommand::class, - 'command.db.wipe' => \Illuminate\Database\Console\WipeCommand::class, - 'command.debugbar.clear' => \Barryvdh\Debugbar\Console\ClearCommand::class, - 'command.down' => \Illuminate\Foundation\Console\DownCommand::class, - 'command.environment' => \Illuminate\Foundation\Console\EnvironmentCommand::class, - 'command.event.cache' => \Illuminate\Foundation\Console\EventCacheCommand::class, - 'command.event.clear' => \Illuminate\Foundation\Console\EventClearCommand::class, - 'command.event.generate' => \Illuminate\Foundation\Console\EventGenerateCommand::class, - 'command.event.list' => \Illuminate\Foundation\Console\EventListCommand::class, - 'command.event.make' => \Illuminate\Foundation\Console\EventMakeCommand::class, - 'command.exception.make' => \Illuminate\Foundation\Console\ExceptionMakeCommand::class, - 'command.factory.make' => \Illuminate\Database\Console\Factories\FactoryMakeCommand::class, - 'command.ide-helper.eloquent' => \Barryvdh\LaravelIdeHelper\Console\EloquentCommand::class, - 'command.ide-helper.generate' => \Barryvdh\LaravelIdeHelper\Console\GeneratorCommand::class, - 'command.ide-helper.meta' => \Barryvdh\LaravelIdeHelper\Console\MetaCommand::class, - 'command.ide-helper.models' => \Barryvdh\LaravelIdeHelper\Console\ModelsCommand::class, - 'command.job.make' => \Illuminate\Foundation\Console\JobMakeCommand::class, - 'command.key.generate' => \Illuminate\Foundation\Console\KeyGenerateCommand::class, - 'command.listener.make' => \Illuminate\Foundation\Console\ListenerMakeCommand::class, - 'command.mail.make' => \Illuminate\Foundation\Console\MailMakeCommand::class, - 'command.middleware.make' => \Illuminate\Routing\Console\MiddlewareMakeCommand::class, - 'command.migrate' => \Illuminate\Database\Console\Migrations\MigrateCommand::class, - 'command.migrate.fresh' => \Illuminate\Database\Console\Migrations\FreshCommand::class, - 'command.migrate.install' => \Illuminate\Database\Console\Migrations\InstallCommand::class, - 'command.migrate.make' => \Illuminate\Database\Console\Migrations\MigrateMakeCommand::class, - 'command.migrate.refresh' => \Illuminate\Database\Console\Migrations\RefreshCommand::class, - 'command.migrate.reset' => \Illuminate\Database\Console\Migrations\ResetCommand::class, - 'command.migrate.rollback' => \Illuminate\Database\Console\Migrations\RollbackCommand::class, - 'command.migrate.status' => \Illuminate\Database\Console\Migrations\StatusCommand::class, - 'command.model.make' => \Illuminate\Foundation\Console\ModelMakeCommand::class, - 'command.notification.make' => \Illuminate\Foundation\Console\NotificationMakeCommand::class, - 'command.notification.table' => \Illuminate\Notifications\Console\NotificationTableCommand::class, - 'command.observer.make' => \Illuminate\Foundation\Console\ObserverMakeCommand::class, - 'command.optimize' => \Illuminate\Foundation\Console\OptimizeCommand::class, - 'command.optimize.clear' => \Illuminate\Foundation\Console\OptimizeClearCommand::class, - 'command.package.discover' => \Illuminate\Foundation\Console\PackageDiscoverCommand::class, - 'command.policy.make' => \Illuminate\Foundation\Console\PolicyMakeCommand::class, - 'command.provider.make' => \Illuminate\Foundation\Console\ProviderMakeCommand::class, - 'command.queue.batches-table' => \Illuminate\Queue\Console\BatchesTableCommand::class, - 'command.queue.clear' => \Illuminate\Queue\Console\ClearCommand::class, - 'command.queue.failed' => \Illuminate\Queue\Console\ListFailedCommand::class, - 'command.queue.failed-table' => \Illuminate\Queue\Console\FailedTableCommand::class, - 'command.queue.flush' => \Illuminate\Queue\Console\FlushFailedCommand::class, - 'command.queue.forget' => \Illuminate\Queue\Console\ForgetFailedCommand::class, - 'command.queue.listen' => \Illuminate\Queue\Console\ListenCommand::class, - 'command.queue.prune-batches' => \Illuminate\Queue\Console\PruneBatchesCommand::class, - 'command.queue.restart' => \Illuminate\Queue\Console\RestartCommand::class, - 'command.queue.retry' => \Illuminate\Queue\Console\RetryCommand::class, - 'command.queue.retry-batch' => \Illuminate\Queue\Console\RetryBatchCommand::class, - 'command.queue.table' => \Illuminate\Queue\Console\TableCommand::class, - 'command.queue.work' => \Illuminate\Queue\Console\WorkCommand::class, - 'command.request.make' => \Illuminate\Foundation\Console\RequestMakeCommand::class, - 'command.resource.make' => \Illuminate\Foundation\Console\ResourceMakeCommand::class, - 'command.route.cache' => \Illuminate\Foundation\Console\RouteCacheCommand::class, - 'command.route.clear' => \Illuminate\Foundation\Console\RouteClearCommand::class, - 'command.route.list' => \Illuminate\Foundation\Console\RouteListCommand::class, - 'command.rule.make' => \Illuminate\Foundation\Console\RuleMakeCommand::class, - 'command.schema.dump' => \Illuminate\Database\Console\DumpCommand::class, - 'command.seed' => \Illuminate\Database\Console\Seeds\SeedCommand::class, - 'command.seeder.make' => \Illuminate\Database\Console\Seeds\SeederMakeCommand::class, - 'command.serve' => \Illuminate\Foundation\Console\ServeCommand::class, - 'command.session.table' => \Illuminate\Session\Console\SessionTableCommand::class, - 'command.storage.link' => \Illuminate\Foundation\Console\StorageLinkCommand::class, - 'command.stub.publish' => \Illuminate\Foundation\Console\StubPublishCommand::class, - 'command.test.make' => \Illuminate\Foundation\Console\TestMakeCommand::class, - 'command.up' => \Illuminate\Foundation\Console\UpCommand::class, - 'command.vendor.publish' => \Illuminate\Foundation\Console\VendorPublishCommand::class, - 'command.view.cache' => \Illuminate\Foundation\Console\ViewCacheCommand::class, - 'command.view.clear' => \Illuminate\Foundation\Console\ViewClearCommand::class, - 'composer' => \Illuminate\Support\Composer::class, - 'cookie' => \Illuminate\Cookie\CookieJar::class, - 'db' => \Illuminate\Database\DatabaseManager::class, - 'db.connection' => \Illuminate\Database\SQLiteConnection::class, - 'db.factory' => \Illuminate\Database\Connectors\ConnectionFactory::class, - 'db.transactions' => \Illuminate\Database\DatabaseTransactionsManager::class, - 'encrypter' => \Illuminate\Encryption\Encrypter::class, - 'events' => \Illuminate\Events\Dispatcher::class, - 'files' => \Illuminate\Filesystem\Filesystem::class, - 'filesystem' => \Illuminate\Filesystem\FilesystemManager::class, - 'filesystem.disk' => \Illuminate\Filesystem\FilesystemAdapter::class, - 'hash' => \Illuminate\Hashing\HashManager::class, - 'hash.driver' => \Illuminate\Hashing\BcryptHasher::class, - 'image-optimizer' => \Spatie\ImageOptimizer\OptimizerChain::class, - 'log' => \Illuminate\Log\LogManager::class, - 'mail.manager' => \Illuminate\Mail\MailManager::class, - 'mailer' => \Illuminate\Mail\Mailer::class, - 'markdown' => \League\CommonMark\CommonMarkConverter::class, - 'markdown.compiler' => \GrahamCampbell\Markdown\View\Compiler\MarkdownCompiler::class, - 'markdown.directive' => \GrahamCampbell\Markdown\View\Directive\MarkdownDirective::class, - 'markdown.environment' => \League\CommonMark\Environment::class, - 'memcached.connector' => \Illuminate\Cache\MemcachedConnector::class, - 'migration.creator' => \Illuminate\Database\Migrations\MigrationCreator::class, - 'migration.repository' => \Illuminate\Database\Migrations\DatabaseMigrationRepository::class, - 'migrator' => \Illuminate\Database\Migrations\Migrator::class, - 'queue' => \Illuminate\Queue\QueueManager::class, - 'queue.connection' => \Illuminate\Queue\SyncQueue::class, - 'queue.failer' => \Illuminate\Queue\Failed\DatabaseUuidFailedJobProvider::class, - 'queue.listener' => \Illuminate\Queue\Listener::class, - 'queue.worker' => \Illuminate\Queue\Worker::class, - 'redirect' => \Illuminate\Routing\Redirector::class, - 'redis' => \Illuminate\Redis\RedisManager::class, - 'router' => \Illuminate\Routing\Router::class, - 'session' => \Illuminate\Session\SessionManager::class, - 'session.store' => \Illuminate\Session\Store::class, - 'translation.loader' => \Illuminate\Translation\FileLoader::class, - 'translator' => \Illuminate\Translation\Translator::class, - 'url' => \Illuminate\Routing\UrlGenerator::class, - 'validation.presence' => \Illuminate\Validation\DatabasePresenceVerifier::class, - 'view' => \Illuminate\View\Factory::class, - 'view.engine.resolver' => \Illuminate\View\Engines\EngineResolver::class, - 'view.finder' => \Illuminate\View\FileViewFinder::class, - ])); - override(\Illuminate\Contracts\Container\Container::makeWith(0), map([ - '' => '@', - 'AccessControl' => \App\ModelFunctions\SessionFunctions::class, - 'App\Actions\Albums\Extensions\PublicIds' => \App\Actions\Albums\Extensions\PublicIds::class, - 'App\Actions\Update\Apply' => \App\Actions\Update\Apply::class, - 'App\Actions\Update\Check' => \App\Actions\Update\Check::class, - 'App\Assets\Helpers' => \App\Assets\Helpers::class, - 'App\Image\ImageHandlerInterface' => \App\Image\ImageHandler::class, - 'App\Metadata\GitHubFunctions' => \App\Metadata\GitHubFunctions::class, - 'App\Metadata\GitRequest' => \App\Metadata\GitRequest::class, - 'App\Metadata\LycheeVersion' => \App\Metadata\LycheeVersion::class, - 'App\ModelFunctions\ConfigFunctions' => \App\ModelFunctions\ConfigFunctions::class, - 'App\ModelFunctions\SessionFunctions' => \App\ModelFunctions\SessionFunctions::class, - 'App\ModelFunctions\SymLinkFunctions' => \App\ModelFunctions\SymLinkFunctions::class, - 'Barryvdh\Debugbar\LaravelDebugbar' => \Barryvdh\Debugbar\LaravelDebugbar::class, - 'Cose\Algorithm\Manager' => \Cose\Algorithm\Manager::class, - 'DarkGhostHunter\Larapass\WebAuthn\PublicKeyCredentialParametersCollection' => \DarkGhostHunter\Larapass\WebAuthn\PublicKeyCredentialParametersCollection::class, - 'DarkGhostHunter\Larapass\WebAuthn\WebAuthnAssertValidator' => \DarkGhostHunter\Larapass\WebAuthn\WebAuthnAssertValidator::class, - 'DarkGhostHunter\Larapass\WebAuthn\WebAuthnAttestCreator' => \DarkGhostHunter\Larapass\WebAuthn\WebAuthnAttestCreator::class, - 'DarkGhostHunter\Larapass\WebAuthn\WebAuthnAttestValidator' => \DarkGhostHunter\Larapass\WebAuthn\WebAuthnAttestValidator::class, - 'Helpers' => \App\Assets\Helpers::class, - 'Illuminate\Auth\Middleware\RequirePassword' => \Illuminate\Auth\Middleware\RequirePassword::class, - 'Illuminate\Broadcasting\BroadcastManager' => \Illuminate\Broadcasting\BroadcastManager::class, - 'Illuminate\Bus\BatchRepository' => \Illuminate\Bus\DatabaseBatchRepository::class, - 'Illuminate\Bus\DatabaseBatchRepository' => \Illuminate\Bus\DatabaseBatchRepository::class, - 'Illuminate\Bus\Dispatcher' => \Illuminate\Bus\Dispatcher::class, - 'Illuminate\Cache\RateLimiter' => \Illuminate\Cache\RateLimiter::class, - 'Illuminate\Console\Scheduling\Schedule' => \Illuminate\Console\Scheduling\Schedule::class, - 'Illuminate\Console\Scheduling\ScheduleFinishCommand' => \Illuminate\Console\Scheduling\ScheduleFinishCommand::class, - 'Illuminate\Console\Scheduling\ScheduleListCommand' => \Illuminate\Console\Scheduling\ScheduleListCommand::class, - 'Illuminate\Console\Scheduling\ScheduleRunCommand' => \Illuminate\Console\Scheduling\ScheduleRunCommand::class, - 'Illuminate\Console\Scheduling\ScheduleTestCommand' => \Illuminate\Console\Scheduling\ScheduleTestCommand::class, - 'Illuminate\Console\Scheduling\ScheduleWorkCommand' => \Illuminate\Console\Scheduling\ScheduleWorkCommand::class, - 'Illuminate\Contracts\Auth\Access\Gate' => \Illuminate\Auth\Access\Gate::class, - 'Illuminate\Contracts\Broadcasting\Broadcaster' => \Illuminate\Broadcasting\Broadcasters\LogBroadcaster::class, - 'Illuminate\Contracts\Console\Kernel' => \App\Console\Kernel::class, - 'Illuminate\Contracts\Debug\ExceptionHandler' => \App\Exceptions\Handler::class, - 'Illuminate\Contracts\Http\Kernel' => \App\Http\Kernel::class, - 'Illuminate\Contracts\Pipeline\Hub' => \Illuminate\Pipeline\Hub::class, - 'Illuminate\Contracts\Queue\EntityResolver' => \Illuminate\Database\Eloquent\QueueEntityResolver::class, - 'Illuminate\Contracts\Routing\ResponseFactory' => \Illuminate\Routing\ResponseFactory::class, - 'Illuminate\Contracts\Validation\UncompromisedVerifier' => \Illuminate\Validation\NotPwnedVerifier::class, - 'Illuminate\Database\Console\DbCommand' => \Illuminate\Database\Console\DbCommand::class, - 'Illuminate\Foundation\Mix' => \Illuminate\Foundation\Mix::class, - 'Illuminate\Foundation\PackageManifest' => \Illuminate\Foundation\PackageManifest::class, - 'Illuminate\Mail\Markdown' => \Illuminate\Mail\Markdown::class, - 'Illuminate\Notifications\ChannelManager' => \Illuminate\Notifications\ChannelManager::class, - 'Illuminate\Routing\Contracts\ControllerDispatcher' => \Illuminate\Routing\ControllerDispatcher::class, - 'Illuminate\Session\Middleware\StartSession' => \Illuminate\Session\Middleware\StartSession::class, - 'Illuminate\Testing\ParallelTesting' => \Illuminate\Testing\ParallelTesting::class, - 'Livewire\LivewireComponentsFinder' => \Livewire\LivewireComponentsFinder::class, - 'Livewire\LivewireManager' => \Livewire\LivewireManager::class, - 'Psr\Http\Message\ResponseInterface' => \Nyholm\Psr7\Response::class, - 'Psr\Http\Message\ServerRequestInterface' => \Nyholm\Psr7\ServerRequest::class, - 'Spatie\ImageOptimizer\OptimizerChain' => \Spatie\ImageOptimizer\OptimizerChain::class, - 'Webauthn\AttestationStatement\AttestationObjectLoader' => \Webauthn\AttestationStatement\AttestationObjectLoader::class, - 'Webauthn\AttestationStatement\AttestationStatementSupportManager' => \Webauthn\AttestationStatement\AttestationStatementSupportManager::class, - 'Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs' => \Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs::class, - 'Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler' => \Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler::class, - 'Webauthn\AuthenticatorAssertionResponseValidator' => \Webauthn\AuthenticatorAssertionResponseValidator::class, - 'Webauthn\AuthenticatorAttestationResponseValidator' => \Webauthn\AuthenticatorAttestationResponseValidator::class, - 'Webauthn\AuthenticatorSelectionCriteria' => \DarkGhostHunter\Larapass\WebAuthn\AuthenticatorSelectionCriteria::class, - 'Webauthn\Counter\CounterChecker' => \Webauthn\Counter\ThrowExceptionIfInvalid::class, - 'Webauthn\PublicKeyCredentialLoader' => \Webauthn\PublicKeyCredentialLoader::class, - 'Webauthn\PublicKeyCredentialRpEntity' => \Webauthn\PublicKeyCredentialRpEntity::class, - 'Webauthn\PublicKeyCredentialSourceRepository' => \DarkGhostHunter\Larapass\Eloquent\WebAuthnCredential::class, - 'Webauthn\TokenBinding\TokenBindingHandler' => \Webauthn\TokenBinding\IgnoreTokenBindingHandler::class, - 'auth' => \Illuminate\Auth\AuthManager::class, - 'auth.driver' => \Illuminate\Auth\SessionGuard::class, - 'blade.compiler' => \Illuminate\View\Compilers\BladeCompiler::class, - 'cache' => \Illuminate\Cache\CacheManager::class, - 'cache.psr6' => \Symfony\Component\Cache\Adapter\Psr16Adapter::class, - 'cache.store' => \Illuminate\Cache\Repository::class, - 'clockwork' => \Clockwork\Clockwork::class, - 'clockwork.authenticator' => \Clockwork\Authentication\NullAuthenticator::class, - 'clockwork.cache' => \Clockwork\DataSource\LaravelCacheDataSource::class, - 'clockwork.eloquent' => \Clockwork\DataSource\EloquentDataSource::class, - 'clockwork.events' => \Clockwork\DataSource\LaravelEventsDataSource::class, - 'clockwork.laravel' => \Clockwork\DataSource\LaravelDataSource::class, - 'clockwork.notifications' => \Clockwork\DataSource\LaravelNotificationsDataSource::class, - 'clockwork.queue' => \Clockwork\DataSource\LaravelQueueDataSource::class, - 'clockwork.redis' => \Clockwork\DataSource\LaravelRedisDataSource::class, - 'clockwork.request' => \Clockwork\Request\Request::class, - 'clockwork.storage' => \Clockwork\Storage\FileStorage::class, - 'clockwork.support' => \Clockwork\Support\Laravel\ClockworkSupport::class, - 'clockwork.swift' => \Clockwork\DataSource\SwiftDataSource::class, - 'clockwork.views' => \Clockwork\DataSource\LaravelViewsDataSource::class, - 'clockwork.xdebug' => \Clockwork\DataSource\XdebugDataSource::class, - 'command.auth.resets.clear' => \Illuminate\Auth\Console\ClearResetsCommand::class, - 'command.cache.clear' => \Illuminate\Cache\Console\ClearCommand::class, - 'command.cache.forget' => \Illuminate\Cache\Console\ForgetCommand::class, - 'command.cache.table' => \Illuminate\Cache\Console\CacheTableCommand::class, - 'command.cast.make' => \Illuminate\Foundation\Console\CastMakeCommand::class, - 'command.channel.make' => \Illuminate\Foundation\Console\ChannelMakeCommand::class, - 'command.clear-compiled' => \Illuminate\Foundation\Console\ClearCompiledCommand::class, - 'command.component.make' => \Illuminate\Foundation\Console\ComponentMakeCommand::class, - 'command.config.cache' => \Illuminate\Foundation\Console\ConfigCacheCommand::class, - 'command.config.clear' => \Illuminate\Foundation\Console\ConfigClearCommand::class, - 'command.console.make' => \Illuminate\Foundation\Console\ConsoleMakeCommand::class, - 'command.controller.make' => \Illuminate\Routing\Console\ControllerMakeCommand::class, - 'command.db.wipe' => \Illuminate\Database\Console\WipeCommand::class, - 'command.debugbar.clear' => \Barryvdh\Debugbar\Console\ClearCommand::class, - 'command.down' => \Illuminate\Foundation\Console\DownCommand::class, - 'command.environment' => \Illuminate\Foundation\Console\EnvironmentCommand::class, - 'command.event.cache' => \Illuminate\Foundation\Console\EventCacheCommand::class, - 'command.event.clear' => \Illuminate\Foundation\Console\EventClearCommand::class, - 'command.event.generate' => \Illuminate\Foundation\Console\EventGenerateCommand::class, - 'command.event.list' => \Illuminate\Foundation\Console\EventListCommand::class, - 'command.event.make' => \Illuminate\Foundation\Console\EventMakeCommand::class, - 'command.exception.make' => \Illuminate\Foundation\Console\ExceptionMakeCommand::class, - 'command.factory.make' => \Illuminate\Database\Console\Factories\FactoryMakeCommand::class, - 'command.ide-helper.eloquent' => \Barryvdh\LaravelIdeHelper\Console\EloquentCommand::class, - 'command.ide-helper.generate' => \Barryvdh\LaravelIdeHelper\Console\GeneratorCommand::class, - 'command.ide-helper.meta' => \Barryvdh\LaravelIdeHelper\Console\MetaCommand::class, - 'command.ide-helper.models' => \Barryvdh\LaravelIdeHelper\Console\ModelsCommand::class, - 'command.job.make' => \Illuminate\Foundation\Console\JobMakeCommand::class, - 'command.key.generate' => \Illuminate\Foundation\Console\KeyGenerateCommand::class, - 'command.listener.make' => \Illuminate\Foundation\Console\ListenerMakeCommand::class, - 'command.mail.make' => \Illuminate\Foundation\Console\MailMakeCommand::class, - 'command.middleware.make' => \Illuminate\Routing\Console\MiddlewareMakeCommand::class, - 'command.migrate' => \Illuminate\Database\Console\Migrations\MigrateCommand::class, - 'command.migrate.fresh' => \Illuminate\Database\Console\Migrations\FreshCommand::class, - 'command.migrate.install' => \Illuminate\Database\Console\Migrations\InstallCommand::class, - 'command.migrate.make' => \Illuminate\Database\Console\Migrations\MigrateMakeCommand::class, - 'command.migrate.refresh' => \Illuminate\Database\Console\Migrations\RefreshCommand::class, - 'command.migrate.reset' => \Illuminate\Database\Console\Migrations\ResetCommand::class, - 'command.migrate.rollback' => \Illuminate\Database\Console\Migrations\RollbackCommand::class, - 'command.migrate.status' => \Illuminate\Database\Console\Migrations\StatusCommand::class, - 'command.model.make' => \Illuminate\Foundation\Console\ModelMakeCommand::class, - 'command.notification.make' => \Illuminate\Foundation\Console\NotificationMakeCommand::class, - 'command.notification.table' => \Illuminate\Notifications\Console\NotificationTableCommand::class, - 'command.observer.make' => \Illuminate\Foundation\Console\ObserverMakeCommand::class, - 'command.optimize' => \Illuminate\Foundation\Console\OptimizeCommand::class, - 'command.optimize.clear' => \Illuminate\Foundation\Console\OptimizeClearCommand::class, - 'command.package.discover' => \Illuminate\Foundation\Console\PackageDiscoverCommand::class, - 'command.policy.make' => \Illuminate\Foundation\Console\PolicyMakeCommand::class, - 'command.provider.make' => \Illuminate\Foundation\Console\ProviderMakeCommand::class, - 'command.queue.batches-table' => \Illuminate\Queue\Console\BatchesTableCommand::class, - 'command.queue.clear' => \Illuminate\Queue\Console\ClearCommand::class, - 'command.queue.failed' => \Illuminate\Queue\Console\ListFailedCommand::class, - 'command.queue.failed-table' => \Illuminate\Queue\Console\FailedTableCommand::class, - 'command.queue.flush' => \Illuminate\Queue\Console\FlushFailedCommand::class, - 'command.queue.forget' => \Illuminate\Queue\Console\ForgetFailedCommand::class, - 'command.queue.listen' => \Illuminate\Queue\Console\ListenCommand::class, - 'command.queue.prune-batches' => \Illuminate\Queue\Console\PruneBatchesCommand::class, - 'command.queue.restart' => \Illuminate\Queue\Console\RestartCommand::class, - 'command.queue.retry' => \Illuminate\Queue\Console\RetryCommand::class, - 'command.queue.retry-batch' => \Illuminate\Queue\Console\RetryBatchCommand::class, - 'command.queue.table' => \Illuminate\Queue\Console\TableCommand::class, - 'command.queue.work' => \Illuminate\Queue\Console\WorkCommand::class, - 'command.request.make' => \Illuminate\Foundation\Console\RequestMakeCommand::class, - 'command.resource.make' => \Illuminate\Foundation\Console\ResourceMakeCommand::class, - 'command.route.cache' => \Illuminate\Foundation\Console\RouteCacheCommand::class, - 'command.route.clear' => \Illuminate\Foundation\Console\RouteClearCommand::class, - 'command.route.list' => \Illuminate\Foundation\Console\RouteListCommand::class, - 'command.rule.make' => \Illuminate\Foundation\Console\RuleMakeCommand::class, - 'command.schema.dump' => \Illuminate\Database\Console\DumpCommand::class, - 'command.seed' => \Illuminate\Database\Console\Seeds\SeedCommand::class, - 'command.seeder.make' => \Illuminate\Database\Console\Seeds\SeederMakeCommand::class, - 'command.serve' => \Illuminate\Foundation\Console\ServeCommand::class, - 'command.session.table' => \Illuminate\Session\Console\SessionTableCommand::class, - 'command.storage.link' => \Illuminate\Foundation\Console\StorageLinkCommand::class, - 'command.stub.publish' => \Illuminate\Foundation\Console\StubPublishCommand::class, - 'command.test.make' => \Illuminate\Foundation\Console\TestMakeCommand::class, - 'command.up' => \Illuminate\Foundation\Console\UpCommand::class, - 'command.vendor.publish' => \Illuminate\Foundation\Console\VendorPublishCommand::class, - 'command.view.cache' => \Illuminate\Foundation\Console\ViewCacheCommand::class, - 'command.view.clear' => \Illuminate\Foundation\Console\ViewClearCommand::class, - 'composer' => \Illuminate\Support\Composer::class, - 'cookie' => \Illuminate\Cookie\CookieJar::class, - 'db' => \Illuminate\Database\DatabaseManager::class, - 'db.connection' => \Illuminate\Database\SQLiteConnection::class, - 'db.factory' => \Illuminate\Database\Connectors\ConnectionFactory::class, - 'db.transactions' => \Illuminate\Database\DatabaseTransactionsManager::class, - 'encrypter' => \Illuminate\Encryption\Encrypter::class, - 'events' => \Illuminate\Events\Dispatcher::class, - 'files' => \Illuminate\Filesystem\Filesystem::class, - 'filesystem' => \Illuminate\Filesystem\FilesystemManager::class, - 'filesystem.disk' => \Illuminate\Filesystem\FilesystemAdapter::class, - 'hash' => \Illuminate\Hashing\HashManager::class, - 'hash.driver' => \Illuminate\Hashing\BcryptHasher::class, - 'image-optimizer' => \Spatie\ImageOptimizer\OptimizerChain::class, - 'log' => \Illuminate\Log\LogManager::class, - 'mail.manager' => \Illuminate\Mail\MailManager::class, - 'mailer' => \Illuminate\Mail\Mailer::class, - 'markdown' => \League\CommonMark\CommonMarkConverter::class, - 'markdown.compiler' => \GrahamCampbell\Markdown\View\Compiler\MarkdownCompiler::class, - 'markdown.directive' => \GrahamCampbell\Markdown\View\Directive\MarkdownDirective::class, - 'markdown.environment' => \League\CommonMark\Environment::class, - 'memcached.connector' => \Illuminate\Cache\MemcachedConnector::class, - 'migration.creator' => \Illuminate\Database\Migrations\MigrationCreator::class, - 'migration.repository' => \Illuminate\Database\Migrations\DatabaseMigrationRepository::class, - 'migrator' => \Illuminate\Database\Migrations\Migrator::class, - 'queue' => \Illuminate\Queue\QueueManager::class, - 'queue.connection' => \Illuminate\Queue\SyncQueue::class, - 'queue.failer' => \Illuminate\Queue\Failed\DatabaseUuidFailedJobProvider::class, - 'queue.listener' => \Illuminate\Queue\Listener::class, - 'queue.worker' => \Illuminate\Queue\Worker::class, - 'redirect' => \Illuminate\Routing\Redirector::class, - 'redis' => \Illuminate\Redis\RedisManager::class, - 'router' => \Illuminate\Routing\Router::class, - 'session' => \Illuminate\Session\SessionManager::class, - 'session.store' => \Illuminate\Session\Store::class, - 'translation.loader' => \Illuminate\Translation\FileLoader::class, - 'translator' => \Illuminate\Translation\Translator::class, - 'url' => \Illuminate\Routing\UrlGenerator::class, - 'validation.presence' => \Illuminate\Validation\DatabasePresenceVerifier::class, - 'view' => \Illuminate\View\Factory::class, - 'view.engine.resolver' => \Illuminate\View\Engines\EngineResolver::class, - 'view.finder' => \Illuminate\View\FileViewFinder::class, - ])); - override(\App::get(0), map([ - '' => '@', - 'AccessControl' => \App\ModelFunctions\SessionFunctions::class, - 'App\Actions\Albums\Extensions\PublicIds' => \App\Actions\Albums\Extensions\PublicIds::class, - 'App\Actions\Update\Apply' => \App\Actions\Update\Apply::class, - 'App\Actions\Update\Check' => \App\Actions\Update\Check::class, - 'App\Assets\Helpers' => \App\Assets\Helpers::class, - 'App\Image\ImageHandlerInterface' => \App\Image\ImageHandler::class, - 'App\Metadata\GitHubFunctions' => \App\Metadata\GitHubFunctions::class, - 'App\Metadata\GitRequest' => \App\Metadata\GitRequest::class, - 'App\Metadata\LycheeVersion' => \App\Metadata\LycheeVersion::class, - 'App\ModelFunctions\ConfigFunctions' => \App\ModelFunctions\ConfigFunctions::class, - 'App\ModelFunctions\SessionFunctions' => \App\ModelFunctions\SessionFunctions::class, - 'App\ModelFunctions\SymLinkFunctions' => \App\ModelFunctions\SymLinkFunctions::class, - 'Barryvdh\Debugbar\LaravelDebugbar' => \Barryvdh\Debugbar\LaravelDebugbar::class, - 'Cose\Algorithm\Manager' => \Cose\Algorithm\Manager::class, - 'DarkGhostHunter\Larapass\WebAuthn\PublicKeyCredentialParametersCollection' => \DarkGhostHunter\Larapass\WebAuthn\PublicKeyCredentialParametersCollection::class, - 'DarkGhostHunter\Larapass\WebAuthn\WebAuthnAssertValidator' => \DarkGhostHunter\Larapass\WebAuthn\WebAuthnAssertValidator::class, - 'DarkGhostHunter\Larapass\WebAuthn\WebAuthnAttestCreator' => \DarkGhostHunter\Larapass\WebAuthn\WebAuthnAttestCreator::class, - 'DarkGhostHunter\Larapass\WebAuthn\WebAuthnAttestValidator' => \DarkGhostHunter\Larapass\WebAuthn\WebAuthnAttestValidator::class, - 'Helpers' => \App\Assets\Helpers::class, - 'Illuminate\Auth\Middleware\RequirePassword' => \Illuminate\Auth\Middleware\RequirePassword::class, - 'Illuminate\Broadcasting\BroadcastManager' => \Illuminate\Broadcasting\BroadcastManager::class, - 'Illuminate\Bus\BatchRepository' => \Illuminate\Bus\DatabaseBatchRepository::class, - 'Illuminate\Bus\DatabaseBatchRepository' => \Illuminate\Bus\DatabaseBatchRepository::class, - 'Illuminate\Bus\Dispatcher' => \Illuminate\Bus\Dispatcher::class, - 'Illuminate\Cache\RateLimiter' => \Illuminate\Cache\RateLimiter::class, - 'Illuminate\Console\Scheduling\Schedule' => \Illuminate\Console\Scheduling\Schedule::class, - 'Illuminate\Console\Scheduling\ScheduleFinishCommand' => \Illuminate\Console\Scheduling\ScheduleFinishCommand::class, - 'Illuminate\Console\Scheduling\ScheduleListCommand' => \Illuminate\Console\Scheduling\ScheduleListCommand::class, - 'Illuminate\Console\Scheduling\ScheduleRunCommand' => \Illuminate\Console\Scheduling\ScheduleRunCommand::class, - 'Illuminate\Console\Scheduling\ScheduleTestCommand' => \Illuminate\Console\Scheduling\ScheduleTestCommand::class, - 'Illuminate\Console\Scheduling\ScheduleWorkCommand' => \Illuminate\Console\Scheduling\ScheduleWorkCommand::class, - 'Illuminate\Contracts\Auth\Access\Gate' => \Illuminate\Auth\Access\Gate::class, - 'Illuminate\Contracts\Broadcasting\Broadcaster' => \Illuminate\Broadcasting\Broadcasters\LogBroadcaster::class, - 'Illuminate\Contracts\Console\Kernel' => \App\Console\Kernel::class, - 'Illuminate\Contracts\Debug\ExceptionHandler' => \App\Exceptions\Handler::class, - 'Illuminate\Contracts\Http\Kernel' => \App\Http\Kernel::class, - 'Illuminate\Contracts\Pipeline\Hub' => \Illuminate\Pipeline\Hub::class, - 'Illuminate\Contracts\Queue\EntityResolver' => \Illuminate\Database\Eloquent\QueueEntityResolver::class, - 'Illuminate\Contracts\Routing\ResponseFactory' => \Illuminate\Routing\ResponseFactory::class, - 'Illuminate\Contracts\Validation\UncompromisedVerifier' => \Illuminate\Validation\NotPwnedVerifier::class, - 'Illuminate\Database\Console\DbCommand' => \Illuminate\Database\Console\DbCommand::class, - 'Illuminate\Foundation\Mix' => \Illuminate\Foundation\Mix::class, - 'Illuminate\Foundation\PackageManifest' => \Illuminate\Foundation\PackageManifest::class, - 'Illuminate\Mail\Markdown' => \Illuminate\Mail\Markdown::class, - 'Illuminate\Notifications\ChannelManager' => \Illuminate\Notifications\ChannelManager::class, - 'Illuminate\Routing\Contracts\ControllerDispatcher' => \Illuminate\Routing\ControllerDispatcher::class, - 'Illuminate\Session\Middleware\StartSession' => \Illuminate\Session\Middleware\StartSession::class, - 'Illuminate\Testing\ParallelTesting' => \Illuminate\Testing\ParallelTesting::class, - 'Livewire\LivewireComponentsFinder' => \Livewire\LivewireComponentsFinder::class, - 'Livewire\LivewireManager' => \Livewire\LivewireManager::class, - 'Psr\Http\Message\ResponseInterface' => \Nyholm\Psr7\Response::class, - 'Psr\Http\Message\ServerRequestInterface' => \Nyholm\Psr7\ServerRequest::class, - 'Spatie\ImageOptimizer\OptimizerChain' => \Spatie\ImageOptimizer\OptimizerChain::class, - 'Webauthn\AttestationStatement\AttestationObjectLoader' => \Webauthn\AttestationStatement\AttestationObjectLoader::class, - 'Webauthn\AttestationStatement\AttestationStatementSupportManager' => \Webauthn\AttestationStatement\AttestationStatementSupportManager::class, - 'Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs' => \Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs::class, - 'Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler' => \Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler::class, - 'Webauthn\AuthenticatorAssertionResponseValidator' => \Webauthn\AuthenticatorAssertionResponseValidator::class, - 'Webauthn\AuthenticatorAttestationResponseValidator' => \Webauthn\AuthenticatorAttestationResponseValidator::class, - 'Webauthn\AuthenticatorSelectionCriteria' => \DarkGhostHunter\Larapass\WebAuthn\AuthenticatorSelectionCriteria::class, - 'Webauthn\Counter\CounterChecker' => \Webauthn\Counter\ThrowExceptionIfInvalid::class, - 'Webauthn\PublicKeyCredentialLoader' => \Webauthn\PublicKeyCredentialLoader::class, - 'Webauthn\PublicKeyCredentialRpEntity' => \Webauthn\PublicKeyCredentialRpEntity::class, - 'Webauthn\PublicKeyCredentialSourceRepository' => \DarkGhostHunter\Larapass\Eloquent\WebAuthnCredential::class, - 'Webauthn\TokenBinding\TokenBindingHandler' => \Webauthn\TokenBinding\IgnoreTokenBindingHandler::class, - 'auth' => \Illuminate\Auth\AuthManager::class, - 'auth.driver' => \Illuminate\Auth\SessionGuard::class, - 'blade.compiler' => \Illuminate\View\Compilers\BladeCompiler::class, - 'cache' => \Illuminate\Cache\CacheManager::class, - 'cache.psr6' => \Symfony\Component\Cache\Adapter\Psr16Adapter::class, - 'cache.store' => \Illuminate\Cache\Repository::class, - 'clockwork' => \Clockwork\Clockwork::class, - 'clockwork.authenticator' => \Clockwork\Authentication\NullAuthenticator::class, - 'clockwork.cache' => \Clockwork\DataSource\LaravelCacheDataSource::class, - 'clockwork.eloquent' => \Clockwork\DataSource\EloquentDataSource::class, - 'clockwork.events' => \Clockwork\DataSource\LaravelEventsDataSource::class, - 'clockwork.laravel' => \Clockwork\DataSource\LaravelDataSource::class, - 'clockwork.notifications' => \Clockwork\DataSource\LaravelNotificationsDataSource::class, - 'clockwork.queue' => \Clockwork\DataSource\LaravelQueueDataSource::class, - 'clockwork.redis' => \Clockwork\DataSource\LaravelRedisDataSource::class, - 'clockwork.request' => \Clockwork\Request\Request::class, - 'clockwork.storage' => \Clockwork\Storage\FileStorage::class, - 'clockwork.support' => \Clockwork\Support\Laravel\ClockworkSupport::class, - 'clockwork.swift' => \Clockwork\DataSource\SwiftDataSource::class, - 'clockwork.views' => \Clockwork\DataSource\LaravelViewsDataSource::class, - 'clockwork.xdebug' => \Clockwork\DataSource\XdebugDataSource::class, - 'command.auth.resets.clear' => \Illuminate\Auth\Console\ClearResetsCommand::class, - 'command.cache.clear' => \Illuminate\Cache\Console\ClearCommand::class, - 'command.cache.forget' => \Illuminate\Cache\Console\ForgetCommand::class, - 'command.cache.table' => \Illuminate\Cache\Console\CacheTableCommand::class, - 'command.cast.make' => \Illuminate\Foundation\Console\CastMakeCommand::class, - 'command.channel.make' => \Illuminate\Foundation\Console\ChannelMakeCommand::class, - 'command.clear-compiled' => \Illuminate\Foundation\Console\ClearCompiledCommand::class, - 'command.component.make' => \Illuminate\Foundation\Console\ComponentMakeCommand::class, - 'command.config.cache' => \Illuminate\Foundation\Console\ConfigCacheCommand::class, - 'command.config.clear' => \Illuminate\Foundation\Console\ConfigClearCommand::class, - 'command.console.make' => \Illuminate\Foundation\Console\ConsoleMakeCommand::class, - 'command.controller.make' => \Illuminate\Routing\Console\ControllerMakeCommand::class, - 'command.db.wipe' => \Illuminate\Database\Console\WipeCommand::class, - 'command.debugbar.clear' => \Barryvdh\Debugbar\Console\ClearCommand::class, - 'command.down' => \Illuminate\Foundation\Console\DownCommand::class, - 'command.environment' => \Illuminate\Foundation\Console\EnvironmentCommand::class, - 'command.event.cache' => \Illuminate\Foundation\Console\EventCacheCommand::class, - 'command.event.clear' => \Illuminate\Foundation\Console\EventClearCommand::class, - 'command.event.generate' => \Illuminate\Foundation\Console\EventGenerateCommand::class, - 'command.event.list' => \Illuminate\Foundation\Console\EventListCommand::class, - 'command.event.make' => \Illuminate\Foundation\Console\EventMakeCommand::class, - 'command.exception.make' => \Illuminate\Foundation\Console\ExceptionMakeCommand::class, - 'command.factory.make' => \Illuminate\Database\Console\Factories\FactoryMakeCommand::class, - 'command.ide-helper.eloquent' => \Barryvdh\LaravelIdeHelper\Console\EloquentCommand::class, - 'command.ide-helper.generate' => \Barryvdh\LaravelIdeHelper\Console\GeneratorCommand::class, - 'command.ide-helper.meta' => \Barryvdh\LaravelIdeHelper\Console\MetaCommand::class, - 'command.ide-helper.models' => \Barryvdh\LaravelIdeHelper\Console\ModelsCommand::class, - 'command.job.make' => \Illuminate\Foundation\Console\JobMakeCommand::class, - 'command.key.generate' => \Illuminate\Foundation\Console\KeyGenerateCommand::class, - 'command.listener.make' => \Illuminate\Foundation\Console\ListenerMakeCommand::class, - 'command.mail.make' => \Illuminate\Foundation\Console\MailMakeCommand::class, - 'command.middleware.make' => \Illuminate\Routing\Console\MiddlewareMakeCommand::class, - 'command.migrate' => \Illuminate\Database\Console\Migrations\MigrateCommand::class, - 'command.migrate.fresh' => \Illuminate\Database\Console\Migrations\FreshCommand::class, - 'command.migrate.install' => \Illuminate\Database\Console\Migrations\InstallCommand::class, - 'command.migrate.make' => \Illuminate\Database\Console\Migrations\MigrateMakeCommand::class, - 'command.migrate.refresh' => \Illuminate\Database\Console\Migrations\RefreshCommand::class, - 'command.migrate.reset' => \Illuminate\Database\Console\Migrations\ResetCommand::class, - 'command.migrate.rollback' => \Illuminate\Database\Console\Migrations\RollbackCommand::class, - 'command.migrate.status' => \Illuminate\Database\Console\Migrations\StatusCommand::class, - 'command.model.make' => \Illuminate\Foundation\Console\ModelMakeCommand::class, - 'command.notification.make' => \Illuminate\Foundation\Console\NotificationMakeCommand::class, - 'command.notification.table' => \Illuminate\Notifications\Console\NotificationTableCommand::class, - 'command.observer.make' => \Illuminate\Foundation\Console\ObserverMakeCommand::class, - 'command.optimize' => \Illuminate\Foundation\Console\OptimizeCommand::class, - 'command.optimize.clear' => \Illuminate\Foundation\Console\OptimizeClearCommand::class, - 'command.package.discover' => \Illuminate\Foundation\Console\PackageDiscoverCommand::class, - 'command.policy.make' => \Illuminate\Foundation\Console\PolicyMakeCommand::class, - 'command.provider.make' => \Illuminate\Foundation\Console\ProviderMakeCommand::class, - 'command.queue.batches-table' => \Illuminate\Queue\Console\BatchesTableCommand::class, - 'command.queue.clear' => \Illuminate\Queue\Console\ClearCommand::class, - 'command.queue.failed' => \Illuminate\Queue\Console\ListFailedCommand::class, - 'command.queue.failed-table' => \Illuminate\Queue\Console\FailedTableCommand::class, - 'command.queue.flush' => \Illuminate\Queue\Console\FlushFailedCommand::class, - 'command.queue.forget' => \Illuminate\Queue\Console\ForgetFailedCommand::class, - 'command.queue.listen' => \Illuminate\Queue\Console\ListenCommand::class, - 'command.queue.prune-batches' => \Illuminate\Queue\Console\PruneBatchesCommand::class, - 'command.queue.restart' => \Illuminate\Queue\Console\RestartCommand::class, - 'command.queue.retry' => \Illuminate\Queue\Console\RetryCommand::class, - 'command.queue.retry-batch' => \Illuminate\Queue\Console\RetryBatchCommand::class, - 'command.queue.table' => \Illuminate\Queue\Console\TableCommand::class, - 'command.queue.work' => \Illuminate\Queue\Console\WorkCommand::class, - 'command.request.make' => \Illuminate\Foundation\Console\RequestMakeCommand::class, - 'command.resource.make' => \Illuminate\Foundation\Console\ResourceMakeCommand::class, - 'command.route.cache' => \Illuminate\Foundation\Console\RouteCacheCommand::class, - 'command.route.clear' => \Illuminate\Foundation\Console\RouteClearCommand::class, - 'command.route.list' => \Illuminate\Foundation\Console\RouteListCommand::class, - 'command.rule.make' => \Illuminate\Foundation\Console\RuleMakeCommand::class, - 'command.schema.dump' => \Illuminate\Database\Console\DumpCommand::class, - 'command.seed' => \Illuminate\Database\Console\Seeds\SeedCommand::class, - 'command.seeder.make' => \Illuminate\Database\Console\Seeds\SeederMakeCommand::class, - 'command.serve' => \Illuminate\Foundation\Console\ServeCommand::class, - 'command.session.table' => \Illuminate\Session\Console\SessionTableCommand::class, - 'command.storage.link' => \Illuminate\Foundation\Console\StorageLinkCommand::class, - 'command.stub.publish' => \Illuminate\Foundation\Console\StubPublishCommand::class, - 'command.test.make' => \Illuminate\Foundation\Console\TestMakeCommand::class, - 'command.up' => \Illuminate\Foundation\Console\UpCommand::class, - 'command.vendor.publish' => \Illuminate\Foundation\Console\VendorPublishCommand::class, - 'command.view.cache' => \Illuminate\Foundation\Console\ViewCacheCommand::class, - 'command.view.clear' => \Illuminate\Foundation\Console\ViewClearCommand::class, - 'composer' => \Illuminate\Support\Composer::class, - 'cookie' => \Illuminate\Cookie\CookieJar::class, - 'db' => \Illuminate\Database\DatabaseManager::class, - 'db.connection' => \Illuminate\Database\SQLiteConnection::class, - 'db.factory' => \Illuminate\Database\Connectors\ConnectionFactory::class, - 'db.transactions' => \Illuminate\Database\DatabaseTransactionsManager::class, - 'encrypter' => \Illuminate\Encryption\Encrypter::class, - 'events' => \Illuminate\Events\Dispatcher::class, - 'files' => \Illuminate\Filesystem\Filesystem::class, - 'filesystem' => \Illuminate\Filesystem\FilesystemManager::class, - 'filesystem.disk' => \Illuminate\Filesystem\FilesystemAdapter::class, - 'hash' => \Illuminate\Hashing\HashManager::class, - 'hash.driver' => \Illuminate\Hashing\BcryptHasher::class, - 'image-optimizer' => \Spatie\ImageOptimizer\OptimizerChain::class, - 'log' => \Illuminate\Log\LogManager::class, - 'mail.manager' => \Illuminate\Mail\MailManager::class, - 'mailer' => \Illuminate\Mail\Mailer::class, - 'markdown' => \League\CommonMark\CommonMarkConverter::class, - 'markdown.compiler' => \GrahamCampbell\Markdown\View\Compiler\MarkdownCompiler::class, - 'markdown.directive' => \GrahamCampbell\Markdown\View\Directive\MarkdownDirective::class, - 'markdown.environment' => \League\CommonMark\Environment::class, - 'memcached.connector' => \Illuminate\Cache\MemcachedConnector::class, - 'migration.creator' => \Illuminate\Database\Migrations\MigrationCreator::class, - 'migration.repository' => \Illuminate\Database\Migrations\DatabaseMigrationRepository::class, - 'migrator' => \Illuminate\Database\Migrations\Migrator::class, - 'queue' => \Illuminate\Queue\QueueManager::class, - 'queue.connection' => \Illuminate\Queue\SyncQueue::class, - 'queue.failer' => \Illuminate\Queue\Failed\DatabaseUuidFailedJobProvider::class, - 'queue.listener' => \Illuminate\Queue\Listener::class, - 'queue.worker' => \Illuminate\Queue\Worker::class, - 'redirect' => \Illuminate\Routing\Redirector::class, - 'redis' => \Illuminate\Redis\RedisManager::class, - 'router' => \Illuminate\Routing\Router::class, - 'session' => \Illuminate\Session\SessionManager::class, - 'session.store' => \Illuminate\Session\Store::class, - 'translation.loader' => \Illuminate\Translation\FileLoader::class, - 'translator' => \Illuminate\Translation\Translator::class, - 'url' => \Illuminate\Routing\UrlGenerator::class, - 'validation.presence' => \Illuminate\Validation\DatabasePresenceVerifier::class, - 'view' => \Illuminate\View\Factory::class, - 'view.engine.resolver' => \Illuminate\View\Engines\EngineResolver::class, - 'view.finder' => \Illuminate\View\FileViewFinder::class, - ])); - override(\App::make(0), map([ - '' => '@', - 'AccessControl' => \App\ModelFunctions\SessionFunctions::class, - 'App\Actions\Albums\Extensions\PublicIds' => \App\Actions\Albums\Extensions\PublicIds::class, - 'App\Actions\Update\Apply' => \App\Actions\Update\Apply::class, - 'App\Actions\Update\Check' => \App\Actions\Update\Check::class, - 'App\Assets\Helpers' => \App\Assets\Helpers::class, - 'App\Image\ImageHandlerInterface' => \App\Image\ImageHandler::class, - 'App\Metadata\GitHubFunctions' => \App\Metadata\GitHubFunctions::class, - 'App\Metadata\GitRequest' => \App\Metadata\GitRequest::class, - 'App\Metadata\LycheeVersion' => \App\Metadata\LycheeVersion::class, - 'App\ModelFunctions\ConfigFunctions' => \App\ModelFunctions\ConfigFunctions::class, - 'App\ModelFunctions\SessionFunctions' => \App\ModelFunctions\SessionFunctions::class, - 'App\ModelFunctions\SymLinkFunctions' => \App\ModelFunctions\SymLinkFunctions::class, - 'Barryvdh\Debugbar\LaravelDebugbar' => \Barryvdh\Debugbar\LaravelDebugbar::class, - 'Cose\Algorithm\Manager' => \Cose\Algorithm\Manager::class, - 'DarkGhostHunter\Larapass\WebAuthn\PublicKeyCredentialParametersCollection' => \DarkGhostHunter\Larapass\WebAuthn\PublicKeyCredentialParametersCollection::class, - 'DarkGhostHunter\Larapass\WebAuthn\WebAuthnAssertValidator' => \DarkGhostHunter\Larapass\WebAuthn\WebAuthnAssertValidator::class, - 'DarkGhostHunter\Larapass\WebAuthn\WebAuthnAttestCreator' => \DarkGhostHunter\Larapass\WebAuthn\WebAuthnAttestCreator::class, - 'DarkGhostHunter\Larapass\WebAuthn\WebAuthnAttestValidator' => \DarkGhostHunter\Larapass\WebAuthn\WebAuthnAttestValidator::class, - 'Helpers' => \App\Assets\Helpers::class, - 'Illuminate\Auth\Middleware\RequirePassword' => \Illuminate\Auth\Middleware\RequirePassword::class, - 'Illuminate\Broadcasting\BroadcastManager' => \Illuminate\Broadcasting\BroadcastManager::class, - 'Illuminate\Bus\BatchRepository' => \Illuminate\Bus\DatabaseBatchRepository::class, - 'Illuminate\Bus\DatabaseBatchRepository' => \Illuminate\Bus\DatabaseBatchRepository::class, - 'Illuminate\Bus\Dispatcher' => \Illuminate\Bus\Dispatcher::class, - 'Illuminate\Cache\RateLimiter' => \Illuminate\Cache\RateLimiter::class, - 'Illuminate\Console\Scheduling\Schedule' => \Illuminate\Console\Scheduling\Schedule::class, - 'Illuminate\Console\Scheduling\ScheduleFinishCommand' => \Illuminate\Console\Scheduling\ScheduleFinishCommand::class, - 'Illuminate\Console\Scheduling\ScheduleListCommand' => \Illuminate\Console\Scheduling\ScheduleListCommand::class, - 'Illuminate\Console\Scheduling\ScheduleRunCommand' => \Illuminate\Console\Scheduling\ScheduleRunCommand::class, - 'Illuminate\Console\Scheduling\ScheduleTestCommand' => \Illuminate\Console\Scheduling\ScheduleTestCommand::class, - 'Illuminate\Console\Scheduling\ScheduleWorkCommand' => \Illuminate\Console\Scheduling\ScheduleWorkCommand::class, - 'Illuminate\Contracts\Auth\Access\Gate' => \Illuminate\Auth\Access\Gate::class, - 'Illuminate\Contracts\Broadcasting\Broadcaster' => \Illuminate\Broadcasting\Broadcasters\LogBroadcaster::class, - 'Illuminate\Contracts\Console\Kernel' => \App\Console\Kernel::class, - 'Illuminate\Contracts\Debug\ExceptionHandler' => \App\Exceptions\Handler::class, - 'Illuminate\Contracts\Http\Kernel' => \App\Http\Kernel::class, - 'Illuminate\Contracts\Pipeline\Hub' => \Illuminate\Pipeline\Hub::class, - 'Illuminate\Contracts\Queue\EntityResolver' => \Illuminate\Database\Eloquent\QueueEntityResolver::class, - 'Illuminate\Contracts\Routing\ResponseFactory' => \Illuminate\Routing\ResponseFactory::class, - 'Illuminate\Contracts\Validation\UncompromisedVerifier' => \Illuminate\Validation\NotPwnedVerifier::class, - 'Illuminate\Database\Console\DbCommand' => \Illuminate\Database\Console\DbCommand::class, - 'Illuminate\Foundation\Mix' => \Illuminate\Foundation\Mix::class, - 'Illuminate\Foundation\PackageManifest' => \Illuminate\Foundation\PackageManifest::class, - 'Illuminate\Mail\Markdown' => \Illuminate\Mail\Markdown::class, - 'Illuminate\Notifications\ChannelManager' => \Illuminate\Notifications\ChannelManager::class, - 'Illuminate\Routing\Contracts\ControllerDispatcher' => \Illuminate\Routing\ControllerDispatcher::class, - 'Illuminate\Session\Middleware\StartSession' => \Illuminate\Session\Middleware\StartSession::class, - 'Illuminate\Testing\ParallelTesting' => \Illuminate\Testing\ParallelTesting::class, - 'Livewire\LivewireComponentsFinder' => \Livewire\LivewireComponentsFinder::class, - 'Livewire\LivewireManager' => \Livewire\LivewireManager::class, - 'Psr\Http\Message\ResponseInterface' => \Nyholm\Psr7\Response::class, - 'Psr\Http\Message\ServerRequestInterface' => \Nyholm\Psr7\ServerRequest::class, - 'Spatie\ImageOptimizer\OptimizerChain' => \Spatie\ImageOptimizer\OptimizerChain::class, - 'Webauthn\AttestationStatement\AttestationObjectLoader' => \Webauthn\AttestationStatement\AttestationObjectLoader::class, - 'Webauthn\AttestationStatement\AttestationStatementSupportManager' => \Webauthn\AttestationStatement\AttestationStatementSupportManager::class, - 'Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs' => \Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs::class, - 'Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler' => \Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler::class, - 'Webauthn\AuthenticatorAssertionResponseValidator' => \Webauthn\AuthenticatorAssertionResponseValidator::class, - 'Webauthn\AuthenticatorAttestationResponseValidator' => \Webauthn\AuthenticatorAttestationResponseValidator::class, - 'Webauthn\AuthenticatorSelectionCriteria' => \DarkGhostHunter\Larapass\WebAuthn\AuthenticatorSelectionCriteria::class, - 'Webauthn\Counter\CounterChecker' => \Webauthn\Counter\ThrowExceptionIfInvalid::class, - 'Webauthn\PublicKeyCredentialLoader' => \Webauthn\PublicKeyCredentialLoader::class, - 'Webauthn\PublicKeyCredentialRpEntity' => \Webauthn\PublicKeyCredentialRpEntity::class, - 'Webauthn\PublicKeyCredentialSourceRepository' => \DarkGhostHunter\Larapass\Eloquent\WebAuthnCredential::class, - 'Webauthn\TokenBinding\TokenBindingHandler' => \Webauthn\TokenBinding\IgnoreTokenBindingHandler::class, - 'auth' => \Illuminate\Auth\AuthManager::class, - 'auth.driver' => \Illuminate\Auth\SessionGuard::class, - 'blade.compiler' => \Illuminate\View\Compilers\BladeCompiler::class, - 'cache' => \Illuminate\Cache\CacheManager::class, - 'cache.psr6' => \Symfony\Component\Cache\Adapter\Psr16Adapter::class, - 'cache.store' => \Illuminate\Cache\Repository::class, - 'clockwork' => \Clockwork\Clockwork::class, - 'clockwork.authenticator' => \Clockwork\Authentication\NullAuthenticator::class, - 'clockwork.cache' => \Clockwork\DataSource\LaravelCacheDataSource::class, - 'clockwork.eloquent' => \Clockwork\DataSource\EloquentDataSource::class, - 'clockwork.events' => \Clockwork\DataSource\LaravelEventsDataSource::class, - 'clockwork.laravel' => \Clockwork\DataSource\LaravelDataSource::class, - 'clockwork.notifications' => \Clockwork\DataSource\LaravelNotificationsDataSource::class, - 'clockwork.queue' => \Clockwork\DataSource\LaravelQueueDataSource::class, - 'clockwork.redis' => \Clockwork\DataSource\LaravelRedisDataSource::class, - 'clockwork.request' => \Clockwork\Request\Request::class, - 'clockwork.storage' => \Clockwork\Storage\FileStorage::class, - 'clockwork.support' => \Clockwork\Support\Laravel\ClockworkSupport::class, - 'clockwork.swift' => \Clockwork\DataSource\SwiftDataSource::class, - 'clockwork.views' => \Clockwork\DataSource\LaravelViewsDataSource::class, - 'clockwork.xdebug' => \Clockwork\DataSource\XdebugDataSource::class, - 'command.auth.resets.clear' => \Illuminate\Auth\Console\ClearResetsCommand::class, - 'command.cache.clear' => \Illuminate\Cache\Console\ClearCommand::class, - 'command.cache.forget' => \Illuminate\Cache\Console\ForgetCommand::class, - 'command.cache.table' => \Illuminate\Cache\Console\CacheTableCommand::class, - 'command.cast.make' => \Illuminate\Foundation\Console\CastMakeCommand::class, - 'command.channel.make' => \Illuminate\Foundation\Console\ChannelMakeCommand::class, - 'command.clear-compiled' => \Illuminate\Foundation\Console\ClearCompiledCommand::class, - 'command.component.make' => \Illuminate\Foundation\Console\ComponentMakeCommand::class, - 'command.config.cache' => \Illuminate\Foundation\Console\ConfigCacheCommand::class, - 'command.config.clear' => \Illuminate\Foundation\Console\ConfigClearCommand::class, - 'command.console.make' => \Illuminate\Foundation\Console\ConsoleMakeCommand::class, - 'command.controller.make' => \Illuminate\Routing\Console\ControllerMakeCommand::class, - 'command.db.wipe' => \Illuminate\Database\Console\WipeCommand::class, - 'command.debugbar.clear' => \Barryvdh\Debugbar\Console\ClearCommand::class, - 'command.down' => \Illuminate\Foundation\Console\DownCommand::class, - 'command.environment' => \Illuminate\Foundation\Console\EnvironmentCommand::class, - 'command.event.cache' => \Illuminate\Foundation\Console\EventCacheCommand::class, - 'command.event.clear' => \Illuminate\Foundation\Console\EventClearCommand::class, - 'command.event.generate' => \Illuminate\Foundation\Console\EventGenerateCommand::class, - 'command.event.list' => \Illuminate\Foundation\Console\EventListCommand::class, - 'command.event.make' => \Illuminate\Foundation\Console\EventMakeCommand::class, - 'command.exception.make' => \Illuminate\Foundation\Console\ExceptionMakeCommand::class, - 'command.factory.make' => \Illuminate\Database\Console\Factories\FactoryMakeCommand::class, - 'command.ide-helper.eloquent' => \Barryvdh\LaravelIdeHelper\Console\EloquentCommand::class, - 'command.ide-helper.generate' => \Barryvdh\LaravelIdeHelper\Console\GeneratorCommand::class, - 'command.ide-helper.meta' => \Barryvdh\LaravelIdeHelper\Console\MetaCommand::class, - 'command.ide-helper.models' => \Barryvdh\LaravelIdeHelper\Console\ModelsCommand::class, - 'command.job.make' => \Illuminate\Foundation\Console\JobMakeCommand::class, - 'command.key.generate' => \Illuminate\Foundation\Console\KeyGenerateCommand::class, - 'command.listener.make' => \Illuminate\Foundation\Console\ListenerMakeCommand::class, - 'command.mail.make' => \Illuminate\Foundation\Console\MailMakeCommand::class, - 'command.middleware.make' => \Illuminate\Routing\Console\MiddlewareMakeCommand::class, - 'command.migrate' => \Illuminate\Database\Console\Migrations\MigrateCommand::class, - 'command.migrate.fresh' => \Illuminate\Database\Console\Migrations\FreshCommand::class, - 'command.migrate.install' => \Illuminate\Database\Console\Migrations\InstallCommand::class, - 'command.migrate.make' => \Illuminate\Database\Console\Migrations\MigrateMakeCommand::class, - 'command.migrate.refresh' => \Illuminate\Database\Console\Migrations\RefreshCommand::class, - 'command.migrate.reset' => \Illuminate\Database\Console\Migrations\ResetCommand::class, - 'command.migrate.rollback' => \Illuminate\Database\Console\Migrations\RollbackCommand::class, - 'command.migrate.status' => \Illuminate\Database\Console\Migrations\StatusCommand::class, - 'command.model.make' => \Illuminate\Foundation\Console\ModelMakeCommand::class, - 'command.notification.make' => \Illuminate\Foundation\Console\NotificationMakeCommand::class, - 'command.notification.table' => \Illuminate\Notifications\Console\NotificationTableCommand::class, - 'command.observer.make' => \Illuminate\Foundation\Console\ObserverMakeCommand::class, - 'command.optimize' => \Illuminate\Foundation\Console\OptimizeCommand::class, - 'command.optimize.clear' => \Illuminate\Foundation\Console\OptimizeClearCommand::class, - 'command.package.discover' => \Illuminate\Foundation\Console\PackageDiscoverCommand::class, - 'command.policy.make' => \Illuminate\Foundation\Console\PolicyMakeCommand::class, - 'command.provider.make' => \Illuminate\Foundation\Console\ProviderMakeCommand::class, - 'command.queue.batches-table' => \Illuminate\Queue\Console\BatchesTableCommand::class, - 'command.queue.clear' => \Illuminate\Queue\Console\ClearCommand::class, - 'command.queue.failed' => \Illuminate\Queue\Console\ListFailedCommand::class, - 'command.queue.failed-table' => \Illuminate\Queue\Console\FailedTableCommand::class, - 'command.queue.flush' => \Illuminate\Queue\Console\FlushFailedCommand::class, - 'command.queue.forget' => \Illuminate\Queue\Console\ForgetFailedCommand::class, - 'command.queue.listen' => \Illuminate\Queue\Console\ListenCommand::class, - 'command.queue.prune-batches' => \Illuminate\Queue\Console\PruneBatchesCommand::class, - 'command.queue.restart' => \Illuminate\Queue\Console\RestartCommand::class, - 'command.queue.retry' => \Illuminate\Queue\Console\RetryCommand::class, - 'command.queue.retry-batch' => \Illuminate\Queue\Console\RetryBatchCommand::class, - 'command.queue.table' => \Illuminate\Queue\Console\TableCommand::class, - 'command.queue.work' => \Illuminate\Queue\Console\WorkCommand::class, - 'command.request.make' => \Illuminate\Foundation\Console\RequestMakeCommand::class, - 'command.resource.make' => \Illuminate\Foundation\Console\ResourceMakeCommand::class, - 'command.route.cache' => \Illuminate\Foundation\Console\RouteCacheCommand::class, - 'command.route.clear' => \Illuminate\Foundation\Console\RouteClearCommand::class, - 'command.route.list' => \Illuminate\Foundation\Console\RouteListCommand::class, - 'command.rule.make' => \Illuminate\Foundation\Console\RuleMakeCommand::class, - 'command.schema.dump' => \Illuminate\Database\Console\DumpCommand::class, - 'command.seed' => \Illuminate\Database\Console\Seeds\SeedCommand::class, - 'command.seeder.make' => \Illuminate\Database\Console\Seeds\SeederMakeCommand::class, - 'command.serve' => \Illuminate\Foundation\Console\ServeCommand::class, - 'command.session.table' => \Illuminate\Session\Console\SessionTableCommand::class, - 'command.storage.link' => \Illuminate\Foundation\Console\StorageLinkCommand::class, - 'command.stub.publish' => \Illuminate\Foundation\Console\StubPublishCommand::class, - 'command.test.make' => \Illuminate\Foundation\Console\TestMakeCommand::class, - 'command.up' => \Illuminate\Foundation\Console\UpCommand::class, - 'command.vendor.publish' => \Illuminate\Foundation\Console\VendorPublishCommand::class, - 'command.view.cache' => \Illuminate\Foundation\Console\ViewCacheCommand::class, - 'command.view.clear' => \Illuminate\Foundation\Console\ViewClearCommand::class, - 'composer' => \Illuminate\Support\Composer::class, - 'cookie' => \Illuminate\Cookie\CookieJar::class, - 'db' => \Illuminate\Database\DatabaseManager::class, - 'db.connection' => \Illuminate\Database\SQLiteConnection::class, - 'db.factory' => \Illuminate\Database\Connectors\ConnectionFactory::class, - 'db.transactions' => \Illuminate\Database\DatabaseTransactionsManager::class, - 'encrypter' => \Illuminate\Encryption\Encrypter::class, - 'events' => \Illuminate\Events\Dispatcher::class, - 'files' => \Illuminate\Filesystem\Filesystem::class, - 'filesystem' => \Illuminate\Filesystem\FilesystemManager::class, - 'filesystem.disk' => \Illuminate\Filesystem\FilesystemAdapter::class, - 'hash' => \Illuminate\Hashing\HashManager::class, - 'hash.driver' => \Illuminate\Hashing\BcryptHasher::class, - 'image-optimizer' => \Spatie\ImageOptimizer\OptimizerChain::class, - 'log' => \Illuminate\Log\LogManager::class, - 'mail.manager' => \Illuminate\Mail\MailManager::class, - 'mailer' => \Illuminate\Mail\Mailer::class, - 'markdown' => \League\CommonMark\CommonMarkConverter::class, - 'markdown.compiler' => \GrahamCampbell\Markdown\View\Compiler\MarkdownCompiler::class, - 'markdown.directive' => \GrahamCampbell\Markdown\View\Directive\MarkdownDirective::class, - 'markdown.environment' => \League\CommonMark\Environment::class, - 'memcached.connector' => \Illuminate\Cache\MemcachedConnector::class, - 'migration.creator' => \Illuminate\Database\Migrations\MigrationCreator::class, - 'migration.repository' => \Illuminate\Database\Migrations\DatabaseMigrationRepository::class, - 'migrator' => \Illuminate\Database\Migrations\Migrator::class, - 'queue' => \Illuminate\Queue\QueueManager::class, - 'queue.connection' => \Illuminate\Queue\SyncQueue::class, - 'queue.failer' => \Illuminate\Queue\Failed\DatabaseUuidFailedJobProvider::class, - 'queue.listener' => \Illuminate\Queue\Listener::class, - 'queue.worker' => \Illuminate\Queue\Worker::class, - 'redirect' => \Illuminate\Routing\Redirector::class, - 'redis' => \Illuminate\Redis\RedisManager::class, - 'router' => \Illuminate\Routing\Router::class, - 'session' => \Illuminate\Session\SessionManager::class, - 'session.store' => \Illuminate\Session\Store::class, - 'translation.loader' => \Illuminate\Translation\FileLoader::class, - 'translator' => \Illuminate\Translation\Translator::class, - 'url' => \Illuminate\Routing\UrlGenerator::class, - 'validation.presence' => \Illuminate\Validation\DatabasePresenceVerifier::class, - 'view' => \Illuminate\View\Factory::class, - 'view.engine.resolver' => \Illuminate\View\Engines\EngineResolver::class, - 'view.finder' => \Illuminate\View\FileViewFinder::class, - ])); - override(\App::makeWith(0), map([ - '' => '@', - 'AccessControl' => \App\ModelFunctions\SessionFunctions::class, - 'App\Actions\Albums\Extensions\PublicIds' => \App\Actions\Albums\Extensions\PublicIds::class, - 'App\Actions\Update\Apply' => \App\Actions\Update\Apply::class, - 'App\Actions\Update\Check' => \App\Actions\Update\Check::class, - 'App\Assets\Helpers' => \App\Assets\Helpers::class, - 'App\Image\ImageHandlerInterface' => \App\Image\ImageHandler::class, - 'App\Metadata\GitHubFunctions' => \App\Metadata\GitHubFunctions::class, - 'App\Metadata\GitRequest' => \App\Metadata\GitRequest::class, - 'App\Metadata\LycheeVersion' => \App\Metadata\LycheeVersion::class, - 'App\ModelFunctions\ConfigFunctions' => \App\ModelFunctions\ConfigFunctions::class, - 'App\ModelFunctions\SessionFunctions' => \App\ModelFunctions\SessionFunctions::class, - 'App\ModelFunctions\SymLinkFunctions' => \App\ModelFunctions\SymLinkFunctions::class, - 'Barryvdh\Debugbar\LaravelDebugbar' => \Barryvdh\Debugbar\LaravelDebugbar::class, - 'Cose\Algorithm\Manager' => \Cose\Algorithm\Manager::class, - 'DarkGhostHunter\Larapass\WebAuthn\PublicKeyCredentialParametersCollection' => \DarkGhostHunter\Larapass\WebAuthn\PublicKeyCredentialParametersCollection::class, - 'DarkGhostHunter\Larapass\WebAuthn\WebAuthnAssertValidator' => \DarkGhostHunter\Larapass\WebAuthn\WebAuthnAssertValidator::class, - 'DarkGhostHunter\Larapass\WebAuthn\WebAuthnAttestCreator' => \DarkGhostHunter\Larapass\WebAuthn\WebAuthnAttestCreator::class, - 'DarkGhostHunter\Larapass\WebAuthn\WebAuthnAttestValidator' => \DarkGhostHunter\Larapass\WebAuthn\WebAuthnAttestValidator::class, - 'Helpers' => \App\Assets\Helpers::class, - 'Illuminate\Auth\Middleware\RequirePassword' => \Illuminate\Auth\Middleware\RequirePassword::class, - 'Illuminate\Broadcasting\BroadcastManager' => \Illuminate\Broadcasting\BroadcastManager::class, - 'Illuminate\Bus\BatchRepository' => \Illuminate\Bus\DatabaseBatchRepository::class, - 'Illuminate\Bus\DatabaseBatchRepository' => \Illuminate\Bus\DatabaseBatchRepository::class, - 'Illuminate\Bus\Dispatcher' => \Illuminate\Bus\Dispatcher::class, - 'Illuminate\Cache\RateLimiter' => \Illuminate\Cache\RateLimiter::class, - 'Illuminate\Console\Scheduling\Schedule' => \Illuminate\Console\Scheduling\Schedule::class, - 'Illuminate\Console\Scheduling\ScheduleFinishCommand' => \Illuminate\Console\Scheduling\ScheduleFinishCommand::class, - 'Illuminate\Console\Scheduling\ScheduleListCommand' => \Illuminate\Console\Scheduling\ScheduleListCommand::class, - 'Illuminate\Console\Scheduling\ScheduleRunCommand' => \Illuminate\Console\Scheduling\ScheduleRunCommand::class, - 'Illuminate\Console\Scheduling\ScheduleTestCommand' => \Illuminate\Console\Scheduling\ScheduleTestCommand::class, - 'Illuminate\Console\Scheduling\ScheduleWorkCommand' => \Illuminate\Console\Scheduling\ScheduleWorkCommand::class, - 'Illuminate\Contracts\Auth\Access\Gate' => \Illuminate\Auth\Access\Gate::class, - 'Illuminate\Contracts\Broadcasting\Broadcaster' => \Illuminate\Broadcasting\Broadcasters\LogBroadcaster::class, - 'Illuminate\Contracts\Console\Kernel' => \App\Console\Kernel::class, - 'Illuminate\Contracts\Debug\ExceptionHandler' => \App\Exceptions\Handler::class, - 'Illuminate\Contracts\Http\Kernel' => \App\Http\Kernel::class, - 'Illuminate\Contracts\Pipeline\Hub' => \Illuminate\Pipeline\Hub::class, - 'Illuminate\Contracts\Queue\EntityResolver' => \Illuminate\Database\Eloquent\QueueEntityResolver::class, - 'Illuminate\Contracts\Routing\ResponseFactory' => \Illuminate\Routing\ResponseFactory::class, - 'Illuminate\Contracts\Validation\UncompromisedVerifier' => \Illuminate\Validation\NotPwnedVerifier::class, - 'Illuminate\Database\Console\DbCommand' => \Illuminate\Database\Console\DbCommand::class, - 'Illuminate\Foundation\Mix' => \Illuminate\Foundation\Mix::class, - 'Illuminate\Foundation\PackageManifest' => \Illuminate\Foundation\PackageManifest::class, - 'Illuminate\Mail\Markdown' => \Illuminate\Mail\Markdown::class, - 'Illuminate\Notifications\ChannelManager' => \Illuminate\Notifications\ChannelManager::class, - 'Illuminate\Routing\Contracts\ControllerDispatcher' => \Illuminate\Routing\ControllerDispatcher::class, - 'Illuminate\Session\Middleware\StartSession' => \Illuminate\Session\Middleware\StartSession::class, - 'Illuminate\Testing\ParallelTesting' => \Illuminate\Testing\ParallelTesting::class, - 'Livewire\LivewireComponentsFinder' => \Livewire\LivewireComponentsFinder::class, - 'Livewire\LivewireManager' => \Livewire\LivewireManager::class, - 'Psr\Http\Message\ResponseInterface' => \Nyholm\Psr7\Response::class, - 'Psr\Http\Message\ServerRequestInterface' => \Nyholm\Psr7\ServerRequest::class, - 'Spatie\ImageOptimizer\OptimizerChain' => \Spatie\ImageOptimizer\OptimizerChain::class, - 'Webauthn\AttestationStatement\AttestationObjectLoader' => \Webauthn\AttestationStatement\AttestationObjectLoader::class, - 'Webauthn\AttestationStatement\AttestationStatementSupportManager' => \Webauthn\AttestationStatement\AttestationStatementSupportManager::class, - 'Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs' => \Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs::class, - 'Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler' => \Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler::class, - 'Webauthn\AuthenticatorAssertionResponseValidator' => \Webauthn\AuthenticatorAssertionResponseValidator::class, - 'Webauthn\AuthenticatorAttestationResponseValidator' => \Webauthn\AuthenticatorAttestationResponseValidator::class, - 'Webauthn\AuthenticatorSelectionCriteria' => \DarkGhostHunter\Larapass\WebAuthn\AuthenticatorSelectionCriteria::class, - 'Webauthn\Counter\CounterChecker' => \Webauthn\Counter\ThrowExceptionIfInvalid::class, - 'Webauthn\PublicKeyCredentialLoader' => \Webauthn\PublicKeyCredentialLoader::class, - 'Webauthn\PublicKeyCredentialRpEntity' => \Webauthn\PublicKeyCredentialRpEntity::class, - 'Webauthn\PublicKeyCredentialSourceRepository' => \DarkGhostHunter\Larapass\Eloquent\WebAuthnCredential::class, - 'Webauthn\TokenBinding\TokenBindingHandler' => \Webauthn\TokenBinding\IgnoreTokenBindingHandler::class, - 'auth' => \Illuminate\Auth\AuthManager::class, - 'auth.driver' => \Illuminate\Auth\SessionGuard::class, - 'blade.compiler' => \Illuminate\View\Compilers\BladeCompiler::class, - 'cache' => \Illuminate\Cache\CacheManager::class, - 'cache.psr6' => \Symfony\Component\Cache\Adapter\Psr16Adapter::class, - 'cache.store' => \Illuminate\Cache\Repository::class, - 'clockwork' => \Clockwork\Clockwork::class, - 'clockwork.authenticator' => \Clockwork\Authentication\NullAuthenticator::class, - 'clockwork.cache' => \Clockwork\DataSource\LaravelCacheDataSource::class, - 'clockwork.eloquent' => \Clockwork\DataSource\EloquentDataSource::class, - 'clockwork.events' => \Clockwork\DataSource\LaravelEventsDataSource::class, - 'clockwork.laravel' => \Clockwork\DataSource\LaravelDataSource::class, - 'clockwork.notifications' => \Clockwork\DataSource\LaravelNotificationsDataSource::class, - 'clockwork.queue' => \Clockwork\DataSource\LaravelQueueDataSource::class, - 'clockwork.redis' => \Clockwork\DataSource\LaravelRedisDataSource::class, - 'clockwork.request' => \Clockwork\Request\Request::class, - 'clockwork.storage' => \Clockwork\Storage\FileStorage::class, - 'clockwork.support' => \Clockwork\Support\Laravel\ClockworkSupport::class, - 'clockwork.swift' => \Clockwork\DataSource\SwiftDataSource::class, - 'clockwork.views' => \Clockwork\DataSource\LaravelViewsDataSource::class, - 'clockwork.xdebug' => \Clockwork\DataSource\XdebugDataSource::class, - 'command.auth.resets.clear' => \Illuminate\Auth\Console\ClearResetsCommand::class, - 'command.cache.clear' => \Illuminate\Cache\Console\ClearCommand::class, - 'command.cache.forget' => \Illuminate\Cache\Console\ForgetCommand::class, - 'command.cache.table' => \Illuminate\Cache\Console\CacheTableCommand::class, - 'command.cast.make' => \Illuminate\Foundation\Console\CastMakeCommand::class, - 'command.channel.make' => \Illuminate\Foundation\Console\ChannelMakeCommand::class, - 'command.clear-compiled' => \Illuminate\Foundation\Console\ClearCompiledCommand::class, - 'command.component.make' => \Illuminate\Foundation\Console\ComponentMakeCommand::class, - 'command.config.cache' => \Illuminate\Foundation\Console\ConfigCacheCommand::class, - 'command.config.clear' => \Illuminate\Foundation\Console\ConfigClearCommand::class, - 'command.console.make' => \Illuminate\Foundation\Console\ConsoleMakeCommand::class, - 'command.controller.make' => \Illuminate\Routing\Console\ControllerMakeCommand::class, - 'command.db.wipe' => \Illuminate\Database\Console\WipeCommand::class, - 'command.debugbar.clear' => \Barryvdh\Debugbar\Console\ClearCommand::class, - 'command.down' => \Illuminate\Foundation\Console\DownCommand::class, - 'command.environment' => \Illuminate\Foundation\Console\EnvironmentCommand::class, - 'command.event.cache' => \Illuminate\Foundation\Console\EventCacheCommand::class, - 'command.event.clear' => \Illuminate\Foundation\Console\EventClearCommand::class, - 'command.event.generate' => \Illuminate\Foundation\Console\EventGenerateCommand::class, - 'command.event.list' => \Illuminate\Foundation\Console\EventListCommand::class, - 'command.event.make' => \Illuminate\Foundation\Console\EventMakeCommand::class, - 'command.exception.make' => \Illuminate\Foundation\Console\ExceptionMakeCommand::class, - 'command.factory.make' => \Illuminate\Database\Console\Factories\FactoryMakeCommand::class, - 'command.ide-helper.eloquent' => \Barryvdh\LaravelIdeHelper\Console\EloquentCommand::class, - 'command.ide-helper.generate' => \Barryvdh\LaravelIdeHelper\Console\GeneratorCommand::class, - 'command.ide-helper.meta' => \Barryvdh\LaravelIdeHelper\Console\MetaCommand::class, - 'command.ide-helper.models' => \Barryvdh\LaravelIdeHelper\Console\ModelsCommand::class, - 'command.job.make' => \Illuminate\Foundation\Console\JobMakeCommand::class, - 'command.key.generate' => \Illuminate\Foundation\Console\KeyGenerateCommand::class, - 'command.listener.make' => \Illuminate\Foundation\Console\ListenerMakeCommand::class, - 'command.mail.make' => \Illuminate\Foundation\Console\MailMakeCommand::class, - 'command.middleware.make' => \Illuminate\Routing\Console\MiddlewareMakeCommand::class, - 'command.migrate' => \Illuminate\Database\Console\Migrations\MigrateCommand::class, - 'command.migrate.fresh' => \Illuminate\Database\Console\Migrations\FreshCommand::class, - 'command.migrate.install' => \Illuminate\Database\Console\Migrations\InstallCommand::class, - 'command.migrate.make' => \Illuminate\Database\Console\Migrations\MigrateMakeCommand::class, - 'command.migrate.refresh' => \Illuminate\Database\Console\Migrations\RefreshCommand::class, - 'command.migrate.reset' => \Illuminate\Database\Console\Migrations\ResetCommand::class, - 'command.migrate.rollback' => \Illuminate\Database\Console\Migrations\RollbackCommand::class, - 'command.migrate.status' => \Illuminate\Database\Console\Migrations\StatusCommand::class, - 'command.model.make' => \Illuminate\Foundation\Console\ModelMakeCommand::class, - 'command.notification.make' => \Illuminate\Foundation\Console\NotificationMakeCommand::class, - 'command.notification.table' => \Illuminate\Notifications\Console\NotificationTableCommand::class, - 'command.observer.make' => \Illuminate\Foundation\Console\ObserverMakeCommand::class, - 'command.optimize' => \Illuminate\Foundation\Console\OptimizeCommand::class, - 'command.optimize.clear' => \Illuminate\Foundation\Console\OptimizeClearCommand::class, - 'command.package.discover' => \Illuminate\Foundation\Console\PackageDiscoverCommand::class, - 'command.policy.make' => \Illuminate\Foundation\Console\PolicyMakeCommand::class, - 'command.provider.make' => \Illuminate\Foundation\Console\ProviderMakeCommand::class, - 'command.queue.batches-table' => \Illuminate\Queue\Console\BatchesTableCommand::class, - 'command.queue.clear' => \Illuminate\Queue\Console\ClearCommand::class, - 'command.queue.failed' => \Illuminate\Queue\Console\ListFailedCommand::class, - 'command.queue.failed-table' => \Illuminate\Queue\Console\FailedTableCommand::class, - 'command.queue.flush' => \Illuminate\Queue\Console\FlushFailedCommand::class, - 'command.queue.forget' => \Illuminate\Queue\Console\ForgetFailedCommand::class, - 'command.queue.listen' => \Illuminate\Queue\Console\ListenCommand::class, - 'command.queue.prune-batches' => \Illuminate\Queue\Console\PruneBatchesCommand::class, - 'command.queue.restart' => \Illuminate\Queue\Console\RestartCommand::class, - 'command.queue.retry' => \Illuminate\Queue\Console\RetryCommand::class, - 'command.queue.retry-batch' => \Illuminate\Queue\Console\RetryBatchCommand::class, - 'command.queue.table' => \Illuminate\Queue\Console\TableCommand::class, - 'command.queue.work' => \Illuminate\Queue\Console\WorkCommand::class, - 'command.request.make' => \Illuminate\Foundation\Console\RequestMakeCommand::class, - 'command.resource.make' => \Illuminate\Foundation\Console\ResourceMakeCommand::class, - 'command.route.cache' => \Illuminate\Foundation\Console\RouteCacheCommand::class, - 'command.route.clear' => \Illuminate\Foundation\Console\RouteClearCommand::class, - 'command.route.list' => \Illuminate\Foundation\Console\RouteListCommand::class, - 'command.rule.make' => \Illuminate\Foundation\Console\RuleMakeCommand::class, - 'command.schema.dump' => \Illuminate\Database\Console\DumpCommand::class, - 'command.seed' => \Illuminate\Database\Console\Seeds\SeedCommand::class, - 'command.seeder.make' => \Illuminate\Database\Console\Seeds\SeederMakeCommand::class, - 'command.serve' => \Illuminate\Foundation\Console\ServeCommand::class, - 'command.session.table' => \Illuminate\Session\Console\SessionTableCommand::class, - 'command.storage.link' => \Illuminate\Foundation\Console\StorageLinkCommand::class, - 'command.stub.publish' => \Illuminate\Foundation\Console\StubPublishCommand::class, - 'command.test.make' => \Illuminate\Foundation\Console\TestMakeCommand::class, - 'command.up' => \Illuminate\Foundation\Console\UpCommand::class, - 'command.vendor.publish' => \Illuminate\Foundation\Console\VendorPublishCommand::class, - 'command.view.cache' => \Illuminate\Foundation\Console\ViewCacheCommand::class, - 'command.view.clear' => \Illuminate\Foundation\Console\ViewClearCommand::class, - 'composer' => \Illuminate\Support\Composer::class, - 'cookie' => \Illuminate\Cookie\CookieJar::class, - 'db' => \Illuminate\Database\DatabaseManager::class, - 'db.connection' => \Illuminate\Database\SQLiteConnection::class, - 'db.factory' => \Illuminate\Database\Connectors\ConnectionFactory::class, - 'db.transactions' => \Illuminate\Database\DatabaseTransactionsManager::class, - 'encrypter' => \Illuminate\Encryption\Encrypter::class, - 'events' => \Illuminate\Events\Dispatcher::class, - 'files' => \Illuminate\Filesystem\Filesystem::class, - 'filesystem' => \Illuminate\Filesystem\FilesystemManager::class, - 'filesystem.disk' => \Illuminate\Filesystem\FilesystemAdapter::class, - 'hash' => \Illuminate\Hashing\HashManager::class, - 'hash.driver' => \Illuminate\Hashing\BcryptHasher::class, - 'image-optimizer' => \Spatie\ImageOptimizer\OptimizerChain::class, - 'log' => \Illuminate\Log\LogManager::class, - 'mail.manager' => \Illuminate\Mail\MailManager::class, - 'mailer' => \Illuminate\Mail\Mailer::class, - 'markdown' => \League\CommonMark\CommonMarkConverter::class, - 'markdown.compiler' => \GrahamCampbell\Markdown\View\Compiler\MarkdownCompiler::class, - 'markdown.directive' => \GrahamCampbell\Markdown\View\Directive\MarkdownDirective::class, - 'markdown.environment' => \League\CommonMark\Environment::class, - 'memcached.connector' => \Illuminate\Cache\MemcachedConnector::class, - 'migration.creator' => \Illuminate\Database\Migrations\MigrationCreator::class, - 'migration.repository' => \Illuminate\Database\Migrations\DatabaseMigrationRepository::class, - 'migrator' => \Illuminate\Database\Migrations\Migrator::class, - 'queue' => \Illuminate\Queue\QueueManager::class, - 'queue.connection' => \Illuminate\Queue\SyncQueue::class, - 'queue.failer' => \Illuminate\Queue\Failed\DatabaseUuidFailedJobProvider::class, - 'queue.listener' => \Illuminate\Queue\Listener::class, - 'queue.worker' => \Illuminate\Queue\Worker::class, - 'redirect' => \Illuminate\Routing\Redirector::class, - 'redis' => \Illuminate\Redis\RedisManager::class, - 'router' => \Illuminate\Routing\Router::class, - 'session' => \Illuminate\Session\SessionManager::class, - 'session.store' => \Illuminate\Session\Store::class, - 'translation.loader' => \Illuminate\Translation\FileLoader::class, - 'translator' => \Illuminate\Translation\Translator::class, - 'url' => \Illuminate\Routing\UrlGenerator::class, - 'validation.presence' => \Illuminate\Validation\DatabasePresenceVerifier::class, - 'view' => \Illuminate\View\Factory::class, - 'view.engine.resolver' => \Illuminate\View\Engines\EngineResolver::class, - 'view.finder' => \Illuminate\View\FileViewFinder::class, - ])); - override(\app(0), map([ - '' => '@', - 'AccessControl' => \App\ModelFunctions\SessionFunctions::class, - 'App\Actions\Albums\Extensions\PublicIds' => \App\Actions\Albums\Extensions\PublicIds::class, - 'App\Actions\Update\Apply' => \App\Actions\Update\Apply::class, - 'App\Actions\Update\Check' => \App\Actions\Update\Check::class, - 'App\Assets\Helpers' => \App\Assets\Helpers::class, - 'App\Image\ImageHandlerInterface' => \App\Image\ImageHandler::class, - 'App\Metadata\GitHubFunctions' => \App\Metadata\GitHubFunctions::class, - 'App\Metadata\GitRequest' => \App\Metadata\GitRequest::class, - 'App\Metadata\LycheeVersion' => \App\Metadata\LycheeVersion::class, - 'App\ModelFunctions\ConfigFunctions' => \App\ModelFunctions\ConfigFunctions::class, - 'App\ModelFunctions\SessionFunctions' => \App\ModelFunctions\SessionFunctions::class, - 'App\ModelFunctions\SymLinkFunctions' => \App\ModelFunctions\SymLinkFunctions::class, - 'Barryvdh\Debugbar\LaravelDebugbar' => \Barryvdh\Debugbar\LaravelDebugbar::class, - 'Cose\Algorithm\Manager' => \Cose\Algorithm\Manager::class, - 'DarkGhostHunter\Larapass\WebAuthn\PublicKeyCredentialParametersCollection' => \DarkGhostHunter\Larapass\WebAuthn\PublicKeyCredentialParametersCollection::class, - 'DarkGhostHunter\Larapass\WebAuthn\WebAuthnAssertValidator' => \DarkGhostHunter\Larapass\WebAuthn\WebAuthnAssertValidator::class, - 'DarkGhostHunter\Larapass\WebAuthn\WebAuthnAttestCreator' => \DarkGhostHunter\Larapass\WebAuthn\WebAuthnAttestCreator::class, - 'DarkGhostHunter\Larapass\WebAuthn\WebAuthnAttestValidator' => \DarkGhostHunter\Larapass\WebAuthn\WebAuthnAttestValidator::class, - 'Helpers' => \App\Assets\Helpers::class, - 'Illuminate\Auth\Middleware\RequirePassword' => \Illuminate\Auth\Middleware\RequirePassword::class, - 'Illuminate\Broadcasting\BroadcastManager' => \Illuminate\Broadcasting\BroadcastManager::class, - 'Illuminate\Bus\BatchRepository' => \Illuminate\Bus\DatabaseBatchRepository::class, - 'Illuminate\Bus\DatabaseBatchRepository' => \Illuminate\Bus\DatabaseBatchRepository::class, - 'Illuminate\Bus\Dispatcher' => \Illuminate\Bus\Dispatcher::class, - 'Illuminate\Cache\RateLimiter' => \Illuminate\Cache\RateLimiter::class, - 'Illuminate\Console\Scheduling\Schedule' => \Illuminate\Console\Scheduling\Schedule::class, - 'Illuminate\Console\Scheduling\ScheduleFinishCommand' => \Illuminate\Console\Scheduling\ScheduleFinishCommand::class, - 'Illuminate\Console\Scheduling\ScheduleListCommand' => \Illuminate\Console\Scheduling\ScheduleListCommand::class, - 'Illuminate\Console\Scheduling\ScheduleRunCommand' => \Illuminate\Console\Scheduling\ScheduleRunCommand::class, - 'Illuminate\Console\Scheduling\ScheduleTestCommand' => \Illuminate\Console\Scheduling\ScheduleTestCommand::class, - 'Illuminate\Console\Scheduling\ScheduleWorkCommand' => \Illuminate\Console\Scheduling\ScheduleWorkCommand::class, - 'Illuminate\Contracts\Auth\Access\Gate' => \Illuminate\Auth\Access\Gate::class, - 'Illuminate\Contracts\Broadcasting\Broadcaster' => \Illuminate\Broadcasting\Broadcasters\LogBroadcaster::class, - 'Illuminate\Contracts\Console\Kernel' => \App\Console\Kernel::class, - 'Illuminate\Contracts\Debug\ExceptionHandler' => \App\Exceptions\Handler::class, - 'Illuminate\Contracts\Http\Kernel' => \App\Http\Kernel::class, - 'Illuminate\Contracts\Pipeline\Hub' => \Illuminate\Pipeline\Hub::class, - 'Illuminate\Contracts\Queue\EntityResolver' => \Illuminate\Database\Eloquent\QueueEntityResolver::class, - 'Illuminate\Contracts\Routing\ResponseFactory' => \Illuminate\Routing\ResponseFactory::class, - 'Illuminate\Contracts\Validation\UncompromisedVerifier' => \Illuminate\Validation\NotPwnedVerifier::class, - 'Illuminate\Database\Console\DbCommand' => \Illuminate\Database\Console\DbCommand::class, - 'Illuminate\Foundation\Mix' => \Illuminate\Foundation\Mix::class, - 'Illuminate\Foundation\PackageManifest' => \Illuminate\Foundation\PackageManifest::class, - 'Illuminate\Mail\Markdown' => \Illuminate\Mail\Markdown::class, - 'Illuminate\Notifications\ChannelManager' => \Illuminate\Notifications\ChannelManager::class, - 'Illuminate\Routing\Contracts\ControllerDispatcher' => \Illuminate\Routing\ControllerDispatcher::class, - 'Illuminate\Session\Middleware\StartSession' => \Illuminate\Session\Middleware\StartSession::class, - 'Illuminate\Testing\ParallelTesting' => \Illuminate\Testing\ParallelTesting::class, - 'Livewire\LivewireComponentsFinder' => \Livewire\LivewireComponentsFinder::class, - 'Livewire\LivewireManager' => \Livewire\LivewireManager::class, - 'Psr\Http\Message\ResponseInterface' => \Nyholm\Psr7\Response::class, - 'Psr\Http\Message\ServerRequestInterface' => \Nyholm\Psr7\ServerRequest::class, - 'Spatie\ImageOptimizer\OptimizerChain' => \Spatie\ImageOptimizer\OptimizerChain::class, - 'Webauthn\AttestationStatement\AttestationObjectLoader' => \Webauthn\AttestationStatement\AttestationObjectLoader::class, - 'Webauthn\AttestationStatement\AttestationStatementSupportManager' => \Webauthn\AttestationStatement\AttestationStatementSupportManager::class, - 'Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs' => \Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs::class, - 'Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler' => \Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler::class, - 'Webauthn\AuthenticatorAssertionResponseValidator' => \Webauthn\AuthenticatorAssertionResponseValidator::class, - 'Webauthn\AuthenticatorAttestationResponseValidator' => \Webauthn\AuthenticatorAttestationResponseValidator::class, - 'Webauthn\AuthenticatorSelectionCriteria' => \DarkGhostHunter\Larapass\WebAuthn\AuthenticatorSelectionCriteria::class, - 'Webauthn\Counter\CounterChecker' => \Webauthn\Counter\ThrowExceptionIfInvalid::class, - 'Webauthn\PublicKeyCredentialLoader' => \Webauthn\PublicKeyCredentialLoader::class, - 'Webauthn\PublicKeyCredentialRpEntity' => \Webauthn\PublicKeyCredentialRpEntity::class, - 'Webauthn\PublicKeyCredentialSourceRepository' => \DarkGhostHunter\Larapass\Eloquent\WebAuthnCredential::class, - 'Webauthn\TokenBinding\TokenBindingHandler' => \Webauthn\TokenBinding\IgnoreTokenBindingHandler::class, - 'auth' => \Illuminate\Auth\AuthManager::class, - 'auth.driver' => \Illuminate\Auth\SessionGuard::class, - 'blade.compiler' => \Illuminate\View\Compilers\BladeCompiler::class, - 'cache' => \Illuminate\Cache\CacheManager::class, - 'cache.psr6' => \Symfony\Component\Cache\Adapter\Psr16Adapter::class, - 'cache.store' => \Illuminate\Cache\Repository::class, - 'clockwork' => \Clockwork\Clockwork::class, - 'clockwork.authenticator' => \Clockwork\Authentication\NullAuthenticator::class, - 'clockwork.cache' => \Clockwork\DataSource\LaravelCacheDataSource::class, - 'clockwork.eloquent' => \Clockwork\DataSource\EloquentDataSource::class, - 'clockwork.events' => \Clockwork\DataSource\LaravelEventsDataSource::class, - 'clockwork.laravel' => \Clockwork\DataSource\LaravelDataSource::class, - 'clockwork.notifications' => \Clockwork\DataSource\LaravelNotificationsDataSource::class, - 'clockwork.queue' => \Clockwork\DataSource\LaravelQueueDataSource::class, - 'clockwork.redis' => \Clockwork\DataSource\LaravelRedisDataSource::class, - 'clockwork.request' => \Clockwork\Request\Request::class, - 'clockwork.storage' => \Clockwork\Storage\FileStorage::class, - 'clockwork.support' => \Clockwork\Support\Laravel\ClockworkSupport::class, - 'clockwork.swift' => \Clockwork\DataSource\SwiftDataSource::class, - 'clockwork.views' => \Clockwork\DataSource\LaravelViewsDataSource::class, - 'clockwork.xdebug' => \Clockwork\DataSource\XdebugDataSource::class, - 'command.auth.resets.clear' => \Illuminate\Auth\Console\ClearResetsCommand::class, - 'command.cache.clear' => \Illuminate\Cache\Console\ClearCommand::class, - 'command.cache.forget' => \Illuminate\Cache\Console\ForgetCommand::class, - 'command.cache.table' => \Illuminate\Cache\Console\CacheTableCommand::class, - 'command.cast.make' => \Illuminate\Foundation\Console\CastMakeCommand::class, - 'command.channel.make' => \Illuminate\Foundation\Console\ChannelMakeCommand::class, - 'command.clear-compiled' => \Illuminate\Foundation\Console\ClearCompiledCommand::class, - 'command.component.make' => \Illuminate\Foundation\Console\ComponentMakeCommand::class, - 'command.config.cache' => \Illuminate\Foundation\Console\ConfigCacheCommand::class, - 'command.config.clear' => \Illuminate\Foundation\Console\ConfigClearCommand::class, - 'command.console.make' => \Illuminate\Foundation\Console\ConsoleMakeCommand::class, - 'command.controller.make' => \Illuminate\Routing\Console\ControllerMakeCommand::class, - 'command.db.wipe' => \Illuminate\Database\Console\WipeCommand::class, - 'command.debugbar.clear' => \Barryvdh\Debugbar\Console\ClearCommand::class, - 'command.down' => \Illuminate\Foundation\Console\DownCommand::class, - 'command.environment' => \Illuminate\Foundation\Console\EnvironmentCommand::class, - 'command.event.cache' => \Illuminate\Foundation\Console\EventCacheCommand::class, - 'command.event.clear' => \Illuminate\Foundation\Console\EventClearCommand::class, - 'command.event.generate' => \Illuminate\Foundation\Console\EventGenerateCommand::class, - 'command.event.list' => \Illuminate\Foundation\Console\EventListCommand::class, - 'command.event.make' => \Illuminate\Foundation\Console\EventMakeCommand::class, - 'command.exception.make' => \Illuminate\Foundation\Console\ExceptionMakeCommand::class, - 'command.factory.make' => \Illuminate\Database\Console\Factories\FactoryMakeCommand::class, - 'command.ide-helper.eloquent' => \Barryvdh\LaravelIdeHelper\Console\EloquentCommand::class, - 'command.ide-helper.generate' => \Barryvdh\LaravelIdeHelper\Console\GeneratorCommand::class, - 'command.ide-helper.meta' => \Barryvdh\LaravelIdeHelper\Console\MetaCommand::class, - 'command.ide-helper.models' => \Barryvdh\LaravelIdeHelper\Console\ModelsCommand::class, - 'command.job.make' => \Illuminate\Foundation\Console\JobMakeCommand::class, - 'command.key.generate' => \Illuminate\Foundation\Console\KeyGenerateCommand::class, - 'command.listener.make' => \Illuminate\Foundation\Console\ListenerMakeCommand::class, - 'command.mail.make' => \Illuminate\Foundation\Console\MailMakeCommand::class, - 'command.middleware.make' => \Illuminate\Routing\Console\MiddlewareMakeCommand::class, - 'command.migrate' => \Illuminate\Database\Console\Migrations\MigrateCommand::class, - 'command.migrate.fresh' => \Illuminate\Database\Console\Migrations\FreshCommand::class, - 'command.migrate.install' => \Illuminate\Database\Console\Migrations\InstallCommand::class, - 'command.migrate.make' => \Illuminate\Database\Console\Migrations\MigrateMakeCommand::class, - 'command.migrate.refresh' => \Illuminate\Database\Console\Migrations\RefreshCommand::class, - 'command.migrate.reset' => \Illuminate\Database\Console\Migrations\ResetCommand::class, - 'command.migrate.rollback' => \Illuminate\Database\Console\Migrations\RollbackCommand::class, - 'command.migrate.status' => \Illuminate\Database\Console\Migrations\StatusCommand::class, - 'command.model.make' => \Illuminate\Foundation\Console\ModelMakeCommand::class, - 'command.notification.make' => \Illuminate\Foundation\Console\NotificationMakeCommand::class, - 'command.notification.table' => \Illuminate\Notifications\Console\NotificationTableCommand::class, - 'command.observer.make' => \Illuminate\Foundation\Console\ObserverMakeCommand::class, - 'command.optimize' => \Illuminate\Foundation\Console\OptimizeCommand::class, - 'command.optimize.clear' => \Illuminate\Foundation\Console\OptimizeClearCommand::class, - 'command.package.discover' => \Illuminate\Foundation\Console\PackageDiscoverCommand::class, - 'command.policy.make' => \Illuminate\Foundation\Console\PolicyMakeCommand::class, - 'command.provider.make' => \Illuminate\Foundation\Console\ProviderMakeCommand::class, - 'command.queue.batches-table' => \Illuminate\Queue\Console\BatchesTableCommand::class, - 'command.queue.clear' => \Illuminate\Queue\Console\ClearCommand::class, - 'command.queue.failed' => \Illuminate\Queue\Console\ListFailedCommand::class, - 'command.queue.failed-table' => \Illuminate\Queue\Console\FailedTableCommand::class, - 'command.queue.flush' => \Illuminate\Queue\Console\FlushFailedCommand::class, - 'command.queue.forget' => \Illuminate\Queue\Console\ForgetFailedCommand::class, - 'command.queue.listen' => \Illuminate\Queue\Console\ListenCommand::class, - 'command.queue.prune-batches' => \Illuminate\Queue\Console\PruneBatchesCommand::class, - 'command.queue.restart' => \Illuminate\Queue\Console\RestartCommand::class, - 'command.queue.retry' => \Illuminate\Queue\Console\RetryCommand::class, - 'command.queue.retry-batch' => \Illuminate\Queue\Console\RetryBatchCommand::class, - 'command.queue.table' => \Illuminate\Queue\Console\TableCommand::class, - 'command.queue.work' => \Illuminate\Queue\Console\WorkCommand::class, - 'command.request.make' => \Illuminate\Foundation\Console\RequestMakeCommand::class, - 'command.resource.make' => \Illuminate\Foundation\Console\ResourceMakeCommand::class, - 'command.route.cache' => \Illuminate\Foundation\Console\RouteCacheCommand::class, - 'command.route.clear' => \Illuminate\Foundation\Console\RouteClearCommand::class, - 'command.route.list' => \Illuminate\Foundation\Console\RouteListCommand::class, - 'command.rule.make' => \Illuminate\Foundation\Console\RuleMakeCommand::class, - 'command.schema.dump' => \Illuminate\Database\Console\DumpCommand::class, - 'command.seed' => \Illuminate\Database\Console\Seeds\SeedCommand::class, - 'command.seeder.make' => \Illuminate\Database\Console\Seeds\SeederMakeCommand::class, - 'command.serve' => \Illuminate\Foundation\Console\ServeCommand::class, - 'command.session.table' => \Illuminate\Session\Console\SessionTableCommand::class, - 'command.storage.link' => \Illuminate\Foundation\Console\StorageLinkCommand::class, - 'command.stub.publish' => \Illuminate\Foundation\Console\StubPublishCommand::class, - 'command.test.make' => \Illuminate\Foundation\Console\TestMakeCommand::class, - 'command.up' => \Illuminate\Foundation\Console\UpCommand::class, - 'command.vendor.publish' => \Illuminate\Foundation\Console\VendorPublishCommand::class, - 'command.view.cache' => \Illuminate\Foundation\Console\ViewCacheCommand::class, - 'command.view.clear' => \Illuminate\Foundation\Console\ViewClearCommand::class, - 'composer' => \Illuminate\Support\Composer::class, - 'cookie' => \Illuminate\Cookie\CookieJar::class, - 'db' => \Illuminate\Database\DatabaseManager::class, - 'db.connection' => \Illuminate\Database\SQLiteConnection::class, - 'db.factory' => \Illuminate\Database\Connectors\ConnectionFactory::class, - 'db.transactions' => \Illuminate\Database\DatabaseTransactionsManager::class, - 'encrypter' => \Illuminate\Encryption\Encrypter::class, - 'events' => \Illuminate\Events\Dispatcher::class, - 'files' => \Illuminate\Filesystem\Filesystem::class, - 'filesystem' => \Illuminate\Filesystem\FilesystemManager::class, - 'filesystem.disk' => \Illuminate\Filesystem\FilesystemAdapter::class, - 'hash' => \Illuminate\Hashing\HashManager::class, - 'hash.driver' => \Illuminate\Hashing\BcryptHasher::class, - 'image-optimizer' => \Spatie\ImageOptimizer\OptimizerChain::class, - 'log' => \Illuminate\Log\LogManager::class, - 'mail.manager' => \Illuminate\Mail\MailManager::class, - 'mailer' => \Illuminate\Mail\Mailer::class, - 'markdown' => \League\CommonMark\CommonMarkConverter::class, - 'markdown.compiler' => \GrahamCampbell\Markdown\View\Compiler\MarkdownCompiler::class, - 'markdown.directive' => \GrahamCampbell\Markdown\View\Directive\MarkdownDirective::class, - 'markdown.environment' => \League\CommonMark\Environment::class, - 'memcached.connector' => \Illuminate\Cache\MemcachedConnector::class, - 'migration.creator' => \Illuminate\Database\Migrations\MigrationCreator::class, - 'migration.repository' => \Illuminate\Database\Migrations\DatabaseMigrationRepository::class, - 'migrator' => \Illuminate\Database\Migrations\Migrator::class, - 'queue' => \Illuminate\Queue\QueueManager::class, - 'queue.connection' => \Illuminate\Queue\SyncQueue::class, - 'queue.failer' => \Illuminate\Queue\Failed\DatabaseUuidFailedJobProvider::class, - 'queue.listener' => \Illuminate\Queue\Listener::class, - 'queue.worker' => \Illuminate\Queue\Worker::class, - 'redirect' => \Illuminate\Routing\Redirector::class, - 'redis' => \Illuminate\Redis\RedisManager::class, - 'router' => \Illuminate\Routing\Router::class, - 'session' => \Illuminate\Session\SessionManager::class, - 'session.store' => \Illuminate\Session\Store::class, - 'translation.loader' => \Illuminate\Translation\FileLoader::class, - 'translator' => \Illuminate\Translation\Translator::class, - 'url' => \Illuminate\Routing\UrlGenerator::class, - 'validation.presence' => \Illuminate\Validation\DatabasePresenceVerifier::class, - 'view' => \Illuminate\View\Factory::class, - 'view.engine.resolver' => \Illuminate\View\Engines\EngineResolver::class, - 'view.finder' => \Illuminate\View\FileViewFinder::class, - ])); - override(\resolve(0), map([ - '' => '@', - 'AccessControl' => \App\ModelFunctions\SessionFunctions::class, - 'App\Actions\Albums\Extensions\PublicIds' => \App\Actions\Albums\Extensions\PublicIds::class, - 'App\Actions\Update\Apply' => \App\Actions\Update\Apply::class, - 'App\Actions\Update\Check' => \App\Actions\Update\Check::class, - 'App\Assets\Helpers' => \App\Assets\Helpers::class, - 'App\Image\ImageHandlerInterface' => \App\Image\ImageHandler::class, - 'App\Metadata\GitHubFunctions' => \App\Metadata\GitHubFunctions::class, - 'App\Metadata\GitRequest' => \App\Metadata\GitRequest::class, - 'App\Metadata\LycheeVersion' => \App\Metadata\LycheeVersion::class, - 'App\ModelFunctions\ConfigFunctions' => \App\ModelFunctions\ConfigFunctions::class, - 'App\ModelFunctions\SessionFunctions' => \App\ModelFunctions\SessionFunctions::class, - 'App\ModelFunctions\SymLinkFunctions' => \App\ModelFunctions\SymLinkFunctions::class, - 'Barryvdh\Debugbar\LaravelDebugbar' => \Barryvdh\Debugbar\LaravelDebugbar::class, - 'Cose\Algorithm\Manager' => \Cose\Algorithm\Manager::class, - 'DarkGhostHunter\Larapass\WebAuthn\PublicKeyCredentialParametersCollection' => \DarkGhostHunter\Larapass\WebAuthn\PublicKeyCredentialParametersCollection::class, - 'DarkGhostHunter\Larapass\WebAuthn\WebAuthnAssertValidator' => \DarkGhostHunter\Larapass\WebAuthn\WebAuthnAssertValidator::class, - 'DarkGhostHunter\Larapass\WebAuthn\WebAuthnAttestCreator' => \DarkGhostHunter\Larapass\WebAuthn\WebAuthnAttestCreator::class, - 'DarkGhostHunter\Larapass\WebAuthn\WebAuthnAttestValidator' => \DarkGhostHunter\Larapass\WebAuthn\WebAuthnAttestValidator::class, - 'Helpers' => \App\Assets\Helpers::class, - 'Illuminate\Auth\Middleware\RequirePassword' => \Illuminate\Auth\Middleware\RequirePassword::class, - 'Illuminate\Broadcasting\BroadcastManager' => \Illuminate\Broadcasting\BroadcastManager::class, - 'Illuminate\Bus\BatchRepository' => \Illuminate\Bus\DatabaseBatchRepository::class, - 'Illuminate\Bus\DatabaseBatchRepository' => \Illuminate\Bus\DatabaseBatchRepository::class, - 'Illuminate\Bus\Dispatcher' => \Illuminate\Bus\Dispatcher::class, - 'Illuminate\Cache\RateLimiter' => \Illuminate\Cache\RateLimiter::class, - 'Illuminate\Console\Scheduling\Schedule' => \Illuminate\Console\Scheduling\Schedule::class, - 'Illuminate\Console\Scheduling\ScheduleFinishCommand' => \Illuminate\Console\Scheduling\ScheduleFinishCommand::class, - 'Illuminate\Console\Scheduling\ScheduleListCommand' => \Illuminate\Console\Scheduling\ScheduleListCommand::class, - 'Illuminate\Console\Scheduling\ScheduleRunCommand' => \Illuminate\Console\Scheduling\ScheduleRunCommand::class, - 'Illuminate\Console\Scheduling\ScheduleTestCommand' => \Illuminate\Console\Scheduling\ScheduleTestCommand::class, - 'Illuminate\Console\Scheduling\ScheduleWorkCommand' => \Illuminate\Console\Scheduling\ScheduleWorkCommand::class, - 'Illuminate\Contracts\Auth\Access\Gate' => \Illuminate\Auth\Access\Gate::class, - 'Illuminate\Contracts\Broadcasting\Broadcaster' => \Illuminate\Broadcasting\Broadcasters\LogBroadcaster::class, - 'Illuminate\Contracts\Console\Kernel' => \App\Console\Kernel::class, - 'Illuminate\Contracts\Debug\ExceptionHandler' => \App\Exceptions\Handler::class, - 'Illuminate\Contracts\Http\Kernel' => \App\Http\Kernel::class, - 'Illuminate\Contracts\Pipeline\Hub' => \Illuminate\Pipeline\Hub::class, - 'Illuminate\Contracts\Queue\EntityResolver' => \Illuminate\Database\Eloquent\QueueEntityResolver::class, - 'Illuminate\Contracts\Routing\ResponseFactory' => \Illuminate\Routing\ResponseFactory::class, - 'Illuminate\Contracts\Validation\UncompromisedVerifier' => \Illuminate\Validation\NotPwnedVerifier::class, - 'Illuminate\Database\Console\DbCommand' => \Illuminate\Database\Console\DbCommand::class, - 'Illuminate\Foundation\Mix' => \Illuminate\Foundation\Mix::class, - 'Illuminate\Foundation\PackageManifest' => \Illuminate\Foundation\PackageManifest::class, - 'Illuminate\Mail\Markdown' => \Illuminate\Mail\Markdown::class, - 'Illuminate\Notifications\ChannelManager' => \Illuminate\Notifications\ChannelManager::class, - 'Illuminate\Routing\Contracts\ControllerDispatcher' => \Illuminate\Routing\ControllerDispatcher::class, - 'Illuminate\Session\Middleware\StartSession' => \Illuminate\Session\Middleware\StartSession::class, - 'Illuminate\Testing\ParallelTesting' => \Illuminate\Testing\ParallelTesting::class, - 'Livewire\LivewireComponentsFinder' => \Livewire\LivewireComponentsFinder::class, - 'Livewire\LivewireManager' => \Livewire\LivewireManager::class, - 'Psr\Http\Message\ResponseInterface' => \Nyholm\Psr7\Response::class, - 'Psr\Http\Message\ServerRequestInterface' => \Nyholm\Psr7\ServerRequest::class, - 'Spatie\ImageOptimizer\OptimizerChain' => \Spatie\ImageOptimizer\OptimizerChain::class, - 'Webauthn\AttestationStatement\AttestationObjectLoader' => \Webauthn\AttestationStatement\AttestationObjectLoader::class, - 'Webauthn\AttestationStatement\AttestationStatementSupportManager' => \Webauthn\AttestationStatement\AttestationStatementSupportManager::class, - 'Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs' => \Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs::class, - 'Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler' => \Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler::class, - 'Webauthn\AuthenticatorAssertionResponseValidator' => \Webauthn\AuthenticatorAssertionResponseValidator::class, - 'Webauthn\AuthenticatorAttestationResponseValidator' => \Webauthn\AuthenticatorAttestationResponseValidator::class, - 'Webauthn\AuthenticatorSelectionCriteria' => \DarkGhostHunter\Larapass\WebAuthn\AuthenticatorSelectionCriteria::class, - 'Webauthn\Counter\CounterChecker' => \Webauthn\Counter\ThrowExceptionIfInvalid::class, - 'Webauthn\PublicKeyCredentialLoader' => \Webauthn\PublicKeyCredentialLoader::class, - 'Webauthn\PublicKeyCredentialRpEntity' => \Webauthn\PublicKeyCredentialRpEntity::class, - 'Webauthn\PublicKeyCredentialSourceRepository' => \DarkGhostHunter\Larapass\Eloquent\WebAuthnCredential::class, - 'Webauthn\TokenBinding\TokenBindingHandler' => \Webauthn\TokenBinding\IgnoreTokenBindingHandler::class, - 'auth' => \Illuminate\Auth\AuthManager::class, - 'auth.driver' => \Illuminate\Auth\SessionGuard::class, - 'blade.compiler' => \Illuminate\View\Compilers\BladeCompiler::class, - 'cache' => \Illuminate\Cache\CacheManager::class, - 'cache.psr6' => \Symfony\Component\Cache\Adapter\Psr16Adapter::class, - 'cache.store' => \Illuminate\Cache\Repository::class, - 'clockwork' => \Clockwork\Clockwork::class, - 'clockwork.authenticator' => \Clockwork\Authentication\NullAuthenticator::class, - 'clockwork.cache' => \Clockwork\DataSource\LaravelCacheDataSource::class, - 'clockwork.eloquent' => \Clockwork\DataSource\EloquentDataSource::class, - 'clockwork.events' => \Clockwork\DataSource\LaravelEventsDataSource::class, - 'clockwork.laravel' => \Clockwork\DataSource\LaravelDataSource::class, - 'clockwork.notifications' => \Clockwork\DataSource\LaravelNotificationsDataSource::class, - 'clockwork.queue' => \Clockwork\DataSource\LaravelQueueDataSource::class, - 'clockwork.redis' => \Clockwork\DataSource\LaravelRedisDataSource::class, - 'clockwork.request' => \Clockwork\Request\Request::class, - 'clockwork.storage' => \Clockwork\Storage\FileStorage::class, - 'clockwork.support' => \Clockwork\Support\Laravel\ClockworkSupport::class, - 'clockwork.swift' => \Clockwork\DataSource\SwiftDataSource::class, - 'clockwork.views' => \Clockwork\DataSource\LaravelViewsDataSource::class, - 'clockwork.xdebug' => \Clockwork\DataSource\XdebugDataSource::class, - 'command.auth.resets.clear' => \Illuminate\Auth\Console\ClearResetsCommand::class, - 'command.cache.clear' => \Illuminate\Cache\Console\ClearCommand::class, - 'command.cache.forget' => \Illuminate\Cache\Console\ForgetCommand::class, - 'command.cache.table' => \Illuminate\Cache\Console\CacheTableCommand::class, - 'command.cast.make' => \Illuminate\Foundation\Console\CastMakeCommand::class, - 'command.channel.make' => \Illuminate\Foundation\Console\ChannelMakeCommand::class, - 'command.clear-compiled' => \Illuminate\Foundation\Console\ClearCompiledCommand::class, - 'command.component.make' => \Illuminate\Foundation\Console\ComponentMakeCommand::class, - 'command.config.cache' => \Illuminate\Foundation\Console\ConfigCacheCommand::class, - 'command.config.clear' => \Illuminate\Foundation\Console\ConfigClearCommand::class, - 'command.console.make' => \Illuminate\Foundation\Console\ConsoleMakeCommand::class, - 'command.controller.make' => \Illuminate\Routing\Console\ControllerMakeCommand::class, - 'command.db.wipe' => \Illuminate\Database\Console\WipeCommand::class, - 'command.debugbar.clear' => \Barryvdh\Debugbar\Console\ClearCommand::class, - 'command.down' => \Illuminate\Foundation\Console\DownCommand::class, - 'command.environment' => \Illuminate\Foundation\Console\EnvironmentCommand::class, - 'command.event.cache' => \Illuminate\Foundation\Console\EventCacheCommand::class, - 'command.event.clear' => \Illuminate\Foundation\Console\EventClearCommand::class, - 'command.event.generate' => \Illuminate\Foundation\Console\EventGenerateCommand::class, - 'command.event.list' => \Illuminate\Foundation\Console\EventListCommand::class, - 'command.event.make' => \Illuminate\Foundation\Console\EventMakeCommand::class, - 'command.exception.make' => \Illuminate\Foundation\Console\ExceptionMakeCommand::class, - 'command.factory.make' => \Illuminate\Database\Console\Factories\FactoryMakeCommand::class, - 'command.ide-helper.eloquent' => \Barryvdh\LaravelIdeHelper\Console\EloquentCommand::class, - 'command.ide-helper.generate' => \Barryvdh\LaravelIdeHelper\Console\GeneratorCommand::class, - 'command.ide-helper.meta' => \Barryvdh\LaravelIdeHelper\Console\MetaCommand::class, - 'command.ide-helper.models' => \Barryvdh\LaravelIdeHelper\Console\ModelsCommand::class, - 'command.job.make' => \Illuminate\Foundation\Console\JobMakeCommand::class, - 'command.key.generate' => \Illuminate\Foundation\Console\KeyGenerateCommand::class, - 'command.listener.make' => \Illuminate\Foundation\Console\ListenerMakeCommand::class, - 'command.mail.make' => \Illuminate\Foundation\Console\MailMakeCommand::class, - 'command.middleware.make' => \Illuminate\Routing\Console\MiddlewareMakeCommand::class, - 'command.migrate' => \Illuminate\Database\Console\Migrations\MigrateCommand::class, - 'command.migrate.fresh' => \Illuminate\Database\Console\Migrations\FreshCommand::class, - 'command.migrate.install' => \Illuminate\Database\Console\Migrations\InstallCommand::class, - 'command.migrate.make' => \Illuminate\Database\Console\Migrations\MigrateMakeCommand::class, - 'command.migrate.refresh' => \Illuminate\Database\Console\Migrations\RefreshCommand::class, - 'command.migrate.reset' => \Illuminate\Database\Console\Migrations\ResetCommand::class, - 'command.migrate.rollback' => \Illuminate\Database\Console\Migrations\RollbackCommand::class, - 'command.migrate.status' => \Illuminate\Database\Console\Migrations\StatusCommand::class, - 'command.model.make' => \Illuminate\Foundation\Console\ModelMakeCommand::class, - 'command.notification.make' => \Illuminate\Foundation\Console\NotificationMakeCommand::class, - 'command.notification.table' => \Illuminate\Notifications\Console\NotificationTableCommand::class, - 'command.observer.make' => \Illuminate\Foundation\Console\ObserverMakeCommand::class, - 'command.optimize' => \Illuminate\Foundation\Console\OptimizeCommand::class, - 'command.optimize.clear' => \Illuminate\Foundation\Console\OptimizeClearCommand::class, - 'command.package.discover' => \Illuminate\Foundation\Console\PackageDiscoverCommand::class, - 'command.policy.make' => \Illuminate\Foundation\Console\PolicyMakeCommand::class, - 'command.provider.make' => \Illuminate\Foundation\Console\ProviderMakeCommand::class, - 'command.queue.batches-table' => \Illuminate\Queue\Console\BatchesTableCommand::class, - 'command.queue.clear' => \Illuminate\Queue\Console\ClearCommand::class, - 'command.queue.failed' => \Illuminate\Queue\Console\ListFailedCommand::class, - 'command.queue.failed-table' => \Illuminate\Queue\Console\FailedTableCommand::class, - 'command.queue.flush' => \Illuminate\Queue\Console\FlushFailedCommand::class, - 'command.queue.forget' => \Illuminate\Queue\Console\ForgetFailedCommand::class, - 'command.queue.listen' => \Illuminate\Queue\Console\ListenCommand::class, - 'command.queue.prune-batches' => \Illuminate\Queue\Console\PruneBatchesCommand::class, - 'command.queue.restart' => \Illuminate\Queue\Console\RestartCommand::class, - 'command.queue.retry' => \Illuminate\Queue\Console\RetryCommand::class, - 'command.queue.retry-batch' => \Illuminate\Queue\Console\RetryBatchCommand::class, - 'command.queue.table' => \Illuminate\Queue\Console\TableCommand::class, - 'command.queue.work' => \Illuminate\Queue\Console\WorkCommand::class, - 'command.request.make' => \Illuminate\Foundation\Console\RequestMakeCommand::class, - 'command.resource.make' => \Illuminate\Foundation\Console\ResourceMakeCommand::class, - 'command.route.cache' => \Illuminate\Foundation\Console\RouteCacheCommand::class, - 'command.route.clear' => \Illuminate\Foundation\Console\RouteClearCommand::class, - 'command.route.list' => \Illuminate\Foundation\Console\RouteListCommand::class, - 'command.rule.make' => \Illuminate\Foundation\Console\RuleMakeCommand::class, - 'command.schema.dump' => \Illuminate\Database\Console\DumpCommand::class, - 'command.seed' => \Illuminate\Database\Console\Seeds\SeedCommand::class, - 'command.seeder.make' => \Illuminate\Database\Console\Seeds\SeederMakeCommand::class, - 'command.serve' => \Illuminate\Foundation\Console\ServeCommand::class, - 'command.session.table' => \Illuminate\Session\Console\SessionTableCommand::class, - 'command.storage.link' => \Illuminate\Foundation\Console\StorageLinkCommand::class, - 'command.stub.publish' => \Illuminate\Foundation\Console\StubPublishCommand::class, - 'command.test.make' => \Illuminate\Foundation\Console\TestMakeCommand::class, - 'command.up' => \Illuminate\Foundation\Console\UpCommand::class, - 'command.vendor.publish' => \Illuminate\Foundation\Console\VendorPublishCommand::class, - 'command.view.cache' => \Illuminate\Foundation\Console\ViewCacheCommand::class, - 'command.view.clear' => \Illuminate\Foundation\Console\ViewClearCommand::class, - 'composer' => \Illuminate\Support\Composer::class, - 'cookie' => \Illuminate\Cookie\CookieJar::class, - 'db' => \Illuminate\Database\DatabaseManager::class, - 'db.connection' => \Illuminate\Database\SQLiteConnection::class, - 'db.factory' => \Illuminate\Database\Connectors\ConnectionFactory::class, - 'db.transactions' => \Illuminate\Database\DatabaseTransactionsManager::class, - 'encrypter' => \Illuminate\Encryption\Encrypter::class, - 'events' => \Illuminate\Events\Dispatcher::class, - 'files' => \Illuminate\Filesystem\Filesystem::class, - 'filesystem' => \Illuminate\Filesystem\FilesystemManager::class, - 'filesystem.disk' => \Illuminate\Filesystem\FilesystemAdapter::class, - 'hash' => \Illuminate\Hashing\HashManager::class, - 'hash.driver' => \Illuminate\Hashing\BcryptHasher::class, - 'image-optimizer' => \Spatie\ImageOptimizer\OptimizerChain::class, - 'log' => \Illuminate\Log\LogManager::class, - 'mail.manager' => \Illuminate\Mail\MailManager::class, - 'mailer' => \Illuminate\Mail\Mailer::class, - 'markdown' => \League\CommonMark\CommonMarkConverter::class, - 'markdown.compiler' => \GrahamCampbell\Markdown\View\Compiler\MarkdownCompiler::class, - 'markdown.directive' => \GrahamCampbell\Markdown\View\Directive\MarkdownDirective::class, - 'markdown.environment' => \League\CommonMark\Environment::class, - 'memcached.connector' => \Illuminate\Cache\MemcachedConnector::class, - 'migration.creator' => \Illuminate\Database\Migrations\MigrationCreator::class, - 'migration.repository' => \Illuminate\Database\Migrations\DatabaseMigrationRepository::class, - 'migrator' => \Illuminate\Database\Migrations\Migrator::class, - 'queue' => \Illuminate\Queue\QueueManager::class, - 'queue.connection' => \Illuminate\Queue\SyncQueue::class, - 'queue.failer' => \Illuminate\Queue\Failed\DatabaseUuidFailedJobProvider::class, - 'queue.listener' => \Illuminate\Queue\Listener::class, - 'queue.worker' => \Illuminate\Queue\Worker::class, - 'redirect' => \Illuminate\Routing\Redirector::class, - 'redis' => \Illuminate\Redis\RedisManager::class, - 'router' => \Illuminate\Routing\Router::class, - 'session' => \Illuminate\Session\SessionManager::class, - 'session.store' => \Illuminate\Session\Store::class, - 'translation.loader' => \Illuminate\Translation\FileLoader::class, - 'translator' => \Illuminate\Translation\Translator::class, - 'url' => \Illuminate\Routing\UrlGenerator::class, - 'validation.presence' => \Illuminate\Validation\DatabasePresenceVerifier::class, - 'view' => \Illuminate\View\Factory::class, - 'view.engine.resolver' => \Illuminate\View\Engines\EngineResolver::class, - 'view.finder' => \Illuminate\View\FileViewFinder::class, - ])); - override(\Psr\Container\ContainerInterface::get(0), map([ - '' => '@', - 'AccessControl' => \App\ModelFunctions\SessionFunctions::class, - 'App\Actions\Albums\Extensions\PublicIds' => \App\Actions\Albums\Extensions\PublicIds::class, - 'App\Actions\Update\Apply' => \App\Actions\Update\Apply::class, - 'App\Actions\Update\Check' => \App\Actions\Update\Check::class, - 'App\Assets\Helpers' => \App\Assets\Helpers::class, - 'App\Image\ImageHandlerInterface' => \App\Image\ImageHandler::class, - 'App\Metadata\GitHubFunctions' => \App\Metadata\GitHubFunctions::class, - 'App\Metadata\GitRequest' => \App\Metadata\GitRequest::class, - 'App\Metadata\LycheeVersion' => \App\Metadata\LycheeVersion::class, - 'App\ModelFunctions\ConfigFunctions' => \App\ModelFunctions\ConfigFunctions::class, - 'App\ModelFunctions\SessionFunctions' => \App\ModelFunctions\SessionFunctions::class, - 'App\ModelFunctions\SymLinkFunctions' => \App\ModelFunctions\SymLinkFunctions::class, - 'Barryvdh\Debugbar\LaravelDebugbar' => \Barryvdh\Debugbar\LaravelDebugbar::class, - 'Cose\Algorithm\Manager' => \Cose\Algorithm\Manager::class, - 'DarkGhostHunter\Larapass\WebAuthn\PublicKeyCredentialParametersCollection' => \DarkGhostHunter\Larapass\WebAuthn\PublicKeyCredentialParametersCollection::class, - 'DarkGhostHunter\Larapass\WebAuthn\WebAuthnAssertValidator' => \DarkGhostHunter\Larapass\WebAuthn\WebAuthnAssertValidator::class, - 'DarkGhostHunter\Larapass\WebAuthn\WebAuthnAttestCreator' => \DarkGhostHunter\Larapass\WebAuthn\WebAuthnAttestCreator::class, - 'DarkGhostHunter\Larapass\WebAuthn\WebAuthnAttestValidator' => \DarkGhostHunter\Larapass\WebAuthn\WebAuthnAttestValidator::class, - 'Helpers' => \App\Assets\Helpers::class, - 'Illuminate\Auth\Middleware\RequirePassword' => \Illuminate\Auth\Middleware\RequirePassword::class, - 'Illuminate\Broadcasting\BroadcastManager' => \Illuminate\Broadcasting\BroadcastManager::class, - 'Illuminate\Bus\BatchRepository' => \Illuminate\Bus\DatabaseBatchRepository::class, - 'Illuminate\Bus\DatabaseBatchRepository' => \Illuminate\Bus\DatabaseBatchRepository::class, - 'Illuminate\Bus\Dispatcher' => \Illuminate\Bus\Dispatcher::class, - 'Illuminate\Cache\RateLimiter' => \Illuminate\Cache\RateLimiter::class, - 'Illuminate\Console\Scheduling\Schedule' => \Illuminate\Console\Scheduling\Schedule::class, - 'Illuminate\Console\Scheduling\ScheduleFinishCommand' => \Illuminate\Console\Scheduling\ScheduleFinishCommand::class, - 'Illuminate\Console\Scheduling\ScheduleListCommand' => \Illuminate\Console\Scheduling\ScheduleListCommand::class, - 'Illuminate\Console\Scheduling\ScheduleRunCommand' => \Illuminate\Console\Scheduling\ScheduleRunCommand::class, - 'Illuminate\Console\Scheduling\ScheduleTestCommand' => \Illuminate\Console\Scheduling\ScheduleTestCommand::class, - 'Illuminate\Console\Scheduling\ScheduleWorkCommand' => \Illuminate\Console\Scheduling\ScheduleWorkCommand::class, - 'Illuminate\Contracts\Auth\Access\Gate' => \Illuminate\Auth\Access\Gate::class, - 'Illuminate\Contracts\Broadcasting\Broadcaster' => \Illuminate\Broadcasting\Broadcasters\LogBroadcaster::class, - 'Illuminate\Contracts\Console\Kernel' => \App\Console\Kernel::class, - 'Illuminate\Contracts\Debug\ExceptionHandler' => \App\Exceptions\Handler::class, - 'Illuminate\Contracts\Http\Kernel' => \App\Http\Kernel::class, - 'Illuminate\Contracts\Pipeline\Hub' => \Illuminate\Pipeline\Hub::class, - 'Illuminate\Contracts\Queue\EntityResolver' => \Illuminate\Database\Eloquent\QueueEntityResolver::class, - 'Illuminate\Contracts\Routing\ResponseFactory' => \Illuminate\Routing\ResponseFactory::class, - 'Illuminate\Contracts\Validation\UncompromisedVerifier' => \Illuminate\Validation\NotPwnedVerifier::class, - 'Illuminate\Database\Console\DbCommand' => \Illuminate\Database\Console\DbCommand::class, - 'Illuminate\Foundation\Mix' => \Illuminate\Foundation\Mix::class, - 'Illuminate\Foundation\PackageManifest' => \Illuminate\Foundation\PackageManifest::class, - 'Illuminate\Mail\Markdown' => \Illuminate\Mail\Markdown::class, - 'Illuminate\Notifications\ChannelManager' => \Illuminate\Notifications\ChannelManager::class, - 'Illuminate\Routing\Contracts\ControllerDispatcher' => \Illuminate\Routing\ControllerDispatcher::class, - 'Illuminate\Session\Middleware\StartSession' => \Illuminate\Session\Middleware\StartSession::class, - 'Illuminate\Testing\ParallelTesting' => \Illuminate\Testing\ParallelTesting::class, - 'Livewire\LivewireComponentsFinder' => \Livewire\LivewireComponentsFinder::class, - 'Livewire\LivewireManager' => \Livewire\LivewireManager::class, - 'Psr\Http\Message\ResponseInterface' => \Nyholm\Psr7\Response::class, - 'Psr\Http\Message\ServerRequestInterface' => \Nyholm\Psr7\ServerRequest::class, - 'Spatie\ImageOptimizer\OptimizerChain' => \Spatie\ImageOptimizer\OptimizerChain::class, - 'Webauthn\AttestationStatement\AttestationObjectLoader' => \Webauthn\AttestationStatement\AttestationObjectLoader::class, - 'Webauthn\AttestationStatement\AttestationStatementSupportManager' => \Webauthn\AttestationStatement\AttestationStatementSupportManager::class, - 'Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs' => \Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs::class, - 'Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler' => \Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler::class, - 'Webauthn\AuthenticatorAssertionResponseValidator' => \Webauthn\AuthenticatorAssertionResponseValidator::class, - 'Webauthn\AuthenticatorAttestationResponseValidator' => \Webauthn\AuthenticatorAttestationResponseValidator::class, - 'Webauthn\AuthenticatorSelectionCriteria' => \DarkGhostHunter\Larapass\WebAuthn\AuthenticatorSelectionCriteria::class, - 'Webauthn\Counter\CounterChecker' => \Webauthn\Counter\ThrowExceptionIfInvalid::class, - 'Webauthn\PublicKeyCredentialLoader' => \Webauthn\PublicKeyCredentialLoader::class, - 'Webauthn\PublicKeyCredentialRpEntity' => \Webauthn\PublicKeyCredentialRpEntity::class, - 'Webauthn\PublicKeyCredentialSourceRepository' => \DarkGhostHunter\Larapass\Eloquent\WebAuthnCredential::class, - 'Webauthn\TokenBinding\TokenBindingHandler' => \Webauthn\TokenBinding\IgnoreTokenBindingHandler::class, - 'auth' => \Illuminate\Auth\AuthManager::class, - 'auth.driver' => \Illuminate\Auth\SessionGuard::class, - 'blade.compiler' => \Illuminate\View\Compilers\BladeCompiler::class, - 'cache' => \Illuminate\Cache\CacheManager::class, - 'cache.psr6' => \Symfony\Component\Cache\Adapter\Psr16Adapter::class, - 'cache.store' => \Illuminate\Cache\Repository::class, - 'clockwork' => \Clockwork\Clockwork::class, - 'clockwork.authenticator' => \Clockwork\Authentication\NullAuthenticator::class, - 'clockwork.cache' => \Clockwork\DataSource\LaravelCacheDataSource::class, - 'clockwork.eloquent' => \Clockwork\DataSource\EloquentDataSource::class, - 'clockwork.events' => \Clockwork\DataSource\LaravelEventsDataSource::class, - 'clockwork.laravel' => \Clockwork\DataSource\LaravelDataSource::class, - 'clockwork.notifications' => \Clockwork\DataSource\LaravelNotificationsDataSource::class, - 'clockwork.queue' => \Clockwork\DataSource\LaravelQueueDataSource::class, - 'clockwork.redis' => \Clockwork\DataSource\LaravelRedisDataSource::class, - 'clockwork.request' => \Clockwork\Request\Request::class, - 'clockwork.storage' => \Clockwork\Storage\FileStorage::class, - 'clockwork.support' => \Clockwork\Support\Laravel\ClockworkSupport::class, - 'clockwork.swift' => \Clockwork\DataSource\SwiftDataSource::class, - 'clockwork.views' => \Clockwork\DataSource\LaravelViewsDataSource::class, - 'clockwork.xdebug' => \Clockwork\DataSource\XdebugDataSource::class, - 'command.auth.resets.clear' => \Illuminate\Auth\Console\ClearResetsCommand::class, - 'command.cache.clear' => \Illuminate\Cache\Console\ClearCommand::class, - 'command.cache.forget' => \Illuminate\Cache\Console\ForgetCommand::class, - 'command.cache.table' => \Illuminate\Cache\Console\CacheTableCommand::class, - 'command.cast.make' => \Illuminate\Foundation\Console\CastMakeCommand::class, - 'command.channel.make' => \Illuminate\Foundation\Console\ChannelMakeCommand::class, - 'command.clear-compiled' => \Illuminate\Foundation\Console\ClearCompiledCommand::class, - 'command.component.make' => \Illuminate\Foundation\Console\ComponentMakeCommand::class, - 'command.config.cache' => \Illuminate\Foundation\Console\ConfigCacheCommand::class, - 'command.config.clear' => \Illuminate\Foundation\Console\ConfigClearCommand::class, - 'command.console.make' => \Illuminate\Foundation\Console\ConsoleMakeCommand::class, - 'command.controller.make' => \Illuminate\Routing\Console\ControllerMakeCommand::class, - 'command.db.wipe' => \Illuminate\Database\Console\WipeCommand::class, - 'command.debugbar.clear' => \Barryvdh\Debugbar\Console\ClearCommand::class, - 'command.down' => \Illuminate\Foundation\Console\DownCommand::class, - 'command.environment' => \Illuminate\Foundation\Console\EnvironmentCommand::class, - 'command.event.cache' => \Illuminate\Foundation\Console\EventCacheCommand::class, - 'command.event.clear' => \Illuminate\Foundation\Console\EventClearCommand::class, - 'command.event.generate' => \Illuminate\Foundation\Console\EventGenerateCommand::class, - 'command.event.list' => \Illuminate\Foundation\Console\EventListCommand::class, - 'command.event.make' => \Illuminate\Foundation\Console\EventMakeCommand::class, - 'command.exception.make' => \Illuminate\Foundation\Console\ExceptionMakeCommand::class, - 'command.factory.make' => \Illuminate\Database\Console\Factories\FactoryMakeCommand::class, - 'command.ide-helper.eloquent' => \Barryvdh\LaravelIdeHelper\Console\EloquentCommand::class, - 'command.ide-helper.generate' => \Barryvdh\LaravelIdeHelper\Console\GeneratorCommand::class, - 'command.ide-helper.meta' => \Barryvdh\LaravelIdeHelper\Console\MetaCommand::class, - 'command.ide-helper.models' => \Barryvdh\LaravelIdeHelper\Console\ModelsCommand::class, - 'command.job.make' => \Illuminate\Foundation\Console\JobMakeCommand::class, - 'command.key.generate' => \Illuminate\Foundation\Console\KeyGenerateCommand::class, - 'command.listener.make' => \Illuminate\Foundation\Console\ListenerMakeCommand::class, - 'command.mail.make' => \Illuminate\Foundation\Console\MailMakeCommand::class, - 'command.middleware.make' => \Illuminate\Routing\Console\MiddlewareMakeCommand::class, - 'command.migrate' => \Illuminate\Database\Console\Migrations\MigrateCommand::class, - 'command.migrate.fresh' => \Illuminate\Database\Console\Migrations\FreshCommand::class, - 'command.migrate.install' => \Illuminate\Database\Console\Migrations\InstallCommand::class, - 'command.migrate.make' => \Illuminate\Database\Console\Migrations\MigrateMakeCommand::class, - 'command.migrate.refresh' => \Illuminate\Database\Console\Migrations\RefreshCommand::class, - 'command.migrate.reset' => \Illuminate\Database\Console\Migrations\ResetCommand::class, - 'command.migrate.rollback' => \Illuminate\Database\Console\Migrations\RollbackCommand::class, - 'command.migrate.status' => \Illuminate\Database\Console\Migrations\StatusCommand::class, - 'command.model.make' => \Illuminate\Foundation\Console\ModelMakeCommand::class, - 'command.notification.make' => \Illuminate\Foundation\Console\NotificationMakeCommand::class, - 'command.notification.table' => \Illuminate\Notifications\Console\NotificationTableCommand::class, - 'command.observer.make' => \Illuminate\Foundation\Console\ObserverMakeCommand::class, - 'command.optimize' => \Illuminate\Foundation\Console\OptimizeCommand::class, - 'command.optimize.clear' => \Illuminate\Foundation\Console\OptimizeClearCommand::class, - 'command.package.discover' => \Illuminate\Foundation\Console\PackageDiscoverCommand::class, - 'command.policy.make' => \Illuminate\Foundation\Console\PolicyMakeCommand::class, - 'command.provider.make' => \Illuminate\Foundation\Console\ProviderMakeCommand::class, - 'command.queue.batches-table' => \Illuminate\Queue\Console\BatchesTableCommand::class, - 'command.queue.clear' => \Illuminate\Queue\Console\ClearCommand::class, - 'command.queue.failed' => \Illuminate\Queue\Console\ListFailedCommand::class, - 'command.queue.failed-table' => \Illuminate\Queue\Console\FailedTableCommand::class, - 'command.queue.flush' => \Illuminate\Queue\Console\FlushFailedCommand::class, - 'command.queue.forget' => \Illuminate\Queue\Console\ForgetFailedCommand::class, - 'command.queue.listen' => \Illuminate\Queue\Console\ListenCommand::class, - 'command.queue.prune-batches' => \Illuminate\Queue\Console\PruneBatchesCommand::class, - 'command.queue.restart' => \Illuminate\Queue\Console\RestartCommand::class, - 'command.queue.retry' => \Illuminate\Queue\Console\RetryCommand::class, - 'command.queue.retry-batch' => \Illuminate\Queue\Console\RetryBatchCommand::class, - 'command.queue.table' => \Illuminate\Queue\Console\TableCommand::class, - 'command.queue.work' => \Illuminate\Queue\Console\WorkCommand::class, - 'command.request.make' => \Illuminate\Foundation\Console\RequestMakeCommand::class, - 'command.resource.make' => \Illuminate\Foundation\Console\ResourceMakeCommand::class, - 'command.route.cache' => \Illuminate\Foundation\Console\RouteCacheCommand::class, - 'command.route.clear' => \Illuminate\Foundation\Console\RouteClearCommand::class, - 'command.route.list' => \Illuminate\Foundation\Console\RouteListCommand::class, - 'command.rule.make' => \Illuminate\Foundation\Console\RuleMakeCommand::class, - 'command.schema.dump' => \Illuminate\Database\Console\DumpCommand::class, - 'command.seed' => \Illuminate\Database\Console\Seeds\SeedCommand::class, - 'command.seeder.make' => \Illuminate\Database\Console\Seeds\SeederMakeCommand::class, - 'command.serve' => \Illuminate\Foundation\Console\ServeCommand::class, - 'command.session.table' => \Illuminate\Session\Console\SessionTableCommand::class, - 'command.storage.link' => \Illuminate\Foundation\Console\StorageLinkCommand::class, - 'command.stub.publish' => \Illuminate\Foundation\Console\StubPublishCommand::class, - 'command.test.make' => \Illuminate\Foundation\Console\TestMakeCommand::class, - 'command.up' => \Illuminate\Foundation\Console\UpCommand::class, - 'command.vendor.publish' => \Illuminate\Foundation\Console\VendorPublishCommand::class, - 'command.view.cache' => \Illuminate\Foundation\Console\ViewCacheCommand::class, - 'command.view.clear' => \Illuminate\Foundation\Console\ViewClearCommand::class, - 'composer' => \Illuminate\Support\Composer::class, - 'cookie' => \Illuminate\Cookie\CookieJar::class, - 'db' => \Illuminate\Database\DatabaseManager::class, - 'db.connection' => \Illuminate\Database\SQLiteConnection::class, - 'db.factory' => \Illuminate\Database\Connectors\ConnectionFactory::class, - 'db.transactions' => \Illuminate\Database\DatabaseTransactionsManager::class, - 'encrypter' => \Illuminate\Encryption\Encrypter::class, - 'events' => \Illuminate\Events\Dispatcher::class, - 'files' => \Illuminate\Filesystem\Filesystem::class, - 'filesystem' => \Illuminate\Filesystem\FilesystemManager::class, - 'filesystem.disk' => \Illuminate\Filesystem\FilesystemAdapter::class, - 'hash' => \Illuminate\Hashing\HashManager::class, - 'hash.driver' => \Illuminate\Hashing\BcryptHasher::class, - 'image-optimizer' => \Spatie\ImageOptimizer\OptimizerChain::class, - 'log' => \Illuminate\Log\LogManager::class, - 'mail.manager' => \Illuminate\Mail\MailManager::class, - 'mailer' => \Illuminate\Mail\Mailer::class, - 'markdown' => \League\CommonMark\CommonMarkConverter::class, - 'markdown.compiler' => \GrahamCampbell\Markdown\View\Compiler\MarkdownCompiler::class, - 'markdown.directive' => \GrahamCampbell\Markdown\View\Directive\MarkdownDirective::class, - 'markdown.environment' => \League\CommonMark\Environment::class, - 'memcached.connector' => \Illuminate\Cache\MemcachedConnector::class, - 'migration.creator' => \Illuminate\Database\Migrations\MigrationCreator::class, - 'migration.repository' => \Illuminate\Database\Migrations\DatabaseMigrationRepository::class, - 'migrator' => \Illuminate\Database\Migrations\Migrator::class, - 'queue' => \Illuminate\Queue\QueueManager::class, - 'queue.connection' => \Illuminate\Queue\SyncQueue::class, - 'queue.failer' => \Illuminate\Queue\Failed\DatabaseUuidFailedJobProvider::class, - 'queue.listener' => \Illuminate\Queue\Listener::class, - 'queue.worker' => \Illuminate\Queue\Worker::class, - 'redirect' => \Illuminate\Routing\Redirector::class, - 'redis' => \Illuminate\Redis\RedisManager::class, - 'router' => \Illuminate\Routing\Router::class, - 'session' => \Illuminate\Session\SessionManager::class, - 'session.store' => \Illuminate\Session\Store::class, - 'translation.loader' => \Illuminate\Translation\FileLoader::class, - 'translator' => \Illuminate\Translation\Translator::class, - 'url' => \Illuminate\Routing\UrlGenerator::class, - 'validation.presence' => \Illuminate\Validation\DatabasePresenceVerifier::class, - 'view' => \Illuminate\View\Factory::class, - 'view.engine.resolver' => \Illuminate\View\Engines\EngineResolver::class, - 'view.finder' => \Illuminate\View\FileViewFinder::class, - ])); +/** + * PhpStorm Meta file, to provide autocomplete information for PhpStorm. + * + * @author Barry vd. Heuvel + * + * @see https://github.com/barryvdh/laravel-ide-helper + */ +override(new \Illuminate\Contracts\Container\Container(), map([ + '' => '@', + 'App\Assets\Helpers' => \App\Assets\Helpers::class, + 'App\Contracts\Models\AbstractSizeVariantNamingStrategy' => \App\Assets\SizeVariantGroupedWithRandomSuffixNamingStrategy::class, + 'App\Contracts\Models\SizeVariantFactory' => \App\Image\SizeVariantDefaultFactory::class, + 'App\Factories\AlbumFactory' => \App\Factories\AlbumFactory::class, + 'App\Metadata\Versions\GitHubVersion' => \App\Metadata\Versions\GitHubVersion::class, + 'App\Metadata\Versions\Remote\GitCommits' => \App\Metadata\Versions\Remote\GitCommits::class, + 'App\Metadata\Versions\Remote\GitTags' => \App\Metadata\Versions\Remote\GitTags::class, + 'App\ModelFunctions\SymLinkFunctions' => \App\ModelFunctions\SymLinkFunctions::class, + 'App\Policies\AlbumQueryPolicy' => \App\Policies\AlbumQueryPolicy::class, + 'App\Policies\PhotoQueryPolicy' => \App\Policies\PhotoQueryPolicy::class, + 'Barryvdh\Debugbar\LaravelDebugbar' => \Barryvdh\Debugbar\LaravelDebugbar::class, + 'Dedoc\Scramble\Infer' => \Dedoc\Scramble\Infer::class, + 'Dedoc\Scramble\Infer\Scope\Index' => \Dedoc\Scramble\Infer\Scope\Index::class, + 'Dedoc\Scramble\Infer\Services\FileParser' => \Dedoc\Scramble\Infer\Services\FileParser::class, + 'Dedoc\Scramble\Support\ServerFactory' => \Dedoc\Scramble\Support\ServerFactory::class, + 'Helpers' => \App\Assets\Helpers::class, + 'Illuminate\Auth\Console\ClearResetsCommand' => \Illuminate\Auth\Console\ClearResetsCommand::class, + 'Illuminate\Broadcasting\BroadcastManager' => \Illuminate\Broadcasting\BroadcastManager::class, + 'Illuminate\Bus\Dispatcher' => \Illuminate\Bus\Dispatcher::class, + 'Illuminate\Cache\Console\PruneStaleTagsCommand' => \Illuminate\Cache\Console\PruneStaleTagsCommand::class, + 'Illuminate\Cache\RateLimiter' => \Illuminate\Cache\RateLimiter::class, + 'Illuminate\Console\Scheduling\ScheduleClearCacheCommand' => \Illuminate\Console\Scheduling\ScheduleClearCacheCommand::class, + 'Illuminate\Console\Scheduling\ScheduleFinishCommand' => \Illuminate\Console\Scheduling\ScheduleFinishCommand::class, + 'Illuminate\Console\Scheduling\ScheduleListCommand' => \Illuminate\Console\Scheduling\ScheduleListCommand::class, + 'Illuminate\Console\Scheduling\ScheduleRunCommand' => \Illuminate\Console\Scheduling\ScheduleRunCommand::class, + 'Illuminate\Console\Scheduling\ScheduleTestCommand' => \Illuminate\Console\Scheduling\ScheduleTestCommand::class, + 'Illuminate\Console\Scheduling\ScheduleWorkCommand' => \Illuminate\Console\Scheduling\ScheduleWorkCommand::class, + 'Illuminate\Contracts\Auth\Access\Gate' => \Illuminate\Auth\Access\Gate::class, + 'Illuminate\Contracts\Console\Kernel' => \App\Console\Kernel::class, + 'Illuminate\Contracts\Http\Kernel' => \App\Http\Kernel::class, + 'Illuminate\Contracts\Queue\EntityResolver' => \Illuminate\Database\Eloquent\QueueEntityResolver::class, + 'Illuminate\Database\Console\DbCommand' => \Illuminate\Database\Console\DbCommand::class, + 'Illuminate\Database\Console\DumpCommand' => \Illuminate\Database\Console\DumpCommand::class, + 'Illuminate\Database\Console\Migrations\InstallCommand' => \Illuminate\Database\Console\Migrations\InstallCommand::class, + 'Illuminate\Database\Console\Migrations\RefreshCommand' => \Illuminate\Database\Console\Migrations\RefreshCommand::class, + 'Illuminate\Database\Console\PruneCommand' => \Illuminate\Database\Console\PruneCommand::class, + 'Illuminate\Database\Console\ShowCommand' => \Illuminate\Database\Console\ShowCommand::class, + 'Illuminate\Database\Console\ShowModelCommand' => \Illuminate\Database\Console\ShowModelCommand::class, + 'Illuminate\Database\Console\TableCommand' => \Illuminate\Database\Console\TableCommand::class, + 'Illuminate\Database\Console\WipeCommand' => \Illuminate\Database\Console\WipeCommand::class, + 'Illuminate\Foundation\Console\ApiInstallCommand' => \Illuminate\Foundation\Console\ApiInstallCommand::class, + 'Illuminate\Foundation\Console\BroadcastingInstallCommand' => \Illuminate\Foundation\Console\BroadcastingInstallCommand::class, + 'Illuminate\Foundation\Console\ChannelListCommand' => \Illuminate\Foundation\Console\ChannelListCommand::class, + 'Illuminate\Foundation\Console\ClearCompiledCommand' => \Illuminate\Foundation\Console\ClearCompiledCommand::class, + 'Illuminate\Foundation\Console\ConfigPublishCommand' => \Illuminate\Foundation\Console\ConfigPublishCommand::class, + 'Illuminate\Foundation\Console\ConfigShowCommand' => \Illuminate\Foundation\Console\ConfigShowCommand::class, + 'Illuminate\Foundation\Console\DocsCommand' => \Illuminate\Foundation\Console\DocsCommand::class, + 'Illuminate\Foundation\Console\DownCommand' => \Illuminate\Foundation\Console\DownCommand::class, + 'Illuminate\Foundation\Console\EnvironmentCommand' => \Illuminate\Foundation\Console\EnvironmentCommand::class, + 'Illuminate\Foundation\Console\EventCacheCommand' => \Illuminate\Foundation\Console\EventCacheCommand::class, + 'Illuminate\Foundation\Console\EventGenerateCommand' => \Illuminate\Foundation\Console\EventGenerateCommand::class, + 'Illuminate\Foundation\Console\EventListCommand' => \Illuminate\Foundation\Console\EventListCommand::class, + 'Illuminate\Foundation\Console\KeyGenerateCommand' => \Illuminate\Foundation\Console\KeyGenerateCommand::class, + 'Illuminate\Foundation\Console\LangPublishCommand' => \Illuminate\Foundation\Console\LangPublishCommand::class, + 'Illuminate\Foundation\Console\OptimizeClearCommand' => \Illuminate\Foundation\Console\OptimizeClearCommand::class, + 'Illuminate\Foundation\Console\OptimizeCommand' => \Illuminate\Foundation\Console\OptimizeCommand::class, + 'Illuminate\Foundation\Console\PackageDiscoverCommand' => \Illuminate\Foundation\Console\PackageDiscoverCommand::class, + 'Illuminate\Foundation\Console\ServeCommand' => \Illuminate\Foundation\Console\ServeCommand::class, + 'Illuminate\Foundation\Console\StorageLinkCommand' => \Illuminate\Foundation\Console\StorageLinkCommand::class, + 'Illuminate\Foundation\Console\StorageUnlinkCommand' => \Illuminate\Foundation\Console\StorageUnlinkCommand::class, + 'Illuminate\Foundation\Console\StubPublishCommand' => \Illuminate\Foundation\Console\StubPublishCommand::class, + 'Illuminate\Foundation\Console\UpCommand' => \Illuminate\Foundation\Console\UpCommand::class, + 'Illuminate\Foundation\Console\ViewCacheCommand' => \Illuminate\Foundation\Console\ViewCacheCommand::class, + 'Illuminate\Foundation\Exceptions\Renderer\Listener' => \Illuminate\Foundation\Exceptions\Renderer\Listener::class, + 'Illuminate\Foundation\Mix' => \Illuminate\Foundation\Mix::class, + 'Illuminate\Foundation\PackageManifest' => \Illuminate\Foundation\PackageManifest::class, + 'Illuminate\Foundation\Vite' => \Illuminate\Foundation\Vite::class, + 'Illuminate\Queue\Console\ClearCommand' => \Illuminate\Queue\Console\ClearCommand::class, + 'Illuminate\Queue\Console\FlushFailedCommand' => \Illuminate\Queue\Console\FlushFailedCommand::class, + 'Illuminate\Queue\Console\ForgetFailedCommand' => \Illuminate\Queue\Console\ForgetFailedCommand::class, + 'Illuminate\Queue\Console\ListFailedCommand' => \Illuminate\Queue\Console\ListFailedCommand::class, + 'Illuminate\Queue\Console\ListenCommand' => \Illuminate\Queue\Console\ListenCommand::class, + 'Illuminate\Queue\Console\PruneBatchesCommand' => \Illuminate\Queue\Console\PruneBatchesCommand::class, + 'Illuminate\Queue\Console\PruneFailedJobsCommand' => \Illuminate\Queue\Console\PruneFailedJobsCommand::class, + 'Illuminate\Queue\Console\RetryBatchCommand' => \Illuminate\Queue\Console\RetryBatchCommand::class, + 'Illuminate\Queue\Console\RetryCommand' => \Illuminate\Queue\Console\RetryCommand::class, + 'Illuminate\Routing\Contracts\CallableDispatcher' => \Illuminate\Routing\CallableDispatcher::class, + 'Illuminate\Routing\Contracts\ControllerDispatcher' => \Illuminate\Routing\ControllerDispatcher::class, + 'Illuminate\Testing\ParallelTesting' => \Illuminate\Testing\ParallelTesting::class, + 'Laravel\Socialite\Contracts\Factory' => \Laravel\Socialite\SocialiteManager::class, + 'Livewire\EventBus' => \Livewire\EventBus::class, + 'Livewire\LivewireManager' => \Livewire\LivewireManager::class, + 'Livewire\Mechanisms\ExtendBlade\DeterministicBladeKeys' => \Livewire\Mechanisms\ExtendBlade\DeterministicBladeKeys::class, + 'Opcodes\LogViewer\LogTypeRegistrar' => \Opcodes\LogViewer\LogTypeRegistrar::class, + 'SocialiteProviders\Manager\Contracts\Helpers\ConfigRetrieverInterface' => \SocialiteProviders\Manager\Helpers\ConfigRetriever::class, + 'blade.compiler' => \Illuminate\View\Compilers\BladeCompiler::class, + 'clockwork.authenticator' => \Clockwork\Authentication\NullAuthenticator::class, + 'clockwork.request' => \Clockwork\Request\Request::class, + 'clockwork.storage' => \Clockwork\Storage\FileStorage::class, + 'clockwork.support' => \Clockwork\Support\Laravel\ClockworkSupport::class, + 'clockwork.xdebug' => \Clockwork\DataSource\XdebugDataSource::class, + 'command.debugbar.clear' => \Barryvdh\Debugbar\Console\ClearCommand::class, + 'command.ide-helper.eloquent' => \Barryvdh\LaravelIdeHelper\Console\EloquentCommand::class, + 'command.ide-helper.generate' => \Barryvdh\LaravelIdeHelper\Console\GeneratorCommand::class, + 'command.ide-helper.meta' => \Barryvdh\LaravelIdeHelper\Console\MetaCommand::class, + 'command.ide-helper.models' => \Barryvdh\LaravelIdeHelper\Console\ModelsCommand::class, + 'db.factory' => \Illuminate\Database\Connectors\ConnectionFactory::class, + 'db.transactions' => \Illuminate\Database\DatabaseTransactionsManager::class, + 'log-viewer' => \Opcodes\LogViewer\LogViewerService::class, + 'mail.manager' => \Illuminate\Mail\MailManager::class, + 'memcached.connector' => \Illuminate\Cache\MemcachedConnector::class, + 'migration.repository' => \Illuminate\Database\Migrations\DatabaseMigrationRepository::class, + 'queue.listener' => \Illuminate\Queue\Listener::class, + 'translation.loader' => \Illuminate\Translation\FileLoader::class, + 'view.engine.resolver' => \Illuminate\View\Engines\EngineResolver::class, +])); +override(\Illuminate\Container\Container::makeWith(0), map([ + '' => '@', + 'App\Assets\Helpers' => \App\Assets\Helpers::class, + 'App\Contracts\Models\AbstractSizeVariantNamingStrategy' => \App\Assets\SizeVariantGroupedWithRandomSuffixNamingStrategy::class, + 'App\Contracts\Models\SizeVariantFactory' => \App\Image\SizeVariantDefaultFactory::class, + 'App\Factories\AlbumFactory' => \App\Factories\AlbumFactory::class, + 'App\Metadata\Versions\GitHubVersion' => \App\Metadata\Versions\GitHubVersion::class, + 'App\Metadata\Versions\Remote\GitCommits' => \App\Metadata\Versions\Remote\GitCommits::class, + 'App\Metadata\Versions\Remote\GitTags' => \App\Metadata\Versions\Remote\GitTags::class, + 'App\ModelFunctions\SymLinkFunctions' => \App\ModelFunctions\SymLinkFunctions::class, + 'App\Policies\AlbumQueryPolicy' => \App\Policies\AlbumQueryPolicy::class, + 'App\Policies\PhotoQueryPolicy' => \App\Policies\PhotoQueryPolicy::class, + 'Barryvdh\Debugbar\LaravelDebugbar' => \Barryvdh\Debugbar\LaravelDebugbar::class, + 'Dedoc\Scramble\Infer' => \Dedoc\Scramble\Infer::class, + 'Dedoc\Scramble\Infer\Scope\Index' => \Dedoc\Scramble\Infer\Scope\Index::class, + 'Dedoc\Scramble\Infer\Services\FileParser' => \Dedoc\Scramble\Infer\Services\FileParser::class, + 'Dedoc\Scramble\Support\ServerFactory' => \Dedoc\Scramble\Support\ServerFactory::class, + 'Helpers' => \App\Assets\Helpers::class, + 'Illuminate\Auth\Console\ClearResetsCommand' => \Illuminate\Auth\Console\ClearResetsCommand::class, + 'Illuminate\Broadcasting\BroadcastManager' => \Illuminate\Broadcasting\BroadcastManager::class, + 'Illuminate\Bus\Dispatcher' => \Illuminate\Bus\Dispatcher::class, + 'Illuminate\Cache\Console\PruneStaleTagsCommand' => \Illuminate\Cache\Console\PruneStaleTagsCommand::class, + 'Illuminate\Cache\RateLimiter' => \Illuminate\Cache\RateLimiter::class, + 'Illuminate\Console\Scheduling\ScheduleClearCacheCommand' => \Illuminate\Console\Scheduling\ScheduleClearCacheCommand::class, + 'Illuminate\Console\Scheduling\ScheduleFinishCommand' => \Illuminate\Console\Scheduling\ScheduleFinishCommand::class, + 'Illuminate\Console\Scheduling\ScheduleListCommand' => \Illuminate\Console\Scheduling\ScheduleListCommand::class, + 'Illuminate\Console\Scheduling\ScheduleRunCommand' => \Illuminate\Console\Scheduling\ScheduleRunCommand::class, + 'Illuminate\Console\Scheduling\ScheduleTestCommand' => \Illuminate\Console\Scheduling\ScheduleTestCommand::class, + 'Illuminate\Console\Scheduling\ScheduleWorkCommand' => \Illuminate\Console\Scheduling\ScheduleWorkCommand::class, + 'Illuminate\Contracts\Auth\Access\Gate' => \Illuminate\Auth\Access\Gate::class, + 'Illuminate\Contracts\Console\Kernel' => \App\Console\Kernel::class, + 'Illuminate\Contracts\Http\Kernel' => \App\Http\Kernel::class, + 'Illuminate\Contracts\Queue\EntityResolver' => \Illuminate\Database\Eloquent\QueueEntityResolver::class, + 'Illuminate\Database\Console\DbCommand' => \Illuminate\Database\Console\DbCommand::class, + 'Illuminate\Database\Console\DumpCommand' => \Illuminate\Database\Console\DumpCommand::class, + 'Illuminate\Database\Console\Migrations\InstallCommand' => \Illuminate\Database\Console\Migrations\InstallCommand::class, + 'Illuminate\Database\Console\Migrations\RefreshCommand' => \Illuminate\Database\Console\Migrations\RefreshCommand::class, + 'Illuminate\Database\Console\PruneCommand' => \Illuminate\Database\Console\PruneCommand::class, + 'Illuminate\Database\Console\ShowCommand' => \Illuminate\Database\Console\ShowCommand::class, + 'Illuminate\Database\Console\ShowModelCommand' => \Illuminate\Database\Console\ShowModelCommand::class, + 'Illuminate\Database\Console\TableCommand' => \Illuminate\Database\Console\TableCommand::class, + 'Illuminate\Database\Console\WipeCommand' => \Illuminate\Database\Console\WipeCommand::class, + 'Illuminate\Foundation\Console\ApiInstallCommand' => \Illuminate\Foundation\Console\ApiInstallCommand::class, + 'Illuminate\Foundation\Console\BroadcastingInstallCommand' => \Illuminate\Foundation\Console\BroadcastingInstallCommand::class, + 'Illuminate\Foundation\Console\ChannelListCommand' => \Illuminate\Foundation\Console\ChannelListCommand::class, + 'Illuminate\Foundation\Console\ClearCompiledCommand' => \Illuminate\Foundation\Console\ClearCompiledCommand::class, + 'Illuminate\Foundation\Console\ConfigPublishCommand' => \Illuminate\Foundation\Console\ConfigPublishCommand::class, + 'Illuminate\Foundation\Console\ConfigShowCommand' => \Illuminate\Foundation\Console\ConfigShowCommand::class, + 'Illuminate\Foundation\Console\DocsCommand' => \Illuminate\Foundation\Console\DocsCommand::class, + 'Illuminate\Foundation\Console\DownCommand' => \Illuminate\Foundation\Console\DownCommand::class, + 'Illuminate\Foundation\Console\EnvironmentCommand' => \Illuminate\Foundation\Console\EnvironmentCommand::class, + 'Illuminate\Foundation\Console\EventCacheCommand' => \Illuminate\Foundation\Console\EventCacheCommand::class, + 'Illuminate\Foundation\Console\EventGenerateCommand' => \Illuminate\Foundation\Console\EventGenerateCommand::class, + 'Illuminate\Foundation\Console\EventListCommand' => \Illuminate\Foundation\Console\EventListCommand::class, + 'Illuminate\Foundation\Console\KeyGenerateCommand' => \Illuminate\Foundation\Console\KeyGenerateCommand::class, + 'Illuminate\Foundation\Console\LangPublishCommand' => \Illuminate\Foundation\Console\LangPublishCommand::class, + 'Illuminate\Foundation\Console\OptimizeClearCommand' => \Illuminate\Foundation\Console\OptimizeClearCommand::class, + 'Illuminate\Foundation\Console\OptimizeCommand' => \Illuminate\Foundation\Console\OptimizeCommand::class, + 'Illuminate\Foundation\Console\PackageDiscoverCommand' => \Illuminate\Foundation\Console\PackageDiscoverCommand::class, + 'Illuminate\Foundation\Console\ServeCommand' => \Illuminate\Foundation\Console\ServeCommand::class, + 'Illuminate\Foundation\Console\StorageLinkCommand' => \Illuminate\Foundation\Console\StorageLinkCommand::class, + 'Illuminate\Foundation\Console\StorageUnlinkCommand' => \Illuminate\Foundation\Console\StorageUnlinkCommand::class, + 'Illuminate\Foundation\Console\StubPublishCommand' => \Illuminate\Foundation\Console\StubPublishCommand::class, + 'Illuminate\Foundation\Console\UpCommand' => \Illuminate\Foundation\Console\UpCommand::class, + 'Illuminate\Foundation\Console\ViewCacheCommand' => \Illuminate\Foundation\Console\ViewCacheCommand::class, + 'Illuminate\Foundation\Exceptions\Renderer\Listener' => \Illuminate\Foundation\Exceptions\Renderer\Listener::class, + 'Illuminate\Foundation\Mix' => \Illuminate\Foundation\Mix::class, + 'Illuminate\Foundation\PackageManifest' => \Illuminate\Foundation\PackageManifest::class, + 'Illuminate\Foundation\Vite' => \Illuminate\Foundation\Vite::class, + 'Illuminate\Queue\Console\ClearCommand' => \Illuminate\Queue\Console\ClearCommand::class, + 'Illuminate\Queue\Console\FlushFailedCommand' => \Illuminate\Queue\Console\FlushFailedCommand::class, + 'Illuminate\Queue\Console\ForgetFailedCommand' => \Illuminate\Queue\Console\ForgetFailedCommand::class, + 'Illuminate\Queue\Console\ListFailedCommand' => \Illuminate\Queue\Console\ListFailedCommand::class, + 'Illuminate\Queue\Console\ListenCommand' => \Illuminate\Queue\Console\ListenCommand::class, + 'Illuminate\Queue\Console\PruneBatchesCommand' => \Illuminate\Queue\Console\PruneBatchesCommand::class, + 'Illuminate\Queue\Console\PruneFailedJobsCommand' => \Illuminate\Queue\Console\PruneFailedJobsCommand::class, + 'Illuminate\Queue\Console\RetryBatchCommand' => \Illuminate\Queue\Console\RetryBatchCommand::class, + 'Illuminate\Queue\Console\RetryCommand' => \Illuminate\Queue\Console\RetryCommand::class, + 'Illuminate\Routing\Contracts\CallableDispatcher' => \Illuminate\Routing\CallableDispatcher::class, + 'Illuminate\Routing\Contracts\ControllerDispatcher' => \Illuminate\Routing\ControllerDispatcher::class, + 'Illuminate\Testing\ParallelTesting' => \Illuminate\Testing\ParallelTesting::class, + 'Laravel\Socialite\Contracts\Factory' => \Laravel\Socialite\SocialiteManager::class, + 'Livewire\EventBus' => \Livewire\EventBus::class, + 'Livewire\LivewireManager' => \Livewire\LivewireManager::class, + 'Livewire\Mechanisms\ExtendBlade\DeterministicBladeKeys' => \Livewire\Mechanisms\ExtendBlade\DeterministicBladeKeys::class, + 'Opcodes\LogViewer\LogTypeRegistrar' => \Opcodes\LogViewer\LogTypeRegistrar::class, + 'SocialiteProviders\Manager\Contracts\Helpers\ConfigRetrieverInterface' => \SocialiteProviders\Manager\Helpers\ConfigRetriever::class, + 'blade.compiler' => \Illuminate\View\Compilers\BladeCompiler::class, + 'clockwork.authenticator' => \Clockwork\Authentication\NullAuthenticator::class, + 'clockwork.request' => \Clockwork\Request\Request::class, + 'clockwork.storage' => \Clockwork\Storage\FileStorage::class, + 'clockwork.support' => \Clockwork\Support\Laravel\ClockworkSupport::class, + 'clockwork.xdebug' => \Clockwork\DataSource\XdebugDataSource::class, + 'command.debugbar.clear' => \Barryvdh\Debugbar\Console\ClearCommand::class, + 'command.ide-helper.eloquent' => \Barryvdh\LaravelIdeHelper\Console\EloquentCommand::class, + 'command.ide-helper.generate' => \Barryvdh\LaravelIdeHelper\Console\GeneratorCommand::class, + 'command.ide-helper.meta' => \Barryvdh\LaravelIdeHelper\Console\MetaCommand::class, + 'command.ide-helper.models' => \Barryvdh\LaravelIdeHelper\Console\ModelsCommand::class, + 'db.factory' => \Illuminate\Database\Connectors\ConnectionFactory::class, + 'db.transactions' => \Illuminate\Database\DatabaseTransactionsManager::class, + 'log-viewer' => \Opcodes\LogViewer\LogViewerService::class, + 'mail.manager' => \Illuminate\Mail\MailManager::class, + 'memcached.connector' => \Illuminate\Cache\MemcachedConnector::class, + 'migration.repository' => \Illuminate\Database\Migrations\DatabaseMigrationRepository::class, + 'queue.listener' => \Illuminate\Queue\Listener::class, + 'translation.loader' => \Illuminate\Translation\FileLoader::class, + 'view.engine.resolver' => \Illuminate\View\Engines\EngineResolver::class, +])); +override(\Illuminate\Contracts\Container\Container::get(0), map([ + '' => '@', + 'App\Assets\Helpers' => \App\Assets\Helpers::class, + 'App\Contracts\Models\AbstractSizeVariantNamingStrategy' => \App\Assets\SizeVariantGroupedWithRandomSuffixNamingStrategy::class, + 'App\Contracts\Models\SizeVariantFactory' => \App\Image\SizeVariantDefaultFactory::class, + 'App\Factories\AlbumFactory' => \App\Factories\AlbumFactory::class, + 'App\Metadata\Versions\GitHubVersion' => \App\Metadata\Versions\GitHubVersion::class, + 'App\Metadata\Versions\Remote\GitCommits' => \App\Metadata\Versions\Remote\GitCommits::class, + 'App\Metadata\Versions\Remote\GitTags' => \App\Metadata\Versions\Remote\GitTags::class, + 'App\ModelFunctions\SymLinkFunctions' => \App\ModelFunctions\SymLinkFunctions::class, + 'App\Policies\AlbumQueryPolicy' => \App\Policies\AlbumQueryPolicy::class, + 'App\Policies\PhotoQueryPolicy' => \App\Policies\PhotoQueryPolicy::class, + 'Barryvdh\Debugbar\LaravelDebugbar' => \Barryvdh\Debugbar\LaravelDebugbar::class, + 'Dedoc\Scramble\Infer' => \Dedoc\Scramble\Infer::class, + 'Dedoc\Scramble\Infer\Scope\Index' => \Dedoc\Scramble\Infer\Scope\Index::class, + 'Dedoc\Scramble\Infer\Services\FileParser' => \Dedoc\Scramble\Infer\Services\FileParser::class, + 'Dedoc\Scramble\Support\ServerFactory' => \Dedoc\Scramble\Support\ServerFactory::class, + 'Helpers' => \App\Assets\Helpers::class, + 'Illuminate\Auth\Console\ClearResetsCommand' => \Illuminate\Auth\Console\ClearResetsCommand::class, + 'Illuminate\Broadcasting\BroadcastManager' => \Illuminate\Broadcasting\BroadcastManager::class, + 'Illuminate\Bus\Dispatcher' => \Illuminate\Bus\Dispatcher::class, + 'Illuminate\Cache\Console\PruneStaleTagsCommand' => \Illuminate\Cache\Console\PruneStaleTagsCommand::class, + 'Illuminate\Cache\RateLimiter' => \Illuminate\Cache\RateLimiter::class, + 'Illuminate\Console\Scheduling\ScheduleClearCacheCommand' => \Illuminate\Console\Scheduling\ScheduleClearCacheCommand::class, + 'Illuminate\Console\Scheduling\ScheduleFinishCommand' => \Illuminate\Console\Scheduling\ScheduleFinishCommand::class, + 'Illuminate\Console\Scheduling\ScheduleListCommand' => \Illuminate\Console\Scheduling\ScheduleListCommand::class, + 'Illuminate\Console\Scheduling\ScheduleRunCommand' => \Illuminate\Console\Scheduling\ScheduleRunCommand::class, + 'Illuminate\Console\Scheduling\ScheduleTestCommand' => \Illuminate\Console\Scheduling\ScheduleTestCommand::class, + 'Illuminate\Console\Scheduling\ScheduleWorkCommand' => \Illuminate\Console\Scheduling\ScheduleWorkCommand::class, + 'Illuminate\Contracts\Auth\Access\Gate' => \Illuminate\Auth\Access\Gate::class, + 'Illuminate\Contracts\Console\Kernel' => \App\Console\Kernel::class, + 'Illuminate\Contracts\Http\Kernel' => \App\Http\Kernel::class, + 'Illuminate\Contracts\Queue\EntityResolver' => \Illuminate\Database\Eloquent\QueueEntityResolver::class, + 'Illuminate\Database\Console\DbCommand' => \Illuminate\Database\Console\DbCommand::class, + 'Illuminate\Database\Console\DumpCommand' => \Illuminate\Database\Console\DumpCommand::class, + 'Illuminate\Database\Console\Migrations\InstallCommand' => \Illuminate\Database\Console\Migrations\InstallCommand::class, + 'Illuminate\Database\Console\Migrations\RefreshCommand' => \Illuminate\Database\Console\Migrations\RefreshCommand::class, + 'Illuminate\Database\Console\PruneCommand' => \Illuminate\Database\Console\PruneCommand::class, + 'Illuminate\Database\Console\ShowCommand' => \Illuminate\Database\Console\ShowCommand::class, + 'Illuminate\Database\Console\ShowModelCommand' => \Illuminate\Database\Console\ShowModelCommand::class, + 'Illuminate\Database\Console\TableCommand' => \Illuminate\Database\Console\TableCommand::class, + 'Illuminate\Database\Console\WipeCommand' => \Illuminate\Database\Console\WipeCommand::class, + 'Illuminate\Foundation\Console\ApiInstallCommand' => \Illuminate\Foundation\Console\ApiInstallCommand::class, + 'Illuminate\Foundation\Console\BroadcastingInstallCommand' => \Illuminate\Foundation\Console\BroadcastingInstallCommand::class, + 'Illuminate\Foundation\Console\ChannelListCommand' => \Illuminate\Foundation\Console\ChannelListCommand::class, + 'Illuminate\Foundation\Console\ClearCompiledCommand' => \Illuminate\Foundation\Console\ClearCompiledCommand::class, + 'Illuminate\Foundation\Console\ConfigPublishCommand' => \Illuminate\Foundation\Console\ConfigPublishCommand::class, + 'Illuminate\Foundation\Console\ConfigShowCommand' => \Illuminate\Foundation\Console\ConfigShowCommand::class, + 'Illuminate\Foundation\Console\DocsCommand' => \Illuminate\Foundation\Console\DocsCommand::class, + 'Illuminate\Foundation\Console\DownCommand' => \Illuminate\Foundation\Console\DownCommand::class, + 'Illuminate\Foundation\Console\EnvironmentCommand' => \Illuminate\Foundation\Console\EnvironmentCommand::class, + 'Illuminate\Foundation\Console\EventCacheCommand' => \Illuminate\Foundation\Console\EventCacheCommand::class, + 'Illuminate\Foundation\Console\EventGenerateCommand' => \Illuminate\Foundation\Console\EventGenerateCommand::class, + 'Illuminate\Foundation\Console\EventListCommand' => \Illuminate\Foundation\Console\EventListCommand::class, + 'Illuminate\Foundation\Console\KeyGenerateCommand' => \Illuminate\Foundation\Console\KeyGenerateCommand::class, + 'Illuminate\Foundation\Console\LangPublishCommand' => \Illuminate\Foundation\Console\LangPublishCommand::class, + 'Illuminate\Foundation\Console\OptimizeClearCommand' => \Illuminate\Foundation\Console\OptimizeClearCommand::class, + 'Illuminate\Foundation\Console\OptimizeCommand' => \Illuminate\Foundation\Console\OptimizeCommand::class, + 'Illuminate\Foundation\Console\PackageDiscoverCommand' => \Illuminate\Foundation\Console\PackageDiscoverCommand::class, + 'Illuminate\Foundation\Console\ServeCommand' => \Illuminate\Foundation\Console\ServeCommand::class, + 'Illuminate\Foundation\Console\StorageLinkCommand' => \Illuminate\Foundation\Console\StorageLinkCommand::class, + 'Illuminate\Foundation\Console\StorageUnlinkCommand' => \Illuminate\Foundation\Console\StorageUnlinkCommand::class, + 'Illuminate\Foundation\Console\StubPublishCommand' => \Illuminate\Foundation\Console\StubPublishCommand::class, + 'Illuminate\Foundation\Console\UpCommand' => \Illuminate\Foundation\Console\UpCommand::class, + 'Illuminate\Foundation\Console\ViewCacheCommand' => \Illuminate\Foundation\Console\ViewCacheCommand::class, + 'Illuminate\Foundation\Exceptions\Renderer\Listener' => \Illuminate\Foundation\Exceptions\Renderer\Listener::class, + 'Illuminate\Foundation\Mix' => \Illuminate\Foundation\Mix::class, + 'Illuminate\Foundation\PackageManifest' => \Illuminate\Foundation\PackageManifest::class, + 'Illuminate\Foundation\Vite' => \Illuminate\Foundation\Vite::class, + 'Illuminate\Queue\Console\ClearCommand' => \Illuminate\Queue\Console\ClearCommand::class, + 'Illuminate\Queue\Console\FlushFailedCommand' => \Illuminate\Queue\Console\FlushFailedCommand::class, + 'Illuminate\Queue\Console\ForgetFailedCommand' => \Illuminate\Queue\Console\ForgetFailedCommand::class, + 'Illuminate\Queue\Console\ListFailedCommand' => \Illuminate\Queue\Console\ListFailedCommand::class, + 'Illuminate\Queue\Console\ListenCommand' => \Illuminate\Queue\Console\ListenCommand::class, + 'Illuminate\Queue\Console\PruneBatchesCommand' => \Illuminate\Queue\Console\PruneBatchesCommand::class, + 'Illuminate\Queue\Console\PruneFailedJobsCommand' => \Illuminate\Queue\Console\PruneFailedJobsCommand::class, + 'Illuminate\Queue\Console\RetryBatchCommand' => \Illuminate\Queue\Console\RetryBatchCommand::class, + 'Illuminate\Queue\Console\RetryCommand' => \Illuminate\Queue\Console\RetryCommand::class, + 'Illuminate\Routing\Contracts\CallableDispatcher' => \Illuminate\Routing\CallableDispatcher::class, + 'Illuminate\Routing\Contracts\ControllerDispatcher' => \Illuminate\Routing\ControllerDispatcher::class, + 'Illuminate\Testing\ParallelTesting' => \Illuminate\Testing\ParallelTesting::class, + 'Laravel\Socialite\Contracts\Factory' => \Laravel\Socialite\SocialiteManager::class, + 'Livewire\EventBus' => \Livewire\EventBus::class, + 'Livewire\LivewireManager' => \Livewire\LivewireManager::class, + 'Livewire\Mechanisms\ExtendBlade\DeterministicBladeKeys' => \Livewire\Mechanisms\ExtendBlade\DeterministicBladeKeys::class, + 'Opcodes\LogViewer\LogTypeRegistrar' => \Opcodes\LogViewer\LogTypeRegistrar::class, + 'SocialiteProviders\Manager\Contracts\Helpers\ConfigRetrieverInterface' => \SocialiteProviders\Manager\Helpers\ConfigRetriever::class, + 'blade.compiler' => \Illuminate\View\Compilers\BladeCompiler::class, + 'clockwork.authenticator' => \Clockwork\Authentication\NullAuthenticator::class, + 'clockwork.request' => \Clockwork\Request\Request::class, + 'clockwork.storage' => \Clockwork\Storage\FileStorage::class, + 'clockwork.support' => \Clockwork\Support\Laravel\ClockworkSupport::class, + 'clockwork.xdebug' => \Clockwork\DataSource\XdebugDataSource::class, + 'command.debugbar.clear' => \Barryvdh\Debugbar\Console\ClearCommand::class, + 'command.ide-helper.eloquent' => \Barryvdh\LaravelIdeHelper\Console\EloquentCommand::class, + 'command.ide-helper.generate' => \Barryvdh\LaravelIdeHelper\Console\GeneratorCommand::class, + 'command.ide-helper.meta' => \Barryvdh\LaravelIdeHelper\Console\MetaCommand::class, + 'command.ide-helper.models' => \Barryvdh\LaravelIdeHelper\Console\ModelsCommand::class, + 'db.factory' => \Illuminate\Database\Connectors\ConnectionFactory::class, + 'db.transactions' => \Illuminate\Database\DatabaseTransactionsManager::class, + 'log-viewer' => \Opcodes\LogViewer\LogViewerService::class, + 'mail.manager' => \Illuminate\Mail\MailManager::class, + 'memcached.connector' => \Illuminate\Cache\MemcachedConnector::class, + 'migration.repository' => \Illuminate\Database\Migrations\DatabaseMigrationRepository::class, + 'queue.listener' => \Illuminate\Queue\Listener::class, + 'translation.loader' => \Illuminate\Translation\FileLoader::class, + 'view.engine.resolver' => \Illuminate\View\Engines\EngineResolver::class, +])); +override(\Illuminate\Contracts\Container\Container::make(0), map([ + '' => '@', + 'App\Assets\Helpers' => \App\Assets\Helpers::class, + 'App\Contracts\Models\AbstractSizeVariantNamingStrategy' => \App\Assets\SizeVariantGroupedWithRandomSuffixNamingStrategy::class, + 'App\Contracts\Models\SizeVariantFactory' => \App\Image\SizeVariantDefaultFactory::class, + 'App\Factories\AlbumFactory' => \App\Factories\AlbumFactory::class, + 'App\Metadata\Versions\GitHubVersion' => \App\Metadata\Versions\GitHubVersion::class, + 'App\Metadata\Versions\Remote\GitCommits' => \App\Metadata\Versions\Remote\GitCommits::class, + 'App\Metadata\Versions\Remote\GitTags' => \App\Metadata\Versions\Remote\GitTags::class, + 'App\ModelFunctions\SymLinkFunctions' => \App\ModelFunctions\SymLinkFunctions::class, + 'App\Policies\AlbumQueryPolicy' => \App\Policies\AlbumQueryPolicy::class, + 'App\Policies\PhotoQueryPolicy' => \App\Policies\PhotoQueryPolicy::class, + 'Barryvdh\Debugbar\LaravelDebugbar' => \Barryvdh\Debugbar\LaravelDebugbar::class, + 'Dedoc\Scramble\Infer' => \Dedoc\Scramble\Infer::class, + 'Dedoc\Scramble\Infer\Scope\Index' => \Dedoc\Scramble\Infer\Scope\Index::class, + 'Dedoc\Scramble\Infer\Services\FileParser' => \Dedoc\Scramble\Infer\Services\FileParser::class, + 'Dedoc\Scramble\Support\ServerFactory' => \Dedoc\Scramble\Support\ServerFactory::class, + 'Helpers' => \App\Assets\Helpers::class, + 'Illuminate\Auth\Console\ClearResetsCommand' => \Illuminate\Auth\Console\ClearResetsCommand::class, + 'Illuminate\Broadcasting\BroadcastManager' => \Illuminate\Broadcasting\BroadcastManager::class, + 'Illuminate\Bus\Dispatcher' => \Illuminate\Bus\Dispatcher::class, + 'Illuminate\Cache\Console\PruneStaleTagsCommand' => \Illuminate\Cache\Console\PruneStaleTagsCommand::class, + 'Illuminate\Cache\RateLimiter' => \Illuminate\Cache\RateLimiter::class, + 'Illuminate\Console\Scheduling\ScheduleClearCacheCommand' => \Illuminate\Console\Scheduling\ScheduleClearCacheCommand::class, + 'Illuminate\Console\Scheduling\ScheduleFinishCommand' => \Illuminate\Console\Scheduling\ScheduleFinishCommand::class, + 'Illuminate\Console\Scheduling\ScheduleListCommand' => \Illuminate\Console\Scheduling\ScheduleListCommand::class, + 'Illuminate\Console\Scheduling\ScheduleRunCommand' => \Illuminate\Console\Scheduling\ScheduleRunCommand::class, + 'Illuminate\Console\Scheduling\ScheduleTestCommand' => \Illuminate\Console\Scheduling\ScheduleTestCommand::class, + 'Illuminate\Console\Scheduling\ScheduleWorkCommand' => \Illuminate\Console\Scheduling\ScheduleWorkCommand::class, + 'Illuminate\Contracts\Auth\Access\Gate' => \Illuminate\Auth\Access\Gate::class, + 'Illuminate\Contracts\Console\Kernel' => \App\Console\Kernel::class, + 'Illuminate\Contracts\Http\Kernel' => \App\Http\Kernel::class, + 'Illuminate\Contracts\Queue\EntityResolver' => \Illuminate\Database\Eloquent\QueueEntityResolver::class, + 'Illuminate\Database\Console\DbCommand' => \Illuminate\Database\Console\DbCommand::class, + 'Illuminate\Database\Console\DumpCommand' => \Illuminate\Database\Console\DumpCommand::class, + 'Illuminate\Database\Console\Migrations\InstallCommand' => \Illuminate\Database\Console\Migrations\InstallCommand::class, + 'Illuminate\Database\Console\Migrations\RefreshCommand' => \Illuminate\Database\Console\Migrations\RefreshCommand::class, + 'Illuminate\Database\Console\PruneCommand' => \Illuminate\Database\Console\PruneCommand::class, + 'Illuminate\Database\Console\ShowCommand' => \Illuminate\Database\Console\ShowCommand::class, + 'Illuminate\Database\Console\ShowModelCommand' => \Illuminate\Database\Console\ShowModelCommand::class, + 'Illuminate\Database\Console\TableCommand' => \Illuminate\Database\Console\TableCommand::class, + 'Illuminate\Database\Console\WipeCommand' => \Illuminate\Database\Console\WipeCommand::class, + 'Illuminate\Foundation\Console\ApiInstallCommand' => \Illuminate\Foundation\Console\ApiInstallCommand::class, + 'Illuminate\Foundation\Console\BroadcastingInstallCommand' => \Illuminate\Foundation\Console\BroadcastingInstallCommand::class, + 'Illuminate\Foundation\Console\ChannelListCommand' => \Illuminate\Foundation\Console\ChannelListCommand::class, + 'Illuminate\Foundation\Console\ClearCompiledCommand' => \Illuminate\Foundation\Console\ClearCompiledCommand::class, + 'Illuminate\Foundation\Console\ConfigPublishCommand' => \Illuminate\Foundation\Console\ConfigPublishCommand::class, + 'Illuminate\Foundation\Console\ConfigShowCommand' => \Illuminate\Foundation\Console\ConfigShowCommand::class, + 'Illuminate\Foundation\Console\DocsCommand' => \Illuminate\Foundation\Console\DocsCommand::class, + 'Illuminate\Foundation\Console\DownCommand' => \Illuminate\Foundation\Console\DownCommand::class, + 'Illuminate\Foundation\Console\EnvironmentCommand' => \Illuminate\Foundation\Console\EnvironmentCommand::class, + 'Illuminate\Foundation\Console\EventCacheCommand' => \Illuminate\Foundation\Console\EventCacheCommand::class, + 'Illuminate\Foundation\Console\EventGenerateCommand' => \Illuminate\Foundation\Console\EventGenerateCommand::class, + 'Illuminate\Foundation\Console\EventListCommand' => \Illuminate\Foundation\Console\EventListCommand::class, + 'Illuminate\Foundation\Console\KeyGenerateCommand' => \Illuminate\Foundation\Console\KeyGenerateCommand::class, + 'Illuminate\Foundation\Console\LangPublishCommand' => \Illuminate\Foundation\Console\LangPublishCommand::class, + 'Illuminate\Foundation\Console\OptimizeClearCommand' => \Illuminate\Foundation\Console\OptimizeClearCommand::class, + 'Illuminate\Foundation\Console\OptimizeCommand' => \Illuminate\Foundation\Console\OptimizeCommand::class, + 'Illuminate\Foundation\Console\PackageDiscoverCommand' => \Illuminate\Foundation\Console\PackageDiscoverCommand::class, + 'Illuminate\Foundation\Console\ServeCommand' => \Illuminate\Foundation\Console\ServeCommand::class, + 'Illuminate\Foundation\Console\StorageLinkCommand' => \Illuminate\Foundation\Console\StorageLinkCommand::class, + 'Illuminate\Foundation\Console\StorageUnlinkCommand' => \Illuminate\Foundation\Console\StorageUnlinkCommand::class, + 'Illuminate\Foundation\Console\StubPublishCommand' => \Illuminate\Foundation\Console\StubPublishCommand::class, + 'Illuminate\Foundation\Console\UpCommand' => \Illuminate\Foundation\Console\UpCommand::class, + 'Illuminate\Foundation\Console\ViewCacheCommand' => \Illuminate\Foundation\Console\ViewCacheCommand::class, + 'Illuminate\Foundation\Exceptions\Renderer\Listener' => \Illuminate\Foundation\Exceptions\Renderer\Listener::class, + 'Illuminate\Foundation\Mix' => \Illuminate\Foundation\Mix::class, + 'Illuminate\Foundation\PackageManifest' => \Illuminate\Foundation\PackageManifest::class, + 'Illuminate\Foundation\Vite' => \Illuminate\Foundation\Vite::class, + 'Illuminate\Queue\Console\ClearCommand' => \Illuminate\Queue\Console\ClearCommand::class, + 'Illuminate\Queue\Console\FlushFailedCommand' => \Illuminate\Queue\Console\FlushFailedCommand::class, + 'Illuminate\Queue\Console\ForgetFailedCommand' => \Illuminate\Queue\Console\ForgetFailedCommand::class, + 'Illuminate\Queue\Console\ListFailedCommand' => \Illuminate\Queue\Console\ListFailedCommand::class, + 'Illuminate\Queue\Console\ListenCommand' => \Illuminate\Queue\Console\ListenCommand::class, + 'Illuminate\Queue\Console\PruneBatchesCommand' => \Illuminate\Queue\Console\PruneBatchesCommand::class, + 'Illuminate\Queue\Console\PruneFailedJobsCommand' => \Illuminate\Queue\Console\PruneFailedJobsCommand::class, + 'Illuminate\Queue\Console\RetryBatchCommand' => \Illuminate\Queue\Console\RetryBatchCommand::class, + 'Illuminate\Queue\Console\RetryCommand' => \Illuminate\Queue\Console\RetryCommand::class, + 'Illuminate\Routing\Contracts\CallableDispatcher' => \Illuminate\Routing\CallableDispatcher::class, + 'Illuminate\Routing\Contracts\ControllerDispatcher' => \Illuminate\Routing\ControllerDispatcher::class, + 'Illuminate\Testing\ParallelTesting' => \Illuminate\Testing\ParallelTesting::class, + 'Laravel\Socialite\Contracts\Factory' => \Laravel\Socialite\SocialiteManager::class, + 'Livewire\EventBus' => \Livewire\EventBus::class, + 'Livewire\LivewireManager' => \Livewire\LivewireManager::class, + 'Livewire\Mechanisms\ExtendBlade\DeterministicBladeKeys' => \Livewire\Mechanisms\ExtendBlade\DeterministicBladeKeys::class, + 'Opcodes\LogViewer\LogTypeRegistrar' => \Opcodes\LogViewer\LogTypeRegistrar::class, + 'SocialiteProviders\Manager\Contracts\Helpers\ConfigRetrieverInterface' => \SocialiteProviders\Manager\Helpers\ConfigRetriever::class, + 'blade.compiler' => \Illuminate\View\Compilers\BladeCompiler::class, + 'clockwork.authenticator' => \Clockwork\Authentication\NullAuthenticator::class, + 'clockwork.request' => \Clockwork\Request\Request::class, + 'clockwork.storage' => \Clockwork\Storage\FileStorage::class, + 'clockwork.support' => \Clockwork\Support\Laravel\ClockworkSupport::class, + 'clockwork.xdebug' => \Clockwork\DataSource\XdebugDataSource::class, + 'command.debugbar.clear' => \Barryvdh\Debugbar\Console\ClearCommand::class, + 'command.ide-helper.eloquent' => \Barryvdh\LaravelIdeHelper\Console\EloquentCommand::class, + 'command.ide-helper.generate' => \Barryvdh\LaravelIdeHelper\Console\GeneratorCommand::class, + 'command.ide-helper.meta' => \Barryvdh\LaravelIdeHelper\Console\MetaCommand::class, + 'command.ide-helper.models' => \Barryvdh\LaravelIdeHelper\Console\ModelsCommand::class, + 'db.factory' => \Illuminate\Database\Connectors\ConnectionFactory::class, + 'db.transactions' => \Illuminate\Database\DatabaseTransactionsManager::class, + 'log-viewer' => \Opcodes\LogViewer\LogViewerService::class, + 'mail.manager' => \Illuminate\Mail\MailManager::class, + 'memcached.connector' => \Illuminate\Cache\MemcachedConnector::class, + 'migration.repository' => \Illuminate\Database\Migrations\DatabaseMigrationRepository::class, + 'queue.listener' => \Illuminate\Queue\Listener::class, + 'translation.loader' => \Illuminate\Translation\FileLoader::class, + 'view.engine.resolver' => \Illuminate\View\Engines\EngineResolver::class, +])); +override(\Illuminate\Contracts\Container\Container::makeWith(0), map([ + '' => '@', + 'App\Assets\Helpers' => \App\Assets\Helpers::class, + 'App\Contracts\Models\AbstractSizeVariantNamingStrategy' => \App\Assets\SizeVariantGroupedWithRandomSuffixNamingStrategy::class, + 'App\Contracts\Models\SizeVariantFactory' => \App\Image\SizeVariantDefaultFactory::class, + 'App\Factories\AlbumFactory' => \App\Factories\AlbumFactory::class, + 'App\Metadata\Versions\GitHubVersion' => \App\Metadata\Versions\GitHubVersion::class, + 'App\Metadata\Versions\Remote\GitCommits' => \App\Metadata\Versions\Remote\GitCommits::class, + 'App\Metadata\Versions\Remote\GitTags' => \App\Metadata\Versions\Remote\GitTags::class, + 'App\ModelFunctions\SymLinkFunctions' => \App\ModelFunctions\SymLinkFunctions::class, + 'App\Policies\AlbumQueryPolicy' => \App\Policies\AlbumQueryPolicy::class, + 'App\Policies\PhotoQueryPolicy' => \App\Policies\PhotoQueryPolicy::class, + 'Barryvdh\Debugbar\LaravelDebugbar' => \Barryvdh\Debugbar\LaravelDebugbar::class, + 'Dedoc\Scramble\Infer' => \Dedoc\Scramble\Infer::class, + 'Dedoc\Scramble\Infer\Scope\Index' => \Dedoc\Scramble\Infer\Scope\Index::class, + 'Dedoc\Scramble\Infer\Services\FileParser' => \Dedoc\Scramble\Infer\Services\FileParser::class, + 'Dedoc\Scramble\Support\ServerFactory' => \Dedoc\Scramble\Support\ServerFactory::class, + 'Helpers' => \App\Assets\Helpers::class, + 'Illuminate\Auth\Console\ClearResetsCommand' => \Illuminate\Auth\Console\ClearResetsCommand::class, + 'Illuminate\Broadcasting\BroadcastManager' => \Illuminate\Broadcasting\BroadcastManager::class, + 'Illuminate\Bus\Dispatcher' => \Illuminate\Bus\Dispatcher::class, + 'Illuminate\Cache\Console\PruneStaleTagsCommand' => \Illuminate\Cache\Console\PruneStaleTagsCommand::class, + 'Illuminate\Cache\RateLimiter' => \Illuminate\Cache\RateLimiter::class, + 'Illuminate\Console\Scheduling\ScheduleClearCacheCommand' => \Illuminate\Console\Scheduling\ScheduleClearCacheCommand::class, + 'Illuminate\Console\Scheduling\ScheduleFinishCommand' => \Illuminate\Console\Scheduling\ScheduleFinishCommand::class, + 'Illuminate\Console\Scheduling\ScheduleListCommand' => \Illuminate\Console\Scheduling\ScheduleListCommand::class, + 'Illuminate\Console\Scheduling\ScheduleRunCommand' => \Illuminate\Console\Scheduling\ScheduleRunCommand::class, + 'Illuminate\Console\Scheduling\ScheduleTestCommand' => \Illuminate\Console\Scheduling\ScheduleTestCommand::class, + 'Illuminate\Console\Scheduling\ScheduleWorkCommand' => \Illuminate\Console\Scheduling\ScheduleWorkCommand::class, + 'Illuminate\Contracts\Auth\Access\Gate' => \Illuminate\Auth\Access\Gate::class, + 'Illuminate\Contracts\Console\Kernel' => \App\Console\Kernel::class, + 'Illuminate\Contracts\Http\Kernel' => \App\Http\Kernel::class, + 'Illuminate\Contracts\Queue\EntityResolver' => \Illuminate\Database\Eloquent\QueueEntityResolver::class, + 'Illuminate\Database\Console\DbCommand' => \Illuminate\Database\Console\DbCommand::class, + 'Illuminate\Database\Console\DumpCommand' => \Illuminate\Database\Console\DumpCommand::class, + 'Illuminate\Database\Console\Migrations\InstallCommand' => \Illuminate\Database\Console\Migrations\InstallCommand::class, + 'Illuminate\Database\Console\Migrations\RefreshCommand' => \Illuminate\Database\Console\Migrations\RefreshCommand::class, + 'Illuminate\Database\Console\PruneCommand' => \Illuminate\Database\Console\PruneCommand::class, + 'Illuminate\Database\Console\ShowCommand' => \Illuminate\Database\Console\ShowCommand::class, + 'Illuminate\Database\Console\ShowModelCommand' => \Illuminate\Database\Console\ShowModelCommand::class, + 'Illuminate\Database\Console\TableCommand' => \Illuminate\Database\Console\TableCommand::class, + 'Illuminate\Database\Console\WipeCommand' => \Illuminate\Database\Console\WipeCommand::class, + 'Illuminate\Foundation\Console\ApiInstallCommand' => \Illuminate\Foundation\Console\ApiInstallCommand::class, + 'Illuminate\Foundation\Console\BroadcastingInstallCommand' => \Illuminate\Foundation\Console\BroadcastingInstallCommand::class, + 'Illuminate\Foundation\Console\ChannelListCommand' => \Illuminate\Foundation\Console\ChannelListCommand::class, + 'Illuminate\Foundation\Console\ClearCompiledCommand' => \Illuminate\Foundation\Console\ClearCompiledCommand::class, + 'Illuminate\Foundation\Console\ConfigPublishCommand' => \Illuminate\Foundation\Console\ConfigPublishCommand::class, + 'Illuminate\Foundation\Console\ConfigShowCommand' => \Illuminate\Foundation\Console\ConfigShowCommand::class, + 'Illuminate\Foundation\Console\DocsCommand' => \Illuminate\Foundation\Console\DocsCommand::class, + 'Illuminate\Foundation\Console\DownCommand' => \Illuminate\Foundation\Console\DownCommand::class, + 'Illuminate\Foundation\Console\EnvironmentCommand' => \Illuminate\Foundation\Console\EnvironmentCommand::class, + 'Illuminate\Foundation\Console\EventCacheCommand' => \Illuminate\Foundation\Console\EventCacheCommand::class, + 'Illuminate\Foundation\Console\EventGenerateCommand' => \Illuminate\Foundation\Console\EventGenerateCommand::class, + 'Illuminate\Foundation\Console\EventListCommand' => \Illuminate\Foundation\Console\EventListCommand::class, + 'Illuminate\Foundation\Console\KeyGenerateCommand' => \Illuminate\Foundation\Console\KeyGenerateCommand::class, + 'Illuminate\Foundation\Console\LangPublishCommand' => \Illuminate\Foundation\Console\LangPublishCommand::class, + 'Illuminate\Foundation\Console\OptimizeClearCommand' => \Illuminate\Foundation\Console\OptimizeClearCommand::class, + 'Illuminate\Foundation\Console\OptimizeCommand' => \Illuminate\Foundation\Console\OptimizeCommand::class, + 'Illuminate\Foundation\Console\PackageDiscoverCommand' => \Illuminate\Foundation\Console\PackageDiscoverCommand::class, + 'Illuminate\Foundation\Console\ServeCommand' => \Illuminate\Foundation\Console\ServeCommand::class, + 'Illuminate\Foundation\Console\StorageLinkCommand' => \Illuminate\Foundation\Console\StorageLinkCommand::class, + 'Illuminate\Foundation\Console\StorageUnlinkCommand' => \Illuminate\Foundation\Console\StorageUnlinkCommand::class, + 'Illuminate\Foundation\Console\StubPublishCommand' => \Illuminate\Foundation\Console\StubPublishCommand::class, + 'Illuminate\Foundation\Console\UpCommand' => \Illuminate\Foundation\Console\UpCommand::class, + 'Illuminate\Foundation\Console\ViewCacheCommand' => \Illuminate\Foundation\Console\ViewCacheCommand::class, + 'Illuminate\Foundation\Exceptions\Renderer\Listener' => \Illuminate\Foundation\Exceptions\Renderer\Listener::class, + 'Illuminate\Foundation\Mix' => \Illuminate\Foundation\Mix::class, + 'Illuminate\Foundation\PackageManifest' => \Illuminate\Foundation\PackageManifest::class, + 'Illuminate\Foundation\Vite' => \Illuminate\Foundation\Vite::class, + 'Illuminate\Queue\Console\ClearCommand' => \Illuminate\Queue\Console\ClearCommand::class, + 'Illuminate\Queue\Console\FlushFailedCommand' => \Illuminate\Queue\Console\FlushFailedCommand::class, + 'Illuminate\Queue\Console\ForgetFailedCommand' => \Illuminate\Queue\Console\ForgetFailedCommand::class, + 'Illuminate\Queue\Console\ListFailedCommand' => \Illuminate\Queue\Console\ListFailedCommand::class, + 'Illuminate\Queue\Console\ListenCommand' => \Illuminate\Queue\Console\ListenCommand::class, + 'Illuminate\Queue\Console\PruneBatchesCommand' => \Illuminate\Queue\Console\PruneBatchesCommand::class, + 'Illuminate\Queue\Console\PruneFailedJobsCommand' => \Illuminate\Queue\Console\PruneFailedJobsCommand::class, + 'Illuminate\Queue\Console\RetryBatchCommand' => \Illuminate\Queue\Console\RetryBatchCommand::class, + 'Illuminate\Queue\Console\RetryCommand' => \Illuminate\Queue\Console\RetryCommand::class, + 'Illuminate\Routing\Contracts\CallableDispatcher' => \Illuminate\Routing\CallableDispatcher::class, + 'Illuminate\Routing\Contracts\ControllerDispatcher' => \Illuminate\Routing\ControllerDispatcher::class, + 'Illuminate\Testing\ParallelTesting' => \Illuminate\Testing\ParallelTesting::class, + 'Laravel\Socialite\Contracts\Factory' => \Laravel\Socialite\SocialiteManager::class, + 'Livewire\EventBus' => \Livewire\EventBus::class, + 'Livewire\LivewireManager' => \Livewire\LivewireManager::class, + 'Livewire\Mechanisms\ExtendBlade\DeterministicBladeKeys' => \Livewire\Mechanisms\ExtendBlade\DeterministicBladeKeys::class, + 'Opcodes\LogViewer\LogTypeRegistrar' => \Opcodes\LogViewer\LogTypeRegistrar::class, + 'SocialiteProviders\Manager\Contracts\Helpers\ConfigRetrieverInterface' => \SocialiteProviders\Manager\Helpers\ConfigRetriever::class, + 'blade.compiler' => \Illuminate\View\Compilers\BladeCompiler::class, + 'clockwork.authenticator' => \Clockwork\Authentication\NullAuthenticator::class, + 'clockwork.request' => \Clockwork\Request\Request::class, + 'clockwork.storage' => \Clockwork\Storage\FileStorage::class, + 'clockwork.support' => \Clockwork\Support\Laravel\ClockworkSupport::class, + 'clockwork.xdebug' => \Clockwork\DataSource\XdebugDataSource::class, + 'command.debugbar.clear' => \Barryvdh\Debugbar\Console\ClearCommand::class, + 'command.ide-helper.eloquent' => \Barryvdh\LaravelIdeHelper\Console\EloquentCommand::class, + 'command.ide-helper.generate' => \Barryvdh\LaravelIdeHelper\Console\GeneratorCommand::class, + 'command.ide-helper.meta' => \Barryvdh\LaravelIdeHelper\Console\MetaCommand::class, + 'command.ide-helper.models' => \Barryvdh\LaravelIdeHelper\Console\ModelsCommand::class, + 'db.factory' => \Illuminate\Database\Connectors\ConnectionFactory::class, + 'db.transactions' => \Illuminate\Database\DatabaseTransactionsManager::class, + 'log-viewer' => \Opcodes\LogViewer\LogViewerService::class, + 'mail.manager' => \Illuminate\Mail\MailManager::class, + 'memcached.connector' => \Illuminate\Cache\MemcachedConnector::class, + 'migration.repository' => \Illuminate\Database\Migrations\DatabaseMigrationRepository::class, + 'queue.listener' => \Illuminate\Queue\Listener::class, + 'translation.loader' => \Illuminate\Translation\FileLoader::class, + 'view.engine.resolver' => \Illuminate\View\Engines\EngineResolver::class, +])); +override(\App::get(0), map([ + '' => '@', + 'App\Assets\Helpers' => \App\Assets\Helpers::class, + 'App\Contracts\Models\AbstractSizeVariantNamingStrategy' => \App\Assets\SizeVariantGroupedWithRandomSuffixNamingStrategy::class, + 'App\Contracts\Models\SizeVariantFactory' => \App\Image\SizeVariantDefaultFactory::class, + 'App\Factories\AlbumFactory' => \App\Factories\AlbumFactory::class, + 'App\Metadata\Versions\GitHubVersion' => \App\Metadata\Versions\GitHubVersion::class, + 'App\Metadata\Versions\Remote\GitCommits' => \App\Metadata\Versions\Remote\GitCommits::class, + 'App\Metadata\Versions\Remote\GitTags' => \App\Metadata\Versions\Remote\GitTags::class, + 'App\ModelFunctions\SymLinkFunctions' => \App\ModelFunctions\SymLinkFunctions::class, + 'App\Policies\AlbumQueryPolicy' => \App\Policies\AlbumQueryPolicy::class, + 'App\Policies\PhotoQueryPolicy' => \App\Policies\PhotoQueryPolicy::class, + 'Barryvdh\Debugbar\LaravelDebugbar' => \Barryvdh\Debugbar\LaravelDebugbar::class, + 'Dedoc\Scramble\Infer' => \Dedoc\Scramble\Infer::class, + 'Dedoc\Scramble\Infer\Scope\Index' => \Dedoc\Scramble\Infer\Scope\Index::class, + 'Dedoc\Scramble\Infer\Services\FileParser' => \Dedoc\Scramble\Infer\Services\FileParser::class, + 'Dedoc\Scramble\Support\ServerFactory' => \Dedoc\Scramble\Support\ServerFactory::class, + 'Helpers' => \App\Assets\Helpers::class, + 'Illuminate\Auth\Console\ClearResetsCommand' => \Illuminate\Auth\Console\ClearResetsCommand::class, + 'Illuminate\Broadcasting\BroadcastManager' => \Illuminate\Broadcasting\BroadcastManager::class, + 'Illuminate\Bus\Dispatcher' => \Illuminate\Bus\Dispatcher::class, + 'Illuminate\Cache\Console\PruneStaleTagsCommand' => \Illuminate\Cache\Console\PruneStaleTagsCommand::class, + 'Illuminate\Cache\RateLimiter' => \Illuminate\Cache\RateLimiter::class, + 'Illuminate\Console\Scheduling\ScheduleClearCacheCommand' => \Illuminate\Console\Scheduling\ScheduleClearCacheCommand::class, + 'Illuminate\Console\Scheduling\ScheduleFinishCommand' => \Illuminate\Console\Scheduling\ScheduleFinishCommand::class, + 'Illuminate\Console\Scheduling\ScheduleListCommand' => \Illuminate\Console\Scheduling\ScheduleListCommand::class, + 'Illuminate\Console\Scheduling\ScheduleRunCommand' => \Illuminate\Console\Scheduling\ScheduleRunCommand::class, + 'Illuminate\Console\Scheduling\ScheduleTestCommand' => \Illuminate\Console\Scheduling\ScheduleTestCommand::class, + 'Illuminate\Console\Scheduling\ScheduleWorkCommand' => \Illuminate\Console\Scheduling\ScheduleWorkCommand::class, + 'Illuminate\Contracts\Auth\Access\Gate' => \Illuminate\Auth\Access\Gate::class, + 'Illuminate\Contracts\Console\Kernel' => \App\Console\Kernel::class, + 'Illuminate\Contracts\Http\Kernel' => \App\Http\Kernel::class, + 'Illuminate\Contracts\Queue\EntityResolver' => \Illuminate\Database\Eloquent\QueueEntityResolver::class, + 'Illuminate\Database\Console\DbCommand' => \Illuminate\Database\Console\DbCommand::class, + 'Illuminate\Database\Console\DumpCommand' => \Illuminate\Database\Console\DumpCommand::class, + 'Illuminate\Database\Console\Migrations\InstallCommand' => \Illuminate\Database\Console\Migrations\InstallCommand::class, + 'Illuminate\Database\Console\Migrations\RefreshCommand' => \Illuminate\Database\Console\Migrations\RefreshCommand::class, + 'Illuminate\Database\Console\PruneCommand' => \Illuminate\Database\Console\PruneCommand::class, + 'Illuminate\Database\Console\ShowCommand' => \Illuminate\Database\Console\ShowCommand::class, + 'Illuminate\Database\Console\ShowModelCommand' => \Illuminate\Database\Console\ShowModelCommand::class, + 'Illuminate\Database\Console\TableCommand' => \Illuminate\Database\Console\TableCommand::class, + 'Illuminate\Database\Console\WipeCommand' => \Illuminate\Database\Console\WipeCommand::class, + 'Illuminate\Foundation\Console\ApiInstallCommand' => \Illuminate\Foundation\Console\ApiInstallCommand::class, + 'Illuminate\Foundation\Console\BroadcastingInstallCommand' => \Illuminate\Foundation\Console\BroadcastingInstallCommand::class, + 'Illuminate\Foundation\Console\ChannelListCommand' => \Illuminate\Foundation\Console\ChannelListCommand::class, + 'Illuminate\Foundation\Console\ClearCompiledCommand' => \Illuminate\Foundation\Console\ClearCompiledCommand::class, + 'Illuminate\Foundation\Console\ConfigPublishCommand' => \Illuminate\Foundation\Console\ConfigPublishCommand::class, + 'Illuminate\Foundation\Console\ConfigShowCommand' => \Illuminate\Foundation\Console\ConfigShowCommand::class, + 'Illuminate\Foundation\Console\DocsCommand' => \Illuminate\Foundation\Console\DocsCommand::class, + 'Illuminate\Foundation\Console\DownCommand' => \Illuminate\Foundation\Console\DownCommand::class, + 'Illuminate\Foundation\Console\EnvironmentCommand' => \Illuminate\Foundation\Console\EnvironmentCommand::class, + 'Illuminate\Foundation\Console\EventCacheCommand' => \Illuminate\Foundation\Console\EventCacheCommand::class, + 'Illuminate\Foundation\Console\EventGenerateCommand' => \Illuminate\Foundation\Console\EventGenerateCommand::class, + 'Illuminate\Foundation\Console\EventListCommand' => \Illuminate\Foundation\Console\EventListCommand::class, + 'Illuminate\Foundation\Console\KeyGenerateCommand' => \Illuminate\Foundation\Console\KeyGenerateCommand::class, + 'Illuminate\Foundation\Console\LangPublishCommand' => \Illuminate\Foundation\Console\LangPublishCommand::class, + 'Illuminate\Foundation\Console\OptimizeClearCommand' => \Illuminate\Foundation\Console\OptimizeClearCommand::class, + 'Illuminate\Foundation\Console\OptimizeCommand' => \Illuminate\Foundation\Console\OptimizeCommand::class, + 'Illuminate\Foundation\Console\PackageDiscoverCommand' => \Illuminate\Foundation\Console\PackageDiscoverCommand::class, + 'Illuminate\Foundation\Console\ServeCommand' => \Illuminate\Foundation\Console\ServeCommand::class, + 'Illuminate\Foundation\Console\StorageLinkCommand' => \Illuminate\Foundation\Console\StorageLinkCommand::class, + 'Illuminate\Foundation\Console\StorageUnlinkCommand' => \Illuminate\Foundation\Console\StorageUnlinkCommand::class, + 'Illuminate\Foundation\Console\StubPublishCommand' => \Illuminate\Foundation\Console\StubPublishCommand::class, + 'Illuminate\Foundation\Console\UpCommand' => \Illuminate\Foundation\Console\UpCommand::class, + 'Illuminate\Foundation\Console\ViewCacheCommand' => \Illuminate\Foundation\Console\ViewCacheCommand::class, + 'Illuminate\Foundation\Exceptions\Renderer\Listener' => \Illuminate\Foundation\Exceptions\Renderer\Listener::class, + 'Illuminate\Foundation\Mix' => \Illuminate\Foundation\Mix::class, + 'Illuminate\Foundation\PackageManifest' => \Illuminate\Foundation\PackageManifest::class, + 'Illuminate\Foundation\Vite' => \Illuminate\Foundation\Vite::class, + 'Illuminate\Queue\Console\ClearCommand' => \Illuminate\Queue\Console\ClearCommand::class, + 'Illuminate\Queue\Console\FlushFailedCommand' => \Illuminate\Queue\Console\FlushFailedCommand::class, + 'Illuminate\Queue\Console\ForgetFailedCommand' => \Illuminate\Queue\Console\ForgetFailedCommand::class, + 'Illuminate\Queue\Console\ListFailedCommand' => \Illuminate\Queue\Console\ListFailedCommand::class, + 'Illuminate\Queue\Console\ListenCommand' => \Illuminate\Queue\Console\ListenCommand::class, + 'Illuminate\Queue\Console\PruneBatchesCommand' => \Illuminate\Queue\Console\PruneBatchesCommand::class, + 'Illuminate\Queue\Console\PruneFailedJobsCommand' => \Illuminate\Queue\Console\PruneFailedJobsCommand::class, + 'Illuminate\Queue\Console\RetryBatchCommand' => \Illuminate\Queue\Console\RetryBatchCommand::class, + 'Illuminate\Queue\Console\RetryCommand' => \Illuminate\Queue\Console\RetryCommand::class, + 'Illuminate\Routing\Contracts\CallableDispatcher' => \Illuminate\Routing\CallableDispatcher::class, + 'Illuminate\Routing\Contracts\ControllerDispatcher' => \Illuminate\Routing\ControllerDispatcher::class, + 'Illuminate\Testing\ParallelTesting' => \Illuminate\Testing\ParallelTesting::class, + 'Laravel\Socialite\Contracts\Factory' => \Laravel\Socialite\SocialiteManager::class, + 'Livewire\EventBus' => \Livewire\EventBus::class, + 'Livewire\LivewireManager' => \Livewire\LivewireManager::class, + 'Livewire\Mechanisms\ExtendBlade\DeterministicBladeKeys' => \Livewire\Mechanisms\ExtendBlade\DeterministicBladeKeys::class, + 'Opcodes\LogViewer\LogTypeRegistrar' => \Opcodes\LogViewer\LogTypeRegistrar::class, + 'SocialiteProviders\Manager\Contracts\Helpers\ConfigRetrieverInterface' => \SocialiteProviders\Manager\Helpers\ConfigRetriever::class, + 'blade.compiler' => \Illuminate\View\Compilers\BladeCompiler::class, + 'clockwork.authenticator' => \Clockwork\Authentication\NullAuthenticator::class, + 'clockwork.request' => \Clockwork\Request\Request::class, + 'clockwork.storage' => \Clockwork\Storage\FileStorage::class, + 'clockwork.support' => \Clockwork\Support\Laravel\ClockworkSupport::class, + 'clockwork.xdebug' => \Clockwork\DataSource\XdebugDataSource::class, + 'command.debugbar.clear' => \Barryvdh\Debugbar\Console\ClearCommand::class, + 'command.ide-helper.eloquent' => \Barryvdh\LaravelIdeHelper\Console\EloquentCommand::class, + 'command.ide-helper.generate' => \Barryvdh\LaravelIdeHelper\Console\GeneratorCommand::class, + 'command.ide-helper.meta' => \Barryvdh\LaravelIdeHelper\Console\MetaCommand::class, + 'command.ide-helper.models' => \Barryvdh\LaravelIdeHelper\Console\ModelsCommand::class, + 'db.factory' => \Illuminate\Database\Connectors\ConnectionFactory::class, + 'db.transactions' => \Illuminate\Database\DatabaseTransactionsManager::class, + 'log-viewer' => \Opcodes\LogViewer\LogViewerService::class, + 'mail.manager' => \Illuminate\Mail\MailManager::class, + 'memcached.connector' => \Illuminate\Cache\MemcachedConnector::class, + 'migration.repository' => \Illuminate\Database\Migrations\DatabaseMigrationRepository::class, + 'queue.listener' => \Illuminate\Queue\Listener::class, + 'translation.loader' => \Illuminate\Translation\FileLoader::class, + 'view.engine.resolver' => \Illuminate\View\Engines\EngineResolver::class, +])); +override(\App::make(0), map([ + '' => '@', + 'App\Assets\Helpers' => \App\Assets\Helpers::class, + 'App\Contracts\Models\AbstractSizeVariantNamingStrategy' => \App\Assets\SizeVariantGroupedWithRandomSuffixNamingStrategy::class, + 'App\Contracts\Models\SizeVariantFactory' => \App\Image\SizeVariantDefaultFactory::class, + 'App\Factories\AlbumFactory' => \App\Factories\AlbumFactory::class, + 'App\Metadata\Versions\GitHubVersion' => \App\Metadata\Versions\GitHubVersion::class, + 'App\Metadata\Versions\Remote\GitCommits' => \App\Metadata\Versions\Remote\GitCommits::class, + 'App\Metadata\Versions\Remote\GitTags' => \App\Metadata\Versions\Remote\GitTags::class, + 'App\ModelFunctions\SymLinkFunctions' => \App\ModelFunctions\SymLinkFunctions::class, + 'App\Policies\AlbumQueryPolicy' => \App\Policies\AlbumQueryPolicy::class, + 'App\Policies\PhotoQueryPolicy' => \App\Policies\PhotoQueryPolicy::class, + 'Barryvdh\Debugbar\LaravelDebugbar' => \Barryvdh\Debugbar\LaravelDebugbar::class, + 'Dedoc\Scramble\Infer' => \Dedoc\Scramble\Infer::class, + 'Dedoc\Scramble\Infer\Scope\Index' => \Dedoc\Scramble\Infer\Scope\Index::class, + 'Dedoc\Scramble\Infer\Services\FileParser' => \Dedoc\Scramble\Infer\Services\FileParser::class, + 'Dedoc\Scramble\Support\ServerFactory' => \Dedoc\Scramble\Support\ServerFactory::class, + 'Helpers' => \App\Assets\Helpers::class, + 'Illuminate\Auth\Console\ClearResetsCommand' => \Illuminate\Auth\Console\ClearResetsCommand::class, + 'Illuminate\Broadcasting\BroadcastManager' => \Illuminate\Broadcasting\BroadcastManager::class, + 'Illuminate\Bus\Dispatcher' => \Illuminate\Bus\Dispatcher::class, + 'Illuminate\Cache\Console\PruneStaleTagsCommand' => \Illuminate\Cache\Console\PruneStaleTagsCommand::class, + 'Illuminate\Cache\RateLimiter' => \Illuminate\Cache\RateLimiter::class, + 'Illuminate\Console\Scheduling\ScheduleClearCacheCommand' => \Illuminate\Console\Scheduling\ScheduleClearCacheCommand::class, + 'Illuminate\Console\Scheduling\ScheduleFinishCommand' => \Illuminate\Console\Scheduling\ScheduleFinishCommand::class, + 'Illuminate\Console\Scheduling\ScheduleListCommand' => \Illuminate\Console\Scheduling\ScheduleListCommand::class, + 'Illuminate\Console\Scheduling\ScheduleRunCommand' => \Illuminate\Console\Scheduling\ScheduleRunCommand::class, + 'Illuminate\Console\Scheduling\ScheduleTestCommand' => \Illuminate\Console\Scheduling\ScheduleTestCommand::class, + 'Illuminate\Console\Scheduling\ScheduleWorkCommand' => \Illuminate\Console\Scheduling\ScheduleWorkCommand::class, + 'Illuminate\Contracts\Auth\Access\Gate' => \Illuminate\Auth\Access\Gate::class, + 'Illuminate\Contracts\Console\Kernel' => \App\Console\Kernel::class, + 'Illuminate\Contracts\Http\Kernel' => \App\Http\Kernel::class, + 'Illuminate\Contracts\Queue\EntityResolver' => \Illuminate\Database\Eloquent\QueueEntityResolver::class, + 'Illuminate\Database\Console\DbCommand' => \Illuminate\Database\Console\DbCommand::class, + 'Illuminate\Database\Console\DumpCommand' => \Illuminate\Database\Console\DumpCommand::class, + 'Illuminate\Database\Console\Migrations\InstallCommand' => \Illuminate\Database\Console\Migrations\InstallCommand::class, + 'Illuminate\Database\Console\Migrations\RefreshCommand' => \Illuminate\Database\Console\Migrations\RefreshCommand::class, + 'Illuminate\Database\Console\PruneCommand' => \Illuminate\Database\Console\PruneCommand::class, + 'Illuminate\Database\Console\ShowCommand' => \Illuminate\Database\Console\ShowCommand::class, + 'Illuminate\Database\Console\ShowModelCommand' => \Illuminate\Database\Console\ShowModelCommand::class, + 'Illuminate\Database\Console\TableCommand' => \Illuminate\Database\Console\TableCommand::class, + 'Illuminate\Database\Console\WipeCommand' => \Illuminate\Database\Console\WipeCommand::class, + 'Illuminate\Foundation\Console\ApiInstallCommand' => \Illuminate\Foundation\Console\ApiInstallCommand::class, + 'Illuminate\Foundation\Console\BroadcastingInstallCommand' => \Illuminate\Foundation\Console\BroadcastingInstallCommand::class, + 'Illuminate\Foundation\Console\ChannelListCommand' => \Illuminate\Foundation\Console\ChannelListCommand::class, + 'Illuminate\Foundation\Console\ClearCompiledCommand' => \Illuminate\Foundation\Console\ClearCompiledCommand::class, + 'Illuminate\Foundation\Console\ConfigPublishCommand' => \Illuminate\Foundation\Console\ConfigPublishCommand::class, + 'Illuminate\Foundation\Console\ConfigShowCommand' => \Illuminate\Foundation\Console\ConfigShowCommand::class, + 'Illuminate\Foundation\Console\DocsCommand' => \Illuminate\Foundation\Console\DocsCommand::class, + 'Illuminate\Foundation\Console\DownCommand' => \Illuminate\Foundation\Console\DownCommand::class, + 'Illuminate\Foundation\Console\EnvironmentCommand' => \Illuminate\Foundation\Console\EnvironmentCommand::class, + 'Illuminate\Foundation\Console\EventCacheCommand' => \Illuminate\Foundation\Console\EventCacheCommand::class, + 'Illuminate\Foundation\Console\EventGenerateCommand' => \Illuminate\Foundation\Console\EventGenerateCommand::class, + 'Illuminate\Foundation\Console\EventListCommand' => \Illuminate\Foundation\Console\EventListCommand::class, + 'Illuminate\Foundation\Console\KeyGenerateCommand' => \Illuminate\Foundation\Console\KeyGenerateCommand::class, + 'Illuminate\Foundation\Console\LangPublishCommand' => \Illuminate\Foundation\Console\LangPublishCommand::class, + 'Illuminate\Foundation\Console\OptimizeClearCommand' => \Illuminate\Foundation\Console\OptimizeClearCommand::class, + 'Illuminate\Foundation\Console\OptimizeCommand' => \Illuminate\Foundation\Console\OptimizeCommand::class, + 'Illuminate\Foundation\Console\PackageDiscoverCommand' => \Illuminate\Foundation\Console\PackageDiscoverCommand::class, + 'Illuminate\Foundation\Console\ServeCommand' => \Illuminate\Foundation\Console\ServeCommand::class, + 'Illuminate\Foundation\Console\StorageLinkCommand' => \Illuminate\Foundation\Console\StorageLinkCommand::class, + 'Illuminate\Foundation\Console\StorageUnlinkCommand' => \Illuminate\Foundation\Console\StorageUnlinkCommand::class, + 'Illuminate\Foundation\Console\StubPublishCommand' => \Illuminate\Foundation\Console\StubPublishCommand::class, + 'Illuminate\Foundation\Console\UpCommand' => \Illuminate\Foundation\Console\UpCommand::class, + 'Illuminate\Foundation\Console\ViewCacheCommand' => \Illuminate\Foundation\Console\ViewCacheCommand::class, + 'Illuminate\Foundation\Exceptions\Renderer\Listener' => \Illuminate\Foundation\Exceptions\Renderer\Listener::class, + 'Illuminate\Foundation\Mix' => \Illuminate\Foundation\Mix::class, + 'Illuminate\Foundation\PackageManifest' => \Illuminate\Foundation\PackageManifest::class, + 'Illuminate\Foundation\Vite' => \Illuminate\Foundation\Vite::class, + 'Illuminate\Queue\Console\ClearCommand' => \Illuminate\Queue\Console\ClearCommand::class, + 'Illuminate\Queue\Console\FlushFailedCommand' => \Illuminate\Queue\Console\FlushFailedCommand::class, + 'Illuminate\Queue\Console\ForgetFailedCommand' => \Illuminate\Queue\Console\ForgetFailedCommand::class, + 'Illuminate\Queue\Console\ListFailedCommand' => \Illuminate\Queue\Console\ListFailedCommand::class, + 'Illuminate\Queue\Console\ListenCommand' => \Illuminate\Queue\Console\ListenCommand::class, + 'Illuminate\Queue\Console\PruneBatchesCommand' => \Illuminate\Queue\Console\PruneBatchesCommand::class, + 'Illuminate\Queue\Console\PruneFailedJobsCommand' => \Illuminate\Queue\Console\PruneFailedJobsCommand::class, + 'Illuminate\Queue\Console\RetryBatchCommand' => \Illuminate\Queue\Console\RetryBatchCommand::class, + 'Illuminate\Queue\Console\RetryCommand' => \Illuminate\Queue\Console\RetryCommand::class, + 'Illuminate\Routing\Contracts\CallableDispatcher' => \Illuminate\Routing\CallableDispatcher::class, + 'Illuminate\Routing\Contracts\ControllerDispatcher' => \Illuminate\Routing\ControllerDispatcher::class, + 'Illuminate\Testing\ParallelTesting' => \Illuminate\Testing\ParallelTesting::class, + 'Laravel\Socialite\Contracts\Factory' => \Laravel\Socialite\SocialiteManager::class, + 'Livewire\EventBus' => \Livewire\EventBus::class, + 'Livewire\LivewireManager' => \Livewire\LivewireManager::class, + 'Livewire\Mechanisms\ExtendBlade\DeterministicBladeKeys' => \Livewire\Mechanisms\ExtendBlade\DeterministicBladeKeys::class, + 'Opcodes\LogViewer\LogTypeRegistrar' => \Opcodes\LogViewer\LogTypeRegistrar::class, + 'SocialiteProviders\Manager\Contracts\Helpers\ConfigRetrieverInterface' => \SocialiteProviders\Manager\Helpers\ConfigRetriever::class, + 'blade.compiler' => \Illuminate\View\Compilers\BladeCompiler::class, + 'clockwork.authenticator' => \Clockwork\Authentication\NullAuthenticator::class, + 'clockwork.request' => \Clockwork\Request\Request::class, + 'clockwork.storage' => \Clockwork\Storage\FileStorage::class, + 'clockwork.support' => \Clockwork\Support\Laravel\ClockworkSupport::class, + 'clockwork.xdebug' => \Clockwork\DataSource\XdebugDataSource::class, + 'command.debugbar.clear' => \Barryvdh\Debugbar\Console\ClearCommand::class, + 'command.ide-helper.eloquent' => \Barryvdh\LaravelIdeHelper\Console\EloquentCommand::class, + 'command.ide-helper.generate' => \Barryvdh\LaravelIdeHelper\Console\GeneratorCommand::class, + 'command.ide-helper.meta' => \Barryvdh\LaravelIdeHelper\Console\MetaCommand::class, + 'command.ide-helper.models' => \Barryvdh\LaravelIdeHelper\Console\ModelsCommand::class, + 'db.factory' => \Illuminate\Database\Connectors\ConnectionFactory::class, + 'db.transactions' => \Illuminate\Database\DatabaseTransactionsManager::class, + 'log-viewer' => \Opcodes\LogViewer\LogViewerService::class, + 'mail.manager' => \Illuminate\Mail\MailManager::class, + 'memcached.connector' => \Illuminate\Cache\MemcachedConnector::class, + 'migration.repository' => \Illuminate\Database\Migrations\DatabaseMigrationRepository::class, + 'queue.listener' => \Illuminate\Queue\Listener::class, + 'translation.loader' => \Illuminate\Translation\FileLoader::class, + 'view.engine.resolver' => \Illuminate\View\Engines\EngineResolver::class, +])); +override(\App::makeWith(0), map([ + '' => '@', + 'App\Assets\Helpers' => \App\Assets\Helpers::class, + 'App\Contracts\Models\AbstractSizeVariantNamingStrategy' => \App\Assets\SizeVariantGroupedWithRandomSuffixNamingStrategy::class, + 'App\Contracts\Models\SizeVariantFactory' => \App\Image\SizeVariantDefaultFactory::class, + 'App\Factories\AlbumFactory' => \App\Factories\AlbumFactory::class, + 'App\Metadata\Versions\GitHubVersion' => \App\Metadata\Versions\GitHubVersion::class, + 'App\Metadata\Versions\Remote\GitCommits' => \App\Metadata\Versions\Remote\GitCommits::class, + 'App\Metadata\Versions\Remote\GitTags' => \App\Metadata\Versions\Remote\GitTags::class, + 'App\ModelFunctions\SymLinkFunctions' => \App\ModelFunctions\SymLinkFunctions::class, + 'App\Policies\AlbumQueryPolicy' => \App\Policies\AlbumQueryPolicy::class, + 'App\Policies\PhotoQueryPolicy' => \App\Policies\PhotoQueryPolicy::class, + 'Barryvdh\Debugbar\LaravelDebugbar' => \Barryvdh\Debugbar\LaravelDebugbar::class, + 'Dedoc\Scramble\Infer' => \Dedoc\Scramble\Infer::class, + 'Dedoc\Scramble\Infer\Scope\Index' => \Dedoc\Scramble\Infer\Scope\Index::class, + 'Dedoc\Scramble\Infer\Services\FileParser' => \Dedoc\Scramble\Infer\Services\FileParser::class, + 'Dedoc\Scramble\Support\ServerFactory' => \Dedoc\Scramble\Support\ServerFactory::class, + 'Helpers' => \App\Assets\Helpers::class, + 'Illuminate\Auth\Console\ClearResetsCommand' => \Illuminate\Auth\Console\ClearResetsCommand::class, + 'Illuminate\Broadcasting\BroadcastManager' => \Illuminate\Broadcasting\BroadcastManager::class, + 'Illuminate\Bus\Dispatcher' => \Illuminate\Bus\Dispatcher::class, + 'Illuminate\Cache\Console\PruneStaleTagsCommand' => \Illuminate\Cache\Console\PruneStaleTagsCommand::class, + 'Illuminate\Cache\RateLimiter' => \Illuminate\Cache\RateLimiter::class, + 'Illuminate\Console\Scheduling\ScheduleClearCacheCommand' => \Illuminate\Console\Scheduling\ScheduleClearCacheCommand::class, + 'Illuminate\Console\Scheduling\ScheduleFinishCommand' => \Illuminate\Console\Scheduling\ScheduleFinishCommand::class, + 'Illuminate\Console\Scheduling\ScheduleListCommand' => \Illuminate\Console\Scheduling\ScheduleListCommand::class, + 'Illuminate\Console\Scheduling\ScheduleRunCommand' => \Illuminate\Console\Scheduling\ScheduleRunCommand::class, + 'Illuminate\Console\Scheduling\ScheduleTestCommand' => \Illuminate\Console\Scheduling\ScheduleTestCommand::class, + 'Illuminate\Console\Scheduling\ScheduleWorkCommand' => \Illuminate\Console\Scheduling\ScheduleWorkCommand::class, + 'Illuminate\Contracts\Auth\Access\Gate' => \Illuminate\Auth\Access\Gate::class, + 'Illuminate\Contracts\Console\Kernel' => \App\Console\Kernel::class, + 'Illuminate\Contracts\Http\Kernel' => \App\Http\Kernel::class, + 'Illuminate\Contracts\Queue\EntityResolver' => \Illuminate\Database\Eloquent\QueueEntityResolver::class, + 'Illuminate\Database\Console\DbCommand' => \Illuminate\Database\Console\DbCommand::class, + 'Illuminate\Database\Console\DumpCommand' => \Illuminate\Database\Console\DumpCommand::class, + 'Illuminate\Database\Console\Migrations\InstallCommand' => \Illuminate\Database\Console\Migrations\InstallCommand::class, + 'Illuminate\Database\Console\Migrations\RefreshCommand' => \Illuminate\Database\Console\Migrations\RefreshCommand::class, + 'Illuminate\Database\Console\PruneCommand' => \Illuminate\Database\Console\PruneCommand::class, + 'Illuminate\Database\Console\ShowCommand' => \Illuminate\Database\Console\ShowCommand::class, + 'Illuminate\Database\Console\ShowModelCommand' => \Illuminate\Database\Console\ShowModelCommand::class, + 'Illuminate\Database\Console\TableCommand' => \Illuminate\Database\Console\TableCommand::class, + 'Illuminate\Database\Console\WipeCommand' => \Illuminate\Database\Console\WipeCommand::class, + 'Illuminate\Foundation\Console\ApiInstallCommand' => \Illuminate\Foundation\Console\ApiInstallCommand::class, + 'Illuminate\Foundation\Console\BroadcastingInstallCommand' => \Illuminate\Foundation\Console\BroadcastingInstallCommand::class, + 'Illuminate\Foundation\Console\ChannelListCommand' => \Illuminate\Foundation\Console\ChannelListCommand::class, + 'Illuminate\Foundation\Console\ClearCompiledCommand' => \Illuminate\Foundation\Console\ClearCompiledCommand::class, + 'Illuminate\Foundation\Console\ConfigPublishCommand' => \Illuminate\Foundation\Console\ConfigPublishCommand::class, + 'Illuminate\Foundation\Console\ConfigShowCommand' => \Illuminate\Foundation\Console\ConfigShowCommand::class, + 'Illuminate\Foundation\Console\DocsCommand' => \Illuminate\Foundation\Console\DocsCommand::class, + 'Illuminate\Foundation\Console\DownCommand' => \Illuminate\Foundation\Console\DownCommand::class, + 'Illuminate\Foundation\Console\EnvironmentCommand' => \Illuminate\Foundation\Console\EnvironmentCommand::class, + 'Illuminate\Foundation\Console\EventCacheCommand' => \Illuminate\Foundation\Console\EventCacheCommand::class, + 'Illuminate\Foundation\Console\EventGenerateCommand' => \Illuminate\Foundation\Console\EventGenerateCommand::class, + 'Illuminate\Foundation\Console\EventListCommand' => \Illuminate\Foundation\Console\EventListCommand::class, + 'Illuminate\Foundation\Console\KeyGenerateCommand' => \Illuminate\Foundation\Console\KeyGenerateCommand::class, + 'Illuminate\Foundation\Console\LangPublishCommand' => \Illuminate\Foundation\Console\LangPublishCommand::class, + 'Illuminate\Foundation\Console\OptimizeClearCommand' => \Illuminate\Foundation\Console\OptimizeClearCommand::class, + 'Illuminate\Foundation\Console\OptimizeCommand' => \Illuminate\Foundation\Console\OptimizeCommand::class, + 'Illuminate\Foundation\Console\PackageDiscoverCommand' => \Illuminate\Foundation\Console\PackageDiscoverCommand::class, + 'Illuminate\Foundation\Console\ServeCommand' => \Illuminate\Foundation\Console\ServeCommand::class, + 'Illuminate\Foundation\Console\StorageLinkCommand' => \Illuminate\Foundation\Console\StorageLinkCommand::class, + 'Illuminate\Foundation\Console\StorageUnlinkCommand' => \Illuminate\Foundation\Console\StorageUnlinkCommand::class, + 'Illuminate\Foundation\Console\StubPublishCommand' => \Illuminate\Foundation\Console\StubPublishCommand::class, + 'Illuminate\Foundation\Console\UpCommand' => \Illuminate\Foundation\Console\UpCommand::class, + 'Illuminate\Foundation\Console\ViewCacheCommand' => \Illuminate\Foundation\Console\ViewCacheCommand::class, + 'Illuminate\Foundation\Exceptions\Renderer\Listener' => \Illuminate\Foundation\Exceptions\Renderer\Listener::class, + 'Illuminate\Foundation\Mix' => \Illuminate\Foundation\Mix::class, + 'Illuminate\Foundation\PackageManifest' => \Illuminate\Foundation\PackageManifest::class, + 'Illuminate\Foundation\Vite' => \Illuminate\Foundation\Vite::class, + 'Illuminate\Queue\Console\ClearCommand' => \Illuminate\Queue\Console\ClearCommand::class, + 'Illuminate\Queue\Console\FlushFailedCommand' => \Illuminate\Queue\Console\FlushFailedCommand::class, + 'Illuminate\Queue\Console\ForgetFailedCommand' => \Illuminate\Queue\Console\ForgetFailedCommand::class, + 'Illuminate\Queue\Console\ListFailedCommand' => \Illuminate\Queue\Console\ListFailedCommand::class, + 'Illuminate\Queue\Console\ListenCommand' => \Illuminate\Queue\Console\ListenCommand::class, + 'Illuminate\Queue\Console\PruneBatchesCommand' => \Illuminate\Queue\Console\PruneBatchesCommand::class, + 'Illuminate\Queue\Console\PruneFailedJobsCommand' => \Illuminate\Queue\Console\PruneFailedJobsCommand::class, + 'Illuminate\Queue\Console\RetryBatchCommand' => \Illuminate\Queue\Console\RetryBatchCommand::class, + 'Illuminate\Queue\Console\RetryCommand' => \Illuminate\Queue\Console\RetryCommand::class, + 'Illuminate\Routing\Contracts\CallableDispatcher' => \Illuminate\Routing\CallableDispatcher::class, + 'Illuminate\Routing\Contracts\ControllerDispatcher' => \Illuminate\Routing\ControllerDispatcher::class, + 'Illuminate\Testing\ParallelTesting' => \Illuminate\Testing\ParallelTesting::class, + 'Laravel\Socialite\Contracts\Factory' => \Laravel\Socialite\SocialiteManager::class, + 'Livewire\EventBus' => \Livewire\EventBus::class, + 'Livewire\LivewireManager' => \Livewire\LivewireManager::class, + 'Livewire\Mechanisms\ExtendBlade\DeterministicBladeKeys' => \Livewire\Mechanisms\ExtendBlade\DeterministicBladeKeys::class, + 'Opcodes\LogViewer\LogTypeRegistrar' => \Opcodes\LogViewer\LogTypeRegistrar::class, + 'SocialiteProviders\Manager\Contracts\Helpers\ConfigRetrieverInterface' => \SocialiteProviders\Manager\Helpers\ConfigRetriever::class, + 'blade.compiler' => \Illuminate\View\Compilers\BladeCompiler::class, + 'clockwork.authenticator' => \Clockwork\Authentication\NullAuthenticator::class, + 'clockwork.request' => \Clockwork\Request\Request::class, + 'clockwork.storage' => \Clockwork\Storage\FileStorage::class, + 'clockwork.support' => \Clockwork\Support\Laravel\ClockworkSupport::class, + 'clockwork.xdebug' => \Clockwork\DataSource\XdebugDataSource::class, + 'command.debugbar.clear' => \Barryvdh\Debugbar\Console\ClearCommand::class, + 'command.ide-helper.eloquent' => \Barryvdh\LaravelIdeHelper\Console\EloquentCommand::class, + 'command.ide-helper.generate' => \Barryvdh\LaravelIdeHelper\Console\GeneratorCommand::class, + 'command.ide-helper.meta' => \Barryvdh\LaravelIdeHelper\Console\MetaCommand::class, + 'command.ide-helper.models' => \Barryvdh\LaravelIdeHelper\Console\ModelsCommand::class, + 'db.factory' => \Illuminate\Database\Connectors\ConnectionFactory::class, + 'db.transactions' => \Illuminate\Database\DatabaseTransactionsManager::class, + 'log-viewer' => \Opcodes\LogViewer\LogViewerService::class, + 'mail.manager' => \Illuminate\Mail\MailManager::class, + 'memcached.connector' => \Illuminate\Cache\MemcachedConnector::class, + 'migration.repository' => \Illuminate\Database\Migrations\DatabaseMigrationRepository::class, + 'queue.listener' => \Illuminate\Queue\Listener::class, + 'translation.loader' => \Illuminate\Translation\FileLoader::class, + 'view.engine.resolver' => \Illuminate\View\Engines\EngineResolver::class, +])); +override(\app(0), map([ + '' => '@', + 'App\Assets\Helpers' => \App\Assets\Helpers::class, + 'App\Contracts\Models\AbstractSizeVariantNamingStrategy' => \App\Assets\SizeVariantGroupedWithRandomSuffixNamingStrategy::class, + 'App\Contracts\Models\SizeVariantFactory' => \App\Image\SizeVariantDefaultFactory::class, + 'App\Factories\AlbumFactory' => \App\Factories\AlbumFactory::class, + 'App\Metadata\Versions\GitHubVersion' => \App\Metadata\Versions\GitHubVersion::class, + 'App\Metadata\Versions\Remote\GitCommits' => \App\Metadata\Versions\Remote\GitCommits::class, + 'App\Metadata\Versions\Remote\GitTags' => \App\Metadata\Versions\Remote\GitTags::class, + 'App\ModelFunctions\SymLinkFunctions' => \App\ModelFunctions\SymLinkFunctions::class, + 'App\Policies\AlbumQueryPolicy' => \App\Policies\AlbumQueryPolicy::class, + 'App\Policies\PhotoQueryPolicy' => \App\Policies\PhotoQueryPolicy::class, + 'Barryvdh\Debugbar\LaravelDebugbar' => \Barryvdh\Debugbar\LaravelDebugbar::class, + 'Dedoc\Scramble\Infer' => \Dedoc\Scramble\Infer::class, + 'Dedoc\Scramble\Infer\Scope\Index' => \Dedoc\Scramble\Infer\Scope\Index::class, + 'Dedoc\Scramble\Infer\Services\FileParser' => \Dedoc\Scramble\Infer\Services\FileParser::class, + 'Dedoc\Scramble\Support\ServerFactory' => \Dedoc\Scramble\Support\ServerFactory::class, + 'Helpers' => \App\Assets\Helpers::class, + 'Illuminate\Auth\Console\ClearResetsCommand' => \Illuminate\Auth\Console\ClearResetsCommand::class, + 'Illuminate\Broadcasting\BroadcastManager' => \Illuminate\Broadcasting\BroadcastManager::class, + 'Illuminate\Bus\Dispatcher' => \Illuminate\Bus\Dispatcher::class, + 'Illuminate\Cache\Console\PruneStaleTagsCommand' => \Illuminate\Cache\Console\PruneStaleTagsCommand::class, + 'Illuminate\Cache\RateLimiter' => \Illuminate\Cache\RateLimiter::class, + 'Illuminate\Console\Scheduling\ScheduleClearCacheCommand' => \Illuminate\Console\Scheduling\ScheduleClearCacheCommand::class, + 'Illuminate\Console\Scheduling\ScheduleFinishCommand' => \Illuminate\Console\Scheduling\ScheduleFinishCommand::class, + 'Illuminate\Console\Scheduling\ScheduleListCommand' => \Illuminate\Console\Scheduling\ScheduleListCommand::class, + 'Illuminate\Console\Scheduling\ScheduleRunCommand' => \Illuminate\Console\Scheduling\ScheduleRunCommand::class, + 'Illuminate\Console\Scheduling\ScheduleTestCommand' => \Illuminate\Console\Scheduling\ScheduleTestCommand::class, + 'Illuminate\Console\Scheduling\ScheduleWorkCommand' => \Illuminate\Console\Scheduling\ScheduleWorkCommand::class, + 'Illuminate\Contracts\Auth\Access\Gate' => \Illuminate\Auth\Access\Gate::class, + 'Illuminate\Contracts\Console\Kernel' => \App\Console\Kernel::class, + 'Illuminate\Contracts\Http\Kernel' => \App\Http\Kernel::class, + 'Illuminate\Contracts\Queue\EntityResolver' => \Illuminate\Database\Eloquent\QueueEntityResolver::class, + 'Illuminate\Database\Console\DbCommand' => \Illuminate\Database\Console\DbCommand::class, + 'Illuminate\Database\Console\DumpCommand' => \Illuminate\Database\Console\DumpCommand::class, + 'Illuminate\Database\Console\Migrations\InstallCommand' => \Illuminate\Database\Console\Migrations\InstallCommand::class, + 'Illuminate\Database\Console\Migrations\RefreshCommand' => \Illuminate\Database\Console\Migrations\RefreshCommand::class, + 'Illuminate\Database\Console\PruneCommand' => \Illuminate\Database\Console\PruneCommand::class, + 'Illuminate\Database\Console\ShowCommand' => \Illuminate\Database\Console\ShowCommand::class, + 'Illuminate\Database\Console\ShowModelCommand' => \Illuminate\Database\Console\ShowModelCommand::class, + 'Illuminate\Database\Console\TableCommand' => \Illuminate\Database\Console\TableCommand::class, + 'Illuminate\Database\Console\WipeCommand' => \Illuminate\Database\Console\WipeCommand::class, + 'Illuminate\Foundation\Console\ApiInstallCommand' => \Illuminate\Foundation\Console\ApiInstallCommand::class, + 'Illuminate\Foundation\Console\BroadcastingInstallCommand' => \Illuminate\Foundation\Console\BroadcastingInstallCommand::class, + 'Illuminate\Foundation\Console\ChannelListCommand' => \Illuminate\Foundation\Console\ChannelListCommand::class, + 'Illuminate\Foundation\Console\ClearCompiledCommand' => \Illuminate\Foundation\Console\ClearCompiledCommand::class, + 'Illuminate\Foundation\Console\ConfigPublishCommand' => \Illuminate\Foundation\Console\ConfigPublishCommand::class, + 'Illuminate\Foundation\Console\ConfigShowCommand' => \Illuminate\Foundation\Console\ConfigShowCommand::class, + 'Illuminate\Foundation\Console\DocsCommand' => \Illuminate\Foundation\Console\DocsCommand::class, + 'Illuminate\Foundation\Console\DownCommand' => \Illuminate\Foundation\Console\DownCommand::class, + 'Illuminate\Foundation\Console\EnvironmentCommand' => \Illuminate\Foundation\Console\EnvironmentCommand::class, + 'Illuminate\Foundation\Console\EventCacheCommand' => \Illuminate\Foundation\Console\EventCacheCommand::class, + 'Illuminate\Foundation\Console\EventGenerateCommand' => \Illuminate\Foundation\Console\EventGenerateCommand::class, + 'Illuminate\Foundation\Console\EventListCommand' => \Illuminate\Foundation\Console\EventListCommand::class, + 'Illuminate\Foundation\Console\KeyGenerateCommand' => \Illuminate\Foundation\Console\KeyGenerateCommand::class, + 'Illuminate\Foundation\Console\LangPublishCommand' => \Illuminate\Foundation\Console\LangPublishCommand::class, + 'Illuminate\Foundation\Console\OptimizeClearCommand' => \Illuminate\Foundation\Console\OptimizeClearCommand::class, + 'Illuminate\Foundation\Console\OptimizeCommand' => \Illuminate\Foundation\Console\OptimizeCommand::class, + 'Illuminate\Foundation\Console\PackageDiscoverCommand' => \Illuminate\Foundation\Console\PackageDiscoverCommand::class, + 'Illuminate\Foundation\Console\ServeCommand' => \Illuminate\Foundation\Console\ServeCommand::class, + 'Illuminate\Foundation\Console\StorageLinkCommand' => \Illuminate\Foundation\Console\StorageLinkCommand::class, + 'Illuminate\Foundation\Console\StorageUnlinkCommand' => \Illuminate\Foundation\Console\StorageUnlinkCommand::class, + 'Illuminate\Foundation\Console\StubPublishCommand' => \Illuminate\Foundation\Console\StubPublishCommand::class, + 'Illuminate\Foundation\Console\UpCommand' => \Illuminate\Foundation\Console\UpCommand::class, + 'Illuminate\Foundation\Console\ViewCacheCommand' => \Illuminate\Foundation\Console\ViewCacheCommand::class, + 'Illuminate\Foundation\Exceptions\Renderer\Listener' => \Illuminate\Foundation\Exceptions\Renderer\Listener::class, + 'Illuminate\Foundation\Mix' => \Illuminate\Foundation\Mix::class, + 'Illuminate\Foundation\PackageManifest' => \Illuminate\Foundation\PackageManifest::class, + 'Illuminate\Foundation\Vite' => \Illuminate\Foundation\Vite::class, + 'Illuminate\Queue\Console\ClearCommand' => \Illuminate\Queue\Console\ClearCommand::class, + 'Illuminate\Queue\Console\FlushFailedCommand' => \Illuminate\Queue\Console\FlushFailedCommand::class, + 'Illuminate\Queue\Console\ForgetFailedCommand' => \Illuminate\Queue\Console\ForgetFailedCommand::class, + 'Illuminate\Queue\Console\ListFailedCommand' => \Illuminate\Queue\Console\ListFailedCommand::class, + 'Illuminate\Queue\Console\ListenCommand' => \Illuminate\Queue\Console\ListenCommand::class, + 'Illuminate\Queue\Console\PruneBatchesCommand' => \Illuminate\Queue\Console\PruneBatchesCommand::class, + 'Illuminate\Queue\Console\PruneFailedJobsCommand' => \Illuminate\Queue\Console\PruneFailedJobsCommand::class, + 'Illuminate\Queue\Console\RetryBatchCommand' => \Illuminate\Queue\Console\RetryBatchCommand::class, + 'Illuminate\Queue\Console\RetryCommand' => \Illuminate\Queue\Console\RetryCommand::class, + 'Illuminate\Routing\Contracts\CallableDispatcher' => \Illuminate\Routing\CallableDispatcher::class, + 'Illuminate\Routing\Contracts\ControllerDispatcher' => \Illuminate\Routing\ControllerDispatcher::class, + 'Illuminate\Testing\ParallelTesting' => \Illuminate\Testing\ParallelTesting::class, + 'Laravel\Socialite\Contracts\Factory' => \Laravel\Socialite\SocialiteManager::class, + 'Livewire\EventBus' => \Livewire\EventBus::class, + 'Livewire\LivewireManager' => \Livewire\LivewireManager::class, + 'Livewire\Mechanisms\ExtendBlade\DeterministicBladeKeys' => \Livewire\Mechanisms\ExtendBlade\DeterministicBladeKeys::class, + 'Opcodes\LogViewer\LogTypeRegistrar' => \Opcodes\LogViewer\LogTypeRegistrar::class, + 'SocialiteProviders\Manager\Contracts\Helpers\ConfigRetrieverInterface' => \SocialiteProviders\Manager\Helpers\ConfigRetriever::class, + 'blade.compiler' => \Illuminate\View\Compilers\BladeCompiler::class, + 'clockwork.authenticator' => \Clockwork\Authentication\NullAuthenticator::class, + 'clockwork.request' => \Clockwork\Request\Request::class, + 'clockwork.storage' => \Clockwork\Storage\FileStorage::class, + 'clockwork.support' => \Clockwork\Support\Laravel\ClockworkSupport::class, + 'clockwork.xdebug' => \Clockwork\DataSource\XdebugDataSource::class, + 'command.debugbar.clear' => \Barryvdh\Debugbar\Console\ClearCommand::class, + 'command.ide-helper.eloquent' => \Barryvdh\LaravelIdeHelper\Console\EloquentCommand::class, + 'command.ide-helper.generate' => \Barryvdh\LaravelIdeHelper\Console\GeneratorCommand::class, + 'command.ide-helper.meta' => \Barryvdh\LaravelIdeHelper\Console\MetaCommand::class, + 'command.ide-helper.models' => \Barryvdh\LaravelIdeHelper\Console\ModelsCommand::class, + 'db.factory' => \Illuminate\Database\Connectors\ConnectionFactory::class, + 'db.transactions' => \Illuminate\Database\DatabaseTransactionsManager::class, + 'log-viewer' => \Opcodes\LogViewer\LogViewerService::class, + 'mail.manager' => \Illuminate\Mail\MailManager::class, + 'memcached.connector' => \Illuminate\Cache\MemcachedConnector::class, + 'migration.repository' => \Illuminate\Database\Migrations\DatabaseMigrationRepository::class, + 'queue.listener' => \Illuminate\Queue\Listener::class, + 'translation.loader' => \Illuminate\Translation\FileLoader::class, + 'view.engine.resolver' => \Illuminate\View\Engines\EngineResolver::class, +])); +override(\resolve(0), map([ + '' => '@', + 'App\Assets\Helpers' => \App\Assets\Helpers::class, + 'App\Contracts\Models\AbstractSizeVariantNamingStrategy' => \App\Assets\SizeVariantGroupedWithRandomSuffixNamingStrategy::class, + 'App\Contracts\Models\SizeVariantFactory' => \App\Image\SizeVariantDefaultFactory::class, + 'App\Factories\AlbumFactory' => \App\Factories\AlbumFactory::class, + 'App\Metadata\Versions\GitHubVersion' => \App\Metadata\Versions\GitHubVersion::class, + 'App\Metadata\Versions\Remote\GitCommits' => \App\Metadata\Versions\Remote\GitCommits::class, + 'App\Metadata\Versions\Remote\GitTags' => \App\Metadata\Versions\Remote\GitTags::class, + 'App\ModelFunctions\SymLinkFunctions' => \App\ModelFunctions\SymLinkFunctions::class, + 'App\Policies\AlbumQueryPolicy' => \App\Policies\AlbumQueryPolicy::class, + 'App\Policies\PhotoQueryPolicy' => \App\Policies\PhotoQueryPolicy::class, + 'Barryvdh\Debugbar\LaravelDebugbar' => \Barryvdh\Debugbar\LaravelDebugbar::class, + 'Dedoc\Scramble\Infer' => \Dedoc\Scramble\Infer::class, + 'Dedoc\Scramble\Infer\Scope\Index' => \Dedoc\Scramble\Infer\Scope\Index::class, + 'Dedoc\Scramble\Infer\Services\FileParser' => \Dedoc\Scramble\Infer\Services\FileParser::class, + 'Dedoc\Scramble\Support\ServerFactory' => \Dedoc\Scramble\Support\ServerFactory::class, + 'Helpers' => \App\Assets\Helpers::class, + 'Illuminate\Auth\Console\ClearResetsCommand' => \Illuminate\Auth\Console\ClearResetsCommand::class, + 'Illuminate\Broadcasting\BroadcastManager' => \Illuminate\Broadcasting\BroadcastManager::class, + 'Illuminate\Bus\Dispatcher' => \Illuminate\Bus\Dispatcher::class, + 'Illuminate\Cache\Console\PruneStaleTagsCommand' => \Illuminate\Cache\Console\PruneStaleTagsCommand::class, + 'Illuminate\Cache\RateLimiter' => \Illuminate\Cache\RateLimiter::class, + 'Illuminate\Console\Scheduling\ScheduleClearCacheCommand' => \Illuminate\Console\Scheduling\ScheduleClearCacheCommand::class, + 'Illuminate\Console\Scheduling\ScheduleFinishCommand' => \Illuminate\Console\Scheduling\ScheduleFinishCommand::class, + 'Illuminate\Console\Scheduling\ScheduleListCommand' => \Illuminate\Console\Scheduling\ScheduleListCommand::class, + 'Illuminate\Console\Scheduling\ScheduleRunCommand' => \Illuminate\Console\Scheduling\ScheduleRunCommand::class, + 'Illuminate\Console\Scheduling\ScheduleTestCommand' => \Illuminate\Console\Scheduling\ScheduleTestCommand::class, + 'Illuminate\Console\Scheduling\ScheduleWorkCommand' => \Illuminate\Console\Scheduling\ScheduleWorkCommand::class, + 'Illuminate\Contracts\Auth\Access\Gate' => \Illuminate\Auth\Access\Gate::class, + 'Illuminate\Contracts\Console\Kernel' => \App\Console\Kernel::class, + 'Illuminate\Contracts\Http\Kernel' => \App\Http\Kernel::class, + 'Illuminate\Contracts\Queue\EntityResolver' => \Illuminate\Database\Eloquent\QueueEntityResolver::class, + 'Illuminate\Database\Console\DbCommand' => \Illuminate\Database\Console\DbCommand::class, + 'Illuminate\Database\Console\DumpCommand' => \Illuminate\Database\Console\DumpCommand::class, + 'Illuminate\Database\Console\Migrations\InstallCommand' => \Illuminate\Database\Console\Migrations\InstallCommand::class, + 'Illuminate\Database\Console\Migrations\RefreshCommand' => \Illuminate\Database\Console\Migrations\RefreshCommand::class, + 'Illuminate\Database\Console\PruneCommand' => \Illuminate\Database\Console\PruneCommand::class, + 'Illuminate\Database\Console\ShowCommand' => \Illuminate\Database\Console\ShowCommand::class, + 'Illuminate\Database\Console\ShowModelCommand' => \Illuminate\Database\Console\ShowModelCommand::class, + 'Illuminate\Database\Console\TableCommand' => \Illuminate\Database\Console\TableCommand::class, + 'Illuminate\Database\Console\WipeCommand' => \Illuminate\Database\Console\WipeCommand::class, + 'Illuminate\Foundation\Console\ApiInstallCommand' => \Illuminate\Foundation\Console\ApiInstallCommand::class, + 'Illuminate\Foundation\Console\BroadcastingInstallCommand' => \Illuminate\Foundation\Console\BroadcastingInstallCommand::class, + 'Illuminate\Foundation\Console\ChannelListCommand' => \Illuminate\Foundation\Console\ChannelListCommand::class, + 'Illuminate\Foundation\Console\ClearCompiledCommand' => \Illuminate\Foundation\Console\ClearCompiledCommand::class, + 'Illuminate\Foundation\Console\ConfigPublishCommand' => \Illuminate\Foundation\Console\ConfigPublishCommand::class, + 'Illuminate\Foundation\Console\ConfigShowCommand' => \Illuminate\Foundation\Console\ConfigShowCommand::class, + 'Illuminate\Foundation\Console\DocsCommand' => \Illuminate\Foundation\Console\DocsCommand::class, + 'Illuminate\Foundation\Console\DownCommand' => \Illuminate\Foundation\Console\DownCommand::class, + 'Illuminate\Foundation\Console\EnvironmentCommand' => \Illuminate\Foundation\Console\EnvironmentCommand::class, + 'Illuminate\Foundation\Console\EventCacheCommand' => \Illuminate\Foundation\Console\EventCacheCommand::class, + 'Illuminate\Foundation\Console\EventGenerateCommand' => \Illuminate\Foundation\Console\EventGenerateCommand::class, + 'Illuminate\Foundation\Console\EventListCommand' => \Illuminate\Foundation\Console\EventListCommand::class, + 'Illuminate\Foundation\Console\KeyGenerateCommand' => \Illuminate\Foundation\Console\KeyGenerateCommand::class, + 'Illuminate\Foundation\Console\LangPublishCommand' => \Illuminate\Foundation\Console\LangPublishCommand::class, + 'Illuminate\Foundation\Console\OptimizeClearCommand' => \Illuminate\Foundation\Console\OptimizeClearCommand::class, + 'Illuminate\Foundation\Console\OptimizeCommand' => \Illuminate\Foundation\Console\OptimizeCommand::class, + 'Illuminate\Foundation\Console\PackageDiscoverCommand' => \Illuminate\Foundation\Console\PackageDiscoverCommand::class, + 'Illuminate\Foundation\Console\ServeCommand' => \Illuminate\Foundation\Console\ServeCommand::class, + 'Illuminate\Foundation\Console\StorageLinkCommand' => \Illuminate\Foundation\Console\StorageLinkCommand::class, + 'Illuminate\Foundation\Console\StorageUnlinkCommand' => \Illuminate\Foundation\Console\StorageUnlinkCommand::class, + 'Illuminate\Foundation\Console\StubPublishCommand' => \Illuminate\Foundation\Console\StubPublishCommand::class, + 'Illuminate\Foundation\Console\UpCommand' => \Illuminate\Foundation\Console\UpCommand::class, + 'Illuminate\Foundation\Console\ViewCacheCommand' => \Illuminate\Foundation\Console\ViewCacheCommand::class, + 'Illuminate\Foundation\Exceptions\Renderer\Listener' => \Illuminate\Foundation\Exceptions\Renderer\Listener::class, + 'Illuminate\Foundation\Mix' => \Illuminate\Foundation\Mix::class, + 'Illuminate\Foundation\PackageManifest' => \Illuminate\Foundation\PackageManifest::class, + 'Illuminate\Foundation\Vite' => \Illuminate\Foundation\Vite::class, + 'Illuminate\Queue\Console\ClearCommand' => \Illuminate\Queue\Console\ClearCommand::class, + 'Illuminate\Queue\Console\FlushFailedCommand' => \Illuminate\Queue\Console\FlushFailedCommand::class, + 'Illuminate\Queue\Console\ForgetFailedCommand' => \Illuminate\Queue\Console\ForgetFailedCommand::class, + 'Illuminate\Queue\Console\ListFailedCommand' => \Illuminate\Queue\Console\ListFailedCommand::class, + 'Illuminate\Queue\Console\ListenCommand' => \Illuminate\Queue\Console\ListenCommand::class, + 'Illuminate\Queue\Console\PruneBatchesCommand' => \Illuminate\Queue\Console\PruneBatchesCommand::class, + 'Illuminate\Queue\Console\PruneFailedJobsCommand' => \Illuminate\Queue\Console\PruneFailedJobsCommand::class, + 'Illuminate\Queue\Console\RetryBatchCommand' => \Illuminate\Queue\Console\RetryBatchCommand::class, + 'Illuminate\Queue\Console\RetryCommand' => \Illuminate\Queue\Console\RetryCommand::class, + 'Illuminate\Routing\Contracts\CallableDispatcher' => \Illuminate\Routing\CallableDispatcher::class, + 'Illuminate\Routing\Contracts\ControllerDispatcher' => \Illuminate\Routing\ControllerDispatcher::class, + 'Illuminate\Testing\ParallelTesting' => \Illuminate\Testing\ParallelTesting::class, + 'Laravel\Socialite\Contracts\Factory' => \Laravel\Socialite\SocialiteManager::class, + 'Livewire\EventBus' => \Livewire\EventBus::class, + 'Livewire\LivewireManager' => \Livewire\LivewireManager::class, + 'Livewire\Mechanisms\ExtendBlade\DeterministicBladeKeys' => \Livewire\Mechanisms\ExtendBlade\DeterministicBladeKeys::class, + 'Opcodes\LogViewer\LogTypeRegistrar' => \Opcodes\LogViewer\LogTypeRegistrar::class, + 'SocialiteProviders\Manager\Contracts\Helpers\ConfigRetrieverInterface' => \SocialiteProviders\Manager\Helpers\ConfigRetriever::class, + 'blade.compiler' => \Illuminate\View\Compilers\BladeCompiler::class, + 'clockwork.authenticator' => \Clockwork\Authentication\NullAuthenticator::class, + 'clockwork.request' => \Clockwork\Request\Request::class, + 'clockwork.storage' => \Clockwork\Storage\FileStorage::class, + 'clockwork.support' => \Clockwork\Support\Laravel\ClockworkSupport::class, + 'clockwork.xdebug' => \Clockwork\DataSource\XdebugDataSource::class, + 'command.debugbar.clear' => \Barryvdh\Debugbar\Console\ClearCommand::class, + 'command.ide-helper.eloquent' => \Barryvdh\LaravelIdeHelper\Console\EloquentCommand::class, + 'command.ide-helper.generate' => \Barryvdh\LaravelIdeHelper\Console\GeneratorCommand::class, + 'command.ide-helper.meta' => \Barryvdh\LaravelIdeHelper\Console\MetaCommand::class, + 'command.ide-helper.models' => \Barryvdh\LaravelIdeHelper\Console\ModelsCommand::class, + 'db.factory' => \Illuminate\Database\Connectors\ConnectionFactory::class, + 'db.transactions' => \Illuminate\Database\DatabaseTransactionsManager::class, + 'log-viewer' => \Opcodes\LogViewer\LogViewerService::class, + 'mail.manager' => \Illuminate\Mail\MailManager::class, + 'memcached.connector' => \Illuminate\Cache\MemcachedConnector::class, + 'migration.repository' => \Illuminate\Database\Migrations\DatabaseMigrationRepository::class, + 'queue.listener' => \Illuminate\Queue\Listener::class, + 'translation.loader' => \Illuminate\Translation\FileLoader::class, + 'view.engine.resolver' => \Illuminate\View\Engines\EngineResolver::class, +])); +override(\Psr\Container\ContainerInterface::get(0), map([ + '' => '@', + 'App\Assets\Helpers' => \App\Assets\Helpers::class, + 'App\Contracts\Models\AbstractSizeVariantNamingStrategy' => \App\Assets\SizeVariantGroupedWithRandomSuffixNamingStrategy::class, + 'App\Contracts\Models\SizeVariantFactory' => \App\Image\SizeVariantDefaultFactory::class, + 'App\Factories\AlbumFactory' => \App\Factories\AlbumFactory::class, + 'App\Metadata\Versions\GitHubVersion' => \App\Metadata\Versions\GitHubVersion::class, + 'App\Metadata\Versions\Remote\GitCommits' => \App\Metadata\Versions\Remote\GitCommits::class, + 'App\Metadata\Versions\Remote\GitTags' => \App\Metadata\Versions\Remote\GitTags::class, + 'App\ModelFunctions\SymLinkFunctions' => \App\ModelFunctions\SymLinkFunctions::class, + 'App\Policies\AlbumQueryPolicy' => \App\Policies\AlbumQueryPolicy::class, + 'App\Policies\PhotoQueryPolicy' => \App\Policies\PhotoQueryPolicy::class, + 'Barryvdh\Debugbar\LaravelDebugbar' => \Barryvdh\Debugbar\LaravelDebugbar::class, + 'Dedoc\Scramble\Infer' => \Dedoc\Scramble\Infer::class, + 'Dedoc\Scramble\Infer\Scope\Index' => \Dedoc\Scramble\Infer\Scope\Index::class, + 'Dedoc\Scramble\Infer\Services\FileParser' => \Dedoc\Scramble\Infer\Services\FileParser::class, + 'Dedoc\Scramble\Support\ServerFactory' => \Dedoc\Scramble\Support\ServerFactory::class, + 'Helpers' => \App\Assets\Helpers::class, + 'Illuminate\Auth\Console\ClearResetsCommand' => \Illuminate\Auth\Console\ClearResetsCommand::class, + 'Illuminate\Broadcasting\BroadcastManager' => \Illuminate\Broadcasting\BroadcastManager::class, + 'Illuminate\Bus\Dispatcher' => \Illuminate\Bus\Dispatcher::class, + 'Illuminate\Cache\Console\PruneStaleTagsCommand' => \Illuminate\Cache\Console\PruneStaleTagsCommand::class, + 'Illuminate\Cache\RateLimiter' => \Illuminate\Cache\RateLimiter::class, + 'Illuminate\Console\Scheduling\ScheduleClearCacheCommand' => \Illuminate\Console\Scheduling\ScheduleClearCacheCommand::class, + 'Illuminate\Console\Scheduling\ScheduleFinishCommand' => \Illuminate\Console\Scheduling\ScheduleFinishCommand::class, + 'Illuminate\Console\Scheduling\ScheduleListCommand' => \Illuminate\Console\Scheduling\ScheduleListCommand::class, + 'Illuminate\Console\Scheduling\ScheduleRunCommand' => \Illuminate\Console\Scheduling\ScheduleRunCommand::class, + 'Illuminate\Console\Scheduling\ScheduleTestCommand' => \Illuminate\Console\Scheduling\ScheduleTestCommand::class, + 'Illuminate\Console\Scheduling\ScheduleWorkCommand' => \Illuminate\Console\Scheduling\ScheduleWorkCommand::class, + 'Illuminate\Contracts\Auth\Access\Gate' => \Illuminate\Auth\Access\Gate::class, + 'Illuminate\Contracts\Console\Kernel' => \App\Console\Kernel::class, + 'Illuminate\Contracts\Http\Kernel' => \App\Http\Kernel::class, + 'Illuminate\Contracts\Queue\EntityResolver' => \Illuminate\Database\Eloquent\QueueEntityResolver::class, + 'Illuminate\Database\Console\DbCommand' => \Illuminate\Database\Console\DbCommand::class, + 'Illuminate\Database\Console\DumpCommand' => \Illuminate\Database\Console\DumpCommand::class, + 'Illuminate\Database\Console\Migrations\InstallCommand' => \Illuminate\Database\Console\Migrations\InstallCommand::class, + 'Illuminate\Database\Console\Migrations\RefreshCommand' => \Illuminate\Database\Console\Migrations\RefreshCommand::class, + 'Illuminate\Database\Console\PruneCommand' => \Illuminate\Database\Console\PruneCommand::class, + 'Illuminate\Database\Console\ShowCommand' => \Illuminate\Database\Console\ShowCommand::class, + 'Illuminate\Database\Console\ShowModelCommand' => \Illuminate\Database\Console\ShowModelCommand::class, + 'Illuminate\Database\Console\TableCommand' => \Illuminate\Database\Console\TableCommand::class, + 'Illuminate\Database\Console\WipeCommand' => \Illuminate\Database\Console\WipeCommand::class, + 'Illuminate\Foundation\Console\ApiInstallCommand' => \Illuminate\Foundation\Console\ApiInstallCommand::class, + 'Illuminate\Foundation\Console\BroadcastingInstallCommand' => \Illuminate\Foundation\Console\BroadcastingInstallCommand::class, + 'Illuminate\Foundation\Console\ChannelListCommand' => \Illuminate\Foundation\Console\ChannelListCommand::class, + 'Illuminate\Foundation\Console\ClearCompiledCommand' => \Illuminate\Foundation\Console\ClearCompiledCommand::class, + 'Illuminate\Foundation\Console\ConfigPublishCommand' => \Illuminate\Foundation\Console\ConfigPublishCommand::class, + 'Illuminate\Foundation\Console\ConfigShowCommand' => \Illuminate\Foundation\Console\ConfigShowCommand::class, + 'Illuminate\Foundation\Console\DocsCommand' => \Illuminate\Foundation\Console\DocsCommand::class, + 'Illuminate\Foundation\Console\DownCommand' => \Illuminate\Foundation\Console\DownCommand::class, + 'Illuminate\Foundation\Console\EnvironmentCommand' => \Illuminate\Foundation\Console\EnvironmentCommand::class, + 'Illuminate\Foundation\Console\EventCacheCommand' => \Illuminate\Foundation\Console\EventCacheCommand::class, + 'Illuminate\Foundation\Console\EventGenerateCommand' => \Illuminate\Foundation\Console\EventGenerateCommand::class, + 'Illuminate\Foundation\Console\EventListCommand' => \Illuminate\Foundation\Console\EventListCommand::class, + 'Illuminate\Foundation\Console\KeyGenerateCommand' => \Illuminate\Foundation\Console\KeyGenerateCommand::class, + 'Illuminate\Foundation\Console\LangPublishCommand' => \Illuminate\Foundation\Console\LangPublishCommand::class, + 'Illuminate\Foundation\Console\OptimizeClearCommand' => \Illuminate\Foundation\Console\OptimizeClearCommand::class, + 'Illuminate\Foundation\Console\OptimizeCommand' => \Illuminate\Foundation\Console\OptimizeCommand::class, + 'Illuminate\Foundation\Console\PackageDiscoverCommand' => \Illuminate\Foundation\Console\PackageDiscoverCommand::class, + 'Illuminate\Foundation\Console\ServeCommand' => \Illuminate\Foundation\Console\ServeCommand::class, + 'Illuminate\Foundation\Console\StorageLinkCommand' => \Illuminate\Foundation\Console\StorageLinkCommand::class, + 'Illuminate\Foundation\Console\StorageUnlinkCommand' => \Illuminate\Foundation\Console\StorageUnlinkCommand::class, + 'Illuminate\Foundation\Console\StubPublishCommand' => \Illuminate\Foundation\Console\StubPublishCommand::class, + 'Illuminate\Foundation\Console\UpCommand' => \Illuminate\Foundation\Console\UpCommand::class, + 'Illuminate\Foundation\Console\ViewCacheCommand' => \Illuminate\Foundation\Console\ViewCacheCommand::class, + 'Illuminate\Foundation\Exceptions\Renderer\Listener' => \Illuminate\Foundation\Exceptions\Renderer\Listener::class, + 'Illuminate\Foundation\Mix' => \Illuminate\Foundation\Mix::class, + 'Illuminate\Foundation\PackageManifest' => \Illuminate\Foundation\PackageManifest::class, + 'Illuminate\Foundation\Vite' => \Illuminate\Foundation\Vite::class, + 'Illuminate\Queue\Console\ClearCommand' => \Illuminate\Queue\Console\ClearCommand::class, + 'Illuminate\Queue\Console\FlushFailedCommand' => \Illuminate\Queue\Console\FlushFailedCommand::class, + 'Illuminate\Queue\Console\ForgetFailedCommand' => \Illuminate\Queue\Console\ForgetFailedCommand::class, + 'Illuminate\Queue\Console\ListFailedCommand' => \Illuminate\Queue\Console\ListFailedCommand::class, + 'Illuminate\Queue\Console\ListenCommand' => \Illuminate\Queue\Console\ListenCommand::class, + 'Illuminate\Queue\Console\PruneBatchesCommand' => \Illuminate\Queue\Console\PruneBatchesCommand::class, + 'Illuminate\Queue\Console\PruneFailedJobsCommand' => \Illuminate\Queue\Console\PruneFailedJobsCommand::class, + 'Illuminate\Queue\Console\RetryBatchCommand' => \Illuminate\Queue\Console\RetryBatchCommand::class, + 'Illuminate\Queue\Console\RetryCommand' => \Illuminate\Queue\Console\RetryCommand::class, + 'Illuminate\Routing\Contracts\CallableDispatcher' => \Illuminate\Routing\CallableDispatcher::class, + 'Illuminate\Routing\Contracts\ControllerDispatcher' => \Illuminate\Routing\ControllerDispatcher::class, + 'Illuminate\Testing\ParallelTesting' => \Illuminate\Testing\ParallelTesting::class, + 'Laravel\Socialite\Contracts\Factory' => \Laravel\Socialite\SocialiteManager::class, + 'Livewire\EventBus' => \Livewire\EventBus::class, + 'Livewire\LivewireManager' => \Livewire\LivewireManager::class, + 'Livewire\Mechanisms\ExtendBlade\DeterministicBladeKeys' => \Livewire\Mechanisms\ExtendBlade\DeterministicBladeKeys::class, + 'Opcodes\LogViewer\LogTypeRegistrar' => \Opcodes\LogViewer\LogTypeRegistrar::class, + 'SocialiteProviders\Manager\Contracts\Helpers\ConfigRetrieverInterface' => \SocialiteProviders\Manager\Helpers\ConfigRetriever::class, + 'blade.compiler' => \Illuminate\View\Compilers\BladeCompiler::class, + 'clockwork.authenticator' => \Clockwork\Authentication\NullAuthenticator::class, + 'clockwork.request' => \Clockwork\Request\Request::class, + 'clockwork.storage' => \Clockwork\Storage\FileStorage::class, + 'clockwork.support' => \Clockwork\Support\Laravel\ClockworkSupport::class, + 'clockwork.xdebug' => \Clockwork\DataSource\XdebugDataSource::class, + 'command.debugbar.clear' => \Barryvdh\Debugbar\Console\ClearCommand::class, + 'command.ide-helper.eloquent' => \Barryvdh\LaravelIdeHelper\Console\EloquentCommand::class, + 'command.ide-helper.generate' => \Barryvdh\LaravelIdeHelper\Console\GeneratorCommand::class, + 'command.ide-helper.meta' => \Barryvdh\LaravelIdeHelper\Console\MetaCommand::class, + 'command.ide-helper.models' => \Barryvdh\LaravelIdeHelper\Console\ModelsCommand::class, + 'db.factory' => \Illuminate\Database\Connectors\ConnectionFactory::class, + 'db.transactions' => \Illuminate\Database\DatabaseTransactionsManager::class, + 'log-viewer' => \Opcodes\LogViewer\LogViewerService::class, + 'mail.manager' => \Illuminate\Mail\MailManager::class, + 'memcached.connector' => \Illuminate\Cache\MemcachedConnector::class, + 'migration.repository' => \Illuminate\Database\Migrations\DatabaseMigrationRepository::class, + 'queue.listener' => \Illuminate\Queue\Listener::class, + 'translation.loader' => \Illuminate\Translation\FileLoader::class, + 'view.engine.resolver' => \Illuminate\View\Engines\EngineResolver::class, +])); - override(\Illuminate\Support\Arr::add(0), type(0)); - override(\Illuminate\Support\Arr::except(0), type(0)); - override(\Illuminate\Support\Arr::first(0), elementType(0)); - override(\Illuminate\Support\Arr::last(0), elementType(0)); - override(\Illuminate\Support\Arr::get(0), elementType(0)); - override(\Illuminate\Support\Arr::only(0), type(0)); - override(\Illuminate\Support\Arr::prepend(0), type(0)); - override(\Illuminate\Support\Arr::pull(0), elementType(0)); - override(\Illuminate\Support\Arr::set(0), type(0)); - override(\Illuminate\Support\Arr::shuffle(0), type(0)); - override(\Illuminate\Support\Arr::sort(0), type(0)); - override(\Illuminate\Support\Arr::sortRecursive(0), type(0)); - override(\Illuminate\Support\Arr::where(0), type(0)); - override(\array_add(0), type(0)); - override(\array_except(0), type(0)); - override(\array_first(0), elementType(0)); - override(\array_last(0), elementType(0)); - override(\array_get(0), elementType(0)); - override(\array_only(0), type(0)); - override(\array_prepend(0), type(0)); - override(\array_pull(0), elementType(0)); - override(\array_set(0), type(0)); - override(\array_sort(0), type(0)); - override(\array_sort_recursive(0), type(0)); - override(\array_where(0), type(0)); - override(\head(0), elementType(0)); - override(\last(0), elementType(0)); - override(\with(0), type(0)); - override(\tap(0), type(0)); - override(\optional(0), type(0)); +override(\Illuminate\Foundation\Testing\Concerns\InteractsWithContainer::mock(0), map(['' => "@&\Mockery\MockInterface"])); +override(\Illuminate\Foundation\Testing\Concerns\InteractsWithContainer::partialMock(0), map(['' => "@&\Mockery\MockInterface"])); +override(\Illuminate\Foundation\Testing\Concerns\InteractsWithContainer::instance(0), type(1)); +override(\Illuminate\Foundation\Testing\Concerns\InteractsWithContainer::spy(0), map(['' => "@&\Mockery\MockInterface"])); +override(\Illuminate\Support\Arr::add(0), type(0)); +override(\Illuminate\Support\Arr::except(0), type(0)); +override(\Illuminate\Support\Arr::first(0), elementType(0)); +override(\Illuminate\Support\Arr::last(0), elementType(0)); +override(\Illuminate\Support\Arr::get(0), elementType(0)); +override(\Illuminate\Support\Arr::only(0), type(0)); +override(\Illuminate\Support\Arr::prepend(0), type(0)); +override(\Illuminate\Support\Arr::pull(0), elementType(0)); +override(\Illuminate\Support\Arr::set(0), type(0)); +override(\Illuminate\Support\Arr::shuffle(0), type(0)); +override(\Illuminate\Support\Arr::sort(0), type(0)); +override(\Illuminate\Support\Arr::sortRecursive(0), type(0)); +override(\Illuminate\Support\Arr::where(0), type(0)); +override(\array_add(0), type(0)); +override(\array_except(0), type(0)); +override(\array_first(0), elementType(0)); +override(\array_last(0), elementType(0)); +override(\array_get(0), elementType(0)); +override(\array_only(0), type(0)); +override(\array_prepend(0), type(0)); +override(\array_pull(0), elementType(0)); +override(\array_set(0), type(0)); +override(\array_sort(0), type(0)); +override(\array_sort_recursive(0), type(0)); +override(\array_where(0), type(0)); +override(\head(0), elementType(0)); +override(\last(0), elementType(0)); +override(\with(0), type(0)); +override(\tap(0), type(0)); +override(\optional(0), type(0)); diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 00000000000..8760090e21e --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,3 @@ +{ + "printWidth": 150 +} diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 169221935d8..00000000000 --- a/.travis.yml +++ /dev/null @@ -1,78 +0,0 @@ -os: - - linux - -dist: bionic - -language: php - -git: - depth: 3 - -php: - - '7.4' - -env: - - SQL=mariadb EXIFTOOL=yes - - SQL=mariadb EXIFTOOL=no - -jobs: - include: - - env: SQL=mariadb EXIFTOOL=yes - if: branch = master - - env: SQL=mariadb EXIFTOOL=no - if: branch = master - - env: SQL=postgresql EXIFTOOL=yes - if: branch = master - - env: SQL=postgresql EXIFTOOL=no - if: branch = master - - env: SQL=sqlite EXIFTOOL=yes - if: branch = master - - env: SQL=sqlite EXIFTOOL=no - if: branch = master - -cache: - directories: - - $HOME/.composer/cache/files - - $HOME/.php-cs-fixer - - $HOME/.local -services: - - postgresql - -addons: - mariadb: '10.3' - apt: - packages: - - php-pecl-http - - php-imagick - - php-mbstring - - php-json - - php-gd - update: true - -before_script: - - printf "\n" | pecl install imagick - # Install ExifTool - - sh -c "if [ '$EXIFTOOL' = 'yes' ] ; then sudo apt-get -y install libimage-exiftool-perl ; fi" - # create db for mariadb - - sh -c "if [ '$SQL' = 'mariadb' ] ; then mysql -e 'create database homestead_test;' ; fi" - # create db for postgresql - - sh -c "if [ '$SQL' = 'postgresql' ] ; then psql -c 'create database homestead_test;' -U postgres ; fi" - # create db for SQLite - - sh -c "if [ '$SQL' = 'sqlite' ] ; then touch database/database.sqlite ; fi" - # composer stuff - - composer self-update - -script: - - bash test_dev.sh - - bash test_dist.sh - -after_success: - - bash codecov.sh - -notifications: - webhooks: - urls: - - https://webhooks.gitter.im/e/c96e902d9f1fe0faeacc - on_success: always # options: [always|never|change] default: always - on_failure: always # options: [always|never|change] default: always - on_start: never # options: [always|never|change] default: always diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000000..bc254eba42c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1 @@ +[See on the LycheeOrg website: https://lycheeorg.dev/docs/contributions.html](https://lycheeorg.dev/docs/contributions.html) diff --git a/LICENSE b/LICENSE index 343145333d5..5662725de28 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ The MIT License (MIT) Copyright (c) 2017-2018 Tobias Reich -Copyright (c) 2018-2020 LycheeOrg +Copyright (c) 2018-2024 LycheeOrg Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Makefile b/Makefile new file mode 100644 index 00000000000..3b7ffa98190 --- /dev/null +++ b/Makefile @@ -0,0 +1,154 @@ +.PHONY: dist-gen dist-clean dist clean test formatting phpstan + +composer: + rm -r vendor 2> /dev/null || true + composer install --prefer-dist --no-dev + php artisan vendor:publish --tag=log-viewer-asset + +npm-build: + rm -r public/build 2> /dev/null || true + rm -r node_modules 2> /dev/null || true + npm ci + npm run build + +dist-gen: clean composer npm-build + @echo "packaging..." + @mkdir Lychee + @mkdir Lychee/public + @mkdir Lychee/public/dist + @mkdir Lychee/public/img + @mkdir Lychee/public/uploads + @mkdir Lychee/public/uploads/import + @mkdir Lychee/public/sym + @cp -r app Lychee + @cp -r bootstrap Lychee + @cp -r config Lychee + @cp -r composer-cache Lychee + @cp -r database Lychee + @cp -r public/build Lychee/public + @cp -r public/dist Lychee/public + @cp -r public/vendor Lychee/public + @cp -r public/installer Lychee/public + @cp -r public/img/* Lychee/public/img + @cp -r public/.htaccess Lychee/public + @cp -r public/.user.ini Lychee/public + @cp -r public/favicon.ico Lychee/public + @cp -r public/index.php Lychee/public + @cp -r public/robots.txt Lychee/public + @cp -r public/web.config Lychee/public + @cp -r lang Lychee + @cp -r resources Lychee + @cp -r routes Lychee + @cp -r scripts Lychee + @cp -r storage Lychee + @cp -r vendor Lychee 2> /dev/null || true + @cp -r .env.example Lychee + @cp -r artisan Lychee + @cp -r composer.json Lychee + @cp -r composer.lock Lychee + @cp -r index.php Lychee + @cp -r LICENSE Lychee + @cp -r README.md Lychee + @cp -r simple_error_template.html Lychee + @cp -r version.md Lychee + @touch Lychee/storage/logs/laravel.log + @touch Lychee/public/dist/user.css + @touch Lychee/public/dist/custom.js + @touch Lychee/public/uploads/import/index.html + @touch Lychee/public/sym/index.html + +dist-clean: dist-gen + find Lychee -wholename '*/[Tt]ests/*' -delete + find Lychee -wholename '*/[Tt]est/*' -delete + @rm -r Lychee/storage/framework/cache/data/* 2> /dev/null || true + @rm Lychee/storage/framework/sessions/* 2> /dev/null || true + @rm Lychee/storage/framework/views/* 2> /dev/null || true + @rm Lychee/storage/logs/* 2> /dev/null || true + +dist: dist-clean + @zip -r Lychee.zip Lychee + +clean: + @rm build/* 2> /dev/null || true + @rm -r Lychee 2> /dev/null || true + @rm -r public/build 2> /dev/null || true + @rm -r node_modules 2> /dev/null || true + @rm -r vendor 2> /dev/null || true + +install: composer npm-build + php artisan migrate + +test: + @if [ -x "vendor/bin/phpunit" ]; then \ + ./vendor/bin/phpunit --stop-on-failure; \ + else \ + echo ""; \ + echo "Please install phpunit:"; \ + echo ""; \ + echo " composer install"; \ + echo ""; \ + fi + +formatting: + @rm .php_cs.cache 2> /dev/null || true + @if [ -x "vendor/bin/php-cs-fixer" ]; then \ + PHP_CS_FIXER_IGNORE_ENV=1 ./vendor/bin/php-cs-fixer fix -v --config=.php-cs-fixer.php; \ + else \ + echo ""; \ + echo "Please install php-cs-fixer:"; \ + echo ""; \ + echo " composer install"; \ + echo ""; \ + fi + npm run format + +phpstan: + vendor/bin/phpstan analyze + +# Generating new versions +gen_minor: + php scripts/gen_release.php + git add database + git add version.md + +release_minor: gen_minor + git commit -S -m "bump to version $(shell cat version.md)" + +gen_major: + php scripts/gen_release.php major + git add database + git add version.md + +release_major: gen_major + git commit -m "bump to version $(shell cat version.md)" + +# Building tests 1 by 1 + +TESTS_PHP := $(shell find tests/Feature_v1 -name "*Test.php" -printf "%f\n") +TEST_DONE := $(addprefix build/,$(TESTS_PHP:.php=.done)) + +build: + mkdir build + +build/Base%.done: + touch build/Base$*.done + +build/%UnitTest.done: + touch build/$*UnitTest.done + +build/%.done: tests/Feature_v1/%.php build + vendor/bin/phpunit --no-coverage --filter $* && touch build/$*.done + +all_tests: $(TEST_DONE) + +test_unit: + vendor/bin/phpunit --testsuite Unit --stop-on-failure --stop-on-error --no-coverage --log-junit report_unit.xml + +test_v1: + vendor/bin/phpunit --testsuite Feature_v1 --stop-on-failure --stop-on-error --no-coverage --log-junit report_v1.xml + +test_v2: + vendor/bin/phpunit --testsuite Feature_v2 --stop-on-failure --stop-on-error --no-coverage --log-junit report_v2.xml + +gen_typescript_types: + php artisan typescript:transform \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000000..41c867f08ec --- /dev/null +++ b/README.md @@ -0,0 +1,139 @@ +

@LycheeOrg

+ +[![GitHub Release][release-shield]](https://github.com/LycheeOrg/Lychee/releases) +[![PHP 8.3 & 8.4][php-shield]](https://lycheeorg.dev/docs/#server-requirements) +[![MIT License][license-shield]](https://github.com/LycheeOrg/Lychee/blob/master/LICENSE) +[![Downloads][download-shield]](https://github.com/LycheeOrg/Lychee/releases) +
+[![Build Status][build-status-shield]](https://github.com/LycheeOrg/Lychee/actions) +[![Code Coverage][codecov-shield]](https://codecov.io/gh/LycheeOrg/Lychee) +[![CII Best Practices Summary][cii-shield]](https://bestpractices.coreinfrastructure.org/projects/2855) +[![OpenSSF Scorecard][ossf-shield]](https://securityscorecards.dev/viewer/?uri=github.com/LycheeOrg/Lychee) +[![Quality Gate Status][sonar-shield]](https://sonarcloud.io/dashboard?id=LycheeOrg_Lychee-Laravel) +
+[![Website][website-shield]](https://lycheeorg.dev) +[![Documentation][docs-shield]](https://lycheeorg./docs/) +[![Changelog][changelog-shield]](https://lycheeorg.dev/docs/releases.html) +[![Docker repository][docker-shield]](https://github.com/LycheeOrg/Lychee-Docker) +[![Gitter][gitter-shield]](https://gitter.im/LycheeOrg/Lobby) +[![Discord][discord-shield]][discord] + +# Lychee: A Stunning and User-Friendly Photo Management System + +Lychee is a free, open-source photo-management tool that runs on your server or web space. Installation is quick and easy, taking just seconds. With Lychee, you can upload, manage, and share your photos seamlessly, just like using a native application. It comes with all the essential features you need, ensuring your photos are stored securely. Read more on our [website](https://LycheeOrg.dev). + +For even more advanced features, consider the Supporter Edition (SE). The SE version offers additional functionality to enhance your experience. Learn more about the Supporter Edition and its benefits [here](https://lycheeorg.dev/get-supporter-edition). + +## Support the Team + +We aim to maintain a free open-source photography library with high quality of code.
+Being in control of our own data, our own pictures is something that we value above all. + +Through [contributions, donations, and sponsorship](https://github.com/sponsors/LycheeOrg), you allow Lychee to thrive. Your donations directly support demo server costs, continuous enhancements, and most importantly bug fixes! + +## Installation + +There are three deployment options available. The simplest is **Docker deployment**, as all dependencies are already predefined and configured. + +### Docker deployment + +An official Docker image can be found at [LycheeOrg/Lychee-Docker](https://github.com/LycheeOrg/Lychee-Docker) or on Docker Hub as [lycheeorg/lychee](https://hub.docker.com/r/lycheeorg/lychee). + +### File-based deployment + +Copy the extracted Zip file from https://github.com/LycheeOrg/Lychee/releases to your webserver + +### Build from Source deployment + +To run Lychee, everything you need is a web-server with PHP 8.3 or later and a database (MySQL/MariaDB, PostgreSQL or SQLite). Follow the instructions to install Lychee on your server. This version of Lychee is built on the Laravel framework. To install: + +1. Clone this repo to your server and set the web root to `lychee/public` +2. Run `composer install --no-dev` to install dependencies +3. Run `npm install` to install node dependencies +4. Run `npm run build` to build the front-end +5. Copy `.env.example` as `.env` and edit it to match your parameters +6. Generate your secret key with `php artisan key:generate` +7. Migrate your database with `php artisan migrate` to create a new database or migrate an existing Lychee installation to the latest framework. + +See detailed instructions on the [Installation](https://lycheeorg.dev/docs/installation.html) page of our documentation. + +### Update + +Updating is as easy as it should be. [Update »](https://lycheeorg.dev/docs/update.html) + +## Configuration + +### Settings + +Sign in and click the gear in the top left corner to change your settings. [Settings »][1] + +### Advanced Features + +Lychee is ready to use straight after installation, but some features require a little more configuration. + +## Documentation + +### Keyboard Shortcuts + +These shortcuts will help you to use Lychee even faster. [Keyboard Shortcuts »](https://lycheeorg.dev/docs/keyboard.html) + +### Dropbox import + +In order to use the Dropbox import from your server, you need a valid drop-ins app key from [their website](https://www.dropbox.com/developers/saver). Lychee will ask you for this key, the first time you try to use the import. Want to change your code? Take a look at [the settings][1] of Lychee. + +### Twitter Cards + +Lychee supports [Twitter Cards](https://developer.twitter.com/en/docs/twitter-for-websites/cards/overview/abouts-cards) and [Open Graph](http://opengraphprotocol.org) for shared images (not albums). In order to use Twitter Cards you need to request an approval for your domain. Simply share an image with Lychee, copy its link and paste it in [Twitter's Card Validator](https://cards-dev.twitter.com/validator). + +### ImageMagick + +Lychee uses [ImageMagick](https://www.imagemagick.org) when installed on your server. In this case you will benefit from a faster processing of your uploads, better looking thumbnails and intermediate sized images for small screen devices. You can disable the usage of [ImageMagick](https://www.imagemagick.org) in the [settings][1]. + +### New Photos Email Notification + +In order to use the new photos email notification you will need to have configured the **MAIL_** variables in your .env to your mail provider & [setup cron](https://laravel.com/docs/scheduling#running-the-scheduler). Once that is complete you then toggle **Send new photos notification emails** in the [settings][1]. Your users will be able to opt-in to the email notifications by entering their email address in the **Notifications** setting in the sidebar. Photo notifications will be grouped and sent out once a week to the site admin, album owner & anyone who the album is shared with, if their email has been added. The admin or user who added the photo to an album, will not receive a email notification for the photos they added. + +## Troubleshooting + +Take a look at the [Documentation](https://lycheeorg.dev/docs/), particularly the [FAQ](https://lycheeorg.dev/docs/faq_troubleshooting.html) if you have problems. Discovered a bug? Please create an issue [here](https://github.com/LycheeOrg/Lychee/issues) on GitHub! You can also contact us directly on [gitter (login with your github account)](https://gitter.im/LycheeOrg/Lobby) or on [discord »][discord]. + +## Notice: `master` & `alpha` branches + +As LycheeOrg is a very small team, we do not have many maintainers. Most of us have an active work/family life, and as a result, it is no longer possible for us to apply proper 4-eyes principle in the coding reviews. + +In order to keep our high code quality, the following changes have been applied. + +- `master` stays as a stable branch and contains 4-eyes peer-reviewed pull-requests. +- `alpha` contains the latest changes (i.e. the above mentionned PR) merged with minimal review. + +With this change, we hope to strike a balance between decently paced development (on `alpha`) and maintaining a robust core (on `master`). + +On Docker, `nightly`/`dev` continues to refer to the latest `master` commit. +The `alpha` tag is updated daily with the content of the associated branch. + +That being said, if you like the gallery and would like to contribute, do not hesitate to open pull request. If you would like to see more functionalities added and help us push Lychee, [Join the team!](https://lycheeorg.dev/docs/contributions.html#joining-the-team) + +## Open Source Community Support + +PhpStorm + +We would like to thank Jetbrains for supporting us with their [Open Source Development - Community Support][jetbrains-opensource] program. + +[1]: https://lycheeorg.dev/docs/settings.html +[build-status-shield]: https://img.shields.io/github/actions/workflow/status/LycheeOrg/Lychee/CICD.yml?branch=master +[codecov-shield]: https://codecov.io/gh/LycheeOrg/Lychee/branch/master/graph/badge.svg +[release-shield]: https://img.shields.io/github/release/LycheeOrg/Lychee.svg +[php-shield]: https://img.shields.io/badge/PHP-8.e%20|%208.4-blue +[license-shield]: https://img.shields.io/github/license/LycheeOrg/Lychee.svg +[cii-shield]: https://img.shields.io/cii/summary/2855.svg +[ossf-shield]: https://api.securityscorecards.dev/projects/github.com/LycheeOrg/Lychee/badge +[sonar-shield]: https://sonarcloud.io/api/project_badges/measure?project=LycheeOrg_Lychee-Laravel&metric=alert_status +[website-shield]: https://img.shields.io/badge/-Website-informational.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAASCAYAAACuLnWgAAABfWlDQ1BpY2MAACiRfZE9SMNAHMVfU6VaKg52EHEIWJ0siIqIk1ahCBVCrdCqg8mlX9CkIWlxcRRcCw5+LFYdXJx1dXAVBMEPECdHJ0UXKfF/SaFFjAfH/Xh373H3DhDqJaZZHWOAplfMZDwmpjOrYuAVQQTQjSHMyMwy5iQpAc/xdQ8fX++iPMv73J+jR81aDPCJxLPMMCvEG8RTmxWD8z5xmBVklficeNSkCxI/cl1x+Y1z3mGBZ4bNVHKeOEws5ttYaWNWMDXiSeKIqumUL6RdVjlvcdZKVda8J39hKKuvLHOd5iDiWMQSJIhQUEURJVQQpVUnxUKS9mMe/gHHL5FLIVcRjBwLKEOD7PjB/+B3t1ZuYtxNCsWAzhfb/hgGArtAo2bb38e23TgB/M/Ald7yl+vA9CfptZYWOQJ6t4GL65am7AGXO0D/kyGbsiP5aQq5HPB+Rt+UAfpugeCa21tzH6cPQIq6StwAB4fASJ6y1z3e3dXe279nmv39AJMecrRgM3JmAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAC4jAAAuIwF4pT92AAAEPUlEQVQ4y6XU34tUZRgH8O/748yZOWfn7OyuOzuzaquuu/4kUTS1i0S8CFLQCCGEuki680+IroS6CQKjC70JRIXooiDCQEkKRUyxxFCydV1Xx5md2fl1fr/ved+3CyEQLDf83j98eHh4vgRLjDSGy7mZirxxjTNKQCenMza5vm67bvaiWb4UwBhDur/8/LZuLBynnb6joMHo/Uj2uh8ZY74hhJiXRlpXL7+V3rn7hZlrlpkmoAUC6oeg1eBEcPumD+D8SyPNG9d30FqzbPkaJAOsxAJJBYwSY9LO73sRQpeCtFttBO0+0n4A6ccQ3RiyFSJt9SDi6KBfr1WXjEShGAyCZFym5pkNS1u3wI8CxH6EzBeQrQiiFyFthkh7kRZa/+dNCADouqGnb378ZkfeOVYo2NOAfWZicPeZNRfFel7vbFMr83vv/np1D5lPUC4MI6c4mMeReAbOG9uulycrP4gwImxw5Jq3acePzrIR+QxiMlP6+sInxy7fO32sKWbHuK0wlK+qve2DC9O/ea4D4+VKFvomwszMAwQLbZQsF8o26OZSrNo1hfFqGUYIaLu4KFeXvw03Jt9Bl37aPnkkAACaJWagHy68V28/GKPKRp67qPaHWfmKqNJHvmcCAdMRGDQFbN48DWuFh/OP7uCPsIGRMRdOEiJrL0AuLiKceTjSuXn96J/3vjo30zp76uLvn40BALUG6CNOimdLhSq4JVEd8rD2yUrYTQMVC4huAtFLIdspaM9g8+QUXtv5KrZuWItRzwOnFqTS8IMYfitANBNAzKduI7j6bpDc/7zZ6HgcAHauP3wy5bW99ezCHscMgzxmSJUEIRTQgEk0aIHAEhQcNqZWjCMOulAA0iQGEgL/SQqSMWQOkIkMQZwg5Xq5MWmOA0BAZx1SaC1XPY4oU4gtCaEzQFIQRmEBIJYCcQkyxyCJJQQkuM7BhBpZoqA1AWEGwgX8vB2Mu/vvVQa3f1quVFocAMKoua/ndypEOQDyeOx0USmOggcEqVIAKLjRSOwMERN4GC4i60SYcIrghkFJQOUZYgawdVN4fdPhk8WV48c3VHb1gKNPPz4I/HO7Jz9sdKIn28K4X4o2qg/mag8GqorCQw4wEjLUoIKg2ewgLo7BuBR/zc1jwGLQFpBwjfya1ZjeuhvV4aktXOY0IUT/8yfPVMj84/eDXvfUrUuXcvVLN5Cv+3AZAy9Q9EyCeMjGxkOHML5uHbqNBhLfB2EEec+DNzKKvOtqyugtxvj+yqqJ2nOR2uzsAQJywmj9StTzqd9oIu33YbQCt20MVstwly3zmWU1CHBFa1MDQdFo7WutBeV8kXH+fc6254bKo+q5SP3hHDPKrCKEHKGUvQOjR562vQEhhAK4bYAvLdu+VigOtPOOI+Mg5ACUUxzQ/1orz0uv3WFKiHKWprbRT2cJYyCUdkdXLO/if+Rvf2QoDtYrAMIAAAAASUVORK5CYII= +[docs-shield]: https://img.shields.io/badge/-Documentation-informational.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAASCAYAAACuLnWgAAABfWlDQ1BpY2MAACiRfZE9SMNAHMVfU6VaKg52EHEIWJ0siIqIk1ahCBVCrdCqg8mlX9CkIWlxcRRcCw5+LFYdXJx1dXAVBMEPECdHJ0UXKfF/SaFFjAfH/Xh373H3DhDqJaZZHWOAplfMZDwmpjOrYuAVQQTQjSHMyMwy5iQpAc/xdQ8fX++iPMv73J+jR81aDPCJxLPMMCvEG8RTmxWD8z5xmBVklficeNSkCxI/cl1x+Y1z3mGBZ4bNVHKeOEws5ttYaWNWMDXiSeKIqumUL6RdVjlvcdZKVda8J39hKKuvLHOd5iDiWMQSJIhQUEURJVQQpVUnxUKS9mMe/gHHL5FLIVcRjBwLKEOD7PjB/+B3t1ZuYtxNCsWAzhfb/hgGArtAo2bb38e23TgB/M/Ald7yl+vA9CfptZYWOQJ6t4GL65am7AGXO0D/kyGbsiP5aQq5HPB+Rt+UAfpugeCa21tzH6cPQIq6StwAB4fASJ6y1z3e3dXe279nmv39AJMecrRgM3JmAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAC4jAAAuIwF4pT92AAAEPUlEQVQ4y6XU34tUZRgH8O/748yZOWfn7OyuOzuzaquuu/4kUTS1i0S8CFLQCCGEuki680+IroS6CQKjC70JRIXooiDCQEkKRUyxxFCydV1Xx5md2fl1fr/ved+3CyEQLDf83j98eHh4vgRLjDSGy7mZirxxjTNKQCenMza5vm67bvaiWb4UwBhDur/8/LZuLBynnb6joMHo/Uj2uh8ZY74hhJiXRlpXL7+V3rn7hZlrlpkmoAUC6oeg1eBEcPumD+D8SyPNG9d30FqzbPkaJAOsxAJJBYwSY9LO73sRQpeCtFttBO0+0n4A6ccQ3RiyFSJt9SDi6KBfr1WXjEShGAyCZFym5pkNS1u3wI8CxH6EzBeQrQiiFyFthkh7kRZa/+dNCADouqGnb378ZkfeOVYo2NOAfWZicPeZNRfFel7vbFMr83vv/np1D5lPUC4MI6c4mMeReAbOG9uulycrP4gwImxw5Jq3acePzrIR+QxiMlP6+sInxy7fO32sKWbHuK0wlK+qve2DC9O/ea4D4+VKFvomwszMAwQLbZQsF8o26OZSrNo1hfFqGUYIaLu4KFeXvw03Jt9Bl37aPnkkAACaJWagHy68V28/GKPKRp67qPaHWfmKqNJHvmcCAdMRGDQFbN48DWuFh/OP7uCPsIGRMRdOEiJrL0AuLiKceTjSuXn96J/3vjo30zp76uLvn40BALUG6CNOimdLhSq4JVEd8rD2yUrYTQMVC4huAtFLIdspaM9g8+QUXtv5KrZuWItRzwOnFqTS8IMYfitANBNAzKduI7j6bpDc/7zZ6HgcAHauP3wy5bW99ezCHscMgzxmSJUEIRTQgEk0aIHAEhQcNqZWjCMOulAA0iQGEgL/SQqSMWQOkIkMQZwg5Xq5MWmOA0BAZx1SaC1XPY4oU4gtCaEzQFIQRmEBIJYCcQkyxyCJJQQkuM7BhBpZoqA1AWEGwgX8vB2Mu/vvVQa3f1quVFocAMKoua/ndypEOQDyeOx0USmOggcEqVIAKLjRSOwMERN4GC4i60SYcIrghkFJQOUZYgawdVN4fdPhk8WV48c3VHb1gKNPPz4I/HO7Jz9sdKIn28K4X4o2qg/mag8GqorCQw4wEjLUoIKg2ewgLo7BuBR/zc1jwGLQFpBwjfya1ZjeuhvV4aktXOY0IUT/8yfPVMj84/eDXvfUrUuXcvVLN5Cv+3AZAy9Q9EyCeMjGxkOHML5uHbqNBhLfB2EEec+DNzKKvOtqyugtxvj+yqqJ2nOR2uzsAQJywmj9StTzqd9oIu33YbQCt20MVstwly3zmWU1CHBFa1MDQdFo7WutBeV8kXH+fc6254bKo+q5SP3hHDPKrCKEHKGUvQOjR562vQEhhAK4bYAvLdu+VigOtPOOI+Mg5ACUUxzQ/1orz0uv3WFKiHKWprbRT2cJYyCUdkdXLO/if+Rvf2QoDtYrAMIAAAAASUVORK5CYII= +[changelog-shield]: https://img.shields.io/badge/-Changelog-informational.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAASCAYAAACuLnWgAAABfWlDQ1BpY2MAACiRfZE9SMNAHMVfU6VaKg52EHEIWJ0siIqIk1ahCBVCrdCqg8mlX9CkIWlxcRRcCw5+LFYdXJx1dXAVBMEPECdHJ0UXKfF/SaFFjAfH/Xh373H3DhDqJaZZHWOAplfMZDwmpjOrYuAVQQTQjSHMyMwy5iQpAc/xdQ8fX++iPMv73J+jR81aDPCJxLPMMCvEG8RTmxWD8z5xmBVklficeNSkCxI/cl1x+Y1z3mGBZ4bNVHKeOEws5ttYaWNWMDXiSeKIqumUL6RdVjlvcdZKVda8J39hKKuvLHOd5iDiWMQSJIhQUEURJVQQpVUnxUKS9mMe/gHHL5FLIVcRjBwLKEOD7PjB/+B3t1ZuYtxNCsWAzhfb/hgGArtAo2bb38e23TgB/M/Ald7yl+vA9CfptZYWOQJ6t4GL65am7AGXO0D/kyGbsiP5aQq5HPB+Rt+UAfpugeCa21tzH6cPQIq6StwAB4fASJ6y1z3e3dXe279nmv39AJMecrRgM3JmAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAC4jAAAuIwF4pT92AAAEPUlEQVQ4y6XU34tUZRgH8O/748yZOWfn7OyuOzuzaquuu/4kUTS1i0S8CFLQCCGEuki680+IroS6CQKjC70JRIXooiDCQEkKRUyxxFCydV1Xx5md2fl1fr/ved+3CyEQLDf83j98eHh4vgRLjDSGy7mZirxxjTNKQCenMza5vm67bvaiWb4UwBhDur/8/LZuLBynnb6joMHo/Uj2uh8ZY74hhJiXRlpXL7+V3rn7hZlrlpkmoAUC6oeg1eBEcPumD+D8SyPNG9d30FqzbPkaJAOsxAJJBYwSY9LO73sRQpeCtFttBO0+0n4A6ccQ3RiyFSJt9SDi6KBfr1WXjEShGAyCZFym5pkNS1u3wI8CxH6EzBeQrQiiFyFthkh7kRZa/+dNCADouqGnb378ZkfeOVYo2NOAfWZicPeZNRfFel7vbFMr83vv/np1D5lPUC4MI6c4mMeReAbOG9uulycrP4gwImxw5Jq3acePzrIR+QxiMlP6+sInxy7fO32sKWbHuK0wlK+qve2DC9O/ea4D4+VKFvomwszMAwQLbZQsF8o26OZSrNo1hfFqGUYIaLu4KFeXvw03Jt9Bl37aPnkkAACaJWagHy68V28/GKPKRp67qPaHWfmKqNJHvmcCAdMRGDQFbN48DWuFh/OP7uCPsIGRMRdOEiJrL0AuLiKceTjSuXn96J/3vjo30zp76uLvn40BALUG6CNOimdLhSq4JVEd8rD2yUrYTQMVC4huAtFLIdspaM9g8+QUXtv5KrZuWItRzwOnFqTS8IMYfitANBNAzKduI7j6bpDc/7zZ6HgcAHauP3wy5bW99ezCHscMgzxmSJUEIRTQgEk0aIHAEhQcNqZWjCMOulAA0iQGEgL/SQqSMWQOkIkMQZwg5Xq5MWmOA0BAZx1SaC1XPY4oU4gtCaEzQFIQRmEBIJYCcQkyxyCJJQQkuM7BhBpZoqA1AWEGwgX8vB2Mu/vvVQa3f1quVFocAMKoua/ndypEOQDyeOx0USmOggcEqVIAKLjRSOwMERN4GC4i60SYcIrghkFJQOUZYgawdVN4fdPhk8WV48c3VHb1gKNPPz4I/HO7Jz9sdKIn28K4X4o2qg/mag8GqorCQw4wEjLUoIKg2ewgLo7BuBR/zc1jwGLQFpBwjfya1ZjeuhvV4aktXOY0IUT/8yfPVMj84/eDXvfUrUuXcvVLN5Cv+3AZAy9Q9EyCeMjGxkOHML5uHbqNBhLfB2EEec+DNzKKvOtqyugtxvj+yqqJ2nOR2uzsAQJywmj9StTzqd9oIu33YbQCt20MVstwly3zmWU1CHBFa1MDQdFo7WutBeV8kXH+fc6254bKo+q5SP3hHDPKrCKEHKGUvQOjR562vQEhhAK4bYAvLdu+VigOtPOOI+Mg5ACUUxzQ/1orz0uv3WFKiHKWprbRT2cJYyCUdkdXLO/if+Rvf2QoDtYrAMIAAAAASUVORK5CYII= +[docker-shield]: https://img.shields.io/badge/-Lychee--Docker-informational.svg?logo=github +[gitter-shield]: https://img.shields.io/gitter/room/LycheeOrg/Lobby.svg?logo=gitter +[jetbrains-opensource]: https://www.jetbrains.com/community/opensource/ +[download-shield]: https://img.shields.io/github/downloads/LycheeOrg/Lychee/total +[discord]: https://discord.gg/JMPvuRQcTf +[discord-shield]: https://img.shields.io/discord/1046911561366765598?logo=discord diff --git a/SECURITY.md b/SECURITY.md index b17b590e106..66cc310dba1 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -5,16 +5,17 @@ Lychee uses a rolling release system, **we do not backport fixes to previously released versions**. Those are the versions where we accept vulnerability reports. -| Version | Supported | -| ------- | ------------------ | -| master | :heavy_check_mark: | -| latest release | :white_check_mark: | -| < 4.0 | :x: | +| Version | Supported | +| ---------------- | ------------------ | +| master | :heavy_check_mark: | +| latest release | :white_check_mark: | +| < latest release | :x: | +| < 6.0 | :x: | ## Reporting a Vulnerability As described in our [contribution guide][1], if you discover a security vulnerability within Lychee, please contact us directly on [gitter][2]. All security vulnerabilities will be promptly addressed. -[1]: https://lycheeorg.github.io/docs/contributions.html#security-vulnerabilities +[1]: https://lycheeorg.dev/docs/contributions.html#security-vulnerabilities [2]: https://gitter.im/LycheeOrg/Lobby diff --git a/Vagrantfile b/Vagrantfile deleted file mode 100644 index a36b1f6202b..00000000000 --- a/Vagrantfile +++ /dev/null @@ -1,49 +0,0 @@ -# -*- mode: ruby -*- -# vi: set ft=ruby : - -require 'json' -require 'yaml' - -VAGRANTFILE_API_VERSION ||= "2" -confDir = $confDir ||= File.expand_path("vendor/laravel/homestead", File.dirname(__FILE__)) - -homesteadYamlPath = File.expand_path("Homestead.yaml", File.dirname(__FILE__)) -homesteadJsonPath = File.expand_path("Homestead.json", File.dirname(__FILE__)) -afterScriptPath = "after.sh" -customizationScriptPath = "user-customizations.sh" -aliasesPath = "aliases" - -require File.expand_path(confDir + '/scripts/homestead.rb') - -Vagrant.require_version '>= 1.9.0' - -Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| - if File.exist? aliasesPath then - config.vm.provision "file", source: aliasesPath, destination: "/tmp/bash_aliases" - config.vm.provision "shell" do |s| - s.inline = "awk '{ sub(\"\r$\", \"\"); print }' /tmp/bash_aliases > /home/vagrant/.bash_aliases" - end - end - - if File.exist? homesteadYamlPath then - settings = YAML::load(File.read(homesteadYamlPath)) - elsif File.exist? homesteadJsonPath then - settings = JSON.parse(File.read(homesteadJsonPath)) - else - abort "Homestead settings file not found in " + File.dirname(__FILE__) - end - - Homestead.configure(config, settings) - - if File.exist? afterScriptPath then - config.vm.provision "shell", path: afterScriptPath, privileged: false, keep_color: true - end - - if File.exist? customizationScriptPath then - config.vm.provision "shell", path: customizationScriptPath, privileged: false, keep_color: true - end - - if defined? VagrantPlugins::HostsUpdater - config.hostsupdater.aliases = settings['sites'].map { |site| site['map'] } - end -end diff --git a/aliases b/aliases deleted file mode 100644 index fc182ff4ca0..00000000000 --- a/aliases +++ /dev/null @@ -1,135 +0,0 @@ -alias ..="cd .." -alias ...="cd ../.." - -alias h='cd ~' -alias c='clear' -alias art=artisan - -alias phpspec='vendor/bin/phpspec' -alias phpunit='vendor/bin/phpunit' -alias serve=serve-laravel - -alias xoff='sudo phpdismod -s cli xdebug' -alias xon='sudo phpenmod -s cli xdebug' - -function artisan() { - php artisan "$@" -} - -function php71() { - sudo update-alternatives --set php /usr/bin/php7.1 - sudo update-alternatives --set php-config /usr/bin/php-config7.1 - sudo update-alternatives --set phpize /usr/bin/phpize7.1 -} - -function php72() { - sudo update-alternatives --set php /usr/bin/php7.2 - sudo update-alternatives --set php-config /usr/bin/php-config7.2 - sudo update-alternatives --set phpize /usr/bin/phpize7.2 -} - -function php73() { - sudo update-alternatives --set php /usr/bin/php7.3 - sudo update-alternatives --set php-config /usr/bin/php-config7.3 - sudo update-alternatives --set phpize /usr/bin/phpize7.3 -} - -function serve-apache() { - if [[ "$1" && "$2" ]] - then - sudo bash /vagrant/vendor/laravel/homestead/scripts/create-certificate.sh "$1" - sudo dos2unix /vagrant/vendor/laravel/homestead/scripts/serve-apache.sh - sudo bash /vagrant/vendor/laravel/homestead/scripts/serve-apache.sh "$1" "$2" 80 443 "${3:-7.1}" - else - echo "Error: missing required parameters." - echo "Usage: " - echo " serve-apache domain path" - fi -} - -function serve-laravel() { - if [[ "$1" && "$2" ]] - then - sudo bash /vagrant/vendor/laravel/homestead/scripts/create-certificate.sh "$1" - sudo dos2unix /vagrant/vendor/laravel/homestead/scripts/serve-laravel.sh - sudo bash /vagrant/vendor/laravel/homestead/scripts/serve-laravel.sh "$1" "$2" 80 443 "${3:-7.1}" - else - echo "Error: missing required parameters." - echo "Usage: " - echo " serve domain path" - fi -} - -function __has_pv() { - $(hash pv 2>/dev/null); - - return $? -} - -function __pv_install_message() { - if ! __has_pv; then - echo $1 - echo "Install pv with \`sudo apt-get install -y pv\` then run this command again." - echo "" - fi -} - -function dbexport() { - FILE=${1:-/vagrant/mysqldump.sql.gz} - - # This gives an estimate of the size of the SQL file - # It appears that 80% is a good approximation of - # the ratio of estimated size to actual size - SIZE_QUERY="select ceil(sum(data_length) * 0.8) as size from information_schema.TABLES" - - __pv_install_message "Want to see export progress?" - - echo "Exporting databases to '$FILE'" - - if __has_pv; then - ADJUSTED_SIZE=$(mysql --vertical -uhomestead -psecret -e "$SIZE_QUERY" 2>/dev/null | grep 'size' | awk '{print $2}') - HUMAN_READABLE_SIZE=$(numfmt --to=iec-i --suffix=B --format="%.3f" $ADJUSTED_SIZE) - - echo "Estimated uncompressed size: $HUMAN_READABLE_SIZE" - mysqldump -uhomestead -psecret --all-databases --skip-lock-tables --routines 2>/dev/null | pv --size=$ADJUSTED_SIZE | gzip > "$FILE" - else - mysqldump -uhomestead -psecret --all-databases --skip-lock-tables --routines 2>/dev/null | gzip > "$FILE" - fi - - echo "Done." -} - -function dbimport() { - FILE=${1:-/vagrant/mysqldump.sql.gz} - - __pv_install_message "Want to see import progress?" - - echo "Importing databases from '$FILE'" - - if __has_pv; then - pv "$FILE" --progress --eta | zcat | mysql -uhomestead -psecret 2>/dev/null - else - cat "$FILE" | zcat | mysql -uhomestead -psecret 2>/dev/null - fi - - echo "Done." -} - -function xphp() { - (php -m | grep -q xdebug) - if [[ $? -eq 0 ]] - then - XDEBUG_ENABLED=true - else - XDEBUG_ENABLED=false - fi - - if ! $XDEBUG_ENABLED; then xon; fi - - php \ - -dxdebug.remote_host=192.168.10.1 \ - -dxdebug.remote_autostart=1 \ - "$@" - - if ! $XDEBUG_ENABLED; then xoff; fi -} diff --git a/app/Actions/Album/Action.php b/app/Actions/Album/Action.php index eada1a98406..c4a8e6a5309 100644 --- a/app/Actions/Album/Action.php +++ b/app/Actions/Album/Action.php @@ -1,15 +1,18 @@ ', ':', '"', '/', '\\', '|', '?', '*', + ]; - public function __construct(ReadAccessFunctions $readAccessFunctions) - { - parent::__construct(); - // Illicit chars - $this->readAccessFunctions = $readAccessFunctions; - $this->badChars = array_merge(array_map('chr', range(0, 31)), ['<', '>', ':', '"', '/', '\\', '|', '?', '*']); - } + protected int $deflateLevel = -1; /** - * @param string $albumID + * @param Collection $albums * * @return StreamedResponse + * + * @throws FrameworkException + * @throws ConfigurationKeyMissingException */ - public function do(array $albumIDs): StreamedResponse + public function do(Collection $albums): StreamedResponse { - $zipTitle = $this->setTitle($albumIDs); - - $response = new StreamedResponse(function () use ($albumIDs) { - $options = new \ZipStream\Option\Archive(); - $options->setEnableZip64(Configs::get_value('zip64', '1') === '1'); - $zip = new ZipStream(null, $options); - - $dirs = []; - foreach ($albumIDs as $albumID) { - //! may Fail - $album = $this->albumFactory->make($albumID); - - $dir = $album->title; - if ($album->smart) { - $publicAlbums = resolve(PublicIds::class)->getPublicAlbumsId(); - $album->setAlbumIDs($publicAlbums); - } - $photos_sql = $album->get_photos(); - - $this->compress_album($photos_sql, $dir, $dirs, '', $album, $albumID, $zip); + // Issue #1950: Setting Model::shouldBeStrict(); in /app/Providers/AppServiceProvider.php breaks recursive album download. + // + // From my understanding it is because when we query an album with it's relations (photos & children), + // the relations of the children are not populated. + // As a result, when we try to query the picture list of those, it breaks. + // In that specific case, it is better to simply disable Model::shouldBeStrict() and eat the recursive SQL queries: + // for this specific case we must allow lazy loading. + Model::shouldBeStrict(false); + + $this->deflateLevel = Configs::getValueAsInt('zip_deflate_level'); + + $responseGenerator = function () use ($albums) { + $zip = new ZipStream(defaultCompressionMethod: $this->deflateLevel === -1 ? ZipMethod::STORE : ZipMethod::DEFLATE, + defaultDeflateLevel: $this->deflateLevel, + enableZip64: Configs::getValueAsBool('zip64'), + defaultEnableZeroHeader: true, sendHttpHeaders: false); + + $usedDirNames = []; + foreach ($albums as $album) { + $this->compressAlbum($album, $usedDirNames, null, $zip); } // finish the zip stream $zip->finish(); - }); - - // Set file type and destination - $response->headers->set('Content-Type', 'application/x-zip'); - $disposition = HeaderUtils::makeDisposition(HeaderUtils::DISPOSITION_ATTACHMENT, $zipTitle . '.zip', mb_check_encoding($zipTitle, 'ASCII') ? '' : 'Album.zip'); - $response->headers->set('Content-Disposition', $disposition); - - // Disable caching - $response->headers->set('Cache-Control', 'no-cache, no-store, must-revalidate'); - $response->headers->set('Pragma', 'no-cache'); - $response->headers->set('Expires', '0'); + }; + + try { + $response = new StreamedResponse($responseGenerator); + // Set file type and destination + $zipTitle = self::createZipTitle($albums); + $disposition = HeaderUtils::makeDisposition( + HeaderUtils::DISPOSITION_ATTACHMENT, + $zipTitle . '.zip', + mb_check_encoding($zipTitle, 'ASCII') ? '' : 'Album.zip' + ); + $response->headers->set('Content-Type', 'application/x-zip'); + $response->headers->set('Content-Disposition', $disposition); + + // Disable caching + $response->headers->set('Cache-Control', 'no-cache, no-store, must-revalidate'); + $response->headers->set('Pragma', 'no-cache'); + $response->headers->set('Expires', '0'); + // @codeCoverageIgnoreStart + } catch (\InvalidArgumentException $e) { + throw new FrameworkException('Symfony\'s response component', $e); + } + // @codeCoverageIgnoreEnd return $response; } /** - * Set the Archive title. + * Create the title of the ZIP archive. + * + * @param Collection $albums + * + * @return string */ - private function setTitle(array $albumIDs) + private static function createZipTitle(Collection $albums): string { - if (count($albumIDs) === 1) { - return $this->makeTitle($albumIDs[0]); - } - - return 'Albums'; + return $albums->containsOneItem() ? + self::createValidTitle($albums->first()->title) : + 'Albums'; } /** - * Given an ID return the desired title (may need refactor). + * Creates a title which only contains valid characters. + * + * Removes all invalid characters from the given title. + * If the title happens to become the empty string after removing all + * illegal characters, the fixed string 'Untitled' is returned. + * + * @param string $title the title with possibly invalid characters + * + * @return string the title without any invalid characters */ - private function makeTitle(string $id) + private static function createValidTitle(string $title): string { - if ($this->albumFactory->is_smart($id)) { - return $id; - } + $validTitle = str_replace(self::BAD_CHARS, '', $title); + $validTitle = pathinfo($validTitle, PATHINFO_FILENAME); - //! will fail if not found - $album = $this->albumFactory->make($id); - - return str_replace($this->badChars, '', $album->title) ?: 'Untitled'; // 'Untitled' if empty string. + return $validTitle !== '' ? $validTitle : 'Untitled'; } /** - * Album compression - * ! include recursive call. + * Returns a unique string. + * + * Returns the input value `$str` possibly augmented by a counter + * suffix `-` such that the returned value is not contained in the + * input array `$used`. + * The method adds the return value to `$used`. + * + * @param string $str the input string which shall be made unique + * @param array $used an input array of previously used strings; + * the output array will contain the result value + * + * @return string the unique string */ - private function compress_album($photos_sql, $dir_name, &$dirs, $parent_dir, $album, $albumID, &$zip) + private function makeUnique(string $str, array &$used): string { - if (!$album->is_downloadable()) { - if ($this->albumFactory->is_smart($albumID)) { - if (!AccessControl::is_logged_in()) { - return; - } - } elseif (!AccessControl::is_current_user($album->owner_id)) { - return; + if (count($used) > 0) { + $i = 1; + $tmp = $str; + while (in_array($tmp, $used, true)) { + $tmp = $str . '-' . $i; + $i++; } + $str = $tmp; } + $used[] = $str; - $dir_name = str_replace($this->badChars, '', $dir_name) ?: 'Untitled'; + return $str; + } - // Check for duplicates - if (!empty($dirs)) { - $i = 1; - $tmp_dir = $dir_name; - while (in_array($tmp_dir, $dirs)) { - // Set new directory name - $tmp_dir = $dir_name . '-' . $i; - $i++; - } - $dir_name = $tmp_dir; + /** + * Compresses an album recursively. + * + * @param AbstractAlbum $album the album which shall be added + * to the archive + * @param array $usedDirNames the list of already used + * directory names on the same level + * as `$album` + * ("siblings" of `$album`) + * @param string|null $fullNameOfParent the fully qualified path name + * of the parent directory + * @param ZipStream $zip the archive + * + * @throws FileNotFoundException + * @throws FileNotReadableException + */ + private function compressAlbum(AbstractAlbum $album, array &$usedDirNames, ?string $fullNameOfParent, ZipStream $zip): void + { + $fullNameOfParent = $fullNameOfParent ?? ''; + + if (!Gate::check(AlbumPolicy::CAN_DOWNLOAD, [AbstractAlbum::class, $album])) { + return; } - $dirs[] = $dir_name; - if ($parent_dir !== '') { - $dir_name = $parent_dir . '/' . $dir_name; + $fullNameOfDirectory = $this->makeUnique(self::createValidTitle($album->title), $usedDirNames); + if ($fullNameOfParent !== '') { + $fullNameOfDirectory = $fullNameOfParent . '/' . $fullNameOfDirectory; } - $files = []; - $photos = $photos_sql->get(); - // We don't bother with additional sorting here; who - // cares in what order photos are zipped? + $usedFileNames = []; + // TODO: Ensure that the size variant `original` for each photo is eagerly loaded as it is needed below. This must be solved in close coordination with `ArchiveAlbumRequest`. + $photos = $album->photos; /** @var Photo $photo */ foreach ($photos as $photo) { - // For photos in smart or tag albums, skip the ones that are not - // downloadable based on their actual parent album. The test for - // album_id == null shouldn't really be needed as all such photos - // in smart albums should be owned by the current user... - if ( - $album->smart && !AccessControl::is_current_user($photo->owner_id) && - !($photo->album_id == null ? $album->is_downloadable() : $photo->album->is_downloadable()) - ) { - continue; - } - - $is_raw = ($photo->type == 'raw'); - - $prefix_url = $is_raw ? 'raw/' : 'big/'; - $url = Storage::path($prefix_url . $photo->url); - // Check if readable - if (!@is_readable($url)) { - Logs::error(__METHOD__, __LINE__, 'Original photo missing: ' . $url); - continue; - } + try { + // For photos in smart or tag albums, skip the ones that are not + // downloadable based on their actual parent album. The test for + // album_id === null shouldn't really be needed as all such photos + // in smart albums should be owned by the current user... + if ( + ($album instanceof BaseSmartAlbum || $album instanceof TagAlbum) && + !Gate::check(PhotoPolicy::CAN_DOWNLOAD, $photo) + ) { + continue; + } - // Get extension of image - $extension = Helpers::getExtension($url, false); + $file = $photo->size_variants->getOriginal()->getFile(); - // Set title for photo - $title = str_replace($this->badChars, '', $photo->title); - if (!isset($title) || $title === '') { - $title = 'Untitled'; - } + // Generate name for file inside the ZIP archive + $fileBaseName = $this->makeUnique(self::createValidTitle($photo->title), $usedFileNames); + $fileName = $fullNameOfDirectory . '/' . $fileBaseName . $file->getExtension(); - $file = $title . ($is_raw ? '' : $extension); - - // Check for duplicates - if (!empty($files)) { - $i = 1; - $tmp_file = $file; - $pos = strrpos($tmp_file, '.'); - while (in_array($tmp_file, $files)) { - // Set new title for photo - if ($pos !== false) { - $tmp_file = substr_replace($file, '-' . $i, $pos, 0); - } else { - // No extension. - $tmp_file = $file . '-' . $i; - } - $i++; + // Reset the execution timeout for every iteration. + try { + set_time_limit(intval(ini_get('max_execution_time'))); + } catch (InfoException) { + // Silently do nothing, if `set_time_limit` is denied. } - $file = $tmp_file; + $zip->addFileFromStream(fileName: $fileName, stream: $file->read(), comment: $photo->title, lastModificationDateTime: $photo->taken_at); + $file->close(); + } catch (\Throwable $e) { + Handler::reportSafely($e); } - // Add to array - $files[] = $file; - - // Reset the execution timeout for every iteration. - set_time_limit(ini_get('max_execution_time')); - - // add a file named 'some_image.jpg' from a local file 'path/to/image.jpg' - $zip->addFileFromPath($dir_name . '/' . $file, $url); - } // foreach ($photos) + } - // Recursively compress subalbums - if (!$album->smart) { + // Recursively compress sub-albums + if ($album instanceof Album) { $subDirs = []; - foreach ($album->children as $subAlbum) { - if ($this->readAccessFunctions->album($subAlbum, true) === 1) { - $subSql = Photo::where('album_id', '=', $subAlbum->id); - $this->compress_album($subSql, $subAlbum->title, $subDirs, $dir_name, $subAlbum, $subAlbum->id, $zip); + // TODO: For higher efficiency, ensure that the photos of each child album together with the original size variant are eagerly loaded. + $subAlbums = $album->children; + foreach ($subAlbums as $subAlbum) { + try { + $this->compressAlbum($subAlbum, $subDirs, $fullNameOfDirectory, $zip); + } catch (\Throwable $e) { + Handler::reportSafely($e); } } } diff --git a/app/Actions/Album/Create.php b/app/Actions/Album/Create.php index ee92bbead34..908858a58c6 100644 --- a/app/Actions/Album/Create.php +++ b/app/Actions/Album/Create.php @@ -1,52 +1,138 @@ albumFactory->makeFromTitle($title); - - $this->set_parent($album, $parent_id); + $album = new Album(); + $album->title = $title; + $this->set_parent($album, $parentAlbum); + $album->save(); + $this->set_permissions($album, $parentAlbum); - return $this->store_album($album); + return $album; } /** * Setups parent album on album structure. * - * @param Album $album - * @param int $parent_id - * @param int $user_id + * @param Album $album + * @param Album|null $parentAlbum * - * @return Album + * @throws UnauthenticatedException */ - private function set_parent(Album &$album, int $parent_id): void + private function set_parent(Album $album, ?Album $parentAlbum): void { - $parent = Album::find($parent_id); - - // we get the parent if it exists. - if ($parent !== null) { - $album->parent_id = $parent->id; - - // Admin can add subalbums to other users' albums. Make sure that + if ($parentAlbum !== null) { + // Admin can add sub-albums to other users' albums. Make sure that // the ownership stays with that user. - $album->owner_id = $parent->owner_id; + $album->owner_id = $parentAlbum->owner_id; + // Don't set attribute `parent_id` manually, but use specialized + // methods of the nested set `NodeTrait`. + $album->appendToNode($parentAlbum); } else { - $album->parent_id = null; - $album->owner_id = AccessControl::id(); + $album->owner_id = $this->intendedOwnerId; + $album->makeRoot(); + } + } + + /** + * Set up the permissions. + * + * @param Album $album + * @param Album|null $parentAlbum + * + * @return void + * + * @throws UnexpectedException + * @throws ConfigurationKeyMissingException + */ + private function set_permissions(Album $album, ?Album $parentAlbum): void + { + $defaultProtectionType = Configs::getValueAsEnum('default_album_protection', DefaultAlbumProtectionType::class); + + if ($defaultProtectionType === DefaultAlbumProtectionType::PUBLIC) { + $album->access_permissions()->saveMany([AccessPermission::ofPublic()]); + } + + if ($defaultProtectionType === DefaultAlbumProtectionType::INHERIT && $parentAlbum !== null) { + $album->access_permissions()->saveMany($this->copyPermission($parentAlbum)); } + + $this->grantFullPermissionsToNewOwner($album); + } + + /** + * Given a parent album, retrieve its access permission and return an array containing copies of them. + * + * @param Album|null $parentAlbum + * + * @return array array of access permissions + */ + private function copyPermission(?Album $parentAlbum): array + { + $parentPermissions = $parentAlbum->access_permissions; + $copyPermissions = []; + foreach ($parentPermissions as $parentPermission) { + $copyPermissions[] = AccessPermission::ofAccessPermission($parentPermission); + } + + return $copyPermissions; + } + + /** + * If album is created by someone who has the album shared with. + * We need to give access all to that person. + * + * @param Album $album + * + * @return void + */ + private function grantFullPermissionsToNewOwner(Album $album) + { + if ($album->owner_id === $this->intendedOwnerId) { + return; + } + + $album->access_permissions() + ->where(APC::USER_ID, '=', $this->intendedOwnerId) + ->where(APC::BASE_ALBUM_ID, '=', $album->id) + ->delete(); + + $accessPerm = AccessPermission::withGrantFullPermissionsToUser($this->intendedOwnerId); + + $album->access_permissions()->save($accessPerm); } } diff --git a/app/Actions/Album/CreateTag.php b/app/Actions/Album/CreateTag.php deleted file mode 100644 index 9d93e08276b..00000000000 --- a/app/Actions/Album/CreateTag.php +++ /dev/null @@ -1,33 +0,0 @@ -albumFactory->makeFromTitle($title); - - $album->parent_id = null; - $album->owner_id = AccessControl::id(); - - $album->smart = true; - $album->showtags = $show_tags; - - return $this->store_album($album); - } -} diff --git a/app/Actions/Album/CreateTagAlbum.php b/app/Actions/Album/CreateTagAlbum.php new file mode 100644 index 00000000000..c29e39b16ca --- /dev/null +++ b/app/Actions/Album/CreateTagAlbum.php @@ -0,0 +1,42 @@ +title = $title; + $album->show_tags = $show_tags; + $album->owner_id = $userId; + $album->save(); + + return $album; + } +} diff --git a/app/Actions/Album/Delete.php b/app/Actions/Album/Delete.php index 8a6030e8632..beb075109c5 100644 --- a/app/Actions/Album/Delete.php +++ b/app/Actions/Album/Delete.php @@ -1,66 +1,232 @@ delete()` on every + * `Album` model, the `Album` model would take care of deleting its + * sub-albums and every album in turn would take care of deleting its photos. + * But this is extremely inefficient due to Laravel's architecture: + * + * - Models are heavyweight god classes such that every instance also carries + * the whole code for serialization/deserialization + * - Models are active records (and don't use the unit-of-work pattern), i.e. + * every deletion of a model directly triggers a DB operation; they are + * not deferred into a batch operation + * + * Moreover, while removing the records for albums and photos from the + * DB can be implemented rather efficiently, the actual file operations may + * take some time. + * Especially, if the files are not stored locally but on a remote file system. + * Hence, this method collects all files which need to be removed. + * The caller can then decide to delete them asynchronously. + */ +class Delete extends Action { /** - * @param string $albumID + * Deletes the designated albums (tag albums and regular albums) from the DB. + * + * The method only deletes the records for albums, photos, their size + * variants and potentially associated symbolic links from the DB. + * The method does not delete the associated files from the physical + * storage. + * Instead, the method returns an object in which all these files have + * been collected. + * This object can (and must) be used to eventually delete the files, + * however doing so can be deferred. + * + * @param string[] $albumIDs the album IDs (contains IDs of regular _and_ tag albums) + * + * @return FileDeleter contains the collected files which became obsolete * - * @return bool + * @throws ModelDBException + * @throws ModelNotFoundException + * @throws UnauthenticatedException */ - public function do(string $albumIDs): bool + public function do(array $albumIDs): FileDeleter { - $no_error = true; - // root = unsorted - if ($albumIDs == 'unsorted') { - $photos = Photo::OwnedBy(AccessControl::id())->where('album_id', '=', null)->get(); + try { + $unsortedPhotoIDs = []; - foreach ($photos as $photo) { - $no_error &= $photo->predelete(); - $no_error &= $photo->delete(); + // Among the smart albums, the unsorted album is special, + // because it provides deletion of photos + if (in_array(UnsortedAlbum::ID, $albumIDs, true)) { + $query = UnsortedAlbum::getInstance()->photos(); + if (Auth::user()?->may_administrate !== true) { + $query->where('owner_id', '=', Auth::id() ?? throw new UnauthenticatedException()); + } + $unsortedPhotoIDs = $query->pluck('id')->all(); } - return $no_error; - } + // Only regular albums are owners of photos, so we only need to + // find all photos in those and their descendants + // Only load necessary attributes for tree; in particular avoid + // loading expensive `min_taken_at` and `max_taken_at`. + /** @var Collection $albums */ + $albums = Album::query() + ->without(['cover', 'thumb']) + ->select(['id', 'parent_id', '_lft', '_rgt', 'track_short_path']) + ->findMany($albumIDs); - $albums = Album::whereIn('id', explode(',', $albumIDs))->get(); + $recursiveAlbumIDs = $albums->pluck('id')->all(); // only IDs which refer to regular albums are incubators for recursive IDs + $recursiveAlbumTracks = $albums->pluck('track_short_path'); - $sqlPhoto = Photo::leftJoin('albums', 'photos.album_id', '=', 'albums.id') - ->select('photos.*'); + /** @var Album $album */ + foreach ($albums as $album) { + // Collect all (aka recursive) sub-albums in each album + $subAlbums = $album->descendants()->without(['cover', 'thumb'])->select(['id', 'track_short_path'])->get(); + $recursiveAlbumIDs = array_merge($recursiveAlbumIDs, $subAlbums->pluck('id')->all()); + $recursiveAlbumTracks = $recursiveAlbumTracks->merge($subAlbums->pluck('track_short_path')); + } + // prune the null values + $recursiveAlbumTracks = $recursiveAlbumTracks->filter(fn ($val) => $val !== null); - foreach ($albums as $album) { - $sqlPhoto = $sqlPhoto->orWhere(fn ($q) => $q->where('albums._lft', '>=', $album->_lft) - ->where('albums._rgt', '<=', $album->_rgt)); - } + // Delete the photos from DB and obtain the list of files which need + // to be deleted later + $fileDeleter = (new PhotoDelete())->do($unsortedPhotoIDs, $recursiveAlbumIDs); + $fileDeleter->addFiles($recursiveAlbumTracks, StorageDiskType::LOCAL->value); - $photos = $sqlPhoto->get(); - foreach ($photos as $photo) { - $no_error &= $photo->predelete(); - $no_error &= $photo->delete(); - } + // Remove the sub-forest spanned by the regular albums + $this->deleteSubForest($albums); + TagAlbum::query()->whereIn('id', $albumIDs)->delete(); - $sql_delete = Album::query(); + // Note, we may need to delete more base albums than those whose + // ID is in `$albumIDs`. + // As we might have deleted more regular albums as part of a subtree + // we simply delete all base albums who neither have an associated + // (regular) album or tag album. + BaseAlbumImpl::query()->whereNotExists(function (BaseBuilder $baseBuilder) { + $baseBuilder->from('albums')->whereColumn('albums.id', '=', 'base_albums.id'); + })->whereNotExists(function (BaseBuilder $baseBuilder) { + $baseBuilder->from('tag_albums')->whereColumn('tag_albums.id', '=', 'base_albums.id'); + })->delete(); - //! We break the tree (because delete() is broken see https://github.com/lazychaser/laravel-nestedset/issues/485) - Schema::disableForeignKeyConstraints(); - foreach ($albums as $album) { - $sql_delete = $sql_delete->orWhere(fn ($q) => $q - ->where('_lft', '>=', $album->_lft)->where('_rgt', '<=', $album->_rgt)); + // We also delete the permissions & sharing. + // Note that we explicitly avoid the smart albums. + AccessPermission::query() + ->whereNotExists(function (BaseBuilder $baseBuilder) { + $baseBuilder->from('albums')->whereColumn('albums.id', '=', APC::ACCESS_PERMISSIONS . '.' . APC::BASE_ALBUM_ID); + }) + ->whereNotExists(function (BaseBuilder $baseBuilder) { + $baseBuilder->from('tag_albums')->whereColumn('tag_albums.id', '=', APC::ACCESS_PERMISSIONS . '.' . APC::BASE_ALBUM_ID); + }) + ->whereNotIn(APC::ACCESS_PERMISSIONS . '.' . APC::BASE_ALBUM_ID, SmartAlbumType::values()) + ->delete(); + + return $fileDeleter; + // @codeCoverageIgnoreStart + } catch (QueryBuilderException|InternalLycheeException $e) { + try { + // if anything goes wrong, don't leave the tree in an inconsistent state + Album::query()->fixTree(); + } catch (\Throwable) { + // Sic! We cannot do anything about the inner exception + } + throw ModelDBException::create('albums', 'deleting', $e); + } catch (\InvalidArgumentException|ArrayException $e) { + try { + // if anything goes wrong, don't leave the tree in an inconsistent state + Album::query()->fixTree(); + } catch (\Throwable) { + // Sic! We cannot do anything about the inner exception + } + throw LycheeAssertionError::createFromUnexpectedException($e); } - $sql_delete->delete(); - Schema::enableForeignKeyConstraints(); + // @codeCoverageIgnoreEnd + } - //? We fix the tree :) - if (Album::isBroken()) { - Album::fixTree(); + /** + * Deletes the given set of regular albums incl. their descendants from DB. + * + * This is ugly as hell and is mostly copy & pasted from + * {@link \Kalnoy\Nestedset\NodeTrait} with adoptions. + * I really liked the code of master@0199212 ways better, but it was + * simply too inefficient + * + * This code also fixes a bug when more than one album with + * sub-albums is deleted, i.e. if we delete a "sub-forest". + * The original code (of the nested set model) updates the + * (lft,rgt)-indices on the DB level for every single deletion. + * However, this way deletion of the second albums fails, if the + * second album has already been hydrated earlier, because the + * indices of the already hydrated models and the indices in the + * DB are out-of-sync. + * Either all remaining models needs to be re-hydrated aka + * "refreshed" from the (already updated) DB after every single + * deletion or the update of the DB needs to be postponed until + * all models have been deleted. + * The latter is more efficient, because we do not reload models + * from the DB. + * + * @param Collection $albums + * + * @return void + * + * @throws ModelNotFoundException + * @throws QueryBuilderException + */ + private function deleteSubForest(Collection $albums): void + { + if ($albums->isEmpty()) { + return; } - return $no_error; + /** @var array $pendingGapsToMake */ + $pendingGapsToMake = []; + $deleteQuery = Album::query(); + // First collect all albums to delete in a single query and + // memorize which indices need to be updated later. + /** @var Album $album */ + foreach ($albums as $album) { + $pendingGapsToMake[] = [ + 'lft' => $album->getLft(), + 'rgt' => $album->getRgt(), + ]; + $deleteQuery->whereDescendantOf($album, 'or', false, true); + } + // For MySQL deletion must be done in correct order otherwise the + // foreign key constraint to `parent_id` fails. + $deleteQuery->orderBy('_lft', 'desc')->delete(); + // _After all_ albums have been deleted, remove the gaps which + // have been created by the removed albums. + // Note, the gaps must be removed beginning with the highest + // values first otherwise the later indices won't be correct. + // To save some DB queries, we could implement a "makeMultiGap". + usort($pendingGapsToMake, fn ($a, $b) => $b['lft'] <=> $a['lft']); + foreach ($pendingGapsToMake as $pendingGap) { + $height = $pendingGap['rgt'] - $pendingGap['lft'] + 1; + (new Album())->newNestedSetQuery()->makeGap($pendingGap['rgt'] + 1, -$height); + Album::$actionsPerformed++; + } } } diff --git a/app/Actions/Album/Extensions/LocationData.php b/app/Actions/Album/Extensions/LocationData.php deleted file mode 100644 index c6288865b75..00000000000 --- a/app/Actions/Album/Extensions/LocationData.php +++ /dev/null @@ -1,45 +0,0 @@ -whereNotNull('latitude') - ->whereNotNull('longitude') - ->with('album') - ->get(); - - /* - * @var Photo - */ - foreach ($photos as $photo_model) { - $photo = $photo_model->toReturnArray(); - $symLinkFunctions->getUrl($photo_model, $photo); - - // Add to return - $return_photos[$photo_counter] = $photo; - - $photo_counter++; - } - - return $return_photos; - } -} diff --git a/app/Actions/Album/Extensions/StoreAlbum.php b/app/Actions/Album/Extensions/StoreAlbum.php deleted file mode 100644 index 816aa0bca08..00000000000 --- a/app/Actions/Album/Extensions/StoreAlbum.php +++ /dev/null @@ -1,46 +0,0 @@ -save()) { - throw new JsonError('Could not save album in database!'); - } - } catch (QueryException $e) { - $errorCode = $e->getCode(); - if ($errorCode == 23000 || $errorCode == 23505) { - // Duplicate entry - do { - usleep(rand(0, 1000000)); - $newId = Helpers::generateID(); - } while ($newId === $album->id); - - $album->id = $newId; - $retry = true; - } else { - Logs::error(__METHOD__, __LINE__, 'Something went wrong, error ' . $errorCode . ', ' . $e->getMessage()); - - throw new JsonError('Something went wrong, error' . $errorCode . ', please check the logs'); - } - } - } while ($retry); - - return $album; - } -} diff --git a/app/Actions/Album/ListAlbums.php b/app/Actions/Album/ListAlbums.php new file mode 100644 index 00000000000..25b4e7782f1 --- /dev/null +++ b/app/Actions/Album/ListAlbums.php @@ -0,0 +1,152 @@ + $albumsFiltering + * @param string|null $parent_id + * + * @return TAlbumSaved[] + */ + public function do(Collection $albumsFiltering, ?string $parent_id): array + { + $albumQueryPolicy = resolve(AlbumQueryPolicy::class); + $unfiltered = $albumQueryPolicy->applyReachabilityFilter( + // We remove all sub albums + // Otherwise it would create cyclic dependency + Album::query() + ->when($albumsFiltering->count() > 0, + function ($q) use ($albumsFiltering) { + $albumsFiltering->each( + fn ($a) => $q->whereNot(fn ($q1) => $q1->where('_lft', '>=', $a->_lft)->where('_rgt', '<=', $a->_rgt)) + ); + + return $q; + }) + ); + $sorting = AlbumSortingCriterion::createDefault(); + $query = (new SortingDecorator($unfiltered)) + ->orderBy($sorting->column, $sorting->order); + + /** @var NsCollection $albums */ + $albums = $query->get(); + /** @var NsCollection $tree */ + $tree = $albums->toTree(null); + + $flat_tree = $this->flatten($tree); + + // Prepend with the possibility to move to root if parent is not already root. + if ($parent_id !== null) { + array_unshift( + $flat_tree, + [ + 'id' => null, + 'title' => __('gallery.root'), + 'original' => __('gallery.root'), + 'short_title' => __('gallery.root'), + 'thumb' => URL::asset('img/no_images.svg'), + ] + ); + } + + return $flat_tree; + } + + /** + * Flatten the tree and create bread crumb paths. + * + * @param NsCollection|Collection $collection + * @param string $prefix + * + * @return TAlbumSaved[] + */ + private function flatten($collection, $prefix = ''): array + { + /** @var TAlbumSaved[] $flatArray */ + $flatArray = []; + foreach ($collection as $node) { + $title = $prefix . ($prefix !== '' ? '/' : '') . $node->title; + $short_title = $this->shorten($title); + $flatArray[] = [ + 'id' => $node->id, + 'title' => $title, + 'original' => $node->title, + 'short_title' => $short_title, + 'thumb' => $node->thumb?->thumbUrl ?? URL::asset('img/no_images.svg'), + ]; + if ($node->children !== null) { + $flatArray = array_merge($flatArray, $this->flatten($node->children, $title)); + unset($node->children); + } + } + + return $flatArray; + } + + /** + * shorten the title to reach a targetted length. + * + * @param string $title to shorten + * + * @return string short version with elipsis + */ + private function shorten(string $title): string + { + $len = strlen($title); + + if ($len <= self::SHORTEN_BY) { + return $title; + } + /** @var Collection $title_split */ + $title_split = collect(explode('/', $title)); + $last_elem = $title_split->last(); + $len_last_elem = strlen($last_elem); + + $num_chunks = $title_split->count() - 1; + + if ($num_chunks === 0) { + return Str::limit($last_elem, self::SHORTEN_BY, '…'); + } + + $title_split = $title_split->take($num_chunks); + /** @var Collection $title_lengths */ + $title_lengths = $title_split->map(fn ($v) => strlen($v)); + + // find best target length. + + $len_to_reduce = self::SHORTEN_BY - $len_last_elem - 2 * $num_chunks; + $unit_target_len = (int) ceil($len_to_reduce / $num_chunks); + + do { + $unit_target_len--; + $title_lengths = $title_lengths->map(fn ($v) => $v <= $unit_target_len ? $v : $unit_target_len + 1); + $resulting_len = $title_lengths->sum(); + } while ($len_to_reduce < $resulting_len); + + $title_split = $title_split->map(fn ($v) => Str::limit($v, $unit_target_len > 0 ? $unit_target_len : 0, '…')); + + return implode('/', $title_split->all()) . '/' . $last_elem; + } +} diff --git a/app/Actions/Album/Merge.php b/app/Actions/Album/Merge.php index cfb459b9387..3bf8166cfb0 100644 --- a/app/Actions/Album/Merge.php +++ b/app/Actions/Album/Merge.php @@ -1,60 +1,59 @@ $albums * - * @return bool + * @throws ModelNotFoundException + * @throws ModelDBException + * @throws QueryBuilderException */ - public function do(string $albumID, array $albumIDs): bool + public function do(Album $targetAlbum, Collection $albums): void { - $album_master = $this->albumFactory->make($albumID); - if ($album_master->is_smart()) { - Logs::error(__METHOD__, __LINE__, 'Merge is not possible on smart albums'); - - return false; - } - - $no_error = true; - // Merge Photos - if (DB::table('photos')->whereIn('album_id', $albumIDs)->count() > 0) { - $no_error &= Photo::whereIn('album_id', $albumIDs)->update(['album_id' => $album_master->id]); - } + // Merge photos of source albums into target + Photo::query() + ->whereIn('album_id', $albums->pluck('id')) + ->update(['album_id' => $targetAlbum->id]); - // Merge Sub-albums - // ! we have to do it via Model::save() in order to not break the tree - $albums = Album::whereIn('parent_id', $albumIDs)->get(); + // Merge sub-albums of source albums into target + /** @var Album $album */ foreach ($albums as $album) { - $album->parent_id = $album_master->id; - $album->save(); - } - - // now we delete the albums - // ! we have to do it via Model::delete() in order to not break the tree - $albums = Album::whereIn('id', $albumIDs)->get(); - foreach ($albums as $album) { - $album->delete(); - } - - if (Album::isBroken()) { - $errors = Album::countErrors(); - $sum = $errors['oddness'] + $errors['duplicates'] + $errors['wrong_parent'] + $errors['missing_parent']; - Logs::warning(__METHOD__, __LINE__, 'Tree is broken with ' . $sum . ' errors.'); - Album::fixTree(); - Logs::notice(__METHOD__, __LINE__, 'Tree has been fixed.'); + foreach ($album->children as $childAlbum) { + // Don't set attribute `parent_id` manually, but use specialized + // methods of the nested set `NodeTrait` to keep the enumeration + // of the tree consistent + // `appendNode` also internally calls `save` on the model + $targetAlbum->appendNode($childAlbum); + } } - $album_master->descendants()->update(['owner_id' => $album_master->owner_id]); - $album_master->get_all_photos()->update(['photos.owner_id' => $album_master->owner_id]); + // Now we delete the source albums + // We must use the special `Delete` action in order to not break the + // tree. + // The returned `FileDeleter` can be ignored as all photos have been + // moved to the new location. + (new Delete())->do($albums->pluck('id')->values()->all()); - return $no_error; + $targetAlbum->fixOwnershipOfChildren(); } } diff --git a/app/Actions/Album/Move.php b/app/Actions/Album/Move.php index 8b0f0211d06..e63d753695c 100644 --- a/app/Actions/Album/Move.php +++ b/app/Actions/Album/Move.php @@ -1,49 +1,50 @@ $albums * - * @return bool + * @throws ModelNotFoundException + * @throws ModelDBException */ - public function do(string $albumID, array $albumIDs): bool + public function do(?Album $targetAlbum, Collection $albums): void { - $album_master = null; - // $albumID = 0 is root - // ! check type - if ($albumID != 0) { - $album_master = $this->albumFactory->make($albumID); - - if ($album_master->is_smart()) { - Logs::error(__METHOD__, __LINE__, 'Move is not possible on smart albums'); - - return false; + // Move source albums into target + if ($targetAlbum !== null) { + /** @var Album $album */ + foreach ($albums as $album) { + // Don't set attribute `parent_id` manually, but use specialized + // methods of the nested set `NodeTrait` to keep the enumeration + // of the tree consistent + // `appendNode` also internally calls `save` on the model + $targetAlbum->appendNode($album); } + $targetAlbum->fixOwnershipOfChildren(); } else { - $albumID = null; - } - - $albums = Album::whereIn('id', $albumIDs)->get(); - $no_error = true; - - foreach ($albums as $album) { - $album->parent_id = $albumID; - $no_error &= $album->save(); - } - // Tree should be updated by itself here. - - if ($no_error && $album_master !== null) { - // updat owner - $album_master->descendants()->update(['owner_id' => $album_master->owner_id]); - $album_master->get_all_photos()->update(['photos.owner_id' => $album_master->owner_id]); + /** @var Album $album */ + foreach ($albums as $album) { + // Don't set attribute `parent_id` manually, but use specialized + // methods of the nested set `NodeTrait` to keep the enumeration + // of the tree consistent + $album->saveAsRoot(); + } } - - return $no_error; } } diff --git a/app/Actions/Album/Photos.php b/app/Actions/Album/Photos.php deleted file mode 100644 index 0d4324de1c1..00000000000 --- a/app/Actions/Album/Photos.php +++ /dev/null @@ -1,116 +0,0 @@ -symLinkFunctions = $symLinkFunctions; - } - - /** - * take a $photo_sql query and return an array containing their pictures. - * - * @param bool $full_photo - * - * @return array - */ - public function get(Album $album): array - { - [$sortingCol, $sortingOrder] = $album->get_sort(); - $photos_sql = $album->get_photos(); - - $previousPhotoID = ''; - $return_photos = []; - $photo_counter = 0; - - /** - * @var Collection[Photo] - */ - $photos = $album->customSort($photos_sql, $sortingCol, $sortingOrder); - - if ($sortingCol === 'title' || $sortingCol === 'description') { - // The result is supposed to be sorted by the user-specified - // column as the primary key and by 'id' as the secondary key. - // Unfortunately, sortBy can't be chained the way orderBy can. - // Instead, we use array_multisort which can be used in a - // stable fashion, preserving the ordering of elements that - // compare equal. We depend here on the collection already - // being sorted by 'id', via the SQL query. - - // Convert to array so that we can use standard PHP functions. - // TODO: use collections? - // * see if this works - // $photos = $photos - // ->sortBy($sortingCol, SORT_NATURAL | SORT_FLAG_CASE, $sortingOrder === 'ASC' ? SORT_ASC : SORT_DESC) - // ->sortBy('id', SORT_ASC); - $photos = $photos->all(); - // Primary sorting key. - $values = array_column($photos, $sortingCol); - // Secondary sorting key -- just preserves current order. - $keys = array_keys($photos); - array_multisort($values, $sortingOrder === 'ASC' ? SORT_ASC : SORT_DESC, SORT_NATURAL | SORT_FLAG_CASE, $keys, SORT_ASC, $photos); - } - - /** @var Photo $photo_model */ - foreach ($photos as $photo_model) { - // Turn data from the database into a front-end friendly format - $photo = $photo_model->toReturnArray(); - $photo['license'] = $photo_model->get_license($album->get_license()); - - $this->symLinkFunctions->getUrl($photo_model, $photo); - if (!AccessControl::is_current_user($photo_model->owner_id) && !$album->is_full_photo_visible()) { - $photo_model->downgrade($photo); - } - - // Set previous and next photoID for navigation purposes - $photo['previousPhoto'] = $previousPhotoID; - $photo['nextPhoto'] = ''; - - // Set current photoID as nextPhoto of previous photo - if ($previousPhotoID !== '') { - $return_photos[$photo_counter - 1]['nextPhoto'] = $photo['id']; - } - $previousPhotoID = $photo['id']; - - // Add to return - $return_photos[$photo_counter] = $photo; - - $photo_counter++; - } - - $this->wrapAroundPhotos($return_photos); - - return $return_photos; - } - - /** - * Set up the wrap around of the photos if setting is true and if there are enough pictures. - */ - private function wrapAroundPhotos(array &$return_photos): void - { - $photo_counter = count($return_photos); - - if ($photo_counter > 1 && Configs::get_value('photos_wraparound', '1') === '1') { - // Enable next and previous for the first and last photo - $lastElement = end($return_photos); - $lastElementId = $lastElement['id']; - $firstElement = reset($return_photos); - $firstElementId = $firstElement['id']; - - $return_photos[$photo_counter - 1]['nextPhoto'] = $firstElementId; - $return_photos[0]['previousPhoto'] = $lastElementId; - } - } -} diff --git a/app/Actions/Album/PositionData.php b/app/Actions/Album/PositionData.php index 16c9833c720..f2b113c92d1 100644 --- a/app/Actions/Album/PositionData.php +++ b/app/Actions/Album/PositionData.php @@ -1,30 +1,52 @@ albumFactory->make($albumID); - - if ($album->smart) { - $album->setAlbumIDs(resolve(PublicIds::class)->getPublicAlbumsId()); - $photos_sql = $album->get_photos(); - } elseif ($data['includeSubAlbums']) { - $photos_sql = $album->get_all_photos(); - } else { - $photos_sql = $album->get_photos(); - } + $photoRelation = ($album instanceof Album && $includeSubAlbums) ? + $album->all_photos() : + $album->photos(); - $return['photos'] = $this->photosLocationData($photos_sql); - $return['id'] = strval($album->id); + // @phpstan-ignore-next-line + $photoRelation + ->with([ + 'album' => function (BelongsTo $b) { + // The album is required for photos to properly + // determine access and visibility rights; but we + // don't need to determine the cover and thumbnail for + // each album + $b->without(['cover', 'thumb']); + }, + 'size_variants' => function (HasMany $r) { + // The web GUI only uses the small and thumb size + // variants to show photos on a map; so we can save + // hydrating the larger size variants + // this really helps, if you want to show thousands + // of photos on a map, as there are up to 7 size + // variants per photo + $r->whereBetween('type', [SizeVariantType::SMALL2X, SizeVariantType::THUMB]); + }, + 'size_variants.sym_links', + ]) + ->whereNotNull('latitude') + ->whereNotNull('longitude'); - return $return; + return new PositionDataResource($album->id, $album->title, $photoRelation->get(), $album instanceof Album ? $album->track_url : null); } } diff --git a/app/Actions/Album/Prepare.php b/app/Actions/Album/Prepare.php deleted file mode 100644 index 0e0b2c23d86..00000000000 --- a/app/Actions/Album/Prepare.php +++ /dev/null @@ -1,47 +0,0 @@ -photos = $photos; - } - - /** - * @param Album $album - * - * @return array - */ - public function do(Album $album): array - { - if ($album->smart) { - $publicAlbums = resolve(PublicIds::class)->getPublicAlbumsId(); - $album->setAlbumIDs($publicAlbums); - } else { - // we only do this when not in smart mode (i.e. no sub albums) - // that way we limit the number of times we have to query. - resolve(PublicIds::class)->setAlbum($album); - } - $return = $album->toReturnArray(); - - // take care of sub albums - $return['albums'] = $album->get_children()->map(fn ($a) => $a->toReturnArray())->values(); - - // take care of photos - $return['photos'] = $this->photos->get($album); - $return['id'] = strval($album->id); - $return['num'] = strval(count($return['photos'])); - - return $return; - } -} diff --git a/app/Actions/Album/SetCover.php b/app/Actions/Album/SetCover.php deleted file mode 100644 index 16fbd2121ee..00000000000 --- a/app/Actions/Album/SetCover.php +++ /dev/null @@ -1,12 +0,0 @@ -property = 'cover_id'; - } -} diff --git a/app/Actions/Album/SetDescription.php b/app/Actions/Album/SetDescription.php deleted file mode 100644 index aaece40a3d4..00000000000 --- a/app/Actions/Album/SetDescription.php +++ /dev/null @@ -1,12 +0,0 @@ -property = 'description'; - } -} diff --git a/app/Actions/Album/SetHeader.php b/app/Actions/Album/SetHeader.php new file mode 100644 index 00000000000..2f678f5d71b --- /dev/null +++ b/app/Actions/Album/SetHeader.php @@ -0,0 +1,38 @@ +header_id = AlbumController::COMPACT_HEADER; + } else { + $album->header_id = ($album->header_id !== $photo?->id || $shall_override) ? $photo?->id : null; + } + $album->save(); + + return $album; + } +} diff --git a/app/Actions/Album/SetLicense.php b/app/Actions/Album/SetLicense.php deleted file mode 100644 index 682836a71be..00000000000 --- a/app/Actions/Album/SetLicense.php +++ /dev/null @@ -1,12 +0,0 @@ -property = 'license'; - } -} diff --git a/app/Actions/Album/SetNSFW.php b/app/Actions/Album/SetNSFW.php deleted file mode 100644 index a7eca21f6b4..00000000000 --- a/app/Actions/Album/SetNSFW.php +++ /dev/null @@ -1,26 +0,0 @@ -property = 'nsfw'; - } - - public function do(string $albumID, ?string $_): bool - { - if ($this->albumFactory->is_smart($albumID)) { - Logs::warning(__METHOD__, __LINE__, 'NSFW tag is not possible on smart albums.'); - - return false; - } - $album = $this->albumFactory->make($albumID); - - return $this->execute($album, ($album->nsfw != 1) ? 1 : 0); - } -} diff --git a/app/Actions/Album/SetProtectionPolicy.php b/app/Actions/Album/SetProtectionPolicy.php new file mode 100644 index 00000000000..bf851837c10 --- /dev/null +++ b/app/Actions/Album/SetProtectionPolicy.php @@ -0,0 +1,72 @@ +is_nsfw = $protectionPolicy->is_nsfw; + $album->save(); + + $active_permissions = $album->public_permissions(); + + if (!$protectionPolicy->is_public) { + $active_permissions?->delete(); + + return; + } + + // Security attributes of the album itself independent of a particular user + $active_permissions ??= new AccessPermission(); + $active_permissions->is_link_required = $protectionPolicy->is_link_required; + $active_permissions->grants_full_photo_access = $protectionPolicy->grants_full_photo_access; + $active_permissions->grants_download = $protectionPolicy->grants_download; + $active_permissions->base_album_id = $album->id; + + // $album->public_permissions = $active_permissions; + + // Set password if provided + if ($shallSetPassword) { + // password is provided => there is a change + if ($password !== null) { + // password is not null => we update the value with the hash + $active_permissions->password = Hash::make($password); + } else { + // we remove the password + $active_permissions->password = null; + } + } + $active_permissions->base_album_id = $album->id; + $active_permissions->save(); + } +} diff --git a/app/Actions/Album/SetPublic.php b/app/Actions/Album/SetPublic.php deleted file mode 100644 index 26d9c5f8de3..00000000000 --- a/app/Actions/Album/SetPublic.php +++ /dev/null @@ -1,52 +0,0 @@ -albumFactory->is_smart($albumID)) { - Logs::error(__METHOD__, __LINE__, 'Not applicable to smart albums.'); - - return false; - } - - $album = $this->albumFactory->make($albumID); - - // Convert values - $album->full_photo = ($values['full_photo'] === '1' ? 1 : 0); - $album->public = ($values['public'] === '1' ? 1 : 0); - $album->viewable = ($values['visible'] === '1' ? 1 : 0); - $album->nsfw = ($values['nsfw'] === '1' ? 1 : 0); - $album->downloadable = ($values['downloadable'] === '1' ? 1 : 0); - $album->share_button_visible = ($values['share_button_visible'] === '1' ? 1 : 0); - - // Set password if provided - if (array_key_exists('password', $values)) { - // password is provided => there is a change - - if (isset($values['password'])) { - // password is not null => we update the value with the hash - $album->password = bcrypt($values['password']); - } else { - // we remove the password - $album->password = null; - } - } - - // Set Public - if (!$album->save()) { - return false; - } - - // Reset permissions for photos - if ($album->public == 1) { - $album->photos()->update(['public' => '0']); - } - - return true; - } -} diff --git a/app/Actions/Album/SetShowTags.php b/app/Actions/Album/SetShowTags.php deleted file mode 100644 index 525f35fbf7c..00000000000 --- a/app/Actions/Album/SetShowTags.php +++ /dev/null @@ -1,27 +0,0 @@ -property = 'showtags'; - } - - public function do(string $albumID, ?string $value): bool - { - $album = $this->albumFactory->make($albumID); - - if (!$album->is_tag_album()) { - Logs::error(__METHOD__, __LINE__, 'Could not change show tags on non tag album'); - - return false; - } - - return $this->execute($album, $value); - } -} diff --git a/app/Actions/Album/SetSmartProtectionPolicy.php b/app/Actions/Album/SetSmartProtectionPolicy.php new file mode 100644 index 00000000000..ef012424dcf --- /dev/null +++ b/app/Actions/Album/SetSmartProtectionPolicy.php @@ -0,0 +1,32 @@ +setPublic(); + } else { + $album->setPrivate(); + } + } +} diff --git a/app/Actions/Album/SetSorting.php b/app/Actions/Album/SetSorting.php deleted file mode 100644 index 06029191bc4..00000000000 --- a/app/Actions/Album/SetSorting.php +++ /dev/null @@ -1,23 +0,0 @@ -albumFactory->is_smart($albumID)) { - Logs::error(__METHOD__, __LINE__, 'Not applicable to smart albums.'); - - return false; - } - - $album = $this->albumFactory->make($albumID); - $album->sorting_col = $value['typePhotos'] ?? ''; - $album->sorting_order = $value['orderPhotos'] ?? 'ASC'; - - return $album->save(); - } -} diff --git a/app/Actions/Album/SetTitle.php b/app/Actions/Album/SetTitle.php deleted file mode 100644 index fe58f6e8784..00000000000 --- a/app/Actions/Album/SetTitle.php +++ /dev/null @@ -1,12 +0,0 @@ -property = 'title'; - } -} diff --git a/app/Actions/Album/Setter.php b/app/Actions/Album/Setter.php deleted file mode 100644 index 700855a8c43..00000000000 --- a/app/Actions/Album/Setter.php +++ /dev/null @@ -1,31 +0,0 @@ -albumFactory->make($albumID); - - return $this->execute($album, $value); - } - - public function execute(Album $album, $value): bool - { - $album->{$this->property} = $value; - - return $album->save(); - } -} diff --git a/app/Actions/Album/Setters.php b/app/Actions/Album/Setters.php deleted file mode 100644 index d9c3e17bb29..00000000000 --- a/app/Actions/Album/Setters.php +++ /dev/null @@ -1,21 +0,0 @@ -update([$this->property => $value]); - } -} diff --git a/app/Actions/Album/Transfer.php b/app/Actions/Album/Transfer.php new file mode 100644 index 00000000000..dd4cec61151 --- /dev/null +++ b/app/Actions/Album/Transfer.php @@ -0,0 +1,38 @@ +owner_id = $userId; + $baseAlbum->save(); + + // No longer necessary because we transfer the ownership + AccessPermission::query()->where('base_album_id', '=', $baseAlbum->id)->where('user_id', '=', $userId)->delete(); + + // If this is an Album, we also need to fix the children and photos ownership + if ($baseAlbum instanceof Album) { + $baseAlbum->makeRoot(); + $baseAlbum->save(); + $baseAlbum->fixOwnershipOfChildren(); + } + } +} diff --git a/app/Actions/Album/Unlock.php b/app/Actions/Album/Unlock.php index e985646c06b..2b17aa9ffe8 100644 --- a/app/Actions/Album/Unlock.php +++ b/app/Actions/Album/Unlock.php @@ -1,64 +1,84 @@ albumPolicy = resolve(AlbumPolicy::class); + } + /** - * Provided a password and an album, check if the album can be - * unlocked. If yes, unlock all albums with the same password. + * Tries to unlock the given album with the given password. + * + * If the password is correct, then all albums which can be unlocked with + * the same password are unlocked, too. * - * @param string $albumID + * @param BaseAlbum $album + * @param string $password * - * @return array + * @throws UnauthorizedException */ - public function do(?string $albumid, $password): bool + public function do(BaseAlbum $album, string $password): void { - if ($this->albumFactory->is_smart($albumid)) { - return false; - } - - $album = $this->albumFactory->make($albumid); - if ($album->is_public()) { - if ($album->password === '') { - return true; - } - if (AccessControl::has_visible_album($album->id)) { - return true; + if ($album->public_permissions() !== null) { + $album_password = $album->public_permissions()->password; + if ( + $album_password === null || + $album_password === '' || + $this->albumPolicy->isUnlocked($album) + ) { + return; } - $password ??= ''; - if (Hash::check($password, $album->password)) { + if (Hash::check($password, $album_password)) { $this->propagate($password); - return true; + return; } + throw new UnauthorizedException('Password is invalid'); } - return false; + throw new UnauthorizedException('Album is not enabled for password-based access'); } /** * Provided a password, add all the albums that the password unlocks. */ - public function propagate(string $password): void + private function propagate(string $password): void { // We add all the albums that the password unlocks so that the // user is not repeatedly asked to enter the password as they // browse through the hierarchy. This should be safe as the // list of such albums is not exposed to the user and is // considered as the last access check criteria. - $albums = Album::whereNotNull('password')->where('password', '!=', '')->get(); - $albumIDs = []; + $albums = BaseAlbumImpl::query() + ->select(['base_albums.id', 'base_albums.owner_id', APC::PASSWORD]) + ->join(APC::ACCESS_PERMISSIONS, 'base_album_id', '=', 'base_albums.id', 'inner') + ->whereNull(APC::ACCESS_PERMISSIONS . '.user_id') + ->whereNotNull(APC::PASSWORD) + ->get(); + /** @var BaseAlbumImpl $album */ foreach ($albums as $album) { if (Hash::check($password, $album->password)) { - $albumIDs[] = $album->id; + $this->albumPolicy->unlock($album); } } - - AccessControl::add_visible_albums($albumIDs); } } diff --git a/app/Actions/Albums/Extensions/PublicIds.php b/app/Actions/Albums/Extensions/PublicIds.php deleted file mode 100644 index e145202836a..00000000000 --- a/app/Actions/Albums/Extensions/PublicIds.php +++ /dev/null @@ -1,177 +0,0 @@ -initNotAccessible(); - } - - /*------------------------------------------------------------------------------- */ - /** - * Queries. - */ - - /** - * Build a query that removes all non public albums - * or public albums which are hidden - * or public albums with a password. - * - * @param Builder - * - * @return Builder - */ - private function notPublicNotViewable(Builder $query): Builder - { - return $query - // remove NOT public - ->where('public', '<>', '1') - // or PUBLIC BUT NOT VIEWABLE (hidden) - ->orWhere(fn ($q) => $q->where('public', '=', '1')->where('viewable', '<>', '1')) - // or PUBLIC BUT PASSWORD LOCKED - ->orWhere(fn ($q) => $q->where('public', '=', '1')->where('password', '<>', '')); - } - - private function init(): Builder - { - // unlocked albums - $query = DB::table('albums')->select('_lft', '_rgt') - ->whereNotIn('id', AccessControl::get_visible_albums()); - - if ($this->parent == null) { - return $query; - } - - // add descendant constraints. - return $query->where('_lft', '>', $this->parent->_lft)->where('_rgt', '<', $this->parent->_rgt); - } - - /** - * Return a collection of Album that are not directly accessible by visibility criteria - * ! we do not include password protected albums from other users. - * - * @return BaseCollection[(_lft, _rgt)] - */ - private function getDirectlyNotAccessible(): BaseCollection - { - if (AccessControl::is_admin()) { - return new BaseCollection(); - } - - if (AccessControl::is_logged_in()) { - $shared_ids = DB::table('user_album')->select('album_id') - ->where('user_id', '=', AccessControl::id()) - ->pluck('album_id'); - - return $this->init() - ->where('owner_id', '<>', AccessControl::id()) - // shared are accessible - ->whereNotIn('id', $shared_ids) - // remove NOT public - ->where(fn ($q) => $this->notPublicNotViewable($q)) - ->get(); - } - - // remove NOT public - return $this->init()->where(fn ($q) => $this->notPublicNotViewable($q)) - ->get(); - } - - /*------------------------------------------------------------------------------- */ - - /** - * Initializers. - */ - private function initNotAccessible(?Album $parent = null): BaseCollection - { - $this->parent = $parent; - - /** - * @var BaseCollection - */ - $directly = $this->getDirectlyNotAccessible(); - - if ($directly->count() > 0) { - $sql = DB::table('albums')->select('id'); - foreach ($directly as $alb) { - $sql = $sql->orWhereBetween('_lft', [$alb->_lft, $alb->_rgt]); - } - - $this->forbidden_list = $sql->pluck('id'); - - return $this->forbidden_list; - } - - $this->forbidden_list = new BaseCollection(); - - return $this->forbidden_list; - } - - /*------------------------------------------------------------------------------- */ - /** - * Getters. - */ - - /** - * This function must only be called from ROOT. In other words for: - * => smart albums - * => search - * => map - * => random - * => RSS. - * - * @return Collection[int] of all recursive albums ID accessible by the current user from the top level - */ - public function getPublicAlbumsId(): BaseCollection - { - $id_not_accessible = $this->getNotAccessible(null); - - return DB::table('albums')->select('id')->whereNotIn('id', $id_not_accessible)->pluck('id'); - } - - /** - * Return an array of ids of albums that are not accessible. - * - * @return array[int] - */ - public function getNotAccessible(): BaseCollection - { - return $this->forbidden_list ?? $this->initNotAccessible(); - } - - /** - * We need to refresh PublicIds in our test suite. - */ - public function refresh() - { - $this->forbidden_list = null; - } - - /*------------------------------------------------------------------------------- */ - - /** - * Setter. - */ - public function setAlbum(Album $album) - { - if ($this->parent == $album) { - return; - } - - $this->initNotAccessible($album); - } -} diff --git a/app/Actions/Albums/Extensions/PublicViewable.php b/app/Actions/Albums/Extensions/PublicViewable.php deleted file mode 100644 index 5339e36511a..00000000000 --- a/app/Actions/Albums/Extensions/PublicViewable.php +++ /dev/null @@ -1,31 +0,0 @@ -where(fn ($query) => $query->where('owner_id', '=', $id) - ->orWhereIn('id', DB::table('user_album')->select('album_id')->where('user_id', '=', $id)) - ->orWhere(fn ($q) => $q->where('public', '=', '1')->where('viewable', '=', '1'))); - } - - // or PUBLIC AND VIEWABLE (not hidden) - return $query->where('public', '=', '1')->where('viewable', '=', '1'); - } -} diff --git a/app/Actions/Albums/Extensions/TopQuery.php b/app/Actions/Albums/Extensions/TopQuery.php deleted file mode 100644 index 0f18ef7299e..00000000000 --- a/app/Actions/Albums/Extensions/TopQuery.php +++ /dev/null @@ -1,23 +0,0 @@ -whereIsRoot(); - - return $this->publicViewable($baseQuery)->orderBy('owner_id', 'ASC'); - } - - return $this->publicViewable(Album::query()->whereIsRoot()); - } -} diff --git a/app/Actions/Albums/PositionData.php b/app/Actions/Albums/PositionData.php index b420574bf3a..61bcb0f07a9 100644 --- a/app/Actions/Albums/PositionData.php +++ b/app/Actions/Albums/PositionData.php @@ -1,38 +1,67 @@ photoQueryPolicy = $photoQueryPolicy; + // caching to avoid further request + Configs::get(); + } /** * Given a list of albums, generate an array to be returned. * - * @param Collection[Album] $albums + * @return PositionDataResource * - * @return array + * @throws InternalLycheeException */ - public function do() + public function do(): PositionDataResource { - // caching to avoid further request - Configs::get(); - - // Initialize return var - $return = []; - - $albumIDs = resolve(PublicIds::class)->getPublicAlbumsId(); - - $query = Photo::with('album')->whereIn('album_id', $albumIDs); - - $full_photo = Configs::get_value('full_photo', '1') == '1'; - $return['photos'] = $this->photosLocationData($query, $full_photo); + $photoQuery = $this->photoQueryPolicy->applySearchabilityFilter( + query: Photo::query() + ->with([ + 'album' => function ($b) { + // The album is required for photos to properly + // determine access and visibility rights; but we + // don't need to determine the cover and thumbnail for + // each album + $b->without(['cover', 'thumb']); + }, + 'size_variants' => function ($r) { + // The web GUI only uses the small and thumb size + // variants to show photos on a map; so we can save + // hydrating the larger size variants + // this really helps, if you want to show thousands + // of photos on a map, as there are up to 7 size + // variants per photo + $r->whereBetween('type', [SizeVariantType::SMALL2X, SizeVariantType::THUMB]); + }, + 'size_variants.sym_links', + ]) + ->whereNotNull('latitude') + ->whereNotNull('longitude'), + origin: null, + include_nsfw: !Configs::getValueAsBool('hide_nsfw_in_map') + ); - return $return; + return new PositionDataResource(null, null, $photoQuery->get(), null); } } diff --git a/app/Actions/Albums/Prepare.php b/app/Actions/Albums/Prepare.php deleted file mode 100644 index 8b93b29398c..00000000000 --- a/app/Actions/Albums/Prepare.php +++ /dev/null @@ -1,44 +0,0 @@ -readAccessFunctions = $readAccessFunctions; - } - - /** - * Given a list of albums, generate an array to be returned. - * - * @param BaseCollection[Album] $albums - * - * @return array - */ - public function do(BaseCollection $albums) - { - $return = []; - foreach ($albums as $_ => $album) { - $album_array = $album->toReturnArray(); - - if (AccessControl::is_logged_in()) { - $album_array['owner'] = $album->owner->name(); - } - - // Add to return - $return[] = $album_array; - } - - return $return; - } -} diff --git a/app/Actions/Albums/Smart.php b/app/Actions/Albums/Smart.php deleted file mode 100644 index 4edb1eedfea..00000000000 --- a/app/Actions/Albums/Smart.php +++ /dev/null @@ -1,76 +0,0 @@ -symLinkFunctions = $symLinkFunctions; - $this->smartFactory = $smartFactory; - $this->tag = $tag; - } - - /** - * Returns an array of top-level albums and shared albums visible to - * the current user. - * Note: the array may include password-protected albums that are not - * accessible (but are visible). - * - * @return array[Collection[Album]]|null - */ - public function get(): ?array - { - /** - * Initialize return var. - */ - $return = []; - - /** - * @var Collection[SmartAlbum] - */ - $publicAlbums = resolve(PublicIds::class)->getPublicAlbumsId(); - $smartAlbums = $this->smartFactory->makeAll(); - - foreach ($this->tag->get() as $tagAlbum) { - $smartAlbums->push($tagAlbum); - } - - /* @var SmartAlbum */ - foreach ($smartAlbums as $smartAlbum) { - if (AccessControl::can_upload() || $smartAlbum->is_public()) { - $smartAlbum->setAlbumIDs($publicAlbums); - $return[$smartAlbum->title] = $smartAlbum->toReturnArray(); - } - } - - if (empty($return)) { - return null; - } - - return $return; - } -} diff --git a/app/Actions/Albums/Tag.php b/app/Actions/Albums/Tag.php deleted file mode 100644 index afc834374d8..00000000000 --- a/app/Actions/Albums/Tag.php +++ /dev/null @@ -1,39 +0,0 @@ -sortingCol = Configs::get_value('sorting_Albums_col'); - $this->sortingOrder = Configs::get_value('sorting_Albums_order'); - } - - public function get(): Collection - { - $sql = $this->createTopleveAlbumsQuery()->where('smart', '=', true); - - return $this->customSort($sql, $this->sortingCol, $this->sortingOrder) - ->map(fn (Album $album) => $album->toTagAlbum()); - } -} diff --git a/app/Actions/Albums/Top.php b/app/Actions/Albums/Top.php index 03c49e50c8d..4f35dd8fb06 100644 --- a/app/Actions/Albums/Top.php +++ b/app/Actions/Albums/Top.php @@ -1,59 +1,115 @@ sortingCol = Configs::get_value('sorting_Albums_col'); - $this->sortingOrder = Configs::get_value('sorting_Albums_order'); + $this->albumQueryPolicy = $albumQueryPolicy; + $this->albumFactory = $albumFactory; + $this->sorting = AlbumSortingCriterion::createDefault(); } /** - * Returns an array of top-level albums and shared albums visible to - * the current user. - * Note: the array may include password-protected albums that are not + * Returns the top-level albums (but not tag albums) visible + * to the current user. + * + * If the user is authenticated, then the result differentiates between + * albums which are owned by the user and "shared" albums which the + * user does not own, but is allowed to see. + * The term "shared album" might be a little misleading here. + * Albums which are owned by the user himself may also be shared (with + * other users.) + * Actually, in this context "shared albums" means "foreign albums". + * + * Note, the result may include password-protected albums that are not * accessible (but are visible). * - * @return array[Collection[Album]] + * @return TopAlbumDTO + * + * @throws InternalLycheeException */ - public function get(): array + public function get(): TopAlbumDTO { - $return = [ - 'albums' => new BaseCollection(), - 'shared_albums' => new BaseCollection(), - ]; + // Do not eagerly load the relation `photos` for each smart album. + // On the albums overview, we only need a thumbnail for each album. + /** @var BaseCollection $smartAlbums */ + $smartAlbums = $this->albumFactory + ->getAllBuiltInSmartAlbums(false) + ->filter(fn ($smartAlbum) => Gate::check(AlbumPolicy::CAN_SEE, $smartAlbum)); + + $tagAlbumQuery = $this->albumQueryPolicy + ->applyVisibilityFilter(TagAlbum::query()->with(['access_permissions', 'owner'])); + + /** @var BaseCollection $tagAlbums */ + /** @phpstan-ignore-next-line */ + $tagAlbums = (new SortingDecorator($tagAlbumQuery)) + ->orderBy($this->sorting->column, $this->sorting->order) + ->get(); + + /** @return AlbumBuilder $query */ + $query = $this->albumQueryPolicy + ->applyVisibilityFilter(Album::query()->with(['access_permissions', 'owner'])->whereIsRoot()); - $sql = $this->createTopleveAlbumsQuery()->where('smart', '=', false); - $albumCollection = $this->customSort($sql, $this->sortingCol, $this->sortingOrder); + $userID = Auth::id(); + if ($userID !== null) { + // For authenticated users we group albums by ownership. + /** @phpstan-ignore-next-line */ + $albums = (new SortingDecorator($query)) + ->orderBy(ColumnSortingType::OWNER_ID, OrderSortingType::ASC) + ->orderBy($this->sorting->column, $this->sorting->order) + ->get(); - if (AccessControl::is_logged_in()) { - $id = AccessControl::id(); - list($return['albums'], $return['shared_albums']) = $albumCollection->partition(fn ($album) => $album->owner_id == $id); + /** + * @var BaseCollection $a + * @var BaseCollection $b + */ + list($a, $b) = $albums->partition(fn ($album) => $album->owner_id === $userID); + + return new TopAlbumDTO($smartAlbums, $tagAlbums, $a->values(), $b->values()); } else { - $return['albums'] = $albumCollection; - } + // For anonymous users we don't want to implicitly expose + // ownership via sorting. + /** @var BaseCollection */ + /** @phpstan-ignore-next-line */ + $albums = (new SortingDecorator($query)) + ->orderBy($this->sorting->column, $this->sorting->order) + ->get(); - return $return; + return new TopAlbumDTO($smartAlbums, $tagAlbums, $albums); + } } } diff --git a/app/Actions/Albums/Tree.php b/app/Actions/Albums/Tree.php deleted file mode 100644 index ca8b05feee8..00000000000 --- a/app/Actions/Albums/Tree.php +++ /dev/null @@ -1,69 +0,0 @@ -sortingCol = Configs::get_value('sorting_Albums_col'); - $this->sortingOrder = Configs::get_value('sorting_Albums_order'); - } - - public function get(): array - { - $return = []; - $PublicIds = resolve(PublicIds::class); - - $sql = Album::query() - ->where('smart', '=', false) - ->whereNotIn('id', $PublicIds->getNotAccessible()) - ->orderBy('owner_id', 'ASC'); - $albumCollection = $this->customSort($sql, $this->sortingCol, $this->sortingOrder); - - if (AccessControl::is_logged_in()) { - $id = AccessControl::id(); - list($albumCollection, $albums_shared) = $albumCollection->partition(fn ($album) => $album->owner_id == $id); - $return['shared_albums'] = $this->prepare($albums_shared->toTree()); - } - - $return['albums'] = $this->prepare($albumCollection->toTree()); - - return $return; - } - - private function prepare($albums) - { - return $albums->map(function ($album) { - $ret = [ - 'id' => strval($album->id), - 'title' => $album->title, - 'parent_id' => strval($album->parent_id), - 'thumb' => optional($album->get_thumb())->toArray(), - ]; - $ret['albums'] = $this->prepare($album->children); - - return $ret; - }); - } -} diff --git a/app/Actions/Db/BaseOptimizer.php b/app/Actions/Db/BaseOptimizer.php new file mode 100644 index 00000000000..d7e91354de2 --- /dev/null +++ b/app/Actions/Db/BaseOptimizer.php @@ -0,0 +1,78 @@ +connection = Schema::connection(null)->getConnection(); + } + + /** + * Get the kind of driver used. + * + * @param array $ret reference array for return messages + * + * @return DbDriverType|null + */ + protected function getDriverType(array &$ret): DbDriverType|null + { + $driverName = DbDriverType::tryFrom($this->connection->getDriverName()); + + $ret[] = match ($driverName) { + DbDriverType::MYSQL => 'MySql/MariaDB detected.', + DbDriverType::PGSQL => 'PostgreSQL detected.', + DbDriverType::SQLITE => 'SQLite detected.', + default => 'Warning:Unknown DBMS.', + }; + + return $driverName; + } + + /** + * Do the stuff. + * + * @return array + */ + abstract public function do(): array; + + /** + * Execute SQL statement. + * + * @param string $sql statment to be executed + * @param string $success success message + * @param array $ret reference array for return messages + * + * @return void + */ + protected function execStatement(string $sql, string $success, array &$ret): void + { + try { + DB::statement($sql); + $ret[] = $success; + // @codeCoverageIgnoreStart + } catch (\Throwable $th) { + $ret[] = 'Error: ' . $th->getMessage(); + } + // @codeCoverageIgnoreEnd + } +} diff --git a/app/Actions/Db/OptimizeDb.php b/app/Actions/Db/OptimizeDb.php new file mode 100644 index 00000000000..71b951834c3 --- /dev/null +++ b/app/Actions/Db/OptimizeDb.php @@ -0,0 +1,44 @@ + + */ + public function do(): array + { + $ret = ['Optimizing Database.']; + $driverName = $this->getDriverType($ret); + /** @var array{name:string,schema:?string,size:int,comment:?string,collation:?string,engine:?string}[] */ + $tables = Schema::getTables(); + + /** @var string|null $sql */ + $sql = match ($driverName) { + DbDriverType::MYSQL => 'OPTIMIZE TABLE ', + DbDriverType::PGSQL => 'VACUUM(FULL, ANALYZE)', + DbDriverType::SQLITE => 'VACUUM', + default => null, + }; + + if ($driverName === DbDriverType::MYSQL) { + foreach ($tables as $table) { + $this->execStatement($sql . $table['name'], $table['name'] . ' optimized.', $ret); + } + } elseif ($driverName !== null) { + $this->execStatement($sql, 'DB optimized.', $ret); + } + + return $ret; + } +} diff --git a/app/Actions/Db/OptimizeTables.php b/app/Actions/Db/OptimizeTables.php new file mode 100644 index 00000000000..88dcd4697fc --- /dev/null +++ b/app/Actions/Db/OptimizeTables.php @@ -0,0 +1,46 @@ + + */ + public function do(): array + { + $ret = ['Optimizing tables.']; + $driverName = $this->getDriverType($ret); + /** @var array{name:string,schema:?string,size:int,comment:?string,collation:?string,engine:?string}[] */ + $tables = Schema::getTables(); + + /** @var string|null $sql */ + $sql = match ($driverName) { + DbDriverType::MYSQL => 'ANALYZE TABLE ', + DbDriverType::PGSQL => 'ANALYZE ', + DbDriverType::SQLITE => 'ANALYZE ', + default => null, + }; + + if ($sql === null) { + // @codeCoverageIgnoreStart + return $ret; + // @codeCoverageIgnoreEnd + } + + foreach ($tables as $table) { + $this->execStatement($sql . $table['name'], $table['name'] . ' analyzed.', $ret); + } + + return $ret; + } +} diff --git a/app/Actions/Diagnostics/Checks/AdminUserExistsCheck.php b/app/Actions/Diagnostics/Checks/AdminUserExistsCheck.php deleted file mode 100644 index 5cdb3be590e..00000000000 --- a/app/Actions/Diagnostics/Checks/AdminUserExistsCheck.php +++ /dev/null @@ -1,17 +0,0 @@ -folders($errors); - $this->userCSS($errors); - } - - public function folders(array &$errors) - { - $paths = ['big', 'medium', 'small', 'thumb', 'import', '']; - - foreach ($paths as $path) { - $p = Storage::path($path); - if (Helpers::hasPermissions($p) === false) { - $errors[] = "Error: '" . $p . "' is missing or has insufficient read/write privileges"; - } - } - } - - public function userCSS(array &$errors) - { - $p = Storage::disk('dist')->path('user.css'); - if (Helpers::hasPermissions($p) === false) { - $errors[] = "Warning: '" . $p . "' does not exist or has insufficient read/write privileges."; - $p = Storage::disk('dist')->path(''); - if (Helpers::hasPermissions($p) === false) { - $errors[] = "Warning: '" . $p . "' has insufficient read/write privileges."; - } - } - } -} diff --git a/app/Actions/Diagnostics/Checks/ConfigSanityCheck.php b/app/Actions/Diagnostics/Checks/ConfigSanityCheck.php deleted file mode 100644 index f998a822c28..00000000000 --- a/app/Actions/Diagnostics/Checks/ConfigSanityCheck.php +++ /dev/null @@ -1,56 +0,0 @@ -get() - */ - public function __construct( - ConfigFunctions $configFunctions - ) { - $this->configFunctions = $configFunctions; - } - - public function check(array &$errors): void - { - // Load settings - $settings = Configs::get(); - - $keys_checked = [ - 'username', 'password', 'sorting_Photos', 'sorting_Albums', - 'imagick', 'skip_duplicates', 'check_for_updates', 'version', - ]; - - foreach ($keys_checked as $key) { - if (!isset($settings[$key])) { - $errors[] = 'Error: ' . $key . ' not set in database'; - } - } - - /* - * Sanity check over all the variables - */ - $this->configFunctions->sanity($errors); - - // Check dropboxKey - if (!isset($settings['dropbox_key'])) { - $errors[] - = 'Warning: Dropbox import not working. No property for dropbox_key.'; - } elseif ($settings['dropbox_key'] == '') { - $errors[] - = 'Warning: Dropbox import not working. dropbox_key is empty.'; - } - } -} diff --git a/app/Actions/Diagnostics/Checks/DBSupportCheck.php b/app/Actions/Diagnostics/Checks/DBSupportCheck.php deleted file mode 100644 index e6938999f91..00000000000 --- a/app/Actions/Diagnostics/Checks/DBSupportCheck.php +++ /dev/null @@ -1,32 +0,0 @@ -binaryName()); - if ($path == '') { - $errors[] = 'Warning: lossless_optimization set to 1 but ' . $binaryPath . $tool->binaryName() . ' not found!'; - } - } - } -} diff --git a/app/Actions/Diagnostics/Checks/IniSettingsCheck.php b/app/Actions/Diagnostics/Checks/IniSettingsCheck.php deleted file mode 100644 index 23f65f802b7..00000000000 --- a/app/Actions/Diagnostics/Checks/IniSettingsCheck.php +++ /dev/null @@ -1,81 +0,0 @@ -convert_size(ini_get('upload_max_filesize')) < $this->convert_size('30M') - ) { - $errors[] - = 'Warning: You may experience problems when uploading a photo of large size. Take a look in the FAQ for details.'; - } - if ( - $this->convert_size(ini_get('post_max_size')) < $this->convert_size('100M') - ) { - $errors[] - = 'Warning: You may experience problems when uploading a photos of large size. Take a look in the FAQ for details.'; - } - if ( - intval(ini_get('max_execution_time')) < 200 - ) { - $errors[] - = 'Warning: You may experience problems when uploading a large amount of photos. Take a look in the FAQ for details.'; - } - if (empty(ini_get('allow_url_fopen'))) { - $errors[] - = 'Warning: You may experience problems with the Dropbox- and URL-Import. Edit your php.ini and set allow_url_fopen to 1.'; - } - - // Check imagick - if (!extension_loaded('imagick')) { - $errors[] - = 'Warning: Pictures that are rotated lose their metadata! Please install Imagick to avoid that.'; - } else { - if (!isset($settings['imagick'])) { - $errors[] - = 'Warning: Pictures that are rotated lose their metadata! Please enable Imagick in settings to avoid that.'; - } - } - - if (!function_exists('exec')) { - $errors[] - = 'Warning: exec function has been disabled. You may experience some error 500, please report them to us.'; - } - } -} diff --git a/app/Actions/Diagnostics/Checks/LycheeDBVersionCheck.php b/app/Actions/Diagnostics/Checks/LycheeDBVersionCheck.php deleted file mode 100644 index ad9da915bde..00000000000 --- a/app/Actions/Diagnostics/Checks/LycheeDBVersionCheck.php +++ /dev/null @@ -1,38 +0,0 @@ -get() - */ - public function __construct( - LycheeVersion $lycheeVersion - ) { - $this->lycheeVersion = $lycheeVersion; - - $this->versions = $this->lycheeVersion->get(); - } - - public function check(array &$errors): void - { - if ($this->lycheeVersion->isRelease && $this->versions['DB']['version'] < $this->versions['Lychee']['version']) { - $errors[] = 'Error: Database is behind file versions. Please apply the migration.'; - } - } -} diff --git a/app/Actions/Diagnostics/Checks/MissingUserCheck.php b/app/Actions/Diagnostics/Checks/MissingUserCheck.php deleted file mode 100644 index c9a436397f9..00000000000 --- a/app/Actions/Diagnostics/Checks/MissingUserCheck.php +++ /dev/null @@ -1,23 +0,0 @@ -select('owner_id')->groupBy('owner_id')->pluck('owner_id'); - $photo_owners = DB::table('photos')->select('owner_id')->groupBy('owner_id')->pluck('owner_id'); - $owner_ids = $album_owners->concat($photo_owners)->unique()->values(); - foreach ($owner_ids as $owner_id) { - $candidate = User::find($owner_id); - if ($candidate == null) { - $errors[] = 'Error: A user is missing! Please create a user with id: "' . $owner_id . '"'; - } - } - } -} diff --git a/app/Actions/Diagnostics/Checks/PHPVersionCheck.php b/app/Actions/Diagnostics/Checks/PHPVersionCheck.php deleted file mode 100644 index 696c968a038..00000000000 --- a/app/Actions/Diagnostics/Checks/PHPVersionCheck.php +++ /dev/null @@ -1,64 +0,0 @@ - 7.2 = DEPRECATED = ERROR - // 28 Nov 2019 => 7.4 = RELEASED => 7.3 = WARNING - // 26 Nov 2020 => 8.0 = RELEASED => 7.4 = WARNING - //? 6 Dec 2020 => 7.3 = DEPRECATED = ERROR - $php_error = 7.4; - $php_warning = 7.4; - $php_latest = 8; - - //! 28 Nov 2021 => 7.4 = DEPRECATED = ERROR - // $php_error = 8; - // $php_warning = 8; - // $php_latest = 8; - - //! 25 Nov 2021 => 8.1 = Released => 8.0 = WARNING - // $php_error = 8; - // $php_warning = 8.1; - // $php_latest = 8.1; - - //! 26 Nov 2022 => 8.0 = DEPRECATED = ERROR - // $php_error = 8.1; - // $php_warning = 8.1; - // $php_latest = 8.1; - - if (floatval(phpversion()) < $php_latest) { - $errors[] = 'Info: Latest version of PHP is ' . $php_latest; - } - - if (floatval(phpversion()) < $php_error) { - $errors[] = 'Error: Upgrade to PHP ' . $php_warning . ' or higher'; - } - - if (floatval(phpversion()) < $php_warning) { - $errors[] = 'Warning: Upgrade to PHP ' . $php_latest . ' or higher'; - } - - // 32 or 64 bits ? - if (PHP_INT_MAX == 2147483647) { - $errors[] = 'Warning: Using 32 bit PHP, recommended upgrade to 64 bit'; - } - - // Extensions - $extensions = ['session', 'exif', 'mbstring', 'gd', 'PDO', 'json', 'zip', 'intl']; - - foreach ($extensions as $extension) { - if (!extension_loaded($extension)) { - $errors[] = 'Error: PHP ' . $extension . ' extension not activated'; - } - } - } -} diff --git a/app/Actions/Diagnostics/Configuration.php b/app/Actions/Diagnostics/Configuration.php index ed5803dd52f..f6b395c2060 100644 --- a/app/Actions/Diagnostics/Configuration.php +++ b/app/Actions/Diagnostics/Configuration.php @@ -1,47 +1,77 @@ configFunctions = $configFunctions; - } - /** * Return the config pieces of information of the Lychee installation. * Note that some information such as password and username are hidden. * - * @return array + * @return array array of messages + * + * @throws QueryBuilderException */ public function get(): array { - // Declare - $configs = []; - - try { - // Load settings - $settings = $this->configFunctions->min_info(); - foreach ($settings as $key => $value) { - if (!is_array($value) && !is_null($value)) { - $configs[] = $this->line($key . ':', $value); - } elseif (is_null($value)) { - $configs[] = 'Error: ' . $key . ' has a NULL value!'; - } - } - } catch (QueryException $e) { - $configs[] = 'Error: ' . $e->getMessage(); + if (!Schema::hasTable('configs')) { + // @codeCoverageIgnoreStart + return ['Error: migration has not been run yet.']; + // @codeCoverageIgnoreEnd } - return $configs; + // Load settings + $settings = Schema::hasColumn('configs', 'is_secret') ? $this->withIsSecret() : $this->withConfidentiality(); + + return $settings->map(function (Configs $setting) { + if (is_null($setting->value)) { + return 'Error: ' . $setting->key . ' has a NULL value!'; + } else { + return Diagnostics::line($setting->key . ':', $setting->value); + } + })->all(); + } + + /** + * This is a fail safe (legacy) in case the migration 2024_04_09_121410 has not been applied. + * + * @return Collection + * + * @throws QueryBuilderException + */ + private function withConfidentiality() + { + return Configs::query() + ->where('confidentiality', '<=', 2) + ->select(['key', 'value']) + ->orderBy('id', 'ASC') + ->get(); + } + + /** + * Normal code path. + * + * @return Collection + * + * @throws QueryBuilderException + */ + private function withIsSecret() + { + return Configs::query() + ->where('is_secret', '=', false) + ->select(['key', 'value']) + ->orderBy('id', 'ASC') + ->get(); } } diff --git a/app/Actions/Diagnostics/Diagnostics.php b/app/Actions/Diagnostics/Diagnostics.php new file mode 100644 index 00000000000..6ace493c31c --- /dev/null +++ b/app/Actions/Diagnostics/Diagnostics.php @@ -0,0 +1,29 @@ +diagnosticsChecksFactory = $diagnosticsChecksFactory; - } + /** + * The array of class pipes. + * + * @var array + */ + private array $pipes = [ + AdminUserExistsCheck::class, + BasicPermissionCheck::class, + ConfigSanityCheck::class, + DBSupportCheck::class, + GDSupportCheck::class, + ImageOptCheck::class, + IniSettingsCheck::class, + AppUrlMatchCheck::class, + MigrationCheck::class, + PHPVersionCheck::class, + TimezoneCheck::class, + UpdatableCheck::class, + ForeignKeyListInfo::class, + DBIntegrityCheck::class, + SmallMediumExistsCheck::class, + PlaceholderExistsCheck::class, + CountSizeVariantsCheck::class, + SupporterCheck::class, + ]; /** * Return the list of error which are currently breaking Lychee. * - * @return array + * @param string[] $skip class names of checks that will be skipped + * + * @return DiagnosticData[] array of messages */ - public function get(): array + public function get(array $skip = []): array { - // Declare - $errors = []; + $filteredPipes = collect($this->pipes); + $this->pipes = $filteredPipes->reject(fn ($p) => in_array((new \ReflectionClass($p))->getShortName(), $skip, true))->all(); - // @codeCoverageIgnoreStart - - $checks = $this->diagnosticsChecksFactory->makeAll(); - - foreach ($checks as $check) { - $check->check($errors); - } - // @codeCoverageIgnoreEnd + /** @var DiagnosticData[] $errors */ + $errors = []; - return $errors; + return app(Pipeline::class) + ->send($errors) + ->through($this->pipes) + ->thenReturn(); } } diff --git a/app/Actions/Diagnostics/Info.php b/app/Actions/Diagnostics/Info.php index d74ce1f91b9..8f482fa9e54 100644 --- a/app/Actions/Diagnostics/Info.php +++ b/app/Actions/Diagnostics/Info.php @@ -1,125 +1,51 @@ lycheeVersion = $lycheeVersion; - $this->versions = $this->lycheeVersion->get(); - } + /** + * The array of class pipes. + * + * @var array + */ + private $pipes = [ + VersionInfo::class, + DockerVersionInfo::class, + InstallTypeInfo::class, + SystemInfo::class, + ExtensionsInfo::class, + CountForeignKeyInfo::class, + ]; /** * get the basic pieces of information of the Lychee installation * such as version number, commit id, operating system ... * - * @return array + * @return string[] array of messages */ public function get(): array { // Declare $infos = []; - // Load settings - $settings = Configs::get(); - - // About Imagick version - $imagick = extension_loaded('imagick'); - if ($imagick === true) { - $imagickVersion = @Imagick::getVersion(); - } else { - // @codeCoverageIgnoreStart - $imagick = '-'; - // @codeCoverageIgnoreEnd - } - if ( - !isset($imagickVersion, $imagickVersion['versionNumber']) - || $imagickVersion === '' - ) { - // @codeCoverageIgnoreStart - $imagickVersion = '-'; - // @codeCoverageIgnoreEnd - } else { - $imagickVersion = $imagickVersion['versionNumber']; - } - - // About GD version - if (function_exists('gd_info')) { - $gdVersion = gd_info(); - } else { - // @codeCoverageIgnoreStart - $gdVersion = ['GD Version' => '-']; - // @codeCoverageIgnoreEnd - } - - // About SQL version - // @codeCoverageIgnoreStart - try { - switch (DB::getDriverName()) { - case 'mysql': - $dbtype = 'MySQL'; - $results = DB::select(DB::raw('select version()')); - $dbver = $results[0]->{'version()'}; - break; - case 'sqlite': - $dbtype = 'SQLite'; - $results = DB::select(DB::raw('select sqlite_version()')); - $dbver = $results[0]->{'sqlite_version()'}; - break; - case 'pgsql': - $dbtype = 'PostgreSQL'; - $results = DB::select(DB::raw('select version()')); - $dbver = $results[0]->{'version'}; - break; - default: - $dbtype = DB::getDriverName(); - $results = DB::select(DB::raw('select version()')); - $dbver = $results[0]->{'version()'}; - break; - } - } catch (QueryException $e) { - $errors[] = 'Error: ' . $e->getMessage(); - $dbtype = 'Unknown SQL'; - $dbver = 'unknown'; - } - - // @codeCoverageIgnoreEnd - - // Output system information - $infos[] = $this->line('Lychee Version (' . $this->versions['channel'] . '):', $this->lycheeVersion->format($this->versions['Lychee'])); - $infos[] = $this->line('DB Version:', $this->versions['DB']['version']); - $infos[] = ''; - $infos[] = $this->line('composer install:', $this->versions['composer']); - $infos[] = $this->line('APP_ENV:', Config::get('app.env')); // check if production - $infos[] = $this->line('APP_DEBUG:', Config::get('app.debug') ? 'true' : 'false'); // check if debug is on (will help in case of error 500) - $infos[] = ''; - $infos[] = $this->line('System:', PHP_OS); - $infos[] = $this->line('PHP Version:', floatval(phpversion())); - $infos[] = $this->line('Max uploaded file size:', ini_get('upload_max_filesize')); - $infos[] = $this->line('Max post size:', ini_get('post_max_size')); - $infos[] = $this->line($dbtype . ' Version:', $dbver); - $infos[] = ''; - $infos[] = $this->line('Imagick:', $imagick); - $infos[] = $this->line('Imagick Active:', $settings['imagick'] ?? 'key not found in settings'); - $infos[] = $this->line('Imagick Version:', $imagickVersion); - $infos[] = $this->line('GD Version:', $gdVersion['GD Version']); - - return $infos; + return app(Pipeline::class) + ->send($infos) + ->through($this->pipes) + ->thenReturn(); } } diff --git a/app/Actions/Diagnostics/Line.php b/app/Actions/Diagnostics/Line.php deleted file mode 100644 index a670a4a6e44..00000000000 --- a/app/Actions/Diagnostics/Line.php +++ /dev/null @@ -1,14 +0,0 @@ -where('may_administrate', '=', true)->count(); + if ($numberOfAdmin === 0) { + // @codeCoverageIgnoreStart + $data[] = DiagnosticData::error('User Admin not found in database. Please run: "php lychee:create_user {username} {password}"', self::class); + // @codeCoverageIgnoreEnd + } + + return $next($data); + } +} diff --git a/app/Actions/Diagnostics/Pipes/Checks/AppUrlMatchCheck.php b/app/Actions/Diagnostics/Pipes/Checks/AppUrlMatchCheck.php new file mode 100644 index 00000000000..1b4ef8d5195 --- /dev/null +++ b/app/Actions/Diagnostics/Pipes/Checks/AppUrlMatchCheck.php @@ -0,0 +1,169 @@ +splitUrl($config_url)[3]; + + $censored_bad = Helpers::censor($bad); + $censored_app_url = $this->getCensorAppUrl(); + $censored_current = $this->getCensorCurrentUrl(); + + if ($bad !== '') { + // @codeCoverageIgnoreStart + $data[] = DiagnosticData::error( + sprintf('APP_URL (%s) contains a sub-path (%s).', $censored_app_url, $censored_bad), + self::class, + [ + sprintf('Instead set APP_DIR to (%s) and APP_URL to (%s) in your .env', $censored_bad, $censored_current), + ] + ); + // @codeCoverageIgnoreEnd + } + + if ($bad !== '') { + // @codeCoverageIgnoreStart + $data[] = DiagnosticData::error( + sprintf('APP_URL (%s) contains a sub-path (%s).', $censored_app_url, $censored_bad), + self::class, + ['This may impact your WebAuthn authentication.'] + ); + // @codeCoverageIgnoreEnd + } + + if (!$this->checkUrlMatchCurrentHost()) { + // @codeCoverageIgnoreStart + $data[] = DiagnosticData::error( + sprintf('APP_URL (%s) does not match the current url (%s).', $censored_app_url, $censored_current), + self::class, + ['This will break WebAuthn authentication.'] + ); + // @codeCoverageIgnoreEnd + } + + $config_url_imgage = config('filesystems.disks.images.url'); + if ($config_url_imgage === '') { + // @codeCoverageIgnoreStart + $data[] = DiagnosticData::error( + 'LYCHEE_UPLOADS_URL is set and empty. This will prevent images to be displayed. Remove the line from your .env', + self::class + ); + // @codeCoverageIgnoreEnd + } + + if (!str_starts_with($config_url_imgage, '/') && !str_starts_with($config_url_imgage, 'http')) { + // @codeCoverageIgnoreStart + $data[] = DiagnosticData::error( + 'LYCHEE_UPLOADS_URL is set but starts with neither a / nor http.', + self::class, + ['This will prevent images from being displayed. Remove the line from your .env'] + ); + // @codeCoverageIgnoreEnd + } + + if (($config_url . $dir_url . '/uploads') === $config_url_imgage && !$this->checkUrlMatchCurrentHost()) { + // @codeCoverageIgnoreStart + $data[] = DiagnosticData::error( + sprintf('APP_URL (%s) does not match the current url (%s).', $censored_app_url, $censored_current), + self::class, + ['This will prevent images from being properly displayed.'] + ); + // @codeCoverageIgnoreEnd + } + + return $next($data); + } + + /** + * Split url into 3 parts: http(s), host, path. + * + * @param string $url + * + * @return array + * + * @throws PcreException + */ + private function splitUrl(string $url): array + { + // https://regex101.com/r/u2YAsS/1 + $pattern = '/((?:http|https)\:\/\/)?([^\/]*)(.*)?/'; + $matches = []; + preg_match($pattern, $url, $matches); + + return $matches; + } + + /** + * Get the censored version of the current URL. + * + * @return string + */ + private function getCensorCurrentUrl(): string + { + $current_url = request()->schemeAndHttpHost(); + [$full, $prefix, $good, $bad] = $this->splitUrl($current_url); + + return $prefix . Helpers::censor($good) . Helpers::censor($bad); + } + + /** + * Retrieve censored version of app.url (APP_URL). + * + * @return string + */ + private function getCensorAppUrl(): string + { + $config_url = config('app.url'); + [$full, $prefix, $good, $bad] = $this->splitUrl($config_url); + + return $prefix . Helpers::censor($good) . Helpers::censor($bad); + } + + /** + * Check if current Url matches APP_URL. + * We need to check against httpHost and with scheme as APP_URL does not necessarily contain the scheme. + * + * @return bool true if Match + */ + private function checkUrlMatchCurrentHost(): bool + { + $config_url = config('app.url'); + + return in_array($config_url, [request()->httpHost(), request()->schemeAndHttpHost()], true); + } +} diff --git a/app/Actions/Diagnostics/Pipes/Checks/BasicPermissionCheck.php b/app/Actions/Diagnostics/Pipes/Checks/BasicPermissionCheck.php new file mode 100644 index 00000000000..b795d16d195 --- /dev/null +++ b/app/Actions/Diagnostics/Pipes/Checks/BasicPermissionCheck.php @@ -0,0 +1,333 @@ + List of real paths to be anonymized + */ + protected array $realPaths = []; + + /** + * @var array Matching list of anonymized paths + */ + protected array $anonymizePaths = []; + + /** + * {@inheritDoc} + */ + public function handle(array &$data, \Closure $next): array + { + $this->folders($data); + $this->userCSS($data); + + return $next($data); + } + + /** + * Check all the folders with the correct permissions. + * + * @param DiagnosticData[] $data + * + * @return void + */ + public function folders(array &$data): void + { + if (!extension_loaded('posix')) { + // @codeCoverageIgnoreStart + return; + // @codeCoverageIgnoreEnd + } + + clearstatcache(true); + $this->numOwnerIssues = 0; + $this->numPermissionIssues = 0; + $this->numAccessIssues = 0; + try { + $groupIDsOrFalse = posix_getgroups(); + // @codeCoverageIgnoreStart + } catch (PosixException) { + $data[] = DiagnosticData::error('Could not determine groups of process', self::class); + + return; + } + // @codeCoverageIgnoreEnd + $this->groupIDs = $groupIDsOrFalse; + $this->groupIDs[] = posix_getegid(); + $this->groupIDs[] = posix_getgid(); + $this->groupIDs = array_unique($this->groupIDs); + $this->groupNames = implode(', ', array_map( + function (int $gid): string { + try { + return posix_getgrgid($gid)['name']; + // @codeCoverageIgnoreStart + } catch (PosixException) { + return ''; + } + // @codeCoverageIgnoreEnd + }, + $this->groupIDs + )); + + $disks = [ + Storage::disk(StorageDiskType::LOCAL->value), + Storage::disk(SymLink::DISK_NAME), + Storage::disk(ProcessableJobFile::DISK_NAME), + Storage::disk(PhotoController::DISK_NAME), + ]; + + foreach ($disks as $disk) { + if ($disk->getAdapter() instanceof LocalFilesystemAdapter) { + $this->checkDirectoryPermissionsRecursively($disk->path(''), $data); + } + } + + if ($this->numOwnerIssues > self::MAX_ISSUE_REPORTS_PER_TYPE) { + // @codeCoverageIgnoreStart + $data[] = DiagnosticData::warn(sprintf('%d more directories with wrong owner', $this->numOwnerIssues - self::MAX_ISSUE_REPORTS_PER_TYPE), self::class); + // @codeCoverageIgnoreEnd + } + if ($this->numPermissionIssues > self::MAX_ISSUE_REPORTS_PER_TYPE) { + // @codeCoverageIgnoreStart + $data[] = DiagnosticData::warn(sprintf('%d more directories with wrong permissions', $this->numPermissionIssues - self::MAX_ISSUE_REPORTS_PER_TYPE), self::class); + // @codeCoverageIgnoreEnd + } + if ($this->numAccessIssues > self::MAX_ISSUE_REPORTS_PER_TYPE) { + // @codeCoverageIgnoreStart + $data[] = DiagnosticData::warn(sprintf('%d more inaccessible directories', $this->numAccessIssues - self::MAX_ISSUE_REPORTS_PER_TYPE), self::class); + // @codeCoverageIgnoreEnd + } + } + + /** + * Check if user.css has the correct permissions. + * + * @param DiagnosticData[] $data + * + * @return void + */ + public function userCSS(array &$data): void + { + $p = Storage::disk('dist')->path('user.css'); + if (!Helpers::hasPermissions($p)) { + // @codeCoverageIgnoreStart + $data[] = DiagnosticData::warn(sprintf("'%s' does not exist or has insufficient read/write privileges.", $this->anonymize($p)), self::class); + + $p = Storage::disk('dist')->path(''); + if (!Helpers::hasPermissions($p)) { + $data[] = DiagnosticData::warn(sprintf("'%s' has insufficient read/write privileges.", $this->anonymize($p)), self::class); + } + // @codeCoverageIgnoreEnd + } + } + + /** + * Check permissions of (local) image directories. + * + * For efficiency reasons only the directory permissions are checked, + * not the permissions of every single file. + * + * @param string $path the path of the directory or file to check + * @param DiagnosticData[] $data the list of errors to append to + * + * @noinspection PhpComposerExtensionStubsInspection + */ + private function checkDirectoryPermissionsRecursively(string $path, array &$data): void + { + try { + if (!is_dir($path)) { + return; + } + + try { + $actualPerm = fileperms($path); + // @codeCoverageIgnoreStart + } catch (FilesystemException) { + $data[] = DiagnosticData::warn(sprintf('Unable to determine permissions for %s', $this->anonymize($path)), self::class); + + return; + } + // @codeCoverageIgnoreEnd + + // `fileperms` also returns the higher bits of the inode mode. + // Hence, we must AND it with 07777 to only get what we are + // interested in + $actualPerm &= 07777; + $owningGroupIdOrFalse = filegroup($path); + if ($owningGroupIdOrFalse !== false) { + try { + $owningGroupNameOrFalse = posix_getgrgid($owningGroupIdOrFalse); + // @codeCoverageIgnoreStart + } catch (PosixException) { + $owningGroupNameOrFalse = false; + } + // @codeCoverageIgnoreEnd + } else { + $owningGroupNameOrFalse = false; + } + /** @var string $owningGroupName */ + $owningGroupName = $owningGroupNameOrFalse === false ? '' : $owningGroupNameOrFalse['name']; + $expectedPerm = self::getConfiguredDirectoryPerm(); + + if (!in_array($owningGroupIdOrFalse, $this->groupIDs, true)) { + // @codeCoverageIgnoreStart + $this->numOwnerIssues++; + if ($this->numOwnerIssues <= self::MAX_ISSUE_REPORTS_PER_TYPE) { + $data[] = DiagnosticData::warn(sprintf('%s is owned by group %s, but should be owned by one out of %s', $this->anonymize($path), $owningGroupName, $this->groupNames), self::class); + } + // @codeCoverageIgnoreEnd + } + + if ($expectedPerm !== $actualPerm) { + // @codeCoverageIgnoreStart + $this->numPermissionIssues++; + if ($this->numPermissionIssues <= self::MAX_ISSUE_REPORTS_PER_TYPE) { + $data[] = DiagnosticData::warn(sprintf('%s has permissions %04o, but should have %04o', $this->anonymize($path), $actualPerm, $expectedPerm), self::class); + } + // @codeCoverageIgnoreEnd + } + + if (!is_writable($path) || !is_readable($path)) { + // @codeCoverageIgnoreStart + $this->numAccessIssues++; + if ($this->numAccessIssues <= self::MAX_ISSUE_REPORTS_PER_TYPE) { + $problem = match (true) { + (!is_writable($path) && !is_readable($path)) => 'neither readable nor writable', + !is_writable($path) => 'not writable', + !is_readable($path) => 'not readable', + default => '', + }; + $data[] = DiagnosticData::error(sprintf('%s is %s by %s', $this->anonymize($path), $problem, $this->groupNames), self::class); + } + // @codeCoverageIgnoreEnd + } + + $dir = new \DirectoryIterator($path); + foreach ($dir as $dirEntry) { + if ($dirEntry->isDir() && !$dirEntry->isDot()) { + $this->checkDirectoryPermissionsRecursively($dirEntry->getPathname(), $data); + } + } + // @codeCoverageIgnoreStart + } catch (\Exception $e) { + $data[] = DiagnosticData::error($e->getMessage(), self::class); + Handler::reportSafely($e); + } + // @codeCoverageIgnoreEnd + } + + /** + * @throws InvalidConfigOption + */ + public static function getConfiguredDirectoryPerm(): int + { + return self::getConfiguredPerm('dir'); + } + + /** + * @throws InvalidConfigOption + */ + public static function getConfiguredFilePerm(): int + { + return self::getConfiguredPerm('file'); + } + + /** + * @param string $type either 'dir' or 'file' + * + * @return int + * + * @phpstan-param 'dir'|'file' $type + * + * @throws InvalidConfigOption + */ + private static function getConfiguredPerm(string $type): int + { + try { + $visibility = (string) config(sprintf('filesystems.disks.%s.visibility', StorageDiskType::LOCAL->value)); + if ($visibility === '') { + // @codeCoverageIgnoreStart + throw new InvalidConfigOption('File/directory visibility not configured'); + // @codeCoverageIgnoreEnd + } + + $perm = (int) config(sprintf('filesystems.disks.%s.permissions.%s.%s', StorageDiskType::LOCAL->value, $type, $visibility)); + if ($perm === 0) { + // @codeCoverageIgnoreStart + throw new InvalidConfigOption('Configured file/directory permission is invalid'); + // @codeCoverageIgnoreEnd + } + + return $perm; + // @codeCoverageIgnoreStart + } catch (ContainerExceptionInterface|BindingResolutionException|NotFoundExceptionInterface $e) { + throw new InvalidConfigOption('Could not read configuration for file/directory permission', $e); + } + // @codeCoverageIgnoreEnd + } + + private function anonymize(string $path): string + { + if (count($this->anonymizePaths) === 0) { + $this->realPaths[] = public_path(); + $this->anonymizePaths[] = Helpers::censor(public_path(), 0.2); + $this->realPaths[] = storage_path(); + $this->anonymizePaths[] = Helpers::censor(storage_path(), 0.4); + $this->realPaths[] = config('filesystems.disks.images.root'); + $this->anonymizePaths[] = Helpers::censor(config('filesystems.disks.images.root'), 0.2); + } + + return str_replace($this->realPaths, $this->anonymizePaths, $path); + } +} diff --git a/app/Actions/Diagnostics/Pipes/Checks/ConfigSanityCheck.php b/app/Actions/Diagnostics/Pipes/Checks/ConfigSanityCheck.php new file mode 100644 index 00000000000..40b818fc194 --- /dev/null +++ b/app/Actions/Diagnostics/Pipes/Checks/ConfigSanityCheck.php @@ -0,0 +1,69 @@ +sanity($data); + $this->checkDropBoxKeyWarning($data); + + return $next($data); + } + + /** + * Warning if the Dropbox key does not exists. + * + * @param DiagnosticData[] $data + * + * @return void + */ + private function checkDropBoxKeyWarning(array &$data): void + { + $dropbox = Configs::getValueAsString('dropbox_key'); + if ($dropbox === '') { + $data[] = DiagnosticData::warn('Dropbox import not working. dropbox_key is empty.', self::class); + $data[] = DiagnosticData::info('To hide this Dropbox warning, set the dropbox_key to "disabled".', self::class); + } + } + + /** + * Sanity check of the config. + * + * @param DiagnosticData[] $return + */ + private function sanity(array &$return): void + { + $configs = Configs::all(['key', 'value', 'type_range']); + + foreach ($configs as $config) { + $message = $config->sanity($config->value); + if ($message !== '') { + $return[] = DiagnosticData::error(str_replace('Error: ', '', $message), self::class); + } + } + } +} diff --git a/app/Actions/Diagnostics/Pipes/Checks/CountSizeVariantsCheck.php b/app/Actions/Diagnostics/Pipes/Checks/CountSizeVariantsCheck.php new file mode 100644 index 00000000000..4f904122721 --- /dev/null +++ b/app/Actions/Diagnostics/Pipes/Checks/CountSizeVariantsCheck.php @@ -0,0 +1,44 @@ +where('size_variants.filesize', '=', 0)->count(); + if ($num > 0) { + // @codeCoverageIgnoreStart + $data[] = DiagnosticData::info( + sprintf('Found %d small images without filesizes.', $num), + self::class, + [sprintf('You can use `php artisan lychee:variant_filesize %d` to compute them.', $num)] + ); + // @codeCoverageIgnoreEnd + } + + return $next($data); + } +} diff --git a/app/Actions/Diagnostics/Pipes/Checks/DBIntegrityCheck.php b/app/Actions/Diagnostics/Pipes/Checks/DBIntegrityCheck.php new file mode 100644 index 00000000000..2afe2f1cac2 --- /dev/null +++ b/app/Actions/Diagnostics/Pipes/Checks/DBIntegrityCheck.php @@ -0,0 +1,48 @@ +where('size_variants.type', '=', 0); + $photos = Photo::query() + ->with(['album']) + ->select(['photos.id', 'title', 'album_id']) + ->joinSub($subJoin, 'size_variants', 'size_variants.photo_id', '=', 'photos.id', 'left') + ->whereNull('size_variants.id') + ->get(); + + foreach ($photos as $photo) { + $data[] = DiagnosticData::error('Photo without Original found -- ' . $photo->title . ' in ' . ($photo->album?->title ?? __('gallery.smart_album.unsorted')), self::class); + } + + return $next($data); + } +} \ No newline at end of file diff --git a/app/Actions/Diagnostics/Pipes/Checks/DBSupportCheck.php b/app/Actions/Diagnostics/Pipes/Checks/DBSupportCheck.php new file mode 100644 index 00000000000..75fe8787afc --- /dev/null +++ b/app/Actions/Diagnostics/Pipes/Checks/DBSupportCheck.php @@ -0,0 +1,52 @@ + $this->sqlite($data), + 'mysql' => $this->mysql($data), + 'pgsql' => $this->pgsql($data), + default => '', + }; + + return $next($data); + } + + /** + * @param DiagnosticData[] $data + * + * @return void + */ + private function sqlite(array &$data): void + { + $fks = DB::select("SELECT m.name , p.* FROM sqlite_master m JOIN pragma_foreign_key_list(m.name) p ON m.name != p.\"table\" WHERE m.type = 'table' ORDER BY m.name;"); + + foreach ($fks as $fk) { + $data[] = DiagnosticData::info( + sprintf('Foreign key: %-30s → %-20s : %s', $fk->name . '.' . $fk->from, $fk->table . '.' . $fk->to, strval($fk->on_update)), + self::class + ); + } + } + + /** + * @param DiagnosticData[] $data + * + * @return void + */ + private function mysql(array &$data): void + { + $fks = DB::select('select * +from information_schema.referential_constraints fks +join information_schema.key_column_usage kcu on fks.constraint_schema = kcu.table_schema +and fks.table_name = kcu.table_name +and fks.constraint_name = kcu.constraint_name +group by fks.constraint_schema, fks.table_name, fks.unique_constraint_schema, fks.referenced_table_name, fks.constraint_name +order by fks.constraint_schema, fks.table_name; +'); + foreach ($fks as $fk) { + $data[] = DiagnosticData::info( + sprintf('Foreign key: %-30s → %-20s : %s', $fk->TABLE_NAME . '.' . $fk->COLUMN_NAME, $fk->REFERENCED_TABLE_NAME . '.' . $fk->REFERENCED_COLUMN_NAME, strval($fk->UPDATE_RULE)), + self::class + ); + } + } + + /** + * @param DiagnosticData[] $data + * + * @return void + */ + private function pgsql(array &$data): void + { + $fks = DB::select('SELECT tc.table_schema, tc.constraint_name, tc.table_name, kcu.column_name, +ccu.table_schema AS foreign_table_schema, +ccu.table_name AS foreign_table_name, +ccu.column_name AS foreign_column_name +FROM +information_schema.table_constraints AS tc +JOIN information_schema.key_column_usage AS kcu + ON tc.constraint_name = kcu.constraint_name + AND tc.table_schema = kcu.table_schema +JOIN information_schema.constraint_column_usage AS ccu + ON ccu.constraint_name = tc.constraint_name + AND ccu.table_schema = tc.table_schema +WHERE tc.constraint_type = \'FOREIGN KEY\';'); + + foreach ($fks as $fk) { + $data[] = DiagnosticData::info(sprintf('Foreign key: %-30s → %-20s', $fk->table_name . '.' . $fk->column_name, $fk->foreign_table_name . '.' . $fk->foreign_column_name), self::class); + } + } +} diff --git a/app/Actions/Diagnostics/Pipes/Checks/GDSupportCheck.php b/app/Actions/Diagnostics/Pipes/Checks/GDSupportCheck.php new file mode 100644 index 00000000000..b1e509aea94 --- /dev/null +++ b/app/Actions/Diagnostics/Pipes/Checks/GDSupportCheck.php @@ -0,0 +1,53 @@ +binaryName()); + if ($path === '') { + $data[] = DiagnosticData::warn('lossless_optimization set to 1 but ' . $binaryPath . $tool->binaryName() . ' not found!', self::class); + } + } + } else { + $data[] = DiagnosticData::warn('lossless_optimization set to 1 but exec() is not enabled.', self::class); + } + + return $next($data); + // @codeCoverageIgnoreEnd + } +} diff --git a/app/Actions/Diagnostics/Pipes/Checks/IniSettingsCheck.php b/app/Actions/Diagnostics/Pipes/Checks/IniSettingsCheck.php new file mode 100644 index 00000000000..dbcfcb4c3cd --- /dev/null +++ b/app/Actions/Diagnostics/Pipes/Checks/IniSettingsCheck.php @@ -0,0 +1,139 @@ +verify->validate()) { + $data[] = DiagnosticData::warn('Your installation has been tampered. Please verify the integrity of your files.', self::class); + } + + if (Helpers::convertSize(ini_get('upload_max_filesize')) < Helpers::convertSize(('30M'))) { + $data[] = DiagnosticData::warn('You may experience problems when uploading a photo of large size. Take a look in the FAQ for details.', self::class); + } + if (Helpers::convertSize(ini_get('post_max_size')) < Helpers::convertSize(('100M'))) { + $data[] = DiagnosticData::warn('You may experience problems when uploading a photo of large size. Take a look in the FAQ for details.', self::class); + } + $max_execution_time = intval(ini_get('max_execution_time')); + if (0 < $max_execution_time && $max_execution_time < 200) { + // @codeCoverageIgnoreStart + $data[] = DiagnosticData::warn('You may experience problems when uploading a photo of large size or handling many/large albums. Take a look in the FAQ for details.', self::class); + // @codeCoverageIgnoreEnd + } + if (filter_var(ini_get('allow_url_fopen'), FILTER_VALIDATE_BOOLEAN) !== true) { + // @codeCoverageIgnoreStart + $data[] = DiagnosticData::warn('You may experience problems with the Dropbox- and URL-Import. Edit your php.ini and set allow_url_fopen to 1.', self::class); + // @codeCoverageIgnoreEnd + } + + // Check imagick + if (!extension_loaded('imagick')) { + // @codeCoverageIgnoreStart + $data[] = DiagnosticData::warn('Pictures that are rotated lose their metadata! Please install Imagick to avoid that.', self::class); + // @codeCoverageIgnoreEnd + } else { + if (!isset($settings['imagick'])) { + // @codeCoverageIgnoreStart + $data[] = DiagnosticData::warn('Pictures that are rotated lose their metadata! Please enable Imagick in settings to avoid that.', self::class); + // @codeCoverageIgnoreEnd + } + } + + if (!Helpers::isExecAvailable()) { + // @codeCoverageIgnoreStart + $data[] = DiagnosticData::warn('exec function has been disabled. You may experience some error 500, please report them to us.', self::class); + // @codeCoverageIgnoreEnd + } + + if (preg_match('!^[-_a-zA-Z]+/\d+(\.\d+)*[a-z]? \(.*\)!', ini_get('user_agent')) === 0) { + // @codeCoverageIgnoreStart + $data[] = DiagnosticData::warn('user_agent for PHP is not properly set. You may experience problems when importing images via URL.', self::class); + // @codeCoverageIgnoreEnd + } + + if (extension_loaded('xdebug')) { + // @codeCoverageIgnoreStart + $msg = config('app.debug') !== true + ? DiagnosticData::error('xdebug is enabled although Lychee is not in debug mode. Outside of debugging, xdebug will generate significant slowdown on your application.', self::class) + : DiagnosticData::warn('xdebug is enabled. This will generate significant slowdown on your application.', self::class); + $data[] = $msg; + // @codeCoverageIgnoreEnd + } + + if (extension_loaded('xdebug')) { + // @codeCoverageIgnoreStart + $data[] = DiagnosticData::info('xdebug mode:' . ini_get('xdebug.mode'), self::class); + $data[] = DiagnosticData::info('xdebug start_with_request:' . ini_get('xdebug.start_with_request'), self::class); + // @codeCoverageIgnoreEnd + } + + if (ini_get('assert.exception') !== '1') { + // @codeCoverageIgnoreStart + $data[] = DiagnosticData::warn('assert.exception is set to false. Lychee assumes that failing assertions throw proper exceptions.', self::class); + // @codeCoverageIgnoreEnd + } + + if (ini_get('zend.assertions') !== '-1' && config('app.debug') !== true) { + // @codeCoverageIgnoreStart + $data[] = DiagnosticData::warn('zend.assertions is enabled although Lychee is not in debug mode. Outside of debugging, code generation for assertions is recommended to be disabled for efficiency reasons', self::class); + // @codeCoverageIgnoreEnd + } + + if (ini_get('zend.assertions') !== '1' && config('app.debug') === true) { + $data[] = DiagnosticData::warn('zend.assertions is disabled although Lychee is in debug mode. For easier debugging code generation for assertions should be enabled.', self::class); + } + + $disabledFunctions = explode(',', ini_get('disable_functions')); + $tmpfileExists = function_exists('tmpfile') && !in_array('tmpfile', $disabledFunctions, true); + if ($tmpfileExists !== true) { + // @codeCoverageIgnoreStart + $data[] = DiagnosticData::error('tmpfile() is disabled, this will prevent you from uploading pictures.', self::class); + // @codeCoverageIgnoreEnd + } + + $path = sys_get_temp_dir(); + if (!is_writable($path)) { + // @codeCoverageIgnoreStart + $data[] = DiagnosticData::error('sys_get_temp_dir() is not writable, this will prevent you from uploading pictures.', self::class); + // @codeCoverageIgnoreEnd + } + if (!is_readable($path)) { + // @codeCoverageIgnoreStart + $data[] = DiagnosticData::error('sys_get_temp_dir() is not readable, this will prevent you from uploading pictures.', self::class); + // @codeCoverageIgnoreEnd + } + + return $next($data); + } +} diff --git a/app/Actions/Diagnostics/Pipes/Checks/MigrationCheck.php b/app/Actions/Diagnostics/Pipes/Checks/MigrationCheck.php new file mode 100644 index 00000000000..0bd159e68a6 --- /dev/null +++ b/app/Actions/Diagnostics/Pipes/Checks/MigrationCheck.php @@ -0,0 +1,62 @@ +isInFuture()) { + // @codeCoverageIgnoreStart + $data[] = DiagnosticData::warn('Database is in advance of file version. Please check your installation.', self::class); + // @codeCoverageIgnoreEnd + } + + return $next($data); + } + + public static function isUpToDate(): bool + { + $installedVersion = resolve(InstalledVersion::class); + $fileVersion = resolve(FileVersion::class); + + $db_ver = $installedVersion->getVersion(); + $file_ver = $fileVersion->getVersion(); + + return $db_ver->toInteger() === $file_ver->toInteger(); + } + + private function isInFuture(): bool + { + $installedVersion = resolve(InstalledVersion::class); + $fileVersion = resolve(FileVersion::class); + + $db_ver = $installedVersion->getVersion(); + $file_ver = $fileVersion->getVersion(); + + return $db_ver->toInteger() > $file_ver->toInteger(); + } +} diff --git a/app/Actions/Diagnostics/Pipes/Checks/PHPVersionCheck.php b/app/Actions/Diagnostics/Pipes/Checks/PHPVersionCheck.php new file mode 100644 index 00000000000..c1d56bfdb6f --- /dev/null +++ b/app/Actions/Diagnostics/Pipes/Checks/PHPVersionCheck.php @@ -0,0 +1,111 @@ +checkPhpVersion($data); + $this->check32Bits($data); + $this->checkExtensions($data); + + return $next($data); + } + + /** + * @param DiagnosticData[] $data + * + * @return void + */ + private function checkPhpVersion(array &$data): void + { + // As we cannot test this as those are just raising warnings which we cannot check via CICD. + // I hereby solemnly declare this code as covered ! + if (floatval(phpversion()) <= self::PHP_ERROR) { + // @codeCoverageIgnoreStart + $data[] = DiagnosticData::error('Upgrade to PHP ' . self::PHP_WARNING . ' or higher', self::class); + // @codeCoverageIgnoreEnd + } elseif (floatval(phpversion()) < self::PHP_WARNING) { + // @codeCoverageIgnoreStart + $data[] = DiagnosticData::warn('Upgrade to PHP ' . self::PHP_LATEST . ' or higher', self::class); + // @codeCoverageIgnoreEnd + } elseif (floatval(phpversion()) < self::PHP_LATEST) { + $data[] = DiagnosticData::info('Latest version of PHP is ' . self::PHP_LATEST, self::class); + } + } + + /** + * @param DiagnosticData[] $data + * + * @return void + */ + private function check32Bits(array &$data): void + { + // 32 or 64 bits ? + if (PHP_INT_MAX === 2147483647) { + // @codeCoverageIgnoreStart + $data[] = DiagnosticData::warn('Using 32 bit PHP, recommended upgrade to 64 bit', self::class); + // @codeCoverageIgnoreEnd + } + } + + /** + * @param DiagnosticData[] $data + * + * @return void + */ + private function checkExtensions(array &$data): void + { + // Extensions + $extensions = [ + 'bcmath', // Required by Laravel + 'ctype', // Required by Laravel + 'dom', // Required by dependencies + 'exif', + 'fileinfo', // Required by Laravel + 'filter', // Required by dependencies + 'gd', + 'json', // Required by Laravel + 'libxml', // Required by dependencies + 'mbstring', // Required by Laravel + 'openssl', // Required by Laravel + 'pcre', // Required by dependencies + 'PDO', // Required by Laravel + 'Phar', // Required by dependencies + 'SimpleXML', // Required by dependencies + 'tokenizer', // Required by Laravel + 'xml', // Required by Laravel + 'xmlwriter', // Required by dependencies + ]; + + foreach ($extensions as $extension) { + if (!extension_loaded($extension)) { + // @codeCoverageIgnoreStart + $data[] = DiagnosticData::error('PHP ' . $extension . ' extension not activated', self::class); + // @codeCoverageIgnoreEnd + } + } + } +} diff --git a/app/Actions/Diagnostics/Pipes/Checks/PlaceholderExistsCheck.php b/app/Actions/Diagnostics/Pipes/Checks/PlaceholderExistsCheck.php new file mode 100644 index 00000000000..e28cd141809 --- /dev/null +++ b/app/Actions/Diagnostics/Pipes/Checks/PlaceholderExistsCheck.php @@ -0,0 +1,79 @@ +isEnabledByConfiguration(SizeVariantType::PLACEHOLDER)) { + return $next($data); + } + + /** @var object{num_placeholder:int,max_num_placeholder:int,num_unencoded_placeholder:int}> $result */ + $result = DB::query() + ->selectSub( + SizeVariant::query() + ->selectRaw('COUNT(*)') + ->where('type', '=', SizeVariantType::PLACEHOLDER), + 'num_placeholder' + ) + ->selectSub( + SizeVariant::query() + ->selectRaw('COUNT(*)') + ->where('type', '=', SizeVariantType::ORIGINAL), + 'max_num_placeholder' + ) + ->selectSub( + SizeVariant::query() + ->selectRaw('COUNT(*)') + ->where('short_path', 'LIKE', '%placeholder/%'), + 'num_unencoded_placeholder' + ) + ->first(); + + $num = $result->num_unencoded_placeholder; + if ($num > 0) { + $data[] = DiagnosticData::info(sprintf(self::INFO_MSG_UNENCODED, $num), self::class, [sprintf(self::INFO_LINE_UNENCODED, $num)]); + } + + $num = $result->max_num_placeholder - $result->num_placeholder; + if ($num > 0) { + $data[] = DiagnosticData::info(sprintf(self::INFO_MSG_MISSING, $num), self::class, [sprintf(self::INFO_LINE_MISSING, $num)]); + } + + return $next($data); + } +} \ No newline at end of file diff --git a/app/Actions/Diagnostics/Pipes/Checks/SmallMediumExistsCheck.php b/app/Actions/Diagnostics/Pipes/Checks/SmallMediumExistsCheck.php new file mode 100644 index 00000000000..dc1b62b0e51 --- /dev/null +++ b/app/Actions/Diagnostics/Pipes/Checks/SmallMediumExistsCheck.php @@ -0,0 +1,154 @@ +selectSub( + SizeVariant::query() + ->selectRaw('COUNT(*)') + ->where('type', '=', SizeVariantType::SMALL), + self::NUM_SMALL + ) + ->selectSub( + SizeVariant::query() + ->selectRaw('COUNT(*)') + ->where('type', '=', SizeVariantType::SMALL2X), + self::NUM_SMALL2X + ) + ->selectSub( + SizeVariant::query() + ->selectRaw('COUNT(*)') + ->where('type', '=', SizeVariantType::MEDIUM), + self::NUM_MEDIUM + ) + ->selectSub( + SizeVariant::query() + ->selectRaw('COUNT(*)') + ->where('type', '=', SizeVariantType::MEDIUM2X), + self::NUM_MEDIUM2X + ) + ->selectSub( + SizeVariant::query() + ->selectRaw('COUNT(*)') + ->where(fn ($q) => $q + ->when($svHelpers->getMaxWidth(SizeVariantType::SMALL) !== 0, fn ($q1) => $q1->where('width', '>', $svHelpers->getMaxWidth(SizeVariantType::SMALL))) + ->when($svHelpers->getMaxHeight(SizeVariantType::SMALL) !== 0, fn ($q2) => $q2->orWhere('height', '>', $svHelpers->getMaxHeight(SizeVariantType::SMALL))) + ) + ->where('type', '=', SizeVariantType::ORIGINAL), + self::MAX_NUM_SMALL + ) + ->selectSub( + SizeVariant::query() + ->selectRaw('COUNT(*)') + ->where(fn ($q) => $q + ->when($svHelpers->getMaxWidth(SizeVariantType::SMALL2X) !== 0, fn ($q1) => $q1->where('width', '>', $svHelpers->getMaxWidth(SizeVariantType::SMALL2X))) + ->when($svHelpers->getMaxHeight(SizeVariantType::SMALL2X) !== 0, fn ($q2) => $q2->orWhere('height', '>', $svHelpers->getMaxHeight(SizeVariantType::SMALL2X))) + ) + ->where('type', '=', SizeVariantType::ORIGINAL), + self::MAX_NUM_SMALL2X + ) + ->selectSub( + SizeVariant::query() + ->selectRaw('COUNT(*)') + ->where(fn ($q) => $q + ->when($svHelpers->getMaxWidth(SizeVariantType::MEDIUM) !== 0, fn ($q1) => $q1->where('width', '>', $svHelpers->getMaxWidth(SizeVariantType::MEDIUM))) + ->when($svHelpers->getMaxHeight(SizeVariantType::MEDIUM) !== 0, fn ($q2) => $q2->orWhere('height', '>', $svHelpers->getMaxHeight(SizeVariantType::MEDIUM))) + ) + ->where('type', '=', SizeVariantType::ORIGINAL), + self::MAX_NUM_MEDIUM + ) + ->selectSub( + SizeVariant::query() + ->selectRaw('COUNT(*)') + ->where(fn ($q) => $q + ->when($svHelpers->getMaxWidth(SizeVariantType::MEDIUM2X) !== 0, fn ($q1) => $q1->where('width', '>', $svHelpers->getMaxWidth(SizeVariantType::MEDIUM2X))) + ->when($svHelpers->getMaxHeight(SizeVariantType::MEDIUM2X) !== 0, fn ($q2) => $q2->orWhere('height', '>', $svHelpers->getMaxHeight(SizeVariantType::MEDIUM2X))) + ) + ->where('type', '=', SizeVariantType::ORIGINAL), + self::MAX_NUM_MEDIUM2X + ) + ->first(); + + $num = $result->{self::MAX_NUM_SMALL} - $result->{self::NUM_SMALL}; // @phpstan-ignore-line + if ($num > 0) { + $data[] = DiagnosticData::info( + sprintf(self::INFO_MSG, $num, SizeVariantType::SMALL->name()), + self::class, + [sprintf(self::INFO_LINE, SizeVariantType::SMALL->name(), $num)] + ); + } + + $num = $result->{self::MAX_NUM_SMALL2X} - $result->{self::NUM_SMALL2X}; // @phpstan-ignore-line + if ($num > 0 && $svHelpers->isEnabledByConfiguration(SizeVariantType::SMALL2X)) { + $data[] = DiagnosticData::info( + sprintf(self::INFO_MSG, $num, SizeVariantType::SMALL2X->name()), + self::class, + [sprintf(self::INFO_LINE, SizeVariantType::SMALL2X->name(), $num)] + ); + } + + $num = $result->{self::MAX_NUM_MEDIUM} - $result->{self::NUM_MEDIUM}; // @phpstan-ignore-line + if ($num > 0) { + $data[] = DiagnosticData::info( + sprintf(self::INFO_MSG, $num, SizeVariantType::MEDIUM->name()), + self::class, + [sprintf(self::INFO_LINE, SizeVariantType::MEDIUM->name(), $num)] + ); + } + + $num = $result->{self::MAX_NUM_MEDIUM2X} - $result->{self::NUM_MEDIUM2X}; // @phpstan-ignore-line + if ($num > 0 && $svHelpers->isEnabledByConfiguration(SizeVariantType::MEDIUM2X)) { + $data[] = DiagnosticData::info( + sprintf(self::INFO_MSG, $num, SizeVariantType::MEDIUM2X->name()), + self::class, + [sprintf(self::INFO_LINE, SizeVariantType::MEDIUM2X->name(), $num)] + ); + } + + return $next($data); + } +} diff --git a/app/Actions/Diagnostics/Pipes/Checks/SupporterCheck.php b/app/Actions/Diagnostics/Pipes/Checks/SupporterCheck.php new file mode 100644 index 00000000000..de756a5b1c7 --- /dev/null +++ b/app/Actions/Diagnostics/Pipes/Checks/SupporterCheck.php @@ -0,0 +1,49 @@ +verify = $verify; + } + + /** + * {@inheritDoc} + */ + public function handle(array &$data, \Closure $next): array + { + if (Configs::getValueAsBool('disable_se_call_for_actions')) { + return $next($data); + } + + if (!$this->verify->is_supporter()) { + // @codeCoverageIgnoreStart + $data[] = DiagnosticData::info('Have you considered supporting Lychee? :)', self::class); + // @codeCoverageIgnoreEnd + } + + return $next($data); + } +} \ No newline at end of file diff --git a/app/Actions/Diagnostics/Pipes/Checks/TimezoneCheck.php b/app/Actions/Diagnostics/Pipes/Checks/TimezoneCheck.php new file mode 100644 index 00000000000..b0bef01136c --- /dev/null +++ b/app/Actions/Diagnostics/Pipes/Checks/TimezoneCheck.php @@ -0,0 +1,43 @@ +getName(); + $tzArray = explode('/', $timezoneName); + + if (count($tzArray) !== 2 || $tzArray[0] === 'Etc') { + $data[] = DiagnosticData::warn('Default timezone not properly set; you might experience strange results when importing photos without explicit EXIF timezone', self::class); + } + + return $next($data); + } +} diff --git a/app/Actions/Diagnostics/Pipes/Checks/UpdatableCheck.php b/app/Actions/Diagnostics/Pipes/Checks/UpdatableCheck.php new file mode 100644 index 00000000000..bf73f80a0c3 --- /dev/null +++ b/app/Actions/Diagnostics/Pipes/Checks/UpdatableCheck.php @@ -0,0 +1,103 @@ +installedVersion = $installedVersion; + } + + /** + * {@inheritDoc} + */ + public function handle(array &$data, \Closure $next): array + { + if (!$this->installedVersion->isRelease()) { + try { + self::assertUpdatability(); + // @codeCoverageIgnoreStart + } catch (ExternalComponentMissingException $e) { + $data[] = DiagnosticData::info($e->getMessage(), self::class); + } catch (ConfigurationException $e) { + $data[] = DiagnosticData::warn($e->getMessage(), self::class); + } catch (InsufficientFilesystemPermissions|VersionControlException $e) { + $data[] = DiagnosticData::error($e->getMessage(), self::class); + } + // @codeCoverageIgnoreEnd + } + + return $next($data); + } + + /** + * Here we throw an exception if we cannot apply an update. + * + * @return void + */ + public static function assertUpdatability(): void + { + $installedVersion = resolve(InstalledVersion::class); + + // we bypass this because we don't care about the other conditions as they don't apply to the release + if ($installedVersion->isRelease()) { + // @codeCoverageIgnoreStart + return; + // @codeCoverageIgnoreEnd + } + if (!Schema::hasTable('configs')) { + // @codeCoverageIgnoreStart + throw new ConfigurationException('Migration is not run'); + // @codeCoverageIgnoreEnd + } + + if (!Configs::getValueAsBool('allow_online_git_pull')) { + throw new ConfigurationException('Online updates are disabled by configuration'); + } + + // When going with the CI, .git is always executable + if (Helpers::isExecAvailable() && exec('command -v git') === '') { + // @codeCoverageIgnoreStart + throw new ExternalComponentMissingException('git (software) is not available.'); + // @codeCoverageIgnoreEnd + } + + $gitHubFunctions = resolve(GitHubVersion::class); + $gitHubFunctions->hydrate(false); + + if (!$gitHubFunctions->hasPermissions()) { + // @codeCoverageIgnoreStart + throw new InsufficientFilesystemPermissions(Helpers::censor(base_path('.git'), 1 / 4) . ' (and subdirectories) are not executable, check the permissions'); + // @codeCoverageIgnoreEnd + } + } +} \ No newline at end of file diff --git a/app/Actions/Diagnostics/Pipes/Infos/CountForeignKeyInfo.php b/app/Actions/Diagnostics/Pipes/Infos/CountForeignKeyInfo.php new file mode 100644 index 00000000000..c0508814843 --- /dev/null +++ b/app/Actions/Diagnostics/Pipes/Infos/CountForeignKeyInfo.php @@ -0,0 +1,89 @@ + $this->sqlite($data), + 'mysql' => $this->mysql($data), + 'pgsql' => $this->pgsql($data), + default => '', + }; + + return $next($data); + } + + /** + * @param array $data + * + * @return void + */ + private function sqlite(array &$data): void + { + $fks = DB::select("SELECT m.name , p.* FROM sqlite_master m JOIN pragma_foreign_key_list(m.name) p ON m.name != p.\"table\" WHERE m.type = 'table' ORDER BY m.name;"); + $data[] = Diagnostics::line('Number of foreign key:', sprintf('%d found.', count($fks))); + } + + /** + * @param array $data + * + * @return void + */ + private function mysql(array &$data): void + { + $fks = DB::select('select * +from information_schema.referential_constraints fks +join information_schema.key_column_usage kcu on fks.constraint_schema = kcu.table_schema + and fks.table_name = kcu.table_name + and fks.constraint_name = kcu.constraint_name + group by fks.constraint_schema, fks.table_name, fks.unique_constraint_schema, fks.referenced_table_name, fks.constraint_name + order by fks.constraint_schema, fks.table_name; + '); + + $data[] = Diagnostics::line('Number of foreign key:', sprintf('%d found.', count($fks))); + } + + /** + * @param array $data + * + * @return void + */ + private function pgsql(array &$data): void + { + $fks = DB::select('SELECT tc.table_schema, tc.constraint_name, tc.table_name, kcu.column_name, +ccu.table_schema AS foreign_table_schema, +ccu.table_name AS foreign_table_name, +ccu.column_name AS foreign_column_name +FROM +information_schema.table_constraints AS tc +JOIN information_schema.key_column_usage AS kcu + ON tc.constraint_name = kcu.constraint_name + AND tc.table_schema = kcu.table_schema +JOIN information_schema.constraint_column_usage AS ccu + ON ccu.constraint_name = tc.constraint_name + AND ccu.table_schema = tc.table_schema +WHERE tc.constraint_type = \'FOREIGN KEY\';'); + + $data[] = Diagnostics::line('Number of foreign key:', sprintf('%d found.', count($fks))); + } +} + diff --git a/app/Actions/Diagnostics/Pipes/Infos/DockerVersionInfo.php b/app/Actions/Diagnostics/Pipes/Infos/DockerVersionInfo.php new file mode 100644 index 00000000000..7456d59048c --- /dev/null +++ b/app/Actions/Diagnostics/Pipes/Infos/DockerVersionInfo.php @@ -0,0 +1,67 @@ +isDocker()) { + $docker = match (true) { + $this->isLinuxServer() => 'linuxserver.io', + $this->isLycheeOrg() => 'lycheeorg', + default => 'custom', + }; + } + + $data[] = Diagnostics::line('Docker:', $docker); + + return $next($data); + } + + /** + * Check if we are running in Docker. + * + * @return bool + */ + public function isDocker(): bool + { + return is_file('/.dockerenv'); + } + + /** + * Check if we are running in Docker. + * + * @return bool + */ + private function isLinuxServer(): bool + { + return is_file('/build_version'); + } + + /** + * Check if we are running in Docker. + * + * @return bool + */ + private function isLycheeOrg(): bool + { + return is_file(base_path('/docker_target')); + } +} diff --git a/app/Actions/Diagnostics/Pipes/Infos/ExtensionsInfo.php b/app/Actions/Diagnostics/Pipes/Infos/ExtensionsInfo.php new file mode 100644 index 00000000000..c38cade2a13 --- /dev/null +++ b/app/Actions/Diagnostics/Pipes/Infos/ExtensionsInfo.php @@ -0,0 +1,63 @@ + '-']; + // @codeCoverageIgnoreEnd + } + + $data[] = Diagnostics::line('exec() Available:', Helpers::isExecAvailable() ? 'yes' : 'no'); + $data[] = Diagnostics::line('Imagick Available:', (string) $imagick); + $data[] = Diagnostics::line('Imagick Enabled:', $settings['imagick'] ?? 'key not found in settings'); + $data[] = Diagnostics::line('Imagick Version:', (string) $imagickVersion); + $data[] = Diagnostics::line('GD Version:', $gdVersion['GD Version']); + + return $next($data); + } +} diff --git a/app/Actions/Diagnostics/Pipes/Infos/InstallTypeInfo.php b/app/Actions/Diagnostics/Pipes/Infos/InstallTypeInfo.php new file mode 100644 index 00000000000..024077854f0 --- /dev/null +++ b/app/Actions/Diagnostics/Pipes/Infos/InstallTypeInfo.php @@ -0,0 +1,46 @@ +installedVersion->isDev() ? 'dev' : '--no-dev') . ($this->verify->validate() ? '' : '*')); + $data[] = Diagnostics::line('APP_ENV:', config('app.env')); // check if production + $data[] = Diagnostics::line('APP_DEBUG:', config('app.debug') === true ? 'true' : 'false'); // check if debug is on (will help in case of error 500) + $data[] = Diagnostics::line('APP_URL:', config('app.url') !== 'http://localhost' ? 'set' : 'default'); // Some people leave that value by default... It is now breaking their visual. + $data[] = Diagnostics::line('APP_DIR:', config('app.dir_url') !== '' ? 'set' : 'default'); // Some people leave that value by default... It is now breaking their visual. + $data[] = Diagnostics::line('LOG_VIEWER_ENABLED:', Features::when('log-viewer', 'true', 'false')); + $data[] = Diagnostics::line('VUEJS_ENABLED:', Features::when('vuejs', 'true', 'false')); + $data[] = Diagnostics::line('PHOTO_PIPES:', Features::when('create-photo-via-pipes', 'true', 'false')); + $data[] = ''; + + return $next($data); + } +} diff --git a/app/Actions/Diagnostics/Pipes/Infos/SystemInfo.php b/app/Actions/Diagnostics/Pipes/Infos/SystemInfo.php new file mode 100644 index 00000000000..57463658a0a --- /dev/null +++ b/app/Actions/Diagnostics/Pipes/Infos/SystemInfo.php @@ -0,0 +1,72 @@ + ['MySQL', 'select version() as version'], + 'sqlite' => ['SQLite', 'select sqlite_version() as version'], + 'pgsql' => ['PostgreSQL', 'select version() as version'], + default => [DB::getDriverName(), 'select version() as version'], + }; + + $dbtype = $sql[0]; + + $pdo = DB::connection()->getPdo(); + $statement = $pdo->query($sql[1]); + if ($statement !== false) { + $dbver = (string) $statement->fetchColumn(); + } else { + $dbver = 'unknown'; + } + } catch (QueryException $e) { + $dbtype = 'Unknown SQL'; + $dbver = 'unknown'; + } + + // @codeCoverageIgnoreEnd + + // Output system information + $data[] = Diagnostics::line('System:', PHP_OS); + $data[] = Diagnostics::line('PHP Version:', phpversion()); + $data[] = Diagnostics::line('PHP User agent:', ini_get('user_agent')); + /** @var CarbonTimeZone|null $timeZone */ + $timeZone = CarbonTimeZone::create(config('app.timezone')); + $data[] = Diagnostics::line('Timezone:', $timeZone?->getName() ?? 'undefined'); + $data[] = Diagnostics::line('Max uploaded file size:', ini_get('upload_max_filesize')); + $data[] = Diagnostics::line('Max post size:', ini_get('post_max_size')); + $data[] = Diagnostics::line('Chunk size:', Helpers::getSymbolByQuantity(UploadConfig::getUploadLimit())); + $data[] = Diagnostics::line('Max execution time: ', ini_get('max_execution_time')); + $data[] = Diagnostics::line($dbtype . ' Version:', $dbver); + $data[] = ''; + + return $next($data); + } +} diff --git a/app/Actions/Diagnostics/Pipes/Infos/VersionInfo.php b/app/Actions/Diagnostics/Pipes/Infos/VersionInfo.php new file mode 100644 index 00000000000..3aeb9cb70dc --- /dev/null +++ b/app/Actions/Diagnostics/Pipes/Infos/VersionInfo.php @@ -0,0 +1,104 @@ +fileVersion->hydrate(withRemote: false); + } + + /** + * {@inheritDoc} + */ + public function handle(array &$data, \Closure $next): array + { + /** @var VersionChannelType $channelName */ + $channelName = $this->getChannelName(); + $lycheeInfoString = $this->fileVersion->getVersion()->toString(); + + if ($channelName !== VersionChannelType::RELEASE) { + if ($this->gitHubFunctions->localHead !== null) { + $gitInfo = new LycheeGitInfo($this->gitHubFunctions); + $lycheeInfoString = $gitInfo->toString(); + } else { + // @codeCoverageIgnoreStart + $lycheeInfoString = 'No git data found.'; + // @codeCoverageIgnoreEnd + } + } + + $data[] = Diagnostics::line($this->getVersionString() . ' (' . $channelName->value . '):', $lycheeInfoString); + $data[] = Diagnostics::line('DB Version:', $this->installedVersion->getVersion()->toString()); + $data[] = ''; + + return $next($data); + } + + /** + * Get channel name. + * + * @return VersionChannelType + */ + public function getChannelName() + { + $lycheeChannelName = VersionChannelType::RELEASE; + + if (!$this->installedVersion->isRelease()) { + $this->gitHubFunctions->hydrate(withRemote: true, useCache: true); + $lycheeChannelName = $this->gitHubFunctions->isRelease() ? VersionChannelType::TAG : VersionChannelType::GIT; + } + + return $lycheeChannelName; + } + + /** + * Retrieve the version string. + * + * SE for supporter edition + * Plus for premium edition + * The star marks a tampered installation + * + * @return string + */ + private function getVersionString(): string + { + $lychee_version = 'Lychee'; + $lychee_version .= match ($this->verify->get_status()) { + Status::SUPPORTER_EDITION => ' SE', + Status::PLUS_EDITION => ' Plus', + default => '', + }; + + if (!$this->verify->validate()) { + $lychee_version .= '*'; + } + $lychee_version .= ' Version'; + + return $lychee_version; + } +} diff --git a/app/Actions/Diagnostics/Space.php b/app/Actions/Diagnostics/Space.php index 3af15ea5151..6701ab59714 100644 --- a/app/Actions/Diagnostics/Space.php +++ b/app/Actions/Diagnostics/Space.php @@ -1,15 +1,18 @@ line('Lychee total space:', $this->diskUsage->get_lychee_space()); - $infos[] = $this->line('Upload folder space:', $this->diskUsage->get_lychee_upload_space()); - $infos[] = $this->line('System total space:', $this->diskUsage->get_total_space()); - $infos[] = $this->line('System free space:', $this->diskUsage->get_free_space() . ' (' + $infos = []; + $infos[] = Diagnostics::line('Lychee total space:', $this->diskUsage->get_lychee_space()); + $infos[] = Diagnostics::line('Upload folder space:', $this->diskUsage->get_lychee_upload_space()); + $infos[] = Diagnostics::line('System total space:', $this->diskUsage->get_total_space()); + $infos[] = Diagnostics::line('System free space:', $this->diskUsage->get_free_space() . ' (' . $this->diskUsage->get_free_percent() . ')'); return $infos; diff --git a/app/Actions/HoneyPot/BasePipe.php b/app/Actions/HoneyPot/BasePipe.php new file mode 100644 index 00000000000..8ba647943e9 --- /dev/null +++ b/app/Actions/HoneyPot/BasePipe.php @@ -0,0 +1,66 @@ +throwNotFound($path); + } +} diff --git a/app/Actions/HoneyPot/EnvAccessTentative.php b/app/Actions/HoneyPot/EnvAccessTentative.php new file mode 100644 index 00000000000..bad36a2e261 --- /dev/null +++ b/app/Actions/HoneyPot/EnvAccessTentative.php @@ -0,0 +1,29 @@ +throwTeaPot($path); + } + + $next($path); + } +} diff --git a/app/Actions/HoneyPot/FlaggedPathsAccessTentative.php b/app/Actions/HoneyPot/FlaggedPathsAccessTentative.php new file mode 100644 index 00000000000..848476d099c --- /dev/null +++ b/app/Actions/HoneyPot/FlaggedPathsAccessTentative.php @@ -0,0 +1,48 @@ + $honeypot_paths_array */ + $honeypot_paths_array = config('honeypot.paths', []); + + /** @var array,suffix:array}> $honeypot_xpaths_array */ + $honeypot_xpaths_array = config('honeypot.xpaths', []); + + foreach ($honeypot_xpaths_array as $xpaths) { + foreach ($xpaths['prefix'] as $prefix) { + foreach ($xpaths['suffix'] as $suffix) { + $honeypot_paths_array[] = $prefix . $suffix; + } + } + } + + // Turn the path array into a regex pattern. + // We escape . and / to avoid confusions with other regex characters + $honeypot_paths = '/^(' . str_replace(['.', '/'], ['\.', '\/'], implode('|', $honeypot_paths_array)) . ')/i'; + + // If the user tries to access a honeypot path, fail with the teapot code. + if (preg_match($honeypot_paths, $path) !== 0) { + $this->throwTeaPot($path); + } + + $next($path); + } +} diff --git a/app/Actions/HoneyPot/HoneyIsActive.php b/app/Actions/HoneyPot/HoneyIsActive.php new file mode 100644 index 00000000000..885ef0b7476 --- /dev/null +++ b/app/Actions/HoneyPot/HoneyIsActive.php @@ -0,0 +1,28 @@ +throwNotFound($path); + } + + $next($path); + } +} diff --git a/app/Actions/Import/Exec.php b/app/Actions/Import/Exec.php index 51ef4a6018f..8caba500273 100644 --- a/app/Actions/Import/Exec.php +++ b/app/Actions/Import/Exec.php @@ -1,144 +1,225 @@ raw_formats = explode('|', strtolower(Configs::get_value('raw_formats', ''))); + Session::forget('cancel'); + $this->importMode = $importMode; + $this->photoCreate = new PhotoCreate($importMode, $intendedOwnerId); + $this->albumCreate = new AlbumCreate($intendedOwnerId); + $this->enableCLIFormatting = $enableCLIFormatting; + $this->memLimit = $memLimit; } /** - * Output status update to stdout (from where StreamedResponse picks it up). - * Every line of output is terminated with a newline so that the front end - * can be sure that it's complete. - * The status can be one of: - * - Status: : - * (A status is always sent for 0 and 100 percent at least). - * - Problem: : - * (We avoid starting a line with 'Error' as that has a special meaning - * in the front end, preventing the completion callback from being - * invoked). - * - Warning: Approaching memory limit. + * Output status update to stdout. + * + * The output is either sent to a web-client via {@link StreamedResponse} + * or to the CLI. + * + * For web-clients this method reports JSON objects. + * The outer caller precedes and terminates the whole output by `[` and + * `]`, resp., in order to indicate the start and end of a JSON + * array. + * This method also inserts the commas between objects. + * + * For CLI output we print lines terminated by a newline character. + * + * If the `ImportReport` carries an exception, the exception is logged + * via the standard mechanism of the exception handler. + * + * @param BaseImportReport $report the report + * + * @return void */ - private function status_update(string $status) + private function report(BaseImportReport $report): void { - if (!$this->statusCLIFormatting) { - // We append a newline to the status string, JSON-encode the - // result, and strip the surrounding '"' characters since this - // isn't a complete JSON string yet. - echo substr(json_encode($status . "\n"), 1, -1); - if (ob_get_level() > 0) { - ob_flush(); + if (!$this->enableCLIFormatting) { + try { + if ($this->firstReportGiven) { + echo ','; + } + echo $report->toJson(); + $this->firstReportGiven = true; + if (ob_get_level() > 0) { + ob_flush(); + } + flush(); + } catch (JsonEncodingException) { + // do nothing } - flush(); } else { - echo substr($status, strpos($status, ' ') + 1) . PHP_EOL; + echo $report->toCLIString() . PHP_EOL; } - } - private function status_progress(string $path, string $msg) - { - $this->status_update('Status: ' . $path . ': ' . $msg); - } - - private function status_warning(string $msg) - { - $this->status_update('Warning: ' . $msg); + if ($report instanceof ImportEventReport && $report->getException() !== null) { + Handler::reportSafely($report->getException()); + } } - private function status_error(string $path, string $msg) + /** + * Removes a trailing `/` from the given path and asserts that the path is usable for import. + * + * @param string $path + * + * @return string + * + * @throws ReservedDirectoryException + * @throws InvalidDirectoryException + */ + private static function normalizePath(string $path): string { - $this->status_update('Problem: ' . $path . ': ' . $msg); - } + try { + if (str_ends_with($path, '/')) { + $path = substr($path, 0, -1); + } + $realPath = realpath($path); - private function parsePath(string &$path, string $origPath) - { - if (!isset($path)) { - // @codeCoverageIgnoreStart - $path = Storage::path('import'); - // @codeCoverageIgnoreEnd - } - if (substr($path, -1) === '/') { - $path = substr($path, 0, -1); - } - if (is_dir($path) === false) { - $this->status_error($origPath, 'Given path is not a directory'); - Logs::error(__METHOD__, __LINE__, 'Given path is not a directory (' . $origPath . ')'); + if (is_dir($realPath) === false) { + throw new InvalidDirectoryException('Given path is not a directory (' . $path . ')'); + } - return false; - } + // Skip folders of Lychee + // Currently we must check for each directory which might be used + // by Lychee below `uploads/` individually, because the folder + // `uploads/import` is a potential source for imports and also + // placed below `uploads`. + // This is a design error and needs to be changed, at last when + // the media is stored remotely on a network storage such as + // AWS S3. + // A much better folder structure would be + // + // ``` + // | + // +-- staging // new directory which temporarily stores media which is not yet, but going to be added to Lychee + // | +-- imports // replaces the current `uploads/import` + // | +-- uploads // temporary storage location for images which have been uploaded via an HTTP POST request + // | +-- downloads // temporary storage location for images which have been downloaded from a remote URL + // +-- vault // replaces the current `uploads/` and could be outsourced to a remote network storage + // +-- original + // +-- medium2x + // +-- medium + // +-- small2x + // +-- small + // +-- thumb2x + // +-- thumb + // ``` + // + // This way we could simply check if the path is anything below `vault` + if ( + $realPath === Storage::path('big') || + $realPath === Storage::path('raw') || + $realPath === Storage::path('original') || + $realPath === Storage::path('medium2x') || + $realPath === Storage::path('medium') || + $realPath === Storage::path('small2x') || + $realPath === Storage::path('small') || + $realPath === Storage::path('thumb2x') || + $realPath === Storage::path('thumb') + ) { + throw new ReservedDirectoryException('The given path is a reserved path of Lychee (' . $path . ')'); + } - // Skip folders of Lychee - if ( - realpath($path) === Storage::path('big') || - realpath($path) === Storage::path('medium') || - realpath($path) === Storage::path('small') || - realpath($path) === Storage::path('thumb') - ) { - $this->status_error($origPath, 'Given path is reserved'); - Logs::error(__METHOD__, __LINE__, 'The given path is a reserved path of Lychee (' . $origPath . ')'); - - return false; + return $path; + } catch (FilesystemException|StringsException) { + throw new InvalidDirectoryException('Given path is not a directory (' . $path . ')'); } - - return true; } - private function setUpIgnoreList($path, $ignore_list) + /** + * Reads a list of files to ignore from `.lycheeignore` in the provided directory. + * + * @param string $path + * + * @return array + * + * @throws FileOperationException + */ + private static function readLocalIgnoreList(string $path): array { - // Let's load the list of filenames to ignore - if (file_exists($path . '/.lycheeignore')) { - $ignore_list_new = file($path . '/.lycheeignore'); - if (isset($ignore_list)) { - $ignore_list = array_merge($ignore_list, $ignore_list_new); - } else { - $ignore_list = $ignore_list_new; + if (is_readable($path . '/.lycheeignore')) { + try { + $result = file($path . '/.lycheeignore'); + } catch (\Throwable) { + throw new FileOperationException('Could not read ' . $path . '/.lycheeignore'); } - } - return $ignore_list; + return $result; + } else { + return []; + } } - private function checkAgainstIgnoreList($file, $ignore_list): bool + /** + * @param string $file + * @param string[] $ignore_list + * + * @return bool + */ + private static function checkAgainstIgnoreList(string $file, array $ignore_list): bool { - if (!isset($ignore_list)) { - return false; - } - $ignore_file = false; foreach ($ignore_list as $value_ignore) { - if ($this->check_file_matches_pattern(basename($file), $value_ignore)) { + if (self::check_file_matches_pattern(basename($file), $value_ignore)) { $ignore_file = true; break; } @@ -147,146 +228,130 @@ private function checkAgainstIgnoreList($file, $ignore_list): bool return $ignore_file; } - private function memWarningCheck() + private function memWarningCheck(): void { - if ($this->memCheck && !$this->memWarningGiven && memory_get_usage() > $this->memLimit) { - // @codeCoverageIgnoreStart - $this->status_warning('Approaching memory limit'); + if ($this->memLimit !== 0 && !$this->memWarningGiven && memory_get_usage() > $this->memLimit) { + $this->report(ImportEventReport::createWarning('mem_limit', null, 'Approaching memory limit')); $this->memWarningGiven = true; - // @codeCoverageIgnoreEnd } } /** - * @param string $path - * @param int $albumID - * @param array $ignore_list - * - * @throws ImagickException + * @throws ImportCancelledException + * @throws FrameworkException */ - public function do( - string $path, - $albumID, - $ignore_list = null - ) { - // Parse path - $origPath = $path; - - if (!$this->parsePath($path, $origPath)) { - return; - } - - // We process breadth-first: first all the files in a directory, - // then the subdirectories. This way, if the process fails along the - // way, it's much easier for the user to figure out what was imported - // and what was not. - - // Update ignore list - $ignore_list = $this->setUpIgnoreList($path, $ignore_list); - - $files = glob($path . '/*'); - $filesTotal = count($files); - $filesCount = 0; - $dirs = []; - $lastStatus = microtime(true); - - // Add '%' at end for CLI output - $percent_symbol = ($this->statusCLIFormatting) ? '%' : ''; - - $this->status_progress($origPath, '0' . $percent_symbol); - foreach ($files as $file) { + private function assertImportNotCancelled(): void + { + try { // re-read session in case cancelling import was requested - session()->start(); + Session::start(); if (Session::has('cancel')) { Session::forget('cancel'); - $this->status_error($origPath, 'Import cancelled'); - Logs::warning(__METHOD__, __LINE__, 'Import cancelled'); - - return; - } - // Reset the execution timeout for every iteration. - set_time_limit(ini_get('max_execution_time')); - - // Report if we might be running out of memory. - $this->memWarningCheck(); - - // Generate the status at most once a second, except for 0% and - // 100%, which are always generated. - $time = microtime(true); - if ($time - $lastStatus >= 1) { - $this->status_progress($origPath, intval($filesCount / $filesTotal * 100) . $percent_symbol); - $lastStatus = $time; + throw new ImportCancelledException(); } + } catch (NotFoundExceptionInterface|ContainerExceptionInterface|BindingResolutionException $e) { + throw new FrameworkException('Laravel\'s session component', $e); + } + } - // Let's check if we should ignore the file - if ($this->checkAgainstIgnoreList($file, $ignore_list)) { - $filesTotal--; - continue; - } + /** + * We process breadth-first: first all the files in a directory, + * then the subdirectories. This way, if the process fails along the + * way, it's much easier for the user to figure out what was imported + * and what was not. + * + * @param string $path + * @param Album|null $parentAlbum + * @param string[] $ignore_list + */ + public function do( + string $path, + ?Album $parentAlbum, + array $ignore_list = [], + ): void { + try { + $path = self::normalizePath($path); + + // Update ignore list + $ignore_list = array_merge($ignore_list, self::readLocalIgnoreList($path)); + + // TODO: Consider to use a modern OO-approach using [`DirectoryIterator`](https://www.php.net/manual/en/class.directoryiterator.php) and [`SplFileInfo`](https://www.php.net/manual/en/class.splfileinfo.php) + /** @var string[] $files */ + $files = glob(preg_quote($path) . '/*'); + + $filesTotal = count($files); + $filesCount = 0; + $dirs = []; + $lastStatus = microtime(true); + + $this->report(ImportProgressReport::create($path, 0)); + foreach ($files as $file) { + $this->assertImportNotCancelled(); + // Reset the execution timeout for every iteration. + try { + set_time_limit((int) ini_get('max_execution_time')); + } catch (InfoException) { + // Silently do nothing, if `set_time_limit` is denied. + } + // Report if we might be running out of memory. + $this->memWarningCheck(); + + // Generate the status at most each third of a second, + // except for 0% and 100%, which are always generated. + // Generating more frequently would create unnecessary many status + // reports; generating less frequently might lead to Firefox + // complaining. + // Firefox considers any response with a delay of >=500ms as + // "unresponsive". + // Taking additional delays on the network layer into account, + // 1/3 second should be fine. + $time = microtime(true); + if ($time - $lastStatus >= 0.3) { + $this->report(ImportProgressReport::create($path, $filesCount / $filesTotal * 100)); + $lastStatus = $time; + } - if (is_dir($file)) { - $dirs[] = $file; - $filesTotal--; - continue; - } + // Let's check if we should ignore the file + if (self::checkAgainstIgnoreList($file, $ignore_list)) { + $filesTotal--; + continue; + } + + if (is_dir($file)) { + $dirs[] = $file; + $filesTotal--; + continue; + } + + $filesCount++; - $filesCount++; - // It is possible to move a file because of directory permissions but - // the file may still be unreadable by the user - if (!is_readable($file)) { - $this->status_error($file, 'Could not read file'); - Logs::error(__METHOD__, __LINE__, 'Could not read file or directory (' . $file . ')'); - continue; - } - $extension = Helpers::getExtension($file, true); - $is_raw = in_array(strtolower($extension), $this->raw_formats, true); - if (@exif_imagetype($file) !== false || in_array(strtolower($extension), $this->validExtensions, true) || $is_raw) { - // Photo or Video try { - if ($this->photo($file, $this->delete_imported, $this->import_via_symlink, $albumID, $this->skip_duplicates, $this->resync_metadata) === false) { - $this->status_error($file, 'Could not import file'); - Logs::error(__METHOD__, __LINE__, 'Could not import file (' . $file . ')'); - } - } catch (PhotoSkippedException $e) { - $this->status_error($file, 'Skipped duplicate'); - } catch (PhotoResyncedException $e) { - $this->status_error($file, 'Skipped duplicate (resynced metadata)'); - } catch (Exception $e) { - $this->status_error($file, 'Could not import file'); - Logs::error(__METHOD__, __LINE__, 'Could not import file (' . $file . ')'); + $this->photoCreate->add(new NativeLocalFile($file), $parentAlbum); + } catch (\Throwable $e) { + $this->report(ImportEventReport::createFromException($e, $file)); } - } else { - $this->status_error($file, 'Unsupported file type'); - Logs::error(__METHOD__, __LINE__, 'Unsupported file type (' . $file . ')'); } - } - $this->status_progress($origPath, '100' . $percent_symbol); - - // Album creation - foreach ($dirs as $dir) { - // Folder - $album = null; - if ($this->skip_duplicates) { - $album = Album::where('parent_id', '=', $albumID == 0 ? null : $albumID) - ->where('title', '=', basename($dir)) - ->get() - ->first(); - } - if ($album === null) { - $create = resolve(Create::class); - $album = $create->create(basename($dir), $albumID); - // this actually should not fail. - if ($album === false) { - // @codeCoverageIgnoreStart - - $this->status_error(basename($dir), ': Could not create album'); - Logs::error(__METHOD__, __LINE__, 'Could not create album in Lychee (' . basename($dir) . ')'); - continue; + $this->report(ImportProgressReport::create($path, 100)); + + // Album creation per directory + foreach ($dirs as $dir) { + $this->assertImportNotCancelled(); + /** @var Album|null */ + $album = $this->importMode->shallSkipDuplicates ? + Album::query() + ->select(['albums.*']) + ->join('base_albums', 'base_albums.id', '=', 'albums.id') + ->where('albums.parent_id', '=', $parentAlbum?->id) + ->where('base_albums.title', '=', basename($dir)) + ->first() : + null; + if ($album === null) { + $album = $this->albumCreate->create(basename($dir), $parentAlbum); } - // @codeCoverageIgnoreEnd + $this->do($dir . '/', $album, $ignore_list); } - $newAlbumID = $album->id; - $this->do($dir . '/', $newAlbumID, $ignore_list); + } catch (\Throwable $e) { + $this->report(ImportEventReport::createFromException($e, $path)); } } @@ -296,24 +361,24 @@ public function do( * * @return bool */ - private function check_file_matches_pattern(string $pattern, string $filename) + private static function check_file_matches_pattern(string $pattern, string $filename): bool { // This function checks if the given filename matches the pattern allowing for // star as wildcard (as in *.jpg) // Example: '*.jpg' matches all jpgs - $pattern = preg_replace_callback('/([^*])/', [$this, 'preg_quote_callback_fct'], $pattern); + $pattern = preg_replace_callback('/([^*])/', [self::class, 'preg_quote_callback_fct'], $pattern); $pattern = str_replace('*', '.*', $pattern); - return (bool) preg_match('/^' . $pattern . '$/i', $filename); + return preg_match('/^' . $pattern . '$/i', $filename) === 1; } /** - * @param array $my_array + * @param array $my_array * * @return string */ - private function preg_quote_callback_fct(array $my_array) + private static function preg_quote_callback_fct(array $my_array): string { return preg_quote($my_array[1], '/'); } diff --git a/app/Actions/Import/Extensions/Checks.php b/app/Actions/Import/Extensions/Checks.php deleted file mode 100644 index 59a0c45522e..00000000000 --- a/app/Actions/Import/Extensions/Checks.php +++ /dev/null @@ -1,21 +0,0 @@ -add will take care of it. - $mime = mime_content_type($path); - - $nameFile = []; - $nameFile['name'] = $path; - $nameFile['type'] = $mime; - $nameFile['tmp_name'] = $path; - - $create = resolve(Create::class); - - // avoid incompatible settings (delete originals takes precedence over symbolic links) - if ($delete_imported) { - $import_via_symlink = false; - } - // (re-syncing metadata makes no sense when importing duplicates) - if (!$skip_duplicates) { - $resync_metadata = false; - } - - if ($create->add($nameFile, $albumID, $delete_imported, $skip_duplicates, $import_via_symlink, $resync_metadata) === false) { - // @codeCoverageIgnoreStart - return false; - // @codeCoverageIgnoreEnd - } - - return true; - } -} diff --git a/app/Actions/Import/FromServer.php b/app/Actions/Import/FromServer.php index b511e3bbf35..ceccfced8f8 100644 --- a/app/Actions/Import/FromServer.php +++ b/app/Actions/Import/FromServer.php @@ -1,91 +1,82 @@ exec = $exec; - } - - public function do($validated) + /** + * @param string[] $paths the server path to import from + * @param Album|null $album the album to import into + * @param ImportMode $importMode the import mode + * @param int $intendedOwnerId the intended owner of those pictures + * + * @return StreamedResponse + */ + public function do(array $paths, ?Album $album, ImportMode $importMode, int $intendedOwnerId): StreamedResponse { - if (isset($validated['delete_imported'])) { - $this->exec->delete_imported = ($validated['delete_imported'] === '1'); - } else { - $this->exec->delete_imported = (Configs::get_value('delete_imported', '0') === '1'); - } - if (isset($validated['import_via_symlink'])) { - $this->exec->import_via_symlink = ($validated['import_via_symlink'] === '1'); - } else { - $this->exec->import_via_symlink = (Configs::get_value('import_via_symlink', '0') === '1'); - } - if (isset($validated['skip_duplicates'])) { - $this->exec->skip_duplicates = ($validated['skip_duplicates'] === '1'); - } else { - $this->exec->skip_duplicates = (Configs::get_value('skip_duplicates', '0') === '1'); - } - if (isset($validated['resync_metadata'])) { - $this->exec->resync_metadata = ($validated['resync_metadata'] === '1'); - } else { - // do we need a default? - // $this->exec->resync_metadata = (Configs::get_value('resync_metadata', '0') === '1'); - $this->exec->resync_metadata = false; - } - - // memory_limit can have a K/M/etc suffix which makes querying it - // more complicated... - if (sscanf(ini_get('memory_limit'), '%d%c', $this->exec->memLimit, $memExt) === 2) { - switch (strtolower($memExt)) { - // @codeCoverageIgnoreStart - case 'k': - $this->exec->memLimit *= 1024; - break; - case 'm': - $this->exec->memLimit *= 1024 * 1024; - break; - case 'g': - $this->exec->memLimit *= 1024 * 1024 * 1024; - break; - case 't': - $this->exec->memLimit *= 1024 * 1024 * 1024 * 1024; - break; - default: - break; - // @codeCoverageIgnoreEnd - } - } - // We set the warning threshold at 90% of the limit. - $this->exec->memLimit = intval($this->exec->memLimit * 0.9); + $exec = new Exec($importMode, $intendedOwnerId, false, $this->determineMemLimit()); $response = new StreamedResponse(); - $response->setCallback(function () use ($validated) { - // Surround the response in '"' characters to make it a valid - // JSON string. - echo '"'; - $this->exec->do($validated['path'], $validated['albumID']); - echo '"'; - }); + $response->headers->set('Content-Type', 'application/json'); + $response->headers->set('Cache-Control', 'no-store'); // nginx-specific voodoo, as per https://symfony.com/doc/current/components/http_foundation.html#streaming-a-response $response->headers->set('X-Accel-Buffering', 'no'); + $response->setCallback(function () use ($paths, $album, $exec) { + // Surround the response by `[]` to make it a valid JSON array. + echo '['; + foreach ($paths as $path) { + $exec->do($path, $album); + } + echo ']'; + }); return $response; } - public function enableCLIStatus() + /** + * Determines the memory limit set by PHP configuration. + * + * The option `memory_limit` may have a K/M/etc. suffix which makes + * querying it more complicated... + * + * @return int the memory limit in bytes + */ + protected function determineMemLimit(): int { - $this->exec->statusCLIFormatting = true; - } + $value = 0; + $suffix = ''; - public function disableMemCheck() - { - $this->exec->memCheck = false; + sscanf(ini_get('memory_limit'), '%d%c', $value, $suffix); + if (!is_int($value) && !is_string($suffix)) { + // @codeCoverageIgnoreStart + return 0; + // @codeCoverageIgnoreEnd + } + + /** @var int $value */ + /** @var string $suffix */ + $value *= match (strtolower($suffix)) { + // @codeCoverageIgnoreStart + 'k' => 1024, + 'm' => 1024 * 1024, + 'g' => 1024 * 1024 * 1024, + 't' => 1024 * 1024 * 1024 * 1024, + default => 1, + // @codeCoverageIgnoreEnd + }; + + // We set the warning threshold at 90% of the limit. + return intval($value * 0.9); } } diff --git a/app/Actions/Import/FromUrl.php b/app/Actions/Import/FromUrl.php index f61a7851526..3fd707de70d 100644 --- a/app/Actions/Import/FromUrl.php +++ b/app/Actions/Import/FromUrl.php @@ -1,66 +1,87 @@ the collection of imported photos + * + * @throws MassImportException + */ + public function do(array $urls, ?Album $album, int $intendedOwnerId): Collection { - $this->checkPermissions(); - } + $result = new Collection(); + $exceptions = []; + $create = new Create( + new ImportMode(deleteImported: true, skipDuplicates: Configs::getValueAsBool('skip_duplicates')), + $intendedOwnerId + ); - public function do(array $urls, $albumId): bool - { - $error = false; + foreach ($urls as $url) { + try { + // Reset the execution timeout for every iteration. + try { + set_time_limit((int) ini_get('max_execution_time')); + } catch (InfoException) { + // Silently do nothing, if `set_time_limit` is denied. + } - foreach ($urls as &$url) { - // Reset the execution timeout for every iteration. - set_time_limit(ini_get('max_execution_time')); + // If the component parameter is specified, this function returns a string (or int in case of PHP_URL_PORT) + /** @var string $path */ + $path = parse_url($url, PHP_URL_PATH); + $extension = '.' . pathinfo($path, PATHINFO_EXTENSION); - // Validate photo type and extension even when $this->photo (=> $photo->add) will do the same. - // This prevents us from downloading invalid photos. - // Verify extension - $extension = Helpers::getExtension($url, true); - if (!$this->isValidExtension($extension)) { - $error = true; - Logs::error(__METHOD__, __LINE__, 'Photo format not supported (' . $url . ')'); - continue; - } + if ($extension !== '.') { + // Validate photo extension even when `$create->add()` will do later. + // This prevents us from downloading unsupported files. + BaseMediaFile::assertIsSupportedOrAcceptedFileExtension($extension); + } - // Verify image - $type = @exif_imagetype($url); - if (!$this->isValidImageType($type) && !in_array(strtolower($extension), $this->validExtensions, true)) { - $error = true; - Logs::error(__METHOD__, __LINE__, 'Photo type not supported (' . $url . ')'); - continue; - } + // Download file + $downloadedFile = new DownloadedFile($url); - $filename = pathinfo($url, PATHINFO_FILENAME) . $extension; - $tmp_name = Storage::path('import/' . $filename); - if (@copy($url, $tmp_name) === false) { - $error = true; - Logs::error(__METHOD__, __LINE__, 'Could not copy file (' . $url . ') to temp-folder (' . $tmp_name . ')'); - continue; + // Import photo/video/raw + $result->add($create->add($downloadedFile, $album)); + } catch (\Throwable $e) { + $exceptions[] = $e; + Handler::reportSafely($e); } + } - // Import photo - if (!$this->photo($tmp_name, true, false, $albumId)) { - $error = true; - Logs::error(__METHOD__, __LINE__, 'Could not import file (' . $tmp_name . ')'); - } + if (count($exceptions) !== 0) { + throw new MassImportException($exceptions); } - return !$error; + return $result; } } diff --git a/app/Actions/Install/ApplyMigration.php b/app/Actions/Install/ApplyMigration.php deleted file mode 100644 index 63095253ca3..00000000000 --- a/app/Actions/Install/ApplyMigration.php +++ /dev/null @@ -1,74 +0,0 @@ - true]); - $this->str_to_array(Artisan::output(), $output); - - /* - * We also double check there is no "QueryException" in the output (just to be sure). - */ - foreach ($output as $line) { - if (strpos($line, 'QueryException') !== false) { - // @codeCoverageIgnoreStart - return true; - // @codeCoverageIgnoreEnd - } - } - - return false; - } - - /** - * @return bool - */ - public function keyGenerate(array &$output) - { - try { - Artisan::call('key:generate', ['--force' => true]); - $this->str_to_array(Artisan::output(), $output); - // @codeCoverageIgnoreStart - } catch (\Exception $e) { - $output[] = $e->getMessage(); - $output[] = 'We could not generate the encryption key.'; - - return true; - } - // @codeCoverageIgnoreEnd - - // key is generated, we can safely remove that file (in theory) - @unlink(base_path('.NO_SECURE_KEY')); - - return false; - } -} diff --git a/app/Actions/Install/DefaultConfig.php b/app/Actions/Install/DefaultConfig.php deleted file mode 100644 index 4ff51dbf7cc..00000000000 --- a/app/Actions/Install/DefaultConfig.php +++ /dev/null @@ -1,139 +0,0 @@ - ['minPhpVersion' => '7.4.0'], - - 'requirements' => [ - 'php' => [ - 'openssl', - 'pdo', - 'mbstring', - 'tokenizer', - 'JSON', - 'exif', - 'gd', - 'intl', - ], - 'apache' => ['mod_rewrite'], - ], - /* - |-------------------------------------------------------------------------- - | Folders Permissions - |-------------------------------------------------------------------------- - | - | This is the default Lychee folders permissions. - | you may want to enable more permissions to allow online updates - | - */ - 'permissions' => [ - '.' => 'file_exists|is_readable|is_writable|is_executable', - 'storage/framework/' => 'file_exists|is_readable|is_writable|is_executable', - 'storage/framework/views/' => 'file_exists|is_readable|is_writable|is_executable', - 'storage/framework/cache/' => 'file_exists|is_readable|is_writable|is_executable', - 'storage/framework/sessions/' => 'file_exists|is_readable|is_writable|is_executable', - 'storage/logs/' => 'file_exists|is_readable|is_writable|is_executable', - 'bootstrap/cache/' => 'file_exists|is_readable|is_writable|is_executable', - 'public/dist/' => 'file_exists|is_readable|is_writable|is_executable', - 'public/img/' => 'file_exists|is_readable|is_writable|is_executable', - 'public/sym/' => 'file_exists|is_readable|is_writable|is_executable', - 'public/uploads/' => 'file_exists|is_readable|is_writable|is_executable', - 'public/uploads/big/' => 'file_exists|is_readable|is_writable|is_executable', - 'public/uploads/import/' => 'file_exists|is_readable|is_writable|is_executable', - 'public/uploads/medium/' => 'file_exists|is_readable|is_writable|is_executable', - 'public/uploads/raw/' => 'file_exists|is_readable|is_writable|is_executable', - 'public/uploads/small/' => 'file_exists|is_readable|is_writable|is_executable', - 'public/uploads/thumb/' => 'file_exists|is_readable|is_writable|is_executable', - ], - // This is from https://github.com/rashidlaasri/LaravelInstaller - // We keep it so we can make the .env edition a bit more friendly (later). - // This will also allow use to give more details of what each settings in the - // .env are for. - // - // 'environment' => [ - // 'form' => [ - // 'rules' => [ - // 'app_name' => 'required|string|max:50', - // 'environment' => 'required|string|max:50', - // 'environment_custom' => 'required_if:environment,other|max:50', - // 'app_debug' => 'required|boolean', - // 'app_log_level' => 'required|string|max:50', - // 'app_url' => 'required|url', - // 'database_connection' => 'required|string|max:50', - // 'database_hostname' => 'required|string|max:50', - // 'database_port' => 'required|numeric', - // 'database_name' => 'required|string|max:50', - // 'database_username' => 'required|string|max:50', - // 'database_password' => 'required|string|max:50', - //// 'broadcast_driver' => 'required|string|max:50', - //// 'cache_driver' => 'required|string|max:50', - // 'session_driver' => 'required|string|max:50', - //// 'queue_driver' => 'required|string|max:50', - //// 'redis_hostname' => 'required|string|max:50', - //// 'redis_password' => 'required|string|max:50', - //// 'redis_port' => 'required|numeric', - //// 'mail_driver' => 'required|string|max:50', - //// 'mail_host' => 'required|string|max:50', - //// 'mail_port' => 'required|string|max:50', - //// 'mail_username' => 'required|string|max:50', - //// 'mail_password' => 'required|string|max:50', - //// 'mail_encryption' => 'required|string|max:50', - //// 'pusher_app_id' => 'max:50', - //// 'pusher_app_key' => 'max:50', - //// 'pusher_app_secret' => 'max:50', - // ], - // ], - // ], - ]; - - /** - * Set the result array permissions and errors. - * - * @return mixed - */ - public function __construct() - { - $db_possibilities = [ - ['mysql', 'mysqli'], - ['mysql', 'pdo_mysql'], - ['pgsql', 'pgsql'], - ['pgsql', 'pdo_pgsql'], - ['sqlite', 'sqlite3'], - ]; - - // additional requirement depending of the .env/base config - foreach ($db_possibilities as $db_possibility) { - if (config('database.default') == $db_possibility[0]) { - $this->config['requirements']['php'][] = $db_possibility[1]; - } - } - } - - public function get_core() - { - return $this->config['core']; - } - - public function get_requirements() - { - return $this->config['requirements']; - } - - public function get_permissions() - { - return $this->config['permissions']; - } -} diff --git a/app/Actions/Install/PermissionsChecker.php b/app/Actions/Install/PermissionsChecker.php deleted file mode 100644 index 45903f17dcc..00000000000 --- a/app/Actions/Install/PermissionsChecker.php +++ /dev/null @@ -1,109 +0,0 @@ -results['permissions'] = []; - $this->results['errors'] = null; - } - - /** - * Return true if we are stupid enough to use Windows. - */ - public function is_win(): bool - { - return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN'; - } - - /** - * Check for the folders permissions. - * - * @param array $folders - * - * @return array - */ - public function check(array $folders): array - { - foreach ($folders as $folder => $permission) { - $this->addFile($folder, $permission, $this->getPermission($folder, $permission)); - } - - return $this->results; - } - - /** - * Get a folder permission. - * - * @param string $folder - * @param string $permissions - * - * @return int the position of 1 determines the errors - */ - private function getPermission(string $folder, string $permissions): int - { - $return = 0; - foreach (explode('|', $permissions) as $permission) { - preg_match('/(!*)(.*)/', $permission, $f); - $return <<= 1; - // we overwrite the value if windows and executable check. - $return |= ($f[2] === 'is_executable' && $this->is_win()) ? 0 : !($f[2](base_path($folder)) xor ($f[1] == '!')); - } - - return $return; - } - - /** - * Add the file to the list of results. - * - * @param $folder - * @param $permission - * @param $isSet - */ - private function addFile($folder, $permission, $isSet) - { - array_push($this->results['permissions'], [ - 'folder' => $folder, - 'permission' => $this->map_perm_set($permission, $isSet), - 'isSet' => $isSet, - ]); - - // set error if $isSet is positive - if ($isSet > 0) { - // @codeCoverageIgnoreStart - $this->results['errors'] = true; - // @codeCoverageIgnoreEnd - } - } - - /** - * map. - */ - private function map_perm_set($permissions, $areSet): array - { - $array_permission = array_reverse(explode('|', $permissions)); - $ret = []; - $i = 0; - foreach ($array_permission as $perm) { - $perm = str_replace('file_', '', $perm); - $perm = str_replace('!', 'not', $perm); - $perm = str_replace('is_', ' ', $perm); - $ret[$i++] = [$perm, $areSet & 1]; - $areSet >>= 1; - } - - return $ret; - } -} diff --git a/app/Actions/Install/RequirementsChecker.php b/app/Actions/Install/RequirementsChecker.php deleted file mode 100644 index d73e337cf3c..00000000000 --- a/app/Actions/Install/RequirementsChecker.php +++ /dev/null @@ -1,149 +0,0 @@ - $requirement_) { - switch ($type) { - // check php requirements - case 'php': - foreach ($requirement_ as $requirement) { - $results['requirements'][$type][$requirement] = true; - if (!extension_loaded($requirement)) { - // @codeCoverageIgnoreStart - $results['requirements'][$type][$requirement] - = false; - $results['errors'] = true; - // @codeCoverageIgnoreEnd - } - } - - if ($this->checkExec()) { - $results['requirements'][$type]['Php exec() available'] = true; - } else { - // @codeCoverageIgnoreStart - $results['requirements'][$type]['Php exec() not available (optional)'] = false; - // @codeCoverageIgnoreEnd - } - - break; - // check apache requirements - case 'apache': - foreach ($requirement_ as $requirement) { - // if function doesn't exist we can't check apache modules - // @codeCoverageIgnoreStart - if (function_exists('apache_get_modules')) { - $results['requirements'][$type][$requirement] - = true; - if (!in_array($requirement, apache_get_modules())) { - $results['requirements'][$type][$requirement] - = false; - $results['errors'] = true; - } - } - // @codeCoverageIgnoreEnd - } - break; - - // @codeCoverageIgnoreStart - default: - break; - // @codeCoverageIgnoreEnd - } - } - - return $results; - } - - /** - * Check PHP version requirement. - * - * @param string|null $minPhpVersion - * - * @return array - */ - public function checkPHPversion(string $minPhpVersion = null) - { - $minVersionPhp = $minPhpVersion; - $currentPhpVersion = $this->getPhpVersionInfo(); - $supported = false; - if ($minPhpVersion == null) { - // @codeCoverageIgnoreStart - $minVersionPhp = $this->getMinPhpVersion(); - // @codeCoverageIgnoreEnd - } - if ( - version_compare($currentPhpVersion['version'], $minVersionPhp) - >= 0 - ) { - $supported = true; - } - - return [ - 'full' => $currentPhpVersion['full'], - 'current' => $currentPhpVersion['version'], - 'minimum' => $minVersionPhp, - 'supported' => $supported, - ]; - } - - /** - * Check if exec is enabled. This will allow us to execute the migration. - * - * @return bool - */ - public function checkExec() - { - $disabled = explode(',', ini_get('disable_functions')); - - return !in_array('exec', $disabled); - } - - /** - * Get current Php version information. - * - * @return array - */ - private static function getPhpVersionInfo() - { - $currentVersionFull = PHP_VERSION; - preg_match("#^\d+(\.\d+)*#", $currentVersionFull, $filtered); - $currentVersion = $filtered[0]; - - return [ - 'full' => $currentVersionFull, - 'version' => $currentVersion, - ]; - } - - /** - * Get minimum PHP version ID. - * - * @return string _minPhpVersion - */ - // @codeCoverageIgnoreStart - protected function getMinPhpVersion() - { - return $this->_minPhpVersion; - } - - // @codeCoverageIgnoreEnd -} diff --git a/app/Actions/InstallUpdate/ApplyUpdate.php b/app/Actions/InstallUpdate/ApplyUpdate.php new file mode 100644 index 00000000000..8eda25e95bb --- /dev/null +++ b/app/Actions/InstallUpdate/ApplyUpdate.php @@ -0,0 +1,50 @@ + application of the updates + */ + private array $pipes = [ + BranchCheck::class, + AllowMigrationCheck::class, + GitPull::class, + ArtisanMigrate::class, + ComposerCall::class, + ]; + + /** + * Applies the migration: + * 1. git pull + * 2. artisan migrate. + * + * @return array the per-line console output + */ + public function run(): array + { + $output = []; + + $output = app(Pipeline::class) + ->send($output) + ->through($this->pipes) + ->thenReturn(); + + return preg_replace('/\033[[][0-9]*;*[0-9]*;*[0-9]*m/', '', $output); + } +} diff --git a/app/Actions/InstallUpdate/CheckUpdate.php b/app/Actions/InstallUpdate/CheckUpdate.php new file mode 100644 index 00000000000..22fb32ff79e --- /dev/null +++ b/app/Actions/InstallUpdate/CheckUpdate.php @@ -0,0 +1,72 @@ +gitHubFunctions->hydrate(); + $this->fileVersion->hydrate(); + } + + /** + * Check for updates and returns the update state. + * + * The return codes have the following semantics: + * - `0` - Not on master branch + * - `1` - Up-to-date + * - `2` - Not up-to-date. + * - `3` - Require migration. + * + * @return UpdateStatus the update state between 0..3 + */ + public function getCode(): UpdateStatus + { + if ($this->installedVersion->isRelease()) { + // @codeCoverageIgnoreStart + return match (false) { + MigrationCheck::isUpToDate() => UpdateStatus::REQUIRE_MIGRATION, + $this->fileVersion->isUpToDate() => UpdateStatus::NOT_UP_TO_DATE, + default => UpdateStatus::UP_TO_DATE, + }; + // @codeCoverageIgnoreEnd + } + + try { + UpdatableCheck::assertUpdatability(); + // @codeCoverageIgnoreStart + if (!$this->gitHubFunctions->isUpToDate()) { + return UpdateStatus::NOT_UP_TO_DATE; + } else { + return UpdateStatus::UP_TO_DATE; + } + // @codeCoverageIgnoreEnd + // @codeCoverageIgnoreStart + } catch (\Exception $e) { + return UpdateStatus::NOT_MASTER; + } + // @codeCoverageIgnoreEnd + } +} diff --git a/app/Actions/InstallUpdate/DefaultConfig.php b/app/Actions/InstallUpdate/DefaultConfig.php new file mode 100644 index 00000000000..7f544ac504b --- /dev/null +++ b/app/Actions/InstallUpdate/DefaultConfig.php @@ -0,0 +1,172 @@ +,requirements:array>,permissions:array} */ + private array $config = [ + /* + |-------------------------------------------------------------------------- + | Server Requirements + |-------------------------------------------------------------------------- + | + | This is our Lychee server requirements, we check if the extension is enabled + | by looping through the array and run "extension_loaded" on it. + | + */ + 'core' => ['minPhpVersion' => '8.3.0'], + + 'requirements' => [ + 'php' => [ + 'bcmath', // Required by Laravel + 'ctype', // Required by Laravel + 'dom', // Required by dependencies + 'exif', + 'fileinfo', // Required by Laravel + 'filter', // Required by dependencies + 'gd', + 'json', // Required by Laravel + 'libxml', // Required by dependencies + 'mbstring', // Required by Laravel + 'openssl', // Required by Laravel + 'pcre', // Required by dependencies + 'PDO', // Required by Laravel + 'Phar', // Required by dependencies + 'SimpleXML', // Required by dependencies + 'tokenizer', // Required by Laravel + 'xml', // Required by Laravel + 'xmlwriter', // Required by dependencies + ], + 'apache' => ['mod_rewrite'], + ], + /* + |-------------------------------------------------------------------------- + | Folders Permissions + |-------------------------------------------------------------------------- + | + | This is the default Lychee folders permissions. + | you may want to enable more permissions to allow online updates + | + */ + 'permissions' => [ + '.' => 'file_exists|is_readable|is_writable|is_executable', + 'database/' => 'file_exists|is_readable|is_writable|is_executable', + 'database/database.sqlite' => 'file_exists|is_readable|is_writable', + 'storage/framework/' => 'file_exists|is_readable|is_writable|is_executable', + 'storage/framework/views/' => 'file_exists|is_readable|is_writable|is_executable', + 'storage/framework/cache/' => 'file_exists|is_readable|is_writable|is_executable', + 'storage/framework/sessions/' => 'file_exists|is_readable|is_writable|is_executable', + 'storage/logs/' => 'file_exists|is_readable|is_writable|is_executable', + 'storage/tmp/extract/' => 'file_exists|is_readable|is_writable|is_executable', + 'storage/tmp/jobs/' => 'file_exists|is_readable|is_writable|is_executable', + 'storage/tmp/uploads/' => 'file_exists|is_readable|is_writable|is_executable', + 'bootstrap/cache/' => 'file_exists|is_readable|is_writable|is_executable', + 'public/dist/' => 'file_exists|is_readable|is_writable|is_executable', + 'public/sym/' => 'file_exists|is_readable|is_writable|is_executable', + 'public/uploads/' => 'file_exists|is_readable|is_writable|is_executable', + ], + // This is from https://github.com/rashidlaasri/LaravelInstaller + // We keep it so we can make the .env edition a bit more friendly (later). + // This will also allow use to give more details of what each settings in the + // .env are for. + // + // 'environment' => [ + // 'form' => [ + // 'rules' => [ + // 'app_name' => 'required|string|max:50', + // 'environment' => 'required|string|max:50', + // 'environment_custom' => 'required_if:environment,other|max:50', + // 'app_debug' => 'required|boolean', + // 'app_log_level' => 'required|string|max:50', + // 'app_url' => 'required|url', + // 'database_connection' => 'required|string|max:50', + // 'database_hostname' => 'required|string|max:50', + // 'database_port' => 'required|numeric', + // 'database_name' => 'required|string|max:50', + // 'database_username' => 'required|string|max:50', + // 'database_password' => 'required|string|max:50', + // // 'broadcast_driver' => 'required|string|max:50', + // // 'cache_driver' => 'required|string|max:50', + // 'session_driver' => 'required|string|max:50', + // // 'queue_driver' => 'required|string|max:50', + // // 'redis_hostname' => 'required|string|max:50', + // // 'redis_password' => 'required|string|max:50', + // // 'redis_port' => 'required|numeric', + // // 'mail_driver' => 'required|string|max:50', + // // 'mail_host' => 'required|string|max:50', + // // 'mail_port' => 'required|string|max:50', + // // 'mail_username' => 'required|string|max:50', + // // 'mail_password' => 'required|string|max:50', + // // 'mail_encryption' => 'required|string|max:50', + // // 'pusher_app_id' => 'max:50', + // // 'pusher_app_key' => 'max:50', + // // 'pusher_app_secret' => 'max:50', + // ], + // ], + // ], + ]; + + /** + * Set the result array permissions and errors. + * + * @throws FrameworkException + */ + public function __construct() + { + try { + $db_possibilities = [ + ['mysql', 'mysqli'], + ['mysql', 'pdo_mysql'], + ['pgsql', 'pgsql'], + ['pgsql', 'pdo_pgsql'], + ['sqlite', 'sqlite3'], + ]; + + // additional requirement depending on the .env/base config + foreach ($db_possibilities as $db_possibility) { + if (config('database.default') === $db_possibility[0]) { + $this->config['requirements']['php'][] = $db_possibility[1]; + } + } + // @codeCoverageIgnoreStart + } catch (BindingResolutionException|ContainerExceptionInterface $e) { + throw new FrameworkException('Laravel\'s container component', $e); + } + // @codeCoverageIgnoreEnd + } + + /** + * @return array + */ + public function get_core(): array + { + return $this->config['core']; + } + + /** + * @return array> + */ + public function get_requirements(): array + { + return $this->config['requirements']; + } + + /** + * @return array + */ + public function get_permissions(): array + { + return $this->config['permissions']; + } +} diff --git a/app/Actions/InstallUpdate/PermissionsChecker.php b/app/Actions/InstallUpdate/PermissionsChecker.php new file mode 100644 index 00000000000..3505901402e --- /dev/null +++ b/app/Actions/InstallUpdate/PermissionsChecker.php @@ -0,0 +1,112 @@ + [], + 'errors' => null, + ]; + + /** + * Return true if we are stupid enough to use Windows. + */ + public function is_win(): bool + { + return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN'; + } + + /** + * Check for the folders permissions. + * + * @param array $folders + * + * @return array{"errors":bool|null,"permissions":array{"folder":string,"permission":(string|int)[][],"isSet":int}[]} + */ + public function check(array $folders): array + { + foreach ($folders as $folder => $permission) { + $this->addFile($folder, $permission, $this->getPermission($folder, $permission)); + } + + return $this->results; + } + + /** + * Get a folder permission. + * + * @param string $folder + * @param string $permissions + * + * @return int the position of 1 determines the errors + */ + private function getPermission(string $folder, string $permissions): int + { + $return = 0; + foreach (explode('|', $permissions) as $permission) { + preg_match('/(!*)(.*)/', $permission, $f); + $return <<= 1; + // we overwrite the value if windows and executable check. + $return |= ($f[2] === 'is_executable' && $this->is_win()) ? 0 : !($f[2](base_path($folder)) xor ($f[1] === '!')); + } + + return $return; + } + + /** + * Add the file to the list of results. + * + * @param string $folder + * @param string $permission + * @param int $isSet + */ + private function addFile(string $folder, string $permission, int $isSet): void + { + $this->results['permissions'][] = [ + 'folder' => $folder, + 'permission' => $this->map_perm_set($permission, $isSet), + 'isSet' => $isSet, + ]; + + // set error if $isSet is positive + if ($isSet > 0) { + // @codeCoverageIgnoreStart + $this->results['errors'] = true; + // @codeCoverageIgnoreEnd + } + } + + /** + * map. + * + * @param string $permissions + * @param int $areSet + * + * @return (string|int)[][] + */ + private function map_perm_set(string $permissions, int $areSet): array + { + $array_permission = array_reverse(explode('|', $permissions)); + $ret = []; + $i = 0; + foreach ($array_permission as $perm) { + $perm = str_replace('file_', '', $perm); + $perm = str_replace('!', 'not', $perm); + $perm = str_replace('is_', ' ', $perm); + $ret[$i++] = [$perm, $areSet & 1]; + $areSet >>= 1; + } + + return $ret; + } +} diff --git a/app/Actions/InstallUpdate/Pipes/AbstractUpdateInstallerPipe.php b/app/Actions/InstallUpdate/Pipes/AbstractUpdateInstallerPipe.php new file mode 100644 index 00000000000..b4f3f4ddb3f --- /dev/null +++ b/app/Actions/InstallUpdate/Pipes/AbstractUpdateInstallerPipe.php @@ -0,0 +1,40 @@ + &$output + * @param \Closure(array $output): array $next + * + * @return array + * + * @codeCoverageIgnore + */ + abstract public function handle(array &$output, \Closure $next): array; + + /** + * Arrayify a string and append it to $output. + * + * @param string $string message text which each message separated by newline + * @param string[] $output list of messages + * + * @return void + */ + protected function strToArray(string $string, array &$output): void + { + $a = explode("\n", $string); + foreach ($a as $aa) { + if ($aa !== '') { + $output[] = $aa; + } + } + } +} \ No newline at end of file diff --git a/app/Actions/InstallUpdate/Pipes/AllowMigrationCheck.php b/app/Actions/InstallUpdate/Pipes/AllowMigrationCheck.php new file mode 100644 index 00000000000..afa6b652d90 --- /dev/null +++ b/app/Actions/InstallUpdate/Pipes/AllowMigrationCheck.php @@ -0,0 +1,44 @@ + true]); + + $this->strToArray(Artisan::output(), $output); + + // Always false on CICD + if ( + !str_contains(end($output), 'Application key set successfully') || + config('app.key') === null + ) { + // @codeCoverageIgnoreStart + $output[] = 'We could not generate the encryption key.'; + throw new InstallationFailedException('Could not generate encryption key'); + // @codeCoverageIgnoreEnd + } + + return $next($output); + } +} \ No newline at end of file diff --git a/app/Actions/InstallUpdate/Pipes/ArtisanMigrate.php b/app/Actions/InstallUpdate/Pipes/ArtisanMigrate.php new file mode 100644 index 00000000000..e8b0033084a --- /dev/null +++ b/app/Actions/InstallUpdate/Pipes/ArtisanMigrate.php @@ -0,0 +1,29 @@ + true]); + + $this->strToArray(Artisan::output(), $output); + + return $next($output); + } +} \ No newline at end of file diff --git a/app/Actions/InstallUpdate/Pipes/ArtisanViewClear.php b/app/Actions/InstallUpdate/Pipes/ArtisanViewClear.php new file mode 100644 index 00000000000..43c05c17a54 --- /dev/null +++ b/app/Actions/InstallUpdate/Pipes/ArtisanViewClear.php @@ -0,0 +1,28 @@ +strToArray(Artisan::output(), $output); + + return $next($output); + } +} \ No newline at end of file diff --git a/app/Actions/InstallUpdate/Pipes/BranchCheck.php b/app/Actions/InstallUpdate/Pipes/BranchCheck.php new file mode 100644 index 00000000000..bee346d8a25 --- /dev/null +++ b/app/Actions/InstallUpdate/Pipes/BranchCheck.php @@ -0,0 +1,41 @@ +isRelease()) { + // @codeCoverageIgnoreStart + return $next($output); + // @codeCoverageIgnoreEnd + } + + $githubFunctions = resolve(GitHubVersion::class); + $githubFunctions->hydrate(false); + + if ($githubFunctions->isMasterBranch()) { + return $next($output); + } + + // @codeCoverageIgnoreStart + $output[] = 'Branch is not ' . GitHubVersion::MASTER; + + return $output; + // @codeCoverageIgnoreEnd + } +} \ No newline at end of file diff --git a/app/Actions/InstallUpdate/Pipes/ComposerCall.php b/app/Actions/InstallUpdate/Pipes/ComposerCall.php new file mode 100644 index 00000000000..5c4e282114d --- /dev/null +++ b/app/Actions/InstallUpdate/Pipes/ComposerCall.php @@ -0,0 +1,55 @@ +isRelease()) { + return $next($output); + } + + // update with respect to installed version + $noDev = $installedVersion->isDev() ? '' : '--no-dev '; + + if (Helpers::isExecAvailable()) { + if (Configs::getValueAsBool('apply_composer_update')) { + // @codeCoverageIgnoreStart + Log::warning(__METHOD__ . ':' . __LINE__ . ' Composer is called on update.'); + + // Composer\Factory::getHomeDir() method + // needs COMPOSER_HOME environment variable set + putenv('COMPOSER_HOME=' . base_path('/composer-cache')); + chdir(base_path()); + exec(sprintf('composer install %s--no-progress 2>&1', $noDev), $output); + chdir(base_path('public')); + // @codeCoverageIgnoreEnd + } else { + $output[] = 'Composer update are always dangerous when automated.'; + $output[] = 'So we did not execute it.'; + $output[] = 'If you want to have composer update applied, please set the setting to 1 at your own risk.'; + } + } + + return $next($output); + } +} \ No newline at end of file diff --git a/app/Actions/InstallUpdate/Pipes/GitPull.php b/app/Actions/InstallUpdate/Pipes/GitPull.php new file mode 100644 index 00000000000..880687ab807 --- /dev/null +++ b/app/Actions/InstallUpdate/Pipes/GitPull.php @@ -0,0 +1,41 @@ +isRelease()) { + // @codeCoverageIgnoreStart + return $next($output); + // @codeCoverageIgnoreEnd + } + + if (Helpers::isExecAvailable()) { + $command = 'git pull --rebase ' . Config::get('urls.git.pull') . ' master 2>&1'; + exec($command, $output); + + return $next($output); + } + + // @codeCoverageIgnoreStart + return $output; + // @codeCoverageIgnoreEnd + } +} \ No newline at end of file diff --git a/app/Actions/InstallUpdate/Pipes/QueryExceptionChecker.php b/app/Actions/InstallUpdate/Pipes/QueryExceptionChecker.php new file mode 100644 index 00000000000..609c14bf1ad --- /dev/null +++ b/app/Actions/InstallUpdate/Pipes/QueryExceptionChecker.php @@ -0,0 +1,36 @@ +> $requirements + * + * @return array{requirements:array>,errors:bool} + */ + public function check(array $requirements): array + { + $results = [ + 'errors' => false, + 'requirements' => [], + ]; + foreach ($requirements as $type => $requirement_) { + if ($type === 'php') { + // check php requirements + foreach ($requirement_ as $requirement) { + $hasExtension = extension_loaded($requirement); + $results['requirements'][$type][$requirement] = $hasExtension; + // Note: Don't use the short-cut assignment `|=`; + // it silently converts the type to integer, because + // `|` is not the logical OR, but the bitwise OR. + $results['errors'] = $results['errors'] || !$hasExtension; + } + + if (Helpers::isExecAvailable()) { + $results['requirements'][$type]['Php exec() available'] = true; + } else { + $results['requirements'][$type]['Php exec() not available (optional)'] = false; + } + } elseif ($type === 'apache') { + // check apache requirements + foreach ($requirement_ as $requirement) { + // if function doesn't exist we can't check apache modules + $hasModule = !function_exists('apache_get_modules') || in_array($requirement, apache_get_modules(), true); + $results['requirements'][$type][$requirement] = $hasModule; + $results['errors'] = $results['errors'] || !$hasModule; + } + } + } + + return $results; + } + + /** + * Check PHP version requirement. + * + * @param string|null $minPhpVersion + * + * @return array{full:string,current:string,minimum:string,supported:bool} + */ + public function checkPHPVersion(?string $minPhpVersion = null): array + { + $minVersionPhp = $minPhpVersion ?? self::MIN_PHP_VERSION; + $currentPhpVersion = self::getPhpVersionInfo(); + $supported = version_compare($currentPhpVersion['version'], $minVersionPhp) >= 0; + + return [ + 'full' => $currentPhpVersion['full'], + 'current' => $currentPhpVersion['version'], + 'minimum' => $minVersionPhp, + 'supported' => $supported, + ]; + } + + /** + * Get current Php version information. + * + * @return array{full:string,version:string} + */ + private static function getPhpVersionInfo(): array + { + $currentVersionFull = PHP_VERSION; + preg_match('#^\d+(\.\d+)*#', $currentVersionFull, $filtered); + $currentVersion = $filtered[0]; + + return [ + 'full' => $currentVersionFull, + 'version' => $currentVersion, + ]; + } +} diff --git a/app/Actions/Oauth/Oauth.php b/app/Actions/Oauth/Oauth.php new file mode 100644 index 00000000000..854efa12f57 --- /dev/null +++ b/app/Actions/Oauth/Oauth.php @@ -0,0 +1,178 @@ +getUserFromOauth($provider); + + $credential = $this->fetchAssociatedUserFromDB($provider, $user->getId()); + + if ($credential !== null) { + Auth::login($credential->user); + + return true; + } + + if (!Configs::getValueAsBool('oauth_create_user_on_first_attempt')) { + throw new UnauthorizedException('User not found!'); + } + + if (User::query()->where('username', '=', $user->getName() ?? $user->getEmail() ?? $user->getId()) + ->when( + $user->getEmail() !== null && $user->getEmail() !== '', + fn ($q) => $q->orWhere('email', '=', $user->getEmail()) + )->exists()) { + throw new UnauthorizedException('User already exists!'); + } + + $create = resolve(Create::class); + $new_user = $create->do( + username: $user->getName() ?? $user->getEmail() ?? $user->getId(), + email: $user->getEmail(), + password: strtr(base64_encode(random_bytes(8)), '+/', '-_'), + mayUpload: Configs::getValueAsBool('oauth_grant_new_user_upload_rights'), + mayEditOwnSettings: Configs::getValueAsBool('oauth_grant_new_user_modification_rights')); + + Auth::login($new_user); + + $this->saveOauth( + provider: $provider, + authedUser_id: $new_user->id, + oauth_id: $user->getId()); + + return true; + } + + /** + * Get the user from the driver. + * + * @param OauthProvidersType $provider + * + * @return ContractsUser + */ + private function getUserFromOauth(OauthProvidersType $provider): ContractsUser + { + return Socialite::driver($provider->value)->user(); + } + + /** + * Fetch the Oauth credential and user associated. + * + * @param OauthProvidersType $provider Oauth provider + * @param string $user_id to fetch with + * + * @return OauthCredential|null credential if found + */ + private function fetchAssociatedUserFromDB(OauthProvidersType $provider, string $user_id): OauthCredential|null + { + return OauthCredential::query() + ->with(['user']) + ->where('token_id', '=', $user_id) + ->where('provider', '=', $provider) + ->first(); + } + + /** + * Authenticate and redirect. + * + * @param OauthProvidersType $provider + * + * @return true + */ + public function registerOrDie(OauthProvidersType $provider): true + { + if (Session::get($provider->value) !== self::OAUTH_REGISTER) { + throw new UnauthorizedException('Registration attempted but not authorized.'); + } + + $user = Socialite::driver($provider->value)->user(); + + /** @var User $authedUser */ + $authedUser = Auth::user(); + + $count_existing = OauthCredential::query() + ->where('provider', '=', $provider) + ->where('user_id', '=', $authedUser->id) + ->count(); + if ($count_existing > 0) { + throw new LycheeLogicException('Oauth credential for that provider already exists.'); + } + + $this->saveOauth( + provider: $provider, + authedUser_id: $authedUser->id, + oauth_id: $user->getId()); + + return true; + } + + /** + * Save a credential for a user. + * + * @param OauthProvidersType $provider of credential + * @param int $authedUser_id user ID already existing in the database + * @param string $oauth_id oauth id on the Oauth server side + * + * @return void + */ + private function saveOauth(OauthProvidersType $provider, int $authedUser_id, string $oauth_id): void + { + $credential = OauthCredential::create([ + 'provider' => $provider, + 'user_id' => $authedUser_id, + 'token_id' => $oauth_id, + ]); + $credential->save(); + } +} diff --git a/app/Actions/Photo/Archive.php b/app/Actions/Photo/Archive.php index 558d3e5f6f5..333d4995a08 100644 --- a/app/Actions/Photo/Archive.php +++ b/app/Actions/Photo/Archive.php @@ -1,203 +1,316 @@ ', ':', '"', '/', '\\', '|', '?', '*', + ]; - public function __construct() - { - // Illicit chars - $this->badChars = array_merge(array_map('chr', range(0, 31)), ['<', '>', ':', '"', '/', '\\', '|', '?', '*']); - } + protected int $deflateLevel = -1; /** - * @param string $albumID + * Returns a response for a downloadable file. + * + * The file is either a media file (if the array of photo IDs contains + * a single element) or a ZIP file (if the array of photo IDs contains + * more than one element). + * + * @param Collection $photos the photos which shall be included in the response + * @param DownloadVariantType $downloadVariant the desired variant of the photo * * @return StreamedResponse + * + * @throws LycheeException */ - public function do(array $photoIDs, $kind_request) + public function do(Collection $photos, DownloadVariantType $downloadVariant): StreamedResponse { - if (count($photoIDs) === 1) { - $response = $this->file($photoIDs[0], $kind_request); + if ($photos->count() === 1) { + $response = $this->file($photos->firstOrFail(), $downloadVariant); } else { - $response = $this->zip($photoIDs, $kind_request); + $response = $this->zip($photos, $downloadVariant); } return $response; } - public function file($photoID, $kind_request) + /** + * Streams a single size variant to the client. + * + * Note: This method will become quite inefficient, when the media files + * are not hosted on the same machine as Lychee, but on a remote file + * hosting service like AWS S3. + * In this case, the file would be streamed from the hoster to Lychee + * first and then streamed from Lychee to the client. + * It would be much more efficient, if the client would directly fetch + * the file from the hoster. + * Practically, we could use `->getUrl` of the size variant in combination + * with `Symfony\Component\HttpFoundation\RedirectResponse`. + * However, the client would not get a "nice" file name, but the + * random file name of the size variant. + * + * @param Photo $photo the photo + * @param DownloadVariantType $downloadVariant the requested size variant + * + * @return StreamedResponse + * + * @throws LycheeException + */ + protected function file(Photo $photo, DownloadVariantType $downloadVariant): StreamedResponse { - $ret = $this->extract_names($photoID, $kind_request); - if ($ret === null) { - return abort(404); + $archiveFileInfo = $this->extractFileInfo($photo, $downloadVariant); + + $responseGenerator = function () use ($archiveFileInfo) { + $outputStream = fopen('php://output', 'wb'); + stream_copy_to_stream($archiveFileInfo->file->read(), $outputStream); + $archiveFileInfo->file->close(); + fclose($outputStream); + }; + + try { + $response = new StreamedResponse($responseGenerator); + $disposition = HeaderUtils::makeDisposition( + HeaderUtils::DISPOSITION_ATTACHMENT, + $archiveFileInfo->getFilename(), + mb_check_encoding($archiveFileInfo->getFilename(), 'ASCII') ? '' : 'Photo' . $archiveFileInfo->file->getExtension() + ); + $response->headers->set('Content-Type', $photo->type); + $response->headers->set('Content-Disposition', $disposition); + $response->headers->set('Content-Length', strval($archiveFileInfo->file->getFilesize())); + // Note: Using insecure hashing algorithm is fine here. + // The ETag header must only be different for different size variants + // Pre-image resistance and collision robustness is not required. + // If a size variant changes, the name of the (physical) file + // changes, too. + // The only reason why we don't use the path directly is that + // we must avoid illegal characters like `/` and md5 returns a + // hexadecimal string. + $response->headers->set('ETag', md5( + $archiveFileInfo->file->getBasename() . + $downloadVariant->value . + $photo->updated_at->toAtomString() . + $archiveFileInfo->file->getFilesize()) + ); + $response->headers->set('Last-Modified', $photo->updated_at->format(\DateTimeInterface::RFC7231)); + + return $response; + } catch (\InvalidArgumentException $e) { + throw new FrameworkException('Symfony\'s response component', $e); } - - list($title, $kind, $extension, $url) = $ret; - - // Set title for photo - $file = $title . $kind . $extension; - - $response = new BinaryFileResponse($url); - - return $response->setContentDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $file); } - public function zip(array $photoIDs, string $kind_request) + /** + * @param Collection $photos + * @param DownloadVariantType $downloadVariant + * + * @return StreamedResponse + * + * @throws FrameworkException + * @throws ConfigurationKeyMissingException + */ + protected function zip(Collection $photos, DownloadVariantType $downloadVariant): StreamedResponse { - $response = new StreamedResponse(function () use ($kind_request, $photoIDs) { - $options = new \ZipStream\Option\Archive(); - $options->setEnableZip64(Configs::get_value('zip64', '1') === '1'); - $zip = new ZipStream(null, $options); - - $files = []; - foreach ($photoIDs as $photoID) { - $ret = $this->extract_names($photoID, $kind_request); - if ($ret == null) { - return abort(404); + $this->deflateLevel = Configs::getValueAsInt('zip_deflate_level'); + + $responseGenerator = function () use ($downloadVariant, $photos) { + $zip = new ZipStream(enableZip64: Configs::getValueAsBool('zip64'), defaultEnableZeroHeader: true, sendHttpHeaders: false); + + // We first need to scan the whole array of files to avoid + // problems with duplicate file names. + // If a file name occurs multiple times, the files are named + // filename-1, filename-2, filename-3 and so on. + // Unfortunately, the naive approach which uses a simple online + // algorithm that only applies a singly pass (without look-ahead) + // and maintains a counter for every file name will fail, if the + // list of file names already contains another files which uses + // the same naming pattern accidentally. + // Assume that the album itself contains the images + // `my-file.jpg`, `my-file-2.jpg`, `my-file.jpg`. + // The naive approach would first store `my-file.jpg` and + // `my-file-2.jpg` (both unaltered). + // Both counters for `my-file.jpg` and `my-file-2.jpg` equal one + // because those file names are actually treated as independent + // file names. + // When the naive approach comes across the last file + // `my-file.jpg`, the counter for `my-file.jpg` is incremented + // and the file is stored as ``my-file-2.jpg`. + // However, this accidentally overwrite the original + // `my-file-2.jpg`. + // Long story short, if we append a counter as a suffix to a + // filename, we must take care that the result is not also used as + // a base file name. + // Further note, that this problem does not occur if both file + // names occurred multiple times. + // E.g., if we had + // - `my-file.jpg`, + // - `my-file-2.jpg`, + // - `my-file.jpg` and + // - `my-file-2.jpg` again, + // then the result would be + // - `my-file-1.jpg`, + // - `my-file-2-1.jpg`, + // - `my-file-2.jpg` and + // - `my-file-2-2.jpg`. + // Note that the problematic case can only occur due to a clash + // of file names between file names which occur multiple times + // (and thus are appended by a suffix) and a file name that only + // occurs a single time. + // + // Here, we take the following approach: + // + // We scan the list of photos once and partition the set of file + // names into a set of unique file names and a set of ambitious + // file names. + // In the second run, all photos with unique file names are + // stored under their unaltered file name. + // For photo with an ambiguous file name a counter for that file + // name is tracked and incremented. + // If the resulting file name accidentally equals one of the + // unique file names, then the counter is incremented until the + // next "free" file name is found. + + $archiveFileInfos = []; + $uniqueFilenames = []; + $ambiguousFilenames = []; + + // Partition the set + /** @var Photo $photo */ + foreach ($photos as $photo) { + $archiveFileInfo = $this->extractFileInfo($photo, $downloadVariant); + $archiveFileInfos[] = $archiveFileInfo; + $filename = $archiveFileInfo->getFilename(); + if (array_key_exists($filename, $ambiguousFilenames)) { + // do nothing + } elseif (array_key_exists($filename, $uniqueFilenames)) { + unset($uniqueFilenames[$filename]); + $ambiguousFilenames[$filename] = 0; + } else { + $uniqueFilenames[$filename] = 0; } + } - list($title, $kind, $extension, $url) = $ret; - - // Set title for photo - $file = $title . $kind . $extension; - // Check for duplicates - if (!empty($files)) { - $i = 1; - $tmp_file = $file; - while (in_array($tmp_file, $files)) { - // Set new title for photo - $tmp_file = $title . $kind . '-' . $i . $extension; - $i++; - } - $file = $tmp_file; + /** @var ArchiveFileInfo $archiveFileInfo */ + foreach ($archiveFileInfos as $archiveFileInfo) { + $trueFilename = $archiveFileInfo->getFilename(); + if (array_key_exists($trueFilename, $uniqueFilenames)) { + // Easy case: Unique file names are used unaltered + $filename = $trueFilename; + } else { + do { + // Append suffix for multiple copies of same file name + // but skip results which exist as a unique file name + $filename = $archiveFileInfo->getFilename( + '-' . ++$ambiguousFilenames[$trueFilename] + ); + } while (array_key_exists($filename, $uniqueFilenames)); } - // Add to array - $files[] = $file; - + $zip->addFileFromStream(fileName: $filename, stream: $archiveFileInfo->file->read(), + compressionMethod: $this->deflateLevel === -1 ? ZipMethod::STORE : ZipMethod::DEFLATE, + deflateLevel: $this->deflateLevel); + $archiveFileInfo->file->close(); // Reset the execution timeout for every iteration. - set_time_limit(ini_get('max_execution_time')); - - $zip->addFileFromPath($file, $url); - } // foreach ($photoIDs) + try { + set_time_limit((int) ini_get('max_execution_time')); + } catch (InfoException) { + // Silently do nothing, if `set_time_limit` is denied. + } + } // finish the zip stream $zip->finish(); - }); - - // Set file type and destination - $response->headers->set('Content-Type', 'application/x-zip'); - $disposition = HeaderUtils::makeDisposition(HeaderUtils::DISPOSITION_ATTACHMENT, 'Photos.zip'); - $response->headers->set('Content-Disposition', $disposition); + }; + + try { + $response = new StreamedResponse($responseGenerator); + $disposition = HeaderUtils::makeDisposition( + HeaderUtils::DISPOSITION_ATTACHMENT, + 'Photos.zip' + ); + $response->headers->set('Content-Type', 'application/x-zip'); + $response->headers->set('Content-Disposition', $disposition); + + // Disable caching + $response->headers->set('Cache-Control', 'no-cache, no-store, must-revalidate'); + $response->headers->set('Pragma', 'no-cache'); + $response->headers->set('Expires', '0'); + } catch (\InvalidArgumentException $e) { + throw new FrameworkException('Symfony\'s response component', $e); + } return $response; } /** - * extract the file names. + * Creates a {@link ArchiveFileInfo} for the indicated photo and variant. + * + * @param Photo $photo the photo whose archive information shall be returned + * @param DownloadVariantType $downloadVariant the desired variant of the photo * - * @param $photoID - * @param $request + * @return ArchiveFileInfo the created archive info * - * @return array|null + * @throws InvalidSizeVariantException */ - public function extract_names($photoID, $kind_input) + protected function extractFileInfo(Photo $photo, DownloadVariantType $downloadVariant): ArchiveFileInfo { - /** @var Photo $photo */ - $photo = Photo::with('album')->findOrFail($photoID); - - if (!AccessControl::is_current_user($photo->owner_id)) { - if ($photo->album_id !== null) { - if (!$photo->album->is_downloadable()) { - return null; + $validFilename = str_replace(self::BAD_CHARS, '', $photo->title); + $baseFilename = $validFilename !== '' ? $validFilename : 'Untitled'; + $baseFilename = pathinfo($baseFilename, PATHINFO_FILENAME); + + if ($downloadVariant === DownloadVariantType::LIVEPHOTOVIDEO) { + $disk = $photo->size_variants->getSizeVariant(SizeVariantType::ORIGINAL)->storage_disk->value; + $sourceFile = new FlysystemFile(Storage::disk($disk), $photo->live_photo_short_path); + $baseFilenameAddon = ''; + } else { + $sv = $photo->size_variants->getSizeVariant($downloadVariant->getSizeVariantType()); + $baseFilenameAddon = ''; + if ($sv !== null) { + $sourceFile = $sv->getFile(); + // The filename of the original size variant shall get no + // particular suffix but remain as is. + // All other size variants (i.e. the generated, smaller ones) + // get size information as suffix. + if ($sv->type !== SizeVariantType::ORIGINAL) { + $baseFilenameAddon = '-' . $sv->width . 'x' . $sv->height; } - } elseif (Configs::get_value('downloadable', '0') === '0') { - return null; + } else { + throw new InvalidSizeVariantException('Size variant missing'); } } - $title = str_replace($this->badChars, '', $photo->title) ?: 'Untitled'; - - $prefix_path = $photo->type == 'raw' ? 'raw/' : 'big/'; - - // determine the file based on given size - if ($photo->isVideo() === false) { - $fileName = $photo->url; - } else { - $fileName = $photo->thumbUrl; - } - - switch ($kind_input) { - case 'FULL': - $path = $prefix_path . $photo->url; - $kind = ''; - break; - case 'LIVEPHOTOVIDEO': - $path = $prefix_path . $photo->livePhotoUrl; - $kind = ''; - break; - case 'MEDIUM2X': - $path = 'medium/' . Helpers::ex2x($fileName); - $kind = '-' . $photo->medium2x_width . 'x' . $photo->medium2x_height; - break; - case 'MEDIUM': - $path = 'medium/' . $fileName; - $kind = '-' . $photo->medium_width . 'x' . $photo->medium_height; - break; - case 'SMALL2X': - $path = 'small/' . Helpers::ex2x($fileName); - $kind = '-' . $photo->small2x_width . 'x' . $photo->small2x_height; - break; - case 'SMALL': - $path = 'small/' . $fileName; - $kind = '-' . $photo->small_width . 'x' . $photo->small_height; - break; - case 'THUMB2X': - $path = 'thumb/' . Helpers::ex2x($photo->thumbUrl); - $kind = '-' . Photo::THUMBNAIL2X_DIM . 'x' . Photo::THUMBNAIL2X_DIM; - break; - case 'THUMB': - $path = 'thumb/' . $photo->thumbUrl; - $kind = '-' . Photo::THUMBNAIL_DIM . 'x' . Photo::THUMBNAIL_DIM; - break; - default: - Logs::error(__METHOD__, __LINE__, 'Invalid kind ' . $kind_input); - - return null; - } - - $fullpath = Storage::path($path); - - // Check the file actually exists - if (!Storage::exists($path)) { - Logs::error(__METHOD__, __LINE__, 'File is missing: ' . $fullpath . ' (' . $title . ')'); - - return null; - } - - // Get extension of image - $extension = ''; - if ($photo->type != 'raw') { - $extension = Helpers::getExtension($fullpath, false); - } - - return [$title, $kind, $extension, $fullpath]; + return new ArchiveFileInfo($baseFilename, $baseFilenameAddon, $sourceFile); } } diff --git a/app/Actions/Photo/Create.php b/app/Actions/Photo/Create.php index b1c43cd1138..060595e9259 100644 --- a/app/Actions/Photo/Create.php +++ b/app/Actions/Photo/Create.php @@ -1,133 +1,339 @@ checkPermissions(); - - $this->public = 0; - $this->star = 0; - $this->albumID = null; - - $this->initParentId($albumID_in); - - // Verify extension - $this->extension = Helpers::getExtension($file['name'], false); - $this->mimeType = $file['type']; - $this->kind = $this->file_type($file, $this->extension); - - // Generate id - $this->photo = new Photo(); - $this->photo->id = Helpers::generateID(); - - // Set paths - $this->tmp_name = $file['tmp_name']; - $this->is_uploaded = is_uploaded_file($file['tmp_name']); - $this->path_prefix = ($this->kind != 'raw') ? 'big/' : 'raw/'; - - // Calculate checksum - $this->photo->checksum = $this->checksum($this->tmp_name); - $duplicate = $this->get_duplicate($this->photo->checksum); - $exists = ($duplicate !== null); - - $this->photo_Url = substr($this->photo->checksum, 0, 32) . $this->extension; - $this->path = Storage::path($this->path_prefix . $this->photo_Url); - /* - * ! From here we need to use a Strategy depending if we have - * ! a duplicate - * ! a "normal" picture - * ! a live picture - * ! a video - */ - - if (!$duplicate) { - $strategy = new StrategyPhoto($import_via_symlink); - } else { - $strategy = new StrategyDuplicate($skip_duplicates, $resync_metadata, $delete_imported); + /** @var ImportParam the strategy parameters prepared and compiled by this class */ + protected ImportParam $strategyParameters; + + public function __construct(?ImportMode $importMode, int $intendedOwnerId) + { + $this->strategyParameters = new ImportParam($importMode, $intendedOwnerId); + } + + /** + * Adds/imports the designated source file to Lychee. + * + * Depending on the type and origin of the source file as well as + * depending on operational settings, this method applies different + * strategies. + * This method may create a new database entry or update an existing + * database entry. + * + * @param NativeLocalFile $sourceFile the source file + * @param int|null $fileLastModifiedTime the timestamp to use if there's no creation date in Exif + * @param AbstractAlbum|null $album the targeted parent album + * + * @return Photo the newly created or updated photo + * + * @throws ModelNotFoundException + * @throws QuotaExceededException + * @throws LycheeException + */ + public function add(NativeLocalFile $sourceFile, ?AbstractAlbum $album, ?int $fileLastModifiedTime = null): Photo + { + $this->checkQuota($sourceFile); + + if (Features::inactive('create-photo-via-pipes')) { + $oldCodePath = new LegacyPhotoCreate($this->strategyParameters->importMode, $this->strategyParameters->intendedOwnerId); + + return $oldCodePath->add($sourceFile, $album, $fileLastModifiedTime); + } + + $initDTO = new InitDTO( + parameters: $this->strategyParameters, + sourceFile: $sourceFile, + album: $album, + fileLastModifiedTime: $fileLastModifiedTime + ); + + /** @var InitDTO $initDTO */ + $initDTO = app(Pipeline::class) + ->send($initDTO) + ->through([ + Init\AssertSupportedMedia::class, + Init\FetchLastModifiedTime::class, + Init\InitParentAlbum::class, + Init\LoadFileMetadata::class, + Init\FindDuplicate::class, + Init\FindLivePartner::class, + ]) + ->thenReturn(); + + if ($initDTO->duplicate !== null) { + return $this->handleDuplicate($initDTO); + } + + if ($initDTO->livePartner === null) { + return $this->handleStandalone($initDTO); + } + + // livePartner !== null + if ($sourceFile->isSupportedVideo()) { + return $this->handleVideoLivePartner($initDTO); + } + + if ($sourceFile->isSupportedImage()) { + return $this->handlePhotoLivePartner($initDTO); + } + + throw new LycheeLogicException('Pipe system for importing video failed'); + } + + /** + * Handle duplicate case. + * + * @param InitDTO $initDTO initial fetched + * + * @return Photo Photo duplicated + * + * @throws PhotoResyncedException + * @throws PhotoSkippedException + */ + private function handleDuplicate(InitDTO $initDTO): Photo + { + $dto = DuplicateDTO::ofInit($initDTO); + + $pipes = []; + if ($dto->shallResyncMetadata) { + $pipes[] = Shared\HydrateMetadata::class; + $pipes[] = Duplicate\SaveIfDirty::class; + } + $pipes[] = Duplicate\ThrowSkipDuplicate::class; + $pipes[] = Duplicate\ReplicateAsPhoto::class; + $pipes[] = Shared\SetStarred::class; + $pipes[] = Shared\SetParentAndOwnership::class; + $pipes[] = Shared\Save::class; + $pipes[] = Shared\NotifyAlbums::class; + + try { + return app(Pipeline::class) + ->send($dto) + ->through($pipes) + ->thenReturn() + ->getPhoto(); + } catch (PhotoResyncedException|PhotoSkippedException $e) { + // duplicate case. Just rethrow. + throw $e; + } + } + + private function handleStandalone(InitDTO $initDTO): Photo + { + $dto = StandaloneDTO::ofInit($initDTO); + + $pipes = [ + Standalone\FixTimeStamps::class, + Standalone\InitNamingStrategy::class, + Shared\HydrateMetadata::class, + Shared\SetStarred::class, + Shared\SetParentAndOwnership::class, + Standalone\SetOriginalChecksum::class, + Standalone\FetchSourceImage::class, + Standalone\ExtractGoogleMotionPictures::class, + Standalone\PlacePhoto::class, + Standalone\PlaceGoogleMotionVideo::class, + Standalone\SetChecksum::class, + Shared\Save::class, + Standalone\CreateOriginalSizeVariant::class, + Standalone\CreateSizeVariants::class, + Standalone\EncodePlaceholder::class, + Standalone\ReplaceOriginalWithBackup::class, + Shared\UploadSizeVariantsToS3::class, + ]; + + return $this->executePipeOnDTO($pipes, $dto)->getPhoto(); + } + + private function handleVideoLivePartner(InitDTO $initDTO): Photo + { + $dto = VideoPartnerDTO::ofInit($initDTO); + + $pipes = [ + VideoPartner\GetVideoPath::class, + VideoPartner\PlaceVideo::class, + VideoPartner\UpdateLivePartner::class, + Shared\Save::class, + ]; + + return $this->executePipeOnDTO($pipes, $dto)->getPhoto(); + } + + /** + * Execute the pipes on the DTO. + * + * @template T of VideoPartnerDTO|StandaloneDTO|PhotoPartnerDTO + * + * @param array $pipes + * @param T $dto + * + * @return T + * + * @throws LycheeException + */ + private function executePipeOnDTO(array $pipes, VideoPartnerDTO|StandaloneDTO|PhotoPartnerDTO $dto): VideoPartnerDTO|StandaloneDTO|PhotoPartnerDTO + { + try { + return app(Pipeline::class) + ->send($dto) + ->through($pipes) + ->thenReturn(); + } catch (LycheeException $e) { + // If source file could not be put into final destination, remove + // freshly created photo from DB to avoid having "zombie" entries. + try { + $dto->getPhoto()->delete(); + } catch (\Throwable) { + // Sic! If anything goes wrong here, we still throw the original exception + } + throw $e; } + } - $strategy->storeFile($this); - $strategy->hydrate($this, $duplicate, $file); + /** + * Adds a photo as partner to an existing video. + * + * Note the asymmetry to {@link handleVideoLivePartner}. + * + * A photo is always added as if it had no partner, even if the video had + * been added first. + * Then the already existing video is added to the freshly added photo. + * Hence, this strategy works mostly like the stand-alone strategy and also + * requires the photo file to be a native, local file in order to be able to + * extract EXIF data. + */ + private function handlePhotoLivePartner(InitDTO $initDTO): Photo + { + // Save old video. + $oldVideo = $initDTO->livePartner; - // set $this->info - $strategy->loadMetadata($this, $file); + // Import Photo as stand alone. + $standAloneDto = StandaloneDTO::ofInit($initDTO); + $standAlonePipes = [ + Standalone\FixTimeStamps::class, + Standalone\InitNamingStrategy::class, + Shared\HydrateMetadata::class, + Shared\SetStarred::class, + Shared\SetParentAndOwnership::class, + Standalone\SetOriginalChecksum::class, + Standalone\FetchSourceImage::class, + Standalone\ExtractGoogleMotionPictures::class, + Standalone\PlacePhoto::class, + Standalone\PlaceGoogleMotionVideo::class, + Standalone\SetChecksum::class, + Shared\Save::class, + Standalone\CreateOriginalSizeVariant::class, + Standalone\CreateSizeVariants::class, + Standalone\EncodePlaceholder::class, + Standalone\ReplaceOriginalWithBackup::class, + Shared\UploadSizeVariantsToS3::class, + ]; + $standAloneDto = $this->executePipeOnDTO($standAlonePipes, $standAloneDto); - $strategy->setParentAndOwnership($this); + // Use file from video as input for Video Partner and import + $videoPartnerDTO = new VideoPartnerDTO( + videoFile: $oldVideo->size_variants->getOriginal()->getFile(), + shallDeleteImported: true, + shallImportViaSymlink: false, + photo: $standAloneDto->getPhoto() + ); + $videoPartnerPipes = [ + VideoPartner\GetVideoPath::class, + VideoPartner\PlaceVideo::class, + VideoPartner\UpdateLivePartner::class, + Shared\Save::class, + ]; + $videoPartnerDTO = $this->executePipeOnDTO($videoPartnerPipes, $videoPartnerDTO); - // set $this->livePhotoPartner - $strategy->findLivePartner($this); + $finalizeDTO = new PhotoPartnerDTO( + photo: $videoPartnerDTO->photo, + oldVideo: $oldVideo + ); - $no_error = true; - $skip_db_entry_creation = false; + // Finalize + $finalize = [ + PhotoPartner\SetOldChecksum::class, + PhotoPartner\DeleteOldVideoPartner::class, + Shared\Save::class, + ]; - $strategy->generate_thumbs($this, $skip_db_entry_creation, $no_error); + return $this->executePipeOnDTO($finalize, $finalizeDTO)->getPhoto(); + } + + /** + * Check whether the user has enough quota to upload the file. + * + * @param NativeLocalFile $sourceFile + * + * @return void + * + * @throws QuotaExceededException + * + * @codeCoverageIgnore + */ + private function checkQuota(NativeLocalFile $sourceFile): void + { + $verify = resolve(Verify::class); - // In case it's a live photo and we've uploaded the video - if ($skip_db_entry_creation === true) { - $res = $this->livePhotoPartner->id; - } else { - $res = $this->save($this->photo); + // if the installation is not validated or + // if the user is not a supporter, we skip. + if (!$verify->validate() || !$verify->is_supporter()) { + return; } - if ($delete_imported && !$this->is_uploaded && ($exists || !$import_via_symlink) && !@unlink($this->tmp_name)) { - Logs::warning(__METHOD__, __LINE__, 'Failed to delete file (' . $this->tmp_name . ')'); + $user = \User::find($this->strategyParameters->intendedOwnerId) ?? throw new ModelNotFoundException(); + + // User does not have quota + if ($user->quota_kb === null) { + return; } - if ($this->albumID) { - $notify = new Notify(); - $notify->do($this->photo); + // Admins can upload without quota + if ($user->may_administrate === true) { + return; } - return $res; + $spaces = (new Spaces())->getFullSpacePerUser($user->id); + $used = $spaces[0]['size']; + + if (($user->quota_kb * 1024) <= $used + $sourceFile->getFilesize()) { + throw new QuotaExceededException(); + } } } diff --git a/app/Actions/Photo/Delete.php b/app/Actions/Photo/Delete.php index ec410a4b8d8..ac67a1c35b7 100644 --- a/app/Actions/Photo/Delete.php +++ b/app/Actions/Photo/Delete.php @@ -1,25 +1,408 @@ delete()` on every + * `Photo` model and the `Photo` model would take care of deleting its + * associated size variants including the media files. + * But this is extremely inefficient due to Laravel's architecture: + * + * - Models are heavyweight god classes such that every instance also carries + * the whole code for serialization/deserialization + * - Models are active records (and don't use the unit-of-work pattern), i.e. + * every deletion of a model directly triggers a DB operation; they are + * not deferred into a batch operation + * + * Moreover, while removing the records for photos and size variants from the + * DB can be implemented rather efficiently, the actual file operations may + * take some time. + * Especially, if the files are not stored locally but on a remote file system. + * Hence, this method collects all files which need to be removed. + * The caller can then decide to delete them asynchronously. + */ +readonly class Delete { - use Save; + protected FileDeleter $fileDeleter; + + public function __construct() + { + $this->fileDeleter = new FileDeleter(); + } + + /** + * Deletes the designated photos from the DB. + * + * The method only deletes the records for photos, their size variants + * and potentially associated symbolic links from the DB. + * The method does not delete the associated files from physical storage. + * Instead, the method returns an object in which all these files have + * been collected. + * This object can (and must) be used to eventually delete the files, + * however doing so can be deferred. + * + * The method allows deleting individual photos designated by + * `$photoIDs` or photos of entire albums designated by `$albumIDs`. + * The latter is more efficient, if albums shall be deleted, because + * it results in more succinct SQL queries. + * Both parameters can be used simultaneously and result in a merged + * deletion of the joined set of photos. + * + * @param string[] $photoIDs the photo IDs + * @param string[] $albumIDs the album IDs + * + * @return FileDeleter contains the collected files which became obsolete + * + * @throws ModelDBException + */ + public function do(array $photoIDs, array $albumIDs = []): FileDeleter + { + // TODO: replace this with pipelines, This is typically the kind of pattern. + try { + $this->collectSizeVariantPathsByPhotoID($photoIDs); + $this->collectSizeVariantPathsByAlbumID($albumIDs); + $this->collectLivePhotoPathsByPhotoID($photoIDs); + $this->collectLivePhotoPathsByAlbumID($albumIDs); + $this->collectSymLinksByPhotoID($photoIDs); + $this->collectSymLinksByAlbumID($albumIDs); + $this->deleteDBRecords($photoIDs, $albumIDs); + // @codeCoverageIgnoreStart + } catch (QueryBuilderException $e) { + throw ModelDBException::create('photos', 'deleting', $e); + } + // @codeCoverageIgnoreEnd + Album::query()->whereIn('header_id', $photoIDs)->update(['header_id' => null]); + + return $this->fileDeleter; + } + + /** + * Collects all short paths of size variants which shall be deleted from + * disk. + * + * Size variants which belong to a photo which has a duplicate that is + * not going to be deleted are skipped. + * + * @param array $photoIDs the photo IDs + * + * @return void + * + * @throws QueryBuilderException + */ + private function collectSizeVariantPathsByPhotoID(array $photoIDs): void + { + try { + if (count($photoIDs) === 0) { + return; + } + + // Maybe consider doing multiple queries for the different storage types. + $sizeVariants = SizeVariant::query() + ->from('size_variants as sv') + ->select(['sv.short_path', 'sv.storage_disk']) + ->join('photos as p', 'p.id', '=', 'sv.photo_id') + ->leftJoin('photos as dup', function (JoinClause $join) use ($photoIDs) { + $join + ->on('dup.checksum', '=', 'p.checksum') + ->whereNotIn('dup.id', $photoIDs); + }) + ->whereIn('p.id', $photoIDs) + ->whereNull('dup.id') + ->get(); + $this->fileDeleter->addSizeVariants($sizeVariants); + // @codeCoverageIgnoreStart + } catch (\InvalidArgumentException $e) { + throw LycheeAssertionError::createFromUnexpectedException($e); + } + // @codeCoverageIgnoreEnd + } + + /** + * Collects all short paths of size variants which shall be deleted from + * disk. + * + * Size variants which belong to a photo which has a duplicate that is + * not going to be deleted are skipped. + * + * @param array $albumIDs the album IDs + * + * @return void + * + * @throws QueryBuilderException + */ + private function collectSizeVariantPathsByAlbumID(array $albumIDs): void + { + try { + if (count($albumIDs) === 0) { + return; + } + + // Maybe consider doing multiple queries for the different storage types. + $sizeVariants = SizeVariant::query() + ->from('size_variants as sv') + ->select(['sv.short_path', 'sv.storage_disk']) + ->join('photos as p', 'p.id', '=', 'sv.photo_id') + ->leftJoin('photos as dup', function (JoinClause $join) use ($albumIDs) { + $join + ->on('dup.checksum', '=', 'p.checksum') + ->whereNotIn('dup.album_id', $albumIDs); + }) + ->whereIn('p.album_id', $albumIDs) + ->whereNull('dup.id') + ->get(); + $this->fileDeleter->addSizeVariants($sizeVariants); + // @codeCoverageIgnoreStart + } catch (\InvalidArgumentException $e) { + throw LycheeAssertionError::createFromUnexpectedException($e); + } + // @codeCoverageIgnoreEnd + } - public function do(array $photoIds) + /** + * Collects all short paths of live photos which shall be deleted from + * disk. + * + * Live photos which have a duplicate that is not going to be deleted are + * skipped. + * + * @param array $photoIDs the photo IDs + * + * @return void + * + * @throws QueryBuilderException + */ + private function collectLivePhotoPathsByPhotoID(array $photoIDs) { - $photos = Photo::whereIn('id', $photoIds)->get(); + try { + if (count($photoIDs) === 0) { + return; + } - $no_error = true; + $livePhotoShortPaths = Photo::query() + ->from('photos as p') + ->select(['p.live_photo_short_path', 'sv.storage_disk']) + ->join('size_variants as sv', function (JoinClause $join) { + $join + ->on('sv.photo_id', '=', 'p.id') + ->where('sv.type', '=', SizeVariantType::ORIGINAL); + }) + ->leftJoin('photos as dup', function (JoinClause $join) use ($photoIDs) { + $join + ->on('dup.live_photo_checksum', '=', 'p.live_photo_checksum') + ->whereNotIn('dup.id', $photoIDs); + }) + ->whereIn('p.id', $photoIDs) + ->whereNull('dup.id') + ->whereNotNull('p.live_photo_short_path') + ->get(['p.live_photo_short_path', 'sv.storage_disk']); - foreach ($photos as $photo) { - $no_error &= $photo->predelete(); - $no_error &= $photo->delete(); + $liveVariantsShortPathsGrouped = $livePhotoShortPaths->groupBy('storage_disk'); + $liveVariantsShortPathsGrouped->each( + fn ($liveVariantsShortPaths, $disk) => + /** @phpstan-ignore-next-line */ + $this->fileDeleter->addFiles($liveVariantsShortPaths->map(fn ($lv) => $lv->live_photo_short_path), $disk) + ); + // @codeCoverageIgnoreStart + } catch (\InvalidArgumentException $e) { + throw LycheeAssertionError::createFromUnexpectedException($e); } + // @codeCoverageIgnoreEnd + } + + /** + * Collects all short paths of live photos which shall be deleted from + * disk. + * + * Live photos which have a duplicate that is not going to be deleted are + * skipped. + * + * @param array $albumIDs the album IDs + * + * @return void + * + * @throws QueryBuilderException + */ + private function collectLivePhotoPathsByAlbumID(array $albumIDs) + { + try { + if (count($albumIDs) === 0) { + return; + } + + $livePhotoShortPaths = Photo::query() + ->from('photos as p') + ->select(['p.live_photo_short_path', 'sv.storage_disk']) + ->join('size_variants as sv', function (JoinClause $join) { + $join + ->on('sv.photo_id', '=', 'p.id') + ->where('sv.type', '=', SizeVariantType::ORIGINAL); + }) + ->leftJoin('photos as dup', function (JoinClause $join) use ($albumIDs) { + $join + ->on('dup.live_photo_checksum', '=', 'p.live_photo_checksum') + ->whereNotIn('dup.album_id', $albumIDs); + }) + ->whereIn('p.album_id', $albumIDs) + ->whereNull('dup.id') + ->whereNotNull('p.live_photo_short_path') + ->get(['p.live_photo_short_path', 'sv.storage_disk']); + + $liveVariantsShortPathsGrouped = $livePhotoShortPaths->groupBy('storage_disk'); + $liveVariantsShortPathsGrouped->each( + /** @phpstan-ignore-next-line */ + fn ($liveVariantsShortPaths, $disk) => $this->fileDeleter->addFiles($liveVariantsShortPaths->map(fn ($lv) => $lv->live_photo_short_path), $disk) + ); + // @codeCoverageIgnoreStart + } catch (\InvalidArgumentException $e) { + throw LycheeAssertionError::createFromUnexpectedException($e); + } + // @codeCoverageIgnoreEnd + } - return $no_error; + /** + * Collects all symbolic links which shall be deleted from disk. + * + * @param array $photoIDs the photo IDs + * + * @return void + * + * @throws QueryBuilderException + */ + private function collectSymLinksByPhotoID(array $photoIDs): void + { + try { + if (count($photoIDs) === 0) { + return; + } + + $symLinkPaths = SymLink::query() + ->from('sym_links', 'sl') + ->select(['sl.short_path']) + ->join('size_variants as sv', 'sv.id', '=', 'sl.size_variant_id') + ->whereIn('sv.photo_id', $photoIDs) + ->pluck('sl.short_path'); + $this->fileDeleter->addSymbolicLinks($symLinkPaths); + // @codeCoverageIgnoreStart + } catch (\InvalidArgumentException $e) { + throw LycheeAssertionError::createFromUnexpectedException($e); + } + // @codeCoverageIgnoreEnd + } + + /** + * Collects all symbolic links which shall be deleted from disk. + * + * @param array $albumIDs the album IDs + * + * @return void + * + * @throws QueryBuilderException + */ + private function collectSymLinksByAlbumID(array $albumIDs): void + { + try { + if (count($albumIDs) === 0) { + return; + } + + $symLinkPaths = SymLink::query() + ->from('sym_links', 'sl') + ->select(['sl.short_path']) + ->join('size_variants as sv', 'sv.id', '=', 'sl.size_variant_id') + ->join('photos as p', 'p.id', '=', 'sv.photo_id') + ->whereIn('p.album_id', $albumIDs) + ->pluck('sl.short_path'); + $this->fileDeleter->addSymbolicLinks($symLinkPaths); + // @codeCoverageIgnoreStart + } catch (\InvalidArgumentException $e) { + throw LycheeAssertionError::createFromUnexpectedException($e); + } + // @codeCoverageIgnoreEnd + } + + /** + * Deletes the records from DB. + * + * The records are deleted in such an order that foreign keys are not + * broken. + * + * @param array $photoIDs the photo IDs + * @param array $albumIDs the album IDs + * + * @return void + * + * @throws QueryBuilderException + */ + private function deleteDBRecords(array $photoIDs, array $albumIDs): void + { + try { + if (count($photoIDs) !== 0) { + SymLink::query() + ->whereExists(function (BaseBuilder $query) use ($photoIDs) { + $query + ->from('size_variants', 'sv') + ->whereColumn('sv.id', '=', 'sym_links.size_variant_id') + ->whereIn('photo_id', $photoIDs); + }) + ->delete(); + } + if (count($albumIDs) !== 0) { + SymLink::query() + ->whereExists(function (BaseBuilder $query) use ($albumIDs) { + $query + ->from('size_variants', 'sv') + ->whereColumn('sv.id', '=', 'sym_links.size_variant_id') + ->join('photos', 'photos.id', '=', 'sv.photo_id') + ->whereIn('photos.album_id', $albumIDs); + }) + ->delete(); + } + if (count($photoIDs) !== 0) { + SizeVariant::query() + ->whereIn('size_variants.photo_id', $photoIDs) + ->delete(); + } + if (count($albumIDs) !== 0) { + SizeVariant::query() + ->whereExists(function (BaseBuilder $query) use ($albumIDs) { + $query + ->from('photos', 'p') + ->whereColumn('p.id', '=', 'size_variants.photo_id') + ->whereIn('p.album_id', $albumIDs); + }) + ->delete(); + } + if (count($photoIDs) !== 0) { + Photo::query()->whereIn('id', $photoIDs)->delete(); + } + if (count($albumIDs) !== 0) { + Photo::query()->whereIn('album_id', $albumIDs)->delete(); + } + // @codeCoverageIgnoreStart + } catch (\InvalidArgumentException $e) { + throw LycheeAssertionError::createFromUnexpectedException($e); + } + // @codeCoverageIgnoreEnd } } diff --git a/app/Actions/Photo/Duplicate.php b/app/Actions/Photo/Duplicate.php index fda3daf55e3..362cad84366 100644 --- a/app/Actions/Photo/Duplicate.php +++ b/app/Actions/Photo/Duplicate.php @@ -1,75 +1,45 @@ albumFactory = $albumFactory; - } - - public function do(array $photoIds, ?string $albumID) + /** + * Duplicates a set of photos. + * + * @param Collection $photos the source photos + * @param Album|null $album the destination album; `null` means root album + * + * @return Collection the duplicates + * + * @throws ModelDBException + */ + public function do(Collection $photos, ?Album $album): Collection { - $photos = Photo::query()->whereIn('id', $photoIds)->get(); - - $duplicate = null; + $duplicates = new Collection(); /** @var Photo $photo */ foreach ($photos as $photo) { - $duplicate = new Photo(); - $duplicate->id = Helpers::generateID(); - $duplicate->title = $photo->title; - $duplicate->description = $photo->description; - $duplicate->url = $photo->url; - $duplicate->tags = $photo->tags; - $duplicate->public = $photo->public; - $duplicate->type = $photo->type; - $duplicate->width = $photo->width; - $duplicate->height = $photo->height; - $duplicate->filesize = $photo->filesize; - $duplicate->iso = $photo->iso; - $duplicate->aperture = $photo->aperture; - $duplicate->make = $photo->make; - $duplicate->model = $photo->model; - $duplicate->lens = $photo->lens; - $duplicate->shutter = $photo->shutter; - $duplicate->focal = $photo->focal; - $duplicate->latitude = $photo->latitude; - $duplicate->longitude = $photo->longitude; - $duplicate->altitude = $photo->altitude; - $duplicate->imgDirection = $photo->imgDirection; - $duplicate->location = $photo->location; - $duplicate->taken_at = $photo->taken_at; - $duplicate->star = $photo->star; - $duplicate->thumbUrl = $photo->thumbUrl; - $duplicate->thumb2x = $photo->thumb2x; - $duplicate->album_id = $albumID ?? $photo->album_id; - if ($duplicate->album_id === '0') { - $duplicate->album_id = null; + $duplicate = $photo->replicate(); + $duplicate->album_id = $album?->id; + $duplicate->setRelation('album', $album); + if ($album !== null) { + $duplicate->owner_id = $album->owner_id; } - $duplicate->checksum = $photo->checksum; - $duplicate->medium_width = $photo->medium_width; - $duplicate->medium_height = $photo->medium_height; - $duplicate->medium2x_width = $photo->medium2x_width; - $duplicate->medium2x_height = $photo->medium2x_height; - $duplicate->small_width = $photo->small_width; - $duplicate->small_height = $photo->small_height; - $duplicate->small2x_width = $photo->small2x_width; - $duplicate->small2x_height = $photo->small2x_height; - $duplicate->owner_id = $photo->owner_id; - $duplicate->livePhotoContentID = $photo->livePhotoContentID; - $duplicate->livePhotoUrl = $photo->livePhotoUrl; - $duplicate->livePhotoChecksum = $photo->livePhotoChecksum; - $this->save($duplicate); + $duplicate->save(); + $duplicates->add($duplicate); } + + return $duplicates; } } diff --git a/app/Actions/Photo/Extensions/ArchiveFileInfo.php b/app/Actions/Photo/Extensions/ArchiveFileInfo.php new file mode 100644 index 00000000000..bd492ac6b95 --- /dev/null +++ b/app/Actions/Photo/Extensions/ArchiveFileInfo.php @@ -0,0 +1,75 @@ +baseFilename . $this->baseFilenameAddon . $extraAddon . $this->file->getExtension(); + } +} diff --git a/app/Actions/Photo/Extensions/Checks.php b/app/Actions/Photo/Extensions/Checks.php deleted file mode 100644 index b57901fcb91..00000000000 --- a/app/Actions/Photo/Extensions/Checks.php +++ /dev/null @@ -1,105 +0,0 @@ -folders($errors); - if (count($errors) > 0) { - Logs::error(__METHOD__, __LINE__, 'An upload-folder is missing or not readable and writable'); - foreach ($errors as $error) { - Logs::error(__METHOD__, __LINE__, $error); - } - throw new FolderIsNotWritable(); - } - } - - public function folderPermission($folder) - { - $path = Storage::path($folder); - - if (Helpers::hasPermissions($path) === false) { - Logs::notice(__METHOD__, __LINE__, 'Skipped extaction of video from live photo, because ' . $path . ' is missing or not readable and writable.'); - throw new FolderIsNotWritable(); - } - - return $path; - } - - /** - * Check if a picture has a duplicate - * We compare the checksum to the other Photos or LivePhotos. - * - * @return false|Photo - */ - public function get_duplicate($checksum, $photoID = null) - { - return Photo::where(function ($q) use ($checksum) { - $q->where('checksum', '=', $checksum) - ->orWhere('livePhotoChecksum', '=', $checksum); - })->where('id', '<>', $photoID)->first(); - } - - /** - * Returns 'photo' if it is a photo - * Returns 'video' if it is a video - * Returns 'raw' if it is an accepted file (we only check extensions). - * - * @throws 'error message' if it is something else - * - * @param $file - * @param $extension - * - * @return string - */ - public function file_type($file, string $extension) - { - // check raw files - $raw_formats = strtolower(Configs::get_value('raw_formats', '')); - if (in_array(strtolower($extension), explode('|', $raw_formats), true)) { - return 'raw'; - } - - if (in_array(strtolower($extension), $this->validExtensions, true)) { - $mimeType = $file['type']; - if (in_array($mimeType, $this->validVideoTypes, true)) { - return 'video'; - } - - return 'photo'; - } - - // let's check for the mimetype - // maybe we don't have a photo - if (!function_exists('exif_imagetype')) { - Logs::error(__METHOD__, __LINE__, 'EXIF library not loaded. Make sure exif is enabled in php.ini'); - throw new JsonError('EXIF library not loaded on the server!'); - } - - $type = @exif_imagetype($file['tmp_name']); - if (in_array($type, $this->validTypes, true)) { - return 'photo'; - } - - Logs::error(__METHOD__, __LINE__, 'Photo type not supported: ' . $file['name']); - throw new JsonError('Photo type not supported!'); - } -} diff --git a/app/Actions/Photo/Extensions/Checksum.php b/app/Actions/Photo/Extensions/Checksum.php deleted file mode 100644 index 6f25dd41431..00000000000 --- a/app/Actions/Photo/Extensions/Checksum.php +++ /dev/null @@ -1,23 +0,0 @@ -validTypes, true); - } - - /** - * Returns a list of valid image types. - * - * @return array - */ - public function getValidImageTypes(): array - { - return $this->validTypes; - } - - /** - * Validates whether $type is a valid video type. - * - * @param string $type - * - * @return bool - */ - public function isValidVideoType(string $type): bool - { - return in_array($type, $this->validVideoTypes, true); - } - - /** - * Returns a list of valid video types. - * - * @return array - */ - public function getValidVideoTypes(): array - { - return $this->validVideoTypes; - } - - /** - * Validates whether $extension is a valid image or video extension. - * - * @param string $extension - * - * @return bool - */ - public function isValidExtension(string $extension): bool - { - return in_array(strtolower($extension), $this->validExtensions, true); - } - - /** - * Returns a list of valid image/video extensions. - * - * @return array - */ - public function getValidExtensions(): array - { - return $this->validExtensions; - } -} diff --git a/app/Actions/Photo/Extensions/ImageEditing.php b/app/Actions/Photo/Extensions/ImageEditing.php deleted file mode 100644 index 79397e818e5..00000000000 --- a/app/Actions/Photo/Extensions/ImageEditing.php +++ /dev/null @@ -1,182 +0,0 @@ -type == 'raw') { - // Create medium file for normal photos and for raws - $mediumMaxWidth = intval(Configs::get_value('medium_max_width')); - $mediumMaxHeight = intval(Configs::get_value('medium_max_height')); - $this->resizePhoto($photo, 'medium', $mediumMaxWidth, $mediumMaxHeight, $frame_tmp); - - if (Configs::get_value('medium_2x') === '1') { - $this->resizePhoto($photo, 'medium2x', $mediumMaxWidth * 2, $mediumMaxHeight * 2, $frame_tmp); - } - } - - $smallMaxWidth = intval(Configs::get_value('small_max_width')); - $smallMaxHeight = intval(Configs::get_value('small_max_height')); - $this->resizePhoto($photo, 'small', $smallMaxWidth, $smallMaxHeight, $frame_tmp); - - if (Configs::get_value('small_2x') === '1') { - $this->resizePhoto($photo, 'small2x', $smallMaxWidth * 2, $smallMaxHeight * 2, $frame_tmp); - } - } - - /** - * @param Photo $photo - * - * @return string Path of the jpg file - */ - public function createJpgFromRaw(Photo $photo): string - { - // we need imagick to do the job - if (!Configs::hasImagick()) { - Logs::notice(__METHOD__, __LINE__, 'Saving JPG of raw file failed: Imagick not installed.'); - - return ''; - } - - $filename = $photo->url; - $url = Storage::path('raw/' . $filename); - $ext = pathinfo($filename)['extension']; - - // test if Imagick supports the filetype - // Query return file extensions as all upper case - if (!in_array(strtoupper($ext), \Imagick::queryformats())) { - Logs::notice(__METHOD__, __LINE__, 'Filetype ' . $ext . ' not supported by Imagick.'); - - return ''; - } - - if (!($tmp_file = tempnam(sys_get_temp_dir(), 'lychee')) || - !rename($tmp_file, $tmp_file . '.jpeg')) { - Logs::notice(__METHOD__, __LINE__, 'Could not create a temporary file.'); - - return ''; - } - $tmp_file .= '.jpeg'; - Logs::notice(__METHOD__, __LINE__, 'Saving JPG of raw file to ' . $tmp_file); - - $resWidth = $resHeight = 0; - $width = $photo->width; - $height = $photo->height; - - try { - $this->imageHandler->scale($url, $tmp_file, $width, $height, $resWidth, $resHeight); - } catch (Exception $e) { - Logs::error(__METHOD__, __LINE__, 'Failed to create JPG from raw file ' . $url . $filename); - - return ''; - } - - return $tmp_file; - } - - /** - * Creates smaller copies of Photo. - * - * @param Photo $photo - * @param string $type - * @param int $maxWidth - * @param int $maxHeight - * @param string Path of the video frame - * - * @return bool - */ - public function resizePhoto(Photo $photo, string $type, int $maxWidth, int $maxHeight, string $frame_tmp = ''): bool - { - $width = $photo->width; - $height = $photo->height; - - if ($frame_tmp === '') { - $filename = $photo->url; - $url = Storage::path('big/' . $filename); - } else { - $filename = $photo->thumbUrl; - $url = $frame_tmp; - } - - // Both image sizes of the same type are stored in the same folder - // ie: medium and medium2x both belong in LYCHEE_UPLOADS_MEDIUM - $pathType = strtoupper($type); - if (($split = strpos($pathType, '2')) !== false) { - $pathType = substr($pathType, 0, $split); - } - - $uploadFolder = Storage::path(strtolower($pathType) . '/'); - if (Helpers::hasPermissions($uploadFolder) === false) { - Logs::notice(__METHOD__, __LINE__, 'Skipped creation of ' . $type . '-photo, because ' . $uploadFolder . ' is missing or not readable and writable.'); - - return false; - } - - // Add the @2x postfix if we're dealing with an HiDPI type - if (strpos($type, '2x') > 0) { - $filename = Helpers::ex2x($filename); - } - - // Is photo big enough? - if (($width <= $maxWidth || $maxWidth == 0) && ($height <= $maxHeight || $maxHeight == 0)) { - Logs::notice(__METHOD__, __LINE__, 'No resize (image is too small: ' . $maxWidth . 'x' . $maxHeight . ')!'); - - return false; - } - - $resWidth = $resHeight = 0; - if (!$this->imageHandler->scale($url, $uploadFolder . $filename, $maxWidth, $maxHeight, $resWidth, $resHeight)) { - Logs::error(__METHOD__, __LINE__, 'Failed to ' . $type . ' resize image'); - - return false; - } - - $photo->{$type . '_width'} = $resWidth; - $photo->{$type . '_height'} = $resHeight; - - return true; - } - - /** - * Create thumbnail for a picture. - * - * @param Photo $photo - * @param string Path of the video frame - * - * @return bool returns true when successful - */ - public function createThumb(Photo $photo, string $frame_tmp = ''): bool - { - Logs::notice(__METHOD__, __LINE__, 'Photo URL is ' . $photo->url); - - $src = ($frame_tmp === '') ? Storage::path('big/' . $photo->url) : $frame_tmp; - $photoName = explode('.', $photo->url); - - $this->imageHandler->crop($src, Storage::path('thumb/' . $photoName[0] . '.jpeg'), Photo::THUMBNAIL_DIM, Photo::THUMBNAIL_DIM); - - if (Configs::get_value('thumb_2x') === '1' && $photo->width >= Photo::THUMBNAIL2X_DIM && $photo->height >= Photo::THUMBNAIL2X_DIM) { - // Retina thumbs - $this->imageHandler->crop($src, Storage::path('thumb/' . $photoName[0] . '@2x.jpeg'), Photo::THUMBNAIL2X_DIM, Photo::THUMBNAIL2X_DIM); - $photo->thumb2x = 1; - } else { - $photo->thumb2x = 0; - } - - return true; - } -} diff --git a/app/Actions/Photo/Extensions/Metadata.php b/app/Actions/Photo/Extensions/Metadata.php deleted file mode 100644 index eb358f2a3d7..00000000000 --- a/app/Actions/Photo/Extensions/Metadata.php +++ /dev/null @@ -1,40 +0,0 @@ -extract($path, $kind); - if ($kind == 'raw') { - $info['type'] = 'raw'; - } - - // Use title of file if IPTC title missing - if ($info['title'] === '') { - if ($kind == 'raw') { - $info['title'] = substr(basename($file['name']), 0, 98); - } elseif ($info['title'] === '') { - $info['title'] = substr(basename($file['name'], $extension), 0, 98); - } - } - - return $info; - } -} diff --git a/app/Actions/Photo/Extensions/ParentAlbum.php b/app/Actions/Photo/Extensions/ParentAlbum.php deleted file mode 100644 index 15ea6fb3b3c..00000000000 --- a/app/Actions/Photo/Extensions/ParentAlbum.php +++ /dev/null @@ -1,33 +0,0 @@ -albumID = null; - if ($albumID_in != '0') { - $album = $factory->make($albumID_in); - - if ($album->is_tag_album()) { - throw new JsonError('Sorry, cannot upload to Tag Album.'); - } - - if (!$album->is_smart()) { - $this->parentAlbum = $album; // we save it so we don't have to query it again later - $this->albumID = $albumID_in; - } else { - $this->public = ($album->id == 'public'); - $this->star = ($album->id == 'starred'); - } - } - } -} diff --git a/app/Actions/Photo/Extensions/Save.php b/app/Actions/Photo/Extensions/Save.php deleted file mode 100644 index eec4199f656..00000000000 --- a/app/Actions/Photo/Extensions/Save.php +++ /dev/null @@ -1,64 +0,0 @@ -save()) { - throw new JsonError('Could not save photo in database!'); - } - } catch (QueryException $e) { - $retry = true; - $this->recover($e, $photo); - } - } while ($retry); - - // return the ID. - return $photo->id; - } - - /** - * Manage recovery from the Exception. - * - * @throws JsonError if code is neither 23000 or 23505 - */ - private function recover(QueryException $e, Photo &$photo) - { - $errorCode = $e->getCode(); - if ($errorCode == 23000 || $errorCode == 23505) { - // houston, we have a duplicate entry problem - do { - // Our ids are based on current system time, so - // wait randomly up to 1s before retrying. - usleep(rand(0, 1000000)); - $newId = Helpers::generateID(); - } while ($newId === $photo->id); - - $photo->id = $newId; - } else { - Logs::error(__METHOD__, __LINE__, 'Something went wrong, error ' . $errorCode . ', ' . $e->getMessage()); - - throw new JsonError('Something went wrong, error' . $errorCode . ', please check the logs'); - } - } -} diff --git a/app/Actions/Photo/Extensions/VideoEditing.php b/app/Actions/Photo/Extensions/VideoEditing.php deleted file mode 100644 index 57f426a0bbe..00000000000 --- a/app/Actions/Photo/Extensions/VideoEditing.php +++ /dev/null @@ -1,171 +0,0 @@ -aperture === '') { - $path = Storage::path('big/' . $photo->url); - - /* @var Extractor $metadataExtractor */ - $metadataExtractor = resolve(Extractor::class); - $info = $metadataExtractor->extract($path, 'video'); - $photo->aperture = $info['aperture']; - } - // we check again, just to be sure. - if ($photo->aperture === '') { - return ''; - } - - /** - * ! check if we can use path instead of this ugly thing. - */ - $ffmpeg = FFMpeg::create(); - /** @var Video */ - $video = $ffmpeg->open(Storage::path('big/' . $photo->url)); - if ( - !($tmp = tempnam(sys_get_temp_dir(), 'lychee')) || - !rename($tmp, $tmp . '.jpeg') - ) { - Logs::notice(__METHOD__, __LINE__, 'Could not create a temporary file.'); - - return ''; - } - $tmp .= '.jpeg'; - Logs::notice(__METHOD__, __LINE__, 'Saving frame to ' . $tmp); - - try { - /** - * ! check if we can use path instead of this ugly thing. - */ - $frame = $video->frame(TimeCode::fromSeconds($photo->aperture / 2)); - $frame->save($tmp); - } catch (Exception $e) { - Logs::notice(__METHOD__, __LINE__, 'Failed to extract snapshot from video ' . $tmp); - } - - // check if the image has data - $success = file_exists($tmp) ? (filesize($tmp) > 0) : false; - - if ($success) { - // Optimize image - if (Configs::get_value('lossless_optimization')) { - ImageOptimizer::optimize($tmp); - } - } else { - Logs::notice(__METHOD__, __LINE__, 'Failed to extract snapshot from video ' . $tmp); - try { - /** - * ! check if we can use path instead of this ugly thing. - */ - $frame = $video->frame(TimeCode::fromSeconds(0)); - $frame->save($tmp); - $success = file_exists($tmp) ? (filesize($tmp) > 0) : false; - if (!$success) { - Logs::notice(__METHOD__, __LINE__, 'Fallback failed to extract snapshot from video ' . $tmp); - } else { - Logs::notice(__METHOD__, __LINE__, 'Fallback successful - snapshot from video ' . $tmp . ' at t=0 created.'); - } - } catch (Exception $e) { - Logs::notice(__METHOD__, __LINE__, 'Fallback failed to extract snapshot from video ' . $tmp); - - return ''; - } - } - - return $tmp; - } - - /** - * Extract the video part of the a Livephoto. - * - * @param Photo $photo - * @param string $type - * @param int $maxWidth - * @param int $maxHeight - * @param string Path of the video frame - * - * @return bool - */ - public function extractVideo(Photo $photo, int $videoLengthBytes, string $frame_tmp = ''): bool - { - // We extract the video from the jpg file - // Google Motion Photo: See here for details - // - - if ($frame_tmp === '') { - $filename = $photo->url; - } else { - $filename = $photo->thumbUrl; - } - - $filename_video_mov = basename($filename, Helpers::getExtension($filename, false)) . '.mov'; - - $uploadFolder = $this->folderPermission('big/'); - - try { - // 1. Extract the video part - $fp = fopen($uploadFolder . $photo->url, 'r'); - $fp_video = tmpfile(); // use a temporary file, will be delted once closed - - // The MP4 file is located in the last bytes of the file - fseek($fp, -1 * $videoLengthBytes, SEEK_END); // It needs to be negative - $data = fread($fp, $videoLengthBytes); - fwrite($fp_video, $data, $videoLengthBytes); - - // 2. Convert file from mp4 to mov, but keeping audio and video codec - // This is needed to LivePhotosKit which only accepts mov files - // Computation is fast, since codecs, resolution, framerate etc. remain unchanged - - /** - * ! check if we can use path instead of this ugly thing. - */ - $ffmpeg = FFMpeg::create(); - $video = $ffmpeg->open(stream_get_meta_data($fp_video)['uri']); - $format = new MOVFormat(); - // Add additional parameter to extract the first video stream - $format->setAdditionalParameters(['-map', '0:0']); - $video->save($format, $uploadFolder . $filename_video_mov); - - // 3. Close files ($fp_video will be again deleted) - fclose($fp); - fclose($fp_video); - - // Save file path; Checksum calclation not needed since - // we do not perform matching for Google Motion Photos (as for iOS Live Photos) - $photo->livePhotoUrl = $filename_video_mov; - } catch (Exception $exception) { - Logs::error(__METHOD__, __LINE__, $exception->getMessage()); - - return false; - } - - return true; - } -} diff --git a/app/Actions/Photo/Move.php b/app/Actions/Photo/Move.php new file mode 100644 index 00000000000..5ba4353fe12 --- /dev/null +++ b/app/Actions/Photo/Move.php @@ -0,0 +1,48 @@ + $photos the source photos + * @param Album|null $album the destination album; `null` means root album + * + * @return void + * + * @throws ModelDBException + */ + public function do(Collection $photos, ?Album $album): void + { + $notify = new Notify(); + + /** @var Photo $photo */ + foreach ($photos as $photo) { + $photo->album_id = $album?->id; + // Avoid unnecessary DB request, when we access the album of a + // photo later (e.g. when a notification is sent). + $photo->setRelation('album', $album); + if ($album !== null) { + $photo->owner_id = $album->owner_id; + } + $photo->save(); + $notify->do($photo); + } + + Album::query()->whereIn('header_id', $photos->map(fn (Photo $p) => $p->id))->update(['header_id' => null]); + } +} diff --git a/app/Actions/Photo/Pipes/Duplicate/ReplicateAsPhoto.php b/app/Actions/Photo/Pipes/Duplicate/ReplicateAsPhoto.php new file mode 100644 index 00000000000..62b90bcc3dc --- /dev/null +++ b/app/Actions/Photo/Pipes/Duplicate/ReplicateAsPhoto.php @@ -0,0 +1,22 @@ +replicatePhoto(); + + return $next($state); + } +} diff --git a/app/Actions/Photo/Pipes/Duplicate/SaveIfDirty.php b/app/Actions/Photo/Pipes/Duplicate/SaveIfDirty.php new file mode 100644 index 00000000000..c0a297aa48b --- /dev/null +++ b/app/Actions/Photo/Pipes/Duplicate/SaveIfDirty.php @@ -0,0 +1,30 @@ +photo->isDirty()) { + Log::notice(__METHOD__ . ':' . __LINE__ . ' Updating metadata of existing photo.'); + $state->photo->save(); + $state->setHasBeenResync(true); + } else { + $state->setHasBeenResync(false); + } + + return $next($state); + } +} diff --git a/app/Actions/Photo/Pipes/Duplicate/ThrowSkipDuplicate.php b/app/Actions/Photo/Pipes/Duplicate/ThrowSkipDuplicate.php new file mode 100644 index 00000000000..576ba1b6112 --- /dev/null +++ b/app/Actions/Photo/Pipes/Duplicate/ThrowSkipDuplicate.php @@ -0,0 +1,29 @@ +shallSkipDuplicates) { + return $next($state); + } + + if ($state->hasBeenReSynced ?? false) { + throw new PhotoResyncedException(); + } + throw new PhotoSkippedException(); + } +} diff --git a/app/Actions/Photo/Pipes/Init/AssertSupportedMedia.php b/app/Actions/Photo/Pipes/Init/AssertSupportedMedia.php new file mode 100644 index 00000000000..e915ded5913 --- /dev/null +++ b/app/Actions/Photo/Pipes/Init/AssertSupportedMedia.php @@ -0,0 +1,33 @@ +sourceFile->assertIsSupportedMediaOrAcceptedRaw(); + + return $next($state); + } +} \ No newline at end of file diff --git a/app/Actions/Photo/Pipes/Init/FetchLastModifiedTime.php b/app/Actions/Photo/Pipes/Init/FetchLastModifiedTime.php new file mode 100644 index 00000000000..260b163ce1e --- /dev/null +++ b/app/Actions/Photo/Pipes/Init/FetchLastModifiedTime.php @@ -0,0 +1,31 @@ +fileLastModifiedTime === null) { + $state->fileLastModifiedTime ??= $state->sourceFile->lastModified(); + } + + return $next($state); + } +} + diff --git a/app/Actions/Photo/Pipes/Init/FindDuplicate.php b/app/Actions/Photo/Pipes/Init/FindDuplicate.php new file mode 100644 index 00000000000..ac6a9f20c93 --- /dev/null +++ b/app/Actions/Photo/Pipes/Init/FindDuplicate.php @@ -0,0 +1,37 @@ +sourceFile)->checksum; + + $state->duplicate = Photo::query() + ->where('checksum', '=', $checksum) + ->orWhere('original_checksum', '=', $checksum) + ->orWhere('live_photo_checksum', '=', $checksum) + ->first(); + + return $next($state); + } +} + diff --git a/app/Actions/Photo/Pipes/Init/FindLivePartner.php b/app/Actions/Photo/Pipes/Init/FindLivePartner.php new file mode 100644 index 00000000000..c22e4b171fc --- /dev/null +++ b/app/Actions/Photo/Pipes/Init/FindLivePartner.php @@ -0,0 +1,54 @@ +exifInfo->livePhotoContentID !== null) { + $state->livePartner = Photo::query() + ->where('live_photo_content_id', '=', $state->exifInfo->livePhotoContentID) + ->where('album_id', '=', $state->album?->id) + ->whereNull('live_photo_short_path')->first(); + } + + // if a potential partner has been found, ensure that it is of a + // different kind then the uploaded media. + if ( + $state->livePartner !== null && !( + BaseMediaFile::isSupportedImageMimeType($state->exifInfo->type) && $state->livePartner->isVideo() || + BaseMediaFile::isSupportedVideoMimeType($state->exifInfo->type) && $state->livePartner->isPhoto() + ) + ) { + $state->livePartner = null; + } + + return $next($state); + } catch (IllegalOrderOfOperationException $e) { + throw LycheeAssertionError::createFromUnexpectedException($e); + } + } +} + diff --git a/app/Actions/Photo/Pipes/Init/InitParentAlbum.php b/app/Actions/Photo/Pipes/Init/InitParentAlbum.php new file mode 100644 index 00000000000..0ed905eaa2a --- /dev/null +++ b/app/Actions/Photo/Pipes/Init/InitParentAlbum.php @@ -0,0 +1,46 @@ +album === null || $state->album instanceof Album) { + return $next($state); + } + + if ($state->album instanceof BaseSmartAlbum) { + if ($state->album instanceof StarredAlbum) { + $state->is_starred = true; + } + + $state->album = null; + + return $next($state); + } + + throw new InvalidPropertyException('The given parent album does not support uploading'); + } +} \ No newline at end of file diff --git a/app/Actions/Photo/Pipes/Init/LoadFileMetadata.php b/app/Actions/Photo/Pipes/Init/LoadFileMetadata.php new file mode 100644 index 00000000000..9bb7dc83268 --- /dev/null +++ b/app/Actions/Photo/Pipes/Init/LoadFileMetadata.php @@ -0,0 +1,41 @@ +exifInfo = Extractor::createFromFile($state->sourceFile, $state->fileLastModifiedTime); + + // Use basename of file if IPTC title missing + if ( + $state->exifInfo->title === null || + $state->exifInfo->title === '' + ) { + $state->exifInfo->title = substr($state->sourceFile->getOriginalBasename(), 0, 98); + } + + return $next($state); + } +} + diff --git a/app/Actions/Photo/Pipes/PhotoPartner/DeleteOldVideoPartner.php b/app/Actions/Photo/Pipes/PhotoPartner/DeleteOldVideoPartner.php new file mode 100644 index 00000000000..5017ecf5242 --- /dev/null +++ b/app/Actions/Photo/Pipes/PhotoPartner/DeleteOldVideoPartner.php @@ -0,0 +1,22 @@ +oldVideo->delete(); + + return $next($state); + } +} diff --git a/app/Actions/Photo/Pipes/PhotoPartner/SetOldChecksum.php b/app/Actions/Photo/Pipes/PhotoPartner/SetOldChecksum.php new file mode 100644 index 00000000000..64bad2094d0 --- /dev/null +++ b/app/Actions/Photo/Pipes/PhotoPartner/SetOldChecksum.php @@ -0,0 +1,23 @@ +photo->live_photo_checksum = $state->oldVideo->checksum; + + return $next($state); + } +} diff --git a/app/Actions/Photo/Pipes/Shared/HydrateMetadata.php b/app/Actions/Photo/Pipes/Shared/HydrateMetadata.php new file mode 100644 index 00000000000..e469147a991 --- /dev/null +++ b/app/Actions/Photo/Pipes/Shared/HydrateMetadata.php @@ -0,0 +1,91 @@ +photo->title === null) { + $state->photo->title = $state->exifInfo->title; + } + if ($state->photo->description === null) { + $state->photo->description = $state->exifInfo->description; + } + if (count($state->photo->tags) === 0) { + $state->photo->tags = $state->exifInfo->tags; + } + if ($state->photo->type === null) { + $state->photo->type = $state->exifInfo->type; + } + if ($state->photo->iso === null) { + $state->photo->iso = $state->exifInfo->iso; + } + if ($state->photo->aperture === null) { + $state->photo->aperture = $state->exifInfo->aperture; + } + if ($state->photo->make === null) { + $state->photo->make = $state->exifInfo->make; + } + if ($state->photo->model === null) { + $state->photo->model = $state->exifInfo->model; + } + if ($state->photo->lens === null) { + $state->photo->lens = $state->exifInfo->lens; + } + if ($state->photo->shutter === null) { + $state->photo->shutter = $state->exifInfo->shutter; + } + if ($state->photo->focal === null) { + $state->photo->focal = $state->exifInfo->focal; + } + if ($state->photo->taken_at === null) { + $state->photo->taken_at = $state->exifInfo->taken_at; + } + if ($state->photo->latitude === null) { + $state->photo->latitude = $state->exifInfo->latitude; + } + if ($state->photo->longitude === null) { + $state->photo->longitude = $state->exifInfo->longitude; + } + if ($state->photo->altitude === null) { + $state->photo->altitude = $state->exifInfo->altitude; + } + if ($state->photo->img_direction === null) { + $state->photo->img_direction = $state->exifInfo->imgDirection; + } + if ($state->photo->location === null) { + $state->photo->location = $state->exifInfo->location; + } + if ($state->photo->live_photo_content_id === null) { + $state->photo->live_photo_content_id = $state->exifInfo->livePhotoContentID; + } + + return $next($state); + } +} \ No newline at end of file diff --git a/app/Actions/Photo/Pipes/Shared/NotifyAlbums.php b/app/Actions/Photo/Pipes/Shared/NotifyAlbums.php new file mode 100644 index 00000000000..5e102b9848e --- /dev/null +++ b/app/Actions/Photo/Pipes/Shared/NotifyAlbums.php @@ -0,0 +1,29 @@ +getPhoto()->album_id !== null) { + $notify = new Notify(); + $notify->do($state->getPhoto()); + } + + return $next($state); + } +} \ No newline at end of file diff --git a/app/Actions/Photo/Pipes/Shared/Save.php b/app/Actions/Photo/Pipes/Shared/Save.php new file mode 100644 index 00000000000..d504468cea8 --- /dev/null +++ b/app/Actions/Photo/Pipes/Shared/Save.php @@ -0,0 +1,25 @@ +getPhoto()->save(); + + return $next($state); + } +} \ No newline at end of file diff --git a/app/Actions/Photo/Pipes/Shared/SetParentAndOwnership.php b/app/Actions/Photo/Pipes/Shared/SetParentAndOwnership.php new file mode 100644 index 00000000000..8676917093d --- /dev/null +++ b/app/Actions/Photo/Pipes/Shared/SetParentAndOwnership.php @@ -0,0 +1,36 @@ +album instanceof Album) { + $state->photo->album_id = $state->album->id; + // Avoid unnecessary DB request, when we access the album of a + // photo later (e.g. when a notification is sent). + $state->photo->setRelation('album', $state->album); + $state->photo->owner_id = $state->album->owner_id; + } else { + $state->photo->album_id = null; + // Avoid unnecessary DB request, when we access the album of a + // photo later (e.g. when a notification is sent). + $state->photo->setRelation('album', null); + $state->photo->owner_id = $state->intendedOwnerId; + } + + return $next($state); + } +} \ No newline at end of file diff --git a/app/Actions/Photo/Pipes/Shared/SetStarred.php b/app/Actions/Photo/Pipes/Shared/SetStarred.php new file mode 100644 index 00000000000..d403c0e27eb --- /dev/null +++ b/app/Actions/Photo/Pipes/Shared/SetStarred.php @@ -0,0 +1,24 @@ +photo->is_starred = $state->is_starred; + + return $next($state); + } +} \ No newline at end of file diff --git a/app/Actions/Photo/Pipes/Shared/UploadSizeVariantsToS3.php b/app/Actions/Photo/Pipes/Shared/UploadSizeVariantsToS3.php new file mode 100644 index 00000000000..84136fe5c43 --- /dev/null +++ b/app/Actions/Photo/Pipes/Shared/UploadSizeVariantsToS3.php @@ -0,0 +1,39 @@ +getPhoto()->size_variants->toCollection() + ->filter(fn ($v) => $v !== null) + ->map(fn (SizeVariant $variant) => new UploadSizeVariantToS3Job($variant)); + + $jobs->each(fn ($job) => $use_job_queues ? dispatch($job) : dispatch_sync($job)); + } + + return $next($state); + } +} diff --git a/app/Actions/Photo/Pipes/Standalone/CreateOriginalSizeVariant.php b/app/Actions/Photo/Pipes/Standalone/CreateOriginalSizeVariant.php new file mode 100644 index 00000000000..6bfdca3b41a --- /dev/null +++ b/app/Actions/Photo/Pipes/Standalone/CreateOriginalSizeVariant.php @@ -0,0 +1,37 @@ +sourceImage?->isLoaded() ? + $state->sourceImage->getDimensions() : + new ImageDimension($state->exifInfo->width, $state->exifInfo->height); + + $state->photo->size_variants->create( + SizeVariantType::ORIGINAL, + $state->targetFile->getRelativePath(), + $imageDim, + $state->streamStat->bytes + ); + + return $next($state); + } +} diff --git a/app/Actions/Photo/Pipes/Standalone/CreateSizeVariants.php b/app/Actions/Photo/Pipes/Standalone/CreateSizeVariants.php new file mode 100644 index 00000000000..d81db394412 --- /dev/null +++ b/app/Actions/Photo/Pipes/Standalone/CreateSizeVariants.php @@ -0,0 +1,39 @@ +sourceImage?->isLoaded()) { + try { + /** @var SizeVariantFactory $sizeVariantFactory */ + $sizeVariantFactory = resolve(SizeVariantFactory::class); + $sizeVariantFactory->init($state->photo, $state->sourceImage, $state->namingStrategy); + $sizeVariantFactory->createSizeVariants(); + } catch (\Throwable $t) { + // Don't re-throw the exception, because we do not want the + // import to fail completely only due to missing size variants. + // There are just too many options why the creation of size + // variants may fail. + Handler::reportSafely($t); + } + } + + return $next($state); + } +} diff --git a/app/Actions/Photo/Pipes/Standalone/EncodePlaceholder.php b/app/Actions/Photo/Pipes/Standalone/EncodePlaceholder.php new file mode 100644 index 00000000000..4307051f387 --- /dev/null +++ b/app/Actions/Photo/Pipes/Standalone/EncodePlaceholder.php @@ -0,0 +1,32 @@ +getPhoto()->size_variants->getPlaceholder(); + if ($placeholder !== null) { + $placeholderEncoder->do($placeholder); + } + + return $next($state); + } catch (\ErrorException $e) { + throw new MediaFileOperationException('Failed to encode placeholder to base64', $e); + } + } +} \ No newline at end of file diff --git a/app/Actions/Photo/Pipes/Standalone/ExtractGoogleMotionPictures.php b/app/Actions/Photo/Pipes/Standalone/ExtractGoogleMotionPictures.php new file mode 100644 index 00000000000..3d5f26ca86d --- /dev/null +++ b/app/Actions/Photo/Pipes/Standalone/ExtractGoogleMotionPictures.php @@ -0,0 +1,41 @@ +exifInfo->microVideoOffset === 0) { + return $next($state); + } + + // Handle Google Motion Pictures + // We must extract the video stream from the original (local) + // file and stash it away, before the original file is moved into + // its (potentially remote) final position + try { + $state->tmpVideoFile = new TemporaryLocalFile(GoogleMotionPictureHandler::FINAL_VIDEO_FILE_EXTENSION, $state->sourceFile->getBasename()); + $gmpHandler = new GoogleMotionPictureHandler(); + $gmpHandler->load($state->sourceFile, $state->exifInfo->microVideoOffset); + $gmpHandler->saveVideoStream($state->tmpVideoFile); + } catch (\Throwable $e) { + Handler::reportSafely($e); + $state->tmpVideoFile = null; + } + + return $next($state); + } +} diff --git a/app/Actions/Photo/Pipes/Standalone/FetchSourceImage.php b/app/Actions/Photo/Pipes/Standalone/FetchSourceImage.php new file mode 100644 index 00000000000..4704f006100 --- /dev/null +++ b/app/Actions/Photo/Pipes/Standalone/FetchSourceImage.php @@ -0,0 +1,46 @@ +photo->isVideo()) { + $videoHandler = new VideoHandler(); + $videoHandler->load($state->sourceFile); + $position = is_numeric($state->photo->aperture) ? floatval($state->photo->aperture) / 2 : 0.0; + $state->sourceImage = $videoHandler->extractFrame($position); + } else { + // Load source image if it is a supported photo format + $state->sourceImage = new ImageHandler(); + $state->sourceImage->load($state->sourceFile); + } + } catch (\Throwable $t) { + // This may happen for videos if FFmpeg is not available to + // extract a still frame, or for raw files (Imagick may be + // able to convert them to jpeg, but there are no + // guarantees, and Imagick may not be available). + $state->sourceImage = null; + + // Log an error without failing. + Handler::reportSafely($t); + } + + return $next($state); + } +} + diff --git a/app/Actions/Photo/Pipes/Standalone/FixTimeStamps.php b/app/Actions/Photo/Pipes/Standalone/FixTimeStamps.php new file mode 100644 index 00000000000..4dc02696d44 --- /dev/null +++ b/app/Actions/Photo/Pipes/Standalone/FixTimeStamps.php @@ -0,0 +1,25 @@ +photo->updateTimestamps(); + + return $next($state); + } +} diff --git a/app/Actions/Photo/Pipes/Standalone/InitNamingStrategy.php b/app/Actions/Photo/Pipes/Standalone/InitNamingStrategy.php new file mode 100644 index 00000000000..58538fc1f94 --- /dev/null +++ b/app/Actions/Photo/Pipes/Standalone/InitNamingStrategy.php @@ -0,0 +1,27 @@ +namingStrategy = resolve(AbstractSizeVariantNamingStrategy::class); + $state->namingStrategy->setPhoto($state->photo); + $state->namingStrategy->setExtension( + $state->sourceFile->getOriginalExtension() + ); + + return $next($state); + } +} diff --git a/app/Actions/Photo/Pipes/Standalone/PlaceGoogleMotionVideo.php b/app/Actions/Photo/Pipes/Standalone/PlaceGoogleMotionVideo.php new file mode 100644 index 00000000000..7b2dd8f3f8d --- /dev/null +++ b/app/Actions/Photo/Pipes/Standalone/PlaceGoogleMotionVideo.php @@ -0,0 +1,38 @@ +tmpVideoFile !== null) { + $videoTargetPath = + pathinfo($state->targetFile->getRelativePath(), PATHINFO_DIRNAME) . + '/' . + pathinfo($state->targetFile->getRelativePath(), PATHINFO_FILENAME) . + $state->tmpVideoFile->getExtension(); + $videoTargetFile = new FlysystemFile($state->targetFile->getDisk(), $videoTargetPath); + $videoTargetFile->write($state->tmpVideoFile->read()); + $state->photo->live_photo_short_path = $videoTargetFile->getRelativePath(); + $state->tmpVideoFile->close(); + $state->tmpVideoFile->delete(); + $state->tmpVideoFile = null; + } + + return $next($state); + } +} diff --git a/app/Actions/Photo/Pipes/Standalone/PlacePhoto.php b/app/Actions/Photo/Pipes/Standalone/PlacePhoto.php new file mode 100644 index 00000000000..6f08a882541 --- /dev/null +++ b/app/Actions/Photo/Pipes/Standalone/PlacePhoto.php @@ -0,0 +1,140 @@ +sourceFile` will be deleted after this step. + // But `$this->sourceImage` remains in memory. + $state->targetFile = $state->namingStrategy->createFile(SizeVariantType::ORIGINAL); + $state->streamStat = $this->putSourceIntoFinalDestination($state); + + return $next($state); + } + + /** + * Moves/copies/symlinks source file to final destination and + * normalizes orientation, if necessary. + * + * Note, {@link AddStandaloneStrategy::$sourceFile} and + * {@link AddStandaloneStrategy::$sourceImage} must be set before this + * method is called. + * + * If import via symbolic link is requested, then a symbolic link + * from `$targetFile` to {@link AddStandaloneStrategy::$sourceFile} is + * created. + * Otherwise the content of {@link AddStandaloneStrategy::$sourceFile} + * is physically copied/moved into `$targetFile`. + * + * If the source file requires normalization, then + * {@link AddStandaloneStrategy::$sourceImage} is saved to `$targetFile`. + * This step implicitly corrects the orientation. + * Otherwise, the original byte stream from + * {@link AddStandaloneStrategy::$sourceFile} is written to `$targetFile` + * without modifications. + * + * @param StandaloneDTO $state State of iteration + * + * @return StreamStats statistics about the final file, may differ from + * the source file due to normalization of orientation + * + * @throws MediaFileOperationException + * @throws ConfigurationException + */ + private function putSourceIntoFinalDestination(StandaloneDTO $state): StreamStats + { + try { + if ($state->shallImportViaSymlink) { + if (!$state->targetFile->isLocalFile()) { + throw new ConfigurationException('Symlinking is only supported on local filesystems'); + } + $targetPath = $state->targetFile->toLocalFile()->getPath(); + $sourcePath = $state->sourceFile->getRealPath(); + // For symlinks we must manually create a non-existing + // parent directory. + // This mimics the behaviour of Flysystem for regular files. + $targetDirectory = pathinfo($targetPath, PATHINFO_DIRNAME); + if (!is_dir($targetDirectory)) { + $umask = \umask(0); + \Safe\mkdir($targetDirectory, BasicPermissionCheck::getConfiguredDirectoryPerm(), true); + \umask($umask); + } + \Safe\symlink($sourcePath, $targetPath); + $streamStat = StreamStat::createFromLocalFile($state->sourceFile); + } else { + $shallNormalize = Configs::getValueAsBool('auto_fix_orientation') && + $state->sourceImage !== null && + $state->exifInfo->orientation !== 1; + + if ($shallNormalize) { + // Saving the loaded image to the final target normalizes + // the image orientation. This comes at the cost that + // the image is re-encoded and hence its quality might + // be reduced. + $streamStat = $state->sourceImage->save($state->targetFile, true); + $this->backupOriginal($state); + } else { + // If the image does not require normalization the + // unaltered source file is copied to the final target. + // Avoiding a re-encoding prevents any potential quality + // loss. + $streamStat = $state->targetFile->write($state->sourceFile->read(), true); + $state->sourceFile->close(); + $state->targetFile->close(); + } + if ($state->shallDeleteImported) { + // This may throw an exception, if the original has been + // readable, but is not writable + // In this case, the media file will have been copied, but + // cannot be "moved". + try { + $state->sourceFile->delete(); + } catch (MediaFileOperationException $e) { + // If deletion failed, we do not cancel the whole + // import, but fall back to copy-semantics and + // log the exception + Handler::reportSafely($e); + } + } + } + + return $streamStat; + } catch (\ErrorException $e) { + throw new MediaFileOperationException('Could move/copy/symlink source file to final destination', $e); + } + } + + /** + * When rotating, we backup the original file to prevent data loss. + * + * @param StandaloneDTO $state + * + * @return void + */ + private function backupOriginal(StandaloneDTO $state) + { + $state->backupFile = $state->namingStrategy->createFile(SizeVariantType::ORIGINAL, true); + $state->backupFile->write($state->sourceFile->read(), true); + } +} diff --git a/app/Actions/Photo/Pipes/Standalone/ReplaceOriginalWithBackup.php b/app/Actions/Photo/Pipes/Standalone/ReplaceOriginalWithBackup.php new file mode 100644 index 00000000000..17f2fab1848 --- /dev/null +++ b/app/Actions/Photo/Pipes/Standalone/ReplaceOriginalWithBackup.php @@ -0,0 +1,32 @@ +backupFile === null) { + return $next($state); + } + + if (Configs::getValueAsBool('keep_original_untouched')) { + $state->targetFile->write($state->backupFile->read()); + $state->targetFile->close(); + } + + $state->backupFile->delete(); + + return $next($state); + } +} diff --git a/app/Actions/Photo/Pipes/Standalone/SetChecksum.php b/app/Actions/Photo/Pipes/Standalone/SetChecksum.php new file mode 100644 index 00000000000..0577eaeb9dd --- /dev/null +++ b/app/Actions/Photo/Pipes/Standalone/SetChecksum.php @@ -0,0 +1,25 @@ +photo->checksum = $state->streamStat->checksum; + + return $next($state); + } +} diff --git a/app/Actions/Photo/Pipes/Standalone/SetOriginalChecksum.php b/app/Actions/Photo/Pipes/Standalone/SetOriginalChecksum.php new file mode 100644 index 00000000000..78a802ac968 --- /dev/null +++ b/app/Actions/Photo/Pipes/Standalone/SetOriginalChecksum.php @@ -0,0 +1,35 @@ +photo->original_checksum = StreamStat::createFromLocalFile($state->sourceFile)->checksum; + + return $next($state); + } +} diff --git a/app/Actions/Photo/Pipes/VideoPartner/GetVideoPath.php b/app/Actions/Photo/Pipes/VideoPartner/GetVideoPath.php new file mode 100644 index 00000000000..47240e529d0 --- /dev/null +++ b/app/Actions/Photo/Pipes/VideoPartner/GetVideoPath.php @@ -0,0 +1,26 @@ +photo->size_variants->getOriginal()->getFile(); + $photoPath = $photoFile->getRelativePath(); + $photoExt = $photoFile->getOriginalExtension(); + $videoExt = $state->videoFile->getOriginalExtension(); + $state->videoPath = substr($photoPath, 0, -strlen($photoExt)) . $videoExt; + + return $next($state); + } +} diff --git a/app/Actions/Photo/Pipes/VideoPartner/PlaceVideo.php b/app/Actions/Photo/Pipes/VideoPartner/PlaceVideo.php new file mode 100644 index 00000000000..bb31f3374d9 --- /dev/null +++ b/app/Actions/Photo/Pipes/VideoPartner/PlaceVideo.php @@ -0,0 +1,124 @@ +value); + if (Features::active('use-s3')) { + $disk = Storage::disk(StorageDiskType::S3->value); + } + $videoTargetFile = new FlysystemFile($disk, $state->videoPath); + + try { + if ($state->videoFile instanceof NativeLocalFile) { + // This is case A (see above) + // The code is very similar to + // AddStandaloneStrategy::putSourceIntoFinalDestination() + // except that we can skip the part about normalization of + // orientation, because we don't support that for videos. + if ($state->shallImportViaSymlink) { + if (!$videoTargetFile->isLocalFile()) { + throw new ConfigurationException('Symlinking is only supported on local filesystems'); + } + $targetPath = $videoTargetFile->toLocalFile()->getPath(); + $sourcePath = $state->videoFile->getRealPath(); + // For symlinks we must manually create a non-existing + // parent directory. + // This mimics the behaviour of Flysystem for regular files. + $targetDirectory = pathinfo($targetPath, PATHINFO_DIRNAME); + if (!is_dir($targetDirectory)) { + $umask = \umask(0); + \Safe\mkdir($targetDirectory, BasicPermissionCheck::getConfiguredDirectoryPerm(), true); + \umask($umask); + } + \Safe\symlink($sourcePath, $targetPath); + $streamStat = StreamStat::createFromLocalFile($state->videoFile); + } else { + $streamStat = $videoTargetFile->write($state->videoFile->read(), true); + $state->videoFile->close(); + $videoTargetFile->close(); + if ($state->shallDeleteImported) { + // This may throw an exception, if the original has been + // readable, but is not writable + // In this case, the media file will have been copied, but + // cannot be "moved". + try { + $state->videoFile->delete(); + } catch (MediaFileOperationException $e) { + // If deletion failed, we do not cancel the whole + // import, but fall back to copy-semantics and + // log the exception + Handler::reportSafely($e); + } + } + } + } elseif ($state->videoFile instanceof FlysystemFile) { + // It seems as if Flysystem calls a primitive \rename under the + // hood, if the storage adapter is the `Local` adapter. + // This also works for symbolic links, so we are good here. + $state->videoFile->move($videoTargetFile->getRelativePath()); + $streamStat = null; + } else { + throw new LycheeAssertionError('Unexpected type of $videoFile: ' . get_class($state->videoFile)); + } + + $state->streamStat = $streamStat; + } catch (\ErrorException $e) { + throw new MediaFileOperationException('Could move/copy/symlink source file to final destination', $e); + } + + return $next($state); + } +} diff --git a/app/Actions/Photo/Pipes/VideoPartner/UpdateLivePartner.php b/app/Actions/Photo/Pipes/VideoPartner/UpdateLivePartner.php new file mode 100644 index 00000000000..6c0a5d13d8e --- /dev/null +++ b/app/Actions/Photo/Pipes/VideoPartner/UpdateLivePartner.php @@ -0,0 +1,23 @@ +photo->live_photo_short_path = $state->videoPath; + $state->photo->live_photo_checksum = $state->streamStat?->checksum; + + return $next($state); + } +} diff --git a/app/Actions/Photo/Prepare.php b/app/Actions/Photo/Prepare.php deleted file mode 100644 index caf5e8f1268..00000000000 --- a/app/Actions/Photo/Prepare.php +++ /dev/null @@ -1,56 +0,0 @@ -toReturnArray(); - - $this->symLinkFunctions->getUrl($photo, $return); - - //! This can probably be refactored - if (!AccessControl::is_current_user($photo->owner_id)) { - if ($photo->album_id != null) { - $album = $photo->album; - if (!$album->is_full_photo_visible()) { - $photo->downgrade($return); - } - - // if 2 : picture is public by album being public (if being in an album). - if ($album->is_public()) { - $return['public'] = '2'; - } - - $return['downloadable'] = $album->is_downloadable() ? '1' : '0'; - $return['share_button_visible'] = $album->is_share_button_visible() ? '1' : '0'; - } else { // Unsorted - if (Configs::get_value('full_photo', '1') != '1') { - $photo->downgrade($return); - } - $return['downloadable'] = Configs::get_value('downloadable', '0'); - $return['share_button_visible'] = Configs::get_value('share_button_visible', '0'); - } - } else { - if ($photo->album_id != null && $photo->album->is_public()) { - $return['public'] = '2'; - } - $return['downloadable'] = '1'; - $return['share_button_visible'] = '1'; - } - - $return['license'] = $photo->get_license(); - - return $return; - } -} diff --git a/app/Actions/Photo/Random.php b/app/Actions/Photo/Random.php deleted file mode 100644 index 8d4ca30637c..00000000000 --- a/app/Actions/Photo/Random.php +++ /dev/null @@ -1,30 +0,0 @@ -setAlbumIDs(resolve(PublicIds::class)->getPublicAlbumsId()); - $photo = $starred->get_photos()->inRandomOrder()->first(); - - if ($photo == null) { - throw new JsonError('no pictures found!'); - } - - $return = $photo->toReturnArray(); - $this->symLinkFunctions->getUrl($photo, $return); - if ($photo->album_id !== null && !$photo->album->is_full_photo_visible()) { - $photo->downgrade($return); - } - - return $return; - } -} diff --git a/app/Actions/Photo/Rotate.php b/app/Actions/Photo/Rotate.php index 39b921c1b6c..ce3ee3516cb 100644 --- a/app/Actions/Photo/Rotate.php +++ b/app/Actions/Photo/Rotate.php @@ -1,177 +1,184 @@ imageHandler = app(ImageHandlerInterface::class); - } - - private function check(Photo $photo, int $direction): bool + protected Photo $photo; + /** @var int either `1` for counterclockwise or `-1` for clockwise rotation */ + protected int $direction; + protected FlysystemFile $sourceFile; + protected AbstractSizeVariantNamingStrategy $namingStrategy; + + /** + * @param Photo $photo + * @param int $direction + * + * @throws MediaFileUnsupportedException thrown, if rotation of $photo + * is not supported + * @throws InvalidRotationDirectionException thrown if $direction neither + * equals -1 nor 1 + * @throws IllegalOrderOfOperationException + * @throws FrameworkException + */ + public function __construct(Photo $photo, int $direction) { - if ($photo->isVideo()) { - Logs::error(__METHOD__, __LINE__, 'Trying to rotate a video'); - - return false; - } - - if ($photo->livePhotoUrl !== null) { - Logs::error(__METHOD__, __LINE__, 'Trying to rotate a live photo'); - - return false; - } - - if ($photo->type == 'raw') { - Logs::error(__METHOD__, __LINE__, 'Trying to rotate a raw file'); - - return false; - } - - // direction is valid? - if (($direction != 1) && ($direction != -1)) { - Logs::error(__METHOD__, __LINE__, 'Direction must be 1 or -1'); - - return false; + try { + if ($photo->isVideo()) { + throw new MediaFileUnsupportedException('Rotation of a video is unsupported'); + } + if ($photo->live_photo_short_path !== null) { + throw new MediaFileUnsupportedException('Rotation of a live photo is unsupported'); + } + if ($photo->isRaw()) { + throw new MediaFileUnsupportedException('Rotation of a raw photo is unsupported'); + } + // direction is valid? + if (($direction !== 1) && ($direction !== -1)) { + throw new InvalidRotationDirectionException(); + } + $this->photo = $photo; + $this->direction = $direction; + $this->sourceFile = $this->photo->size_variants->getOriginal()->getFile(); + $this->namingStrategy = resolve(AbstractSizeVariantNamingStrategy::class); + $this->namingStrategy->setPhoto($this->photo); + } catch (BindingResolutionException $e) { + throw new FrameworkException('Laravel\'s container component', $e); } - - return true; } - public function do(Photo $photo, int $direction) + /** + * Rotates the photo and its duplicates and re-generates all size variants. + * + * @return Photo the updated (i.e. rotated) photo + * + * @throws LycheeException + */ + public function do(): Photo { - if (!$this->check($photo, $direction)) { - return false; + // Load the previous original image and rotate it + $image = new ImageHandler(); + $image->load($this->sourceFile); + try { + $image->rotate(90 * $this->direction); + } catch (LycheeDomainException $e) { + throw LycheeAssertionError::createFromUnexpectedException($e); } - // Generate a temporary name for the rotated file. - $big_folder = Storage::path('big/'); - $url = $photo->url; - $path = $big_folder . $url; - $extension = Helpers::getExtension($url); - if ( - !($new_tmp = tempnam($big_folder, 'lychee')) || - !@rename($new_tmp, $new_tmp . $extension) - ) { - Logs::notice(__METHOD__, __LINE__, 'Could not create a temporary file.'); - - return false; - } - $new_tmp .= $extension; - - // Rotate the original image. - if ($this->imageHandler->rotate($path, ($direction == 1) ? 90 : -90, $new_tmp) === false) { - Logs::error(__METHOD__, __LINE__, 'Failed to rotate ' . $path); - - return false; - } - - // We will use new names to avoid problems with image caching. - $new_prefix = substr($this->checksum($new_tmp), 0, 32); - $new_url = $new_prefix . $extension; - $new_path = $big_folder . $new_url; - - // Rename the temporary file, now that we know its final name. - if (!@rename($new_tmp, $new_path)) { - Logs::error(__METHOD__, __LINE__, 'Failed to rename ' . $new_tmp); - - return false; - } - - $photo->url = $new_url; - $old_width = $photo->width; - $photo->width = $photo->height; - $photo->height = $old_width; - - // The file size may have changed after the rotation. - /* @var Extractor $metadataExtractor */ - $metadataExtractor = resolve(Extractor::class); - $photo->filesize = $metadataExtractor->filesize($new_path); - // Also restore the original date. - if ($photo->taken_at !== null) { - @touch($new_path, $photo->taken_at->getTimestamp()); + // Delete all size variants from current photo, this will also take + // care of erasing the actual "physical" files from storage and any + // potential symbolic link which points to one of the original files. + // This will bring photo entity into the same state as it would be if + // we were importing a new photo. + // This also deletes the original size variant + $this->photo->size_variants->deleteAll(); + + // We reset the photo of the naming strategy after the size + // variants have been deleted, in case the naming strategy has based + // its choice on the existing size variants. + // As the photo has no size variants anymore, we must set the + // extension manually from the source file we saved earlier. + $this->namingStrategy->setPhoto($this->photo); + $this->namingStrategy->setExtension($this->sourceFile->getExtension()); + + // Create new target file for rotated original size variant, + // and stream it into the final place + $targetFile = $this->namingStrategy->createFile(SizeVariantType::ORIGINAL); + $streamStat = $image->save($targetFile, true); + + // The checksum has been changed due to rotation. + $oldChecksum = $this->photo->checksum; + $this->photo->checksum = $streamStat->checksum; + $this->photo->save(); + + // Re-create original size variant of photo + $newOriginalSizeVariant = $this->photo->size_variants->create( + SizeVariantType::ORIGINAL, + $targetFile->getRelativePath(), + $image->getDimensions(), + $streamStat->bytes + ); + + // Re-create remaining size variants + try { + /** @var SizeVariantFactory $sizeVariantFactory */ + $sizeVariantFactory = resolve(SizeVariantFactory::class); + $sizeVariantFactory->init($this->photo, $image, $this->namingStrategy); + $newSizeVariants = $sizeVariantFactory->createSizeVariants(); + } catch (\Throwable $t) { + // Don't re-throw the exception, because we do not want the + // rotation operation to fail completely only due to missing size + // variants. + // There are just too many options why the creation of size + // variants may fail. + Handler::reportSafely($t); + $newSizeVariants = new Collection(); } + // Add new original size variant to collection of newly created + // size variants; we need this to correctly update the duplicates + // below + $newSizeVariants->add($newOriginalSizeVariant); - // Delete all old image files, including the original. - if ($photo->thumbUrl != '') { - @unlink(Storage::path('thumb/' . $photo->thumbUrl)); - if ($photo->thumb2x != 0) { - @unlink(Storage::path('thumb/' . Helpers::ex2x($photo->thumbUrl))); - $photo->thumb2x = 0; - } - $photo->thumbUrl = ''; - } - if ($photo->small_width !== null) { - @unlink(Storage::path('small/' . $url)); - $photo->small_width = null; - $photo->small_height = null; - if ($photo->small2x_width !== null) { - @unlink(Storage::path('small/' . Helpers::ex2x($url))); - $photo->small2x_width = null; - $photo->small2x_height = null; - } - } - if ($photo->medium_width !== null) { - @unlink(Storage::path('medium/' . $url)); - $photo->medium_width = null; - $photo->medium_height = null; - if ($photo->medium2x_width !== null) { - @unlink(Storage::path('medium/' . Helpers::ex2x($url))); - $photo->medium2x_width = null; - $photo->medium2x_height = null; + // Deal with duplicates. We simply update all of them to match. + $duplicates = Photo::query() + ->where('checksum', '=', $oldChecksum) + ->get(); + /** @var Photo $duplicate */ + foreach ($duplicates as $duplicate) { + $duplicate->checksum = $this->photo->checksum; + // Note: It is not correct to simply update the existing size + // variants of the duplicates. + // Due to rotation the number and type of size variants may have + // changed, too. + // So we actually have to do a 3-way merge and update: + // 1. delete size variants of the duplicates which do not exist + // anymore, + // 2. update size variants of the duplicates which still exist, + // and + // 3. add new size variants to duplicates which + // haven't existed before. + // For simplicity, we simply delete all size variants of the + // duplicates and re-create them. + // Deleting the size variants of the duplicates has also the + // advantage that the actual files are erased from storage. + $duplicate->size_variants->deleteAll(); + /** @var SizeVariant $newSizeVariant */ + foreach ($newSizeVariants as $newSizeVariant) { + $duplicate->size_variants->create( + $newSizeVariant->type, + $newSizeVariant->short_path, + new ImageDimension($newSizeVariant->width, $newSizeVariant->height), + $newSizeVariant->filesize + ); } + $duplicate->save(); } - @unlink($path); - - // Create new thumbs and intermediate sizes. - if ($this->createThumb($photo) === false) { - Logs::error(__METHOD__, __LINE__, 'Could not create thumbnail for photo'); - } else { - $photo->thumbUrl = $new_prefix . '.jpeg'; - } - $this->createSmallerImages($photo); - - // Finally save the updated photo. - $photo->save(); - - // Deal with duplicates. We simply update all of them to match. - Photo::query() - ->where('checksum', $photo->checksum) - ->where('id', '<>', $photo->id) - ->update([ - 'url' => $photo->url, - 'width' => $photo->width, - 'height' => $photo->height, - 'filesize' => $photo->filesize, - 'thumbUrl' => $photo->thumbUrl, - 'thumb2x' => $photo->thumb2x, - 'small_width' => $photo->small_width, - 'small_height' => $photo->small_height, - 'small2x_width' => $photo->small2x_width, - 'small2x_height' => $photo->small2x_height, - 'medium_width' => $photo->medium_width, - 'medium_height' => $photo->medium_height, - 'medium2x_width' => $photo->medium2x_width, - 'medium2x_height' => $photo->medium2x_height, - ]); - return true; + return $this->photo; } } diff --git a/app/Actions/Photo/SetAlbum.php b/app/Actions/Photo/SetAlbum.php deleted file mode 100644 index 00fd4536585..00000000000 --- a/app/Actions/Photo/SetAlbum.php +++ /dev/null @@ -1,44 +0,0 @@ -property = 'album_id'; - $this->albumFactory = $albumFactory; - } - - public function execute(array $photoIDs, string $albumID) - { - $album = null; - - if ($albumID != '0') { - $album = $this->albumFactory->make($albumID); - - if ($album->is_tag_album()) { - throw new JsonError('Sorry, cannot Set to tag Album.'); - } - - if ($album->is_smart()) { - throw new JsonError('Sorry, cannot Set to smart Album.'); - } - - foreach ($photoIDs as $id) { - $photo = Photo::find($id); - $notify = new Notify(); - $notify->do($photo, $albumID); - } - } - - return $this->do($photoIDs, $albumID == '0' ? null : $albumID); - } -} diff --git a/app/Actions/Photo/SetDescription.php b/app/Actions/Photo/SetDescription.php deleted file mode 100644 index 7de953c6e93..00000000000 --- a/app/Actions/Photo/SetDescription.php +++ /dev/null @@ -1,11 +0,0 @@ -property = 'description'; - } -} diff --git a/app/Actions/Photo/SetLicense.php b/app/Actions/Photo/SetLicense.php deleted file mode 100644 index 6a3afd3afad..00000000000 --- a/app/Actions/Photo/SetLicense.php +++ /dev/null @@ -1,11 +0,0 @@ -property = 'license'; - } -} diff --git a/app/Actions/Photo/SetPublic.php b/app/Actions/Photo/SetPublic.php deleted file mode 100644 index f2843d3dfa4..00000000000 --- a/app/Actions/Photo/SetPublic.php +++ /dev/null @@ -1,11 +0,0 @@ -property = 'public'; - } -} diff --git a/app/Actions/Photo/SetStar.php b/app/Actions/Photo/SetStar.php deleted file mode 100644 index 431fa0c6216..00000000000 --- a/app/Actions/Photo/SetStar.php +++ /dev/null @@ -1,11 +0,0 @@ -property = 'star'; - } -} diff --git a/app/Actions/Photo/SetTags.php b/app/Actions/Photo/SetTags.php deleted file mode 100644 index 0f08c644a3a..00000000000 --- a/app/Actions/Photo/SetTags.php +++ /dev/null @@ -1,11 +0,0 @@ -property = 'tags'; - } -} diff --git a/app/Actions/Photo/SetTitle.php b/app/Actions/Photo/SetTitle.php deleted file mode 100644 index e0a6fcbfe8f..00000000000 --- a/app/Actions/Photo/SetTitle.php +++ /dev/null @@ -1,11 +0,0 @@ -property = 'title'; - } -} diff --git a/app/Actions/Photo/Setter.php b/app/Actions/Photo/Setter.php deleted file mode 100644 index e0f8a4c9b77..00000000000 --- a/app/Actions/Photo/Setter.php +++ /dev/null @@ -1,31 +0,0 @@ -execute($photo, $value); - } - - public function execute(Photo $photo, $value): bool - { - $photo->{$this->property} = $value; - - return $photo->save(); - } -} diff --git a/app/Actions/Photo/Setters.php b/app/Actions/Photo/Setters.php deleted file mode 100644 index 6b805e6b089..00000000000 --- a/app/Actions/Photo/Setters.php +++ /dev/null @@ -1,21 +0,0 @@ -update([$this->property => $value]); - } -} diff --git a/app/Actions/Photo/Strategies/StrategyDuplicate.php b/app/Actions/Photo/Strategies/StrategyDuplicate.php deleted file mode 100644 index 1fa522b1e69..00000000000 --- a/app/Actions/Photo/Strategies/StrategyDuplicate.php +++ /dev/null @@ -1,97 +0,0 @@ -skip_duplicates = $skip_duplicates; - $this->resync_metadata = $resync_metadata; - $this->delete_imported = $delete_imported; - } - - public function storeFile(Create $create) - { - Logs::notice(__METHOD__, __LINE__, 'Nothing to store, image is a duplicate'); - } - - public function hydrate(Create &$create, ?Photo &$existing = null, ?array $file = null) - { - $create->photo_Url = $existing->url; - $create->path = Storage::path($create->path_prefix . $existing->url); - $create->photo->thumbUrl = $existing->thumbUrl; - $create->photo->thumb2x = $existing->thumb2x; - $create->photo->medium_width = $existing->medium_width; - $create->photo->medium_height = $existing->medium_height; - $create->photo->medium2x_width = $existing->medium2x_width; - $create->photo->medium2x_height = $existing->medium2x_height; - $create->photo->small_width = $existing->small_width; - $create->photo->small_height = $existing->small_height; - $create->photo->small2x_width = $existing->small2x_width; - $create->photo->small2x_height = $existing->small2x_height; - $create->photo->livePhotoUrl = $existing->livePhotoUrl; - $create->photo->livePhotoChecksum = $existing->livePhotoChecksum; - $create->photo->checksum = $existing->checksum; - $create->photo->type = $existing->type; - $create->mimeType = $create->photo->type; - - // Photo already exists - // Check if the user wants to skip duplicates - if ($this->skip_duplicates) { - $metadataChanged = false; - - // Before we skip entirely, check if there is a sidecar file and if the metadata needs to be updated (from a sidecar) - if ($this->resync_metadata === true) { - $info = $this->getMetadata($file, $create->path, $create->kind, $create->extension); - $attr = $existing->attributesToArray(); - foreach ($info as $key => $value) { - if (array_key_exists($key, $attr) // check if key exists, even if null - && (($existing->$key !== null && $value !== $existing->$key) || ($existing->$key === null && $value !== null && $value !== '')) - && $value != $existing->$key) { // avoid false positives when comparing variables of different types (e.g string vs int) - $metadataChanged = true; - $existing->$key = $value; - } - } - } - - if ($metadataChanged === true) { - Logs::notice(__METHOD__, __LINE__, 'Updating metdata of existing photo.'); - $existing->save(); - - $res = new PhotoResyncedException('This photo has been skipped because it\'s already in your library, but its metadata has been updated.'); - } else { - Logs::notice(__METHOD__, __LINE__, 'Skipped upload of existing photo because skipDuplicates is activated'); - - $res = new PhotoSkippedException('This photo has been skipped because it\'s already in your library.'); - } - - if ($this->delete_imported && !$create->is_uploaded) { - @unlink($create->tmp_name); - } - - throw $res; - } - //? else we do not skip duplicate and continue. - } - - public function generate_thumbs(Create &$create, bool &$skip_db_entry_creation, bool &$no_error) - { - Logs::notice(__METHOD__, __LINE__, 'Nothing to generate, image is a duplicate'); - } -} diff --git a/app/Actions/Photo/Strategies/StrategyPhoto.php b/app/Actions/Photo/Strategies/StrategyPhoto.php deleted file mode 100644 index 9f5bdc69bb8..00000000000 --- a/app/Actions/Photo/Strategies/StrategyPhoto.php +++ /dev/null @@ -1,154 +0,0 @@ -imageHandler = app(ImageHandlerInterface::class); - $this->import_via_symlink = $import_via_symlink; - } - - public function storeFile(Create $create) - { - // Import if not uploaded via web - if (!$create->is_uploaded) { - // TODO: use the storage facade here - // Check if the user wants to create symlinks instead of copying the photo - if ($this->import_via_symlink) { - if (!symlink($create->tmp_name, $create->path)) { - // @codeCoverageIgnoreStart - Logs::error(__METHOD__, __LINE__, 'Could not create symlink'); - - throw new JsonError('Could not create symlink!'); - // @codeCoverageIgnoreEnd - } - } elseif (!@copy($create->tmp_name, $create->path)) { - // @codeCoverageIgnoreStart - Logs::error(__METHOD__, __LINE__, 'Could not copy photo to uploads'); - - throw new JsonError('Could not copy photo to uploads!'); - // @codeCoverageIgnoreEnd - } - } else { - // TODO: use the storage facade here - if (!@move_uploaded_file($create->tmp_name, $create->path)) { - Logs::error(__METHOD__, __LINE__, 'Could not move photo to uploads'); - - throw new JsonError('Could not move photo to uploads!'); - } - } - } - - public function hydrate(Create &$create, ?Photo &$existing = null, ?array $file = null) - { - // do nothing. - } - - public function generate_thumbs(Create &$create, bool &$skip_db_entry_creation, bool &$no_error) - { - // Generate small files for 2 options: - // (1) There is no Live Photo Partner - // (2) There is a partner and we're uploading a photo - if (($create->livePhotoPartner == null) || !(in_array($create->photo->type, $create->validVideoTypes, true))) { - // Set orientation based on EXIF data - // but do not rotate if the image shall not be modified - if ( - $create->photo->type === 'image/jpeg' - && isset($create->info['orientation']) - && $create->info['orientation'] !== '' - ) { - // If we are importing via symlink, we don't actually overwrite - // the source but we still need to fix the dimensions. - $pretend = (!$create->is_uploaded && $this->import_via_symlink); - $rotation = $this->imageHandler->autoRotate($create->path, $create->info, $pretend); - - if ($rotation !== [false, false]) { - $create->photo->width = $rotation['width']; - $create->photo->height = $rotation['height']; - - if (!$pretend) { - // If the image was rotated, the size may have changed. - /* @var Extractor $metadataExtractor */ - $metadataExtractor = resolve(Extractor::class); - $create->photo->filesize = $metadataExtractor->filesize($create->path); - } - } - } - - // Set original date - if ($create->info['taken_at'] !== null) { - @touch($create->path, $create->info['taken_at']->getTimestamp()); - } - - // For videos extract a frame from the middle - $frame_tmp = ''; - if (in_array($create->photo->type, $create->validVideoTypes, true)) { - try { - $frame_tmp = $this->extractVideoFrame($create->photo); - } catch (Exception $exception) { - Logs::error(__METHOD__, __LINE__, $exception->getMessage()); - } - } - - if ($create->kind == 'raw') { - try { - $frame_tmp = $this->createJpgFromRaw($create->photo); - } catch (Exception $exception) { - Logs::error(__METHOD__, __LINE__, $exception->getMessage()); - } - } - - // Create Thumb - if ($create->kind == 'raw' && $frame_tmp == '') { - $create->photo->thumbUrl = ''; - $create->photo->thumb2x = 0; - } elseif (!in_array($create->photo->type, $create->validVideoTypes, true) || $frame_tmp !== '') { - if (!$this->createThumb($create->photo, $frame_tmp)) { - Logs::error(__METHOD__, __LINE__, 'Could not create thumbnail for photo'); - - throw new JsonError('Could not create thumbnail for photo!'); - } - - $create->photo->thumbUrl = basename($create->photo_Url, $create->extension) . '.jpeg'; - - $this->createSmallerImages($create->photo, $frame_tmp); - - //? GoogleMicroVideoOffset - if ($create->info['MicroVideoOffset']) { - $this->extractVideo($create->photo, $create->info['MicroVideoOffset'], $frame_tmp); - } - - if ($frame_tmp !== '') { - unlink($frame_tmp); - } - } else { - $create->photo->thumbUrl = ''; - $create->photo->thumb2x = 0; - } - } else { - // We're uploading a video -> overwrite everything from partner - $create->livePhotoPartner->livePhotoUrl = $create->photo->url; - $create->livePhotoPartner->livePhotoChecksum = $create->photo->checksum; - $no_error &= $create->livePhotoPartner->save(); - $skip_db_entry_creation = true; - } - } -} diff --git a/app/Actions/Photo/Strategies/StrategyPhotoBase.php b/app/Actions/Photo/Strategies/StrategyPhotoBase.php deleted file mode 100644 index 9fd3442372d..00000000000 --- a/app/Actions/Photo/Strategies/StrategyPhotoBase.php +++ /dev/null @@ -1,104 +0,0 @@ -getMetadata($file, $create->path, $create->kind, $create->extension); - - $create->photo->title = $info['title']; - $create->photo->url = $create->photo_Url; - $create->photo->description = $info['description']; - $create->photo->tags = $info['tags']; - $create->photo->width = $info['width'] ? $info['width'] : 0; - $create->photo->height = $info['height'] ? $info['height'] : 0; - $create->photo->type = ($info['type'] ? $info['type'] : $create->mimeType); - $create->photo->filesize = $info['filesize']; - $create->photo->iso = $info['iso']; - $create->photo->aperture = $info['aperture']; - $create->photo->make = $info['make']; - $create->photo->model = $info['model']; - $create->photo->lens = $info['lens']; - $create->photo->shutter = $info['shutter']; - $create->photo->focal = $info['focal']; - $create->photo->taken_at = $info['taken_at']; - $create->photo->latitude = $info['latitude']; - $create->photo->longitude = $info['longitude']; - $create->photo->altitude = $info['altitude']; - $create->photo->imgDirection = $info['imgDirection']; - $create->photo->location = $info['location']; - $create->photo->livePhotoContentID = $info['livePhotoContentID']; - $create->photo->public = $create->public; - $create->photo->star = $create->star; - - $create->info = $info; - } - - public function getMetadata($file, $path, $kind, $extension): array - { - // forward call to trait. - return $this->getFileMetadata($file, $path, $kind, $extension); - } - - public function setParentAndOwnership(Create &$create) - { - if ($create->parentAlbum !== null) { - $create->photo->album_id = $create->albumID; - $create->photo->owner_id = $create->parentAlbum->owner_id; - } else { - $create->photo->album_id = null; - $create->photo->owner_id = AccessControl::id(); - } - } - - public function findLivePartner(Create &$create) - { - $livePhotoPartner = null; - if ($create->photo->livePhotoContentID) { - // Todo: We need to search for pairs (Video + Photo) - // Photo+Photo or Video+Video does not work - - $livePhotoPartner = Photo::query() - ->where('livePhotoContentID', '=', $create->photo->livePhotoContentID) - ->where('album_id', '=', $create->photo->album_id) - ->whereNull('livePhotoUrl')->first(); - } - - if ($livePhotoPartner != null) { - // if both are a photo or a video -> it's not a live photo - if (in_array($create->photo->type, $create->validVideoTypes, true) === in_array($livePhotoPartner->type, $create->validVideoTypes, true)) { - $livePhotoPartner = null; - } - } - - if ($livePhotoPartner != null) { - // I'm uploading a photo, video already exists - if (!(in_array($create->photo->type, $create->validVideoTypes, true))) { - $create->photo->livePhotoUrl = $create->livePhotoPartner->url; - $create->photo->livePhotoChecksum = $create->livePhotoPartner->checksum; - // Todo: Delete the livePhotoPartner - - $create->livePhotoPartner->predelete(true); - $create->livePhotoPartner->delete(); - } - } - - $create->livePhotoPartner = $livePhotoPartner; - } -} diff --git a/app/Actions/Photo/SymLinker.php b/app/Actions/Photo/SymLinker.php deleted file mode 100644 index 8addde5d655..00000000000 --- a/app/Actions/Photo/SymLinker.php +++ /dev/null @@ -1,16 +0,0 @@ -symLinkFunctions = $symLinkFunctions; - } -} diff --git a/app/Actions/Photo/Toggle.php b/app/Actions/Photo/Toggle.php deleted file mode 100644 index 2fbdcbacda1..00000000000 --- a/app/Actions/Photo/Toggle.php +++ /dev/null @@ -1,31 +0,0 @@ -execute($photo); - } - - public function execute(Photo $photo): bool - { - $photo->{$this->property} = $photo->{$this->property} != 1 ? 1 : 0; - - return $photo->save(); - } -} diff --git a/app/Actions/Photo/Toggles.php b/app/Actions/Photo/Toggles.php deleted file mode 100644 index 25e95c065c3..00000000000 --- a/app/Actions/Photo/Toggles.php +++ /dev/null @@ -1,36 +0,0 @@ -update([$this->property => DB::raw('1 XOR `' . $this->property . '`')]); - } catch (QueryException $e) { - // for Sqlite we need the slow approach - $photos = Photo::whereIn('id', $photoIDs)->get(); - $no_error = true; - foreach ($photos as $photo) { - $photo->{$this->property} = $photo->{$this->property} != 1 ? 1 : 0; - $no_error &= $photo->save(); - } - } - - return $no_error; - } -} diff --git a/app/Actions/Profile/UpdateLogin.php b/app/Actions/Profile/UpdateLogin.php new file mode 100644 index 00000000000..866c67e4d05 --- /dev/null +++ b/app/Actions/Profile/UpdateLogin.php @@ -0,0 +1,89 @@ +username) { + return $user; + } + + // Check if username already exists + if (User::query()->where('username', '=', $username)->count() !== 0) { + Log::channel('login')->warning(__METHOD__ . ':' . __LINE__ . sprintf('User (%s) tried to change their identity to (%s) from %s', $user->username, $username, $ip)); + throw new ConflictingPropertyException('Username already exists.'); + } + + // Change username + Log::channel('login')->notice(__METHOD__ . ':' . __LINE__ . sprintf('User (%s) changed their identity to (%s) from %s', $user->username, $username, $ip)); + $user->username = $username; + + return $user; + } + + /** + * Update the email of the user. + * + * @param User $user + * @param ?string $email + * + * @return User + */ + public function updateEmail(User &$user, ?string $email): User + { + $user->email = $email; + if ($email === null) { + $user->notifications()->delete(); + } + + return $user; + } + + /** + * Update the password of the user. + * + * @param User $user + * @param ?string $password + * + * @return User + */ + public function updatePassword(User &$user, ?string $password): User + { + if ($password === null) { + return $user; + } + + $user->password = Hash::make($password); + + return $user; + } +} diff --git a/app/Actions/RSS/Generate.php b/app/Actions/RSS/Generate.php index 5fdd842afb7..d1826989e7b 100644 --- a/app/Actions/RSS/Generate.php +++ b/app/Actions/RSS/Generate.php @@ -1,97 +1,84 @@ symLinkFunctions = $symLinkFunctions; - } + protected PhotoQueryPolicy $photoQueryPolicy; - private function make_enclosure(array $photo_array) + public function __construct(PhotoQueryPolicy $photoQueryPolicy) { - $enclosure = new \stdClass(); - - $path = public_path($photo_array['url']); - $enclosure->length = File::size($path); - $enclosure->mime_type = File::mimeType($path); - $enclosure->url = url('/' . $photo_array['url']); - - return $enclosure; + $this->photoQueryPolicy = $photoQueryPolicy; } - private function create_link(Photo $photo_model, array &$photo_array) + private function create_link_to_page(Photo $photo_model): string { - if ($photo_model->album_id != null) { - if (!$photo_model->album->is_full_photo_visible()) { - $photo_model->downgrade($photo_array); - } - - return '#' . $photo_model->album_id . '/' . $photo_model->id; + if ($photo_model->album_id !== null) { + return url('/gallery/' . $photo_model->album_id . '/' . $photo_model->id); } - if (Configs::get_value('full_photo', '1') != '1') { - $photo_model->downgrade($photo_array); - } - - return 'view?p=' . $photo_model->id; + return url('/view?p=' . $photo_model->id); } - private function toFeedItem(Photo $photo_model) + private function toFeedItem(Photo $photo_model): FeedItem { - $photo_array = $photo_model->toReturnArray(); - - $this->symLinkFunctions->getUrl($photo_model, $photo_array); - - $photo_array['url'] = $photo_array['url'] ?: ($photo_array['medium2x'] ?: $photo_array['medium']); - // TODO: this will need to be fixed for s3 and when the upload folder is NOT the Lychee folder. - $enclosure = $this->make_enclosure($photo_array); - - $id = $this->create_link($photo_model, $photo_array); - - return FeedItem::create([ - 'id' => url('/' . $id), + $page_link = $this->create_link_to_page($photo_model); + $sizeVariant = $photo_model->size_variants->getOriginal(); + $feedItem = [ + 'id' => $page_link, 'title' => $photo_model->title, - 'summary' => $photo_model->description, + 'summary' => $photo_model->description ?? '', 'updated' => $photo_model->updated_at, - 'link' => $photo_array['url'], - 'enclosure' => $enclosure->url, - 'enclosureLength' => $enclosure->length, - 'enclosureType' => $enclosure->mime_type, - 'author' => $photo_model->owner->username, - ]); + 'link' => $page_link, + 'enclosure' => $sizeVariant->url, + 'enclosureType' => $photo_model->type, + 'enclosureLength' => $sizeVariant->filesize, + 'authorName' => $photo_model->owner->username, + ]; + + return FeedItem::create($feedItem); } - public function do() + /** + * @return Collection + * + * @throws InternalLycheeException + */ + public function do(): Collection { - $publicIds = resolve(PublicIds::class)->getNotAccessible(); - $rss_recent = intval(Configs::get_value('rss_recent_days', '7')); - $rss_max = Configs::get_Value('rss_max_items', '100'); - $nowMinus = Carbon::now()->subDays($rss_recent)->toDateTimeString(); + $rss_recent = Configs::getValueAsInt('rss_recent_days'); + $rss_max = Configs::getValueAsInt('rss_max_items'); + try { + $nowMinus = Carbon::now()->subDays($rss_recent)->toDateTimeString(); + } catch (UnitException|InvalidFormatException $e) { + throw new FrameworkException('Date/Time component (Carbon)', $e); + } - $photos = Photo::with('album', 'owner') - ->where('created_at', '>=', $nowMinus) - // we select photo which album IS PUBLICALLY ACCESSIBLE - // or PHOTO MARKED AS PUBLIC. - ->where(fn ($q) => $q->whereIn('album_id', $publicIds)->orWhere('public', '=', '1')) + /** @var Collection $photos */ + $photos = $this->photoQueryPolicy + ->applySearchabilityFilter( + query: Photo::query()->with(['album', 'owner', 'size_variants', 'size_variants.sym_links']), + origin: null, + include_nsfw: !Configs::getValueAsBool('hide_nsfw_in_rss') + ) + ->where('photos.created_at', '>=', $nowMinus) ->limit($rss_max) ->get(); diff --git a/app/Actions/ReadAccessFunctions.php b/app/Actions/ReadAccessFunctions.php deleted file mode 100644 index cb1add02442..00000000000 --- a/app/Actions/ReadAccessFunctions.php +++ /dev/null @@ -1,116 +0,0 @@ -owner_id)) { - return 1; // access granted - } - - // Check if the album is shared with us - if ( - AccessControl::is_logged_in() && - $album->shared_with->map(function ($user) { - return $user->id; - })->contains(AccessControl::id()) - ) { - return 1; // access granted - } - - if ( - !$album->is_public() || - ($obeyHidden && $album->viewable !== 1) - ) { - return 2; // Warning: Album private! - } - - if ($album->password == '') { - return 1; // access granted - } - - if (AccessControl::has_visible_album($album->id)) { - return 1; // access granted - } - - return 3; // Please enter password first. // Warning: Wrong password! - } - - /** - * Check if a (public) user has access to an album - * if 0 : album does not exist - * if 1 : access is granted - * if 2 : album is private - * if 3 : album is password protected and require user input. - * - * @param int|string $album: Album object or Album id - * @param bool obeyHidden - * - * @return int - */ - public function albumID($album, bool $obeyHidden = false): int - { - if (in_array($album, [ - 'starred', - 'public', - 'recent', - 'unsorted', - ])) { - if (AccessControl::is_logged_in() && AccessControl::can_upload()) { - return 1; - } - if (($album === 'recent' && Configs::get_value('public_recent', '0') === '1') || - ($album === 'starred' && Configs::get_value('public_starred', '0') === '1') - ) { - return 1; // access granted - } else { - return 2; // Warning: Album private! - } - } - - $album = Album::findOrFail($album); - - return $this->album($album, $obeyHidden); - } - - /** - * Check if a (public) user has access to a picture. - * - * @param Photo $photo - * - * @return bool - */ - public function photo(Photo $photo) - { - if (AccessControl::is_current_user($photo->owner_id)) { - return true; - } - if ($photo->public === 1) { - return true; - } - if ($this->albumID($photo->album_id) === 1) { - return true; - } - - return false; - } -} diff --git a/app/Actions/Search/AlbumSearch.php b/app/Actions/Search/AlbumSearch.php index 09a7ca4ea4c..3e82acfe002 100644 --- a/app/Actions/Search/AlbumSearch.php +++ b/app/Actions/Search/AlbumSearch.php @@ -1,37 +1,98 @@ getPublicAlbumsId(); + $this->albumQueryPolicy = $albumQueryPolicy; + } - $query = Album::with(['owner'])->whereIn('id', $albumIDs); + /** + * @param string[] $terms + * + * @return Collection + * + * @throws InternalLycheeException + */ + public function queryTagAlbums(array $terms): Collection + { + // Note: `applyVisibilityFilter` already adds a JOIN clause with `base_albums`. + // No need to add a second JOIN clause. + $albumQuery = $this->albumQueryPolicy->applyVisibilityFilter( + TagAlbum::query() + ); + $this->addSearchCondition($terms, $albumQuery); - foreach ($terms as $term) { - $query->where( - fn (Builder $query) => $query->where('title', 'like', '%' . $term . '%') - ->orWhere('description', 'like', '%' . $term . '%') - ); - } + $sorting = AlbumSortingCriterion::createDefault(); - $albums = $query->get(); + /** @phpstan-ignore-next-line */ + return (new SortingDecorator($albumQuery)) + ->orderBy($sorting->column, $sorting->order) + ->get(); + } - return $albums->map(function ($album_model) { - $album = $album_model->toReturnArray(); + /** + * @param string[] $terms + * + * @return Collection + * + * @throws InternalLycheeException + */ + public function queryAlbums(array $terms): Collection + { + $albumQuery = Album::query() + ->select(['albums.*']) + ->join('base_albums', 'base_albums.id', '=', 'albums.id'); + $this->addSearchCondition($terms, $albumQuery); + $this->albumQueryPolicy->applyBrowsabilityFilter($albumQuery); + + $sorting = AlbumSortingCriterion::createDefault(); - if (AccessControl::is_logged_in()) { - $album['owner'] = $album_model->owner->username; - } + return (new SortingDecorator($albumQuery)) + ->orderBy($sorting->column, $sorting->order) + ->get(); + } - return $album; - }); + /** + * Adds the search conditions to the provided query builder. + * + * @param string[] $terms + * @param AlbumBuilder|TagAlbumBuilder|FixedQueryBuilder|FixedQueryBuilder $query + * + * @return void + * + * @throws QueryBuilderException + */ + private function addSearchCondition(array $terms, AlbumBuilder|TagAlbumBuilder|FixedQueryBuilder $query): void + { + foreach ($terms as $term) { + $query->where( + fn (AlbumBuilder|TagAlbumBuilder|FixedQueryBuilder $query) => $query + ->where('base_albums.title', 'like', '%' . $term . '%') + ->orWhere('base_albums.description', 'like', '%' . $term . '%') + ); + } } } diff --git a/app/Actions/Search/PhotoSearch.php b/app/Actions/Search/PhotoSearch.php index c6cca7919cd..38ebc2ff65c 100644 --- a/app/Actions/Search/PhotoSearch.php +++ b/app/Actions/Search/PhotoSearch.php @@ -1,74 +1,79 @@ symLinkFunctions = $symLinkFunctions; + public function __construct(PhotoQueryPolicy $photoQueryPolicy) + { + $this->photoQueryPolicy = $photoQueryPolicy; } - private function unsorted_or_public(Builder $query) + /** + * Apply search directly. + * + * @param array $terms + * + * @return Collection photos + * + * @throws InternalLycheeException + */ + public function query(array $terms): Collection { - if (AccessControl::is_admin()) { - return $query->orWhere('album_id', '=', null); - } - - if (AccessControl::can_upload()) { - $query = $query->orWhere(fn ($q) => $q->where('album_id', '=', null)->where('owner_id', '=', AccessControl::id())); - } + $query = $this->sqlQuery($terms); + $sorting = PhotoSortingCriterion::createDefault(); - if (Configs::get_value('public_photos_hidden', '1') === '0') { - $query = $query->orWhere('public', '=', 1); - } - - return $query; + return (new SortingDecorator($query)) + ->orderBy($sorting->column, $sorting->order)->get(); } - public function query(array $terms) + /** + * Create the query manually. + * + * @param array $terms + * @param Album|null $album the optional top album which is used as a search base + * + * @return FixedQueryBuilder + */ + public function sqlQuery(array $terms, ?Album $album = null): Builder { - $albumIDs = resolve(PublicIds::class)->getPublicAlbumsId(); - - $query = Photo::with('album') - ->where(fn ($q) => $this->unsorted_or_public($q->whereIn('album_id', $albumIDs))); + $query = $this->photoQueryPolicy->applySearchabilityFilter( + query: Photo::query()->with(['album', 'size_variants', 'size_variants.sym_links']), + origin: $album, + include_nsfw: !Configs::getValueAsBool('hide_nsfw_in_search') + ); - foreach ($terms as $escaped_term) { + foreach ($terms as $term) { $query->where( - fn (Builder $query) => $query->where('title', 'like', '%' . $escaped_term . '%') - ->orWhere('description', 'like', '%' . $escaped_term . '%') - ->orWhere('tags', 'like', '%' . $escaped_term . '%') - ->orWhere('location', 'like', '%' . $escaped_term . '%') - ->orWhere('model', 'like', '%' . $escaped_term . '%') - ->orWhere('taken_at', 'like', '%' . $escaped_term . '%') + fn (FixedQueryBuilder $query) => $query + ->where('title', 'like', '%' . $term . '%') + ->orWhere('description', 'like', '%' . $term . '%') + ->orWhere('tags', 'like', '%' . $term . '%') + ->orWhere('location', 'like', '%' . $term . '%') + ->orWhere('model', 'like', '%' . $term . '%') + ->orWhere('taken_at', 'like', '%' . $term . '%') ); } - $photos = $query->get(); - - return $photos->map( - function ($photo) { - $photo_array = $photo->toReturnArray(); - $this->symLinkFunctions->getUrl($photo, $photo_array); - - return $photo_array; - } - ); + return $query; } } diff --git a/app/Actions/Settings/Login.php b/app/Actions/Settings/Login.php deleted file mode 100644 index f10a415fa6b..00000000000 --- a/app/Actions/Settings/Login.php +++ /dev/null @@ -1,82 +0,0 @@ -has('oldPassword') ? $request['oldPassword'] : ''; - $oldUsername = $request->has('oldUsername') ? $request['oldUsername'] : ''; - - if (Legacy::SetPassword($request)) { - return true; - } - - // > 4.0.8 - $adminUser = User::find(0); - if ($adminUser->password === '' && $adminUser->username === '') { - $adminUser->username = bcrypt($request['username']); - $adminUser->password = bcrypt($request['password']); - $adminUser->save(); - AccessControl::login($adminUser); - - return true; - } - - if (AccessControl::is_admin()) { - if ($adminUser->password === '' || Hash::check($oldPassword, $adminUser->password)) { - $adminUser->username = bcrypt($request['username']); - $adminUser->password = bcrypt($request['password']); - $adminUser->save(); - unset($adminUser); - - return true; - } - unset($adminUser); - - throw new JsonError('Current password entered incorrectly!'); - } - - // is this necessary ? - if (AccessControl::is_logged_in()) { - $id = AccessControl::id(); - - // this is probably sensitive to timing attacks... - $user = User::findOrFail($id); - - if ($user->lock) { - Logs::notice(__METHOD__, __LINE__, 'Locked user (' . $user->username . ') tried to change their identity from ' . $request->ip()); - throw new JsonError('Locked account!'); - } - - if (User::where('username', '=', $request['username'])->where('id', '!=', $id)->count()) { - Logs::notice(__METHOD__, __LINE__, 'User (' . $user->username . ') tried to change their identity to ' . $request['username'] . ' from ' . $request->ip()); - - throw new JsonError('Username already exists.'); - } - - if ($user->username == $oldUsername && Hash::check($oldPassword, $user->password)) { - Logs::notice(__METHOD__, __LINE__, 'User (' . $user->username . ') changed their identity for (' . $request['username'] . ') from ' . $request->ip()); - - $user->username = $request['username']; - $user->password = bcrypt($request['password']); - - return $user->save(); - } - Logs::notice(__METHOD__, __LINE__, 'User (' . $user->username . ') tried to change their identity from ' . $request->ip()); - - throw new JsonError('Old username or password entered incorrectly!'); - } - - return false; - } -} diff --git a/app/Actions/Sharing/ListShare.php b/app/Actions/Sharing/ListShare.php index a63bdaab707..b9c14b2feaf 100644 --- a/app/Actions/Sharing/ListShare.php +++ b/app/Actions/Sharing/ListShare.php @@ -1,57 +1,155 @@ select( - 'user_album.id', - 'user_id', - 'album_id', + try { + // Active shares, optionally filtered by album ID, participant ID + // and or owner ID + /** @var Collection $shared */ + $shared = AccessPermission::query()->select([ + APC::ACCESS_PERMISSIONS . '.id', + APC::ACCESS_PERMISSIONS . '.user_id', + DB::raw('base_album_id as album_id'), 'username', 'title', - 'parent_id' - ) - ->join('users', 'user_id', 'users.id') - ->join('albums', 'album_id', 'albums.id'); + ]) + ->join('users', 'user_id', '=', 'users.id', 'inner') + ->join('base_albums', 'base_album_id', '=', 'base_albums.id') + ->when($participant !== null, fn ($q) => $q->where('user_base_album.user_id', '=', $participant->id)) + ->when($owner !== null, fn ($q) => $q->where('base_albums.owner_id', '=', $owner->id)) + ->when($baseAlbum !== null, fn ($q) => $q->where('base_albums.id', '=', $baseAlbum->id)) + ->orderBy('title', 'ASC') + ->orderBy('username', 'ASC') + ->get(); + + // Existing albums which can be shared optionally filtered by + // album ID and/or owner ID + /** @var Collection $albums */ + $albums = DB::table('base_albums') + ->leftJoin('albums', 'albums.id', '=', 'base_albums.id') + ->select(['base_albums.id', 'title', 'parent_id']) + ->when($owner !== null, fn ($q) => $q->where('owner_id', '=', $owner->id)) + ->when($baseAlbum !== null, fn ($q) => $q->where('base_albums.id', '=', $baseAlbum->id)) + ->orderBy('title', 'ASC') + ->get(); + $this->linkAlbums($albums); + $albums->each(function ($album) { + /** @phpstan-ignore-next-line */ + $album->title = $this->breadcrumbPath($album); + }); + $albums->each(function ($album) { + /** @var object{parent_id:string,parent:object} $album */ + unset($album->parent_id); + unset($album->parent); + }); - $albums_query = Album::select(['id', 'title', 'parent_id']); + // Existing users with whom an album can be shared optionally + // filtered by participant ID + /** @var Collection $users */ + $users = DB::table('users')->select(['id', 'username']) + ->when($participant !== null, fn ($q) => $q->where('id', '=', $participant->id)) + ->when($participant === null, fn ($q) => $q->where('may_administrate', '=', false)) + ->orderBy('username', 'ASC') + ->get() + ->each(function ($user) { + $user->id = intval($user->id); + }); - // apply filter - if ($UserId != 0) { - $shared_query = $shared_query->where('albums.owner_id', '=', $UserId); - $albums_query = $albums_query->where('owner_id', '=', $UserId); + return new SharesResource($shared, $albums, $users); + } catch (\InvalidArgumentException $e) { + throw new QueryBuilderException($e); } + } - // get arrays - $shared = $shared_query->orderBy('title', 'ASC') - ->orderBy('username', 'ASC') - ->get() - ->each(function (&$s) { - $s->album_id = strval($s->album_id); - $s->title = Album::getFullPath($s); - }); + /** + * Creates the breadcrumb path of an album. + * + * @param \App\Models\Album $album this is not really an album but a very + * stripped down version of an album with + * only the following properties: + * `title`, `parent` and `parent_id` (unused here) + * + * @return string the breadcrumb path + */ + private function breadcrumbPath(object $album): string + { + $title = [$album->title]; + $parent = $album->parent; + while ($parent) { + array_unshift($title, $parent->title); + $parent = $parent->parent; + } + + return implode('/', $title); + } - $albums = $albums_query->get()->each(function (&$album) { - $album->title = Album::getFullPath($album); - }); + /** @phpstan-ignore-next-line */ + private function linkAlbums(Collection $albums): void + { + if ($albums->isEmpty()) { + return; + } - $users = User::select(['id', 'username']) - ->where('id', '>', 0) - ->orderBy('username', 'ASC')->get(); + $groupedAlbums = $albums->groupBy('parent_id'); - return [ - 'shared' => $shared, - 'albums' => $albums, - 'users' => $users, - ]; + foreach ($albums as $album) { + // We must ensure that for each album the property `parent` is + // defined as `breadcrumbPath` accesses this property. + // At the same time, we must not _unconditionally_ initialize this + // property with `null`, as the `parent` property might already + // have been set to its final value in case the parent of current + // object has already been processed earlier and has initialized + // the property (see `foreach` below). + // Keep in mind that the order of albums is arbitrary, hence + // we cannot guarantee whether parents are processed before its + // children or vice versa. + // However, we must not use `$album->parent_id !== null` to check + // whether there is such a parent object eventually. + // An album may have a parent (i.e. `$album->parent_id !== null` + // holds), but the parent might not be part of the result set, + // if the query has been restricted to a particular album and + // the album tree has become disintegrated into a forest of + // subtrees. + if (!isset($album->parent)) { + $album->parent = null; + } + $childAlbums = $groupedAlbums->get($album->id, []); + foreach ($childAlbums as $childAlbum) { + $childAlbum->parent = $album; + } + } } } diff --git a/app/Actions/Sharing/Share.php b/app/Actions/Sharing/Share.php new file mode 100644 index 00000000000..c574b632e0a --- /dev/null +++ b/app/Actions/Sharing/Share.php @@ -0,0 +1,40 @@ +user_id = $user_id; + $perm->base_album_id = $base_album_id; + $perm->grants_full_photo_access = $accessPermissionResource->grants_full_photo_access; + $perm->grants_download = $accessPermissionResource->grants_download; + $perm->grants_upload = $accessPermissionResource->grants_upload; + $perm->grants_edit = $accessPermissionResource->grants_edit; + $perm->grants_delete = $accessPermissionResource->grants_delete; + $perm->load('user'); + $perm->load('album'); + $perm->save(); + + return $perm; + } +} \ No newline at end of file diff --git a/app/Actions/SizeVariant/Delete.php b/app/Actions/SizeVariant/Delete.php new file mode 100644 index 00000000000..2e50ff5f9bd --- /dev/null +++ b/app/Actions/SizeVariant/Delete.php @@ -0,0 +1,107 @@ +delete()` on every + * `SizeVariant` model and the `SizeVariant` model would take care of deleting + * its associated media files. + * But this is extremely inefficient due to Laravel's architecture: + * + * - Models are heavyweight god classes such that every instance also carries + * the whole code for serialization/deserialization + * - Models are active records (and don't use the unit-of-work pattern), i.e. + * every deletion of a model directly triggers a DB operation; they are + * not deferred into a batch operation + * + * Moreover, while removing the records for size variants from the DB can be + * implemented rather efficiently, the actual file operations may take some + * time. + * Especially, if the files are not stored locally but on a remote file system. + * Hence, this method collects all files which need to be removed. + * The caller can then decide to delete them asynchronously. + */ +class Delete +{ + /** + * Deletes the designated size variants from the DB. + * + * The method only deletes the records for size variants and potentially + * associated symbolic links from the DB. + * The method does not delete the associated files from the physical + * storage. + * Instead, the method returns an object in which all these files have + * been collected. + * This object can (and must) be used to eventually delete the files, + * however doing so can be deferred. + * + * @param int[] $svIDs the size variant IDs + * + * @return FileDeleter contains the collected files which became obsolete + * + * @throws ModelDBException + */ + public function do(array $svIDs): FileDeleter + { + try { + $fileDeleter = new FileDeleter(); + + // Get all short paths of size variants which are going to be deleted. + // But exclude those short paths which are duplicated by a size + // variant which is not going to be deleted. + $sizeVariants = SizeVariant::query() + ->from('size_variants as sv') + ->select(['sv.short_path', 'sv.storage_disk']) + ->leftJoin('size_variants as dup', function (JoinClause $join) use ($svIDs) { + $join + ->on('dup.short_path', '=', 'sv.short_path') + ->whereNotIn('dup.id', $svIDs); + }) + ->whereIn('sv.id', $svIDs) + ->whereNull('dup.id') + ->get(); + $fileDeleter->addSizeVariants($sizeVariants); + + // Get all short paths of symbolic links which point to size variants + // which are going to be deleted + $symLinkPaths = SymLink::query() + ->select(['sym_links.short_path']) + ->whereIn('sym_links.size_variant_id', $svIDs) + ->pluck('sym_links.short_path'); + $fileDeleter->addSymbolicLinks($symLinkPaths); + + // Delete records from DB in "inverse" order to not break foreign keys + SymLink::query() + ->whereIn('sym_links.size_variant_id', $svIDs) + ->delete(); + SizeVariant::query() + ->whereIn('id', $svIDs) + ->delete(); + + return $fileDeleter; + // @codeCoverageIgnoreStart + } catch (QueryBuilderException $e) { + throw ModelDBException::create('size variants', 'deleting', $e); + } catch (\InvalidArgumentException $e) { + throw LycheeAssertionError::createFromUnexpectedException($e); + } + // @codeCoverageIgnoreEnd + } +} diff --git a/app/Actions/Statistics/Spaces.php b/app/Actions/Statistics/Spaces.php new file mode 100644 index 00000000000..5703ddf9574 --- /dev/null +++ b/app/Actions/Statistics/Spaces.php @@ -0,0 +1,426 @@ + + */ + public function getFullSpacePerUser(?int $owner_id = null): Collection + { + return DB::table('users') + ->when($owner_id !== null, fn ($query) => $query->where('users.id', '=', $owner_id)) + ->joinSub( + query: DB::table('photos')->select(['photos.id', 'photos.owner_id']), + as: 'photos', + first: 'photos.owner_id', + operator: '=', + second: 'users.id', + type: 'left' + ) + ->joinSub( + query: DB::table('size_variants') + ->select(['size_variants.photo_id', 'size_variants.filesize']) + ->where('size_variants.type', '!=', 7), + as: 'size_variants', + first: 'size_variants.photo_id', + operator: '=', + second: 'photos.id', + type: 'left' + ) + ->select( + 'users.id', + 'username', + DB::raw('SUM(size_variants.filesize) as size') + ) + ->groupBy('users.id', 'username') + ->orderBy('users.id', 'asc') + ->get() + ->map(fn ($item) => [ + 'id' => intval($item->id), + 'username' => strval($item->username), + 'size' => intval($item->size), + ]); + } + + /** + * Return the amount of data stored on the server (optionally for a user). + * + * @param int|null $owner_id + * + * @return Collection + */ + public function getSpacePerSizeVariantTypePerUser(?int $owner_id = null): Collection + { + return DB::table('size_variants') + ->when($owner_id !== null, fn ($query) => $query + ->joinSub( + query: DB::table('photos')->select(['photos.id', 'photos.owner_id']), + as: 'photos', + first: 'photos.id', + operator: '=', + second: 'size_variants.photo_id' + ) + ->where('photos.owner_id', '=', $owner_id)) + ->select( + 'size_variants.type', + DB::raw('SUM(size_variants.filesize) as size') + ) + ->where('size_variants.type', '!=', 7) + ->groupBy('size_variants.type') + ->orderBy('size_variants.type', 'asc') + ->get() + ->map(fn ($item) => [ + 'type' => SizeVariantType::from($item->type), + 'size' => intval($item->size), + ]); + } + + /** + * Return the amount of data stored on the server (optionally for an album). + * + * @param string $album_id + * + * @return Collection + */ + public function getSpacePerSizeVariantTypePerAlbum(string $album_id): Collection + { + $query = DB::table('albums') + ->where('albums.id', '=', $album_id) + ->joinSub( + query: DB::table('albums', 'descendants')->select('descendants.id', 'descendants._lft', 'descendants._rgt'), + as: 'descendants', + first: function (JoinClause $join) { + $join->on('albums._lft', '<=', 'descendants._lft') + ->on('albums._rgt', '>=', 'descendants._rgt'); + } + ) + ->joinSub( + query: DB::table('photos'), + as: 'photos', + first: 'photos.album_id', + operator: '=', + second: 'descendants.id', + ) + ->joinSub( + query: DB::table('size_variants') + ->select(['size_variants.id', 'size_variants.photo_id', 'size_variants.type', 'size_variants.filesize']) + ->where('size_variants.type', '!=', 7), + as: 'size_variants', + first: 'size_variants.photo_id', + operator: '=', + second: 'photos.id', + ) + ->select( + 'size_variants.type', + DB::raw('SUM(size_variants.filesize) as size') + ) + ->groupBy('size_variants.type') + ->orderBy('size_variants.type', 'asc'); + + return $query->get() + ->map(fn ($item) => [ + 'type' => SizeVariantType::from($item->type), + 'size' => intval($item->size), + ]); + } + + /** + * Return size statistics per album. + * + * @param string|null $album_id + * @param int|null $owner_id + * + * @return Collection + */ + public function getSpacePerAlbum(?string $album_id = null, ?int $owner_id = null) + { + $query = DB::table('albums') + ->when($album_id !== null, + fn ($query) => $query + ->joinSub( + query: DB::table('albums', 'parent')->select('parent.id', 'parent._lft', 'parent._rgt'), + as: 'parent', + first: function (JoinClause $join) { + $join->on('albums._lft', '>=', 'parent._lft') + ->on('albums._rgt', '<=', 'parent._rgt'); + } + ) + ->where('parent.id', '=', $album_id) + ) + ->when($owner_id !== null, fn ($query) => $query->joinSub( + query: DB::table('base_albums')->select(['base_albums.id', 'base_albums.owner_id']), + as: 'base_albums', + first: 'base_albums.id', + operator: '=', + second: 'albums.id' + ) + ->where('base_albums.owner_id', '=', $owner_id)) + ->joinSub( + query: DB::table('photos'), + as: 'photos', + first: 'photos.album_id', + operator: '=', + second: 'albums.id' + ) + ->joinSub( + query: DB::table('size_variants') + ->select(['size_variants.id', 'size_variants.photo_id', 'size_variants.filesize']) + ->where('size_variants.type', '!=', 7), + as: 'size_variants', + first: 'size_variants.photo_id', + operator: '=', + second: 'photos.id' + ) + ->select( + 'albums.id', + 'albums._lft', + 'albums._rgt', + DB::raw('SUM(size_variants.filesize) as size'), + )->groupBy('albums.id') + ->orderBy('albums._lft', 'asc'); + + return $query + ->get() + ->map(fn ($item) => [ + 'id' => strval($item->id), + 'left' => intval($item->_lft), + 'right' => intval($item->_rgt), + 'size' => intval($item->size), + ]); + } + + /** + * Same as above but with full size (including sub-albums). + * + * @param string|null $album_id + * @param int|null $owner_id + * + * @return Collection + */ + public function getTotalSpacePerAlbum(?string $album_id = null, ?int $owner_id = null) + { + $query = DB::table('albums') + ->when($album_id !== null, fn ($query) => $query->where('albums.id', '=', $album_id)) + ->when($owner_id !== null, fn ($query) => $query->joinSub( + query: DB::table('base_albums')->select(['base_albums.id', 'base_albums.owner_id']), + as: 'base_albums', + first: 'base_albums.id', + operator: '=', + second: 'albums.id' + ) + ->where('base_albums.owner_id', '=', $owner_id)) + ->joinSub( + query: DB::table('albums', 'descendants')->select('descendants.id', 'descendants._lft', 'descendants._rgt'), + as: 'descendants', + first: function (JoinClause $join) { + $join->on('albums._lft', '<=', 'descendants._lft') + ->on('albums._rgt', '>=', 'descendants._rgt'); + } + ) + ->joinSub( + query: DB::table('photos'), + as: 'photos', + first: 'photos.album_id', + operator: '=', + second: 'descendants.id' + ) + ->joinSub( + query: DB::table('size_variants') + ->select(['size_variants.id', 'size_variants.photo_id', 'size_variants.filesize']) + ->where('size_variants.type', '!=', 7), + as: 'size_variants', + first: 'size_variants.photo_id', + operator: '=', + second: 'photos.id' + ) + ->select( + 'albums.id', + 'albums._lft', + 'albums._rgt', + DB::raw('SUM(size_variants.filesize) as size'), + )->groupBy('albums.id') + ->orderBy('albums._lft', 'asc'); + + return $query + ->get() + ->map(fn ($item) => [ + 'id' => strval($item->id), + 'left' => intval($item->_lft), + 'right' => intval($item->_rgt), + 'size' => intval($item->size), + ]); + } + + /** + * Return size statistics (number of photos rather than bytes) per album. + * + * @param string|null $album_id + * @param int|null $owner_id + * + * @return Collection + */ + public function getPhotoCountPerAlbum(?string $album_id = null, ?int $owner_id = null) + { + $query = DB::table('albums') + ->when($album_id !== null, + fn ($query) => $query + ->joinSub( + query: DB::table('albums', 'parent')->select('parent.id', 'parent._lft', 'parent._rgt'), + as: 'parent', + first: function (JoinClause $join) { + $join->on('albums._lft', '>=', 'parent._lft') + ->on('albums._rgt', '<=', 'parent._rgt'); + } + ) + ->where('parent.id', '=', $album_id) + ) + ->joinSub( + query: DB::table('base_albums')->select(['base_albums.id', 'base_albums.owner_id', 'base_albums.title', 'base_albums.is_nsfw']), + as: 'base_albums', + first: 'base_albums.id', + operator: '=', + second: 'albums.id' + ) + ->when($owner_id !== null, fn ($query) => $query->where('base_albums.owner_id', '=', $owner_id)) + ->joinSub( + query: DB::table('photos')->select(['photos.id', 'photos.album_id']), + as: 'photos', + first: 'photos.album_id', + operator: '=', + second: 'albums.id' + ) + ->joinSub( + query: DB::table('users')->select(['users.id', 'users.username']), + as: 'users', + first: 'users.id', + operator: '=', + second: 'base_albums.owner_id' + ) + ->select( + 'albums.id', + 'username', + 'base_albums.title', + 'base_albums.is_nsfw', + 'albums._lft', + 'albums._rgt', + DB::raw('COUNT(photos.id) as num_photos'), + )->groupBy( + 'albums.id', + 'username', + 'base_albums.title', + 'base_albums.is_nsfw', + 'albums._lft', + 'albums._rgt', + ) + ->orderBy('albums._lft', 'asc'); + + return $query + ->get() + ->map(fn ($item) => [ + 'id' => strval($item->id), + 'username' => strval($item->username), + 'title' => strval($item->title), + 'is_nsfw' => boolval($item->is_nsfw), + 'left' => intval($item->_lft), + 'right' => intval($item->_rgt), + 'num_photos' => intval($item->num_photos), + 'num_descendants' => intval(($item->_rgt - $item->_lft - 1) / 2), + ]); + } + + /** + * Same as above but including sub-albums. + * + * @param string|null $album_id + * @param int|null $owner_id + * + * @return Collection + */ + public function getTotalPhotoCountPerAlbum(?string $album_id = null, ?int $owner_id = null) + { + $query = DB::table('albums') + ->when($album_id !== null, fn ($query) => $query->where('albums.id', '=', $album_id)) + ->joinSub( + query: DB::table('base_albums')->select(['base_albums.id', 'base_albums.owner_id', 'base_albums.title', 'base_albums.is_nsfw']), + as: 'base_albums', + first: 'base_albums.id', + operator: '=', + second: 'albums.id' + ) + ->when($owner_id !== null, fn ($query) => $query->where('base_albums.owner_id', '=', $owner_id)) + ->joinSub( + query: DB::table('albums', 'descendants')->select('descendants.id', 'descendants._lft', 'descendants._rgt'), + as: 'descendants', + first: function (JoinClause $join) { + $join->on('albums._lft', '<=', 'descendants._lft') + ->on('albums._rgt', '>=', 'descendants._rgt'); + } + ) + ->joinSub( + query: DB::table('photos')->select(['photos.id', 'photos.album_id']), + as: 'photos', + first: 'photos.album_id', + operator: '=', + second: 'descendants.id' + ) + ->joinSub( + query: DB::table('users')->select(['users.id', 'users.username']), + as: 'users', + first: 'users.id', + operator: '=', + second: 'base_albums.owner_id' + ) + ->select( + 'albums.id', + 'username', + 'base_albums.title', + 'base_albums.is_nsfw', + 'albums._lft', + 'albums._rgt', + DB::raw('COUNT(photos.id) as num_photos'), + )->groupBy( + 'albums.id', + 'username', + 'base_albums.title', + 'base_albums.is_nsfw', + 'albums._lft', + 'albums._rgt', + ) + ->orderBy('albums._lft', 'asc'); + + return $query + ->get() + ->map(fn ($item) => [ + 'id' => strval($item->id), + 'username' => strval($item->username), + 'title' => strval($item->title), + 'is_nsfw' => boolval($item->is_nsfw), + 'left' => intval($item->_lft), + 'right' => intval($item->_rgt), + 'num_photos' => intval($item->num_photos), + 'num_descendants' => intval(($item->_rgt - $item->_lft - 1) / 2), + ]); + } +} diff --git a/app/Actions/Update/Apply.php b/app/Actions/Update/Apply.php deleted file mode 100644 index c40a98cada1..00000000000 --- a/app/Actions/Update/Apply.php +++ /dev/null @@ -1,152 +0,0 @@ -lycheeVersion = $lycheeVersion; - $this->githubFunctions = $githubFunctions; - } - - /** - * If we are in a production environment we actually require a double check.. - * - * @param array $output - */ - private function check_prod_env_allow_migration(array &$output) - { - if (Config::get('app.env') == 'production') { - // @codeCoverageIgnoreStart - // we cannot code cov this part. APP_ENV is dev in testing mode. - if (Configs::get_value('force_migration_in_production') == '1') { - Logs::warning(__METHOD__, __LINE__, 'Force update is production.'); - - return true; - } - - $output[] = 'Update not applied: `APP_ENV` in `.env` is `production` and `force_migration_in_production` is set to `0`.'; - Logs::warning(__METHOD__, __LINE__, 'Update not applied: `APP_ENV` in `.env` is `production` and `force_migration_in_production` is set to `0`.'); - - return false; - // @codeCoverageIgnoreEnd - } - - return true; - } - - /** - * call composer over exec. - * - * @param array $output - */ - private function call_composer(array &$output) - { - if (Configs::get_value('apply_composer_update', '0') == '1') { - // @codeCoverageIgnoreStart - Logs::warning(__METHOD__, __LINE__, 'Composer is called on update.'); - - // Composer\Factory::getHomeDir() method - // needs COMPOSER_HOME environment variable set - putenv('COMPOSER_HOME=' . base_path('/composer-cache')); - chdir(base_path()); - exec('composer install --no-dev --no-progress --no-suggest 2>&1', $output); - chdir(base_path('public')); - // @codeCoverageIgnoreEnd - } else { - $output[] = 'Composer update are always dangerous when automated.'; - $output[] = 'So we did not execute it.'; - $output[] = 'If you want to have composer update applied, please set the setting to 1 at your own risk.'; - } - } - - /** - * Arrayify a string and append it to $output. - * - * @param $string - * @param array $output - * - * @return array - */ - private function str_to_array($string, array &$output) - { - $a = explode("\n", $string); - foreach ($a as $aa) { - if ($aa != '') { - $output[] = $aa; - } - } - - return $output; - } - - /** - * call git over exec. - * - * @param array $output - */ - private function git_pull(array &$output) - { - $command = 'git pull --rebase ' . Config::get('urls.git.pull') . ' master 2>&1'; - exec($command, $output); - } - - /** - * call for migrate via the Artisan Facade. - * - * @param array $output - */ - public function artisan(array &$output) - { - Artisan::call('migrate', ['--force' => true]); - $this->str_to_array(Artisan::output(), $output); - } - - /** - * Clean coloring from the command line. - */ - public function filter(array &$output) - { - $output = preg_replace('/\033[[][0-9]*;*[0-9]*;*[0-9]*m/', '', $output); - } - - /** - * Apply the migration: - * 1. git pull - * 2. artisan migrate. - * - * @return array - */ - public function run() - { - $output = []; - if ( - $this->githubFunctions->is_master_branch() && - $this->check_prod_env_allow_migration($output) - ) { - $this->lycheeVersion->isRelease or $this->git_pull($output); - $this->artisan($output); - $this->lycheeVersion->isRelease or $this->call_composer($output); - } - $this->filter($output); - - return $output; - } -} diff --git a/app/Actions/Update/Check.php b/app/Actions/Update/Check.php deleted file mode 100644 index a627f1470a2..00000000000 --- a/app/Actions/Update/Check.php +++ /dev/null @@ -1,173 +0,0 @@ -gitHubFunctions = $gitHubFunctions; - $this->gitRequest = $gitRequest; - $this->lycheeVersion = $lycheeVersion; - } - - /** - * @throws NoOnlineUpdateException - * @throws GitNotAvailableException - * @throws ExecNotAvailableException - * @throws GitNotExecutableException - */ - public function canUpdate() - { - // we bypass this because we don't care about the other conditions as they don't apply to the release - if ($this->lycheeVersion->isRelease) { - // @codeCoverageIgnoreStart - return true; - // @codeCoverageIgnoreEnd - } - - if (Configs::get_value('allow_online_git_pull', '0') == '0') { - throw new NoOnlineUpdateException(); - } - - // When going with the CI, .git is always executable and exec is also available - // @codeCoverageIgnoreStart - if (!function_exists('exec')) { - throw new ExecNotAvailableException(); - } - if (exec('command -v git') == '') { - throw new GitNotAvailableException(); - } - - if (!$this->gitHubFunctions->has_permissions()) { - throw new GitNotExecutableException(); - } - // @codeCoverageIgnoreEnd - - return true; - } - - /** - * Cath the Exception and return the boolean equivalent. - * - * @return bool - */ - private function canUpdateBool() - { - try { - return $this->canUpdate(); - // @codeCoverageIgnoreStart - } catch (Exception $e) { - return false; - } - // @codeCoverageIgnoreEnd - } - - /** - * Clear cache and check if up to date. - * - * @return bool - * - * @throws NotMasterException - * @throws NotInCacheException - */ - private function forget_and_check() - { - $this->gitRequest->clear_cache(); - - return $this->gitHubFunctions->is_up_to_date(false); - } - - /** - * Check for updates, return text or an exception if not possible. - * - * @throws NotMasterException - * @throws NotInCacheException - */ - public function getText() - { - $up_to_date = $this->forget_and_check(); - - if (!$up_to_date) { - // @codeCoverageIgnoreStart - return $this->gitHubFunctions->get_behind_text(); - // @codeCoverageIgnoreEnd - } else { - return 'Already up to date'; - } - } - - /** - * Check for updates, returns the code - * 0 - Not Master - * 1 - Not in cache - * 1 - Up to date - * 2 - Not up to date. - * 3 - Require migration. - */ - public function getCode() - { - if ($this->lycheeVersion->isRelease) { - // @codeCoverageIgnoreStart - $versions = $this->lycheeVersion->get(); - - return 3 * intval($versions['DB']['version'] < $versions['Lychee']['version']); - // @codeCoverageIgnoreEnd - } - - $update = $this->canUpdateBool(); - - if ($update) { - try { - // @codeCoverageIgnoreStart - if (!$this->gitHubFunctions->is_up_to_date()) { - return 2; - } else { - return 1; - } - // @codeCoverageIgnoreEnd - } catch (NotInCacheException $e) { - return 1; - // @codeCoverageIgnoreStart - } catch (NotMasterException $e) { - return 0; - } - } - - return 0; - // @codeCoverageIgnoreEnd - } -} diff --git a/app/Actions/User/Create.php b/app/Actions/User/Create.php index 0d3336fce5d..f57c3fd9767 100644 --- a/app/Actions/User/Create.php +++ b/app/Actions/User/Create.php @@ -1,24 +1,53 @@ count()) { - throw new JsonError('username must be unique'); + /** + * @throws InvalidPropertyException + * @throws ModelDBException + */ + public function do( + string $username, + string $password, + ?string $email = null, + bool $mayUpload = false, + bool $mayEditOwnSettings = false, + ?int $quota_kb = null, + ?string $note = null, + ): User { + if (User::query()->where('username', '=', $username)->count() !== 0) { + throw new ConflictingPropertyException('Username already exists'); + } + if ($quota_kb === 0) { + $default = Configs::getValueAsInt('default_user_quota'); + $quota_kb = $default === 0 ? null : $default; } - $user = new User(); - $user->upload = ($data['upload'] == '1'); - $user->lock = ($data['lock'] == '1'); - $user->username = $data['username']; - $user->password = bcrypt($data['password']); + $user->may_upload = $mayUpload; + $user->may_edit_own_settings = $mayEditOwnSettings; + $user->may_administrate = false; + $user->username = $username; + $user->email = $email; + $user->password = Hash::make($password); + $user->quota_kb = $quota_kb; + $user->note = $note; + $user->save(); - return @$user->save(); + return $user; } } diff --git a/app/Actions/User/Notify.php b/app/Actions/User/Notify.php index 8dce85c8e2f..fecb2733351 100644 --- a/app/Actions/User/Notify.php +++ b/app/Actions/User/Notify.php @@ -1,43 +1,58 @@ album_id); - } - - $album_users = $album->shared_with; - - $owner = User::find($album->owner_id); - $album_users->push($owner); - - if ($album->owner_id != 0) { - $admin = User::find(0); - $album_users->push($admin); - } - - $album_users = $album_users->unique() - ->whereNotNull('email') - ->where('id', '!=', AccessControl::id()); - - return Notification::send($album_users, new PhotoAdded($request)); - } else { - return true; + if (!Configs::getValueAsBool('new_photos_notification')) { + return; + } + + // Admin user is always notified + $users = User::query()->where('may_administrate', '=', true)->get(); + + $album = $photo->album; + if ($album !== null) { + $users = $users->concat($album->shared_with); + $users->push($album->owner); } + + $users = $users + ->unique('id', true) + ->whereNotNull('email') + ->where('id', '!=', Auth::id()); + + Notification::send($users, new PhotoAdded($photo)); } } diff --git a/app/Actions/User/Save.php b/app/Actions/User/Save.php index 593eedb7167..9af96ef3ed4 100644 --- a/app/Actions/User/Save.php +++ b/app/Actions/User/Save.php @@ -1,26 +1,62 @@ where('id', '!=', $data['id'])->count()) { - throw new JsonError('username must be unique'); + /** + * @param User $user + * @param string $username + * @param string|null $password see {@link HasPasswordTrait::password()} for the difference between the values `''` and `null` + * @param bool $mayUpload + * @param bool $mayEditOwnSettings + * + * @return void + * + * @throws InvalidPropertyException + * @throws ModelDBException + */ + public function do(User $user, + string $username, + ?string $password, + bool $mayUpload, + bool $mayEditOwnSettings, + ?int $quota_kb = null, + ?string $note = null, + ): void { + if (User::query() + ->where('username', '=', $username) + ->where('id', '!=', $user->id) + ->count() !== 0 + ) { + throw new ConflictingPropertyException('Username already exists'); } - // check for duplicate name here ! - $user->username = $data['username']; - $user->upload = ($data['upload'] == '1'); - $user->lock = ($data['lock'] == '1'); - if (isset($data['password'])) { - $user->password = bcrypt($data['password']); + if ($quota_kb === 0) { + $default = \Configs::getValueAsInt('default_user_quota'); + $quota_kb = $default === 0 ? null : $default; } - return $user->save(); + $user->username = $username; + $user->may_upload = $mayUpload; + $user->may_edit_own_settings = $mayEditOwnSettings; + $user->note = $note; + $user->quota_kb = $quota_kb; + if ($password !== null && $password !== '') { + $user->password = Hash::make($password); + } + $user->save(); } } diff --git a/app/Actions/User/TokenDisable.php b/app/Actions/User/TokenDisable.php new file mode 100644 index 00000000000..296a7f9c51c --- /dev/null +++ b/app/Actions/User/TokenDisable.php @@ -0,0 +1,31 @@ +token = null; + $user->save(); + + return $user; + } +} diff --git a/app/Actions/User/TokenReset.php b/app/Actions/User/TokenReset.php new file mode 100644 index 00000000000..9cffc5fe4d5 --- /dev/null +++ b/app/Actions/User/TokenReset.php @@ -0,0 +1,32 @@ +token = hash('SHA512', $token); + $user->save(); + + return $token; + } +} diff --git a/app/Actions/WebAuth/Delete.php b/app/Actions/WebAuth/Delete.php deleted file mode 100644 index 0e6105876ca..00000000000 --- a/app/Actions/WebAuth/Delete.php +++ /dev/null @@ -1,16 +0,0 @@ -removeCredential($ids); - - return 'true'; - } -} diff --git a/app/Actions/WebAuth/GenerateAuthentication.php b/app/Actions/WebAuth/GenerateAuthentication.php deleted file mode 100644 index 3044fd466a2..00000000000 --- a/app/Actions/WebAuth/GenerateAuthentication.php +++ /dev/null @@ -1,18 +0,0 @@ -first(); - - // Create an assertion for the given user (or a blank one if not found); - return WebAuthn::generateAssertion($user); - } -} diff --git a/app/Actions/WebAuth/GenerateRegistration.php b/app/Actions/WebAuth/GenerateRegistration.php deleted file mode 100644 index 76dd6da29b5..00000000000 --- a/app/Actions/WebAuth/GenerateRegistration.php +++ /dev/null @@ -1,21 +0,0 @@ -webAuthnCredentials->map(fn ($cred) => ['id' => $cred->id]); - } -} diff --git a/app/Actions/WebAuth/VerifyAuthentication.php b/app/Actions/WebAuth/VerifyAuthentication.php deleted file mode 100644 index 9c8b9bfd4dd..00000000000 --- a/app/Actions/WebAuth/VerifyAuthentication.php +++ /dev/null @@ -1,73 +0,0 @@ -getUserFromCredentials($credential); - if ($user) { - AccessControl::login($user); - - return response()->json('Authenticated!', 200); - } - } - - return response()->json('Something went wrong with your device!', 422); - } - - /** - * Return the user that should authenticate via WebAuthn. - * - * @param array $credentials - * - * @return \Illuminate\Contracts\Auth\Authenticatable|\DarkGhostHunter\Larapass\Contracts\WebAuthnAuthenticatable|null - */ - protected function getUserFromCredentials(array $credentials) - { - // We will try to ask the User Provider for any user for the given credentials. - // If there is one, we will then return an array of credentials ID that the - // authenticator may use to sign the subsequent challenge by the server. - if ($this->isSignedChallenge($credentials)) { - $id = $this->binaryID($credentials['rawId']); - if ($id) { - return User::getFromCredentialId($id); - } - } - - return null; - } - - /** - * Transforms the raw ID string into a binary string. - * - * @param string $rawId - * - * @return string|null - */ - protected function binaryID(string $rawId) - { - return base64_decode(strtr($rawId, '-_', '+/'), true); - } - - /** - * Check if the credentials are for a public key signed challenge. - * - * @param array $credentials - * - * @return bool - */ - protected function isSignedChallenge(array $credentials) - { - return isset($credentials['id'], $credentials['rawId'], $credentials['type'], $credentials['response']); - } -} diff --git a/app/Actions/WebAuth/VerifyRegistration.php b/app/Actions/WebAuth/VerifyRegistration.php deleted file mode 100644 index f6a59dd9ded..00000000000 --- a/app/Actions/WebAuth/VerifyRegistration.php +++ /dev/null @@ -1,28 +0,0 @@ -addCredential($credential); - - return response()->json('Device registered!', 200); - } else { - return response()->json('Something went wrong with your device!', 422); - } - } -} diff --git a/app/Assets/ArrayToTextTable.php b/app/Assets/ArrayToTextTable.php new file mode 100644 index 00000000000..53c0c40a788 --- /dev/null +++ b/app/Assets/ArrayToTextTable.php @@ -0,0 +1,374 @@ + + * @copyright Copyright (c) 2015 Mathieu Viossat + * @license http://opensource.org/licenses/MIT + * + * @see https://github.com/MathieuViossat/arraytotexttable + */ + +namespace App\Assets; + +use App\Contracts\Laminas\DecoratorInterface; +use App\Metadata\Laminas\Unicode; +use Safe\Exceptions\MbstringException; +use Safe\Exceptions\PcreException; +use function Safe\mb_internal_encoding; +use function Safe\preg_match_all; + +class ArrayToTextTable +{ + public const ALIGNLEFT = STR_PAD_RIGHT; + public const ALIGNCENTER = STR_PAD_BOTH; + public const ALIGNRIGHT = STR_PAD_LEFT; + + /** @var array> */ + protected array $data; + /** @var array */ + protected array $keys; + /** @var array */ + protected array $widths; + protected DecoratorInterface $decorator; + protected string $indentation; + protected bool|string $displayKeys; + /** @var array */ + protected array $ignoredKeys; + protected bool $upperKeys; + protected int $keysAlignment; + protected int $valuesAlignment; + protected ?\Closure $formatter; + + /** + * Create a table. + * + * @param array> $rawData + * + * @return void + */ + public function __construct(array $rawData = []) + { + $this->setData($rawData) + ->setDecorator(new Unicode()) + ->setIgnoredKeys([]) + ->setIndentation('') + ->setDisplayKeys('auto') + ->setUpperKeys(true) + ->setKeysAlignment(self::ALIGNCENTER) + ->setValuesAlignment(self::ALIGNLEFT) + ->setFormatter(null); + } + + public function __toString(): string + { + return $this->getTable(); + } + + /** + * return the table. + * + * @param array|object>|null $rawData + * + * @return string + * + * @throws PcreException + * @throws MbstringException + */ + public function getTable(?array $rawData = null): string + { + if (!is_null($rawData)) { + $this->setData($rawData); + } + + $data = $this->prepare(); + $i = $this->indentation; + $d = $this->decorator; + + $displayKeys = $this->displayKeys; + if ($displayKeys === 'auto') { + $displayKeys = false; + foreach ($this->keys as $key) { + if (!is_int($key)) { + $displayKeys = true; + break; + } + } + } + + $table = $i . $this->line($d->getTopLeft(), $d->getHorizontal(), $d->getHorizontalDown(), $d->getTopRight()) . PHP_EOL; + + if ($displayKeys === true || $displayKeys === 'auto') { + $keysRow = array_combine($this->keys, $this->keys); + if ($this->upperKeys) { + $keysRow = array_map('mb_strtoupper', $keysRow); + } + $table .= $i . implode(PHP_EOL, $this->row($keysRow, $this->keysAlignment)) . PHP_EOL; + + $table .= $i . $this->line($d->getVerticalRight(), $d->getHorizontal(), $d->getCross(), $d->getVerticalLeft()) . PHP_EOL; + } + + foreach ($data as $row) { + $table .= $i . implode(PHP_EOL, $this->row($row, $this->valuesAlignment)) . PHP_EOL; + } + + $table .= $i . $this->line($d->getBottomLeft(), $d->getHorizontal(), $d->getHorizontalUp(), $d->getBottomRight()) . PHP_EOL; + + return $table; + } + + /** + * @param array|object>|null $data + * + * @return self + */ + public function setData(array|null $data): self + { + if (!is_array($data)) { + $data = []; + } + + $arrayData = []; + foreach ($data as $row) { + if (is_array($row)) { + $arrayData[] = $row; + } elseif (is_object($row)) { + $arrayData[] = get_object_vars($row); + } + } + + $this->data = $arrayData; + + return $this; + } + + public function setDecorator(DecoratorInterface $decorator): self + { + $this->decorator = $decorator; + + return $this; + } + + public function setIndentation(string $indentation): self + { + $this->indentation = $indentation; + + return $this; + } + + public function setDisplayKeys(string|bool $displayKeys): self + { + $this->displayKeys = $displayKeys; + + return $this; + } + + public function setUpperKeys(bool $upperKeys): self + { + $this->upperKeys = $upperKeys; + + return $this; + } + + public function setKeysAlignment(int $keysAlignment): self + { + $this->keysAlignment = $keysAlignment; + + return $this; + } + + public function setValuesAlignment(int $valuesAlignment): self + { + $this->valuesAlignment = $valuesAlignment; + + return $this; + } + + public function setFormatter(?\Closure $formatter): self + { + $this->formatter = $formatter; + + return $this; + } + + /** + * @param array $ignoredKeys + * + * @return ArrayToTextTable + */ + public function setIgnoredKeys(array $ignoredKeys): self + { + $this->ignoredKeys = $ignoredKeys; + + return $this; + } + + protected function line(string $left, string $horizontal, string $link, string $right): string + { + $line = $left; + foreach ($this->keys as $key) { + if (!in_array($key, $this->ignoredKeys, true)) { + $line .= str_repeat($horizontal, $this->widths[$key] + 2) . $link; + } + } + + if (mb_strlen($line) > mb_strlen($left)) { + $line = mb_substr($line, 0, -mb_strlen($horizontal)); + } + + return $line . $right; + } + + /** + * @param array $row + * @param int $alignment + * + * @return array + * + * @throws MbstringException + * @throws PcreException + */ + protected function row(array $row, int $alignment): array + { + $data = []; + $height = 1; + foreach ($this->keys as $key) { + $data[$key] = isset($row[$key]) ? static::valueToLines($row[$key]) : ['']; + $height = max($height, count($data[$key])); + } + + $rowLines = []; + for ($i = 0; $i < $height; $i++) { + $rowLine = []; + foreach ($data as $key => $value) { + $rowLine[$key] = isset($value[$i]) ? $value[$i] : ''; + } + $rowLines[] = $this->rowLine($rowLine, $alignment); + } + + return $rowLines; + } + + /** + * @param array $row + * @param int $alignment + * + * @return string + * + * @throws MbstringException + * @throws PcreException + */ + protected function rowLine(array $row, int $alignment): string + { + $line = $this->decorator->getVertical(); + + foreach ($row as $key => $value) { + if (!in_array($key, $this->ignoredKeys, true)) { + $line .= ' ' . static::mb_str_pad($value, $this->widths[$key], ' ', $alignment) . ' ' . $this->decorator->getVertical(); + } + } + + if (count($row) === 0) { + $line .= $this->decorator->getVertical(); + } + + return $line; + } + + /** + * @return array> + * + * @throws PcreException + */ + protected function prepare(): array + { + $this->keys = []; + $this->widths = []; + + $data = $this->data; + + if ($this->formatter instanceof \Closure) { + foreach ($data as &$row) { + array_walk($row, $this->formatter, $this); + } + unset($row); + } + + foreach ($data as $row) { + $this->keys = array_merge($this->keys, array_keys($row)); + } + $this->keys = array_unique($this->keys); + + foreach ($this->keys as $key) { + $this->setWidth($key, $key); + } + + foreach ($data as $row) { + foreach ($row as $columnKey => $columnValue) { + $this->setWidth($columnKey, $columnValue); + } + } + + return $data; + } + + protected static function countCJK(string $string): int + { + return preg_match_all('/[\p{Han}\p{Katakana}\p{Hiragana}\p{Hangul}]/u', $string); + } + + protected function setWidth(string $key, ?string $value): void + { + if (!isset($this->widths[$key])) { + $this->widths[$key] = 0; + } + + foreach (static::valueToLines($value) as $line) { + $width = mb_strlen($line) + self::countCJK($line); + if ($width > $this->widths[$key]) { + $this->widths[$key] = $width; + } + } + } + + /** + * @param string|null $value + * + * @return array + */ + protected static function valueToLines(?string $value): array + { + return explode("\n", $value); + } + + protected static function mb_str_pad( + string $input, + int $pad_length, + string $pad_string = ' ', + int $pad_type = STR_PAD_RIGHT, + string|null $encoding = null, + ): string { + /** @var string $encoding */ + $encoding = $encoding === null ? mb_internal_encoding() : $encoding; + $pad_before = $pad_type === STR_PAD_BOTH || $pad_type === STR_PAD_LEFT; + $pad_after = $pad_type === STR_PAD_BOTH || $pad_type === STR_PAD_RIGHT; + $pad_length -= mb_strlen($input, $encoding) + self::countCJK($input); + $target_length = $pad_before && $pad_after ? $pad_length / 2 : $pad_length; + + $repeat_times = (int) ceil($target_length / mb_strlen($pad_string, $encoding)); + $repeated_string = str_repeat($pad_string, max(0, $repeat_times)); + $before = $pad_before ? mb_substr($repeated_string, 0, (int) floor($target_length), $encoding) : ''; + $after = $pad_after ? mb_substr($repeated_string, 0, (int) ceil($target_length), $encoding) : ''; + + return $before . $input . $after; + } +} diff --git a/app/Assets/BaseSizeVariantNamingStrategy.php b/app/Assets/BaseSizeVariantNamingStrategy.php new file mode 100644 index 00000000000..1d97659ea22 --- /dev/null +++ b/app/Assets/BaseSizeVariantNamingStrategy.php @@ -0,0 +1,56 @@ +photo->isPhoto()) + ) { + return self::THUMB_EXTENSION; + } + + if ($sizeVariant === SizeVariantType::PLACEHOLDER) { + return self::PLACEHOLDER_EXTENSION; + } + + if ($this->extension === '') { + throw new MissingValueException('extension'); + } + + return $this->extension; + } +} diff --git a/app/Assets/Features.php b/app/Assets/Features.php new file mode 100644 index 00000000000..d679d63e63b --- /dev/null +++ b/app/Assets/Features.php @@ -0,0 +1,143 @@ + $featureNames to check + * + * @return bool is inactive + */ + public static function allAreActive(array $featureNames): bool + { + return array_reduce( + $featureNames, + fn ($bool, $featureName) => $bool && self::active($featureName), + true); + } + + /** + * Determine if any of the given features are active. + * + * @param array $featureNames to check + * + * @return bool is inactive + */ + public static function someAreActive(array $featureNames): bool + { + return array_reduce( + $featureNames, + fn (bool $bool, string $featureName) => $bool || self::active($featureName), + false); + } + + /** + * Determine if all of the given features are inactive. + * + * @param array $featureNames to check + * + * @return bool is inactive + */ + public static function allAreInactive(array $featureNames): bool + { + return array_reduce( + $featureNames, + fn (bool $bool, string $featureName) => $bool && self::inactive($featureName), + true); + } + + /** + * Determine if any of the given features are inactive. + * + * @param array $featureNames to check + * + * @return bool is inactive + */ + public static function someAreInactive(array $featureNames): bool + { + return array_reduce( + $featureNames, + fn (bool $bool, string $featureName) => $bool || self::inactive($featureName), + false); + } + + /** + * Determine whether a feature is active. + * + * @template T + * + * @param string|array $featureNames to check + * @param T|\Closure(): T $valIfTrue what happens or Value if we features are enabled + * @param T|\Closure(): T $valIfFalse what happens or Value if we features are disabled + * + * @return T + */ + public static function when(string|array $featureNames, mixed $valIfTrue, mixed $valIfFalse): mixed + { + $retValue = match (is_array($featureNames)) { + true => self::allAreActive($featureNames) ? $valIfTrue : $valIfFalse, + false => self::active($featureNames) ? $valIfTrue : $valIfFalse, + }; + + return is_callable($retValue) ? $retValue() : $retValue; + } + + /** + * Assert whether the feature exists or not. + * Throws an exception if not. + * + * @param string $featureName name of the feature to check + * + * @return void + * + * @throws FeaturesDoesNotExistsException + */ + private static function exists(string $featureName): void + { + if (!is_bool(config('features.' . $featureName))) { + throw new FeaturesDoesNotExistsException(sprintf('No feature with name %s found.', $featureName)); + } + } +} diff --git a/app/Assets/Helpers.php b/app/Assets/Helpers.php index 0ec5f37dc56..7d778b9cefd 100644 --- a/app/Assets/Helpers.php +++ b/app/Assets/Helpers.php @@ -1,24 +1,20 @@ numTab = 0; - } - /** * Add UnixTimeStamp to file path suffix. * @@ -39,63 +35,19 @@ public function cacheBusting(string $filePath): string return $filePath; } - /** - * return device type as string: - * desktop, mobile, pda, dect, tablet, gaming, ereader, - * media, headset, watch, emulator, television, monitor, - * camera, printer, signage, whiteboard, devboard, inflight, - * appliance, gps, car, pos, bot, projector. - * - * @return string - */ - public function getDeviceType(): string - { - $result = new BrowserParser(getallheaders(), ['cache' => app('cache.store')]); - - return $result->getType(); - } - - /* - * Generate an id from current microtime. - * - * @return string generated ID - */ - public function generateID(): string - { - // Generate id based on the current microtime - - if ( - PHP_INT_MAX == 2147483647 - || Configs::get_value('force_32bit_ids', '0') === '1' - ) { - // For 32-bit installations, we can only afford to store the - // full seconds in id. The calling code needs to be able to - // handle duplicate ids. Note that this also exposes us to - // the year 2038 problem. - $id = sprintf('%010d', microtime(true)); - } else { - // Ensure 4 digits after the decimal point, 15 characters - // total (including the decimal point), 0-padded on the - // left if needed (shouldn't be needed unless we move back in - // time :-) ) - $id = sprintf('%015.4f', microtime(true)); - $id = str_replace('.', '', $id); - } - - return $id; - } - /** * Return the 32bit truncated version of a number seen as string. * * @param string $id * @param int $prevShortId + * @param int $phpMax predefined so set to MAX php during migration + * but allow to actually test the code * * @return string updated ID */ - public function trancateIf32(string $id, int $prevShortId = 0): string + public function trancateIf32(string $id, int $prevShortId = 0, int $phpMax = PHP_INT_MAX): string { - if (PHP_INT_MAX > 2147483647) { + if ($phpMax > 2147483647) { return $id; } @@ -105,35 +57,7 @@ public function trancateIf32(string $id, int $prevShortId = 0): string $shortId = $prevShortId + 1; } - return $shortId; - } - - /** - * Returns the extension of the filename (path or URI) or an empty string. - * - * @param string $filename - * @param bool $isURI - * - * @return string extension of the filename starting with a dot - */ - public function getExtension(string $filename, bool $isURI = false): string - { - // If $filename is an URI, get only the path component - if ($isURI === true) { - $filename = parse_url($filename, PHP_URL_PATH); - } - - $extension = pathinfo($filename, PATHINFO_EXTENSION); - - // Special cases - // https://github.com/electerious/Lychee/issues/482 - list($extension) = explode(':', $extension, 2); - - if (empty($extension) === false) { - $extension = '.' . $extension; - } - - return $extension; + return (string) $shortId; } /** @@ -147,14 +71,10 @@ public function hasPermissions(string $path): bool { // Check if the given path is readable and writable // Both functions are also verifying that the path exists - if ( - file_exists($path) === true && is_readable($path) === true - && is_writeable($path) === true - ) { - return true; - } - - return false; + return + file_exists($path) && + is_readable($path) && + is_writeable($path); } /** @@ -169,9 +89,10 @@ public function hasFullPermissions(string $path): bool // Check if the given path is readable and writable // Both functions are also verifying that the path exists if ( - file_exists($path) === true && is_readable($path) === true - && is_executable($path) === true - && is_writeable($path) === true + file_exists($path) === true && + is_readable($path) === true && + is_executable($path) === true && + is_writeable($path) === true ) { return true; } @@ -188,106 +109,182 @@ public function hasFullPermissions(string $path): bool * * @return int * - * @throws DivideByZeroException + * @throws ZeroModuloException */ public function gcd(int $a, int $b): int { - if ($b == 0) { - throw new DivideByZeroException(); + if ($b === 0) { + throw new ZeroModuloException(); } - return ($a % $b) ? $this->gcd($b, $a % $b) : $b; + return ($a % $b) !== 0 ? $this->gcd($b, $a % $b) : $b; } /** - * Properly convert a boolean to a string - * the default php function returns '' in case of false, this is not the behavior we want. + * From https://www.php.net/manual/en/function.disk-total-space.php. + * + * @param float $bytes + * + * @return string */ - public function str_of_bool(bool $b): string + public function getSymbolByQuantity(float $bytes): string { - return $b ? '1' : '0'; + $symbols = [ + 'B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB', + ]; + $exp = intval(floor(log($bytes) / log(1024.0))); + + if ($exp >= sizeof($symbols)) { + // if the number is too large, we fall back to the largest available symbol + $exp = sizeof($symbols) - 1; + } + + return sprintf('%.2f %s', ($bytes / pow(1024, $exp)), $symbols[$exp]); } /** - * Given a filename generate the @2x corresponding filename. - * This is used for thumbs, small and medium. + * Check if the `exec` function is available. + * + * @return bool */ - public function ex2x(string $filename): string + public function isExecAvailable(): bool { - $filename2x = explode('.', $filename); + $disabledFunctions = explode(',', ini_get('disable_functions')); - return (count($filename2x) === 2) ? - $filename2x[0] . '@2x.' . $filename2x[1] : - $filename2x[0] . '@2x'; + return function_exists('exec') && !in_array('exec', $disabledFunctions, true); } /** - * Returns the available licenses. + * Given a duration convert it into hms. + * + * @param int|float $d length in seconds + * + * @return string equivalent time string formatted */ - public function get_all_licenses(): array + public function secondsToHMS(int|float $d): string { - return [ - 'none', - 'reserved', - 'CC0', - 'CC-BY-1.0', - 'CC-BY-2.0', - 'CC-BY-2.5', - 'CC-BY-3.0', - 'CC-BY-4.0', - 'CC-BY-NC-1.0', - 'CC-BY-NC-2.0', - 'CC-BY-NC-2.5', - 'CC-BY-NC-3.0', - 'CC-BY-NC-4.0', - 'CC-BY-NC-ND-1.0', - 'CC-BY-NC-ND-2.0', - 'CC-BY-NC-ND-2.5', - 'CC-BY-NC-ND-3.0', - 'CC-BY-NC-ND-4.0', - 'CC-BY-NC-SA-1.0', - 'CC-BY-NC-SA-2.0', - 'CC-BY-NC-SA-2.5', - 'CC-BY-NC-SA-3.0', - 'CC-BY-NC-SA-4.0', - 'CC-BY-ND-1.0', - 'CC-BY-ND-2.0', - 'CC-BY-ND-2.5', - 'CC-BY-ND-3.0', - 'CC-BY-ND-4.0', - 'CC-BY-SA-1.0', - 'CC-BY-SA-2.0', - 'CC-BY-SA-2.5', - 'CC-BY-SA-3.0', - 'CC-BY-SA-4.0', - ]; + $h = (int) floor($d / 3600); + $m = (int) floor(($d % 3600) / 60); + $s = (int) floor($d % 60); + + return ($h > 0 ? $h . 'h' : '') + . ($m > 0 ? $m . 'm' : '') + . ($s > 0 || ($h === 0 && $m === 0) ? $s . 's' : ''); } /** - * Return incrementing numbers. + * Return true if the upload_max_filesize is bellow what we want. */ - public function data_index(): int + public function convertSize(string $size): int { - $this->numTab++; + $size = trim($size); + $last = strtolower($size[strlen($size) - 1]); + $size = intval($size); + + switch ($last) { + case 'g': + $size *= 1024; + // no break + case 'm': + $size *= 1024; + // no break + case 'k': + $size *= 1024; + } - return $this->numTab; + return $size; } /** - * Reset and return incrementing numbers. + * Converts a decimal degree into integer degree, minutes and seconds. + * + * @param float|null $decimal + * @param bool $type - indicates if the passed decimal indicates a + * latitude (`true`) or a longitude (`false`) + * + * @returns string */ - public function data_index_r(): int + public function decimalToDegreeMinutesSeconds(float|null $decimal, bool $type): string|null { - $this->numTab = 1; + if ($decimal === null) { + return null; + } + + $d = abs($decimal); + + // absolute value of decimal must be smaller than 180; + if ($d > 180) { + return ''; + } + + // set direction; north assumed + if ($type && $decimal < 0) { + $direction = 'S'; + } elseif (!$type && $decimal < 0) { + $direction = 'W'; + } elseif (!$type) { + $direction = 'E'; + } else { + $direction = 'N'; + } - return $this->numTab; + // get degrees + $degrees = floor($d); + + // get seconds + $seconds = ($d - $degrees) * 3600; + + // get minutes + $minutes = floor($seconds / 60); + + // reset seconds + $seconds = floor($seconds - $minutes * 60); + + return $degrees . '° ' . $minutes . "' " . $seconds . '" ' . $direction; + } + + /** + * Censor a word by replacing half of its character by stars. + * + * @param string $string to censor + * @param float $percentOfClear the amount of the original string that remains untouched. The lower the value, the higher the censoring. + * + * @return string + */ + public function censor(string $string, float $percentOfClear = 0.5): string + { + $strLength = strlen($string); + if ($strLength === 0) { + return ''; + } + + // Length of replacement + $censored_length = $strLength - (int) floor($strLength * $percentOfClear); + + // we leave half the space in front and behind. + $start = (int) floor(($strLength - $censored_length) / 2); + + $replacement = str_repeat('*', $censored_length); + + return substr_replace($string, $replacement, $start, $censored_length); } /** - * Reset the incrementing number. + * Format exception trace as text. + * + * @param \Exception $e + * + * @return string + * + * @codeCoverageIgnore */ - public function data_index_set(int $idx = 0): void + public function exceptionTraceToText(\Exception $e): string { - $this->numTab = $idx; + $renderer = new ArrayToTextTable(); + + return $renderer->getTable(collect($e->getTrace())->map(fn (array $err) => [ + 'class' => $err['class'] ?? $err['file'] ?? '?', + 'line' => $err['line'] ?? '?', + 'function' => $err['function']])->all()); } } diff --git a/app/Assets/SizeVariantGroupedWithRandomSuffixNamingStrategy.php b/app/Assets/SizeVariantGroupedWithRandomSuffixNamingStrategy.php new file mode 100644 index 00000000000..c747c46e2f1 --- /dev/null +++ b/app/Assets/SizeVariantGroupedWithRandomSuffixNamingStrategy.php @@ -0,0 +1,165 @@ +cachedRndMiddlePath = self::createRndMiddlePath(); + } + + /** + * {@inheritDoc} + * + * @throws InsufficientEntropyException + */ + public function setPhoto(?Photo $photo): void + { + try { + parent::setPhoto($photo); + + $origFile = $this->photo?->size_variants->getOriginal()?->getFile(); + if ($origFile !== null) { + $existingRelPath = $origFile->getRelativePath(); + $matches = []; + // Extract random base path + // As the naming strategy has been changed in the past, we must + // not assume that an existing original size variant already has + // the right pattern. + // + // In order to handle UNIX and Windows directory separators, + // we must match for `/` and `\`. + // Note the funny number of four (!) backslashes inside the + // character class `[/\\\\]`. + // This is not an error! + // PHP uses the backslash itself to escape character inside + // a string. + // So `\\` becomes one backslash on the PHP level. + // The POSIX regex engine uses backslash for escaping, too, + // so we need four. + // + // As it is unspecified how the beginning of the path is + // reported, we must be prepared for an optional `/` or `./` + // at the beginning. + if (\Safe\preg_match( + '#^\.?[/\\\\]?' . + SizeVariantType::ORIGINAL->name() . '[/\\\\]' . + '([0-9a-f]{2})[/\\\\]' . + '([0-9a-f]{2})[/\\\\]' . + '([0-9a-f]{' . (self::NAME_LENGTH - 4) . '})\.#i', + $existingRelPath, + $matches + ) === 1) { + // If we have a match, we use the middle path of the original + // size variant + $this->cachedRndMiddlePath = $matches[1] . DIRECTORY_SEPARATOR . $matches[2] . DIRECTORY_SEPARATOR . $matches[3]; + } else { + // If we don't have a match, we create a new random base path. + // @codeCoverageIgnoreStart + $this->cachedRndMiddlePath = self::createRndMiddlePath(); + // @codeCoverageIgnoreEnd + } + } else { + $this->cachedRndMiddlePath = self::createRndMiddlePath(); + } + // @codeCoverageIgnoreStart + } catch (PcreException $e) { + throw LycheeAssertionError::createFromUnexpectedException($e); + } + // @codeCoverageIgnoreEnd + } + + /** + * {@inheritDoc} + */ + public function createFile(SizeVariantType $sizeVariant, bool $isBackup = false): FlysystemFile + { + $relativePath = + $sizeVariant->name() . DIRECTORY_SEPARATOR . + $this->cachedRndMiddlePath . + ($isBackup ? '_orig' : '') . + $this->generateExtension($sizeVariant); + + return new FlysystemFile(Storage::disk(StorageDiskType::LOCAL->value), $relativePath); + } + + /** + * Draws a fresh random base path. + * + * @throws InsufficientEntropyException + */ + protected static function createRndMiddlePath(): string + { + try { + $rndStr = bin2hex(random_bytes(self::NAME_LENGTH / 2)); + + return + substr($rndStr, 0, 2) . + DIRECTORY_SEPARATOR . + substr($rndStr, 2, 2) . + DIRECTORY_SEPARATOR . + substr($rndStr, 4); + // @codeCoverageIgnoreStart + } catch (\Exception $e) { + throw new InsufficientEntropyException($e); + } + // @codeCoverageIgnoreEnd + } +} diff --git a/app/Casts/ArrayCast.php b/app/Casts/ArrayCast.php new file mode 100644 index 00000000000..56e1b801c87 --- /dev/null +++ b/app/Casts/ArrayCast.php @@ -0,0 +1,55 @@ + + */ +class ArrayCast implements CastsAttributes +{ + /** + * @param Model $model the associated model class + * @param string $key the name of the SQL column holding the stringified array + * @param mixed $value the stringified array + * @param array $attributes all SQL attributes of the entity + * + * @return array the array + */ + public function get(Model $model, string $key, mixed $value, array $attributes): array + { + return ($value === null || $value === '') ? [] : explode(',', strval($value)); + } + + /** + * @param Model $model the associated model class + * @param string $key the name of the SQL column holding the stringified array + * @param (string|null)[]|null $value the array + * @param array $attributes + * + * @return array An associative map of SQL columns and their values + */ + public function set(Model $model, string $key, mixed $value, array $attributes): array + { + // Normalize the input value + // The array must not contain empty tags and tags which contain a comma + // TODO: Either use a separate table to store the tags or another encoding (e.g. JSON) which also allows commas in tags + + $arr = !is_array($value) ? [] : array_values(array_filter( + $value, + fn ($elem) => ($elem !== null && $elem !== '' && !str_contains($elem, ',')), + )); + + return [ + $key => count($arr) === 0 ? null : implode(',', $arr), + ]; + } +} diff --git a/app/Casts/DateTimeWithTimezoneCast.php b/app/Casts/DateTimeWithTimezoneCast.php index 321a3ef9b12..519792bb07f 100644 --- a/app/Casts/DateTimeWithTimezoneCast.php +++ b/app/Casts/DateTimeWithTimezoneCast.php @@ -1,14 +1,28 @@ + */ class DateTimeWithTimezoneCast implements CastsAttributes { - const TZ_ATTRIBUTE_SUFFIX = '_orig_tz'; + public const TZ_ATTRIBUTE_SUFFIX = '_orig_tz'; /** * Cast the given value into a Carbon object which respects the timezone @@ -20,31 +34,37 @@ class DateTimeWithTimezoneCast implements CastsAttributes * $key . '_orig_tz' and which stores the original timezone of the * (key, value)-pair at hand. * - * @param Model $model the associated model class - * @param string $key the name of the SQL column holding the datetime - * @param string $value the SQL datetime string - * @param array $attributes all SQL attributes of the entity + * @param Model $model the associated model class + * @param string $key the name of the SQL column holding the datetime + * @param mixed $value the SQL datetime string + * @param array $attributes all SQL attributes of the entity * * @return Carbon|null The Carbon object with a properly set timezone + * + * @throws LycheeInvalidArgumentException + * @throws MissingModelAttributeException + * @throws LycheeDomainException + * @throws InvalidFormatException + * @throws InvalidTimeZoneException */ - public function get($model, string $key, $value, array $attributes): ?Carbon + public function get(Model $model, string $key, $value, array $attributes): ?Carbon { $tzKey = $key . self::TZ_ATTRIBUTE_SUFFIX; if ($value === null) { return null; } if (!is_string($value)) { - throw new \InvalidArgumentException('$value must be an SQL datetime string'); + throw new LycheeInvalidArgumentException('$value must be an SQL datetime string'); } if (array_key_exists($tzKey, $attributes)) { $tz = $attributes[$tzKey]; } else { - throw new \InvalidArgumentException('Missing column \'' . $tzKey . '\''); + throw new MissingModelAttributeException(get_class($model), $tzKey); } // If the datetime value is non-null, then the accompanying timezone // must not be null neither. - if (!is_string($tz) || empty($tz)) { - throw new \InvalidArgumentException('Column \'' . $key . '\' is not null, but column \'' . $tzKey . '\' is either not a string, an empty string or null'); + if (!is_string($tz) || $tz === '') { + throw new LycheeDomainException('Column \'' . $key . '\' is not null, but column \'' . $tzKey . '\' is either not a string, an empty string or null'); } $result = $model->asDateTime($value); $result->setTimezone($tz); @@ -55,20 +75,27 @@ public function get($model, string $key, $value, array $attributes): ?Carbon /** * Converts the given value into an SQL string for storage. * - * @param Model $model the associated model class - * @param string $key the name of the SQL column holding the datetime - * @param Carbon|null $value the Carbon object of the model - * @param array $attributes + * @param Model $model the associated model class + * @param string $key the name of the SQL column holding the datetime + * @param Carbon|null $value the Carbon object of the model + * @param array $attributes + * + * @return array An associative map of SQL columns and their values * - * @return array An associative map of SQL columns and their values + * @throws LycheeInvalidArgumentException + * @throws InvalidTimeZoneException */ - public function set($model, string $key, $value, array $attributes): array + public function set(Model $model, string $key, mixed $value, array $attributes): array { if ($value !== null && !($value instanceof Carbon)) { - throw new \InvalidArgumentException('$value must extend \DateTimeInterface'); + $type = gettype($value); + if ($type === 'object') { + $type = get_class($value); + } + throw new LycheeInvalidArgumentException('"' . $type . '" does not implement \DateTimeInterface'); } $sqlDatetimeString = $model->fromDateTime($value); - $sqlTimezoneString = $value === null ? null : $value->getTimezone()->getName(); + $sqlTimezoneString = $value?->getTimezone()->getName(); $tzKey = $key . self::TZ_ATTRIBUTE_SUFFIX; return [ diff --git a/app/Casts/MustNotSetCast.php b/app/Casts/MustNotSetCast.php new file mode 100644 index 00000000000..b897ff5ca17 --- /dev/null +++ b/app/Casts/MustNotSetCast.php @@ -0,0 +1,68 @@ +alternative = $alternative; + } + + /** + * The mutator of the attribute. + * + * This function is called by the framework during an attempt to set the + * affected attribute. + * This mutator always throws an exception and thus prevents the attribute + * from being altered. + * + * @param Model $model the model which owns the attribute + * @param string $key the name of attribute which has been + * attempted to be set + * @param mixed $value the value which has been attempted to assign + * to the attribute + * @param array $attributes all attributes of the model + * + * @return void + * + * @throws IllegalOrderOfOperationException + */ + public function set(Model $model, string $key, mixed $value, array $attributes): void + { + $msg = 'must not set read-only attribute \'' . get_class($model) . '::$' . $key . '\' directly'; + if ($this->alternative !== null) { + $msg = $msg . ', use \'' . get_class($model) . '::$' . $this->alternative . ' instead'; + } + throw new IllegalOrderOfOperationException($msg); + } +} diff --git a/app/Console/Commands/DecodeGpsLocations.php b/app/Console/Commands/DecodeGpsLocations.php deleted file mode 100644 index b0f047f1067..00000000000 --- a/app/Console/Commands/DecodeGpsLocations.php +++ /dev/null @@ -1,63 +0,0 @@ -whereNotNull('longitude')->where( - function ($query) { - $query->where('location', '=', '')->orWhereNull('location'); - }) - ->get(); - - if (count($photos) == 0) { - $this->line('No photos or videos require processing.'); - - return 0; - } - - $cachedProvider = Geodecoder::getGeocoderProvider(); - foreach ($photos as $photo) { - $this->line('Processing ' . $photo->title . '...'); - - $photo->location = Geodecoder::decodeLocation_core($photo->latitude, $photo->longitude, $cachedProvider); - $photo->save(); - } - } -} diff --git a/app/Console/Commands/Diagnostics.php b/app/Console/Commands/Diagnostics.php index 5ecbc046372..b857556d606 100644 --- a/app/Console/Commands/Diagnostics.php +++ b/app/Console/Commands/Diagnostics.php @@ -1,43 +1,54 @@ $array */ - private function block(string $str, array $array) + private function block(string $str, array $array): void { $this->line($this->col->cyan($str)); $this->line($this->col->cyan(str_pad('', strlen($str), '-'))); @@ -59,19 +73,58 @@ private function block(string $str, array $array) } } + /** + * Format the block. + * + * @param string $str + * @param DiagnosticData[] $array + */ + private function blockDiagnostic(string $str, array $array): void + { + $this->line($this->col->cyan($str)); + $this->line($this->col->cyan(str_pad('', strlen($str), '-'))); + + foreach ($array as $elem) { + $prefix = match ($elem->type) { + MessageType::ERROR => $this->col->red('Error: '), + MessageType::WARNING => $this->col->yellow('Warning: '), + default => $this->col->green('Info: '), + }; + $this->line($prefix . $elem->message); + foreach ($elem->details as $detail) { + $this->line(' ' . $detail); + } + } + } + /** * Execute the console command. * - * @return mixed + * @return int + * + * @throws ExternalLycheeException */ - public function handle() + public function handle(): int { - $this->line(''); - $this->line(''); - $this->block('Diagnostics', resolve(Errors::class)->get()); - $this->line(''); - $this->block('System Information', resolve(Info::class)->get()); - $this->line(''); - $this->block('Config Information', resolve(Configuration::class)->get()); + /** @var string[] $skip_diagnostics */ + $skip_diagnostics = config('app.skip_diagnostics_checks'); + /** @var string[] $options */ + $options = $this->option('skip'); + if (sizeof($options) > 0) { + $skip_diagnostics = $options; + } + try { + $this->line(''); + $this->line(''); + $this->blockDiagnostic('Smart Diagnostics', resolve(Errors::class)->get($skip_diagnostics)); + $this->line(''); + $this->block('System Information', resolve(Info::class)->get()); + $this->line(''); + $this->block('Config Information', resolve(Configuration::class)->get()); + } catch (QueryBuilderException $e) { + throw new UnexpectedException($e); + } + + return 0; } } diff --git a/app/Console/Commands/ExifLens.php b/app/Console/Commands/ExifLens.php deleted file mode 100644 index 5c9d45ce7fc..00000000000 --- a/app/Console/Commands/ExifLens.php +++ /dev/null @@ -1,125 +0,0 @@ -metadataExtractor = $metadataExtractor; - } - - /** - * Execute the console command. - * - * @return mixed - */ - public function handle() - { - $argument = $this->argument('nb'); - $from = $this->argument('from'); - $timeout = $this->argument('tm'); - set_time_limit($timeout); - - // we use lens because this is the one which is most likely to be empty. - $photos = Photo::where('lens', '=', '') - ->whereNotIn('type', $this->getValidVideoTypes()) - ->offset($from) - ->limit($argument) - ->get(); - if (count($photos) == 0) { - $this->line('No pictures requires EXIF updates.'); - - return false; - } - - $i = $from; - foreach ($photos as $photo) { - $url = Storage::path('big/' . $photo->url); - if (file_exists($url)) { - $info = $this->metadataExtractor->extract($url, $photo->type); - $updated = false; - if ($photo->filesize == '' && $info['filesize'] != '') { - $photo->filesize = $info['filesize']; - $updated = true; - } - if ($photo->iso == '' && $info['iso'] != '') { - $photo->iso = $info['iso']; - $updated = true; - } - if ($photo->aperture == '' && $info['aperture'] != '') { - $photo->aperture = $info['aperture']; - $updated = true; - } - if ($photo->make == '' && $info['make'] != '') { - $photo->make = $info['make']; - $updated = true; - } - if ($photo->getAttribute('model') == '' && $info['model'] != '') { - $photo->setAttribute('model', $info['model']); - $updated = true; - } - if ($photo->lens == '' && $info['lens'] != '') { - $photo->lens = $info['lens']; - $updated = true; - } - if ($photo->shutter == '' && $info['shutter'] != '') { - $photo->shutter = $info['shutter']; - $updated = true; - } - if ($photo->focal == '' && $info['focal'] != '') { - $photo->focal = $info['focal']; - $updated = true; - } - if ($updated) { - if ($photo->save()) { - $this->line($i . ': EXIF updated for ' . $photo->title); - } else { - $this->line($i . ': Failed to update EXIF for ' . $photo->title); - } - } else { - $this->line($i . ': Could not get EXIF data/nothing to update for ' . $photo->title . '.'); - } - } else { - $this->line($i . ': File does not exist for ' . $photo->title . '.'); - } - $i++; - } - } -} diff --git a/app/Console/Commands/FixPermissions.php b/app/Console/Commands/FixPermissions.php new file mode 100644 index 00000000000..17888c57e74 --- /dev/null +++ b/app/Console/Commands/FixPermissions.php @@ -0,0 +1,152 @@ +path(''), + Storage::disk('symbolic')->path(''), + ]; + + if (!extension_loaded('posix')) { + $this->error('Non-POSIX OS detected: Command unsupported'); + + return -1; + } + + $this->isDryRun = filter_var($this->option('dry-run'), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) !== false; + + clearstatcache(true); + $this->effUserId = posix_geteuid(); + + foreach ($directories as $directory) { + $this->line(sprintf('Scanning: %s', $directory)); + $this->fixPermissionsRecursively($directory); + } + + if ($this->isDryRun && $this->changesExpected > 0) { + $this->line(''); + $this->line('To apply those modifications, run php artisan lychee:fix-permissions --dry-run=0'); + } + if ($this->isDryRun && $this->changesExpected === 0) { + $this->line(''); + $this->line('Nothing to fix.'); + } + $this->warn('This command cannot check for correct group ownership; the web diagnostic may report errors which are not detected by this tool'); + + return 0; + } + + /** + * Fixes a directory and its children recursively. + */ + private function fixPermissionsRecursively(string $path): void + { + try { + $actualPerm = fileperms($path); + + // `fileperms` also returns the higher bits of the inode mode. + // Hence, we must AND it with 07777 to only get what we are + // interested in + $actualPerm &= 07777; + + $ownerId = fileowner($path); + $fileType = filetype($path); + + $expectedPerm = match ($fileType) { + 'dir' => BasicPermissionCheck::getConfiguredDirectoryPerm(), + 'file' => BasicPermissionCheck::getConfiguredFilePerm(), + default => $actualPerm, // we do not care for links and other special files + }; + + if ($expectedPerm !== $actualPerm) { + $this->warn( + sprintf('%s has permissions %04o, but should have %04o', $path, $actualPerm, $expectedPerm) + ); + + if ($this->isDryRun) { + $this->info(sprintf( + ' => Would change permissions of %s from %04o to %04o', $path, $actualPerm, $expectedPerm + )); + $this->changesExpected++; + } else { + if ($ownerId === $this->effUserId) { + $this->info(sprintf( + ' => Changing permissions of %s from %04o to %04o', $path, $actualPerm, $expectedPerm + )); + chmod($path, $expectedPerm); + } else { + $this->error( + sprintf('Cannot change permissions of %s from %04o to %04o as current user is not the owner', $path, $actualPerm, $expectedPerm) + ); + } + } + } + + if ($fileType === 'dir') { + $dir = new \DirectoryIterator($path); + foreach ($dir as $dirEntry) { + if ($dirEntry->isDir() && !$dirEntry->isDot() || $dirEntry->isFile()) { + $this->fixPermissionsRecursively($dirEntry->getPathname()); + } + } + } + } catch (FilesystemException) { + $this->warn(sprintf('Unable to determine permissions for %s' . PHP_EOL, $path)); + } catch (\Exception $e) { + $this->error($e->getMessage()); + } + } +} diff --git a/app/Console/Commands/FixTree.php b/app/Console/Commands/FixTree.php new file mode 100644 index 00000000000..a7d0c901871 --- /dev/null +++ b/app/Console/Commands/FixTree.php @@ -0,0 +1,62 @@ +countErrors(); + + $this->line('Tree statistics'); + $this->line(' Oddness: ' . $stat['oddness']); + $this->line(' Duplicates: ' . $stat['duplicates']); + $this->line(' Wrong parents: ' . $stat['wrong_parent']); + $this->line(' Missing parents: ' . $stat['missing_parent']); + + $totalErrors = $stat['oddness'] + $stat['duplicates'] + $stat['wrong_parent'] + $stat['missing_parent']; + if ($totalErrors === 0) { + $this->line('Everything OK, nothing to fix.'); + + return 0; + } + + $this->line('Found ' . $totalErrors . ' errors.'); + $fixedNodes = $query->fixTree(); + $this->line('Fixed ' . $fixedNodes . ' nodes.'); + + return 0; + } +} diff --git a/app/Console/Commands/GenerateThumbs.php b/app/Console/Commands/GenerateThumbs.php deleted file mode 100644 index 51cf4932107..00000000000 --- a/app/Console/Commands/GenerateThumbs.php +++ /dev/null @@ -1,108 +0,0 @@ -argument('type'); - - if (!in_array($type, self::THUMB_TYPES)) { - $this->error(sprintf('Type %s is not one of %s', $type, implode(', ', self::THUMB_TYPES))); - - return 1; - } - - set_time_limit($this->argument('timeout')); - - $multiplier = 1; - $basicType = $type; - if (($split = strpos($basicType, '2')) !== false) { - $basicType = substr($basicType, 0, $split); - $multiplier = 2; - } - - $maxWidth = intval(Configs::get_value($basicType . '_max_width')) * $multiplier; - $maxHeight = intval(Configs::get_value($basicType . '_max_height')) * $multiplier; - - $this->line( - sprintf( - 'Will attempt to generate up to %s %s (%dx%d) images with a timeout of %d seconds...', - $this->argument('amount'), - $type, - $maxWidth, - $maxHeight, - $this->argument('timeout') - ) - ); - - $photos = Photo::where($type, '=', '') - ->where('type', 'like', 'image/%') - ->take($this->argument('amount')) - ->get(); - - if (count($photos) == 0) { - $this->line('No picture requires ' . $type . '.'); - - return 0; - } - - $bar = $this->output->createProgressBar(count($photos)); - $bar->start(); - - foreach ($photos as $photo) { - if ($this->resizePhoto( - $photo, - $type, - $maxWidth, - $maxHeight - )) { - $photo->save(); - $this->line(' ' . $type . ' (' . $photo->{$type} . ') for ' . $photo->title . ' created.'); - } else { - $this->line(' Could not create ' . $type . ' for ' . $photo->title . ' (' . $photo->width . 'x' . $photo->height . ').'); - } - $bar->advance(); - } - - $bar->finish(); - $this->line(' '); - } -} diff --git a/app/Console/Commands/Ghostbuster.php b/app/Console/Commands/Ghostbuster.php index 87ffe5cf38c..492fb3ba243 100644 --- a/app/Console/Commands/Ghostbuster.php +++ b/app/Console/Commands/Ghostbuster.php @@ -1,28 +1,46 @@ line(''); - $removeDeadSymLinks = (bool) $this->argument('removeDeadSymLinks'); - $dryrun = (bool) $this->argument('dryrun'); - if ($removeDeadSymLinks) { - $this->line('Also parsing database for pictures where the url does not point to an existing file.'); - $this->line($this->col->yellow('This may modify the database.')); - $this->line(''); - } - if (!$dryrun) { - $this->line($this->col->red("This is not a drill! Let's delete those files!")); - $this->line(''); - } + try { + // The asymmetry in the three lines below regarding `=== true` + // and `!== false` is by intention for improved safety. + // `filter_var` is tri-state and returns `null` for an + // unrecognized boolean value. + // In case of errors, i.e. in the `null` case, we want the first + // two to default to `false` and the third to default to `true`. + $removeDeadSymLinks = filter_var($this->option('removeDeadSymLinks'), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) === true; + $removeZombiePhotos = filter_var($this->option('removeZombiePhotos'), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) === true; + $dryrun = filter_var($this->option('dryrun'), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) !== false; + $uploadDisk = Features::active('use-s3') + ? Storage::disk(StorageDiskType::S3->value) + : Storage::disk(StorageDiskType::LOCAL->value); + $symlinkDisk = Storage::disk(SymLink::DISK_NAME); + $isLocalDisk = $uploadDisk->getAdapter() instanceof LocalFilesystemAdapter; - $path = Storage::path('big'); - $files = array_slice(scandir($path), 2); - $total = 0; + $this->line(''); + if (!$isLocalDisk) { + $this->line($this->col->red('Using non-local disk to store images, USE AT YOUR OWN RISKS! This code is not battle tested.')); + $this->line(''); + } - foreach ($files as $url) { - if ($url == 'index.html') { - continue; + if ($removeDeadSymLinks && !$isLocalDisk) { + $this->line($this->col->yellow('Removal of dead symlinks requested, but filesystem does not support symlinks.')); + $this->line('Proceeding as if removeDeadSymlinks was not set.'); + $this->line(''); + $removeDeadSymLinks = false; + } + if ($removeDeadSymLinks) { + $this->line('Also parsing database for photos with dead symbolic links.'); + $this->line($this->col->yellow('This may modify the database.')); + $this->line(''); + } + if (!$dryrun) { + $this->line($this->col->red("This is not a drill! Let's delete those files!")); + $this->line(''); } - $isDeadSymlink = is_link($path . '/' . $url) && !file_exists(readlink($path . '/' . $url)); - $photos = Photo::where(function ($query) use ($url) { - return $query->where('url', '=', $url)->orWhere('livePhotoUrl', '=', $url); - })->get(); - - if (count($photos) === 0 || ($isDeadSymlink && $removeDeadSymLinks)) { - $photoName = explode('.', $url); - - $to_delete = []; - $to_delete[] = 'thumb/' . $photoName[0] . '.jpeg'; - $to_delete[] = 'thumb/' . $photoName[0] . '@2x.jpeg'; - - // for videos - $to_delete[] = 'small/' . $photoName[0] . '.jpeg'; - $to_delete[] = 'small/' . $photoName[0] . '@2x.jpeg'; - $to_delete[] = 'medium/' . $photoName[0] . '.jpeg'; - $to_delete[] = 'medium/' . $photoName[0] . '@2x.jpeg'; - - // for normal pictures - $to_delete[] = 'small/' . $url; - $to_delete[] = 'small/' . Helpers::ex2x($url); - $to_delete[] = 'medium/' . $url; - $to_delete[] = 'medium/' . Helpers::ex2x($url); - $to_delete[] = 'big/' . $url; - - foreach ($to_delete as $del) { - $delete = 0; - if (Storage::exists($del)) { - $delete = 1; - } elseif (file_exists($path . '/' . $del)) { - // symbolic link... - $delete = 2; - } + /** @var string[] $filenames */ + $filenames = $uploadDisk->allFiles(); - if ($delete > 0) { - $total++; - if ($dryrun) { - $this->line(str_pad($del, 50) . $this->col->red(' file will be removed') . '.'); - } else { - if ($delete == 1) { - Storage::delete($del); - } else { - // symbolic link - unlink($path . '/' . $del); - } - $this->line($this->col->red('removed file: ') . $del); - } - } + $totalDeadSymLinks = 0; + $totalFiles = 0; + $totalDbEntries = 0; + + /** @var string $filename */ + foreach ($filenames as $filename) { + if (str_contains($filename, 'index.html')) { + continue; } - if ($isDeadSymlink && $removeDeadSymLinks) { - foreach ($photos as $photo) { - if ($dryrun) { - $this->line(str_pad($photo->url, 50) . $this->col->red(' photo will be removed') . '.'); - } else { - // Laravel apparently doesn't think dead symlinks 'exist', so manually remove the original here. - unlink($path . '/' . $url); + $isDeadSymlink = false; + if ($isLocalDisk) { + $fullPath = $uploadDisk->path($filename); + $isDeadSymlink = is_link($fullPath) && !file_exists(readlink($fullPath)); + } - $photo->predelete(); - $photo->delete(); + /** @var Collection $photos */ + $photos = Photo::query() + ->where('live_photo_short_path', '=', $filename) + ->get(); + /** @var Collection $sizeVariants */ + $sizeVariants = SizeVariant::query() + ->with('photo') + ->where('short_path', '=', $filename) + ->get(); - $this->line($this->col->red('removed photo: ') . $photo->url); + if ($isDeadSymlink && $removeDeadSymLinks) { + $totalDeadSymLinks++; + if ($dryrun) { + $this->line(str_pad($filename, 50) . $this->col->red(' is dead symlink and would be removed') . '.'); + } else { + // Laravel apparently doesn't think dead symlinks 'exist', so use low-level commands + unlink($uploadDisk->path($filename)); + $this->line(str_pad($filename, 50) . $this->col->red(' removed') . '.'); + $totalDbEntries += $sizeVariants->count() + $photos->count(); + /** @var SizeVariant $sizeVariant */ + foreach ($sizeVariants as $sizeVariant) { + $sizeVariant->photo->delete(); + } + /** @var Photo $photo */ + foreach ($photos as $photo) { + $photo->live_photo_short_path = null; + $photo->save(); } } + } elseif ($photos->count() + $sizeVariants->count() === 0) { + // Remove orphaned files + $totalFiles++; + if ($dryrun) { + $this->line(str_pad($filename, 50) . $this->col->red(' would be removed') . '.'); + } else { + $uploadDisk->delete($filename); + $this->line(str_pad($filename, 50) . $this->col->red(' removed') . '.'); + } } } - } - $this->line(''); - - if ($total == 0) { - $this->line($this->col->green('No pictures found to be deleted')); - } - if ($total > 0 && $dryrun) { - $this->line($total . ' pictures will be deleted.'); $this->line(''); - $this->line("Rerun the command '" . $this->col->yellow('php artisan lychee:ghostbuster ' . ($removeDeadSymLinks ? '1' : '0') . ' 0') . "' to effectively remove the files."); - } - if ($total > 0 && !$dryrun) { - $this->line($total . ' pictures have been deleted.'); - } - $sym_dir = Storage::drive('symbolic')->path(''); - $syms = array_slice(scandir($sym_dir), 3); + if ($removeZombiePhotos) { + $sizeVariants = SizeVariant::query() + ->with('photo') + ->get(); + /** @var SizeVariant $sizeVariant */ + foreach ($sizeVariants as $sizeVariant) { + if ($sizeVariant->getFile()->exists()) { + continue; + } + $totalDbEntries++; + if ($dryrun) { + $this->line(str_pad($sizeVariant->short_path, 50) . $this->col->red(' does not exist and photo would be removed') . '.'); + } else { + if ($sizeVariant->type === SizeVariantType::ORIGINAL) { + $sizeVariant->photo->delete(); + } else { + $sizeVariant->delete(); + } + $this->line(str_pad($sizeVariant->short_path, 50) . $this->col->red(' removed') . '.'); + } + } + } - foreach ($syms as $sym) { - $link_path = $sym_dir . $sym; - if (!file_exists(readlink($link_path))) { - unlink($link_path); - $this->line($this->col->red('removed symbolic link: ') . $link_path); + $total = $totalDeadSymLinks + $totalFiles + $totalDbEntries; + if ($total === 0) { + $this->line($this->col->green('No pictures found to be deleted')); + } + if ($total > 0 && $dryrun) { + $this->line($totalDeadSymLinks . ' dead symbolic links would be deleted.'); + $this->line($totalFiles . ' files would be deleted.'); + $this->line($totalDbEntries . ' photos would be deleted or sanitized'); + $this->line(''); + $this->line("Rerun the command '" . $this->col->yellow('php artisan lychee:ghostbuster --removeDeadSymLinks ' . ($removeDeadSymLinks ? '1' : '0') . ' --removeZombiePhotos ' . ($removeZombiePhotos ? '1' : '0') . ' --dryrun 0') . "' to effectively remove the files."); + } + if ($total > 0 && !$dryrun) { + $this->line($totalDeadSymLinks . ' dead symbolic links have been deleted.'); + $this->line($totalFiles . ' files have been deleted.'); + $this->line($totalDbEntries . ' photos have been deleted or sanitized'); } - } - return 1; + // Method $symlinkDisk->allFiles() crashes, if the scanned directory + // contains symbolic links. + // So we must use low-level methods here. + $symlinkDiskPath = $symlinkDisk->path(''); + $symLinks = array_slice(scandir($symlinkDiskPath), 3); + /** @var string $symLink */ + foreach ($symLinks as $symLink) { + $fullPath = $symlinkDiskPath . $symLink; + $isDeadSymlink = !file_exists(readlink($fullPath)); + if ($isDeadSymlink) { + // Laravel apparently doesn't think dead symlinks 'exist', so use low-level commands + unlink($fullPath); + $this->line($this->col->red('removed symbolic link: ') . $fullPath); + } + } + + return 0; + } catch (SymfonyConsoleException|\LogicException $e) { + throw new UnexpectedException($e); + } } } diff --git a/app/Console/Commands/ImageProcessing/DecodeGpsLocations.php b/app/Console/Commands/ImageProcessing/DecodeGpsLocations.php new file mode 100644 index 00000000000..00d7be9efa4 --- /dev/null +++ b/app/Console/Commands/ImageProcessing/DecodeGpsLocations.php @@ -0,0 +1,79 @@ +whereNotNull('latitude') + ->whereNotNull('longitude')->where( + function ($query) { + $query->where('location', '=', '')->orWhereNull('location'); + } + ) + ->get(); + + if (count($photos) === 0) { + $this->line('No photos or videos require processing.'); + + return 0; + } + + $cachedProvider = Geodecoder::getGeocoderProvider(); + /** @var Photo $photo */ + foreach ($photos as $photo) { + $this->line('Processing ' . $photo->title . '...'); + + $photo->location = Geodecoder::decodeLocation_core($photo->latitude, $photo->longitude, $cachedProvider); + $photo->save(); + } + + return 0; + } +} diff --git a/app/Console/Commands/ImageProcessing/EncodePlaceholders.php b/app/Console/Commands/ImageProcessing/EncodePlaceholders.php new file mode 100644 index 00000000000..13d3ca3caa2 --- /dev/null +++ b/app/Console/Commands/ImageProcessing/EncodePlaceholders.php @@ -0,0 +1,69 @@ +argument('limit'); + $timeout = (int) $this->argument('tm'); + + try { + set_time_limit($timeout); + } catch (InfoException) { + // Silently do nothing, if `set_time_limit` is denied. + } + + $placeholders = SizeVariant::query() + ->where('short_path', 'LIKE', '%placeholder/%') + ->limit($limit) + ->get(); + if (count($placeholders) === 0) { + $this->line('No placeholders require encoding.'); + + return 0; + } + + $placeholderEncoder = new PlaceholderEncoder(); + foreach ($placeholders as $placeholder) { + $placeholderEncoder->do($placeholder); + } + + return 0; + } catch (\Throwable $e) { + throw new UnexpectedException($e); + } + } +} diff --git a/app/Console/Commands/ImageProcessing/ExifLens.php b/app/Console/Commands/ImageProcessing/ExifLens.php new file mode 100644 index 00000000000..c89bb81b652 --- /dev/null +++ b/app/Console/Commands/ImageProcessing/ExifLens.php @@ -0,0 +1,160 @@ +argument('limit'); + $offset = (int) $this->argument('offset'); + $timeout = (int) $this->argument('tm'); + + try { + set_time_limit($timeout); + } catch (InfoException) { + // Silently do nothing, if `set_time_limit` is denied. + } + + // we use lens because this is the one which is most likely to be empty. + $photos = Photo::query()->with(['size_variants' => function ($r) { + $r->where('type', '=', SizeVariantType::ORIGINAL); + }]) + ->where('lens', '=', '') + ->whereNotIn('type', BaseMediaFile::SUPPORTED_VIDEO_MIME_TYPES) + ->offset($offset) + ->limit($limit) + ->get(); + if (count($photos) === 0) { + $this->line('No pictures requires EXIF updates.'); + + return -1; + } + + $i = $offset; + /** @var Photo $photo */ + foreach ($photos as $photo) { + try { + $localFile = $photo->size_variants->getOriginal()->getFile()->toLocalFile(); + $info = Extractor::createFromFile($localFile, filemtime($localFile->getRealPath())); + $updated = false; + if ($photo->size_variants->getOriginal()->filesize === 0) { + $photo->size_variants->getOriginal()->filesize = $localFile->getFilesize(); + $updated = true; + } + if ( + ($photo->iso === null || $photo->iso === '') && + $info->iso !== null && + $info->iso !== '' + ) { + $photo->iso = $info->iso; + $updated = true; + } + if ( + ($photo->aperture === null || $photo->aperture === '') && + $info->aperture !== null && + $info->aperture !== '' + ) { + $photo->aperture = $info->aperture; + $updated = true; + } + if ( + ($photo->make === null || $photo->make === '') && + $info->make !== null && + $info->make !== '' + ) { + $photo->make = $info->make; + $updated = true; + } + if ( + ($photo->model === null || $photo->model === '') && + $info->model !== null && + $info->model !== '' + ) { + $photo->model = $info->model; + $updated = true; + } + if ( + ($photo->lens === null || $photo->lens === '') && + $info->lens !== null && + $info->lens !== '' + ) { + $photo->lens = $info->lens; + $updated = true; + } + if ( + ($photo->shutter === null || $photo->shutter === '') && + $info->shutter !== null && + $info->shutter !== '' + ) { + $photo->shutter = $info->shutter; + $updated = true; + } + if ( + ($photo->focal === null || $photo->focal === '') && + $info->focal !== null && + $info->focal !== '' + ) { + $photo->focal = $info->focal; + $updated = true; + } + if ($updated) { + $photo->save(); + $photo->size_variants->getOriginal()->save(); + $this->line($i . ': EXIF updated for ' . $photo->title); + } else { + $this->line($i . ': Could not get EXIF data/nothing to update for ' . $photo->title . '.'); + } + } catch (ModelDBException $e) { + $this->line($i . ': Failed to update EXIF for ' . $photo->title); + $this->line($i . ': ' . $e->getMessage()); + } + $i++; + } + + return 0; + } catch (\Throwable $e) { + throw new UnexpectedException($e); + } + } +} diff --git a/app/Console/Commands/ImageProcessing/GenerateThumbs.php b/app/Console/Commands/ImageProcessing/GenerateThumbs.php new file mode 100644 index 00000000000..0452f648f27 --- /dev/null +++ b/app/Console/Commands/ImageProcessing/GenerateThumbs.php @@ -0,0 +1,148 @@ + + */ + public const SIZE_VARIANTS = [ + 'placeholder' => SizeVariantType::PLACEHOLDER, + 'thumb' => SizeVariantType::THUMB, + 'thumb2x' => SizeVariantType::THUMB2X, + 'small' => SizeVariantType::SMALL, + 'small2x' => SizeVariantType::SMALL2X, + 'medium' => SizeVariantType::MEDIUM, + 'medium2x' => SizeVariantType::MEDIUM2X, + ]; + + /** + * The name and signature of the console command. + * + * @var string + */ + protected $signature = 'lychee:generate_thumbs {type : thumb name} {amount=100 : amount of photos to process} {timeout=600 : timeout time requirement}'; + + /** + * The console command description. + * + * @var string + */ + protected $description = 'Generate intermediate thumbs if missing'; + + /** + * Execute the console command. + * + * @return int + * + * @throws ExternalLycheeException + */ + public function handle(): int + { + try { + $sizeVariantName = strval($this->argument('type')); + if (!array_key_exists($sizeVariantName, self::SIZE_VARIANTS)) { + $this->error(sprintf('Type %s is not one of %s', $sizeVariantName, implode(', ', array_keys(self::SIZE_VARIANTS)))); + + return 1; + } + + if (Features::active('use-s3')) { + $this->error('This tool does not support S3 file storage.'); + + return 1; + } + + $sizeVariantType = self::SIZE_VARIANTS[$sizeVariantName]; + + $amount = (int) $this->argument('amount'); + $timeout = (int) $this->argument('timeout'); + + try { + set_time_limit($timeout); + } catch (InfoException) { + // Silently do nothing, if `set_time_limit` is denied. + } + + $this->line( + sprintf( + 'Will attempt to generate up to %d %s images with a timeout of %d seconds...', + $amount, + $sizeVariantName, + $timeout + ) + ); + + $photos = Photo::query() + ->where('type', 'like', 'image/%') + ->with('size_variants') + ->whereDoesntHave('size_variants', function (Builder $query) use ($sizeVariantType) { + $query->where('type', '=', $sizeVariantType); + }) + ->take($amount) + ->get(); + + if (count($photos) === 0) { + $this->line('No picture requires ' . $sizeVariantName . '.'); + + return 0; + } + + $bar = $this->output->createProgressBar(count($photos)); + $bar->start(); + + // Initialize factory for size variants + $sizeVariantFactory = resolve(SizeVariantFactory::class); + /** @var Photo $photo */ + foreach ($photos as $photo) { + $sizeVariant = null; + + try { + $sizeVariantFactory->init($photo); + $sizeVariant = $sizeVariantFactory->createSizeVariantCond($sizeVariantType); + } catch (MediaFileOperationException $e) { + $sizeVariant = null; + } + + if ($sizeVariant !== null) { + $this->line(' ' . $sizeVariantName . ' (' . $sizeVariant->width . 'x' . $sizeVariant->height . ') for ' . $photo->title . ' created.'); + } else { + $this->line(' Did not create ' . $sizeVariantName . ' for ' . $photo->id . ' .'); + } + $bar->advance(); + } + + $bar->finish(); + $this->line(' '); + + return 0; + } catch (LycheeException|SymfonyConsoleException $e) { + if ($e instanceof ExternalLycheeException) { + throw $e; + } else { + throw new UnexpectedException($e); + } + } + } +} diff --git a/app/Console/Commands/ImageProcessing/Takedate.php b/app/Console/Commands/ImageProcessing/Takedate.php new file mode 100644 index 00000000000..24dc89ff1a8 --- /dev/null +++ b/app/Console/Commands/ImageProcessing/Takedate.php @@ -0,0 +1,189 @@ +msgSection = $output->section(); + $this->progressBar = new ProgressBar($output->section()); + $this->progressBar->setFormat('Photo %current%/%max% [%bar%] %percent:3s%%'); + } + + /** + * Outputs an warning. + * + * @param string $msg the message + * + * @return void + */ + private function printWarning(Photo $photo, string $msg): void + { + $this->msgSection->writeln('Warning: Photo "' . $photo->title . '" (ID=' . $photo->id . '): ' . $msg); + } + + /** + * Outputs an informational message. + * + * @param string $msg the message + * + * @return void + */ + private function printInfo(Photo $photo, string $msg): void + { + $this->msgSection->writeln('Info: Photo "' . $photo->title . '" (ID=' . $photo->id . '): ' . $msg); + } + + /** + * Execute the console command. + * + * @return int + * + * @throws ExternalLycheeException + */ + public function handle(): int + { + try { + $limit = intval($this->argument('limit')); + $offset = intval($this->argument('offset')); + $timeout = intval($this->argument('time')); + $setCreationTime = $this->option('set-upload-time') === true; + $force = $this->option('force') === true; + try { + set_time_limit($timeout); + } catch (InfoException) { + // Silently do nothing, if `set_time_limit` is denied. + } + + // For faster iteration we eagerly load the original size variant, + // but only the original size variant + $photoQuery = Photo::query()->with(['size_variants' => function ($r) { + $r->where('type', '=', SizeVariantType::ORIGINAL); + }]); + + if (!$force) { + $photoQuery->whereNull('taken_at'); + } + + // ATTENTION: We must call `count` first, otherwise `offset` and + // `limit` won't have an effect. + $count = $photoQuery->count(); + if ($count === 0) { + $this->line('No pictures require takedate updates.'); + + return -1; + } + + // We must stipulate a particular order, otherwise `offset` and `limit` have random effects + $photoQuery->orderBy('id'); + + if ($offset !== 0) { + $photoQuery->offset($offset); + } + + if ($limit !== 0) { + $photoQuery->limit($limit); + } + + $this->progressBar->setMaxSteps($limit === 0 ? $count : min($count, $limit)); + + // Unfortunately, `->getLazy` ignores `offset` and `limit`, so we must + // use a regular collection which might run out of memory for large + // values of `limit`. + $photos = $photoQuery->get(); + /** @var Photo $photo */ + foreach ($photos as $photo) { + $this->progressBar->advance(); + $localFile = $photo->size_variants->getOriginal()->getFile()->toLocalFile(); + + $info = Extractor::createFromFile($localFile, filemtime($localFile->getRealPath())); + if ($info->taken_at !== null) { + // Note: `equalTo` only checks if two times indicate the same + // instant of time on the universe's timeline, i.e. equality + // comparison is always done in UTC. + // For example "2022-01-31 20:50 CET" is deemed equal to + // "2022-01-31 19:50 GMT". + // So, we must check for equality of timezones separately. + if ($photo->taken_at->equalTo($info->taken_at) && $photo->taken_at->timezoneName === $info->taken_at->timezoneName) { + $this->printInfo($photo, 'Takestamp up-to-date.'); + } else { + $photo->taken_at = $info->taken_at; + $this->printInfo($photo, 'Takestamp set to ' . $photo->taken_at->format(self::DATETIME_FORMAT) . '.'); + } + } else { + $this->printWarning($photo, 'Failed to extract takestamp data from media file.'); + } + + if ($setCreationTime) { + $created_at = $localFile->lastModified(); + if ($created_at === $photo->created_at->timestamp) { + $this->printInfo($photo, 'Upload time up-to-date.'); + } else { + $photo->created_at = Carbon::createFromTimestamp($created_at); + $this->printInfo($photo, 'Upload time set to ' . $photo->created_at->format(self::DATETIME_FORMAT) . '.'); + } + } + + $photo->save(); + } + + return 0; + } catch (SymfonyConsoleException|InternalLycheeException|SymfonyStringException $e) { + throw new UnexpectedException($e); + } + } +} diff --git a/app/Console/Commands/ImageProcessing/VariantFilesize.php b/app/Console/Commands/ImageProcessing/VariantFilesize.php new file mode 100644 index 00000000000..0c8ff2ffba6 --- /dev/null +++ b/app/Console/Commands/ImageProcessing/VariantFilesize.php @@ -0,0 +1,71 @@ +argument('limit')); + + if ($this->confirm('This command can take a long time for large instances. Do you really want to run it now ?')) { + $variants_query = SizeVariant::query() + ->where('filesize', '=', 0)->orderBy('id'); + + $count = $variants_query->count(); + if ($count === 0) { + $this->line('All filesize variants already set in database.'); + + return $exit_code; + } + + // Internally, only holds $limit entries at once + $variants = $variants_query->lazyById($limit); + + $this->withProgressBar($variants, function (SizeVariant $variant) use (&$exit_code) { + $variantFile = $variant->getFile(); + if ($variantFile->exists()) { + $variant->filesize = $variantFile->getFilesize(); + if (!$variant->save()) { + $this->line('Failed to update filesize for ' . $variantFile->getRelativePath() . '.'); + $exit_code = -1; + } + } else { + $this->line('No file found at ' . $variantFile->getRelativePath() . '.'); + $exit_code = -1; + } + }); + } + + return $exit_code; + } +} diff --git a/app/Console/Commands/ImageProcessing/VideoData.php b/app/Console/Commands/ImageProcessing/VideoData.php new file mode 100644 index 00000000000..c0ebbc8251d --- /dev/null +++ b/app/Console/Commands/ImageProcessing/VideoData.php @@ -0,0 +1,131 @@ +argument('timeout')); + $count = intval($this->argument('count')); + try { + try { + set_time_limit($timeout); + } catch (InfoException) { + // Silently do nothing, if `set_time_limit` is denied. + } + + $this->line( + sprintf( + 'Will attempt to generate up to %d video thumbnails/metadata with a timeout of %d seconds...', + $count, + $timeout + ) + ); + + $photos = Photo::query() + ->with(['size_variants']) + ->whereIn('type', BaseMediaFile::SUPPORTED_VIDEO_MIME_TYPES) + ->whereDoesntHave('size_variants', function (Builder $query) { + $query->where('type', '=', SizeVariantType::THUMB); + }) + ->take($count) + ->get(); + + if (count($photos) === 0) { + $this->line('No videos require processing'); + + return 0; + } + + // Initialize factory for size variants + $sizeVariantFactory = resolve(SizeVariantFactory::class); + /** @var Photo $photo */ + foreach ($photos as $photo) { + $this->line('Processing ' . $photo->title . '...'); + $originalSizeVariant = $photo->size_variants->getOriginal(); + $file = $originalSizeVariant->getFile()->toLocalFile(); + + $info = Extractor::createFromFile($file, filemtime($file->getRealPath())); + + if ($originalSizeVariant->width === 0 && $info->width !== 0) { + $originalSizeVariant->width = $info->width; + } + if ($originalSizeVariant->height === 0 && $info->height !== 0) { + $originalSizeVariant->height = $info->height; + } + if ($photo->focal === null) { + $photo->focal = $info->focal; + } + if ($photo->aperture === null) { + $photo->aperture = $info->aperture; + } + if ($photo->latitude === null) { + $photo->latitude = $info->latitude; + } + if ($photo->longitude === null) { + $photo->longitude = $info->longitude; + } + if ($photo->isDirty()) { + $this->line('Updated metadata'); + } + + // TODO: Fix this line before PR; init needs more parameters + $sizeVariantFactory->init($photo); + $sizeVariantFactory->createSizeVariants(); + + $photo->save(); + } + + return 0; + } catch (SymfonyConsoleException|LycheeException|\InvalidArgumentException $e) { + if ($e instanceof ExternalLycheeException) { + throw $e; + } else { + throw new UnexpectedException($e); + } + } + } +} diff --git a/app/Console/Commands/Laravel/KeyGenerateCommand.php b/app/Console/Commands/Laravel/KeyGenerateCommand.php new file mode 100644 index 00000000000..358d08e2a4c --- /dev/null +++ b/app/Console/Commands/Laravel/KeyGenerateCommand.php @@ -0,0 +1,48 @@ +hasOption('no-override') || $this->option('no-override') === false || strlen(Config::get('app.key', '')) === 0) { + return parent::setKeyInEnvironmentFile($key); + } + + return false; + } +} diff --git a/app/Console/Commands/Laravel/LangFilesToJson.php b/app/Console/Commands/Laravel/LangFilesToJson.php new file mode 100644 index 00000000000..2a4e3586bf6 --- /dev/null +++ b/app/Console/Commands/Laravel/LangFilesToJson.php @@ -0,0 +1,96 @@ + $data + * + * @return array + * + * @throws PcreException + */ + public function convert(array $data): array + { + $result = []; + + foreach ($data as $key => $value) { + if (is_array($value)) { + $result[$key] = $this->convert($value); + } else { + if (strpos($value, ':') !== false) { + $value = preg_replace('/:(\w+)/', '{$1}', $value); + } + $result[$key] = $value; + } + } + + return $result; + } + + /** + * Execute the console command. + */ + public function handle(): void + { + $sourceDir = base_path('lang/'); + $targetDir = base_path('lang/'); + + $languages = array_diff(scandir($sourceDir), ['.', '..']); + + foreach ($languages as $language) { + if (!is_dir($sourceDir . $language)) { + continue; + } + + $languageDir = $sourceDir . $language . '/'; + /** @var string[] */ + $files = array_diff(scandir($languageDir), ['.', '..']); + + $translations = []; + + foreach ($files as $file) { + $filePath = $languageDir . $file; + $translation = require $filePath; + + $translation = $this->convert($translation); + + $translations[str_replace('.php', '', $file)] = $translation; + } + + $targetPath = $targetDir . $language . '.json'; + + file_put_contents($targetPath, json_encode($translations, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); + } + + $this->info('Language files compiled to JSON successfully!'); + } +} \ No newline at end of file diff --git a/app/Console/Commands/Laravel/Migrate.php b/app/Console/Commands/Laravel/Migrate.php new file mode 100644 index 00000000000..12c2f7ea78d --- /dev/null +++ b/app/Console/Commands/Laravel/Migrate.php @@ -0,0 +1,57 @@ +hasOption('clever') && $this->option('clever') === true; + $confirmationDefault = match ($this->option('dont-confirm')) { + 'assume-yes' => true, + 'assume-no' => false, + null => null, + default => throw new InvalidOptionException(sprintf('Unexpected option value %s for --dont-confirm', strval($this->option('dont-confirm')))), + }; + $hasPreviousCache = file_exists($this->laravel->getCachedConfigPath()) || file_exists($this->laravel->getCachedRoutesPath()); + + $this->call('optimize:clear'); + + if ($shallBeClever && !$hasPreviousCache) { + return; + } + + if ($this->isNonProductive()) { + $this->alert('Application not in Production!'); + + $hasConfirmed = $confirmationDefault ?? $this->confirm('Do you really wish to run this command?'); + + if (!$hasConfirmed) { + $this->comment('Command Canceled!'); + + return; + } + } + + parent::handle(); + } + + /** + * Checks whether Lychee is running in a non-production environment. + * + * Note, this method deliberately tends to `true` in case of doubt. + * This means if anything indicates that the setup might be used for + * developing or testing purposes, the result is `true`. + * Such indicators are the environment setting, enabled debug mode or + * debug bar, installed PhpUnit or PhpStan. + * If we are not in production mode, this command asks for confirmation, + * and we rather ask one time too often than not. + * + * @return bool `true`, if Lychee is found to run in non-production mode + * + * @throws BindingResolutionException + * @throws NotFoundExceptionInterface + * @throws ContainerExceptionInterface + */ + protected function isNonProductive(): bool + { + return + 'production' !== $this->getLaravel()->environment() || + true === config('app.debug', false) || + true === config('debugbar.enabled', false) || + file_exists(base_path('vendor/bin/phpunit')) || + file_exists(base_path('vendor/bin/phpstan')) || + file_exists(base_path('vendor/bin/phpstan.phar')); + } +} diff --git a/app/Console/Commands/Legacy/ResetAdmin.php b/app/Console/Commands/Legacy/ResetAdmin.php new file mode 100644 index 00000000000..af1af020d6b --- /dev/null +++ b/app/Console/Commands/Legacy/ResetAdmin.php @@ -0,0 +1,40 @@ +line('Command deprecated. Instead, use php artisan lychee:update_user or lychee:create_user {username} {password} --may-administrate.'); + + return 1; + } +} \ No newline at end of file diff --git a/app/Console/Commands/Npm.php b/app/Console/Commands/Npm.php deleted file mode 100644 index f0cb122baf6..00000000000 --- a/app/Console/Commands/Npm.php +++ /dev/null @@ -1,61 +0,0 @@ -argument('cmd'); - $ret = []; - if (!file_exists('public/Lychee-front/package-lock.json')) { - $cmd = 'cd public/Lychee-front; npm install'; - $this->info('execute: ' . $cmd); - exec($cmd, $ret); - foreach ($ret as $retline) { - $this->line($retline); - } - } - if ($argument == 'start') { - $cmd = 'cd public/Lychee-front; npm start'; - } else { - $cmd = 'cd public/Lychee-front; npm run compile'; - } - $this->info('execute: ' . $cmd); - exec($cmd, $ret); - foreach ($ret as $retline) { - $this->line($retline); - } - } -} diff --git a/app/Console/Commands/PhotosAddedNotification.php b/app/Console/Commands/PhotosAddedNotification.php index a2c25abd8bc..931f612d5e0 100644 --- a/app/Console/Commands/PhotosAddedNotification.php +++ b/app/Console/Commands/PhotosAddedNotification.php @@ -1,5 +1,11 @@ get(); - - foreach ($users as $user) { - $photos = []; + if (!Configs::getValueAsBool('new_photos_notification')) { + return 0; + } + $users = User::query()->whereNotNull('email')->get(); - foreach ($user->unreadNotifications as $notification) { - $photo = Photo::find($notification->data['id']); + /** @var User $user */ + foreach ($users as $user) { + $photos = []; - if ($photo && $photo->thumbUrl) { - if (!isset($photos[$photo->album_id])) { - $photos[$photo->album_id] = [ - 'name' => $photo->album->title, - 'photos' => [], - ]; - } + /** @var DatabaseNotification $notification */ + foreach ($user->unreadNotifications()->get() as $notification) { + /** @var Photo|null $photo */ + $photo = Photo::query() + ->with(['size_variants', 'size_variants.sym_links']) + ->find($notification->data['id']); - logger(Storage::url(Photo::VARIANT_2_PATH_PREFIX[Photo::VARIANT_THUMB] . '/' . $photo->thumbUrl)); + if ($photo !== null) { + if (!isset($photos[$photo->album_id])) { + $photos[$photo->album_id] = [ + 'name' => $photo->album->title, + 'photos' => [], + ]; + } - // If the url config doesn't contain a trailing slash then add it - if (substr(config('app.url'), -1) == '/') { - $trailing_slash = ''; - } else { - $trailing_slash = '/'; - } + $thumbUrl = $photo->size_variants->getThumb()?->url; - $photos[$photo->album_id]['photos'][$photo->id] = [ - 'thumb' => Storage::url(Photo::VARIANT_2_PATH_PREFIX[Photo::VARIANT_THUMB] . '/' . $photo->thumbUrl), - 'link' => config('app.url') . $trailing_slash . 'r/' . $photo->album_id . '/' . $photo->id, - ]; + // Mail clients do not like relative paths. + // if url does not start with 'http', it is not absolute... + if (!Str::startsWith('http', $thumbUrl)) { + $thumbUrl = URL::asset($thumbUrl); } - } - if (count($photos) > 0) { - try { - Mail::to($user->email)->send(new PhotosAdded($photos)); - $user->notifications()->delete(); - } catch (Exception $e) { - Logs::error(__METHOD__, __LINE__, 'Failed to send email notification for ' . $user->username); + // If the url config doesn't contain a trailing slash then add it + if (str_ends_with(config('app.url'), '/')) { + $trailing_slash = ''; + } else { + $trailing_slash = '/'; } + + $photos[$photo->album_id]['photos'][$photo->id] = [ + 'title' => $photo->title, + 'thumb' => $thumbUrl, + // TODO: Clean this up. There should be a better way to get the URL of a photo than constructing it manually + 'link' => config('app.url') . $trailing_slash . 'r/' . $photo->album_id . '/' . $photo->id, + ]; } } + + if (count($photos) > 0) { + Mail::to($user->email)->send(new PhotosAdded($photos)); + $user->notifications()->delete(); + } } + + return 0; } } diff --git a/app/Console/Commands/ResetAdmin.php b/app/Console/Commands/ResetAdmin.php deleted file mode 100644 index 9fc20d98248..00000000000 --- a/app/Console/Commands/ResetAdmin.php +++ /dev/null @@ -1,73 +0,0 @@ -col = $colorize; - } - - /** - * Execute the console command. - * - * @return mixed - */ - public function handle() - { - Legacy::resetAdmin(); - - // delete to avoid collisions. - User::where('username', '=', '')->delete(); - User::where('password', '=', '')->delete(); - User::where('id', '=', 0)->delete(); - - // recreate an admin user - $user = new User(); - $user->username = Configs::get_value('username', ''); - $user->password = Configs::get_value('password', ''); - $user->save(); - - // created user will have a id which is NOT 0. - // we want this user to have an ID of 0 as it is the ADMIN ID. - $user->id = 0; - $user->save(); - - $this->line($this->col->yellow('Admin username and password reset.')); - } -} diff --git a/app/Console/Commands/ShowLogs.php b/app/Console/Commands/ShowLogs.php deleted file mode 100644 index 6a23a4524d5..00000000000 --- a/app/Console/Commands/ShowLogs.php +++ /dev/null @@ -1,104 +0,0 @@ -col = $colorize; - } - - /** - * Execute the console command. - * - * @return mixed - */ - public function handle() - { - $action = $this->argument('action'); - $n = (int) $this->argument('n'); - $order = $this->argument('order'); - - if ($action == 'clean') { - Logs::truncate(); - $this->line($this->col->yellow('Log table has been emptied.')); - - return; - } - // we are in the show part but in the case where 'show' has not be defined. - // as a results arguments are shifted: n <- action, order <- n. - elseif ($action != 'show') { - $n = (int) $this->argument('action'); - $order = $this->argument('n'); - } - $this->action_show($n, $order); - } - - private function action_show($n, $order) - { - $order = ($order == 'ASC' || $order == 'DESC') ? $order : 'DESC'; - - if (Logs::count() == 0) { - $this->line($this->col->green('Everything looks fine, Lychee has not reported any problems!')); - } else { - $logs = Logs::orderBy('id', $order)->limit($n)->get(); - foreach ($logs->reverse() as $log) { - $this->line($this->col->magenta($log->created_at) - . ' -- ' - . $this->color_type(str_pad($log->type, 7)) - . ' -- ' - . $this->col->blue($log->function) - . ' -- ' - . $this->col->green($log->line) - . ' -- ' . $log->text); - } - } - } - - private function color_type($type) - { - switch ($type) { - case 'error ': - return $this->col->red($type); - case 'warning': - return $this->col->yellow($type); - case 'notice ': - return $this->col->cyan($type); - default: - return $type; - } - } -} diff --git a/app/Console/Commands/Sync.php b/app/Console/Commands/Sync.php index 8154e03e520..47fed66cfb7 100644 --- a/app/Console/Commands/Sync.php +++ b/app/Console/Commands/Sync.php @@ -1,12 +1,22 @@ signature = sprintf( - $this->signature, - Configs::get_value('delete_imported', '0'), - Configs::get_value('import_via_symlink', '0'), - Configs::get_value('skip_duplicates', '0') - ); + try { + $this->signature = sprintf( + $this->signature, + Configs::getValueAsString('delete_imported'), + Configs::getValueAsString('import_via_symlink'), + Configs::getValueAsString('skip_duplicates') + ); + } catch (ConfigurationKeyMissingException) { + // Catching this exception is necessary as artisan package:discover + // is called after each composer installation/update and artisan + // tries to instantiate every command. + $this->signature = sprintf($this->signature, '0', '0', '0'); + } parent::__construct(); } /** * Execute the console command. * - * @return mixed + * @return int + * + * @throws ExternalLycheeException */ - public function handle(Exec $exec) + public function handle(): int { - $directory = $this->argument('dir'); - $owner_id = (int) $this->option('owner_id'); // in case no ID provided -> import as root user - $album_id = (int) $this->option('album_id'); // in case no ID provided -> import to root folder - - // Enable CLI formatting of status - $exec->statusCLIFormatting = true; - $exec->memCheck = false; - $exec->resync_metadata = $this->option('resync_metadata'); - $exec->delete_imported = $this->option('delete_imported') === '1'; - $exec->import_via_symlink = $this->option('import_via_symlink') === '1'; - $exec->skip_duplicates = $this->option('skip_duplicates') === '1'; - - if ($exec->import_via_symlink && $exec->delete_imported) { - $this->error('The settings for import via symbolic links and deletion of imported files are conflicting'); - $this->info(' Use --import_via_symlink={0|1} and --delete-imported={0|1} explicitly to apply a conflict-free setting'); - - return 1; - } + try { + $directories = $this->argument('dir'); + if (!is_array($directories)) { + $this->error('List of directories not recognized.'); - AccessControl::log_as_id($owner_id); + return 1; + } + $owner_id = (int) $this->option('owner_id'); // in case no ID provided -> import as root user + $album_id = $this->option('album_id'); // in case no ID provided -> import to root folder + if (is_array($album_id)) { + $this->error('Only one value for album_id is allowed.'); - $this->info('Start syncing.'); + return 1; + } + /** @var Album $album */ + $album = $album_id !== null ? Album::query()->findOrFail($album_id) : null; // in case no ID provided -> import to root folder - try { - $exec->do($directory, $album_id); - } catch (Exception $e) { - $this->error($e); - } + $deleteImported = $this->option('delete_imported') === '1'; + $importViaSymlink = $this->option('import_via_symlink') === '1'; + $skipDuplicates = $this->option('skip_duplicates') === '1'; + $resyncMetadata = $this->option('resync_metadata') === true; // ! Because the option is --resync_metadata the return type of $this->option() is already bool. - $this->info('Done syncing.'); + if ($importViaSymlink && $deleteImported) { + $this->error('The settings for import via symbolic links and deletion of imported files are conflicting'); + $this->info(' Use --import_via_symlink={0|1} and --delete-imported={0|1} explicitly to apply a conflict-free setting'); + + return 1; + } + + $exec = new Exec( + new ImportMode( + $deleteImported, + $skipDuplicates, + $importViaSymlink, + $resyncMetadata + ), + $owner_id, + true, + 0 + ); + + $this->info('Start syncing.'); + + foreach ($directories as $directory) { + try { + $exec->do($directory, $album); + } catch (\Exception $e) { + $this->error($e); + } + } + + $this->info('Done syncing.'); + + return 0; + } catch (SymfonyConsoleException $e) { + throw new UnexpectedException($e); + } } } diff --git a/app/Console/Commands/Takedate.php b/app/Console/Commands/Takedate.php deleted file mode 100644 index bd35de8e7f3..00000000000 --- a/app/Console/Commands/Takedate.php +++ /dev/null @@ -1,104 +0,0 @@ -argument('nb'); - $from = $this->argument('from'); - $timeout = $this->argument('tm'); - $timestamps = $this->option('timestamp'); - $force = $this->option('force'); - set_time_limit($timeout); - - if ($argument == 0) { - $argument = PHP_INT_MAX; - } - if ($force) { - $photos = Photo::offset($from)->limit($argument)->get(); - } else { - $photos = Photo::whereNull('taken_at')->offset($from)->limit($argument)->get(); - } - if (count($photos) == 0) { - $this->line('No pictures require takedate updates.'); - - return false; - } - - $i = $from - 1; - /* @var Photo $photo */ - foreach ($photos as $photo) { - $url = Storage::path('big/' . $photo->url); - $i++; - if (!file_exists($url)) { - $this->line($i . ': File ' . $url . ' not found for ' . $photo->title . '.'); - continue; - } - $info = $metadataExtractor->extract($url, $photo->type); - /* @var \DateTime $stamp */ - $stamp = $info['taken_at']; - if ($stamp != null) { - if ($stamp == $photo->takestamp) { - $this->line($i . ': Takestamp up to date for ' . $photo->title); - continue; - } - $photo->taken_at = $stamp; - if ($photo->save()) { - $this->line($i . ': Takestamp updated to ' . $stamp->format('d M Y \a\t H:i') . ' for ' . $photo->title); - } else { - $this->line($i . ': Failed to update takestamp for ' . $photo->title); - } - continue; - } - if (!$timestamps) { - $this->line($i . ': Failed to get Takestamp data for ' . $photo->title . '.'); - continue; - } - if (is_link($url)) { - $url = readlink($url); - } - $created_at = filemtime($url); - if ($created_at == $photo->created_at->timestamp) { - $this->line($i . ': Created_at up to date for ' . $photo->title); - continue; - } - $photo->created_at->setTimestamp($created_at); - if ($photo->save()) { - $this->line($i . ': Created_at updated to ' . $photo->created_at->format('d M Y \a\t H:i') . ' for ' . $photo->title); - } else { - $this->line($i . ': Failed to update created_at for ' . $photo->title); - } - } - } -} diff --git a/app/Console/Commands/UserManagment/CreateUser.php b/app/Console/Commands/UserManagment/CreateUser.php new file mode 100644 index 00000000000..d1de53368a5 --- /dev/null +++ b/app/Console/Commands/UserManagment/CreateUser.php @@ -0,0 +1,79 @@ +create = $create; + } + + /** + * Execute the console command. + * + * @return int + * + * @throws ExternalLycheeException + */ + public function handle(): int + { + $username = strval($this->argument('username')); + $password = strval($this->argument('password')); + + $count = User::query()->count(); + + $mayAdministrate = $count < 1 || $this->option('may-administrate') === true; + $mayEditOwnSettings = $mayAdministrate || $this->option('may-edit-own-settings') === true; + $mayUpload = $mayAdministrate || $this->option('may-upload') === true; + + $user = $this->create->do( + username: $username, + password: $password, + mayUpload: $mayUpload, + mayEditOwnSettings: $mayEditOwnSettings); + $user->may_administrate = $mayAdministrate; + $user->save(); + + $this->line(sprintf('Successfully created%s user %s ', $mayAdministrate ? ' admin' : '', $username)); + + return 0; + } +} diff --git a/app/Console/Commands/UserManagment/UpdateUser.php b/app/Console/Commands/UserManagment/UpdateUser.php new file mode 100644 index 00000000000..0e800721ab1 --- /dev/null +++ b/app/Console/Commands/UserManagment/UpdateUser.php @@ -0,0 +1,70 @@ +argument('username')); + + /** @var User|null $user */ + $user = User::query()->where('username', '=', $username)->first(); + + if ($user === null) { + $this->error('user not found'); + + return 1; + } + + $password = strval($this->argument('password')); + + if ($password !== '') { + $user->password = Hash::make($password); + $user->save(); + + $this->line('Successfully updated user ' . $username); + + return 0; + } + + $this->error('wrong password'); + + return 1; + } +} diff --git a/app/Console/Commands/Utilities/Colorize.php b/app/Console/Commands/Utilities/Colorize.php index 35ac6178ec2..1aa39c12d0c 100644 --- a/app/Console/Commands/Utilities/Colorize.php +++ b/app/Console/Commands/Utilities/Colorize.php @@ -1,36 +1,42 @@ ' . $string . ''; } - public function magenta($string) + public function magenta(string $string): string { return '' . $string . ''; } - public function green($string) + public function green(string $string): string { return '' . $string . ''; } - public function yellow($string) + public function yellow(string $string): string { return '' . $string . ''; } - public function cyan($string) + public function cyan(string $string): string { return '' . $string . ''; } - public function blue($string) + public function blue(string $string): string { return '' . $string . ''; } -} \ No newline at end of file +} diff --git a/app/Console/Commands/VideoData.php b/app/Console/Commands/VideoData.php deleted file mode 100644 index 4fa8a0f09ed..00000000000 --- a/app/Console/Commands/VideoData.php +++ /dev/null @@ -1,163 +0,0 @@ -imageHandler = app(ImageHandlerInterface::class); - $this->metadataExtractor = $metadataExtractor; - } - - /** - * Execute the console command. - * - * @return mixed - */ - public function handle() - { - set_time_limit($this->argument('timeout')); - - $this->line( - sprintf( - 'Will attempt to generate up to %s video thumbnails/metadata with a timeout of %d seconds...', - $this->argument('count'), - $this->argument('timeout') - ) - ); - - $photos = Photo::whereIn('type', $this->getValidVideoTypes()) - ->where('width', '=', 0) - ->take($this->argument('count')) - ->get(); - - if (count($photos) == 0) { - $this->line('No videos require processing'); - - return 0; - } - - /** @var Photo $photo */ - foreach ($photos as $photo) { - $this->line('Processing ' . $photo->title . '...'); - $url = Storage::path('big/' . $photo->url); - - if ($photo->thumbUrl != '') { - $thumb = Storage::path('thumb/') . $photo->thumbUrl; - if (file_exists($thumb)) { - $urlBase = explode('.', $photo->url); - $thumbBase = explode('.', $photo->thumbUrl); - if ($urlBase[0] !== $thumbBase[0]) { - $photo->thumbUrl = $urlBase[0] . '.' . $thumbBase[1]; - rename($thumb, Storage::path('thumb/') . $photo->thumbUrl); - $this->line('Renamed thumb to match the video file'); - } - } - } - - if (file_exists($url)) { - $info = $this->metadataExtractor->extract($url, $photo->type); - - $updated = false; - if ($photo->width == 0 && $info['width'] !== 0) { - $photo->width = $info['width']; - $updated = true; - } - if ($photo->height == 0 && $info['height'] !== 0) { - $photo->height = $info['height']; - $updated = true; - } - if ($photo->focal == '' && $info['focal'] !== '') { - $photo->focal = $info['focal']; - $updated = true; - } - if ($photo->aperture == '' && $info['aperture'] !== '') { - $photo->aperture = $info['aperture']; - $updated = true; - } - if ($photo->latitude == null && $info['latitude'] !== null) { - $photo->latitude = $info['latitude']; - $updated = true; - } - if ($photo->longitude == null && $info['longitude'] !== null) { - $photo->longitude = $info['longitude']; - $updated = true; - } - if ($updated) { - $this->line('Updated metadata'); - } - - if ($photo->thumbUrl === '' || $photo->thumb2x === 0 || $photo->small_width === null || $photo->small2x_width === null) { - $frame_tmp = ''; - try { - $frame_tmp = $this->extractVideoFrame($photo); - } catch (\Exception $exception) { - $this->line($exception->getMessage()); - } - if ($frame_tmp !== '') { - $this->line('Extracted video frame for thumbnails'); - if ($photo->thumbUrl === '' || $photo->thumb2x === 0) { - if (!$this->createThumb($photo, $frame_tmp)) { - $this->line('Could not create thumbnail for video'); - } - $urlBase = explode('.', $photo->url); - $photo->thumbUrl = $urlBase[0] . '.jpeg'; - } - if ($photo->small_width === null || $photo->small2x_width === null) { - $this->createSmallerImages($photo, $frame_tmp); - } - unlink($frame_tmp); - } - } - } else { - $this->line('File does not exist'); - } - - $photo->save(); - } - } -} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index b9fcebdb6ce..7751e165a65 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -1,28 +1,30 @@ command('lychee:photos_added_notification')->weekly(); } @@ -31,11 +33,17 @@ protected function schedule(Schedule $schedule) * Register the commands for the application. * * @return void + * + * @throws \ReflectionException + * @throws \RuntimeException + * @throws DirectoryNotFoundException */ - protected function commands() + protected function commands(): void { $this->load(__DIR__ . '/Commands'); - - require base_path('routes/console.php'); + $this->load(__DIR__ . '/Commands/Laravel'); + $this->load(__DIR__ . '/Commands/Legacy'); + $this->load(__DIR__ . '/Commands/UserManagment'); + $this->load(__DIR__ . '/Commands/ImageProcessing'); } } diff --git a/app/Constants/AccessPermissionConstants.php b/app/Constants/AccessPermissionConstants.php new file mode 100644 index 00000000000..bcf39bcb13b --- /dev/null +++ b/app/Constants/AccessPermissionConstants.php @@ -0,0 +1,29 @@ +128 bit) of randomness are considered sufficient to only + * allow for a small chance to guess an ID. + * The length must be divisible by 4 as otherwise the Base64 encoding + * uses the character `=` for padding which must not be used within a URL. + * We use Base64 encoding (instead of an encoding with hex digits), + * because Base64 encoding is more space efficient and also more time + * efficient when used as a primary ID in a database. + * + * @var int + */ + public const ID_LENGTH = 24; + public const ID_TYPE = 'string'; + public const LEGACY_ID_NAME = 'legacy_id'; + public const LEGACY_ID_TYPE = 'integer'; +} \ No newline at end of file diff --git a/app/Contracts/AddPhotoStrategyInterface.php b/app/Contracts/AddPhotoStrategyInterface.php deleted file mode 100644 index 73ea98bdb21..00000000000 --- a/app/Contracts/AddPhotoStrategyInterface.php +++ /dev/null @@ -1,21 +0,0 @@ - + */ +interface DTO extends Arrayable, Jsonable, \JsonSerializable +{ + /** + * Convert the instance into a JSON string. + * + * The error message is inspired by {@link JsonEncodingException::forModel()}. + * + * @param int $options + * + * @return string + * + * @throws JsonEncodingException + */ + public function toJson($options = 0): string; + + /** + * Serializes this object into an array. + * + * @see Arrayable::toArray() + * + * @return array The serialized properties of this object + * + * @throws \JsonException + */ + public function jsonSerialize(): array; + + /** + * {@inheritDoc} + */ + public function toArray(): array; +} \ No newline at end of file diff --git a/app/Contracts/DiagnosticCheckInterface.php b/app/Contracts/DiagnosticCheckInterface.php deleted file mode 100644 index 1ae0cc80594..00000000000 --- a/app/Contracts/DiagnosticCheckInterface.php +++ /dev/null @@ -1,8 +0,0 @@ - + */ + public function albums(): Collection; +} diff --git a/app/Contracts/Http/Requests/HasBaseAlbum.php b/app/Contracts/Http/Requests/HasBaseAlbum.php new file mode 100644 index 00000000000..813164887f1 --- /dev/null +++ b/app/Contracts/Http/Requests/HasBaseAlbum.php @@ -0,0 +1,19 @@ + + */ + public function configs(): Collection; +} diff --git a/app/Contracts/Http/Requests/HasCopyright.php b/app/Contracts/Http/Requests/HasCopyright.php new file mode 100644 index 00000000000..b56ef6507ae --- /dev/null +++ b/app/Contracts/Http/Requests/HasCopyright.php @@ -0,0 +1,17 @@ + + */ + public function photos(): Collection; +} diff --git a/app/Contracts/Http/Requests/HasQuotaKB.php b/app/Contracts/Http/Requests/HasQuotaKB.php new file mode 100644 index 00000000000..f08b81f7f9e --- /dev/null +++ b/app/Contracts/Http/Requests/HasQuotaKB.php @@ -0,0 +1,17 @@ +> + */ + public static function rules(): array; + + // TODO: Associate error message to above rules. +} diff --git a/app/Contracts/Image/BinaryBlob.php b/app/Contracts/Image/BinaryBlob.php new file mode 100644 index 00000000000..7c2b7292f32 --- /dev/null +++ b/app/Contracts/Image/BinaryBlob.php @@ -0,0 +1,77 @@ +write($sourceBlob->read()) + * + * using streams. + * This API is inspired by Flysystem. + * + * @property ?resource $stream + */ +interface BinaryBlob +{ + /** + * Returns a stream from which can be read. + * + * To free the stream after use, call {@link BinaryBlob::close()}. + * Calling `read` multiple times is safe. + * The read pointer of the stream will be reset to the beginning of + * the stream, without closing the stream in between. + * + * @return resource + * + * @throws MediaFileOperationException + */ + public function read(); + + /** + * Writes the content of the provided stream into the blob. + * + * @param resource $stream the input stream which provides the input to write + * @param bool $collectStatistics if true, the method returns statistics about the stream + * + * @return ?StreamStats optional statistics about the stream, if requested + * + * @throws MediaFileOperationException + */ + public function write($stream, bool $collectStatistics = false): ?StreamStats; + + /** + * Closes the internal stream/buffer. + * + * The associated buffer is implicitly freed when this object becomes + * unreachable and is garbage-collected. + * Calling this function frees the memory explicitly. + * Note, the content of the freed buffer is lost (unless saved somewhere + * otherwise). + * It is safe to call {@link BinaryBlob::read()} and + * {@link BinaryBlob::write()} again after this method. + * A new buffer will be created, if needed. + * + * @return void + * + * @throws MediaFileOperationException + */ + public function close(): void; +} diff --git a/app/Contracts/Image/ImageHandlerInterface.php b/app/Contracts/Image/ImageHandlerInterface.php new file mode 100644 index 00000000000..4704552e430 --- /dev/null +++ b/app/Contracts/Image/ImageHandlerInterface.php @@ -0,0 +1,122 @@ + $photos + * @property Thumb|null $thumb + * @property Collection $access_permissions + */ +interface AbstractAlbum +{ + /** + * @return Relation>|Builder + */ + public function photos(): Relation|Builder; + + /** + * Returns the permissions for the public user. + * + * @return ?AccessPermission + */ + public function public_permissions(): AccessPermission|null; +} diff --git a/app/Contracts/Models/AbstractSizeVariantNamingStrategy.php b/app/Contracts/Models/AbstractSizeVariantNamingStrategy.php new file mode 100644 index 00000000000..01db20c71b0 --- /dev/null +++ b/app/Contracts/Models/AbstractSizeVariantNamingStrategy.php @@ -0,0 +1,70 @@ +extension = $extension; + } + + /** + * Sets the photo for which names of size variants shall be generated. + * + * @param Photo|null $photo the photo whose size variants shall be named + * + * @return void + */ + public function setPhoto(?Photo $photo): void + { + $this->photo = $photo; + $this->extension = ''; + if ($this->photo !== null && ($sv = $this->photo->size_variants->getOriginal()) !== null) { + $this->extension = $sv->getFile()->getExtension(); + } + } + + /** + * Creates a file for the designated size variant. + * + * @param SizeVariantType $sizeVariant the size variant + * @param bool $isBackup whether to create a backup file + * + * @return FlysystemFile the file + * + * @throws LycheeException + * + * @codeCoverageIgnore + */ + abstract public function createFile(SizeVariantType $sizeVariant, bool $isBackup = false): FlysystemFile; +} diff --git a/app/Contracts/Models/HasRandomID.php b/app/Contracts/Models/HasRandomID.php new file mode 100644 index 00000000000..5022602baa5 --- /dev/null +++ b/app/Contracts/Models/HasRandomID.php @@ -0,0 +1,13 @@ + the collection of created size variants + * + * @throws LycheeException + */ + public function createSizeVariants(): Collection; +} diff --git a/app/Contracts/PhotoCreate/DuplicatePipe.php b/app/Contracts/PhotoCreate/DuplicatePipe.php new file mode 100644 index 00000000000..31b7f777830 --- /dev/null +++ b/app/Contracts/PhotoCreate/DuplicatePipe.php @@ -0,0 +1,25 @@ + + */ +abstract class AbstractDTO implements DTO +{ + /** + * Convert the instance into a JSON string. + * + * The error message is inspired by {@link JsonEncodingException::forModel()}. + * + * @param int $options + * + * @return string + * + * @throws JsonEncodingException + */ + public function toJson($options = 0): string + { + try { + // Note, we must not use the option `JSON_THROW_ON_ERROR` here, + // because this does not clear `json_last_error()` from any + // previous, stalled error message. + // But `\Illuminate\Http\JsonResponse::setData()` falsy assumes + // that this method does so. + // Hence, we call `json_encode` _without_ specifying + // `JSON_THROW_ON_ERROR` and then mimic that behaviour. + $json = json_encode($this->jsonSerialize(), $options); + if (json_last_error() !== JSON_ERROR_NONE) { + // @codeCoverageIgnoreStart + throw new \JsonException(json_last_error_msg(), json_last_error()); + // @codeCoverageIgnoreEnd + } + + return $json; + // @codeCoverageIgnoreStart + } catch (\JsonException $e) { + throw new JsonEncodingException('Error encoding DTO [' . get_class($this) . ']', 0, $e); + } + // @codeCoverageIgnoreEnd + } + + /** + * Serializes this object into an array. + * + * @see Arrayable::toArray() + * + * @return array The serialized properties of this object + * + * @throws \JsonException + */ + public function jsonSerialize(): array + { + try { + return $this->toArray(); + // @codeCoverageIgnoreStart + } catch (\Exception $e) { + throw new \JsonException(get_class($this) . '::toArray() failed', 0, $e); + // @codeCoverageIgnoreEnd + } + } + + /** + * {@inheritDoc} + * + * @codeCoverageIgnore + */ + abstract public function toArray(): array; +} diff --git a/app/DTO/AlbumSortingCriterion.php b/app/DTO/AlbumSortingCriterion.php new file mode 100644 index 00000000000..abdd2a2723a --- /dev/null +++ b/app/DTO/AlbumSortingCriterion.php @@ -0,0 +1,35 @@ +toColumnSortingType(); + + $orderSorting = Configs::getValueAsEnum('sorting_albums_order', OrderSortingType::class); + + return new self( + $columnSorting ?? ColumnSortingType::CREATED_AT, + $orderSorting ?? OrderSortingType::ASC + ); + } +} diff --git a/app/DTO/ArrayableDTO.php b/app/DTO/ArrayableDTO.php new file mode 100644 index 00000000000..46de7e4ff0c --- /dev/null +++ b/app/DTO/ArrayableDTO.php @@ -0,0 +1,49 @@ + + */ +class ArrayableDTO extends AbstractDTO +{ + /** + * By default, we return an array containing the PUBLIC attributes of the DTO. + * + * @return array the serialized properties of this object + */ + public function toArray(): array + { + $result = []; + $cls = new \ReflectionClass($this); + $props = $cls->getProperties(\ReflectionProperty::IS_PUBLIC); + foreach ($props as $prop) { + $propertyValue = $prop->getValue($this); + if (is_object($propertyValue)) { + if ($propertyValue instanceof Arrayable) { + $propertyValue = $propertyValue->toArray(); + } elseif ($propertyValue instanceof \BackedEnum) { + $propertyValue = $propertyValue->value; + } else { + throw new LycheeLogicException(sprintf('Unable to convert %s into an array', get_class($propertyValue))); + } + } + $result[$prop->getName()] = $propertyValue; + } + + return $result; + } +} \ No newline at end of file diff --git a/app/DTO/BacktraceRecord.php b/app/DTO/BacktraceRecord.php new file mode 100644 index 00000000000..55cb0a87c7f --- /dev/null +++ b/app/DTO/BacktraceRecord.php @@ -0,0 +1,167 @@ + + */ +class BacktraceRecord extends AbstractDTO +{ + public const UNKNOWN_PLACEHOLDER = ''; + public const NAMESPACE_SEPARATOR = '::'; + + protected string $basePath; + protected string $file; + protected int $line; + protected string $class; + protected string $function; + + /** + * Constructor. + * + * @param string $file the filename + * @param int $line the line (0 indicates "unknown") + * @param string $class the class name + * @param string $function the function name + */ + public function __construct(string $file = '', int $line = 0, string $class = '', string $function = '') + { + $this->basePath = base_path(); + $this->file = $file; + $this->line = $line; + $this->class = $class; + $this->function = $function; + } + + /** + * Gets the file name. + * + * @return string the file name + */ + public function getFile(): string + { + return $this->file; + } + + /** + * Gets the beautified file name. + * + * Beautification means two things: + * + * - The base path is stripped off from the prefix of the file name. + * The installation directory depends on the setup and does not provide + * any helpful information. + * Moreover, the log limits this attribute to 100 characters. + * - An empty file name (this may happen for low-level function inside + * the PHP engine) is replaced by the special string + * {@link self::UNKNOWN_PLACEHOLDER} to avoid the wrong impression that + * logging might have failed, if the value was empty. + * + * @return string the beautified file name + */ + public function getFileBeautified(): string + { + return $this->file !== '' ? + Str::replaceFirst($this->basePath, '', $this->file) : + self::UNKNOWN_PLACEHOLDER; + } + + /** + * Gets the line number. + * + * @return int the line number + */ + public function getLine(): int + { + return $this->line; + } + + /** + * Gets the class name. + * + * @return string the class name + */ + public function getClass(): string + { + return $this->class; + } + + /** + * Gets the function name. + * + * Note: In PHP terminology, the function name is the bare name of + * function without any namespace indication. + * Hence, the function name does not indicate if it is a global function + * or a method inside a class. + * For most practical use cases, you want to use + * {@link BacktraceRecord::getMethodBeautified()}. + * + * @return string the function name + */ + public function getFunction(): string + { + return $this->function; + } + + /** + * Gets the beautified function name. + * + * See {@link BacktraceRecord::getFunction} for a definition of the term + * "function". + * + * Beautification means that an empty function name is replaced by the + * special string {@link self::UNKNOWN_PLACEHOLDER} to avoid the wrong + * impression that logging might have failed, if the value was empty. + * + * The function name is empty (or unknown), if the error has occurred + * in the global namespace, i.e. in a top level script. + * + * @return string the function name + */ + public function getFunctionBeautified(): string + { + return $this->function !== '' ? $this->function : self::UNKNOWN_PLACEHOLDER; + } + + /** + * Gets the beautified method name. + * + * Note: In PHP terminology, the method name includes a namespace + * indication. + * + * The return value can have one of the following three patterns: + * + * - `'::'`, outside any function in the global name space + * - `'::foo'`, global method `foo` + * - `'bar::foo'`, method `foo` of class or trait `bar` + * + * Note that the fourth option `bar::` is impossible, because + * no code can exist inside a class, but outside a method. + * + * @return string + */ + public function getMethodBeautified(): string + { + return $this->class . self::NAMESPACE_SEPARATOR . $this->getFunctionBeautified(); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'file' => $this->getFileBeautified(), + 'line' => $this->line, + 'method' => $this->getMethodBeautified(), + ]; + } +} diff --git a/app/DTO/BaseImportReport.php b/app/DTO/BaseImportReport.php new file mode 100644 index 00000000000..085f4eb4979 --- /dev/null +++ b/app/DTO/BaseImportReport.php @@ -0,0 +1,59 @@ + + */ +abstract class BaseImportReport extends AbstractDTO +{ + /** + * Indicates the type (i.e. the subclass) of this class. + * This information is required by the front-end to correctly cast + * the response into the correct type. + * + * @var string + */ + protected string $type; + + protected function __construct(string $type) + { + $this->type = $type; + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'type' => $this->type, + ]; + } + + /** + * @return string + * + * @codeCoverageIgnore + */ + abstract public function toCLIString(): string; +} diff --git a/app/DTO/DiagnosticData.php b/app/DTO/DiagnosticData.php new file mode 100644 index 00000000000..dcc7e33d1a7 --- /dev/null +++ b/app/DTO/DiagnosticData.php @@ -0,0 +1,72 @@ +height > 0 ? $this->width / $this->height : 0; + } +} diff --git a/app/DTO/ImportEventReport.php b/app/DTO/ImportEventReport.php new file mode 100644 index 00000000000..15462f36032 --- /dev/null +++ b/app/DTO/ImportEventReport.php @@ -0,0 +1,66 @@ +subtype = $subtype; + $this->severity = $severity; + $this->path = $path; + $this->message = $message; + $this->throwable = $throwable; + } + + public static function createWarning(string $subtype, ?string $path, string $message): self + { + return new self($subtype, SeverityType::WARNING, $path, $message); + } + + public static function createFromException(\Throwable $e, ?string $path): self + { + return new self(class_basename($e), ExceptionHandler::getLogSeverity($e), $path, $e->getMessage(), $e); + } + + public function getException(): ?\Throwable + { + return $this->throwable; + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return array_merge(parent::toArray(), [ + 'subtype' => $this->subtype, + 'severity' => $this->severity->value, + 'path' => $this->path, + 'message' => $this->message, + ]); + } + + public function toCLIString(): string + { + return $this->path . ($this->path !== null ? ': ' : '') . $this->message; + } +} diff --git a/app/DTO/ImportMode.php b/app/DTO/ImportMode.php new file mode 100644 index 00000000000..f88e11f9128 --- /dev/null +++ b/app/DTO/ImportMode.php @@ -0,0 +1,34 @@ +shallDeleteImported = $deleteImported; + $this->shallSkipDuplicates = $skipDuplicates; + // avoid incompatible settings (delete originals takes precedence over symbolic links) + $this->shallImportViaSymlink = $deleteImported ? false : $importViaSymlink; + // (re-syncing metadata makes no sense when importing duplicates) + $this->shallResyncMetadata = !$skipDuplicates ? false : $resyncMetadata; + } +} diff --git a/app/DTO/ImportParam.php b/app/DTO/ImportParam.php new file mode 100644 index 00000000000..8cd220a79bf --- /dev/null +++ b/app/DTO/ImportParam.php @@ -0,0 +1,35 @@ +importMode = $importMode; + $this->intendedOwnerId = $intendedOwnerId; + } +} diff --git a/app/DTO/ImportProgressReport.php b/app/DTO/ImportProgressReport.php new file mode 100644 index 00000000000..23339b78247 --- /dev/null +++ b/app/DTO/ImportProgressReport.php @@ -0,0 +1,45 @@ +path = $path; + $this->progress = $progress; + } + + public static function create(string $path, int $progress): self + { + return new self($path, $progress); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return array_merge(parent::toArray(), [ + 'path' => $this->path, + 'progress' => $this->progress, + ]); + } + + public function toCLIString(): string + { + return $this->path . ': ' . $this->progress . '%'; + } +} diff --git a/app/DTO/LycheeGitInfo.php b/app/DTO/LycheeGitInfo.php new file mode 100644 index 00000000000..ca11ff629bf --- /dev/null +++ b/app/DTO/LycheeGitInfo.php @@ -0,0 +1,30 @@ +branch = $gvc->localBranch ?? '??'; + $this->commit = $gvc->localHead ?? '??'; + $this->additional = $gvc->getBehindTest(); + } + + public function toString(): string + { + return sprintf('%s (%s) -- %s', $this->branch, $this->commit, $this->additional); + } +} diff --git a/app/DTO/PhotoCreate/DuplicateDTO.php b/app/DTO/PhotoCreate/DuplicateDTO.php new file mode 100644 index 00000000000..060668f3d2b --- /dev/null +++ b/app/DTO/PhotoCreate/DuplicateDTO.php @@ -0,0 +1,72 @@ +importMode->shallResyncMetadata, + shallSkipDuplicates: $initDTO->importMode->shallSkipDuplicates, + intendedOwnerId: $initDTO->intendedOwnerId, + is_starred: $initDTO->is_starred, + exifInfo: $initDTO->exifInfo, + album: $initDTO->album, + photo: $initDTO->duplicate, + ); + } + + public function getPhoto(): Photo + { + return $this->photo; + } + + public function setHasBeenResync(bool $val): void + { + $this->hasBeenReSynced = $val; + } + + public function replicatePhoto(): void + { + $dup = $this->photo; + $this->photo = $dup->replicate(); + } +} diff --git a/app/DTO/PhotoCreate/InitDTO.php b/app/DTO/PhotoCreate/InitDTO.php new file mode 100644 index 00000000000..ea5a4a2eafd --- /dev/null +++ b/app/DTO/PhotoCreate/InitDTO.php @@ -0,0 +1,61 @@ +sourceFile = $sourceFile; + $this->importMode = $parameters->importMode; + $this->intendedOwnerId = $parameters->intendedOwnerId; + $this->is_starred = $parameters->is_starred; + $this->exifInfo = $parameters->exifInfo; + $this->album = $album; + $this->fileLastModifiedTime = $fileLastModifiedTime; + } +} diff --git a/app/DTO/PhotoCreate/PhotoPartnerDTO.php b/app/DTO/PhotoCreate/PhotoPartnerDTO.php new file mode 100644 index 00000000000..657503578c7 --- /dev/null +++ b/app/DTO/PhotoCreate/PhotoPartnerDTO.php @@ -0,0 +1,30 @@ +photo; + } +} diff --git a/app/DTO/PhotoCreate/StandaloneDTO.php b/app/DTO/PhotoCreate/StandaloneDTO.php new file mode 100644 index 00000000000..50ccc1d32e3 --- /dev/null +++ b/app/DTO/PhotoCreate/StandaloneDTO.php @@ -0,0 +1,67 @@ +sourceFile, + is_starred: $initDTO->is_starred, + exifInfo: $initDTO->exifInfo, + album: $initDTO->album, + intendedOwnerId: $initDTO->intendedOwnerId, + shallImportViaSymlink: $initDTO->importMode->shallImportViaSymlink, + shallDeleteImported: $initDTO->importMode->shallDeleteImported, + ); + } + + public function getPhoto(): Photo + { + return $this->photo; + } +} diff --git a/app/DTO/PhotoCreate/VideoPartnerDTO.php b/app/DTO/PhotoCreate/VideoPartnerDTO.php new file mode 100644 index 00000000000..2b8fd768a05 --- /dev/null +++ b/app/DTO/PhotoCreate/VideoPartnerDTO.php @@ -0,0 +1,44 @@ +photo; + } + + public static function ofInit(InitDTO $initDTO): VideoPartnerDTO + { + return new VideoPartnerDTO( + videoFile: $initDTO->sourceFile, + photo: $initDTO->livePartner, + shallImportViaSymlink: $initDTO->importMode->shallImportViaSymlink, + shallDeleteImported: $initDTO->importMode->shallDeleteImported, + ); + } +} diff --git a/app/DTO/PhotoSortingCriterion.php b/app/DTO/PhotoSortingCriterion.php new file mode 100644 index 00000000000..ca8d4ced897 --- /dev/null +++ b/app/DTO/PhotoSortingCriterion.php @@ -0,0 +1,35 @@ +toColumnSortingType(); + + $orderSorting = Configs::getValueAsEnum('sorting_photos_order', OrderSortingType::class); + + return new self( + $columnSorting ?? ColumnSortingType::CREATED_AT, + $orderSorting ?? OrderSortingType::ASC + ); + } +} diff --git a/app/DTO/SortingCriterion.php b/app/DTO/SortingCriterion.php new file mode 100644 index 00000000000..b18c6f9553e --- /dev/null +++ b/app/DTO/SortingCriterion.php @@ -0,0 +1,31 @@ + $smart_albums + * @param Collection $tag_albums + * @param Collection $albums + * @param Collection|null $shared_albums + */ + public function __construct( + public Collection $smart_albums, + public Collection $tag_albums, + public Collection $albums, + public ?Collection $shared_albums = null, + ) { + } +} diff --git a/app/DTO/Version.php b/app/DTO/Version.php new file mode 100644 index 00000000000..376bce7d04c --- /dev/null +++ b/app/DTO/Version.php @@ -0,0 +1,104 @@ + $version || $version > 999999) { + throw new LycheeInvalidArgumentException('unexpected version value'); + } + + return new self(intdiv($version, 10000), intdiv($version % 10000, 100), $version % 100); + } + + /** + * Converts a string into a three-part version. + * + * The method supports two different formats. + * If the string contains exactly two dots (.) as separators, then the + * method splits the string at the dots and uses the resulting + * components as major, minor and patch level. + * Otherwise, the string must have exactly 6 characters. + * The string is split into three components with 2 characters each. + * Other formats are not supported. + * + * @param string $version + * + * @return self + * + * @throws LycheeInvalidArgumentException + */ + public static function createFromString(string $version): self + { + $version = trim($version); + $exploded = explode('.', $version); + if (count($exploded) === 3) { + return new self(intval($exploded[0]), intval($exploded[1]), intval($exploded[2])); + } + if (strlen($version) === 5) { + $version = '0' . $version; + } + if (strlen($version) === 6) { + $exploded = str_split($version, 2); + + return new self(intval($exploded[0]), intval($exploded[1]), intval($exploded[2])); + } + throw new LycheeInvalidArgumentException('unexpected version value'); + } + + /** + * Converts the version into an integer which is suitable for comparison. + * + * The minor and patch level always have two digits, i.e. the version + * "4.3.1" is returned as 40301. + * + * @return int + */ + public function toInteger(): int + { + return 10000 * $this->major + 100 * $this->minor + $this->patch; + } + + public function toString(): string + { + return $this->__toString(); + } + + public function __toString(): string + { + return $this->major . '.' . $this->minor . '.' . $this->patch; + } +} diff --git a/app/Eloquent/FixedQueryBuilder.php b/app/Eloquent/FixedQueryBuilder.php new file mode 100644 index 00000000000..24b7fa288df --- /dev/null +++ b/app/Eloquent/FixedQueryBuilder.php @@ -0,0 +1,62 @@ +` and `Builder` + * has a `@mixin` for `Illuminate\Database\Query\Builder`, PhpStan does not + * consider this mixin as part of this class, because this mixin is treated + * in a special way by the Larastan extension, but Larastan does not know + * anything about our `FixedQueryBuilder`. + * For this reason me must repeat all the methods defined by + * `Illuminate\Database\Query\Builder`. + * Moreover, many of these methods return `$this`, which is why we cannot use + * `@mixin` as otherwise the return type does not match. + * See this [PhpStan Playground](https://phpstan.org/r/f3415be1-fe6b-43fb-8be1-f712cd3e24b1) + * for an explanation what happens. + * + * @template TModelClass of \Illuminate\Database\Eloquent\Model + * + * @method $this addSelect(array|mixed $column) + * @method int count(string $columns = '*') + * @method $this from(\Closure|\Illuminate\Database\Query\Builder|string $table, ?string $as = null) + * @method $this join(string $table, \Closure|string $first, ?string $operator = null, ?string $second = null, string $type = 'inner', bool $where = false) + * @method $this limit(int $value) + * @method $this offset(int $value) + * @method $this select(array|mixed $columns = ['*']) + * @method $this join(string $table, \Closure|string $first, ?string $operator = null, $second = null, $type = 'inner', $where = false) + * @method $this joinSub(\Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder|string $query, string $as, \Closure|string $first, ?string $operator = null, $second = null, $type = 'inner', $where = false) + * @method $this leftJoin(string $table, \Closure|string $first, ?string $operator = null, $second = null, $type = 'inner', $where = false) + * @method $this take(int $value) + * @method void truncate() + * @method $this whereColumn(string|array $first, ?string $operator = null, ?string $second = null, string $boolean = 'and') + * @method $this whereExists(Closure $callback, string $boolean = 'and', bool $not = false) + * @method $this whereIn(string $column, mixed $values, string $boolean = 'and', bool $not = false) + * @method $this whereNotExists(Closure $callback, string $boolean = 'and') + * @method $this whereNotNull(string|array $columns, string $boolean = 'and') + * @method $this whereNotIn(string $column, mixed $values, string $boolean = 'and') + * @method $this whereNull(string|array $columns, string $boolean = 'and', bool $not = false) + * @method $this orderByDesc($column) + * + * @extends Builder + */ +class FixedQueryBuilder extends Builder +{ + /** @phpstan-use FixedQueryBuilderTrait */ + use FixedQueryBuilderTrait; +} diff --git a/app/Eloquent/FixedQueryBuilderTrait.php b/app/Eloquent/FixedQueryBuilderTrait.php new file mode 100644 index 00000000000..097f19d02ef --- /dev/null +++ b/app/Eloquent/FixedQueryBuilderTrait.php @@ -0,0 +1,294 @@ +where(...)->orderBy(...)->get(); + * } catch (\Throwable $e) { + * throw new QueryBuilderException($e); + * } + * + * or use a decorator like this trait. + * In order to keep our actual "business logic" clean from work-arounds for + * awkward design decisions of Eloquent, we use this decorator. + * Hopefully, the necessity for this trait will vanish in the future after + * Eloquent has adopted to proper error handling. + * See [Laravel Discussion #40020](https://github.com/laravel/framework/discussions/40020). + * + * _Note:_ This trait does not wrap every method of the underlying + * {@link \Illuminate\Database\Eloquent\Builder}; only those which are used + * by Lychee. + * + * @template TModelClass of \Illuminate\Database\Eloquent\Model + */ +trait FixedQueryBuilderTrait +{ + /** + * Add a basic where clause to the query. + * + * @param \Closure|string|array|Expression $column + * @param mixed $operator + * @param mixed $value + * @param string $boolean + * + * @return $this + * + * @throws QueryBuilderException + */ + public function where($column, $operator = null, $value = null, $boolean = 'and'): static + { + try { + // @phpstan-ignore-next-line; due to the Larastan rules set PhpStan falsely assumes we are calling a static method + return parent::where($column, $operator, $value, $boolean); + // @codeCoverageIgnoreStart + } catch (\Throwable $e) { + throw new QueryBuilderException($e); + } + // @codeCoverageIgnoreEnd + } + + /** + * Add a "where in" clause to the query. + * + * @param string $column + * @param mixed $values + * @param string $boolean + * @param bool $not + * + * @return $this + * + * @throws QueryBuilderException + */ + public function whereIn($column, $values, $boolean = 'and', $not = false): static + { + try { + // @phpstan-ignore-next-line; due to the Larastan rules set PhpStan falsely assumes we are calling a static method + return parent::whereIn($column, $values, $boolean, $not); + // @codeCoverageIgnoreStart + } catch (\Throwable $e) { + throw new QueryBuilderException($e); + } + // @codeCoverageIgnoreEnd + } + + /** + * Add a "where not in" clause to the query. + * + * @param string $column + * @param mixed $values + * @param string $boolean + * + * @return $this + * + * @throws QueryBuilderException + */ + public function whereNotIn($column, $values, $boolean = 'and'): static + { + try { + // @phpstan-ignore-next-line; due to the Larastan rules set PhpStan falsely assumes we are calling a static method + return parent::whereNotIn($column, $values, $boolean); + // @codeCoverageIgnoreStart + } catch (\Throwable $e) { + throw new QueryBuilderException($e); + } + // @codeCoverageIgnoreEnd + } + + /** + * Set the columns to be selected. + * + * @param array|mixed $columns + * + * @return $this + * + * @throws QueryBuilderException + */ + public function select($columns = ['*']): static + { + try { + // @phpstan-ignore-next-line; due to the Larastan rules set PhpStan falsely assumes we are calling a static method + return parent::select($columns); + // @codeCoverageIgnoreStart + } catch (\Throwable $e) { + throw new QueryBuilderException($e); + } + // @codeCoverageIgnoreEnd + } + + /** + * Add a join clause to the query. + * + * @param string $table + * @param \Closure|string $first + * @param string|null $operator + * @param string|null $second + * @param string $type + * @param bool $where + * + * @return $this + * + * @throws QueryBuilderException + */ + public function join($table, $first, $operator = null, $second = null, $type = 'inner', $where = false): static + { + try { + // @phpstan-ignore-next-line; due to the Larastan rules set PhpStan falsely assumes we are calling a static method + return parent::join($table, $first, $operator, $second, $type, $where); + // @codeCoverageIgnoreStart + } catch (\Throwable $e) { + throw new QueryBuilderException($e); + } + // @codeCoverageIgnoreEnd + } + + /** + * Add a subquery join clause to the query. + * + * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder|string $query + * @param string $as + * @param \Closure|string $first + * @param string|null $operator + * @param string|null $second + * @param string $type + * @param bool $where + * + * @return $this + * + * @throws \InvalidArgumentException + */ + public function joinSub($query, $as, $first, $operator = null, $second = null, $type = 'inner', $where = false): static + { + try { + // @phpstan-ignore-next-line; due to the Larastan rules set PhpStan falsely assumes we are calling a static method + return parent::joinSub($query, $as, $first, $operator, $second, $type, $where); + // @codeCoverageIgnoreStart + } catch (\Throwable $e) { + throw new QueryBuilderException($e); + } + // @codeCoverageIgnoreEnd + } + + /** + * Add a left join to the query. + * + * @param string $table + * @param \Closure|string $first + * @param string|null $operator + * @param string|null $second + * + * @return $this + * + * @throws QueryBuilderException + */ + public function leftJoin($table, $first, $operator = null, $second = null): static + { + try { + // @phpstan-ignore-next-line; due to the Larastan rules set PhpStan falsely assumes we are calling a static method + return parent::leftJoin($table, $first, $operator, $second); + // @codeCoverageIgnoreStart + } catch (\Throwable $e) { + throw new QueryBuilderException($e); + } + // @codeCoverageIgnoreEnd + } + + /** + * Add an "order by" clause to the query. + * + * @param \Closure|Builder|BaseBuilder|Expression|string $column + * @param string $direction + * + * @return $this + * + * @throws QueryBuilderException + */ + public function orderBy($column, $direction = 'asc'): static + { + try { + // The parent class is Eloquent\Builder and Eloquent\Builder::orderBy() + // accepts exactly the types for columns as listed above + // (see source code of the framework). + // However, the buggy larastan ruleset lies to PhpStan about the + // types and hence we must ignore this line. + // + // @phpstan-ignore-next-line + return parent::orderBy($column, $direction); + // @codeCoverageIgnoreStart + } catch (\Throwable $e) { + throw new QueryBuilderException($e); + } + // @codeCoverageIgnoreEnd + } + + /** + * Add a new select column to the query. + * + * @param array|mixed $column + * + * @return $this + * + * @throws QueryBuilderException + */ + public function addSelect($column): static + { + try { + // @phpstan-ignore-next-line; due to the Larastan rules set PhpStan falsely assumes we are calling a static method + return parent::addSelect($column); + // @codeCoverageIgnoreStart + } catch (\Throwable $e) { + throw new QueryBuilderException($e); + } + // @codeCoverageIgnoreEnd + } + + /** + * Add an "or where" clause to the query. + * + * @param \Closure|string|array|Expression $column + * @param mixed $operator + * @param mixed $value + * + * @return $this + * + * @throws QueryBuilderException + */ + public function orWhere($column, $operator = null, $value = null): static + { + try { + // @phpstan-ignore-next-line; due to the Larastan rules set PhpStan falsely assumes we are calling a static method + return parent::orWhere($column, $operator, $value); + // @codeCoverageIgnoreStart + } catch (\Throwable $e) { + throw new QueryBuilderException($e); + } + // @codeCoverageIgnoreEnd + } +} diff --git a/app/Enum/AlbumDecorationOrientation.php b/app/Enum/AlbumDecorationOrientation.php new file mode 100644 index 00000000000..fe5dec5ab30 --- /dev/null +++ b/app/Enum/AlbumDecorationOrientation.php @@ -0,0 +1,22 @@ + AspectRatioCSSType::aspect5by4, + self::aspect4by5 => AspectRatioCSSType::aspect4by5, + self::aspect3by2 => AspectRatioCSSType::aspect3by2, + self::aspect1by1 => AspectRatioCSSType::aspect1by1, + self::aspect2by3 => AspectRatioCSSType::aspect2by3, + self::aspect1byx9 => AspectRatioCSSType::aspect1byx9, + }; + } +} diff --git a/app/Enum/ColumnSortingAlbumType.php b/app/Enum/ColumnSortingAlbumType.php new file mode 100644 index 00000000000..0c289b43d07 --- /dev/null +++ b/app/Enum/ColumnSortingAlbumType.php @@ -0,0 +1,35 @@ +value); + } +} diff --git a/app/Enum/ColumnSortingPhotoType.php b/app/Enum/ColumnSortingPhotoType.php new file mode 100644 index 00000000000..4ac41b09beb --- /dev/null +++ b/app/Enum/ColumnSortingPhotoType.php @@ -0,0 +1,40 @@ +value); + } +} diff --git a/app/Enum/ColumnSortingType.php b/app/Enum/ColumnSortingType.php new file mode 100644 index 00000000000..452d7c3b53d --- /dev/null +++ b/app/Enum/ColumnSortingType.php @@ -0,0 +1,31 @@ + SizeVariantType::THUMB, + self::THUMB2X => SizeVariantType::THUMB2X, + self::SMALL => SizeVariantType::SMALL, + self::SMALL2X => SizeVariantType::SMALL2X, + self::MEDIUM => SizeVariantType::MEDIUM, + self::MEDIUM2X => SizeVariantType::MEDIUM2X, + self::ORIGINAL => SizeVariantType::ORIGINAL, + self::LIVEPHOTOVIDEO => null, + }; + } +} \ No newline at end of file diff --git a/app/Enum/FileStatus.php b/app/Enum/FileStatus.php new file mode 100644 index 00000000000..66fe6fd1b1d --- /dev/null +++ b/app/Enum/FileStatus.php @@ -0,0 +1,19 @@ + 'ready', + self::SUCCESS => 'success', + self::FAILURE => 'failure', + self::STARTED => 'started', + }; + } +} diff --git a/app/Enum/LicenseType.php b/app/Enum/LicenseType.php new file mode 100644 index 00000000000..89a92a88d0e --- /dev/null +++ b/app/Enum/LicenseType.php @@ -0,0 +1,107 @@ + + */ + public static function localized(): array + { + return [ + self::NONE->value => 'None', + self::RESERVED->value => __('gallery.album_reserved'), + self::CC0->value => 'CC0 - Public Domain', + self::CC_BY_1_0->value => 'CC Attribution 1.0', + self::CC_BY_2_0->value => 'CC Attribution 2.0', + self::CC_BY_2_5->value => 'CC Attribution 2.5', + self::CC_BY_3_0->value => 'CC Attribution 3.0', + self::CC_BY_4_0->value => 'CC Attribution 4.0', + self::CC_BY_ND_1_0->value => 'CC Attribution-NoDerivatives 1.0', + self::CC_BY_ND_2_0->value => 'CC Attribution-NoDerivatives 2.0', + self::CC_BY_ND_2_5->value => 'CC Attribution-NoDerivatives 2.5', + self::CC_BY_ND_3_0->value => 'CC Attribution-NoDerivatives 3.0', + self::CC_BY_ND_4_0->value => 'CC Attribution-NoDerivatives 4.0', + self::CC_BY_SA_1_0->value => 'CC Attribution-ShareAlike 1.0', + self::CC_BY_SA_2_0->value => 'CC Attribution-ShareAlike 2.0', + self::CC_BY_SA_2_5->value => 'CC Attribution-ShareAlike 2.5', + self::CC_BY_SA_3_0->value => 'CC Attribution-ShareAlike 3.0', + self::CC_BY_SA_4_0->value => 'CC Attribution-ShareAlike 4.0', + self::CC_BY_NC_1_0->value => 'CC Attribution-NonCommercial 1.0', + self::CC_BY_NC_2_0->value => 'CC Attribution-NonCommercial 2.0', + self::CC_BY_NC_2_5->value => 'CC Attribution-NonCommercial 2.5', + self::CC_BY_NC_3_0->value => 'CC Attribution-NonCommercial 3.0', + self::CC_BY_NC_4_0->value => 'CC Attribution-NonCommercial 4.0', + self::CC_BY_NC_ND_1_0->value => 'CC Attribution-NonCommercial-NoDerivatives 1.0', + self::CC_BY_NC_ND_2_0->value => 'CC Attribution-NonCommercial-NoDerivatives 2.0', + self::CC_BY_NC_ND_2_5->value => 'CC Attribution-NonCommercial-NoDerivatives 2.5', + self::CC_BY_NC_ND_3_0->value => 'CC Attribution-NonCommercial-NoDerivatives 3.0', + self::CC_BY_NC_ND_4_0->value => 'CC Attribution-NonCommercial-NoDerivatives 4.0', + self::CC_BY_NC_SA_1_0->value => 'CC Attribution-NonCommercial-ShareAlike 1.0', + self::CC_BY_NC_SA_2_0->value => 'CC Attribution-NonCommercial-ShareAlike 2.0', + self::CC_BY_NC_SA_2_5->value => 'CC Attribution-NonCommercial-ShareAlike 2.5', + self::CC_BY_NC_SA_3_0->value => 'CC Attribution-NonCommercial-ShareAlike 3.0', + self::CC_BY_NC_SA_4_0->value => 'CC Attribution-NonCommercial-ShareAlike 4.0', + ]; + } + + /** + * Return the localization string of current. + * + * @return string + */ + public function localization(): string + { + return self::localized()[$this->value]; + } +} diff --git a/app/Enum/MapProviders.php b/app/Enum/MapProviders.php new file mode 100644 index 00000000000..eb0577fc895 --- /dev/null +++ b/app/Enum/MapProviders.php @@ -0,0 +1,44 @@ + 'https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}{r}.png', + self::OpenStreetMapOrg => 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + self::OpenStreetMapDe => 'https://tile.openstreetmap.de/{z}/{x}/{y}.png ', + self::OpenStreetMapFr => 'https://{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png ', + self::RRZE => 'https://{s}.osm.rrze.fau.de/osmhd/{z}/{x}/{y}.png', + }; + } + + public function getAtributionHtml(): string + { + return match ($this) { + self::Wikimedia => 'Wikimedia', + self::OpenStreetMapOrg => '© ' . __('gallery.map.osm_contributors') . '', + self::OpenStreetMapDe => '© ' . __('gallery.map.osm_contributors') . '', + self::OpenStreetMapFr => '© ' . __('gallery.map.osm_contributors') . '', + self::RRZE => '© ' . __('gallery.map.osm_contributors') . '', + }; + } +} diff --git a/app/Enum/MessageType.php b/app/Enum/MessageType.php new file mode 100644 index 00000000000..5cc13f47849 --- /dev/null +++ b/app/Enum/MessageType.php @@ -0,0 +1,16 @@ + 'placeholder', + self::THUMB => 'thumb', + self::THUMB2X => 'thumb2x', + self::SMALL => 'small', + self::SMALL2X => 'small2x', + self::MEDIUM => 'medium', + self::MEDIUM2X => 'medium2x', + self::ORIGINAL => 'original', + }; + } + + /** + * Given a sizeVariantType return the localized name. + * + * @return string + */ + public function localization(): string + { + return match ($this) { + self::PLACEHOLDER => __('gallery.placeholder'), + self::THUMB => __('gallery.thumb'), + self::THUMB2X => __('gallery.thumb_hidpi'), + self::SMALL => __('gallery.small'), + self::SMALL2X => __('gallery.small_hidpi'), + self::MEDIUM => __('gallery.medium'), + self::MEDIUM2X => __('gallery.medium_hidpi'), + self::ORIGINAL => __('gallery.original'), + }; + } +} \ No newline at end of file diff --git a/app/Enum/SmartAlbumType.php b/app/Enum/SmartAlbumType.php new file mode 100644 index 00000000000..40437e54b17 --- /dev/null +++ b/app/Enum/SmartAlbumType.php @@ -0,0 +1,40 @@ + Configs::getValueAsBool('enable_unsorted'), + self::STARRED => Configs::getValueAsBool('enable_starred'), + self::RECENT => Configs::getValueAsBool('enable_recent'), + self::ON_THIS_DAY => Configs::getValueAsBool('enable_on_this_day'), + }; + } +} \ No newline at end of file diff --git a/app/Enum/StorageDiskType.php b/app/Enum/StorageDiskType.php new file mode 100644 index 00000000000..00ccb634b73 --- /dev/null +++ b/app/Enum/StorageDiskType.php @@ -0,0 +1,15 @@ + 1, 'Failed' => 2, 'Success' => 3] + */ +trait DecorateBackedEnum +{ + /** + * Returns a list of name covered by the enum. + * + * @return string[] + */ + public static function names(): array + { + return array_column(self::cases(), 'name'); + } + + /** + * Returns a list of values covered by the enum. + * + * @return (string|int)[] + */ + public static function values(): array + { + return array_column(self::cases(), 'value'); + } + + /** + * Returns an associative array [name => value]. + * + * @return array + */ + public static function array(): array + { + return array_combine(self::names(), self::values()); + } +} diff --git a/app/Enum/UpdateStatus.php b/app/Enum/UpdateStatus.php new file mode 100644 index 00000000000..2b328c075df --- /dev/null +++ b/app/Enum/UpdateStatus.php @@ -0,0 +1,24 @@ +getCode()) ? $previous->getCode() : 0; + } + parent::__construct($httpStatusCode, $message, $previous, [], $code ?? 0); + } +} diff --git a/app/Exceptions/ComposerNotAvailableException.php b/app/Exceptions/ComposerNotAvailableException.php deleted file mode 100644 index 41f6a3c966c..00000000000 --- a/app/Exceptions/ComposerNotAvailableException.php +++ /dev/null @@ -1,16 +0,0 @@ - + */ + public const EXCEPTION2SEVERITY = [ + HttpHoneyPotException::class => SeverityType::NOTICE, // In theory this is a 404, but because it touches honey we don't really care. + PhotoResyncedException::class => SeverityType::WARNING, + PhotoSkippedException::class => SeverityType::WARNING, + ImportCancelledException::class => SeverityType::NOTICE, + ConfigurationException::class => SeverityType::NOTICE, + LocationDecodingFailed::class => SeverityType::ERROR, + ]; + + /** + * {@inheritDoc} */ protected $dontReport = [ - DecryptException::class, + TokenMismatchException::class, + SessionExpiredException::class, + NoWriteAccessOnLogsExceptions::class, + ViteException::class, + ]; + + /** @var array> */ + protected $exception_checks = [ + NoEncryptionKey::class, + AccessDBDenied::class, + InstallationHandler::class, + AdminSetterHandler::class, + MigrationHandler::class, + ViteManifestNotFoundHandler::class, + LegacyIdExceptionHandler::class, ]; + /** @var array> */ + protected $force_exception_to_http = [ + ViteException::class, + ]; + + /** + * {@inheritDoc} + */ + protected $internalDontReport = []; + + /** @var string the application path */ + protected string $appPath; + + public function __construct(Container $container) + { + parent::__construct($container); + // Cache the application path to avoid multiple function calls + // and potential exceptions in `report()` + $this->appPath = app_path(); + } + /** - * A list of the inputs that are never flashed for validation exceptions. + * Maps an exception to something else. + * + * The method is called before {@link Handler::report()}, + * {@link Handler::renderHttpException()}, + * {@link Handler::convertExceptionToArray()}. + * + * We overwrite this method to wrap the following exception into proper + * HTTP exceptions which masquerades them and avoids that the framework + * handles them in special ways: + * + * - {@link TokenMismatchException} + * - {@link AuthenticationException} + * + * Note, that the default Laravel handler actually replaces exceptions by + * other exception at **three** places. + * The method {@link ExceptionHandler::render()} is the entry point for + * exception handling. + * This method calls + * + * - {@link ExceptionHandler::mapException()} + * - {@link ExceptionHandler::prepareException()} + * + * in that order which both replace exceptions. + * Finally, the parent method {@link ExceptionHandler::render()} also + * replaces some exceptions, too. + * We hook into the earliest of the three methods, i.e. `mapException`. + * + * **`TokenMismatchException`** + * + * Per default, the framework eventually replaces + * {@link TokenMismatchException} by generic HTTP exception in + * {@link ExceptionHandler::prepareException()}. + * We want to keep it more specific in order to detect this kind of + * exception more easily in the frontend. * - * @var array + * **`AuthenticationException`** + * + * Per default, the framework replaces {@link AuthenticationException} + * by a redirection to the route `login` in + * {@link ExceptionHandler::render()}. + * This is problematic for various reasons: + * + * 1. We do not really have a dedicated login page to which users + * could be redirected. + * Our login dialog is implemented in JavaScript. + * Surely, we could use the main page `/gallery` as a redirection + * target, but it would probably confuse people to be redirected there + * without obvious reason. + * 2. In theory, all requests for content type `text/html` should always + * succeed. + * Any interaction which might trigger an authorization error is done + * via JavaScript and JSON requests. + * If an authorization error occurs for an HTML request, this indicates + * a programming error. + * In this case we want to be informed about that, and we want users + * to tell us so, instead of suppressing the error by silent + * redirection (cp. previous point). + * Moreover, such an event always implies that the backend and the + * frontend are out-of-sync with respect to the authentication state. + * The backend considers the session to be unauthenticated while the + * frontend considers the user still to be authenticated. + * In particular, users could not even login again, even if the knew + * what was going on, because the frontend did not provide the option + * to do so. + * Hence, we are in an unrecoverable situation anyway. + * 3. For JSON requests, we want the structure of the JSON response to + * match our error reporting scheme as defined by + * {@link Handler::convertExceptionToArray} such that the frontend + * can properly interpret and display it. + * By default, the framework would return a JSON response whose format + * is unique to the {@link AuthenticationException}. + * + * @param \Throwable $e + * + * @return \Throwable */ - protected $dontFlash = [ - 'password', - 'password_confirmation', - ]; + protected function mapException(\Throwable $e): \Throwable + { + if ($e instanceof TokenMismatchException) { + return new SessionExpiredException(SessionExpiredException::DEFAULT_MESSAGE, $e); + } + + if ($e instanceof AuthenticationException) { + return new UnauthenticatedException(UnauthenticatedException::DEFAULT_MESSAGE, $e); + } + + return parent::mapException($e); + } /** - * Report or log an exception. + * Prepare a response for the given exception. * - * @param Throwable $exception + * This method is called by the framework, _after_ the framework has + * decided that the client expects a HTML response, but _before_ the + * actual work horse {@link Handler::renderHttpException} is called. * - * @return void + * This method is 99% identical to the parent method except for a tiny + * bug fix which adds the original exception to the encapsulating + * `HttpException`. + * + * @param Request $request + * @param \Throwable $e * - * @throws Throwable + * @return RedirectResponse|Response + * + * @throws BindingResolutionException + * @throws \InvalidArgumentException + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface */ - public function report(Throwable $exception) + protected function prepareResponse($request, \Throwable $e): RedirectResponse|Response { - // @codeCoverageIgnoreStart - parent::report($exception); - // @codeCoverageIgnoreEnd + if (!$this->isHttpException($e)) { + if ($this->mustForceToHttpException($e) || config('app.debug') !== true) { + $e = new HttpException(500, $e->getMessage(), $e); + } else { + return $this->toIlluminateResponse($this->convertExceptionToResponse($e), $e); + } + } + + /** @var HttpExceptionInterface $e */ + return $this->toIlluminateResponse($this->renderHttpException($e), $e); } /** - * Render an exception into an HTTP response. + * Check if the exception must be converted to HttpException. * - * @param \Illuminate\Http\Request $request - * @param Throwable $exception + * @param \Throwable $e to check * - * @return \Illuminate\Http\Response + * @return bool true if conversion is required */ - public function render($request, Throwable $exception) + protected function mustForceToHttpException(\Throwable $e): bool { - $checks = []; - $checks[] = new NoEncryptionKey(); - $checks[] = new InvalidPayload(); - $checks[] = new AccessDBDenied(); - $checks[] = new ApplyComposer(); - $checks[] = new ModelNotFound(); + // This loop order is more efficient: + // We take the first layer of the exception, check if match any of the forced conversion + // then the next layer etc... + do { + foreach ($this->force_exception_to_http as $exception) { + if ($e instanceof $exception) { + return true; + } + } + } while ($e = $e->getPrevious()); + + return false; + } + + /** + * Renders the given HttpException into HTML. + * + * This method is called by the framework if + * 1. `config('app.debug')` is not set, i.e. the application is not in debug mode + * 2. the client expects an HTML response + * + * **Attention:** + * This method is a misnomer caused by the framework. + * The framework provides two methods `renderHttpException` and + * `renderJsonException` with the former being called if the client + * expects HTML. + * Hence, the method should rather be named `renderHtmlException`. + * That current name of the method, if meant as an antonym to + * `renderJsonException` is obviously nonsense as JSON is also transported + * over HTTP. + * + * @param HttpExceptionInterface $e + * + * @return SymfonyResponse + * + * @noinspection PhpDocMissingThrowsInspection + * @noinspection PhpUnhandledExceptionInspection + */ + protected function renderHttpException(HttpExceptionInterface $e): SymfonyResponse + { + // If we are in debug mode, we use the internal method of the parent + // method to render a useful response with backtrace, etc., depending + // on the available extensions (i.e. Whoops, Symfony renderer, etc.) + // If we are in non-debug mode, we render our own template that + // matches Lychee's style and only contains rudimentary information. + $defaultResponse = config('app.debug') === true ? + $this->convertExceptionToResponse($e) : + response()->view('error.error', [ + 'code' => $e->getStatusCode(), + 'type' => class_basename($e), + 'message' => $e->getMessage(), + ], $e->getStatusCode(), $e->getHeaders()); + + // We check, if any of our special handlers wants to do something. + + /** @var HttpExceptionHandler[] $checks */ + $checks = collect($this->exception_checks) + ->map(fn ($c) => new $c()) + ->toArray(); foreach ($checks as $check) { - if ($check->check($request, $exception)) { - // @codeCoverageIgnoreStart - return $check->go(); - // @codeCoverageIgnoreEnd + if ($check->check($e)) { + return $check->renderHttpException($defaultResponse, $e); } } - // @codeCoverageIgnoreStart - return parent::render($request, $exception); - // @codeCoverageIgnoreEnd + return $defaultResponse; + } + + /** + * Converts the given exception to an array. + * + * The result only includes details about the exception, if the + * application is in debug mode. + * Identical to + * {@link \Illuminate\Foundation\Exceptions\Handler::convertExceptionToAray()} + * but recursively adds the previous exceptions, too. + * + * @param \Throwable $e + * + * @return array + */ + protected function convertExceptionToArray(\Throwable $e): array + { + try { + // debub mode. + if (config('app.debug') === true) { + return $this->convertDebugExceptionToArray($e); + } + + // normal use + return [ + 'message' => $this->isHttpException($e) ? $e->getMessage() : 'Server Error', + 'exception' => class_basename($e), + ]; + } catch (\Throwable) { + return []; + } + } + + /** + * Converts the given exception to an array. + * + * The result only includes details about the exception, if the + * application is in debug mode. + * Identical to + * {@link \Illuminate\Foundation\Exceptions\Handler::convertExceptionToAray()} + * but recursively adds the previous exceptions, too. + * + * @param \Throwable|null $e + * + * @return ($e is null ? null : array) + */ + private function convertDebugExceptionToArray(\Throwable|null $e): array|null + { + if ($e === null) { + return null; + } + + $previous_exception = $this->convertDebugExceptionToArray($e->getPrevious()); + + return [ + 'message' => $e->getMessage(), + 'exception' => get_class($e), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'trace' => collect($e->getTrace())->map(function ($trace) { + return Arr::except($trace, ['args']); + })->all(), + 'previous_exception' => $previous_exception, + ]; + } + + /** + * Called by the framework if an exception occurs for logging purposes. + * + * As we have our own home-brewed logging mechanism via {@link Logs} + * which does not implement {@link \Psr\Log\LoggerInterface} and does + * not register with the service container, we override the method. + */ + public function report(\Throwable $e): void + { + $e = $this->mapException($e); + + if ($this->shouldntReport($e)) { + return; + } + + // We use the severity of the first exception for all subsequent + // exceptions, because a causing exception should never be reported + // with a higher severity than the eventual exception + $severity = self::getLogSeverity($e); + + $msg = ''; + do { + $cause = $this->findCause($e); + if (count($cause) === 2) { + $msg_ = $cause[1]->getMethodBeautified() . ':' . $cause[1]->getLine() . ' ' . $e->getMessage() . '; caused by'; + $msg = $msg_ . PHP_EOL . $msg; + } + + if ($e->getPrevious() !== null) { + $msg_ = $cause[0]->getMethodBeautified() . ':' . $cause[0]->getLine() . ' ' . $e->getMessage() . '; caused by'; + } else { + $msg_ = $cause[0]->getMethodBeautified() . ':' . $cause[0]->getLine() . ' ' . $e->getMessage(); + } + $msg = $msg_ . PHP_EOL . $msg; + } while ($e = $e->getPrevious()); + try { + Log::log($severity->value, $msg); + /** @phpstan-ignore-next-line // Yes it is thrown, trust me.... */ + } catch (\UnexpectedValueException $e2) { + throw new NoWriteAccessOnLogsExceptions($e2); + // abort(507, 'Could not write in the logs. Check that storage/logs/ and containing files have proper permissions.'); + } + } + + /** + * @param \Throwable $e + * + * @return SeverityType + */ + public static function getLogSeverity(\Throwable $e): SeverityType + { + return array_key_exists(get_class($e), self::EXCEPTION2SEVERITY) ? + self::EXCEPTION2SEVERITY[get_class($e)] : + SeverityType::ERROR; + } + + /** + * Returns up to two interesting backtrace entries which might help to + * pinpoint the cause of an exception. + * + * The first backtrace entry always points the most inner function which + * originally has thrown the exception. + * The can point to a file of the Lychee source code, but may also point + * to a file which is part of the PHP engine or one of the libraries. + * + * The second backtrace entry is optional and - if it included - always + * points to the most inner method of the Lychee source code on the + * stack which eventually has led to the exception. + * + * Laravel's backtraces are usually hundreds of frames deep with a lot + * of anonymous closures in between. + * Printing everything only litters the log with needless entries and + * won't help to keep track of what really happened. + * The two entries above have been chosen to be the most interesting ones. + * The first directly points to the failing line, the second one (if not + * identical to the first) indicates the last line of Lychee code which + * has been passed before the exception occurred. + * + * The standard backtrace reported by PHP is oddly strange. + * The attribute pair file/line on the one hand-side and class/function + * on the other hand-side of a standard PHP backtrace are off-by-one. + * The reported file/line of an entry of the backtrace don't refer to + * the position *inside* the reported class/function, but where + * class/method has been invoked. + * In particular, if one wants to know the position where the + * exception has been thrown, then one must not look up + * `backtrace[0]['file']` and `backtrace[0]['line']`, resp., but + * use `getFile` and `getLine()` of the exception. + * + * @param \Throwable $e + * + * @return BacktraceRecord[] + */ + private function findCause(\Throwable $e): array + { + $result = []; + $backtrace = $e->getTrace(); + + // Special rule for legacy PHP errors which are caught via + // `set_error_handler`, converted into an `ErrorException` and + // re-injected into the "modern" exception handling procedure + // + // The `set_error_handler` routine is special (thank you, PHP, for + // nothing) in two ways: (a) the `file` parameter is not filled + // (WTF?), and (b) the top entry of the backtrace points to + // `set_error_handler` which is not really part of the frame stack + // and does not provide any helpful information. + // + // For all who don't know the background: PHP provides two different + // approaches to indicate and handle error conditions which both + // interrupt the normal program flow: + // + // a) the engine error reporting system (legacy approach) + // b) exceptions (modern approach) + // + // The legacy approach is very similar to POSIX signal handling in the + // sense that one can register a static, global error handler and the + // PHP engine calls this handler whenever some error has occurred + // anywhere in the program. + // This error handler is not part of the normal program stack, but + // "lives" outside the normal program stack. + // When the error handler returns, the normal program flow and call + // stack is resumed. + // + // The modern approach uses exceptions which bubble up the call stack + // until they are caught and handled. + // + // In order to unify the error handling, the default `error_handler` + // nowadays wraps the reported error into a `\ErrorException` which + // then is thrown as if it was thrown by the method which caused the + // error in the first place. + // Unfortunately, this messes with the backtrace. + // + // Hopefully, the whole legacy PHP error reporting system will be + // nuked some day. + // PHP 8 made a great step into that direction + // (e.g., see https://wiki.php.net/rfc/consistent_type_errors, + // https://wiki.php.net/rfc/engine_warnings, + // https://wiki.php.net/rfc/lsp_errors). + // I really like the sentence about the dark ages of PHP ;-). + // + // And hopefully, this is the only special rule we need and nobody + // never ever misuses `\ErrorException` for "normal" exceptions. + $offset = $e instanceof \ErrorException ? 1 : 0; + + $file = $e->getFile(); + $line = $e->getLine(); + $class = $backtrace[$offset]['class'] ?? ''; + $function = $backtrace[$offset]['function'] ?? ''; + + // Always add the most inner frame + $result[] = new BacktraceRecord( + $file, + $line, + $class, + $function + ); + + // If this frame is part of our own code, we are done. + // We are also done, if there are no more frame on the backtrace + if (str_contains($file, $this->appPath) || count($backtrace) <= $offset + 1) { + return $result; + } + + // Try to find the most inner method of our own code + + // Normally, every backtrace entry must have a `file` and `line` + // attribute. + // But in view of the problems with legacy error handling, this + // must not be taken for granted. + // It seems that for certain low level methods which are part of + // the PHP engine (like `fopen`) this cannot be taken for granted. + // As this method must not fail, we are better safe than sorry. + $file = $backtrace[$offset]['file'] ?? ''; + $line = $backtrace[$offset]['line'] ?? 0; + + for ($idx = $offset + 1; $idx < count($backtrace); $idx++) { + $class = $backtrace[$idx]['class'] ?? ''; + $function = $backtrace[$idx]['function'] ?? ''; + // If this frame is part of our own code, we are done. + if (str_contains($file, $this->appPath)) { + break; + } + $file = $backtrace[$idx]['file'] ?? ''; + $line = $backtrace[$idx]['line'] ?? 0; + } + + $result[] = new BacktraceRecord( + $file, + $line, + $class, + $function + ); + + return $result; + } + + /** + * An exception-free replacement for Laravel's global `report` function. + * + * Normally, if one is inside a `catch`-block handling exceptions, one + * does not like to deal with another (new) exception. + * If `report` threw an exception, what should we do about it anyway? + * Report it? ;-) + * Even though the Laravel framework is very reluctant to document the + * exceptions thrown by their methods, one of the few Laravel methods + * which documents an exception surprisingly is + * {@link \Illuminate\Contracts\Debug\ExceptionHandler::report()}. + * Unfortunately, it is an unspecific `\Throwable`. + * Even worse, we know that our own implementation of that method + * {@link Handler::report()} does not even throw an exception. + * + * Here, we rectify this situation by provided an alternative function + * which does not throw another exception. + * This also makes the IDE happy again, because we don't use an + * exception throwing method inside an exception handler. + * + * @param \Throwable $e + * + * @return void + */ + public static function reportSafely(\Throwable $e): void + { + try { + report($e); + } catch (\Throwable) { + // Simply do nothing. + // If even exception reporting does not work, we are lost anyway. + // There is nothing we could do, except maybe die. + } } } diff --git a/app/Exceptions/Handlers/AccessDBDenied.php b/app/Exceptions/Handlers/AccessDBDenied.php index a526a8fbb99..2df446e33d2 100644 --- a/app/Exceptions/Handlers/AccessDBDenied.php +++ b/app/Exceptions/Handlers/AccessDBDenied.php @@ -1,37 +1,57 @@ getMessage(), 'Access denied') !== false); + do { + if ($e instanceof QueryException && str_contains($e->getMessage(), 'Access denied')) { + return true; + } + } while ($e = $e->getPrevious()); + + return false; } /** - * @return Response + * {@inheritDoc} */ - // @codeCoverageIgnoreStart - public function go() + public function renderHttpException(SymfonyResponse $defaultResponse, HttpException $e): SymfonyResponse { - return ToInstall::go(); - } + try { + $redirectResponse = ToInstall::go(); + $contentType = $defaultResponse->headers->get('Content-Type'); + if ($contentType !== null && $contentType !== '') { + $redirectResponse->headers->set('Content-Type', $contentType); + $content = $defaultResponse->getContent(); + $redirectResponse->setContent($content !== false ? $content : null); + } - // @codeCoverageIgnoreEnd + return $redirectResponse; + } catch (\Throwable) { + return $defaultResponse; + } + } } diff --git a/app/Exceptions/Handlers/AdminSetterHandler.php b/app/Exceptions/Handlers/AdminSetterHandler.php new file mode 100644 index 00000000000..9afdf3d8496 --- /dev/null +++ b/app/Exceptions/Handlers/AdminSetterHandler.php @@ -0,0 +1,75 @@ +toAdminSetter = true; + + return true; + } + if ($e instanceof AdminUserAlreadySetException) { + $this->toAdminSetter = false; + + return true; + } + } while ($e = $e->getPrevious()); + + return false; + } + + /** + * {@inheritDoc} + */ + public function renderHttpException(SymfonyResponse $defaultResponse, HttpException $e): SymfonyResponse + { + try { + if ($this->toAdminSetter) { + $redirectResponse = ToAdminSetter::go(); + $contentType = $defaultResponse->headers->get('Content-Type'); + if ($contentType !== null && $contentType !== '') { + $redirectResponse->headers->set('Content-Type', $contentType); + $content = $defaultResponse->getContent(); + $redirectResponse->setContent($content !== false ? $content : null); + } + + return $redirectResponse; + } else { + return $defaultResponse; + } + } catch (\Throwable) { + return $defaultResponse; + } + } +} diff --git a/app/Exceptions/Handlers/ApplyComposer.php b/app/Exceptions/Handlers/ApplyComposer.php deleted file mode 100644 index 0601b4b7932..00000000000 --- a/app/Exceptions/Handlers/ApplyComposer.php +++ /dev/null @@ -1,35 +0,0 @@ -getFile(), 'laravel/framework/src/Illuminate/Routing/Router.php') !== false); - } - - /** - * @return Response|View - */ - // @codeCoverageIgnoreStart - public function go() - { - return response()->view('error.error', ['code' => '500', 'message' => 'Missing dependency, please do: composer install --no-dev
(or use the release channel.)']); - } -} diff --git a/app/Exceptions/Handlers/InstallationHandler.php b/app/Exceptions/Handlers/InstallationHandler.php new file mode 100644 index 00000000000..61ada43bd07 --- /dev/null +++ b/app/Exceptions/Handlers/InstallationHandler.php @@ -0,0 +1,75 @@ +toInstall = true; + + return true; + } + if ($e instanceof InstallationAlreadyCompletedException) { + $this->toInstall = false; + + return true; + } + } while ($e = $e->getPrevious()); + + return false; + } + + /** + * {@inheritDoc} + */ + public function renderHttpException(SymfonyResponse $defaultResponse, HttpException $e): SymfonyResponse + { + try { + if ($this->toInstall) { + $redirectResponse = ToInstall::go(); + $contentType = $defaultResponse->headers->get('Content-Type'); + if ($contentType !== null && $contentType !== '') { + $redirectResponse->headers->set('Content-Type', $contentType); + $content = $defaultResponse->getContent(); + $redirectResponse->setContent($content !== false ? $content : null); + } + + return $redirectResponse; + } else { + return $defaultResponse; + } + } catch (\Throwable) { + return $defaultResponse; + } + } +} diff --git a/app/Exceptions/Handlers/InvalidPayload.php b/app/Exceptions/Handlers/InvalidPayload.php deleted file mode 100644 index 1950dfd0775..00000000000 --- a/app/Exceptions/Handlers/InvalidPayload.php +++ /dev/null @@ -1,35 +0,0 @@ -json(['error' => 'Session timed out'], 400); - } - - // @codeCoverageIgnoreEnd -} diff --git a/app/Exceptions/Handlers/LegacyIdExceptionHandler.php b/app/Exceptions/Handlers/LegacyIdExceptionHandler.php new file mode 100644 index 00000000000..35b8f0f5da9 --- /dev/null +++ b/app/Exceptions/Handlers/LegacyIdExceptionHandler.php @@ -0,0 +1,53 @@ +getMessage(), 'Numeric value out of range: 1264') + ) { + return true; + } + } while ($e = $e->getPrevious()); + + return false; + } + + /** + * {@inheritDoc} + */ + public function renderHttpException(SymfonyResponse $defaultResponse, HttpException $e): SymfonyResponse + { + return response()->view('error.error', [ + 'code' => $e->getStatusCode(), + 'type' => class_basename($e), + 'message' => 'SQLSTATE: Numeric value out of range: 1264 for column \'legacy_id\'. To fix, please set `force_32bit_ids` to `1` in your Settings => More.', + ], $e->getStatusCode(), $e->getHeaders()); + } +} diff --git a/app/Exceptions/Handlers/MigrationHandler.php b/app/Exceptions/Handlers/MigrationHandler.php new file mode 100644 index 00000000000..93b44c8528b --- /dev/null +++ b/app/Exceptions/Handlers/MigrationHandler.php @@ -0,0 +1,72 @@ +toMigration = true; + + return true; + } + if ($e instanceof MigrationAlreadyCompletedException) { + $this->toMigration = false; + + return true; + } + } while ($e = $e->getPrevious()); + + return false; + } + + /** + * {@inheritDoc} + */ + public function renderHttpException(SymfonyResponse $defaultResponse, HttpException $e): SymfonyResponse + { + try { + $redirectResponse = $this->toMigration ? ToMigration::go() : ToHome::go(); + $contentType = $defaultResponse->headers->get('Content-Type'); + if ($contentType !== null && $contentType !== '') { + $redirectResponse->headers->set('Content-Type', $contentType); + $content = $defaultResponse->getContent(); + $redirectResponse->setContent($content !== false ? $content : null); + } + + return $redirectResponse; + } catch (\Throwable) { + return $defaultResponse; + } + } +} diff --git a/app/Exceptions/Handlers/ModelNotFound.php b/app/Exceptions/Handlers/ModelNotFound.php deleted file mode 100644 index fbbd03ae182..00000000000 --- a/app/Exceptions/Handlers/ModelNotFound.php +++ /dev/null @@ -1,33 +0,0 @@ -json('false', 200); - } - - // @codeCoverageIgnoreEnd -} diff --git a/app/Exceptions/Handlers/NoEncryptionKey.php b/app/Exceptions/Handlers/NoEncryptionKey.php index 55cfae3677c..58632e05447 100644 --- a/app/Exceptions/Handlers/NoEncryptionKey.php +++ b/app/Exceptions/Handlers/NoEncryptionKey.php @@ -1,45 +1,57 @@ getMessage() === 'No application encryption key has been specified.'; + do { + if ($e instanceof MissingAppKeyException) { + return true; + } + } while ($e = $e->getPrevious()); + + return false; } /** - * @return Response|View + * {@inheritDoc} */ - // @codeCoverageIgnoreStart - public function go() + public function renderHttpException(SymfonyResponse $defaultResponse, HttpException $e): SymfonyResponse { try { - touch(base_path('.NO_SECURE_KEY')); + $redirectResponse = ToInstall::go(); + $contentType = $defaultResponse->headers->get('Content-Type'); + if ($contentType !== null && $contentType !== '') { + $redirectResponse->headers->set('Content-Type', $contentType); + $content = $defaultResponse->getContent(); + $redirectResponse->setContent($content !== false ? $content : null); + } - return ToInstall::go(); - } catch (Exception $e) { - return response()->view('error.error', ['code' => '500', 'message' => 'WRITE ACCESS REQUIRED on ' . base_path() . '
in order to create .NO_SECURE_KEY, .env, installed.log files']); + return $redirectResponse; + } catch (\Throwable) { + return $defaultResponse; } } - - // @codeCoverageIgnoreEnd } diff --git a/app/Exceptions/Handlers/ViteManifestNotFoundHandler.php b/app/Exceptions/Handlers/ViteManifestNotFoundHandler.php new file mode 100644 index 00000000000..df3911b0989 --- /dev/null +++ b/app/Exceptions/Handlers/ViteManifestNotFoundHandler.php @@ -0,0 +1,48 @@ +getPrevious()); + + return false; + } + + /** + * {@inheritDoc} + */ + public function renderHttpException(SymfonyResponse $defaultResponse, HttpException $e): SymfonyResponse + { + return response()->view('error.error', [ + 'code' => $e->getStatusCode(), + 'type' => class_basename($e), + 'message' => 'Vite manifest not found, please execute `npm run dev`', + ], $e->getStatusCode(), $e->getHeaders()); + } +} diff --git a/app/Exceptions/HttpHoneyPotException.php b/app/Exceptions/HttpHoneyPotException.php new file mode 100644 index 00000000000..d34a0f40a64 --- /dev/null +++ b/app/Exceptions/HttpHoneyPotException.php @@ -0,0 +1,31 @@ +getCode() : 0, $previous); + } + + public static function createFromUnexpectedException(\Throwable $previous): self + { + return new self('Unexpected exception: ' . get_class($previous), $previous); + } +} diff --git a/app/Exceptions/Internal/LycheeDomainException.php b/app/Exceptions/Internal/LycheeDomainException.php new file mode 100644 index 00000000000..a7a246e9c83 --- /dev/null +++ b/app/Exceptions/Internal/LycheeDomainException.php @@ -0,0 +1,19 @@ +getCode() : 0, $previous); + } +} diff --git a/app/Exceptions/Internal/LycheeInvalidArgumentException.php b/app/Exceptions/Internal/LycheeInvalidArgumentException.php new file mode 100644 index 00000000000..0e9e3a53754 --- /dev/null +++ b/app/Exceptions/Internal/LycheeInvalidArgumentException.php @@ -0,0 +1,19 @@ +message); - } -} diff --git a/app/Exceptions/JsonWarning.php b/app/Exceptions/JsonWarning.php deleted file mode 100644 index 079d2d3709a..00000000000 --- a/app/Exceptions/JsonWarning.php +++ /dev/null @@ -1,23 +0,0 @@ -message); - } -} diff --git a/app/Exceptions/LocationDecodingFailed.php b/app/Exceptions/LocationDecodingFailed.php new file mode 100644 index 00000000000..3d546d17734 --- /dev/null +++ b/app/Exceptions/LocationDecodingFailed.php @@ -0,0 +1,13 @@ + 1) { + $msg = 'Several photos could not be imported'; + $prev = null; + } else { + throw new LycheeDomainException('$listOfExceptions must not be empty'); + } + parent::__construct(Response::HTTP_UNPROCESSABLE_ENTITY, $msg, $prev); + $this->previousExceptions = $listOfExceptions; + } + + /** + * @return \Throwable[] + */ + public function previousExceptions(): array + { + return $this->previousExceptions; + } +} \ No newline at end of file diff --git a/app/Exceptions/MediaFileMissingException.php b/app/Exceptions/MediaFileMissingException.php new file mode 100644 index 00000000000..91c84dd5697 --- /dev/null +++ b/app/Exceptions/MediaFileMissingException.php @@ -0,0 +1,27 @@ + Could not $operationName $modelName + * + * @param string $modelName the name of the model + * @param string $operationName the failed operation in gerund + * form, typically "creating", + * "updating", "deleting", ... + * @param \Throwable|null $previous an optional previous exception + * + * @return ModelDBException + */ + public static function create(string $modelName, string $operationName, ?\Throwable $previous = null): ModelDBException + { + return new ModelDBException(Str::ucfirst($operationName) . ' ' . $modelName . ' failed', $previous); + } +} \ No newline at end of file diff --git a/app/Exceptions/NoOnlineUpdateException.php b/app/Exceptions/NoOnlineUpdateException.php deleted file mode 100644 index 07f20c37899..00000000000 --- a/app/Exceptions/NoOnlineUpdateException.php +++ /dev/null @@ -1,16 +0,0 @@ -value => UnsortedAlbum::class, + SmartAlbumType::STARRED->value => StarredAlbum::class, + SmartAlbumType::RECENT->value => RecentAlbum::class, + SmartAlbumType::ON_THIS_DAY->value => OnThisDayAlbum::class, + ]; + /** - * @var SmartFactory + * Returns an existing instance of an album with the given ID or fails + * with an exception. + * + * @param string $albumID the ID of the requested album + * @param bool $withRelations indicates if the relations of an + * album (i.e. photos and sub-albums, + * if applicable) shall be loaded, too. + * + * @return AbstractAlbum the album for the ID + * + * @throws ModelNotFoundException thrown, if no album with the given ID exists + * @throws InvalidSmartIdException should not be thrown; otherwise this + * indicates an internal bug + */ + public function findAbstractAlbumOrFail(string $albumID, bool $withRelations = true): AbstractAlbum + { + $smartAlbumType = SmartAlbumType::tryFrom($albumID); + if ($smartAlbumType !== null) { + return $this->createSmartAlbum($smartAlbumType, $withRelations); + } + + return $this->findBaseAlbumOrFail($albumID, $withRelations); + } + + /** + * Same as above but in the case of albumID being null, it returns null. + * + * @param string|null $albumID the ID of the requested album + * @param bool $withRelations indicates if the relations of an + * album (i.e. photos and sub-albums, + * if applicable) shall be loaded, too. + * + * @return AbstractAlbum|null the album for the ID or null if ID is null + * + * @throws ModelNotFoundException thrown, if no album with the given ID exists + * @throws InvalidSmartIdException should not be thrown; otherwise this + * indicates an internal bug */ - private $smartFactory; + public function findNullalbleAbstractAlbumOrFail(?string $albumID, bool $withRelations = true): ?AbstractAlbum + { + if ($albumID === null) { + return null; + } - public function __construct(SmartFactory $smartFactory) + return $this->findAbstractAlbumOrFail($albumID, $withRelations); + } + + /** + * Returns an existing model instance of an album with the given ID or + * fails with an exception. + * + * @param string $albumID the ID of the requested album + * @param bool $withRelations indicates if the relations of an + * album (i.e. photos and sub-albums, + * if applicable) shall be loaded, too. + * + * @return BaseAlbum the album for the ID + * + * @throws ModelNotFoundException thrown, if no album with the given ID exists + * + * @noinspection PhpIncompatibleReturnTypeInspection + */ + public function findBaseAlbumOrFail(string $albumID, bool $withRelations = true): BaseAlbum { - $this->smartFactory = $smartFactory; + $albumQuery = Album::query(); + $tagAlbumQuery = TagAlbum::query(); + + if ($withRelations) { + $albumQuery->with(['access_permissions', 'photos', 'children', 'photos.size_variants']); + $tagAlbumQuery->with(['photos']); + } + + try { + return $albumQuery->findOrFail($albumID); + } catch (ModelNotFoundException) { + try { + return $tagAlbumQuery->findOrFail($albumID); + } catch (ModelNotFoundException) { + throw (new ModelNotFoundException())->setModel(BaseAlbumImpl::class, [$albumID]); + } + } } /** - * In the case of is_smart we forward the call to the smart factory. + * Returns a collection of {@link AbstractAlbum} instances whose IDs are + * contained in the given set of IDs. + * + * @param string[] $albumIDs a list of IDs + * @param bool $withRelations indicates if the relations of an + * album (i.e. photos and sub-albums, + * if applicable) shall be loaded, too. + * + * @return Collection a possibly empty list of + * {@link AbstractAlbum} * - * @param string|ing + * @throws ModelNotFoundException */ - public function is_smart($kind): bool + public function findAbstractAlbumsOrFail(array $albumIDs, bool $withRelations = true): Collection { - return $this->smartFactory->is_smart($kind); + // Remove root (ID===`null`) and duplicates + $albumIDs = array_diff(array_unique($albumIDs), [null]); + $smartAlbumIDs = array_intersect($albumIDs, SmartAlbumType::values()); + $modelAlbumIDs = array_diff($albumIDs, SmartAlbumType::values()); + + $smartAlbums = []; + foreach ($smartAlbumIDs as $smartID) { + $smartAlbumType = SmartAlbumType::tryFrom($smartID) + ?? throw LycheeAssertionError::createFromUnexpectedException(new InvalidSmartIdException($smartID)); + $smartAlbums[] = $this->createSmartAlbum($smartAlbumType, $withRelations); + } + + /** @phpstan-ignore-next-line phpstan stan complain of incompatibility of types while both are subtypes... */ + return new Collection(array_merge( + $smartAlbums, + $this->findBaseAlbumsOrFail($modelAlbumIDs, $withRelations)->all() + )); } /** - * @param string $albumID + * Returns a collection of {@link BaseAlbum} instances whose IDs are + * contained in the given set of IDs. * - * @return Album|SmartAlbum|TagAlbum + * @param string[] $albumIDs a list of IDs + * @param bool $withRelations indicates if the relations of an + * album (i.e. photos and sub-albums, + * if applicable) shall be loaded, too. + * + * @return Collection a possibly empty list of {@link BaseAlbum} + * + * @throws ModelNotFoundException */ - public function make(string $albumId) + public function findBaseAlbumsOrFail(array $albumIDs, bool $withRelations = true): Collection { - if ($this->smartFactory->is_smart($albumId)) { - return $this->smartFactory->make($albumId); + // Remove root. + // Since we count the result we need to ensure that there are no + // duplicates. + $albumIDs = array_diff(array_unique($albumIDs), [null]); + + $tagAlbumQuery = TagAlbum::query(); + $albumQuery = Album::query(); + + if ($withRelations) { + $tagAlbumQuery->with(['photos']); + $albumQuery->with(['photos', 'children', 'photos.size_variants']); } - //! We need to catch that one, otherwise it is returned as a 404 by Laravel - $album = Album::findOrFail($albumId); + /** @var Collection $result */ + $result = new Collection(array_merge( + $tagAlbumQuery->findMany($albumIDs)->all(), + $albumQuery->findMany($albumIDs)->all(), + )); - if ($album->smart) { - // we reload it. - return TagAlbum::findOrFail($albumId); + if ($result->count() !== count($albumIDs)) { + throw (new ModelNotFoundException())->setModel(BaseAlbumImpl::class, $albumIDs); } - return $album; + return $result; + } + + /** + * Returns a collection of {@link \App\SmartAlbums\BaseSmartAlbum} with + * one instance for each built-in smart album. + * + * @param bool $withRelations Eagerly loads the relation + * {@link BaseSmartAlbum::photos()} + * for each smart album + * + * @return Collection + * + * @throws InvalidSmartIdException + */ + public function getAllBuiltInSmartAlbums(bool $withRelations = true): Collection + { + $smartAlbums = new Collection(); + collect(SmartAlbumType::cases()) + ->filter(fn (SmartAlbumType $s) => $s->is_enabled()) + ->each(fn (SmartAlbumType $s) => $smartAlbums->put($s->value, $this->createSmartAlbum($s, $withRelations))); + + return $smartAlbums; } - public function makeFromTitle(string $title): Album + /** + * Returns the instance of the built-in smart album with the designated ID. + * + * @param SmartAlbumType $smartAlbumId the ID of the smart album + * @param bool $withRelations Eagerly loads the relation + * {@link BaseSmartAlbum::photos()} + * for the smart album + * + * @return BaseSmartAlbum + * + * @throws InvalidSmartIdException + */ + public function createSmartAlbum(SmartAlbumType $smartAlbumId, bool $withRelations = true): BaseSmartAlbum { - $album = new Album(); - $album->id = Helpers::generateID(); - $album->title = $title; - $album->description = ''; + /** @var BaseSmartAlbum $smartAlbum */ + $smartAlbum = call_user_func(self::BUILTIN_SMARTS_CLASS[$smartAlbumId->value] . '::getInstance'); + if ($withRelations) { + // Just try to get the photos. + // This loads the relation from DB and caches it. + // @phpstan-ignore-next-line : PhpStan will complain about unused variable. + $ignore = $smartAlbum->photos; + } - return $album; + return $smartAlbum; } } diff --git a/app/Factories/DiagnosticsChecksFactory.php b/app/Factories/DiagnosticsChecksFactory.php deleted file mode 100644 index 968fd48a8a0..00000000000 --- a/app/Factories/DiagnosticsChecksFactory.php +++ /dev/null @@ -1,36 +0,0 @@ -checks[] = $class_candidate; - } - } - } - - public function makeAll(): array - { - $checks_ret = []; - - foreach ($this->checks as $check) { - $checks_ret[] = resolve($check); // take care of dependency injection <3 - } - - return $checks_ret; - } -} diff --git a/app/Factories/LangFactory.php b/app/Factories/LangFactory.php deleted file mode 100644 index 64ef84de8e9..00000000000 --- a/app/Factories/LangFactory.php +++ /dev/null @@ -1,49 +0,0 @@ -langs[$lang->code()] = $class_candidate; - } - } - } - - public function exists($code) - { - return array_key_exists($code, $this->langs); - } - - /** - * Factory method. - */ - public function make(string $kind): Language - { - if ($this->exists($kind)) { - return resolve($this->langs[$kind]); - } - - return resolve(English::class); - } - - public function getCodes() - { - return array_keys($this->langs); - } -} diff --git a/app/Factories/SmartFactory.php b/app/Factories/SmartFactory.php deleted file mode 100644 index 562cdba7af0..00000000000 --- a/app/Factories/SmartFactory.php +++ /dev/null @@ -1,56 +0,0 @@ - UnsortedAlbum::class, - 'starred' => StarredAlbum::class, - 'public' => PublicAlbum::class, - 'recent' => RecentAlbum::class, - ]; - - public function is_smart($kind): bool - { - return array_key_exists($kind, $this->base_smarts); - } - - /** - * Factory method. - */ - public function make(string $kind): SmartAlbum - { - if ($this->is_smart($kind)) { - return resolve($this->base_smarts[$kind]); - } - - if ($kind == 'tag') { - return resolve(TagAlbum::class); - } - - return null; - } - - public function makeAll(): Collection - { - $smartAlbums = new Collection(); - - foreach ($this->base_smarts as $smart_kind => $_) { - $smartAlbums->push($this->make($smart_kind)); - } - - return $smartAlbums; - } -} diff --git a/app/Http/Controllers/Admin/DiagnosticsController.php b/app/Http/Controllers/Admin/DiagnosticsController.php new file mode 100644 index 00000000000..24ec835c72a --- /dev/null +++ b/app/Http/Controllers/Admin/DiagnosticsController.php @@ -0,0 +1,168 @@ + + */ + public function errors(Errors $errors): array + { + return ErrorLine::collect($errors->get()); + } + + /** + * Get the space usage. + * ! This is slow. + * + * @param DiagnosticsRequest $_request + * @param Space $space + * + * @return string[] + */ + public function space(DiagnosticsRequest $_request, Space $space) + { + return $space->get(); + } + + /** + * Get info of the installation. + * + * @param DiagnosticsRequest $_request + * @param Info $info + * + * @return string[] + */ + public function info(DiagnosticsRequest $_request, Info $info): array + { + return $info->get(); + } + + /** + * Get the configuration of the installation. + * + * @param DiagnosticsRequest $_request + * @param Configuration $config + * + * @return string[] + */ + public function config(DiagnosticsRequest $_request, Configuration $config): array + { + return $config->get(); + } + + /** + * Just call the phpinfo function. + * Cannot be tested. + * + * @param DiagnosticsRequest $_request + * + * @return void + * + * @codeCoverageIgnore + */ + public function phpinfo(DiagnosticsRequest $_request): void + { + phpinfo(); + } + + /** + * Return the table of access permissions currently available on the server. + * + * @return Permissions + */ + public function getFullAccessPermissions(DiagnosticsRequest $_request, AlbumQueryPolicy $albumQueryPolicy): Permissions + { + $data1 = AccessPermission::query() + ->join('base_albums', 'base_albums.id', '=', APC::BASE_ALBUM_ID) + ->select([ + APC::BASE_ALBUM_ID, + APC::IS_LINK_REQUIRED, + APC::GRANTS_FULL_PHOTO_ACCESS, + APC::GRANTS_DOWNLOAD, + APC::GRANTS_EDIT, + APC::GRANTS_UPLOAD, + APC::GRANTS_DELETE, + APC::PASSWORD, + APC::USER_ID, + 'title', + ]) + ->when( + Auth::check(), + fn ($q1) => $q1 + ->where(APC::USER_ID, '=', Auth::id()) + ->orWhere( + fn ($q2) => $q2->whereNull(APC::USER_ID) + ->whereNotIn( + 'access_permissions.' . APC::BASE_ALBUM_ID, + fn ($q3) => $q3->select('acc_per.' . APC::BASE_ALBUM_ID) + ->from('access_permissions', 'acc_per') + ->where(APC::USER_ID, '=', Auth::id()) + ) + ) + ) + ->when( + !Auth::check(), + fn ($q1) => $q1->whereNull(APC::USER_ID) + ) + ->orderBy(APC::BASE_ALBUM_ID) + ->get(); + + $query2 = DB::table('base_albums'); + $albumQueryPolicy->joinSubComputedAccessPermissions($query2, 'base_albums.id', 'inner', '', true); + $data2 = $query2 + ->select([ + APC::BASE_ALBUM_ID, + APC::IS_LINK_REQUIRED, + APC::GRANTS_FULL_PHOTO_ACCESS, + APC::GRANTS_DOWNLOAD, + APC::GRANTS_EDIT, + APC::GRANTS_UPLOAD, + APC::GRANTS_DELETE, + APC::PASSWORD, + APC::USER_ID, + 'title', + ]) + ->orderBy(APC::BASE_ALBUM_ID) + ->get() + ->map(function ($e) { + $e->is_link_required = $e->is_link_required === 1; + $e->grants_download = $e->grants_download === 1; + $e->grants_upload = $e->grants_upload === 1; + $e->grants_delete = $e->grants_delete === 1; + $e->grants_edit = $e->grants_edit === 1; + $e->grants_full_photo_access = $e->grants_full_photo_access === 1; + + return $e; + }); + + return new Permissions(json_encode($data1, JSON_PRETTY_PRINT), json_encode($data2, JSON_PRETTY_PRINT)); + } +} diff --git a/app/Http/Controllers/Admin/JobsController.php b/app/Http/Controllers/Admin/JobsController.php new file mode 100644 index 00000000000..864c185a69b --- /dev/null +++ b/app/Http/Controllers/Admin/JobsController.php @@ -0,0 +1,37 @@ + + */ + public function list(ShowJobsRequest $request): PaginatedDataCollection + { + $jobs = JobHistory::with(['owner']) + ->when(!Auth::user()->may_administrate, fn ($query) => $query->where('owner_id', '=', Auth::id())) + ->orderBy('id', 'desc') + ->paginate(Configs::getValueAsInt('log_max_num_line')); + + return JobHistoryResource::collect($jobs, PaginatedDataCollection::class); + } +} diff --git a/app/Http/Controllers/Admin/Maintenance/Cleaning.php b/app/Http/Controllers/Admin/Maintenance/Cleaning.php new file mode 100644 index 00000000000..33e158603e8 --- /dev/null +++ b/app/Http/Controllers/Admin/Maintenance/Cleaning.php @@ -0,0 +1,95 @@ +skip. + * + * @return string[] + */ + public function do(CleaningRequest $request): array + { + if (!$this->check($request)->is_not_empty) { + return []; + } + + $results = []; + foreach (new \DirectoryIterator($request->path()) as $fileInfo) { + if ($fileInfo->isDot()) { + continue; + } + if (in_array($fileInfo->getFilename(), $this->skip, true)) { + continue; + } + $results[] = sprintf(__('maintenance.cleaning.result'), $fileInfo->getFilename()); + + if ($fileInfo->isDir()) { + rmdir($fileInfo->getRealPath()); + continue; + } + unlink($fileInfo->getRealPath()); + } + + return $results; + } + + /** + * Check whether there are files to be removed. + * If not, we will not display the module to reduce complexity. + * + * @return CleaningState + */ + public function check(CleaningRequest $request): CleaningState + { + $cleaning_state = new CleaningState($request->path(), false); + + if (!is_dir($request->path())) { + Log::warning('directory ' . $request->path() . ' not found!'); + $cleaning_state->is_not_empty = false; + + return $cleaning_state; + } + + if (!(new \FilesystemIterator($request->path()))->valid()) { + $cleaning_state->is_not_empty = false; + + return $cleaning_state; + } + + $files_found = false; + foreach (new \DirectoryIterator($request->path()) as $fileInfo) { + if ($fileInfo->isDot()) { + continue; + } + if (in_array($fileInfo->getFilename(), $this->skip, true)) { + continue; + } + $files_found = true; + } + $cleaning_state->is_not_empty = $files_found; + + return $cleaning_state; + } +} diff --git a/app/Http/Controllers/Admin/Maintenance/FixJobs.php b/app/Http/Controllers/Admin/Maintenance/FixJobs.php new file mode 100644 index 00000000000..4979fec67f3 --- /dev/null +++ b/app/Http/Controllers/Admin/Maintenance/FixJobs.php @@ -0,0 +1,54 @@ +check($_request) === 0) { + return; + } + + JobHistory::query() + ->whereIn('status', $this->waitingJobsTypes) + ->update(['status' => JobStatus::FAILURE]); + } + + /** + * Check if there are any waiting jobs. + * If not, we will not display the module to reduce complexity. + * + * @return int + */ + public function check(MaintenanceRequest $_request): int + { + return JobHistory::whereIn('status', $this->waitingJobsTypes)->count(); + } +} diff --git a/app/Http/Controllers/Admin/Maintenance/FixTree.php b/app/Http/Controllers/Admin/Maintenance/FixTree.php new file mode 100644 index 00000000000..594f2a3ef15 --- /dev/null +++ b/app/Http/Controllers/Admin/Maintenance/FixTree.php @@ -0,0 +1,38 @@ +countErrors(); + + return new TreeState( + $stats['oddness'] ?? 0, + $stats['duplicates'] ?? 0, + $stats['wrong_parent'] ?? 0, + $stats['missing_parent'] ?? 0 + ); + } +} diff --git a/app/Http/Controllers/Admin/Maintenance/FullTree.php b/app/Http/Controllers/Admin/Maintenance/FullTree.php new file mode 100644 index 00000000000..7624e532d0b --- /dev/null +++ b/app/Http/Controllers/Admin/Maintenance/FullTree.php @@ -0,0 +1,48 @@ +update($albumInstance, $request->albums(), $keyName); + } + + /** + * Display the current full tree of albums. + * + * @return Collection + */ + public function check(MaintenanceRequest $request): Collection + { + $albums = Album::query()->join('base_albums', 'base_albums.id', '=', 'albums.id')->select(['albums.id', 'title', 'parent_id', '_lft', '_rgt'])->orderBy('_lft', 'asc')->toBase()->get(); + + return AlbumTree::collect($albums); + } +} diff --git a/app/Http/Controllers/Admin/Maintenance/GenSizeVariants.php b/app/Http/Controllers/Admin/Maintenance/GenSizeVariants.php new file mode 100644 index 00000000000..e6fb97b2484 --- /dev/null +++ b/app/Http/Controllers/Admin/Maintenance/GenSizeVariants.php @@ -0,0 +1,91 @@ +where('type', 'like', 'image/%') + ->with('size_variants') + ->whereDoesntHave('size_variants', function (Builder $query) use ($request) { + $query->where('type', '=', $request->kind()); + }) + ->take(100) + ->get(); + + $generated = 0; + /** @var Photo $photo */ + foreach ($photos as $photo) { + // @codeCoverageIgnoreStart + $sizeVariantFactory->init($photo); + try { + $sizeVariant = $sizeVariantFactory->createSizeVariantCond($request->kind()); + if ($request->kind() === SizeVariantType::PLACEHOLDER && $sizeVariant !== null) { + $placeholderEncoder->do($sizeVariant); + } + if ($sizeVariant !== null) { + $generated++; + Log::notice($request->kind()->value . ' (' . $sizeVariant->width . 'x' . $sizeVariant->height . ') for ' . $photo->title . ' created.'); + } else { + Log::error('Did not create ' . $request->kind()->value . ' for ' . $photo->title . '.'); + } + } catch (MediaFileOperationException $e) { + Log::error('Failed to create ' . $request->kind()->value . ' for photo id ' . $photo->id . ''); + } + // @codeCoverageIgnoreEnd + } + } + + /** + * Check how many images need to be created. + * + * @return int + */ + public function check(CreateThumbsRequest $request, SizeVariantDimensionHelpers $svHelpers): int + { + if (!$svHelpers->isEnabledByConfiguration($request->kind())) { + return 0; + } + + $numGenerated = SizeVariant::query()->where('type', '=', $request->kind())->count(); + + $totalToHave = SizeVariant::query()->where(fn ($q) => $q + ->when($svHelpers->getMaxWidth($request->kind()) !== 0, fn ($q1) => $q1->where('width', '>', $svHelpers->getMaxWidth($request->kind()))) + ->when($svHelpers->getMaxHeight($request->kind()) !== 0, fn ($q2) => $q2->orWhere('height', '>', $svHelpers->getMaxHeight($request->kind()))) + ) + ->where('type', '=', SizeVariantType::ORIGINAL) + ->count(); + + return $totalToHave - $numGenerated; + } +} diff --git a/app/Http/Controllers/Admin/Maintenance/MissingFileSizes.php b/app/Http/Controllers/Admin/Maintenance/MissingFileSizes.php new file mode 100644 index 00000000000..a5133124a6c --- /dev/null +++ b/app/Http/Controllers/Admin/Maintenance/MissingFileSizes.php @@ -0,0 +1,75 @@ +where('storage_disk', '=', StorageDiskType::LOCAL) + ->where('filesize', '=', 0) + ->orderBy('id'); + // Internally, only holds $limit entries at once + $variants = $variants_query->lazyById(500); + + $generated = 0; + + foreach ($variants as $variant) { + // @codeCoverageIgnoreStart + $variantFile = $variant->getFile(); + if ($variantFile->exists()) { + try { + $variant->filesize = $variantFile->getFilesize(); + if (!$variant->save()) { + Log::error('Failed to update filesize for ' . $variantFile->getRelativePath() . '.'); + } else { + $generated++; + } + } catch (UnableToRetrieveMetadata) { + Log::error($variant->id . ' : Failed to get filesize for ' . $variantFile->getRelativePath() . '.'); + } + } else { + Log::error($variant->id . ' : No file found at ' . $variantFile->getRelativePath() . '.'); + } + // @codeCoverageIgnoreEnd + } + } + + /** + * Check how many images have missing file sizes.. + * + * @return int + */ + public function check(MaintenanceRequest $request): int + { + return SizeVariant::query() + ->where('filesize', '=', 0) + // TODO: remove s3 support here. + ->count(); + } +} diff --git a/app/Http/Controllers/Admin/Maintenance/Model/Album.php b/app/Http/Controllers/Admin/Maintenance/Model/Album.php new file mode 100644 index 00000000000..32de4885ea0 --- /dev/null +++ b/app/Http/Controllers/Admin/Maintenance/Model/Album.php @@ -0,0 +1,26 @@ + + */ +class Album extends Model implements Node +{ + /** @phpstan-use NodeTrait */ + use NodeTrait; + public $timestamps = false; +} \ No newline at end of file diff --git a/app/Http/Controllers/Admin/Maintenance/Optimize.php b/app/Http/Controllers/Admin/Maintenance/Optimize.php new file mode 100644 index 00000000000..d90480c575e --- /dev/null +++ b/app/Http/Controllers/Admin/Maintenance/Optimize.php @@ -0,0 +1,34 @@ +do()) + ->merge(collect($optimizeTables->do())) + ->all(); + } +} diff --git a/app/Http/Controllers/Admin/Maintenance/RegisterController.php b/app/Http/Controllers/Admin/Maintenance/RegisterController.php new file mode 100644 index 00000000000..dde4a09f18e --- /dev/null +++ b/app/Http/Controllers/Admin/Maintenance/RegisterController.php @@ -0,0 +1,40 @@ +key()->getValue()); + $verify = resolve(Verify::class); + $is_supporter = $verify->is_supporter(); + if ($is_supporter) { + return new RegisterData(true); + } + + // Not valid, reset the key. + Configs::set('license_key', ''); + + return new RegisterData(false); + } +} diff --git a/app/Http/Controllers/Admin/SettingsController.php b/app/Http/Controllers/Admin/SettingsController.php new file mode 100644 index 00000000000..21c9f8d37c3 --- /dev/null +++ b/app/Http/Controllers/Admin/SettingsController.php @@ -0,0 +1,116 @@ +when(config('features.hide-lychee-SE', false) === true, fn ($q) => $q->where('cat', '!=', 'lychee SE')) + ->when(!$request->is_se() && !Configs::getValueAsBool('enable_se_preview'), fn ($q) => $q->where('level', '=', 0)) + ->orderBy('cat', 'asc')->get(); + + return new ConfigCollectionResource($editable_configs); + } + + /** + * Set a limited number of configurations with the new values. + * + * @param SetConfigsRequest $request + * + * @return ConfigCollectionResource + */ + public function setConfigs(SetConfigsRequest $request): ConfigCollectionResource + { + $configs = $request->configs(); + $configs->each(function ($config) { + Configs::query()->where('key', $config->key)->update(['value' => $config->value ?? '']); + }); + + Configs::invalidateCache(); + + return new ConfigCollectionResource(Configs::orderBy('cat', 'asc')->get()); + } + + /** + * Give the list of available languages. + * + * @return string[] + */ + public function getLanguages(GetAllConfigsRequest $request): array + { + // @phpstan-ignore-next-line + return collect(config('app.supported_locale'))->filter(function ($value, $key) { + return !str_contains($value, 'json'); + })->values()->toArray(); + } + + /** + * Takes the js input text and puts it into `dist/custom.js`. + * This allows admins to actually execute custom js code on their + * Lychee-Laravel installation. + * + * @param SetJSSettingRequest $request + * + * @return void + * + * @throws InsufficientFilesystemPermissions + */ + public function setJS(SetJSSettingRequest $request): void + { + $js = $request->getJs(); + if (Storage::disk('dist')->put('custom.js', $js) === false) { + if (Storage::disk('dist')->get('custom.js') !== $js) { + throw new InsufficientFilesystemPermissions('Could not save JS'); + } + } + } + + /** + * Takes the css input text and put it into `dist/user.css`. + * This allows admins to actually personalize the look of their + * installation. + * + * @param SetCSSSettingRequest $request + * + * @return void + * + * @throws InsufficientFilesystemPermissions + */ + public function setCSS(SetCSSSettingRequest $request): void + { + $css = $request->getCss(); + if (Storage::disk('dist')->put('user.css', $css) === false) { + if (Storage::disk('dist')->get('user.css') !== $css) { + throw new InsufficientFilesystemPermissions('Could not save CSS'); + } + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Admin/UpdateController.php b/app/Http/Controllers/Admin/UpdateController.php new file mode 100644 index 00000000000..6100de9655c --- /dev/null +++ b/app/Http/Controllers/Admin/UpdateController.php @@ -0,0 +1,149 @@ +applyUpdate = $applyUpdate; + } + + /** + * Retrieve Update data from the server. + * + * @param UpdateRequest $request + * @param VersionInfo $versionInfo + * @param DockerVersionInfo $dockerVersionInfo + * + * @return UpdateInfo + */ + public function get(UpdateRequest $request, VersionInfo $versionInfo, DockerVersionInfo $dockerVersionInfo): UpdateInfo + { + /** @var VersionChannelType $channelName */ + $channelName = $versionInfo->getChannelName(); + $info = $versionInfo->fileVersion->getVersion()->toString(); + $extra = ''; + + if ($channelName !== VersionChannelType::RELEASE) { + if ($versionInfo->gitHubFunctions->localHead !== null) { + $branch = $versionInfo->gitHubFunctions->localBranch ?? '??'; + $commit = $versionInfo->gitHubFunctions->localHead ?? '??'; + $info = sprintf('%s (%s)', $branch, $commit); + $extra = $versionInfo->gitHubFunctions->getBehindTest(); + } else { + // @codeCoverageIgnoreStart + $info = 'No git data found.'; + // @codeCoverageIgnoreEnd + } + } + + return new UpdateInfo($info, $extra, $channelName, $dockerVersionInfo->isDocker()); + } + + /** + * Checking if any updates are available. + * + * @return UpdateCheckInfo + */ + public function check(UpdateRequest $request, GitHubVersion $gitHubFunctions, VersionInfo $versionInfo, DockerVersionInfo $dockerVersionInfo): UpdateCheckInfo + { + return new UpdateCheckInfo($gitHubFunctions->getBehindTest(), !$dockerVersionInfo->isDocker() && (!$gitHubFunctions->isUpToDate() || !$versionInfo->fileVersion->isUpToDate())); + } + + /** + * Updates Lychee and returns the messages as a JSON object. + * + * The method requires PHP to have shell access. + * Except for the return type this method is identical to + * {@link UpdateController::view()}. + * + * @param UpdateRequest $request + * + * @return array{updateMsgs: array} + * + * @throws LycheeException + */ + public function apply(UpdateRequest $request): array + { + UpdatableCheck::assertUpdatability(); + + return ['updateMsgs' => $this->applyUpdate->run()]; + } + + /** + * Updates Lychee and returns the messages as an HTML view. + * + * The method requires PHP to have shell access. + * Except for the return type this method is identical to + * {@link UpdateController::apply()}. + * + * @param UpdateRequest $request + * + * @return View + * + * @throws LycheeException + */ + public function view(UpdateRequest $request): View + { + UpdatableCheck::assertUpdatability(); + + $output = $this->applyUpdate->run(); + + return view('update.results', ['code' => '200', 'message' => 'Upgrade results', 'output' => $output]); + } + + /** + * Migrates the Lychee DB and returns a HTML view. + * + * **TODO:** Consolidate with {@link \App\Http\Controllers\Install\MigrationController::view()}. + * + * **ATTENTION:** This method serves a somewhat similar purpose as + * `MigrationController::view()` except that the latter does not only + * trigger a migration, but also generates a new API key. + * Also note, that this method internally uses + * {@link ApplyUpdate::migrate()} while `MigrationController::view` + * uses {@link \App\Actions\InstallUpdate\ApplyMigration::migrate()}. + * However, both methods are very similar, too. + * The whole code around installation/upgrade/migration should be + * thoroughly revised and refactored. + * + * @param MigrateRequest $request + * + * @return View|Response + */ + public function migrate(MigrateRequest $request): View|Response + { + $output = []; + $output = $this->applyUpdate->run(); + + return view('update.results', ['code' => '200', 'message' => 'Migration results', 'output' => $output]); + } +} diff --git a/app/Http/Controllers/Admin/UserManagementController.php b/app/Http/Controllers/Admin/UserManagementController.php new file mode 100644 index 00000000000..62fe52908e1 --- /dev/null +++ b/app/Http/Controllers/Admin/UserManagementController.php @@ -0,0 +1,110 @@ + + */ + public function list(ManagmentListUsersRequest $request, Spaces $spaces): Collection + { + /** @var Collection $users */ + $users = User::select(['id', 'username', 'may_administrate', 'may_upload', 'may_edit_own_settings', 'quota_kb', 'description', 'note'])->orderBy('id', 'asc')->get(); + $spacesPerUser = $spaces->getFullSpacePerUser(); + /** @var Collection $zipped */ + $zipped = $users->zip($spacesPerUser); + + return $zipped->map(fn ($item) => new UserManagementResource($item[0], $item[1], $request->is_se())); + } + + /** + * Save modification done to a user. + * Note that an admin can change the password of a user at will. + * + * @param SetUserSettingsRequest $request + * @param Save $save + * + * @return void + */ + public function save(SetUserSettingsRequest $request, Save $save): void + { + $save->do( + user: $request->user2(), + username: $request->username(), + password: $request->password(), + mayUpload: $request->mayUpload(), + mayEditOwnSettings: $request->mayEditOwnSettings(), + quota_kb: $request->quota_kb(), + note: $request->note() + ); + } + + /** + * Deletes a user. + * + * The albums and photos owned by the user are re-assigned to the + * admin user. + * + * @param DeleteUserRequest $request + * + * @return void + */ + public function delete(DeleteUserRequest $request): void + { + if ($request->user2()->id === Auth::id()) { + throw new UnauthorizedException('You are not allowed to delete yourself'); + } + $request->user2()->delete(); + } + + /** + * Create a new user. + * + * @param AddUserRequest $request + * @param Create $create + * + * @return UserManagementResource + */ + public function create(AddUserRequest $request, Create $create): UserManagementResource + { + $user = $create->do( + username: $request->username(), + password: $request->password(), + mayUpload: $request->mayUpload(), + mayEditOwnSettings: $request->mayEditOwnSettings(), + quota_kb: $request->quota_kb(), + note: $request->note() + ); + + return new UserManagementResource($user, ['id' => $user->id, 'size' => 0], $request->is_se()); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Administration/DiagnosticsController.php b/app/Http/Controllers/Administration/DiagnosticsController.php deleted file mode 100644 index fed4a748c9c..00000000000 --- a/app/Http/Controllers/Administration/DiagnosticsController.php +++ /dev/null @@ -1,83 +0,0 @@ -get(); - - if (AccessControl::is_admin() || AccessControl::noLogin()) { - $infos = resolve(Info::class)->get(); - $configs = resolve(Configuration::class)->get(); - } else { - $infos = ['You must be logged in to see this.']; - $configs = ['You must be logged in to see this.']; - } - - return [ - 'errors' => $errors, - 'infos' => $infos, - 'configs' => $configs, - ]; - } - - /** - * This function return the Diagnostic data as an JSON array. - * should be used for AJAX request. - * - * @return array - */ - public function get(CheckUpdate $checkUpdate) - { - $ret = $this->get_data(); - $ret['update'] = $checkUpdate->getCode(); - - return $ret; - } - - /** - * Return the diagnostic information as a page. - * - * @return View - */ - public function show() - { - return view('diagnostics', $this->get_data()); - } - - /** - * Return the size used by Lychee. - * We now separate this from the initial get() call as this is quite time consuming. - * - * @return array - */ - public function get_size() - { - $infos = ['You must be logged in to see this.']; - if (AccessControl::is_admin() || AccessControl::noLogin()) { - $infos = resolve(Space::class)->get(); - } - - return $infos; - } -} diff --git a/app/Http/Controllers/Administration/LogController.php b/app/Http/Controllers/Administration/LogController.php deleted file mode 100644 index e1f1469f8fe..00000000000 --- a/app/Http/Controllers/Administration/LogController.php +++ /dev/null @@ -1,65 +0,0 @@ -limit(intval(Configs::get_value('log_max_num_line', 1000)))->get(); - } - - /** - * display the Logs. - * - * @return View|string - */ - public function display() - { - if (Logs::count() == 0) { - return 'Everything looks fine, Lychee has not reported any problems!'; - } else { - $logs = $this->list(); - - return view('logs.list', ['logs' => $logs]); - } - } - - /** - * Empty the log table. - * - * @return string - */ - public static function clear() - { - DB::table('logs')->truncate(); - - return 'Log cleared'; - } - - /** - * This function does pretty much the same as clear but only does it on notice - * and also keeps the log of the loggin attempts. - * - * @return string - */ - public static function clearNoise() - { - Logs::where('function', '!=', 'App\Http\Controllers\SessionController::login')->where('type', '=', 'notice')->delete(); - - return 'Log Noise cleared'; - } -} diff --git a/app/Http/Controllers/Administration/SettingsController.php b/app/Http/Controllers/Administration/SettingsController.php deleted file mode 100644 index d641ef541f7..00000000000 --- a/app/Http/Controllers/Administration/SettingsController.php +++ /dev/null @@ -1,374 +0,0 @@ -do($request) ? 'true' : 'false'; - } - - /** - * Define the default sorting type. - * - * @param Request $request - * - * @return string - */ - public function setSorting(Request $request) - { - $validated = $request->validate([ - 'typeAlbums' => 'required|string', - 'orderAlbums' => 'required|string', - 'typePhotos' => 'required|string', - 'orderPhotos' => 'required|string', - ]); - - Configs::set('sorting_Photos_col', $validated['typePhotos']); - Configs::set('sorting_Photos_order', $validated['orderPhotos']); - Configs::set('sorting_Albums_col', $validated['typeAlbums']); - Configs::set('sorting_Albums_order', $validated['orderAlbums']); - - return 'true'; - } - - /** - * Set the lang used by the Lychee installation. - * - * @param Request $request - * - * @return string - */ - public function setLang(Request $request) - { - $validated = $request->validate(['lang' => 'required|string']); - - foreach (Lang::get_lang_available() as $lang) { - if ($validated['lang'] == $lang) { - return Configs::set('lang', $lang) ? 'true' : 'false'; - } - } - - Logs::error(__METHOD__, __LINE__, 'Could not update settings. Unknown lang.'); - - return 'false'; - } - - /** - * Set the layout of the albums - * 0: squares - * 1: flickr justified - * 2: flickr unjustified. - * - * @param Request $request - * - * @return string - */ - public function setLayout(Request $request) - { - $validated = $request->validate(['layout' => 'required|string']); - - return Configs::set('layout', $validated['layout']) ? 'true' : 'false'; - } - - /** - * Set the dropbox key for the API. - * - * @param Request $request - * - * @return string - */ - public function setDropboxKey(Request $request) - { - $validated = $request->validate(['key' => 'string|nullable']); - - return Configs::set('dropbox_key', $validated['key']) ? 'true' : 'false'; - } - - /** - * Allow public user to use the search function. - * - * @param Request $request - * - * @return string - */ - public function setPublicSearch(Request $request) - { - $validated = $request->validate(['public_search' => 'required|string']); - - if ($validated['public_search'] == '1') { - return Configs::set('public_search', '1') ? 'true' : 'false'; - } - - return Configs::set('public_search', '0') ? 'true' : 'false'; - } - - /** - * Show NSFW albums by default or not. - * - * @param Request $request - * - * @return string - */ - public function setNSFWVisible(Request $request) - { - $validated = $request->validate(['nsfw_visible' => 'required|string']); - - if ($validated['nsfw_visible'] == '1') { - return Configs::set('nsfw_visible', '1') ? 'true' : 'false'; - } - - return Configs::set('nsfw_visible', '0') ? 'true' : 'false'; - } - - /** - * Select the image overlay used: - * none: no overlay - * desc: description of the photo - * date: date of the photo - * exif: exif information. - * - * @param Request $request - * - * @return string - */ - public function setImageOverlayType(Request $request) - { - $validated = $request->validate(['image_overlay_type' => 'required|string']); - - return Configs::set('image_overlay_type', $validated['image_overlay_type']) ? 'true' : 'false'; - } - - /** - * Define the default license of the pictures. - * - * @param Request $request - * - * @return string - */ - public function setDefaultLicense(Request $request) - { - $validated = $request->validate(['license' => 'required|string']); - - foreach (Helpers::get_all_licenses() as $license) { - if ($license === $validated['license']) { - return Configs::set('default_license', $license) ? 'true' : 'false'; - } - } - - Logs::error(__METHOD__, __LINE__, 'Could not find the submitted license'); - - return 'false'; - } - - /** - * Enable display of photo coordinates on map. - * - * @param Request $request - * - * @return string - */ - public function setMapDisplay(Request $request) - { - $request->validate(['map_display' => 'required|string']); - - if ($request['map_display'] == '1') { - return Configs::set('map_display', '1') ? 'true' : 'false'; - } - - return Configs::set('map_display', '0') ? 'true' : 'false'; - } - - /** - * Enable display of photos on map for public albums. - * - * @param Request $request - * - * @return string - */ - public function setMapDisplayPublic(Request $request) - { - $request->validate(['map_display_public' => 'required|string']); - - if ($request['map_display_public'] == '1') { - return Configs::set('map_display_public', '1') ? 'true' : 'false'; - } - - return Configs::set('map_display_public', '0') ? 'true' : 'false'; - } - - /** - * Set provider of OSM map tiles. - * - * @param Request $request - * - * @return string - */ - public function setMapProvider(Request $request) - { - $request->validate(['map_provider' => 'required|string']); - - return Configs::set('map_provider', $request['map_provider']) ? 'true' : 'false'; - } - - /** - * Enable display of photos of subalbums on map. - * - * @param Request $request - * - * @return string - */ - public function setMapIncludeSubalbums(Request $request) - { - $request->validate(['map_include_subalbums' => 'required|string']); - - if ($request['map_include_subalbums'] == '1') { - return Configs::set('map_include_subalbums', '1') ? 'true' : 'false'; - } - - return Configs::set('map_include_subalbums', '0') ? 'true' : 'false'; - } - - /** - * Enable decoding of GPS data into location names. - * - * @param Request $request - * - * @return string - */ - public function setLocationDecoding(Request $request) - { - $request->validate(['location_decoding' => 'required|string']); - - return Configs::set('location_decoding', $request['location_decoding']) ? 'true' : 'false'; - } - - /** - * Enable display of location name. - * - * @param Request $request - * - * @return string - */ - public function setLocationShow(Request $request) - { - $request->validate(['location_show' => 'required|string']); - - return Configs::set('location_show', $request['location_show']) ? 'true' : 'false'; - } - - /** - * Enable display of location name for public albums. - * - * @param Request $request - * - * @return string - */ - public function setLocationShowPublic(Request $request) - { - $request->validate(['location_show_public' => 'required|string']); - - return Configs::set( - 'location_show_public', - $request['location_show_public'] - ) ? 'true' : 'false'; - } - - /** - * Enable sending of new photos notification emails. - * - * @param Request $request - * - * @return string - */ - public function setNewPhotosNotification(Request $request) - { - $request->validate(['new_photos_notification' => 'required|string']); - - if ($request['new_photos_notification'] == '1') { - return Configs::set('new_photos_notification', '1') ? 'true' : 'false'; - } - - return Configs::set('new_photos_notification', '0') ? 'true' : 'false'; - } - - /** - * take the css input text and put it into dist/user.css - * this allow admins to actually personalize the look of their installation. - * - * @param Request $request - * - * @return string - */ - public function setCSS(Request $request) - { - $request->validate(['css' => 'nullable|string']); - $css = $request->get('css'); - $css = $css == null ? '' : $css; - - if (!Storage::disk('dist')->put('user.css', $css)) { - Logs::error(__METHOD__, __LINE__, 'Could not save css.'); - - return 'false'; - } - - return 'true'; - } - - /** - * Return ALL the settings. This is not filtered! - * Fortunately this is behind an admin middlewear. - * This is used in the advanced settings part. - * - * @return Collection - */ - public function getAll() - { - return Configs::orderBy('cat', 'ASC')->get(); - } - - /** - * Get a list of settings and save them in the database - * if the associated key exists. - * - * @param Request $request - * - * @return string - */ - public function saveAll(Request $request) - { - $no_error = true; - foreach ($request->except(['_token', 'function', '/api/Settings::saveAll']) as $key => $value) { - $value = ($value == null) ? '' : $value; - $no_error &= Configs::set($key, $value); - } - - return $no_error ? 'true' : 'false'; - } -} diff --git a/app/Http/Controllers/Administration/SharingController.php b/app/Http/Controllers/Administration/SharingController.php deleted file mode 100644 index 061685d176f..00000000000 --- a/app/Http/Controllers/Administration/SharingController.php +++ /dev/null @@ -1,71 +0,0 @@ -do(AccessControl::id()); - } - - /** - * Add a sharing between selected users and selected albums. - * - * @param Request $request - * - * @return string - */ - public function add(Request $request) - { - $request->validate([ - 'UserIDs' => 'string|required', - 'albumIDs' => 'string|required', - ]); - - $users = User::whereIn('id', explode(',', $request['UserIDs']))->get(); - - foreach ($users as $user) { - $user->shared()->sync(explode(',', $request['albumIDs']), false); - } - - return 'true'; - } - - /** - * Given a list of shared ID we delete them - * This function is the only reason why we test SharedIDs in - * app/Http/Middleware/UploadCheck.php. - * - * FIXME: make sure that the Lychee-front is sending the correct ShareIDs - * - * @param Request $request - * - * @return string - */ - public function delete(Request $request) - { - $request->validate([ - 'ShareIDs' => 'string|required', - ]); - - DB::table('user_album') - ->whereIn('id', explode(',', $request['ShareIDs']))->delete(); - - return 'true'; - } -} diff --git a/app/Http/Controllers/Administration/UpdateController.php b/app/Http/Controllers/Administration/UpdateController.php deleted file mode 100644 index 885e5bc1cb5..00000000000 --- a/app/Http/Controllers/Administration/UpdateController.php +++ /dev/null @@ -1,79 +0,0 @@ -getText()); - // @codeCoverageIgnoreStart - } catch (Exception $e) { - return Response::error($e->getMessage()); // Not master - } - // @codeCoverageIgnoreEnd - } - - /** - * This requires a php to have a shell access. - * This method execute the update (git pull). - * - * @return array|string - */ - public function apply(CheckUpdate $checkUpdate, ApplyUpdate $applyUpdate) - { - try { - $checkUpdate->canUpdate(); - // @codeCoverageIgnoreStart - } catch (Exception $e) { - return Response::error($e->getMessage()); - } - // @codeCoverageIgnoreEnd - - // @codeCoverageIgnoreStart - return $applyUpdate->run(); - } - - public function force(Request $request, IsMigrated $isMigrated, ApplyUpdate $applyUpdate) - { - if ($isMigrated->assert()) { - return redirect()->route('home'); - } - - if ( - AccessControl::is_admin() || AccessControl::noLogin() || - AccessControl::log_as_admin($request['username'] ?? '', $request['password'] ?? '', $request->ip()) - ) { - $output = []; - $applyUpdate->artisan($output); - $applyUpdate->filter($output); - - if (AccessControl::noLogin()) { - AccessControl::logout(); - } - - return '
' . implode("\n", $output) . '
'; - } else { - return view('error.update', ['code' => '403', 'message' => 'Incorrect username or password']); - } - } -} diff --git a/app/Http/Controllers/Administration/UserController.php b/app/Http/Controllers/Administration/UserController.php deleted file mode 100644 index 1afb3cf8e9f..00000000000 --- a/app/Http/Controllers/Administration/UserController.php +++ /dev/null @@ -1,112 +0,0 @@ -', 0)->get(); - } - - /** - * Save modification done to a user. - * Note that an admin can change the password of a user at will. - * - * @param UserPostRequest $request - * - * @return string - */ - public function save(UserPostRequest $request, Save $save) - { - $user = User::findOrFail($request['id']); - - return $save->do($user, $request->all()) ? 'true' : 'false'; - } - - /** - * Delete a user. - * FIXME: What happen to the albums owned ? - * - * @param UserPostIdRequest $request - * - * @return string - */ - public function delete(UserPostIdRequest $request) - { - $user = User::findOrFail($request['id']); - - return $user->delete() ? 'true' : 'false'; - } - - /** - * Create a new user. - * - * @param Request $request - * - * @return string - */ - public function create(Request $request, Create $create) - { - $data = $request->validate([ - 'username' => 'required|string|max:100', - 'password' => 'required|string|max:50', - 'upload' => 'required', - 'lock' => 'required', - ]); - - return $create->do($data) ? 'true' : 'false'; - } - - /** - * Update the email of a user. - * Will delete all notifications if the email is left empty. - * - * @param Request $request - * - * @return string - */ - public function updateEmail(Request $request, Save $save) - { - if ($request->email != '') { - $request->validate([ - 'email' => 'email|max:100', - ]); - } - - $user = AccessControl::user(); - - $user->email = $request->email; - - if (is_null($request->email)) { - $user->notifications()->delete(); - } - - return $user->save() ? 'true' : 'false'; - } - - /** - * Return the email address of a user. - * - * @return string - */ - public function getEmail() - { - $user = AccessControl::user(); - - if ($user->email) { - return json_encode($user->email); - } else { - return json_encode(''); - } - } -} diff --git a/app/Http/Controllers/Administration/WebAuthController.php b/app/Http/Controllers/Administration/WebAuthController.php deleted file mode 100644 index 1a6ccbe56b4..00000000000 --- a/app/Http/Controllers/Administration/WebAuthController.php +++ /dev/null @@ -1,113 +0,0 @@ -generateRegistration = $generateRegistration; - $this->verifyRegistration = $verifyRegistration; - $this->generateAuthentication = $generateAuthentication; - $this->verifyAuthentication = $verifyAuthentication; - $this->listDevices = $listDevices; - $this->deleteDevices = $deleteDevices; - } - - /** - * You can manage the user credentials thanks to the WebAuthnAuthenticatable contract directly from within the user instance. The most useful methods are:. - * - * hasCredential(): Checks if the user has a given Credential ID. - * addCredential(): Adds a new Credential Source. - * removeCredential(): Removes an existing Credential by its ID. - * flushCredentials(): Removes all credentials. You can exclude credentials by their id. - * enableCredential(): Includes an existing Credential ID from authentication. - * disableCredential(): Excludes an existing Credential ID from authentication. - * getFromCredentialId(): Returns the user using the given Credential ID, if any. - */ - public function GenerateRegistration(Request $request) - { - return $this->generateRegistration->do(); - } - - public function VerifyRegistration(Request $request) - { - $data = $request->validate($this->attestationRules()); - - return $this->verifyRegistration->do($data); - } - - public function GenerateAuthentication(Request $request) - { - $user_id = $request->input('user_id'); - - return $this->generateAuthentication->do($user_id); - } - - public function VerifyAuthentication(Request $request) - { - $credential = $request->validate($this->assertionRules()); - - return $this->verifyAuthentication->do($credential); - } - - public function List() - { - return $this->listDevices->do(); - } - - public function Delete(Request $request) - { - $id = $request->validate(['id' => 'required|string']); - - return $this->deleteDevices->do($id); - } -} diff --git a/app/Http/Controllers/AlbumController.php b/app/Http/Controllers/AlbumController.php deleted file mode 100644 index 0dcc7a4b0be..00000000000 --- a/app/Http/Controllers/AlbumController.php +++ /dev/null @@ -1,317 +0,0 @@ -validate([ - 'title' => 'string|required|max:100', - 'parent_id' => 'int|nullable', - ]); - - $album = $create->create($request['title'], $request['parent_id']); - - return Response::json($album->id, JSON_NUMERIC_CHECK); - } - - /** - * Add a new album generated by tags. - * - * @param Request $request - * - * @return false|string - */ - public function addByTags(Request $request, CreateTag $create) - { - $request->validate([ - 'title' => 'string|required|max:100', - 'tags' => 'string', - ]); - - $album = $create->create($request['title'], $request['tags']); - - return Response::json($album->id, JSON_NUMERIC_CHECK); - } - - /** - * Provided an albumID, returns the album. - * - * @param Request $request - * - * @return array|string - */ - public function get(AlbumIDRequest $request, AlbumFactory $albumFactory, Prepare $prepare) - { - $validated = $request->validated(); - $album = $albumFactory->make($validated['albumID']); - - return $prepare->do($album); - } - - /** - * Provided an albumID, returns the album with only map related data. - * - * @param Request $request - * - * @return array|string - */ - public function getPositionData(AlbumIDRequest $request, PositionData $positionData) - { - $validated = $request->validate(['includeSubAlbums' => 'string|required']); - - return $positionData->get($request['albumID'], $validated); - } - - /** - * Provided the albumID and passwords, return whether the album can be accessed or not. - * - * @param Request $request - * - * @return string - */ - public function getPublic(AlbumIDRequest $request, Unlock $unlock) - { - $request->validate(['password' => 'string|nullable']); - - return $unlock->do($request['albumID'], $request['password']) ? 'true' : 'false'; - } - - /** - * Provided a title and albumIDs, change the title of the albums. - * - * @param Request $request - * - * @return string - */ - public function setTitle(AlbumIDsRequest $request, SetTitle $setTitle) - { - $request->validate(['title' => 'string|required|max:100']); - - return $setTitle->do(explode(',', $request['albumIDs']), $request['title']) ? 'true' : 'false'; - } - - /** - * Change the sharing properties of the album. - * - * @param Request $request - * - * @return bool|string - */ - public function setPublic(AlbumIDRequestInt $request, SetPublic $setPublic) - { - $validated = $request->validate([ - 'public' => 'integer|required', - 'visible' => 'integer|required', - 'nsfw' => 'integer|required', - 'downloadable' => 'integer|required', - 'share_button_visible' => 'integer|required', - 'full_photo' => 'integer|required', - 'password' => 'sometimes|string|nullable', - ]); - - return $setPublic->do($request['albumID'], $validated) ? 'true' : 'false'; // we should return a 422 or similar - } - - /** - * Change the description of the album. - * - * @param Request $request - * - * @return bool|string - */ - public function setDescription(AlbumIDRequestInt $request, SetDescription $setDescription) - { - $request->validate(['description' => 'string|nullable|max:1000']); - - return $setDescription->do($request['albumID'], $request['description'] ?? '') ? 'true' : 'false'; - } - - /** - * Change show tags of the tag album. - * - * @param Request $request - * - * @return bool|string - */ - public function setShowTags(AlbumIDRequestInt $request, SetShowTags $setShowTags) - { - $request->validate(['show_tags' => 'string|required|max:1000|min:1']); - - return $setShowTags->do($request['albumID'], $request['show_tags']) ? 'true' : 'false'; - } - - /** - * Set cover image of the album. - * - * @param Request $request - * - * @return bool|string - */ - public function setCover(AlbumIDRequestInt $request, SetCover $setCover) - { - $request->validate([ - 'photoID' => 'integer|nullable', - ]); - - return $setCover->do($request['albumID'], $request['photoID']) ? 'true' : 'false'; - } - - /** - * Set the license of the Album. - * - * @param Request $request - * - * @return string - */ - public function setLicense(AlbumIDRequestInt $request, SetLicense $setLicense) - { - $request->validate(['license' => 'required|string']); - - $licenses = Helpers::get_all_licenses(); - - if (!in_array($request['license'], $licenses, true)) { - Logs::error(__METHOD__, __LINE__, 'License not recognised: ' . $request['license']); - - return Response::error('License not recognised!'); - } - - return $setLicense->do($request['albumID'], $request['license']) ? 'true' : 'false'; - } - - /** - * Delete the album and all pictures in the album. - * - * @param Request $request - * - * @return string - */ - public function delete(AlbumIDsRequest $request, Delete $delete) - { - return $delete->do($request['albumIDs']) ? 'true' : 'false'; - } - - /** - * Merge albums. The first of the list is the destination of the merge. - * - * @param Request $request - * - * @return string - */ - public function merge(AlbumIDsRequest $request, Merge $merge) - { - // Convert to array - $albumIDs = explode(',', $request['albumIDs']); - // Get first albumID - $albumID = array_shift($albumIDs); - - return $merge->do($albumID, $albumIDs) ? 'true' : 'false'; - } - - /** - * Move multiple albums into another album. - * - * @param Request $request - * - * @return string - */ - public function move(AlbumIDsRequest $request, Move $move) - { - // Convert to array - $albumIDs = explode(',', $request['albumIDs']); - - // Get first albumID - $albumID = array_shift($albumIDs); - - return $move->do($albumID, $albumIDs) ? 'true' : 'false'; - } - - /** - * Set if an album contains sensitive pictures. - * - * @param Request $request - * - * @return string - */ - public function setNSFW(Request $request, SetNSFW $setNSFW) - { - $request->validate(['albumID' => 'required|string']); - - return $setNSFW->do($request['albumID'], '_') ? 'true' : 'false'; - } - - /** - * Define the default sorting type. - * - * @param Request $request - * - * @return string - */ - public function setSorting(AlbumIDRequest $request, SetSorting $setSorting) - { - $validated = $request->validate([ - 'typePhotos' => 'nullable', - 'orderPhotos' => 'required|string', - ]); - - return $setSorting->do($request['albumID'], $validated) ? 'true' : 'false'; - } - - /** - * Return the archive of the pictures of the album and its subalbums. - * - * @param Request $request - * - * @return string|StreamedResponse - */ - public function getArchive(AlbumIDsRequest $request, Archive $archive) - { - if (Storage::getDefaultDriver() === 's3') { - Logs::error(__METHOD__, __LINE__, 'getArchive not implemented for S3'); - - return 'false'; - } - - $albumIDs = explode(',', $request['albumIDs']); - - return $archive->do($albumIDs); - } -} diff --git a/app/Http/Controllers/AlbumsController.php b/app/Http/Controllers/AlbumsController.php deleted file mode 100644 index e50083b6764..00000000000 --- a/app/Http/Controllers/AlbumsController.php +++ /dev/null @@ -1,57 +0,0 @@ - null, - 'albums' => null, - 'shared_albums' => null, - ]; - - // $toplevel containts Collection[Album] accessible at the root: albums shared_albums. - $toplevel = $top->get(); - - $return['albums'] = $prepareAlbums->do($toplevel['albums']); - $return['shared_albums'] = $prepareAlbums->do($toplevel['shared_albums']); - - $return['smartalbums'] = $smart->get(); - - return $return; - } - - /** - * @return array as the full tree of visible albums - */ - public function tree(Tree $tree) - { - return $tree->get(); - } - - /** - * @return array|string returns an array of photos of all albums or false on failure - */ - public function getPositionData(PositionData $positionData) - { - return $positionData->do(); - } -} diff --git a/app/Http/Controllers/AuthController.php b/app/Http/Controllers/AuthController.php new file mode 100644 index 00000000000..5b83ccf5d98 --- /dev/null +++ b/app/Http/Controllers/AuthController.php @@ -0,0 +1,94 @@ + $request->username(), + 'password' => $request->password(), + ])) { + Log::channel('login')->notice(__METHOD__ . ':' . __LINE__ . ' -- User (' . $request->username() . ') has logged in from ' . $request->ip()); + + return; + } + + Log::channel('login')->error(__METHOD__ . ':' . __LINE__ . ' -- User (' . $request->username() . ') has tried to log in from ' . $request->ip()); + throw new UnauthenticatedException('Unknown user or invalid password'); + } + + /** + * Unsets the session values. + * + * @return void + */ + public function logout(): void + { + Auth::logout(); + Session::flush(); + } + + /** + * Get the global rights of the current user. + */ + public function getGlobalRights(): GlobalRightsResource + { + return new GlobalRightsResource(); + } + + /** + * First function being called via AJAX. + * + * @return UserResource + */ + public function getCurrentUser(): UserResource + { + /** @var User|null $user */ + $user = Auth::user(); + + return new UserResource($user); + } + + /** + * Return the configuration for the authentication. + * + * @return AuthConfig + */ + public function getConfig(): AuthConfig + { + return new AuthConfig(); + } +} diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php deleted file mode 100644 index 3a406222441..00000000000 --- a/app/Http/Controllers/Controller.php +++ /dev/null @@ -1,15 +0,0 @@ -route('home'); - } - - $functions = []; - - /** - * Session::init. - */ - $session_init = resolve(SessionController::class); - $return_session = []; - $return_session['name'] = 'Session::init()'; - $return_session['type'] = 'string'; - $return_session['data'] = json_encode($session_init->init()); - - $functions[] = $return_session; - - /** - * Albums::get. - */ - $albums_controller = resolve(AlbumsController::class); - $top = resolve(Top::class); - $smart = resolve(Smart::class); - $prepareAlbums = resolve(AlbumsPrepare::class); - - $return_albums = []; - $return_albums['name'] = 'Albums::get'; - $return_albums['type'] = 'string'; - $return_albums['data'] = json_encode($albums_controller->get($top, $smart, $prepareAlbums)); - - $functions[] = $return_albums; - - /** - * Album::get. - */ - $return_album_list = []; - $return_album_list['name'] = 'Album::get'; - $return_album_list['type'] = 'array'; - $return_album_list['kind'] = 'albumID'; - $return_album_list['array'] = []; - - /** - * @var Collection[Album] - */ - $albums = Album::where('public', '=', '1') - ->where('viewable', '=', '1') - ->get(); - /* - * @var Album - */ - $prepare = resolve(Prepare::class); - foreach ($albums as $album) { - /** - * Copy paste from Album::get(). - */ - // Get photos - // Get album information - - $return_album_json = $prepare->do($album); - - $return_album = []; - $return_album['id'] = $album->id; - $return_album['data'] = json_encode($return_album_json); - - $return_album_list['array'][] = $return_album; - } - - $functions[] = $return_album_list; - - /** - * Photo::get. - */ - $return_photo_list = []; - $return_photo_list['name'] = 'Photo::get'; - $return_photo_list['type'] = 'array'; - $return_photo_list['kind'] = 'photoID'; - $return_photo_list['array'] = []; - - foreach ($albums as $album) { - /** @var Photo $photo */ - foreach ($album->photos as $photo) { - $return_photo = []; - $return_photo_json = $photo->toReturnArray(); - $return_photo_json['original_album'] = $return_photo_json['album']; - $return_photo_json['album'] = $album->id; - $return_photo['id'] = $photo->id; - $return_photo['data'] = json_encode($return_photo_json); - - $return_photo_list['array'][] = $return_photo; - } - } - - $functions[] = $return_photo_list; - - $contents = view('demo', ['functions' => $functions]); - $response = Response::make($contents, 200); - $response->header('Content-Type', 'text/plain'); - - return $response; - } -} diff --git a/app/Http/Controllers/FrameController.php b/app/Http/Controllers/FrameController.php deleted file mode 100644 index 26f56ce31cc..00000000000 --- a/app/Http/Controllers/FrameController.php +++ /dev/null @@ -1,67 +0,0 @@ -configFunctions = $configFunctions; - } - - /** - * Return the page /frame if enabled. - * - * @return false|string - */ - public function init() - { - Configs::get(); - - if (Configs::get_value('Mod_Frame') != '1') { - return redirect()->route('home'); - } - - $lang = Lang::get_lang(Configs::where('key', '=', 'lang')->first()); - $lang['language'] = Configs::get_value('lang'); - - $infos = $this->configFunctions->get_pages_infos(); - $title = Configs::get_value('site_title'); - - return view('frame', ['locale' => $lang, 'title' => $title, 'infos' => $infos, 'rss_enable' => false]); - } - - /** - * Return is the refresh rate of the the Frame if it is enabled. - * - * @return array|string - */ - public function getSettings() - { - Configs::get(); - - if (Configs::get_value('Mod_Frame') != '1') { - return Response::error('Frame is not enabled'); - } - - $return = []; - $return['refresh'] = Configs::get_value('Mod_Frame_refresh') * 1000; - - return $return; - } -} diff --git a/app/Http/Controllers/Gallery/AlbumController.php b/app/Http/Controllers/Gallery/AlbumController.php new file mode 100644 index 00000000000..2a73eb4518d --- /dev/null +++ b/app/Http/Controllers/Gallery/AlbumController.php @@ -0,0 +1,376 @@ +album()); + $albumResource = null; + + if ($config->is_accessible) { + $albumResource = match (true) { + $request->album() instanceof BaseSmartAlbum => new SmartAlbumResource($request->album()), + $request->album() instanceof TagAlbum => new TagAlbumResource($request->album()), + $request->album() instanceof Album => new AlbumResource($request->album()), + default => throw new LycheeLogicException('This should not happen'), + }; + } + + return new AbstractAlbumResource($config, $albumResource); + } + + /** + * Create an album. + * + * @param AddAlbumRequest $request + * + * @return string + */ + public function createAlbum(AddAlbumRequest $request): string + { + /** @var int $ownerId */ + $ownerId = Auth::id() ?? throw new UnauthenticatedException(); + $create = new Create($ownerId); + + return $create->create($request->title(), $request->parent_album())->id; + } + + /** + * Create a tag album. + * + * @param AddTagAlbumRequest $request + * + * @return string + */ + public function createTagAlbum(AddTagAlbumRequest $request, CreateTagAlbum $create): string + { + return $create->create($request->title(), $request->tags())->id; + } + + /** + * Update the info of an Album. + * + * @param UpdateAlbumRequest $request + * + * @return EditableBaseAlbumResource + */ + public function updateAlbum(UpdateAlbumRequest $request, SetHeader $setHeader): EditableBaseAlbumResource + { + $album = $request->album(); + if ($album === null) { + throw new LycheeLogicException('album is null'); + } + $album->title = $request->title(); + $album->description = $request->description(); + $album->license = $request->license(); + $album->album_thumb_aspect_ratio = $request->aspectRatio(); + $album->copyright = $request->copyright(); + $album->photo_sorting = $request->photoSortingCriterion(); + $album->album_sorting = $request->albumSortingCriterion(); + $album->photo_layout = $request->photoLayout(); + + $album->album_timeline = $request->album_timeline(); + $album->photo_timeline = $request->photo_timeline(); + + $album = $setHeader->do( + album: $album, + is_compact: $request->is_compact(), + photo: $request->photo(), + shall_override: true); + + return EditableBaseAlbumResource::fromModel($album); + } + + /** + * Update the info of a Tag Album. + * + * @param UpdateTagAlbumRequest $request + * + * @return EditableBaseAlbumResource + */ + public function updateTagAlbum(UpdateTagAlbumRequest $request): EditableBaseAlbumResource + { + $album = $request->album(); + if ($album === null) { + throw new LycheeLogicException('album is null'); + } + $album->title = $request->title(); + $album->description = $request->description(); + $album->show_tags = $request->tags(); + $album->copyright = $request->copyright(); + $album->photo_sorting = $request->photoSortingCriterion(); + $album->photo_layout = $request->photoLayout(); + $album->photo_timeline = $request->photo_timeline(); + $album->save(); + + return EditableBaseAlbumResource::fromModel($album); + } + + /** + * Update the protection policy of an Abstract Album. + * + * @param SetAlbumProtectionPolicyRequest $request + * @param SetProtectionPolicy $setProtectionPolicy + * @param SetSmartProtectionPolicy $setSmartProtectionPolicy + * + * @return AlbumProtectionPolicy + */ + public function updateProtectionPolicy(SetAlbumProtectionPolicyRequest $request, + SetProtectionPolicy $setProtectionPolicy, + SetSmartProtectionPolicy $setSmartProtectionPolicy): AlbumProtectionPolicy + { + if ($request->album() instanceof BaseSmartAlbum) { + $setSmartProtectionPolicy->do( + $request->album(), + $request->albumProtectionPolicy()->is_public + ); + + return AlbumProtectionPolicy::ofSmartAlbum($request->album()); + } + + /** @var BaseAlbum $album */ + $album = $request->album(); + $setProtectionPolicy->do( + $album, + $request->albumProtectionPolicy(), + $request->isPasswordProvided(), + $request->password() + ); + + return AlbumProtectionPolicy::ofBaseAlbum($album->refresh()); + } + + /** + * Delete the album and all of its pictures. + * + * @param DeleteAlbumsRequest $request the request + * @param Delete $delete the delete action + * + * @return void + */ + public function delete(DeleteAlbumsRequest $request, Delete $delete): void + { + $fileDeleter = $delete->do($request->albumIds()); + App::terminating(fn () => $fileDeleter->do()); + } + + /** + * Get the list of albums. + * + * @param TargetListAlbumRequest $request + * @param ListAlbums $listAlbums + * + * @return array + */ + public function getTargetListAlbums(TargetListAlbumRequest $request, ListAlbums $listAlbums) + { + $albums = $request->albums(); + $parent_id = $albums->count() > 0 ? $albums->first()->parent_id : null; + + return TargetAlbumResource::collect($listAlbums->do($albums, $parent_id)); + } + + /** + * Merge albums. The first of the list is the destination of the merge. + * + * @param MergeAlbumsRequest $request + * @param Merge $merge + * + * @return void + */ + public function merge(MergeAlbumsRequest $request, Merge $merge): void + { + $merge->do($request->album(), $request->albums()); + } + + /** + * Move multiple albums into another album. + * + * @param MoveAlbumsRequest $request + * @param Move $move + * + * @return void + */ + public function move(MoveAlbumsRequest $request, Move $move): void + { + $move->do($request->album(), $request->albums()); + } + + /** + * Transfer the ownership of the album to another user. + * + * @param TransferAlbumRequest $request + * @param Transfer $transfer + * + * @return void + */ + public function transfer(TransferAlbumRequest $request, Transfer $transfer): void + { + $transfer->do($request->album(), $request->user2()->id); + } + + /** + * Set the album cover (the square thumb). + * + * @param SetAsCoverRequest $request + * + * @return void + */ + public function cover(SetAsCoverRequest $request): void + { + $album = $request->album(); + $album->cover_id = ($album->cover_id === $request->photo()->id) ? null : $request->photo()->id; + $album->save(); + } + + /** + * Set the album header (the hero banner). + * + * @param $request + * + * @return void + */ + public function header(SetAsHeaderRequest $request, SetHeader $setHeader): void + { + $setHeader->do($request->album(), $request->is_compact(), $request->photo()); + } + + /** + * Rename an album. + * + * @param RenameAlbumRequest $request + * + * @return void + */ + public function rename(RenameAlbumRequest $request): void + { + $album = $request->album(); + $album->title = $request->title(); + $album->save(); + } + + /** + * Return the archive of the pictures of the album and its sub-albums. + * + * @param ZipRequest $request + * @param AlbumArchive $album_archive + * @param PhotoArchive $photo_archive + * + * @return StreamedResponse + */ + public function getArchive(ZipRequest $request, AlbumArchive $album_archive, PhotoArchive $photo_archive): StreamedResponse + { + if ($request->albums()->count() > 0) { + return $album_archive->do($request->albums()); + } + + return $photo_archive->do($request->photos(), $request->sizeVariant()); + } + + /** + * Provided the albumID and password, return whether the album can be accessed or not. + * + * @param UnlockAlbumRequest $request + * @param Unlock $unlock + * + * @return void + */ + public function unlock(UnlockAlbumRequest $request, Unlock $unlock): void + { + $unlock->do($request->album(), $request->password()); + } + + /** + * Upload a track for the Album. + * + * @param SetAlbumTrackRequest $request + * + * @return void + */ + public function setTrack(SetAlbumTrackRequest $request): void + { + $request->album()->setTrack($request->file); + } + + /** + * Delete a track from the Album. + * + * @param DeleteTrackRequest $request + * + * @return void + */ + public function deleteTrack(DeleteTrackRequest $request): void + { + $request->album()->deleteTrack(); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Gallery/AlbumsController.php b/app/Http/Controllers/Gallery/AlbumsController.php new file mode 100644 index 00000000000..d6af7b624ac --- /dev/null +++ b/app/Http/Controllers/Gallery/AlbumsController.php @@ -0,0 +1,30 @@ +get(), new RootConfig()); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Gallery/ConfigController.php b/app/Http/Controllers/Gallery/ConfigController.php new file mode 100644 index 00000000000..b75ec2b88c7 --- /dev/null +++ b/app/Http/Controllers/Gallery/ConfigController.php @@ -0,0 +1,59 @@ +photoQueryPolicy = resolve(PhotoQueryPolicy::class); + } + + /** + * Return an image and the timeout if the frame is supported. + * + * @param FrameRequest $request + * + * @return FrameData + */ + public function get(FrameRequest $request): FrameData + { + $timeout = Configs::getValueAsInt('mod_frame_refresh'); + $photo = $this->loadPhoto($request->album(), 5); + + if ($photo === null) { + return new FrameData($timeout, '', ''); + } + + $src = $photo->size_variants->getMedium()?->url ?? $photo->size_variants->getOriginal()?->url; + + if ($photo->size_variants->getMedium() !== null && $photo->size_variants->getMedium2x() !== null) { + $srcset = $photo->size_variants->getMedium()->url . ' ' . $photo->size_variants->getMedium()->width . 'w,'; + $srcset .= $photo->size_variants->getMedium2x()->url . ' ' . $photo->size_variants->getMedium2x()->width . 'w'; + } else { + $srcset = ''; + } + + return new FrameData($timeout, $src, $srcset); + } + + /** + * Return the full random image data instead of just the URLs. + * + * @param FrameRequest $request + * + * @return PhotoResource + */ + public function random(FrameRequest $request): PhotoResource + { + $photo = $this->loadPhoto($request->album(), 5); + + return PhotoResource::fromModel($photo); + } + + /** + * Recursively search for a photo to display. + * + * @param AbstractAlbum|null $album + * @param int $retries + * + * @return Photo|null + */ + private function loadPhoto(AbstractAlbum|null $album, int $retries = 5): ?Photo + { + // avoid infinite recursion + if ($retries === 0) { + return null; + } + + $query = null; + + // default query + if ($album === null) { + $query = $this->photoQueryPolicy->applySearchabilityFilter( + query: Photo::query()->with(['album', 'size_variants', 'size_variants.sym_links']), + origin: null, + include_nsfw: !Configs::getValueAsBool('hide_nsfw_in_frame') + ); + } else { + $query = $album->photos()->with(['album', 'size_variants', 'size_variants.sym_links']); + } + + /** @var ?Photo $photo */ + // PHPStan does not understand that `firstOrFail` returns `Photo`, but assumes that it returns `Model` + // @phpstan-ignore-next-line + $photo = $query->inRandomOrder()->first(); + if ($photo === null) { + $album === null ? throw new PhotoCollectionEmptyException() : throw new PhotoCollectionEmptyException('Photo collection of ' . $album->title . ' is empty'); + } + + // retry + if ($photo->isVideo()) { + return $this->loadPhoto($album, $retries - 1); + } + + return $photo; + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Gallery/MapController.php b/app/Http/Controllers/Gallery/MapController.php new file mode 100644 index 00000000000..7bc8829cc59 --- /dev/null +++ b/app/Http/Controllers/Gallery/MapController.php @@ -0,0 +1,58 @@ +rootPositionData = resolve(RootPositionData::class); + $this->albumPositionData = resolve(AlbumPositionData::class); + } + + /** + * Return the configuration data for the Map. + * + * @return MapProviderData + */ + public function getProvider(): MapProviderData + { + return new MapProviderData(); + } + + /** + * Return the Map data for an album or root. + * + * @param MapDataRequest $request + * + * @return PositionDataResource + */ + public function getData(MapDataRequest $request): PositionDataResource + { + $album = $request->album(); + + if ($album === null) { + return $this->rootPositionData->do(); + } + + $includeSubAlbums = Configs::getValueAsBool('map_include_subalbums'); + + return $this->albumPositionData->get($album, $includeSubAlbums); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Gallery/PhotoController.php b/app/Http/Controllers/Gallery/PhotoController.php new file mode 100644 index 00000000000..5d57fa3d477 --- /dev/null +++ b/app/Http/Controllers/Gallery/PhotoController.php @@ -0,0 +1,258 @@ +meta(); + $file = new UploadedFile($request->uploaded_file_chunk()); + + // Set up meta data if not already present + $meta->extension ??= '.' . pathinfo($meta->file_name, PATHINFO_EXTENSION); + $meta->uuid_name ??= strtr(base64_encode(random_bytes(12)), '+/', '-_') . $meta->extension; + + $final = new NativeLocalFile(Storage::disk(self::DISK_NAME)->path($meta->uuid_name)); + $final->append($file->read()); + + if ($meta->chunk_number < $meta->total_chunks) { + // Not the last chunk + return $meta; + } + + // Last chunk + $meta->stage = FileStatus::PROCESSING; + + return $this->process($final, $request->album(), $request->file_last_modified_time(), $meta); + } + + private function process( + NativeLocalFile $final, + ?AbstractAlbum $album, + ?int $file_last_modified_time, + UploadMetaResource $meta): UploadMetaResource + { + $processableFile = new ProcessableJobFile( + $final->getOriginalExtension(), + $meta->file_name + ); + $processableFile->write($final->read()); + + $final->close(); + $final->delete(); + $processableFile->close(); + // End of work-around + + if (Configs::getValueAsBool('use_job_queues')) { + ProcessImageJob::dispatch($processableFile, $album, $file_last_modified_time); + $meta->stage = FileStatus::READY; + + return $meta; + } + + $job = new ProcessImageJob($processableFile, $album, $file_last_modified_time); + $job->handle(resolve(AlbumFactory::class)); + $meta->stage = FileStatus::DONE; + + return $meta; + } + + /** + * Upload a picture from a URL. + * + * @param FromUrlRequest $request + * @param FromUrl $fromUrl + * + * @return string + */ + public function fromUrl(FromUrlRequest $request, FromUrl $fromUrl): string + { + /** @var int $userId */ + $userId = Auth::id(); + $fromUrl->do($request->urls(), $request->album(), $userId); + + return 'success'; + } + + /** + * Update the info of a picture. + * + * @param EditPhotoRequest $request + * + * @return PhotoResource + */ + public function update(EditPhotoRequest $request): PhotoResource + { + $photo = $request->photo(); + $photo->title = $request->title(); + $photo->description = $request->description(); + $photo->created_at = $request->uploadDate(); + $photo->tags = $request->tags(); + $photo->license = $request->license()->value; + $photo->save(); + + return PhotoResource::fromModel($photo); + } + + /** + * Set the is-starred attribute of the given photos. + * + * @param SetPhotosStarredRequest $request + * + * @return void + */ + public function star(SetPhotosStarredRequest $request): void + { + foreach ($request->photos() as $photo) { + $photo->is_starred = $request->isStarred(); + $photo->save(); + } + } + + /** + * Moves the photos to an album. + * + * @param MovePhotosRequest $request + * @param Move $move + * + * @return void + */ + public function move(MovePhotosRequest $request, Move $move): void + { + $move->do($request->photos(), $request->album()); + } + + /** + * Delete one or more photos. + * + * @param DeletePhotosRequest $request + * @param Delete $delete + * + * @return void + */ + public function delete(DeletePhotosRequest $request, Delete $delete): void + { + $fileDeleter = $delete->do($request->photoIds()); + App::terminating(fn () => $fileDeleter->do()); + } + + /** + * Given a photoID and a direction (+1: 90° clockwise, -1: 90° counterclockwise) rotate an image. + * + * @param RotatePhotoRequest $request + * + * @return PhotoResource + */ + public function rotate(RotatePhotoRequest $request): PhotoResource + { + if (!Configs::getValueAsBool('editor_enabled')) { + throw new ConfigurationException('support for rotation disabled by configuration'); + } + + $rotateStrategy = new Rotate($request->photo(), $request->direction()); + $photo = $rotateStrategy->do(); + + return PhotoResource::fromModel($photo); + } + + /** + * Copy a photos to an album. + * Only the SQL entry is duplicated for space reason. + * + * @param CopyPhotosRequest $request + * @param Duplicate $duplicate + * + * @return void + */ + public function copy(CopyPhotosRequest $request, Duplicate $duplicate): void + { + $duplicate->do($request->photos(), $request->album()); + } + + /** + * Rename a photo. + * + * @param RenamePhotoRequest $request + * + * @return void + */ + public function rename(RenamePhotoRequest $request): void + { + $photo = $request->photo(); + $photo->title = $request->title; + $photo->save(); + } + + /** + * Set the tags of a photo. + * + * @param SetPhotosTagsRequest $request + * + * @return void + */ + public function tags(SetPhotosTagsRequest $request): void + { + $tags = $request->tags(); + + /** @var Photo $photo */ + foreach ($request->photos() as $photo) { + if ($request->shallOverride) { + $photo->tags = $tags; + } else { + $photo->tags = array_unique(array_merge($photo->tags, $tags)); + } + $photo->save(); + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Gallery/SearchController.php b/app/Http/Controllers/Gallery/SearchController.php new file mode 100644 index 00000000000..46d597ca5be --- /dev/null +++ b/app/Http/Controllers/Gallery/SearchController.php @@ -0,0 +1,71 @@ +terms(); + $album = $request->album(); + + if (!$album instanceof Album) { + $album = null; + } + + /** @var LengthAwarePaginator $photoResults */ + /** @disregard P1013 Undefined method withQueryString() (stupid intelephense) */ + $photoResults = $photoSearch + ->sqlQuery($terms, $album) + ->orderBy(ColumnSortingPhotoType::TAKEN_AT->value, OrderSortingType::ASC->value) + ->paginate(Configs::getValueAsInt('search_pagination_limit')); + + $albumResults = $albumSearch->queryAlbums($terms); + + return ResultsResource::fromData($albumResults, $photoResults); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Gallery/SharingController.php b/app/Http/Controllers/Gallery/SharingController.php new file mode 100644 index 00000000000..444029e6b64 --- /dev/null +++ b/app/Http/Controllers/Gallery/SharingController.php @@ -0,0 +1,125 @@ + + */ + public function create(AddSharingRequest $request, Share $share): array + { + // delete any already created. + AccessPermission::whereIn('user_id', $request->userIds()) + ->whereIn('base_album_id', $request->albumIds()) + ->delete(); + + $access_permissions = []; + // Not optimal, but this is barely used, so who cares. + // A better approach would be to do a massive insert in a single SQL query from the cross product. + foreach ($request->userIds() as $user_id) { + foreach ($request->albumIds() as $album_id) { + $access_permissions[] = $share->do($request->permResource(), $user_id, $album_id); + } + } + + return AccessPermissionResource::collect($access_permissions); + } + + /** + * Edit sharing permissions. + * + * @param EditSharingRequest $request + * + * @return AccessPermissionResource + */ + public function edit(EditSharingRequest $request): AccessPermissionResource + { + $perm = $request->perm(); + $perm->update([ + 'grants_full_photo_access' => $request->permResource()->grants_full_photo_access, + 'grants_download' => $request->permResource()->grants_download, + 'grants_upload' => $request->permResource()->grants_upload, + 'grants_edit' => $request->permResource()->grants_edit, + 'grants_delete' => $request->permResource()->grants_delete, + ]); + + return AccessPermissionResource::fromModel($perm); + } + + /** + * List sharing permissions. + * + * @param ListSharingRequest $request + * + * @return Collection + */ + public function list(ListSharingRequest $request): Collection + { + $query = AccessPermission::with(['album', 'user']); + $query = $query->whereNotNull(APC::USER_ID); + $query = $query->where(APC::BASE_ALBUM_ID, '=', $request->album()->id); + + return AccessPermissionResource::collect($query->get()); + } + + /** + * List all sharing permissions. + * + * @param ListAllSharingRequest $request + * + * @return Collection + */ + public function listAll(ListAllSharingRequest $request): Collection + { + $query = AccessPermission::with(['album', 'user']); + $query = $query->when( + !Auth::user()->may_administrate, + fn ($q) => $q->whereIn('base_album_id', BaseAlbumImpl::select('id') + ->where('owner_id', '=', Auth::id()))); + $query = $query->whereNotNull('user_id'); + $query = $query->orderBy('base_album_id', 'asc'); + + return AccessPermissionResource::collect($query->get()); + } + + /** + * Delete sharing permissions. + * + * @param DeleteSharingRequest $request + * + * @return void + */ + public function delete(DeleteSharingRequest $request): void + { + AccessPermission::query()->where('id', '=', $request->perm()->id)->delete(); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/HoneyPotController.php b/app/Http/Controllers/HoneyPotController.php new file mode 100644 index 00000000000..473fe4d82c4 --- /dev/null +++ b/app/Http/Controllers/HoneyPotController.php @@ -0,0 +1,44 @@ + + */ + private array $pipes = [ + HoneyIsActive::class, + EnvAccessTentative::class, + FlaggedPathsAccessTentative::class, + DefaultNotFound::class, + ]; + + public function __invoke(string $path = ''): void + { + app(Pipeline::class) + ->send($path) + ->through($this->pipes) + ->thenReturn(); + } +} diff --git a/app/Http/Controllers/ImportController.php b/app/Http/Controllers/ImportController.php deleted file mode 100644 index 34f521f80dc..00000000000 --- a/app/Http/Controllers/ImportController.php +++ /dev/null @@ -1,54 +0,0 @@ -do($urls, $request['albumID']) ? 'true' : 'false'; - } - - /** - * @param ImportServerRequest $request - * - * @return bool|string - * - * @throws ImagickException - */ - public function server(ImportServerRequest $request, FromServer $fromServer) - { - $validated = $request->validated(); - Session::forget('cancel'); - - return $fromServer->do($validated); - } - - public function serverCancel() - { - Session::put('cancel', true); - - return 'true'; - } -} diff --git a/app/Http/Controllers/IndexController.php b/app/Http/Controllers/IndexController.php deleted file mode 100644 index d1cd04e012b..00000000000 --- a/app/Http/Controllers/IndexController.php +++ /dev/null @@ -1,103 +0,0 @@ -configFunctions = $configFunctions; - $this->symLinkFunctions = $symLinkFunctions; - $this->lycheeVersion = $lycheeVersion; - } - - /** - * Display the landing page if enabled - * otherwise display the gallery. - * - * @return View - */ - public function show() - { - if (Configs::get_value('landing_page_enable', '0') == '1') { - $lang = Lang::get_lang(Configs::get_value('lang')); - $lang['language'] = Configs::get_value('lang'); - - $infos = $this->configFunctions->get_pages_infos(); - - $menus = Page::menu()->get(); - - $title = Configs::get_value('site_title', Config::get('defines.defaults.SITE_TITLE')); - $rss_enable = (Configs::get_value('rss_enable', '0') == '1') ? true : false; - - $page_config = []; - $page_config['show_hosted_by'] = false; - $page_config['display_socials'] = false; - - return view('landing', ['locale' => $lang, 'title' => $title, 'infos' => $infos, 'menus' => $menus, 'page_config' => $page_config, 'rss_enable' => $rss_enable]); - } - - return $this->gallery(); - } - - /** - * Just call the phpinfo function. - * Cannot be tested. - * - * @return string - */ - // @codeCoverageIgnoreStart - public function phpinfo() - { - return (string) phpinfo(); - } - - // @codeCoverageIgnoreEnd - - /** - * Display the gallery. - * - * @return View - */ - public function gallery() - { - $this->symLinkFunctions->remove_outdated(); - $infos = $this->configFunctions->get_pages_infos(); - - $lang = Lang::get_lang(Configs::get_value('lang')); - $lang['language'] = Configs::get_value('lang'); - - $title = Configs::get_value('site_title', Config::get('defines.defaults.SITE_TITLE')); - $rss_enable = (Configs::get_value('rss_enable', '0') == '1') ? true : false; - $page_config = []; - $page_config['show_hosted_by'] = true; - $page_config['display_socials'] = Configs::get_value('display_social_in_gallery', '0') == '1'; - - return view('gallery', ['locale' => $lang, 'title' => $title, 'infos' => $infos, 'page_config' => $page_config, 'rss_enable' => $rss_enable]); - } -} diff --git a/app/Http/Controllers/Install/EnvController.php b/app/Http/Controllers/Install/EnvController.php index e5631cda870..cfe7b7a0660 100644 --- a/app/Http/Controllers/Install/EnvController.php +++ b/app/Http/Controllers/Install/EnvController.php @@ -1,47 +1,53 @@ has('envConfig')) { - $env = str_replace("\r", '', $request->get('envConfig')); - try { + try { + if ($request->has('envConfig')) { + $env = str_replace("\r", '', $request->get('envConfig')); file_put_contents(base_path('.env'), $env, LOCK_EX); - } catch (\Exception $e) { - $oups = new PanicAttack(); - $oups->handle($e->getMessage()); + $exists = true; + } elseif (file_exists(base_path('.env'))) { + $env = file_get_contents(base_path('.env')); + $exists = true; + } else { + $env = file_get_contents(base_path('.env.example')); + $exists = false; } - $exists = true; - } - return view('install.env', [ - 'title' => 'Lychee-installer', - 'step' => 3, - 'env' => $env, - 'exists' => $exists, - ]); + return view('install.env', [ + 'title' => 'Lychee-installer', + 'step' => 3, + 'env' => $env, + 'exists' => $exists, + ]); + } catch (\ErrorException $e) { + // possibly thrown by low-level methods like `file_put_contents` + throw new InstallationFailedException('I/O error for file `.env`', $e); + } } } diff --git a/app/Http/Controllers/Install/MigrationController.php b/app/Http/Controllers/Install/MigrationController.php index ff3d8737dc5..d9a749769a8 100644 --- a/app/Http/Controllers/Install/MigrationController.php +++ b/app/Http/Controllers/Install/MigrationController.php @@ -1,57 +1,58 @@ applyMigration = $applyMigration; - } - - /** - * @return array + * Migrates the Lychee DB and generates a new API key. + * + * @return View */ - public function view() + public function view(): View { $output = []; - - $error = $this->applyMigration->migrate($output); - $output[] = ''; - if (!$error) { - $error = $this->applyMigration->keyGenerate($output); - } - $output[] = ''; - if (!$error) { - $this->installed($output); + $hasErrors = false; + try { + $output = app(Pipeline::class) + ->send($output) + ->through([ + ArtisanViewClear::class, + ArtisanMigrate::class, + QueryExceptionChecker::class, + Spacer::class, + ArtisanKeyGenerate::class, + Spacer::class, + ]) + ->thenReturn(); + } catch (InstallationFailedException) { + $hasErrors = true; } - $error = $error ? true : null; return view('install.migrate', [ 'title' => 'Lychee-installer', 'step' => 4, 'lines' => $output, - 'errors' => $error, + 'errors' => $hasErrors, ]); } - - /** - * @param array $output - */ - public function installed(array &$output) - { - $dateStamp = date('Y-m-d H:i:s'); - $message = 'Lychee INSTALLED on ' . $dateStamp; - file_put_contents(base_path('installed.log'), $message); - $output[] = $message; - $output[] = 'Created installed.log'; - } } diff --git a/app/Http/Controllers/Install/PermissionsController.php b/app/Http/Controllers/Install/PermissionsController.php index ee452022480..95f84fb6857 100644 --- a/app/Http/Controllers/Install/PermissionsController.php +++ b/app/Http/Controllers/Install/PermissionsController.php @@ -1,25 +1,26 @@ permissions->check( $this->config->get_permissions() diff --git a/app/Http/Controllers/Install/RequirementsController.php b/app/Http/Controllers/Install/RequirementsController.php index 890f615d393..0c06fd93679 100644 --- a/app/Http/Controllers/Install/RequirementsController.php +++ b/app/Http/Controllers/Install/RequirementsController.php @@ -1,25 +1,26 @@ requirements->checkPHPversion( + $phpSupportInfo = $this->requirements->checkPHPVersion( $this->config->get_core()['minPhpVersion'] ); $reqs = $this->requirements->check( diff --git a/app/Http/Controllers/Install/SetUpAdminController.php b/app/Http/Controllers/Install/SetUpAdminController.php new file mode 100644 index 00000000000..d63583100df --- /dev/null +++ b/app/Http/Controllers/Install/SetUpAdminController.php @@ -0,0 +1,71 @@ + 'Lychee-installer', + 'step' => 5, + ]); + } + + /** + * Set up the admin user. + * Called on POST request. + * + * @return View + */ + public function create(SetUpAdminRequest $request): View + { + $error = ''; + try { + $user = new User(); + $user->may_upload = true; + $user->may_edit_own_settings = true; + $user->may_administrate = true; + $user->username = $request->username(); + $user->password = Hash::make($request->password()); + $user->save(); + } catch (\Throwable $e) { + $error = $e->getMessage(); + $error .= '
' . $e->getPrevious()->getMessage(); + } + + if ($error === '') { + return view('install.setup-success', [ + 'title' => 'Lychee-setup-admin', + 'step' => 5, + ]); + } + + return view('install.setup-admin', [ + 'title' => 'Lychee-setup-admin', + 'step' => 5, + 'error' => $error, + ]); + } +} diff --git a/app/Http/Controllers/Install/WelcomeController.php b/app/Http/Controllers/Install/WelcomeController.php index cb7448fa66e..222f73becf1 100644 --- a/app/Http/Controllers/Install/WelcomeController.php +++ b/app/Http/Controllers/Install/WelcomeController.php @@ -1,15 +1,22 @@ 0, ]); } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/LandingPageController.php b/app/Http/Controllers/LandingPageController.php new file mode 100644 index 00000000000..228ba5625dd --- /dev/null +++ b/app/Http/Controllers/LandingPageController.php @@ -0,0 +1,23 @@ +oauth->validateProviderOrDie($provider); + + // We are already logged in: Registration operation + if (Auth::check()) { + $this->oauth->registerOrDie($providerEnum); + + return redirect(route('profile')); + } + + // Authentication operation + $this->oauth->authenticateOrDie($providerEnum); + + return redirect(route('gallery')); + } + + /** + * Function called to authenticate a user to an Oauth server. + * + * @param string $provider + * + * @return HttpFoundationRedirectResponse + */ + public function authenticate(string $provider) + { + if (Auth::check()) { + throw new UnauthorizedException('User already authenticated.'); + } + + $providerEnum = $this->oauth->validateProviderOrDie($provider); + + return Socialite::driver($providerEnum->value)->redirect(); + } + + /** + * Add some security on registration. + * + * @param string $provider + * + * @return HttpFoundationRedirectResponse + */ + public function register(string $provider) + { + Auth::user() ?? throw new UnauthenticatedException(); + if (!Request::hasValidSignature(false)) { + throw new UnauthorizedException('Registration attempted but not initialized.'); + } + + $providerEnum = $this->oauth->validateProviderOrDie($provider); + Session::put($providerEnum->value, OauthAction::OAUTH_REGISTER); + + return Socialite::driver($providerEnum->value)->redirect(); + } + + /** + * List Oauth data. + * + * @return OauthRegistrationData[]|OauthProvidersType[] + */ + public function list(): array + { + if (Auth::check()) { + return $this->withUserData(); + } + + return $this->available(); + } + + /** + * Delete the Oauth registration for a user. + * + * @param ClearOauthRequest $request + * + * @return void + */ + public function clear(ClearOauthRequest $request): void + { + /** @var User $user */ + $user = Auth::user() ?? throw new UnauthenticatedException(); + $user->oauthCredentials()->where('provider', '=', $request->provider())->delete(); + } + + /** + * List available end points and registrations URLS. + * + * @return OauthRegistrationData[] + */ + private function withUserData(): array + { + $oauthData = []; + + /** @var User $user */ + $user = Auth::user() ?? throw new UnauthenticatedException(); + + $credentials = $user->oauthCredentials()->get(); + + foreach (OauthProvidersType::cases() as $provider) { + $client_id = config('services.' . $provider->value . '.client_id'); + if ($client_id === null || $client_id === '') { + continue; + } + + // We create a signed route for 5 minutes + $route = URL::signedRoute( + name: 'oauth-register', + parameters: ['provider' => $provider->value], + expiration: now()->addMinutes(5), + absolute: false); + + $oauthData[] = new OauthRegistrationData( + providerType: $provider, + isEnabled: $credentials->search(fn (OauthCredential $c) => $c->provider === $provider) !== false, + registrationRoute: $route, + ); + } + + return $oauthData; + } + + /** + * List available end points. + * + * @return OauthProvidersType[] + */ + private function available(): array + { + $oauthAvailable = []; + + foreach (OauthProvidersType::cases() as $oauthProvider) { + $client_id = config('services.' . $oauthProvider->value . '.client_id'); + if ($client_id === null || $client_id === '') { + continue; + } + + $oauthAvailable[] = $oauthProvider; + } + + return $oauthAvailable; + } +} \ No newline at end of file diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php deleted file mode 100644 index 60e3921e805..00000000000 --- a/app/Http/Controllers/PageController.php +++ /dev/null @@ -1,81 +0,0 @@ -configFunctions = $configFunctions; - } - - /** - * given a URL: http://example.com/ - * fetches in the tables if the page exists and returns it - * return 404 otherwise. - * - * @param Request $request - * @param $page - * - * @return View - */ - public function page(Request $request, $page) - { - $page = Page::enabled()->where('link', '/' . $page)->first(); - - if ($page == null) { - abort(404); - } - - $lang = Lang::get_lang(); - $lang['language'] = Configs::get_value('lang'); - - $infos = $this->configFunctions->get_pages_infos(); - $title = Configs::get_value('site_title', Config::get('defines.defaults.SITE_TITLE')); - $rss_enable = (Configs::get_value('rss_enable', '0') == '1') ? true : false; - $menus = Page::menu()->get(); - - $contents = $page->content; - $page_config = []; - $page_config['show_hosted_by'] = false; - $page_config['display_socials'] = false; - - return view('page', ['locale' => $lang, 'title' => $title, 'infos' => $infos, 'menus' => $menus, 'contents' => $contents, 'page_config' => $page_config, 'rss_enable' => $rss_enable]); - } - - /** - * TODO: add function to allow the edition of pages. - * - * @param Request $request - */ - public function edit(Request $request, $page) - { - } - - /** - * TODO: add function to save the edition of pages. - * - * @param Request $request - */ - public function save(Request $request, $page) - { - } -} diff --git a/app/Http/Controllers/PhotoController.php b/app/Http/Controllers/PhotoController.php deleted file mode 100644 index faafef5a852..00000000000 --- a/app/Http/Controllers/PhotoController.php +++ /dev/null @@ -1,291 +0,0 @@ -symLinkFunctions = $symLinkFunctions; - } - - /** - * Given a photoID returns the data of the photo. - * - * @param PhotoIDRequest $request - * - * @return ?array - */ - public function get(PhotoIDRequest $request, Prepare $prepare) - { - /** @var ?Photo $photo */ - $photo = Photo::with('album')->findOrFail($request['photoID']); - - return $prepare->do($photo); - } - - /** - * Return a random public photo (starred) - * This is used in the Frame Controller. - * - * @return array - */ - public function getRandom(Random $random) - { - return $random->do(); - } - - /** - * Add a function given an AlbumID. - * - * @param Request $request - * - * @return false|string - */ - public function add(AlbumIDRequest $request, Create $create) - { - try { - $request->validate(['0' => 'required']); - } catch (ValidationException $e) { - return Response::error('validation failed'); - } - - if (!$request->hasfile('0')) { - return Response::error('missing files'); - } - - // Only process the first photo in the array - $file = $request->file('0'); - - $nameFile = []; - $nameFile['name'] = $file->getClientOriginalName(); - $nameFile['type'] = $file->getMimeType(); - $nameFile['tmp_name'] = $file->getPathName(); - - try { - $res = $create->add($nameFile, $request['albumID'], false, (Configs::get_value('skip_duplicates', '0') === '1')); - } catch (JsonWarning $e) { - $res = $e->render(); - } catch (JsonError $e) { - $res = $e->render(); - } - - return $res; - } - - /** - * Change the title of a photo. - * - * @param Request $request - * - * @return string - */ - public function setTitle(PhotoIDsRequest $request, SetTitle $setTitle) - { - $request->validate(['title' => 'required|string|max:100']); - - return $setTitle->do(explode(',', $request['photoIDs']), $request['title']) ? 'true' : 'false'; - } - - /** - * Set if a photo is a favorite. - * - * @param Request $request - * - * @return string - */ - public function setStar(PhotoIDsRequest $request, SetStar $setStar) - { - return $setStar->do(explode(',', $request['photoIDs']), $request['title']) ? 'true' : 'false'; - } - - /** - * Set the description of a photo. - * - * @param Request $request - * - * @return string - */ - public function setDescription(PhotoIDRequest $request, SetDescription $setDescription) - { - $request->validate(['description' => 'string|nullable']); - - return $setDescription->do($request['photoID'], $request['description'] ?? '') ? 'true' : 'false'; - } - - /** - * Define if a photo is public. - * We do not advise the use of this and would rather see people use albums visibility - * This would highly simplify the code if we remove this. Do we really want to keep it ? - * - * @param Request $request - * - * @return string - */ - public function setPublic(PhotoIDRequest $request, SetPublic $setPublic) - { - return $setPublic->do($request['photoID']) ? 'true' : 'false'; - } - - /** - * Set the tags of a photo. - * - * @param Request $request - * - * @return string - */ - public function setTags(PhotoIDsRequest $request, SetTags $setTags) - { - $request->validate(['tags' => 'string|nullable']); - - return $setTags->do(explode(',', $request['photoIDs']), $request['tags'] ?? '') ? 'true' : 'false'; - } - - /** - * Define the album of a photo. - * - * @param Request $request - * - * @return string - */ - public function setAlbum(PhotoIDsRequest $request, SetAlbum $setAlbum) - { - $request->validate(['albumID' => 'required|string']); - - return $setAlbum->execute(explode(',', $request['photoIDs']), $request['albumID']) ? 'true' : 'false'; - } - - /** - * Define the license of the photo. - * - * @param Request $request - * - * @return false|string - */ - public function setLicense(PhotoIDRequest $request, SetLicense $setLicense) - { - $request->validate(['license' => 'required|string']); - - $licenses = Helpers::get_all_licenses(); - - if (!in_array($request['license'], $licenses, true)) { - Logs::error(__METHOD__, __LINE__, 'License not recognised: ' . $request['license']); - - return Response::error('License not recognised!'); - } - - return $setLicense->do($request['photoID'], $request['license']) ? 'true' : 'false'; - } - - /** - * Delete a photo. - * - * @param Request $request - * - * @return string - */ - public function delete(PhotoIDsRequest $request, Delete $delete) - { - return $delete->do(explode(',', $request['photoIDs'])) ? 'true' : 'false'; - } - - /** - * Duplicate a photo. - * Only the SQL entry is duplicated for space reason. - * - * @param Request $request - * - * @return string - */ - public function duplicate(PhotoIDsRequest $request, Duplicate $duplicate) - { - $request->validate(['albumID' => 'string']); - - $duplicate->do(explode(',', $request['photoIDs']), $request['albumID'] ?? null); - - return 'true'; - } - - /** - * Return the archive of pictures or just a picture if only one. - * - * @param Request $request - * - * @return StreamedResponse|Response|string|void - */ - public function getArchive(PhotoIDsRequest $request, Archive $archive) - { - if (Storage::getDefaultDriver() === 's3') { - Logs::error(__METHOD__, __LINE__, 'getArchive not implemented for S3'); - - return 'false'; - } - - $request->validate([ - 'kind' => 'nullable|string', - ]); - - $photoIDs = explode(',', $request['photoIDs']); - - $response = $archive->do($photoIDs, $request['kind']); - - // Disable caching - $response->headers->set('Cache-Control', 'no-cache, no-store, must-revalidate'); - $response->headers->set('Pragma', 'no-cache'); - $response->headers->set('Expires', '0'); - - return $response; - } - - /** - * GET to manually clear the symlinks. - * - * @return string - * - * @throws \Exception - */ - public function clearSymLink() - { - return $this->symLinkFunctions->clearSymLink(); - } -} diff --git a/app/Http/Controllers/PhotoEditorController.php b/app/Http/Controllers/PhotoEditorController.php deleted file mode 100644 index 0bfb4329b31..00000000000 --- a/app/Http/Controllers/PhotoEditorController.php +++ /dev/null @@ -1,38 +0,0 @@ -validate(['direction' => 'integer|required']); - - $photo = Photo::findOrFail($request['photoID']); - - if (!$rotate->do($photo, intval($request['direction']))) { - return 'false'; - } - - return $prepare->do($photo); - } -} diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php new file mode 100644 index 00000000000..5b1a1d938bc --- /dev/null +++ b/app/Http/Controllers/ProfileController.php @@ -0,0 +1,93 @@ +username() !== null && + $request->username() !== '' && + Configs::getValueAsBool('allow_username_change')) { + $updateLogin->updateUsername($currentUser, $request->username(), $request->ip()); + } + + $currentUser = $updateLogin->updatePassword( + $currentUser, + $request->password() + ); + + $currentUser = $updateLogin->updateEmail( + $currentUser, + $request->email() + ); + + $currentUser->save(); + // Update the session with the new credentials of the user. + // Otherwise, the session is out-of-sync and falsely assumes the user + // to be unauthenticated upon the next request. + Auth::login($currentUser); + + return new UserResource($currentUser); + } + + /** + * Reset the token of the currently authenticated user. + * + * @return UserToken + * + * @throws UnauthenticatedException + * @throws ModelDBException + * @throws \Exception + */ + public function resetToken(ChangeTokenRequest $request, TokenReset $tokenReset): UserToken + { + $token = $tokenReset->do(); + + return new UserToken($token); + } + + /** + * Disable the token of the currently authenticated user. + * + * @return void + * + * @throws UnauthenticatedException + * @throws ModelDBException + */ + public function unsetToken(ChangeTokenRequest $request, TokenDisable $tokenDisable): void + { + $tokenDisable->do(); + } +} diff --git a/app/Http/Controllers/RSSController.php b/app/Http/Controllers/RSSController.php index e52ac7d1921..b5f9b5b21cc 100644 --- a/app/Http/Controllers/RSSController.php +++ b/app/Http/Controllers/RSSController.php @@ -1,22 +1,36 @@ + * + * @throws LycheeException */ - public function getRSS(Generate $generate) + public function getRSS(Generate $generate): Collection { - if (Configs::get_value('rss_enable', '0') != '1') { - abort(404); + if (!Configs::getValueAsBool('rss_enable')) { + throw new ConfigurationException('RSS is disabled by configuration'); } return $generate->do(); diff --git a/app/Http/Controllers/RedirectController.php b/app/Http/Controllers/RedirectController.php deleted file mode 100644 index 13654257400..00000000000 --- a/app/Http/Controllers/RedirectController.php +++ /dev/null @@ -1,50 +0,0 @@ -filled('password')) { - if (Configs::get_value('unlock_password_photos_with_url_param', '0') == '1') { - $unlock->propagate($request['password']); - } else { - $unlock->do($albumid, $request['password']); - } - } - } - - /** - * Trivial redirection. - * - * @param Request $request - * @param string $albumid - */ - public function album(Request $request, $albumid, Unlock $unlock) - { - $this->passwordManagement($request, $albumid, $unlock); - - return redirect('gallery#' . $albumid); - } - - /** - * Trivial redirection. - * - * @param Request $request - * @param string $albumid - * @param string $photoid - */ - public function photo(Request $request, $albumid, $photoid, Unlock $unlock) - { - $this->passwordManagement($request, $albumid, $unlock); - - return redirect('gallery#' . $albumid . '/' . $photoid); - } -} diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php deleted file mode 100644 index f9324863281..00000000000 --- a/app/Http/Controllers/SearchController.php +++ /dev/null @@ -1,65 +0,0 @@ -validate(['term' => 'required|string']); - - $terms = explode(' ', $request['term']); - - $escaped_terms = []; - foreach ($terms as $term) { - $escaped_terms[] = $this->escape_like($term); - } - - // Initialize return var - $return = []; - $return['albums'] = $albumSearch->query($escaped_terms); - $return['photos'] = $photoSearch->query($escaped_terms); - $return['hash'] = md5(json_encode($return)); - - return $return; - } -} diff --git a/app/Http/Controllers/SessionController.php b/app/Http/Controllers/SessionController.php deleted file mode 100644 index 7a0659151c3..00000000000 --- a/app/Http/Controllers/SessionController.php +++ /dev/null @@ -1,163 +0,0 @@ -configFunctions = $configFunctions; - $this->gitHubFunctions = $gitHubFunctions; - } - - /** - * First function being called via AJAX. - * - * @return array|bool (array containing config information or killing the session) - */ - public function init() - { - $logged_in = AccessControl::is_logged_in(); - - // Return settings - $return = []; - - $return['api_V2'] = true; // we are using api_V2 - $return['sub_albums'] = true; // Lychee-laravel does have sub albums - - // Check if login credentials exist and login if they don't - if (AccessControl::noLogin() === true || $logged_in === true) { - // we the the UserID (it is set to 0 if there is no login/password = admin) - $user_id = AccessControl::id(); - - if ($user_id == 0) { - $return['status'] = Config::get('defines.status.LYCHEE_STATUS_LOGGEDIN'); - $return['admin'] = true; - $return['upload'] = true; // not necessary - - $return['config'] = $this->configFunctions->admin(); - - $return['config']['location'] = base_path('public/'); - } else { - $user = User::find($user_id); - - if ($user == null) { - Logs::notice(__METHOD__, __LINE__, 'UserID ' . $user_id . ' not found!'); - - return $this->logout(); - } else { - $return['status'] = Config::get('defines.status.LYCHEE_STATUS_LOGGEDIN'); - - $return['config'] = $this->configFunctions->public(); - $return['lock'] = ($user->lock == '1'); // can user change their password - $return['upload'] = ($user->upload == '1'); // can user upload ? - $return['username'] = $user->username; - } - } - - // here we say whether we looged in because there is no login/password or if we actually entered a login/password - $return['config']['login'] = $logged_in; - $return['config']['lang_available'] = Lang::get_lang_available(); - } else { - // Logged out - $return['config'] = $this->configFunctions->public(); - if (Configs::get_value('hide_version_number', '1') != '0') { - $return['config']['version'] = ''; - } - $return['status'] = Config::get('defines.status.LYCHEE_STATUS_LOGGEDOUT'); - } - - $deviceType = Helpers::getDeviceType(); - // UI behaviour needs to be slightly modified if client is a TV - $return['config_device'] = $this->configFunctions->get_config_device($deviceType); - - // we also return the local - $return['locale'] = Lang::get_lang(Configs::get_value('lang')); - - $return['update_json'] = 0; - $return['update_available'] = false; - - $this->gitHubFunctions->checkUpdates($return); - - return $return; - } - - /** - * Login tentative. - * - * @param Request $request - * - * @return string - */ - public function login(UsernamePasswordRequest $request) - { - // No login - if (AccessControl::noLogin() === true) { - Logs::warning(__METHOD__, __LINE__, 'DEFAULT LOGIN!'); - - return 'true'; - } - - // this is probably sensitive to timing attacks... - if (AccessControl::log_as_admin($request['username'], $request['password'], $request->ip()) === true) { - return 'true'; - } - - if (AccessControl::log_as_user($request['username'], $request['password'], $request->ip()) === true) { - return 'true'; - } - - Logs::error(__METHOD__, __LINE__, 'User (' . $request['username'] . ') has tried to log in from ' . $request->ip()); - - return 'false'; - } - - /** - * Unset the session values. - * - * @return bool returns true when logout was successful - */ - public function logout() - { - Session::flush(); - - return 'true'; - } - - /** - * Show the session values. - */ - public function show() - { - dd(Session::all()); - } -} diff --git a/app/Http/Controllers/StatisticsController.php b/app/Http/Controllers/StatisticsController.php new file mode 100644 index 00000000000..48f0a5cf5ef --- /dev/null +++ b/app/Http/Controllers/StatisticsController.php @@ -0,0 +1,112 @@ + + */ + public function getSpacePerUser(SpacePerUserRequest $request, Spaces $spaces): Collection + { + $spaceData = $spaces->getFullSpacePerUser( + owner_id: $request->ownerId() + ); + + return UserSpace::collect($spaceData); + } + + /** + * Fetch the used space per SizeVariant type. + * + * @param SpaceSizeVariantRequest $request + * @param Spaces $spaces + * + * @return Collection + */ + public function getSpacePerSizeVariantType(SpaceSizeVariantRequest $request, Spaces $spaces): Collection + { + $albumId = $request->album()?->id; + $ownerId = $albumId === null ? $request->ownerId() : null; + + $spaceData = $albumId === null + ? $spaces->getSpacePerSizeVariantTypePerUser(owner_id: $ownerId) + : $spaces->getSpacePerSizeVariantTypePerAlbum(album_id: $albumId); + + return Sizes::collect($spaceData); + } + + /** + * Fetch the used space and number of photos per Album (without descendants). + * + * @param SpacePerAlbumRequest $request + * @param Spaces $spaces + * + * @return Collection + */ + public function getSpacePerAlbum(SpacePerAlbumRequest $request, Spaces $spaces): Collection + { + $albumId = $request->album()?->id; + $ownerId = $albumId === null ? $request->ownerId() : null; + $spaceData = $spaces->getSpacePerAlbum( + album_id: $albumId, + owner_id: $ownerId + ); + $countData = $spaces->getPhotoCountPerAlbum( + album_id: $albumId, + owner_id: $ownerId); + + /** @var Collection $zipped */ + $zipped = $spaceData->zip($countData); + + return $zipped->map(fn ($z) => new Album($z[0], $z[1])); + } + + /** + * Fetch the used space and number of photos per Album with descendants + * ! Slow query. + * + * @param SpacePerAlbumRequest $request + * @param Spaces $spaces + * + * @return Collection + */ + public function getTotalSpacePerAlbum(SpacePerAlbumRequest $request, Spaces $spaces): Collection + { + $albumId = $request->album()?->id; + $ownerId = $albumId === null ? $request->ownerId() : null; + $spaceData = $spaces->getTotalSpacePerAlbum( + album_id: $albumId, + owner_id: $ownerId + ); + $countData = $spaces->getTotalPhotoCountPerAlbum( + album_id: $albumId, + owner_id: $ownerId); + + /** @var Collection $zipped */ + $zipped = $spaceData->zip($countData); + + return $zipped->map(fn ($z) => new Album($z[0], $z[1])); + } +} diff --git a/app/Http/Controllers/UsersController.php b/app/Http/Controllers/UsersController.php new file mode 100644 index 00000000000..f9624254c15 --- /dev/null +++ b/app/Http/Controllers/UsersController.php @@ -0,0 +1,47 @@ + + */ + public function list(ListUsersRequest $_request): Collection + { + return LightUserResource::collect(User::where('id', '!=', Auth::id())->orderBy('username')->get()); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/VersionController.php b/app/Http/Controllers/VersionController.php new file mode 100644 index 00000000000..d034657df30 --- /dev/null +++ b/app/Http/Controllers/VersionController.php @@ -0,0 +1,25 @@ +middleware([]); - } - - /** - * View is only used when sharing a single picture. - * - * @param Request $request - * - * @return View|void - */ - public function view(Request $request) - { - $request->validate([ - 'p' => 'required', - ]); - - /** @var Photo $photo */ - $photo = Photo::find($request->get('p')); - - if ($photo == null) { - Logs::error(__METHOD__, __LINE__, 'Could not find photo in database'); - - return abort(404); - } - - // is the picture public ? - $public = $photo->public == '1'; - - // is the album (if exist) public ? - if ($photo->album_id != null) { - $public = $photo->album->public == '1' || $public; - } - // return 403 if not allowed - if (!$public) { - return abort(403); - } - - if ($photo->medium == '1') { - $dir = 'medium'; - } else { - $dir = 'big'; - } - - $title = Configs::get_value('site_title', Config::get('defines.defaults.SITE_TITLE')); - $rss_enable = Configs::get_value('rss_enable', '0') == '1'; - - $url = config('app.url') . $request->server->get('REQUEST_URI'); - $picture = config('app.url') . '/uploads/' . $dir . '/' . $photo->url; - - return view('view', [ - 'url' => $url, - 'photo' => $photo, - 'picture' => $picture, - 'title' => $title, - 'rss_enable' => $rss_enable, - ]); - } -} diff --git a/app/Http/Controllers/VueController.php b/app/Http/Controllers/VueController.php new file mode 100644 index 00000000000..1bbcf64f71b --- /dev/null +++ b/app/Http/Controllers/VueController.php @@ -0,0 +1,91 @@ +findAbstractAlbumOrFail($albumId, false); + + session()->now('access', $this->check($album)); + session()->now('album', $album); + } + + if ($photoId !== null) { + $photo = Photo::findOrFail($photoId); + Gate::authorize(PhotoPolicy::CAN_SEE, [Photo::class, $photo]); + session()->now('photo', $photo); + } + } catch (ModelNotFoundException) { + throw new NotFoundHttpException(); + } + + return view('vueapp'); + } + + /** + * Check if user can access the album. + * + * @param AbstractAlbum $album + * + * @return bool true if access, false if password required + * + * @throws UnauthorizedException if user is not authorized at all + */ + private function check(AbstractAlbum $album): bool + { + $result = Gate::check(AlbumPolicy::CAN_ACCESS, [AbstractAlbum::class, $album]); + if ( + !$result && + $album instanceof BaseAlbum && + $album->public_permissions()?->password !== null + ) { + return false; + } + + return $result ? true : throw new UnauthorizedException(); + } +} diff --git a/app/Http/Controllers/WebAuthn/WebAuthnLoginController.php b/app/Http/Controllers/WebAuthn/WebAuthnLoginController.php new file mode 100644 index 00000000000..d3f45f3e20a --- /dev/null +++ b/app/Http/Controllers/WebAuthn/WebAuthnLoginController.php @@ -0,0 +1,118 @@ +validate([ + 'user_id' => 'sometimes|int', + 'username' => 'sometimes|string', + ]); + + $username = $fields['username'] ?? null; + $authenticatable = $fields['user_id'] ?? ($username !== null ? ['username' => $username] : null); + + return $request->toVerify($authenticatable); + } + + /** + * Log the user in. + * + * 1. We retrieve the credentials candidate + * 2. Double check the challenge is signed. + * 3. Retrieve the User from the credential ID, we will use it to validate later (otherwise keys like yubikey4 are not working). + * 4. Validate the credentials + * 5. Log in on success + * + * @param AssertedRequest $request + * + * @return void + */ + public function login(AssertedRequest $request, AssertionValidator $validator): void + { + $credentials = $request->validated(); + + if (!$this->isSignedChallenge($credentials)) { + throw new HttpException(Response::HTTP_UNPROCESSABLE_ENTITY, 'Response is not signed.'); + } + $associatedUser = $this->retrieveByCredentials($credentials); + + if ($associatedUser === null) { + throw new HttpException(Response::HTTP_UNPROCESSABLE_ENTITY, 'Associated user does not exists.'); + } + + $jsonTransport = new JsonTransport($request->only(AssertionValidation::REQUEST_KEYS)); + + $credential = $validator + ->send(new AssertionValidation($jsonTransport, $associatedUser)) + ->thenReturn() + ->credential; + + if ($credential === null) { + throw new UnauthenticatedException('Invalid credentials'); + } + + /** @var \Illuminate\Contracts\Auth\Authenticatable $authenticatable */ + $authenticatable = $credential->authenticatable; + Auth::login($authenticatable); + } + + /** + * Check if the credentials are for a public key signed challenge. + * + * @param array $credentials + * + * @return bool + */ + private function isSignedChallenge(array $credentials): bool + { + return isset($credentials['id'], $credentials['rawId'], $credentials['response'], $credentials['type']); + } + + /** + * Retrieve a user by the given credentials. + * + * @param array $credentials + * + * @return User|null + */ + public function retrieveByCredentials(array $credentials): User|null + { + /** @var User|null $user */ + $user = User::whereHas('webAuthnCredentials', + fn ($query) => $query->whereKey($credentials['id'])->whereEnabled() + )->first(); + + return $user; + } +} diff --git a/app/Http/Controllers/WebAuthn/WebAuthnManageController.php b/app/Http/Controllers/WebAuthn/WebAuthnManageController.php new file mode 100644 index 00000000000..bbcdab77f7a --- /dev/null +++ b/app/Http/Controllers/WebAuthn/WebAuthnManageController.php @@ -0,0 +1,64 @@ + + * + * @throws UnauthenticatedException + */ + public function list(ListCredentialsRequest $request): Collection + { + /** @var \App\Models\User $user */ + $user = Auth::user() ?? throw new UnauthenticatedException(); + + return WebAuthnResource::collect($user->webAuthnCredentials); + } + + /** + * Delete a WebAuthn credential. + * + * @throws UnauthenticatedException + */ + public function delete(DeleteCredentialRequest $request): void + { + /** @var \App\Models\User $user */ + $user = Auth::user() ?? throw new UnauthenticatedException(); + + $user->webAuthnCredentials()->where('id', $request->getId())->delete(); + } + + /** + * Edit credential. + * + * @param EditCredentialRequest $request + * + * @return void + */ + public function edit(EditCredentialRequest $request): void + { + $credential = $request->getCredential(); + $credential->alias = $request->getAlias(); + $credential->save(); + } +} diff --git a/app/Http/Controllers/WebAuthn/WebAuthnRegisterController.php b/app/Http/Controllers/WebAuthn/WebAuthnRegisterController.php new file mode 100644 index 00000000000..dc0c7af38b9 --- /dev/null +++ b/app/Http/Controllers/WebAuthn/WebAuthnRegisterController.php @@ -0,0 +1,49 @@ +user = Auth::user() ?? throw new UnauthenticatedException(); + + return $request + ->fastRegistration() + ->toCreate(); + } + + /** + * Registers a device for further WebAuthn authentication. + * + * @param AttestedRequest $request + * + * @return void + */ + public function register(AttestedRequest $request): void + { + /** @disregard P1014 */ + $request->user = Auth::user() ?? throw new UnauthenticatedException(); + $request->save(); + } +} diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 7b796e221c3..650eb53811d 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -1,5 +1,11 @@ */ protected $middleware = [ - // \App\Http\Middleware\TrustHosts::class, - \App\Http\Middleware\TrustProxies::class, - // \Fruitcake\Cors\HandleCors::class, - \App\Http\Middleware\PreventRequestsDuringMaintenance::class, + \App\Http\Middleware\FixStatusCode::class, + \Illuminate\Http\Middleware\TrustProxies::class, + \Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance::class, \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, \App\Http\Middleware\TrimStrings::class, \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, @@ -27,36 +32,49 @@ class Kernel extends HttpKernel /** * The application's route middleware groups. * - * @var array + * @var array> */ protected $middlewareGroups = [ 'web' => [ - \App\Http\Middleware\EncryptCookies::class, + 'installation:complete', + 'admin_user:set', + 'accept_content_type:html', + \Illuminate\Cookie\Middleware\EncryptCookies::class, \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, \Illuminate\Session\Middleware\StartSession::class, \Illuminate\Session\Middleware\AuthenticateSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class, \App\Http\Middleware\VerifyCsrfToken::class, \Illuminate\Routing\Middleware\SubstituteBindings::class, + \App\Http\Middleware\DisableCSP::class, ], 'web-admin' => [ - \App\Http\Middleware\EncryptCookies::class, + 'accept_content_type:html', + \Illuminate\Cookie\Middleware\EncryptCookies::class, \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, \Illuminate\Session\Middleware\StartSession::class, \Illuminate\Session\Middleware\AuthenticateSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class, \App\Http\Middleware\VerifyCsrfToken::class, \Illuminate\Routing\Middleware\SubstituteBindings::class, - 'admin', + \App\Http\Middleware\DisableCSP::class, ], - 'install' => [ - \App\Http\Middleware\InstalledCheck::class, + 'web-install' => [ + 'accept_content_type:html', + 'installation:incomplete', ], 'api' => [ - 'throttle:api', + 'accept_content_type:json', + 'content_type:json', + \Illuminate\Cookie\Middleware\EncryptCookies::class, + \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, + \Illuminate\Session\Middleware\StartSession::class, + \Illuminate\Session\Middleware\AuthenticateSession::class, + \Illuminate\View\Middleware\ShareErrorsFromSession::class, + \App\Http\Middleware\VerifyCsrfToken::class, \Illuminate\Routing\Middleware\SubstituteBindings::class, ], ]; @@ -64,26 +82,22 @@ class Kernel extends HttpKernel /** * The application's route middleware. * - * These middleware may be assigned to groups or used individually. + * These middlewares may be assigned to groups or used individually. * - * @var array + * @var array */ - protected $routeMiddleware = [ - // 'auth' => \Illuminate\Auth\Middleware\Authenticate::class, - // 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, - 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, - 'can' => \Illuminate\Auth\Middleware\Authorize::class, - // 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, - // 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, - 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, + protected $middlewareAliases = [ + 'installation' => \App\Http\Middleware\InstallationStatus::class, + 'admin_user' => \App\Http\Middleware\AdminUserStatus::class, + 'migration' => \App\Http\Middleware\MigrationStatus::class, + 'content_type' => \App\Http\Middleware\ContentType::class, + 'accept_content_type' => \App\Http\Middleware\AcceptContentType::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, - // 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, - - 'login' => \App\Http\Middleware\LoginCheck::class, - 'read' => \App\Http\Middleware\ReadCheck::class, - 'admin' => \App\Http\Middleware\AdminCheck::class, - 'upload' => \App\Http\Middleware\UploadCheck::class, - 'installed' => \App\Http\Middleware\DBExists::class, - 'migrated' => \App\Http\Middleware\MigrationCheck::class, + 'login_required_v1' => \App\Legacy\V1\Middleware\LoginRequiredV1::class, // remove me in non-legacy build + 'login_required' => \App\Http\Middleware\LoginRequired::class, + 'cache_control' => \App\Http\Middleware\CacheControl::class, + 'support' => \LycheeVerify\Http\Middleware\VerifySupporterStatus::class, + 'config_integrity' => \App\Http\Middleware\ConfigIntegrity::class, + 'unlock_with_password' => \App\Http\Middleware\UnlockWithPassword::class, ]; } diff --git a/app/Http/Livewire/Album.php b/app/Http/Livewire/Album.php deleted file mode 100644 index a506dde4780..00000000000 --- a/app/Http/Livewire/Album.php +++ /dev/null @@ -1,95 +0,0 @@ -album = $album; - $this->info = []; - $this->info['albums'] = []; - - $this->albumFactory = $albumFactory; - $this->photosAction = $photosAction; - } - - public function render() - { - switch (Configs::get_value('layout')) { - case '0': - $this->layout = Album::SQUARE; - break; - case '1': - $this->layout = Album::FLKR; - break; - case '2': - $this->layout = Album::MASONRY; - break; - default: - $this->layout = Album::FLKR; - } - - if ($this->album->smart) { - $publicAlbums = resolve(PublicIds::class)->getPublicAlbumsId(); - $this->album->setAlbumIDs($publicAlbums); - } else { - // we only do this when not in smart mode (i.e. no sub albums) - // that way we limit the number of times we have to query. - resolve(PublicIds::class)->setAlbum($this->album); - } - $this->info = $this->album->toReturnArray(); - - // take care of sub albums - $this->info['albums'] = $this->album->get_children()->map(fn ($a) => $a->toReturnArray())->values(); - - // take care of photos - $this->photos = $this->photosAction->get($this->album); - $this->info['id'] = strval($this->album->id); - $this->info['num'] = strval(count($this->photos)); - - return view('livewire.album'); - } -} \ No newline at end of file diff --git a/app/Http/Livewire/Albums.php b/app/Http/Livewire/Albums.php deleted file mode 100644 index 38f65596df4..00000000000 --- a/app/Http/Livewire/Albums.php +++ /dev/null @@ -1,57 +0,0 @@ -prepareAlbum = $prepareAlbum; - $this->top = $top; - $this->smart = $smart; - - // $toplevel containts Collection[Album] accessible at the root: albums shared_albums. - $toplevel = $this->top->get(); - - $this->albums = $this->prepareAlbum->do($toplevel['albums']); - $this->shared_albums = $this->prepareAlbum->do($toplevel['shared_albums']); - - $this->smartalbums = $this->smart->get(); - } - - /** - * Render component. - */ - public function render() - { - return view('livewire.albums'); - } -} diff --git a/app/Http/Livewire/Fullpage.php b/app/Http/Livewire/Fullpage.php deleted file mode 100644 index 0b758ac0f38..00000000000 --- a/app/Http/Livewire/Fullpage.php +++ /dev/null @@ -1,80 +0,0 @@ -mode = 'albums'; - } else { - $this->mode = 'album'; - $this->album = $albumFactory->make($albumId); - - if ($photoId != null) { - $this->mode = 'photo'; - $this->photo = Photo::with('album')->findOrFail($photoId); - } - } - } - - public function openAlbum($albumId) - { - return redirect('/livewire/' . $albumId); - } - - public function openPhoto($photoId) - { - return redirect('/livewire/' . $this->album->id . '/' . $photoId); - } - - // Ideal we would like to avoid the redirect as they are slow. - public function back() - { - if ($this->photo != null) { - // $this->photo = null; - return redirect('/livewire/' . $this->album->id); - } - if ($this->album != null) { - if ($this->album->is_smart()) { - // $this->album = null; - return redirect('/livewire/'); - } - if ($this->album->parent_id != null) { - return redirect('/livewire/' . $this->album->parent_id); - } - - return redirect('/livewire/'); - } - } - - public function render() - { - return view('livewire.fullpage'); - } -} diff --git a/app/Http/Livewire/Header.php b/app/Http/Livewire/Header.php deleted file mode 100644 index d4d9d542ac1..00000000000 --- a/app/Http/Livewire/Header.php +++ /dev/null @@ -1,35 +0,0 @@ -title = Configs::get_value('site_title', Config::get('defines.defaults.SITE_TITLE')); - if ($album != null) { - $this->title = $album->title; - } - $this->mode = $mode ?? 'albums'; - } - - public function render() - { - return view('livewire.header'); - } -} - diff --git a/app/Http/Livewire/LeftMenu.php b/app/Http/Livewire/LeftMenu.php deleted file mode 100644 index 6a5a38c4156..00000000000 --- a/app/Http/Livewire/LeftMenu.php +++ /dev/null @@ -1,13 +0,0 @@ -album = $photo->album; - $this->photo = $photo; - $this->prepare = $prepare; - } - - public function render() - { - $this->data = $this->prepare->do($this->photo); - - return view('livewire.photo'); - } -} diff --git a/app/Http/Livewire/PhotoOverlay.php b/app/Http/Livewire/PhotoOverlay.php deleted file mode 100644 index c85dedcf296..00000000000 --- a/app/Http/Livewire/PhotoOverlay.php +++ /dev/null @@ -1,111 +0,0 @@ -photo_data = $data; - $overlay_type = Configs::get_value('image_overlay_type', 'none'); - - $this->idx = array_search($overlay_type, $this->types, true); - } - - private function checkOverlayType(): string - { - if ($this->idx < 0) { - return 'none'; - } - - $n = count($this->types); - for ($i = 0; $i < $n; $i++) { - $type = $this->types[($this->idx + $i) % $n]; - if ($type === 'date' || $type === 'none') { - return $type; - } - if ($type === 'desc' && $this->photo_data['description'] !== '') { - return $type; - } - if ($type === 'exif' && $this->genExifHash() !== '') { - return $type; - } - } - } - - private function genExifHash() - { - $exifHash = $this->photo_data['make']; - $exifHash .= $this->photo_data['model']; - $exifHash .= $this->photo_data['shutter']; - if (Str::contains($this->photo_data['type'], 'video')) { - $exifHash .= $this->photo_data['aperture']; - $exifHash .= $this->photo_data['focal']; - } - $exifHash .= $this->photo_data['iso']; - - return $exifHash; - } - - public function render() - { - $this->title = $this->photo_data['title']; - - $this->type = $this->checkOverlayType(); - $this->description = $this->photo_data['description']; - if ($this->photo_data['taken_at'] !== '') { - $this->camera_date = true; - $this->date = $this->photo_data['taken_at']; - } else { - $this->camera_date = false; - $this->date = $this->data['sysdate']; - } - - $exif1 = ''; - $exif2 = ''; - if ($this->genExifHash() !== '') { - if ($this->photo_data['shutter'] !== '') { - $exif1 = str_replace('s', 'sec', $this->photo_data['shutter']); - } - if ($this->photo_data['aperture'] !== '') { - $this->c($exif1, ' at ', str_replace('f/', 'ƒ / ', $this->photo_data['aperture'])); - } - if ($this->photo_data['iso'] !== '') { - $this->c($exif1, ', ', Lang::get('PHOTO_ISO') . ' ' . $this->photo_data['iso']); - } - if ($this->photo_data['focal'] !== '') { - $exif2 = $this->photo_data['focal'] . ($this->photo_data['lens'] !== '' ? ' (' . $this->photo_data['lens'] . ')' : ''); - } - } - $this->exif1 = trim($exif1); - $this->exif2 = trim($exif2); - - return view('livewire.photo-overlay'); - } - - private function c(string &$in, string $glue, string $content): void - { - if ($in !== '') { - $in .= $glue; - } - $in .= $content; - } -} diff --git a/app/Http/Livewire/Sidebar.php b/app/Http/Livewire/Sidebar.php deleted file mode 100644 index dc491ea6b00..00000000000 --- a/app/Http/Livewire/Sidebar.php +++ /dev/null @@ -1,119 +0,0 @@ -album = $album; - DebugBar::notice($album); - $this->photo = $photo; - } - - public function generateAlbumStructure() - { - $this->title = Lang::get('ALBUM_ABOUT'); - $this->data = []; - $basic = new \stdClass(); - $basic->title = Lang::get('ALBUM_BASICS'); - $basic->content = []; - - $basic->content[] = ['head' => Lang::get('ALBUM_TITLE'), 'value' => $this->album->title]; - if ($this->album->description != '') { - ['head' => Lang::get('ALBUM_DESCRIPTION'), 'value' => $this->album->description]; - } - - if ($this->album->is_tag_album()) { - $basic->content[] = ['head' => Lang::get('ALBUM_SHOW_TAGS'), 'value' => $this->album->showtags]; - } - - $album = new \stdClass(); - $album->title = Lang::get('ALBUM_ALBUM'); - $album->content = [ - ['head' => Lang::get('ALBUM_CREATED'), 'value' => $this->album->created_at->format('F Y')], - ]; - if ($this->album->children->count() > 0) { - $album->content[] = ['head' => Lang::get('ALBUM_SUBALBUMS'), 'value' => $this->album->children->count()]; - } - - $counted = $this->album->photos->countBy(function (Photo $photo) { - return $photo->isVideo() ? 'videos' : 'photos'; - })->all(); - if (isset($counted['photos'])) { - $album->content[] = ['head' => Lang::get('ALBUM_IMAGES'), 'value' => $counted['photos']]; - } - if (isset($counted['videos'])) { - $album->content[] = ['head' => Lang::get('ALBUM_VIDEOS'), 'value' => $counted['videos']]; - } - if (isset($counted['photos'])) { - if ($this->album->sorting_col === '') { - $sorting = Lang::get('DEFAULT'); - } else { - $sorting = $this->album->sorting_col + ' ' + $this->album->sorting_order; - } - - $album->content[] = ['head' => Lang::get('ALBUM_ORDERING'), 'value' => $sorting]; - } - - $share = new \stdClass(); - $share->title = Lang::get('ALBUM_SHARING'); - $_public = $this->album->is_public() ? Lang::get('ALBUM_SHR_YES') : Lang::get('ALBUM_SHR_NO'); - $_hidden = $this->album->viewable == '0' ? Lang::get('ALBUM_SHR_YES') : Lang::get('ALBUM_SHR_NO'); // TODO : double check; - $_downloadable = $this->album->is_downloadable() ? Lang::get('ALBUM_SHR_YES') : Lang::get('ALBUM_SHR_NO'); - $_share_button_visible = $this->album->is_share_button_visible() ? Lang::get('ALBUM_SHR_YES') : Lang::get('ALBUM_SHR_NO'); - $_password = $this->album->password != '' ? Lang::get('ALBUM_SHR_YES') : Lang::get('ALBUM_SHR_NO'); - $share->content = [ - ['head' => Lang::get('ALBUM_PUBLIC'), 'value' => $_public], - ['head' => Lang::get('ALBUM_HIDDEN'), 'value' => $_hidden], - ['head' => Lang::get('ALBUM_DOWNLOADABLE'), 'value' => $_downloadable], - ['head' => Lang::get('ALBUM_SHARE_BUTTON_VISIBLE'), 'value' => $_share_button_visible], - ['head' => Lang::get('ALBUM_PASSWORD'), 'value' => $_password], - ]; - if ($this->album->owner_id != null) { - $share->content[] = ['head' => Lang::get('ALBUM_OWNER'), 'value' => $this->album->owner->name()]; - } - - $license = new \stdClass(); - $license->title = Lang::get('ALBUM_REUSE'); - $license->content = [ - ['head' => Lang::get('ALBUM_LICENSE'), 'value' => $this->album->get_license()], - ]; - - $this->data = [$basic, $album, $license]; - - if (AccessControl::is_logged_in()) { - $this->data[] = $share; - } - } - - public function render() - { - if ($this->album != null) { - $this->generateAlbumStructure(); - } else { - $this->data = []; - $this->title = ''; - } - - return view('livewire.sidebar'); - } -} \ No newline at end of file diff --git a/app/Http/Middleware/AcceptContentType.php b/app/Http/Middleware/AcceptContentType.php new file mode 100644 index 00000000000..2e35b70740e --- /dev/null +++ b/app/Http/Middleware/AcceptContentType.php @@ -0,0 +1,104 @@ + + */ + protected $except = [ + ]; + + /** + * Handles the incoming request. + * + * @param Request $request the incoming request to serve + * @param \Closure $next the next operation to be applied to the + * request + * @param string $contentType the content type which must be acceptable + * by the client; either {@link self::JSON}, + * {@link self::HTML}, or {@link self::ANY} + * + * @return mixed + * + * @throws UnexpectedContentType + * @throws LycheeInvalidArgumentException + */ + public function handle(Request $request, \Closure $next, string $contentType): mixed + { + // Skip $except + if ($this->inExceptArray($request)) { + return $next($request); + } + + if ($contentType === self::JSON) { + if (!$request->expectsJson()) { + throw new UnexpectedContentType(self::JSON); + } + } elseif ($contentType === self::HTML) { + if (!$request->acceptsHtml()) { + throw new UnexpectedContentType(self::HTML); + } + } elseif ($contentType === self::ANY) { + // Don't call `$request->acceptsAnyContentType`. It is broken. + $acceptable = $request->getAcceptableContentTypes(); + if ( + sizeof($acceptable) !== 0 && + !in_array('*', $acceptable, true) && + !in_array('*/*', $acceptable, true) + ) { + throw new UnexpectedContentType(self::ANY); + } + } else { + throw new LycheeInvalidArgumentException('$contentType must either be "' . self::JSON . '", "' . self::HTML . '" or "' . self::ANY . '"'); + } + + return $next($request); + } + + /** + * Determine if the request has a URI that should pass through CSRF verification. + * + * @param \Illuminate\Http\Request $request + * + * @return bool + */ + protected function inExceptArray($request) + { + foreach ($this->except as $except) { + if ($except !== '/') { + $except = trim($except, '/'); + } + + if ($request->fullUrlIs($except) || $request->is($except)) { + return true; + } + } + + return false; + } +} diff --git a/app/Http/Middleware/AdminCheck.php b/app/Http/Middleware/AdminCheck.php deleted file mode 100644 index 621d4a70317..00000000000 --- a/app/Http/Middleware/AdminCheck.php +++ /dev/null @@ -1,44 +0,0 @@ -isInstalled = $isInstalled; - } - - /** - * Handle an incoming request. - * - * @param Request $request - * @param Closure $next - * - * @return mixed - */ - public function handle($request, Closure $next) - { - if (!$this->isInstalled->assert()) { - return $next($request); - } - - if (!AccessControl::is_admin()) { - return response('false'); - } - - return $next($request); - } -} diff --git a/app/Http/Middleware/AdminUserStatus.php b/app/Http/Middleware/AdminUserStatus.php new file mode 100644 index 00000000000..372f6f305f3 --- /dev/null +++ b/app/Http/Middleware/AdminUserStatus.php @@ -0,0 +1,75 @@ +hasAdminUser = $hasAdminUser; + } + + /** + * Handles an incoming request. + * + * @param Request $request the incoming request to serve + * @param \Closure $next the next operation to be applied to the + * request + * @param string $requiredStatus the required installation status; either + * {@link self::SET} or + * {@link self::UNSET} + * + * @return mixed + * + * @throws LycheeException + */ + public function handle(Request $request, \Closure $next, string $requiredStatus): mixed + { + if ($requiredStatus === self::SET) { + if ($this->hasAdminUser->assert()) { + return $next($request); + } else { + throw new AdminUserRequiredException(); + } + } elseif ($requiredStatus === self::UNSET) { + if ($this->hasAdminUser->assert()) { + throw new AdminUserAlreadySetException(); + } else { + return $next($request); + } + } else { + throw new LycheeInvalidArgumentException('$requiredStatus must either be "' . self::SET . '" or "' . self::UNSET . '"'); + } + } +} diff --git a/app/Http/Middleware/Authenticate.php b/app/Http/Middleware/Authenticate.php deleted file mode 100644 index 8c4c9ff3bfc..00000000000 --- a/app/Http/Middleware/Authenticate.php +++ /dev/null @@ -1,22 +0,0 @@ -expectsJson()) { - return route('home'); - } - } -} diff --git a/app/Http/Middleware/CacheControl.php b/app/Http/Middleware/CacheControl.php new file mode 100644 index 00000000000..72a9aae96ec --- /dev/null +++ b/app/Http/Middleware/CacheControl.php @@ -0,0 +1,31 @@ +headers->set('Cache-Control', 'private;max_age=' . $age); + + return $response; + } +} \ No newline at end of file diff --git a/app/Http/Middleware/Checks/ExistsDB.php b/app/Http/Middleware/Checks/ExistsDB.php deleted file mode 100644 index 9d5665707b5..00000000000 --- a/app/Http/Middleware/Checks/ExistsDB.php +++ /dev/null @@ -1,23 +0,0 @@ -where('may_administrate', '=', true)->count() > 0; + } else { + // If the column does not exist yet but we are executing this script + // it means that there exists already an admin user (with ID = 0). + return true; + } + } +} \ No newline at end of file diff --git a/app/Http/Middleware/Checks/IsInstalled.php b/app/Http/Middleware/Checks/IsInstalled.php index d71fde56960..29f149a5b9d 100644 --- a/app/Http/Middleware/Checks/IsInstalled.php +++ b/app/Http/Middleware/Checks/IsInstalled.php @@ -1,40 +1,58 @@ getMessage(), 'SQLSTATE[HY000] [1045]')) { + return false; + } + // Not coverable by tests unless we actually remove the php dependencies... + if (Str::contains($e->getMessage(), 'could not find driver')) { + return false; + } + throw $e; + } catch (BindingResolutionException|NotFoundExceptionInterface|ContainerExceptionInterface $e) { + throw new FrameworkException('Laravel\'s container component', $e); } } -} +} \ No newline at end of file diff --git a/app/Http/Middleware/Checks/IsMigrated.php b/app/Http/Middleware/Checks/IsMigrated.php index f43e061288c..6e2483189ee 100644 --- a/app/Http/Middleware/Checks/IsMigrated.php +++ b/app/Http/Middleware/Checks/IsMigrated.php @@ -1,39 +1,25 @@ lycheeVersion = $lycheeVersion; - } - - /** - * @param string $version in the shape of xxyyzz + * Returns true if the DB version is up to date. * - * @return string xx.yy.zz + * @return bool */ - private function intify(string $version): int - { - $v = explode('.', $version); - - return 10000 * ($v[0] ?? 0) + 100 * ($v[1] ?? 0) + ($v[2] ?? 0); - } - public function assert(): bool { - $db_ver = $this->lycheeVersion->getDBVersion()['version']; - $file_ver = $this->lycheeVersion->getFileVersion()['version']; - - return $this->intify($db_ver) == $this->intify($file_ver); + return MigrationCheck::isUpToDate(); } } diff --git a/app/Http/Middleware/ConfigIntegrity.php b/app/Http/Middleware/ConfigIntegrity.php new file mode 100644 index 00000000000..9ad85d2fa7c --- /dev/null +++ b/app/Http/Middleware/ConfigIntegrity.php @@ -0,0 +1,52 @@ +whereIn('key', self::SE_FIELDS)->update(['level' => 1]); + } catch (\Exception $e) { + // Do nothing: we are not installed yet, so we fail silently. + } + + return $next($request); + } +} \ No newline at end of file diff --git a/app/Http/Middleware/ContentType.php b/app/Http/Middleware/ContentType.php new file mode 100644 index 00000000000..4d85e3144a7 --- /dev/null +++ b/app/Http/Middleware/ContentType.php @@ -0,0 +1,57 @@ +isJson()) { + throw new UnexpectedContentType(self::JSON); + } + } elseif ($contentType === self::MULTIPART) { + if ($request->getContentTypeFormat() !== 'form') { + throw new UnexpectedContentType(self::MULTIPART); + } + } else { + throw new LycheeInvalidArgumentException('$contentType must either be "' . self::JSON . '" or "' . self::MULTIPART . '"'); + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/DBExists.php b/app/Http/Middleware/DBExists.php deleted file mode 100644 index abd2e017a2b..00000000000 --- a/app/Http/Middleware/DBExists.php +++ /dev/null @@ -1,40 +0,0 @@ -existsDB = $existsDB; - } - - /** - * Handle an incoming request. - * - * @param Request $request - * @param Closure $next - * - * @return mixed - */ - public function handle($request, Closure $next) - { - if (!$this->existsDB->assert()) { - return ToInstall::go(); - } - - return $next($request); - } -} diff --git a/app/Http/Middleware/DisableCSP.php b/app/Http/Middleware/DisableCSP.php new file mode 100644 index 00000000000..227547a846a --- /dev/null +++ b/app/Http/Middleware/DisableCSP.php @@ -0,0 +1,85 @@ +getRequestUri() === $dir_url . '/docs/api' || + $request->getRequestUri() === $dir_url . '/request-docs' + ) { + config(['secure-headers.csp.enable' => false]); + } + + if ($request->getRequestUri() === $dir_url . '/' . config('log-viewer.route_path', 'Logs')) { + // We must disable unsafe-eval because vue3 used by log-viewer requires it. + // We must disable unsafe-inline (and hashes) because log-viewer uses inline script with parameter to boot. + // Those parameters are not know by Lychee if someone modifies the config. + // We only do that in that specific case. It is disabled by default otherwise. + config(['secure-headers.csp.script-src.unsafe-eval' => true]); + config(['secure-headers.csp.script-src.unsafe-inline' => true]); + config(['secure-headers.csp.script-src.hashes.sha256' => []]); + } + + // disable unsafe-eval if we are on a VueJS page + if (Features::active('vuejs')) { + $this->handleVueJS(); + } + + return $next($request); + } + + /** + * Disabling rules because ... VueJS. + * + * @return void + * + * @throws BindingResolutionException + */ + private function handleVueJS() + { + // We have to disable unsafe-eval because Livewire requires it... + // So stupid.... + config(['secure-headers.csp.script-src.unsafe-eval' => true]); + + // if the public/hot file exists, it means that we need to disable CSP completely + // As we will be reloading on the fly the page and Vite has poor CSP support. + if (File::exists(public_path('hot'))) { + config(['secure-headers.csp.enable' => false]); + } + } +} diff --git a/app/Http/Middleware/EncryptCookies.php b/app/Http/Middleware/EncryptCookies.php deleted file mode 100644 index ea1cf5a225a..00000000000 --- a/app/Http/Middleware/EncryptCookies.php +++ /dev/null @@ -1,16 +0,0 @@ -getContent(); + // Note: The content is always empty for binary file or streamed + // responses at this stage, because their content is sent + // asynchronously. + // Hence, we must not overwrite the status code with 204 for those + // kinds of responses. + if ( + ($content === false || $content === '') && + !($response instanceof BinaryFileResponse) && + !($response instanceof StreamedResponse) + ) { + $response->setStatusCode(Response::HTTP_NO_CONTENT); + } + + return $response; + } +} diff --git a/app/Http/Middleware/InstallationStatus.php b/app/Http/Middleware/InstallationStatus.php new file mode 100644 index 00000000000..7f089085327 --- /dev/null +++ b/app/Http/Middleware/InstallationStatus.php @@ -0,0 +1,75 @@ +isInstalled = $isInstalled; + } + + /** + * Handles an incoming request. + * + * @param Request $request the incoming request to serve + * @param \Closure $next the next operation to be applied to the + * request + * @param string $requiredStatus the required installation status; either + * {@link self::COMPLETE} or + * {@link self::INCOMPLETE} + * + * @return mixed + * + * @throws LycheeException + */ + public function handle(Request $request, \Closure $next, string $requiredStatus): mixed + { + if ($requiredStatus === self::COMPLETE) { + if ($this->isInstalled->assert()) { + return $next($request); + } else { + throw new InstallationRequiredException(); + } + } elseif ($requiredStatus === self::INCOMPLETE) { + if ($this->isInstalled->assert()) { + throw new InstallationAlreadyCompletedException(); + } else { + return $next($request); + } + } else { + throw new LycheeInvalidArgumentException('$requiredStatus must either be "' . self::COMPLETE . '" or "' . self::INCOMPLETE . '"'); + } + } +} diff --git a/app/Http/Middleware/InstalledCheck.php b/app/Http/Middleware/InstalledCheck.php deleted file mode 100644 index bf2d4682907..00000000000 --- a/app/Http/Middleware/InstalledCheck.php +++ /dev/null @@ -1,40 +0,0 @@ -isInstalled = $isInstalled; - } - - /** - * Handle an incoming request. - * - * @param Request $request - * @param Closure $next - * - * @return mixed - */ - public function handle($request, Closure $next) - { - if ($this->isInstalled->assert()) { - return ToHome::go(); - } - - return $next($request); - } -} diff --git a/app/Http/Middleware/LoginCheck.php b/app/Http/Middleware/LoginCheck.php deleted file mode 100644 index 50049fa55dd..00000000000 --- a/app/Http/Middleware/LoginCheck.php +++ /dev/null @@ -1,29 +0,0 @@ -route('gallery'); + } + + if (!Configs::getValueAsBool('login_required')) { + // Login is not required. Proceed. + return $next($request); + } + + if ($requiredStatus === self::ALBUM && Configs::getValueAsBool('login_required_root_only')) { + return $next($request); + } + + throw new UnauthenticatedException('Login required.'); + } +} diff --git a/app/Http/Middleware/MigrationCheck.php b/app/Http/Middleware/MigrationCheck.php deleted file mode 100644 index 0d219fda691..00000000000 --- a/app/Http/Middleware/MigrationCheck.php +++ /dev/null @@ -1,39 +0,0 @@ -isMigrated = $isMigrated; - } - - /** - * Handle an incoming request. - * - * @param Request $request - * @param Closure $next - * - * @return mixed - */ - public function handle($request, Closure $next) - { - if (!$this->isMigrated->assert()) { - return response()->view('error.update', ['code' => '503', 'message' => 'Database version is behind, please apply migration.']); - } - - return $next($request); - } -} diff --git a/app/Http/Middleware/MigrationStatus.php b/app/Http/Middleware/MigrationStatus.php new file mode 100644 index 00000000000..3f3a5d9bfa9 --- /dev/null +++ b/app/Http/Middleware/MigrationStatus.php @@ -0,0 +1,75 @@ +isMigrated = $isMigrated; + } + + /** + * Handle an incoming request. + * + * @param Request $request the incoming request to serve + * @param \Closure $next the next operation to be applied to the + * request + * @param string $requiredStatus the required migration status; either + * {@link self::COMPLETE} or + * {@link self::INCOMPLETE} + * + * @return mixed + * + * @throws LycheeException + */ + public function handle(Request $request, \Closure $next, string $requiredStatus): mixed + { + if ($requiredStatus === self::COMPLETE) { + if ($this->isMigrated->assert()) { + return $next($request); + } else { + throw new MigrationRequiredException(); + } + } elseif ($requiredStatus === self::INCOMPLETE) { + if ($this->isMigrated->assert()) { + throw new MigrationAlreadyCompletedException(); + } else { + return $next($request); + } + } else { + throw new LycheeInvalidArgumentException('$requiredStatus must either be "' . self::COMPLETE . '" or "' . self::INCOMPLETE . '"'); + } + } +} diff --git a/app/Http/Middleware/PreventRequestsDuringMaintenance.php b/app/Http/Middleware/PreventRequestsDuringMaintenance.php deleted file mode 100644 index f5c499b22b9..00000000000 --- a/app/Http/Middleware/PreventRequestsDuringMaintenance.php +++ /dev/null @@ -1,16 +0,0 @@ -readAccessFunctions = $readAccessFunctions; - } - - /** - * Handle an incoming request. - * - * @param Request $request - * @param Closure $next - * - * @return mixed - */ - public function handle($request, Closure $next) - { - $albumIDs = []; - if ($request->has('albumIDs')) { - $albumIDs = explode(',', $request['albumIDs']); - } - if ($request->has('albumID')) { - $albumIDs[] = $request['albumID']; - } - foreach ($albumIDs as $albumID) { - $sess = $this->readAccessFunctions->albumID($albumID); - if ($sess === 0) { - Logs::error(__METHOD__, __LINE__, 'Could not find specified album'); - - return response('false'); - } - if ($sess === 2) { - return response('"Warning: Album private!"'); - } - if ($sess === 3) { - return response('"Warning: Wrong password!"'); - } - } - - $photoIDs = []; - if ($request->has('photoIDs')) { - $photoIDs = explode(',', $request['photoIDs']); - } - if ($request->has('photoID')) { - $photoIDs[] = $request['photoID']; - } - foreach ($photoIDs as $photoID) { - $photo = Photo::with('album')->find($photoID); - if ($photo === null) { - Logs::error(__METHOD__, __LINE__, 'Could not find specified photo'); - - return response('false'); - } - if ($this->readAccessFunctions->photo($photo) === false) { - return response('false'); - } - } - - return $next($request); // access granted - } -} diff --git a/app/Http/Middleware/RedirectIfAuthenticated.php b/app/Http/Middleware/RedirectIfAuthenticated.php deleted file mode 100644 index 7421d95dff0..00000000000 --- a/app/Http/Middleware/RedirectIfAuthenticated.php +++ /dev/null @@ -1,32 +0,0 @@ -check()) { - return redirect(RouteServiceProvider::HOME); - } - } - - return $next($request); - } -} diff --git a/app/Http/Middleware/TrimStrings.php b/app/Http/Middleware/TrimStrings.php index e2393eb1a7e..cdeb6e430d1 100644 --- a/app/Http/Middleware/TrimStrings.php +++ b/app/Http/Middleware/TrimStrings.php @@ -1,5 +1,11 @@ */ protected $except = [ 'password', diff --git a/app/Http/Middleware/TrustHosts.php b/app/Http/Middleware/TrustHosts.php deleted file mode 100644 index 7a0794d31bf..00000000000 --- a/app/Http/Middleware/TrustHosts.php +++ /dev/null @@ -1,20 +0,0 @@ -allSubdomainsOfApplicationUrl(), - ]; - } -} \ No newline at end of file diff --git a/app/Http/Middleware/TrustProxies.php b/app/Http/Middleware/TrustProxies.php deleted file mode 100644 index dfd990c0106..00000000000 --- a/app/Http/Middleware/TrustProxies.php +++ /dev/null @@ -1,25 +0,0 @@ -albumFactory = $albumFactory; + $this->unlock = $unlock; + } + + /** + * Handle an incoming request. + * If a password is provided, we try to unlock the album or fail silently. + * + * @param Request $request the incoming request to serve + * @param \Closure $next the next operation to be applied to the + * request + */ + public function handle(Request $request, \Closure $next): mixed + { + $album_id = $request->route('albumId'); + if ($album_id === null || !is_string($album_id)) { + throw new LycheeLogicException('No albumId provided as url parameter.'); + } + + if (in_array($album_id, SmartAlbumType::values(), true)) { + return $next($request); + } + + if (!$request->filled('password')) { + return $next($request); + } + + if (!Configs::getValueAsBool('unlock_password_photos_with_url_param')) { + Log::warning('password provided but unlock_password_photos_with_url_param is disabled.'); + + return $next($request); + } + + try { + $album = $this->albumFactory->findBaseAlbumOrFail($album_id); + $this->unlock->do($album, $request['password']); + } catch (\Exception) { + // fail silently + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/UploadCheck.php b/app/Http/Middleware/UploadCheck.php deleted file mode 100644 index ee7704b94e4..00000000000 --- a/app/Http/Middleware/UploadCheck.php +++ /dev/null @@ -1,187 +0,0 @@ -albumFactory = $albumFactory; - } - - /** - * Handle an incoming request. - * - * @param Request $request - * @param Closure $next - * - * @return mixed - */ - public function handle(Request $request, Closure $next) - { - // not logged! - if (!AccessControl::is_logged_in()) { - return response('false'); - } - - // is admin - if (AccessControl::is_admin()) { - return $next($request); - } - - $user = AccessControl::user(); - - // is not admin and does not have upload rights - if (!$user->upload) { - return response('false'); - } - - $ret = $this->album_check($request, $user->id); - if ($ret === false) { - return response('false'); - } - - $ret = $this->photo_check($request, $user->id); - if ($ret === false) { - return response('false'); - } - - // Only used for /api/Sharing::Delete - $ret = $this->share_check($request, $user->id); - if ($ret === false) { - return response('false'); - } - - return $next($request); - } - - /** - * Take of checking if a user can actually modify that Album. - * - * @param $request - * @param int $user_id - * - * @return ResponseFactory|Response|mixed - */ - public function album_check(Request $request, int $user_id) - { - $albumIDs = []; - if ($request->has('albumIDs')) { - $albumIDs = explode(',', $request['albumIDs']); - } - if ($request->has('albumID')) { - $albumIDs[] = $request['albumID']; - } - if ($request->has('parent_id')) { - $albumIDs[] = $request['parent_id']; - } - - // Remove smart albums (they get a pass). - for ($i = 0; $i < count($albumIDs);) { - if ($this->albumFactory->is_smart($albumIDs[$i]) || $albumIDs[$i] === '0') { - array_splice($albumIDs, $i, 1); - } else { - $i++; - } - } - - // Since we count the result we need to ensure no duplicates. - $albumIDs = array_unique($albumIDs); - - if (count($albumIDs) > 0) { - $count = Album::whereIn('id', $albumIDs)->where('owner_id', '=', $user_id)->count(); - if ($count !== count($albumIDs)) { - Logs::error(__METHOD__, __LINE__, 'Albums not found or ownership mismatch!'); - - return false; - } - } - - return true; - } - - /** - * Check if the user is authorized to do anything to that picture. - * - * @param Request $request - * @param int $user_id - * - * @return ResponseFactory|Response|mixed - */ - public function photo_check(Request $request, int $user_id) - { - $photoIDs = []; - if ($request->has('photoIDs')) { - $photoIDs = explode(',', $request['photoIDs']); - } - if ($request->has('photoID')) { - $photoIDs[] = $request['photoID']; - } - - // Since we count the result we need to ensure no duplicates. - $photoIDs = array_unique($photoIDs); - - if (count($photoIDs) > 0) { - $count = Photo::whereIn('id', $photoIDs)->where('owner_id', '=', $user_id)->count(); - if ($count !== count($photoIDs)) { - Logs::error(__METHOD__, __LINE__, 'Photos not found or ownership mismatch!'); - - return false; - } - } - - return true; - } - - /** - * @param Request $request - * @param int $user_id - * - * @return bool - */ - public function share_check(Request $request, int $user_id) - { - if ($request->has('ShareIDs')) { - $shareIDs = $request['ShareIDs']; - - $albums = Album::whereIn('id', function (Builder $query) use ($shareIDs) { - $query->select('album_id') - ->from('user_album') - ->whereIn('id', explode(',', $shareIDs)); - })->select('owner_id')->get(); - - if ($albums == null) { - Logs::error(__METHOD__, __LINE__, 'Could not find specified albums'); - - return false; - } - $no_error = true; - foreach ($albums as $album_t) { - $no_error &= ($album_t->owner_id == $user_id); - } - if ($no_error) { - return true; - } - - Logs::error(__METHOD__, __LINE__, 'Album ownership mismatch!'); - - return false; - } - } -} diff --git a/app/Http/Middleware/VerifyCsrfToken.php b/app/Http/Middleware/VerifyCsrfToken.php index 1ed21ef14e5..089700bdac7 100644 --- a/app/Http/Middleware/VerifyCsrfToken.php +++ b/app/Http/Middleware/VerifyCsrfToken.php @@ -1,10 +1,16 @@ */ protected $except = [ // entry points... @@ -21,43 +27,42 @@ class VerifyCsrfToken extends Middleware ]; /** - * The goal of this function is to allow to bypass the CSRF token requirement - * if an Authorization value is provided in the header and matches the apiKey. + * Attempts to verify the CSRF token unless an API token is provided. * - * FIXME: Do we want to hash this API key ? Might actually be a good idea... + * Note, if the API token is given but invalid (i.e. refers to a + * non-existing user), then {@link \App\Services\Auth\SessionOrTokenGuard} + * bails out. * - * @param $request - * @param Closure $next + * @param Request $request + * @param \Closure $next * * @return mixed * * @throws TokenMismatchException */ - public function handle($request, Closure $next) + public function handle($request, \Closure $next): mixed { - if ($request->is('api/*')) { - /** - * default value is '' - * we force it in case of the migration has not been done. - */ - $apiKey = Configs::get_value('api_key', ''); - - /* - * if apiKey is the empty string we directly return the parent handle. - */ - if ($apiKey && $apiKey == '') { - return parent::handle($request, $next); - } - - /* - * We are currently checking for Authorization. - * Do we also want to check if there is a POST value with the apiKey ? - */ - if ($apiKey && $request->header('Authorization') === $apiKey) { - return $next($request); - } + $token = $request->headers->get(SessionOrTokenGuard::HTTP_TOKEN_HEADER); + if (is_string($token) && $token !== '') { + return $next($request); } return parent::handle($request, $next); } + + /** + * Determine if the HTTP request uses a ‘read’ verb. + * + * @param \Illuminate\Http\Request $request + * + * @return bool + */ + protected function isReading($request) + { + if (str_starts_with($request->route()->uri, 'api/v2')) { + return false; + } + + return parent::isReading($request); + } } diff --git a/app/Http/Redirections/ToAdminSetter.php b/app/Http/Redirections/ToAdminSetter.php new file mode 100644 index 00000000000..795c49ca7ea --- /dev/null +++ b/app/Http/Redirections/ToAdminSetter.php @@ -0,0 +1,33 @@ + 'no-cache, must-revalidate', + ]); + } +} diff --git a/app/Http/Redirections/ToHome.php b/app/Http/Redirections/ToHome.php new file mode 100644 index 00000000000..d987620d194 --- /dev/null +++ b/app/Http/Redirections/ToHome.php @@ -0,0 +1,27 @@ + 'no-cache, must-revalidate', + ]); + } +} diff --git a/app/Http/Redirections/ToInstall.php b/app/Http/Redirections/ToInstall.php new file mode 100644 index 00000000000..a1017b75093 --- /dev/null +++ b/app/Http/Redirections/ToInstall.php @@ -0,0 +1,33 @@ + 'no-cache, must-revalidate', + ]); + } +} diff --git a/app/Http/Redirections/ToMigration.php b/app/Http/Redirections/ToMigration.php new file mode 100644 index 00000000000..c5d52b28d57 --- /dev/null +++ b/app/Http/Redirections/ToMigration.php @@ -0,0 +1,29 @@ + 'no-cache, must-revalidate', + ]); + } +} diff --git a/app/Http/Requests/AbstractEmptyRequest.php b/app/Http/Requests/AbstractEmptyRequest.php new file mode 100644 index 00000000000..aca424b1bb6 --- /dev/null +++ b/app/Http/Requests/AbstractEmptyRequest.php @@ -0,0 +1,31 @@ +parentAlbum]); + } + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return [ + RequestAttribute::PARENT_ID_ATTRIBUTE => ['present', new RandomIDRule(true)], + RequestAttribute::TITLE_ATTRIBUTE => ['required', new TitleRule()], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + /** @var string|null */ + $parentAlbumID = $values[RequestAttribute::PARENT_ID_ATTRIBUTE]; + $this->parent_album = $parentAlbumID === null ? + null : + Album::query()->findOrFail($parentAlbumID); + $this->title = $values[RequestAttribute::TITLE_ATTRIBUTE]; + } +} diff --git a/app/Http/Requests/Album/AddTagAlbumRequest.php b/app/Http/Requests/Album/AddTagAlbumRequest.php new file mode 100644 index 00000000000..6bf7697b370 --- /dev/null +++ b/app/Http/Requests/Album/AddTagAlbumRequest.php @@ -0,0 +1,58 @@ + ['required', new TitleRule()], + RequestAttribute::TAGS_ATTRIBUTE => 'required|array|min:1', + RequestAttribute::TAGS_ATTRIBUTE . '.*' => 'required|string|min:1', + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + $this->title = $values[RequestAttribute::TITLE_ATTRIBUTE]; + $this->tags = $values[RequestAttribute::TAGS_ATTRIBUTE]; + } +} diff --git a/app/Http/Requests/Album/DeleteAlbumsRequest.php b/app/Http/Requests/Album/DeleteAlbumsRequest.php new file mode 100644 index 00000000000..6d0403dae62 --- /dev/null +++ b/app/Http/Requests/Album/DeleteAlbumsRequest.php @@ -0,0 +1,56 @@ +albumIds()]); + } + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return [ + RequestAttribute::ALBUM_IDS_ATTRIBUTE => 'required|array|min:1', + RequestAttribute::ALBUM_IDS_ATTRIBUTE . '.*' => ['required', new AlbumIDRule(false)], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + // As we are going to delete the albums anyway, we don't load the + // models for efficiency reasons. + // Instead, we use mass deletion via low-level SQL queries later. + $this->albumIds = $values[RequestAttribute::ALBUM_IDS_ATTRIBUTE]; + } +} diff --git a/app/Http/Requests/Album/DeleteTrackRequest.php b/app/Http/Requests/Album/DeleteTrackRequest.php new file mode 100644 index 00000000000..403f2caae62 --- /dev/null +++ b/app/Http/Requests/Album/DeleteTrackRequest.php @@ -0,0 +1,43 @@ + ['required', new AlbumIDRule(false)], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + /** @var string|null */ + $albumID = $values[RequestAttribute::ALBUM_ID_ATTRIBUTE]; + $this->album = Album::query()->findOrFail($albumID); + } +} diff --git a/app/Http/Requests/Album/GetAlbumRequest.php b/app/Http/Requests/Album/GetAlbumRequest.php new file mode 100644 index 00000000000..dc13101cc15 --- /dev/null +++ b/app/Http/Requests/Album/GetAlbumRequest.php @@ -0,0 +1,65 @@ +album]); + + // In case of a password protected album, we must throw an exception + // with a special error message ("Password required") such that the + // front-end shows the password dialog if a password is set, but + // does not show the dialog otherwise. + if ( + !$result && + $this->album instanceof BaseAlbum && + $this->album->public_permissions()?->password !== null + ) { + throw new PasswordRequiredException(); + } + + return $result; + } + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return [ + RequestAttribute::ALBUM_ID_ATTRIBUTE => ['required', new AlbumIDRule(false)], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + $this->album = $this->albumFactory->findAbstractAlbumOrFail($values[RequestAttribute::ALBUM_ID_ATTRIBUTE]); + } +} diff --git a/app/Http/Requests/Album/MergeAlbumsRequest.php b/app/Http/Requests/Album/MergeAlbumsRequest.php new file mode 100644 index 00000000000..277ede87b92 --- /dev/null +++ b/app/Http/Requests/Album/MergeAlbumsRequest.php @@ -0,0 +1,59 @@ + + */ +class MergeAlbumsRequest extends BaseApiRequest implements HasAlbum, HasAlbums +{ + use HasAlbumTrait; + /** @phpstan-use HasAlbumsTrait */ + use HasAlbumsTrait; + use AuthorizeCanEditAlbumAlbumsTrait; + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return [ + RequestAttribute::ALBUM_ID_ATTRIBUTE => ['required', new RandomIDRule(false)], + RequestAttribute::ALBUM_IDS_ATTRIBUTE => 'required|array|min:1', + RequestAttribute::ALBUM_IDS_ATTRIBUTE . '.*' => ['required', new AlbumIDRule(false)], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + /** @var string $id */ + $id = $values[RequestAttribute::ALBUM_ID_ATTRIBUTE]; + /** @var array $ids */ + $ids = $values[RequestAttribute::ALBUM_IDS_ATTRIBUTE]; + $this->album = Album::query()->findOrFail($id); + // @phpstan-ignore-next-line + $this->albums = Album::query() + ->with(['children']) + ->findOrFail($ids); + } +} diff --git a/app/Http/Requests/Album/MoveAlbumsRequest.php b/app/Http/Requests/Album/MoveAlbumsRequest.php new file mode 100644 index 00000000000..a526780c6ea --- /dev/null +++ b/app/Http/Requests/Album/MoveAlbumsRequest.php @@ -0,0 +1,59 @@ + + */ +class MoveAlbumsRequest extends BaseApiRequest implements HasAlbum, HasAlbums +{ + use HasAlbumTrait; + /** @phpstan-use HasAlbumsTrait */ + use HasAlbumsTrait; + use AuthorizeCanEditAlbumAlbumsTrait; + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return [ + RequestAttribute::ALBUM_ID_ATTRIBUTE => ['present', new RandomIDRule(true)], + RequestAttribute::ALBUM_IDS_ATTRIBUTE => 'required|array|min:1', + RequestAttribute::ALBUM_IDS_ATTRIBUTE . '.*' => ['required', new AlbumIDRule(false)], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + /** @var string|null $id */ + $id = $values[RequestAttribute::ALBUM_ID_ATTRIBUTE]; + /** @var array $ids */ + $ids = $values[RequestAttribute::ALBUM_IDS_ATTRIBUTE]; + $this->album = $id === null ? + null : + Album::findOrFail($id); + /** @phpstan-ignore-next-line */ + $this->albums = Album::findOrFail($ids); + } +} diff --git a/app/Http/Requests/Album/RenameAlbumRequest.php b/app/Http/Requests/Album/RenameAlbumRequest.php new file mode 100644 index 00000000000..83986358940 --- /dev/null +++ b/app/Http/Requests/Album/RenameAlbumRequest.php @@ -0,0 +1,55 @@ +album]); + } + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return [ + RequestAttribute::ALBUM_ID_ATTRIBUTE => ['required', new RandomIDRule(true)], + RequestAttribute::TITLE_ATTRIBUTE => ['required', new TitleRule()], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + $this->album = $this->albumFactory->findBaseAlbumOrFail($values[RequestAttribute::ALBUM_ID_ATTRIBUTE]); + $this->title = $values[RequestAttribute::TITLE_ATTRIBUTE]; + } +} diff --git a/app/Http/Requests/Album/SetAlbumProtectionPolicyRequest.php b/app/Http/Requests/Album/SetAlbumProtectionPolicyRequest.php new file mode 100644 index 00000000000..46404e7811c --- /dev/null +++ b/app/Http/Requests/Album/SetAlbumProtectionPolicyRequest.php @@ -0,0 +1,93 @@ +album instanceof BaseSmartAlbum) { + return Auth::user()?->may_administrate === true; + } + + return Gate::check(AlbumPolicy::CAN_EDIT, [AbstractAlbum::class, $this->album]); + } + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return [ + RequestAttribute::ALBUM_ID_ATTRIBUTE => ['required', new AlbumIDRule(false)], + RequestAttribute::PASSWORD_ATTRIBUTE => ['sometimes', new PasswordRule(true)], + RequestAttribute::IS_PUBLIC_ATTRIBUTE => 'required|boolean', + RequestAttribute::IS_LINK_REQUIRED_ATTRIBUTE => 'required|boolean', + RequestAttribute::IS_NSFW_ATTRIBUTE => 'required|boolean', + RequestAttribute::GRANTS_DOWNLOAD_ATTRIBUTE => 'required|boolean', + RequestAttribute::GRANTS_FULL_PHOTO_ACCESS_ATTRIBUTE => 'required|boolean', + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + $this->album = $this->albumFactory->findAbstractAlbumOrFail( + $values[RequestAttribute::ALBUM_ID_ATTRIBUTE] + ); + $this->albumProtectionPolicy = new AlbumProtectionPolicy( + is_public: static::toBoolean($values[RequestAttribute::IS_PUBLIC_ATTRIBUTE]), + is_link_required: static::toBoolean($values[RequestAttribute::IS_LINK_REQUIRED_ATTRIBUTE]), + is_nsfw: static::toBoolean($values[RequestAttribute::IS_NSFW_ATTRIBUTE]), + grants_full_photo_access: static::toBoolean($values[RequestAttribute::GRANTS_FULL_PHOTO_ACCESS_ATTRIBUTE]), + grants_download: static::toBoolean($values[RequestAttribute::GRANTS_DOWNLOAD_ATTRIBUTE]), + ); + $this->isPasswordProvided = array_key_exists(RequestAttribute::PASSWORD_ATTRIBUTE, $values); + $this->password = $this->isPasswordProvided ? $values[RequestAttribute::PASSWORD_ATTRIBUTE] : null; + } + + /** + * @return AlbumProtectionPolicy + */ + public function albumProtectionPolicy(): AlbumProtectionPolicy + { + return $this->albumProtectionPolicy; + } + + public function isPasswordProvided(): bool + { + return $this->isPasswordProvided; + } +} diff --git a/app/Http/Requests/Album/SetAlbumTrackRequest.php b/app/Http/Requests/Album/SetAlbumTrackRequest.php new file mode 100644 index 00000000000..3abb8150c85 --- /dev/null +++ b/app/Http/Requests/Album/SetAlbumTrackRequest.php @@ -0,0 +1,54 @@ + ['required', new AlbumIDRule(false)], + self::FILE_ATTRIBUTE => 'required|file', + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + /** @var string|null */ + $albumID = $values[RequestAttribute::ALBUM_ID_ATTRIBUTE]; + $this->album = Album::query()->findOrFail($albumID); + $this->file = $files[self::FILE_ATTRIBUTE]; + } + + public function uploadedFile(): UploadedFile + { + return $this->file; + } +} diff --git a/app/Http/Requests/Album/SetAsCoverRequest.php b/app/Http/Requests/Album/SetAsCoverRequest.php new file mode 100644 index 00000000000..b47fa8b3e1e --- /dev/null +++ b/app/Http/Requests/Album/SetAsCoverRequest.php @@ -0,0 +1,67 @@ +album]) && + Gate::check(PhotoPolicy::CAN_EDIT, [Photo::class, $this->photo]); + } + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return [ + RequestAttribute::ALBUM_ID_ATTRIBUTE => ['required', new RandomIDRule(false)], + RequestAttribute::PHOTO_ID_ATTRIBUTE => ['required', new RandomIDRule(false)], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + $album = $this->albumFactory->findBaseAlbumOrFail( + $values[RequestAttribute::ALBUM_ID_ATTRIBUTE] + ); + + if (!$album instanceof Album) { + throw ValidationException::withMessages([RequestAttribute::ALBUM_ID_ATTRIBUTE => 'album type not supported.']); + } + + $this->album = $album; + /** @var string $photoId */ + $photoId = $values[RequestAttribute::PHOTO_ID_ATTRIBUTE]; + $this->photo = Photo::query()->findOrFail($photoId); + } +} diff --git a/app/Http/Requests/Album/SetAsHeaderRequest.php b/app/Http/Requests/Album/SetAsHeaderRequest.php new file mode 100644 index 00000000000..672f4129c7c --- /dev/null +++ b/app/Http/Requests/Album/SetAsHeaderRequest.php @@ -0,0 +1,75 @@ +album]) && + ($this->is_compact || ($this->photo->album_id === $this->album->id)); + } + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return [ + RequestAttribute::ALBUM_ID_ATTRIBUTE => ['required', new RandomIDRule(false)], + RequestAttribute::HEADER_ID_ATTRIBUTE => ['required', new RandomIDRule(true)], + RequestAttribute::IS_COMPACT_ATTRIBUTE => ['required', 'boolean'], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + $album = $this->albumFactory->findBaseAlbumOrFail( + $values[RequestAttribute::ALBUM_ID_ATTRIBUTE] + ); + + if (!$album instanceof Album) { + throw ValidationException::withMessages([RequestAttribute::ALBUM_ID_ATTRIBUTE => 'album type not supported.']); + } + + $this->album = $album; + $this->is_compact = static::toBoolean($values[RequestAttribute::IS_COMPACT_ATTRIBUTE]); + + if ($this->is_compact) { + return; + } + + /** @var string $photoId */ + $photoId = $values[RequestAttribute::HEADER_ID_ATTRIBUTE]; + $this->photo = Photo::query()->findOrFail($photoId); + } +} diff --git a/app/Http/Requests/Album/TargetListAlbumRequest.php b/app/Http/Requests/Album/TargetListAlbumRequest.php new file mode 100644 index 00000000000..95f7cf26672 --- /dev/null +++ b/app/Http/Requests/Album/TargetListAlbumRequest.php @@ -0,0 +1,57 @@ + + */ +class TargetListAlbumRequest extends BaseApiRequest implements HasAlbums +{ + /** @phpstan-use HasAlbumsTrait */ + use HasAlbumsTrait; + + /** + * {@inheritDoc} + */ + public function authorize(): bool + { + return Gate::check(AlbumPolicy::CAN_EDIT_ID, [AbstractAlbum::class, $this->albums->map(fn (Album $album): string => $album->id)->toArray()]); + } + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return [ + RequestAttribute::ALBUM_IDS_ATTRIBUTE => 'sometimes|array|min:1', + RequestAttribute::ALBUM_IDS_ATTRIBUTE . '.*' => ['required', new AlbumIDRule(false)], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + $album_ids = $values[RequestAttribute::ALBUM_IDS_ATTRIBUTE] ?? []; + /** @phpstan-ignore-next-line */ + $this->albums = $this->albumFactory->findAbstractAlbumsOrFail($album_ids); + } +} diff --git a/app/Http/Requests/Album/TransferAlbumRequest.php b/app/Http/Requests/Album/TransferAlbumRequest.php new file mode 100644 index 00000000000..9e6f9e8c30c --- /dev/null +++ b/app/Http/Requests/Album/TransferAlbumRequest.php @@ -0,0 +1,61 @@ +album()]); + } + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return [ + RequestAttribute::ALBUM_ID_ATTRIBUTE => ['required', new AlbumIDRule(false)], + RequestAttribute::USER_ID_ATTRIBUTE => ['required', new IntegerIDRule(false)], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + /** @var int $user_id */ + $user_id = $values[RequestAttribute::USER_ID_ATTRIBUTE]; + $this->user2 = User::findOrFail($user_id); + // We don't need the full albums, just the IDs, so we don't load the + // models for efficiency reasons. + // Instead, we use mass deletion via low-level SQL queries later. + $this->album = $this->albumFactory->findBaseAlbumOrFail($values[RequestAttribute::ALBUM_ID_ATTRIBUTE]); + } +} diff --git a/app/Http/Requests/Album/UnlockAlbumRequest.php b/app/Http/Requests/Album/UnlockAlbumRequest.php new file mode 100644 index 00000000000..7636fd822f1 --- /dev/null +++ b/app/Http/Requests/Album/UnlockAlbumRequest.php @@ -0,0 +1,54 @@ + ['required', new RandomIDRule(false)], + RequestAttribute::PASSWORD_ATTRIBUTE => ['required', new PasswordRule(false)], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + $this->album = $this->albumFactory->findBaseAlbumOrFail( + $values[RequestAttribute::ALBUM_ID_ATTRIBUTE] + ); + $this->password = $values[RequestAttribute::PASSWORD_ATTRIBUTE]; + } +} diff --git a/app/Http/Requests/Album/UpdateAlbumRequest.php b/app/Http/Requests/Album/UpdateAlbumRequest.php new file mode 100644 index 00000000000..2000724add2 --- /dev/null +++ b/app/Http/Requests/Album/UpdateAlbumRequest.php @@ -0,0 +1,164 @@ +album]) && + ($this->is_compact || + $this->photo === null || + $this->photo->album_id === $this->album->id); + } + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return [ + RequestAttribute::ALBUM_ID_ATTRIBUTE => ['required', new RandomIDRule(false)], + RequestAttribute::TITLE_ATTRIBUTE => ['required', new TitleRule()], + RequestAttribute::LICENSE_ATTRIBUTE => ['required', new Enum(LicenseType::class)], + RequestAttribute::DESCRIPTION_ATTRIBUTE => ['present', new DescriptionRule()], + RequestAttribute::PHOTO_SORTING_COLUMN_ATTRIBUTE => ['present', 'nullable', new Enum(ColumnSortingPhotoType::class)], + RequestAttribute::PHOTO_SORTING_ORDER_ATTRIBUTE => [ + 'required_with:' . RequestAttribute::PHOTO_SORTING_COLUMN_ATTRIBUTE, + 'nullable', new Enum(OrderSortingType::class), + ], + RequestAttribute::ALBUM_SORTING_COLUMN_ATTRIBUTE => ['present', 'nullable', new Enum(ColumnSortingAlbumType::class)], + RequestAttribute::ALBUM_SORTING_ORDER_ATTRIBUTE => [ + 'required_with:' . RequestAttribute::ALBUM_SORTING_COLUMN_ATTRIBUTE, + 'nullable', new Enum(OrderSortingType::class), + ], + RequestAttribute::ALBUM_ASPECT_RATIO_ATTRIBUTE => ['present', 'nullable', new Enum(AspectRatioType::class)], + RequestAttribute::ALBUM_PHOTO_LAYOUT => ['present', 'nullable', new Enum(PhotoLayoutType::class)], + RequestAttribute::COPYRIGHT_ATTRIBUTE => ['present', 'nullable', new CopyrightRule()], + RequestAttribute::IS_COMPACT_ATTRIBUTE => ['required', 'boolean'], + RequestAttribute::HEADER_ID_ATTRIBUTE => ['present', new RandomIDRule(true)], + RequestAttribute::ALBUM_TIMELINE_ALBUM => ['present', 'nullable', new Enum(TimelineAlbumGranularity::class), new EnumRequireSupportRule(TimelinePhotoGranularity::class, [TimelinePhotoGranularity::DEFAULT, TimelinePhotoGranularity::DISABLED], $this->verify)], + RequestAttribute::ALBUM_TIMELINE_PHOTO => ['present', 'nullable', new Enum(TimelinePhotoGranularity::class), new EnumRequireSupportRule(TimelinePhotoGranularity::class, [TimelinePhotoGranularity::DEFAULT, TimelinePhotoGranularity::DISABLED], $this->verify)], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + $album = $this->albumFactory->findBaseAlbumOrFail( + $values[RequestAttribute::ALBUM_ID_ATTRIBUTE] + ); + + if (!$album instanceof Album) { + throw ValidationException::withMessages([RequestAttribute::ALBUM_ID_ATTRIBUTE => 'album type not supported.']); + } + + $this->album = $album; + $this->title = $values[RequestAttribute::TITLE_ATTRIBUTE]; + $this->description = $values[RequestAttribute::DESCRIPTION_ATTRIBUTE]; + $this->license = LicenseType::tryFrom($values[RequestAttribute::LICENSE_ATTRIBUTE]); + + $photoColumn = ColumnSortingPhotoType::tryFrom($values[RequestAttribute::PHOTO_SORTING_COLUMN_ATTRIBUTE]); + $photoOrder = OrderSortingType::tryFrom($values[RequestAttribute::PHOTO_SORTING_ORDER_ATTRIBUTE]); + + $this->photoSortingCriterion = $photoColumn === null ? + null : + new PhotoSortingCriterion($photoColumn->toColumnSortingType(), $photoOrder); + + $albumColumn = ColumnSortingPhotoType::tryFrom($values[RequestAttribute::ALBUM_SORTING_COLUMN_ATTRIBUTE]); + $albumOrder = OrderSortingType::tryFrom($values[RequestAttribute::ALBUM_SORTING_ORDER_ATTRIBUTE]); + + $this->albumSortingCriterion = $albumColumn === null ? + null : + new AlbumSortingCriterion($albumColumn->toColumnSortingType(), $albumOrder); + + $this->aspectRatio = AspectRatioType::tryFrom($values[RequestAttribute::ALBUM_ASPECT_RATIO_ATTRIBUTE]); + $this->photoLayout = PhotoLayoutType::tryFrom($values[RequestAttribute::ALBUM_PHOTO_LAYOUT]); + $this->album_timeline = TimelineAlbumGranularity::tryFrom($values[RequestAttribute::ALBUM_TIMELINE_ALBUM]); + $this->photo_timeline = TimelinePhotoGranularity::tryFrom($values[RequestAttribute::ALBUM_TIMELINE_PHOTO]); + + $this->copyright = $values[RequestAttribute::COPYRIGHT_ATTRIBUTE]; + + $this->is_compact = static::toBoolean($values[RequestAttribute::IS_COMPACT_ATTRIBUTE]); + + if ($this->is_compact) { + return; + } + + /** @var string|null $photoId */ + $photoId = $values[RequestAttribute::HEADER_ID_ATTRIBUTE]; + $this->photo = $photoId !== null ? Photo::query()->findOrFail($photoId) : null; + } +} diff --git a/app/Http/Requests/Album/UpdateTagAlbumRequest.php b/app/Http/Requests/Album/UpdateTagAlbumRequest.php new file mode 100644 index 00000000000..f06740d723f --- /dev/null +++ b/app/Http/Requests/Album/UpdateTagAlbumRequest.php @@ -0,0 +1,107 @@ + ['required', new RandomIDRule(false)], + RequestAttribute::TITLE_ATTRIBUTE => ['required', new TitleRule()], + RequestAttribute::DESCRIPTION_ATTRIBUTE => ['present', new DescriptionRule()], + RequestAttribute::PHOTO_SORTING_COLUMN_ATTRIBUTE => ['present', 'nullable', new Enum(ColumnSortingPhotoType::class)], + RequestAttribute::PHOTO_SORTING_ORDER_ATTRIBUTE => [ + 'required_with:' . RequestAttribute::PHOTO_SORTING_COLUMN_ATTRIBUTE, + 'nullable', new Enum(OrderSortingType::class), + ], + RequestAttribute::TAGS_ATTRIBUTE => 'required|array|min:1', + RequestAttribute::TAGS_ATTRIBUTE . '.*' => 'required|string|min:1', + RequestAttribute::COPYRIGHT_ATTRIBUTE => ['present', 'nullable', new CopyrightRule()], + RequestAttribute::ALBUM_PHOTO_LAYOUT => ['present', 'nullable', new Enum(PhotoLayoutType::class)], + RequestAttribute::ALBUM_TIMELINE_PHOTO => ['present', 'nullable', new Enum(TimelinePhotoGranularity::class), new EnumRequireSupportRule(TimelinePhotoGranularity::class, [TimelinePhotoGranularity::DEFAULT, TimelinePhotoGranularity::DISABLED], $this->verify)], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + $album = $this->albumFactory->findBaseAlbumOrFail( + $values[RequestAttribute::ALBUM_ID_ATTRIBUTE] + ); + + if (!$album instanceof TagAlbum) { + throw ValidationException::withMessages([RequestAttribute::ALBUM_ID_ATTRIBUTE => 'album type not supported.']); + } + + $this->album = $album; + $this->title = $values[RequestAttribute::TITLE_ATTRIBUTE]; + $this->description = $values[RequestAttribute::DESCRIPTION_ATTRIBUTE]; + + $photoColumn = ColumnSortingPhotoType::tryFrom($values[RequestAttribute::PHOTO_SORTING_COLUMN_ATTRIBUTE]); + $photoOrder = OrderSortingType::tryFrom($values[RequestAttribute::PHOTO_SORTING_ORDER_ATTRIBUTE]); + + $this->photoSortingCriterion = $photoColumn === null ? + null : + new PhotoSortingCriterion($photoColumn->toColumnSortingType(), $photoOrder); + + $this->photoLayout = PhotoLayoutType::tryFrom($values[RequestAttribute::ALBUM_PHOTO_LAYOUT]); + $this->photo_timeline = TimelinePhotoGranularity::tryFrom($values[RequestAttribute::ALBUM_TIMELINE_PHOTO]); + $this->copyright = $values[RequestAttribute::COPYRIGHT_ATTRIBUTE]; + $this->tags = $values[RequestAttribute::TAGS_ATTRIBUTE]; + } +} diff --git a/app/Http/Requests/Album/ZipRequest.php b/app/Http/Requests/Album/ZipRequest.php new file mode 100644 index 00000000000..b9ad3e30d9d --- /dev/null +++ b/app/Http/Requests/Album/ZipRequest.php @@ -0,0 +1,142 @@ + + */ +class ZipRequest extends BaseApiRequest implements HasAlbums, HasPhotos, HasSizeVariant +{ + /** @use HasAlbumsTrait */ + use HasAlbumsTrait; + use HasPhotosTrait; + use HasSizeVariantTrait; + + /** + * {@inheritDoc} + */ + public function authorize(): bool + { + /** @var AbstractAlbum $album */ + foreach ($this->albums as $album) { + if (!Gate::check(AlbumPolicy::CAN_DOWNLOAD, $album)) { + return false; + } + } + + /** @var Photo $photo */ + foreach ($this->photos as $photo) { + if (!Gate::check(PhotoPolicy::CAN_DOWNLOAD, $photo)) { + return false; + } + } + + return true; + } + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return [ + RequestAttribute::ALBUM_IDS_ATTRIBUTE => ['sometimes', new AlbumIDListRule()], + RequestAttribute::PHOTO_IDS_ATTRIBUTE => ['sometimes', new RandomIDListRule()], + RequestAttribute::SIZE_VARIANT_ATTRIBUTE => ['required_if_accepted:photos_ids', new Enum(DownloadVariantType::class)], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + $album_ids = $values[RequestAttribute::ALBUM_IDS_ATTRIBUTE] ?? null; + $album_ids = $album_ids === null ? [] : explode(',', $album_ids); + $this->processAlbums($album_ids); + + // only interesting if we have no albums + $this->sizeVariant = DownloadVariantType::tryFrom($values[RequestAttribute::SIZE_VARIANT_ATTRIBUTE] ?? ''); + + $photo_ids = $values[RequestAttribute::PHOTO_IDS_ATTRIBUTE] ?? null; + $photo_ids = $photo_ids === null ? [] : explode(',', $photo_ids); + $this->processPhotos($photo_ids); + } + + /** + * Process albums. Set to empty collection if no albums are requested. + * + * @param string[] $album_ids + * + * @return void + */ + private function processAlbums(array $album_ids): void + { + if (count($album_ids) === 0) { + $this->albums = collect(); + + return; + } + + // TODO: `App\Actions\Album\Archive::compressAlbum` iterates over the original size variant of each photo in the album; we should eagerly load them for higher efficiency. + $this->albums = $this->albumFactory->findAbstractAlbumsOrFail($album_ids); + } + + /** + * Process photos. Set to empty collection if no photos are requested. + * + * @param string[] $photo_ids + * + * @return void + */ + private function processPhotos(array $photo_ids): void + { + if (count($photo_ids) === 0) { + $this->photos = collect(); + + return; + } + + $photoQuery = Photo::query()->with(['album']); + // The condition is required, because Lychee also supports to archive + // the "live video" as a size variant which is not a proper size variant + $variant = $this->sizeVariant->getSizeVariantType(); + if ($variant !== null) { // NOT LIVE PHOTO + // If a proper size variant is requested, eagerly load the size + // variants but only the requested type due to efficiency reasons + $photoQuery = $photoQuery->with([ + 'size_variants' => fn ($r) => $r->where('type', '=', $variant), + ]); + } + + // `findOrFail` returns the union `Photo|Collection` + // which is not assignable to `Collection`; but as we query + // with an array of IDs we never get a single entity (even if the + // array only contains a single ID). + $this->photos = $photoQuery->findOrFail($photo_ids); + } +} diff --git a/app/Http/Requests/AlbumRequests/AlbumIDRequest.php b/app/Http/Requests/AlbumRequests/AlbumIDRequest.php deleted file mode 100644 index 0e8fdfbdca3..00000000000 --- a/app/Http/Requests/AlbumRequests/AlbumIDRequest.php +++ /dev/null @@ -1,30 +0,0 @@ - 'required|string', - ]; - } -} diff --git a/app/Http/Requests/AlbumRequests/AlbumIDRequestInt.php b/app/Http/Requests/AlbumRequests/AlbumIDRequestInt.php deleted file mode 100644 index b50b3175715..00000000000 --- a/app/Http/Requests/AlbumRequests/AlbumIDRequestInt.php +++ /dev/null @@ -1,30 +0,0 @@ - 'required|integer', - ]; - } -} diff --git a/app/Http/Requests/AlbumRequests/AlbumIDsRequest.php b/app/Http/Requests/AlbumRequests/AlbumIDsRequest.php deleted file mode 100644 index 66b568496d8..00000000000 --- a/app/Http/Requests/AlbumRequests/AlbumIDsRequest.php +++ /dev/null @@ -1,30 +0,0 @@ - 'required|string', - ]; - } -} diff --git a/app/Http/Requests/BaseApiRequest.php b/app/Http/Requests/BaseApiRequest.php new file mode 100644 index 00000000000..82a96c4c9bc --- /dev/null +++ b/app/Http/Requests/BaseApiRequest.php @@ -0,0 +1,173 @@ +albumFactory = resolve(AlbumFactory::class); + $this->verify = resolve(Verify::class); + parent::__construct($query, $request, $attributes, $cookies, $files, $server, $content); + } catch (BindingResolutionException $e) { + throw new FrameworkException('Laravel\'s provider component', $e); + } + } + + /** + * Validate the class instance. + * + * Fixes another Laravel stupidity. + * We must **first** validate the input parameters of the request + * for syntactical correctness, and **then** authorize the request. + * Rationale: Whether a user is authorized to perform a specific action or + * not typically depends on the input parameters (e.g. the ID of the model, + * the property the user wants to change, the new value of the property, + * etc.). + * Hence, the input should be validated **before** a potential DB query is + * executed to determine the user's authorization. + * The original Laravel method tries to authorize the user first and + * then validate the request + * (see {@link \Illuminate\Validation\ValidatesWhenResolvedTrait::validateResolved()}). + * + * @return void + * + * @throws BindingResolutionException + * @throws ValidationException + * @throws UnauthorizedException + * @throws BadRequestException + */ + public function validateResolved(): void + { + // 1. Validate the request + $this->prepareForValidation(); + $instance = $this->getValidatorInstance(); + if ($instance->fails()) { + // the default implementation throws `ValidationException` + $this->failedValidation($instance); + } + $this->passedValidation(); + + // 2. Authorize the request + if (!$this->passesAuthorization()) { + $this->failedAuthorization(); + } + } + + /** + * Called by the framework after successful input validation. + * + * Simply forwards the call to {@link BaseApiRequest::processValidatedValues()} + * of the child class. + * + * @throws ValidationException + * @throws BadRequestException + * @throws ModelNotFoundException + * @throws InvalidSmartIdException + * @throws QueryBuilderException + */ + protected function passedValidation() + { + $this->processValidatedValues($this->validated(), $this->allFiles()); + } + + /** + * Handles a failed authorization attempt. + * + * Always throws either {@link UnauthorizedException} or + * {@link UnauthenticatedException}. + * + * @return void + * + * @throws UnauthorizedException + * @throws UnauthenticatedException + */ + protected function failedAuthorization(): void + { + throw Auth::check() ? new UnauthorizedException() : new UnauthenticatedException(); + } + + /** + * Converts the input value to a boolean. + * + * Opposed to trivial type-casting the conversion also correctly recognizes + * the inputs `0`, `1`, `'0'`, `'1'`, `'true'` and `'false'`. + * + * @param mixed $value + * + * @return bool + */ + protected static function toBoolean(mixed $value): bool + { + return filter_var($value, FILTER_VALIDATE_BOOLEAN); + } + + /** + * Determines if the user is authorized to make this request. + * + * @return bool + * + * @throws LycheeException + */ + abstract public function authorize(): bool; + + /** + * Returns the validation rules that apply to the request. + * + * @return array> + */ + abstract public function rules(): array; + + /** + * Post-processes the validated values. + * + * @param array $values + * @param UploadedFile[] $files + * + * @return void + * + * @throws ModelNotFoundException + * @throws InvalidSmartIdException + * @throws QueryBuilderException + */ + abstract protected function processValidatedValues(array $values, array $files): void; +} diff --git a/app/Http/Requests/Diagnostics/DiagnosticsRequest.php b/app/Http/Requests/Diagnostics/DiagnosticsRequest.php new file mode 100644 index 00000000000..e40453420d1 --- /dev/null +++ b/app/Http/Requests/Diagnostics/DiagnosticsRequest.php @@ -0,0 +1,28 @@ +album]); + } + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return [ + RequestAttribute::ALBUM_ID_ATTRIBUTE => ['sometimes', new AlbumIDRule(true)], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + if (!Configs::getValueAsBool('mod_frame_enabled')) { + throw new UnauthorizedException(); + } + + $randomAlbumId = Configs::getValueAsString('random_album_id'); + $albumId = $values[RequestAttribute::ALBUM_ID_ATTRIBUTE] ?? (($randomAlbumId !== '') ? $randomAlbumId : null); + $this->album = $this->albumFactory->findNullalbleAbstractAlbumOrFail($albumId); + } +} \ No newline at end of file diff --git a/app/Http/Requests/ImportRequests/ImportServerRequest.php b/app/Http/Requests/ImportRequests/ImportServerRequest.php deleted file mode 100644 index efbd0a2e5b9..00000000000 --- a/app/Http/Requests/ImportRequests/ImportServerRequest.php +++ /dev/null @@ -1,35 +0,0 @@ - 'string|required', - 'albumID' => 'int|required', - 'delete_imported' => 'int', - 'import_via_symlink' => 'int', - 'skip_duplicates' => 'int', - 'resync_metadata' => 'int', - ]; - } -} diff --git a/app/Http/Requests/ImportRequests/ImportUrlRequest.php b/app/Http/Requests/ImportRequests/ImportUrlRequest.php deleted file mode 100644 index ee291e81311..00000000000 --- a/app/Http/Requests/ImportRequests/ImportUrlRequest.php +++ /dev/null @@ -1,31 +0,0 @@ - 'string|required', - 'albumID' => 'string|required', - ]; - } -} diff --git a/app/Http/Requests/Install/SetUpAdminRequest.php b/app/Http/Requests/Install/SetUpAdminRequest.php new file mode 100644 index 00000000000..fb38008ae9e --- /dev/null +++ b/app/Http/Requests/Install/SetUpAdminRequest.php @@ -0,0 +1,55 @@ +> + */ + public function rules(): array + { + return [ + RequestAttribute::USERNAME_ATTRIBUTE => ['required', new UsernameRule()], + RequestAttribute::PASSWORD_ATTRIBUTE => ['required', 'confirmed', new PasswordRule(false)], + ]; + } + + /** + * This Request is only available if the application is not installed yet. + * Thus, there's no authorization check here. + * + * @return bool + */ + public function authorize(): bool + { + return true; + } + + public function passedValidation() + { + $values = $this->validated(); + $this->username = $values[RequestAttribute::USERNAME_ATTRIBUTE]; + $this->password = $values[RequestAttribute::PASSWORD_ATTRIBUTE]; + } +} diff --git a/app/Http/Requests/Jobs/ShowJobsRequest.php b/app/Http/Requests/Jobs/ShowJobsRequest.php new file mode 100644 index 00000000000..08c9b9fc8e1 --- /dev/null +++ b/app/Http/Requests/Jobs/ShowJobsRequest.php @@ -0,0 +1,25 @@ + [ + 'required', + 'string', + Rule::in([ + 'filesystems.disks.extract-jobs.root', + 'filesystems.disks.image-jobs.root', + 'filesystems.disks.image-upload.root']), + ], + ]; + } + + protected function processValidatedValues(array $values, array $files): void + { + $this->path = config($values[RequestAttribute::SINGLE_PATH_ATTRIBUTE]); + } + + /** + * {@inheritDoc} + */ + public function authorize(): bool + { + return Gate::check(SettingsPolicy::CAN_EDIT, Configs::class); + } + + public function path(): string + { + return $this->path; + } +} diff --git a/app/Http/Requests/Maintenance/CreateThumbsRequest.php b/app/Http/Requests/Maintenance/CreateThumbsRequest.php new file mode 100644 index 00000000000..70eda0504b4 --- /dev/null +++ b/app/Http/Requests/Maintenance/CreateThumbsRequest.php @@ -0,0 +1,55 @@ + [ + 'required', + Rule::in([ + SizeVariantType::PLACEHOLDER, + SizeVariantType::SMALL->value, + SizeVariantType::SMALL2X->value, + SizeVariantType::MEDIUM->value, + SizeVariantType::MEDIUM2X->value]), + ], + ]; + } + + protected function processValidatedValues(array $values, array $files): void + { + $this->kind = SizeVariantType::from($values[RequestAttribute::SIZE_VARIANT_ATTRIBUTE]); + } + + /** + * {@inheritDoc} + */ + public function authorize(): bool + { + return Gate::check(SettingsPolicy::CAN_EDIT, Configs::class); + } + + public function kind(): SizeVariantType + { + return $this->kind; + } +} diff --git a/app/Http/Requests/Maintenance/FullTreeUpdateRequest.php b/app/Http/Requests/Maintenance/FullTreeUpdateRequest.php new file mode 100644 index 00000000000..e0a8582c849 --- /dev/null +++ b/app/Http/Requests/Maintenance/FullTreeUpdateRequest.php @@ -0,0 +1,58 @@ + + */ + private array $albums; + + /** + * {@inheritDoc} + */ + public function authorize(): bool + { + return Gate::check(SettingsPolicy::CAN_EDIT, Configs::class); + } + + public function rules(): array + { + return [ + 'albums' => 'required|array|min:1', + 'albums.*' => 'required|array', + 'albums.*.id' => ['required', new AlbumIDRule(false)], + 'albums.*._lft' => 'required|integer|min:1', + 'albums.*._rgt' => 'required|integer|min:1', + 'albums.*.parent_id' => [new AlbumIDRule(true)], + ]; + } + + protected function processValidatedValues( + array $values, + array $files, + ): void { + $this->albums = $values['albums']; + } + + /** + * @return array + */ + public function albums(): array + { + return $this->albums; + } +} diff --git a/app/Http/Requests/Maintenance/MaintenanceRequest.php b/app/Http/Requests/Maintenance/MaintenanceRequest.php new file mode 100644 index 00000000000..f67ec8de34f --- /dev/null +++ b/app/Http/Requests/Maintenance/MaintenanceRequest.php @@ -0,0 +1,25 @@ + $this->username(), 'password' => $this->password()]); + + // Check if logged in AND is admin + return $isLoggedIn && Gate::check(SettingsPolicy::CAN_UPDATE, Configs::class); + } + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return [ + RequestAttribute::USERNAME_ATTRIBUTE => ['sometimes', new UsernameRule()], + RequestAttribute::PASSWORD_ATTRIBUTE => ['sometimes', new PasswordRule(false)], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + $this->username = $values[RequestAttribute::USERNAME_ATTRIBUTE] ?? ''; + $this->password = $values[RequestAttribute::PASSWORD_ATTRIBUTE] ?? ''; + } + + /** + * {@inheritDoc} + */ + protected function failedAuthorization(): void + { + throw new HttpResponseException(response()->view('update.error', ['code' => '403', 'message' => 'Incorrect username or password'], 403)); + } +} diff --git a/app/Http/Requests/Maintenance/RegisterRequest.php b/app/Http/Requests/Maintenance/RegisterRequest.php new file mode 100644 index 00000000000..40a3a15aa49 --- /dev/null +++ b/app/Http/Requests/Maintenance/RegisterRequest.php @@ -0,0 +1,47 @@ + ['required', 'string', 'max:255'], + ]; + } + + protected function processValidatedValues( + #[\SensitiveParameter] + array $values, + array $files): void + { + $this->key = new \SensitiveParameterValue($values['key']); + } + + /** + * {@inheritDoc} + */ + public function authorize(): bool + { + return Gate::check(SettingsPolicy::CAN_EDIT, Configs::class); + } + + public function key(): \SensitiveParameterValue + { + return $this->key; + } +} diff --git a/app/Http/Requests/Maintenance/UpdateRequest.php b/app/Http/Requests/Maintenance/UpdateRequest.php new file mode 100644 index 00000000000..1b35c8c28ce --- /dev/null +++ b/app/Http/Requests/Maintenance/UpdateRequest.php @@ -0,0 +1,25 @@ +album]); + } + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return [ + RequestAttribute::ALBUM_ID_ATTRIBUTE => ['sometimes', new RandomIDRule(true)], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + /** @var string|null $albumId */ + $albumId = $values[RequestAttribute::ALBUM_ID_ATTRIBUTE] ?? null; + $this->album = $this->albumFactory->findNullalbleAbstractAlbumOrFail($albumId); + } +} \ No newline at end of file diff --git a/app/Http/Requests/Photo/CopyPhotosRequest.php b/app/Http/Requests/Photo/CopyPhotosRequest.php new file mode 100644 index 00000000000..107b2ad7cad --- /dev/null +++ b/app/Http/Requests/Photo/CopyPhotosRequest.php @@ -0,0 +1,56 @@ + 'required|array|min:1', + RequestAttribute::PHOTO_IDS_ATTRIBUTE . '.*' => ['required', new RandomIDRule(false)], + RequestAttribute::ALBUM_ID_ATTRIBUTE => ['present', new RandomIDRule(true)], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + /** @var array $photosIDs */ + $photosIDs = $values[RequestAttribute::PHOTO_IDS_ATTRIBUTE]; + $this->photos = Photo::query() + ->with(['size_variants']) + ->findOrFail($photosIDs); + /** @var string|null */ + $targetAlbumID = $values[RequestAttribute::ALBUM_ID_ATTRIBUTE]; + $this->album = $targetAlbumID === null ? + null : + Album::query()->findOrFail($targetAlbumID); + } +} diff --git a/app/Http/Requests/Photo/DeletePhotosRequest.php b/app/Http/Requests/Photo/DeletePhotosRequest.php new file mode 100644 index 00000000000..3d67199052c --- /dev/null +++ b/app/Http/Requests/Photo/DeletePhotosRequest.php @@ -0,0 +1,53 @@ +photoIds()]); + } + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return [ + RequestAttribute::PHOTO_IDS_ATTRIBUTE => 'required|array|min:1', + RequestAttribute::PHOTO_IDS_ATTRIBUTE . '.*' => ['required', new RandomIDRule(false)], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + // As we are going to delete the photos anyway, we don't load the + // models for efficiency reasons. + // Instead, we use mass deletion via low-level SQL queries later. + $this->photoIds = $values[RequestAttribute::PHOTO_IDS_ATTRIBUTE]; + } +} diff --git a/app/Http/Requests/Photo/EditPhotoRequest.php b/app/Http/Requests/Photo/EditPhotoRequest.php new file mode 100644 index 00000000000..3a95390aa05 --- /dev/null +++ b/app/Http/Requests/Photo/EditPhotoRequest.php @@ -0,0 +1,86 @@ +photo); + } + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return [ + RequestAttribute::PHOTO_ID_ATTRIBUTE => ['required', new RandomIDRule(false)], + RequestAttribute::TITLE_ATTRIBUTE => ['required', new TitleRule()], + RequestAttribute::DESCRIPTION_ATTRIBUTE => ['present', new DescriptionRule()], + RequestAttribute::TAGS_ATTRIBUTE => ['present', 'array'], + RequestAttribute::TAGS_ATTRIBUTE . '.*' => ['required', 'string', 'min:1'], + RequestAttribute::LICENSE_ATTRIBUTE => ['required', new Enum(LicenseType::class)], + RequestAttribute::UPLOAD_DATE_ATTRIBUTE => ['required', 'date'], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + /** @var string $photoID */ + $photoID = $values[RequestAttribute::PHOTO_ID_ATTRIBUTE]; + + $this->photo = Photo::query() + ->with(['size_variants', 'size_variants.sym_links']) + ->findOrFail($photoID); + + $this->title = $values[RequestAttribute::TITLE_ATTRIBUTE]; + $this->description = $values[RequestAttribute::DESCRIPTION_ATTRIBUTE]; + $this->tags = $values[RequestAttribute::TAGS_ATTRIBUTE]; + $this->upload_date = Carbon::parse($values[RequestAttribute::UPLOAD_DATE_ATTRIBUTE]); + $this->license = LicenseType::tryFrom($values[RequestAttribute::LICENSE_ATTRIBUTE]); + } +} diff --git a/app/Http/Requests/Photo/FromUrlRequest.php b/app/Http/Requests/Photo/FromUrlRequest.php new file mode 100644 index 00000000000..4e309872354 --- /dev/null +++ b/app/Http/Requests/Photo/FromUrlRequest.php @@ -0,0 +1,83 @@ +album]); + } + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return [ + RequestAttribute::ALBUM_ID_ATTRIBUTE => ['present', new RandomIDRule(true)], + RequestAttribute::URLS_ATTRIBUTE => 'required|array|min:1', + RequestAttribute::URLS_ATTRIBUTE . '.*' => 'required|string', + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + $this->album = null; + /** @var string|null $id */ + $id = $values[RequestAttribute::ALBUM_ID_ATTRIBUTE]; + if ($id !== null) { + $this->album = Album::query()->findOrFail($id); + } + $this->urls = $values[RequestAttribute::URLS_ATTRIBUTE]; + + // The replacement below looks suspicious. + // If it was really necessary, then there would be much more special + // characters (for example umlauts in international domain names) + // which would require replacement by their corresponding %-encoding. + // However, I assume that the PHP method `fopen` is happily fine with + // any character and internally handles special characters itself. + // Hence, either use a proper encoding method here instead of our + // home-brewed, poor-man replacement or drop it entirely. + // TODO: Find out what is needed and proceed accordingly. + // ? We can't use URL encode because we need to preserve :// and ? + $this->urls = str_replace(' ', '%20', $this->urls); + } + + /** + * @return string[] + */ + public function urls(): array + { + return $this->urls; + } +} \ No newline at end of file diff --git a/app/Http/Requests/Photo/MovePhotosRequest.php b/app/Http/Requests/Photo/MovePhotosRequest.php new file mode 100644 index 00000000000..54bae753f89 --- /dev/null +++ b/app/Http/Requests/Photo/MovePhotosRequest.php @@ -0,0 +1,53 @@ + 'required|array|min:1', + RequestAttribute::PHOTO_IDS_ATTRIBUTE . '.*' => ['required', new RandomIDRule(false)], + RequestAttribute::ALBUM_ID_ATTRIBUTE => ['present', new RandomIDRule(true)], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + /** @var array $photosIss */ + $photosIss = $values[RequestAttribute::PHOTO_IDS_ATTRIBUTE]; + $this->photos = Photo::query() + ->findOrFail($photosIss); + /** @var string|null */ + $targetAlbumID = $values[RequestAttribute::ALBUM_ID_ATTRIBUTE]; + $this->album = $targetAlbumID === null ? null : Album::query()->findOrFail($targetAlbumID); + } +} diff --git a/app/Http/Requests/Photo/RenamePhotoRequest.php b/app/Http/Requests/Photo/RenamePhotoRequest.php new file mode 100644 index 00000000000..45d4d66b639 --- /dev/null +++ b/app/Http/Requests/Photo/RenamePhotoRequest.php @@ -0,0 +1,57 @@ +photo); + } + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return [ + RequestAttribute::PHOTO_ID_ATTRIBUTE => ['required', new RandomIDRule(false)], + RequestAttribute::TITLE_ATTRIBUTE => ['required', new TitleRule()], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + /** @var string $photoID */ + $photoID = $values[RequestAttribute::PHOTO_ID_ATTRIBUTE]; + $this->photo = Photo::query()->findOrFail($photoID); + $this->title = $values[RequestAttribute::TITLE_ATTRIBUTE]; + } +} diff --git a/app/Http/Requests/Photo/RotatePhotoRequest.php b/app/Http/Requests/Photo/RotatePhotoRequest.php new file mode 100644 index 00000000000..374fb6ec902 --- /dev/null +++ b/app/Http/Requests/Photo/RotatePhotoRequest.php @@ -0,0 +1,55 @@ + ['required', new RandomIDRule(false)], + RequestAttribute::DIRECTION_ATTRIBUTE => ['required', Rule::in([-1, 1])], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + /** @var ?string $photoID */ + $photoID = $values[RequestAttribute::PHOTO_ID_ATTRIBUTE]; + $this->photo = Photo::query() + ->with(['size_variants']) + ->findOrFail($photoID); + $this->direction = intval($values[RequestAttribute::DIRECTION_ATTRIBUTE]); + } + + public function direction(): int + { + return $this->direction; + } +} diff --git a/app/Http/Requests/Photo/SetPhotosStarredRequest.php b/app/Http/Requests/Photo/SetPhotosStarredRequest.php new file mode 100644 index 00000000000..b1409377067 --- /dev/null +++ b/app/Http/Requests/Photo/SetPhotosStarredRequest.php @@ -0,0 +1,58 @@ + 'required|array|min:1', + RequestAttribute::PHOTO_IDS_ATTRIBUTE . '.*' => ['required', new RandomIDRule(false)], + RequestAttribute::IS_STARRED_ATTRIBUTE => 'required|boolean', + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + /** @var array $photosIDs */ + $photosIDs = $values[RequestAttribute::PHOTO_IDS_ATTRIBUTE]; + $this->photos = Photo::query()->findOrFail($photosIDs); + $this->isStarred = static::toBoolean($values[RequestAttribute::IS_STARRED_ATTRIBUTE]); + } + + public function isStarred(): bool + { + return $this->isStarred; + } +} diff --git a/app/Http/Requests/Photo/SetPhotosTagsRequest.php b/app/Http/Requests/Photo/SetPhotosTagsRequest.php new file mode 100644 index 00000000000..0f39225bfd5 --- /dev/null +++ b/app/Http/Requests/Photo/SetPhotosTagsRequest.php @@ -0,0 +1,54 @@ + 'required|boolean', + RequestAttribute::PHOTO_IDS_ATTRIBUTE => 'required|array|min:1', + RequestAttribute::PHOTO_IDS_ATTRIBUTE . '.*' => ['required', new RandomIDRule(false)], + RequestAttribute::TAGS_ATTRIBUTE => 'present|array', + RequestAttribute::TAGS_ATTRIBUTE . '.*' => 'required|string|min:1', + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + /** @var array $photosIDs */ + $photosIDs = $values[RequestAttribute::PHOTO_IDS_ATTRIBUTE]; + $this->photos = Photo::query()->findOrFail($photosIDs); + $this->tags = $values[RequestAttribute::TAGS_ATTRIBUTE]; + $this->shallOverride = $values[RequestAttribute::SHALL_OVERRIDE_ATTRIBUTE]; + } +} \ No newline at end of file diff --git a/app/Http/Requests/Photo/UploadPhotoRequest.php b/app/Http/Requests/Photo/UploadPhotoRequest.php new file mode 100644 index 00000000000..231d44ffad1 --- /dev/null +++ b/app/Http/Requests/Photo/UploadPhotoRequest.php @@ -0,0 +1,99 @@ +album]); + } + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return [ + RequestAttribute::ALBUM_ID_ATTRIBUTE => ['present', new AlbumIDRule(true)], + RequestAttribute::FILE_LAST_MODIFIED_TIME => 'sometimes|nullable|numeric', + RequestAttribute::FILE_ATTRIBUTE => ['required', 'file'], + 'file_name' => 'required|string', + 'uuid_name' => ['present', new FileUuidRule()], + 'extension' => ['present', new ExtensionRule()], + 'chunk_number' => 'required|integer|min:1', + 'total_chunks' => 'required|integer|gte:chunk_number', + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + $this->album = $this->albumFactory->findNullalbleAbstractAlbumOrFail($values[RequestAttribute::ALBUM_ID_ATTRIBUTE]); + // Convert the File Last Modified to seconds instead of milliseconds + $val = $values[RequestAttribute::FILE_LAST_MODIFIED_TIME] ?? null; + $this->file_last_modified_time = $val !== null ? intval($val) : null; + $this->file_chunk = $files[RequestAttribute::FILE_ATTRIBUTE]; + $this->meta = new UploadMetaResource( + file_name: $values['file_name'], + extension: $values['extension'] ?? null, + uuid_name: $values['uuid_name'] ?? null, + stage: FileStatus::UPLOADING, + chunk_number: $values['chunk_number'], + total_chunks: $values['total_chunks'], + ); + } + + public function uploaded_file_chunk(): UploadedFile + { + return $this->file_chunk; + } + + public function file_last_modified_time(): ?int + { + return $this->file_last_modified_time !== null ? intval($this->file_last_modified_time / 1000) : null; + } + + public function meta(): UploadMetaResource + { + return $this->meta; + } +} diff --git a/app/Http/Requests/PhotoRequests/PhotoIDRequest.php b/app/Http/Requests/PhotoRequests/PhotoIDRequest.php deleted file mode 100644 index a9653b1f42e..00000000000 --- a/app/Http/Requests/PhotoRequests/PhotoIDRequest.php +++ /dev/null @@ -1,30 +0,0 @@ - 'required|string', - ]; - } -} diff --git a/app/Http/Requests/PhotoRequests/PhotoIDsRequest.php b/app/Http/Requests/PhotoRequests/PhotoIDsRequest.php deleted file mode 100644 index 621b68e1ffe..00000000000 --- a/app/Http/Requests/PhotoRequests/PhotoIDsRequest.php +++ /dev/null @@ -1,30 +0,0 @@ - 'required|string', - ]; - } -} diff --git a/app/Http/Requests/Profile/ChangeTokenRequest.php b/app/Http/Requests/Profile/ChangeTokenRequest.php new file mode 100644 index 00000000000..2c3b512065f --- /dev/null +++ b/app/Http/Requests/Profile/ChangeTokenRequest.php @@ -0,0 +1,25 @@ + ['required', 'string', new Enum(OauthProvidersType::class)], + ]; + } + + protected function processValidatedValues(array $values, array $files): void + { + $this->provider = OauthProvidersType::from($values[RequestAttribute::PROVIDER_ATTRIBUTE]); + } + + public function provider(): OauthProvidersType + { + return $this->provider; + } +} diff --git a/app/Http/Requests/Profile/UpdateProfileRequest.php b/app/Http/Requests/Profile/UpdateProfileRequest.php new file mode 100644 index 00000000000..78c11c3ce04 --- /dev/null +++ b/app/Http/Requests/Profile/UpdateProfileRequest.php @@ -0,0 +1,94 @@ + ['required', new UsernameRule(true)], + RequestAttribute::PASSWORD_ATTRIBUTE => ['sometimes', 'confirmed', new PasswordRule(false)], + RequestAttribute::OLD_PASSWORD_ATTRIBUTE => ['required', new PasswordRule(false), new CurrentPasswordRule()], + RequestAttribute::EMAIL_ATTRIBUTE => ['present', 'nullable', 'email:rfc,dns', 'max:100'], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + $this->password = $values[RequestAttribute::PASSWORD_ATTRIBUTE] ?? null; + $this->oldPassword = $values[RequestAttribute::OLD_PASSWORD_ATTRIBUTE]; + + $this->username = trim($values[RequestAttribute::USERNAME_ATTRIBUTE]); + $this->username = $this->username === '' ? null : $this->username; + + $this->email = trim($values[RequestAttribute::EMAIL_ATTRIBUTE] ?? ''); + $this->email = $this->email === '' ? null : $this->email; + } + + /** + * Returns the previous password. + * + * See {@link HasPasswordTrait::password()} for an explanation of the + * semantic difference between the return values `null` and `''`. + * + * @return string|null + */ + public function oldPassword(): ?string + { + return $this->oldPassword; + } + + /** + * Return the new username chosen. + * if Username is null, this means that the user does not want to update it. + * + * @return ?string + */ + public function username(): ?string + { + return $this->username; + } + + public function email(): ?string + { + return $this->email; + } +} \ No newline at end of file diff --git a/app/Http/Requests/Search/GetSearchRequest.php b/app/Http/Requests/Search/GetSearchRequest.php new file mode 100644 index 00000000000..b81d1fc95af --- /dev/null +++ b/app/Http/Requests/Search/GetSearchRequest.php @@ -0,0 +1,68 @@ +album]); + } + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return [ + RequestAttribute::TERM_ATTRIBUTE => ['required', 'string'], + RequestAttribute::ALBUM_ID_ATTRIBUTE => ['sometimes', new RandomIDRule(true)], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + $albumId = $values[RequestAttribute::ALBUM_ID_ATTRIBUTE] ?? null; + $this->album = $this->albumFactory->findNullalbleAbstractAlbumOrFail($albumId); + + // Escape special characters for a LIKE query + $this->terms = explode(' ', str_replace( + ['\\', '%', '_'], + ['\\\\', '\\%', '\\_'], + base64_decode($values[RequestAttribute::TERM_ATTRIBUTE], true) + )); + } +} \ No newline at end of file diff --git a/app/Http/Requests/Search/InitSearchRequest.php b/app/Http/Requests/Search/InitSearchRequest.php new file mode 100644 index 00000000000..9a5da9487ec --- /dev/null +++ b/app/Http/Requests/Search/InitSearchRequest.php @@ -0,0 +1,56 @@ +album]); + } + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return [ + RequestAttribute::ALBUM_ID_ATTRIBUTE => ['present', new RandomIDRule(true)], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + $albumId = $values[RequestAttribute::ALBUM_ID_ATTRIBUTE] ?? null; + $this->album = $this->albumFactory->findNullalbleAbstractAlbumOrFail($albumId); + } +} \ No newline at end of file diff --git a/app/Http/Requests/Session/LoginRequest.php b/app/Http/Requests/Session/LoginRequest.php new file mode 100644 index 00000000000..ddeacde530f --- /dev/null +++ b/app/Http/Requests/Session/LoginRequest.php @@ -0,0 +1,52 @@ + ['required', new UsernameRule()], + RequestAttribute::PASSWORD_ATTRIBUTE => ['required', new PasswordRule(false)], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + $this->username = $values[RequestAttribute::USERNAME_ATTRIBUTE]; + $this->password = $values[RequestAttribute::PASSWORD_ATTRIBUTE]; + } +} diff --git a/app/Http/Requests/Settings/GetAllConfigsRequest.php b/app/Http/Requests/Settings/GetAllConfigsRequest.php new file mode 100644 index 00000000000..73ea79c3630 --- /dev/null +++ b/app/Http/Requests/Settings/GetAllConfigsRequest.php @@ -0,0 +1,29 @@ +css; + } + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return [self::ATTRIBUTE => 'present|nullable|string']; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + $this->css = $values[self::ATTRIBUTE] ?? ''; + } +} diff --git a/app/Http/Requests/Settings/SetConfigsRequest.php b/app/Http/Requests/Settings/SetConfigsRequest.php new file mode 100644 index 00000000000..b4a85c41e53 --- /dev/null +++ b/app/Http/Requests/Settings/SetConfigsRequest.php @@ -0,0 +1,65 @@ + ['required'], + RequestAttribute::CONFIGS_ARRAY_KEY_ATTRIBUTE => ['required', new ConfigKeyRule(), new ConfigKeyRequireSupportRule($this->verify)], + RequestAttribute::CONFIGS_ARRAY_VALUE_ATTRIBUTE => ['present', new ConfigValueRule()], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + $editableConfigs = []; + foreach ($values[RequestAttribute::CONFIGS_ATTRIBUTE] as $config) { + $editableConfigs[] = new EditableConfigResource($config[RequestAttribute::CONFIGS_KEY_ATTRIBUTE], $config[RequestAttribute::CONFIGS_VALUE_ATTRIBUTE]); + } + $this->configs = collect($editableConfigs); + } +} \ No newline at end of file diff --git a/app/Http/Requests/Settings/SetJSSettingRequest.php b/app/Http/Requests/Settings/SetJSSettingRequest.php new file mode 100644 index 00000000000..039fb264dcf --- /dev/null +++ b/app/Http/Requests/Settings/SetJSSettingRequest.php @@ -0,0 +1,49 @@ +js; + } + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return [self::ATTRIBUTE => 'present|nullable|string']; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + $this->js = $values[self::ATTRIBUTE] ?? ''; + } +} \ No newline at end of file diff --git a/app/Http/Requests/Sharing/AddSharingRequest.php b/app/Http/Requests/Sharing/AddSharingRequest.php new file mode 100644 index 00000000000..53872653867 --- /dev/null +++ b/app/Http/Requests/Sharing/AddSharingRequest.php @@ -0,0 +1,78 @@ +albumIds]); + } + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return [ + RequestAttribute::ALBUM_IDS_ATTRIBUTE => 'required|array|min:1', + RequestAttribute::ALBUM_IDS_ATTRIBUTE . '.*' => ['required', new RandomIDRule(false)], + RequestAttribute::USER_IDS_ATTRIBUTE => 'required|array|min:1', + RequestAttribute::USER_IDS_ATTRIBUTE . '.*' => ['required', new IntegerIDRule(false)], + RequestAttribute::GRANTS_DOWNLOAD_ATTRIBUTE => ['required', 'boolean'], + RequestAttribute::GRANTS_FULL_PHOTO_ACCESS_ATTRIBUTE => ['required', 'boolean'], + RequestAttribute::GRANTS_UPLOAD_ATTRIBUTE => ['required', 'boolean'], + RequestAttribute::GRANTS_EDIT_ATTRIBUTE => ['required', 'boolean'], + RequestAttribute::GRANTS_DELETE_ATTRIBUTE => ['required', 'boolean'], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + $this->albumIds = $values[RequestAttribute::ALBUM_IDS_ATTRIBUTE]; + $this->userIds = $values[RequestAttribute::USER_IDS_ATTRIBUTE]; + $this->permResource = new AccessPermissionResource( + grants_edit: static::toBoolean($values[RequestAttribute::GRANTS_EDIT_ATTRIBUTE]), + grants_delete: static::toBoolean($values[RequestAttribute::GRANTS_DELETE_ATTRIBUTE]), + grants_download: static::toBoolean($values[RequestAttribute::GRANTS_DOWNLOAD_ATTRIBUTE]), + grants_full_photo_access: static::toBoolean($values[RequestAttribute::GRANTS_FULL_PHOTO_ACCESS_ATTRIBUTE]), + grants_upload: static::toBoolean($values[RequestAttribute::GRANTS_UPLOAD_ATTRIBUTE]), + ); + } +} \ No newline at end of file diff --git a/app/Http/Requests/Sharing/DeleteSharingRequest.php b/app/Http/Requests/Sharing/DeleteSharingRequest.php new file mode 100644 index 00000000000..4d34c7db05c --- /dev/null +++ b/app/Http/Requests/Sharing/DeleteSharingRequest.php @@ -0,0 +1,63 @@ +perm->album]); + } + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return [ + RequestAttribute::PERMISSION_ID => ['required', new IntegerIDRule(false)], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + /** @var int $id */ + $id = $values[RequestAttribute::PERMISSION_ID]; + $this->perm = AccessPermission::with(['album', 'user'])->findOrFail($id); + } +} \ No newline at end of file diff --git a/app/Http/Requests/Sharing/EditSharingRequest.php b/app/Http/Requests/Sharing/EditSharingRequest.php new file mode 100644 index 00000000000..c7e17f8a703 --- /dev/null +++ b/app/Http/Requests/Sharing/EditSharingRequest.php @@ -0,0 +1,77 @@ +perm->album]); + } + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return [ + RequestAttribute::PERMISSION_ID => ['required', new IntegerIDRule(false)], + RequestAttribute::GRANTS_DOWNLOAD_ATTRIBUTE => ['required', 'boolean'], + RequestAttribute::GRANTS_FULL_PHOTO_ACCESS_ATTRIBUTE => ['required', 'boolean'], + RequestAttribute::GRANTS_UPLOAD_ATTRIBUTE => ['required', 'boolean'], + RequestAttribute::GRANTS_EDIT_ATTRIBUTE => ['required', 'boolean'], + RequestAttribute::GRANTS_DELETE_ATTRIBUTE => ['required', 'boolean'], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + /** @var int $id */ + $id = $values[RequestAttribute::PERMISSION_ID]; + $this->perm = AccessPermission::with(['album', 'user'])->findOrFail($id); + + $this->permResource = new AccessPermissionResource( + grants_edit: static::toBoolean($values[RequestAttribute::GRANTS_EDIT_ATTRIBUTE]), + grants_delete: static::toBoolean($values[RequestAttribute::GRANTS_DELETE_ATTRIBUTE]), + grants_download: static::toBoolean($values[RequestAttribute::GRANTS_DOWNLOAD_ATTRIBUTE]), + grants_full_photo_access: static::toBoolean($values[RequestAttribute::GRANTS_FULL_PHOTO_ACCESS_ATTRIBUTE]), + grants_upload: static::toBoolean($values[RequestAttribute::GRANTS_UPLOAD_ATTRIBUTE]), + ); + } +} \ No newline at end of file diff --git a/app/Http/Requests/Sharing/ListAllSharingRequest.php b/app/Http/Requests/Sharing/ListAllSharingRequest.php new file mode 100644 index 00000000000..44308599519 --- /dev/null +++ b/app/Http/Requests/Sharing/ListAllSharingRequest.php @@ -0,0 +1,30 @@ +album]); + } + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return [ + RequestAttribute::ALBUM_ID_ATTRIBUTE => ['required', new RandomIDRule(false)], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + $this->album = $this->albumFactory->findBaseAlbumOrFail($values[RequestAttribute::ALBUM_ID_ATTRIBUTE]); + } +} \ No newline at end of file diff --git a/app/Http/Requests/Statistics/SpacePerAlbumRequest.php b/app/Http/Requests/Statistics/SpacePerAlbumRequest.php new file mode 100644 index 00000000000..88cc287f2b2 --- /dev/null +++ b/app/Http/Requests/Statistics/SpacePerAlbumRequest.php @@ -0,0 +1,66 @@ +album === null) { + return Gate::check(SettingsPolicy::CAN_SEE_STATISTICS, [Configs::class]); + } + + return Auth::check() && Gate::check(AlbumPolicy::CAN_ACCESS, [AbstractAlbum::class, $this->album]); + } + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return [ + RequestAttribute::ALBUM_ID_ATTRIBUTE => ['sometimes', new RandomIDRule(true)], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + /** @var string|null */ + $albumID = $values[RequestAttribute::ALBUM_ID_ATTRIBUTE] ?? null; + $this->album = $this->albumFactory->findNullalbleAbstractAlbumOrFail($albumID); + + // Filter only to user if user is not admin + if (Auth::check() && Auth::user()?->may_administrate !== true) { + $this->owner_id = intval(Auth::id()); + } + } +} diff --git a/app/Http/Requests/Statistics/SpacePerUserRequest.php b/app/Http/Requests/Statistics/SpacePerUserRequest.php new file mode 100644 index 00000000000..6faee001819 --- /dev/null +++ b/app/Http/Requests/Statistics/SpacePerUserRequest.php @@ -0,0 +1,41 @@ +may_administrate !== true) { + $this->owner_id = intval(Auth::id()); + } + } +} diff --git a/app/Http/Requests/Statistics/SpaceSizeVariantRequest.php b/app/Http/Requests/Statistics/SpaceSizeVariantRequest.php new file mode 100644 index 00000000000..32148057da7 --- /dev/null +++ b/app/Http/Requests/Statistics/SpaceSizeVariantRequest.php @@ -0,0 +1,66 @@ +album === null) { + return Gate::check(UserPolicy::CAN_EDIT, [User::class]); + } + + return Auth::check() && Gate::check(AlbumPolicy::CAN_ACCESS, [AbstractAlbum::class, $this->album]); + } + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return [ + RequestAttribute::ALBUM_ID_ATTRIBUTE => ['sometimes', new RandomIDRule(true)], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + /** @var string|null */ + $albumID = $values[RequestAttribute::ALBUM_ID_ATTRIBUTE] ?? null; + $this->album = $this->albumFactory->findNullalbleAbstractAlbumOrFail($albumID); + + // Filter only to user if user is not admin + if (Auth::check() && Auth::user()?->may_administrate !== true) { + $this->owner_id = intval(Auth::id()); + } + } +} diff --git a/app/Http/Requests/Traits/Authorize/AuthorizeCanEditAlbumAlbumsTrait.php b/app/Http/Requests/Traits/Authorize/AuthorizeCanEditAlbumAlbumsTrait.php new file mode 100644 index 00000000000..0960c020f54 --- /dev/null +++ b/app/Http/Requests/Traits/Authorize/AuthorizeCanEditAlbumAlbumsTrait.php @@ -0,0 +1,35 @@ +album])) { + return false; + } + + /** @var AbstractAlbum $album */ + foreach ($this->albums as $album) { + if (!Gate::check(AlbumPolicy::CAN_EDIT, $album)) { + return false; + } + } + + return true; + } +} \ No newline at end of file diff --git a/app/Http/Requests/Traits/Authorize/AuthorizeCanEditAlbumTrait.php b/app/Http/Requests/Traits/Authorize/AuthorizeCanEditAlbumTrait.php new file mode 100644 index 00000000000..120b01139b0 --- /dev/null +++ b/app/Http/Requests/Traits/Authorize/AuthorizeCanEditAlbumTrait.php @@ -0,0 +1,28 @@ +album]); + } +} \ No newline at end of file diff --git a/app/Http/Requests/Traits/Authorize/AuthorizeCanEditPhotoTrait.php b/app/Http/Requests/Traits/Authorize/AuthorizeCanEditPhotoTrait.php new file mode 100644 index 00000000000..b07c033af1b --- /dev/null +++ b/app/Http/Requests/Traits/Authorize/AuthorizeCanEditPhotoTrait.php @@ -0,0 +1,28 @@ +photo]); + } +} \ No newline at end of file diff --git a/app/Http/Requests/Traits/Authorize/AuthorizeCanEditPhotosAlbumTrait.php b/app/Http/Requests/Traits/Authorize/AuthorizeCanEditPhotosAlbumTrait.php new file mode 100644 index 00000000000..541baceace3 --- /dev/null +++ b/app/Http/Requests/Traits/Authorize/AuthorizeCanEditPhotosAlbumTrait.php @@ -0,0 +1,40 @@ +album])) { + return false; + } + + /** @var Photo $photo */ + foreach ($this->photos as $photo) { + if (!Gate::check(PhotoPolicy::CAN_EDIT, $photo)) { + return false; + } + } + + return true; + } +} \ No newline at end of file diff --git a/app/Http/Requests/Traits/Authorize/AuthorizeCanEditPhotosTrait.php b/app/Http/Requests/Traits/Authorize/AuthorizeCanEditPhotosTrait.php new file mode 100644 index 00000000000..4cbfafc7e32 --- /dev/null +++ b/app/Http/Requests/Traits/Authorize/AuthorizeCanEditPhotosTrait.php @@ -0,0 +1,34 @@ +photos as $photo) { + if (!Gate::check(PhotoPolicy::CAN_EDIT, $photo)) { + return false; + } + } + + return true; + } +} \ No newline at end of file diff --git a/app/Http/Requests/Traits/HasAbstractAlbumTrait.php b/app/Http/Requests/Traits/HasAbstractAlbumTrait.php new file mode 100644 index 00000000000..dc1ba8518f5 --- /dev/null +++ b/app/Http/Requests/Traits/HasAbstractAlbumTrait.php @@ -0,0 +1,24 @@ +album; + } +} diff --git a/app/Http/Requests/Traits/HasAccessPermissionResourceTrait.php b/app/Http/Requests/Traits/HasAccessPermissionResourceTrait.php new file mode 100644 index 00000000000..b676fe52df6 --- /dev/null +++ b/app/Http/Requests/Traits/HasAccessPermissionResourceTrait.php @@ -0,0 +1,24 @@ +permResource; + } +} diff --git a/app/Http/Requests/Traits/HasAccessPermissionTrait.php b/app/Http/Requests/Traits/HasAccessPermissionTrait.php new file mode 100644 index 00000000000..4d0088f13ac --- /dev/null +++ b/app/Http/Requests/Traits/HasAccessPermissionTrait.php @@ -0,0 +1,24 @@ +perm; + } +} diff --git a/app/Http/Requests/Traits/HasAlbumIdTrait.php b/app/Http/Requests/Traits/HasAlbumIdTrait.php new file mode 100644 index 00000000000..d8ce427722c --- /dev/null +++ b/app/Http/Requests/Traits/HasAlbumIdTrait.php @@ -0,0 +1,22 @@ +albumId; + } +} diff --git a/app/Http/Requests/Traits/HasAlbumIdsTrait.php b/app/Http/Requests/Traits/HasAlbumIdsTrait.php new file mode 100644 index 00000000000..580025cec88 --- /dev/null +++ b/app/Http/Requests/Traits/HasAlbumIdsTrait.php @@ -0,0 +1,25 @@ +albumIds; + } +} diff --git a/app/Http/Requests/Traits/HasAlbumSortingCriterionTrait.php b/app/Http/Requests/Traits/HasAlbumSortingCriterionTrait.php new file mode 100644 index 00000000000..6cb9dd9f218 --- /dev/null +++ b/app/Http/Requests/Traits/HasAlbumSortingCriterionTrait.php @@ -0,0 +1,24 @@ +albumSortingCriterion; + } +} diff --git a/app/Http/Requests/Traits/HasAlbumTrait.php b/app/Http/Requests/Traits/HasAlbumTrait.php new file mode 100644 index 00000000000..621422625b8 --- /dev/null +++ b/app/Http/Requests/Traits/HasAlbumTrait.php @@ -0,0 +1,24 @@ +album; + } +} diff --git a/app/Http/Requests/Traits/HasAlbumsTrait.php b/app/Http/Requests/Traits/HasAlbumsTrait.php new file mode 100644 index 00000000000..10eabf38de0 --- /dev/null +++ b/app/Http/Requests/Traits/HasAlbumsTrait.php @@ -0,0 +1,30 @@ + + */ + protected Collection $albums; + + /** + * @return Collection + */ + public function albums(): Collection + { + return $this->albums; + } +} diff --git a/app/Http/Requests/Traits/HasAspectRatioTrait.php b/app/Http/Requests/Traits/HasAspectRatioTrait.php new file mode 100644 index 00000000000..deeae144eff --- /dev/null +++ b/app/Http/Requests/Traits/HasAspectRatioTrait.php @@ -0,0 +1,24 @@ +aspectRatio; + } +} diff --git a/app/Http/Requests/Traits/HasBaseAlbumTrait.php b/app/Http/Requests/Traits/HasBaseAlbumTrait.php new file mode 100644 index 00000000000..54045496533 --- /dev/null +++ b/app/Http/Requests/Traits/HasBaseAlbumTrait.php @@ -0,0 +1,24 @@ +album; + } +} diff --git a/app/Http/Requests/Traits/HasCompactBooleanTrait.php b/app/Http/Requests/Traits/HasCompactBooleanTrait.php new file mode 100644 index 00000000000..b3acfb38c2a --- /dev/null +++ b/app/Http/Requests/Traits/HasCompactBooleanTrait.php @@ -0,0 +1,19 @@ +is_compact; + } +} diff --git a/app/Http/Requests/Traits/HasConfigsTrait.php b/app/Http/Requests/Traits/HasConfigsTrait.php new file mode 100644 index 00000000000..5fe8ab1b409 --- /dev/null +++ b/app/Http/Requests/Traits/HasConfigsTrait.php @@ -0,0 +1,26 @@ + */ + protected Collection $configs; + + /** + * @return Collection + */ + public function configs(): Collection + { + return $this->configs; + } +} diff --git a/app/Http/Requests/Traits/HasCopyrightTrait.php b/app/Http/Requests/Traits/HasCopyrightTrait.php new file mode 100644 index 00000000000..e545ffbe345 --- /dev/null +++ b/app/Http/Requests/Traits/HasCopyrightTrait.php @@ -0,0 +1,22 @@ +copyright; + } +} diff --git a/app/Http/Requests/Traits/HasDescriptionTrait.php b/app/Http/Requests/Traits/HasDescriptionTrait.php new file mode 100644 index 00000000000..39ec05d471b --- /dev/null +++ b/app/Http/Requests/Traits/HasDescriptionTrait.php @@ -0,0 +1,22 @@ +description; + } +} diff --git a/app/Http/Requests/Traits/HasLicenseTrait.php b/app/Http/Requests/Traits/HasLicenseTrait.php new file mode 100644 index 00000000000..9c419aefe16 --- /dev/null +++ b/app/Http/Requests/Traits/HasLicenseTrait.php @@ -0,0 +1,24 @@ +license; + } +} \ No newline at end of file diff --git a/app/Http/Requests/Traits/HasNoteTrait.php b/app/Http/Requests/Traits/HasNoteTrait.php new file mode 100644 index 00000000000..3ca9fb51e8d --- /dev/null +++ b/app/Http/Requests/Traits/HasNoteTrait.php @@ -0,0 +1,22 @@ +note; + } +} diff --git a/app/Http/Requests/Traits/HasOwnerIdTrait.php b/app/Http/Requests/Traits/HasOwnerIdTrait.php new file mode 100644 index 00000000000..7fa4b5036f7 --- /dev/null +++ b/app/Http/Requests/Traits/HasOwnerIdTrait.php @@ -0,0 +1,22 @@ +owner_id; + } +} diff --git a/app/Http/Requests/Traits/HasParentAlbumTrait.php b/app/Http/Requests/Traits/HasParentAlbumTrait.php new file mode 100644 index 00000000000..916272b7d06 --- /dev/null +++ b/app/Http/Requests/Traits/HasParentAlbumTrait.php @@ -0,0 +1,24 @@ +parent_album; + } +} diff --git a/app/Http/Requests/Traits/HasPasswordTrait.php b/app/Http/Requests/Traits/HasPasswordTrait.php new file mode 100644 index 00000000000..b286c186184 --- /dev/null +++ b/app/Http/Requests/Traits/HasPasswordTrait.php @@ -0,0 +1,37 @@ +password; + } +} diff --git a/app/Http/Requests/Traits/HasPhotoIdsTrait.php b/app/Http/Requests/Traits/HasPhotoIdsTrait.php new file mode 100644 index 00000000000..3290ae36486 --- /dev/null +++ b/app/Http/Requests/Traits/HasPhotoIdsTrait.php @@ -0,0 +1,25 @@ +photoIds; + } +} diff --git a/app/Http/Requests/Traits/HasPhotoLayoutTrait.php b/app/Http/Requests/Traits/HasPhotoLayoutTrait.php new file mode 100644 index 00000000000..f61d1f77279 --- /dev/null +++ b/app/Http/Requests/Traits/HasPhotoLayoutTrait.php @@ -0,0 +1,24 @@ +photoLayout; + } +} diff --git a/app/Http/Requests/Traits/HasPhotoSortingCriterionTrait.php b/app/Http/Requests/Traits/HasPhotoSortingCriterionTrait.php new file mode 100644 index 00000000000..1a356be870d --- /dev/null +++ b/app/Http/Requests/Traits/HasPhotoSortingCriterionTrait.php @@ -0,0 +1,24 @@ +photoSortingCriterion; + } +} diff --git a/app/Http/Requests/Traits/HasPhotoTrait.php b/app/Http/Requests/Traits/HasPhotoTrait.php new file mode 100644 index 00000000000..f451c8bdb2b --- /dev/null +++ b/app/Http/Requests/Traits/HasPhotoTrait.php @@ -0,0 +1,24 @@ +photo; + } +} diff --git a/app/Http/Requests/Traits/HasPhotosTrait.php b/app/Http/Requests/Traits/HasPhotosTrait.php new file mode 100644 index 00000000000..f3137077502 --- /dev/null +++ b/app/Http/Requests/Traits/HasPhotosTrait.php @@ -0,0 +1,28 @@ + + */ + protected Collection $photos; + + /** + * @return Collection + */ + public function photos(): Collection + { + return $this->photos; + } +} diff --git a/app/Http/Requests/Traits/HasQuotaKBTrait.php b/app/Http/Requests/Traits/HasQuotaKBTrait.php new file mode 100644 index 00000000000..d857027d455 --- /dev/null +++ b/app/Http/Requests/Traits/HasQuotaKBTrait.php @@ -0,0 +1,25 @@ +quota_kb; + } +} diff --git a/app/Http/Requests/Traits/HasSeStatusBooleanTrait.php b/app/Http/Requests/Traits/HasSeStatusBooleanTrait.php new file mode 100644 index 00000000000..be4dada4445 --- /dev/null +++ b/app/Http/Requests/Traits/HasSeStatusBooleanTrait.php @@ -0,0 +1,28 @@ +is_se === null) { + $this->is_se = $this->verify->validate() && $this->verify->is_supporter(); + } + + return $this->is_se; + } +} diff --git a/app/Http/Requests/Traits/HasSizeVariantTrait.php b/app/Http/Requests/Traits/HasSizeVariantTrait.php new file mode 100644 index 00000000000..53150402529 --- /dev/null +++ b/app/Http/Requests/Traits/HasSizeVariantTrait.php @@ -0,0 +1,24 @@ +sizeVariant; + } +} diff --git a/app/Http/Requests/Traits/HasTagAlbumTrait.php b/app/Http/Requests/Traits/HasTagAlbumTrait.php new file mode 100644 index 00000000000..fa87bc7c0c2 --- /dev/null +++ b/app/Http/Requests/Traits/HasTagAlbumTrait.php @@ -0,0 +1,24 @@ +album; + } +} diff --git a/app/Http/Requests/Traits/HasTagsTrait.php b/app/Http/Requests/Traits/HasTagsTrait.php new file mode 100644 index 00000000000..4bbc7656679 --- /dev/null +++ b/app/Http/Requests/Traits/HasTagsTrait.php @@ -0,0 +1,25 @@ +tags; + } +} diff --git a/app/Http/Requests/Traits/HasTermsTrait.php b/app/Http/Requests/Traits/HasTermsTrait.php new file mode 100644 index 00000000000..12badf2ad0e --- /dev/null +++ b/app/Http/Requests/Traits/HasTermsTrait.php @@ -0,0 +1,25 @@ +terms; + } +} diff --git a/app/Http/Requests/Traits/HasTimelineAlbumTrait.php b/app/Http/Requests/Traits/HasTimelineAlbumTrait.php new file mode 100644 index 00000000000..013c9eaf1ed --- /dev/null +++ b/app/Http/Requests/Traits/HasTimelineAlbumTrait.php @@ -0,0 +1,24 @@ +album_timeline; + } +} diff --git a/app/Http/Requests/Traits/HasTimelinePhotoTrait.php b/app/Http/Requests/Traits/HasTimelinePhotoTrait.php new file mode 100644 index 00000000000..9e3b2b6548d --- /dev/null +++ b/app/Http/Requests/Traits/HasTimelinePhotoTrait.php @@ -0,0 +1,24 @@ +photo_timeline; + } +} diff --git a/app/Http/Requests/Traits/HasTitleTrait.php b/app/Http/Requests/Traits/HasTitleTrait.php new file mode 100644 index 00000000000..fb9b299b156 --- /dev/null +++ b/app/Http/Requests/Traits/HasTitleTrait.php @@ -0,0 +1,22 @@ +title; + } +} diff --git a/app/Http/Requests/Traits/HasUploadDateTrait.php b/app/Http/Requests/Traits/HasUploadDateTrait.php new file mode 100644 index 00000000000..9b6e2807855 --- /dev/null +++ b/app/Http/Requests/Traits/HasUploadDateTrait.php @@ -0,0 +1,24 @@ +upload_date; + } +} \ No newline at end of file diff --git a/app/Http/Requests/Traits/HasUserIdTrait.php b/app/Http/Requests/Traits/HasUserIdTrait.php new file mode 100644 index 00000000000..2fb8532603d --- /dev/null +++ b/app/Http/Requests/Traits/HasUserIdTrait.php @@ -0,0 +1,25 @@ +userId; + } +} diff --git a/app/Http/Requests/Traits/HasUserIdsTrait.php b/app/Http/Requests/Traits/HasUserIdsTrait.php new file mode 100644 index 00000000000..33205939879 --- /dev/null +++ b/app/Http/Requests/Traits/HasUserIdsTrait.php @@ -0,0 +1,25 @@ + + */ + protected array $userIds = []; + + /** + * @return array + */ + public function userIds(): array + { + return $this->userIds; + } +} diff --git a/app/Http/Requests/Traits/HasUserTrait.php b/app/Http/Requests/Traits/HasUserTrait.php new file mode 100644 index 00000000000..0e62fe7b021 --- /dev/null +++ b/app/Http/Requests/Traits/HasUserTrait.php @@ -0,0 +1,35 @@ +user2; + } +} \ No newline at end of file diff --git a/app/Http/Requests/Traits/HasUsernameTrait.php b/app/Http/Requests/Traits/HasUsernameTrait.php new file mode 100644 index 00000000000..a9a840e9b99 --- /dev/null +++ b/app/Http/Requests/Traits/HasUsernameTrait.php @@ -0,0 +1,22 @@ +username; + } +} diff --git a/app/Http/Requests/UserManagement/AddUserRequest.php b/app/Http/Requests/UserManagement/AddUserRequest.php new file mode 100644 index 00000000000..f20574d33c2 --- /dev/null +++ b/app/Http/Requests/UserManagement/AddUserRequest.php @@ -0,0 +1,90 @@ + ['required', new UsernameRule()], + RequestAttribute::PASSWORD_ATTRIBUTE => ['required', new PasswordRule(false)], + RequestAttribute::MAY_UPLOAD_ATTRIBUTE => 'present|boolean', + RequestAttribute::MAY_EDIT_OWN_SETTINGS_ATTRIBUTE => 'present|boolean', + RequestAttribute::HAS_QUOTA_ATTRIBUTE => ['sometimes', 'boolean', new BooleanRequireSupportRule(false, $this->verify)], + RequestAttribute::QUOTA_ATTRIBUTE => ['sometimes', 'int', new IntegerRequireSupportRule(0, $this->verify)], + RequestAttribute::NOTE_ATTRIBUTE => ['sometimes', 'string', new StringRequireSupportRule('', $this->verify)], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + $this->username = $values[RequestAttribute::USERNAME_ATTRIBUTE]; + $this->password = $values[RequestAttribute::PASSWORD_ATTRIBUTE]; + $this->mayUpload = static::toBoolean($values[RequestAttribute::MAY_UPLOAD_ATTRIBUTE]); + $this->mayEditOwnSettings = static::toBoolean($values[RequestAttribute::MAY_EDIT_OWN_SETTINGS_ATTRIBUTE]); + $has_quota = static::toBoolean($values[RequestAttribute::HAS_QUOTA_ATTRIBUTE] ?? false); + $this->quota_kb = $has_quota ? intval($values[RequestAttribute::QUOTA_ATTRIBUTE]) : null; + $this->note = $values[RequestAttribute::NOTE_ATTRIBUTE] ?? ''; + } + + public function mayUpload(): bool + { + return $this->mayUpload; + } + + public function mayEditOwnSettings(): bool + { + return $this->mayEditOwnSettings; + } +} diff --git a/app/Http/Requests/UserManagement/DeleteUserRequest.php b/app/Http/Requests/UserManagement/DeleteUserRequest.php new file mode 100644 index 00000000000..b8acefc439b --- /dev/null +++ b/app/Http/Requests/UserManagement/DeleteUserRequest.php @@ -0,0 +1,51 @@ + ['required', new IntegerIDRule(false)], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + /** @var int $userID */ + $userID = $values[RequestAttribute::ID_ATTRIBUTE]; + $this->user2 = User::query()->findOrFail($userID); + } +} diff --git a/app/Http/Requests/UserManagement/ManagmentListUsersRequest.php b/app/Http/Requests/UserManagement/ManagmentListUsersRequest.php new file mode 100644 index 00000000000..217ee4e006e --- /dev/null +++ b/app/Http/Requests/UserManagement/ManagmentListUsersRequest.php @@ -0,0 +1,29 @@ + ['required', new IntegerIDRule(false)], + RequestAttribute::USERNAME_ATTRIBUTE => ['required', new UsernameRule(), 'min:1'], + RequestAttribute::PASSWORD_ATTRIBUTE => ['sometimes', new PasswordRule(false)], + RequestAttribute::MAY_UPLOAD_ATTRIBUTE => 'present|boolean', + RequestAttribute::MAY_EDIT_OWN_SETTINGS_ATTRIBUTE => 'present|boolean', + RequestAttribute::HAS_QUOTA_ATTRIBUTE => ['sometimes', 'boolean', new BooleanRequireSupportRule(false, $this->verify)], + RequestAttribute::QUOTA_ATTRIBUTE => ['sometimes', 'int', new IntegerRequireSupportRule(0, $this->verify)], + RequestAttribute::NOTE_ATTRIBUTE => ['sometimes', 'nullable', 'string', new StringRequireSupportRule('', $this->verify)], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + $this->username = $values[RequestAttribute::USERNAME_ATTRIBUTE]; + if (array_key_exists(RequestAttribute::PASSWORD_ATTRIBUTE, $values)) { + // See {@link HasPasswordTrait::password()} for an explanation + // of the semantic difference between `null` and `''`. + $this->password = $values[RequestAttribute::PASSWORD_ATTRIBUTE] ?? ''; + } else { + $this->password = null; + } + $this->mayUpload = static::toBoolean($values[RequestAttribute::MAY_UPLOAD_ATTRIBUTE]); + $this->mayEditOwnSettings = static::toBoolean($values[RequestAttribute::MAY_EDIT_OWN_SETTINGS_ATTRIBUTE]); + /** @var int $userID */ + $userID = $values[RequestAttribute::ID_ATTRIBUTE]; + $this->user2 = User::query()->findOrFail($userID); + $has_quota = static::toBoolean($values[RequestAttribute::HAS_QUOTA_ATTRIBUTE] ?? false); + $this->quota_kb = $has_quota ? intval($values[RequestAttribute::QUOTA_ATTRIBUTE]) : null; + $this->note = $values[RequestAttribute::NOTE_ATTRIBUTE] ?? ''; + } + + public function mayUpload(): bool + { + return $this->mayUpload; + } + + public function mayEditOwnSettings(): bool + { + return $this->mayEditOwnSettings; + } +} diff --git a/app/Http/Requests/UserRequests/UserPostIdRequest.php b/app/Http/Requests/UserRequests/UserPostIdRequest.php deleted file mode 100644 index 85c2a1d5f19..00000000000 --- a/app/Http/Requests/UserRequests/UserPostIdRequest.php +++ /dev/null @@ -1,30 +0,0 @@ - 'required|numeric|min:1', - ]; - } -} diff --git a/app/Http/Requests/UserRequests/UserPostRequest.php b/app/Http/Requests/UserRequests/UserPostRequest.php deleted file mode 100644 index 2e502552ab1..00000000000 --- a/app/Http/Requests/UserRequests/UserPostRequest.php +++ /dev/null @@ -1,33 +0,0 @@ - 'required|numeric|min:1', - 'username' => 'required|string|max:100', - 'upload' => 'required', - 'lock' => 'required', - ]; - } -} diff --git a/app/Http/Requests/UserRequests/UsernamePasswordRequest.php b/app/Http/Requests/UserRequests/UsernamePasswordRequest.php deleted file mode 100644 index 866877f153e..00000000000 --- a/app/Http/Requests/UserRequests/UsernamePasswordRequest.php +++ /dev/null @@ -1,31 +0,0 @@ - 'required|string', - 'password' => 'required|string', - ]; - } -} diff --git a/app/Http/Requests/Users/ListUsersRequest.php b/app/Http/Requests/Users/ListUsersRequest.php new file mode 100644 index 00000000000..4ba82902a55 --- /dev/null +++ b/app/Http/Requests/Users/ListUsersRequest.php @@ -0,0 +1,25 @@ + 'required|string', + ]; + } + + protected function processValidatedValues(array $values, array $files): void + { + $this->id = $values[RequestAttribute::ID_ATTRIBUTE]; + } + + public function getId(): string + { + return $this->id; + } +} diff --git a/app/Http/Requests/WebAuthn/EditCredentialRequest.php b/app/Http/Requests/WebAuthn/EditCredentialRequest.php new file mode 100644 index 00000000000..a31055ebbe2 --- /dev/null +++ b/app/Http/Requests/WebAuthn/EditCredentialRequest.php @@ -0,0 +1,56 @@ + 'required|string', + RequestAttribute::ALIAS_ATTRIBUTE => 'required|string|min:5|max:255', + ]; + } + + protected function processValidatedValues(array $values, array $files): void + { + /** @var string $id */ + $id = $values[RequestAttribute::ID_ATTRIBUTE]; + $this->credential = WebAuthnCredential::query()->findOrFail($id); + $this->alias = $values[RequestAttribute::ALIAS_ATTRIBUTE]; + } + + public function getCredential(): WebAuthnCredential + { + return $this->credential; + } + + public function getAlias(): string + { + return $this->alias; + } +} diff --git a/app/Http/Requests/WebAuthn/ListCredentialsRequest.php b/app/Http/Requests/WebAuthn/ListCredentialsRequest.php new file mode 100644 index 00000000000..9e9b32237da --- /dev/null +++ b/app/Http/Requests/WebAuthn/ListCredentialsRequest.php @@ -0,0 +1,23 @@ + */ + public array $configs; + + /** + * @param Collection $configs + * + * @return void + */ + public function __construct(Collection $configs) + { + $configs + // Group by category + ->chunkWhile(fn (Configs $value, int $key, Collection $chunk) => $value->cat === $chunk->last()->cat) + // For each category, map the configs to ConfigResource + ->each(function (Collection $chunk) { + $configs_data = ConfigResource::collect($chunk->all()); + $this->configs[$chunk->first()->cat] = $configs_data; + }); + } +} \ No newline at end of file diff --git a/app/Http/Resources/Collections/PositionDataResource.php b/app/Http/Resources/Collections/PositionDataResource.php new file mode 100644 index 00000000000..8c4d2d5eeb8 --- /dev/null +++ b/app/Http/Resources/Collections/PositionDataResource.php @@ -0,0 +1,47 @@ + */ + #[LiteralTypeScriptType('App.Http.Resources.Models.PhotoResource[]')] + public ?Collection $photos; + + /** + * @param string|null $id the ID of the album; `null` for root album + * @param string|null $title the title of the album; `null` if untitled + * @param Collection $photos the collection of photos with position data to be shown on map + * @param string|null $track_url the URL of the album's track + */ + public function __construct( + ?string $id, + ?string $title, + Collection $photos, + ?string $track_url, + ) { + $this->id = $id; + $this->title = $title; + $this->track_url = $track_url; + $this->photos = PhotoResource::collect($photos); + } +} diff --git a/app/Http/Resources/Collections/RootAlbumResource.php b/app/Http/Resources/Collections/RootAlbumResource.php new file mode 100644 index 00000000000..919cc54eaf4 --- /dev/null +++ b/app/Http/Resources/Collections/RootAlbumResource.php @@ -0,0 +1,86 @@ + */ + public Collection $smart_albums; + /** @var Collection */ + public Collection $tag_albums; + /** @var Collection */ + public Collection $albums; + /** @var Collection */ + public Collection $shared_albums; + public RootConfig $config; + public RootAlbumRightsResource $rights; + + /** + * @param Collection $smart_albums + * @param Collection $tag_albums + * @param Collection $albums + * @param Collection $shared_albums + * @param RootConfig $config + * + * @return void + */ + public function __construct( + Collection $smart_albums, + Collection $tag_albums, + Collection $albums, + Collection $shared_albums, + RootConfig $config, + RootAlbumRightsResource $rights, + ) { + $this->smart_albums = $smart_albums; + $this->tag_albums = $tag_albums; + $this->albums = $albums; + $sorting = Configs::getValueAsEnum('sorting_albums_col', ColumnSortingType::class); + $album_granularity = Configs::getValueAsEnum('timeline_albums_granularity', TimelineAlbumGranularity::class); + $this->albums = TimelineData::setTimeLineDataForAlbums($this->albums, $sorting, $album_granularity); + $this->shared_albums = $shared_albums; + $this->config = $config; + $this->rights = $rights; + } + + public static function fromDTO(TopAlbumDTO $dto, RootConfig $config): self + { + return new self( + smart_albums: ThumbAlbumResource::collect($dto->smart_albums->values()), + tag_albums: ThumbAlbumResource::collect($dto->tag_albums), + albums: ThumbAlbumResource::collect($dto->albums), + shared_albums: $dto->shared_albums !== null ? ThumbAlbumResource::collect($dto->shared_albums) : collect([]), + config: $config, + rights: new RootAlbumRightsResource() + ); + } +} \ No newline at end of file diff --git a/app/Http/Resources/Diagnostics/AlbumTree.php b/app/Http/Resources/Diagnostics/AlbumTree.php new file mode 100644 index 00000000000..d597e10c9cc --- /dev/null +++ b/app/Http/Resources/Diagnostics/AlbumTree.php @@ -0,0 +1,37 @@ +id, + $album->title, + $album->parent_id, + $album->_lft, + $album->_rgt + ); + } +} diff --git a/app/Http/Resources/Diagnostics/CleaningState.php b/app/Http/Resources/Diagnostics/CleaningState.php new file mode 100644 index 00000000000..8ebeaed93f5 --- /dev/null +++ b/app/Http/Resources/Diagnostics/CleaningState.php @@ -0,0 +1,29 @@ +path = str_replace(storage_path() . '/', '', $path); + $this->base = str_replace(base_path() . '/', '', $path); + $this->is_not_empty = $is_not_empty; + } +} diff --git a/app/Http/Resources/Diagnostics/ErrorLine.php b/app/Http/Resources/Diagnostics/ErrorLine.php new file mode 100644 index 00000000000..ad33d963eb8 --- /dev/null +++ b/app/Http/Resources/Diagnostics/ErrorLine.php @@ -0,0 +1,44 @@ +type, + $line->message, + str_replace('App\\Actions\\Diagnostics\\Pipes\\Checks\\', '', $line->from), + $line->details, + ); + } +} diff --git a/app/Http/Resources/Diagnostics/Permissions.php b/app/Http/Resources/Diagnostics/Permissions.php new file mode 100644 index 00000000000..6c1c8fa5cbb --- /dev/null +++ b/app/Http/Resources/Diagnostics/Permissions.php @@ -0,0 +1,22 @@ +id = $album->id; + $this->title = $album->title; + $this->description = $album->description; + $this->copyright = $album->copyright; + $this->photo_sorting = $album->photo_sorting; + $this->is_model_album = false; + $this->license = null; + $this->album_sorting = null; + $this->header_id = null; + $this->cover_id = null; + $this->photo_layout = $album->photo_layout; + $this->album_timeline = null; + $this->photo_timeline = $album->photo_timeline; + + if ($album instanceof Album) { + $this->is_model_album = true; + $this->license = $album->license; + $this->album_sorting = $album->album_sorting; + $this->header_id = $album->header_id; + $this->cover_id = $album->cover_id; + $this->aspect_ratio = $album->album_thumb_aspect_ratio; + $this->album_timeline = $album->album_timeline; + } + + if ($album instanceof TagAlbum) { + $this->tags = $album->show_tags; + } + } + + public static function fromModel(Album|TagAlbum $album): EditableBaseAlbumResource + { + return new self($album); + } +} diff --git a/app/Http/Resources/Editable/EditableConfigResource.php b/app/Http/Resources/Editable/EditableConfigResource.php new file mode 100644 index 00000000000..efa2802ce10 --- /dev/null +++ b/app/Http/Resources/Editable/EditableConfigResource.php @@ -0,0 +1,25 @@ +key = $key; + $this->value = $value; + } +} diff --git a/app/Http/Resources/Editable/UploadMetaResource.php b/app/Http/Resources/Editable/UploadMetaResource.php new file mode 100644 index 00000000000..b636654df8e --- /dev/null +++ b/app/Http/Resources/Editable/UploadMetaResource.php @@ -0,0 +1,27 @@ +public_permissions(); + + $this->is_accessible = $is_accessible; + $this->is_base_album = $album instanceof BaseAlbum; + $this->is_model_album = $album instanceof Album; + $this->is_password_protected = !$is_accessible && $public_perm?->password !== null; + $this->is_nsfw_warning_visible = + $album instanceof BaseAlbum && + $album->is_nsfw && + (Auth::check() ? Configs::getValueAsBool('nsfw_warning_admin') : Configs::getValueAsBool('nsfw_warning')); + + $this->setIsMapAccessible(); + $this->setIsSearchAccessible($this->is_base_album); + $this->is_mod_frame_enabled = Configs::getValueAsBool('mod_frame_enabled') && $album->photos->count() > 0; + if ($album instanceof Album && $album->album_thumb_aspect_ratio !== null) { + $this->album_thumb_css_aspect_ratio = $album->album_thumb_aspect_ratio->css(); + } else { + $this->album_thumb_css_aspect_ratio = Configs::getValueAsEnum('default_album_thumb_aspect_ratio', AspectRatioType::class)->css(); + } + + $this->photo_layout = (($album instanceof BaseAlbum) ? $album->photo_layout : null) ?? Configs::getValueAsEnum('layout', PhotoLayoutType::class); + + // Set default values. + $this->is_photo_timeline_enabled = Configs::getValueAsBool('timeline_photos_enabled'); + $this->is_album_timeline_enabled = Configs::getValueAsBool('timeline_albums_enabled'); + + if ($album instanceof Album) { + $this->is_album_timeline_enabled = $album->album_timeline !== null || $this->is_album_timeline_enabled; + $this->is_album_timeline_enabled = $album->album_timeline !== TimelineAlbumGranularity::DISABLED && $this->is_album_timeline_enabled; + } + + if ($album instanceof BaseAlbum) { + $this->is_photo_timeline_enabled = $album->photo_timeline !== null || $this->is_photo_timeline_enabled; + $this->is_photo_timeline_enabled = $album->photo_timeline !== TimelinePhotoGranularity::DISABLED && $this->is_photo_timeline_enabled; + } + + // Masking to require login for timeline or allow it to be public. + $this->is_photo_timeline_enabled = $this->is_photo_timeline_enabled && (Configs::getValueAsBool('timeline_photos_public') || Auth::check()); + $this->is_album_timeline_enabled = $this->is_album_timeline_enabled && (Configs::getValueAsBool('timeline_albums_public') || Auth::check()); + } + + public function setIsMapAccessible(): void + { + $map_display = Configs::getValueAsBool('map_display'); + $public_display = Auth::check() || Configs::getValueAsBool('map_display_public'); + $this->is_map_accessible = $map_display && $public_display; + } + + public function setIsSearchAccessible(bool $is_base_album): void + { + $this->is_search_accessible = (Auth::check() || Configs::getValueAsBool('search_public')) && $is_base_album; + } +} \ No newline at end of file diff --git a/app/Http/Resources/GalleryConfigs/FooterConfig.php b/app/Http/Resources/GalleryConfigs/FooterConfig.php new file mode 100644 index 00000000000..2e0a37b262d --- /dev/null +++ b/app/Http/Resources/GalleryConfigs/FooterConfig.php @@ -0,0 +1,49 @@ +footer_additional_text = Configs::getValueAsString('footer_additional_text'); + $this->footer_show_copyright = Configs::getValueAsBool('footer_show_copyright'); + $this->footer_show_social_media = Configs::getValueAsBool('footer_show_social_media'); + $site_copyright_begin = Configs::getValueAsString('site_copyright_begin'); + $site_copyright_end = Configs::getValueAsString('site_copyright_end'); + $site_owner = Configs::getValueAsString('site_owner'); + $this->sm_facebook_url = Configs::getValueAsString('sm_facebook_url'); + $this->sm_flickr_url = Configs::getValueAsString('sm_flickr_url'); + $this->sm_instagram_url = Configs::getValueAsString('sm_instagram_url'); + $this->sm_twitter_url = Configs::getValueAsString('sm_twitter_url'); + $this->sm_youtube_url = Configs::getValueAsString('sm_youtube_url'); + + $copy_right_year = $site_copyright_begin; + if ($site_copyright_begin !== $site_copyright_end) { + $copy_right_year = $copy_right_year . '-' . $site_copyright_end; + } + + $this->copyright = $copy_right_year !== '' ? sprintf(__('landing.copyright'), $site_owner, $copy_right_year) : ''; + } +} diff --git a/app/Http/Resources/GalleryConfigs/InitConfig.php b/app/Http/Resources/GalleryConfigs/InitConfig.php new file mode 100644 index 00000000000..a6db1a4c1e1 --- /dev/null +++ b/app/Http/Resources/GalleryConfigs/InitConfig.php @@ -0,0 +1,155 @@ +is_debug_enabled = config('app.debug'); + + // NSFW settings + $this->are_nsfw_visible = Configs::getValueAsBool('nsfw_visible'); + $this->is_nsfw_background_blurred = Configs::getValueAsBool('nsfw_blur'); // blur the thumbnails + $this->nsfw_banner_override = Configs::getValueAsString('nsfw_banner_override'); // override the banner text. + $this->is_nsfw_banner_backdrop_blurred = Configs::getValueAsBool('nsfw_banner_blur_backdrop'); // blur the backdrop of the warning banner. + + // keybinding help popup + $this->show_keybinding_help_popup = Configs::getValueAsBool('show_keybinding_help_popup'); + + // Image overlay settings + $this->image_overlay_type = Configs::getValueAsEnum('image_overlay_type', ImageOverlayType::class); + $this->can_rotate = Configs::getValueAsBool('editor_enabled'); + $this->can_autoplay = Configs::getValueAsBool('autoplay_enabled'); + + // Thumbs configuration + $this->display_thumb_album_overlay = Configs::getValueAsEnum('display_thumb_album_overlay', ThumbOverlayVisibilityType::class); + $this->display_thumb_photo_overlay = Configs::getValueAsEnum('display_thumb_photo_overlay', ThumbOverlayVisibilityType::class); + $this->album_subtitle_type = Configs::getValueAsEnum('album_subtitle_type', ThumbAlbumSubtitleType::class); + $this->album_decoration = Configs::getValueAsEnum('album_decoration', AlbumDecorationType::class); + $this->album_decoration_orientation = Configs::getValueAsEnum('album_decoration_orientation', AlbumDecorationOrientation::class); + $this->number_albums_per_row_mobile = Configs::getValueAsInt('number_albums_per_row_mobile'); + + // Clockwork + $this->has_clockwork_in_menu(); + + // Slideshow settings + $this->slideshow_timeout = Configs::getValueAsInt('slideshow_timeout'); + + // Timeline settings + $this->is_timeline_left_border_visible = Configs::getValueAsBool('timeline_left_border_enabled'); + + // Site title & dropbox key if logged in as admin. + $this->title = Configs::getValueAsString('site_title'); + $this->dropbox_api_key = Auth::user()?->may_administrate === true ? Configs::getValueAsString('dropbox_key') : 'disabled'; + + $this->set_supporter_properties(); + } + + /** + * For clockwork we need to check that it is enabled or that we are in debug mode. + * Furthermore we need to check if the web interface is enabled. + * + * @return void + */ + private function has_clockwork_in_menu(): void + { + // Defining clockwork URL + $clockWorkEnabled = config('clockwork.enable') === true || (config('app.debug') === true && config('clockwork.enable') === null); + $clockWorkWeb = config('clockwork.web'); + + $this->clockwork_url = match (true) { + $clockWorkEnabled && ($clockWorkWeb === true) => URL::asset('clockwork/app'), + is_string($clockWorkWeb) => $clockWorkWeb . '/app', + default => null, + }; + } + + /** + * We set the properties related to Lychee SE. + * + * @return void + */ + private function set_supporter_properties() + { + $verify = resolve(Verify::class); + $is_supporter = $verify->is_supporter(); + + // We enable Lychee SE if the user is a supporter. + $this->is_se_enabled = $verify->validate() && $is_supporter; + + // We disable preview if we are already a supporter. + $this->is_se_preview_enabled = !$is_supporter && Configs::getValueAsBool('enable_se_preview'); + + // We hide the info if we are already a supporter (or the user requests it). + $this->is_se_info_hidden = $is_supporter || Configs::getValueAsBool('disable_se_call_for_actions'); + } +} \ No newline at end of file diff --git a/app/Http/Resources/GalleryConfigs/LandingPageResource.php b/app/Http/Resources/GalleryConfigs/LandingPageResource.php new file mode 100644 index 00000000000..41356b04b4d --- /dev/null +++ b/app/Http/Resources/GalleryConfigs/LandingPageResource.php @@ -0,0 +1,36 @@ +footer = new FooterConfig(); + $this->landing_page_enable = Configs::getValueAsBool('landing_page_enable'); + $this->landing_background = Configs::getValueAsString('landing_background'); + $this->landing_subtitle = Configs::getValueAsString('landing_subtitle'); + $this->landing_title = Configs::getValueAsString('landing_title'); + $this->site_owner = Configs::getValueAsString('site_owner'); + $this->site_title = Configs::getValueAsString('site_title'); + } +} diff --git a/app/Http/Resources/GalleryConfigs/MapProviderData.php b/app/Http/Resources/GalleryConfigs/MapProviderData.php new file mode 100644 index 00000000000..e5c34810cc3 --- /dev/null +++ b/app/Http/Resources/GalleryConfigs/MapProviderData.php @@ -0,0 +1,28 @@ +attribution = $map_providers->getAtributionHtml(); + $this->layer = $map_providers->getLayer(); + } +} \ No newline at end of file diff --git a/app/Http/Resources/GalleryConfigs/PhotoLayoutConfig.php b/app/Http/Resources/GalleryConfigs/PhotoLayoutConfig.php new file mode 100644 index 00000000000..d8790d86ac0 --- /dev/null +++ b/app/Http/Resources/GalleryConfigs/PhotoLayoutConfig.php @@ -0,0 +1,32 @@ +photo_layout_justified_row_height = Configs::getValueAsInt('photo_layout_justified_row_height'); + $this->photo_layout_masonry_column_width = Configs::getValueAsInt('photo_layout_masonry_column_width'); + $this->photo_layout_grid_column_width = Configs::getValueAsInt('photo_layout_grid_column_width'); + $this->photo_layout_square_column_width = Configs::getValueAsInt('photo_layout_square_column_width'); + $this->photo_layout_gap = Configs::getValueAsInt('photo_layout_gap'); + } +} diff --git a/app/Http/Resources/GalleryConfigs/RegisterData.php b/app/Http/Resources/GalleryConfigs/RegisterData.php new file mode 100644 index 00000000000..7f848322386 --- /dev/null +++ b/app/Http/Resources/GalleryConfigs/RegisterData.php @@ -0,0 +1,20 @@ +whereNotNull('longitude')->count() > 0; + $map_display = Configs::getValueAsBool('map_display'); + $public_display = $is_logged_in || Configs::getValueAsBool('map_display_public'); + + $this->is_map_accessible = $count_locations && $map_display && $public_display; + $this->is_mod_frame_enabled = $this->checkModFrameEnabled(); + + $timeline_photos_enabled = Configs::getValueAsBool('timeline_photos_enabled'); + $timeline_photos_public = Configs::getValueAsBool('timeline_photos_public'); + $this->is_photo_timeline_enabled = $timeline_photos_enabled && ($is_logged_in || $timeline_photos_public); + + $timeline_albums_enabled = Configs::getValueAsBool('timeline_albums_enabled'); + $timeline_albums_public = Configs::getValueAsBool('timeline_albums_public'); + $this->is_album_timeline_enabled = $timeline_albums_enabled && ($is_logged_in || $timeline_albums_public); + $this->timeline_album_granularity = Configs::getValueAsEnum('timeline_albums_granularity', TimelineAlbumGranularity::class); + + $this->is_search_accessible = $is_logged_in || Configs::getValueAsBool('search_public'); + + $this->album_thumb_css_aspect_ratio = Configs::getValueAsEnum('default_album_thumb_aspect_ratio', AspectRatioType::class)->css(); + $this->show_keybinding_help_button = Configs::getValueAsBool('show_keybinding_help_button'); + $this->login_button_position = Configs::getValueAsString('login_button_position'); + $this->back_button_enabled = Configs::getValueAsBool('back_button_enabled'); + $this->back_button_text = Configs::getValueAsString('back_button_text'); + $this->back_button_url = Configs::getValueAsString('back_button_url'); + } + + private function checkModFrameEnabled(): bool + { + if (!Configs::getValueAsBool('mod_frame_enabled')) { + return false; + } + + $factory = resolve(AlbumFactory::class); + try { + $album = $factory->findAbstractAlbumOrFail(Configs::getValueAsString('random_album_id')); + + return Gate::check(\AlbumPolicy::CAN_ACCESS, [AbstractAlbum::class, $album]); + } catch (\Throwable) { + Log::critical('Could not find random album for frame with ID:' . Configs::getValueAsString('random_album_id')); + + return false; + } + } +} + diff --git a/app/Http/Resources/GalleryConfigs/UploadConfig.php b/app/Http/Resources/GalleryConfigs/UploadConfig.php new file mode 100644 index 00000000000..bedae52d021 --- /dev/null +++ b/app/Http/Resources/GalleryConfigs/UploadConfig.php @@ -0,0 +1,47 @@ +upload_processing_limit = max(1, Configs::getValueAsInt('upload_processing_limit')); + $this->upload_chunk_size = self::getUploadLimit(); + } + + public static function getUploadLimit(): int + { + $size = Configs::getValueAsInt('upload_chunk_size'); + if ($size === 0) { + try { + $size = (int) min( + Helpers::convertSize(ini_get('upload_max_filesize')), + Helpers::convertSize(ini_get('post_max_size')), + Helpers::convertSize(ini_get('memory_limit')) / 10 + ); + } catch (InfoException $e) { + return 1024 * 1024; + } + } + + return $size; + } +} diff --git a/app/Http/Resources/Models/AbstractAlbumResource.php b/app/Http/Resources/Models/AbstractAlbumResource.php new file mode 100644 index 00000000000..98c0c35097c --- /dev/null +++ b/app/Http/Resources/Models/AbstractAlbumResource.php @@ -0,0 +1,26 @@ +config = $config; + $this->resource = $resource; + } +} diff --git a/app/Http/Resources/Models/AccessPermissionResource.php b/app/Http/Resources/Models/AccessPermissionResource.php new file mode 100644 index 00000000000..dbc64f475ee --- /dev/null +++ b/app/Http/Resources/Models/AccessPermissionResource.php @@ -0,0 +1,47 @@ +id, + user_id: $accessPermission->user_id, + username: $accessPermission->user->name, + album_title: $accessPermission->album->title, + album_id: $accessPermission->base_album_id, + grants_full_photo_access: $accessPermission->grants_full_photo_access, + grants_download: $accessPermission->grants_download, + grants_upload: $accessPermission->grants_upload, + grants_edit: $accessPermission->grants_edit, + grants_delete: $accessPermission->grants_delete + ); + } +} diff --git a/app/Http/Resources/Models/AlbumResource.php b/app/Http/Resources/Models/AlbumResource.php new file mode 100644 index 00000000000..3578b36ed43 --- /dev/null +++ b/app/Http/Resources/Models/AlbumResource.php @@ -0,0 +1,120 @@ + */ + #[LiteralTypeScriptType('App.Http.Resources.Models.ThumbAlbumResource[]')] + public ?Collection $albums; + /** @var ?Collection */ + #[LiteralTypeScriptType('App.Http.Resources.Models.PhotoResource[]')] + public ?Collection $photos; + + // thumb + public ?string $cover_id; + public ?ThumbResource $thumb; + + // security + public AlbumProtectionPolicy $policy; + public AlbumRightsResource $rights; + public PreFormattedAlbumData $preFormattedData; + public ?EditableBaseAlbumResource $editable; + + public function __construct(Album $album) + { + $this->id = $album->id; + $this->title = $album->title; + $this->description = $album->description; + $this->owner_name = Auth::check() ? $album->owner->name : null; + $this->copyright = $album->copyright; + + // attributes + $this->track_url = $album->track_url; + $this->license = $album->license->localization(); + // TODO: Investigate later why this string is 24 characters long. + $this->header_id = trim($album->header_id); + + // children + $this->parent_id = $album->parent_id; + $this->has_albums = !$album->isLeaf(); + $this->albums = $album->relationLoaded('children') ? ThumbAlbumResource::collect($album->children) : null; + $this->photos = $album->relationLoaded('photos') ? PhotoResource::collect($album->photos) : null; + if ($this->photos !== null) { + // Prep collection with first and last link + which id is next. + $this->prepPhotosCollection(); + + // setup timeline data + $photo_granularity = $this->getPhotoTimeline($album->photo_timeline); + $this->photos = TimelineData::setTimeLineDataForPhotos($this->photos, $photo_granularity); + } + + if ($this->albums->count() > 0) { + // setup timeline data + $sorting = $album->album_sorting?->column ?? Configs::getValueAsEnum('sorting_albums_col', ColumnSortingType::class); + $album_granularity = $this->getAlbumTimeline($album->album_timeline); + $this->albums = TimelineData::setTimeLineDataForAlbums($this->albums, $sorting, $album_granularity); + } + + // thumb + $this->cover_id = $album->cover_id; + $this->thumb = ThumbResource::fromModel($album->thumb); + + // security + $this->policy = AlbumProtectionPolicy::ofBaseAlbum($album); + $this->rights = new AlbumRightsResource($album); + $url = $this->getHeaderUrl($album); + $this->preFormattedData = new PreFormattedAlbumData($album, $url); + + if ($this->rights->can_edit) { + $this->editable = EditableBaseAlbumResource::fromModel($album); + } + } + + public static function fromModel(Album $album): AlbumResource + { + return new self($album); + } +} diff --git a/app/Http/Resources/Models/ConfigResource.php b/app/Http/Resources/Models/ConfigResource.php new file mode 100644 index 00000000000..3b7fa9c7252 --- /dev/null +++ b/app/Http/Resources/Models/ConfigResource.php @@ -0,0 +1,40 @@ +key = $c->key; + $this->type = ConfigType::tryFrom($c->type_range) ?? $c->type_range; + $this->value = $c->value; + $this->documentation = $c->description; + $this->details = $c->details; + $this->require_se = $c->level > 0; + } + + public static function fromModel(Configs $c): ConfigResource + { + return new self($c); + } +} \ No newline at end of file diff --git a/app/Http/Resources/Models/JobHistoryResource.php b/app/Http/Resources/Models/JobHistoryResource.php new file mode 100644 index 00000000000..b34ae393d18 --- /dev/null +++ b/app/Http/Resources/Models/JobHistoryResource.php @@ -0,0 +1,39 @@ +username = $jobHistory->owner->username; + $this->status = $jobHistory->status->name(); + $this->created_at = $jobHistory->created_at->toIso8601String(); + $this->updated_at = $jobHistory->updated_at->toIso8601String(); + $this->job = $jobHistory->job; + } + + public static function fromModel(JobHistory $jobHistory): JobHistoryResource + { + return new self($jobHistory); + } +} \ No newline at end of file diff --git a/app/Http/Resources/Models/LightUserResource.php b/app/Http/Resources/Models/LightUserResource.php new file mode 100644 index 00000000000..0bc56e0fe55 --- /dev/null +++ b/app/Http/Resources/Models/LightUserResource.php @@ -0,0 +1,31 @@ +id = $user->id; + $this->username = $user->username; + } + + public static function fromModel(User $c): LightUserResource + { + return new self($c); + } +} diff --git a/app/Http/Resources/Models/PhotoResource.php b/app/Http/Resources/Models/PhotoResource.php new file mode 100644 index 00000000000..d7c3951c8ad --- /dev/null +++ b/app/Http/Resources/Models/PhotoResource.php @@ -0,0 +1,125 @@ +id = $photo->id; + $this->album_id = $photo->album_id; + $this->altitude = $photo->altitude; + $this->aperture = $photo->aperture; + $this->checksum = $photo->checksum; + $this->created_at = $photo->created_at->toIso8601String(); + $this->description = $photo->description ?? ''; + $this->focal = $photo->focal; + $this->is_starred = $photo->is_starred; + $this->iso = $photo->iso; + $this->latitude = $photo->latitude; + $this->lens = $photo->lens; + $this->license = $photo->license; + $this->live_photo_checksum = $photo->live_photo_checksum; + $this->live_photo_content_id = $photo->live_photo_content_id; + $this->live_photo_url = $photo->live_photo_url; + $this->setLocation($photo); + $this->longitude = $photo->longitude; + $this->make = $photo->make; + $this->model = $photo->model; + $this->original_checksum = $photo->original_checksum; + $this->shutter = $photo->shutter; + $this->size_variants = new SizeVariantsResouce($photo); + $this->tags = $photo->tags; + $this->taken_at = $photo->taken_at?->toIso8601String(); + $this->taken_at_orig_tz = $photo->taken_at_orig_tz; + $this->title = $photo->title; + $this->type = $photo->type; + $this->updated_at = $photo->updated_at->toIso8601String(); + $this->rights = new PhotoRightsResource($photo); + $this->next_photo_id = null; + $this->previous_photo_id = null; + $this->preformatted = new PreformattedPhotoData($photo, $this->size_variants->original); + $this->precomputed = new PreComputedPhotoData($photo); + + $this->timeline_data_carbon = $photo->taken_at ?? $photo->created_at; + } + + public static function fromModel(Photo $photo): PhotoResource + { + return new self($photo); + } + + private function setLocation(Photo $photo): void + { + $showLocation = Configs::getValueAsBool('location_show') && (Auth::check() || Configs::getValueAsBool('location_show_public')); + $this->location = $showLocation ? $photo->location : null; + } + + /** + * Accessors to the Carbon instances. + * + * @return Carbon + */ + public function timeline_date_carbon(): Carbon + { + return $this->timeline_data_carbon; + } +} diff --git a/app/Http/Resources/Models/SizeVariantResource.php b/app/Http/Resources/Models/SizeVariantResource.php new file mode 100644 index 00000000000..72c2341d1c5 --- /dev/null +++ b/app/Http/Resources/Models/SizeVariantResource.php @@ -0,0 +1,36 @@ +type = $sizeVariant->type; + $this->locale = $sizeVariant->type->localization(); + $this->filesize = Helpers::getSymbolByQuantity(floatval($sizeVariant->filesize)); + $this->height = $sizeVariant->height; + $this->width = $sizeVariant->width; + $this->url = !$noUrl ? $sizeVariant->url : null; + } +} diff --git a/app/Http/Resources/Models/SizeVariantsResouce.php b/app/Http/Resources/Models/SizeVariantsResouce.php new file mode 100644 index 00000000000..86e2a15f71e --- /dev/null +++ b/app/Http/Resources/Models/SizeVariantsResouce.php @@ -0,0 +1,55 @@ +relationLoaded('size_variants') ? $photo->size_variants : null; + $downgrade = !Gate::check(PhotoPolicy::CAN_ACCESS_FULL_PHOTO, [Photo::class, $photo]) && + !$photo->isVideo() && + $size_variants?->hasMedium() === true; + + $original = $size_variants?->getSizeVariant(SizeVariantType::ORIGINAL); + $medium = $size_variants?->getSizeVariant(SizeVariantType::MEDIUM); + $medium2x = $size_variants?->getSizeVariant(SizeVariantType::MEDIUM2X); + $small = $size_variants?->getSizeVariant(SizeVariantType::SMALL); + $small2x = $size_variants?->getSizeVariant(SizeVariantType::SMALL2X); + $thumb = $size_variants?->getSizeVariant(SizeVariantType::THUMB); + $thumb2x = $size_variants?->getSizeVariant(SizeVariantType::THUMB2X); + $placeholder = $size_variants?->getSizeVariant(SizeVariantType::PLACEHOLDER); + + $this->medium = $medium?->toResource(); + $this->medium2x = $medium2x?->toResource(); + $this->original = $original?->toResource($downgrade); + $this->small = $small?->toResource(); + $this->small2x = $small2x?->toResource(); + $this->thumb = $thumb?->toResource(); + $this->thumb2x = $thumb2x?->toResource(); + $this->placeholder = $placeholder?->toResource(); + } +} diff --git a/app/Http/Resources/Models/SmartAlbumResource.php b/app/Http/Resources/Models/SmartAlbumResource.php new file mode 100644 index 00000000000..9f2fda79912 --- /dev/null +++ b/app/Http/Resources/Models/SmartAlbumResource.php @@ -0,0 +1,68 @@ + */ + #[LiteralTypeScriptType('App.Http.Resources.Models.PhotoResource[]')] + public ?Collection $photos; + public ?ThumbResource $thumb; + public AlbumProtectionPolicy $policy; + public AlbumRightsResource $rights; + public PreFormattedAlbumData $preFormattedData; + + public function __construct(BaseSmartAlbum $smartAlbum) + { + $this->id = $smartAlbum->id; + $this->title = $smartAlbum->title; + /** @phpstan-ignore-next-line */ + $this->photos = $smartAlbum->relationLoaded('photos') ? PhotoResource::collect($smartAlbum->getPhotos()) : null; + $this->prepPhotosCollection(); + if ($this->photos !== null) { + // Prep collection with first and last link + which id is next. + $this->prepPhotosCollection(); + + // setup timeline data + $photo_granularity = Configs::getValueAsEnum('timeline_photos_granularity', TimelinePhotoGranularity::class); + $this->photos = TimelineData::setTimeLineDataForPhotos($this->photos, $photo_granularity); + } + + $this->thumb = ThumbResource::fromModel($smartAlbum->thumb); + $this->policy = AlbumProtectionPolicy::ofSmartAlbum($smartAlbum); + $this->rights = new AlbumRightsResource($smartAlbum); + $url = $this->getHeaderUrl($smartAlbum); + $this->preFormattedData = new PreFormattedAlbumData($smartAlbum, $url); + } + + public static function fromModel(BaseSmartAlbum $smartAlbum): SmartAlbumResource + { + return new self($smartAlbum); + } +} diff --git a/app/Http/Resources/Models/TagAlbumResource.php b/app/Http/Resources/Models/TagAlbumResource.php new file mode 100644 index 00000000000..e7ca328b2fd --- /dev/null +++ b/app/Http/Resources/Models/TagAlbumResource.php @@ -0,0 +1,94 @@ + */ + #[LiteralTypeScriptType('App.Http.Resources.Models.PhotoResource[]')] + public ?Collection $photos; + + // thumb + public ThumbResource|null $thumb; + + // security + public AlbumProtectionPolicy $policy; + public AlbumRightsResource $rights; + public PreFormattedAlbumData $preFormattedData; + public ?EditableBaseAlbumResource $editable; + + public function __construct(TagAlbum $tagAlbum) + { + // basic + $this->id = $tagAlbum->id; + $this->title = $tagAlbum->title; + $this->owner_name = Auth::check() ? $tagAlbum->owner->name : null; + $this->is_tag_album = true; + $this->show_tags = $tagAlbum->show_tags; + $this->copyright = $tagAlbum->copyright; + + // children + $this->photos = $tagAlbum->relationLoaded('photos') ? PhotoResource::collect($tagAlbum->photos) : null; + if ($this->photos !== null) { + // Prep collection with first and last link + which id is next. + $this->prepPhotosCollection(); + + // setup timeline data + $photo_granularity = $this->getPhotoTimeline($tagAlbum->photo_timeline); + $this->photos = TimelineData::setTimeLineDataForPhotos($this->photos, $photo_granularity); + } + + // thumb + $this->thumb = ThumbResource::fromModel($tagAlbum->thumb); + + // security + $this->policy = AlbumProtectionPolicy::ofBaseAlbum($tagAlbum); + $this->rights = new AlbumRightsResource($tagAlbum); + $url = $this->getHeaderUrl($tagAlbum); + $this->preFormattedData = new PreFormattedAlbumData($tagAlbum, $url); + + if ($this->rights->can_edit) { + $this->editable = EditableBaseAlbumResource::fromModel($tagAlbum); + } + } + + public static function fromModel(TagAlbum $tagAlbum): TagAlbumResource + { + return new self($tagAlbum); + } +} diff --git a/app/Http/Resources/Models/TargetAlbumResource.php b/app/Http/Resources/Models/TargetAlbumResource.php new file mode 100644 index 00000000000..c7cbd42ed37 --- /dev/null +++ b/app/Http/Resources/Models/TargetAlbumResource.php @@ -0,0 +1,49 @@ +id = $values['id']; + $this->title = $values['title']; + $this->original = $values['original']; + $this->short_title = $values['short_title']; + $this->thumb = $values['thumb']; + } + + /** + * @param TAlbumSaved $a + * + * @return TargetAlbumResource + */ + public static function fromArray(array $a): TargetAlbumResource + { + return new self($a); + } +} diff --git a/app/Http/Resources/Models/ThumbAlbumResource.php b/app/Http/Resources/Models/ThumbAlbumResource.php new file mode 100644 index 00000000000..4993b498862 --- /dev/null +++ b/app/Http/Resources/Models/ThumbAlbumResource.php @@ -0,0 +1,142 @@ +id = $data->id; + $this->thumb = ThumbResource::fromModel($data->thumb); + $this->title = $data->title; + + if ($data instanceof BaseSmartAlbum) { + $policy = AlbumProtectionPolicy::ofSmartAlbum($data); + } else { + /** @var BaseAlbum $data */ + $this->min_taken_at_carbon = $data->min_taken_at; + $this->max_taken_at_carbon = $data->max_taken_at; + $this->max_taken_at = $this->max_taken_at_carbon?->format($date_format); + $this->min_taken_at = $this->min_taken_at_carbon?->format($date_format); + + $this->formatMinMaxDate(); + + $this->created_at_carbon = $data->created_at; + $this->created_at = $this->created_at_carbon->format($date_format); + $policy = AlbumProtectionPolicy::ofBaseAlbum($data); + $this->description = Str::limit($data->description, 100); + $this->owner = $data->owner->username; + } + + if ($data instanceof Album) { + $this->num_photos = $data->num_photos; + $this->num_subalbums = $data->num_children; + } + + $this->is_nsfw = $policy->is_nsfw; + $this->is_public = $policy->is_public; + $this->is_link_required = $policy->is_link_required; + $this->is_password_required = $policy->is_password_required; + + $this->is_tag_album = $data instanceof TagAlbum; + // This aims to indicate whether the current thumb is used to determine the parent. + $this->has_subalbum = $data instanceof Album && !$data->isLeaf(); + + $this->rights = new AlbumRightsResource($data); + } + + public static function fromModel(AbstractAlbum $album): ThumbAlbumResource + { + return new self($album); + } + + private function formatMinMaxDate(): void + { + if ($this->max_taken_at === null || $this->min_taken_at === null) { + return; + } + if ($this->max_taken_at === $this->min_taken_at) { + $this->formatted_min_max = $this->max_taken_at; + + return; + } + + if (Configs::getValueAsEnum('thumb_min_max_order', DateOrderingType::class) === DateOrderingType::YOUNGER_OLDER) { + $this->formatted_min_max = $this->max_taken_at . ' - ' . $this->min_taken_at; + } else { + $this->formatted_min_max = $this->min_taken_at . ' - ' . $this->max_taken_at; + } + } + + /** + * Accessors to the Carbon instances. + * + * @return Carbon + */ + public function created_at_carbon(): Carbon + { + return $this->created_at_carbon; + } + + public function min_taken_at_carbon(): ?Carbon + { + return $this->min_taken_at_carbon; + } + + public function max_taken_at_carbon(): ?Carbon + { + return $this->max_taken_at_carbon; + } +} diff --git a/app/Http/Resources/Models/ThumbResource.php b/app/Http/Resources/Models/ThumbResource.php new file mode 100644 index 00000000000..7f66a848cc7 --- /dev/null +++ b/app/Http/Resources/Models/ThumbResource.php @@ -0,0 +1,48 @@ +id = $id; + $this->type = $type; + $this->thumb = $thumbUrl; + $this->thumb2x = $thumb2xUrl; + $this->placeholder = $placeholderUrl; + } + + /** + * Produce a thumb resource from a Thumb object if existing. + * + * @param Thumb|null $thumb + * + * @return ThumbResource|null + */ + public static function fromModel(?Thumb $thumb): ?self + { + if ($thumb === null) { + return null; + } + + return new self($thumb->id, $thumb->type, $thumb->thumbUrl, $thumb->thumb2xUrl, $thumb->placeholderUrl); + } +} diff --git a/app/Http/Resources/Models/UserManagementResource.php b/app/Http/Resources/Models/UserManagementResource.php new file mode 100644 index 00000000000..e471c86c0d3 --- /dev/null +++ b/app/Http/Resources/Models/UserManagementResource.php @@ -0,0 +1,53 @@ +id = $user->id; + $this->username = $user->username; + $this->may_administrate = $user->may_administrate; + $this->may_upload = $user->may_upload || $user->may_administrate; + $this->may_edit_own_settings = $user->may_edit_own_settings || $user->may_administrate; + if ($is_se) { + $this->quota_kb = $user->quota_kb; + $this->description = $user->description; + $this->note = $user->note; + $this->space = $space['size']; + } + if ($user->id !== $space['id']) { + throw new \RuntimeException('User and space id do not match'); + } + } +} diff --git a/app/Http/Resources/Models/UserResource.php b/app/Http/Resources/Models/UserResource.php new file mode 100644 index 00000000000..82222a06f44 --- /dev/null +++ b/app/Http/Resources/Models/UserResource.php @@ -0,0 +1,30 @@ +id = $user?->id; + $this->has_token = $user?->token !== null; + $this->username = $user?->username; + $this->email = $user?->email; + } +} diff --git a/app/Http/Resources/Models/Utils/AlbumProtectionPolicy.php b/app/Http/Resources/Models/Utils/AlbumProtectionPolicy.php new file mode 100644 index 00000000000..03f7c322121 --- /dev/null +++ b/app/Http/Resources/Models/Utils/AlbumProtectionPolicy.php @@ -0,0 +1,80 @@ +public_permissions() !== null, + is_link_required: $baseAlbum->public_permissions()?->is_link_required === true, + is_nsfw: $baseAlbum->is_nsfw, + grants_full_photo_access: $baseAlbum->public_permissions()?->grants_full_photo_access === true, + grants_download: $baseAlbum->public_permissions()?->grants_download === true, + is_password_required: $baseAlbum->public_permissions()?->password !== null, + ); + } + + /** + * Given a smart album, returns the Protection Policy associated to it. + * + * @param BaseSmartAlbum $baseSmartAlbum + * + * @return AlbumProtectionPolicy + */ + public static function ofSmartAlbum(BaseSmartAlbum $baseSmartAlbum): self + { + return new self( + is_public: $baseSmartAlbum->public_permissions() !== null, + is_link_required: false, + is_nsfw: false, + grants_full_photo_access: $baseSmartAlbum->public_permissions()?->grants_full_photo_access === true, + grants_download: $baseSmartAlbum->public_permissions()?->grants_download === true, + is_password_required: false, + ); + } +} diff --git a/app/Http/Resources/Models/Utils/PreComputedPhotoData.php b/app/Http/Resources/Models/Utils/PreComputedPhotoData.php new file mode 100644 index 00000000000..78a8086048f --- /dev/null +++ b/app/Http/Resources/Models/Utils/PreComputedPhotoData.php @@ -0,0 +1,55 @@ +is_video = $photo->isVideo(); + $this->is_raw = $photo->isRaw(); + $this->is_livephoto = $photo->live_photo_url !== null; + $this->is_camera_date = $photo->taken_at !== null; + $this->has_exif = $this->genExifHash($photo) !== ''; + $this->has_location = $this->has_location($photo); + } + + private function has_location(Photo $photo): bool + { + return $photo->longitude !== null && + $photo->latitude !== null && + $photo->altitude !== null; + } + + private function genExifHash(Photo $photo): string + { + $exifHash = $photo->make; + $exifHash .= $photo->model; + $exifHash .= $photo->shutter; + if (!$photo->isVideo()) { + $exifHash .= $photo->aperture; + $exifHash .= $photo->focal; + } + $exifHash .= $photo->iso; + + return $exifHash; + } +} diff --git a/app/Http/Resources/Models/Utils/PreFormattedAlbumData.php b/app/Http/Resources/Models/Utils/PreFormattedAlbumData.php new file mode 100644 index 00000000000..eaa74350fc1 --- /dev/null +++ b/app/Http/Resources/Models/Utils/PreFormattedAlbumData.php @@ -0,0 +1,75 @@ +url = $url; + $this->title = $album->title; + if ($album instanceof BaseAlbum) { + $this->min_taken_at = $album->min_taken_at?->format($min_max_date_format); + $this->max_taken_at = $album->max_taken_at?->format($min_max_date_format); + $this->formatMinMaxDate(); + $this->created_at = $album->created_at->format($create_date_format); + $this->description = Markdown::convert(trim($album->description ?? ''))->getContent(); + $this->copyright = $album->copyright; + } + if ($album instanceof Album) { + $this->num_children = $album->num_children; + $this->num_photos = $album->num_photos; + $this->license = $album->license === LicenseType::NONE ? '' : $album->license->localization(); + } + } + + private function formatMinMaxDate(): void + { + if ($this->max_taken_at === null || $this->min_taken_at === null) { + return; + } + if ($this->max_taken_at === $this->min_taken_at) { + $this->min_max_text = $this->max_taken_at; + + return; + } + + if (Configs::getValueAsEnum('header_min_max_order', DateOrderingType::class) === DateOrderingType::YOUNGER_OLDER) { + $this->min_max_text = $this->max_taken_at . ' - ' . $this->min_taken_at; + } else { + $this->min_max_text = $this->min_taken_at . ' - ' . $this->max_taken_at; + } + } +} diff --git a/app/Http/Resources/Models/Utils/PreformattedPhotoData.php b/app/Http/Resources/Models/Utils/PreformattedPhotoData.php new file mode 100644 index 00000000000..26b236349dc --- /dev/null +++ b/app/Http/Resources/Models/Utils/PreformattedPhotoData.php @@ -0,0 +1,66 @@ +created_at = $photo->created_at->format($date_format_uploaded); + $this->taken_at = $photo->taken_at?->format($date_format_taken_at); + $this->date_overlay = ($photo->taken_at ?? $photo->created_at)->format($overlay_date_format) ?? ''; + + $this->shutter = str_replace('s', 'sec', $photo->shutter ?? ''); + $this->aperture = str_replace('f/', '', $photo->aperture ?? ''); + $this->iso = sprintf(__('gallery.photo.details.iso'), $photo->iso); + $this->lens = ($photo->lens === '' || $photo->lens === null) ? '' : sprintf('(%s)', $photo->lens); + + $this->duration = Helpers::secondsToHMS(intval($photo->aperture)); + $this->fps = $photo->focal === null ? $photo->focal . ' fps' : ''; + + $this->filesize = $original?->filesize ?? '0'; + $this->resolution = $original?->width . ' x ' . $original?->height; + $this->latitude = Helpers::decimalToDegreeMinutesSeconds($photo->latitude, true); + $this->longitude = Helpers::decimalToDegreeMinutesSeconds($photo->longitude, false); + $this->altitude = $photo->altitude !== null ? round($photo->altitude, 1) . 'm' : null; + $this->license = $photo->license !== LicenseType::NONE ? $photo->license->localization() : ''; + $this->description = ($photo->description ?? '') === '' ? '' : Markdown::convert($photo->description)->getContent(); + } +} diff --git a/app/Http/Resources/Models/Utils/TimelineData.php b/app/Http/Resources/Models/Utils/TimelineData.php new file mode 100644 index 00000000000..e3c9d922077 --- /dev/null +++ b/app/Http/Resources/Models/Utils/TimelineData.php @@ -0,0 +1,120 @@ + $photo->timeline_date_carbon()->format($timeline_date_format_year), + TimelinePhotoGranularity::MONTH => $photo->timeline_date_carbon()->format($timeline_date_format_month), + TimelinePhotoGranularity::DAY => $photo->timeline_date_carbon()->format($timeline_date_format_day), + TimelinePhotoGranularity::HOUR => $photo->timeline_date_carbon()->format($timeline_photo_date_format_hour), + TimelinePhotoGranularity::DEFAULT, TimelinePhotoGranularity::DISABLED => throw new LycheeLogicException('DEFAULT is not a valid granularity for photos'), + }; + + $timeDate = match ($granularity) { + TimelinePhotoGranularity::YEAR => $photo->timeline_date_carbon()->format('Y'), + TimelinePhotoGranularity::MONTH => $photo->timeline_date_carbon()->format('Y-m'), + TimelinePhotoGranularity::DAY => $photo->timeline_date_carbon()->format('Y-m-d'), + TimelinePhotoGranularity::HOUR => $photo->timeline_date_carbon()->format('Y-m-d H'), + TimelinePhotoGranularity::DEFAULT, TimelinePhotoGranularity::DISABLED => throw new LycheeLogicException('DEFAULT is not a valid granularity for photos'), + }; + + return new TimelineData(timeDate: $timeDate, format: $format); + } + + private static function fromAlbum(ThumbAlbumResource $album, ColumnSortingType $columnSorting, TimelineAlbumGranularity $granularity): ?self + { + $timeline_date_format_year = Configs::getValueAsString('timeline_album_date_format_year'); + $timeline_date_format_month = Configs::getValueAsString('timeline_album_date_format_month'); + $timeline_date_format_day = Configs::getValueAsString('timeline_album_date_format_day'); + $date = match ($columnSorting) { + ColumnSortingType::CREATED_AT => $album->created_at_carbon(), + ColumnSortingType::MAX_TAKEN_AT => $album->max_taken_at_carbon(), + ColumnSortingType::MIN_TAKEN_AT => $album->min_taken_at_carbon(), + default => null, + }; + + if ($date === null) { + return null; + } + + $format = match ($granularity) { + TimelineAlbumGranularity::YEAR => $date->format($timeline_date_format_year), + TimelineAlbumGranularity::MONTH => $date->format($timeline_date_format_month), + TimelineAlbumGranularity::DAY => $date->format($timeline_date_format_day), + TimelineAlbumGranularity::DEFAULT, TimelineAlbumGranularity::DISABLED => throw new LycheeLogicException('DEFAULT/DISABLED is not a valid granularity for albums'), + }; + + $timeDate = match ($granularity) { + TimelineAlbumGranularity::YEAR => $date->format('Y'), + TimelineAlbumGranularity::MONTH => $date->format('Y-m'), + TimelineAlbumGranularity::DAY => $date->format('Y-m-d'), + TimelineAlbumGranularity::DEFAULT, TimelineAlbumGranularity::DISABLED => throw new LycheeLogicException('DEFAULT/DISABLED is not a valid granularity for albums'), + }; + + return new TimelineData(timeDate: $timeDate, format: $format); + } + + /** + * @param Collection $albums + * @param ColumnSortingType $columnSorting + * @param TimelineAlbumGranularity $granularity + * + * @return Collection + */ + public static function setTimeLineDataForAlbums(Collection $albums, ColumnSortingType $columnSorting, TimelineAlbumGranularity $granularity): Collection + { + return $albums->map(function (ThumbAlbumResource $album) use ($columnSorting, $granularity) { + $album->timeline = TimelineData::fromAlbum($album, $columnSorting, $granularity); + + return $album; + }); + } + + /** + * @param Collection $photos + * @param TimelinePhotoGranularity $granularity + * + * @return Collection + */ + public static function setTimeLineDataForPhotos(Collection $photos, TimelinePhotoGranularity $granularity): Collection + { + return $photos->map(function (PhotoResource $photo) use ($granularity) { + $photo->timeline = TimelineData::fromPhoto($photo, $granularity); + + return $photo; + }); + } +} \ No newline at end of file diff --git a/app/Http/Resources/Models/Utils/UserToken.php b/app/Http/Resources/Models/Utils/UserToken.php new file mode 100644 index 00000000000..11533af5261 --- /dev/null +++ b/app/Http/Resources/Models/Utils/UserToken.php @@ -0,0 +1,21 @@ +id = $credential->id; + $this->alias = $credential->alias; + $this->created_at = $credential->created_at->toIso8601String(); + } + + public static function fromModel(WebAuthnCredential $credential): WebAuthnResource + { + return new self($credential); + } +} diff --git a/app/Http/Resources/Oauth/OauthRegistrationData.php b/app/Http/Resources/Oauth/OauthRegistrationData.php new file mode 100644 index 00000000000..4822fcf67c5 --- /dev/null +++ b/app/Http/Resources/Oauth/OauthRegistrationData.php @@ -0,0 +1,24 @@ +isInstanceOf(Data::class); + } + + /** + * @param Type $type the type being transformed to schema + */ + public function toSchema(Type $type): ?OpenApiType + { + /** @phpstan-ignore-next-line */ + $reflect = new \ReflectionClass($type->name); + $props = $reflect->getProperties(\ReflectionProperty::IS_PUBLIC); + + $ret = new OpenApiObjectType(); + collect($props)->each(function ($prop) use ($ret) { + $toConvertType = $this->convertReflected($prop->getType()); + $ret->addProperty($prop->name, $this->openApiTransformer->transform($toConvertType)); + }); + + return $ret; + } + + /** + * Set a reference to that object in the return. + * + * @param ObjectType $type + * + * @return Reference + */ + public function reference(ObjectType $type): Reference + { + return new Reference('schemas', $type->name, $this->components); + } + + /** + * Given a pure reflected PHP type, we return the corresponding Scramble type equivalent before Generator conversion. + * + * @return Type + * + * @throws \InvalidArgumentException + * @throws LycheeLogicException + */ + private function convertReflected(\ReflectionNamedType|\ReflectionUnionType|\ReflectionType|null $type): Type + { + if ($type === null) { + return new NullType(); + } + + if ($type instanceof \ReflectionUnionType) { + return $this->handleUnionType($type); + } + + if ($type instanceof \ReflectionIntersectionType) { + throw new LycheeLogicException('Intersection types are not supported.'); + } + + if (!$type instanceof \ReflectionNamedType) { + throw new LycheeLogicException('Unexpected reflection type.'); + } + + $name = $type->getName(); + if ($type->isBuiltin()) { + return $this->handleBuiltin($name); + } + + return match ($name) { + 'Spatie\LaravelData\Data' => throw new LycheeLogicException('Spatie\LaravelData\Data should not be used as return type.'), + 'Illuminate\Support\Collection' => new ArrayType(), // refactor me later. + default => new ObjectType($name), + }; + } + + private function handleUnionType(\ReflectionUnionType $union): Type + { + $types = collect($union->getTypes())->map(fn ($type) => $this->convertReflected($type))->all(); + $unionType = new Union($types); + + return $unionType; + } + + private function handleBuiltin(string $type): Type + { + return match ($type) { + 'null' => new NullType(), + 'int' => new IntegerType(), + 'float' => new FloatType(), + 'bool' => new BooleanType(), + 'array' => new ArrayType(), + 'string' => new StringType(), + default => throw new LycheeLogicException('Unknown type: ' . $type), + }; + } +} \ No newline at end of file diff --git a/app/Http/Resources/Rights/AlbumRightsResource.php b/app/Http/Resources/Rights/AlbumRightsResource.php new file mode 100644 index 00000000000..342ad72a0da --- /dev/null +++ b/app/Http/Resources/Rights/AlbumRightsResource.php @@ -0,0 +1,46 @@ +can_edit = Gate::check(AlbumPolicy::CAN_EDIT, [AbstractAlbum::class, $abstractAlbum]); + $this->can_share = Gate::check(AlbumPolicy::CAN_SHARE, [AbstractAlbum::class, $abstractAlbum]); + $this->can_share_with_users = Gate::check(AlbumPolicy::CAN_SHARE_WITH_USERS, [AbstractAlbum::class, $abstractAlbum]); + $this->can_download = Gate::check(AlbumPolicy::CAN_DOWNLOAD, [AbstractAlbum::class, $abstractAlbum]); + $this->can_upload = Gate::check(AlbumPolicy::CAN_UPLOAD, [AbstractAlbum::class, $abstractAlbum]); + $this->can_move = Gate::check(AlbumPolicy::CAN_DELETE, [AbstractAlbum::class, $abstractAlbum]) && $abstractAlbum instanceof Album; + $this->can_delete = Gate::check(AlbumPolicy::CAN_DELETE, [AbstractAlbum::class, $abstractAlbum]); + $this->can_transfer = Gate::check(AlbumPolicy::CAN_TRANSFER, [AbstractAlbum::class, $abstractAlbum]); + $this->can_access_original = Gate::check(AlbumPolicy::CAN_ACCESS_FULL_PHOTO, [AbstractAlbum::class, $abstractAlbum]); + } +} diff --git a/app/Http/Resources/Rights/GlobalRightsResource.php b/app/Http/Resources/Rights/GlobalRightsResource.php new file mode 100644 index 00000000000..10cfab5f085 --- /dev/null +++ b/app/Http/Resources/Rights/GlobalRightsResource.php @@ -0,0 +1,29 @@ +root_album = new RootAlbumRightsResource(); + $this->settings = new SettingsRightsResource(); + $this->user_management = new UserManagementRightsResource(); + $this->user = new UserRightsResource(); + } +} diff --git a/app/Http/Resources/Rights/PhotoRightsResource.php b/app/Http/Resources/Rights/PhotoRightsResource.php new file mode 100644 index 00000000000..4d0efa49bee --- /dev/null +++ b/app/Http/Resources/Rights/PhotoRightsResource.php @@ -0,0 +1,37 @@ +can_edit = Gate::check(PhotoPolicy::CAN_EDIT, [Photo::class, $photo]); + $this->can_download = Gate::check(PhotoPolicy::CAN_DOWNLOAD, [Photo::class, $photo]); + $this->can_access_full_photo = Gate::check(PhotoPolicy::CAN_ACCESS_FULL_PHOTO, [Photo::class, $photo]); + } +} \ No newline at end of file diff --git a/app/Http/Resources/Rights/RootAlbumRightsResource.php b/app/Http/Resources/Rights/RootAlbumRightsResource.php new file mode 100644 index 00000000000..b8fd4a81cf4 --- /dev/null +++ b/app/Http/Resources/Rights/RootAlbumRightsResource.php @@ -0,0 +1,28 @@ +can_edit = Gate::check(AlbumPolicy::CAN_EDIT, [AbstractAlbum::class, null]); + $this->can_upload = Gate::check(AlbumPolicy::CAN_UPLOAD, [AbstractAlbum::class, null]); + } +} diff --git a/app/Http/Resources/Rights/SettingsRightsResource.php b/app/Http/Resources/Rights/SettingsRightsResource.php new file mode 100644 index 00000000000..e206a405a90 --- /dev/null +++ b/app/Http/Resources/Rights/SettingsRightsResource.php @@ -0,0 +1,36 @@ +can_edit = Gate::check(SettingsPolicy::CAN_EDIT, [Configs::class]); + $this->can_see_logs = Gate::check(SettingsPolicy::CAN_SEE_LOGS, [Configs::class]); + $this->can_clear_logs = Gate::check(SettingsPolicy::CAN_CLEAR_LOGS, [Configs::class]); + $this->can_see_diagnostics = Gate::check(SettingsPolicy::CAN_SEE_DIAGNOSTICS, [Configs::class]); + $this->can_update = Gate::check(SettingsPolicy::CAN_UPDATE, [Configs::class]); + $this->can_access_dev_tools = Gate::check(SettingsPolicy::CAN_ACCESS_DEV_TOOLS, [Configs::class]); + } +} diff --git a/app/Http/Resources/Rights/UserManagementRightsResource.php b/app/Http/Resources/Rights/UserManagementRightsResource.php new file mode 100644 index 00000000000..68f7792c315 --- /dev/null +++ b/app/Http/Resources/Rights/UserManagementRightsResource.php @@ -0,0 +1,32 @@ +can_create = Gate::check(UserPolicy::CAN_CREATE_OR_EDIT_OR_DELETE, [User::class]); + $this->can_list = Gate::check(UserPolicy::CAN_LIST, [User::class]); + $this->can_edit = Gate::check(UserPolicy::CAN_CREATE_OR_EDIT_OR_DELETE, [User::class]); + $this->can_delete = Gate::check(UserPolicy::CAN_CREATE_OR_EDIT_OR_DELETE, [User::class]); + } +} diff --git a/app/Http/Resources/Rights/UserRightsResource.php b/app/Http/Resources/Rights/UserRightsResource.php new file mode 100644 index 00000000000..7e756f1eb29 --- /dev/null +++ b/app/Http/Resources/Rights/UserRightsResource.php @@ -0,0 +1,26 @@ +can_edit = Gate::check(UserPolicy::CAN_EDIT, [User::class]); + } +} diff --git a/app/Http/Resources/Root/AuthConfig.php b/app/Http/Resources/Root/AuthConfig.php new file mode 100644 index 00000000000..fc7ed1394c8 --- /dev/null +++ b/app/Http/Resources/Root/AuthConfig.php @@ -0,0 +1,36 @@ + */ + public readonly array $oauthProviders; + public readonly bool $u2f_enabled; + + public function __construct() + { + $providers = []; + foreach (OauthProvidersType::cases() as $oauth) { + $client_id = config('services.' . $oauth->value . '.client_id'); + if ($client_id === null || $client_id === '') { + continue; + } + $providers[] = $oauth; + } + $this->oauthProviders = $providers; + $this->u2f_enabled = WebAuthnCredential::query()->whereNull('disabled_at')->count() > 0; + } +} \ No newline at end of file diff --git a/app/Http/Resources/Root/VersionResource.php b/app/Http/Resources/Root/VersionResource.php new file mode 100644 index 00000000000..78693e8f50e --- /dev/null +++ b/app/Http/Resources/Root/VersionResource.php @@ -0,0 +1,44 @@ +version = resolve(InstalledVersion::class)->getVersion()->toString(); + } + + $fileVersion = resolve(FileVersion::class); + $gitHubVersion = resolve(GitHubVersion::class); + + if (Configs::getValueAsBool('check_for_updates')) { + // @codeCoverageIgnoreStart + $fileVersion->hydrate(); + $gitHubVersion->hydrate(); + // @codeCoverageIgnoreEnd + } + + $this->is_new_release_available = !$fileVersion->isUpToDate(); + $this->is_git_update_available = !$gitHubVersion->isUpToDate(); + } +} diff --git a/app/Http/Resources/Search/InitResource.php b/app/Http/Resources/Search/InitResource.php new file mode 100644 index 00000000000..29a0bbd727a --- /dev/null +++ b/app/Http/Resources/Search/InitResource.php @@ -0,0 +1,30 @@ +search_minimum_length = Configs::getValueAsInt('search_minimum_length_required'); + $this->photo_layout = Configs::getValueAsEnum('search_photos_layout', PhotoLayoutType::class); + } +} \ No newline at end of file diff --git a/app/Http/Resources/Search/ResultsResource.php b/app/Http/Resources/Search/ResultsResource.php new file mode 100644 index 00000000000..44c99b1ab11 --- /dev/null +++ b/app/Http/Resources/Search/ResultsResource.php @@ -0,0 +1,81 @@ + */ + #[LiteralTypeScriptType('App.Http.Resources.Models.ThumbAlbumResource[]')] + public Collection $albums; + + /** @var Collection */ + #[LiteralTypeScriptType('App.Http.Resources.Models.PhotoResource[]')] + public Collection $photos; + + public int $current_page; + public int $from; + public int $last_page; + public int $per_page; + public int $to; + public int $total; + + /** + * @param Collection $albums + * @param LengthAwarePaginator&Paginator $photos + * + * @return void + */ + public function __construct( + Collection $albums, + LengthAwarePaginator $photos, + ) { + $this->albums = $albums; + $this->photos = collect($photos->items()); + $this->current_page = $photos->currentPage(); + $this->from = $photos->firstItem() ?? 0; + $this->last_page = $photos->lastPage(); + $this->per_page = $photos->perPage(); + $this->to = $photos->lastItem() ?? 0; + $this->total = $photos->total(); + + $this->prepPhotosCollection(); + } + + /** + * @param Collection $albums + * @param LengthAwarePaginator $photos + * + * @return ResultsResource + */ + public static function fromData(Collection $albums, LengthAwarePaginator $photos): self + { + return new self( + albums: ThumbAlbumResource::collect($albums), + photos: PhotoResource::collect($photos), // @phpstan-ignore-line + ); + } +} \ No newline at end of file diff --git a/app/Http/Resources/Sharing/ListedAlbumsResource.php b/app/Http/Resources/Sharing/ListedAlbumsResource.php new file mode 100644 index 00000000000..e97d76b718f --- /dev/null +++ b/app/Http/Resources/Sharing/ListedAlbumsResource.php @@ -0,0 +1,28 @@ +id = $albumListed->id; + $this->title = $albumListed->title; + } +} diff --git a/app/Http/Resources/Sharing/SharedAlbumResource.php b/app/Http/Resources/Sharing/SharedAlbumResource.php new file mode 100644 index 00000000000..1f2024410b6 --- /dev/null +++ b/app/Http/Resources/Sharing/SharedAlbumResource.php @@ -0,0 +1,36 @@ +id = $albumShared->id; + $this->user_id = $albumShared->user_id; + $this->album_id = $albumShared->album_id; + $this->username = $albumShared->username; + $this->title = $albumShared->title; + } +} diff --git a/app/Http/Resources/Sharing/SharesResource.php b/app/Http/Resources/Sharing/SharesResource.php new file mode 100644 index 00000000000..04d6b2c5dd8 --- /dev/null +++ b/app/Http/Resources/Sharing/SharesResource.php @@ -0,0 +1,46 @@ + */ + #[LiteralTypeScriptType('App.Http.Resources.Sharing.SharedAlbumResource[]')] + public Collection $shared; + /** @var Collection */ + #[LiteralTypeScriptType('App.Http.Resources.Sharing.ListedAlbumsResource[]')] + public Collection $albums; + /** @var Collection */ + #[LiteralTypeScriptType('App.Http.Resources.Sharing.UserSharedResource[]')] + public Collection $users; + + /** + * @param Collection $shared + * @param Collection $albums + * @param Collection $users + * + * @return void + */ + public function __construct( + Collection $shared, + Collection $albums, + Collection $users) + { + $this->shared = $shared->map(fn ($s) => new SharedAlbumResource($s)); + $this->albums = $albums->map(fn ($a) => new ListedAlbumsResource($a)); + $this->users = $users->map(fn ($u) => new UserSharedResource($u)); + } +} diff --git a/app/Http/Resources/Sharing/UserSharedResource.php b/app/Http/Resources/Sharing/UserSharedResource.php new file mode 100644 index 00000000000..92a54818c08 --- /dev/null +++ b/app/Http/Resources/Sharing/UserSharedResource.php @@ -0,0 +1,26 @@ +id = $user->id; + $this->username = $user->username; + } +} diff --git a/app/Http/Resources/Statistics/Album.php b/app/Http/Resources/Statistics/Album.php new file mode 100644 index 00000000000..e3f0be227a8 --- /dev/null +++ b/app/Http/Resources/Statistics/Album.php @@ -0,0 +1,43 @@ +username = $count_data['username']; + $this->title = $count_data['title']; + $this->is_nsfw = $count_data['is_nsfw']; + $this->left = $count_data['left']; + $this->right = $count_data['right']; + $this->num_photos = $count_data['num_photos']; + $this->num_descendants = $count_data['num_descendants']; + $this->size = $space_data['size']; + } +} diff --git a/app/Http/Resources/Statistics/Sizes.php b/app/Http/Resources/Statistics/Sizes.php new file mode 100644 index 00000000000..cfec7285813 --- /dev/null +++ b/app/Http/Resources/Statistics/Sizes.php @@ -0,0 +1,43 @@ +type = $sizes['type']; + $this->label = $sizes['type']->localization(); + $this->size = $sizes['size']; + } + + /** + * @param array{type:SizeVariantType,size:int} $sizes + * + * @return Sizes + */ + public static function fromArray(array $sizes): self + { + return new self($sizes); + } +} diff --git a/app/Http/Resources/Statistics/UserSpace.php b/app/Http/Resources/Statistics/UserSpace.php new file mode 100644 index 00000000000..c9fa549509e --- /dev/null +++ b/app/Http/Resources/Statistics/UserSpace.php @@ -0,0 +1,42 @@ +id = $user_data['id']; + $this->username = $user_data['username']; + $this->size = $user_data['size']; + } + + /** + * @param array{id:int,username:string,size:int} $user_data + * + * @return UserSpace + */ + public static function fromArray(array $user_data): self + { + return new self($user_data); + } +} diff --git a/app/Http/Resources/Traits/HasHeaderUrl.php b/app/Http/Resources/Traits/HasHeaderUrl.php new file mode 100644 index 00000000000..ed640ad4c6b --- /dev/null +++ b/app/Http/Resources/Traits/HasHeaderUrl.php @@ -0,0 +1,78 @@ +header_id === AlbumController::COMPACT_HEADER) { + return null; + } + + if ($album->photos->isEmpty()) { + return null; + } + + // TODO : already use the prefetched data for photos instead of 2 extra queries? + + return $this->getByQuery($album); + } + + private function getByQuery(AbstractAlbum $album): ?string + { + $headerSizeVariant = null; + + if ($album instanceof Album && $album->header_id !== null) { + $headerSizeVariant = SizeVariant::query() + ->where('photo_id', '=', $album->header_id) + ->whereIn('type', [SizeVariantType::MEDIUM, SizeVariantType::SMALL2X, SizeVariantType::SMALL]) + ->orderBy('type', 'asc') + ->first(); + } + + if ($headerSizeVariant !== null) { + return $headerSizeVariant->url; + } + + $query_ratio = SizeVariant::query() + ->select('photo_id') + ->whereBelongsTo($album->photos) + ->where('ratio', '>', 1) // ! we prefer landscape first. + ->whereIn('type', [SizeVariantType::MEDIUM, SizeVariantType::SMALL2X, SizeVariantType::SMALL]); + $num = $query_ratio->count() - 1; + $photo = $query_ratio->skip(rand(0, $num))->first(); + + if ($photo === null) { + $query = SizeVariant::query() + ->select('photo_id') + ->whereBelongsTo($album->photos) + ->whereIn('type', [SizeVariantType::MEDIUM, SizeVariantType::SMALL2X, SizeVariantType::SMALL]); + $num = $query->count() - 1; + $photo = $query->skip(rand(0, $num))->first(); + } + + return $photo === null ? null : SizeVariant::query() + ->where('photo_id', '=', $photo->photo_id) + ->where('type', '>', 1) + ->orderBy('type', 'asc') + ->first()?->url; + } +} \ No newline at end of file diff --git a/app/Http/Resources/Traits/HasPrepPhotoCollection.php b/app/Http/Resources/Traits/HasPrepPhotoCollection.php new file mode 100644 index 00000000000..8ca3bef3a66 --- /dev/null +++ b/app/Http/Resources/Traits/HasPrepPhotoCollection.php @@ -0,0 +1,36 @@ + $photos + */ +trait HasPrepPhotoCollection +{ + private function prepPhotosCollection(): void + { + $previous_photo = null; + $this->photos->each(function (PhotoResource &$photo) use (&$previous_photo) { + if ($previous_photo !== null) { + $previous_photo->next_photo_id = $photo->id; + } + $photo->previous_photo_id = $previous_photo?->id; + $previous_photo = $photo; + }); + + if ($this->photos->count() > 1 && Configs::getValueAsBool('photos_wraparound')) { + $this->photos->first()->previous_photo_id = $this->photos->last()->id; + $this->photos->last()->next_photo_id = $this->photos->first()->id; + } + } +} \ No newline at end of file diff --git a/app/Http/Resources/Traits/HasTimelineData.php b/app/Http/Resources/Traits/HasTimelineData.php new file mode 100644 index 00000000000..025bab6e786 --- /dev/null +++ b/app/Http/Resources/Traits/HasTimelineData.php @@ -0,0 +1,38 @@ +> $files + * @param Collection $sizeVariants + * @param Collection $symbolicLinks + * + * @return void + */ + public function __construct( + protected array $files = [], + protected Collection $sizeVariants = new Collection(), + protected Collection $symbolicLinks = new Collection(), + ) { + } + + /** + * @param Collection $sizeVariants + * + * @return void + */ + public function addSizeVariants(Collection $sizeVariants): void + { + $this->sizeVariants = $this->sizeVariants->merge($sizeVariants); + } + + /** + * @param Collection $symbolicLinks + * + * @return void + */ + public function addSymbolicLinks(Collection $symbolicLinks): void + { + $this->symbolicLinks = $this->symbolicLinks->merge($symbolicLinks); + } + + /** + * Give the possility to add files with their associated storage to the deleter. + * + * @param Collection $paths + * @param string $diskName + * + * @return void + */ + public function addFiles(Collection $paths, string $diskName): void + { + $this->files[$diskName] = ($this->files[$diskName] ?? new Collection())->merge($paths); + } + + /** + * Map the list of sizeVariants to their proper storage type for later processing. + * + * @return void + */ + private function convertSizeVariantsList() + { + /** @var Collection> $grouped */ + $grouped = $this->sizeVariants->groupBy('storage_disk'); + $grouped->each( + fn (Collection $svs, string $k) => $this->files[$k] = ($this->files[$k] ?? new Collection())->merge($svs->pluck('short_path')) + ); + } + + /** + * Deletes the collected files. + * + * @return void + * + * @throws MediaFileOperationException + */ + public function do(): void + { + /** @var \Throwable|null $firstException */ + $firstException = null; + + $this->convertSizeVariantsList(); + + foreach ($this->files as $storageType => $fileList) { + $disk = Storage::disk($storageType); + + // If the disk uses the local driver, we use low-level routines as + // these are also able to handle symbolic links in case of doubt + $isLocalDisk = $disk->getAdapter() instanceof LocalFilesystemAdapter; + if ($isLocalDisk) { + foreach ($fileList as $file) { + try { + $absolutePath = $disk->path($file); + // Note, `file_exist` returns `false` for existing, + // but dead links. + // So the first part takes care of deleting links no matter + // if they are dead or alive. + // The latter part deletes (regular) files, but avoids errors + // in case the file doesn't exist. + if (is_link($absolutePath) || file_exists($absolutePath)) { + unlink($absolutePath); + } + } catch (\Throwable $e) { + $firstException = $firstException ?? $e; + } + } + } else { + // If the disk is not local, we can assume that each file is a regular file + foreach ($fileList as $file) { + try { + if ($disk->exists($file)) { + if (!$disk->delete($file)) { + $firstException = $firstException ?? new FileDeletionException($storageType, $file); + } + } + } catch (\Throwable $e) { + $firstException = $firstException ?? $e; + } + } + } + } + + // TODO: When we use proper `File` objects, each file knows its associated disk + // In the mean time, we assume that any symbolic link is stored on the same disk + $symlinkDisk = Storage::disk(SymLink::DISK_NAME); + foreach ($this->symbolicLinks as $symbolicLink) { + try { + $absolutePath = $symlinkDisk->path($symbolicLink); + // Laravel and Flysystem does not support symbolic links. + // So we must use low-level methods here. + if (is_link($absolutePath) || file_exists($absolutePath)) { + unlink($absolutePath); + } + } catch (\Throwable $e) { + $firstException = $firstException ?? $e; + } + } + + if ($firstException !== null) { + throw new MediaFileOperationException('Could not delete some files', $firstException); + } + } +} \ No newline at end of file diff --git a/app/Image/Files/AbstractBinaryBlob.php b/app/Image/Files/AbstractBinaryBlob.php new file mode 100644 index 00000000000..e61da3628de --- /dev/null +++ b/app/Image/Files/AbstractBinaryBlob.php @@ -0,0 +1,127 @@ +write($sourceBlob->read()) + * + * using streams. + * This API is inspired by Flysystem. + */ +abstract class AbstractBinaryBlob implements BinaryBlob +{ + /** @var ?resource */ + protected $stream; + + /** + * @throws MediaFileOperationException + */ + public function __destruct() + { + $this->close(); + } + + public function __clone() + { + // The stream belongs to the original object, it is not ours. + $this->stream = null; + } + + /** + * Returns a stream from which can be read. + * + * To free the stream after use, call {@link BinaryBlob::close()}. + * Calling `read` multiple times is safe. + * The read pointer of the stream will be reset to the beginning of + * the stream, without closing the stream in between. + * + * @return resource + * + * @throws MediaFileOperationException + */ + abstract public function read(); + + /** + * Writes the content of the provided stream into the blob. + * + * @param resource $stream the input stream which provides the input to write + * @param bool $collectStatistics if true, the method returns statistics about the stream + * + * @return ?StreamStats optional statistics about the stream, if requested + * + * @throws MediaFileOperationException + */ + abstract public function write($stream, bool $collectStatistics = false): ?StreamStats; + + /** + * Closes the internal stream/buffer. + * + * The associated buffer is implicitly freed when this object becomes + * unreachable and is garbage-collected. + * Calling this function frees the memory explicitly. + * Note, the content of the freed buffer is lost (unless saved somewhere + * otherwise). + * It is safe to call {@link BinaryBlob::read()} and + * {@link BinaryBlob::write()} again after this method. + * A new buffer will be created, if needed. + * + * @return void + * + * @throws MediaFileOperationException + */ + public function close(): void + { + try { + if (is_resource($this->stream)) { + \Safe\fclose($this->stream); + $this->stream = null; + } + } catch (\ErrorException $e) { + throw new MediaFileOperationException($e->getMessage(), $e); + } + } + + /** + * Appends {@link StreamStatFilter} to the read-direction of the provided stream. + * + * @param resource $stream the stream whose statistic shall be collected + * + * @return StreamStat the stream statistics + */ + protected static function appendStatFilter($stream): StreamStat + { + $streamStat = new StreamStat(); + stream_filter_append( + $stream, + StreamStatFilter::REGISTERED_NAME, + STREAM_FILTER_READ, + $streamStat + ); + + return $streamStat; + } +} diff --git a/app/Image/Files/BaseMediaFile.php b/app/Image/Files/BaseMediaFile.php new file mode 100644 index 00000000000..cef53adfe7b --- /dev/null +++ b/app/Image/Files/BaseMediaFile.php @@ -0,0 +1,379 @@ + '.gif', + 'image/jpeg' => '.jpg', + 'image/png' => '.png', + 'image/webp' => '.webp', + 'video/mp4' => '.mp4', + 'video/mpeg' => '.mpg', + 'image/x-tga' => '.mpg', + 'video/ogg' => '.ogv', + 'video/webm' => '.webm', + 'video/quicktime' => '.mov', + 'video/x-ms-asf' => '.wmv', + 'video/x-ms-wmv' => '.wmv', + 'video/x-msvideo' => '.avi', + 'video/x-m4v' => '.avi', + 'application/octet-stream' => '.mp4', + ]; + + /** @var string[] the accepted raw file extensions minus supported extensions */ + private static array $cachedAcceptedRawFileExtensions = []; + + /** + * Writes the content of the provided stream into the file. + * + * Any previous content of the file is overwritten. + * The new content is buffered in memory and may not be synced to disk + * until {@link MediaFile::close()} is called. + * If you want to be sure, that the content is really written to disk, + * explicitly call {@link MediaFile::close()}. + * The freshly written content can immediately be read back via + * {@link MediaFile::read} without closing the file in between. + * + * @param resource $stream the input stream which provides the input to write + * @param bool $collectStatistics if true, the method returns statistics about the stream + * + * @return ?StreamStats optional statistics about the stream, if requested + * + * @throws MediaFileOperationException + */ + abstract public function write($stream, bool $collectStatistics = false): ?StreamStats; + + /** + * Deletes the file. + * + * In case the file does not exist, the method is a silent no-op. + * + * @return void + * + * @throws MediaFileOperationException + */ + abstract public function delete(): void; + + /** + * Moves the file to the new location efficiently. + * + * Basically the file is renamed; however, this kind of "renaming" also + * may change the path of the file. + * Note, that the path is interpreted relative to the "mount" point of + * the underlying filesystem implementation. + * + * @param string $newPath + * + * @return void + * + * @throws MediaFileOperationException + */ + abstract public function move(string $newPath): void; + + /** Checks if the file exists. + * + * @return bool true, if the file exists + */ + abstract public function exists(): bool; + + /** + * Returns the time of last modification as UNIX timestamp. + * + * @return int the time of last modification since epoch + * + * @throws MediaFileOperationException + */ + abstract public function lastModified(): int; + + /** + * Returns the size of the file in bytes. + * + * @return int the file size in bytes + * + * @throws MediaFileOperationException + */ + abstract public function getFilesize(): int; + + /** + * Returns the extension of the file incl. a preceding dot. + * + * @return string + */ + abstract public function getExtension(): string; + + /** + * Returns the original extension of the file incl. the preceding dot. + * + * Normally, the original extension equals the extension. + * However, for temporary copies of downloaded or uploaded files the + * original extension is the extension as used by the source while the + * actual, physical extension is typically random. + * + * @return string + */ + public function getOriginalExtension(): string + { + return $this->getExtension(); + } + + /** + * Returns the basename of the file. + * + * The basename of a file is the name of the file without any + * preceding path and without a file extension. + * For example, the basename of the file `/path/to/my-image.jpg` is + * `my-image`. + * Note, this terminology conflicts how the term "basename" is used in + * the PHP documentation. + * + * @return string + */ + abstract public function getBasename(): string; + + /** + * Returns the original basename of the file. + * + * Normally, the original basename equals the basename. + * However, for temporary copies of downloaded or uploaded files the + * original basename is the basename as used by the source while the + * actual, physical basename is typically random. + * + * @return string + */ + public function getOriginalBasename(): string + { + return $this->getBasename(); + } + + /** + * Checks if the given MIME type designates a supported image type. + * + * @param string $mimeType the MIME type + * + * @return bool + */ + public static function isSupportedImageMimeType(string $mimeType): bool + { + return in_array($mimeType, self::SUPPORTED_IMAGE_MIME_TYPES, true); + } + + /** + * Checks if the given MIME type designates a supported video type. + * + * @param string $mimeType the MIME type + * + * @return bool + */ + public static function isSupportedVideoMimeType(string $mimeType): bool + { + return in_array($mimeType, self::SUPPORTED_VIDEO_MIME_TYPES, true); + } + + /** + * Checks if the given file extension is a supported image extension. + * + * @param string $extension the file extension + * + * @return bool + */ + public static function isSupportedImageFileExtension(string $extension): bool + { + return in_array(strtolower($extension), self::SUPPORTED_IMAGE_FILE_EXTENSIONS, true); + } + + /** + * Checks if the given file extension is a supported image extension. + * + * @param string $extension the file extension + * + * @return bool + */ + public static function isSupportedVideoFileExtension(string $extension): bool + { + return in_array(strtolower($extension), self::SUPPORTED_VIDEO_FILE_EXTENSIONS, true); + } + + /** + * Checks if the given file extension is supported. + * + * @param string $extension the file extension + * + * @return bool + */ + public static function isSupportedFileExtension(string $extension): bool + { + return + self::isSupportedImageFileExtension($extension) || + self::isSupportedVideoFileExtension($extension); + } + + /** + * Returns {@link MediaFile::$cachedAcceptedRawFileExtensions} and creates it, if necessary. + * + * @return string[] + */ + protected static function getSanitizedAcceptedRawFileExtensions(): array + { + if (count(self::$cachedAcceptedRawFileExtensions) === 0) { + $tmp = explode('|', strtolower(Configs::getValueAsString('raw_formats'))); + // Explode may return `false` on error + // Our supported file extensions always take precedence over any + // custom configured extension + self::$cachedAcceptedRawFileExtensions = array_diff($tmp, self::SUPPORTED_IMAGE_FILE_EXTENSIONS, self::SUPPORTED_VIDEO_FILE_EXTENSIONS); + } + + return self::$cachedAcceptedRawFileExtensions; + } + + /** + * Checks if the given extension is accepted as raw. + * + * @param string $extension the file extension + * + * @return bool + */ + public static function isAcceptedRawFileExtension(string $extension): bool + { + return in_array( + strtolower($extension), + self::getSanitizedAcceptedRawFileExtensions(), + true + ); + } + + /** + * Check if the given extension is supported or accepted. + * + * @param string $extension the file extension + * + * @return bool + */ + public static function isSupportedOrAcceptedFileExtension(string $extension): bool + { + return + self::isSupportedFileExtension($extension) || + self::isAcceptedRawFileExtension($extension); + } + + /** + * Asserts that the given extension is supported or accepted. + * + * @param string $extension the file extension + * + * @return void + * + * @throws MediaFileUnsupportedException + */ + public static function assertIsSupportedOrAcceptedFileExtension(string $extension): void + { + if (!self::isSupportedOrAcceptedFileExtension($extension)) { + throw new MediaFileUnsupportedException(MediaFileUnsupportedException::DEFAULT_MESSAGE . ' (bad extension: ' . $extension . ')'); + } + } + + /** + * Check if the given mimetype is supported or accepted. + * + * @param ?string $mimeType the file mimetype + * + * @return bool + */ + public static function isSupportedMimeType(?string $mimeType): bool + { + return + self::isSupportedImageMimeType($mimeType) || + self::isSupportedVideoMimeType($mimeType); + } + + /** + * Returns the default file extension for the given MIME type or an empty string if there is no default extension. + * + * @param string $mimeType a MIME type + * + * @return string the default file extension for the given MIME type + */ + public static function getDefaultFileExtensionForMimeType(string $mimeType): string + { + return self::MIME_TYPES_TO_FILE_EXTENSIONS[strtolower($mimeType)] ?? ''; + } +} diff --git a/app/Image/Files/DownloadedFile.php b/app/Image/Files/DownloadedFile.php new file mode 100644 index 00000000000..3d8719c44d6 --- /dev/null +++ b/app/Image/Files/DownloadedFile.php @@ -0,0 +1,137 @@ + Content-Type > Inferred MIME type + + if (self::isSupportedOrAcceptedFileExtension($extension)) { + parent::__construct($extension, $basename); + $this->originalMimeType = $originalMimeType; + $this->write($downloadStream); + fclose($downloadStream); + + return; + } + + if (self::isSupportedMimeType($originalMimeType)) { + $extension = self::getDefaultFileExtensionForMimeType($originalMimeType); + parent::__construct($extension, $basename); + $this->originalMimeType = $originalMimeType; + $this->write($downloadStream); + fclose($downloadStream); + + return; + } + + $temp = tmpfile(); + stream_copy_to_stream($downloadStream, $temp); + fclose($downloadStream); + + rewind($temp); + $originalMimeType = mime_content_type($temp); + + if (self::isSupportedMimeType($originalMimeType)) { + $extension = self::getDefaultFileExtensionForMimeType($originalMimeType); + parent::__construct($extension, $basename); + $this->originalMimeType = $originalMimeType; + rewind($temp); + $this->write($temp); + fclose($temp); + + return; + } + + fclose($temp); + throw new MediaFileUnsupportedException(MediaFileUnsupportedException::DEFAULT_MESSAGE . ' (bad file type: ' . $originalMimeType . ')'); + } catch (\ErrorException|PcreException $e) { + throw new MediaFileOperationException($e->getMessage(), $e); + } + } + + /** + * Returns the MIME type of the file. + * + * @param bool $fallbackToClientMimeType flag to use the provided MIME + * type by client-side, if the + * internal PHP mechanism detects + * "application/octet-stream" + * + * @return string the MIME type + * + * @throws MediaFileOperationException + */ + public function getMimeType(bool $fallbackToClientMimeType = true): string + { + parent::getMimeType(); + if ($this->cachedMimeType === 'application/octet-stream' && $fallbackToClientMimeType) { + return $this->originalMimeType; + } else { + return $this->cachedMimeType; + } + } +} diff --git a/app/Image/Files/FlysystemFile.php b/app/Image/Files/FlysystemFile.php new file mode 100644 index 00000000000..89858fc96d3 --- /dev/null +++ b/app/Image/Files/FlysystemFile.php @@ -0,0 +1,201 @@ +disk = $disk; + $this->relativePath = $relativePath; + } + + /** + * {@inheritDoc} + */ + public function read() + { + try { + if (is_resource($this->stream)) { + fclose($this->stream); + } + + $this->stream = $this->disk->readStream($this->relativePath); + if (!is_resource($this->stream)) { + $this->stream = null; + throw new FlySystemLycheeException('Filesystem::readStream failed'); + } + } catch (\ErrorException|FilesystemException|FileNotFoundException $e) { + throw new MediaFileOperationException($e->getMessage(), $e); + } + + return $this->stream; + } + + /** + * {@inheritDoc} + */ + public function write($stream, bool $collectStatistics = false): ?StreamStat + { + try { + $streamStat = $collectStatistics ? static::appendStatFilter($stream) : null; + + // The underlying Flysystem currently behaves inconsistent + // with respect to whether it honors the umask value or not. + // Hence, we explicitly set umask to zero to achieve consistent + // behaviour. + // Setting umask can be removed, after + // https://github.com/thephpleague/flysystem/issues/1584 + // has been solved. + // Also consider the warning in + // https://www.php.net/manual/en/function.umask.php + // regarding timing issues: + // "Avoid using this function [...]. It is better to change the + // file permissions with chmod() after creating the file. Using + // umask() can lead to unexpected behavior of concurrently running + // scripts and the webserver itself because they all use the same + // umask." + // However, Lychee cannot use `chmod`, because Flysystem may + // also recursively create missing parent directories and/or + // not be local. + // This problem must be fixed on the library layer. + $umask = \umask(0); + if (!$this->disk->writeStream($this->relativePath, $stream)) { + throw new FlySystemLycheeException('Filesystem::writeStream failed'); + } + \umask($umask); + + return $streamStat; + } catch (\ErrorException|FilesystemException $e) { + throw new MediaFileOperationException($e->getMessage(), $e); + } + } + + /** + * {@inheritDoc} + */ + public function delete(): void + { + try { + if (!$this->disk->delete($this->relativePath)) { + throw new FlySystemLycheeException('Filesystem::delete failed'); + } + } catch (\ErrorException|FilesystemException $e) { + throw new MediaFileOperationException($e->getMessage(), $e); + } + } + + /** + * {@inheritDoc} + */ + public function move(string $newPath): void + { + if (!$this->disk->move($this->relativePath, $newPath)) { + throw new MediaFileOperationException('could not move file'); + } + $this->relativePath = $newPath; + } + + /** + * {@inheritDoc} + */ + public function exists(): bool + { + return $this->disk->exists($this->relativePath); + } + + /** + * {@inheritDoc} + */ + public function lastModified(): int + { + return $this->disk->lastModified($this->relativePath); + } + + /** + * {@inheritDoc} + */ + public function getFilesize(): int + { + return $this->disk->size($this->relativePath); + } + + /** + * Returns the relative path of the file wrt. the underlying Flysystem disk. + * + * @return string the relative path + */ + public function getRelativePath(): string + { + return $this->relativePath; + } + + /** + * @return FilesystemAdapter the disk this file is stored on + */ + public function getDisk(): FilesystemAdapter + { + return $this->disk; + } + + /** + * {@inheritDoc} + */ + public function getExtension(): string + { + $ext = pathinfo($this->relativePath, PATHINFO_EXTENSION); + + return $ext !== '' ? '.' . $ext : ''; + } + + /** + * {@inheritDoc} + */ + public function getBasename(): string + { + return pathinfo($this->relativePath, PATHINFO_FILENAME); + } + + /** + * Determines if this file is a local file. + * + * @return bool + */ + public function isLocalFile(): bool + { + return $this->disk->getAdapter() instanceof LocalFilesystemAdapter; + } + + /** + * @throws MediaFileOperationException + */ + public function toLocalFile(): NativeLocalFile + { + if (!$this->isLocalFile()) { + throw new MediaFileOperationException('file is not hosted locally'); + } + + return new NativeLocalFile($this->disk->path($this->relativePath)); + } +} diff --git a/app/Image/Files/InMemoryBuffer.php b/app/Image/Files/InMemoryBuffer.php new file mode 100644 index 00000000000..ceebe932119 --- /dev/null +++ b/app/Image/Files/InMemoryBuffer.php @@ -0,0 +1,101 @@ +stream(); + \Safe\rewind($this->stream); + + return $this->stream; + } catch (\ErrorException $e) { + throw new MediaFileOperationException($e->getMessage(), $e); + } + } + + /** + * Writes the content of the provided stream into the buffer. + * + * Any previous content is overwritten. + * The freshly written content can immediately be read back via + * {@link MediaFile::read}. + * + * @param resource $stream the input stream to copy from + * @param bool $collectStatistics if true, the method returns statistics about the stream + * + * @return ?StreamStat optional statistics about the stream, if requested + * + * @throws MediaFileOperationException + */ + public function write($stream, bool $collectStatistics = false): ?StreamStat + { + try { + $streamStat = $collectStatistics ? static::appendStatFilter($stream) : null; + + $this->stream(); + \Safe\ftruncate($this->stream, 0); + \Safe\rewind($this->stream); + \Safe\stream_copy_to_stream($stream, $this->stream); + + return $streamStat; + } catch (\ErrorException $e) { + throw new MediaFileOperationException($e->getMessage(), $e); + } + } + + /** + * Returns a stream from which can be read/written/seeked. + * + * Calling `stream` multiple times is safe. + * As long as a stream is opened, it will always return the same + * stream and not modify the position of the read/write pointer. + * If no stream is opened, a new buffer will be created. + * + * @return resource a readable stream + * + * @throws MediaFileOperationException + */ + public function stream() + { + try { + if (!is_resource($this->stream)) { + $this->stream = \Safe\fopen('php://temp/maxmemory:' . self::MAX_SIZE, 'r+b'); + } + + return $this->stream; + } catch (\ErrorException $e) { + throw new MediaFileOperationException($e->getMessage(), $e); + } + } +} diff --git a/app/Image/Files/LoadTemporaryFileTrait.php b/app/Image/Files/LoadTemporaryFileTrait.php new file mode 100644 index 00000000000..ebdf9e0f1ba --- /dev/null +++ b/app/Image/Files/LoadTemporaryFileTrait.php @@ -0,0 +1,60 @@ +getFileBasePath() . + DIRECTORY_SEPARATOR . + strtr(base64_encode(random_bytes(12)), '+/', '-_') . + $fileExtension; + $this->stream = fopen($tempFilePath, 'x+b'); + } catch (\ErrorException|\Exception $e) { + $tempFilePath = null; + $lastException = $e; + } + } while ($tempFilePath === null && $retryCounter > 0); + if ($tempFilePath === null) { + throw new MediaFileOperationException('unable to create temporary file', $lastException); + } + + return $tempFilePath; + } +} diff --git a/app/Image/Files/NativeLocalFile.php b/app/Image/Files/NativeLocalFile.php new file mode 100644 index 00000000000..d77026cbe41 --- /dev/null +++ b/app/Image/Files/NativeLocalFile.php @@ -0,0 +1,382 @@ +path = $path; + $this->cachedMimeType = null; + } + + /** + * {@inheritDoc} + */ + public function read() + { + try { + if (is_resource($this->stream)) { + rewind($this->stream); + } else { + $this->stream = fopen($this->getPath(), 'rb'); + } + + return $this->stream; + } catch (\ErrorException $e) { + throw new MediaFileOperationException($e->getMessage(), $e); + } + } + + /** + * {@inheritDoc} + * + * If new content is written to the file, the internally cached mime + * type is cleared. + * The mime type will be re-determined again upon the next invocation of + * {@link NativeLocalFile::getMimeType()}. + * This can be avoided by passing the MIME type of the stream. + * + * @param string|null $mimeType the mime type of `$stream` + * + * @returns void + */ + public function write($stream, bool $collectStatistics = false, ?string $mimeType = null): ?StreamStat + { + try { + $streamStat = $collectStatistics ? static::appendStatFilter($stream) : null; + + if (is_resource($this->stream)) { + ftruncate($this->stream, 0); + rewind($this->stream); + } else { + $this->stream = fopen($this->getPath(), 'w+b'); + } + $this->cachedMimeType = null; + stream_copy_to_stream($stream, $this->stream); + $this->cachedMimeType = $mimeType; + // File statistics info (filesize, access mode, etc.) are cached + // by PHP to avoid costly I/O calls. + // If cache is not cleared, an old size may be reported after + // write. + clearstatcache(true, $this->getPath()); + + return $streamStat; + } catch (\ErrorException $e) { + throw new MediaFileOperationException($e->getMessage(), $e); + } + } + + /** + * If new content is written to the file, the internally cached mime + * type is cleared. + * The mime type will be re-determined again upon the next invocation of + * {@link NativeLocalFile::getMimeType()}. + * This can be avoided by passing the MIME type of the stream. + * + * @param resource $stream the input stream which provides the input to write + * @param string|null $mimeType the mime type of `$stream` + * + * @returns void + */ + public function append($stream, bool $collectStatistics = false, ?string $mimeType = null): ?StreamStat + { + try { + $streamStat = $collectStatistics ? static::appendStatFilter($stream) : null; + + if (!is_resource($this->stream)) { + $this->stream = fopen($this->getPath(), 'a+b'); + } + $this->cachedMimeType = null; + stream_copy_to_stream($stream, $this->stream); + $this->cachedMimeType = $mimeType; + // File statistics info (filesize, access mode, etc.) are cached + // by PHP to avoid costly I/O calls. + // If cache is not cleared, an old size may be reported after + // write. + clearstatcache(true, $this->getPath()); + + return $streamStat; + } catch (\ErrorException $e) { + throw new MediaFileOperationException($e->getMessage(), $e); + } + } + + /** + * {@inheritDoc} + */ + public function delete(): void + { + try { + // Close stream before deletion in case any stream is opened + $this->close(); + // `is_file` returns false for links, so we must check separately with `is_link` + if (is_link($this->path) || is_file($this->path)) { + unlink($this->path); + } + } catch (\ErrorException $e) { + throw new MediaFileOperationException($e->getMessage(), $e); + } + } + + /** + * {@inheritDoc} + */ + public function move(string $newPath): void + { + try { + rename($this->path, $newPath); + $this->path = $newPath; + } catch (\ErrorException $e) { + throw new MediaFileOperationException($e->getMessage(), $e); + } + } + + /** + * {@inheritDoc} + * + * If the represented file is a symbolic link, then the method only + * returns true, if the link (as a file) exists and the target of the + * link exists, too. + */ + public function exists(): bool + { + try { + return is_file(realpath($this->path)); + } catch (\ErrorException) { + return false; + } + } + + /** + * {@inheritDoc} + */ + public function lastModified(): int + { + try { + return filemtime($this->getPath()); + } catch (\ErrorException $e) { + throw new MediaFileOperationException($e->getMessage(), $e); + } + } + + /** + * {@inheritDoc} + */ + public function getFilesize(): int + { + try { + return filesize($this->getPath()); + } catch (\ErrorException $e) { + throw new MediaFileOperationException($e->getMessage(), $e); + } + } + + /** + * Returns the path of the file. + * + * @return string + */ + public function getPath(): string + { + return $this->path; + } + + /** + * Returns the real path of the file after all symbolic links and + * relative path components such as `'..'` have been resolved. + * + * Throws an exception, if the file does not exist. + * + * @return string + * + * @throws MediaFileOperationException + */ + public function getRealPath(): string + { + try { + return realpath($this->path); + } catch (\ErrorException $e) { + throw new MediaFileOperationException($e->getMessage(), $e); + } + } + + /** + * {@inheritDoc} + */ + public function getExtension(): string + { + $ext = pathinfo($this->path, PATHINFO_EXTENSION); + + return $ext !== '' ? '.' . $ext : ''; + } + + /** + * {@inheritDoc} + */ + public function getBasename(): string + { + return pathinfo($this->path, PATHINFO_FILENAME); + } + + /** + * Returns the MIME type of the file. + * + * @return string the MIME type + * + * @throws MediaFileOperationException + */ + public function getMimeType(): string + { + try { + if ($this->cachedMimeType === null) { + $this->cachedMimeType = mime_content_type($this->getPath()); + } + + return $this->cachedMimeType; + } catch (\ErrorException $e) { + throw new MediaFileOperationException($e->getMessage(), $e); + } + } + + /** + * Checks if the file is a valid image type acc. to {@link MediaFile::SUPPORTED_PHP_EXIF_IMAGE_TYPES}. + * + * @return bool true, if the file has a valid EXIF type + */ + protected function hasSupportedExifImageType(): bool + { + try { + return in_array(exif_imagetype($this->getPath()), self::SUPPORTED_PHP_EXIF_IMAGE_TYPES, true); + } catch (\ErrorException|MediaFileOperationException) { + // `exif_imagetype` emit an engine error E_NOTICE, if it is unable + // to read enough bytes from the file to determine the image type. + // This may happen for short "raw" files. + return false; + } + } + + /** + * Checks if the file is a supported image. + * + * @return bool + * + * @throws MediaFileOperationException + */ + public function isSupportedImage(): bool + { + $mime = $this->getMimeType(); + $ext = $this->getOriginalExtension(); + + return + self::isSupportedImageMimeType($mime) && + self::isSupportedImageFileExtension($ext) && + $this->hasSupportedExifImageType(); + } + + /** + * Checks if the file is a supported video. + * + * @return bool + * + * @throws MediaFileOperationException + */ + public function isSupportedVideo(): bool + { + $mime = $this->getMimeType(); + $ext = $this->getOriginalExtension(); + + return + self::isSupportedVideoMimeType($mime) && + self::isSupportedVideoFileExtension($ext); + } + + /** + * Checks if the file is supported (image or video). + * + * @return bool true, if the file is supported + * + * @throws MediaFileOperationException + */ + public function isSupported(): bool + { + return + $this->isSupportedImage() || + $this->isSupportedVideo(); + } + + /** + * Checks if the file is not supported, but an accepted raw media. + * + * @return bool + */ + public function isAcceptedRaw(): bool + { + return in_array( + strtolower($this->getOriginalExtension()), + self::getSanitizedAcceptedRawFileExtensions(), + true + ); + } + + /** + * Checks if the file is supported or accepted (i.e. image, video or raw). + * + * @return bool true, if the file is supported or accepted + * + * @throws MediaFileOperationException + */ + public function isSupportedMediaOrAcceptedRaw(): bool + { + return $this->isSupported() || $this->isAcceptedRaw(); + } + + /** + * Asserts that the file is supported or accepted (i.e. image, video or raw). + * + * @return void + * + * @throws MediaFileUnsupportedException + * @throws MediaFileOperationException + */ + public function assertIsSupportedMediaOrAcceptedRaw(): void + { + if (!$this->isSupportedMediaOrAcceptedRaw()) { + throw new MediaFileUnsupportedException(); + } + } +} diff --git a/app/Image/Files/ProcessableJobFile.php b/app/Image/Files/ProcessableJobFile.php new file mode 100644 index 00000000000..5cce1d2dc93 --- /dev/null +++ b/app/Image/Files/ProcessableJobFile.php @@ -0,0 +1,66 @@ +load($fileExtension); + parent::__construct($tempFilePath); + $this->fakeBaseName = $fakeBaseName; + } + + /** + * {@inheritDoc} + */ + protected function getFileBasePath(): string + { + $tempDirPath = Storage::disk(self::DISK_NAME)->path(''); + + if (!file_exists($tempDirPath)) { + mkdir($tempDirPath); + } + + return $tempDirPath; + } + + /** + * {@inheritDoc} + */ + public function getOriginalBasename(): string + { + return $this->fakeBaseName; + } +} diff --git a/app/Image/Files/TemporaryJobFile.php b/app/Image/Files/TemporaryJobFile.php new file mode 100644 index 00000000000..807ded8457c --- /dev/null +++ b/app/Image/Files/TemporaryJobFile.php @@ -0,0 +1,72 @@ +delete(); + parent::__destruct(); + } + + /** + * Load a temporary file with a previously generated file name. + * + * @param string $filePath the path of a Processable Job file + * @param string $fakeBaseName the fake base name of the file; e.g. the original name prior to up-/download + * + * @throws MediaFileOperationException + */ + public function __construct(string $filePath, string $fakeBaseName = '') + { + $lastException = null; + $retryCounter = 5; + do { + try { + $tempFilePath = $filePath; + $retryCounter--; + // We open wih c+b because the file already exists (from ProcessableJobFile) + $this->stream = fopen($tempFilePath, 'c+b'); + } catch (\ErrorException|\Exception $e) { + $tempFilePath = null; + $lastException = $e; + } + } while ($tempFilePath === null && $retryCounter > 0); + if ($tempFilePath === null) { + throw new MediaFileOperationException('unable to create temporary file', $lastException); + } + parent::__construct($tempFilePath); + $this->fakeBaseName = $fakeBaseName; + } + + /** + * {@inheritDoc} + */ + public function getOriginalBasename(): string + { + return $this->fakeBaseName; + } +} diff --git a/app/Image/Files/TemporaryLocalFile.php b/app/Image/Files/TemporaryLocalFile.php new file mode 100644 index 00000000000..7afbcc70475 --- /dev/null +++ b/app/Image/Files/TemporaryLocalFile.php @@ -0,0 +1,64 @@ +delete(); + parent::__destruct(); + } + + /** + * Creates a new temporary file with a random file name. + * + * @param string $fileExtension the file extension of the new temporary file incl. a preceding dot + * @param string $fakeBaseName the fake base name of the file; e.g. the original name prior to up-/download + * + * @throws MediaFileOperationException + */ + public function __construct(string $fileExtension, string $fakeBaseName = '') + { + $tempFilePath = $this->load($fileExtension); + parent::__construct($tempFilePath); + $this->fakeBaseName = $fakeBaseName; + } + + /** + * {@inheritDoc} + */ + protected function getFileBasePath(): string + { + return sys_get_temp_dir(); + } + + /** + * {@inheritDoc} + */ + public function getOriginalBasename(): string + { + return $this->fakeBaseName; + } +} diff --git a/app/Image/Files/UploadedFile.php b/app/Image/Files/UploadedFile.php new file mode 100644 index 00000000000..2c2ca74a5a3 --- /dev/null +++ b/app/Image/Files/UploadedFile.php @@ -0,0 +1,75 @@ +baseFile = $file; + $path = $file->getRealPath(); + if ($path === false) { + throw new MediaFileOperationException('The uploaded file does not exist'); + } + + parent::__construct($path); + } + + /** + * {@inheritDoc} + */ + public function getOriginalExtension(): string + { + return '.' . pathinfo($this->baseFile->getClientOriginalName(), PATHINFO_EXTENSION); + } + + /** + * {@inheritDoc} + */ + public function getOriginalBasename(): string + { + return pathinfo($this->baseFile->getClientOriginalName(), PATHINFO_FILENAME); + } + + /** + * Returns the MIME type of the file. + * + * @param bool $fallbackToClientMimeType flag to use the provided MIME + * type by client-side, if the + * internal PHP mechanism detects + * "application/octet-stream" + * + * @return string the MIME type + * + * @throws MediaFileOperationException + */ + public function getMimeType(bool $fallbackToClientMimeType = true): string + { + parent::getMimeType(); + if ($this->cachedMimeType === 'application/octet-stream' && $fallbackToClientMimeType) { + return $this->baseFile->getClientMimeType(); + } + + return $this->cachedMimeType; + } +} diff --git a/app/Image/GdHandler.php b/app/Image/GdHandler.php deleted file mode 100644 index 1b8693ba73b..00000000000 --- a/app/Image/GdHandler.php +++ /dev/null @@ -1,348 +0,0 @@ - imagesx($image), 'height' => imagesy($image)]; - } - - /** - * {@inheritdoc} - */ - public function __construct(int $compressionQuality) - { - $this->compressionQuality = $compressionQuality; - } - - /** - * {@inheritdoc} - */ - public function scale( - string $source, - string $destination, - int $newWidth, - int $newHeight, - int &$resWidth, - int &$resHeight - ): bool { - $res = $this->readImage($source); - if ($res === false) { - return false; - } - list($sourceImg, $mime, $width, $height) = $res; - - if ($newWidth == 0) { - $newWidth = $newHeight * ($width / $height); - } else { - $tmpHeight = $newWidth / ($width / $height); - if ($newHeight != 0 && $tmpHeight > $newHeight) { - $newWidth = $newHeight * ($width / $height); - } else { - $newHeight = $tmpHeight; - } - } - - $image = imagecreatetruecolor($newWidth, $newHeight); - - imagecopyresampled($image, $sourceImg, 0, 0, 0, 0, $newWidth, $newHeight, $width, $height); - - if ($this->writeImage($destination, $image, $mime) === false) { - return false; - } - - imagedestroy($image); - imagedestroy($sourceImg); - - $resWidth = $newWidth; - $resHeight = $newHeight; - - // Optimize image - if (Configs::get_value('lossless_optimization', '0') == '1') { - ImageOptimizer::optimize($destination); - } - - return true; - } - - /** - * {@inheritdoc} - */ - public function crop( - string $source, - string $destination, - int $newWidth, - int $newHeight - ): bool { - $res = $this->readImage($source); - if ($res === false) { - return false; - } - list($sourceImg, , $width, $height) = $res; - - if ($width < $height) { - $newSize = $width; - $startWidth = 0; - $startHeight = $height / 2 - $width / 2; - } else { - $newSize = $height; - $startWidth = $width / 2 - $height / 2; - $startHeight = 0; - } - - $image = imagecreatetruecolor($newWidth, $newHeight); - - $this->fastImageCopyResampled($image, $sourceImg, 0, 0, $startWidth, $startHeight, $newWidth, $newHeight, $newSize, $newSize); - - if (imagejpeg($image, $destination, $this->compressionQuality) === false) { - return false; - } - - imagedestroy($image); - imagedestroy($sourceImg); - - // Optimize image - if (Configs::get_value('lossless_optimization', '0') == '1') { - ImageOptimizer::optimize($destination); - } - - return true; - } - - /** - * {@inheritdoc} - */ - public function autoRotate(string $path, array $info, bool $pretend = false): array - { - $image = imagecreatefromjpeg($path); - - $orientation = isset($info['orientation']) && $info['orientation'] !== '' ? $info['orientation'] : 1; - $rotate = $orientation !== 1; - - $dimensions = $this->autoRotateInternal($image, $orientation); - - if ($rotate && !$pretend) { - imagejpeg($image, $path, 100); - } - - imagedestroy($image); - - return $dimensions; - } - - /** - * {@inheritdoc} - */ - public function rotate(string $source, int $angle, string $destination = null): bool - { - $res = $this->readImage($source); - if ($res === false) { - return false; - } - list($image, $mime) = $res; - - $image = imagerotate($image, -$angle, 0); - if ($image === false) { - return false; - } - - $ret = $this->writeImage($destination ?? $source, $image, $mime, 100); - - imagedestroy($image); - - return $ret; - } - - /** - * Plug-and-Play fastImageCopyResampled function replaces much slower imagecopyresampled. - * Just include this function and change all "imagecopyresampled" references to "fastImageCopyResampled". - * Typically from 30 to 60 times faster when reducing high resolution images down to thumbnail size using the default quality setting. - * Author: Tim Eckel - Date: 09/07/07 - Version: 1.1 - Project: FreeRingers.net - Freely distributable - These comments must remain. - * - * Optional "quality" parameter (defaults is 3). Fractional values are allowed, for example 1.5. Must be greater than zero. - * Between 0 and 1 = Fast, but mosaic results, closer to 0 increases the mosaic effect. - * 1 = Up to 350 times faster. Poor results, looks very similar to imagecopyresized. - * 2 = Up to 95 times faster. Images appear a little sharp, some prefer this over a quality of 3. - * 3 = Up to 60 times faster. Will give high quality smooth results very close to imagecopyresampled, just faster. - * 4 = Up to 25 times faster. Almost identical to imagecopyresampled for most images. - * 5 = No speedup. Just uses imagecopyresampled, no advantage over imagecopyresampled. - * - * @param resource &$dst_image - * @param resource $src_image - * @param int $dst_x - * @param int $dst_y - * @param int $src_x - * @param int $src_y - * @param int $dst_w - * @param int $dst_h - * @param int $src_w - * @param int $src_h - * @param int $quality - * - * @return bool - */ - private function fastImageCopyResampled( - &$dst_image, - $src_image, - int $dst_x, - int $dst_y, - int $src_x, - int $src_y, - int $dst_w, - int $dst_h, - int $src_w, - int $src_h, - int $quality = 4 - ): bool { - if (empty($src_image) || empty($dst_image) || $quality <= 0) { - return false; - } - - if ($quality < 5 && (($dst_w * $quality) < $src_w || ($dst_h * $quality) < $src_h)) { - $temp = imagecreatetruecolor($dst_w * $quality + 1, $dst_h * $quality + 1); - imagecopyresized($temp, $src_image, 0, 0, $src_x, $src_y, $dst_w * $quality + 1, $dst_h * $quality + 1, $src_w, $src_h); - imagecopyresampled($dst_image, $temp, $dst_x, $dst_y, 0, 0, $dst_w, $dst_h, $dst_w * $quality, $dst_h * $quality); - imagedestroy($temp); - } else { - imagecopyresampled($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h); - } - - return true; - } - - /** - * @param string $source - * - * @return array|false - */ - private function readImage(string $source) - { - list(, , $mime) = getimagesize($source); - - switch ($mime) { - case IMAGETYPE_JPEG: - case IMAGETYPE_JPEG2000: - $image = imagecreatefromjpeg($source); - break; - case IMAGETYPE_PNG: - $image = imagecreatefrompng($source); - break; - case IMAGETYPE_GIF: - $image = imagecreatefromgif($source); - break; - case IMAGETYPE_WEBP: - $image = imagecreatefromwebp($source); - break; - default: - Logs::error(__METHOD__, __LINE__, 'Type of photo "' . $mime . '" is not supported'); - - return false; - break; - } - - if ($image === false) { - return false; - } - - // the image may need to be rotated prior to any processing - try { - $exif = exif_read_data($source); - } catch (\Exception $e) { - $exif = []; - } - $orientation = isset($exif['Orientation']) && $exif['Orientation'] !== '' ? $exif['Orientation'] : 1; - $dimensions = $this->autoRotateInternal($image, $orientation); - - return [$image, $mime, $dimensions['width'], $dimensions['height']]; - } - - /** - * @param string $destination - * @param resource $image - * @param int $mime - * @param int $quality - * - * @return bool - */ - private function writeImage(string $destination, $image, int $mime, int $quality = null): bool - { - $ret = false; - - switch ($mime) { - case IMAGETYPE_JPEG: - case IMAGETYPE_JPEG2000: - $ret = imagejpeg($image, $destination, $quality ?? $this->compressionQuality); - break; - case IMAGETYPE_PNG: - $ret = imagepng($image, $destination); - break; - case IMAGETYPE_GIF: - $ret = imagegif($image, $destination); - break; - case IMAGETYPE_WEBP: - $ret = imagewebp($image, $destination); - break; - } - - return $ret; - } -} diff --git a/app/Image/Handlers/BaseImageHandler.php b/app/Image/Handlers/BaseImageHandler.php new file mode 100644 index 00000000000..e406373d634 --- /dev/null +++ b/app/Image/Handlers/BaseImageHandler.php @@ -0,0 +1,80 @@ +compressionQuality = Configs::getValueAsInt('compression_quality'); + } + + public function __destruct() + { + $this->reset(); + } + + /** + * Optimizes a local image, if enabled. + * + * If lossless optimization is enabled via configuration, this method + * tries to apply the optimization to the provided file. + * If the file is not a local file, optimization is skipped and a warning + * is logged. + * + * TODO: Do we really need it? It does neither seem lossless nor doing anything useful. + * + * @param MediaFile $file + * @param bool $collectStatistics if true, the method returns statistics about the stream + * + * @return StreamStat|null optional statistics about the stream, if optimization took place and if requested + * + * @throws MediaFileOperationException + * @throws ConfigurationKeyMissingException + */ + protected static function applyLosslessOptimizationConditionally(MediaFile $file, bool $collectStatistics = false): ?StreamStats + { + if (Configs::getValueAsBool('lossless_optimization')) { + if ($file instanceof NativeLocalFile) { + ImageOptimizer::optimize($file->getRealPath()); + + return $collectStatistics ? StreamStat::createFromLocalFile($file) : null; + } elseif ($file instanceof FlysystemFile && $file->isLocalFile()) { + $localFile = $file->toLocalFile(); + ImageOptimizer::optimize($localFile->getRealPath()); + + return $collectStatistics ? StreamStat::createFromLocalFile($localFile) : null; + } else { + Log::warning(__METHOD__ . ':' . __LINE__ . ' Skipping lossless optimization; optimization is requested by configuration but only supported for local files'); + + return null; + } + } else { + return null; + } + } +} diff --git a/app/Image/Handlers/GdHandler.php b/app/Image/Handlers/GdHandler.php new file mode 100644 index 00000000000..5b6e66e63b3 --- /dev/null +++ b/app/Image/Handlers/GdHandler.php @@ -0,0 +1,494 @@ +gdImage !== null) { + $dim = $this->getDimensions(); + // We exploit `imagecrop` to get a deep copy of the image; + // see long explanation above + $this->gdImage = imagecrop($this->gdImage, ['x' => 0, 'y' => 0, 'width' => $dim->width, 'height' => $dim->height]); + } + } catch (\ErrorException $e) { + throw new ImageProcessingException('Failed to clone image', $e); + } + } + + /** + * {@inheritdoc} + */ + public function reset(): void + { + $this->gdImage = null; + $this->gdImageType = 0; + } + + /** + * {@inheritDoc} + */ + public function load(MediaFile $file): void + { + try { + $inMemoryBuffer = new InMemoryBuffer(); + $this->reset(); + + $originalStream = $file->read(); + if (stream_get_meta_data($originalStream)['seekable']) { + $inputStream = $originalStream; + } else { + // We make an in-memory copy of the provided stream, + // because we must be able to seek/rewind the stream. + // For example, a readable stream from a remote location (i.e. + // a "download" stream) is only forward readable once. + $inMemoryBuffer->write($originalStream); + $inputStream = $inMemoryBuffer->read(); + } + + $imgBinary = stream_get_contents($inputStream); + rewind($inputStream); + + // Determine the type of image, so that we can later save the + // image using the same type + error_clear_last(); + $gdImgStat = getimagesizefromstring($imgBinary); + if ($gdImgStat === false) { + throw ImageException::createFromPhpError(); + } else { + $this->gdImageType = $gdImgStat[2]; + } + if (!in_array($this->gdImageType, self::SUPPORTED_IMAGE_TYPES, true)) { + $this->reset(); + throw new MediaFileUnsupportedException('Type of photo is not supported'); + } + + // Load image + error_clear_last(); + /** @var \GdImage $img */ + $img = imagecreatefromstring($imgBinary); + $this->gdImage = $img; + + // Get EXIF data to determine whether rotation is required + // `exif_read_data` only supports JPEGs + if (in_array($this->gdImageType, [IMAGETYPE_JPEG, IMAGETYPE_JPEG2000], true)) { + error_clear_last(); + // `exif_read_data` raises E_WARNING/E_NOTICE errors for unsupported + // tags, which could result in exceptions being thrown, even though + // the function would otherwise succeed to return valid tags. + // We explicitly disable this undesirable behavior and use + // the silence operator to suck out as much EXIF data as + // possible even if some EXIF tags are unsupported. + // As this way, `exif_read_data` does not throw any exception + // at all (even for catastrophic errors), we need to check + // manually, if we need to throw an exception. + // TODO: Replace `exif_read_data` by `\Safe\exif_read_data` after https://github.com/thecodingmachine/safe/issues/215 has been resolved + // @phpstan-ignore-next-line + $exifData = @exif_read_data($inputStream); + $phpError = error_get_last(); + if ($exifData === false || $phpError !== null) { + $exception = ImageException::createFromPhpError(); + if ($exifData === false) { + // something went wrong catastrophically, throw the + // exception as `exif_read_data` would have done without @ + throw $exception; + } else { + // exif_read_data() returned an array and has been able + // to extract some useful data, but still reported a + // warning; don't throw the exception, but log it and + // proceed + Handler::reportSafely($exception); + } + } + + // Auto-rotate image + $orientation = array_key_exists('Orientation', $exifData) && is_numeric($exifData['Orientation']) ? (int) $exifData['Orientation'] : 1; + $this->autoRotate($orientation); + } + } catch (\ErrorException $e) { + $this->reset(); + throw new MediaFileOperationException('Failed to load image', $e); + } finally { + $inMemoryBuffer->close(); + $file->close(); + } + } + + /** + * {@inheritDoc} + */ + public function save(MediaFile $file, bool $collectStatistics = false): ?StreamStats + { + if ($this->gdImage === null) { + throw new MediaFileOperationException('No image loaded'); + } + try { + // We write the image into a memory buffer first, because + // we don't know if the file is a local file (or hosted elsewhere) + // and if the file supports seekable streams + $inMemoryBuffer = new InMemoryBuffer(); + + match ($this->gdImageType) { + IMAGETYPE_JPEG, + IMAGETYPE_JPEG2000 => imagejpeg($this->gdImage, $inMemoryBuffer->stream(), $this->compressionQuality), + IMAGETYPE_PNG => imagepng($this->gdImage, $inMemoryBuffer->stream()), + IMAGETYPE_GIF => imagegif($this->gdImage, $inMemoryBuffer->stream()), + IMAGETYPE_WEBP => imagewebp($this->gdImage, $inMemoryBuffer->stream()), + default => throw new \AssertionError('uncovered image type'), + }; + + $streamStat = $file->write($inMemoryBuffer->read(), $collectStatistics); + $file->close(); + $inMemoryBuffer->close(); + + return parent::applyLosslessOptimizationConditionally($file) ?? $streamStat; + } catch (\ErrorException $e) { + throw new MediaFileOperationException('Failed to save image', $e); + } + } + + /** + * Rotates and flips a photo based on the designated EXIF orientation. + * + * @param int $orientation the orientation value (1..8) as defined by EXIF specification, default is 1 (means up-right and not mirrored/flipped) + * + * @return void + * + * @throws ImageProcessingException + */ + private function autoRotate(int $orientation): void + { + try { + $angle = match ($orientation) { + 0, 1, 2, 4 => 0, + 3 => -180, + 5, 6 => -90, + 7, 8 => 90, + default => throw new ImageProcessingException('Image orientation out of range'), + }; + + $flip = match ($orientation) { + 0, 1, 3, 6, 8 => 0, + 2, 7, 5 => IMG_FLIP_HORIZONTAL, + 4 => IMG_FLIP_VERTICAL, + default => throw new ImageProcessingException('Image orientation out of range'), + }; + + if ($angle !== 0) { + $this->gdImage = $this->imagerotate($this->gdImage, $angle, 0); + } + + if ($flip !== 0) { + imageflip($this->gdImage, $flip); + } + } catch (\ErrorException $e) { + throw new ImageProcessingException('Failed to auto-rotate image', $e); + } + } + + /** + * {@inheritdoc} + */ + public function cloneAndScale(ImageDimension $dstDim): ImageHandlerInterface + { + try { + $srcDim = $this->getDimensions(); + + if ($dstDim->width === 0 && $dstDim->height !== 0) { + $scale = $dstDim->height / $srcDim->height; + } elseif ($dstDim->width !== 0 && $dstDim->height === 0) { + $scale = $dstDim->width / $srcDim->width; + } elseif ($dstDim->width !== 0 && $dstDim->height !== 0) { + $scale = min($dstDim->width / $srcDim->width, $dstDim->height / $srcDim->height); + } else { + throw new LycheeDomainException('Width and height must not be zero simultaneously'); + } + + $width = (int) round($scale * $srcDim->width); + $height = (int) round($scale * $srcDim->height); + + $clonedGdImage = imagecreatetruecolor($width, $height); + $this->fastImageCopyResampled($clonedGdImage, $this->gdImage, 0, 0, 0, 0, $width, $height, $srcDim->width, $srcDim->height); + + $clone = new self(); + $clone->compressionQuality = $this->compressionQuality; + $clone->gdImage = $clonedGdImage; + $clone->gdImageType = $this->gdImageType; + + return $clone; + } catch (\ErrorException $e) { + $this->reset(); + throw new ImageProcessingException('Failed to scale image', $e); + } + } + + /** + * {@inheritdoc} + */ + public function cloneAndCrop(ImageDimension $dstDim): ImageHandlerInterface + { + try { + $srcDim = $this->getDimensions(); + + $srcWHRatio = $srcDim->width / $srcDim->height; + $dstWHRatio = $dstDim->width / $dstDim->height; + + if ($dstWHRatio > $srcWHRatio) { + // The designated ratio is wider than the source ratio + // Hence, we must crop off the height + $width = $srcDim->width; + $x = 0; + $height = (int) round($srcDim->width / $dstWHRatio); + $y = (int) round(($srcDim->height - $height) / 2); + } else { + // Inverse case: we must crop off the width + $width = (int) round($srcDim->height * $dstWHRatio); + $x = (int) round(($srcDim->width - $width) / 2); + $height = $srcDim->height; + $y = 0; + } + + $clonedGdImage = imagecreatetruecolor($dstDim->width, $dstDim->height); + $this->fastImageCopyResampled($clonedGdImage, $this->gdImage, 0, 0, $x, $y, $dstDim->width, $dstDim->height, $width, $height); + + $clone = new self(); + $clone->compressionQuality = $this->compressionQuality; + $clone->gdImage = $clonedGdImage; + $clone->gdImageType = $this->gdImageType; + + return $clone; + } catch (\ErrorException $e) { + $this->reset(); + throw new ImageProcessingException('Failed to crop image', $e); + } + } + + /** + * {@inheritdoc} + */ + public function rotate(int $angle): ImageDimension + { + try { + $this->gdImage = $this->imagerotate($this->gdImage, -$angle, 0); + + return $this->getDimensions(); + } catch (\ErrorException $e) { + $this->reset(); + throw new ImageProcessingException('Failed to rotate image', $e); + } + } + + /** + * Plug-and-Play fastImageCopyResampled function replaces much slower imagecopyresampled. + * Just include this function and change all "imagecopyresampled" references to "fastImageCopyResampled". + * Typically from 30 to 60 times faster when reducing high resolution images down to thumbnail size using the default quality setting. + * Author: Tim Eckel - Date: 09/07/07 - Version: 1.1 - Project: FreeRingers.net - Freely distributable - These comments must remain. + * + * Optional "quality" parameter (default is 4). Fractional values are allowed, for example 1.5. Must be greater than zero. + * Between 0 and 1 = Fast, but mosaic results, closer to 0 increases the mosaic effect. + * 1 = Up to 350 times faster. Poor results, looks very similar to imagecopyresized. + * 2 = Up to 95 times faster. Images appear a little sharp, some prefer this over a quality of 3. + * 3 = Up to 60 times faster. Will give high quality smooth results very close to imagecopyresampled, just faster. + * 4 = Up to 25 times faster. Almost identical to imagecopyresampled for most images. + * 5 = No speedup. Just uses imagecopyresampled, no advantage over imagecopyresampled. + * + * @param \GdImage $dst_image + * @param \GdImage $src_image + * @param int $dst_x + * @param int $dst_y + * @param int $src_x + * @param int $src_y + * @param int $dst_w + * @param int $dst_h + * @param int $src_w + * @param int $src_h + * @param int $quality + * + * @return void + * + * @throws ImageProcessingException + */ + private function fastImageCopyResampled( + $dst_image, + $src_image, + int $dst_x, + int $dst_y, + int $src_x, + int $src_y, + int $dst_w, + int $dst_h, + int $src_w, + int $src_h, + int $quality = 4, + ): void { + try { + if ($quality < 5 && (($dst_w * $quality) < $src_w || ($dst_h * $quality) < $src_h)) { + $temp = imagecreatetruecolor($dst_w * $quality + 1, $dst_h * $quality + 1); + imagecopyresized($temp, $src_image, 0, 0, $src_x, $src_y, $dst_w * $quality + 1, $dst_h * $quality + 1, $src_w, $src_h); + imagecopyresampled($dst_image, $temp, $dst_x, $dst_y, 0, 0, $dst_w, $dst_h, $dst_w * $quality, $dst_h * $quality); + } else { + imagecopyresampled($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h); + } + } catch (\ErrorException $e) { + throw new ImageProcessingException('Could not resample image', $e); + } + } + + /** + * {@inheritDoc} + */ + public function getDimensions(): ImageDimension + { + try { + return new ImageDimension(imagesx($this->gdImage), imagesy($this->gdImage)); + } catch (\ErrorException $e) { + throw new ImageProcessingException('Could not determine dimensions of image', $e); + } + } + + public function isLoaded(): bool + { + return $this->gdImageType !== 0 && $this->gdImage !== null; + } + + /** + * CORRECTED from Safe/imagerotate. + * + * Rotates the image image using the given + * angle in degrees. + * + * The center of rotation is the center of the image, and the rotated + * image may have different dimensions than the original image. + * + * @param \GdImage $image a GdImage object, returned by one of the image creation functions, + * such as imagecreatetruecolor + * @param float $angle Rotation angle, in degrees. The rotation angle is interpreted as the + * number of degrees to rotate the image anticlockwise. + * @param int $background_color Specifies the color of the uncovered zone after the rotation + * + * @return \GdImage returns an image object for the rotated image + * + * @throws ImageException + */ + private function imagerotate($image, float $angle, int $background_color) + { + error_clear_last(); + // @phpstan-ignore-next-line + $safeResult = \imagerotate($image, $angle, $background_color); + if ($safeResult === false) { + throw ImageException::createFromPhpError(); + } + + return $safeResult; + } +} diff --git a/app/Image/Handlers/GoogleMotionPictureHandler.php b/app/Image/Handlers/GoogleMotionPictureHandler.php new file mode 100644 index 00000000000..705faa1db73 --- /dev/null +++ b/app/Image/Handlers/GoogleMotionPictureHandler.php @@ -0,0 +1,126 @@ +workingCopy->delete(); + } + + /** + * Loads a video stream from a Google Motion picture. + * + * @param NativeLocalFile $file the Google Motion Picture + * @param int $videoLength the length of the video stream in bytes from the end of the file; `0` indicates the video stream fills the whole file + * + * @return void + * + * @throws ExternalComponentMissingException + * @throws ConfigurationException + * @throws InvalidConfigOption + * @throws MediaFileOperationException + */ + public function load(NativeLocalFile $file, int $videoLength = 0): void + { + if (!Configs::hasFFmpeg()) { + throw new ConfigurationException('FFmpeg is disabled by configuration'); + } + + try { + $this->workingCopy = new TemporaryLocalFile(self::PRELIMINARY_VIDEO_FILE_EXTENSION, $file->getBasename()); + $readStream = $file->read(); + if ($videoLength !== 0) { + fseek($readStream, -$videoLength, SEEK_END); + } + $this->workingCopy->write($readStream); + $file->close(); + + $ffmpeg = FFMpeg::create([ + 'ffmpeg.binaries' => Configs::getValueAsString('ffmpeg_path'), + 'ffprobe.binaries' => Configs::getValueAsString('ffprobe_path'), + ]); + $audioOrVideo = $ffmpeg->open($this->workingCopy->getRealPath()); + if ($audioOrVideo instanceof Video) { + $this->video = $audioOrVideo; + } else { + throw new MediaFileOperationException('No video stream found'); + } + } catch (ExecutableNotFoundException $e) { + throw new ExternalComponentMissingException('FFmpeg not found', $e); + } catch (InvalidArgumentException $e) { + throw new MediaFileOperationException('FFmpeg could not open media file', $e); + } catch (RuntimeException $e) { + throw new MediaFileOperationException('Could not load video stream from Google Motion Picture', $e); + } + } + + /** + * Save the video stream to the provided file. + * + * @param NativeLocalFile $file the file to write into + * + * @return void + * + * @throws MediaFileOperationException + */ + public function saveVideoStream(NativeLocalFile $file): void + { + try { + $format = new MOVFormat(); + // Add additional parameter to extract the first video stream + $format->setAdditionalParameters(['-map', '0:0']); + $this->video->save($format, $file->getRealPath()); + } catch (RuntimeException $e) { + throw new MediaFileOperationException('Could not save video stream from Google Motion Picture', $e); + } catch (InvalidArgumentException $e) { + throw LycheeAssertionError::createFromUnexpectedException($e); + } + } +} diff --git a/app/Image/Handlers/ImageHandler.php b/app/Image/Handlers/ImageHandler.php new file mode 100644 index 00000000000..47bebd14e57 --- /dev/null +++ b/app/Image/Handlers/ImageHandler.php @@ -0,0 +1,143 @@ +engineClasses[] = ImagickHandler::class; + } + $this->engineClasses[] = GdHandler::class; + } + + public function __clone() + { + if ($this->engine !== null) { + $this->engine = clone $this->engine; + } + } + + /** + * {@inheritDoc} + */ + public function load(MediaFile $file): void + { + $this->reset(); + $lastException = null; + + foreach ($this->engineClasses as $engineClass) { + try { + $engine = new $engineClass(); + if ($engine instanceof ImageHandlerInterface) { + $this->engine = $engine; + $this->engine->load($file); + + return; + } else { + throw new LycheeLogicException('$engine is not an instance of ImageHandlerInterface'); + } + } catch (\Throwable $e) { + // Report the error to the log, but don't fail yet. + Handler::reportSafely($e); + $lastException = $e; + $this->engine = null; + } + } + + throw new MediaFileOperationException(self::NO_HANDLER_EXCEPTION_MSG, $lastException); + } + + /** + * {@inheritDoc} + */ + public function save(MediaFile $file, bool $collectStatistics = false): ?StreamStats + { + return $this->engine->save($file, $collectStatistics); + } + + /** + * {@inheritDoc} + */ + public function reset(): void + { + $this->engine?->reset(); + $this->engine = null; + } + + /** + * {@inheritDoc} + */ + public function cloneAndScale(ImageDimension $dstDim): ImageHandlerInterface + { + return $this->engine->cloneAndScale($dstDim); + } + + /** + * {@inheritDoc} + */ + public function cloneAndCrop(ImageDimension $dstDim): ImageHandlerInterface + { + return $this->engine->cloneAndCrop($dstDim); + } + + /** + * {@inheritDoc} + */ + public function rotate(int $angle): ImageDimension + { + return $this->engine->rotate($angle); + } + + /** + * {@inheritDoc} + */ + public function getDimensions(): ImageDimension + { + return $this->engine->getDimensions(); + } + + /** + * {@inheritDoc} + */ + public function isLoaded(): bool + { + return $this->engine !== null && $this->engine->isLoaded(); + } +} diff --git a/app/Image/Handlers/ImagickHandler.php b/app/Image/Handlers/ImagickHandler.php new file mode 100644 index 00000000000..31d18864d7a --- /dev/null +++ b/app/Image/Handlers/ImagickHandler.php @@ -0,0 +1,219 @@ +imImage !== null) { + $this->imImage = clone $this->imImage; + } + } + + /** + * {@inheritdoc} + */ + public function reset(): void + { + $this->imImage?->clear(); + $this->imImage = null; + } + + /** + * {@inheritDoc} + */ + public function load(MediaFile $file): void + { + try { + $inMemoryBuffer = null; + + $this->reset(); + + $originalStream = $file->read(); + if (stream_get_meta_data($originalStream)['seekable']) { + $inputStream = $originalStream; + } else { + // We make an in-memory copy of the provided stream, + // because we must be able to seek/rewind the stream. + // For example, a readable stream from a remote location (i.e. + // a "download" stream) is only forward readable once. + $inMemoryBuffer = new InMemoryBuffer(); + $inMemoryBuffer->write($originalStream); + $inputStream = $inMemoryBuffer->read(); + } + + $this->imImage = new \Imagick(); + $this->imImage->readImageFile($inputStream); + $this->autoRotate(); + } catch (\ImagickException $e) { + throw new MediaFileOperationException('Failed to load image', $e); + } finally { + $inMemoryBuffer?->close(); + $file->close(); + } + } + + /** + * {@inheritDoc} + */ + public function save(MediaFile $file, bool $collectStatistics = false): ?StreamStats + { + if ($this->imImage === null) { + throw new MediaFileOperationException('No image loaded'); + } + try { + $this->imImage->setImageCompressionQuality($this->compressionQuality); + $profiles = $this->imImage->getImageProfiles('icc', true); + // Remove metadata to save some bytes + $this->imImage->stripImage(); + // Re-add color profiles + if (key_exists('icc', $profiles)) { + $this->imImage->profileImage('icc', $profiles['icc']); + } + + // We write the image into a memory buffer first, because + // we don't know if the file is a local file (or hosted elsewhere) + // and if the file supports seekable streams + $inMemoryBuffer = new InMemoryBuffer(); + $this->imImage->writeImageFile($inMemoryBuffer->stream(), ltrim($file->getExtension(), '.')); + $streamStat = $file->write($inMemoryBuffer->read(), $collectStatistics); + $file->close(); + $inMemoryBuffer->close(); + + return parent::applyLosslessOptimizationConditionally($file, $collectStatistics) ?? $streamStat; + } catch (\ImagickException $e) { + throw new MediaFileOperationException('Failed to save image', $e); + } + } + + /** + * Rotates the image such that it is oriented in upright direction;. + * + * @return void + * + * @throws ImageProcessingException + */ + private function autoRotate(): void + { + try { + $orientation = $this->imImage->getImageOrientation(); + + $needsFlop = match ($orientation) { + \Imagick::ORIENTATION_TOPRIGHT, \Imagick::ORIENTATION_BOTTOMLEFT, \Imagick::ORIENTATION_LEFTTOP, \Imagick::ORIENTATION_RIGHTBOTTOM => true, + \Imagick::ORIENTATION_TOPLEFT, \Imagick::ORIENTATION_BOTTOMRIGHT, \Imagick::ORIENTATION_RIGHTTOP, \Imagick::ORIENTATION_LEFTBOTTOM, \Imagick::ORIENTATION_UNDEFINED => false, + }; + + $angle = match ($orientation) { + \Imagick::ORIENTATION_TOPLEFT, \Imagick::ORIENTATION_TOPRIGHT, \Imagick::ORIENTATION_UNDEFINED => 0, + \Imagick::ORIENTATION_BOTTOMRIGHT, \Imagick::ORIENTATION_BOTTOMLEFT => 180, + \Imagick::ORIENTATION_LEFTTOP, \Imagick::ORIENTATION_LEFTBOTTOM => -90, + \Imagick::ORIENTATION_RIGHTTOP, \Imagick::ORIENTATION_RIGHTBOTTOM => 90, + }; + + if ($needsFlop && !$this->imImage->flopImage()) { + throw new \ImagickException('Failed to flop image'); + } + + if ($angle !== 0 && !$this->imImage->rotateImage(new \ImagickPixel(), $angle)) { + throw new \ImagickException('Failed to rotate image'); + } + + if (!$this->imImage->setImageOrientation(\Imagick::ORIENTATION_TOPLEFT)) { + throw new \ImagickException('Failed to set orientation'); + } + } catch (\ImagickException $exception) { + throw new ImageProcessingException('Failed to auto-rotate image', $exception); + } + } + + /** + * {@inheritdoc} + */ + public function cloneAndScale(ImageDimension $dstDim): ImageHandlerInterface + { + try { + $clone = clone $this; + if (!$clone->imImage->scaleImage( + $dstDim->width, $dstDim->height, $dstDim->width !== 0 && $dstDim->height !== 0 + )) { + throw new \ImagickException('Failed to scale image'); + } + + return $clone; + } catch (\ImagickException $e) { + throw new ImageProcessingException('Failed to scale image', $e); + } + } + + /** + * {@inheritdoc} + */ + public function cloneAndCrop(ImageDimension $dstDim): ImageHandlerInterface + { + try { + $clone = clone $this; + if (!$clone->imImage->cropThumbnailImage($dstDim->width, $dstDim->height)) { + throw new \ImagickException('Failed to crop image'); + } + + return $clone; + } catch (\ImagickException $e) { + throw new ImageProcessingException('Failed to crop image', $e); + } + } + + /** + * {@inheritdoc} + */ + public function rotate(int $angle): ImageDimension + { + try { + if (!$this->imImage->rotateImage(new \ImagickPixel(), $angle)) { + throw new \ImagickException('Failed to rotate image'); + } + + return $this->getDimensions(); + } catch (\ImagickException $e) { + throw new ImageProcessingException('Failed to rotate image', $e); + } + } + + /** + * {@inheritDoc} + */ + public function getDimensions(): ImageDimension + { + try { + return new ImageDimension( + $this->imImage->getImageWidth(), + $this->imImage->getImageHeight(), + ); + } catch (\ImagickException $e) { + throw new ImageProcessingException('Could not determine dimensions of image', $e); + } + } + + public function isLoaded(): bool + { + return $this->imImage !== null; + } +} diff --git a/app/Image/Handlers/VideoHandler.php b/app/Image/Handlers/VideoHandler.php new file mode 100644 index 00000000000..f9738c63743 --- /dev/null +++ b/app/Image/Handlers/VideoHandler.php @@ -0,0 +1,98 @@ + Configs::getValueAsString('ffmpeg_path'), + 'ffprobe.binaries' => Configs::getValueAsString('ffprobe_path'), + ]); + $audioOrVideo = $ffmpeg->open($file->getRealPath()); + if ($audioOrVideo instanceof Video) { + $this->video = $audioOrVideo; + } else { + throw new MediaFileOperationException('No video streams found.'); + } + } catch (ExecutableNotFoundException $e) { + throw new ExternalComponentMissingException('FFmpeg not found', $e); + } catch (InvalidArgumentException $e) { + throw new MediaFileOperationException('FFmpeg could not open media file', $e); + } + } + + /** + * Extracts and returns a frame from the video. + * + * @param float $framePosition + * + * @return ImageHandlerInterface + * + * @throws MediaFileOperationException + * @throws ImageProcessingException + * @throws MediaFileUnsupportedException + */ + public function extractFrame(float $framePosition = 0.0): ImageHandlerInterface + { + try { + // A temporary, local file for the extracted frame + $frameFile = new TemporaryLocalFile('.jpg'); + $frame = $this->video->frame(TimeCode::fromSeconds($framePosition)); + $frame->save($frameFile->getRealPath()); + + // Load the extracted frame into the image handler + $frame = new ImageHandler(); + $frame->load($frameFile); + $frameFile->delete(); + + return $frame; + } catch (RuntimeException $e) { + throw new MediaFileOperationException('Could not extract frame from video file', $e); + } + } +} diff --git a/app/Image/ImageHandler.php b/app/Image/ImageHandler.php deleted file mode 100644 index 8bc64cf6a04..00000000000 --- a/app/Image/ImageHandler.php +++ /dev/null @@ -1,106 +0,0 @@ -compressionQuality = $compressionQuality; - $this->engines = []; - if (Configs::hasImagick()) { - $this->engines[] = new ImagickHandler($this->compressionQuality); - } - $this->engines[] = new GdHandler($this->compressionQuality); - } - - /** - * @param string $source - * @param string $destination - * @param int $newWidth - * @param int $newHeight - * @param int &$resWidth - * @param int &$resHeight - * - * @return bool - */ - public function scale(string $source, string $destination, int $newWidth, int $newHeight, int &$resWidth, int &$resHeight): bool - { - $i = 0; - while ($i < count($this->engines) && !$this->engines[$i]->scale($source, $destination, $newWidth, $newHeight, $resWidth, $resHeight)) { - $i++; - } - - return $i != count($this->engines); - } - - /** - * @param string $source - * @param string $destination - * @param int $newWidth - * @param int $newHeight - * - * @return bool - */ - public function crop(string $source, string $destination, int $newWidth, int $newHeight): bool - { - $i = 0; - while ($i < count($this->engines) && !$this->engines[$i]->crop($source, $destination, $newWidth, $newHeight)) { - $i++; - } - - return $i != count($this->engines); - } - - /** - * Rotates and flips a photo based on its EXIF orientation. - * - * @param string $path - * @param array $info - * @param bool $pretend - * - * @return array - */ - public function autoRotate(string $path, array $info, bool $pretend = false): array - { - $i = 0; - $ret = [false, false]; - while ($i < count($this->engines) && ($ret = $this->engines[$i]->autoRotate($path, $info, $pretend)) == [false, false]) { - $i++; - } - - return $ret; - } - - /** - * @param string $source - * @param int $angle - * @param string $destination - * - * @return bool - */ - public function rotate(string $source, int $angle, string $destination = null): bool - { - if ($angle != 90 && $angle != -90) { - return false; - } - - $i = 0; - while ($i < count($this->engines) && !$this->engines[$i]->rotate($source, $angle, $destination)) { - $i++; - } - - return $i != count($this->engines); - } -} diff --git a/app/Image/ImageHandlerInterface.php b/app/Image/ImageHandlerInterface.php deleted file mode 100644 index 3c6d0ec6de6..00000000000 --- a/app/Image/ImageHandlerInterface.php +++ /dev/null @@ -1,65 +0,0 @@ -getImageOrientation(); - - switch ($orientation) { - case \Imagick::ORIENTATION_TOPLEFT: - // nothing to do - break; - case \Imagick::ORIENTATION_TOPRIGHT: - $image->flopImage(); - break; - case \Imagick::ORIENTATION_BOTTOMRIGHT: - $image->rotateImage(new \ImagickPixel(), 180); - break; - case \Imagick::ORIENTATION_BOTTOMLEFT: - $image->flopImage(); - $image->rotateImage(new \ImagickPixel(), 180); - break; - case \Imagick::ORIENTATION_LEFTTOP: - $image->flopImage(); - $image->rotateImage(new \ImagickPixel(), -90); - break; - case \Imagick::ORIENTATION_RIGHTTOP: - $image->rotateImage(new \ImagickPixel(), 90); - break; - case \Imagick::ORIENTATION_RIGHTBOTTOM: - $image->flopImage(); - $image->rotateImage(new \ImagickPixel(), 90); - break; - case \Imagick::ORIENTATION_LEFTBOTTOM: - $image->rotateImage(new \ImagickPixel(), -90); - break; - } - - $image->setImageOrientation(\Imagick::ORIENTATION_TOPLEFT); - - return [ - 'width' => $image->getImageWidth(), - 'height' => $image->getImageHeight(), - ]; - } catch (ImagickException $exception) { - Logs::error(__METHOD__, __LINE__, $exception->getMessage()); - - return [false, false]; - } - } - - /** - * {@inheritdoc} - */ - public function __construct(int $compressionQuality) - { - $this->compressionQuality = $compressionQuality; - } - - /** - * {@inheritdoc} - */ - public function scale( - string $source, - string $destination, - int $newWidth, - int $newHeight, - int &$resWidth, - int &$resHeight - ): bool { - try { - // Read image - $image = new \Imagick(); - $image->readImage($source); - // the image may need to be rotated prior to scaling - $this->autoRotateInternal($image); - - $image->setImageCompressionQuality($this->compressionQuality); - - $profiles = $image->getImageProfiles('icc', true); - - $image->scaleImage($newWidth, $newHeight, ($newWidth != 0 && $newHeight != 0)); - - // Remove metadata to save some bytes - $image->stripImage(); - - if (!empty($profiles)) { - $image->profileImage('icc', $profiles['icc']); - } - - $image->writeImage($destination); - Logs::notice(__METHOD__, __LINE__, 'Saving thumb to ' . $destination); - $resWidth = $image->getImageWidth(); - $resHeight = $image->getImageHeight(); - $image->clear(); - $image->destroy(); - - // Optimize image - if (Configs::get_value('lossless_optimization', '0') == '1') { - ImageOptimizer::optimize($destination); - } - } catch (ImagickException $exception) { - Logs::error(__METHOD__, __LINE__, $exception->getMessage()); - - return false; - } - - return true; - } - - /** - * {@inheritdoc} - */ - public function crop( - string $source, - string $destination, - int $newWidth, - int $newHeight - ): bool { - try { - $image = new \Imagick(); - $image->readImage($source); - // the image may need to be rotated prior to cropping - $this->autoRotateInternal($image); - - $image->setImageCompressionQuality($this->compressionQuality); - - $profiles = $image->getImageProfiles('icc', true); - - $image->cropThumbnailImage($newWidth, $newHeight); - - // Remove metadata to save some bytes - $image->stripImage(); - - if (!empty($profiles)) { - $image->profileImage('icc', $profiles['icc']); - } - - $image->writeImage($destination); - Logs::notice(__METHOD__, __LINE__, 'Saving thumb to ' . $destination); - $image->clear(); - $image->destroy(); - - // Optimize image - if (Configs::get_value('lossless_optimization', '0') == '1') { - ImageOptimizer::optimize($destination); - } - } catch (ImagickException $exception) { - Logs::error(__METHOD__, __LINE__, $exception->getMessage()); - - return false; - } - - return true; - } - - /** - * {@inheritdoc} - */ - public function autoRotate(string $path, array $info, bool $pretend = false): array - { - try { - $image = new \Imagick(); - $image->readImage($path); - - $rotate = $image->getImageOrientation() !== \Imagick::ORIENTATION_TOPLEFT; - - $dimensions = $this->autoRotateInternal($image); - - if ($rotate && !$pretend) { - $image->writeImage($path); - } - - $image->clear(); - $image->destroy(); - - return $dimensions; - } catch (ImagickException $exception) { - Logs::error(__METHOD__, __LINE__, $exception->getMessage()); - - return [false, false]; - } - } - - /** - * {@inheritdoc} - */ - public function rotate(string $source, int $angle, string $destination = null): bool - { - try { - $image = new \Imagick(); - if ($image->readImage($source) === false) { - return false; - } - // the image may need to be rotated upright prior to the requested rotation - $this->autoRotateInternal($image); - - if ($image->rotateImage(new \ImagickPixel(), $angle) === false) { - return false; - } - - $ret = $image->writeImage($destination); - - $image->clear(); - $image->destroy(); - - return $ret; - } catch (ImagickException $exception) { - Logs::error(__METHOD__, __LINE__, $exception->getMessage()); - - return false; - } - } -} diff --git a/app/Image/PlaceholderEncoder.php b/app/Image/PlaceholderEncoder.php new file mode 100644 index 00000000000..e4277d7390c --- /dev/null +++ b/app/Image/PlaceholderEncoder.php @@ -0,0 +1,182 @@ +getFile(); + $workingImage = new InMemoryBuffer(); + + $this->createGdImage($sizeVariant->getFile()); + $this->compressImage($this->gdImage, $workingImage); + $this->encodeBase64Placeholder($workingImage); + $this->savePlaceholder($workingImage, $sizeVariant); + + // delete original file since we now have no reference to it + $originalFile->delete(); + } catch (\ErrorException $e) { + throw new MediaFileOperationException('Failed to encode placeholder to base64', $e); + } + } + + /** + * Returns a GdImage object from the provided file. + * + * @param MediaFile $file + * + * @return void + * + * @throws FilesystemException + * @throws ImageException + * @throws StreamException + */ + private function createGdImage(MediaFile $file): void + { + $inMemoryBuffer = new InMemoryBuffer(); + + $originalStream = $file->read(); + if (stream_get_meta_data($originalStream)['seekable']) { + $inputStream = $originalStream; + } else { + // We make an in-memory copy of the provided stream, + // because we must be able to seek/rewind the stream. + // For example, a readable stream from a remote location (i.e. + // a "download" stream) is only forward readable once. + $inMemoryBuffer->write($originalStream); + $inputStream = $inMemoryBuffer->read(); + } + $imgBinary = stream_get_contents($inputStream); + + rewind($inputStream); + /** @var \GdImage $referenceImage */ + $referenceImage = imagecreatefromstring($imgBinary); + // webp does not support palette images + imagepalettetotruecolor($referenceImage); + + $this->gdImage = $referenceImage; + } + + /** + * Compress webp image to acceptable size for DB. + * + * @param \GdImage $source source Image + * @param InMemoryBuffer $output the file to write to + * + * @return void + * + * @throws ImageException + * @throws FilesystemException + */ + private function compressImage(\GdImage $source, InMemoryBuffer $output): void + { + $quality = self::IMAGE_QUALITY; + $retries = 0; + // Given a proper placeholder source image (16px x 16px) it should + // almost always be sufficiently compressed on the first attempt. + // But just in case it isn't we try to compress again. + do { + // ensure buffer is empty before trying to compress again + $emptyStream = \Safe\fopen('php://temp', 'w+'); + $output->write($emptyStream); + \Safe\fclose($emptyStream); + + imagewebp($source, $output->stream(), $quality); + $filesize = \Safe\fstat($output->read())['size']; + + $quality -= 5; + $retries++; + } while ($filesize > self::COMPRESSION_SIZE_LIMIT && $retries <= self::MAX_COMPRESSION_RETRIES); + } + + /** + * Encodes provided image file to base64. + * + * @param InMemoryBuffer $file + * + * @return void + * + * @throws StreamException + */ + private function encodeBase64Placeholder(InMemoryBuffer $file): void + { + $inMemoryBuffer = new InMemoryBuffer(); + + stream_filter_append($inMemoryBuffer->read(), 'convert.base64-encode', STREAM_FILTER_WRITE); + $inMemoryBuffer->write($file->read()); + + $file->write($inMemoryBuffer->read()); + $inMemoryBuffer->close(); + } + + /** + * Saves base64 string and size to DB. + * + * @param InMemoryBuffer $file + * @param SizeVariant $sizeVariant + * + * @return void + * + * @throws FilesystemException + * @throws StreamException + */ + private function savePlaceholder(InMemoryBuffer $file, SizeVariant $sizeVariant): void + { + $base64Length = \Safe\fstat($file->read())['size']; + if ($base64Length <= self::BASE64_SIZE_LIMIT) { + $sizeVariant->filesize = $base64Length; + $sizeVariant->short_path = stream_get_contents($file->read()); + $sizeVariant->save(); + } else { + throw new MediaFileOperationException('Encoded image is too large.'); + } + } +} \ No newline at end of file diff --git a/app/Image/SizeVariantDefaultFactory.php b/app/Image/SizeVariantDefaultFactory.php new file mode 100644 index 00000000000..b8bb0b134fd --- /dev/null +++ b/app/Image/SizeVariantDefaultFactory.php @@ -0,0 +1,201 @@ +svDimensionHelpers = new SizeVariantDimensionHelpers(); + + try { + $this->photo = $photo; + if ($referenceImage !== null && $referenceImage->isLoaded()) { + $this->referenceImage = $referenceImage; + } else { + $this->loadReferenceImage(); + } + $this->namingStrategy = $namingStrategy ?? resolve(AbstractSizeVariantNamingStrategy::class); + // Ensure that the naming strategy is linked to this photo + $this->namingStrategy->setPhoto($this->photo); + } catch (BindingResolutionException $e) { + throw new FrameworkException('Laravel\'s container component', $e); + } + } + + /** + * Loads the reference image from the original size variant of the associated photo. + * + * @throws ExternalComponentMissingException + * @throws ConfigurationException + * @throws MediaFileOperationException + * @throws IllegalOrderOfOperationException + * @throws MediaFileUnsupportedException + * @throws ImageProcessingException + */ + protected function loadReferenceImage(): void + { + $originalFile = $this->photo->size_variants->getOriginal()->getFile(); + + if (!$this->photo->isVideo()) { + $this->referenceImage = new ImageHandler(); + $this->referenceImage->load($originalFile); + } else { + if ($originalFile->isLocalFile()) { + // If the original size variant is hosted locally, + // we can directly take it as a source file + $sourceFile = $originalFile->toLocalFile(); + } else { + // If the original size variant is hosted remotely, + // we must download it first; we exploit the temporary file + // for that + $sourceFile = new TemporaryLocalFile($originalFile->getOriginalExtension(), $this->photo->title); + $sourceFile->write($originalFile->read(), false, $this->photo->type); + } + + $videoHandler = new VideoHandler(); + $videoHandler->load($sourceFile); + $position = is_numeric($this->photo->aperture) ? floatval($this->photo->aperture) / 2 : 0.0; + $this->referenceImage = $videoHandler->extractFrame($position); + + // Clean up + if ($sourceFile instanceof TemporaryLocalFile) { + $sourceFile->delete(); + } + } + } + + /** + * {@inheritDoc} + */ + public function createSizeVariants(): Collection + { + $allVariants = [ + SizeVariantType::PLACEHOLDER, + SizeVariantType::THUMB, + SizeVariantType::THUMB2X, + SizeVariantType::SMALL, + SizeVariantType::SMALL2X, + SizeVariantType::MEDIUM, + SizeVariantType::MEDIUM2X, + ]; + $collection = new Collection(); + + foreach ($allVariants as $variant) { + $sv = $this->createSizeVariantCond($variant); + if ($sv !== null) { + $collection->add($sv); + } + } + + return $collection; + } + + /** + * {@inheritDoc} + */ + public function createSizeVariantCond(SizeVariantType $sizeVariant): ?SizeVariant + { + if ($sizeVariant === SizeVariantType::ORIGINAL) { + throw new InvalidSizeVariantException('createSizeVariantCond() must not be used to create original size'); + } + if (!$this->svDimensionHelpers->isEnabledByConfiguration($sizeVariant)) { + return null; + } + // Don't generate medium size variants for videos, because the current web front-end has no use for it. Let's save some storage space. + if ($this->photo->isVideo() && ($sizeVariant === SizeVariantType::MEDIUM || $sizeVariant === SizeVariantType::MEDIUM2X)) { + return null; + } + // Don't re-create existing size variant + if ($this->photo->size_variants->getSizeVariant($sizeVariant) !== null) { + return null; + } + + $maxDim = $this->svDimensionHelpers->getMaxDimensions($sizeVariant); + $realDim = $this->referenceImage->getDimensions(); + + return $this->svDimensionHelpers->isLargeEnough($realDim, $maxDim, $sizeVariant) ? + $this->createSizeVariantInternal($sizeVariant, $maxDim) : + null; + } + + /** + * Generates the designated size variant unconditionally. + * + * The method does not check whether the size variant already exist + * and will overwrite an existing one of the same type. + * + * @param SizeVariantType $sizeVariant the desired size variant; admissible + * values are: + * {@link SizeVariantType::THUMB}, + * {@link SizeVariantType::THUMB2X}, + * {@link SizeVariantType::SMALL}, + * {@link SizeVariantType::SMALL2X}, + * {@link SizeVariantType::MEDIUM} and + * {@link SizeVariantType::MEDIUM2X} + * @param ImageDimension $maxDim the designated dimensions of the + * size variant + * + * @return SizeVariant the generated size variant + * + * @throws LycheeException + */ + private function createSizeVariantInternal(SizeVariantType $sizeVariant, ImageDimension $maxDim): SizeVariant + { + $svImage = match ($sizeVariant) { + SizeVariantType::THUMB, SizeVariantType::THUMB2X, SizeVariantType::PLACEHOLDER => $this->referenceImage->cloneAndCrop($maxDim), + default => $this->referenceImage->cloneAndScale($maxDim), + }; + + $svFile = $this->namingStrategy->createFile($sizeVariant); + $svImage->save($svFile); + + return $this->photo->size_variants->create( + $sizeVariant, + $svFile->getRelativePath(), + $svImage->getDimensions(), + $svFile->getFilesize() + ); + } +} diff --git a/app/Image/SizeVariantDimensionHelpers.php b/app/Image/SizeVariantDimensionHelpers.php new file mode 100644 index 00000000000..fc4abeb1e0a --- /dev/null +++ b/app/Image/SizeVariantDimensionHelpers.php @@ -0,0 +1,131 @@ +getMaxWidth($sizeVariant); + $maxHeight = $this->getMaxHeight($sizeVariant); + + return new ImageDimension($maxWidth, $maxHeight); + } + + /** + * Checks whether the requested size variant is enabled by configuration. + * + * This function always returns true, for size variants which are not + * configurable and are always enabled (e.g. a thumb). + * Hence, it is safe to call this function for all size variants. + * For size variants which may be enabled/disabled through configuration at + * runtime, the method only returns true, if + * + * 1. the size variant is enabled, and + * 2. the allowed maximum width or maximum height is not zero. + * + * In other words, even if a size variant is enabled, this function + * still returns false, if both the allowed maximum width and height + * equal zero. + * + * @param SizeVariantType $sizeVariant the indicated size variant + * + * @return bool true, if the size variant is enabled and the allowed width + * or height is unequal to zero + * + * @throws InvalidSizeVariantException + */ + public function isEnabledByConfiguration(SizeVariantType $sizeVariant): bool + { + $maxDim = $this->getMaxDimensions($sizeVariant); + if ($maxDim->width === 0 && $maxDim->height === 0) { + return false; + } + + return match ($sizeVariant) { + SizeVariantType::MEDIUM2X => Configs::getValueAsBool('medium_2x'), + SizeVariantType::SMALL2X => Configs::getValueAsBool('small_2x'), + SizeVariantType::THUMB2X => Configs::getValueAsBool('thumb_2x'), + SizeVariantType::PLACEHOLDER => Configs::getValueAsBool('low_quality_image_placeholder'), + SizeVariantType::SMALL, SizeVariantType::MEDIUM, SizeVariantType::THUMB => true, + default => throw new InvalidSizeVariantException('unknown size variant: ' . $sizeVariant->value), + }; + } + + /** + * Given dimension and SizeVariant type, provide a check whether a SizeVariant should be created + * under the constraints provided. + * + * @param ImageDimension $realDim the dimension of original + * @param ImageDimension $maxDim the max dimension of target size variant + * @param SizeVariantType $sizeVariant type of size variant to be created + * + * @return bool true, if the size is big enough for creation + */ + public function isLargeEnough(ImageDimension $realDim, ImageDimension $maxDim, SizeVariantType $sizeVariant): bool + { + return match ($sizeVariant) { + SizeVariantType::THUMB, SizeVariantType::PLACEHOLDER => true, + SizeVariantType::THUMB2X => $realDim->width >= $maxDim->width && $realDim->height >= $maxDim->height, + default => ($realDim->width >= $maxDim->width && $maxDim->width !== 0) || ($realDim->height >= $maxDim->height && $maxDim->height !== 0), + }; + } + + /** + * Return the max width for the SizeVariant. + * + * @return int + */ + public function getMaxWidth(SizeVariantType $sizeVariant): int + { + return match ($sizeVariant) { + SizeVariantType::MEDIUM2X => 2 * Configs::getValueAsInt('medium_max_width'), + SizeVariantType::MEDIUM => Configs::getValueAsInt('medium_max_width'), + SizeVariantType::SMALL2X => 2 * Configs::getValueAsInt('small_max_width'), + SizeVariantType::SMALL => Configs::getValueAsInt('small_max_width'), + SizeVariantType::THUMB2X => SizeVariantDefaultFactory::THUMBNAIL2X_DIM, + SizeVariantType::THUMB => SizeVariantDefaultFactory::THUMBNAIL_DIM, + SizeVariantType::PLACEHOLDER => SizeVariantDefaultFactory::PLACEHOLDER_DIM, + default => throw new InvalidSizeVariantException('No applicable for original'), + }; + } + + /** + * Return the max height for the SizeVariant. + * + * @return int + */ + public function getMaxHeight(SizeVariantType $sizeVariant): int + { + return match ($sizeVariant) { + SizeVariantType::MEDIUM2X => 2 * Configs::getValueAsInt('medium_max_height'), + SizeVariantType::MEDIUM => Configs::getValueAsInt('medium_max_height'), + SizeVariantType::SMALL2X => 2 * Configs::getValueAsInt('small_max_height'), + SizeVariantType::SMALL => Configs::getValueAsInt('small_max_height'), + SizeVariantType::THUMB2X => SizeVariantDefaultFactory::THUMBNAIL2X_DIM, + SizeVariantType::THUMB => SizeVariantDefaultFactory::THUMBNAIL_DIM, + SizeVariantType::PLACEHOLDER => SizeVariantDefaultFactory::PLACEHOLDER_DIM, + default => throw new InvalidSizeVariantException('unknown size variant: ' . $sizeVariant->value), + }; + } +} \ No newline at end of file diff --git a/app/Image/StreamStat.php b/app/Image/StreamStat.php new file mode 100644 index 00000000000..b120f5ff65a --- /dev/null +++ b/app/Image/StreamStat.php @@ -0,0 +1,69 @@ +bytes = $bytes; + $this->checksum = $checksum; + } + + /** + * Creates a new object from a native local file. + * + * Use this method rarely! The class is intended to be used with streams. + * In particular, the checksum of file (or binary blob) can be + * calculated on the fly while the content of the file is written + * via {@link BinaryBlob::write()}. + * Calculating the stream statistics on-the-fly avoids reading back the + * file from disk after it has been written. + * This method is merely meant for the rare cases where we don't have + * an in-memory copy of the file in the first place. + * + * @throws MediaFileOperationException + */ + public static function createFromLocalFile(NativeLocalFile $file): StreamStat + { + try { + error_clear_last(); + $checksum = hash_file(StreamStatFilter::HASH_ALGO_NAME, $file->getPath()); + if ($checksum === false) { + // @codeCoverageIgnoreStart + $error = error_get_last(); + throw new \ErrorException($error['message'] ?? 'An error occurred', 0, $error['type'] ?? 1); + // @codeCoverageIgnoreEnd + } + + return new StreamStat($file->getFilesize(), $checksum); + // @codeCoverageIgnoreStart + } catch (\ErrorException $e) { + throw new MediaFileOperationException($e->getMessage(), $e); + } + // @codeCoverageIgnoreEnd + } +} diff --git a/app/Image/StreamStatFilter.php b/app/Image/StreamStatFilter.php new file mode 100644 index 00000000000..68774e84a87 --- /dev/null +++ b/app/Image/StreamStatFilter.php @@ -0,0 +1,76 @@ +datalen; + if ($this->params instanceof StreamStat) { + $this->params->bytes += $bucket->datalen; + \hash_update($this->hashContext, $bucket->data); + } + stream_bucket_append($out, $bucket); + } + + return PSFS_PASS_ON; + } + + /** + * Called when the stream is closed. + * + * {@inheritDoc} + * + * Finalizes the hash. + */ + public function onClose(): void + { + if ($this->params instanceof StreamStat) { + $this->params->checksum = \hash_final($this->hashContext); + } + parent::onClose(); + } + + /** + * Called when the stream is opened. + * + * {@inheritDoc} + * + * Initializes the hash. + */ + public function onCreate(): bool + { + if ($this->params instanceof StreamStat) { + $this->params->bytes = 0; + $this->hashContext = \hash_init(self::HASH_ALGO_NAME); + } + + return parent::onCreate(); + } +} diff --git a/app/Jobs/ProcessImageJob.php b/app/Jobs/ProcessImageJob.php new file mode 100644 index 00000000000..b50b0c2987a --- /dev/null +++ b/app/Jobs/ProcessImageJob.php @@ -0,0 +1,137 @@ +filePath = $file->getPath(); + $this->originalBaseName = $file->getOriginalBasename(); + + $this->albumID = null; + $album_name = __('gallery.smart_album.unsorted'); + + if ($album instanceof AbstractAlbum) { + $this->albumID = $album->id; + $album_name = $album->title; + } + + if (is_string($album)) { + $this->albumID = $album; + $album_name = resolve(AlbumFactory::class)->findAbstractAlbumOrFail($this->albumID)->title; + } + + $this->userId = Auth::user()->id; + $this->fileLastModifiedTime = $fileLastModifiedTime; + + // Set up our new history record. + $this->history = new JobHistory(); + $this->history->owner_id = $this->userId; + $this->history->job = Str::limit(sprintf('Process Image: %s added to %s.', $this->originalBaseName, $album_name), 200); + $this->history->status = JobStatus::READY; + + $this->history->save(); + } + + /** + * Execute the job. + * + * Here we handle the execution of the image processing. + * This will create the model, reformat the image etc. + */ + public function handle(AlbumFactory $albumFactory): Photo + { + $this->history->status = JobStatus::STARTED; + $this->history->save(); + + $copiedFile = new TemporaryJobFile($this->filePath, $this->originalBaseName); + + // As the file has been uploaded, the (temporary) source file shall be + // deleted + $create = new Create( + new ImportMode(deleteImported: true, skipDuplicates: Configs::getValueAsBool('skip_duplicates')), + $this->userId + ); + + $album = null; + if ($this->albumID !== null) { + $album = $albumFactory->findAbstractAlbumOrFail($this->albumID); + } + + $photo = $create->add($copiedFile, $album, $this->fileLastModifiedTime); + + // Once the job has finished, set history status to 1. + $this->history->status = JobStatus::SUCCESS; + $this->history->save(); + + return $photo; + } + + /** + * Catch failures. + * + * @param \Throwable $th + * + * @return void + */ + public function failed(\Throwable $th): void + { + $this->history->status = JobStatus::FAILURE; + $this->history->save(); + + if ($th->getCode() === 999) { + $this->release(); + } else { + Log::error(__LINE__ . ':' . __FILE__ . ' ' . $th->getMessage(), $th->getTrace()); + } + } +} diff --git a/app/Jobs/UploadSizeVariantToS3Job.php b/app/Jobs/UploadSizeVariantToS3Job.php new file mode 100644 index 00000000000..780c9580538 --- /dev/null +++ b/app/Jobs/UploadSizeVariantToS3Job.php @@ -0,0 +1,106 @@ +variant = $variant; + + // Set up our new history record. + $this->history = new JobHistory(); + $this->history->owner_id = Auth::user()->id; + $this->history->job = Str::limit(sprintf('Upload sizeVariant to S3: %s.', $this->variant->short_path), 200); + $this->history->status = JobStatus::READY; + $this->history->save(); + } + + public function handle(): void + { + $this->history->status = JobStatus::STARTED; + $this->history->save(); + + Storage::disk(StorageDiskType::S3->value)->writeStream( + $this->variant->short_path, + Storage::disk(StorageDiskType::LOCAL->value)->readStream($this->variant->short_path) + ); + + Storage::disk(StorageDiskType::LOCAL->value)->delete($this->variant->short_path); + + $this->variant->storage_disk = StorageDiskType::S3; + $this->variant->save(); + + $this->handleVideoPartner(); + + // Once the job has finished, set history status to 1. + $this->history->status = JobStatus::SUCCESS; + $this->history->save(); + } + + public function failed(\Throwable $th): void + { + $this->history->status = JobStatus::FAILURE; + $this->history->save(); + + if ($th->getCode() === 999) { + $this->release(); + } else { + Log::error(__LINE__ . ':' . __FILE__ . ' ' . $th->getMessage(), $th->getTrace()); + } + } + + /** + * If we have a live partner, then we also upload it. + */ + private function handleVideoPartner(): void + { + if ($this->variant->type !== SizeVariantType::ORIGINAL) { + return; + } + + $photo = Photo::query()->where('id', '=', $this->variant->photo_id)->first(); + + if ($photo->live_photo_short_path === null) { + return; + } + + Storage::disk(StorageDiskType::S3->value)->writeStream( + $photo->live_photo_short_path, + Storage::disk(StorageDiskType::LOCAL->value)->readStream($photo->live_photo_short_path) + ); + + Storage::disk(StorageDiskType::LOCAL->value)->delete($photo->live_photo_short_path); + } +} diff --git a/app/Legacy/Actions/Photo/Create.php b/app/Legacy/Actions/Photo/Create.php new file mode 100644 index 00000000000..1cb59c7a718 --- /dev/null +++ b/app/Legacy/Actions/Photo/Create.php @@ -0,0 +1,249 @@ +strategyParameters = new ImportParam($importMode, $intendedOwnerId); + } + + /** + * Adds/imports the designated source file to Lychee. + * + * Depending on the type and origin of the source file as well as + * depending on operational settings, this method applies different + * strategies. + * This method may create a new database entry or update an existing + * database entry. + * + * @param NativeLocalFile $sourceFile the source file + * @param int|null $fileLastModifiedTime the timestamp to use if there's no creation date in Exif + * @param AbstractAlbum|null $album the targeted parent album + * + * @return Photo the newly created or updated photo + * + * @throws ModelNotFoundException + * @throws LycheeException + */ + public function add(NativeLocalFile $sourceFile, ?AbstractAlbum $album, ?int $fileLastModifiedTime = null): Photo + { + $fileLastModifiedTime ??= filemtime($sourceFile->getRealPath()); + + $sourceFile->assertIsSupportedMediaOrAcceptedRaw(); + + // Fill in information about targeted parent album + // throws InvalidPropertyException + $this->initParentAlbum($album); + + // Fill in metadata extracted from source file + $this->loadFileMetadata($sourceFile, $fileLastModifiedTime); + + // Look up potential duplicates/partners in order to select the + // proper strategy + $duplicate = $this->get_duplicate(StreamStat::createFromLocalFile($sourceFile)->checksum); + $livePartner = $this->findLivePartner( + $this->strategyParameters->exifInfo->livePhotoContentID, + $this->strategyParameters->exifInfo->type, + $this->strategyParameters->album + ); + + /* + * From here we need to use a strategy depending on whether we have + * + * - a duplicate + * - a "stand-alone" media file (i.e. a photo or video without a partner) + * - a photo which is the partner of an already existing video + * - a video which is the partner of an already existing photo + */ + if ($duplicate !== null) { + $strategy = new AddDuplicateStrategy($this->strategyParameters, $duplicate); + } else { + if ($livePartner === null) { + $strategy = new AddStandaloneStrategy($this->strategyParameters, $sourceFile); + } else { + if ($sourceFile->isSupportedVideo()) { + $strategy = new AddVideoPartnerStrategy($this->strategyParameters, $sourceFile, $livePartner); + } elseif ($sourceFile->isSupportedImage()) { + $strategy = new AddPhotoPartnerStrategy($this->strategyParameters, $sourceFile, $livePartner); + } else { + // Accepted, but unsupported raw files are added as stand-alone files + $strategy = new AddStandaloneStrategy($this->strategyParameters, $sourceFile); + } + } + } + + $photo = $strategy->do(); + + if ($photo->album_id !== null) { + $notify = new Notify(); + $notify->do($photo); + } + + return $photo; + } + + /** + * Extracts the meta-data of the source file and initializes + * {@link ImportParam::$exifInfo} of {@link Create::$strategyParameters}. + * + * @param NativeLocalFile $sourceFile the source file + * @param int $fileLastModifiedTime the timestamp to use if there's no creation date in Exif + * + * @return void + * + * @throws ExternalComponentMissingException + * @throws MediaFileOperationException + * @throws ExternalComponentFailedException + */ + protected function loadFileMetadata(NativeLocalFile $sourceFile, int $fileLastModifiedTime): void + { + $this->strategyParameters->exifInfo = Extractor::createFromFile($sourceFile, $fileLastModifiedTime); + + // Use basename of file if IPTC title missing + if ( + $this->strategyParameters->exifInfo->title === null || + $this->strategyParameters->exifInfo->title === '' + ) { + $this->strategyParameters->exifInfo->title = substr($sourceFile->getOriginalBasename(), 0, 98); + } + } + + /** + * Finds a "lonely" live partner if it exists. + * + * A lonely live partner is a media entry which + * - has the same content ID + * - is in the same album + * - which has an "opposed" mime type (i.e. only mixed (video,photo) or + * (photo,video) pairs can be partners + * - which has no live partner yet + * + * @param string|null $contentID the content id to identify a matching partner + * @param string $mimeType the mime type of the media which a partner is looked for, e.g. the returned {@link Photo} has an "opposed" mime type + * @param Album|null $album the album of which the partner must be member of + * + * @return Photo|null The live partner if found + * + * @throws QueryBuilderException + */ + protected function findLivePartner( + ?string $contentID, + string $mimeType, + ?Album $album, + ): ?Photo { + try { + $livePartner = null; + // find a potential partner which has the same content id + if ($contentID !== null) { + /** @var Photo|null $livePartner */ + $livePartner = Photo::query() + ->where('live_photo_content_id', '=', $contentID) + ->where('album_id', '=', $album?->id) + ->whereNull('live_photo_short_path')->first(); + } + // if a potential partner has been found, ensure that it is of a + // different kind then the uploaded media. + if ( + $livePartner !== null && !( + BaseMediaFile::isSupportedImageMimeType($mimeType) && $livePartner->isVideo() || + BaseMediaFile::isSupportedVideoMimeType($mimeType) && $livePartner->isPhoto() + ) + ) { + $livePartner = null; + } + + return $livePartner; + } catch (IllegalOrderOfOperationException $e) { + throw LycheeAssertionError::createFromUnexpectedException($e); + } + } + + /** + * Sets the (regular) parent album of {@link Create::$strategyParameters} + * according to the provided parent album. + * + * If the provided parent album equals `null` or is already a (regular) + * album, then the strategy is set to that album. + * If the provided parent album is one of the built-in smart albums, + * then the (regular) parent album of the strategy is set to `null` (aka + * the root album) and the other properties of the strategy are tweaked + * such that the photo will be shown by the smart album. + * + * @param AbstractAlbum|null $album the targeted parent album + * + * @throws InvalidPropertyException + */ + protected function initParentAlbum(?AbstractAlbum $album = null): void + { + if ($album === null) { + $this->strategyParameters->album = null; + } elseif ($album instanceof Album) { + $this->strategyParameters->album = $album; + } elseif ($album instanceof BaseSmartAlbum) { + $this->strategyParameters->album = null; + if ($album instanceof StarredAlbum) { + $this->strategyParameters->is_starred = true; + } + } else { + throw new InvalidPropertyException('The given parent album does not support uploading'); + } + } + + /** + * Check if a picture has a duplicate + * We compare the checksum to the other Photos or LivePhotos. + * + * @param string $checksum + * + * @return ?Photo + */ + public function get_duplicate(string $checksum): ?Photo + { + /** @var Photo|null $photo */ + $photo = Photo::query() + ->where('checksum', '=', $checksum) + ->orWhere('original_checksum', '=', $checksum) + ->orWhere('live_photo_checksum', '=', $checksum) + ->first(); + + return $photo; + } +} diff --git a/app/Legacy/Actions/Photo/Strategies/AbstractAddStrategy.php b/app/Legacy/Actions/Photo/Strategies/AbstractAddStrategy.php new file mode 100644 index 00000000000..eff088adb83 --- /dev/null +++ b/app/Legacy/Actions/Photo/Strategies/AbstractAddStrategy.php @@ -0,0 +1,125 @@ +photo->title === null) { + $this->photo->title = $this->parameters->exifInfo->title; + } + if ($this->photo->description === null) { + $this->photo->description = $this->parameters->exifInfo->description; + } + if (count($this->photo->tags) === 0) { + $this->photo->tags = $this->parameters->exifInfo->tags; + } + if ($this->photo->type === null) { + $this->photo->type = $this->parameters->exifInfo->type; + } + if ($this->photo->iso === null) { + $this->photo->iso = $this->parameters->exifInfo->iso; + } + if ($this->photo->aperture === null) { + $this->photo->aperture = $this->parameters->exifInfo->aperture; + } + if ($this->photo->make === null) { + $this->photo->make = $this->parameters->exifInfo->make; + } + if ($this->photo->model === null) { + $this->photo->model = $this->parameters->exifInfo->model; + } + if ($this->photo->lens === null) { + $this->photo->lens = $this->parameters->exifInfo->lens; + } + if ($this->photo->shutter === null) { + $this->photo->shutter = $this->parameters->exifInfo->shutter; + } + if ($this->photo->focal === null) { + $this->photo->focal = $this->parameters->exifInfo->focal; + } + if ($this->photo->taken_at === null) { + $this->photo->taken_at = $this->parameters->exifInfo->taken_at; + } + if ($this->photo->latitude === null) { + $this->photo->latitude = $this->parameters->exifInfo->latitude; + } + if ($this->photo->longitude === null) { + $this->photo->longitude = $this->parameters->exifInfo->longitude; + } + if ($this->photo->altitude === null) { + $this->photo->altitude = $this->parameters->exifInfo->altitude; + } + if ($this->photo->img_direction === null) { + $this->photo->img_direction = $this->parameters->exifInfo->imgDirection; + } + if ($this->photo->location === null) { + $this->photo->location = $this->parameters->exifInfo->location; + } + if ($this->photo->live_photo_content_id === null) { + $this->photo->live_photo_content_id = $this->parameters->exifInfo->livePhotoContentID; + } + } + + /** + * @throws UnauthenticatedException + */ + protected function setParentAndOwnership(): void + { + if ($this->parameters->album !== null) { + $this->photo->album_id = $this->parameters->album->id; + // Avoid unnecessary DB request, when we access the album of a + // photo later (e.g. when a notification is sent). + $this->photo->setRelation('album', $this->parameters->album); + $this->photo->owner_id = $this->parameters->album->owner_id; + } else { + $this->photo->album_id = null; + // Avoid unnecessary DB request, when we access the album of a + // photo later (e.g. when a notification is sent). + $this->photo->setRelation('album', null); + $this->photo->owner_id = $this->parameters->intendedOwnerId; + } + } +} diff --git a/app/Legacy/Actions/Photo/Strategies/AddDuplicateStrategy.php b/app/Legacy/Actions/Photo/Strategies/AddDuplicateStrategy.php new file mode 100644 index 00000000000..9a4f383312f --- /dev/null +++ b/app/Legacy/Actions/Photo/Strategies/AddDuplicateStrategy.php @@ -0,0 +1,65 @@ +parameters->importMode->shallResyncMetadata) { + $this->hydrateMetadata(); + if ($this->photo->isDirty()) { + Log::notice(__METHOD__ . ':' . __LINE__ . ' Updating metadata of existing photo.'); + $this->photo->save(); + $hasBeenReSynced = true; + } + } + + if ($this->parameters->importMode->shallSkipDuplicates) { + if ($hasBeenReSynced) { + throw new PhotoResyncedException(); + } else { + throw new PhotoSkippedException(); + } + } else { + // Duplicate the existing photo, this will also duplicate all + // size variants without actually duplicating physical files + $existing = $this->photo; + $this->photo = $existing->replicate(); + // Adopt settings of duplicated photo acc. to target album + $this->photo->is_starred = $this->parameters->is_starred; + $this->setParentAndOwnership(); + $this->photo->save(); + } + + return $this->photo; + } +} diff --git a/app/Legacy/Actions/Photo/Strategies/AddPhotoPartnerStrategy.php b/app/Legacy/Actions/Photo/Strategies/AddPhotoPartnerStrategy.php new file mode 100644 index 00000000000..65e34a0b295 --- /dev/null +++ b/app/Legacy/Actions/Photo/Strategies/AddPhotoPartnerStrategy.php @@ -0,0 +1,78 @@ +existingVideo = $existingVideo; + } + + /** + * @return Photo + * + * @throws LycheeException + */ + public function do(): Photo + { + // First add the source file as if it was a stand-alone photo + // This creates and persists $this->photo as a new DB entry + parent::do(); + + // Now we re-use the same strategy as if the freshly created photo + // entity had been uploaded first and as if the already existing video + // had been uploaded after that. + // We use the original size variant of the video as the "source file" + // We request that the "imported" file shall be deleted, this actually + // "steals away" the stored video file from the existing video entity + // and moves it to the correct destination of a live partner for the + // photo. + $parameters = new ImportParam( + new ImportMode(deleteImported: true), + $this->parameters->intendedOwnerId + ); + $videoStrategy = new AddVideoPartnerStrategy( + $parameters, + $this->existingVideo->size_variants->getOriginal()->getFile(), + $this->photo + ); + $videoStrategy->do(); + + // If the video is uploaded already, we must copy over the checksum + $this->photo->live_photo_checksum = $this->existingVideo->checksum; + + // Delete the existing video from whom we have stolen the video file + // `delete()` also takes care of erasing all other size variants + // from storage + $this->existingVideo->delete(); + + return $this->photo; + } +} diff --git a/app/Legacy/Actions/Photo/Strategies/AddStandaloneStrategy.php b/app/Legacy/Actions/Photo/Strategies/AddStandaloneStrategy.php new file mode 100644 index 00000000000..095a33e39b0 --- /dev/null +++ b/app/Legacy/Actions/Photo/Strategies/AddStandaloneStrategy.php @@ -0,0 +1,329 @@ +photo->updateTimestamps(); + $this->sourceImage = null; + $this->sourceFile = $sourceFile; + $this->namingStrategy = resolve(AbstractSizeVariantNamingStrategy::class); + $this->namingStrategy->setPhoto($this->photo); + $this->namingStrategy->setExtension( + $this->sourceFile->getOriginalExtension() + ); + } catch (BindingResolutionException $e) { + throw new FrameworkException('Laravel\'s container component', $e); + } + } + + /** + * @return Photo + * + * @throws LycheeException + */ + public function do(): Photo + { + // Create and save "bare" photo object without size variants + $this->hydrateMetadata(); + $this->photo->is_starred = $this->parameters->is_starred; + $this->setParentAndOwnership(); + + // Unfortunately, we must read the entire file once to create the + // true, original checksum. + // It does **not** suffice to use the stream statistics, when the + // image file is loaded, because we cannot guarantee that the image + // loader reads the file entirely in one pass. + // a) The image loader may decide to seek in the file, skip + // certain parts (like EXIF information), re-read chunks of the + // file multiple times or out-of-order. + // b) The image loader may not load the entire file, if the image + // stream is shorter than the file and followed by additional + // non-image information (e.g. as it is the case for Google + // Live Photos) + $this->photo->original_checksum = StreamStat::createFromLocalFile($this->sourceFile)->checksum; + + try { + try { + if ($this->photo->isVideo()) { + $videoHandler = new VideoHandler(); + $videoHandler->load($this->sourceFile); + $position = is_numeric($this->photo->aperture) ? floatval($this->photo->aperture) / 2 : 0.0; + $this->sourceImage = $videoHandler->extractFrame($position); + } else { + // Load source image if it is a supported photo format + $this->sourceImage = new ImageHandler(); + $this->sourceImage->load($this->sourceFile); + } + } catch (\Throwable $e) { + // This may happen for videos if FFmpeg is not available to + // extract a still frame, or for raw files (Imagick may be + // able to convert them to jpeg, but there are no + // guarantees, and Imagick may not be available). + $this->sourceImage = null; + + // Log an error without failing. + Handler::reportSafely($e); + } + + // Handle Google Motion Pictures + // We must extract the video stream from the original (local) + // file and stash it away, before the original file is moved into + // its (potentially remote) final position + if ($this->parameters->exifInfo->microVideoOffset !== 0) { + try { + $tmpVideoFile = new TemporaryLocalFile(GoogleMotionPictureHandler::FINAL_VIDEO_FILE_EXTENSION, $this->sourceFile->getBasename()); + $gmpHandler = new GoogleMotionPictureHandler(); + $gmpHandler->load($this->sourceFile, $this->parameters->exifInfo->microVideoOffset); + $gmpHandler->saveVideoStream($tmpVideoFile); + } catch (\Throwable $e) { + Handler::reportSafely($e); + $tmpVideoFile = null; + } + } else { + $tmpVideoFile = null; + } + + // Create target file and symlink/copy/move source file to + // target. + // If import strategy request to delete the source file. + // `$this->sourceFile` will be deleted after this step. + // But `$this->sourceImage` remains in memory. + $targetFile = $this->namingStrategy->createFile(SizeVariantType::ORIGINAL); + $streamStat = $this->putSourceIntoFinalDestination($targetFile); + + // If we have a temporary video file from a Google Motion Picture, + // we must move the preliminary extracted video file next to the + // final target file + if ($tmpVideoFile !== null) { + // @TODO S3 How should live videos be handled? + $videoTargetPath = + pathinfo($targetFile->getRelativePath(), PATHINFO_DIRNAME) . + '/' . + pathinfo($targetFile->getRelativePath(), PATHINFO_FILENAME) . + $tmpVideoFile->getExtension(); + $videoTargetFile = new FlysystemFile($targetFile->getDisk(), $videoTargetPath); + $videoTargetFile->write($tmpVideoFile->read()); + $this->photo->live_photo_short_path = $videoTargetFile->getRelativePath(); + $tmpVideoFile->close(); + $tmpVideoFile->delete(); + $tmpVideoFile = null; + } + + // The original and final checksum may differ, if the photo has + // been rotated by `putSourceIntoFinalDestination` while being + // moved into final position. + $this->photo->checksum = $streamStat->checksum; + $this->photo->save(); + + // Create original size variant of photo + // If the image has been loaded (and potentially auto-rotated) + // take the dimension from the image. + // As a fallback for media files from which no image could be extracted (e.g. unsupported file formats) we use the EXIF data. + $imageDim = $this->sourceImage?->isLoaded() ? + $this->sourceImage->getDimensions() : + new ImageDimension($this->parameters->exifInfo->width, $this->parameters->exifInfo->height); + $originalVariant = $this->photo->size_variants->create( + SizeVariantType::ORIGINAL, + $targetFile->getRelativePath(), + $imageDim, + $streamStat->bytes + ); + } catch (LycheeException $e) { + // If source file could not be put into final destination, remove + // freshly created photo from DB to avoid having "zombie" entries. + try { + $this->photo->delete(); + } catch (\Throwable) { + // Sic! If anything goes wrong here, we still throw the original exception + } + throw $e; + } + + // Create remaining size variants if we were able to successfully + // extract a reference image above + if ($this->sourceImage?->isLoaded()) { + try { + /** @var SizeVariantFactory $sizeVariantFactory */ + $sizeVariantFactory = resolve(SizeVariantFactory::class); + $sizeVariantFactory->init($this->photo, $this->sourceImage, $this->namingStrategy); + $variants = $sizeVariantFactory->createSizeVariants(); + $variants->push($originalVariant); + + $placeholder = $variants->firstWhere('type', SizeVariantType::PLACEHOLDER); + if ($placeholder !== null) { + $placeholderEncoder = new PlaceholderEncoder(); + $placeholderEncoder->do($placeholder); + } + + if (Features::active('use-s3')) { + // If enabled, upload all size variants to the remote bucket and delete the local files after that + $variants->each(function (SizeVariant $variant) { + if (Configs::getValueAsBool('use_job_queues')) { + UploadSizeVariantToS3Job::dispatch($variant); + } else { + $job = new UploadSizeVariantToS3Job($variant); + $job->handle(); + } + }); + } + } catch (\Throwable $t) { + // Don't re-throw the exception, because we do not want the + // import to fail completely only due to missing size variants. + // There are just too many options why the creation of size + // variants may fail. + Handler::reportSafely($t); + } + } + + return $this->photo; + } + + /** + * Moves/copies/symlinks source file to final destination and + * normalizes orientation, if necessary. + * + * Note, {@link AddStandaloneStrategy::$sourceFile} and + * {@link AddStandaloneStrategy::$sourceImage} must be set before this + * method is called. + * + * If import via symbolic link is requested, then a symbolic link + * from `$targetFile` to {@link AddStandaloneStrategy::$sourceFile} is + * created. + * Otherwise the content of {@link AddStandaloneStrategy::$sourceFile} + * is physically copied/moved into `$targetFile`. + * + * If the source file requires normalization, then + * {@link AddStandaloneStrategy::$sourceImage} is saved to `$targetFile`. + * This step implicitly corrects the orientation. + * Otherwise, the original byte stream from + * {@link AddStandaloneStrategy::$sourceFile} is written to `$targetFile` + * without modifications. + * + * @param FlysystemFile $targetFile the target file + * + * @return StreamStats statistics about the final file, may differ from + * the source file due to normalization of orientation + * + * @throws MediaFileOperationException + * @throws ConfigurationException + */ + private function putSourceIntoFinalDestination(FlysystemFile $targetFile): StreamStats + { + try { + if ($this->parameters->importMode->shallImportViaSymlink) { + if (!$targetFile->isLocalFile()) { + throw new ConfigurationException('Symlinking is only supported on local filesystems'); + } + $targetPath = $targetFile->toLocalFile()->getPath(); + $sourcePath = $this->sourceFile->getRealPath(); + // For symlinks we must manually create a non-existing + // parent directory. + // This mimics the behaviour of Flysystem for regular files. + $targetDirectory = pathinfo($targetPath, PATHINFO_DIRNAME); + if (!is_dir($targetDirectory)) { + $umask = \umask(0); + \Safe\mkdir($targetDirectory, BasicPermissionCheck::getConfiguredDirectoryPerm(), true); + \umask($umask); + } + \Safe\symlink($sourcePath, $targetPath); + $streamStat = StreamStat::createFromLocalFile($this->sourceFile); + } else { + $shallNormalize = Configs::getValueAsBool('auto_fix_orientation') && + $this->sourceImage !== null && + $this->parameters->exifInfo->orientation !== 1; + + if ($shallNormalize) { + // Saving the loaded image to the final target normalizes + // the image orientation. This comes at the cost that + // the image is re-encoded and hence its quality might + // be reduced. + $streamStat = $this->sourceImage->save($targetFile, true); + } else { + // If the image does not require normalization the + // unaltered source file is copied to the final target. + // Avoiding a re-encoding prevents any potential quality + // loss. + $streamStat = $targetFile->write($this->sourceFile->read(), true); + $this->sourceFile->close(); + $targetFile->close(); + } + if ($this->parameters->importMode->shallDeleteImported) { + // This may throw an exception, if the original has been + // readable, but is not writable + // In this case, the media file will have been copied, but + // cannot be "moved". + try { + $this->sourceFile->delete(); + } catch (MediaFileOperationException $e) { + // If deletion failed, we do not cancel the whole + // import, but fall back to copy-semantics and + // log the exception + Handler::reportSafely($e); + } + } + } + + return $streamStat; + } catch (\ErrorException $e) { + throw new MediaFileOperationException('Could move/copy/symlink source file to final destination', $e); + } + } +} diff --git a/app/Legacy/Actions/Photo/Strategies/AddVideoPartnerStrategy.php b/app/Legacy/Actions/Photo/Strategies/AddVideoPartnerStrategy.php new file mode 100644 index 00000000000..313a39506ae --- /dev/null +++ b/app/Legacy/Actions/Photo/Strategies/AddVideoPartnerStrategy.php @@ -0,0 +1,157 @@ +videoSourceFile = $videoSourceFile; + } + + /** + * @return Photo + * + * @throws MediaFileOperationException + * @throws ModelDBException + * @throws ConfigurationException + */ + public function do(): Photo + { + $photoFile = $this->photo->size_variants->getOriginal()->getFile(); + $photoPath = $photoFile->getRelativePath(); + $photoExt = $photoFile->getOriginalExtension(); + $videoExt = $this->videoSourceFile->getOriginalExtension(); + $videoPath = substr($photoPath, 0, -strlen($photoExt)) . $videoExt; + $videoTargetFile = new FlysystemFile(Storage::disk(StorageDiskType::LOCAL->value), $videoPath); + $streamStat = $this->putSourceIntoFinalDestination($videoTargetFile); + $this->photo->live_photo_short_path = $videoPath; + $this->photo->live_photo_checksum = $streamStat?->checksum; + $this->photo->save(); + + return $this->photo; + } + + /** + * Puts the video source file into the final position at video target file. + * + * We need to distinguish two cases: + * + * A) The video source file is a native, local file. + * + * In this case, the video file has just been uploaded (and the photo + * partner is already on the target disk). + * The video file must be put onto the target disk, the same way as it + * would for a stand-alone upload. + * + * B) The video source file is a FlysystemFile, too. + * + * In this case, the video file is already on the final disk, but in the + * wrong position. + * In that case we can and must rename it. + * Note, that we must take a little extra care, if the final disk is also + * local and the video file has been imported via a symbolic link. + * We want to rename the symbolic link, not the target of the symbolic + * link. + * + * @param FlysystemFile $videoTargetFile + * + * @return StreamStat|null statistics about the uploaded video file; `null` if no file has been uploaded, but renamed in-place + * + * @throws MediaFileOperationException + * @throws ConfigurationException + */ + protected function putSourceIntoFinalDestination(FlysystemFile $videoTargetFile): ?StreamStat + { + try { + if ($this->videoSourceFile instanceof NativeLocalFile) { + // This is case A (see above) + // The code is very similar to + // AddStandaloneStrategy::putSourceIntoFinalDestination() + // except that we can skip the part about normalization of + // orientation, because we don't support that for videos. + if ($this->parameters->importMode->shallImportViaSymlink) { + if (!$videoTargetFile->isLocalFile()) { + throw new ConfigurationException('Symlinking is only supported on local filesystems'); + } + $targetPath = $videoTargetFile->toLocalFile()->getPath(); + $sourcePath = $this->videoSourceFile->getRealPath(); + // For symlinks we must manually create a non-existing + // parent directory. + // This mimics the behaviour of Flysystem for regular files. + $targetDirectory = pathinfo($targetPath, PATHINFO_DIRNAME); + if (!is_dir($targetDirectory)) { + $umask = \umask(0); + \Safe\mkdir($targetDirectory, BasicPermissionCheck::getConfiguredDirectoryPerm(), true); + \umask($umask); + } + \Safe\symlink($sourcePath, $targetPath); + $streamStat = StreamStat::createFromLocalFile($this->videoSourceFile); + } else { + $streamStat = $videoTargetFile->write($this->videoSourceFile->read(), true); + $this->videoSourceFile->close(); + $videoTargetFile->close(); + if ($this->parameters->importMode->shallDeleteImported) { + // This may throw an exception, if the original has been + // readable, but is not writable + // In this case, the media file will have been copied, but + // cannot be "moved". + try { + $this->videoSourceFile->delete(); + } catch (MediaFileOperationException $e) { + // If deletion failed, we do not cancel the whole + // import, but fall back to copy-semantics and + // log the exception + Handler::reportSafely($e); + } + } + } + } elseif ($this->videoSourceFile instanceof FlysystemFile) { + // It seems as if Flysystem calls a primitive \rename under the + // hood, if the storage adapter is the `Local` adapter. + // This also works for symbolic links, so we are good here. + $this->videoSourceFile->move($videoTargetFile->getRelativePath()); + $streamStat = null; + } else { + throw new LycheeAssertionError('Unexpected type of $videoSourceFile: ' . get_class($this->videoSourceFile)); + } + + return $streamStat; + } catch (\ErrorException $e) { + throw new MediaFileOperationException('Could move/copy/symlink source file to final destination', $e); + } + } +} diff --git a/app/Legacy/Actions/Settings/UpdateLogin.php b/app/Legacy/Actions/Settings/UpdateLogin.php new file mode 100644 index 00000000000..7ca04a4876e --- /dev/null +++ b/app/Legacy/Actions/Settings/UpdateLogin.php @@ -0,0 +1,87 @@ +password)) { + Log::channel('login')->notice(__METHOD__ . ':' . __LINE__ . sprintf('User (%s) tried to change their identity from %s', $user->username, $ip)); + + throw new UnauthenticatedException('Previous password is invalid'); + } + + if ($username !== null && + $username !== '' && + Configs::getValueAsBool('allow_username_change')) { + $this->updateUsername($user, $username, $ip); + } + + $user->password = Hash::make($password); + $user->save(); + + return $user; + } + + /** + * Update Username if it does not already exists. + * + * @param User $user + * @param string $username + * @param string $ip + * + * @return void + * + * @throws ConfigurationKeyMissingException + * @throws QueryBuilderException + * @throws ConflictingPropertyException + */ + private function updateUsername(User &$user, string $username, string $ip): void + { + if (User::query()->where('username', '=', $username)->where('id', '!=', $user->id)->count() !== 0) { + Log::channel('login')->warning(__METHOD__ . ':' . __LINE__ . sprintf('User (%s) tried to change their identity to (%s) from %s', $user->username, $username, $ip)); + throw new ConflictingPropertyException('Username already exists.'); + } + + if ($username !== $user->username) { + Log::channel('login')->notice(__METHOD__ . ':' . __LINE__ . sprintf('User (%s) changed their identity for (%s) from %s', $user->username, $username, $ip)); + $user->username = $username; + } + } +} diff --git a/app/Legacy/AdminAuthentication.php b/app/Legacy/AdminAuthentication.php new file mode 100644 index 00000000000..ecf397ea41c --- /dev/null +++ b/app/Legacy/AdminAuthentication.php @@ -0,0 +1,102 @@ +getVersion(); + + // For version up to 4.0.8 the admin password is stored in the settings + /** @codeCoverageIgnore */ + if ($db_version_number->toInteger() <= 40008) { + // @codeCoverageIgnoreStart + return self::logAsAdminFromConfig($username, $password, $ip); + // @codeCoverageIgnoreEnd + } + + // For version up to 4.6.3 + $admin_id = $db_version_number->toInteger() <= 40603 ? 0 : 1; + // Note there is a small edge case where a user could be at version 4.6.3 AND having already bumped the ID. + // We consider this risk to be too small to actually mitigate it. + + /** @var User|null $adminUser */ + $adminUser = User::query()->find($admin_id); + + // Admin User exists, so we check against it. + if ($adminUser !== null && Hash::check($username, $adminUser->username) && Hash::check($password, $adminUser->password)) { + Auth::login($adminUser); + Log::channel('login')->notice(__METHOD__ . ':' . __LINE__ . ' User (' . $username . ') has logged in from ' . $ip); + + // update the admin username so we do not need to go through here anymore. + $adminUser->username = $username; + $adminUser->save(); + + return true; + } + + return false; + } + + /** + * This is only applicable if we are up to version 4.0.8 in which the refactoring of admin append. + * + * @param string $username + * @param string $password + * @param string $ip + * + * @return bool + * + * @codeCoverageIgnore + */ + public static function logAsAdminFromConfig(string $username, string $password, string $ip): bool + { + $username_hash = Configs::getValueAsString('username'); + $password_hash = Configs::getValueAsString('password'); + + if (Hash::check($username, $username_hash) && Hash::check($password, $password_hash)) { + // Prior version 4.6.3 we are using ID 0 as admin + // We create admin at ID 0 because the 2022_12_10_183251_increment_user_i_ds will be taking care to push it to 1. + /** @var User $adminUser */ + $adminUser = User::query()->findOrNew(0); + $adminUser->username = $username; + $adminUser->password = Hash::make($password); + $adminUser->save(); + + Auth::login($adminUser); + Log::channel('login')->notice(__METHOD__ . ':' . __LINE__ . ' User (' . $username . ') has logged in from ' . $ip . ' (legacy)'); + + return true; + } + + return false; + } +} \ No newline at end of file diff --git a/app/Legacy/BaseConfigMigration.php b/app/Legacy/BaseConfigMigration.php new file mode 100644 index 00000000000..d92abbc65a5 --- /dev/null +++ b/app/Legacy/BaseConfigMigration.php @@ -0,0 +1,48 @@ + + * + * @codeCoverageIgnore + */ + abstract public function getConfigs(): array; + + /** + * Run the migrations. + */ + final public function up(): void + { + DB::table('configs')->insert($this->getConfigs()); + } + + /** + * Reverse the migrations. + * + * @codeCoverageIgnore + */ + final public function down(): void + { + $keys = collect($this->getConfigs())->map(fn ($v) => $v['key'])->all(); + DB::table('configs')->whereIn('key', $keys)->delete(); + } +} diff --git a/app/Legacy/Legacy.php b/app/Legacy/Legacy.php index 99d3008eade..97d63690064 100644 --- a/app/Legacy/Legacy.php +++ b/app/Legacy/Legacy.php @@ -1,70 +1,112 @@ orWhere('key', '=', 'password')->update(['value' => '']); - } - - public static function SetPassword($request) + public static function isLegacyModelID(string $id): bool { - $configs = Configs::get(); - if (Configs::get('version', '040000') < '040008') { - if ($configs['password'] === '' && $configs['username'] === '') { - Configs::set('username', bcrypt($request['username'])); - Configs::set('password', bcrypt($request['password'])); - - return true; - } - } + $modernIDRule = new RandomIDRule(true); + $legacyIDRule = new IntegerIDRule(false); - return false; + return !$modernIDRule->passes('id', $id) && + $legacyIDRule->passes('id', $id); } - public static function noLogin(): bool + /** + * Translates an ID from legacy format to modern format. + * + * @param int $id the legacy ID + * @param string $tableName the table name which should be used to look + * up the ID; either `photos` or `base_albums` + * @param Request $request the request which triggered the lookup + * (required for proper logging) + * + * @return string|null the modern ID + * + * @throws QueryBuilderException thrown by the ORM layer in case of error + * @throws ConfigurationException thrown, if the translation between + * legacy and modern IDs is disabled + */ + private static function translateLegacyID(int $id, string $tableName, Request $request): ?string { - // LEGACY STUFF - $configs = Configs::get(); + try { + $newID = (string) DB::table($tableName) + ->where('legacy_id', '=', intval($id)) + ->value('id'); - if (Configs::get_value('version', '040000') <= '040008') { - // Check if login credentials exist and login if they don't - if ( - isset($configs['username']) && $configs['username'] === '' && - isset($configs['password']) && $configs['password'] === '' - ) { - Session::put('login', true); - Session::put('UserID', 0); + if ($newID !== '') { + $referer = strval($request->header('Referer', '(unknown)')); + $msg = ' Request for ' . $tableName . + ' with legacy ID ' . $id . + ' instead of new ID ' . $newID . + ' from ' . $referer; + if (!Configs::getValueAsBool('legacy_id_redirection')) { + $msg .= ' (translation disabled by configuration)'; + throw new ConfigurationException($msg); + } + Log::warning(__METHOD__ . ':' . __LINE__ . $msg); - return true; + return $newID; } - } - return false; + return null; + } catch (\InvalidArgumentException $e) { + throw new QueryBuilderException($e); + } } - public static function log_as_admin(string $username, string $password, string $ip): bool + /** + * Translates an album ID from legacy format to modern format. + * + * @param int $albumID the legacy ID + * @param Request $request the request which triggered the lookup + * (required for proper logging) + * + * @return string|null the modern ID + * + * @throws QueryBuilderException thrown by the ORM layer in case of error + * @throws ConfigurationException thrown, if the translation between + * legacy and modern IDs is disabled + */ + public static function translateLegacyAlbumID(int $albumID, Request $request): ?string { - $configs = Configs::get(); - - if (Hash::check($username, $configs['username']) && Hash::check($password, $configs['password'])) { - Session::put('login', true); - Session::put('UserID', 0); - Logs::notice(__METHOD__, __LINE__, 'User (' . $username . ') has logged in from ' . $ip . ' (legacy)'); - - return true; - } + return self::translateLegacyID($albumID, 'base_albums', $request); + } - return false; + /** + * Translates a photo ID from legacy format to modern format. + * + * @param int $photoID the legacy ID + * @param Request $request the request which triggered the lookup + * (required for proper logging) + * + * @return string|null the modern ID + * + * @throws QueryBuilderException thrown by the ORM layer in case of error + * @throws ConfigurationException thrown, if the translation between + * legacy and modern IDs is disabled + */ + public static function translateLegacyPhotoID(int $photoID, Request $request): ?string + { + return self::translateLegacyID($photoID, 'photos', $request); } } diff --git a/app/Legacy/V1/Actions/Albums/Tree.php b/app/Legacy/V1/Actions/Albums/Tree.php new file mode 100644 index 00000000000..2df1866f5cd --- /dev/null +++ b/app/Legacy/V1/Actions/Albums/Tree.php @@ -0,0 +1,100 @@ +albumQueryPolicy = $albumQueryPolicy; + $this->sorting = AlbumSortingCriterion::createDefault(); + } + + /** + * @return AlbumForestResource + * + * @throws InternalLycheeException + */ + public function get(): AlbumForestResource + { + /* + * Note, strictly speaking + * {@link AlbumQueryPolicy::applyBrowsabilityFilter()} + * would be the correct function in order to scope the query below, + * because we only want albums which are browsable. + * But + * {@link AlbumQueryPolicy::applyBrowsabilityFilter()} + * is rather slow for large sets of albums (O(n²) runtime). + * Luckily, + * {@link AlbumQueryPolicy::applyReachabilityFilter()} + * is sufficient here, although it does only consider an album's + * reachability _locally_. + * We rely on `->toTree` below to remove orphaned sub-tress and hence + * only return a tree of browsable albums. + */ + $query = new SortingDecorator( + $this->albumQueryPolicy->applyReachabilityFilter(Album::query()) + ); + if (Auth::check()) { + // For authenticated users we group albums by ownership. + $query->orderBy(ColumnSortingType::OWNER_ID, OrderSortingType::ASC); + } + $query->orderBy($this->sorting->column, $this->sorting->order); + + /** @var NsCollection $albums */ + $albums = $query->get(); + /** @var ?NsCollection $sharedAlbums */ + $sharedAlbums = null; + $userID = Auth::id(); + if ($userID !== null) { + // ATTENTION: + // For this to work correctly, it is crucial that all child albums + // below each top-level album have the same owner! + // Otherwise, this partitioning tears apart albums of the same + // (sub)-tree and then `toTree` will return garbage as it does + // not find connected paths within `$albums` or `$sharedAlbums`, + // resp. + /** @var NsCollection $albums */ + /** @var ?NsCollection $sharedAlbums */ + list($albums, $sharedAlbums) = $albums->partition(fn (Album $album) => $album->owner_id === $userID); + } + + // We must explicitly pass `null` as the ID of the root album + // as there are several top-level albums below root. + // Otherwise, `toTree` uses the ID of the album with the lowest + // `_lft` value as the (wrong) root album. + /** @var BaseCollection $albumsTree */ + $albumsTree = $albums->toTree(null); + /** @var BaseCollection $sharedTree */ + $sharedTree = $sharedAlbums?->toTree(null); + + return new AlbumForestResource($albumsTree, $sharedTree); + } +} diff --git a/app/Legacy/V1/Contracts/Http/Requests/HasAbstractAlbum.php b/app/Legacy/V1/Contracts/Http/Requests/HasAbstractAlbum.php new file mode 100644 index 00000000000..118a8621722 --- /dev/null +++ b/app/Legacy/V1/Contracts/Http/Requests/HasAbstractAlbum.php @@ -0,0 +1,19 @@ + + */ + public function albums(): Collection; +} diff --git a/app/Legacy/V1/Contracts/Http/Requests/HasAspectRatio.php b/app/Legacy/V1/Contracts/Http/Requests/HasAspectRatio.php new file mode 100644 index 00000000000..e3050ef08a4 --- /dev/null +++ b/app/Legacy/V1/Contracts/Http/Requests/HasAspectRatio.php @@ -0,0 +1,19 @@ + + */ + public function photos(): Collection; +} diff --git a/app/Legacy/V1/Contracts/Http/Requests/HasSizeVariant.php b/app/Legacy/V1/Contracts/Http/Requests/HasSizeVariant.php new file mode 100644 index 00000000000..46b07299729 --- /dev/null +++ b/app/Legacy/V1/Contracts/Http/Requests/HasSizeVariant.php @@ -0,0 +1,19 @@ +isAuthorized(); + + $errors = $this->formatErrors($collectErrors->get(config('app.skip_diagnostics_checks') ?? [])); + $infos = $authorized ? $collectInfo->get() : [self::ERROR_MSG]; + $configs = $authorized ? $collectConfig->get() : [self::ERROR_MSG]; + + return new DiagnosticInfo($errors, $infos, $configs, $checkUpdate->getCode()); + } + + /** + * Format the block. + * + * @param DiagnosticData[] $array + * + * @return string[] list of messages + */ + private function formatErrors(array $array): array + { + $ret = []; + foreach ($array as $elem) { + $prefix = match ($elem->type) { + MessageType::ERROR => 'Error: ', + MessageType::WARNING => 'Warning: ', + default => 'Info: ', + }; + $ret[] = $prefix . $elem->message; + foreach ($elem->details as $detail) { + $ret[] = ' ' . $detail; + } + } + + return $ret; + } + + /** + * Return the diagnostic information as a page. + * + * @return View + * + * @throws FrameworkException + * @throws InvalidTimeZoneException + * @throws LycheeException + */ + public function view(): View + { + try { + return view('diagnostics', $this->get()); + } catch (BindingResolutionException $e) { + throw new FrameworkException('Laravel\'s view component', $e); + } + } + + /** + * Return the size used by Lychee. + * We now separate this from the initial get() call as this is quite time consuming. + * + * @return string[] list of messages + * + * @throws ModelDBException + */ + public function getSize(Space $space): array + { + return $this->isAuthorized() ? $space->get() : [self::ERROR_MSG]; + } + + /** + * Return the table of access permissions currently available on the server. + * + * @return View + */ + public function getFullAccessPermissions(AlbumQueryPolicy $albumQueryPolicy): View + { + if (!$this->isAuthorized() && config('app.debug') !== true) { + throw new UnauthorizedException(); + } + + $data1 = AccessPermission::query() + ->join('base_albums', 'base_albums.id', '=', APC::BASE_ALBUM_ID) + ->select([ + APC::BASE_ALBUM_ID, + APC::IS_LINK_REQUIRED, + APC::GRANTS_FULL_PHOTO_ACCESS, + APC::GRANTS_DOWNLOAD, + APC::GRANTS_EDIT, + APC::GRANTS_UPLOAD, + APC::GRANTS_DELETE, + APC::PASSWORD, + APC::USER_ID, + 'title', + ]) + ->when( + Auth::check(), + fn ($q1) => $q1 + ->where(APC::USER_ID, '=', Auth::id()) + ->orWhere( + fn ($q2) => $q2->whereNull(APC::USER_ID) + ->whereNotIn( + 'access_permissions.' . APC::BASE_ALBUM_ID, + fn ($q3) => $q3->select('acc_per.' . APC::BASE_ALBUM_ID) + ->from('access_permissions', 'acc_per') + ->where(APC::USER_ID, '=', Auth::id()) + ) + ) + ) + ->when( + !Auth::check(), + fn ($q1) => $q1->whereNull(APC::USER_ID) + ) + ->orderBy(APC::BASE_ALBUM_ID) + ->get(); + + $query2 = DB::table('base_albums'); + $albumQueryPolicy->joinSubComputedAccessPermissions($query2, 'base_albums.id', 'inner', '', true); + $data2 = $query2 + ->select([ + APC::BASE_ALBUM_ID, + APC::IS_LINK_REQUIRED, + APC::GRANTS_FULL_PHOTO_ACCESS, + APC::GRANTS_DOWNLOAD, + APC::GRANTS_EDIT, + APC::GRANTS_UPLOAD, + APC::GRANTS_DELETE, + APC::PASSWORD, + APC::USER_ID, + 'title', + ]) + ->orderBy(APC::BASE_ALBUM_ID) + ->get() + ->map(function ($e) { + $e->is_link_required = $e->is_link_required === 1; + $e->grants_download = $e->grants_download === 1; + $e->grants_upload = $e->grants_upload === 1; + $e->grants_delete = $e->grants_delete === 1; + $e->grants_edit = $e->grants_edit === 1; + $e->grants_full_photo_access = $e->grants_full_photo_access === 1; + + return $e; + }); + + return view('access-permissions', ['data1' => json_encode($data1, JSON_PRETTY_PRINT), 'data2' => json_encode($data2, JSON_PRETTY_PRINT)]); + } +} diff --git a/app/Legacy/V1/Controllers/Administration/JobController.php b/app/Legacy/V1/Controllers/Administration/JobController.php new file mode 100644 index 00000000000..ef08a0e7f14 --- /dev/null +++ b/app/Legacy/V1/Controllers/Administration/JobController.php @@ -0,0 +1,49 @@ + + * + * @throws QueryBuilderException + */ + public function list(ShowJobsRequest $request, string $order = 'desc'): Collection + { + // PHPStan does not understand that `get` returns `Collection`, but assumes that it returns `Collection` + // @phpstan-ignore-next-line + return JobHistory::query() + ->orderBy('id', $order) + ->limit(Configs::getValueAsInt('log_max_num_line')) + ->get(); + } + + /** + * display the Jobs. + * + * @return View + * + * @throws QueryBuilderException + */ + public function view(ShowJobsRequest $request): View + { + return view('jobs', ['jobs' => $this->list($request)]); + } +} diff --git a/app/Legacy/V1/Controllers/Administration/OptimizeController.php b/app/Legacy/V1/Controllers/Administration/OptimizeController.php new file mode 100644 index 00000000000..1a5834c5548 --- /dev/null +++ b/app/Legacy/V1/Controllers/Administration/OptimizeController.php @@ -0,0 +1,42 @@ +optimizeDb = $optimizeDb; + $this->optimizeTables = $optimizeTables; + } + + /** + * display the Jobs. + * + * @return View + * + * @throws QueryBuilderException + */ + public function view(OptimizeRequest $request): View + { + $result = collect($this->optimizeDb->do())->merge(collect($this->optimizeTables->do()))->all(); + + return view('list', ['lines' => $result]); + } +} diff --git a/app/Legacy/V1/Controllers/Administration/SettingsController.php b/app/Legacy/V1/Controllers/Administration/SettingsController.php new file mode 100644 index 00000000000..e7255b0d3b9 --- /dev/null +++ b/app/Legacy/V1/Controllers/Administration/SettingsController.php @@ -0,0 +1,453 @@ +photoSortingColumn()); + Configs::set('sorting_photos_order', $request->photoSortingOrder()); + Configs::set('sorting_albums_col', $request->albumSortingColumn()); + Configs::set('sorting_albums_order', $request->albumSortingOrder()); + } + + /** + * Set the lang used by the Lychee installation. + * + * @param SetLangSettingRequest $request + * + * @return void + * + * @throws InvalidConfigOption + */ + public function setLang(SetLangSettingRequest $request): void + { + Configs::set($request->getSettingName(), $request->getSettingValue()); + } + + /** + * Set the layout of the albums + * 0: squares + * 1: flickr justified + * 2: flickr unjustified. + * + * @param SetLayoutSettingRequest $request + * + * @return void + * + * @throws InvalidConfigOption + */ + public function setLayout(SetLayoutSettingRequest $request): void + { + Configs::set($request->getSettingName(), $request->getSettingValue()); + } + + /** + * Set the dropbox key for the API. + * + * @param SetDropboxKeySettingRequest $request + * + * @return void + * + * @throws InvalidConfigOption + */ + public function setDropboxKey(SetDropboxKeySettingRequest $request): void + { + Configs::set($request->getSettingName(), $request->getSettingValue()); + } + + /** + * Allow public user to use the search function. + * + * @param SetPublicSearchSettingRequest $request + * + * @return void + * + * @throws InvalidConfigOption + * @throws BadRequestException + */ + public function setPublicSearch(SetPublicSearchSettingRequest $request): void + { + Configs::set($request->getSettingName(), $request->getSettingValue()); + } + + /** + * Given a smart album we (un)set the public properties. + * TODO: Give possibility to also change the grants_full_photo_access and grants_download. + * + * @param SetSmartAlbumVisibilityRequest $request + * + * @return void + * + * @throws ConfigurationKeyMissingException + * @throws ModelDBException + */ + public function setSmartAlbumVisibility(SetSmartAlbumVisibilityRequest $request): void + { + /** @var BaseSmartAlbum $album */ + $album = $request->album(); + if ($request->is_public() && $album->public_permissions() === null) { + $access_permissions = AccessPermission::ofPublic(); + $access_permissions->base_album_id = $album->id; + $access_permissions->save(); + } + + if (!$request->is_public() && $album->public_permissions() !== null) { + $perm = $album->public_permissions(); + $perm->delete(); + } + } + + /** + * Show NSFW albums by default or not. + * + * @param SetNSFWVisibilityRequest $request + * + * @return void + * + * @throws InvalidConfigOption + * @throws BadRequestException + */ + public function setNSFWVisible(SetNSFWVisibilityRequest $request): void + { + Configs::set($request->getSettingName(), $request->getSettingValue()); + } + + /** + * Select the decorations of albums. + * + * Sub-album and photo counts: + * none: no badges. + * layers: show folder icon on albums with sub-albums (if any). + * album: like 'original' but with number of sub-albums (if any). + * photo: show number of photos in album (if any). + * all: show number of sub-albums as well as photos. + * + * Orientation of album decorations. This is only relevant if + * both sub-album and photo decorations are shown. These are simply + * the options for CSS 'flex-direction': + * row: horizontal decorations (photos, albums). + * row-reverse: horizontal decorations (albums, photos). + * column: vertical decorations (top albums, bottom photos). + * column-reverse: vertical decorations (top photos, bottom albums). + * + * @param SetAlbumDecorationRequest $request + * + * @return void + * + * @throws InvalidConfigOption + */ + public function setAlbumDecoration(SetAlbumDecorationRequest $request): void + { + Configs::set('album_decoration', $request->albumDecoration()); + Configs::set('album_decoration_orientation', $request->albumDecorationOrientation()); + } + + /** + * Select the image overlay used: + * none: no overlay + * desc: description of the photo + * date: date of the photo + * exif: exif information. + * + * @param SetImageOverlaySettingRequest $request + * + * @return void + * + * @throws InvalidConfigOption + */ + public function setImageOverlayType(SetImageOverlaySettingRequest $request): void + { + Configs::set($request->getSettingName(), $request->getSettingValue()); + } + + /** + * Define the default license of the pictures. + * + * @param SetDefaultLicenseSettingRequest $request + * + * @return void + * + * @throws InvalidConfigOption + */ + public function setDefaultLicense(SetDefaultLicenseSettingRequest $request): void + { + Configs::set($request->getSettingName(), $request->getSettingValue()); + } + + /** + * Enable display of photo coordinates on map. + * + * @param SetMapDisplaySettingRequest $request + * + * @return void + * + * @throws InvalidConfigOption + * @throws BadRequestException + */ + public function setMapDisplay(SetMapDisplaySettingRequest $request): void + { + Configs::set($request->getSettingName(), $request->getSettingValue()); + } + + /** + * Enable display of photos on map for public albums. + * + * @param SetMapDisplayPublicSettingRequest $request + * + * @return void + * + * @throws InvalidConfigOption + * @throws BadRequestException + */ + public function setMapDisplayPublic(SetMapDisplayPublicSettingRequest $request): void + { + Configs::set($request->getSettingName(), $request->getSettingValue()); + } + + /** + * Set provider of OSM map tiles. + * + * This configuration option is not used by the backend itself, but only + * by the frontend. + * The configured value is transmitted to the frontend as part of the + * response for `Session::init` + * (cp. {@link \App\Legacy\V1\Controllers\SessionController::init()}) as the + * confidentiality of this configuration option is `public`. + * + * @param SetMapProviderSettingRequest $request + * + * @return void + * + * @throws InvalidConfigOption + */ + public function setMapProvider(SetMapProviderSettingRequest $request): void + { + Configs::set($request->getSettingName(), $request->getSettingValue()); + } + + /** + * Enable display of photos of sub-albums on map. + * + * @param SetMapIncludeSubAlbumsSettingRequest $request + * + * @return void + * + * @throws InvalidConfigOption + * @throws BadRequestException + */ + public function setMapIncludeSubAlbums(SetMapIncludeSubAlbumsSettingRequest $request): void + { + Configs::set($request->getSettingName(), $request->getSettingValue()); + } + + /** + * Enable decoding of GPS data into location names. + * + * @param SetLocationDecodingSettingRequest $request + * + * @return void + * + * @throws InvalidConfigOption + * @throws BadRequestException + */ + public function setLocationDecoding(SetLocationDecodingSettingRequest $request): void + { + Configs::set($request->getSettingName(), $request->getSettingValue()); + } + + /** + * Enable display of location name. + * + * @param SetLocationShowSettingRequest $request + * + * @return void + * + * @throws InvalidConfigOption + * @throws BadRequestException + */ + public function setLocationShow(SetLocationShowSettingRequest $request): void + { + Configs::set($request->getSettingName(), $request->getSettingValue()); + } + + /** + * Enable display of location name for public albums. + * + * @param SetLocationShowPublicSettingRequest $request + * + * @return void + * + * @throws InvalidConfigOption + * @throws BadRequestException + */ + public function setLocationShowPublic(SetLocationShowPublicSettingRequest $request): void + { + Configs::set($request->getSettingName(), $request->getSettingValue()); + } + + /** + * Enable sending of new photos notification emails. + * + * @param SetNewPhotosNotificationSettingRequest $request + * + * @return void + * + * @throws InvalidConfigOption + * @throws BadRequestException + */ + public function setNewPhotosNotification(SetNewPhotosNotificationSettingRequest $request): void + { + Configs::set($request->getSettingName(), $request->getSettingValue()); + } + + /** + * Takes the css input text and put it into `dist/user.css`. + * This allows admins to actually personalize the look of their + * installation. + * + * @param SetCSSSettingRequest $request + * + * @return void + * + * @throws InsufficientFilesystemPermissions + */ + public function setCSS(SetCSSSettingRequest $request): void + { + /** @var string $css */ + $css = $request->getSettingValue(); + if (Storage::disk('dist')->put('user.css', $css) === false) { + if (Storage::disk('dist')->get('user.css') !== $css) { + throw new InsufficientFilesystemPermissions('Could not save CSS'); + } + } + } + + /** + * Takes the js input text and put it into `dist/custom.js`. + * This allows admins to actually execute custom js code on their + * Lychee-Laravel installation. + * + * @param SetJSSettingRequest $request + * + * @return void + * + * @throws InsufficientFilesystemPermissions + */ + public function setJS(SetJSSettingRequest $request): void + { + /** @var string $js */ + $js = $request->getSettingValue(); + if (Storage::disk('dist')->put('custom.js', $js) === false) { + if (Storage::disk('dist')->get('custom.js') !== $js) { + throw new InsufficientFilesystemPermissions('Could not save JS'); + } + } + } + + /** + * Returns ALL settings. This is not filtered! + * Fortunately, this is behind an admin middleware. + * This is used in the advanced settings part. + * + * @return Collection + * + * @throws QueryBuilderException + */ + public function getAll(GetSetAllSettingsRequest $request): Collection + { + return Configs::query() + ->orderBy('cat') + ->orderBy('id') + // Only display settings which are not part of SE + ->where('level', '=', 0) + ->whereNotIn('key', ['email']) + ->get(); + } + + /** + * Get a list of settings and save them in the database + * if the associated key exists. + * + * @param GetSetAllSettingsRequest $request + * + * @return void + * + * @throws InvalidConfigOption + */ + public function saveAll(GetSetAllSettingsRequest $request): void + { + $lastException = null; + // Select all the SE settings. + $except = DB::table('configs') + ->select('key') + ->where('level', '=', '1') + ->pluck('key') + // Concat bunch of things coming from the POST request. + ->concat(['_token', 'function', '/api/Settings::saveAll']) + // Convert to array. + ->all(); + foreach ($request->except($except) as $key => $value) { + $value ??= ''; + try { + Configs::set($key, $value); + } catch (InvalidConfigOption $e) { + $lastException = $e; + } + } + if ($lastException !== null) { + throw $lastException; + } + } +} diff --git a/app/Legacy/V1/Controllers/Administration/SharingController.php b/app/Legacy/V1/Controllers/Administration/SharingController.php new file mode 100644 index 00000000000..396ee325d6b --- /dev/null +++ b/app/Legacy/V1/Controllers/Administration/SharingController.php @@ -0,0 +1,132 @@ +do($request->participant(), $request->owner(), $request->album()); + } + + /** + * Add a sharing between selected users and selected albums. + * + * @param AddSharesRequest $request + * + * @return void + * + * @throws QueryBuilderException + */ + public function add(AddSharesRequest $request): void + { + $this->updateLinks($request->userIDs(), $request->albumIDs()); + } + + /** + * Set the shares for the given album. + * + * Note: This method *sets* the shares (in contrast to *add*). + * This means, any user not given in the list of user IDs is removed + * if the album has been shared with this user before. + * + * @param SetSharesByAlbumRequest $request + * + * @return void + */ + public function setByAlbum(SetSharesByAlbumRequest $request): void + { + // Clear previous (otherwise we can only add). + try { + DB::table(APC::ACCESS_PERMISSIONS) + ->where(APC::BASE_ALBUM_ID, '=', $request->album()->id) + ->delete(); + } catch (\Throwable $e) { + throw new QueryBuilderException($e); + } + + $this->updateLinks($request->userIDs(), [$request->album()->id]); + } + + /** + * Apply the modification. + * + * @param array $userIds + * @param array $albumIDs + * + * @return void + */ + private function updateLinks(array $userIds, array $albumIDs): void + { + /** @var Collection $users */ + $users = User::query() + ->whereIn('id', $userIds) + ->get(); + + /** @var User $user */ + foreach ($users as $user) { + $user->shared()->syncWithPivotValues( + $albumIDs, + [ + APC::IS_LINK_REQUIRED => false, // In sharing no required link is needed + APC::GRANTS_DOWNLOAD => Configs::getValueAsBool('grants_download'), + APC::GRANTS_FULL_PHOTO_ACCESS => Configs::getValueAsBool('grants_full_photo_access'), + ], + false); + } + } + + /** + * Given a list of shared ID we delete them + * This function is the only reason why we test SharedIDs in + * app/Http/Middleware/UploadCheck.php. + * + * FIXME: make sure that the Lychee-front is sending the correct ShareIDs + * + * @param DeleteSharingRequest $request + * + * @return void + * + * @throws QueryBuilderException + */ + public function delete(DeleteSharingRequest $request): void + { + try { + DB::table(APC::ACCESS_PERMISSIONS) + ->whereIn('id', $request->shareIDs()) + ->delete(); + } catch (\Throwable $e) { + throw new QueryBuilderException($e); + } + } +} diff --git a/app/Legacy/V1/Controllers/Administration/UpdateController.php b/app/Legacy/V1/Controllers/Administration/UpdateController.php new file mode 100644 index 00000000000..c5488bf8870 --- /dev/null +++ b/app/Legacy/V1/Controllers/Administration/UpdateController.php @@ -0,0 +1,147 @@ +applyUpdate = $applyUpdate; + } + + /** + * Return if up to date or the number of commits behind + * This invalidates the cache for the url. + * + * @param UpdateRequest $request + * + * @return array{updateStatus: string} + * + * @throws VersionControlException + */ + public function check(UpdateRequest $request): array + { + $gitHubFunctions = resolve(GitHubVersion::class); + $gitHubFunctions->hydrate(true, false); + + return ['updateStatus' => $gitHubFunctions->getBehindTest()]; + } + + /** + * Updates Lychee and returns the messages as a JSON object. + * + * The method requires PHP to have shell access. + * Except for the return type this method is identical to + * {@link UpdateController::view()}. + * + * @param UpdateRequest $request + * + * @return array{updateMsgs: array} + * + * @throws LycheeException + */ + public function apply(UpdateRequest $request): array + { + UpdatableCheck::assertUpdatability(); + + return ['updateMsgs' => $this->applyUpdate->run()]; + } + + /** + * Updates Lychee and returns the messages as an HTML view. + * + * The method requires PHP to have shell access. + * Except for the return type this method is identical to + * {@link UpdateController::apply()}. + * + * @param UpdateRequest $request + * + * @return View + * + * @throws LycheeException + */ + public function view(UpdateRequest $request): View + { + UpdatableCheck::assertUpdatability(); + + $output = $this->applyUpdate->run(); + + return view('update.results', ['code' => '200', 'message' => 'Upgrade results', 'output' => $output]); + } + + /** + * Migrates the Lychee DB and returns a HTML view. + * + * **TODO:** Consolidate with {@link \App\Http\Controllers\Install\MigrationController::view()}. + * + * **ATTENTION:** This method serves a somewhat similar purpose as + * `MigrationController::view()` except that the latter does not only + * trigger a migration, but also generates a new API key. + * Also note, that this method internally uses + * {@link ApplyUpdate::migrate()} while `MigrationController::view` + * uses {@link \App\Actions\InstallUpdate\ApplyMigration::migrate()}. + * However, both methods are very similar, too. + * The whole code around installation/upgrade/migration should + * thoroughly be revised an refactored. + * + * @param MigrateRequest $request + * + * @return View|Response + */ + public function migrate(MigrateRequest $request): View|Response + { + $output = []; + $output = $this->applyUpdate->run(); + + return view('update.results', ['code' => '200', 'message' => 'Migration results', 'output' => $output]); + } +} diff --git a/app/Legacy/V1/Controllers/Administration/UserController.php b/app/Legacy/V1/Controllers/Administration/UserController.php new file mode 100644 index 00000000000..3ea2729606d --- /dev/null +++ b/app/Legacy/V1/Controllers/Administration/UserController.php @@ -0,0 +1,123 @@ +do( + $request->username(), + $request->password(), + $request->oldPassword(), + $request->ip() + ); + // Update the session with the new credentials of the user. + // Otherwise, the session is out-of-sync and falsely assumes the user + // to be unauthenticated upon the next request. + Auth::login($currentUser); + + return UserResource::make($currentUser); + } + + /** + * Updates the email address of the currently authenticated user. + * Deletes all notifications if the email address is empty. + * + * TODO: Why is this an independent request? IMHO this should be combined with the other user settings. + * + * @param SetEmailRequest $request + * + * @return void + * + * @throws InternalLycheeException + * @throws ModelDBException + * @throws UnauthenticatedException + */ + public function setEmail(SetEmailRequest $request): void + { + try { + /** @var User $user */ + $user = Auth::user() ?? throw new UnauthenticatedException(); + + $user->email = $request->email(); + + if ($request->email() === null) { + $user->notifications()->delete(); + } + + $user->save(); + } catch (\InvalidArgumentException $e) { + throw new FrameworkException('Laravel\'s notification module', $e); + } + } + + /** + * Returns the currently authenticated user or `null` if no user + * is authenticated. + * + * @return UserResource + */ + public function getAuthenticatedUser(): UserResource + { + return UserResource::make(Auth::user() ?? throw new UnauthenticatedException()); + } + + /** + * Reset the token of the currently authenticated user. + * + * @return array{'token': string} + * + * @throws UnauthenticatedException + * @throws ModelDBException + * @throws \Exception + */ + public function resetToken(ChangeTokenRequest $request, TokenReset $tokenReset): array + { + $token = $tokenReset->do(); + + return ['token' => $token]; + } + + /** + * Disable the token of the currently authenticated user. + * + * @return void + * + * @throws UnauthenticatedException + * @throws ModelDBException + */ + public function unsetToken(ChangeTokenRequest $request, TokenDisable $tokenDisable): void + { + $tokenDisable->do(); + } +} diff --git a/app/Legacy/V1/Controllers/Administration/UsersController.php b/app/Legacy/V1/Controllers/Administration/UsersController.php new file mode 100644 index 00000000000..c08a70ce4a9 --- /dev/null +++ b/app/Legacy/V1/Controllers/Administration/UsersController.php @@ -0,0 +1,107 @@ + + * + * @throws QueryBuilderException + */ + public function list(ListUsersRequest $request): ResourceCollection + { + return UserManagementResource::collection(User::query()->whereNot('id', '=', Auth::id())->get()); + } + + /** + * Save modification done to a user. + * Note that an admin can change the password of a user at will. + * + * @param SetUserSettingsRequest $request + * @param Save $save + * + * @return void + * + * @throws InvalidPropertyException + * @throws ModelDBException + */ + public function save(SetUserSettingsRequest $request, Save $save): void + { + $save->do( + $request->user2(), + $request->username(), + $request->password(), + $request->mayUpload(), + $request->mayEditOwnSettings() + ); + } + + /** + * Deletes a user. + * + * The albums and photos owned by the user are re-assigned to the + * admin user. + * + * @param DeleteUserRequest $request + * + * @return void + * + * @throws ModelDBException + * @throws UnauthenticatedException + * @throws InvalidFormatException + */ + public function delete(DeleteUserRequest $request): void + { + if ($request->user2()->id === Auth::id()) { + throw new UnauthorizedException('You are not allowed to delete yourself'); + } + $request->user2()->delete(); + } + + /** + * Create a new user. + * + * @param AddUserRequest $request + * @param Create $create + * + * @return UserManagementResource + * + * @throws InvalidPropertyException + * @throws ModelDBException + */ + public function create(AddUserRequest $request, Create $create): UserManagementResource + { + $user = $create->do( + username: $request->username(), + password: $request->password(), + mayUpload: $request->mayUpload(), + mayEditOwnSettings: $request->mayEditOwnSettings()); + + return UserManagementResource::make($user)->setStatus(201); + } +} diff --git a/app/Legacy/V1/Controllers/AlbumController.php b/app/Legacy/V1/Controllers/AlbumController.php new file mode 100644 index 00000000000..64504170baa --- /dev/null +++ b/app/Legacy/V1/Controllers/AlbumController.php @@ -0,0 +1,392 @@ +create($request->title(), $request->parentAlbum()); + + return AlbumResource::make($album)->setStatus(201); + } + + /** + * Add a new album generated by tags. + * + * @param AddTagAlbumRequest $request + * @param CreateTagAlbum $create + * + * @return TagAlbumResource + * + * @throws LycheeException + */ + public function addTagAlbum(AddTagAlbumRequest $request, CreateTagAlbum $create): TagAlbumResource + { + $tagAlbum = $create->create($request->title(), $request->tags()); + + return TagAlbumResource::make($tagAlbum)->setStatus(201); + } + + /** + * Provided an albumID, returns the album. + * + * @param GetAlbumRequest $request + * + * @return AlbumResource|TagAlbumResource|SmartAlbumResource + */ + public function get(GetAlbumRequest $request): AlbumResource|TagAlbumResource|SmartAlbumResource + { + return match (true) { + $request->album() instanceof BaseSmartAlbum => SmartAlbumResource::make($request->album()), + $request->album() instanceof TagAlbum => TagAlbumResource::make($request->album()), + $request->album() instanceof Album => AlbumResource::make($request->album()), + default => throw new LycheeLogicException('This should not happen'), + }; + } + + /** + * Provided an albumID, returns the album with only map related data. + * + * @param GetAlbumPositionDataRequest $request + * @param PositionData $positionData + * + * @return PositionDataResource + */ + public function getPositionData(GetAlbumPositionDataRequest $request, PositionData $positionData): PositionDataResource + { + return $positionData->get($request->album(), $request->includeSubAlbums()); + } + + /** + * Provided the albumID and password, return whether the album can be accessed or not. + * + * @param UnlockAlbumRequest $request + * @param Unlock $unlock + * + * @return void + * + * @throws LycheeException + */ + public function unlock(UnlockAlbumRequest $request, Unlock $unlock): void + { + $unlock->do($request->album(), $request->password()); + } + + /** + * Provided a title and albumIDs, change the title of the albums. + * + * @param SetAlbumsTitleRequest $request + * + * @return void + * + * @throws LycheeException + */ + public function setTitle(SetAlbumsTitleRequest $request): void + { + /** @var BaseAlbum $album */ + foreach ($request->albums() as $album) { + $album->title = $request->title(); + $album->save(); + } + } + + /** + * Sets the protection policy of the album. + * + * @param SetAlbumProtectionPolicyRequest $request + * @param SetProtectionPolicy $setProtectionPolicy + * + * @return void + * + * @throws LycheeException + */ + public function setProtectionPolicy(SetAlbumProtectionPolicyRequest $request, SetProtectionPolicy $setProtectionPolicy): void + { + $setProtectionPolicy->do( + $request->album(), + $request->albumProtectionPolicy(), + $request->isPasswordProvided(), + $request->password() + ); + } + + /** + * Change the description of the album. + * + * @param SetAlbumDescriptionRequest $request + * + * @return void + * + * @throws ModelDBException + */ + public function setDescription(SetAlbumDescriptionRequest $request): void + { + $request->album()->description = $request->description(); + $request->album()->save(); + } + + /** + * Change the copyright of the album. + * + * @param SetAlbumCopyrightRequest $request + * + * @return void + * + * @throws ModelDBException + */ + public function setCopyright(SetAlbumCopyrightRequest $request): void + { + $request->album()->copyright = $request->copyright(); + $request->album()->save(); + } + + /** + * Change show tags of the tag album. + * + * @param SetAlbumTagsRequest $request + * + * @return void + * + * @throws ModelDBException + */ + public function setShowTags(SetAlbumTagsRequest $request): void + { + $request->album()->show_tags = $request->tags(); + $request->album()->save(); + } + + /** + * Set cover image of the album. + * + * @param SetAlbumCoverRequest $request + * + * @return void + * + * @throws ModelDBException + */ + public function setCover(SetAlbumCoverRequest $request): void + { + $request->album()->cover_id = $request->photo()?->id; + $request->album()->save(); + } + + /** + * Set header image of the album. + * + * @param SetAlbumHeaderRequest $request + * + * @return void + */ + public function setHeader(SetAlbumHeaderRequest $request): void + { + $request->album()->header_id = $request->photo()?->id; + $request->album()->save(); + } + + /** + * Set the license of the Album. + * + * @param SetAlbumLicenseRequest $request + * + * @return void + * + * @throws ModelDBException + */ + public function setLicense(SetAlbumLicenseRequest $request): void + { + $request->album()->license = $request->license(); + $request->album()->save(); + } + + /** + * Upload a track for the Album. + * + * @param SetAlbumTrackRequest $request + * + * @return void + * + * @throws MediaFileOperationException + * @throws ModelDBException + */ + public function setTrack(SetAlbumTrackRequest $request): void + { + $request->album()->setTrack($request->file); + } + + /** + * Delete a track from the Album. + * + * @param DeleteTrackRequest $request + * + * @return void + * + * @throws ModelDBException + */ + public function deleteTrack(DeleteTrackRequest $request): void + { + $request->album()->deleteTrack(); + } + + /** + * Delete the album and all of its pictures. + * + * @param DeleteAlbumsRequest $request the request + * @param Delete $delete the delete action + * + * @return void + * + * @throws ModelDBException + * @throws MediaFileOperationException + */ + public function delete(DeleteAlbumsRequest $request, Delete $delete): void + { + $fileDeleter = $delete->do($request->albumIDs()); + App::terminating(fn () => $fileDeleter->do()); + } + + /** + * Merge albums. The first of the list is the destination of the merge. + * + * @param MergeAlbumsRequest $request + * @param Merge $merge + * + * @return void + * + * @throws LycheeException + * @throws ModelNotFoundException + */ + public function merge(MergeAlbumsRequest $request, Merge $merge): void + { + $merge->do($request->album(), $request->albums()); + } + + /** + * Move multiple albums into another album. + * + * @param MoveAlbumsRequest $request + * @param Move $move + * + * @return void + * + * @throws LycheeException + * @throws ModelNotFoundException + */ + public function move(MoveAlbumsRequest $request, Move $move): void + { + $move->do($request->album(), $request->albums()); + } + + /** + * Sets whether an album contains sensitive pictures. + * + * @param SetAlbumNSFWRequest $request + * + * @return void + * + * @throws ModelDBException + */ + public function setNSFW(SetAlbumNSFWRequest $request): void + { + $request->album()->is_nsfw = $request->isNSFW(); + $request->album()->save(); + } + + /** + * Define the default sorting type. + * + * @param SetAlbumSortingRequest $request + * + * @return void + * + * @throws LycheeException + */ + public function setSorting(SetAlbumSortingRequest $request): void + { + $request->album()->photo_sorting = $request->sortingCriterion(); + $request->album()->save(); + } + + /** + * Return the archive of the pictures of the album and its sub-albums. + * + * @param ArchiveAlbumsRequest $request + * @param Archive $archive + * + * @return StreamedResponse + * + * @throws LycheeException + */ + public function getArchive(ArchiveAlbumsRequest $request, Archive $archive): StreamedResponse + { + return $archive->do($request->albums()); + } +} diff --git a/app/Legacy/V1/Controllers/AlbumsController.php b/app/Legacy/V1/Controllers/AlbumsController.php new file mode 100644 index 00000000000..0602a577aa1 --- /dev/null +++ b/app/Legacy/V1/Controllers/AlbumsController.php @@ -0,0 +1,62 @@ +get(); + + return new TopAlbumsResource( + smart_albums: $dto->smart_albums, + tag_albums: $dto->tag_albums, + albums: $dto->albums, + shared_albums: $dto->shared_albums); + } + + /** + * @return AlbumForestResource the full tree of visible albums + * + * @throws LycheeException + */ + public function tree(Tree $tree): AlbumForestResource + { + return $tree->get(); + } + + /** + * @return PositionDataResource returns visible photos which have positioning data + * + * @throws LycheeException + */ + public function getPositionData(PositionData $positionData): PositionDataResource + { + return $positionData->do(); + } +} diff --git a/app/Legacy/V1/Controllers/ImportController.php b/app/Legacy/V1/Controllers/ImportController.php new file mode 100644 index 00000000000..56bea4dfd8e --- /dev/null +++ b/app/Legacy/V1/Controllers/ImportController.php @@ -0,0 +1,99 @@ +do($request->urls(), $request->album(), $currentUserId); + + return PhotoResource::collection($photos); + } + + /** + * @param ImportServerRequest $request + * @param FromServer $fromServer + * + * @return StreamedResponse + */ + public function server(ImportServerRequest $request, FromServer $fromServer): StreamedResponse + { + /** @var int $currentUserId */ + $currentUserId = Auth::id() ?? throw new UnauthenticatedException(); + + return $fromServer->do( + $request->paths(), $request->album(), $request->importMode(), $currentUserId + ); + } + + /** + * @return void + */ + public function serverCancel(CancelImportServerRequest $request): void + { + Session::put('cancel', true); + } +} diff --git a/app/Legacy/V1/Controllers/IndexController.php b/app/Legacy/V1/Controllers/IndexController.php new file mode 100644 index 00000000000..b13b423e32d --- /dev/null +++ b/app/Legacy/V1/Controllers/IndexController.php @@ -0,0 +1,238 @@ +symLinkFunctions = $symLinkFunctions; + } + + /** + * Display the landing page if enabled + * otherwise display the gallery. + * + * @return View + * + * @throws FrameworkException + * @throws ModelDBException + * @throws ConfigurationKeyMissingException + */ + public function show(): View + { + try { + if (Configs::getValueAsBool('landing_page_enable')) { + $infos = [ + 'owner' => Configs::getValueAsString('site_owner'), + 'title' => Configs::getValueAsString('landing_title'), + 'subtitle' => Configs::getValueAsString('landing_subtitle'), + 'facebook' => Configs::getValueAsString('sm_facebook_url'), + 'flickr' => Configs::getValueAsString('sm_flickr_url'), + 'twitter' => Configs::getValueAsString('sm_twitter_url'), + 'instagram' => Configs::getValueAsString('sm_instagram_url'), + 'youtube' => Configs::getValueAsString('sm_youtube_url'), + 'background' => Configs::getValueAsString('landing_background'), + 'copyright_enable' => Configs::getValueAsString('footer_show_copyright'), + 'copyright_year' => Configs::getValueAsString('site_copyright_begin'), + 'additional_footer_text' => Configs::getValueAsString('footer_additional_text'), + ]; + if (Configs::getValueAsString('site_copyright_begin') !== Configs::getValueAsString('site_copyright_end')) { + $infos['copyright_year'] = Configs::getValueAsString('site_copyright_begin') . '-' . Configs::getValueAsString('site_copyright_end'); + } + + $title = Configs::getValueAsString('site_title'); + $rss_enable = Configs::getValueAsBool('rss_enable'); + + $page_config = []; + $page_config['show_hosted_by'] = false; + $page_config['display_socials'] = true; + + return view('landing', [ + 'title' => $title, + 'infos' => $infos, + 'page_config' => $page_config, + 'rss_enable' => $rss_enable, + 'user_css_url' => self::getUserCustomFiles('user.css'), + 'user_js_url' => self::getUserCustomFiles('custom.js'), + ]); + } + + return $this->frontend(); + } catch (BindingResolutionException $e) { + throw new FrameworkException('Laravel\'s container component', $e); + } + } + + /** + * Just call the phpinfo function. + * Cannot be tested. + * + * @return void + * + * @codeCoverageIgnore + */ + public function phpinfo(): void + { + Gate::authorize(SettingsPolicy::CAN_SEE_DIAGNOSTICS, Configs::class); + + phpinfo(); + } + + /** + * Returns the frontend in "gallery mode". + * + * This is an alias for {@link IndexController::frontend()} with default + * parameters. + * + * @deprecated + * + * @return View + * + * @throws FrameworkException + * @throws ModelDBException + * @throws ConfigurationKeyMissingException + */ + public function gallery(): View + { + return $this->frontend(); + } + + /** + * Returns the frontend in "frame mode". + * + * This is an alias for {@link IndexController::frontend()} with default + * parameters. + * This method can be removed as soon as the frontend fully supports + * client-side navigation with a proper path component in the URL + * instead of using URL fragments. + * See: https://github.com/LycheeOrg/Lychee-front/issues/343 + * Until then, this method allows us to use `/frame` as the URL path + * by catching this URL on the server-side and returning the frontend. + * + * @deprecated + * + * @return View + * + * @throws FrameworkException + * @throws ModelDBException + * @throws ConfigurationKeyMissingException + */ + public function frame(): View + { + return $this->frontend(); + } + + /** + * Returns the frontend in "view mode". + * + * The view mode is used to display a single photo in a search engine + * and social media friendly way. + * This method can be removed as soon as the frontend fully supports + * client-side navigation with a proper path component in the URL + * instead of using URL fragments. + * See: https://github.com/LycheeOrg/Lychee-front/issues/343 + * Until then, this method allows us to use `/view` as the URL path + * by catching this URL on the server-side and returning the frontend. + * + * @deprecated + * + * @param GetPhotoViewRequest $request + * + * @return View + * + * @throws FrameworkException + * @throws ConfigurationKeyMissingException + * @throws ModelDBException + */ + public function view(GetPhotoViewRequest $request): View + { + $photo = $request->photo(); + + return $this->frontend( + $photo->title, + $photo->description, + url()->to($photo->size_variants->getMedium()?->url ?? $photo->size_variants->getOriginal()->url) + ); + } + + /** + * Returns the frontend with pre-rendered meta tags in the HTML header. + * + * @param string|null $title the specific title; this method prefixes the title with the site title + * @param string|null $description the description; this method appends `' – via Lychee'` to the description + * @param string|null $imageUrl an optional URL to an image displayed on the page + * + * @throws FrameworkException + * @throws ConfigurationKeyMissingException + * @throws ModelDBException + */ + protected function frontend(?string $title = null, ?string $description = null, ?string $imageUrl = null): View + { + try { + $this->symLinkFunctions->remove_outdated(); + $siteTitle = Configs::getValueAsString('site_title'); + $title ??= ''; + $description ??= ''; + + return view('frontend', [ + 'pageTitle' => $siteTitle . ($siteTitle !== '' && $title !== '' ? ' – ' : '') . $title, + 'pageDescription' => $description !== '' ? $description . ' – via Lychee' : '', + 'siteOwner' => Configs::getValueAsString('site_owner'), + 'imageUrl' => $imageUrl ?? '', + 'pageUrl' => url()->current(), + 'rssEnable' => Configs::getValueAsBool('rss_enable'), + 'bodyHtml' => file_get_contents(public_path('dist/frontend.html')), + 'userCssUrl' => self::getUserCustomFiles('user.css'), + 'userJsUrl' => self::getUserCustomFiles('custom.js'), + ]); + } catch (BindingResolutionException $e) { + throw new FrameworkException('Laravel\'s container component', $e); + } + } + + /** + * Returns user.css url with cache busting if file has been updated. + * + * @param string $fileName + * + * @return string + */ + public static function getUserCustomFiles(string $fileName): string + { + $cssCacheBusting = ''; + /** @disregard P1013 */ + if (Storage::disk('dist')->fileExists($fileName)) { + $cssCacheBusting = '?' . Storage::disk('dist')->lastModified($fileName); + } + + /** @disregard P1013 */ + return Storage::disk('dist')->url($fileName) . $cssCacheBusting; + } +} diff --git a/app/Legacy/V1/Controllers/LegacyController.php b/app/Legacy/V1/Controllers/LegacyController.php new file mode 100644 index 00000000000..abdff04ca61 --- /dev/null +++ b/app/Legacy/V1/Controllers/LegacyController.php @@ -0,0 +1,51 @@ +albumID(); + $legacyPhotoID = $request->photoID(); + + $return = ['albumID' => null, 'photoID' => null]; + if ($legacyAlbumID !== null) { + $return['albumID'] = Legacy::translateLegacyAlbumID($request->albumID(), $request); + } + if ($legacyPhotoID !== null) { + $return['photoID'] = Legacy::translateLegacyPhotoID($legacyPhotoID, $request); + } + + return $return; + } +} diff --git a/app/Legacy/V1/Controllers/PhotoController.php b/app/Legacy/V1/Controllers/PhotoController.php new file mode 100644 index 00000000000..db80676e34a --- /dev/null +++ b/app/Legacy/V1/Controllers/PhotoController.php @@ -0,0 +1,339 @@ +photo()); + } + + /** + * Returns a random photo (from the configured album). + * Only photos with enough access rights are included. + * This is used in the Frame Controller. + * + * @param PhotoQueryPolicy $photoQueryPolicy + * + * @return PhotoResource + * + * @throws ModelNotFoundException + * @throws InternalLycheeException + * @throws \InvalidArgumentException + * + * @noinspection PhpIncompatibleReturnTypeInspection + */ + public function getRandom(PhotoQueryPolicy $photoQueryPolicy): PhotoResource + { + $randomAlbumId = Configs::getValueAsString('random_album_id'); + + if ($randomAlbumId === '') { + $query = $photoQueryPolicy->applySearchabilityFilter( + query: Photo::query()->with(['album', 'size_variants', 'size_variants.sym_links']), + origin: null, + include_nsfw: !Configs::getValueAsBool('hide_nsfw_in_frame')); + } else { + $query = $this->albumFactory->findAbstractAlbumOrFail($randomAlbumId) + ->photos() + ->with(['album', 'size_variants', 'size_variants.sym_links']); + } + + $num = $query->count() - 1; + + return PhotoResource::make($query->skip(rand(0, $num))->firstOrFail()); + } + + /** + * Adds a photo given an AlbumID. + * + * @param AddPhotoRequest $request + * + * @return PhotoResource|JsonResponse + * + * @throws LycheeException + * @throws ModelNotFoundException + */ + public function add(AddPhotoRequest $request): PhotoResource|JsonResponse + { + // This code is a nasty work-around which should not exist. + // PHP stores a temporary copy of the uploaded file without a file + // extension. + // Unfortunately, most of our methods pass around absolute file paths + // instead of proper `File` object. + // During the process we have a lot of code which tries to + // re-determine the MIME type of the file based on the file path. + // This is not only inefficient, but the original MIME type (of the + // uploaded file) gets lost on the way. + // As a work-around we store the uploaded file with a file extension. + // Unfortunately, we cannot simply re-name the file, because this + // might break due to permission problems for certain installation + // if the temporarily uploaded file is stored in the system-global + // temporary directory below another mount point or another Docker + // image than the Lychee installation. + // Hence, we must make a deep copy. + // TODO: Remove this code again, if all other TODOs regarding MIME and file handling are properly refactored and we have stopped using absolute file paths as the least common denominator to pass around files. + $uploadedFile = new UploadedFile($request->uploadedFile()); + $processableFile = new ProcessableJobFile( + $uploadedFile->getOriginalExtension(), + $uploadedFile->getOriginalBasename() + ); + $processableFile->write($uploadedFile->read()); + + $uploadedFile->close(); + $uploadedFile->delete(); + $processableFile->close(); + // End of work-around + + if (Configs::getValueAsBool('use_job_queues')) { + ProcessImageJob::dispatch($processableFile, $request->album(), $request->fileLastModifiedTime()); + + return new JsonResponse(null, 201); + } + + $job = new ProcessImageJob($processableFile, $request->album(), $request->fileLastModifiedTime()); + $photo = $job->handle($this->albumFactory); + $isNew = $photo->created_at->toIso8601String() === $photo->updated_at->toIso8601String(); + + return PhotoResource::make($photo)->setStatus($isNew ? 201 : 200); + } + + /** + * Change the title of a photo. + * + * @param SetPhotosTitleRequest $request + * + * @return void + * + * @throws LycheeException + */ + public function setTitle(SetPhotosTitleRequest $request): void + { + $title = $request->title(); + /** @var Photo $photo */ + foreach ($request->photos() as $photo) { + $photo->title = $title; + $photo->save(); + } + } + + /** + * Set the is-starred attribute of the given photos. + * + * @param SetPhotosStarredRequest $request + * + * @return void + */ + public function setStar(SetPhotosStarredRequest $request): void + { + /** @var Photo $photo */ + foreach ($request->photos() as $photo) { + $photo->is_starred = $request->isStarred(); + $photo->save(); + } + } + + /** + * Set the description of a photo. + * + * @param SetPhotoDescriptionRequest $request + * + * @return void + */ + public function setDescription(SetPhotoDescriptionRequest $request): void + { + $request->photo()->description = $request->description(); + $request->photo()->save(); + } + + /** + * Set the tags of a photo. + * + * @param SetPhotosTagsRequest $request + * + * @return void + */ + public function setTags(SetPhotosTagsRequest $request): void + { + $tags = $request->tags(); + + /** @var Photo $photo */ + foreach ($request->photos() as $photo) { + if ($request->shallOverride) { + $photo->tags = $tags; + } else { + $photo->tags = array_unique(array_merge($photo->tags, $tags)); + } + $photo->save(); + } + } + + /** + * Moves the photos to an album. + * + * @param MovePhotosRequest $request + * @param Move $move + * + * @return void + * + * @throws LycheeException + */ + public function setAlbum(MovePhotosRequest $request, Move $move): void + { + $move->do($request->photos(), $request->album()); + } + + /** + * Sets the license of the photo. + * + * @param SetPhotoLicenseRequest $request + * + * @return void + * + * @throws LycheeException + */ + public function setLicense(SetPhotoLicenseRequest $request): void + { + $request->photo()->license = $request->license(); + $request->photo()->save(); + } + + /** + * Sets the license of the photo. + * + * @param SetPhotoUploadDateRequest $request + * + * @return void + * + * @throws LycheeException + */ + public function setUploadDate(SetPhotoUploadDateRequest $request): void + { + $request->photo()->created_at = $request->requestDate(); + $request->photo()->save(); + } + + /** + * Delete one or more photos. + * + * @param DeletePhotosRequest $request + * @param Delete $delete + * + * @return void + * + * @throws ModelDBException + * @throws MediaFileOperationException + */ + public function delete(DeletePhotosRequest $request, Delete $delete): void + { + $fileDeleter = $delete->do($request->photoIDs()); + App::terminating(fn () => $fileDeleter->do()); + } + + /** + * Duplicates a set of photos. + * Only the SQL entry is duplicated for space reason. + * + * @param DuplicatePhotosRequest $request + * @param Duplicate $duplicate + * + * @return JsonResponse the collection of duplicated photos + * + * @throws ModelDBException + */ + public function duplicate(DuplicatePhotosRequest $request, Duplicate $duplicate): JsonResponse + { + $duplicates = $duplicate->do($request->photos(), $request->album()); + + return PhotoResource::collection($duplicates)->toResponse($request)->setStatusCode(201); + } + + /** + * Return the archive of pictures or just a picture if only one. + * + * @param ArchivePhotosRequest $request + * @param Archive $archive + * + * @return SymfonyResponse + * + * @throws LycheeException + */ + public function getArchive(ArchivePhotosRequest $request, Archive $archive): SymfonyResponse + { + return $archive->do($request->photos(), $request->sizeVariant()); + } + + /** + * GET to manually clear the symlinks. + * + * @param ClearSymLinkRequest $request + * + * @return void + * + * @throws ModelDBException + * @throws LycheeException + */ + public function clearSymLink(ClearSymLinkRequest $request): void + { + $this->symLinkFunctions->clearSymLink(); + } +} diff --git a/app/Legacy/V1/Controllers/PhotoEditorController.php b/app/Legacy/V1/Controllers/PhotoEditorController.php new file mode 100644 index 00000000000..feccc6bcae8 --- /dev/null +++ b/app/Legacy/V1/Controllers/PhotoEditorController.php @@ -0,0 +1,41 @@ +photo(), $request->direction()); + $photo = $rotateStrategy->do(); + + return PhotoResource::make($photo); + } +} diff --git a/app/Legacy/V1/Controllers/RSSController.php b/app/Legacy/V1/Controllers/RSSController.php new file mode 100644 index 00000000000..6c01181c20b --- /dev/null +++ b/app/Legacy/V1/Controllers/RSSController.php @@ -0,0 +1,39 @@ + + * + * @throws LycheeException + */ + public function getRSS(Generate $generate): Collection + { + if (!Configs::getValueAsBool('rss_enable')) { + throw new ConfigurationException('RSS is disabled by configuration'); + } + + return $generate->do(); + } +} diff --git a/app/Legacy/V1/Controllers/RedirectController.php b/app/Legacy/V1/Controllers/RedirectController.php new file mode 100644 index 00000000000..137ad2da0f2 --- /dev/null +++ b/app/Legacy/V1/Controllers/RedirectController.php @@ -0,0 +1,117 @@ +unlock = $unlock; + $this->albumFactory = $albumFactory; + } + + /** + * Trivial redirection. + * + * @param Request $request + * @param string $albumID + * + * @return RedirectResponse + * + * @throws LycheeException + * @throws ModelNotFoundException + */ + public function album(Request $request, string $albumID): SymfonyResponse + { + return $this->photo($request, $albumID, null); + } + + /** + * Trivial redirection. + * + * @param Request $request + * @param string $albumID + * @param string|null $photoID + * + * @return RedirectResponse + * + * @throws LycheeException + * @throws ModelNotFoundException + */ + public function photo(Request $request, string $albumID, ?string $photoID): SymfonyResponse + { + try { + if (Legacy::isLegacyModelID($albumID)) { + $albumID = Legacy::translateLegacyAlbumID(intval($albumID), $request); + } + + if ($photoID !== null && Legacy::isLegacyModelID($photoID)) { + $photoID = Legacy::translateLegacyPhotoID(intval($photoID), $request); + } + + if ( + $request->filled('password') && + Configs::getValueAsBool('unlock_password_photos_with_url_param') + ) { + $album = $this->albumFactory->findBaseAlbumOrFail($albumID); + $this->unlock->do($album, $request['password']); + } + + // If we are using vuejs by default, we redirect to vuejs url intead. + if (Features::active('vuejs')) { + return $photoID === null ? + redirect(route('gallery-album', ['albumId' => $albumID])) : + redirect(route('gallery-photo', ['albumId' => $albumID, 'photoId' => $photoID])); + } + + return $photoID === null ? + redirect('gallery#' . $albumID) : + redirect('gallery#' . $albumID . '/' . $photoID); + } catch (BindingResolutionException $e) { + throw new FrameworkException('Lychee redirection component', $e); + } + } + + /** + * Redirection to landing or gallery depending on the settings. + * Otherwise attach a JS hook if legacy is enabled. + * + * @return View|SymfonyResponse + */ + public function view(): View|SymfonyResponse + { + $base_route = Configs::getValueAsBool('landing_page_enable') ? route('landing') : route('gallery'); + if (Features::active('legacy_v4_redirect') === false) { + return redirect($base_route); + } + + return view('hook-redirection', [ + 'gallery' => route('gallery'), + 'base' => $base_route, + ]); + } +} diff --git a/app/Legacy/V1/Controllers/SearchController.php b/app/Legacy/V1/Controllers/SearchController.php new file mode 100644 index 00000000000..6386ca38fc3 --- /dev/null +++ b/app/Legacy/V1/Controllers/SearchController.php @@ -0,0 +1,41 @@ +queryAlbums($request->terms()), + $albumSearch->queryTagAlbums($request->terms()), + $photoSearch->query($request->terms()) + ); + } +} diff --git a/app/Legacy/V1/Controllers/SessionController.php b/app/Legacy/V1/Controllers/SessionController.php new file mode 100644 index 00000000000..f91c5ad9323 --- /dev/null +++ b/app/Legacy/V1/Controllers/SessionController.php @@ -0,0 +1,92 @@ +logout(); + throw $e; + } catch (BindingResolutionException $e) { + throw new FrameworkException('Laravel\'s container component', $e); + } + } + + /** + * Login tentative. + * + * @param LoginRequest $request + * + * @return void + * + * @throws UnauthenticatedException + * @throws ModelDBException + */ + public function login(LoginRequest $request): void + { + if (AdminAuthentication::loginAsAdmin($request->username(), $request->password(), $request->ip())) { + return; + } + + if (Auth::attempt(['username' => $request->username(), 'password' => $request->password()])) { + Log::channel('login')->notice(__METHOD__ . ':' . __LINE__ . ' -- User (' . $request->username() . ') has logged in from ' . $request->ip()); + + return; + } + + // TODO: We could avoid this separate log entry and let the exception handler to all the logging, if we would add "context" (see Laravel docs) to those exceptions which need it. + Log::channel('login')->error(__METHOD__ . ':' . __LINE__ . ' -- User (' . $request->username() . ') has tried to log in from ' . $request->ip()); + + throw new UnauthenticatedException('Unknown user or invalid password'); + } + + /** + * Unsets the session values. + * + * @return void + */ + public function logout(): void + { + /** @var \App\Models\User $user */ + $user = Auth::user(); + Log::channel('login')->info(__METHOD__ . ':' . __LINE__ . ' -- User (' . $user->username . ') has logged out.'); + Auth::logout(); + Session::flush(); + } +} diff --git a/app/Legacy/V1/DTO/DiagnosticInfo.php b/app/Legacy/V1/DTO/DiagnosticInfo.php new file mode 100644 index 00000000000..5b5d3530757 --- /dev/null +++ b/app/Legacy/V1/DTO/DiagnosticInfo.php @@ -0,0 +1,29 @@ +route('login'); + } catch (ConfigurationKeyMissingException $e) { + Log::warning(__METHOD__ . ':' . __LINE__ . ' ' . $e->getMessage()); + + return $next($request); + } + } +} diff --git a/app/Legacy/V1/Requests/Album/AddAlbumRequest.php b/app/Legacy/V1/Requests/Album/AddAlbumRequest.php new file mode 100644 index 00000000000..ee717c8b064 --- /dev/null +++ b/app/Legacy/V1/Requests/Album/AddAlbumRequest.php @@ -0,0 +1,56 @@ +parentAlbum]); + } + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return AddAlbumRuleSet::rules(); + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + /** @var string|null */ + $parentAlbumID = $values[RequestAttribute::PARENT_ID_ATTRIBUTE]; + $this->parentAlbum = $parentAlbumID === null ? + null : + Album::query()->findOrFail($parentAlbumID); + $this->title = $values[RequestAttribute::TITLE_ATTRIBUTE]; + } +} diff --git a/app/Legacy/V1/Requests/Album/AddTagAlbumRequest.php b/app/Legacy/V1/Requests/Album/AddTagAlbumRequest.php new file mode 100644 index 00000000000..2ce78e1f469 --- /dev/null +++ b/app/Legacy/V1/Requests/Album/AddTagAlbumRequest.php @@ -0,0 +1,54 @@ +title = $values[RequestAttribute::TITLE_ATTRIBUTE]; + $this->tags = $values[RequestAttribute::TAGS_ATTRIBUTE]; + } +} diff --git a/app/Legacy/V1/Requests/Album/ArchiveAlbumsRequest.php b/app/Legacy/V1/Requests/Album/ArchiveAlbumsRequest.php new file mode 100644 index 00000000000..b432462f7de --- /dev/null +++ b/app/Legacy/V1/Requests/Album/ArchiveAlbumsRequest.php @@ -0,0 +1,63 @@ + + */ +final class ArchiveAlbumsRequest extends BaseApiRequest implements HasAlbums +{ + /** @use HasAlbumsTrait */ + use HasAlbumsTrait; + + /** + * {@inheritDoc} + */ + public function authorize(): bool + { + /** @var AbstractAlbum $album */ + foreach ($this->albums as $album) { + if (!Gate::check(AlbumPolicy::CAN_ACCESS, $album)) { + return false; + } + } + + return true; + } + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return [ + RequestAttribute::ALBUM_IDS_ATTRIBUTE => ['required', new AlbumIDListRule()], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + // TODO: `App\Actions\Album\Archive::compressAlbum` iterates over the original size variant of each photo in the album; we should eagerly load them for higher efficiency. + $this->albums = $this->albumFactory->findAbstractAlbumsOrFail( + explode(',', $values[RequestAttribute::ALBUM_IDS_ATTRIBUTE]) + ); + } +} diff --git a/app/Legacy/V1/Requests/Album/DeleteAlbumsRequest.php b/app/Legacy/V1/Requests/Album/DeleteAlbumsRequest.php new file mode 100644 index 00000000000..0a9b614fc57 --- /dev/null +++ b/app/Legacy/V1/Requests/Album/DeleteAlbumsRequest.php @@ -0,0 +1,50 @@ +albumIDs()]); + } + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return DeleteAlbumsRuleSet::rules(); + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + // As we are going to delete the albums anyway, we don't load the + // models for efficiency reasons. + // Instead, we use mass deletion via low-level SQL queries later. + $this->albumIDs = $values[RequestAttribute::ALBUM_IDS_ATTRIBUTE]; + } +} diff --git a/app/Legacy/V1/Requests/Album/DeleteTrackRequest.php b/app/Legacy/V1/Requests/Album/DeleteTrackRequest.php new file mode 100644 index 00000000000..dcf16897150 --- /dev/null +++ b/app/Legacy/V1/Requests/Album/DeleteTrackRequest.php @@ -0,0 +1,41 @@ +album = Album::query()->findOrFail($albumID); + } +} diff --git a/app/Legacy/V1/Requests/Album/GetAlbumPositionDataRequest.php b/app/Legacy/V1/Requests/Album/GetAlbumPositionDataRequest.php new file mode 100644 index 00000000000..47270ede119 --- /dev/null +++ b/app/Legacy/V1/Requests/Album/GetAlbumPositionDataRequest.php @@ -0,0 +1,63 @@ +album]); + } + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return [ + RequestAttribute::ALBUM_ID_ATTRIBUTE => ['required', new AlbumIDRule(false)], + self::INCLUDE_SUB_ALBUMS_ATTRIBUTE => 'required|boolean', + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + // Avoid loading all photos and sub-albums of an album, because + // \App\Actions\Album\PositionData::get is only interested in a + // particular subset of photos. + $this->album = $this->albumFactory->findAbstractAlbumOrFail($values[RequestAttribute::ALBUM_ID_ATTRIBUTE], false); + $this->includeSubAlbums = static::toBoolean($values[self::INCLUDE_SUB_ALBUMS_ATTRIBUTE]); + } + + public function includeSubAlbums(): bool + { + return $this->includeSubAlbums; + } +} diff --git a/app/Legacy/V1/Requests/Album/GetAlbumRequest.php b/app/Legacy/V1/Requests/Album/GetAlbumRequest.php new file mode 100644 index 00000000000..ed3206927ea --- /dev/null +++ b/app/Legacy/V1/Requests/Album/GetAlbumRequest.php @@ -0,0 +1,63 @@ +album]); + + // In case of a password protected album, we must throw an exception + // with a special error message ("Password required") such that the + // front-end shows the password dialog if a password is set, but + // does not show the dialog otherwise. + if ( + !$result && + $this->album instanceof BaseAlbum && + $this->album->public_permissions()?->password !== null + ) { + throw new PasswordRequiredException(); + } + + return $result; + } + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return BasicAlbumIdRuleSet::rules(); + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + $this->album = $this->albumFactory->findAbstractAlbumOrFail($values[RequestAttribute::ALBUM_ID_ATTRIBUTE]); + } +} diff --git a/app/Legacy/V1/Requests/Album/MergeAlbumsRequest.php b/app/Legacy/V1/Requests/Album/MergeAlbumsRequest.php new file mode 100644 index 00000000000..db586d35187 --- /dev/null +++ b/app/Legacy/V1/Requests/Album/MergeAlbumsRequest.php @@ -0,0 +1,54 @@ + + */ +final class MergeAlbumsRequest extends BaseApiRequest implements HasAlbum, HasAlbums +{ + use HasAlbumTrait; + /** @phpstan-use HasAlbumsTrait */ + use HasAlbumsTrait; + use AuthorizeCanEditAlbumAlbumsTrait; + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return MergeAlbumsRuleSet::rules(); + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + /** @var string $id */ + $id = $values[RequestAttribute::ALBUM_ID_ATTRIBUTE]; + /** @var array $ids */ + $ids = $values[RequestAttribute::ALBUM_IDS_ATTRIBUTE]; + $this->album = Album::query()->findOrFail($id); + // @phpstan-ignore-next-line + $this->albums = Album::query() + ->with(['children']) + ->findOrFail($ids); + } +} diff --git a/app/Legacy/V1/Requests/Album/MoveAlbumsRequest.php b/app/Legacy/V1/Requests/Album/MoveAlbumsRequest.php new file mode 100644 index 00000000000..36da0867b0b --- /dev/null +++ b/app/Legacy/V1/Requests/Album/MoveAlbumsRequest.php @@ -0,0 +1,54 @@ + + */ +final class MoveAlbumsRequest extends BaseApiRequest implements HasAlbum, HasAlbums +{ + use HasAlbumTrait; + /** @phpstan-use HasAlbumsTrait */ + use HasAlbumsTrait; + use AuthorizeCanEditAlbumAlbumsTrait; + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return MoveAlbumsRuleSet::rules(); + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + /** @var string|null $id */ + $id = $values[RequestAttribute::ALBUM_ID_ATTRIBUTE]; + /** @var array $ids */ + $ids = $values[RequestAttribute::ALBUM_IDS_ATTRIBUTE]; + $this->album = $id === null ? + null : + Album::findOrFail($id); + /** @phpstan-ignore-next-line */ + $this->albums = Album::findOrFail($ids); + } +} diff --git a/app/Legacy/V1/Requests/Album/SetAlbumCopyrightRequest.php b/app/Legacy/V1/Requests/Album/SetAlbumCopyrightRequest.php new file mode 100644 index 00000000000..f652f3ea1ff --- /dev/null +++ b/app/Legacy/V1/Requests/Album/SetAlbumCopyrightRequest.php @@ -0,0 +1,48 @@ + ['required', new RandomIDRule(false)], + RequestAttribute::COPYRIGHT_ATTRIBUTE => ['required', new CopyrightRule()], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + $this->album = $this->albumFactory->findBaseAlbumOrFail( + $values[RequestAttribute::ALBUM_ID_ATTRIBUTE], false + ); + $this->copyright = $values[RequestAttribute::COPYRIGHT_ATTRIBUTE]; + } +} diff --git a/app/Legacy/V1/Requests/Album/SetAlbumCoverRequest.php b/app/Legacy/V1/Requests/Album/SetAlbumCoverRequest.php new file mode 100644 index 00000000000..f6fb8a56d2e --- /dev/null +++ b/app/Legacy/V1/Requests/Album/SetAlbumCoverRequest.php @@ -0,0 +1,60 @@ +album]) && + ($this->photo === null || Gate::check(PhotoPolicy::CAN_SEE, $this->photo)); + } + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return SetAlbumCoverRuleSet::rules(); + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + /** @var string|null */ + $albumID = $values[RequestAttribute::ALBUM_ID_ATTRIBUTE]; + + $this->album = Album::query()->findOrFail($albumID); + /** @var ?string $photoID */ + $photoID = $values[RequestAttribute::PHOTO_ID_ATTRIBUTE]; + $this->photo = $photoID === null ? null : Photo::query()->findOrFail($photoID); + } +} diff --git a/app/Legacy/V1/Requests/Album/SetAlbumDescriptionRequest.php b/app/Legacy/V1/Requests/Album/SetAlbumDescriptionRequest.php new file mode 100644 index 00000000000..7f8d2216be4 --- /dev/null +++ b/app/Legacy/V1/Requests/Album/SetAlbumDescriptionRequest.php @@ -0,0 +1,48 @@ + ['required', new RandomIDRule(false)], + RequestAttribute::DESCRIPTION_ATTRIBUTE => ['present', new DescriptionRule()], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + $this->album = $this->albumFactory->findBaseAlbumOrFail( + $values[RequestAttribute::ALBUM_ID_ATTRIBUTE] + ); + $this->description = $values[RequestAttribute::DESCRIPTION_ATTRIBUTE]; + } +} diff --git a/app/Legacy/V1/Requests/Album/SetAlbumHeaderRequest.php b/app/Legacy/V1/Requests/Album/SetAlbumHeaderRequest.php new file mode 100644 index 00000000000..9fac85b73a8 --- /dev/null +++ b/app/Legacy/V1/Requests/Album/SetAlbumHeaderRequest.php @@ -0,0 +1,60 @@ +album]) && + ($this->photo === null || Gate::check(PhotoPolicy::CAN_SEE, $this->photo)); + } + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return SetAlbumHeaderRuleSet::rules(); + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + /** @var string|null */ + $albumID = $values[RequestAttribute::ALBUM_ID_ATTRIBUTE]; + + $this->album = Album::query()->findOrFail($albumID); + /** @var ?string $photoID */ + $photoID = $values[RequestAttribute::PHOTO_ID_ATTRIBUTE]; + $this->photo = $photoID === null ? null : Photo::query()->findOrFail($photoID); + } +} diff --git a/app/Legacy/V1/Requests/Album/SetAlbumLicenseRequest.php b/app/Legacy/V1/Requests/Album/SetAlbumLicenseRequest.php new file mode 100644 index 00000000000..a6b4e312a33 --- /dev/null +++ b/app/Legacy/V1/Requests/Album/SetAlbumLicenseRequest.php @@ -0,0 +1,51 @@ + ['required', new RandomIDRule(false)], + RequestAttribute::LICENSE_ATTRIBUTE => ['required', new Enum(LicenseType::class)], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + /** @var string|null */ + $albumID = $values[RequestAttribute::ALBUM_ID_ATTRIBUTE]; + + $this->album = Album::query()->findOrFail($albumID); + $this->license = LicenseType::tryFrom($values[RequestAttribute::LICENSE_ATTRIBUTE]); + } +} diff --git a/app/Legacy/V1/Requests/Album/SetAlbumNSFWRequest.php b/app/Legacy/V1/Requests/Album/SetAlbumNSFWRequest.php new file mode 100644 index 00000000000..72f2588fc48 --- /dev/null +++ b/app/Legacy/V1/Requests/Album/SetAlbumNSFWRequest.php @@ -0,0 +1,54 @@ + ['required', new RandomIDRule(false)], + RequestAttribute::IS_NSFW_ATTRIBUTE => 'required|boolean', + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + $this->album = $this->albumFactory->findBaseAlbumOrFail( + $values[RequestAttribute::ALBUM_ID_ATTRIBUTE] + ); + $this->isNSFW = static::toBoolean($values[RequestAttribute::IS_NSFW_ATTRIBUTE]); + } + + public function isNSFW(): bool + { + return $this->isNSFW; + } +} diff --git a/app/Legacy/V1/Requests/Album/SetAlbumProtectionPolicyRequest.php b/app/Legacy/V1/Requests/Album/SetAlbumProtectionPolicyRequest.php new file mode 100644 index 00000000000..5f4935f19ee --- /dev/null +++ b/app/Legacy/V1/Requests/Album/SetAlbumProtectionPolicyRequest.php @@ -0,0 +1,78 @@ + ['required', new RandomIDRule(false)], + RequestAttribute::PASSWORD_ATTRIBUTE => ['sometimes', new PasswordRule(true)], + RequestAttribute::IS_PUBLIC_ATTRIBUTE => 'required|boolean', + RequestAttribute::IS_LINK_REQUIRED_ATTRIBUTE => 'required|boolean', + RequestAttribute::IS_NSFW_ATTRIBUTE => 'required|boolean', + RequestAttribute::GRANTS_DOWNLOAD_ATTRIBUTE => 'required|boolean', + RequestAttribute::GRANTS_FULL_PHOTO_ACCESS_ATTRIBUTE => 'required|boolean', + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + $this->album = $this->albumFactory->findBaseAlbumOrFail( + $values[RequestAttribute::ALBUM_ID_ATTRIBUTE] + ); + $this->albumProtectionPolicy = new AlbumProtectionPolicy( + is_public: static::toBoolean($values[RequestAttribute::IS_PUBLIC_ATTRIBUTE]), + is_link_required: static::toBoolean($values[RequestAttribute::IS_LINK_REQUIRED_ATTRIBUTE]), + is_nsfw: static::toBoolean($values[RequestAttribute::IS_NSFW_ATTRIBUTE]), + grants_full_photo_access: static::toBoolean($values[RequestAttribute::GRANTS_FULL_PHOTO_ACCESS_ATTRIBUTE]), + grants_download: static::toBoolean($values[RequestAttribute::GRANTS_DOWNLOAD_ATTRIBUTE]), + ); + $this->isPasswordProvided = array_key_exists(RequestAttribute::PASSWORD_ATTRIBUTE, $values); + $this->password = $this->isPasswordProvided ? $values[RequestAttribute::PASSWORD_ATTRIBUTE] : null; + } + + /** + * @return AlbumProtectionPolicy + */ + public function albumProtectionPolicy(): AlbumProtectionPolicy + { + return $this->albumProtectionPolicy; + } + + public function isPasswordProvided(): bool + { + return $this->isPasswordProvided; + } +} diff --git a/app/Legacy/V1/Requests/Album/SetAlbumSortingRequest.php b/app/Legacy/V1/Requests/Album/SetAlbumSortingRequest.php new file mode 100644 index 00000000000..a28d1f78e33 --- /dev/null +++ b/app/Legacy/V1/Requests/Album/SetAlbumSortingRequest.php @@ -0,0 +1,59 @@ + ['required', new RandomIDRule(false)], + RequestAttribute::SORTING_COLUMN_ATTRIBUTE => ['present', 'nullable', new Enum(ColumnSortingPhotoType::class)], + RequestAttribute::SORTING_ORDER_ATTRIBUTE => [ + 'required_with:' . RequestAttribute::SORTING_COLUMN_ATTRIBUTE, + 'nullable', new Enum(OrderSortingType::class), + ], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + $this->album = $this->albumFactory->findBaseAlbumOrFail($values[RequestAttribute::ALBUM_ID_ATTRIBUTE]); + + $column = ColumnSortingPhotoType::tryFrom($values[RequestAttribute::SORTING_COLUMN_ATTRIBUTE]); + $order = OrderSortingType::tryFrom($values[RequestAttribute::SORTING_ORDER_ATTRIBUTE]); + + $this->sortingCriterion = $column === null ? + null : + new PhotoSortingCriterion($column->toColumnSortingType(), $order); + } +} diff --git a/app/Legacy/V1/Requests/Album/SetAlbumTagsRequest.php b/app/Legacy/V1/Requests/Album/SetAlbumTagsRequest.php new file mode 100644 index 00000000000..be306050d1a --- /dev/null +++ b/app/Legacy/V1/Requests/Album/SetAlbumTagsRequest.php @@ -0,0 +1,45 @@ +album = TagAlbum::query()->findOrFail($id); + $this->tags = $values[RequestAttribute::SHOW_TAGS_ATTRIBUTE]; + } +} \ No newline at end of file diff --git a/app/Legacy/V1/Requests/Album/SetAlbumTrackRequest.php b/app/Legacy/V1/Requests/Album/SetAlbumTrackRequest.php new file mode 100644 index 00000000000..5b532dcfc3c --- /dev/null +++ b/app/Legacy/V1/Requests/Album/SetAlbumTrackRequest.php @@ -0,0 +1,54 @@ + ['required', new AlbumIDRule(false)], + self::FILE_ATTRIBUTE => 'required|file', + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + /** @var string|null */ + $albumID = $values[RequestAttribute::ALBUM_ID_ATTRIBUTE]; + $this->album = Album::query()->findOrFail($albumID); + $this->file = $files[self::FILE_ATTRIBUTE]; + } + + public function uploadedFile(): UploadedFile + { + return $this->file; + } +} diff --git a/app/Legacy/V1/Requests/Album/SetAlbumsTitleRequest.php b/app/Legacy/V1/Requests/Album/SetAlbumsTitleRequest.php new file mode 100644 index 00000000000..d30f7955446 --- /dev/null +++ b/app/Legacy/V1/Requests/Album/SetAlbumsTitleRequest.php @@ -0,0 +1,69 @@ +, even though it is indeed true. + * This violate LSP and contra variance. + * + * SetAlbumsTitleRuleSet ensure that we are actually dealing with BaseAlbum + * + * @implements HasAlbums<\App\Models\Album|\App\Models\TagAlbum> + */ +final class SetAlbumsTitleRequest extends BaseApiRequest implements HasTitle, HasAlbums +{ + use HasTitleTrait; + /** @use HasAlbumsTrait<\App\Models\Album|\App\Models\TagAlbum> */ + use HasAlbumsTrait; + + /** + * {@inheritDoc} + */ + public function authorize(): bool + { + /** @var AbstractAlbum $album */ + foreach ($this->albums as $album) { + if (!Gate::check(AlbumPolicy::CAN_EDIT, $album)) { + return false; + } + } + + return true; + } + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return SetAlbumsTitleRuleSet::rules(); + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + $this->albums = $this->albumFactory->findBaseAlbumsOrFail( + $values[RequestAttribute::ALBUM_IDS_ATTRIBUTE], false + ); + $this->title = $values[RequestAttribute::TITLE_ATTRIBUTE]; + } +} diff --git a/app/Legacy/V1/Requests/Album/UnlockAlbumRequest.php b/app/Legacy/V1/Requests/Album/UnlockAlbumRequest.php new file mode 100644 index 00000000000..30c84ea0294 --- /dev/null +++ b/app/Legacy/V1/Requests/Album/UnlockAlbumRequest.php @@ -0,0 +1,50 @@ +album = $this->albumFactory->findBaseAlbumOrFail( + $values[RequestAttribute::ALBUM_ID_ATTRIBUTE] + ); + $this->password = $values[RequestAttribute::PASSWORD_ATTRIBUTE]; + } +} diff --git a/app/Legacy/V1/Requests/Import/CancelImportServerRequest.php b/app/Legacy/V1/Requests/Import/CancelImportServerRequest.php new file mode 100644 index 00000000000..9ce7cdee492 --- /dev/null +++ b/app/Legacy/V1/Requests/Import/CancelImportServerRequest.php @@ -0,0 +1,25 @@ +album = $albumID === null ? + null : + Album::query()->findOrFail($albumID); + // The replacement below looks suspicious. + // If it was really necessary, then there would be much more special + // characters (e.i. for example umlauts in international domain names) + // which would require replacement by their corresponding %-encoding. + // However, I assume that the PHP method `fopen` is happily fine with + // any character and internally handles special characters itself. + // Hence, either use a proper encoding method here instead of our + // home-brewed, poor-man replacement or drop it entirely. + // TODO: Find out what is needed and proceed accordingly. + $this->urls = str_replace(' ', '%20', $values[RequestAttribute::URLS_ATTRIBUTE]); + } + + /** + * @return string[] + */ + public function urls(): array + { + return $this->urls; + } +} diff --git a/app/Legacy/V1/Requests/Import/ImportServerRequest.php b/app/Legacy/V1/Requests/Import/ImportServerRequest.php new file mode 100644 index 00000000000..e829e36fa1f --- /dev/null +++ b/app/Legacy/V1/Requests/Import/ImportServerRequest.php @@ -0,0 +1,86 @@ +album = $albumID === null ? + null : + Album::query()->findOrFail($albumID); + $this->paths = $values[RequestAttribute::PATH_ATTRIBUTE]; + $this->importMode = new ImportMode( + isset($values[RequestAttribute::DELETE_IMPORTED_ATTRIBUTE]) ? + static::toBoolean($values[RequestAttribute::DELETE_IMPORTED_ATTRIBUTE]) : + Configs::getValueAsBool('delete_imported'), + isset($values[RequestAttribute::SKIP_DUPLICATES_ATTRIBUTE]) ? + static::toBoolean($values[RequestAttribute::SKIP_DUPLICATES_ATTRIBUTE]) : + Configs::getValueAsBool('skip_duplicates'), + isset($values[RequestAttribute::IMPORT_VIA_SYMLINK_ATTRIBUTE]) ? + static::toBoolean($values[RequestAttribute::IMPORT_VIA_SYMLINK_ATTRIBUTE]) : + Configs::getValueAsBool('import_via_symlink'), + isset($values[RequestAttribute::RESYNC_METADATA_ATTRIBUTE]) && + static::toBoolean($values[RequestAttribute::RESYNC_METADATA_ATTRIBUTE]) + ); + } + + /** + * @return string[] + */ + public function paths(): array + { + return $this->paths; + } + + public function importMode(): ImportMode + { + return $this->importMode; + } +} diff --git a/app/Legacy/V1/Requests/Legacy/TranslateIDRequest.php b/app/Legacy/V1/Requests/Legacy/TranslateIDRequest.php new file mode 100644 index 00000000000..a52a9cd45e1 --- /dev/null +++ b/app/Legacy/V1/Requests/Legacy/TranslateIDRequest.php @@ -0,0 +1,71 @@ + [ + 'sometimes', + 'required_without:' . RequestAttribute::PHOTO_ID_ATTRIBUTE, + new IntegerIDRule(false), + ], + RequestAttribute::PHOTO_ID_ATTRIBUTE => [ + 'sometimes', + 'required_without:' . RequestAttribute::ALBUM_ID_ATTRIBUTE, + new IntegerIDRule(false), + ], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + $this->albumID = $values[RequestAttribute::ALBUM_ID_ATTRIBUTE] ?? null; + $this->photoID = $values[RequestAttribute::PHOTO_ID_ATTRIBUTE] ?? null; + } + + /** + * @return int|null + */ + public function albumID(): ?int + { + return $this->albumID; + } + + /** + * @return int|null + */ + public function photoID(): ?int + { + return $this->photoID; + } +} diff --git a/app/Legacy/V1/Requests/Logs/ShowJobsRequest.php b/app/Legacy/V1/Requests/Logs/ShowJobsRequest.php new file mode 100644 index 00000000000..446975899a7 --- /dev/null +++ b/app/Legacy/V1/Requests/Logs/ShowJobsRequest.php @@ -0,0 +1,25 @@ +album = $albumID === null ? + null : + $this->albumFactory->findAbstractAlbumOrFail($albumID); + // Convert the File Last Modified to seconds instead of milliseconds + $val = $values[RequestAttribute::FILE_LAST_MODIFIED_TIME] ?? null; + $this->fileLastModifiedTime = $val !== null ? intval($val) : null; + $this->file = $files[RequestAttribute::FILE_ATTRIBUTE]; + } + + public function uploadedFile(): UploadedFile + { + return $this->file; + } + + public function fileLastModifiedTime(): ?int + { + return $this->fileLastModifiedTime !== null ? intval($this->fileLastModifiedTime / 1000) : null; + } +} diff --git a/app/Legacy/V1/Requests/Photo/ArchivePhotosRequest.php b/app/Legacy/V1/Requests/Photo/ArchivePhotosRequest.php new file mode 100644 index 00000000000..d333ce1ca61 --- /dev/null +++ b/app/Legacy/V1/Requests/Photo/ArchivePhotosRequest.php @@ -0,0 +1,77 @@ +photos as $photo) { + if (!Gate::check(PhotoPolicy::CAN_DOWNLOAD, $photo)) { + return false; + } + } + + return true; + } + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return ArchivePhotosRuleSet::rules(); + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + $this->sizeVariant = DownloadVariantType::from($values[RequestAttribute::SIZE_VARIANT_ATTRIBUTE]); + + $photoQuery = Photo::query()->with(['album']); + // The condition is required, because Lychee also supports to archive + // the "live video" as a size variant which is not a proper size variant + $variant = $this->sizeVariant->getSizeVariantType(); + if ($variant !== null) { // NOT LIVE PHOTO + // If a proper size variant is requested, eagerly load the size + // variants but only the requested type due to efficiency reasons + $photoQuery = $photoQuery->with([ + 'size_variants' => fn ($r) => $r->where('type', '=', $variant), + ]); + } + // `findOrFail` returns the union `Photo|Collection` + // which is not assignable to `Collection`; but as we query + // with an array of IDs we never get a single entity (even if the + // array only contains a single ID). + $this->photos = $photoQuery->findOrFail( + explode(',', $values[RequestAttribute::PHOTO_IDS_ATTRIBUTE]) + ); + } +} diff --git a/app/Legacy/V1/Requests/Photo/ClearSymLinkRequest.php b/app/Legacy/V1/Requests/Photo/ClearSymLinkRequest.php new file mode 100644 index 00000000000..8b4a0f8cb31 --- /dev/null +++ b/app/Legacy/V1/Requests/Photo/ClearSymLinkRequest.php @@ -0,0 +1,23 @@ +may_administrate === true; + } +} diff --git a/app/Legacy/V1/Requests/Photo/DeletePhotosRequest.php b/app/Legacy/V1/Requests/Photo/DeletePhotosRequest.php new file mode 100644 index 00000000000..3150bc3385c --- /dev/null +++ b/app/Legacy/V1/Requests/Photo/DeletePhotosRequest.php @@ -0,0 +1,50 @@ +photoIDs()]); + } + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return DeletePhotosRuleSet::rules(); + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + // As we are going to delete the photos anyway, we don't load the + // models for efficiency reasons. + // Instead, we use mass deletion via low-level SQL queries later. + $this->photoIDs = $values[RequestAttribute::PHOTO_IDS_ATTRIBUTE]; + } +} diff --git a/app/Legacy/V1/Requests/Photo/DuplicatePhotosRequest.php b/app/Legacy/V1/Requests/Photo/DuplicatePhotosRequest.php new file mode 100644 index 00000000000..ca98eff29ef --- /dev/null +++ b/app/Legacy/V1/Requests/Photo/DuplicatePhotosRequest.php @@ -0,0 +1,52 @@ + $photosIDs */ + $photosIDs = $values[RequestAttribute::PHOTO_IDS_ATTRIBUTE]; + $this->photos = Photo::query() + ->with(['size_variants']) + ->findOrFail($photosIDs); + /** @var string|null */ + $targetAlbumID = $values[RequestAttribute::ALBUM_ID_ATTRIBUTE]; + $this->album = $targetAlbumID === null ? + null : + Album::query()->findOrFail($targetAlbumID); + } +} diff --git a/app/Legacy/V1/Requests/Photo/GetPhotoRequest.php b/app/Legacy/V1/Requests/Photo/GetPhotoRequest.php new file mode 100644 index 00000000000..c7235387d46 --- /dev/null +++ b/app/Legacy/V1/Requests/Photo/GetPhotoRequest.php @@ -0,0 +1,51 @@ +photo); + } + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return GetPhotoRuleSet::rules(); + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + /** @var ?string $photoID */ + $photoID = $values[RequestAttribute::PHOTO_ID_ATTRIBUTE]; + $this->photo = Photo::query() + ->with(['size_variants', 'size_variants.sym_links']) + ->findOrFail($photoID); + } +} diff --git a/app/Legacy/V1/Requests/Photo/MovePhotosRequest.php b/app/Legacy/V1/Requests/Photo/MovePhotosRequest.php new file mode 100644 index 00000000000..4fc634706f1 --- /dev/null +++ b/app/Legacy/V1/Requests/Photo/MovePhotosRequest.php @@ -0,0 +1,49 @@ + $photosIDs */ + $photosIDs = $values[RequestAttribute::PHOTO_IDS_ATTRIBUTE]; + $this->photos = Photo::query() + ->findOrFail($photosIDs); + /** @var string|null */ + $targetAlbumID = $values[RequestAttribute::ALBUM_ID_ATTRIBUTE]; + $this->album = $targetAlbumID === null ? null : Album::query()->findOrFail($targetAlbumID); + } +} diff --git a/app/Legacy/V1/Requests/Photo/RotatePhotoRequest.php b/app/Legacy/V1/Requests/Photo/RotatePhotoRequest.php new file mode 100644 index 00000000000..1730bfcefbc --- /dev/null +++ b/app/Legacy/V1/Requests/Photo/RotatePhotoRequest.php @@ -0,0 +1,51 @@ +photo = Photo::query() + ->with(['size_variants']) + ->findOrFail($photoID); + $this->direction = intval($values[RequestAttribute::DIRECTION_ATTRIBUTE]); + } + + public function direction(): int + { + return $this->direction; + } +} diff --git a/app/Legacy/V1/Requests/Photo/SetPhotoDescriptionRequest.php b/app/Legacy/V1/Requests/Photo/SetPhotoDescriptionRequest.php new file mode 100644 index 00000000000..907986e06a5 --- /dev/null +++ b/app/Legacy/V1/Requests/Photo/SetPhotoDescriptionRequest.php @@ -0,0 +1,45 @@ +photo = Photo::query()->findOrFail($photoID); + $this->description = $values[RequestAttribute::DESCRIPTION_ATTRIBUTE]; + } +} diff --git a/app/Legacy/V1/Requests/Photo/SetPhotoLicenseRequest.php b/app/Legacy/V1/Requests/Photo/SetPhotoLicenseRequest.php new file mode 100644 index 00000000000..9a71227538f --- /dev/null +++ b/app/Legacy/V1/Requests/Photo/SetPhotoLicenseRequest.php @@ -0,0 +1,46 @@ +photo = Photo::query()->findOrFail($photoID); + $this->license = LicenseType::tryFrom($values[RequestAttribute::LICENSE_ATTRIBUTE]); + } +} diff --git a/app/Legacy/V1/Requests/Photo/SetPhotoUploadDateRequest.php b/app/Legacy/V1/Requests/Photo/SetPhotoUploadDateRequest.php new file mode 100644 index 00000000000..d96d146d268 --- /dev/null +++ b/app/Legacy/V1/Requests/Photo/SetPhotoUploadDateRequest.php @@ -0,0 +1,46 @@ +photo = Photo::query()->findOrFail($photoID); + $this->date = Carbon::parse($values[RequestAttribute::DATE_ATTRIBUTE]); + } +} diff --git a/app/Legacy/V1/Requests/Photo/SetPhotosStarredRequest.php b/app/Legacy/V1/Requests/Photo/SetPhotosStarredRequest.php new file mode 100644 index 00000000000..ba06f220e94 --- /dev/null +++ b/app/Legacy/V1/Requests/Photo/SetPhotosStarredRequest.php @@ -0,0 +1,54 @@ + $photosIDs */ + $photosIDs = $values[RequestAttribute::PHOTO_IDS_ATTRIBUTE]; + $this->photos = Photo::query()->findOrFail($photosIDs); + $this->isStarred = static::toBoolean($values[RequestAttribute::IS_STARRED_ATTRIBUTE]); + } + + public function isStarred(): bool + { + return $this->isStarred; + } +} diff --git a/app/Legacy/V1/Requests/Photo/SetPhotosTagsRequest.php b/app/Legacy/V1/Requests/Photo/SetPhotosTagsRequest.php new file mode 100644 index 00000000000..e65ef17f22f --- /dev/null +++ b/app/Legacy/V1/Requests/Photo/SetPhotosTagsRequest.php @@ -0,0 +1,48 @@ + $photosIDs */ + $photosIDs = $values[RequestAttribute::PHOTO_IDS_ATTRIBUTE]; + $this->photos = Photo::query()->findOrFail($photosIDs); + $this->tags = $values[RequestAttribute::TAGS_ATTRIBUTE]; + $this->shallOverride = $values[RequestAttribute::SHALL_OVERRIDE_ATTRIBUTE]; + } +} \ No newline at end of file diff --git a/app/Legacy/V1/Requests/Photo/SetPhotosTitleRequest.php b/app/Legacy/V1/Requests/Photo/SetPhotosTitleRequest.php new file mode 100644 index 00000000000..9dcd1158802 --- /dev/null +++ b/app/Legacy/V1/Requests/Photo/SetPhotosTitleRequest.php @@ -0,0 +1,45 @@ + $photosIDs */ + $photosIDs = $values[RequestAttribute::PHOTO_IDS_ATTRIBUTE]; + $this->photos = Photo::query()->findOrFail($photosIDs); + $this->title = $values[RequestAttribute::TITLE_ATTRIBUTE]; + } +} diff --git a/app/Legacy/V1/Requests/Search/SearchRequest.php b/app/Legacy/V1/Requests/Search/SearchRequest.php new file mode 100644 index 00000000000..f3e63686933 --- /dev/null +++ b/app/Legacy/V1/Requests/Search/SearchRequest.php @@ -0,0 +1,60 @@ + 'required|string']; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + // Escape special characters for a LIKE query + $this->terms = explode(' ', str_replace( + ['\\', '%', '_'], + ['\\\\', '\\%', '\\_'], + $values[self::TERM_ATTRIBUTE] + )); + } + + /** + * @return string[] + */ + public function terms(): array + { + return $this->terms; + } +} diff --git a/app/Legacy/V1/Requests/Settings/AbstractSettingRequest.php b/app/Legacy/V1/Requests/Settings/AbstractSettingRequest.php new file mode 100644 index 00000000000..9cb7a80cc7b --- /dev/null +++ b/app/Legacy/V1/Requests/Settings/AbstractSettingRequest.php @@ -0,0 +1,43 @@ +name; + } + + public function getSettingValue(): string|int|bool|\BackedEnum + { + return $this->value; + } +} diff --git a/app/Legacy/V1/Requests/Settings/GetSetAllSettingsRequest.php b/app/Legacy/V1/Requests/Settings/GetSetAllSettingsRequest.php new file mode 100644 index 00000000000..d095759f1d9 --- /dev/null +++ b/app/Legacy/V1/Requests/Settings/GetSetAllSettingsRequest.php @@ -0,0 +1,29 @@ +username(), $this->password(), $this->ip()); + $isLoggedIn = $isLoggedIn || Auth::attempt(['username' => $this->username(), 'password' => $this->password()]); + + // Check if logged in AND is admin + return $isLoggedIn && Gate::check(SettingsPolicy::CAN_UPDATE, Configs::class); + } + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return [ + RequestAttribute::USERNAME_ATTRIBUTE => ['sometimes', new UsernameRule()], + RequestAttribute::PASSWORD_ATTRIBUTE => ['sometimes', new PasswordRule(false)], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + $this->username = $values[RequestAttribute::USERNAME_ATTRIBUTE] ?? ''; + $this->password = $values[RequestAttribute::PASSWORD_ATTRIBUTE] ?? ''; + } + + /** + * {@inheritDoc} + */ + protected function failedAuthorization(): void + { + throw new HttpResponseException(response()->view('update.error', ['code' => '403', 'message' => 'Incorrect username or password'], 403)); + } +} diff --git a/app/Legacy/V1/Requests/Settings/OptimizeRequest.php b/app/Legacy/V1/Requests/Settings/OptimizeRequest.php new file mode 100644 index 00000000000..6ca3f9bfe60 --- /dev/null +++ b/app/Legacy/V1/Requests/Settings/OptimizeRequest.php @@ -0,0 +1,29 @@ + ['required', new Enum(AlbumDecorationType::class)], + RequestAttribute::ALBUM_DECORATION_ORIENTATION_ATTRIBUTE => ['required', new Enum(AlbumDecorationOrientation::class)], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + $this->albumDecoration = AlbumDecorationType::from($values[RequestAttribute::ALBUM_DECORATION_ATTRIBUTE]); + $this->albumDecorationOrientation = AlbumDecorationOrientation::from($values[RequestAttribute::ALBUM_DECORATION_ORIENTATION_ATTRIBUTE]); + } + + /** + * @return AlbumDecorationType + */ + public function albumDecoration(): AlbumDecorationType + { + return $this->albumDecoration; + } + + /** + * @return AlbumDecorationOrientation + */ + public function albumDecorationOrientation(): AlbumDecorationOrientation + { + return $this->albumDecorationOrientation; + } +} diff --git a/app/Legacy/V1/Requests/Settings/SetCSSSettingRequest.php b/app/Legacy/V1/Requests/Settings/SetCSSSettingRequest.php new file mode 100644 index 00000000000..dd9ee34aa84 --- /dev/null +++ b/app/Legacy/V1/Requests/Settings/SetCSSSettingRequest.php @@ -0,0 +1,31 @@ + 'present|nullable|string']; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + $this->name = self::ATTRIBUTE; + $this->value = $values[self::ATTRIBUTE] ?? ''; + } +} diff --git a/app/Legacy/V1/Requests/Settings/SetDefaultLicenseSettingRequest.php b/app/Legacy/V1/Requests/Settings/SetDefaultLicenseSettingRequest.php new file mode 100644 index 00000000000..2e2a2c4c68c --- /dev/null +++ b/app/Legacy/V1/Requests/Settings/SetDefaultLicenseSettingRequest.php @@ -0,0 +1,26 @@ + ['required', new Enum(LicenseType::class)]]; + } + + protected function processValidatedValues(array $values, array $files): void + { + $this->name = 'default_license'; + $this->value = LicenseType::from($values['license']); + } +} diff --git a/app/Legacy/V1/Requests/Settings/SetDropboxKeySettingRequest.php b/app/Legacy/V1/Requests/Settings/SetDropboxKeySettingRequest.php new file mode 100644 index 00000000000..1e6b8c77933 --- /dev/null +++ b/app/Legacy/V1/Requests/Settings/SetDropboxKeySettingRequest.php @@ -0,0 +1,23 @@ + 'present|string|nullable']; + } + + protected function processValidatedValues(array $values, array $files): void + { + $this->name = 'dropbox_key'; + $this->value = $values['key']; + } +} diff --git a/app/Legacy/V1/Requests/Settings/SetImageOverlaySettingRequest.php b/app/Legacy/V1/Requests/Settings/SetImageOverlaySettingRequest.php new file mode 100644 index 00000000000..c17aab4e500 --- /dev/null +++ b/app/Legacy/V1/Requests/Settings/SetImageOverlaySettingRequest.php @@ -0,0 +1,30 @@ + ['required', new Enum(ImageOverlayType::class)], + ]; + } + + protected function processValidatedValues(array $values, array $files): void + { + $this->name = self::ATTRIBUTE; + $this->value = ImageOverlayType::from($values[self::ATTRIBUTE]); + } +} diff --git a/app/Legacy/V1/Requests/Settings/SetJSSettingRequest.php b/app/Legacy/V1/Requests/Settings/SetJSSettingRequest.php new file mode 100644 index 00000000000..c19e187502a --- /dev/null +++ b/app/Legacy/V1/Requests/Settings/SetJSSettingRequest.php @@ -0,0 +1,31 @@ + 'present|nullable|string']; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + $this->name = self::ATTRIBUTE; + $this->value = $values[self::ATTRIBUTE] ?? ''; + } +} \ No newline at end of file diff --git a/app/Legacy/V1/Requests/Settings/SetLangSettingRequest.php b/app/Legacy/V1/Requests/Settings/SetLangSettingRequest.php new file mode 100644 index 00000000000..ba7f3fc5c7e --- /dev/null +++ b/app/Legacy/V1/Requests/Settings/SetLangSettingRequest.php @@ -0,0 +1,29 @@ + ['required', 'string', Rule::in(config('app.supported_locale'))], + ]; + } + + protected function processValidatedValues(array $values, array $files): void + { + $this->name = self::ATTRIBUTE; + $this->value = $values[self::ATTRIBUTE]; + } +} diff --git a/app/Legacy/V1/Requests/Settings/SetLayoutSettingRequest.php b/app/Legacy/V1/Requests/Settings/SetLayoutSettingRequest.php new file mode 100644 index 00000000000..79cdbb75a69 --- /dev/null +++ b/app/Legacy/V1/Requests/Settings/SetLayoutSettingRequest.php @@ -0,0 +1,30 @@ + ['required', new Enum(PhotoLayoutType::class)], + ]; + } + + protected function processValidatedValues(array $values, array $files): void + { + $this->name = self::ATTRIBUTE; + $this->value = PhotoLayoutType::from($values[self::ATTRIBUTE]); + } +} diff --git a/app/Legacy/V1/Requests/Settings/SetLocationDecodingSettingRequest.php b/app/Legacy/V1/Requests/Settings/SetLocationDecodingSettingRequest.php new file mode 100644 index 00000000000..085bbb49543 --- /dev/null +++ b/app/Legacy/V1/Requests/Settings/SetLocationDecodingSettingRequest.php @@ -0,0 +1,25 @@ + 'required|boolean']; + } + + protected function processValidatedValues(array $values, array $files): void + { + $this->name = self::ATTRIBUTE; + $this->value = self::toBoolean($values[self::ATTRIBUTE]); + } +} diff --git a/app/Legacy/V1/Requests/Settings/SetLocationShowPublicSettingRequest.php b/app/Legacy/V1/Requests/Settings/SetLocationShowPublicSettingRequest.php new file mode 100644 index 00000000000..c87e36baba7 --- /dev/null +++ b/app/Legacy/V1/Requests/Settings/SetLocationShowPublicSettingRequest.php @@ -0,0 +1,25 @@ + 'required|boolean']; + } + + protected function processValidatedValues(array $values, array $files): void + { + $this->name = self::ATTRIBUTE; + $this->value = self::toBoolean($values[self::ATTRIBUTE]); + } +} diff --git a/app/Legacy/V1/Requests/Settings/SetLocationShowSettingRequest.php b/app/Legacy/V1/Requests/Settings/SetLocationShowSettingRequest.php new file mode 100644 index 00000000000..3e945c540ac --- /dev/null +++ b/app/Legacy/V1/Requests/Settings/SetLocationShowSettingRequest.php @@ -0,0 +1,25 @@ + 'required|boolean']; + } + + protected function processValidatedValues(array $values, array $files): void + { + $this->name = self::ATTRIBUTE; + $this->value = self::toBoolean($values[self::ATTRIBUTE]); + } +} diff --git a/app/Legacy/V1/Requests/Settings/SetMapDisplayPublicSettingRequest.php b/app/Legacy/V1/Requests/Settings/SetMapDisplayPublicSettingRequest.php new file mode 100644 index 00000000000..bf94d3ea902 --- /dev/null +++ b/app/Legacy/V1/Requests/Settings/SetMapDisplayPublicSettingRequest.php @@ -0,0 +1,25 @@ + 'required|boolean']; + } + + protected function processValidatedValues(array $values, array $files): void + { + $this->name = self::ATTRIBUTE; + $this->value = self::toBoolean($values[self::ATTRIBUTE]); + } +} diff --git a/app/Legacy/V1/Requests/Settings/SetMapDisplaySettingRequest.php b/app/Legacy/V1/Requests/Settings/SetMapDisplaySettingRequest.php new file mode 100644 index 00000000000..91b72b906b4 --- /dev/null +++ b/app/Legacy/V1/Requests/Settings/SetMapDisplaySettingRequest.php @@ -0,0 +1,25 @@ + 'required|boolean']; + } + + protected function processValidatedValues(array $values, array $files): void + { + $this->name = self::ATTRIBUTE; + $this->value = self::toBoolean($values[self::ATTRIBUTE]); + } +} diff --git a/app/Legacy/V1/Requests/Settings/SetMapIncludeSubAlbumsSettingRequest.php b/app/Legacy/V1/Requests/Settings/SetMapIncludeSubAlbumsSettingRequest.php new file mode 100644 index 00000000000..42613df560e --- /dev/null +++ b/app/Legacy/V1/Requests/Settings/SetMapIncludeSubAlbumsSettingRequest.php @@ -0,0 +1,25 @@ + 'required|boolean']; + } + + protected function processValidatedValues(array $values, array $files): void + { + $this->name = self::ATTRIBUTE; + $this->value = self::toBoolean($values[self::ATTRIBUTE]); + } +} diff --git a/app/Legacy/V1/Requests/Settings/SetMapProviderSettingRequest.php b/app/Legacy/V1/Requests/Settings/SetMapProviderSettingRequest.php new file mode 100644 index 00000000000..274c55bd18a --- /dev/null +++ b/app/Legacy/V1/Requests/Settings/SetMapProviderSettingRequest.php @@ -0,0 +1,30 @@ + ['required', new Enum(MapProviders::class)], + ]; + } + + protected function processValidatedValues(array $values, array $files): void + { + $this->name = self::ATTRIBUTE; + $this->value = MapProviders::from($values[self::ATTRIBUTE]); + } +} diff --git a/app/Legacy/V1/Requests/Settings/SetNSFWVisibilityRequest.php b/app/Legacy/V1/Requests/Settings/SetNSFWVisibilityRequest.php new file mode 100644 index 00000000000..6e78daa744a --- /dev/null +++ b/app/Legacy/V1/Requests/Settings/SetNSFWVisibilityRequest.php @@ -0,0 +1,25 @@ + 'required|boolean']; + } + + protected function processValidatedValues(array $values, array $files): void + { + $this->name = self::ATTRIBUTE; + $this->value = self::toBoolean($values[self::ATTRIBUTE]); + } +} diff --git a/app/Legacy/V1/Requests/Settings/SetNewPhotosNotificationSettingRequest.php b/app/Legacy/V1/Requests/Settings/SetNewPhotosNotificationSettingRequest.php new file mode 100644 index 00000000000..8aa00a9f15e --- /dev/null +++ b/app/Legacy/V1/Requests/Settings/SetNewPhotosNotificationSettingRequest.php @@ -0,0 +1,25 @@ + 'required|boolean']; + } + + protected function processValidatedValues(array $values, array $files): void + { + $this->name = self::ATTRIBUTE; + $this->value = self::toBoolean($values[self::ATTRIBUTE]); + } +} diff --git a/app/Legacy/V1/Requests/Settings/SetPublicSearchSettingRequest.php b/app/Legacy/V1/Requests/Settings/SetPublicSearchSettingRequest.php new file mode 100644 index 00000000000..b61b0eeb146 --- /dev/null +++ b/app/Legacy/V1/Requests/Settings/SetPublicSearchSettingRequest.php @@ -0,0 +1,28 @@ + 'required_without:search_public|boolean', // legacy + 'search_public' => 'required_without:public_search|boolean', // new value + ]; + } + + protected function processValidatedValues(array $values, array $files): void + { + $this->name = self::ATTRIBUTE; + $this->value = self::toBoolean($values['search_public'] ?? $values['public_search']); + } +} diff --git a/app/Legacy/V1/Requests/Settings/SetSmartAlbumVisibilityRequest.php b/app/Legacy/V1/Requests/Settings/SetSmartAlbumVisibilityRequest.php new file mode 100644 index 00000000000..4acd6783619 --- /dev/null +++ b/app/Legacy/V1/Requests/Settings/SetSmartAlbumVisibilityRequest.php @@ -0,0 +1,57 @@ + [ + 'required', + // We could use the Enum(SmartAlbumType::class) rule, but this is more targetted. + new In([ + SmartAlbumType::RECENT->value, + SmartAlbumType::STARRED->value, + SmartAlbumType::ON_THIS_DAY->value, + ]), + ], + RequestAttribute::IS_PUBLIC_ATTRIBUTE => 'required|boolean', + ]; + } + + protected function processValidatedValues(array $values, array $files): void + { + $this->album = $this->albumFactory->findAbstractAlbumOrFail($values[RequestAttribute::ALBUM_ID_ATTRIBUTE]); + $this->is_public = self::toBoolean($values[RequestAttribute::IS_PUBLIC_ATTRIBUTE]); + } +} diff --git a/app/Legacy/V1/Requests/Settings/SetSortingSettingsRequest.php b/app/Legacy/V1/Requests/Settings/SetSortingSettingsRequest.php new file mode 100644 index 00000000000..e5a38935b10 --- /dev/null +++ b/app/Legacy/V1/Requests/Settings/SetSortingSettingsRequest.php @@ -0,0 +1,95 @@ + ['required', new Enum(ColumnSortingPhotoType::class)], + self::PHOTO_SORTING_ORDER_ATTRIBUTE => ['required', new Enum(OrderSortingType::class)], + self::ALBUM_SORTING_COLUMN_ATTRIBUTE => ['required', new Enum(ColumnSortingAlbumType::class)], + self::ALBUM_SORTING_ORDER_ATTRIBUTE => ['required', new Enum(OrderSortingType::class)], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + $this->photoSortingColumn = ColumnSortingPhotoType::from($values[self::PHOTO_SORTING_COLUMN_ATTRIBUTE]); + $this->photoSortingOrder = OrderSortingType::from($values[self::PHOTO_SORTING_ORDER_ATTRIBUTE]); + $this->albumSortingColumn = ColumnSortingAlbumType::from($values[self::ALBUM_SORTING_COLUMN_ATTRIBUTE]); + $this->albumSortingOrder = OrderSortingType::from($values[self::ALBUM_SORTING_ORDER_ATTRIBUTE]); + } + + /** + * @return ColumnSortingPhotoType + */ + public function photoSortingColumn(): ColumnSortingPhotoType + { + return $this->photoSortingColumn; + } + + /** + * @return OrderSortingType + */ + public function photoSortingOrder(): OrderSortingType + { + return $this->photoSortingOrder; + } + + /** + * @return ColumnSortingAlbumType + */ + public function albumSortingColumn(): ColumnSortingAlbumType + { + return $this->albumSortingColumn; + } + + /** + * @return OrderSortingType + */ + public function albumSortingOrder(): OrderSortingType + { + return $this->albumSortingOrder; + } +} diff --git a/app/Legacy/V1/Requests/Settings/UpdateRequest.php b/app/Legacy/V1/Requests/Settings/UpdateRequest.php new file mode 100644 index 00000000000..963a7ea7347 --- /dev/null +++ b/app/Legacy/V1/Requests/Settings/UpdateRequest.php @@ -0,0 +1,29 @@ +albumIDs]); + } + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return [ + RequestAttribute::ALBUM_IDS_ATTRIBUTE => 'required|array|min:1', + RequestAttribute::ALBUM_IDS_ATTRIBUTE . '.*' => ['required', new RandomIDRule(false)], + RequestAttribute::USER_IDS_ATTRIBUTE => 'required|array|min:1', + RequestAttribute::USER_IDS_ATTRIBUTE . '.*' => ['required', new IntegerIDRule(false)], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + $this->albumIDs = $values[RequestAttribute::ALBUM_IDS_ATTRIBUTE]; + $this->userIDs = $values[RequestAttribute::USER_IDS_ATTRIBUTE]; + } +} diff --git a/app/Legacy/V1/Requests/Sharing/DeleteSharingRequest.php b/app/Legacy/V1/Requests/Sharing/DeleteSharingRequest.php new file mode 100644 index 00000000000..878ef91605c --- /dev/null +++ b/app/Legacy/V1/Requests/Sharing/DeleteSharingRequest.php @@ -0,0 +1,60 @@ + + */ + protected array $shareIDs = []; + + /** + * {@inheritDoc} + */ + public function authorize(): bool + { + return Gate::check(AlbumPolicy::CAN_SHARE_ID, [AbstractAlbum::class, $this->shareIDs]); + } + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return [ + self::SHARE_IDS_ATTRIBUTE => 'required|array|min:1', + self::SHARE_IDS_ATTRIBUTE . '.*' => ['required', new IntegerIDRule(false)], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + $this->shareIDs = $values[self::SHARE_IDS_ATTRIBUTE]; + } + + /** + * @return array + */ + public function shareIDs(): array + { + return $this->shareIDs; + } +} diff --git a/app/Legacy/V1/Requests/Sharing/ListSharingRequest.php b/app/Legacy/V1/Requests/Sharing/ListSharingRequest.php new file mode 100644 index 00000000000..369763b670f --- /dev/null +++ b/app/Legacy/V1/Requests/Sharing/ListSharingRequest.php @@ -0,0 +1,137 @@ +album])) { + return false; + } + + if ($user->may_administrate === true) { + return true; + } + + if ( + ($this->owner?->id === $user->id) || + ($this->participant?->id === $user->id) + ) { + return true; + } + + return false; + } + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return [ + RequestAttribute::ALBUM_ID_ATTRIBUTE => ['sometimes', new RandomIDRule(false)], + self::OWNER_ID_ATTRIBUTE => ['sometimes', new IntegerIDRule(false)], + self::PARTICIPANT_ID_ATTRIBUTE => ['sometimes', new IntegerIDRule(false)], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + $this->album = key_exists(RequestAttribute::ALBUM_ID_ATTRIBUTE, $values) ? + $this->albumFactory->findBaseAlbumOrFail($values[RequestAttribute::ALBUM_ID_ATTRIBUTE]) : + null; + + $this->owner = null; + $this->participant = null; + if (key_exists(self::OWNER_ID_ATTRIBUTE, $values)) { + /** @var int $ownerID */ + $ownerID = $values[self::OWNER_ID_ATTRIBUTE]; + $this->owner = User::query()->findOrFail($ownerID); + } + if (key_exists(self::PARTICIPANT_ID_ATTRIBUTE, $values)) { + /** @var int $participantID */ + $participantID = $values[self::PARTICIPANT_ID_ATTRIBUTE]; + $this->participant = User::query()->findOrFail($participantID); + } + } + + /** + * Returns the optional album owner to which the list of shares shall be + * restricted. + * + * @return User|null + */ + public function owner(): ?User + { + return $this->owner; + } + + /** + * Returns the optional share participant to which the list of shares + * shall be restricted. + * + * @return User|null + */ + public function participant(): ?User + { + return $this->participant; + } +} diff --git a/app/Legacy/V1/Requests/Sharing/SetSharesByAlbumRequest.php b/app/Legacy/V1/Requests/Sharing/SetSharesByAlbumRequest.php new file mode 100644 index 00000000000..53d920335ff --- /dev/null +++ b/app/Legacy/V1/Requests/Sharing/SetSharesByAlbumRequest.php @@ -0,0 +1,61 @@ +album]); + } + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return [ + RequestAttribute::ALBUM_ID_ATTRIBUTE => ['required', new RandomIDRule(false)], + RequestAttribute::USER_IDS_ATTRIBUTE => 'present|array', + RequestAttribute::USER_IDS_ATTRIBUTE . '.*' => ['required', new IntegerIDRule(false)], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + $this->album = $this->albumFactory->findBaseAlbumOrFail($values[RequestAttribute::ALBUM_ID_ATTRIBUTE]); + $this->userIDs = $values[RequestAttribute::USER_IDS_ATTRIBUTE]; + } +} \ No newline at end of file diff --git a/app/Legacy/V1/Requests/Traits/Authorize/AuthorizeCanEditAlbumAlbumsTrait.php b/app/Legacy/V1/Requests/Traits/Authorize/AuthorizeCanEditAlbumAlbumsTrait.php new file mode 100644 index 00000000000..14bd48eed8d --- /dev/null +++ b/app/Legacy/V1/Requests/Traits/Authorize/AuthorizeCanEditAlbumAlbumsTrait.php @@ -0,0 +1,35 @@ +album])) { + return false; + } + + /** @var AbstractAlbum $album */ + foreach ($this->albums as $album) { + if (!Gate::check(AlbumPolicy::CAN_EDIT, $album)) { + return false; + } + } + + return true; + } +} \ No newline at end of file diff --git a/app/Legacy/V1/Requests/Traits/Authorize/AuthorizeCanEditAlbumTrait.php b/app/Legacy/V1/Requests/Traits/Authorize/AuthorizeCanEditAlbumTrait.php new file mode 100644 index 00000000000..c7aae7e4ea0 --- /dev/null +++ b/app/Legacy/V1/Requests/Traits/Authorize/AuthorizeCanEditAlbumTrait.php @@ -0,0 +1,28 @@ +album]); + } +} \ No newline at end of file diff --git a/app/Legacy/V1/Requests/Traits/Authorize/AuthorizeCanEditPhotoTrait.php b/app/Legacy/V1/Requests/Traits/Authorize/AuthorizeCanEditPhotoTrait.php new file mode 100644 index 00000000000..7b164f5578c --- /dev/null +++ b/app/Legacy/V1/Requests/Traits/Authorize/AuthorizeCanEditPhotoTrait.php @@ -0,0 +1,28 @@ +photo]); + } +} \ No newline at end of file diff --git a/app/Legacy/V1/Requests/Traits/Authorize/AuthorizeCanEditPhotosAlbumTrait.php b/app/Legacy/V1/Requests/Traits/Authorize/AuthorizeCanEditPhotosAlbumTrait.php new file mode 100644 index 00000000000..cf8c5a6a11e --- /dev/null +++ b/app/Legacy/V1/Requests/Traits/Authorize/AuthorizeCanEditPhotosAlbumTrait.php @@ -0,0 +1,40 @@ +album])) { + return false; + } + + /** @var Photo $photo */ + foreach ($this->photos as $photo) { + if (!Gate::check(PhotoPolicy::CAN_EDIT, $photo)) { + return false; + } + } + + return true; + } +} \ No newline at end of file diff --git a/app/Legacy/V1/Requests/Traits/Authorize/AuthorizeCanEditPhotosTrait.php b/app/Legacy/V1/Requests/Traits/Authorize/AuthorizeCanEditPhotosTrait.php new file mode 100644 index 00000000000..dffe6f8b867 --- /dev/null +++ b/app/Legacy/V1/Requests/Traits/Authorize/AuthorizeCanEditPhotosTrait.php @@ -0,0 +1,34 @@ +photos as $photo) { + if (!Gate::check(PhotoPolicy::CAN_EDIT, $photo)) { + return false; + } + } + + return true; + } +} \ No newline at end of file diff --git a/app/Legacy/V1/Requests/Traits/HasAbstractAlbumTrait.php b/app/Legacy/V1/Requests/Traits/HasAbstractAlbumTrait.php new file mode 100644 index 00000000000..47737d5d53d --- /dev/null +++ b/app/Legacy/V1/Requests/Traits/HasAbstractAlbumTrait.php @@ -0,0 +1,24 @@ +album; + } +} diff --git a/app/Legacy/V1/Requests/Traits/HasAlbumIDTrait.php b/app/Legacy/V1/Requests/Traits/HasAlbumIDTrait.php new file mode 100644 index 00000000000..7d32b60b377 --- /dev/null +++ b/app/Legacy/V1/Requests/Traits/HasAlbumIDTrait.php @@ -0,0 +1,22 @@ +albumID; + } +} diff --git a/app/Legacy/V1/Requests/Traits/HasAlbumIDsTrait.php b/app/Legacy/V1/Requests/Traits/HasAlbumIDsTrait.php new file mode 100644 index 00000000000..c6ed192d39b --- /dev/null +++ b/app/Legacy/V1/Requests/Traits/HasAlbumIDsTrait.php @@ -0,0 +1,25 @@ +albumIDs; + } +} diff --git a/app/Legacy/V1/Requests/Traits/HasAlbumSortingCriterionTrait.php b/app/Legacy/V1/Requests/Traits/HasAlbumSortingCriterionTrait.php new file mode 100644 index 00000000000..71f7014690d --- /dev/null +++ b/app/Legacy/V1/Requests/Traits/HasAlbumSortingCriterionTrait.php @@ -0,0 +1,24 @@ +albumSortingCriterion; + } +} diff --git a/app/Legacy/V1/Requests/Traits/HasAlbumTrait.php b/app/Legacy/V1/Requests/Traits/HasAlbumTrait.php new file mode 100644 index 00000000000..39f9c056bb7 --- /dev/null +++ b/app/Legacy/V1/Requests/Traits/HasAlbumTrait.php @@ -0,0 +1,24 @@ +album; + } +} diff --git a/app/Legacy/V1/Requests/Traits/HasAlbumsTrait.php b/app/Legacy/V1/Requests/Traits/HasAlbumsTrait.php new file mode 100644 index 00000000000..4cbb24d6106 --- /dev/null +++ b/app/Legacy/V1/Requests/Traits/HasAlbumsTrait.php @@ -0,0 +1,30 @@ + + */ + protected Collection $albums; + + /** + * @return Collection + */ + public function albums(): Collection + { + return $this->albums; + } +} diff --git a/app/Legacy/V1/Requests/Traits/HasAspectRatioTrait.php b/app/Legacy/V1/Requests/Traits/HasAspectRatioTrait.php new file mode 100644 index 00000000000..295db73dca8 --- /dev/null +++ b/app/Legacy/V1/Requests/Traits/HasAspectRatioTrait.php @@ -0,0 +1,24 @@ +aspectRatio(); + } +} diff --git a/app/Legacy/V1/Requests/Traits/HasBaseAlbumTrait.php b/app/Legacy/V1/Requests/Traits/HasBaseAlbumTrait.php new file mode 100644 index 00000000000..b5b0a5f7dcb --- /dev/null +++ b/app/Legacy/V1/Requests/Traits/HasBaseAlbumTrait.php @@ -0,0 +1,24 @@ +album; + } +} diff --git a/app/Legacy/V1/Requests/Traits/HasCopyrightTrait.php b/app/Legacy/V1/Requests/Traits/HasCopyrightTrait.php new file mode 100644 index 00000000000..a58183e0662 --- /dev/null +++ b/app/Legacy/V1/Requests/Traits/HasCopyrightTrait.php @@ -0,0 +1,22 @@ +copyright; + } +} diff --git a/app/Legacy/V1/Requests/Traits/HasDateTrait.php b/app/Legacy/V1/Requests/Traits/HasDateTrait.php new file mode 100644 index 00000000000..172062bb06a --- /dev/null +++ b/app/Legacy/V1/Requests/Traits/HasDateTrait.php @@ -0,0 +1,24 @@ +date; + } +} \ No newline at end of file diff --git a/app/Legacy/V1/Requests/Traits/HasDescriptionTrait.php b/app/Legacy/V1/Requests/Traits/HasDescriptionTrait.php new file mode 100644 index 00000000000..cfea6702e88 --- /dev/null +++ b/app/Legacy/V1/Requests/Traits/HasDescriptionTrait.php @@ -0,0 +1,22 @@ +description; + } +} diff --git a/app/Legacy/V1/Requests/Traits/HasIsPublicTrait.php b/app/Legacy/V1/Requests/Traits/HasIsPublicTrait.php new file mode 100644 index 00000000000..f8583dd5116 --- /dev/null +++ b/app/Legacy/V1/Requests/Traits/HasIsPublicTrait.php @@ -0,0 +1,22 @@ +is_public; + } +} diff --git a/app/Legacy/V1/Requests/Traits/HasLicenseTrait.php b/app/Legacy/V1/Requests/Traits/HasLicenseTrait.php new file mode 100644 index 00000000000..fe6388c4568 --- /dev/null +++ b/app/Legacy/V1/Requests/Traits/HasLicenseTrait.php @@ -0,0 +1,24 @@ +license; + } +} \ No newline at end of file diff --git a/app/Legacy/V1/Requests/Traits/HasOptionalUserTrait.php b/app/Legacy/V1/Requests/Traits/HasOptionalUserTrait.php new file mode 100644 index 00000000000..ea033cad6d9 --- /dev/null +++ b/app/Legacy/V1/Requests/Traits/HasOptionalUserTrait.php @@ -0,0 +1,35 @@ +user2; + } +} \ No newline at end of file diff --git a/app/Legacy/V1/Requests/Traits/HasParentAlbumTrait.php b/app/Legacy/V1/Requests/Traits/HasParentAlbumTrait.php new file mode 100644 index 00000000000..0d2c7d853d4 --- /dev/null +++ b/app/Legacy/V1/Requests/Traits/HasParentAlbumTrait.php @@ -0,0 +1,24 @@ +parentAlbum; + } +} diff --git a/app/Legacy/V1/Requests/Traits/HasPasswordTrait.php b/app/Legacy/V1/Requests/Traits/HasPasswordTrait.php new file mode 100644 index 00000000000..7dc5a4857a7 --- /dev/null +++ b/app/Legacy/V1/Requests/Traits/HasPasswordTrait.php @@ -0,0 +1,37 @@ +password; + } +} diff --git a/app/Legacy/V1/Requests/Traits/HasPhotoIDTrait.php b/app/Legacy/V1/Requests/Traits/HasPhotoIDTrait.php new file mode 100644 index 00000000000..038cd5f0b4a --- /dev/null +++ b/app/Legacy/V1/Requests/Traits/HasPhotoIDTrait.php @@ -0,0 +1,22 @@ +photoID; + } +} diff --git a/app/Legacy/V1/Requests/Traits/HasPhotoIDsTrait.php b/app/Legacy/V1/Requests/Traits/HasPhotoIDsTrait.php new file mode 100644 index 00000000000..ae44b203a58 --- /dev/null +++ b/app/Legacy/V1/Requests/Traits/HasPhotoIDsTrait.php @@ -0,0 +1,25 @@ +photoIDs; + } +} diff --git a/app/Legacy/V1/Requests/Traits/HasPhotoSortingCriterionTrait.php b/app/Legacy/V1/Requests/Traits/HasPhotoSortingCriterionTrait.php new file mode 100644 index 00000000000..2064f9fb5fe --- /dev/null +++ b/app/Legacy/V1/Requests/Traits/HasPhotoSortingCriterionTrait.php @@ -0,0 +1,24 @@ +photoSortingCriterion; + } +} diff --git a/app/Legacy/V1/Requests/Traits/HasPhotoTrait.php b/app/Legacy/V1/Requests/Traits/HasPhotoTrait.php new file mode 100644 index 00000000000..a0f45fcb44e --- /dev/null +++ b/app/Legacy/V1/Requests/Traits/HasPhotoTrait.php @@ -0,0 +1,24 @@ +photo; + } +} diff --git a/app/Legacy/V1/Requests/Traits/HasPhotosTrait.php b/app/Legacy/V1/Requests/Traits/HasPhotosTrait.php new file mode 100644 index 00000000000..a0fd5c31433 --- /dev/null +++ b/app/Legacy/V1/Requests/Traits/HasPhotosTrait.php @@ -0,0 +1,28 @@ + + */ + protected Collection $photos; + + /** + * @return Collection + */ + public function photos(): Collection + { + return $this->photos; + } +} diff --git a/app/Legacy/V1/Requests/Traits/HasSizeVariantTrait.php b/app/Legacy/V1/Requests/Traits/HasSizeVariantTrait.php new file mode 100644 index 00000000000..fc8571f9ec8 --- /dev/null +++ b/app/Legacy/V1/Requests/Traits/HasSizeVariantTrait.php @@ -0,0 +1,24 @@ +sizeVariant; + } +} diff --git a/app/Legacy/V1/Requests/Traits/HasSortingCriterionTrait.php b/app/Legacy/V1/Requests/Traits/HasSortingCriterionTrait.php new file mode 100644 index 00000000000..30e30cc80f1 --- /dev/null +++ b/app/Legacy/V1/Requests/Traits/HasSortingCriterionTrait.php @@ -0,0 +1,24 @@ +sortingCriterion; + } +} diff --git a/app/Legacy/V1/Requests/Traits/HasTagAlbumTrait.php b/app/Legacy/V1/Requests/Traits/HasTagAlbumTrait.php new file mode 100644 index 00000000000..51dea8fc7f7 --- /dev/null +++ b/app/Legacy/V1/Requests/Traits/HasTagAlbumTrait.php @@ -0,0 +1,24 @@ +album; + } +} diff --git a/app/Legacy/V1/Requests/Traits/HasTagsTrait.php b/app/Legacy/V1/Requests/Traits/HasTagsTrait.php new file mode 100644 index 00000000000..b5144e97844 --- /dev/null +++ b/app/Legacy/V1/Requests/Traits/HasTagsTrait.php @@ -0,0 +1,25 @@ +tags; + } +} diff --git a/app/Legacy/V1/Requests/Traits/HasTitleTrait.php b/app/Legacy/V1/Requests/Traits/HasTitleTrait.php new file mode 100644 index 00000000000..f42ae508b62 --- /dev/null +++ b/app/Legacy/V1/Requests/Traits/HasTitleTrait.php @@ -0,0 +1,22 @@ +title; + } +} diff --git a/app/Legacy/V1/Requests/Traits/HasUserIDTrait.php b/app/Legacy/V1/Requests/Traits/HasUserIDTrait.php new file mode 100644 index 00000000000..bf5b31ad4ba --- /dev/null +++ b/app/Legacy/V1/Requests/Traits/HasUserIDTrait.php @@ -0,0 +1,25 @@ +userID; + } +} diff --git a/app/Legacy/V1/Requests/Traits/HasUserIDsTrait.php b/app/Legacy/V1/Requests/Traits/HasUserIDsTrait.php new file mode 100644 index 00000000000..bc0d598d035 --- /dev/null +++ b/app/Legacy/V1/Requests/Traits/HasUserIDsTrait.php @@ -0,0 +1,25 @@ + + */ + protected array $userIDs = []; + + /** + * @return array + */ + public function userIDs(): array + { + return $this->userIDs; + } +} diff --git a/app/Legacy/V1/Requests/Traits/HasUserTrait.php b/app/Legacy/V1/Requests/Traits/HasUserTrait.php new file mode 100644 index 00000000000..3ad5ceeb88b --- /dev/null +++ b/app/Legacy/V1/Requests/Traits/HasUserTrait.php @@ -0,0 +1,35 @@ +user2; + } +} \ No newline at end of file diff --git a/app/Legacy/V1/Requests/Traits/HasUsernameTrait.php b/app/Legacy/V1/Requests/Traits/HasUsernameTrait.php new file mode 100644 index 00000000000..4da4aa540e0 --- /dev/null +++ b/app/Legacy/V1/Requests/Traits/HasUsernameTrait.php @@ -0,0 +1,22 @@ +username; + } +} diff --git a/app/Legacy/V1/Requests/User/ChangeLoginRequest.php b/app/Legacy/V1/Requests/User/ChangeLoginRequest.php new file mode 100644 index 00000000000..2a114430192 --- /dev/null +++ b/app/Legacy/V1/Requests/User/ChangeLoginRequest.php @@ -0,0 +1,83 @@ +password = $values[RequestAttribute::PASSWORD_ATTRIBUTE]; + $this->oldPassword = $values[RequestAttribute::OLD_PASSWORD_ATTRIBUTE]; + + // We do not allow '' as a username. So any such input will be cast to null + if (array_key_exists(RequestAttribute::USERNAME_ATTRIBUTE, $values)) { + $this->username = trim($values[RequestAttribute::USERNAME_ATTRIBUTE]); + $this->username = $this->username === '' ? null : $this->username; + } else { + $this->username = null; + } + } + + /** + * Returns the previous password. + * + * See {@link HasPasswordTrait::password()} for an explanation of the + * semantic difference between the return values `null` and `''`. + * + * @return string|null + */ + public function oldPassword(): ?string + { + return $this->oldPassword; + } + + /** + * Return the new username chosen. + * if Username is null, this means that the user does not want to update it. + * + * @return ?string + */ + public function username(): ?string + { + return $this->username; + } +} \ No newline at end of file diff --git a/app/Legacy/V1/Requests/User/ChangeTokenRequest.php b/app/Legacy/V1/Requests/User/ChangeTokenRequest.php new file mode 100644 index 00000000000..9f4d4293332 --- /dev/null +++ b/app/Legacy/V1/Requests/User/ChangeTokenRequest.php @@ -0,0 +1,25 @@ +email = $values[RequestAttribute::EMAIL_ATTRIBUTE]; + } + + public function email(): ?string + { + return $this->email; + } +} diff --git a/app/Legacy/V1/Requests/Users/AddUserRequest.php b/app/Legacy/V1/Requests/Users/AddUserRequest.php new file mode 100644 index 00000000000..f2e0a1ebef7 --- /dev/null +++ b/app/Legacy/V1/Requests/Users/AddUserRequest.php @@ -0,0 +1,66 @@ +username = $values[RequestAttribute::USERNAME_ATTRIBUTE]; + $this->password = $values[RequestAttribute::PASSWORD_ATTRIBUTE]; + $this->mayUpload = static::toBoolean($values[RequestAttribute::MAY_UPLOAD_ATTRIBUTE]); + $this->mayEditOwnSettings = static::toBoolean($values[RequestAttribute::MAY_EDIT_OWN_SETTINGS_ATTRIBUTE]); + } + + public function mayUpload(): bool + { + return $this->mayUpload; + } + + public function mayEditOwnSettings(): bool + { + return $this->mayEditOwnSettings; + } +} diff --git a/app/Legacy/V1/Requests/Users/DeleteUserRequest.php b/app/Legacy/V1/Requests/Users/DeleteUserRequest.php new file mode 100644 index 00000000000..ded5057fc5f --- /dev/null +++ b/app/Legacy/V1/Requests/Users/DeleteUserRequest.php @@ -0,0 +1,51 @@ + ['required', new IntegerIDRule(false)], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + /** @var int $userID */ + $userID = $values[RequestAttribute::ID_ATTRIBUTE]; + $this->user2 = User::query()->findOrFail($userID); + } +} diff --git a/app/Legacy/V1/Requests/Users/ListUsersRequest.php b/app/Legacy/V1/Requests/Users/ListUsersRequest.php new file mode 100644 index 00000000000..a00ae8cb4ea --- /dev/null +++ b/app/Legacy/V1/Requests/Users/ListUsersRequest.php @@ -0,0 +1,25 @@ +username = $values[RequestAttribute::USERNAME_ATTRIBUTE]; + if (array_key_exists(RequestAttribute::PASSWORD_ATTRIBUTE, $values)) { + // See {@link HasPasswordTrait::password()} for an explanation + // of the semantic difference between `null` and `''`. + $this->password = $values[RequestAttribute::PASSWORD_ATTRIBUTE] ?? ''; + } else { + $this->password = null; + } + $this->mayUpload = static::toBoolean($values[RequestAttribute::MAY_UPLOAD_ATTRIBUTE]); + $this->mayEditOwnSettings = static::toBoolean($values[RequestAttribute::MAY_EDIT_OWN_SETTINGS_ATTRIBUTE]); + /** @var int $userID */ + $userID = $values[RequestAttribute::ID_ATTRIBUTE]; + $this->user2 = User::query()->findOrFail($userID); + } + + public function mayUpload(): bool + { + return $this->mayUpload; + } + + public function mayEditOwnSettings(): bool + { + return $this->mayEditOwnSettings; + } +} diff --git a/app/Legacy/V1/Requests/View/GetPhotoViewRequest.php b/app/Legacy/V1/Requests/View/GetPhotoViewRequest.php new file mode 100644 index 00000000000..a7e38d3cb6f --- /dev/null +++ b/app/Legacy/V1/Requests/View/GetPhotoViewRequest.php @@ -0,0 +1,53 @@ +photo); + } + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return [ + self::URL_QUERY_PARAM => ['required', new RandomIDRule(false)], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + /** @var ?string $photoID */ + $photoID = $values[self::URL_QUERY_PARAM]; + $this->photo = Photo::query() + ->with(['album', 'size_variants', 'size_variants.sym_links']) + ->findOrFail($photoID); + } +} diff --git a/app/Legacy/V1/Resources/Collections/AlbumCollectionResource.php b/app/Legacy/V1/Resources/Collections/AlbumCollectionResource.php new file mode 100644 index 00000000000..ae4ad68839b --- /dev/null +++ b/app/Legacy/V1/Resources/Collections/AlbumCollectionResource.php @@ -0,0 +1,25 @@ + $albums + * @param Collection|null $sharedAlbums + * + * @return void + */ + public function __construct( + public Collection $albums, + public ?Collection $sharedAlbums = null, + ) { + // Laravel applies a shortcut when this value === null but not when it is something else. + parent::__construct('must_not_be_null'); + + $this->albums = $albums; + $this->sharedAlbums = $sharedAlbums ?? new Collection(); + } + + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'albums' => AlbumTreeResource::collection($this->albums), + 'shared_albums' => AlbumTreeResource::collection($this->sharedAlbums), + ]; + } +} \ No newline at end of file diff --git a/app/Legacy/V1/Resources/Collections/PhotoCollectionResource.php b/app/Legacy/V1/Resources/Collections/PhotoCollectionResource.php new file mode 100644 index 00000000000..42e994102fb --- /dev/null +++ b/app/Legacy/V1/Resources/Collections/PhotoCollectionResource.php @@ -0,0 +1,72 @@ +|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + if ($this->collection->count() === 0) { + return []; + } + + $photos = []; + $i = 0; + + /** @var PhotoResource $photoResource the photo */ + foreach ($this->collection as $photoResource) { + // We need to specify the return type to inform Phpstan that the appropriate property exists. + // Alternatively we could document properly the PhotoResource::toArray() but then the phpdoc + // of returns becomes a bit too messy. + /** @var array{id:string} $photoArray */ + $photoArray = $photoResource->toArray($request); + $photos[] = $photoArray; + if ($i > 0) { + $photos[$i - 1]['next_photo_id'] = $photos[$i]['id']; + $photos[$i]['previous_photo_id'] = $photos[$i - 1]['id']; + } + $i++; + } + + $count = count($photos); + + if ($count > 1 && Configs::getValueAsBool('photos_wraparound')) { + $photos[0]['previous_photo_id'] = $photos[$count - 1]['id']; + $photos[$count - 1]['next_photo_id'] = $photos[0]['id']; + } else { + $photos[0]['previous_photo_id'] = null; + $photos[$count - 1]['next_photo_id'] = null; + } + + return $photos; + } +} diff --git a/app/Legacy/V1/Resources/Collections/PositionDataResource.php b/app/Legacy/V1/Resources/Collections/PositionDataResource.php new file mode 100644 index 00000000000..3de1dffb3dc --- /dev/null +++ b/app/Legacy/V1/Resources/Collections/PositionDataResource.php @@ -0,0 +1,58 @@ + $photos the collection of photos with position data to be shown on map + * @param string|null $track_url the URL of the album's track + */ + public function __construct( + ?string $id, + ?string $title, + Collection $photos, + ?string $track_url, + ) { + parent::__construct($photos); + $this->id = $id; + $this->title = $title; + $this->track_url = $track_url; + } + + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'id' => $this->id, + 'title' => $this->title, + 'photos' => PhotoResource::collection($this->resource), + 'track_url' => $this->track_url, + ]; + } +} \ No newline at end of file diff --git a/app/Legacy/V1/Resources/Collections/TopAlbumsResource.php b/app/Legacy/V1/Resources/Collections/TopAlbumsResource.php new file mode 100644 index 00000000000..dd5cf3db206 --- /dev/null +++ b/app/Legacy/V1/Resources/Collections/TopAlbumsResource.php @@ -0,0 +1,65 @@ + $smart_albums + * @param Collection $tag_albums + * @param Collection $albums + * @param Collection|null $shared_albums + * + * @return void + */ + public function __construct( + public Collection $smart_albums, + public Collection $tag_albums, + public Collection $albums, + public ?Collection $shared_albums = null, + ) { + // Laravel applies a shortcut when this value === null but not when it is something else. + parent::__construct('must_not_be_null'); + + $this->shared_albums ??= new Collection(); + } + + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'smart_albums' => SmartAlbumResource::collection($this->smart_albums), + 'tag_albums' => TagAlbumResource::collection($this->tag_albums), + 'albums' => AlbumResource::collection($this->albums), + 'shared_albums' => AlbumResource::collection($this->shared_albums), + ]; + } +} \ No newline at end of file diff --git a/app/Legacy/V1/Resources/ConfigurationResource.php b/app/Legacy/V1/Resources/ConfigurationResource.php new file mode 100644 index 00000000000..63753b69e0a --- /dev/null +++ b/app/Legacy/V1/Resources/ConfigurationResource.php @@ -0,0 +1,196 @@ + + */ + public function toArray($request): array + { + $lycheeVersion = resolve(InstalledVersion::class); + $isAdmin = Auth::user()?->may_administrate === true; + $rss_feeds = []; + + if (Configs::getValueAsBool('rss_enable')) { + try { + /** @var array $feeds */ + $feeds = resolve(Repository::class)->get('feed.feeds', []); + foreach ($feeds as $name => $feed) { + $rss_feeds[] = [ + 'url' => route("feeds.{$name}"), + 'mimetype' => FeedContentType::forLink($feed['format'] ?? 'atom'), + 'title' => $feed['title'] ?? '', + ]; + } + } catch (\Throwable $e) { + // do nothing, but report the exception, if the + // configuration for the RSS feed cannot be loaded or + // if the route to any RSS feed or the mime type of any + // feed cannot be resolved + Handler::reportSafely($e); + $rss_feeds = []; + } + } + + /** @phpstan-ignore-next-line */ + return [ + // Computed + 'lang_available' => $this->when(Auth::check(), config('app.supported_locale')), + 'version' => $this->when(Auth::check() || !Configs::getValueAsBool('hide_version_number'), $lycheeVersion->getVersion()), + 'rss_feeds' => $rss_feeds, + 'allow_username_change' => $this->when(Auth::check(), Configs::getValueAsBool('allow_username_change')), + + // Config attributes + // Admin + $this->mergeWhen($isAdmin, [ + // computerd + 'location' => base_path('public/'), + + // from config + 'SA_enabled' => SmartAlbumType::UNSORTED->is_enabled() && + SmartAlbumType::STARRED->is_enabled() && + SmartAlbumType::RECENT->is_enabled() && + SmartAlbumType::ON_THIS_DAY->is_enabled(), + 'SL_enable' => Configs::getValueAsBool('SL_enable'), + 'SL_for_admin' => Configs::getValueAsBool('SL_for_admin'), + 'SL_life_time_days' => Configs::getValueAsInt('SL_life_time_days'), + 'allow_online_git_pull' => Configs::getValueAsBool('allow_online_git_pull'), + 'apply_composer_update' => Configs::getValueAsBool('apply_composer_update'), + 'compression_quality' => Configs::getValueAsInt('compression_quality'), + 'default_license' => Configs::getValueAsEnum('default_license', LicenseType::class), + 'delete_imported' => Configs::getValueAsBool('delete_imported'), + 'dropbox_key' => Configs::getValueAsString('dropbox_key'), + 'editor_enabled' => Configs::getValueAsBool('editor_enabled'), + 'auto_fix_orientation' => Configs::getValueAsBool('auto_fix_orientation'), + 'force_32bit_ids' => Configs::getValueAsBool('force_32bit_ids'), + 'force_migration_in_production' => Configs::getValueAsBool('force_migration_in_production'), + 'has_exiftool' => Configs::getValueAsBool('has_exiftool'), + 'has_ffmpeg' => Configs::getValueAsBool('has_ffmpeg'), + 'hide_version_number' => Configs::getValueAsBool('hide_version_number'), + 'imagick' => Configs::getValueAsBool('imagick'), + 'import_via_symlink' => Configs::getValueAsBool('import_via_symlink'), + 'landing_background' => Configs::getValueAsString('landing_background'), + 'landing_subtitle' => Configs::getValueAsString('landing_subtitle'), + 'landing_title' => Configs::getValueAsString('landing_title'), + 'local_takestamp_video_formats' => Configs::getValueAsString('local_takestamp_video_formats'), + 'log_max_num_line' => Configs::getValueAsInt('log_max_num_line'), + 'lossless_optimization' => Configs::getValueAsBool('lossless_optimization'), + 'medium_2x' => Configs::getValueAsBool('medium_2x'), + 'medium_max_height' => Configs::getValueAsInt('medium_max_height'), + 'medium_max_width' => Configs::getValueAsInt('medium_max_width'), + 'prefer_available_xmp_metadata' => Configs::getValueAsBool('prefer_available_xmp_metadata'), + 'raw_formats' => Configs::getValueAsString('raw_formats'), + 'recent_age' => Configs::getValueAsInt('recent_age'), + 'skip_duplicates' => Configs::getValueAsBool('skip_duplicates'), + 'small_2x' => Configs::getValueAsBool('small_2x'), + 'small_max_height' => Configs::getValueAsInt('small_max_height'), + 'small_max_width' => Configs::getValueAsInt('small_max_width'), + 'thumb_2x' => Configs::getValueAsBool('thumb_2x'), + 'unlock_password_photos_with_url_param' => Configs::getValueAsBool('unlock_password_photos_with_url_param'), + 'use_last_modified_date_when_no_exif_date' => Configs::getValueAsBool('use_last_modified_date_when_no_exif_date'), + 'smart_album_visibilty' => [ + 'recent' => RecentAlbum::getInstance()->public_permissions() !== null, + 'starred' => StarredAlbum::getInstance()->public_permissions() !== null, + 'on_this_day' => OnThisDayAlbum::getInstance()->public_permissions() !== null, + ], + ]), + + 'album_decoration' => Configs::getValueAsEnum('album_decoration', AlbumDecorationType::class), + 'album_decoration_orientation' => Configs::getValueAsEnum('album_decoration_orientation', AlbumDecorationOrientation::class), + 'album_subtitle_type' => Configs::getValueAsEnum('album_subtitle_type', ThumbAlbumSubtitleType::class), + 'check_for_updates' => Configs::getValueAsBool('check_for_updates'), + 'default_album_protection' => Configs::getValueAsEnum('default_album_protection', DefaultAlbumProtectionType::class), + 'feeds' => [], + 'footer_additional_text' => Configs::getValueAsString('footer_additional_text'), + 'footer_show_copyright' => Configs::getValueAsBool('footer_show_copyright'), + 'footer_show_social_media' => Configs::getValueAsBool('footer_show_social_media'), + 'grants_download' => Configs::getValueAsBool('grants_download'), + 'grants_full_photo_access' => Configs::getValueAsBool('grants_full_photo_access'), + 'image_overlay_type' => Configs::getValueAsEnum('image_overlay_type', ImageOverlayType::class), + 'landing_page_enable' => Configs::getValueAsBool('landing_page_enable'), + 'lang' => Configs::getValueAsString('lang'), + 'layout' => Configs::getValueAsEnum('layout', PhotoLayoutType::class), + 'legacy_id_redirection' => Configs::getValueAsBool('legacy_id_redirection'), + 'location_decoding' => Configs::getValueAsBool('location_decoding'), + 'location_decoding_timeout' => Configs::getValueAsInt('location_decoding_timeout'), + 'location_show' => Configs::getValueAsBool('location_show'), + 'location_show_public' => Configs::getValueAsBool('location_show_public'), + 'map_display' => Configs::getValueAsBool('map_display'), + 'map_display_direction' => Configs::getValueAsString('map_display_direction'), + 'map_display_public' => Configs::getValueAsBool('map_display_public'), + 'map_include_subalbums' => Configs::getValueAsBool('map_include_subalbums'), + 'map_provider' => Configs::getValueAsEnum('map_provider', MapProviders::class), + 'mod_frame_enabled' => Configs::getValueAsBool('mod_frame_enabled'), + 'mod_frame_refresh' => Configs::getValueAsInt('mod_frame_refresh'), + 'new_photos_notification' => Configs::getValueAsBool('new_photos_notification'), + 'nsfw_banner_override' => Configs::getValueAsString('nsfw_banner_override'), + 'nsfw_blur' => Configs::getValueAsBool('nsfw_blur'), + 'nsfw_visible' => Configs::getValueAsBool('nsfw_visible'), + 'nsfw_warning' => Configs::getValueAsBool('nsfw_warning'), + 'nsfw_warning_admin' => Configs::getValueAsBool('nsfw_warning_admin'), + 'photos_wraparound' => Configs::getValueAsBool('photos_wraparound'), + 'public_search' => Configs::getValueAsBool('search_public'), // legacy + 'rss_enable' => Configs::getValueAsBool('rss_enable'), + 'rss_max_items' => Configs::getValueAsInt('rss_max_items'), + 'rss_recent_days' => Configs::getValueAsInt('rss_recent_days'), + 'share_button_visible' => Configs::getValueAsBool('share_button_visible'), + 'site_copyright_begin' => Configs::getValueAsInt('site_copyright_begin'), + 'site_copyright_end' => Configs::getValueAsInt('site_copyright_end'), + 'site_owner' => Configs::getValueAsString('site_owner'), + 'site_title' => Configs::getValueAsString('site_title'), + 'sm_facebook_url' => Configs::getValueAsString('sm_facebook_url'), + 'sm_flickr_url' => Configs::getValueAsString('sm_flickr_url'), + 'sm_instagram_url' => Configs::getValueAsString('sm_instagram_url'), + 'sm_twitter_url' => Configs::getValueAsString('sm_twitter_url'), + 'sm_youtube_url' => Configs::getValueAsString('sm_youtube_url'), + 'sorting_albums' => AlbumSortingCriterion::createDefault(), + 'sorting_photos' => PhotoSortingCriterion::createDefault(), + 'swipe_tolerance_x' => Configs::getValueAsInt('swipe_tolerance_x'), + 'swipe_tolerance_y' => Configs::getValueAsInt('swipe_tolerance_y'), + 'update_check_every_days' => Configs::getValueAsInt('update_check_every_days'), + 'upload_processing_limit' => Configs::getValueAsInt('upload_processing_limit'), + 'zip64' => Configs::getValueAsBool('zip64'), + 'zip_deflate_level' => Configs::getValueAsInt('zip_deflate_level'), + ]; + } +} diff --git a/app/Legacy/V1/Resources/InitResource.php b/app/Legacy/V1/Resources/InitResource.php new file mode 100644 index 00000000000..cb25af4c9d5 --- /dev/null +++ b/app/Legacy/V1/Resources/InitResource.php @@ -0,0 +1,56 @@ + + */ + public function toArray($request): array + { + $fileVersion = resolve(FileVersion::class); + $gitHubVersion = resolve(GitHubVersion::class); + + if (Configs::getValueAsBool('check_for_updates')) { + $fileVersion->hydrate(); + $gitHubVersion->hydrate(); + } + + // we also return the locale + $locale = include base_path('lang/' . app()->getLocale() . '/lychee.php'); + + return [ + 'user' => $this->when(Auth::check(), UserResource::make(Auth::user()), null), + 'rights' => GlobalRightsResource::make(), + 'config' => ConfigurationResource::make(), + 'update_json' => !$fileVersion->isUpToDate(), + 'update_available' => !$gitHubVersion->isUpToDate(), + 'locale' => $locale, + ]; + } +} diff --git a/app/Legacy/V1/Resources/Models/AlbumResource.php b/app/Legacy/V1/Resources/Models/AlbumResource.php new file mode 100644 index 00000000000..84c9adcd054 --- /dev/null +++ b/app/Legacy/V1/Resources/Models/AlbumResource.php @@ -0,0 +1,78 @@ +|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + // basic + 'id' => $this->resource->id, + 'title' => $this->resource->title, + 'owner_name' => $this->when(Auth::check(), $this->resource->owner->name), + 'copyright' => $this->resource->copyright, + + // attributes + 'description' => $this->resource->description, + 'track_url' => $this->resource->track_url, + 'license' => $this->resource->license->localization(), + 'sorting' => $this->resource->photo_sorting, + 'header_id' => $this->resource->header_id, + + // children + 'parent_id' => $this->resource->parent_id, + 'has_albums' => !$this->resource->isLeaf(), + 'albums' => AlbumCollectionResource::make($this->whenLoaded('children')), + 'photos' => PhotoCollectionResource::make($this->whenLoaded('photos')), + 'num_subalbums' => $this->resource->num_children, + 'num_photos' => $this->resource->num_photos, + + // thumb + 'cover_id' => $this->resource->cover_id, + 'thumb' => $this->resource->thumb, + + // timestamps + 'created_at' => $this->resource->created_at->toIso8601String(), + 'updated_at' => $this->resource->updated_at->toIso8601String(), + 'max_taken_at' => $this->resource->max_taken_at?->toIso8601String(), + 'min_taken_at' => $this->resource->min_taken_at?->toIso8601String(), + + // security + 'policy' => AlbumProtectionPolicy::ofBaseAlbum($this->resource), + 'rights' => AlbumRightsResource::make($this->resource)->toArray($request), + ]; + } +} diff --git a/app/Legacy/V1/Resources/Models/AlbumTreeResource.php b/app/Legacy/V1/Resources/Models/AlbumTreeResource.php new file mode 100644 index 00000000000..699b392c139 --- /dev/null +++ b/app/Legacy/V1/Resources/Models/AlbumTreeResource.php @@ -0,0 +1,49 @@ +|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + // basic + 'id' => $this->resource->id, + 'title' => $this->resource->title, + 'parent_id' => $this->resource->parent_id, + 'thumb' => $this->resource->thumb, + 'albums' => AlbumTreeResource::collection($this->whenLoaded('children')), + ]; + } +} \ No newline at end of file diff --git a/app/Legacy/V1/Resources/Models/PhotoResource.php b/app/Legacy/V1/Resources/Models/PhotoResource.php new file mode 100644 index 00000000000..a70e441efa6 --- /dev/null +++ b/app/Legacy/V1/Resources/Models/PhotoResource.php @@ -0,0 +1,187 @@ +|Arrayable|\JsonSerializable + */ + public function toArray($request) + { + /** @var SizeVariants|MissingValue $size_variants */ + $size_variants = $this->whenLoaded('size_variants'); + if ($size_variants instanceof MissingValue) { + $size_variants = null; + } + $downgrade = !Gate::check(PhotoPolicy::CAN_ACCESS_FULL_PHOTO, [Photo::class, $this->resource]) && + !$this->resource->isVideo() && + $size_variants?->hasMedium() === true; + + $medium = $size_variants?->getSizeVariant(SizeVariantType::MEDIUM); + $medium2x = $size_variants?->getSizeVariant(SizeVariantType::MEDIUM2X); + $original = $size_variants?->getSizeVariant(SizeVariantType::ORIGINAL); + $small = $size_variants?->getSizeVariant(SizeVariantType::SMALL); + $small2x = $size_variants?->getSizeVariant(SizeVariantType::SMALL2X); + $thumb = $size_variants?->getSizeVariant(SizeVariantType::THUMB); + $thumb2x = $size_variants?->getSizeVariant(SizeVariantType::THUMB2X); + $placeholder = $size_variants?->getSizeVariant(SizeVariantType::PLACEHOLDER); + + return [ + 'id' => $this->resource->id, + 'album_id' => $this->resource->album_id, + 'altitude' => $this->resource->altitude, + 'aperture' => $this->resource->aperture, + 'checksum' => $this->resource->checksum, + 'created_at' => $this->resource->created_at->toIso8601String(), + 'description' => $this->resource->description, + 'focal' => $this->resource->focal, + 'img_direction' => null, + 'is_starred' => $this->resource->is_starred, + 'iso' => $this->resource->iso, + 'latitude' => $this->resource->latitude, + 'lens' => $this->resource->lens, + 'license' => $this->resource->license, + 'live_photo_checksum' => $this->resource->live_photo_checksum, + 'live_photo_content_id' => $this->resource->live_photo_content_id, + 'live_photo_url' => $this->resource->live_photo_url, + 'location' => $this->resource->location, + 'longitude' => $this->resource->longitude, + 'make' => $this->resource->make, + 'model' => $this->resource->model, + 'original_checksum' => $this->resource->original_checksum, + 'shutter' => $this->resource->shutter, + 'size_variants' => [ + 'medium' => $medium === null ? null : SizeVariantResource::make($medium)->toArray($request), + 'medium2x' => $medium2x === null ? null : SizeVariantResource::make($medium2x)->toArray($request), + 'original' => $original === null ? null : SizeVariantResource::make($original)->setNoUrl($downgrade)->toArray($request), + 'small' => $small === null ? null : SizeVariantResource::make($small)->toArray($request), + 'small2x' => $small2x === null ? null : SizeVariantResource::make($small2x)->toArray($request), + 'thumb' => $thumb === null ? null : SizeVariantResource::make($thumb)->toArray($request), + 'thumb2x' => $thumb2x === null ? null : SizeVariantResource::make($thumb2x)->toArray($request), + 'placeholder' => $placeholder === null ? null : SizeVariantResource::make($placeholder)->toArray($request), + ], + 'tags' => $this->resource->tags, + 'taken_at' => $this->resource->taken_at?->toIso8601String(), + 'taken_at_orig_tz' => $this->resource->taken_at_orig_tz, + 'title' => $this->resource->title, + 'type' => $this->resource->type, + 'updated_at' => $this->resource->updated_at->toIso8601String(), + 'rights' => PhotoRightsResource::make($this->resource)->toArray($request), + 'next_photo_id' => null, + 'previous_photo_id' => null, + 'preformatted' => $this->preformatted($original), + 'precomputed' => $this->precomputed(), + ]; + } + + /** + * @param SizeVariant|null $original + * + * @return array + */ + private function preformatted(?SizeVariant $original): array + { + $overlay_date_format = Configs::getValueAsString('date_format_photo_overlay'); + $date_format_uploaded = Configs::getValueAsString('date_format_sidebar_uploaded'); + $date_format_taken_at = Configs::getValueAsString('date_format_sidebar_taken_at'); + + return [ + 'created_at' => $this->resource->created_at->format($date_format_uploaded), + 'taken_at' => $this->resource->taken_at?->format($date_format_taken_at), + 'date_overlay' => ($this->resource->taken_at ?? $this->resource->created_at)->format($overlay_date_format) ?? '', + + 'shutter' => str_replace('s', 'sec', $this->resource->shutter ?? ''), + 'aperture' => str_replace('f/', '', $this->resource->aperture ?? ''), + 'iso' => sprintf(__('gallery.photo.details.iso'), $this->resource->iso), + 'lens' => ($this->resource->lens === '' || $this->resource->lens === null) ? '' : sprintf('(%s)', $this->resource->lens), + + 'duration' => Helpers::secondsToHMS(intval($this->resource->aperture)), + 'fps' => $this->resource->focal === null ? $this->resource->focal . ' fps' : '', + + 'filesize' => Helpers::getSymbolByQuantity($original?->filesize ?? 0), + 'resolution' => $original?->width . ' x ' . $original?->height, + 'latitude' => Helpers::decimalToDegreeMinutesSeconds($this->resource->latitude, true), + 'longitude' => Helpers::decimalToDegreeMinutesSeconds($this->resource->longitude, false), + 'altitude' => $this->resource->altitude !== null ? round($this->resource->altitude, 1) . 'm' : '', + 'license' => $this->resource->license !== LicenseType::NONE ? $this->resource->license->localization() : '', + 'description' => ($this->resource->description ?? '') === '' ? '' : Markdown::convert($this->resource->description)->getContent(), + ]; + } + + /** + * @return array + */ + private function precomputed(): array + { + return [ + 'is_video' => $this->resource->isVideo(), + 'is_raw' => $this->resource->isRaw(), + 'is_livephoto' => $this->resource->live_photo_url !== null, + 'is_camera_date' => $this->resource->taken_at !== null, + 'has_exif' => $this->genExifHash() !== '', + 'has_location' => $this->has_location(), + ]; + } + + private function has_location(): bool + { + return $this->resource->longitude !== null && + $this->resource->latitude !== null && + $this->resource->altitude !== null; + } + + private function genExifHash(): string + { + $exifHash = $this->resource->make; + $exifHash .= $this->resource->model; + $exifHash .= $this->resource->shutter; + if (!$this->resource->isVideo()) { + $exifHash .= $this->resource->aperture; + $exifHash .= $this->resource->focal; + } + $exifHash .= $this->resource->iso; + + return $exifHash; + } +} diff --git a/app/Legacy/V1/Resources/Models/SizeVariantResource.php b/app/Legacy/V1/Resources/Models/SizeVariantResource.php new file mode 100644 index 00000000000..bf3ab10d785 --- /dev/null +++ b/app/Legacy/V1/Resources/Models/SizeVariantResource.php @@ -0,0 +1,58 @@ +noUrl = $noUrl; + + return $this; + } + + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'type' => $this->resource->type, + 'filesize' => $this->resource->filesize, + 'height' => $this->resource->height, + 'width' => $this->resource->width, + 'url' => $this->when(!$this->noUrl, $this->resource->url), + ]; + } +} diff --git a/app/Legacy/V1/Resources/Models/SmartAlbumResource.php b/app/Legacy/V1/Resources/Models/SmartAlbumResource.php new file mode 100644 index 00000000000..85b9cc6656b --- /dev/null +++ b/app/Legacy/V1/Resources/Models/SmartAlbumResource.php @@ -0,0 +1,55 @@ +|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + // basic + 'id' => $this->resource->id, + 'title' => $this->resource->title, + + // We use getPhotos() to be sure to not execute and cache the photos. + // Some of the tests do check what is the value of the thumb id as a result, + // if the id is not in thumb (intended behaviour we want to check) + // but still in the photos (supposed to be null), this fail the test. + 'photos' => $this->whenLoaded('photos', PhotoCollectionResource::make($this->resource->getPhotos() ?? []), null), + + // thumb + 'thumb' => $this->resource->thumb, + + // security + 'policy' => AlbumProtectionPolicy::ofSmartAlbum($this->resource)->toArray(), + 'rights' => AlbumRightsResource::make($this->resource)->toArray($request), + ]; + } +} diff --git a/app/Legacy/V1/Resources/Models/TagAlbumResource.php b/app/Legacy/V1/Resources/Models/TagAlbumResource.php new file mode 100644 index 00000000000..defab864ef0 --- /dev/null +++ b/app/Legacy/V1/Resources/Models/TagAlbumResource.php @@ -0,0 +1,68 @@ +|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + // basic + 'id' => $this->resource->id, + 'title' => $this->resource->title, + 'owner_name' => $this->when(Auth::check(), $this->resource->owner->name), + 'is_tag_album' => true, + + // attributes + 'description' => $this->resource->description, + 'show_tags' => $this->resource->show_tags, + + // children + 'photos' => PhotoCollectionResource::make($this->whenLoaded('photos')), + + // thumb + 'thumb' => $this->resource->thumb, + + // timestamps + 'created_at' => $this->resource->created_at->toIso8601String(), + 'updated_at' => $this->resource->updated_at->toIso8601String(), + 'max_taken_at' => $this->resource->min_taken_at?->toIso8601String(), + 'min_taken_at' => $this->resource->max_taken_at?->toIso8601String(), + + // security + 'policy' => AlbumProtectionPolicy::ofBaseAlbum($this->resource), + 'rights' => AlbumRightsResource::make($this->resource)->toArray($request), + ]; + } +} diff --git a/app/Legacy/V1/Resources/Models/UserManagementResource.php b/app/Legacy/V1/Resources/Models/UserManagementResource.php new file mode 100644 index 00000000000..fc1cc702552 --- /dev/null +++ b/app/Legacy/V1/Resources/Models/UserManagementResource.php @@ -0,0 +1,49 @@ +|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + if ($this->resource === null) { + throw new LycheeLogicException('Trying to convert a null user into an array.'); + } + + return [ + 'id' => $this->resource->id, + 'username' => $this->resource->username, + 'may_administrate' => $this->resource->may_administrate, + 'may_upload' => $this->resource->may_upload, + 'may_edit_own_settings' => $this->resource->may_edit_own_settings, + ]; + } +} diff --git a/app/Legacy/V1/Resources/Models/UserResource.php b/app/Legacy/V1/Resources/Models/UserResource.php new file mode 100644 index 00000000000..5e51f60ea4d --- /dev/null +++ b/app/Legacy/V1/Resources/Models/UserResource.php @@ -0,0 +1,48 @@ +|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + if ($this->resource === null) { + throw new LycheeLogicException('Trying to convert a null user into an array.'); + } + + return [ + 'id' => $this->resource->id, + 'has_token' => $this->resource->token !== null, + 'username' => $this->resource->username, + 'email' => $this->resource->email, + ]; + } +} diff --git a/app/Legacy/V1/Resources/Rights/AlbumRightsResource.php b/app/Legacy/V1/Resources/Rights/AlbumRightsResource.php new file mode 100644 index 00000000000..f88deab0293 --- /dev/null +++ b/app/Legacy/V1/Resources/Rights/AlbumRightsResource.php @@ -0,0 +1,60 @@ +can_edit = Gate::check(AlbumPolicy::CAN_EDIT, [AbstractAlbum::class, $abstractAlbum]); + $this->can_share_with_users = Gate::check(AlbumPolicy::CAN_SHARE_WITH_USERS, [AbstractAlbum::class, $abstractAlbum]); + $this->can_download = Gate::check(AlbumPolicy::CAN_DOWNLOAD, [AbstractAlbum::class, $abstractAlbum]); + $this->can_upload = Gate::check(AlbumPolicy::CAN_UPLOAD, [AbstractAlbum::class, $abstractAlbum]); + } + + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'can_edit' => $this->can_edit, + 'can_share_with_users' => $this->can_share_with_users, + 'can_download' => $this->can_download, + 'can_upload' => $this->can_upload, + ]; + } +} diff --git a/app/Legacy/V1/Resources/Rights/GlobalRightsResource.php b/app/Legacy/V1/Resources/Rights/GlobalRightsResource.php new file mode 100644 index 00000000000..58be8cf6d13 --- /dev/null +++ b/app/Legacy/V1/Resources/Rights/GlobalRightsResource.php @@ -0,0 +1,40 @@ +|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'root_album' => RootAlbumRightsResource::make(), + 'settings' => SettingsRightsResource::make(), + 'user_management' => UserManagementRightsResource::make(), + 'user' => UserRightsResource::make(), + ]; + } +} diff --git a/app/Legacy/V1/Resources/Rights/PhotoRightsResource.php b/app/Legacy/V1/Resources/Rights/PhotoRightsResource.php new file mode 100644 index 00000000000..bf7db704e30 --- /dev/null +++ b/app/Legacy/V1/Resources/Rights/PhotoRightsResource.php @@ -0,0 +1,56 @@ +can_edit = Gate::check(PhotoPolicy::CAN_EDIT, [Photo::class, $photo]); + $this->can_download = Gate::check(PhotoPolicy::CAN_DOWNLOAD, [Photo::class, $photo]); + $this->can_access_full_photo = Gate::check(PhotoPolicy::CAN_ACCESS_FULL_PHOTO, [Photo::class, $photo]); + } + + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'can_edit' => $this->can_edit, + 'can_download' => $this->can_download, + 'can_access_full_photo' => $this->can_access_full_photo, + ]; + } +} \ No newline at end of file diff --git a/app/Legacy/V1/Resources/Rights/RootAlbumRightsResource.php b/app/Legacy/V1/Resources/Rights/RootAlbumRightsResource.php new file mode 100644 index 00000000000..ffbe1d91290 --- /dev/null +++ b/app/Legacy/V1/Resources/Rights/RootAlbumRightsResource.php @@ -0,0 +1,44 @@ +|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + // Needed to allow interaction such as moving albums + 'can_edit' => Gate::check(AlbumPolicy::CAN_UPLOAD, [AbstractAlbum::class, null]), + // Needed to allow upload at root level (into unsorted) + 'can_upload' => Gate::check(AlbumPolicy::CAN_UPLOAD, [AbstractAlbum::class, null]), + 'can_import_from_server' => Gate::check(AlbumPolicy::CAN_IMPORT_FROM_SERVER, [AbstractAlbum::class]), + ]; + } +} diff --git a/app/Legacy/V1/Resources/Rights/SettingsRightsResource.php b/app/Legacy/V1/Resources/Rights/SettingsRightsResource.php new file mode 100644 index 00000000000..36b695a2fa8 --- /dev/null +++ b/app/Legacy/V1/Resources/Rights/SettingsRightsResource.php @@ -0,0 +1,44 @@ +|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'can_edit' => Gate::check(SettingsPolicy::CAN_EDIT, [Configs::class]), + 'can_see_logs' => Gate::check(SettingsPolicy::CAN_SEE_LOGS, [Configs::class]), + 'can_clear_logs' => Gate::check(SettingsPolicy::CAN_CLEAR_LOGS, [Configs::class]), + 'can_see_diagnostics' => Gate::check(SettingsPolicy::CAN_SEE_DIAGNOSTICS, [Configs::class]), + 'can_update' => Gate::check(SettingsPolicy::CAN_UPDATE, [Configs::class]), + ]; + } +} diff --git a/app/Legacy/V1/Resources/Rights/UserManagementRightsResource.php b/app/Legacy/V1/Resources/Rights/UserManagementRightsResource.php new file mode 100644 index 00000000000..5bda30ae11d --- /dev/null +++ b/app/Legacy/V1/Resources/Rights/UserManagementRightsResource.php @@ -0,0 +1,43 @@ +|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'can_create' => Gate::check(UserPolicy::CAN_CREATE_OR_EDIT_OR_DELETE, [User::class]), + 'can_list' => Gate::check(UserPolicy::CAN_LIST, [User::class]), + 'can_edit' => Gate::check(UserPolicy::CAN_CREATE_OR_EDIT_OR_DELETE, [User::class]), + 'can_delete' => Gate::check(UserPolicy::CAN_CREATE_OR_EDIT_OR_DELETE, [User::class]), + ]; + } +} diff --git a/app/Legacy/V1/Resources/Rights/UserRightsResource.php b/app/Legacy/V1/Resources/Rights/UserRightsResource.php new file mode 100644 index 00000000000..faca579b64a --- /dev/null +++ b/app/Legacy/V1/Resources/Rights/UserRightsResource.php @@ -0,0 +1,40 @@ +|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'can_edit' => Gate::check(UserPolicy::CAN_EDIT, [User::class]), + ]; + } +} diff --git a/app/Legacy/V1/Resources/SearchResource.php b/app/Legacy/V1/Resources/SearchResource.php new file mode 100644 index 00000000000..b69ebebfa45 --- /dev/null +++ b/app/Legacy/V1/Resources/SearchResource.php @@ -0,0 +1,68 @@ + $albums + * @param Collection $tag_albums + * @param Collection $photos + * + * @return void + */ + public function __construct( + public Collection $albums, + public Collection $tag_albums, + public Collection $photos, + ) { + // Laravel applies a shortcut when this value === null but not when it is something else. + parent::__construct('must_not_be_null'); + } + + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + $albumIDs = $this->albums->reduce(fn (string $carry, Album $item) => $carry . $item->id, ''); + $tagAlbumsIds = $this->tag_albums->reduce(fn (string $carry, TagAlbum $item) => $carry . $item->id, ''); + $photosIds = $this->photos->reduce(fn (string $carry, Photo $item) => $carry . $item->id, ''); + // The checksum is used by the web front-end as an efficient way to + // avoid rebuilding the GUI, if two subsequent searches return the + // same result. + // The front-end performs a live search, while the user is typing + // a term. + // If the GUI was rebuilt every time after the user had typed the + // next character of a search term although the search result might + // stay the same, the GUI would flicker. + // The checksum is just over the id, we do not need a full conversion of the data. + $checksum = md5($albumIDs . $tagAlbumsIds . $photosIds); + + return [ + 'albums' => AlbumResource::collection($this->albums)->toArray($request), + 'tag_albums' => TagAlbumResource::collection($this->tag_albums)->toArray($request), + 'photos' => PhotoResource::collection($this->photos)->toArray($request), + 'checksum' => $checksum, + ]; + } +} diff --git a/app/Legacy/V1/Resources/Traits/WithStatus.php b/app/Legacy/V1/Resources/Traits/WithStatus.php new file mode 100644 index 00000000000..c96a8f3c21a --- /dev/null +++ b/app/Legacy/V1/Resources/Traits/WithStatus.php @@ -0,0 +1,35 @@ +status = $status; + + return $this; + } + + /** + * @param Request $request + * + * @return JsonResponse + */ + public function toResponse($request): JsonResponse + { + return (new ResourceResponse($this))->toResponse($request)->setStatusCode($this->status); + } +} \ No newline at end of file diff --git a/app/Legacy/V1/RuleSets/AddAlbumRuleSet.php b/app/Legacy/V1/RuleSets/AddAlbumRuleSet.php new file mode 100644 index 00000000000..bbfa04f49d0 --- /dev/null +++ b/app/Legacy/V1/RuleSets/AddAlbumRuleSet.php @@ -0,0 +1,25 @@ + ['present', new RandomIDRule(true)], + RequestAttribute::TITLE_ATTRIBUTE => ['required', new TitleRule()], + ]; + } +} diff --git a/app/Legacy/V1/RuleSets/Album/AddAlbumRuleSet.php b/app/Legacy/V1/RuleSets/Album/AddAlbumRuleSet.php new file mode 100644 index 00000000000..ed2081e393b --- /dev/null +++ b/app/Legacy/V1/RuleSets/Album/AddAlbumRuleSet.php @@ -0,0 +1,31 @@ + ['present', new RandomIDRule(true)], + RequestAttribute::TITLE_ATTRIBUTE => ['required', new TitleRule()], + ]; + } +} diff --git a/app/Legacy/V1/RuleSets/Album/AddTagAlbumRuleSet.php b/app/Legacy/V1/RuleSets/Album/AddTagAlbumRuleSet.php new file mode 100644 index 00000000000..9de7290f310 --- /dev/null +++ b/app/Legacy/V1/RuleSets/Album/AddTagAlbumRuleSet.php @@ -0,0 +1,31 @@ + ['required', new TitleRule()], + RequestAttribute::TAGS_ATTRIBUTE => 'required|array|min:1', + RequestAttribute::TAGS_ATTRIBUTE . '.*' => 'required|string|min:1', + ]; + } +} diff --git a/app/Legacy/V1/RuleSets/Album/BasicAlbumIdRuleSet.php b/app/Legacy/V1/RuleSets/Album/BasicAlbumIdRuleSet.php new file mode 100644 index 00000000000..6b4557c2037 --- /dev/null +++ b/app/Legacy/V1/RuleSets/Album/BasicAlbumIdRuleSet.php @@ -0,0 +1,29 @@ + ['required', new AlbumIDRule(false)], + ]; + } +} diff --git a/app/Legacy/V1/RuleSets/Album/DeleteAlbumsRuleSet.php b/app/Legacy/V1/RuleSets/Album/DeleteAlbumsRuleSet.php new file mode 100644 index 00000000000..63f98c94afd --- /dev/null +++ b/app/Legacy/V1/RuleSets/Album/DeleteAlbumsRuleSet.php @@ -0,0 +1,30 @@ + 'required|array|min:1', + RequestAttribute::ALBUM_IDS_ATTRIBUTE . '.*' => ['required', new AlbumIDRule(false)], + ]; + } +} diff --git a/app/Legacy/V1/RuleSets/Album/MergeAlbumsRuleSet.php b/app/Legacy/V1/RuleSets/Album/MergeAlbumsRuleSet.php new file mode 100644 index 00000000000..26afae25281 --- /dev/null +++ b/app/Legacy/V1/RuleSets/Album/MergeAlbumsRuleSet.php @@ -0,0 +1,32 @@ + ['required', new RandomIDRule(false)], + RequestAttribute::ALBUM_IDS_ATTRIBUTE => 'required|array|min:1', + RequestAttribute::ALBUM_IDS_ATTRIBUTE . '.*' => ['required', new AlbumIDRule(false)], + ]; + } +} diff --git a/app/Legacy/V1/RuleSets/Album/MoveAlbumsRuleSet.php b/app/Legacy/V1/RuleSets/Album/MoveAlbumsRuleSet.php new file mode 100644 index 00000000000..1d8544c894d --- /dev/null +++ b/app/Legacy/V1/RuleSets/Album/MoveAlbumsRuleSet.php @@ -0,0 +1,32 @@ + ['present', new RandomIDRule(true)], + RequestAttribute::ALBUM_IDS_ATTRIBUTE => 'required|array|min:1', + RequestAttribute::ALBUM_IDS_ATTRIBUTE . '.*' => ['required', new AlbumIDRule(false)], + ]; + } +} diff --git a/app/Legacy/V1/RuleSets/Album/SetAlbumCoverRuleSet.php b/app/Legacy/V1/RuleSets/Album/SetAlbumCoverRuleSet.php new file mode 100644 index 00000000000..7c5d37a6f8d --- /dev/null +++ b/app/Legacy/V1/RuleSets/Album/SetAlbumCoverRuleSet.php @@ -0,0 +1,30 @@ + ['required', new RandomIDRule(false)], + RequestAttribute::PHOTO_ID_ATTRIBUTE => ['present', new RandomIDRule(true)], + ]; + } +} diff --git a/app/Legacy/V1/RuleSets/Album/SetAlbumHeaderRuleSet.php b/app/Legacy/V1/RuleSets/Album/SetAlbumHeaderRuleSet.php new file mode 100644 index 00000000000..c56b1c149b8 --- /dev/null +++ b/app/Legacy/V1/RuleSets/Album/SetAlbumHeaderRuleSet.php @@ -0,0 +1,30 @@ + ['required', new RandomIDRule(true)], + RequestAttribute::PHOTO_ID_ATTRIBUTE => ['present', new RandomIDRule(true)], + ]; + } +} \ No newline at end of file diff --git a/app/Legacy/V1/RuleSets/Album/SetAlbumProtectionPolicyRuleSet.php b/app/Legacy/V1/RuleSets/Album/SetAlbumProtectionPolicyRuleSet.php new file mode 100644 index 00000000000..94cf0a30f07 --- /dev/null +++ b/app/Legacy/V1/RuleSets/Album/SetAlbumProtectionPolicyRuleSet.php @@ -0,0 +1,36 @@ + ['required', new RandomIDRule(false)], + RequestAttribute::PASSWORD_ATTRIBUTE => ['sometimes', new PasswordRule(true)], + RequestAttribute::IS_PUBLIC_ATTRIBUTE => 'required|boolean', + RequestAttribute::IS_LINK_REQUIRED_ATTRIBUTE => 'required|boolean', + RequestAttribute::IS_NSFW_ATTRIBUTE => 'required|boolean', + RequestAttribute::GRANTS_DOWNLOAD_ATTRIBUTE => 'required|boolean', + RequestAttribute::GRANTS_FULL_PHOTO_ACCESS_ATTRIBUTE => 'required|boolean', + ]; + } +} diff --git a/app/Legacy/V1/RuleSets/Album/SetAlbumSortingRuleSet.php b/app/Legacy/V1/RuleSets/Album/SetAlbumSortingRuleSet.php new file mode 100644 index 00000000000..37043d1f8cf --- /dev/null +++ b/app/Legacy/V1/RuleSets/Album/SetAlbumSortingRuleSet.php @@ -0,0 +1,34 @@ + ['required', new RandomIDRule(false)], + RequestAttribute::ALBUM_SORTING_COLUMN_ATTRIBUTE => ['present', 'nullable', new Enum(ColumnSortingAlbumType::class)], + RequestAttribute::ALBUM_SORTING_ORDER_ATTRIBUTE => [ + 'required_with:' . RequestAttribute::ALBUM_SORTING_COLUMN_ATTRIBUTE, + 'nullable', new Enum(OrderSortingType::class), + ], + ]; + } +} diff --git a/app/Legacy/V1/RuleSets/Album/SetAlbumTagRuleSet.php b/app/Legacy/V1/RuleSets/Album/SetAlbumTagRuleSet.php new file mode 100644 index 00000000000..ed5b8f91b2f --- /dev/null +++ b/app/Legacy/V1/RuleSets/Album/SetAlbumTagRuleSet.php @@ -0,0 +1,31 @@ + ['required', new RandomIDRule(false)], + RequestAttribute::SHOW_TAGS_ATTRIBUTE => 'required|array|min:1', + RequestAttribute::SHOW_TAGS_ATTRIBUTE . '.*' => 'required|string|min:1', + ]; + } +} diff --git a/app/Legacy/V1/RuleSets/Album/SetAlbumsTitleRuleSet.php b/app/Legacy/V1/RuleSets/Album/SetAlbumsTitleRuleSet.php new file mode 100644 index 00000000000..197cb5e7edb --- /dev/null +++ b/app/Legacy/V1/RuleSets/Album/SetAlbumsTitleRuleSet.php @@ -0,0 +1,32 @@ + 'required|array|min:1', + RequestAttribute::ALBUM_IDS_ATTRIBUTE . '.*' => ['required', new RandomIDRule(false)], + RequestAttribute::TITLE_ATTRIBUTE => ['required', new TitleRule()], + ]; + } +} diff --git a/app/Legacy/V1/RuleSets/Album/UnlockAlbumRuleSet.php b/app/Legacy/V1/RuleSets/Album/UnlockAlbumRuleSet.php new file mode 100644 index 00000000000..a4783011a63 --- /dev/null +++ b/app/Legacy/V1/RuleSets/Album/UnlockAlbumRuleSet.php @@ -0,0 +1,31 @@ + ['required', new RandomIDRule(false)], + RequestAttribute::PASSWORD_ATTRIBUTE => ['required', new PasswordRule(false)], + ]; + } +} diff --git a/app/Legacy/V1/RuleSets/ChangeLoginRuleSet.php b/app/Legacy/V1/RuleSets/ChangeLoginRuleSet.php new file mode 100644 index 00000000000..5a3cf23d1c3 --- /dev/null +++ b/app/Legacy/V1/RuleSets/ChangeLoginRuleSet.php @@ -0,0 +1,26 @@ + ['sometimes', new UsernameRule(true)], + RequestAttribute::PASSWORD_ATTRIBUTE => ['required', 'confirmed', new PasswordRule(false)], + RequestAttribute::OLD_PASSWORD_ATTRIBUTE => ['required', new PasswordRule(false)], + ]; + } +} diff --git a/app/Legacy/V1/RuleSets/Import/ImportFromUrlRuleSet.php b/app/Legacy/V1/RuleSets/Import/ImportFromUrlRuleSet.php new file mode 100644 index 00000000000..81ef5e055bb --- /dev/null +++ b/app/Legacy/V1/RuleSets/Import/ImportFromUrlRuleSet.php @@ -0,0 +1,31 @@ + ['present', new RandomIDRule(true)], + RequestAttribute::URLS_ATTRIBUTE => 'required|array|min:1', + RequestAttribute::URLS_ATTRIBUTE . '.*' => 'required|string', + ]; + } +} diff --git a/app/Legacy/V1/RuleSets/Import/ImportServerRuleSet.php b/app/Legacy/V1/RuleSets/Import/ImportServerRuleSet.php new file mode 100644 index 00000000000..b200d136751 --- /dev/null +++ b/app/Legacy/V1/RuleSets/Import/ImportServerRuleSet.php @@ -0,0 +1,35 @@ + ['present', new RandomIDRule(true)], + RequestAttribute::PATH_ATTRIBUTE => 'required|array|min:1', + RequestAttribute::PATH_ATTRIBUTE . '.*' => 'required|string|distinct', + RequestAttribute::DELETE_IMPORTED_ATTRIBUTE => 'sometimes|boolean', + RequestAttribute::SKIP_DUPLICATES_ATTRIBUTE => 'sometimes|boolean', + RequestAttribute::IMPORT_VIA_SYMLINK_ATTRIBUTE => 'sometimes|boolean', + RequestAttribute::RESYNC_METADATA_ATTRIBUTE => 'sometimes|boolean', + ]; + } +} diff --git a/app/Legacy/V1/RuleSets/LoginRuleSet.php b/app/Legacy/V1/RuleSets/LoginRuleSet.php new file mode 100644 index 00000000000..f846fc403a8 --- /dev/null +++ b/app/Legacy/V1/RuleSets/LoginRuleSet.php @@ -0,0 +1,25 @@ + ['required', new UsernameRule()], + RequestAttribute::PASSWORD_ATTRIBUTE => ['required', new PasswordRule(false)], + ]; + } +} diff --git a/app/Legacy/V1/RuleSets/Photo/AddPhotoRuleSet.php b/app/Legacy/V1/RuleSets/Photo/AddPhotoRuleSet.php new file mode 100644 index 00000000000..875bc41c99c --- /dev/null +++ b/app/Legacy/V1/RuleSets/Photo/AddPhotoRuleSet.php @@ -0,0 +1,31 @@ + ['present', new AlbumIDRule(true)], + RequestAttribute::FILE_LAST_MODIFIED_TIME => 'sometimes|nullable|numeric', + RequestAttribute::FILE_ATTRIBUTE => 'required|file', + ]; + } +} diff --git a/app/Legacy/V1/RuleSets/Photo/ArchivePhotosRuleSet.php b/app/Legacy/V1/RuleSets/Photo/ArchivePhotosRuleSet.php new file mode 100644 index 00000000000..f1ebcf6f9f4 --- /dev/null +++ b/app/Legacy/V1/RuleSets/Photo/ArchivePhotosRuleSet.php @@ -0,0 +1,32 @@ + ['required', new RandomIDListRule()], + RequestAttribute::SIZE_VARIANT_ATTRIBUTE => ['required', new Enum(DownloadVariantType::class)], + ]; + } +} diff --git a/app/Legacy/V1/RuleSets/Photo/DeletePhotosRuleSet.php b/app/Legacy/V1/RuleSets/Photo/DeletePhotosRuleSet.php new file mode 100644 index 00000000000..dd93f5e6e9e --- /dev/null +++ b/app/Legacy/V1/RuleSets/Photo/DeletePhotosRuleSet.php @@ -0,0 +1,30 @@ + 'required|array|min:1', + RequestAttribute::PHOTO_IDS_ATTRIBUTE . '.*' => ['required', new RandomIDRule(false)], + ]; + } +} diff --git a/app/Legacy/V1/RuleSets/Photo/DuplicatePhotosRuleSet.php b/app/Legacy/V1/RuleSets/Photo/DuplicatePhotosRuleSet.php new file mode 100644 index 00000000000..b1697da2afa --- /dev/null +++ b/app/Legacy/V1/RuleSets/Photo/DuplicatePhotosRuleSet.php @@ -0,0 +1,31 @@ + 'required|array|min:1', + RequestAttribute::PHOTO_IDS_ATTRIBUTE . '.*' => ['required', new RandomIDRule(false)], + RequestAttribute::ALBUM_ID_ATTRIBUTE => ['present', new RandomIDRule(true)], + ]; + } +} diff --git a/app/Legacy/V1/RuleSets/Photo/GetPhotoRuleSet.php b/app/Legacy/V1/RuleSets/Photo/GetPhotoRuleSet.php new file mode 100644 index 00000000000..ed6ef738098 --- /dev/null +++ b/app/Legacy/V1/RuleSets/Photo/GetPhotoRuleSet.php @@ -0,0 +1,29 @@ + ['required', new RandomIDRule(false)], + ]; + } +} diff --git a/app/Legacy/V1/RuleSets/Photo/MovePhotosRuleSet.php b/app/Legacy/V1/RuleSets/Photo/MovePhotosRuleSet.php new file mode 100644 index 00000000000..b847630b9a2 --- /dev/null +++ b/app/Legacy/V1/RuleSets/Photo/MovePhotosRuleSet.php @@ -0,0 +1,31 @@ + 'required|array|min:1', + RequestAttribute::PHOTO_IDS_ATTRIBUTE . '.*' => ['required', new RandomIDRule(false)], + RequestAttribute::ALBUM_ID_ATTRIBUTE => ['present', new RandomIDRule(true)], + ]; + } +} diff --git a/app/Legacy/V1/RuleSets/Photo/RotatePhotoRuleSet.php b/app/Legacy/V1/RuleSets/Photo/RotatePhotoRuleSet.php new file mode 100644 index 00000000000..ab8fad1c0f4 --- /dev/null +++ b/app/Legacy/V1/RuleSets/Photo/RotatePhotoRuleSet.php @@ -0,0 +1,33 @@ + ['required', new RandomIDRule(false)], + RequestAttribute::DIRECTION_ATTRIBUTE => ['required', Rule::in([-1, 1])], + ]; + } +} diff --git a/app/Legacy/V1/RuleSets/Photo/SetPhotoDescriptionRuleSet.php b/app/Legacy/V1/RuleSets/Photo/SetPhotoDescriptionRuleSet.php new file mode 100644 index 00000000000..7accc978ea9 --- /dev/null +++ b/app/Legacy/V1/RuleSets/Photo/SetPhotoDescriptionRuleSet.php @@ -0,0 +1,31 @@ + ['required', new RandomIDRule(false)], + RequestAttribute::DESCRIPTION_ATTRIBUTE => ['present', new DescriptionRule()], + ]; + } +} diff --git a/app/Legacy/V1/RuleSets/Photo/SetPhotoLicenseRuleSet.php b/app/Legacy/V1/RuleSets/Photo/SetPhotoLicenseRuleSet.php new file mode 100644 index 00000000000..9afa3e47675 --- /dev/null +++ b/app/Legacy/V1/RuleSets/Photo/SetPhotoLicenseRuleSet.php @@ -0,0 +1,32 @@ + ['required', new RandomIDRule(false)], + RequestAttribute::LICENSE_ATTRIBUTE => ['required', new Enum(LicenseType::class)], + ]; + } +} diff --git a/app/Legacy/V1/RuleSets/Photo/SetPhotoUploadDateRuleSet.php b/app/Legacy/V1/RuleSets/Photo/SetPhotoUploadDateRuleSet.php new file mode 100644 index 00000000000..84bc3c52713 --- /dev/null +++ b/app/Legacy/V1/RuleSets/Photo/SetPhotoUploadDateRuleSet.php @@ -0,0 +1,30 @@ + ['required', new RandomIDRule(false)], + RequestAttribute::DATE_ATTRIBUTE => 'required|date', + ]; + } +} diff --git a/app/Legacy/V1/RuleSets/Photo/SetPhotosStarredRuleSet.php b/app/Legacy/V1/RuleSets/Photo/SetPhotosStarredRuleSet.php new file mode 100644 index 00000000000..81bea3e19ee --- /dev/null +++ b/app/Legacy/V1/RuleSets/Photo/SetPhotosStarredRuleSet.php @@ -0,0 +1,31 @@ + 'required|array|min:1', + RequestAttribute::PHOTO_IDS_ATTRIBUTE . '.*' => ['required', new RandomIDRule(false)], + RequestAttribute::IS_STARRED_ATTRIBUTE => 'required|boolean', + ]; + } +} diff --git a/app/Legacy/V1/RuleSets/Photo/SetPhotosTagsRuleSet.php b/app/Legacy/V1/RuleSets/Photo/SetPhotosTagsRuleSet.php new file mode 100644 index 00000000000..6234cd1f614 --- /dev/null +++ b/app/Legacy/V1/RuleSets/Photo/SetPhotosTagsRuleSet.php @@ -0,0 +1,33 @@ + 'required|boolean', + RequestAttribute::PHOTO_IDS_ATTRIBUTE => 'required|array|min:1', + RequestAttribute::PHOTO_IDS_ATTRIBUTE . '.*' => ['required', new RandomIDRule(false)], + RequestAttribute::TAGS_ATTRIBUTE => 'present|array', + RequestAttribute::TAGS_ATTRIBUTE . '.*' => 'required|string|min:1', + ]; + } +} \ No newline at end of file diff --git a/app/Legacy/V1/RuleSets/Photo/SetPhotosTitleRuleSet.php b/app/Legacy/V1/RuleSets/Photo/SetPhotosTitleRuleSet.php new file mode 100644 index 00000000000..1919ae912ef --- /dev/null +++ b/app/Legacy/V1/RuleSets/Photo/SetPhotosTitleRuleSet.php @@ -0,0 +1,32 @@ + 'required|array|min:1', + RequestAttribute::PHOTO_IDS_ATTRIBUTE . '.*' => ['required', new RandomIDRule(false)], + RequestAttribute::TITLE_ATTRIBUTE => ['required', new TitleRule()], + ]; + } +} diff --git a/app/Legacy/V1/RuleSets/Session/LoginRuleSet.php b/app/Legacy/V1/RuleSets/Session/LoginRuleSet.php new file mode 100644 index 00000000000..8ac12ba2af7 --- /dev/null +++ b/app/Legacy/V1/RuleSets/Session/LoginRuleSet.php @@ -0,0 +1,28 @@ + ['required', new UsernameRule()], + RequestAttribute::PASSWORD_ATTRIBUTE => ['required', new PasswordRule(false)], + ]; + } +} diff --git a/app/Legacy/V1/RuleSets/User/ChangeLoginRuleSet.php b/app/Legacy/V1/RuleSets/User/ChangeLoginRuleSet.php new file mode 100644 index 00000000000..e69cea97eb4 --- /dev/null +++ b/app/Legacy/V1/RuleSets/User/ChangeLoginRuleSet.php @@ -0,0 +1,29 @@ + ['sometimes', new UsernameRule(true)], + RequestAttribute::PASSWORD_ATTRIBUTE => ['required', new PasswordRule(false)], + RequestAttribute::OLD_PASSWORD_ATTRIBUTE => ['required', new PasswordRule(false)], + ]; + } +} diff --git a/app/Legacy/V1/RuleSets/User/SetEmailRuleSet.php b/app/Legacy/V1/RuleSets/User/SetEmailRuleSet.php new file mode 100644 index 00000000000..2eff66a344e --- /dev/null +++ b/app/Legacy/V1/RuleSets/User/SetEmailRuleSet.php @@ -0,0 +1,25 @@ + 'present|nullable|email:rfc,dns|max:100', + ]; + } +} diff --git a/app/Legacy/V1/RuleSets/Users/AddUserRuleSet.php b/app/Legacy/V1/RuleSets/Users/AddUserRuleSet.php new file mode 100644 index 00000000000..f11592cfcc7 --- /dev/null +++ b/app/Legacy/V1/RuleSets/Users/AddUserRuleSet.php @@ -0,0 +1,30 @@ + ['required', new UsernameRule()], + RequestAttribute::PASSWORD_ATTRIBUTE => ['required', new PasswordRule(false)], + RequestAttribute::MAY_UPLOAD_ATTRIBUTE => 'present|boolean', + RequestAttribute::MAY_EDIT_OWN_SETTINGS_ATTRIBUTE => 'present|boolean', + ]; + } +} diff --git a/app/Legacy/V1/RuleSets/Users/SetUserSettingsRuleSet.php b/app/Legacy/V1/RuleSets/Users/SetUserSettingsRuleSet.php new file mode 100644 index 00000000000..7fd0e89aad9 --- /dev/null +++ b/app/Legacy/V1/RuleSets/Users/SetUserSettingsRuleSet.php @@ -0,0 +1,32 @@ + ['required', new IntegerIDRule(false)], + RequestAttribute::USERNAME_ATTRIBUTE => ['required', new UsernameRule(), 'min:1'], + RequestAttribute::PASSWORD_ATTRIBUTE => ['sometimes', new PasswordRule(false)], + RequestAttribute::MAY_UPLOAD_ATTRIBUTE => 'present|boolean', + RequestAttribute::MAY_EDIT_OWN_SETTINGS_ATTRIBUTE => 'present|boolean', + ]; + } +} diff --git a/app/Legacy/V1/RuleSets/WebAuthn/DeleteCredentialRuleSet.php b/app/Legacy/V1/RuleSets/WebAuthn/DeleteCredentialRuleSet.php new file mode 100644 index 00000000000..2bc24d1844f --- /dev/null +++ b/app/Legacy/V1/RuleSets/WebAuthn/DeleteCredentialRuleSet.php @@ -0,0 +1,23 @@ + 'required|string']; + } +} diff --git a/app/Locale/ChineseSimplified.php b/app/Locale/ChineseSimplified.php deleted file mode 100644 index 5bc17cb9f17..00000000000 --- a/app/Locale/ChineseSimplified.php +++ /dev/null @@ -1,476 +0,0 @@ - '用户名', - 'PASSWORD' => '密码', - 'ENTER' => '确定', - 'CANCEL' => '取消', - 'SIGN_IN' => '登录', - 'CLOSE' => '关闭', - 'SETTINGS' => '设置', - 'SEARCH' => '搜索 ...', - 'MORE' => '更多', - 'DEFAULT' => '默认', - - 'USERS' => '用户', - 'U2F' => '通用两步验证(U2F)', - 'NOTIFICATIONS' => 'Notifications', - 'SHARING' => '共享', - 'CHANGE_LOGIN' => '修改登录信息', - 'CHANGE_SORTING' => '修改排序', - 'SET_DROPBOX' => '设置 Dropbox', - 'ABOUT_LYCHEE' => '关于 Lychee', - 'DIAGNOSTICS' => '诊断', - 'DIAGNOSTICS_GET_SIZE' => '请求空间占用信息', - 'LOGS' => '查看日志', - 'SIGN_OUT' => '注销登录', - 'UPDATE_AVAILABLE' => '可用更新!', - 'MIGRATION_AVAILABLE' => '可用迁移!', - 'DEFAULT_LICENSE' => '为新上传设置默认许可证:', - 'SET_LICENSE' => '设置许可证', - 'SET_OVERLAY_TYPE' => '设置叠层', - 'SET_MAP_PROVIDER' => '设置 OpenStreetMap 图层提供者', - - 'SMART_ALBUMS' => '智能相册', - 'SHARED_ALBUMS' => '已共享的相册', - 'ALBUMS' => '相册', - 'PHOTOS' => '照片', - 'SEARCH_RESULTS' => '搜索结果', - - 'RENAME' => '重命名', - 'RENAME_ALL' => '重命名已选中', - 'MERGE' => '合并', - 'MERGE_ALL' => '合并选中', - 'MAKE_PUBLIC' => '设为公开', - 'SHARE_ALBUM' => '分享相册', - 'SHARE_PHOTO' => '分享照片', - 'VISIBILITY_ALBUM' => '相册可见性', - 'VISIBILITY_PHOTO' => '照片可见性', - 'DOWNLOAD_ALBUM' => '下载相册', - 'ABOUT_ALBUM' => '关于相册', - 'DELETE_ALBUM' => '删除相册', - 'MOVE_ALBUM' => '移动相册', - 'FULLSCREEN_ENTER' => '进入全屏幕', - 'FULLSCREEN_EXIT' => '退出全屏幕', - - 'SHARING_ALBUM_USERS' => 'Share this album with users', - 'WAIT_FETCH_DATA' => 'Please wait while we get the data...', - 'SHARING_ALBUM_USERS_NO_USERS' => 'There are no users to share the album with', - 'SHARING_ALBUM_USERS_LONG_MESSAGE' => 'Select the users to share this album with', - - 'DELETE_ALBUM_QUESTION' => '删除相册和照片', - 'KEEP_ALBUM' => '保留相册', - 'DELETE_ALBUM_CONFIRMATION_1' => '是否确认删除相册', - 'DELETE_ALBUM_CONFIRMATION_2' => '以及相册中包含的所有照片?操作后不可恢复!', - - 'DELETE_ALBUMS_QUESTION' => '删除相册和照片', - 'KEEP_ALBUMS' => '保留相册', - 'DELETE_ALBUMS_CONFIRMATION_1' => '是否确认删除全部', - 'DELETE_ALBUMS_CONFIRMATION_2' => '选中的相册和其中的所有照片?操作后不可恢复!', - - 'DELETE_UNSORTED_CONFIRM' => '是否确认删除\'未分类\'的所有照片?
操作后不可恢复!', - 'CLEAR_UNSORTED' => '清除未分类', - 'KEEP_UNSORTED' => '保留未分类', - - 'EDIT_SHARING' => '编辑共享', - 'MAKE_PRIVATE' => '设为私有', - - 'CLOSE_ALBUM' => '关闭相册', - 'CLOSE_PHOTO' => '关闭照片', - 'CLOSE_MAP' => '关闭地图', - - 'ADD' => '添加', - 'MOVE' => '移动', - 'MOVE_ALL' => '移动选中', - 'DUPLICATE' => '创建副本', - 'DUPLICATE_ALL' => '复制选定的', - 'COPY_TO' => '复制到...', - 'COPY_ALL_TO' => '选定副本到...', - 'DELETE' => '删除', - 'DELETE_ALL' => '删除已选中', - 'DOWNLOAD' => '下载', - 'DOWNLOAD_ALL' => '下载已选中', - 'UPLOAD_PHOTO' => '上传相片', - 'IMPORT_LINK' => '从链接导入', - 'IMPORT_DROPBOX' => '从 Dropbox 导入', - 'IMPORT_SERVER' => '从服务器导入', - 'NEW_ALBUM' => '新建相册', - 'NEW_TAG_ALBUM' => '新建标签相册', - - 'TITLE_NEW_ALBUM' => '输入新相册的标题:', - 'UNTITLED' => '未命名', - 'UNSORTED' => '未分类', - 'STARRED' => '星标', - 'RECENT' => '最新', - 'PUBLIC' => '公开', - 'NUM_PHOTOS' => '照片', - - 'CREATE_ALBUM' => '创建相册', - 'CREATE_TAG_ALBUM' => '创建标签相册', - - 'STAR_PHOTO' => '星标此照片', - 'STAR' => '星标', - 'STAR_ALL' => '为所选照片加星标', - 'TAGS' => '标签', - 'TAGS_ALL' => '为所选照片打标签', - 'UNSTAR_PHOTO' => '取消星标', - 'SET_COVER' => '设置为相册封面', - 'REMOVE_COVER' => '取消设置为相册封面', - - 'FULL_PHOTO' => '打开原图', - 'ABOUT_PHOTO' => '关于照片', - 'DISPLAY_FULL_MAP' => '地图', - 'DIRECT_LINK' => '直链', - 'DIRECT_LINKS' => '直链', - - 'ALBUM_ABOUT' => '关于', - 'ALBUM_BASICS' => '基本信息', - 'ALBUM_TITLE' => '标题', - 'ALBUM_NEW_TITLE' => '输入新的相册标题:', - 'ALBUMS_NEW_TITLE_1' => '设置标题为', - 'ALBUMS_NEW_TITLE_2' => '已选择的相册:', - 'ALBUM_SET_TITLE' => '设置标题', - 'ALBUM_DESCRIPTION' => '描述', - 'ALBUM_SHOW_TAGS' => '要显示的标签', - 'ALBUM_NEW_DESCRIPTION' => '输入新的相册描述:', - 'ALBUM_SET_DESCRIPTION' => '设置描述', - 'ALBUM_NEW_SHOWTAGS' => '输入将在此相册中可见的照片的标签:', - 'ALBUM_SET_SHOWTAGS' => '设置要显示的标签', - 'ALBUM_ALBUM' => '相册', - 'ALBUM_CREATED' => '创建时间', - 'ALBUM_IMAGES' => '图片数', - 'ALBUM_VIDEOS' => '视频数', - 'ALBUM_SUBALBUMS' => '子相册数', - 'ALBUM_SHARING' => '共享', - 'ALBUM_SHR_YES' => '是', - 'ALBUM_SHR_NO' => '否', - 'ALBUM_PUBLIC' => '公开', - 'ALBUM_PUBLIC_EXPL' => '相册可被他人查看,但需遵守以下限制。', - 'ALBUM_FULL' => '原始图像', - 'ALBUM_FULL_EXPL' => '完整分辨率图像可用。', - 'ALBUM_HIDDEN' => '隐藏', - 'ALBUM_HIDDEN_EXPL' => '只有拥有直链的人才能查看此相册。', - 'ALBUM_MARK_NSFW' => '将相册标记为敏感内容', - 'ALBUM_UNMARK_NSFW' => '取消相册的敏感内容标记', - 'ALBUM_NSFW' => '敏感内容', - 'ALBUM_NSFW_EXPL' => '相册被标记为包含敏感内容。', - 'ALBUM_DOWNLOADABLE' => '可下载', - 'ALBUM_DOWNLOADABLE_EXPL' => '您画廊的访客可以下载此相册。', - 'ALBUM_SHARE_BUTTON_VISIBLE' => '分享按钮可见', - 'ALBUM_SHARE_BUTTON_VISIBLE_EXPL' => '显示社交媒体分享链接。', - 'ALBUM_PASSWORD' => '密码', - 'ALBUM_PASSWORD_PROT' => '受到密码保护', - 'ALBUM_PASSWORD_PROT_EXPL' => '只有使用正确的密码才可以访问相册。', - 'ALBUM_PASSWORD_REQUIRED' => '此相册受到密码保护。请在下方输入密码以查看相册内的照片:', - 'ALBUM_MERGE_1' => '你确定要合并相册', - 'ALBUM_MERGE_2' => '到相册', - 'ALBUMS_MERGE' => '你确定要合并所有已选择的相册到相册', - 'MERGE_ALBUM' => '合并相册', - 'DONT_MERGE' => '不要合并', - 'ALBUM_MOVE_1' => '你确定要移动相册', - 'ALBUM_MOVE_2' => '到相册', - 'ALBUMS_MOVE' => '你确定要移动所有已选择的相册到相册', - 'MOVE_ALBUMS' => '移动相册', - 'NOT_MOVE_ALBUMS' => '不要移动', - 'ROOT' => '相册', - 'ALBUM_REUSE' => '重用', - 'ALBUM_LICENSE' => '许可证', - 'ALBUM_SET_LICENSE' => '设置许可证', - 'ALBUM_LICENSE_HELP' => '需要有关选择的帮助吗?', - 'ALBUM_LICENSE_NONE' => '无', - 'ALBUM_RESERVED' => '所有权利保留', - 'ALBUM_SET_ORDER' => '设置排序', - 'ALBUM_ORDERING' => '排序依据', - - 'PHOTO_ABOUT' => '关于', - 'PHOTO_BASICS' => '基本信息', - 'PHOTO_TITLE' => '标题', - 'PHOTO_NEW_TITLE' => '输入新的照片标题:', - 'PHOTO_SET_TITLE' => '设置标题', - 'PHOTO_UPLOADED' => '已上传', - 'PHOTO_DESCRIPTION' => '描述', - 'PHOTO_NEW_DESCRIPTION' => '输入新的照片描述', - 'PHOTO_SET_DESCRIPTION' => '设置描述', - 'PHOTO_NEW_LICENSE' => '添加许可证', - 'PHOTO_SET_LICENSE' => '设置许可证', - 'PHOTO_LICENSE' => '许可证', - 'PHOTO_REUSE' => '重用', - 'PHOTO_LICENSE_NONE' => '无', - 'PHOTO_RESERVED' => '所有权利保留', - 'PHOTO_LATITUDE' => '纬度', - 'PHOTO_LONGITUDE' => '经度', - 'PHOTO_ALTITUDE' => '海拔', - 'PHOTO_IMGDIRECTION' => '方向', - 'PHOTO_LOCATION' => '地点', - 'PHOTO_IMAGE' => '图片信息', - 'PHOTO_VIDEO' => '视频', - 'PHOTO_SIZE' => '大小', - 'PHOTO_FORMAT' => '格式', - 'PHOTO_RESOLUTION' => '分辨率', - 'PHOTO_DURATION' => '时长', - 'PHOTO_FPS' => '帧率', - 'PHOTO_TAGS' => '标签', - 'PHOTO_NOTAGS' => '无标签', - 'PHOTO_NEW_TAGS' => '为该照片添加标签。你可以用逗号分隔多个标签:', - 'PHOTO_NEW_TAGS_1' => '设置你的标签于', - 'PHOTO_NEW_TAGS_2' => '已选择的照片。已存在的标签会被覆盖。你可以用逗号分隔多个标签:', - 'PHOTO_SET_TAGS' => '设置标签', - 'PHOTO_CAMERA' => '相机信息', - 'PHOTO_CAPTURED' => '拍摄时间', - 'PHOTO_MAKE' => '设备', - 'PHOTO_TYPE' => '类型/型号', - 'PHOTO_LENS' => '镜头', - 'PHOTO_SHUTTER' => '快门速度', - 'PHOTO_APERTURE' => '光圈', - 'PHOTO_FOCAL' => '焦距', - 'PHOTO_ISO' => '感光度', - 'PHOTO_SHARING' => '共享', - 'PHOTO_SHR_PLUBLIC' => '公开', - 'PHOTO_SHR_ALB' => '是 (相册)', - 'PHOTO_SHR_PHT' => '是 (照片)', - 'PHOTO_SHR_NO' => '否', - 'PHOTO_DELETE' => '删除照片', - 'PHOTO_KEEP' => '保留照片', - 'PHOTO_DELETE_1' => '是否要删除照片', - 'PHOTO_DELETE_2' => '?此操作不可恢复!', - 'PHOTO_DELETE_ALL_1' => '是否要删除全部', - 'PHOTO_DELETE_ALL_2' => '已选择的照片?此操作不可恢复!', - 'PHOTOS_NEW_TITLE_1' => '设置照片标题于', - 'PHOTOS_NEW_TITLE_2' => '已选择的照片:', - 'PHOTO_MAKE_PRIVATE_ALBUM' => '此照片位于公开相册中。要使其私有或公开,请编辑所在相册的可见性。', - 'PHOTO_SHOW_ALBUM' => '显示相册', - 'PHOTO_PUBLIC' => '公开', - 'PHOTO_PUBLIC_EXPL' => '照片可被他人查看,但需遵守以下限制。', - 'PHOTO_FULL' => '原始图像', - 'PHOTO_FULL_EXPL' => '完整分辨率图像可用。', - 'PHOTO_HIDDEN' => '隐藏', - 'PHOTO_HIDDEN_EXPL' => '只有拥有直链的人才能查看此照片。', - 'PHOTO_DOWNLOADABLE' => '可下载', - 'PHOTO_DOWNLOADABLE_EXPL' => '您画廊的访客可以下载此照片。', - 'PHOTO_SHARE_BUTTON_VISIBLE' => '分享按钮可见', - 'PHOTO_SHARE_BUTTON_VISIBLE_EXPL' => '显示社交媒体分享链接。', - 'PHOTO_PASSWORD_PROT' => '受到密码保护', - 'PHOTO_PASSWORD_PROT_EXPL' => '只有使用正确的密码才可以访问照片。', - 'PHOTO_EDIT_SHARING_TEXT' => '此照片的共享属性将被修改为:', - 'PHOTO_NO_EDIT_SHARING_TEXT' => '因为此照片位于一个公开相册中,其继承了相册的可见性设置。其当前的可见性仅在下方作为提示的作用而显示。', - 'PHOTO_EDIT_GLOBAL_SHARING_TEXT' => ' - 此照片的可见性可以使用全局的 Lychee 设置进行更细致的调整。其当前的可见性仅在下方作为提示的作用而显示。', - 'PHOTO_SHARING_CONFIRM' => '保存', - - 'LOADING' => '载入中', - 'ERROR' => '错误', - 'ERROR_TEXT' => '噢,似乎出了一些问题。请刷新页面后再试!', - 'ERROR_DB_1' => '无法连接主机数据库,访问被拒绝。请仔细检查主机、用户名和密码,确保其允许从当前位置访问。', - 'ERROR_DB_2' => '无法创建数据库。请仔细检查主机、用户名和密码,确保指定的用户拥有在数据库中修改和添加内容的权限。', - 'ERROR_CONFIG_FILE' => "无法保存此配置。'data/' 拒绝访问。请为其他用户设置 'data/''uploads/' 目录的读、写权限。查看自述文件以获取更多信息。", - 'ERROR_UNKNOWN' => '发生未知问题。请再试一次,检查您的安装和服务器。请查看自述文件以获取更多信息。', - 'ERROR_LOGIN' => '无法保存登录信息。请用另一个用户名和密码再试一次!', - 'ERROR_MAP_DEACTIVATED' => '地图功能已在设置中停用。', - 'ERROR_SEARCH_DEACTIVATED' => '搜索功能已在设置中停用。', - 'SUCCESS' => 'OK', - 'RETRY' => '重试', - - 'SETTINGS_SUCCESS_LOGIN' => '登录信息已更新.', - 'SETTINGS_SUCCESS_SORT' => '排序顺序已更新。', - 'SETTINGS_SUCCESS_DROPBOX' => 'Dropbox 密钥已更新。', - 'SETTINGS_SUCCESS_LANG' => '语言已更新。', - 'SETTINGS_SUCCESS_LAYOUT' => '布局已更新', - 'SETTINGS_SUCCESS_IMAGE_OVERLAY' => 'EXIF 叠层设置已更新', - 'SETTINGS_SUCCESS_PUBLIC_SEARCH' => '公开搜索已更新', - 'SETTINGS_SUCCESS_LICENSE' => '默认许可证已更新', - 'SETTINGS_SUCCESS_MAP_DISPLAY' => '地图显示设置已更新', - 'SETTINGS_SUCCESS_MAP_DISPLAY_PUBLIC' => '公开相册的地图显示设置已更新', - 'SETTINGS_SUCCESS_MAP_PROVIDER' => '地图供应商设置已更新', - - 'U2F_NOT_SUPPORTED' => 'U2F 不被支持。 抱歉。', - 'U2F_NOT_SECURE' => '环境不安全。U2F 不可用', - 'U2F_REGISTER_KEY' => '注册新设备。', - 'U2F_REGISTRATION_SUCCESS' => '注册成功!', - 'U2F_AUTHENTIFICATION_SUCCESS' => '认证成功!', - 'U2F_CREDENTIALS' => '认证信息', - 'U2F_CREDENTIALS_DELETED' => '认证信息已删除!', - - 'NEW_PHOTOS_NOTIFICATION' => 'Send new photos notification emails.', - 'SETTINGS_SUCCESS_NEW_PHOTOS_NOTIFICATION' => 'New photos notification updated', - 'USER_EMAIL_INSTRUCTION' => 'Add your email below to enable receiving email notifications.
To stop receiving emails, simply remove your email below.', - - 'DB_INFO_TITLE' => '请在下方输入你的数据库连接信息:', - 'DB_INFO_HOST' => '数据库主机(可选)', - 'DB_INFO_USER' => '数据库用户名', - 'DB_INFO_PASSWORD' => '数据库密码', - 'DB_INFO_TEXT' => 'Lychee 将创建自己的数据库。如果需要,你可以输入已有数据库的名称:', - 'DB_NAME' => '数据库名称(可选)', - 'DB_PREFIX' => '表前缀(可选)', - 'DB_CONNECT' => '连接', - - 'LOGIN_TITLE' => '输入管理员用户名和密码:', - 'LOGIN_USERNAME' => '新用户名', - 'LOGIN_PASSWORD' => '新密码', - 'LOGIN_PASSWORD_CONFIRM' => '确认密码', - 'LOGIN_CREATE' => '创建', - - 'PASSWORD_TITLE' => '输入您当前的密码:', - 'USERNAME_CURRENT' => '当前用户名', - 'PASSWORD_CURRENT' => '当前密码', - 'PASSWORD_TEXT' => '您的用户名和密码将被修改为:', - 'PASSWORD_CHANGE' => '修改登录信息', - - 'EDIT_SHARING_TITLE' => '编辑共享', - 'EDIT_SHARING_TEXT' => '此相册的共享属性将被修改为:', - 'SHARE_ALBUM_TEXT' => '此相册将会以下列的属性共享:', - 'ALBUM_SHARING_CONFIRM' => '保存', - - 'SORT_ALBUM_BY_1' => '相册排序:根据', - 'SORT_ALBUM_BY_2' => '的', - 'SORT_ALBUM_BY_3' => '排序。', - - 'SORT_ALBUM_SELECT_1' => '创建时间', - 'SORT_ALBUM_SELECT_2' => '标题', - 'SORT_ALBUM_SELECT_3' => '描述', - 'SORT_ALBUM_SELECT_4' => '公开', - 'SORT_ALBUM_SELECT_5' => '最新', - 'SORT_ALBUM_SELECT_6' => '最老', - - 'SORT_PHOTO_BY_1' => '照片排序:根据', - 'SORT_PHOTO_BY_2' => '的', - 'SORT_PHOTO_BY_3' => '排序。', - - 'SORT_PHOTO_SELECT_1' => '上传时间', - 'SORT_PHOTO_SELECT_2' => '创建时间', - 'SORT_PHOTO_SELECT_3' => '标题', - 'SORT_PHOTO_SELECT_4' => '描述', - 'SORT_PHOTO_SELECT_5' => '公开', - 'SORT_PHOTO_SELECT_6' => '喜欢', - 'SORT_PHOTO_SELECT_7' => '照片格式', - - 'SORT_ASCENDING' => '升序', - 'SORT_DESCENDING' => '降序', - 'SORT_CHANGE' => '修改排序', - - 'DROPBOX_TITLE' => '设置 Dropbox 密钥', - 'DROPBOX_TEXT' => "要从 Dropbox 导入照片,您需要一个有效的插件应用密钥,请转到 他们的网站。为你自己生成个人密钥并输入到下面:", - - 'LANG_TEXT' => '将 Lychee 的语言修改为:', - 'LANG_TITLE' => '修改语言', - 'PUBLIC_SEARCH_TEXT' => '允许公共搜索:', - 'OVERLAY_TYPE' => '用于图像叠层中的数据:', - 'OVERLAY_NONE' => 'No overlay', - 'OVERLAY_EXIF' => '照片 EXIF 数据', - 'OVERLAY_DESCRIPTION' => '照片描述', - 'OVERLAY_DATE' => '照片拍摄日期', - 'MAP_DISPLAY_TEXT' => '启用地图(由 OpenStreetMap 提供):', - 'MAP_DISPLAY_PUBLIC_TEXT' => '为公共相册启用地图(由 OpenStreetMap 提供):', - 'MAP_PROVIDER' => 'OpenStreetMap 图层提供者:', - 'MAP_PROVIDER_WIKIMEDIA' => 'Wikimedia', - 'MAP_PROVIDER_OSM_ORG' => 'OpenStreetMap.org(无 HiDPI)', - 'MAP_PROVIDER_OSM_DE' => 'OpenStreetMap.de(无 HiDPI)', - 'MAP_PROVIDER_OSM_FR' => 'OpenStreetMap.fr(无 HiDPI)', - 'MAP_PROVIDER_RRZE' => 'University of Erlangen, Germany(仅 HiDPI)', - 'MAP_INCLUDE_SUBALBUMS_TEXT' => '在地图上包含子相册的图片:', - 'LOCATION_DECODING' => '将 GPS 数据解码为地点名称', - 'LOCATION_SHOW' => '显示地点名称', - 'LOCATION_SHOW_PUBLIC' => '为公开模式显示地点名称', - 'LAYOUT_TYPE' => '照片布局:', - 'LAYOUT_SQUARES' => '方形缩略图', - 'LAYOUT_JUSTIFIED' => '保持长宽比,两端对齐', - 'LAYOUT_UNJUSTIFIED' => '保持长宽比,不对齐', - 'SET_LAYOUT' => '更改布局', - - 'NSFW_VISIBLE_TEXT_1' => '使敏感相册默认可见。', - 'NSFW_VISIBLE_TEXT_2' => '如果相册是公开的,其将仍然可以访问,只是会从视图中隐藏并可以通过按下H键来显示。', - 'SETTINGS_SUCCESS_NSFW_VISIBLE' => '敏感相册的默认可见性成功更新。', - - 'VIEW_NO_RESULT' => '无结果', - 'VIEW_NO_PUBLIC_ALBUMS' => '没有公开相册', - 'VIEW_NO_CONFIGURATION' => '没有配置', - 'VIEW_PHOTO_NOT_FOUND' => '照片未找到', - - 'NO_TAGS' => '没有标签', - - 'UPLOAD_MANAGE_NEW_PHOTOS' => '您现在可以管理您的新照片了。', - 'UPLOAD_COMPLETE' => '上传完成', - 'UPLOAD_COMPLETE_FAILED' => '有一个或多个照片上传失败。', - 'UPLOAD_IMPORTING' => '导入', - 'UPLOAD_IMPORTING_URL' => '导入 URL', - 'UPLOAD_UPLOADING' => '上传中', - 'UPLOAD_FINISHED' => '已完成', - 'UPLOAD_PROCESSING' => '处理中', - 'UPLOAD_FAILED' => '失败', - 'UPLOAD_FAILED_ERROR' => '上传失败。服务器返回了一个错误!', - 'UPLOAD_FAILED_WARNING' => '上传失败。服务器返回了一个警告!', - 'UPLOAD_CANCELLED' => 'Cancelled', - 'UPLOAD_SKIPPED' => '已跳过', - 'UPLOAD_UPDATED' => 'Updated', - 'UPLOAD_IMPORT_SKIPPED_DUPLICATE' => 'This photo has been skipped because it\'s already in your library.', - 'UPLOAD_IMPORT_RESYNCED_DUPLICATE' => 'This photo has been skipped because it\'s already in your library, but its metadata has been updated.', - 'UPLOAD_ERROR_CONSOLE' => '请查看浏览器控制台获取详细信息。', - 'UPLOAD_UNKNOWN' => '服务器返回未知响应。请查看浏览器控制台获取详细信息。', - 'UPLOAD_ERROR_UNKNOWN' => '上传失败。服务器返回了一个未知错误!', - 'UPLOAD_ERROR_POSTSIZE' => '上传失败。PHP 的 post_max_size 限制过小!', - 'UPLOAD_ERROR_FILESIZE' => '上传失败。PHP 的 upload_max_filesize 限制过小!', - 'UPLOAD_IN_PROGRESS' => 'Lychee 当前正在上传!', - 'UPLOAD_IMPORT_WARN_ERR' => '导入已完成,但返回了警告或错误。请查看日志(设置->显示日志)以获取详细信息。', - 'UPLOAD_IMPORT_COMPLETE' => '导入完成', - 'UPLOAD_IMPORT_INSTR' => '输入照片的直链以导入:', - 'UPLOAD_IMPORT' => '导入', - 'UPLOAD_IMPORT_SERVER' => '从服务器导入', - 'UPLOAD_IMPORT_SERVER_FOLD' => '文件夹为空或其中没有可读的文件。请查看日志(设置->显示日志)以获取详细信息。', - 'UPLOAD_IMPORT_SERVER_INSTR' => '此操作将会导入位于下列目录中的所有图片、文件夹和子文件夹。', - 'UPLOAD_ABSOLUTE_PATH' => '目录的绝对路径', - 'UPLOAD_IMPORT_SERVER_EMPT' => '无法导入空文件夹!', - 'UPLOAD_IMPORT_DELETE_ORIGINALS' => '删除原始图像', - 'UPLOAD_IMPORT_DELETE_ORIGINALS_EXPL' => '原始图像将在导入后尝试删除。', - 'UPLOAD_IMPORT_VIA_SYMLINK' => 'Symbolic links', - 'UPLOAD_IMPORT_VIA_SYMLINK_EXPL' => 'Import files using symbolic links to originals.', - 'UPLOAD_IMPORT_SKIP_DUPLICATES' => 'Skip duplicates', - 'UPLOAD_IMPORT_SKIP_DUPLICATES_EXPL' => 'Existing media files are skipped.', - 'UPLOAD_IMPORT_RESYNC_METADATA' => 'Re-sync metadata', - 'UPLOAD_IMPORT_RESYNC_METADATA_EXPL' => 'Update metadata of existing media files.', - 'UPLOAD_IMPORT_LOW_MEMORY' => '内存不足!', - 'UPLOAD_IMPORT_LOW_MEMORY_EXPL' => '此服务器上的导入进程已经接近内存上限并可能过早地被中断。', - 'UPLOAD_WARNING' => '警告', - 'UPLOAD_IMPORT_NOT_A_DIRECTORY' => '指定的路径不是一个可读的目录!', - 'UPLOAD_IMPORT_PATH_RESERVED' => '指定的路径是 Lychee 的保留目录!', - 'UPLOAD_IMPORT_UNREADABLE' => '不能读取文件!', - 'UPLOAD_IMPORT_FAILED' => '不能导入文件!', - 'UPLOAD_IMPORT_UNSUPPORTED' => '不支持的文件类型!', - 'UPLOAD_IMPORT_ALBUM_FAILED' => '不能创建相册!', - 'UPLOAD_IMPORT_CANCELLED' => 'Import cancelled', - - 'ABOUT_SUBTITLE' => '自托管照片管理的正确之选', - 'ABOUT_DESCRIPTION' => '是一个自由的照片管理工具,其其运行于您的服务器或Web空间。仅需几分钟即可安装。Lychee 为您提供了像原生应用那样上传、管理和分享照片所需的一切,您的所有照片都将安全地存储。', - 'FOOTER_COPYRIGHT' => '本网站上的所有图像均受制于版权', - 'HOSTED_WITH_LYCHEE' => '由 Lychee 托管', - - 'URL_COPY_TO_CLIPBOARD' => '复制到剪贴板', - 'URL_COPIED_TO_CLIPBOARD' => 'URL 已经复制到剪贴板!', - 'PHOTO_DIRECT_LINKS_TO_IMAGES' => '图像文件的直链:', - 'PHOTO_MEDIUM' => '中等尺寸', - 'PHOTO_MEDIUM_HIDPI' => '中等尺寸 HiDPI', - 'PHOTO_SMALL' => '缩略图', - 'PHOTO_SMALL_HIDPI' => '缩略图 HiDPI', - 'PHOTO_THUMB' => '方形缩略图', - 'PHOTO_THUMB_HIDPI' => '方形缩略图 HiDPI', - 'PHOTO_LIVE_VIDEO' => '实况照片(Live-Photo)的视频部分', - 'PHOTO_VIEW' => 'Lychee 照片查看:', - - 'PHOTO_EDIT_ROTATECWISE' => '顺时针旋转', - 'PHOTO_EDIT_ROTATECCWISE' => '逆时针旋转', - ]; - - return $locale; - } -} diff --git a/app/Locale/ChineseTraditional.php b/app/Locale/ChineseTraditional.php deleted file mode 100644 index 1ea640ac998..00000000000 --- a/app/Locale/ChineseTraditional.php +++ /dev/null @@ -1,475 +0,0 @@ - '帳號名稱', - 'PASSWORD' => '密碼', - 'ENTER' => '確定', - 'CANCEL' => '取消', - 'SIGN_IN' => '登入', - 'CLOSE' => '關閉', - 'SETTINGS' => '設定', - 'SEARCH' => '搜尋...', - 'MORE' => '更多', - 'DEFAULT' => '默認', - - 'USERS' => '使用者', - 'U2F' => 'U2F', - 'NOTIFICATIONS' => 'Notifications', - 'SHARING' => '分享', - 'CHANGE_LOGIN' => '修改登入訊息', - 'CHANGE_SORTING' => '修改排序', - 'SET_DROPBOX' => '設定Dropbox', - 'ABOUT_LYCHEE' => '關於Lychee', - 'DIAGNOSTICS' => '診斷', - 'DIAGNOSTICS_GET_SIZE' => '請求空間使用', - 'LOGS' => '查看日誌', - 'SIGN_OUT' => '登出', - 'UPDATE_AVAILABLE' => '可用更新!', - 'MIGRATION_AVAILABLE' => '可進行轉移!', - 'DEFAULT_LICENSE' => '新上傳的默認許可證:', - 'SET_LICENSE' => '設置許可證', - 'SET_OVERLAY_TYPE' => '設置疊加', - 'SET_MAP_PROVIDER' => '設置OpenStreetMap圖層提供者', - - 'SMART_ALBUMS' => '智能相簿', - 'SHARED_ALBUMS' => '共享的相簿', - 'ALBUMS' => '相簿', - 'PHOTOS' => '照片', - 'SEARCH_RESULTS' => '搜索結果', - - 'RENAME' => '重新命名', - 'RENAME_ALL' => '重新命名成功', - 'MERGE' => '合併', - 'MERGE_ALL' => '合併成功', - 'MAKE_PUBLIC' => '設為公開', - 'SHARE_ALBUM' => '分享相簿', - 'SHARE_PHOTO' => '分享照片', - 'VISIBILITY_ALBUM' => '相冊隱私設定', - 'VISIBILITY_PHOTO' => '照片隱私設定', - 'DOWNLOAD_ALBUM' => '下載相簿', - 'ABOUT_ALBUM' => '關於相簿', - 'DELETE_ALBUM' => '刪除相簿', - 'MOVE_ALBUM' => '移動相簿', - 'FULLSCREEN_ENTER' => '全螢幕模式', - 'FULLSCREEN_EXIT' => '結束全螢幕模式', - - 'SHARING_ALBUM_USERS' => 'Share this album with users', - 'WAIT_FETCH_DATA' => 'Please wait while we get the data...', - 'SHARING_ALBUM_USERS_NO_USERS' => 'There are no users to share the album with', - 'SHARING_ALBUM_USERS_LONG_MESSAGE' => 'Select the users to share this album with', - - 'DELETE_ALBUM_QUESTION' => '刪除相簿和照片', - 'KEEP_ALBUM' => '保留相簿', - 'DELETE_ALBUM_CONFIRMATION_1' => '確定要刪除相簿', - 'DELETE_ALBUM_CONFIRMATION_2' => '以及相簿內包含的所有照片?此動作無法還原!', - - 'DELETE_ALBUMS_QUESTION' => '刪除相簿和照片', - 'KEEP_ALBUMS' => '保留相簿', - 'DELETE_ALBUMS_CONFIRMATION_1' => '確定要刪除全部照片', - 'DELETE_ALBUMS_CONFIRMATION_2' => '選取的相簿和其中的所有照片?此動作無法還原!', - - 'DELETE_UNSORTED_CONFIRM' => '確定刪除\'未分類\'的所有照片?
此動作無法還原!', - 'CLEAR_UNSORTED' => '清除未分類', - 'KEEP_UNSORTED' => '保留未分類', - - 'EDIT_SHARING' => '編輯共享', - 'MAKE_PRIVATE' => '設為私人', - - 'CLOSE_ALBUM' => '關閉相簿', - 'CLOSE_PHOTO' => '關閉照片', - 'CLOSE_MAP' => '關閉地圖', - - 'ADD' => '添加', - 'MOVE' => '移動', - 'MOVE_ALL' => '移動已選項目', - 'DUPLICATE' => '創建副本', - 'DUPLICATE_ALL' => '複製已選項目', - 'COPY_TO' => '複製到...', - 'COPY_ALL_TO' => '複製到...', - 'DELETE' => '刪除', - 'DELETE_ALL' => '删除已選項目', - 'DOWNLOAD' => '下載', - 'DOWNLOAD_ALL' => '下載已選項目', - 'UPLOAD_PHOTO' => '上傳照片', - 'IMPORT_LINK' => '從連結導入', - 'IMPORT_DROPBOX' => '從Dropbox導入', - 'IMPORT_SERVER' => '從伺服器導入', - 'NEW_ALBUM' => '創建新相簿', - 'NEW_TAG_ALBUM' => '新的標籤相簿', - - 'TITLE_NEW_ALBUM' => '輸入相簿標題:', - 'UNTITLED' => '未命名', - 'UNSORTED' => '未分類', - 'STARRED' => '我的最愛', - 'RECENT' => '最新', - 'PUBLIC' => '公開', - 'NUM_PHOTOS' => '照片', - - 'CREATE_ALBUM' => '創建相簿', - 'CREATE_TAG_ALBUM' => '創建標籤相簿', - - 'STAR_PHOTO' => '加入我的最愛', - 'STAR' => '我的最愛', - 'STAR_ALL' => '將已選的標記為收藏夾', - 'TAGS' => '標籤', - 'TAGS_ALL' => '批量標籤', - 'UNSTAR_PHOTO' => '從我的最愛中移除', - 'SET_COVER' => 'Set Album Cover', - 'REMOVE_COVER' => 'Remove Album Cover', - - 'FULL_PHOTO' => '打開原圖', - 'ABOUT_PHOTO' => '照片資訊', - 'DISPLAY_FULL_MAP' => '地圖', - 'DIRECT_LINK' => '外部連結', - 'DIRECT_LINKS' => '內部連結', - - 'ALBUM_ABOUT' => '關於', - 'ALBUM_BASICS' => '基本資訊', - 'ALBUM_TITLE' => '標題', - 'ALBUM_NEW_TITLE' => '輸入新的相簿標題:', - 'ALBUMS_NEW_TITLE_1' => '設定標題為', - 'ALBUMS_NEW_TITLE_2' => '已選擇的所有相簿:', - 'ALBUM_SET_TITLE' => '設定標題', - 'ALBUM_DESCRIPTION' => '描述', - 'ALBUM_SHOW_TAGS' => '顯示標籤', - 'ALBUM_NEW_DESCRIPTION' => '輸入新的相簿描述:', - 'ALBUM_SET_DESCRIPTION' => '編輯描述', - 'ALBUM_NEW_SHOWTAGS' => '輸入將在此相冊中顯示的照片標籤:', - 'ALBUM_SET_SHOWTAGS' => '設定顯示標籤', - 'ALBUM_ALBUM' => '相簿', - 'ALBUM_CREATED' => '創建時間', - 'ALBUM_IMAGES' => '圖片資訊', - 'ALBUM_VIDEOS' => '影片', - 'ALBUM_SUBALBUMS' => '子相簿', - 'ALBUM_SHARING' => '分享', - 'ALBUM_SHR_YES' => '是', - 'ALBUM_SHR_NO' => '否', - 'ALBUM_PUBLIC' => '公開', - 'ALBUM_PUBLIC_EXPL' => '相簿可以被其他人查看,但需遵守以下限制 : ', - 'ALBUM_FULL' => '原圖', - 'ALBUM_FULL_EXPL' => '提供完整解析度照片', - 'ALBUM_HIDDEN' => '隱藏', - 'ALBUM_HIDDEN_EXPL' => '只有知道連結者才能檢視', - 'ALBUM_MARK_NSFW' => 'Mark album as sensitive', - 'ALBUM_UNMARK_NSFW' => 'Unmark album as sensitive', - 'ALBUM_NSFW' => 'Sensitive', - 'ALBUM_NSFW_EXPL' => 'Album is marked to contain sensitive content.', - 'ALBUM_DOWNLOADABLE' => '下載', - 'ALBUM_DOWNLOADABLE_EXPL' => '訪客可以下載該相簿', - 'ALBUM_SHARE_BUTTON_VISIBLE' => '顯示分享按鈕', - 'ALBUM_SHARE_BUTTON_VISIBLE_EXPL' => '顯示社交媒體分享鏈接。', - 'ALBUM_PASSWORD' => '密碼', - 'ALBUM_PASSWORD_PROT' => '密碼保護', - 'ALBUM_PASSWORD_PROT_EXPL' => '只有輸入正確的密碼才可以查看相簿。', - 'ALBUM_PASSWORD_REQUIRED' => '此相簿設有密碼保護。請輸入密碼:', - 'ALBUM_MERGE_1' => '你確定要合併相簿', - 'ALBUM_MERGE_2' => '到該相簿', - 'ALBUMS_MERGE' => '你確定要合併所有已選擇的相簿到該相簿?', - 'MERGE_ALBUM' => '合併相簿', - 'DONT_MERGE' => '不要合併', - 'ALBUM_MOVE_1' => '您確定要移動相簿', - 'ALBUM_MOVE_2' => '到該相簿', - 'ALBUMS_MOVE' => '你確定要合併所有已選擇的相簿到該相簿?', - 'MOVE_ALBUMS' => '相簿移動', - 'NOT_MOVE_ALBUMS' => '不要移動', - 'ROOT' => '相簿', - 'ALBUM_REUSE' => '重複利用', - 'ALBUM_LICENSE' => '許可證', - 'ALBUM_SET_LICENSE' => '設定許可證', - 'ALBUM_LICENSE_HELP' => '需要選擇幫助嗎?', - 'ALBUM_LICENSE_NONE' => '不須', - 'ALBUM_RESERVED' => '版權所有', - 'ALBUM_SET_ORDER' => '設定排序方式', - 'ALBUM_ORDERING' => '排序方式', - - 'PHOTO_ABOUT' => '關於', - 'PHOTO_BASICS' => '基本資訊', - 'PHOTO_TITLE' => '標題', - 'PHOTO_NEW_TITLE' => '輸入新的相片標題:', - 'PHOTO_SET_TITLE' => '設定標題', - 'PHOTO_UPLOADED' => '已上傳', - 'PHOTO_DESCRIPTION' => '描述', - 'PHOTO_NEW_DESCRIPTION' => '輸入新的照片描述', - 'PHOTO_SET_DESCRIPTION' => '編輯描述', - 'PHOTO_NEW_LICENSE' => '新增許可證', - 'PHOTO_SET_LICENSE' => '設定許可證', - 'PHOTO_LICENSE' => '許可證', - 'PHOTO_REUSE' => '重複利用', - 'PHOTO_LICENSE_NONE' => '無', - 'PHOTO_RESERVED' => '版權所有', - 'PHOTO_LATITUDE' => '緯度', - 'PHOTO_LONGITUDE' => '經度', - 'PHOTO_ALTITUDE' => '高度', - 'PHOTO_IMGDIRECTION' => '方向', - 'PHOTO_LOCATION' => '位置', - 'PHOTO_IMAGE' => '照片資訊', - 'PHOTO_VIDEO' => '影片', - 'PHOTO_SIZE' => '大小', - 'PHOTO_FORMAT' => '格式', - 'PHOTO_RESOLUTION' => '解析度', - 'PHOTO_DURATION' => '持續時間', - 'PHOTO_FPS' => '影格速率', - 'PHOTO_TAGS' => '標籤', - 'PHOTO_NOTAGS' => '無標籤', - 'PHOTO_NEW_TAGS' => '為該照片添加標籤(用逗號分隔):', - 'PHOTO_NEW_TAGS_1' => '大量標籤', - 'PHOTO_NEW_TAGS_2' => '標籤已選照片(已存在的標籤會被覆蓋;用逗號分隔):', - 'PHOTO_SET_TAGS' => '設定標籤', - 'PHOTO_CAMERA' => '相機資訊', - 'PHOTO_CAPTURED' => '拍攝時間', - 'PHOTO_MAKE' => '設備', - 'PHOTO_TYPE' => '類型/型號', - 'PHOTO_LENS' => '鏡片', - 'PHOTO_SHUTTER' => '快門速度', - 'PHOTO_APERTURE' => '光圈', - 'PHOTO_FOCAL' => '焦距', - 'PHOTO_ISO' => 'ISO感光度', - 'PHOTO_SHARING' => '共享', - 'PHOTO_SHR_PLUBLIC' => '公開', - 'PHOTO_SHR_ALB' => '是 (相簿)', - 'PHOTO_SHR_PHT' => '是 (照片)', - 'PHOTO_SHR_NO' => '否', - 'PHOTO_DELETE' => '刪除照片', - 'PHOTO_KEEP' => '保留照片', - 'PHOTO_DELETE_1' => '是否要刪除照片', - 'PHOTO_DELETE_2' => '此動作無法還原!', - 'PHOTO_DELETE_ALL_1' => '是否要刪除除所有', - 'PHOTO_DELETE_ALL_2' => '已選擇的照片?此動作無法還原!', - 'PHOTOS_NEW_TITLE_1' => '批量編輯照片標題', - 'PHOTOS_NEW_TITLE_2' => '已選的照片:', - 'PHOTO_MAKE_PRIVATE_ALBUM' => '此照片位於公開相簿中。編輯所在相簿的隱私設定,將其設置為公開或私有。', - 'PHOTO_SHOW_ALBUM' => '顯示相簿', - 'PHOTO_PUBLIC' => '公開', - 'PHOTO_PUBLIC_EXPL' => '他人可以查看照片,但受以下限制', - 'PHOTO_FULL' => '原圖', - 'PHOTO_FULL_EXPL' => '完整解析度照片可用', - 'PHOTO_HIDDEN' => '隱藏', - 'PHOTO_HIDDEN_EXPL' => '只有具有直接鏈接的人才能查看此照片。', - 'PHOTO_DOWNLOADABLE' => '允許下載', - 'PHOTO_DOWNLOADABLE_EXPL' => '您畫廊的訪客可以下載這張照片。', - 'PHOTO_SHARE_BUTTON_VISIBLE' => '顯示分享按鈕', - 'PHOTO_SHARE_BUTTON_VISIBLE_EXPL' => '顯示社交媒體分享鏈接', - 'PHOTO_PASSWORD_PROT' => '密碼保護', - 'PHOTO_PASSWORD_PROT_EXPL' => '僅允許有效密碼檢視照片', - 'PHOTO_EDIT_SHARING_TEXT' => '此照片的共享屬性將更改為以下內容:', - 'PHOTO_NO_EDIT_SHARING_TEXT' => '由於此照片位於公開相簿中,因此它會繼承該相冊的公開範圍設置。 下面顯示了它的當前可見性,僅供參考。', - 'PHOTO_EDIT_GLOBAL_SHARING_TEXT' => '可以使用全局Lychee設置微調這張照片的可見性。 下面顯示了它的當前可見性,僅供參考。', - 'PHOTO_SHARING_CONFIRM' => '保存', - - 'LOADING' => '載入中', - 'ERROR' => '錯誤', - 'ERROR_TEXT' => '噢,似乎出了一些問題。請重整頁面後再試一次!', - 'ERROR_DB_1' => '無法連接數據庫,訪問被拒絕。請仔細檢查主機,用戶名和密碼,確保允許從當前位置訪問。', - 'ERROR_DB_2' => '無法創建數據庫。請仔細檢查主機,用戶名和密碼,確保該擁有權在數據庫中添加和修改內容。', - 'ERROR_CONFIG_FILE' => "無法保存設置。'data/' 拒絕訪問。請為其他用戶設置 'data/''uploads/' 目錄的讀寫權限。查看自述文件以獲取更多信息。", - 'ERROR_UNKNOWN' => '發生未知問題!請再試一次,檢查您的安裝和伺服器。請查看自述文件以獲取更多信息。', - 'ERROR_LOGIN' => '無法保存登錄信息。請用另一個用戶名和密碼再試一次!', - 'ERROR_MAP_DEACTIVATED' => '地圖功能已被設為停用。', - 'ERROR_SEARCH_DEACTIVATED' => '搜索功能已在設為停用。', - 'SUCCESS' => '好', - 'RETRY' => '重試', - - 'SETTINGS_SUCCESS_LOGIN' => '登錄信息已更新', - 'SETTINGS_SUCCESS_SORT' => '排序順序已更新。', - 'SETTINGS_SUCCESS_DROPBOX' => 'Dropbox密鑰已更新', - 'SETTINGS_SUCCESS_LANG' => '語言已更新', - 'SETTINGS_SUCCESS_LAYOUT' => '佈局已更新', - 'SETTINGS_SUCCESS_IMAGE_OVERLAY' => 'EXIF覆蓋設置已更新', - 'SETTINGS_SUCCESS_PUBLIC_SEARCH' => '公開搜尋已更新', - 'SETTINGS_SUCCESS_LICENSE' => '默認許可證已更新', - 'SETTINGS_SUCCESS_MAP_DISPLAY' => '地圖顯示設置已更新', - 'SETTINGS_SUCCESS_MAP_DISPLAY_PUBLIC' => '公開相簿的地圖顯示設置已更新', - 'SETTINGS_SUCCESS_MAP_PROVIDER' => '地圖提供商設置已更新', - - 'U2F_NOT_SUPPORTED' => 'U2F not supported. Sorry.', - 'U2F_NOT_SECURE' => 'Environment not secured. U2F not available.', - 'U2F_REGISTER_KEY' => 'Register new device.', - 'U2F_REGISTRATION_SUCCESS' => 'Registration successful!', - 'U2F_AUTHENTIFICATION_SUCCESS' => 'Authentication successful!', - 'U2F_CREDENTIALS' => 'Credentials', - 'U2F_CREDENTIALS_DELETED' => 'Credentials deleted!', - - 'NEW_PHOTOS_NOTIFICATION' => 'Send new photos notification emails.', - 'SETTINGS_SUCCESS_NEW_PHOTOS_NOTIFICATION' => 'New photos notification updated', - 'USER_EMAIL_INSTRUCTION' => 'Add your email below to enable receiving email notifications.
To stop receiving emails, simply remove your email below.', - - 'DB_INFO_TITLE' => '請輸入數據庫信息', - 'DB_INFO_HOST' => '數據庫主機(選填)', - 'DB_INFO_USER' => '數據庫用戶名', - 'DB_INFO_PASSWORD' => '數據庫密碼', - 'DB_INFO_TEXT' => 'Lychee將自行創建數據庫。如果需要,可以使用現有數據庫', - 'DB_NAME' => '數據庫名稱(選填)', - 'DB_PREFIX' => '表前綴(選填)', - 'DB_CONNECT' => '連接', - - 'LOGIN_TITLE' => '輸入管理員用戶名和密碼:', - 'LOGIN_USERNAME' => '新用戶名', - 'LOGIN_PASSWORD' => '新密碼', - 'LOGIN_PASSWORD_CONFIRM' => '確認密碼', - 'LOGIN_CREATE' => '創建', - - 'PASSWORD_TITLE' => '當前密碼', - 'USERNAME_CURRENT' => '當前用戶名', - 'PASSWORD_CURRENT' => '當前密碼', - 'PASSWORD_TEXT' => '用戶名和密碼將被修改為:', - 'PASSWORD_CHANGE' => '修改登入訊息', - - 'EDIT_SHARING_TITLE' => '編輯共享', - 'EDIT_SHARING_TEXT' => '相簿的共享屬性將被修改為:', - 'SHARE_ALBUM_TEXT' => '該相簿的共享屬性:', - 'ALBUM_SHARING_CONFIRM' => '保存', - - 'SORT_ALBUM_BY_1' => '相簿排序', - 'SORT_ALBUM_BY_2' => '在一個', - 'SORT_ALBUM_BY_3' => '排序', - - 'SORT_ALBUM_SELECT_1' => '創建時間', - 'SORT_ALBUM_SELECT_2' => '標題', - 'SORT_ALBUM_SELECT_3' => '描述', - 'SORT_ALBUM_SELECT_4' => '公開', - 'SORT_ALBUM_SELECT_5' => '最新', - 'SORT_ALBUM_SELECT_6' => '最老', - - 'SORT_PHOTO_BY_1' => '照片排序', - 'SORT_PHOTO_BY_2' => '在一個', - 'SORT_PHOTO_BY_3' => '排序', - - 'SORT_PHOTO_SELECT_1' => '發佈時間', - 'SORT_PHOTO_SELECT_2' => '創建時間', - 'SORT_PHOTO_SELECT_3' => '標題', - 'SORT_PHOTO_SELECT_4' => '描述', - 'SORT_PHOTO_SELECT_5' => '公開', - 'SORT_PHOTO_SELECT_6' => '喜歡', - 'SORT_PHOTO_SELECT_7' => '照片格式', - - 'SORT_ASCENDING' => '升序', - 'SORT_DESCENDING' => '降序', - 'SORT_CHANGE' => '修改排序', - - 'DROPBOX_TITLE' => '設置Dropbox私鑰', - 'DROPBOX_TEXT' => "要從Dropbox導入照片,需要一個有效的應用私鑰,請到官網獲取。輸入你自己生成的私鑰:", - - 'LANG_TEXT' => '將Lychee語言更改為:', - 'LANG_TITLE' => '改變語言', - 'PUBLIC_SEARCH_TEXT' => '允許公共搜索:', - 'OVERLAY_TYPE' => '圖像疊加中要使用的數據:', - 'OVERLAY_NONE' => 'None', - 'OVERLAY_EXIF' => '照片EXIF數據', - 'OVERLAY_DESCRIPTION' => '照片說明', - 'OVERLAY_DATE' => '拍攝日期', - 'MAP_DISPLAY_TEXT' => '啟用地圖(由OpenStreetMap提供):', - 'MAP_DISPLAY_PUBLIC_TEXT' => '為公開相冊啟用地圖(由OpenStreetMap提供):', - 'MAP_PROVIDER' => '地圖的提供者:', - 'MAP_PROVIDER_WIKIMEDIA' => '維基媒體', - 'MAP_PROVIDER_OSM_ORG' => 'OpenStreetMap.org (無 HiDPI)', - 'MAP_PROVIDER_OSM_DE' => 'OpenStreetMap.de (無 HiDPI)', - 'MAP_PROVIDER_OSM_FR' => 'OpenStreetMap.fr (無 HiDPI)', - 'MAP_PROVIDER_RRZE' => '德國埃爾蘭根大學 (只有 HiDPI)', - 'MAP_INCLUDE_SUBALBUMS_TEXT' => '在地圖上包括子相冊的照片:', - 'LOCATION_DECODING' => '將GPS數據解碼為位置名稱', - 'LOCATION_SHOW' => '顯示地點名稱', - 'LOCATION_SHOW_PUBLIC' => '顯示公共模式的位置名稱', - 'LAYOUT_TYPE' => '照片佈局:', - 'LAYOUT_SQUARES' => '方形縮略圖', - 'LAYOUT_JUSTIFIED' => '有方面,有道理', - 'LAYOUT_UNJUSTIFIED' => '有方面,沒有道理', - 'SET_LAYOUT' => '變更版面', - - 'NSFW_VISIBLE_TEXT_1' => 'Make Sensitive albums visible by default.', - 'NSFW_VISIBLE_TEXT_2' => 'If the album is public, it is still accessible, just hidden from the view and can be revealed by pressing H.', - 'SETTINGS_SUCCESS_NSFW_VISIBLE' => 'Default sensitive album visibility updated with success.', - - 'VIEW_NO_RESULT' => '無結果', - 'VIEW_NO_PUBLIC_ALBUMS' => '沒有公開相簿', - 'VIEW_NO_CONFIGURATION' => '没有配置', - 'VIEW_PHOTO_NOT_FOUND' => '没找到照片', - - 'NO_TAGS' => '沒有標籤', - - 'UPLOAD_MANAGE_NEW_PHOTOS' => '現在可以管理你的新照片了', - 'UPLOAD_COMPLETE' => '上傳完成', - 'UPLOAD_COMPLETE_FAILED' => '有幾個照片上傳失敗了', - 'UPLOAD_IMPORTING' => '導入', - 'UPLOAD_IMPORTING_URL' => '導入 URL', - 'UPLOAD_UPLOADING' => '上傳中', - 'UPLOAD_FINISHED' => '已完成', - 'UPLOAD_PROCESSING' => '處理中', - 'UPLOAD_FAILED' => '失敗', - 'UPLOAD_FAILED_ERROR' => '上傳失敗。伺服器傳回了一個錯誤!', - 'UPLOAD_FAILED_WARNING' => '上傳失敗。伺服器傳回了一個警告!', - 'UPLOAD_CANCELLED' => 'Cancelled', - 'UPLOAD_SKIPPED' => '已跳過', - 'UPLOAD_UPDATED' => 'Updated', - 'UPLOAD_IMPORT_SKIPPED_DUPLICATE' => 'This photo has been skipped because it\'s already in your library.', - 'UPLOAD_IMPORT_RESYNCED_DUPLICATE' => 'This photo has been skipped because it\'s already in your library, but its metadata has been updated.', - 'UPLOAD_ERROR_CONSOLE' => '請查看瀏覽器控制台獲取詳細信息。', - 'UPLOAD_UNKNOWN' => '伺服器傳回了未知響應。請查看瀏覽器控制台獲取詳細信息。', - 'UPLOAD_ERROR_UNKNOWN' => '上傳失敗。伺服器回傳了一個未知錯誤!', - 'UPLOAD_ERROR_POSTSIZE' => 'Upload failed. The PHP post_max_size may be too small! Otherwise check the FAQ.', - 'UPLOAD_ERROR_FILESIZE' => 'Upload failed. The PHP upload_max_filesize may be too small! Otherwise check the FAQ.', - 'UPLOAD_IN_PROGRESS' => 'Lychee當前正在上傳!', - 'UPLOAD_IMPORT_WARN_ERR' => '導入成功,但返回了的警告或錯誤。請查看日誌(設置->顯示日誌)以獲取詳細信息。', - 'UPLOAD_IMPORT_COMPLETE' => '導入完成', - 'UPLOAD_IMPORT_INSTR' => '輸入照片鏈接直接導入:', - 'UPLOAD_IMPORT' => '導入', - 'UPLOAD_IMPORT_SERVER' => '從伺服器導入', - 'UPLOAD_IMPORT_SERVER_FOLD' => '文件夾中沒有可讀的文件。請查看日誌(設置->顯示日誌)以獲取詳細信息。', - 'UPLOAD_IMPORT_SERVER_INSTR' => '此操作將導入位於以下目錄中的所有照片,文件夾和子文件夾。', - 'UPLOAD_ABSOLUTE_PATH' => '絕對路徑', - 'UPLOAD_IMPORT_SERVER_EMPT' => '無法導入空文件夾!', - 'UPLOAD_IMPORT_DELETE_ORIGINALS' => '刪除原件', - 'UPLOAD_IMPORT_DELETE_ORIGINALS_EXPL' => '如果可能,原始文件將在導入後刪除。', - 'UPLOAD_IMPORT_VIA_SYMLINK' => 'Symbolic links', - 'UPLOAD_IMPORT_VIA_SYMLINK_EXPL' => 'Import files using symbolic links to originals.', - 'UPLOAD_IMPORT_SKIP_DUPLICATES' => 'Skip duplicates', - 'UPLOAD_IMPORT_SKIP_DUPLICATES_EXPL' => 'Existing media files are skipped.', - 'UPLOAD_IMPORT_RESYNC_METADATA' => 'Re-sync metadata', - 'UPLOAD_IMPORT_RESYNC_METADATA_EXPL' => 'Update metadata of existing media files.', - 'UPLOAD_IMPORT_LOW_MEMORY' => '內存不足!', - 'UPLOAD_IMPORT_LOW_MEMORY_EXPL' => '伺服器上的導入過程已接近內存限制,並可能最終被提前終止。', - 'UPLOAD_WARNING' => '警告', - 'UPLOAD_IMPORT_NOT_A_DIRECTORY' => '給定的路徑不是可讀目錄!', - 'UPLOAD_IMPORT_PATH_RESERVED' => '給定的路徑是Lychee的保留路徑!', - 'UPLOAD_IMPORT_UNREADABLE' => '無法讀取文件!', - 'UPLOAD_IMPORT_FAILED' => '無法導入文件!', - 'UPLOAD_IMPORT_UNSUPPORTED' => '不支援的文件類型!', - 'UPLOAD_IMPORT_ALBUM_FAILED' => '無法創建相簿!', - 'UPLOAD_IMPORT_CANCELLED' => 'Import cancelled', - - 'ABOUT_SUBTITLE' => 'Lychee自主託管的照片管理程序', - 'ABOUT_DESCRIPTION' => '是一個免費的照片管理工具,可在您的伺服器或網站空間上運行。安裝僅需幾秒鐘。
上傳,管理和分享照片(例如從本機應用程序)。
Lychee提供您所需的一切,所有照片均安全存儲。', - 'FOOTER_COPYRIGHT' => '本網站的照片均受版權所有', - 'HOSTED_WITH_LYCHEE' => '使用Lychee託管照片(繁中由CYL翻譯)', - - 'URL_COPY_TO_CLIPBOARD' => '複製到剪貼板', - 'URL_COPIED_TO_CLIPBOARD' => '複製到剪貼板的URL!', - 'PHOTO_DIRECT_LINKS_TO_IMAGES' => '指向圖像文件的直接鏈接:', - 'PHOTO_MEDIUM' => '中等', - 'PHOTO_MEDIUM_HIDPI' => '中等解析度', - 'PHOTO_SMALL' => '低', - 'PHOTO_SMALL_HIDPI' => '低解析度', - 'PHOTO_THUMB' => '方形圖', - 'PHOTO_THUMB_HIDPI' => '方形解析度', - 'PHOTO_LIVE_VIDEO' => '實時照片的視頻部分', - 'PHOTO_VIEW' => 'Lychee照片瀏覽:', - - 'PHOTO_EDIT_ROTATECWISE' => '順時針旋轉', - 'PHOTO_EDIT_ROTATECCWISE' => '逆時針旋轉', - ]; - - return $locale; - } -} diff --git a/app/Locale/Czech.php b/app/Locale/Czech.php deleted file mode 100644 index cd9dc852105..00000000000 --- a/app/Locale/Czech.php +++ /dev/null @@ -1,482 +0,0 @@ - 'Uživatelské jméno', - 'PASSWORD' => 'Heslo', - 'ENTER' => 'Vložit', - 'CANCEL' => 'Storno', - 'SIGN_IN' => 'Příhlásit se', - 'CLOSE' => 'Zavřít', - 'SETTINGS' => 'Nastavení', - 'SEARCH' => 'Hledat ...', - 'MORE' => 'Rozšířená nastavení', - 'DEFAULT' => 'Default', - - 'USERS' => 'Uživatelé', - 'U2F' => 'U2F', - 'NOTIFICATIONS' => 'Notifications', - 'SHARING' => 'Sdílení', - 'CHANGE_LOGIN' => 'Změnit přihlášení', - 'CHANGE_SORTING' => 'Změnt řazení', - 'SET_DROPBOX' => 'Nastavit Dropbox', - 'ABOUT_LYCHEE' => 'O Lychee', - 'DIAGNOSTICS' => 'Diagnostika', - 'DIAGNOSTICS_GET_SIZE' => 'Request space usage', - 'LOGS' => 'Protokoly', - 'SIGN_OUT' => 'Odhlásit se', - 'UPDATE_AVAILABLE' => 'Update je k dispozici!', - 'MIGRATION_AVAILABLE' => 'Migration available!', - 'DEFAULT_LICENSE' => 'Výchozí licence pro nové uploady:', - 'SET_LICENSE' => 'Nastavit licenci', - 'SET_OVERLAY_TYPE' => 'Nastavit překrytí', - 'SET_MAP_PROVIDER' => 'Nastavit providera OpenStreetMap', - 'SAVE_RISK' => 'Uložit změny, rizika jsou mi známa!', - - 'SMART_ALBUMS' => 'Chytrá alba', - 'SHARED_ALBUMS' => 'Sdílená alba', - 'ALBUMS' => 'Alba', - 'PHOTOS' => 'Obrázky', - 'SEARCH_RESULTS' => 'Výsledky hledání', - - 'RENAME' => 'Přejmenovat', - 'RENAME_ALL' => 'Přejmenovat vybrané', - 'MERGE' => 'Sloučit', - 'MERGE_ALL' => 'Sloučit vybrané', - 'MAKE_PUBLIC' => 'Zveřejnit', - 'SHARE_ALBUM' => 'Sdílet album', - 'SHARE_PHOTO' => 'Sdílet fotografii', - 'VISIBILITY_ALBUM' => 'Viditelnost alba', - 'VISIBILITY_PHOTO' => 'Viditelnost fotografie', - 'DOWNLOAD_ALBUM' => 'Stáhnout album', - 'ABOUT_ALBUM' => 'O albu', - 'DELETE_ALBUM' => 'Smazat album', - 'MOVE_ALBUM' => 'Přesunout album', - 'FULLSCREEN_ENTER' => 'Spustit režim celé obrazovky', - 'FULLSCREEN_EXIT' => 'Ukončit režim celé obrazovky', - - 'SHARING_ALBUM_USERS' => 'Share this album with users', - 'WAIT_FETCH_DATA' => 'Please wait while we get the data...', - 'SHARING_ALBUM_USERS_NO_USERS' => 'There are no users to share the album with', - 'SHARING_ALBUM_USERS_LONG_MESSAGE' => 'Select the users to share this album with', - - 'DELETE_ALBUM_QUESTION' => 'Mazání alba a fotografie', - 'KEEP_ALBUM' => 'Ponechat album', - 'DELETE_ALBUM_CONFIRMATION_1' => 'Opravdu smazat album', - 'DELETE_ALBUM_CONFIRMATION_2' => 'a všechny fotografie, které obsahuje? Tento krok je nevratný!', - - 'DELETE_ALBUMS_QUESTION' => 'Mazání alb a fotografií', - 'KEEP_ALBUMS' => 'Ponechat alba', - 'DELETE_ALBUMS_CONFIRMATION_1' => 'Opravdu smazat všechna vybraná', - 'DELETE_ALBUMS_CONFIRMATION_2' => 'alba a fotografie, které obsahují? Tento krok je nevratný!', - - 'DELETE_UNSORTED_CONFIRM' => 'Opravdu odstranit všechny \'Nesetříděné\' fotografie?
Tento krok je nevratný!', - 'CLEAR_UNSORTED' => 'Odstranit Nesetříděné', - 'KEEP_UNSORTED' => 'Ponechat Nesetříděné', - - 'EDIT_SHARING' => 'Upravit sdílení', - 'MAKE_PRIVATE' => 'Nastavit jako privátní', - - 'CLOSE_ALBUM' => 'Zavřít album', - 'CLOSE_PHOTO' => 'Zavřít fotografii', - 'CLOSE_MAP' => 'Zavřít mapu', - - 'ADD' => 'Přidat', - 'MOVE' => 'Přesunout', - 'MOVE_ALL' => 'Přesunout vybrané', - 'DUPLICATE' => 'Duplikovat', - 'DUPLICATE_ALL' => 'Duplikovat vybrané', - 'COPY_TO' => 'Kopírovat do...', - 'COPY_ALL_TO' => 'Kopírovat vybrané do...', - 'DELETE' => 'Odstranit', - 'DELETE_ALL' => 'Odstranit vybrané', - 'DOWNLOAD' => 'Stáhnout', - 'DOWNLOAD_ALL' => 'Stánout vybrané', - 'UPLOAD_PHOTO' => 'Odeslat fotografii', - 'IMPORT_LINK' => 'Importovat z odkazu', - 'IMPORT_DROPBOX' => 'Importovat z Dropboxu', - 'IMPORT_SERVER' => 'Importovat ze serveru', - 'NEW_ALBUM' => 'Nové album', - 'NEW_TAG_ALBUM' => 'Nové tag album', - - 'TITLE_NEW_ALBUM' => 'Zadejte název nového alba:', - 'UNTITLED' => 'Bezejmanné', - 'UNSORTED' => 'Nesetříděné', - 'STARRED' => 'Oblíbené', - 'RECENT' => 'Poslední', - 'PUBLIC' => 'Veřejné', - 'NUM_PHOTOS' => 'fotografií', - - 'CREATE_ALBUM' => 'Vytvořit album', - 'CREATE_TAG_ALBUM' => 'Vytvořit Tag album', - - 'STAR_PHOTO' => 'Označit jako oblíbené', - 'STAR' => 'Označit jako oblíbené', - 'STAR_ALL' => 'Vše označit jako oblíbené', - 'TAGS' => 'Štítek', - 'TAGS_ALL' => 'Oštítkovat vše', - 'UNSTAR_PHOTO' => 'Odebrat z oblíbených', - 'SET_COVER' => 'Set Album Cover', - 'REMOVE_COVER' => 'Remove Album Cover', - - 'FULL_PHOTO' => 'Otevřít originál', - 'ABOUT_PHOTO' => 'O fotografii', - 'DISPLAY_FULL_MAP' => 'Mapa', - 'DIRECT_LINK' => 'Přímý odkaz', - 'DIRECT_LINKS' => 'Přímé odkazy', - - 'ALBUM_ABOUT' => 'O albu', - 'ALBUM_BASICS' => 'Základní informace', - 'ALBUM_TITLE' => 'Název', - 'ALBUM_NEW_TITLE' => 'Zadat nový název alba:', - 'ALBUMS_NEW_TITLE_1' => 'Zadat nový název pro', - 'ALBUMS_NEW_TITLE_2' => 'vybraná alba:', - 'ALBUM_SET_TITLE' => 'Uložit název', - 'ALBUM_DESCRIPTION' => 'Popis', - 'ALBUM_SHOW_TAGS' => 'Zobrazené tagy', - 'ALBUM_NEW_DESCRIPTION' => 'Zadat nový popis pro album:', - 'ALBUM_SET_DESCRIPTION' => 'Uložit popis', - 'ALBUM_NEW_SHOWTAGS' => 'Zadejte tagy fotografií, které budou viditelné v albu:', - 'ALBUM_SET_SHOWTAGS' => 'Tagy k zobrazení', - 'ALBUM_ALBUM' => 'Album', - 'ALBUM_CREATED' => 'Vytvořeno', - 'ALBUM_IMAGES' => 'Obrázky', - 'ALBUM_VIDEOS' => 'Videa', - 'ALBUM_SUBALBUMS' => 'Subalba', - 'ALBUM_SHARING' => 'Sdílení', - 'ALBUM_SHR_YES' => 'Ano', - 'ALBUM_SHR_NO' => 'Ne', - 'ALBUM_PUBLIC' => 'Veřejné', - 'ALBUM_PUBLIC_EXPL' => 'Album může být zobrazeno ostatním s následujícím omezením:', - 'ALBUM_FULL' => 'Originál', - 'ALBUM_FULL_EXPL' => 'Plné rozlišení k dispozici.', - 'ALBUM_HIDDEN' => 'Skryté', - 'ALBUM_HIDDEN_EXPL' => 'Pouze pro návštěvníky s přímým odkazem alba.', - 'ALBUM_MARK_NSFW' => 'Mark album as sensitive', - 'ALBUM_UNMARK_NSFW' => 'Unmark album as sensitive', - 'ALBUM_NSFW' => 'Sensitive', - 'ALBUM_NSFW_EXPL' => 'Album is marked to contain sensitive content.', - 'ALBUM_DOWNLOADABLE' => 'Stažitelné', - 'ALBUM_DOWNLOADABLE_EXPL' => 'Album mouhou stáhnout pouze jeho návštěvníci.', - 'ALBUM_SHARE_BUTTON_VISIBLE' => 'Tlačítko sdílet je viditelné', - 'ALBUM_SHARE_BUTTON_VISIBLE_EXPL' => 'Zobrazit odkazy pro sdílení na socíálních sítích.', - 'ALBUM_PASSWORD' => 'Heslo', - 'ALBUM_PASSWORD_PROT' => 'Chráněné heslem', - 'ALBUM_PASSWORD_PROT_EXPL' => 'Přístup do alba pouze s platným heslem.', - 'ALBUM_PASSWORD_REQUIRED' => 'Toto album je chráněno heslem. K jeho zobrazení zadejte prosím platné heslo:', - 'ALBUM_MERGE_1' => 'Sloučení vybraných alb', - 'ALBUM_MERGE_2' => 'do jednoho alba', - 'ALBUMS_MERGE' => 'Sloučit', - 'MERGE_ALBUM' => 'Sloučit alba', - 'DONT_MERGE' => 'Neslučovat', - 'ALBUM_MOVE_1' => 'Opravdu přesunout album', - 'ALBUM_MOVE_2' => 'do alba', - 'ALBUMS_MOVE' => 'Opravdu přesunout vybraná alba do alba', - 'MOVE_ALBUMS' => 'Přesunout album', - 'NOT_MOVE_ALBUMS' => 'Nepřesouvat', - 'ROOT' => 'Alba', - 'ALBUM_REUSE' => 'Použití', - 'ALBUM_LICENSE' => 'Licence', - 'ALBUM_SET_LICENSE' => 'Nastavit licenci', - 'ALBUM_LICENSE_HELP' => 'Potřebujete pomoci s výběrem?', - 'ALBUM_LICENSE_NONE' => 'Žádná', - 'ALBUM_RESERVED' => 'Všechna práva vyhrazena', - 'ALBUM_SET_ORDER' => 'Set Order', - 'ALBUM_ORDERING' => 'Order by', - - 'PHOTO_ABOUT' => 'O fotografii', - 'PHOTO_BASICS' => 'Základní informace', - 'PHOTO_TITLE' => 'Název', - 'PHOTO_NEW_TITLE' => 'Zadat nový název fotografie:', - 'PHOTO_SET_TITLE' => 'Uložit název', - 'PHOTO_UPLOADED' => 'Odesláno', - 'PHOTO_DESCRIPTION' => 'Popis', - 'PHOTO_NEW_DESCRIPTION' => 'Zadejte nový název pro tuto fotografii:', - 'PHOTO_SET_DESCRIPTION' => 'Uložit popis', - 'PHOTO_NEW_LICENSE' => 'Přidat licenci', - 'PHOTO_SET_LICENSE' => 'Uložit licenci', - 'PHOTO_LICENSE' => 'Licence', - 'PHOTO_REUSE' => 'Opakované použití', - 'PHOTO_LICENSE_NONE' => 'Žádná', - 'PHOTO_RESERVED' => 'Všechna práva vyhrazena', - 'PHOTO_LATITUDE' => 'Zeměpisná šířka', - 'PHOTO_LONGITUDE' => 'Zeměpisná délka', - 'PHOTO_ALTITUDE' => 'Nadmořská výška', - 'PHOTO_IMGDIRECTION' => 'Směr', - 'PHOTO_LOCATION' => 'Location', - 'PHOTO_IMAGE' => 'Fotografie', - 'PHOTO_VIDEO' => 'Video', - 'PHOTO_SIZE' => 'Velikost', - 'PHOTO_FORMAT' => 'Formát', - 'PHOTO_RESOLUTION' => 'Rozlišení', - 'PHOTO_DURATION' => 'Trvání', - 'PHOTO_FPS' => 'Frekvence snímků', - 'PHOTO_TAGS' => 'Štítky', - 'PHOTO_NOTAGS' => 'Bez štítků', - 'PHOTO_NEW_TAGS' => 'Zadejte štítky pro tento obrázek. Jednotlivé štítky oddělte čárkou:', - 'PHOTO_NEW_TAGS_1' => 'Zadejte štítky pro všechny', - 'PHOTO_NEW_TAGS_2' => 'vybrané fotografie. Stávající štítky budou přepsány. Jednotlivé štítky oddělte čárkou:', - 'PHOTO_SET_TAGS' => 'Uložit štítky', - 'PHOTO_CAMERA' => 'Fotoaparát', - 'PHOTO_CAPTURED' => 'Pořízeno', - 'PHOTO_MAKE' => 'Značka', - 'PHOTO_TYPE' => 'Typ/model', - 'PHOTO_LENS' => 'Objektiv', - 'PHOTO_SHUTTER' => 'Uzávěrka', - 'PHOTO_APERTURE' => 'Clona', - 'PHOTO_FOCAL' => 'Fokus', - 'PHOTO_ISO' => 'ISO', - 'PHOTO_SHARING' => 'Sdílet', - 'PHOTO_SHR_PLUBLIC' => 'Veřejné', - 'PHOTO_SHR_ALB' => 'Ano (Album)', - 'PHOTO_SHR_PHT' => 'Ano (Foto)', - 'PHOTO_SHR_NO' => 'Ne', - 'PHOTO_DELETE' => 'Odstranit fotografii', - 'PHOTO_KEEP' => 'Ponechat fotografii', - 'PHOTO_DELETE_1' => 'Opravdu odstranit fotografii', - 'PHOTO_DELETE_2' => '? Tento krok je nevratný!', - 'PHOTO_DELETE_ALL_1' => 'Opravdu odstranit všechny', - 'PHOTO_DELETE_ALL_2' => 'vybrané fotografie? Tento krok je nevratný!', - 'PHOTOS_NEW_TITLE_1' => 'Zadejte nový název pro všechny', - 'PHOTOS_NEW_TITLE_2' => 'vybrané fotografie:', - 'PHOTO_MAKE_PRIVATE_ALBUM' => 'Tato fotografie je umístěna ve veřejném albu. Fotografii jako veřejnou nebo soukromou musíte nastavit v albu, v nemž je umístěna.', - 'PHOTO_SHOW_ALBUM' => 'Zobrazit album', - 'PHOTO_PUBLIC' => 'Veřejné', - 'PHOTO_PUBLIC_EXPL' => 'Fotografie může být zobrazena ostatním s následujícím omezením.', - 'PHOTO_FULL' => 'Originál', - 'PHOTO_FULL_EXPL' => 'Plné rozlišení k dispozici.', - 'PHOTO_HIDDEN' => 'Skrytá', - 'PHOTO_HIDDEN_EXPL' => 'Fotografii mohou zobrazit jen návštěvnící z přímého odkazu.', - 'PHOTO_DOWNLOADABLE' => 'Stažitelná', - 'PHOTO_DOWNLOADABLE_EXPL' => 'Fotografii mohou stáhnout pouze návštěvnící alba.', - 'PHOTO_SHARE_BUTTON_VISIBLE' => 'Share button is visible', - 'PHOTO_SHARE_BUTTON_VISIBLE_EXPL' => 'Display social media sharing links.', - 'PHOTO_PASSWORD_PROT' => 'Zabezpečená heslem', - 'PHOTO_PASSWORD_PROT_EXPL' => 'Přístup k fotografii pouze s platným heslem.', - 'PHOTO_EDIT_SHARING_TEXT' => 'Vlastnosti sdílení fotografie budou změněny takto:', - 'PHOTO_NO_EDIT_SHARING_TEXT' => 'Protože je tato fotografie umístěna ve veřejném albu, zdědí i nastavení tohoto veřejného alba. Aktuální stav viditelnosti je uveden pouze pro informaci.', - 'PHOTO_EDIT_GLOBAL_SHARING_TEXT' => 'Viditelnost této fotografie lze doladit pomocí globálních nastavení. Aktuální stav viditelnosti je uveden pouze pro informaci.', - 'PHOTO_SHARING_CONFIRM' => 'Uložit', - - 'LOADING' => 'Probíhá příprava', - 'ERROR' => 'Chyba', - 'ERROR_TEXT' => 'Něco není v pořádku. Obnovte stránku a postup zopakujte!', - 'ERROR_DB_1' => 'Nelze se připojit k databázi, přístup byl odmítnut. Zkontrolujte hostitele, uživatelské jméno a heslo a ověřte si, že máte povolen přístup k databázi ze současné lokality.', - 'ERROR_DB_2' => 'Nelze vytvořit databázi. Zkontrolujte hostitele, uživatelské jméno a heslo a ověřte si, že máte práva ke zápisu/změnám obsahu databáze.', - 'ERROR_CONFIG_FILE' => "Konfiguraci nelze uložit. Přístup odmítnut v 'data/'. Nastavte správně RWE práva pro ostatní v 'data/' a 'uploads/'. Další informace jsou k dispozici v souboru README.", - 'ERROR_UNKNOWN' => 'Neočekávaná chyba. Postup prosím opakujte a ujistěte se o správnosti instalace na serveru. Další informace jsou k dispozici v souboru README.', - 'ERROR_LOGIN' => 'Nelze uložit přihlašovací informace. Opakujte prosím postup s jiným uřivatelským jménem a heslem!', - 'ERROR_MAP_DEACTIVATED' => 'Funkce Mapy byla v nastavení deaktivována.', - 'ERROR_SEARCH_DEACTIVATED' => 'Funkce hledání byla v nastavení deaktivována.', - 'SUCCESS' => 'OK', - 'RETRY' => 'Opakovat', - - 'SETTINGS_WARNING' => 'Změna rozšířených nastavení může mít negativní vliv na stabilitu, bezpečnost a rychlost Lychee. Měňte pouze to, co opravdu dobře chápete.', - 'SETTINGS_SUCCESS_LOGIN' => 'Přihlašovací údaje byly aktualizovány.', - 'SETTINGS_SUCCESS_SORT' => 'Stav řazení byl aktulizován.', - 'SETTINGS_SUCCESS_DROPBOX' => 'Dropbox Key byl aktualizován.', - 'SETTINGS_SUCCESS_LANG' => 'Jazyk byl aktualizován', - 'SETTINGS_SUCCESS_LAYOUT' => 'Vzhled byl aktualizován', - 'SETTINGS_SUCCESS_IMAGE_OVERLAY' => 'EXIF překryv byl aktulizován', - 'SETTINGS_SUCCESS_PUBLIC_SEARCH' => 'Veřejné vyhledávání bylo aktulizováno', - 'SETTINGS_SUCCESS_LICENSE' => 'Výchozí licence byla aktualizována', - 'SETTINGS_SUCCESS_CSS' => 'CSS aktualizováno', - 'SETTINGS_SUCCESS_UPDATE' => 'Nastavení úspešně aktualizováno', - 'SETTINGS_SUCCESS_MAP_DISPLAY' => 'Nastavení zobrazeni Map bylo aktualizováno', - 'SETTINGS_SUCCESS_MAP_DISPLAY_PUBLIC' => 'Nastavení zobrazeni Map pro veřejná alba bylo aktualizováno', - 'SETTINGS_SUCCESS_MAP_PROVIDER' => 'Poskytovatel Map byl aktualizován', - - 'U2F_NOT_SUPPORTED' => 'U2F not supported. Sorry.', - 'U2F_NOT_SECURE' => 'Environment not secured. U2F not available.', - 'U2F_REGISTER_KEY' => 'Register new device.', - 'U2F_REGISTRATION_SUCCESS' => 'Registration successful!', - 'U2F_AUTHENTIFICATION_SUCCESS' => 'Authentication successful!', - 'U2F_CREDENTIALS' => 'Credentials', - 'U2F_CREDENTIALS_DELETED' => 'Credentials deleted!', - - 'NEW_PHOTOS_NOTIFICATION' => 'Send new photos notification emails.', - 'SETTINGS_SUCCESS_NEW_PHOTOS_NOTIFICATION' => 'New photos notification updated', - 'USER_EMAIL_INSTRUCTION' => 'Add your email below to enable receiving email notifications.
To stop receiving emails, simply remove your email below.', - - 'DB_INFO_TITLE' => 'Níže zadejte parametry připojení k databázi:', - 'DB_INFO_HOST' => 'Hostitel (volitelný)', - 'DB_INFO_USER' => 'Jméno uživatele databáze', - 'DB_INFO_PASSWORD' => 'Heslo uživatele databáze', - 'DB_INFO_TEXT' => 'Lychee vytvoří svou vlastní databázi. Pokud již databáze existuje, zadejte její název:', - 'DB_NAME' => 'Název databáze (volitelné)', - 'DB_PREFIX' => 'Prefix tabulek (volitelné)', - 'DB_CONNECT' => 'Připojit', - - 'LOGIN_TITLE' => 'Zadejte jméno uživatele a heslo pro svoji instalaci:', - 'LOGIN_USERNAME' => 'Jméno uživatele', - 'LOGIN_PASSWORD' => 'Heslo', - 'LOGIN_PASSWORD_CONFIRM' => 'Zopakujte heslo', - 'LOGIN_CREATE' => 'Vytvořit uživatele', - - 'PASSWORD_TITLE' => 'Zadejte aktuální heslo:', - 'USERNAME_CURRENT' => 'Aktuální uživatelské jméno', - 'PASSWORD_CURRENT' => 'Aktuální heslo', - 'PASSWORD_TEXT' => 'Vaše uživatelské jméno a heslo budou změněny následovně:', - 'PASSWORD_CHANGE' => 'Změnit přihlášení', - - 'EDIT_SHARING_TITLE' => 'Editace sdílení', - 'EDIT_SHARING_TEXT' => 'Vlastnosti sdílení tohoto alba budou změněny následovně:', - 'SHARE_ALBUM_TEXT' => 'Album bude sdíleno s následujícími parametry:', - 'ALBUM_SHARING_CONFIRM' => 'Uložit', - - 'SORT_ALBUM_BY_1' => 'Řadit alba podle', - 'SORT_ALBUM_BY_2' => 've', - 'SORT_ALBUM_BY_3' => 'pořadí.', - - 'SORT_ALBUM_SELECT_1' => 'Data vytvoření', - 'SORT_ALBUM_SELECT_2' => 'Názvu', - 'SORT_ALBUM_SELECT_3' => 'Popisu', - 'SORT_ALBUM_SELECT_4' => 'Stavu zveřejnění', - 'SORT_ALBUM_SELECT_5' => 'Nejmladšího data snímku', - 'SORT_ALBUM_SELECT_6' => 'Nejstaršího data snímku', - - 'SORT_PHOTO_BY_1' => 'Řadit alba podle', - 'SORT_PHOTO_BY_2' => 've', - 'SORT_PHOTO_BY_3' => 'pořadí.', - - 'SORT_PHOTO_SELECT_1' => 'Data uložení', - 'SORT_PHOTO_SELECT_2' => 'Data záznamu', - 'SORT_PHOTO_SELECT_3' => 'Názvu', - 'SORT_PHOTO_SELECT_4' => 'Popisu', - 'SORT_PHOTO_SELECT_5' => 'Stavu zveřejnění', - 'SORT_PHOTO_SELECT_6' => 'Oblíbenosti', - 'SORT_PHOTO_SELECT_7' => 'Formátu', - - 'SORT_ASCENDING' => 'Vzestupném', - 'SORT_DESCENDING' => 'Sestupném', - 'SORT_CHANGE' => 'Změnit řazení', - - 'DROPBOX_TITLE' => 'Dropbox - nastavení', - 'DROPBOX_TEXT' => "Pro uspěšný import fotografií z Dropboxu je řeba platný API klíč, který lze získat na stránkách Dropboxu. Vygenerovaný osobní klíč zadejte níže:", - - 'LANG_TEXT' => 'Změnit jazyk Lychee na:', - 'LANG_TITLE' => 'Změnit jazyk', - - 'CSS_TEXT' => 'Vlastní CSS:', - 'CSS_TITLE' => 'Změnit CSS', - 'PUBLIC_SEARCH_TEXT' => 'Veřejné vyhledávání povoleno:', - 'OVERLAY_TYPE' => 'Data, která budou použita na překryvu:', - 'OVERLAY_NONE' => 'None', - 'OVERLAY_EXIF' => 'EXIF data', - 'OVERLAY_DESCRIPTION' => 'Popis', - 'OVERLAY_DATE' => 'Datum pořízení', - 'MAP_DISPLAY_TEXT' => 'Povolit Mapy (poskytovatel OpenStreetMap):', - 'MAP_DISPLAY_PUBLIC_TEXT' => 'Povolit mapy pro veřejná alba (poskytovatel OpenStreetMap):', - 'MAP_PROVIDER' => 'Poskytovatel OpenStreetMap názvů:', - 'MAP_PROVIDER_WIKIMEDIA' => 'Wikimedia', - 'MAP_PROVIDER_OSM_ORG' => 'OpenStreetMap.org (bez HiDPI)', - 'MAP_PROVIDER_OSM_DE' => 'OpenStreetMap.de (bez HiDPI)', - 'MAP_PROVIDER_OSM_FR' => 'OpenStreetMap.fr (bez HiDPI)', - 'MAP_PROVIDER_RRZE' => 'Universita v Erlangenu, Německo (pouze HiDPI)', - 'MAP_INCLUDE_SUBALBUMS_TEXT' => 'Včetně fotografií v subalbech:', - 'LOCATION_DECODING' => 'Přeložít GPS data na název místa', - 'LOCATION_SHOW' => 'Zobrazit název místa', - 'LOCATION_SHOW_PUBLIC' => 'Zobrazit název místa v public módu', - 'LAYOUT_TYPE' => 'Vzhled fotografií:', - 'LAYOUT_SQUARES' => 'Čtvercové náhledy', - 'LAYOUT_JUSTIFIED' => 'V poměru stran, zarovnáno', - 'LAYOUT_UNJUSTIFIED' => 'V poměru stran, nezarovnáno', - 'SET_LAYOUT' => 'Změnit vzhled', - - 'NSFW_VISIBLE_TEXT_1' => 'Make Sensitive albums visible by default.', - 'NSFW_VISIBLE_TEXT_2' => 'If the album is public, it is still accessible, just hidden from the view and can be revealed by pressing H.', - 'SETTINGS_SUCCESS_NSFW_VISIBLE' => 'Default sensitive album visibility updated with success.', - - 'VIEW_NO_RESULT' => 'Bez výsledku', - 'VIEW_NO_PUBLIC_ALBUMS' => 'Veřejná alba nejsou k dispozici', - 'VIEW_NO_CONFIGURATION' => 'Žádná konfigurace', - 'VIEW_PHOTO_NOT_FOUND' => 'Fotografie nenalezena', - - 'NO_TAGS' => 'Žádné štítky', - - 'UPLOAD_MANAGE_NEW_PHOTOS' => 'Nyní můžete spravovat nové nové obrázky.', - 'UPLOAD_COMPLETE' => 'Upload dokončen', - 'UPLOAD_COMPLETE_FAILED' => 'Chyba při uploadu jedné nebo více fotografií.', - 'UPLOAD_IMPORTING' => 'Import', - 'UPLOAD_IMPORTING_URL' => 'URL pro import', - 'UPLOAD_UPLOADING' => 'Probíhá upload', - 'UPLOAD_FINISHED' => 'Dokončeno', - 'UPLOAD_PROCESSING' => 'Zpracovává se', - 'UPLOAD_FAILED' => 'Selhání', - 'UPLOAD_FAILED_ERROR' => 'Upload selhal. Server vrátil chybu!', - 'UPLOAD_FAILED_WARNING' => 'Upload selhal. Server vrátil upozornění!', - 'UPLOAD_CANCELLED' => 'Cancelled', - 'UPLOAD_SKIPPED' => 'Vynecháno', - 'UPLOAD_UPDATED' => 'Updated', - 'UPLOAD_IMPORT_SKIPPED_DUPLICATE' => 'This photo has been skipped because it\'s already in your library.', - 'UPLOAD_IMPORT_RESYNCED_DUPLICATE' => 'This photo has been skipped because it\'s already in your library, but its metadata has been updated.', - 'UPLOAD_ERROR_CONSOLE' => 'Podrobnosti získáte v konzoli svého prohlížeče.', - 'UPLOAD_UNKNOWN' => 'Server vrátil neočkávanou dopověď. Podrobnosti získáte v konzoli svého prohlížeče.', - 'UPLOAD_ERROR_UNKNOWN' => 'Upload selhal. Server vrátil neznámou chybu!', - 'UPLOAD_ERROR_POSTSIZE' => 'Upload failed. The PHP post_max_size may be too small! Otherwise check the FAQ.', - 'UPLOAD_ERROR_FILESIZE' => 'Upload failed. The PHP upload_max_filesize may be too small! Otherwise check the FAQ.', - 'UPLOAD_IN_PROGRESS' => 'Právě probíhá upload na Lychee!', - 'UPLOAD_IMPORT_WARN_ERR' => 'Import byl dokončen s upozorněními nebo chybami. Podrobnosti si prosím prohlédněte v protokolu (Nastavení -> Protokoly).', - 'UPLOAD_IMPORT_COMPLETE' => 'Import dokončen', - 'UPLOAD_IMPORT_INSTR' => 'Zadejte prosím přímý odkaz k fotografii, která má být naimportována:', - 'UPLOAD_IMPORT' => 'Importovat', - 'UPLOAD_IMPORT_SERVER' => 'Import ze serveru', - 'UPLOAD_IMPORT_SERVER_FOLD' => 'Složka je prázdná nebo neobsahuje soubory, které lze zpracovat. Podrobnosti si prosím prohlédněte v protokolu (Nastavení -> Protokoly).', - 'UPLOAD_IMPORT_SERVER_INSTR' => 'Tato akce importuje všechny fotografie, včetně složek a podsložek, které jsou v uvedeném umístění k dispozici.', - 'UPLOAD_ABSOLUTE_PATH' => 'Absolutní cesta ke složce', - 'UPLOAD_IMPORT_SERVER_EMPT' => 'Import neproběhl, protože složka je prázdná!', - 'UPLOAD_IMPORT_DELETE_ORIGINALS' => 'Odstranit původní soubory', - 'UPLOAD_IMPORT_DELETE_ORIGINALS_EXPL' => 'Pokud to bude možné, původní soubory budou po importu odstraněny.', - 'UPLOAD_IMPORT_VIA_SYMLINK' => 'Symbolic links', - 'UPLOAD_IMPORT_VIA_SYMLINK_EXPL' => 'Import files using symbolic links to originals.', - 'UPLOAD_IMPORT_SKIP_DUPLICATES' => 'Skip duplicates', - 'UPLOAD_IMPORT_SKIP_DUPLICATES_EXPL' => 'Existing media files are skipped.', - 'UPLOAD_IMPORT_RESYNC_METADATA' => 'Re-sync metadata', - 'UPLOAD_IMPORT_RESYNC_METADATA_EXPL' => 'Update metadata of existing media files.', - 'UPLOAD_IMPORT_LOW_MEMORY' => 'Není dostatek paměti!', - 'UPLOAD_IMPORT_LOW_MEMORY_EXPL' => 'Proces importu alokuje příliš mnoho paměti serveru a může být tedy neočekávaně přerušen.', - 'UPLOAD_WARNING' => 'Upozornění', - 'UPLOAD_IMPORT_NOT_A_DIRECTORY' => 'Uvedená cesta není čitelnou složkou!', - 'UPLOAD_IMPORT_PATH_RESERVED' => 'Uvedená cesta je rezervována pro Lychee!', - 'UPLOAD_IMPORT_UNREADABLE' => 'Soubor nelze přečíst!', - 'UPLOAD_IMPORT_FAILED' => 'Soubor nelze importovat!', - 'UPLOAD_IMPORT_UNSUPPORTED' => 'Nepodporovaný typ souboru!', - 'UPLOAD_IMPORT_ALBUM_FAILED' => 'Album nelze vytvořit!', - 'UPLOAD_IMPORT_CANCELLED' => 'Import cancelled', - - 'ABOUT_SUBTITLE' => 'Ideální řešení vlastního hostování a správy fotografií', - 'ABOUT_DESCRIPTION' => 'je open-source nástroj na správu fotogragfií na Vašem serveru nebo webu. Instalace je hotova dílem okmažiku. Upload, správa a sdílení fotografií se provádí běžnými aplikacemi. Lychee přináší vše, co je třeba pro bezpečné online uložení Vašich fotografií.', - 'FOOTER_COPYRIGHT' => 'Všechny fotografie na tomto webu jsou ve vlastnictví Copyright ', - 'HOSTED_WITH_LYCHEE' => 'Hostováno na Lychee', - - 'URL_COPY_TO_CLIPBOARD' => 'Kopírovat do schránky', - 'URL_COPIED_TO_CLIPBOARD' => 'URL zkopírována do schránky!', - 'PHOTO_DIRECT_LINKS_TO_IMAGES' => 'Přímý odkaz k souborům:', - 'PHOTO_MEDIUM' => 'Střední', - 'PHOTO_MEDIUM_HIDPI' => 'Střední HiDPI', - 'PHOTO_SMALL' => 'Náhled', - 'PHOTO_SMALL_HIDPI' => 'Náhled HiDPI', - 'PHOTO_THUMB' => 'Čtvercový náhled', - 'PHOTO_THUMB_HIDPI' => 'Čtvercový náhled HiDPI', - 'PHOTO_LIVE_VIDEO' => 'Video part of live-photo', - 'PHOTO_VIEW' => 'Zobrazení foto Lychee:', - - 'PHOTO_EDIT_ROTATECWISE' => 'Otočit doprava', - 'PHOTO_EDIT_ROTATECCWISE' => 'Otočit doleva', - ]; - - return $locale; - } -} diff --git a/app/Locale/Dutch.php b/app/Locale/Dutch.php deleted file mode 100644 index 9de60127c27..00000000000 --- a/app/Locale/Dutch.php +++ /dev/null @@ -1,475 +0,0 @@ - 'gebruikersnaam', - 'PASSWORD' => 'wachtwoord', - 'ENTER' => 'Enter', - 'CANCEL' => 'Annuleer', - 'SIGN_IN' => 'Log in', - 'CLOSE' => 'Sluit', - 'SETTINGS' => 'Settings', - 'SEARCH' => 'Search ...', - 'MORE' => 'More', - 'DEFAULT' => 'Default', - - 'USERS' => 'Users', - 'U2F' => 'U2F', - 'NOTIFICATIONS' => 'Notifications', - 'SHARING' => 'Sharing', - 'CHANGE_LOGIN' => 'Verander Login', - 'CHANGE_SORTING' => 'Verander Sortering', - 'SET_DROPBOX' => 'Set Dropbox', - 'ABOUT_LYCHEE' => 'Over Lychee', - 'DIAGNOSTICS' => 'Diagnostics', - 'DIAGNOSTICS_GET_SIZE' => 'Request space usage', - 'LOGS' => 'Laat logs zien', - 'SIGN_OUT' => 'Log uit', - 'UPDATE_AVAILABLE' => 'Update beschikbaar!', - 'MIGRATION_AVAILABLE' => 'Migration available!', - 'DEFAULT_LICENSE' => 'Default license for new uploads:', - 'SET_LICENSE' => 'Set License', - 'SET_OVERLAY_TYPE' => 'Set Overlay', - 'SET_MAP_PROVIDER' => 'Set OpenStreetMap tiles provider', - - 'SMART_ALBUMS' => 'Slimme albums', - 'SHARED_ALBUMS' => 'Shared albums', - 'ALBUMS' => 'Albums', - 'PHOTOS' => 'Pictures', - 'SEARCH_RESULTS' => 'Search results', - - 'RENAME' => 'Hernoem', - 'RENAME_ALL' => 'Geselecteerde Hernoem', - 'MERGE' => 'Voeg samen', - 'MERGE_ALL' => 'Geselecteerd samenvoegen', - 'MAKE_PUBLIC' => 'Maak Publiek', - 'SHARE_ALBUM' => 'Deel Album', - 'SHARE_PHOTO' => 'Deel Photo', - 'VISIBILITY_ALBUM' => 'Album Visibility', - 'VISIBILITY_PHOTO' => 'Photo Visibility', - 'DOWNLOAD_ALBUM' => 'Download Album', - 'ABOUT_ALBUM' => 'Over Album', - 'DELETE_ALBUM' => 'Verwijder Album', - 'MOVE_ALBUM' => 'Move Album', - 'FULLSCREEN_ENTER' => 'Enter Fullscreen', - 'FULLSCREEN_EXIT' => 'Exit Fullscreen', - - 'SHARING_ALBUM_USERS' => 'Share this album with users', - 'WAIT_FETCH_DATA' => 'Please wait while we get the data...', - 'SHARING_ALBUM_USERS_NO_USERS' => 'There are no users to share the album with', - 'SHARING_ALBUM_USERS_LONG_MESSAGE' => 'Select the users to share this album with', - - 'DELETE_ALBUM_QUESTION' => 'Verwijder Album en Foto\'s', - 'KEEP_ALBUM' => 'Behoud Album', - 'DELETE_ALBUM_CONFIRMATION_1' => 'Weet je zeker dat je dit album en alle foto\'s die het', - 'DELETE_ALBUM_CONFIRMATION_2' => 'bevat wilt verwijderen? Deze actie kan niet ongedaan gemaakt worden!', - - 'DELETE_ALBUMS_QUESTION' => 'Verwijder Albums en Foto\'s', - 'KEEP_ALBUMS' => 'Behoud Albums', - 'DELETE_ALBUMS_CONFIRMATION_1' => 'Weet je zeker dat je deze albums en alle foto\'s die ze', - 'DELETE_ALBUMS_CONFIRMATION_2' => 'bevatten wilt verwijderen? Deze actie kan niet ongedaan gemaakt worden!', - - 'DELETE_UNSORTED_CONFIRM' => 'Weet je zeker dat je alle foto\'s van \'Ongesoorteerd\' wilt verwijdren?
Deze actie kan niet ongedaan gemaakt worden!', - 'CLEAR_UNSORTED' => 'Wis Ongesoorteerd', - 'KEEP_UNSORTED' => 'Behoud Ongesoorteerd', - - 'EDIT_SHARING' => 'Bewerk delen', - 'MAKE_PRIVATE' => 'Maak privé', - - 'CLOSE_ALBUM' => 'Sluit Album', - 'CLOSE_PHOTO' => 'Sluit Foto', - 'CLOSE_MAP' => 'Close Map', - - 'ADD' => 'Voeg toe', - 'MOVE' => 'Verplaats', - 'MOVE_ALL' => 'Verplaatsen Geselecteerd', - 'DUPLICATE' => 'Dupliceer', - 'DUPLICATE_ALL' => 'Duplicaat Geselecteerd', - 'COPY_TO' => 'Copy to...', - 'COPY_ALL_TO' => 'Kopiëren geselecteerd om....', - 'DELETE' => 'Verwijder', - 'DELETE_ALL' => 'Geselecteerde verwijderen', - 'DOWNLOAD' => 'Download', - 'DOWNLOAD_ALL' => 'Download Geselecteerd', - 'UPLOAD_PHOTO' => 'Upload Foto', - 'IMPORT_LINK' => 'Importeer van Link', - 'IMPORT_DROPBOX' => 'Importeer van Dropbox', - 'IMPORT_SERVER' => 'Importeer van Server', - 'NEW_ALBUM' => 'Nieuw Album', - 'NEW_TAG_ALBUM' => 'New Tag Album', - - 'TITLE_NEW_ALBUM' => 'Voer een titel voor het album in:', - 'UNTITLED' => 'Ongetiteld', - 'UNSORTED' => 'Ongesoorteerd', - 'STARRED' => 'Met ster', - 'RECENT' => 'Recentelijk', - 'PUBLIC' => 'Publiekelijk', - 'NUM_PHOTOS' => 'Foto\'s', - - 'CREATE_ALBUM' => 'Maak Album', - 'CREATE_TAG_ALBUM' => 'Create Tag Album', - - 'STAR_PHOTO' => 'Markeer met ster', - 'STAR' => 'Ster', - 'STAR_ALL' => 'Markeer geselecteerd als favorieten', - 'TAGS' => 'Tags', - 'TAGS_ALL' => 'Geselecteerde tags', - 'UNSTAR_PHOTO' => 'Verwijder ster markeering', - 'SET_COVER' => 'Set Album Cover', - 'REMOVE_COVER' => 'Remove Album Cover', - - 'FULL_PHOTO' => 'Open Original', - 'ABOUT_PHOTO' => 'Over Foto', - 'DISPLAY_FULL_MAP' => 'Map', - 'DIRECT_LINK' => 'Directe Link', - 'DIRECT_LINKS' => 'Directe Links', - - 'ALBUM_ABOUT' => 'Over', - 'ALBUM_BASICS' => 'Basics', - 'ALBUM_TITLE' => 'Titel', - 'ALBUM_NEW_TITLE' => 'Geef dit album een nieuwe titel:', - 'ALBUMS_NEW_TITLE_1' => 'Geef alle geselecteerde', - 'ALBUMS_NEW_TITLE_2' => 'albums een nieuwe titel:', - 'ALBUM_SET_TITLE' => 'Sla Titel op', - 'ALBUM_DESCRIPTION' => 'Onderwerk', - 'ALBUM_SHOW_TAGS' => 'Tags to show', - 'ALBUM_NEW_DESCRIPTION' => 'Geef een nieuwe omschrijving in:', - 'ALBUM_SET_DESCRIPTION' => 'Sla Omschrijving op', - 'ALBUM_NEW_SHOWTAGS' => 'Enter tags of photos that will be visible in this album:', - 'ALBUM_SET_SHOWTAGS' => 'Set tags to show', - 'ALBUM_ALBUM' => 'Album', - 'ALBUM_CREATED' => 'Aangemaakt', - 'ALBUM_IMAGES' => 'Afbeeldingen', - 'ALBUM_VIDEOS' => 'Videos', - 'ALBUM_SUBALBUMS' => 'Subalbums', - 'ALBUM_SHARING' => 'Deel', - 'ALBUM_SHR_YES' => 'Ja', - 'ALBUM_SHR_NO' => 'Nee', - 'ALBUM_PUBLIC' => 'Publiekelijk', - 'ALBUM_PUBLIC_EXPL' => 'Album can be viewed by others, subject to the restrictions below.', - 'ALBUM_FULL' => 'Original', - 'ALBUM_FULL_EXPL' => 'Full-resolution pictures are available.', - 'ALBUM_HIDDEN' => 'Verborgen', - 'ALBUM_HIDDEN_EXPL' => 'Alleen mensen met een link kunnen dit album bekjiken.', - 'ALBUM_MARK_NSFW' => 'Mark album as sensitive', - 'ALBUM_UNMARK_NSFW' => 'Unmark album as sensitive', - 'ALBUM_NSFW' => 'Sensitive', - 'ALBUM_NSFW_EXPL' => 'Album is marked to contain sensitive content.', - 'ALBUM_DOWNLOADABLE' => 'Downloadbaar', - 'ALBUM_DOWNLOADABLE_EXPL' => 'Bezoekers van jouw Lychee kunnen dit album downloaden.', - 'ALBUM_SHARE_BUTTON_VISIBLE' => 'Share button is visible', - 'ALBUM_SHARE_BUTTON_VISIBLE_EXPL' => 'Display social media sharing links.', - 'ALBUM_PASSWORD' => 'Wachtwoord', - 'ALBUM_PASSWORD_PROT' => 'Met wachtwoord beschermt', - 'ALBUM_PASSWORD_PROT_EXPL' => 'Album alleen beschikbaar met een geldig wachtwoord.', - 'ALBUM_PASSWORD_REQUIRED' => 'Dit album is met een wachtwoord beschermt, voer het wachtwoord in:', - 'ALBUM_MERGE_1' => 'Weet je zeker dat je dit album wilt samenvoegen', - 'ALBUM_MERGE_2' => 'met het album', - 'ALBUMS_MERGE' => 'Weet je zeker dat je alle albums wilt samenvoegen naar', - 'MERGE_ALBUM' => 'Voeg albums samen', - 'DONT_MERGE' => 'Voeg niet samen', - 'ALBUM_MOVE_1' => 'Are you sure you want to move the album', - 'ALBUM_MOVE_2' => 'into the album', - 'ALBUMS_MOVE' => 'Are you sure you want to move all selected albums into the album', - 'MOVE_ALBUMS' => 'Move Albums', - 'NOT_MOVE_ALBUMS' => "Don't Move", - 'ROOT' => 'Albums', - 'ALBUM_REUSE' => 'Reuse', - 'ALBUM_LICENSE' => 'License', - 'ALBUM_SET_LICENSE' => 'Set License', - 'ALBUM_LICENSE_HELP' => 'Need help choosing?', - 'ALBUM_LICENSE_NONE' => 'None', - 'ALBUM_RESERVED' => 'All Rights Reserved', - 'ALBUM_SET_ORDER' => 'Set Order', - 'ALBUM_ORDERING' => 'Order by', - - 'PHOTO_ABOUT' => 'Over', - 'PHOTO_BASICS' => 'Basics', - 'PHOTO_TITLE' => 'Titel', - 'PHOTO_NEW_TITLE' => 'Geef deze foto een nieuwe titel:', - 'PHOTO_SET_TITLE' => 'Sla Titel op', - 'PHOTO_UPLOADED' => 'Geupload', - 'PHOTO_DESCRIPTION' => 'Omschrijving', - 'PHOTO_NEW_DESCRIPTION' => 'Geef deze foto een nieuwe omschrijving:', - 'PHOTO_SET_DESCRIPTION' => 'Sla omschrijving op', - 'PHOTO_NEW_LICENSE' => 'Add a License', - 'PHOTO_SET_LICENSE' => 'Set License', - 'PHOTO_LICENSE' => 'License', - 'PHOTO_REUSE' => 'Reuse', - 'PHOTO_LICENSE_NONE' => 'None', - 'PHOTO_RESERVED' => 'All Rights Reserved', - 'PHOTO_LATITUDE' => 'Latitude', - 'PHOTO_LONGITUDE' => 'Longitude', - 'PHOTO_ALTITUDE' => 'Altitude', - 'PHOTO_IMGDIRECTION' => 'Direction', - 'PHOTO_LOCATION' => 'Location', - 'PHOTO_IMAGE' => 'Afbeelding', - 'PHOTO_VIDEO' => 'Video', - 'PHOTO_SIZE' => 'Grootte', - 'PHOTO_FORMAT' => 'Formaat', - 'PHOTO_RESOLUTION' => 'Resolutie', - 'PHOTO_DURATION' => 'Duration', - 'PHOTO_FPS' => 'Frame rate', - 'PHOTO_TAGS' => 'Tags', - 'PHOTO_NOTAGS' => 'Geen Tags', - 'PHOTO_NEW_TAGS' => 'Voer je tags voor deze foto in, meerdere tags kunnen worden gescheiden door komma\'s:', - 'PHOTO_NEW_TAGS_1' => 'Voer je tags in voor alle', - 'PHOTO_NEW_TAGS_2' => 'geselecteerde foto\'s, meerdere tags kunnen worden gescheiden door komma\'s:', - 'PHOTO_SET_TAGS' => 'Sla Tags op', - 'PHOTO_CAMERA' => 'Camera', - 'PHOTO_CAPTURED' => 'Gefotografeerd', - 'PHOTO_MAKE' => 'Fabricant', - 'PHOTO_TYPE' => 'Type/Model', - 'PHOTO_LENS' => 'Lens', - 'PHOTO_SHUTTER' => 'Sluitertijd', - 'PHOTO_APERTURE' => 'Diafragma', - 'PHOTO_FOCAL' => 'Brandpuntafstand', - 'PHOTO_ISO' => 'ISO', - 'PHOTO_SHARING' => 'Deling', - 'PHOTO_SHR_PLUBLIC' => 'Publiekelijk', - 'PHOTO_SHR_ALB' => 'Ja (Album)', - 'PHOTO_SHR_PHT' => 'Ja (Foto)', - 'PHOTO_SHR_NO' => 'Nee', - 'PHOTO_DELETE' => 'Verwijder Foto', - 'PHOTO_KEEP' => 'Behoud Foto', - 'PHOTO_DELETE_1' => 'Weet je zeker dat je deze foto\'s wilt verwijderen?', - 'PHOTO_DELETE_2' => 'Deze actie kan niet ongedaan gemaakt worden!', - 'PHOTO_DELETE_ALL_1' => 'Weet je zeker dat je alle geslecteerd foto\'s wilt verwijderen?', - 'PHOTO_DELETE_ALL_2' => 'Deze actie kan niet ongedaan gemaakt worden!', - 'PHOTOS_NEW_TITLE_1' => 'Voer een titel in voor alle', - 'PHOTOS_NEW_TITLE_2' => 'geselecteerde foto\'s:', - 'PHOTO_MAKE_PRIVATE_ALBUM' => 'Deze foto bevind zich in een gedeeld album. Om de zichtbaarheid van deze foto te wijzigen pas je de zichtbaarheid van het album aan.', - 'PHOTO_SHOW_ALBUM' => 'Geef album weer', - 'PHOTO_PUBLIC' => 'Public', - 'PHOTO_PUBLIC_EXPL' => 'Photo can be viewed by others, subject to the restrictions below.', - 'PHOTO_FULL' => 'Original', - 'PHOTO_FULL_EXPL' => 'Full-resolution picture is available.', - 'PHOTO_HIDDEN' => 'Hidden', - 'PHOTO_HIDDEN_EXPL' => 'Only people with the direct link can view this photo.', - 'PHOTO_DOWNLOADABLE' => 'Downloadable', - 'PHOTO_DOWNLOADABLE_EXPL' => 'Visitors of your gallery can download this photo.', - 'PHOTO_SHARE_BUTTON_VISIBLE' => 'Share button is visible', - 'PHOTO_SHARE_BUTTON_VISIBLE_EXPL' => 'Display social media sharing links.', - 'PHOTO_PASSWORD_PROT' => 'Password protected', - 'PHOTO_PASSWORD_PROT_EXPL' => 'Photo only accessible with a valid password.', - 'PHOTO_EDIT_SHARING_TEXT' => 'The sharing properties of this photo will be changed to the following:', - 'PHOTO_NO_EDIT_SHARING_TEXT' => 'Because this photo is located in a public album, it inherits that album\'s visibility settings. Its current visibility is shown below for informational purposes only.', - 'PHOTO_EDIT_GLOBAL_SHARING_TEXT' => 'The visibility of this photo can be fine-tuned using global Lychee settings. Its current visibility is shown below for informational purposes only.', - 'PHOTO_SHARING_CONFIRM' => 'Save', - - 'LOADING' => 'Laden', - 'ERROR' => 'Error', - 'ERROR_TEXT' => 'Whoops, er is iets misgegaan. Herlaad de pagina en probeer het opnieuw!', - 'ERROR_DB_1' => 'Kan geen verbinding opzetten met de database. Controleer je host, gebruikersnaam en wachtwoord. Controleer ook of toegang vanaf je huidige locatie is toegestaan.', - 'ERROR_DB_2' => 'Kan geen database aanmaken. Unable to create the database. Controleer je host, gebruikersnaam en wachtwoord. Controleer of de gebruiker de database kan bewerken.', - 'ERROR_CONFIG_FILE' => "Kan configuatie niet opslaan. Toegant tot 'data/' geweigerd. Geef iedereen lees, schijf en uitvoer permissie op 'data/' en 'uploads/'. Kijk naar de readme voor meer informatie.", - 'ERROR_UNKNOWN' => 'Er is iets onverwachts gebeurd. Probeer het opnieuw of controleer je installatie en server. Kijk naar de readme voor meer informatie.', - 'ERROR_LOGIN' => 'Kan login niet opslaan. Probeer het opnieuw met een andere gebruikersnaam en/of wachtwoord!', - 'ERROR_MAP_DEACTIVATED' => 'Map functionality has been deactivated under settings.', - 'ERROR_SEARCH_DEACTIVATED' => 'Search functionality has been deactivated under settings.', - 'SUCCESS' => 'OK', - 'RETRY' => 'Probeer opnieuw', - - 'SETTINGS_SUCCESS_LOGIN' => 'Login Info updated.', - 'SETTINGS_SUCCESS_SORT' => 'Sorting order updated.', - 'SETTINGS_SUCCESS_DROPBOX' => 'Dropbox Key updated.', - 'SETTINGS_SUCCESS_LANG' => 'Language updated', - 'SETTINGS_SUCCESS_LAYOUT' => 'Layout updated', - 'SETTINGS_SUCCESS_IMAGE_OVERLAY' => 'EXIF Overlay setting updated', - 'SETTINGS_SUCCESS_PUBLIC_SEARCH' => 'Publieke zoekactie bijgewerkt', - 'SETTINGS_SUCCESS_LICENSE' => 'Default license updated', - 'SETTINGS_SUCCESS_MAP_DISPLAY' => 'Map display settings updated', - 'SETTINGS_SUCCESS_MAP_DISPLAY_PUBLIC' => 'Map display settings for public albums updated', - 'SETTINGS_SUCCESS_MAP_PROVIDER' => 'Map provider settings updated', - - 'U2F_NOT_SUPPORTED' => 'U2F not supported. Sorry.', - 'U2F_NOT_SECURE' => 'Environment not secured. U2F not available.', - 'U2F_REGISTER_KEY' => 'Register new device.', - 'U2F_REGISTRATION_SUCCESS' => 'Registration successful!', - 'U2F_AUTHENTIFICATION_SUCCESS' => 'Authentication successful!', - 'U2F_CREDENTIALS' => 'Credentials', - 'U2F_CREDENTIALS_DELETED' => 'Credentials deleted!', - - 'NEW_PHOTOS_NOTIFICATION' => 'Send new photos notification emails.', - 'SETTINGS_SUCCESS_NEW_PHOTOS_NOTIFICATION' => 'New photos notification updated', - 'USER_EMAIL_INSTRUCTION' => 'Add your email below to enable receiving email notifications.
To stop receiving emails, simply remove your email below.', - - 'DB_INFO_TITLE' => 'Voer je database connectie gegevens hieronder in:', - 'DB_INFO_HOST' => 'Database Host (optioneel)', - 'DB_INFO_USER' => 'Database Gebruikersnaam', - 'DB_INFO_PASSWORD' => 'Database Wachtwoord', - 'DB_INFO_TEXT' => 'Lychee maakt z\n eigen database. Indien gewenst kan je ook een bestaande database naam invoeren:', - 'DB_NAME' => 'Database Naam (optioneel)', - 'DB_PREFIX' => 'Tabel voorvoegsel (optioneel)', - 'DB_CONNECT' => 'Verbin', - - 'LOGIN_TITLE' => 'Voer een gebruikersnaam en wachtwoord in voor je installatie:', - 'LOGIN_USERNAME' => 'Nieuw Gebruikersnaam', - 'LOGIN_PASSWORD' => 'Nieuw Wachtwoord', - 'LOGIN_PASSWORD_CONFIRM' => 'Confirm Password', - 'LOGIN_CREATE' => 'Maak Login', - - 'PASSWORD_TITLE' => 'Voer je huidige wachtwoord in:', - 'USERNAME_CURRENT' => 'Current Username', - 'PASSWORD_CURRENT' => 'Huidig Wachtwoord', - 'PASSWORD_TEXT' => 'Je gebruikersnaam en wachtwoord worden verandert naar:', - 'PASSWORD_CHANGE' => 'Verander Login', - - 'EDIT_SHARING_TITLE' => 'Bewerk delen', - 'EDIT_SHARING_TEXT' => 'De deelinstellingen van dit album worden alsvolgt ingesteld:', - 'SHARE_ALBUM_TEXT' => 'Dit album wordt gedeeld met de volgende instellingen:', - 'ALBUM_SHARING_CONFIRM' => 'Save', - - 'SORT_ALBUM_BY_1' => 'Sorteer albums op', - 'SORT_ALBUM_BY_2' => 'in een', - 'SORT_ALBUM_BY_3' => 'volgorde.', - - 'SORT_ALBUM_SELECT_1' => 'Aangemaakt op', - 'SORT_ALBUM_SELECT_2' => 'Titel', - 'SORT_ALBUM_SELECT_3' => 'Omschrijving', - 'SORT_ALBUM_SELECT_4' => 'Publiekelijk', - 'SORT_ALBUM_SELECT_5' => 'Nieuwste foto datum', - 'SORT_ALBUM_SELECT_6' => 'Oudste foto datum', - - 'SORT_PHOTO_BY_1' => 'Sorteer albums op', - 'SORT_PHOTO_BY_2' => 'in een', - 'SORT_PHOTO_BY_3' => 'volgorde.', - - 'SORT_PHOTO_SELECT_1' => 'Upload Tijd', - 'SORT_PHOTO_SELECT_2' => 'Aangemaakt op', - 'SORT_PHOTO_SELECT_3' => 'Titel', - 'SORT_PHOTO_SELECT_4' => 'Omschrijving', - 'SORT_PHOTO_SELECT_5' => 'Publiekelijk', - 'SORT_PHOTO_SELECT_6' => 'Ster', - 'SORT_PHOTO_SELECT_7' => 'Foto formaat', - - 'SORT_ASCENDING' => 'Oplopende', - 'SORT_DESCENDING' => 'Aflopende', - 'SORT_CHANGE' => 'Change Sorting', - - 'DROPBOX_TITLE' => 'Stel Dropbox sleutel in', - 'DROPBOX_TEXT' => "Om foto\'s vanuit Dropbox te kunnen importeren moet je een geldige drop-ins app sleutel hebben van hun website. Genereer een sleutel en voer die in:", - - 'LANG_TEXT' => 'Change Lychee language for:', - 'LANG_TITLE' => 'Change Language', - 'PUBLIC_SEARCH_TEXT' => 'Openbare zoekactie toegestaan:', - 'OVERLAY_TYPE' => 'Photo overlay:', - 'OVERLAY_NONE' => 'None', - 'OVERLAY_EXIF' => 'EXIF data', - 'OVERLAY_DESCRIPTION' => 'Description', - 'OVERLAY_DATE' => 'Date taken', - 'MAP_DISPLAY_TEXT' => 'Enable maps (provided by OpenStreetMap):', - 'MAP_DISPLAY_PUBLIC_TEXT' => 'Enable maps for public albums (provided by OpenStreetMap):', - 'MAP_PROVIDER' => 'Provider of OpenStreetMap tiles:', - 'MAP_PROVIDER_WIKIMEDIA' => 'Wikimedia', - 'MAP_PROVIDER_OSM_ORG' => 'OpenStreetMap.org (no HiDPI)', - 'MAP_PROVIDER_OSM_DE' => 'OpenStreetMap.de (no HiDPI)', - 'MAP_PROVIDER_OSM_FR' => 'OpenStreetMap.fr (no HiDPI)', - 'MAP_PROVIDER_RRZE' => 'University of Erlangen, Germany (only HiDPI)', - 'MAP_INCLUDE_SUBALBUMS_TEXT' => 'Include photos of subalbums on map:', - 'LOCATION_DECODING' => 'Decode GPS data into location name', - 'LOCATION_SHOW' => 'Show location name', - 'LOCATION_SHOW_PUBLIC' => 'Show location name for public mode', - 'LAYOUT_TYPE' => 'Layout of photos:', - 'LAYOUT_SQUARES' => 'Square thumbnails', - 'LAYOUT_JUSTIFIED' => 'With aspect, justified', - 'LAYOUT_UNJUSTIFIED' => 'With aspect, unjustified', - 'SET_LAYOUT' => 'Change layout', - - 'NSFW_VISIBLE_TEXT_1' => 'Make Sensitive albums visible by default.', - 'NSFW_VISIBLE_TEXT_2' => 'If the album is public, it is still accessible, just hidden from the view and can be revealed by pressing H.', - 'SETTINGS_SUCCESS_NSFW_VISIBLE' => 'Default sensitive album visibility updated with success.', - - 'VIEW_NO_RESULT' => 'Geen resultaten', - 'VIEW_NO_PUBLIC_ALBUMS' => 'Geen publieke albums', - 'VIEW_NO_CONFIGURATION' => 'Geen configutatie', - 'VIEW_PHOTO_NOT_FOUND' => 'Foto niet gevonden', - - 'NO_TAGS' => 'Geen tags', - - 'UPLOAD_MANAGE_NEW_PHOTOS' => 'Je kan je nieuwe foto(\'s) nu beheren.', - 'UPLOAD_COMPLETE' => 'Upload voltooid', - 'UPLOAD_COMPLETE_FAILED' => 'Fout bij het uploaden van een of meerdere foto\'s.', - 'UPLOAD_IMPORTING' => 'Importeren', - 'UPLOAD_IMPORTING_URL' => 'Importeren van URL', - 'UPLOAD_UPLOADING' => 'Uploaden', - 'UPLOAD_FINISHED' => 'Afgerond', - 'UPLOAD_PROCESSING' => 'Verwerken', - 'UPLOAD_FAILED' => 'Mislukt', - 'UPLOAD_FAILED_ERROR' => 'Upload mislukt. Server gaf een error!', - 'UPLOAD_FAILED_WARNING' => 'Upload mislukt. Server gaf een waarschuwing!', - 'UPLOAD_CANCELLED' => 'Cancelled', - 'UPLOAD_SKIPPED' => 'Overgeslagen', - 'UPLOAD_UPDATED' => 'Updated', - 'UPLOAD_IMPORT_SKIPPED_DUPLICATE' => 'This photo has been skipped because it\'s already in your library.', - 'UPLOAD_IMPORT_RESYNCED_DUPLICATE' => 'This photo has been skipped because it\'s already in your library, but its metadata has been updated.', - 'UPLOAD_ERROR_CONSOLE' => 'Kijk naar je browsers console voor meer informatie.', - 'UPLOAD_UNKNOWN' => 'Server gaf een onbekende terugkoppeling, kijk naar je browsers console voor meer informatie.', - 'UPLOAD_ERROR_UNKNOWN' => 'Upload mislukt. Server gaf een onbekende error!', - 'UPLOAD_ERROR_POSTSIZE' => 'Upload failed. The PHP post_max_size may be too small! Otherwise check the FAQ.', - 'UPLOAD_ERROR_FILESIZE' => 'Upload failed. The PHP upload_max_filesize may be too small! Otherwise check the FAQ.', - 'UPLOAD_IN_PROGRESS' => 'Lychee is aan het uploaden!', - 'UPLOAD_IMPORT_WARN_ERR' => 'De import is voltooid maar gaf waarschuwingen of errors terug. Kijk naar de logs (instellingen -> Show Log) for further details.', - 'UPLOAD_IMPORT_COMPLETE' => 'Import complete', - 'UPLOAD_IMPORT_INSTR' => 'Please enter the direct link to a photo to import it:', - 'UPLOAD_IMPORT' => 'Import', - 'UPLOAD_IMPORT_SERVER' => 'Importing from server', - 'UPLOAD_IMPORT_SERVER_FOLD' => 'Folder empty or no readable files to process. Please take a look at the log (Settings -> Laat logs zien) voor meer informatie.', - 'UPLOAD_IMPORT_SERVER_INSTR' => 'Deze actie importeert alle foto\'s, folders en sub-folders vanuit de volgende folder.', - 'UPLOAD_ABSOLUTE_PATH' => 'Absoluut pad naar de folder', - 'UPLOAD_IMPORT_SERVER_EMPT' => 'Kan de import niet starten, folder is leeg!', - 'UPLOAD_IMPORT_DELETE_ORIGINALS' => 'Delete originals', - 'UPLOAD_IMPORT_DELETE_ORIGINALS_EXPL' => 'De orginele bestanden worden verwijderd na de import indien mogelijk.', - 'UPLOAD_IMPORT_VIA_SYMLINK' => 'Symbolic links', - 'UPLOAD_IMPORT_VIA_SYMLINK_EXPL' => 'Import files using symbolic links to originals.', - 'UPLOAD_IMPORT_SKIP_DUPLICATES' => 'Skip duplicates', - 'UPLOAD_IMPORT_SKIP_DUPLICATES_EXPL' => 'Existing media files are skipped.', - 'UPLOAD_IMPORT_RESYNC_METADATA' => 'Re-sync metadata', - 'UPLOAD_IMPORT_RESYNC_METADATA_EXPL' => 'Update metadata of existing media files.', - 'UPLOAD_IMPORT_LOW_MEMORY' => 'Low memory condition!', - 'UPLOAD_IMPORT_LOW_MEMORY_EXPL' => 'The import process on the server is approaching the memory limit and may end up being terminated prematurely.', - 'UPLOAD_WARNING' => 'Warning', - 'UPLOAD_IMPORT_NOT_A_DIRECTORY' => 'The given path is not a readable directory!', - 'UPLOAD_IMPORT_PATH_RESERVED' => 'The given path is a reserved path of Lychee!', - 'UPLOAD_IMPORT_UNREADABLE' => 'Could not read the file!', - 'UPLOAD_IMPORT_FAILED' => 'Could not import the file!', - 'UPLOAD_IMPORT_UNSUPPORTED' => 'Unsupported file type!', - 'UPLOAD_IMPORT_ALBUM_FAILED' => 'Could not create the album!', - 'UPLOAD_IMPORT_CANCELLED' => 'Import cancelled', - - 'ABOUT_SUBTITLE' => 'Self-hosted photo-management done right', - 'ABOUT_DESCRIPTION' => 'is a free photo-management tool, which runs on your server or web-space. Installing is a matter of seconds. Upload, manage and share photos like from a native application. Lychee comes with everything you need and all your photos are stored securely.', - 'FOOTER_COPYRIGHT' => 'Alle afbeeldingen op deze website zijn onderworpen aan het auteursrecht van', - 'HOSTED_WITH_LYCHEE' => 'Hosted with Lychee', - - 'URL_COPY_TO_CLIPBOARD' => 'Copy to clipboard', - 'URL_COPIED_TO_CLIPBOARD' => 'Copied URL to clipboard!', - 'PHOTO_DIRECT_LINKS_TO_IMAGES' => 'Direct links to image files:', - 'PHOTO_MEDIUM' => 'Medium', - 'PHOTO_MEDIUM_HIDPI' => 'Medium HiDPI', - 'PHOTO_SMALL' => 'Thumb', - 'PHOTO_SMALL_HIDPI' => 'Thumb HiDPI', - 'PHOTO_THUMB' => 'Square thumb', - 'PHOTO_THUMB_HIDPI' => 'Square thumb HiDPI', - 'PHOTO_LIVE_VIDEO' => 'Video part of live-photo', - 'PHOTO_VIEW' => 'Lychee Photo View:', - - 'PHOTO_EDIT_ROTATECWISE' => 'Rotate clockwise', - 'PHOTO_EDIT_ROTATECCWISE' => 'Rotate counter-clockwise', - ]; - - return $locale; - } -} diff --git a/app/Locale/English.php b/app/Locale/English.php deleted file mode 100644 index 03c18baf2e9..00000000000 --- a/app/Locale/English.php +++ /dev/null @@ -1,476 +0,0 @@ - 'username', - 'PASSWORD' => 'password', - 'ENTER' => 'Enter', - 'CANCEL' => 'Cancel', - 'SIGN_IN' => 'Sign In', - 'CLOSE' => 'Close', - 'SETTINGS' => 'Settings', - 'SEARCH' => 'Search ...', - 'MORE' => 'More', - 'DEFAULT' => 'Default', - - 'USERS' => 'Users', - 'U2F' => 'U2F', - 'NOTIFICATIONS' => 'Notifications', - 'SHARING' => 'Sharing', - 'CHANGE_LOGIN' => 'Change Login', - 'CHANGE_SORTING' => 'Change Sorting', - 'SET_DROPBOX' => 'Set Dropbox', - 'ABOUT_LYCHEE' => 'About Lychee', - 'DIAGNOSTICS' => 'Diagnostics', - 'DIAGNOSTICS_GET_SIZE' => 'Request space usage', - 'LOGS' => 'Show Logs', - 'SIGN_OUT' => 'Sign Out', - 'UPDATE_AVAILABLE' => 'Update available!', - 'MIGRATION_AVAILABLE' => 'Migration available!', - 'DEFAULT_LICENSE' => 'Default license for new uploads:', - 'SET_LICENSE' => 'Set License', - 'SET_OVERLAY_TYPE' => 'Set Overlay', - 'SET_MAP_PROVIDER' => 'Set OpenStreetMap tiles provider', - - 'SMART_ALBUMS' => 'Smart albums', - 'SHARED_ALBUMS' => 'Shared albums', - 'ALBUMS' => 'Albums', - 'PHOTOS' => 'Pictures', - 'SEARCH_RESULTS' => 'Search results', - - 'RENAME' => 'Rename', - 'RENAME_ALL' => 'Rename Selected', - 'MERGE' => 'Merge', - 'MERGE_ALL' => 'Merge Selected', - 'MAKE_PUBLIC' => 'Make Public', - 'SHARE_ALBUM' => 'Share Album', - 'SHARE_PHOTO' => 'Share Photo', - 'VISIBILITY_ALBUM' => 'Album Visibility', - 'VISIBILITY_PHOTO' => 'Photo Visibility', - 'DOWNLOAD_ALBUM' => 'Download Album', - 'ABOUT_ALBUM' => 'About Album', - 'DELETE_ALBUM' => 'Delete Album', - 'MOVE_ALBUM' => 'Move Album', - 'FULLSCREEN_ENTER' => 'Enter Fullscreen', - 'FULLSCREEN_EXIT' => 'Exit Fullscreen', - - 'SHARING_ALBUM_USERS' => 'Share this album with users', - 'WAIT_FETCH_DATA' => 'Please wait while we get the data...', - 'SHARING_ALBUM_USERS_NO_USERS' => 'There are no users to share the album with', - 'SHARING_ALBUM_USERS_LONG_MESSAGE' => 'Select the users to share this album with', - - 'DELETE_ALBUM_QUESTION' => 'Delete Album and Photos', - 'KEEP_ALBUM' => 'Keep Album', - 'DELETE_ALBUM_CONFIRMATION_1' => 'Are you sure you want to delete the album', - 'DELETE_ALBUM_CONFIRMATION_2' => 'and all of the photos it contains? This action can\'t be undone!', - - 'DELETE_ALBUMS_QUESTION' => 'Delete Albums and Photos', - 'KEEP_ALBUMS' => 'Keep Albums', - 'DELETE_ALBUMS_CONFIRMATION_1' => 'Are you sure you want to delete all', - 'DELETE_ALBUMS_CONFIRMATION_2' => 'selected albums and all of the photos they contain? This action can\'t be undone!', - - 'DELETE_UNSORTED_CONFIRM' => 'Are you sure you want to delete all photos from \'Unsorted\'?
This action can\'t be undone!', - 'CLEAR_UNSORTED' => 'Clear Unsorted', - 'KEEP_UNSORTED' => 'Keep Unsorted', - - 'EDIT_SHARING' => 'Edit Sharing', - 'MAKE_PRIVATE' => 'Make Private', - - 'CLOSE_ALBUM' => 'Close Album', - 'CLOSE_PHOTO' => 'Close Photo', - 'CLOSE_MAP' => 'Close Map', - - 'ADD' => 'Add', - 'MOVE' => 'Move', - 'MOVE_ALL' => 'Move Selected', - 'DUPLICATE' => 'Duplicate', - 'DUPLICATE_ALL' => 'Duplicate Selected', - 'COPY_TO' => 'Copy to...', - 'COPY_ALL_TO' => 'Copy Selected to...', - 'DELETE' => 'Delete', - 'DELETE_ALL' => 'Delete Selected', - 'DOWNLOAD' => 'Download', - 'DOWNLOAD_ALL' => 'Download Selected', - 'UPLOAD_PHOTO' => 'Upload Photo', - 'IMPORT_LINK' => 'Import from Link', - 'IMPORT_DROPBOX' => 'Import from Dropbox', - 'IMPORT_SERVER' => 'Import from Server', - 'NEW_ALBUM' => 'New Album', - 'NEW_TAG_ALBUM' => 'New Tag Album', - - 'TITLE_NEW_ALBUM' => 'Enter a title for the new album:', - 'UNTITLED' => 'Untilted', - 'UNSORTED' => 'Unsorted', - 'STARRED' => 'Starred', - 'RECENT' => 'Recent', - 'PUBLIC' => 'Public', - 'NUM_PHOTOS' => 'Photos', - - 'CREATE_ALBUM' => 'Create Album', - 'CREATE_TAG_ALBUM' => 'Create Tag Album', - - 'STAR_PHOTO' => 'Star Photo', - 'STAR' => 'Star', - 'STAR_ALL' => 'Star Selected', - 'TAGS' => 'Tag', - 'TAGS_ALL' => 'Tag Selected', - 'UNSTAR_PHOTO' => 'Unstar Photo', - 'SET_COVER' => 'Set Album Cover', - 'REMOVE_COVER' => 'Remove Album Cover', - - 'FULL_PHOTO' => 'Open Original', - 'ABOUT_PHOTO' => 'About Photo', - 'DISPLAY_FULL_MAP' => 'Map', - 'DIRECT_LINK' => 'Direct Link', - 'DIRECT_LINKS' => 'Direct Links', - - 'ALBUM_ABOUT' => 'About', - 'ALBUM_BASICS' => 'Basics', - 'ALBUM_TITLE' => 'Title', - 'ALBUM_NEW_TITLE' => 'Enter a new title for this album:', - 'ALBUMS_NEW_TITLE_1' => 'Enter a title for all', - 'ALBUMS_NEW_TITLE_2' => 'selected albums:', - 'ALBUM_SET_TITLE' => 'Set Title', - 'ALBUM_DESCRIPTION' => 'Description', - 'ALBUM_SHOW_TAGS' => 'Tags to show', - 'ALBUM_NEW_DESCRIPTION' => 'Enter a new description for this album:', - 'ALBUM_SET_DESCRIPTION' => 'Set Description', - 'ALBUM_NEW_SHOWTAGS' => 'Enter tags of photos that will be visible in this album:', - 'ALBUM_SET_SHOWTAGS' => 'Set tags to show', - 'ALBUM_ALBUM' => 'Album', - 'ALBUM_CREATED' => 'Created', - 'ALBUM_IMAGES' => 'Images', - 'ALBUM_VIDEOS' => 'Videos', - 'ALBUM_SUBALBUMS' => 'Subalbums', - 'ALBUM_SHARING' => 'Share', - 'ALBUM_SHR_YES' => 'YES', - 'ALBUM_SHR_NO' => 'No', - 'ALBUM_PUBLIC' => 'Public', - 'ALBUM_PUBLIC_EXPL' => 'Album can be viewed by others, subject to the restrictions below.', - 'ALBUM_FULL' => 'Original', - 'ALBUM_FULL_EXPL' => 'Full-resolution pictures are available.', - 'ALBUM_HIDDEN' => 'Hidden', - 'ALBUM_HIDDEN_EXPL' => 'Only people with the direct link can view this album.', - 'ALBUM_MARK_NSFW' => 'Mark album as sensitive', - 'ALBUM_UNMARK_NSFW' => 'Unmark album as sensitive', - 'ALBUM_NSFW' => 'Sensitive', - 'ALBUM_NSFW_EXPL' => 'Album is marked to contain sensitive content.', - 'ALBUM_DOWNLOADABLE' => 'Downloadable', - 'ALBUM_DOWNLOADABLE_EXPL' => 'Visitors of your gallery can download this album.', - 'ALBUM_SHARE_BUTTON_VISIBLE' => 'Share button is visible', - 'ALBUM_SHARE_BUTTON_VISIBLE_EXPL' => 'Display social media sharing links.', - 'ALBUM_PASSWORD' => 'Password', - 'ALBUM_PASSWORD_PROT' => 'Password protected', - 'ALBUM_PASSWORD_PROT_EXPL' => 'Album only accessible with a valid password.', - 'ALBUM_PASSWORD_REQUIRED' => 'This album is protected by a password. Enter the password below to view the photos of this album:', - 'ALBUM_MERGE_1' => 'Are you sure you want to merge the album', - 'ALBUM_MERGE_2' => 'into the album', - 'ALBUMS_MERGE' => 'Are you sure you want to merge all selected albums into the album', - 'MERGE_ALBUM' => 'Merge Albums', - 'DONT_MERGE' => "Don't Merge", - 'ALBUM_MOVE_1' => 'Are you sure you want to move the album', - 'ALBUM_MOVE_2' => 'into the album', - 'ALBUMS_MOVE' => 'Are you sure you want to move all selected albums into the album', - 'MOVE_ALBUMS' => 'Move Albums', - 'NOT_MOVE_ALBUMS' => "Don't Move", - 'ROOT' => 'Albums', - 'ALBUM_REUSE' => 'Reuse', - 'ALBUM_LICENSE' => 'License', - 'ALBUM_SET_LICENSE' => 'Set License', - 'ALBUM_LICENSE_HELP' => 'Need help choosing?', - 'ALBUM_LICENSE_NONE' => 'None', - 'ALBUM_RESERVED' => 'All Rights Reserved', - 'ALBUM_SET_ORDER' => 'Set Order', - 'ALBUM_ORDERING' => 'Order by', - - 'PHOTO_ABOUT' => 'About', - 'PHOTO_BASICS' => 'Basics', - 'PHOTO_TITLE' => 'Title', - 'PHOTO_NEW_TITLE' => 'Enter a new title for this photo:', - 'PHOTO_SET_TITLE' => 'Set Title', - 'PHOTO_UPLOADED' => 'Uploaded', - 'PHOTO_DESCRIPTION' => 'Description', - 'PHOTO_NEW_DESCRIPTION' => 'Enter a new description for this photo:', - 'PHOTO_SET_DESCRIPTION' => 'Set Description', - 'PHOTO_NEW_LICENSE' => 'Add a License', - 'PHOTO_SET_LICENSE' => 'Set License', - 'PHOTO_LICENSE' => 'License', - 'PHOTO_REUSE' => 'Reuse', - 'PHOTO_LICENSE_NONE' => 'None', - 'PHOTO_RESERVED' => 'All Rights Reserved', - 'PHOTO_LATITUDE' => 'Latitude', - 'PHOTO_LONGITUDE' => 'Longitude', - 'PHOTO_ALTITUDE' => 'Altitude', - 'PHOTO_IMGDIRECTION' => 'Direction', - 'PHOTO_LOCATION' => 'Location', - 'PHOTO_IMAGE' => 'Image', - 'PHOTO_VIDEO' => 'Video', - 'PHOTO_SIZE' => 'Size', - 'PHOTO_FORMAT' => 'Format', - 'PHOTO_RESOLUTION' => 'Resolution', - 'PHOTO_DURATION' => 'Duration', - 'PHOTO_FPS' => 'Frame rate', - 'PHOTO_TAGS' => 'Tags', - 'PHOTO_NOTAGS' => 'No Tags', - 'PHOTO_NEW_TAGS' => 'Enter your tags for this photo. You can add multiple tags by separating them with a comma:', - 'PHOTO_NEW_TAGS_1' => 'Enter your tags for all', - 'PHOTO_NEW_TAGS_2' => 'selected photos. Existing tags will be overwritten. You can add multiple tags by separating them with a comma:', - 'PHOTO_SET_TAGS' => 'Set Tags', - 'PHOTO_CAMERA' => 'Camera', - 'PHOTO_CAPTURED' => 'Captured', - 'PHOTO_MAKE' => 'Make', - 'PHOTO_TYPE' => 'Type/Model', - 'PHOTO_LENS' => 'Lens', - 'PHOTO_SHUTTER' => 'Shutter Speed', - 'PHOTO_APERTURE' => 'Aperture', - 'PHOTO_FOCAL' => 'Focal Length', - 'PHOTO_ISO' => 'ISO', - 'PHOTO_SHARING' => 'Sharing', - 'PHOTO_SHR_PLUBLIC' => 'Public', - 'PHOTO_SHR_ALB' => 'Yes (Album)', - 'PHOTO_SHR_PHT' => 'Yes (Photo)', - 'PHOTO_SHR_NO' => 'No', - 'PHOTO_DELETE' => 'Delete Photo', - 'PHOTO_KEEP' => 'Keep Photo', - 'PHOTO_DELETE_1' => 'Are you sure you want to delete the photo', - 'PHOTO_DELETE_2' => '? This action can\'t be undone!', - 'PHOTO_DELETE_ALL_1' => 'Are you sure you want to delete all', - 'PHOTO_DELETE_ALL_2' => 'selected photo? This action can\'t be undone!', - 'PHOTOS_NEW_TITLE_1' => 'Enter a title for all', - 'PHOTOS_NEW_TITLE_2' => 'selected photos:', - 'PHOTO_MAKE_PRIVATE_ALBUM' => 'This photo is located in a public album. To make this photo private or public, edit the visibility of the associated album.', - 'PHOTO_SHOW_ALBUM' => 'Show Album', - 'PHOTO_PUBLIC' => 'Public', - 'PHOTO_PUBLIC_EXPL' => 'Photo can be viewed by others, subject to the restrictions below.', - 'PHOTO_FULL' => 'Original', - 'PHOTO_FULL_EXPL' => 'Full-resolution picture is available.', - 'PHOTO_HIDDEN' => 'Hidden', - 'PHOTO_HIDDEN_EXPL' => 'Only people with the direct link can view this photo.', - 'PHOTO_DOWNLOADABLE' => 'Downloadable', - 'PHOTO_DOWNLOADABLE_EXPL' => 'Visitors of your gallery can download this photo.', - 'PHOTO_SHARE_BUTTON_VISIBLE' => 'Share button is visible', - 'PHOTO_SHARE_BUTTON_VISIBLE_EXPL' => 'Display social media sharing links.', - 'PHOTO_PASSWORD_PROT' => 'Password protected', - 'PHOTO_PASSWORD_PROT_EXPL' => 'Photo only accessible with a valid password.', - 'PHOTO_EDIT_SHARING_TEXT' => 'The sharing properties of this photo will be changed to the following:', - 'PHOTO_NO_EDIT_SHARING_TEXT' => 'Because this photo is located in a public album, it inherits that album\'s visibility settings. Its current visibility is shown below for informational purposes only.', - 'PHOTO_EDIT_GLOBAL_SHARING_TEXT' => 'The visibility of this photo can be fine-tuned using global Lychee settings. Its current visibility is shown below for informational purposes only.', - 'PHOTO_SHARING_CONFIRM' => 'Save', - - 'LOADING' => 'Loading', - 'ERROR' => 'Error', - 'ERROR_TEXT' => 'Whoops, it looks like something went wrong. Please reload the site and try again!', - 'ERROR_DB_1' => 'Unable to connect to host database because access was denied. Double-check your host, username and password and ensure that access from your current location is permitted.', - 'ERROR_DB_2' => 'Unable to create the database. Double-check your host, username and password and ensure that the specified user has the rights to modify and add content to the database.', - 'ERROR_CONFIG_FILE' => "Unable to save this configuration. Permission denied in 'data/'. Please set the read, write and execute rights for others in 'data/' and 'uploads/'. Take a look at the readme for more information.", - 'ERROR_UNKNOWN' => 'Something unexpected happened. Please try again and check your installation and server. Take a look at the readme for more information.', - 'ERROR_LOGIN' => 'Unable to save login. Please try again with another username and password!', - 'ERROR_MAP_DEACTIVATED' => 'Map functionality has been deactivated under settings.', - 'ERROR_SEARCH_DEACTIVATED' => 'Search functionality has been deactivated under settings.', - 'SUCCESS' => 'OK', - 'RETRY' => 'Retry', - - 'SETTINGS_SUCCESS_LOGIN' => 'Login Info updated.', - 'SETTINGS_SUCCESS_SORT' => 'Sorting order updated.', - 'SETTINGS_SUCCESS_DROPBOX' => 'Dropbox Key updated.', - 'SETTINGS_SUCCESS_LANG' => 'Language updated', - 'SETTINGS_SUCCESS_LAYOUT' => 'Layout updated', - 'SETTINGS_SUCCESS_IMAGE_OVERLAY' => 'EXIF Overlay setting updated', - 'SETTINGS_SUCCESS_PUBLIC_SEARCH' => 'Public search updated', - 'SETTINGS_SUCCESS_LICENSE' => 'Default license updated', - 'SETTINGS_SUCCESS_MAP_DISPLAY' => 'Map display settings updated', - 'SETTINGS_SUCCESS_MAP_DISPLAY_PUBLIC' => 'Map display settings for public albums updated', - 'SETTINGS_SUCCESS_MAP_PROVIDER' => 'Map provider settings updated', - - 'U2F_NOT_SUPPORTED' => 'U2F not supported. Sorry.', - 'U2F_NOT_SECURE' => 'Environment not secured. U2F not available.', - 'U2F_REGISTER_KEY' => 'Register new device.', - 'U2F_REGISTRATION_SUCCESS' => 'Registration successful!', - 'U2F_AUTHENTIFICATION_SUCCESS' => 'Authentication successful!', - 'U2F_CREDENTIALS' => 'Credentials', - 'U2F_CREDENTIALS_DELETED' => 'Credentials deleted!', - - 'NEW_PHOTOS_NOTIFICATION' => 'Send new photos notification emails.', - 'SETTINGS_SUCCESS_NEW_PHOTOS_NOTIFICATION' => 'New photos notification updated', - 'USER_EMAIL_INSTRUCTION' => 'Add your email below to enable receiving email notifications.
To stop receiving emails, simply remove your email below.', - - 'DB_INFO_TITLE' => 'Enter your database connection details below:', - 'DB_INFO_HOST' => 'Database Host (optional)', - 'DB_INFO_USER' => 'Database Username', - 'DB_INFO_PASSWORD' => 'Database Password', - 'DB_INFO_TEXT' => 'Lychee will create its own database. If required, you can enter the name of an existing database instead:', - 'DB_NAME' => 'Database Name (optional)', - 'DB_PREFIX' => 'Table prefix (optional)', - 'DB_CONNECT' => 'Connect', - - 'LOGIN_TITLE' => 'Enter a username and password for your installation:', - 'LOGIN_USERNAME' => 'New Username', - 'LOGIN_PASSWORD' => 'New Password', - 'LOGIN_PASSWORD_CONFIRM' => 'Confirm Password', - 'LOGIN_CREATE' => 'Create Login', - - 'PASSWORD_TITLE' => 'Enter your current password:', - 'USERNAME_CURRENT' => 'Current Username', - 'PASSWORD_CURRENT' => 'Current Password', - 'PASSWORD_TEXT' => 'Your username and password will be changed to the following:', - 'PASSWORD_CHANGE' => 'Change Login', - - 'EDIT_SHARING_TITLE' => 'Edit Sharing', - 'EDIT_SHARING_TEXT' => 'The sharing properties of this album will be changed to the following:', - 'SHARE_ALBUM_TEXT' => 'This album will be shared with the following properties:', - 'ALBUM_SHARING_CONFIRM' => 'Save', - - 'SORT_ALBUM_BY_1' => 'Sort albums by', - 'SORT_ALBUM_BY_2' => 'in an', - 'SORT_ALBUM_BY_3' => 'order.', - - 'SORT_ALBUM_SELECT_1' => 'Creation Time', - 'SORT_ALBUM_SELECT_2' => 'Title', - 'SORT_ALBUM_SELECT_3' => 'Description', - 'SORT_ALBUM_SELECT_4' => 'Public', - 'SORT_ALBUM_SELECT_5' => 'Latest Take Date', - 'SORT_ALBUM_SELECT_6' => 'Oldest Take Date', - - 'SORT_PHOTO_BY_1' => 'Sort photos by', - 'SORT_PHOTO_BY_2' => 'in an', - 'SORT_PHOTO_BY_3' => 'order.', - - 'SORT_PHOTO_SELECT_1' => 'Upload Time', - 'SORT_PHOTO_SELECT_2' => 'Take Date', - 'SORT_PHOTO_SELECT_3' => 'Title', - 'SORT_PHOTO_SELECT_4' => 'Description', - 'SORT_PHOTO_SELECT_5' => 'Public', - 'SORT_PHOTO_SELECT_6' => 'Star', - 'SORT_PHOTO_SELECT_7' => 'Photo Format', - - 'SORT_ASCENDING' => 'Ascending', - 'SORT_DESCENDING' => 'Descending', - 'SORT_CHANGE' => 'Change Sorting', - - 'DROPBOX_TITLE' => 'Set Dropbox Key', - 'DROPBOX_TEXT' => "In order to import photos from your Dropbox, you need a valid drop-ins app key from their website. Generate yourself a personal key and enter it below:", - - 'LANG_TEXT' => 'Change Lychee language for:', - 'LANG_TITLE' => 'Change Language', - 'PUBLIC_SEARCH_TEXT' => 'Public search allowed:', - 'OVERLAY_TYPE' => 'Photo overlay:', - 'OVERLAY_NONE' => 'None', - 'OVERLAY_EXIF' => 'EXIF data', - 'OVERLAY_DESCRIPTION' => 'Description', - 'OVERLAY_DATE' => 'Date taken', - 'MAP_DISPLAY_TEXT' => 'Enable maps (provided by OpenStreetMap):', - 'MAP_DISPLAY_PUBLIC_TEXT' => 'Enable maps for public albums (provided by OpenStreetMap):', - 'LOCATION_DECODING' => 'Decode GPS data into location name', - 'LOCATION_SHOW' => 'Show location name', - 'LOCATION_SHOW_PUBLIC' => 'Show location name for public mode', - 'MAP_PROVIDER' => 'Provider of OpenStreetMap tiles:', - 'MAP_PROVIDER_WIKIMEDIA' => 'Wikimedia', - 'MAP_PROVIDER_OSM_ORG' => 'OpenStreetMap.org (no HiDPI)', - 'MAP_PROVIDER_OSM_DE' => 'OpenStreetMap.de (no HiDPI)', - 'MAP_PROVIDER_OSM_FR' => 'OpenStreetMap.fr (no HiDPI)', - 'MAP_PROVIDER_RRZE' => 'University of Erlangen, Germany (only HiDPI)', - 'MAP_INCLUDE_SUBALBUMS_TEXT' => 'Include photos of subalbums on map:', - - 'LAYOUT_TYPE' => 'Layout of photos:', - 'LAYOUT_SQUARES' => 'Square thumbnails', - 'LAYOUT_JUSTIFIED' => 'With aspect, justified', - 'LAYOUT_UNJUSTIFIED' => 'With aspect, unjustified', - 'SET_LAYOUT' => 'Change layout', - - 'NSFW_VISIBLE_TEXT_1' => 'Make Sensitive albums visible by default.', - 'NSFW_VISIBLE_TEXT_2' => 'If the album is public, it is still accessible, just hidden from the view and can be revealed by pressing H.', - 'SETTINGS_SUCCESS_NSFW_VISIBLE' => 'Default sensitive album visibility updated with success.', - - 'VIEW_NO_RESULT' => 'No results', - 'VIEW_NO_PUBLIC_ALBUMS' => 'No public albums', - 'VIEW_NO_CONFIGURATION' => 'No configuration', - 'VIEW_PHOTO_NOT_FOUND' => 'Photo not found', - - 'NO_TAGS' => 'No Tags', - - 'UPLOAD_MANAGE_NEW_PHOTOS' => 'You can now manage your new photo(s).', - 'UPLOAD_COMPLETE' => 'Upload complete', - 'UPLOAD_COMPLETE_FAILED' => 'Failed to upload one or more photos.', - 'UPLOAD_IMPORTING' => 'Importing', - 'UPLOAD_IMPORTING_URL' => 'Importing URL', - 'UPLOAD_UPLOADING' => 'Uploading', - 'UPLOAD_FINISHED' => 'Finished', - 'UPLOAD_PROCESSING' => 'Processing', - 'UPLOAD_FAILED' => 'Failed', - 'UPLOAD_FAILED_ERROR' => 'Upload failed. Server returned an error!', - 'UPLOAD_FAILED_WARNING' => 'Upload failed. Server returned a warning!', - 'UPLOAD_CANCELLED' => 'Cancelled', - 'UPLOAD_SKIPPED' => 'Skipped', - 'UPLOAD_UPDATED' => 'Updated', - 'UPLOAD_IMPORT_SKIPPED_DUPLICATE' => 'This photo has been skipped because it\'s already in your library.', - 'UPLOAD_IMPORT_RESYNCED_DUPLICATE' => 'This photo has been skipped because it\'s already in your library, but its metadata has been updated.', - 'UPLOAD_ERROR_CONSOLE' => 'Please take a look at the console of your browser for further details.', - 'UPLOAD_UNKNOWN' => 'Server returned an unknown response. Please take a look at the console of your browser for further details.', - 'UPLOAD_ERROR_UNKNOWN' => 'Upload failed. Server returned an unkown error!', - 'UPLOAD_ERROR_POSTSIZE' => 'Upload failed. The PHP post_max_size may be too small! Otherwise check the FAQ.', - 'UPLOAD_ERROR_FILESIZE' => 'Upload failed. The PHP upload_max_filesize may be too small! Otherwise check the FAQ.', - 'UPLOAD_IN_PROGRESS' => 'Lychee is currently uploading!', - 'UPLOAD_IMPORT_WARN_ERR' => 'The import has been finished, but returned warnings or errors. Please take a look at the log (Settings -> Show Log) for further details.', - 'UPLOAD_IMPORT_COMPLETE' => 'Import complete', - 'UPLOAD_IMPORT_INSTR' => 'Please enter the direct link to a photo to import it:', - 'UPLOAD_IMPORT' => 'Import', - 'UPLOAD_IMPORT_SERVER' => 'Importing from server', - 'UPLOAD_IMPORT_SERVER_FOLD' => 'Folder empty or no readable files to process. Please take a look at the log (Settings -> Show Log) for further details.', - 'UPLOAD_IMPORT_SERVER_INSTR' => 'Import all photos, folders and sub-folders located in the folder with the following absolute path (on server):', - 'UPLOAD_ABSOLUTE_PATH' => 'Absolute path to directory', - 'UPLOAD_IMPORT_SERVER_EMPT' => 'Could not start import because the folder was empty!', - 'UPLOAD_IMPORT_DELETE_ORIGINALS' => 'Delete originals', - 'UPLOAD_IMPORT_DELETE_ORIGINALS_EXPL' => 'Original files will be deleted after the import when possible.', - 'UPLOAD_IMPORT_VIA_SYMLINK' => 'Symbolic links', - 'UPLOAD_IMPORT_VIA_SYMLINK_EXPL' => 'Import files using symbolic links to originals.', - 'UPLOAD_IMPORT_SKIP_DUPLICATES' => 'Skip duplicates', - 'UPLOAD_IMPORT_SKIP_DUPLICATES_EXPL' => 'Existing media files are skipped.', - 'UPLOAD_IMPORT_RESYNC_METADATA' => 'Re-sync metadata', - 'UPLOAD_IMPORT_RESYNC_METADATA_EXPL' => 'Update metadata of existing media files.', - 'UPLOAD_IMPORT_LOW_MEMORY' => 'Low memory condition!', - 'UPLOAD_IMPORT_LOW_MEMORY_EXPL' => 'The import process on the server is approaching the memory limit and may end up being terminated prematurely.', - 'UPLOAD_WARNING' => 'Warning', - 'UPLOAD_IMPORT_NOT_A_DIRECTORY' => 'The given path is not a readable directory!', - 'UPLOAD_IMPORT_PATH_RESERVED' => 'The given path is a reserved path of Lychee!', - 'UPLOAD_IMPORT_UNREADABLE' => 'Could not read the file!', - 'UPLOAD_IMPORT_FAILED' => 'Could not import the file!', - 'UPLOAD_IMPORT_UNSUPPORTED' => 'Unsupported file type!', - 'UPLOAD_IMPORT_ALBUM_FAILED' => 'Could not create the album!', - 'UPLOAD_IMPORT_CANCELLED' => 'Import cancelled', - - 'ABOUT_SUBTITLE' => 'Self-hosted photo-management done right', - 'ABOUT_DESCRIPTION' => 'is a free photo-management tool, which runs on your server or web-space. Installing is a matter of seconds. Upload, manage and share photos like from a native application. Lychee comes with everything you need and all your photos are stored securely.', - 'FOOTER_COPYRIGHT' => 'All images on this website are subject to Copyright by ', - 'HOSTED_WITH_LYCHEE' => 'Hosted with Lychee', - - 'URL_COPY_TO_CLIPBOARD' => 'Copy to clipboard', - 'URL_COPIED_TO_CLIPBOARD' => 'Copied URL to clipboard!', - 'PHOTO_DIRECT_LINKS_TO_IMAGES' => 'Direct links to image files:', - 'PHOTO_MEDIUM' => 'Medium', - 'PHOTO_MEDIUM_HIDPI' => 'Medium HiDPI', - 'PHOTO_SMALL' => 'Thumb', - 'PHOTO_SMALL_HIDPI' => 'Thumb HiDPI', - 'PHOTO_THUMB' => 'Square thumb', - 'PHOTO_THUMB_HIDPI' => 'Square thumb HiDPI', - 'PHOTO_LIVE_VIDEO' => 'Video part of live-photo', - 'PHOTO_VIEW' => 'Lychee Photo View:', - - 'PHOTO_EDIT_ROTATECWISE' => 'Rotate clockwise', - 'PHOTO_EDIT_ROTATECCWISE' => 'Rotate counter-clockwise', - ]; - - return $locale; - } -} diff --git a/app/Locale/French.php b/app/Locale/French.php deleted file mode 100644 index 6e433155ee5..00000000000 --- a/app/Locale/French.php +++ /dev/null @@ -1,475 +0,0 @@ - 'Nom d’utilisateur', - 'PASSWORD' => 'Mot de passe', - 'ENTER' => 'Enter', - 'CANCEL' => 'Annuler', - 'SIGN_IN' => 'Connexion', - 'CLOSE' => 'Fermer', - 'SETTINGS' => 'Paramètres', - 'SEARCH' => 'Rechercher…', - 'MORE' => 'Plus', - 'DEFAULT' => 'Valeur par defaut', - 'ALBUM_SET_ORDER' => 'Changer l\'ordre', - 'ALBUM_ORDERING' => 'Trier par', - - 'USERS' => 'Utilisateurs', - 'U2F' => 'U2F', - 'NOTIFICATIONS' => 'Notifications', - 'SHARING' => 'Partage', - 'CHANGE_LOGIN' => 'Changer le nom d’utilisateur', - 'CHANGE_SORTING' => 'Changer le tri', - 'SET_DROPBOX' => 'Paramétrer Dropbox', - 'ABOUT_LYCHEE' => 'À propos de Lychee', - 'DIAGNOSTICS' => 'Diagnostiques', - 'DIAGNOSTICS_GET_SIZE' => 'Calculer l’espace utilisé', - 'LOGS' => 'Afficher les logs', - 'SIGN_OUT' => 'Se déconnecter', - 'UPDATE_AVAILABLE' => 'Une mise à jour est disponible !', - 'MIGRATION_AVAILABLE' => 'Une migration disponible !', - 'DEFAULT_LICENSE' => 'License par defaut pour les nouveaux ajouts:', - 'SET_LICENSE' => 'Selectioner une license', - 'SET_OVERLAY_TYPE' => 'Selectioner le type d’Overlay', - 'SET_MAP_PROVIDER' => 'Selectioner le fournisseur de données cartographiques', - - 'SMART_ALBUMS' => 'Smart Albums', - 'SHARED_ALBUMS' => 'Albums partagés', - 'ALBUMS' => 'Albums', - 'PHOTOS' => 'Photos', - 'SEARCH_RESULTS' => 'Résultats', - - 'RENAME' => 'Renommer', - 'RENAME_ALL' => 'Renommer la sélection', - 'MERGE' => 'Fusionner', - 'MERGE_ALL' => 'Fusionner la sélection', - 'MAKE_PUBLIC' => 'Rendre public', - 'SHARE_ALBUM' => 'Partager l’album', - 'SHARE_PHOTO' => 'Partager la photo', - 'VISIBILITY_ALBUM' => 'Visibilité de l’album', - 'VISIBILITY_PHOTO' => 'Visibilité de la Photo', - 'DOWNLOAD_ALBUM' => 'Télécharger l’album', - 'ABOUT_ALBUM' => 'À propos de l’album', - 'DELETE_ALBUM' => 'Supprimer l’album', - 'MOVE_ALBUM' => 'Déplacer l’album', - 'FULLSCREEN_ENTER' => 'Entrer en mode plein écran', - 'FULLSCREEN_EXIT' => 'Sortir du mode plein écran', - - 'SHARING_ALBUM_USERS' => 'Partager l’album avec des utilisateurs', - 'WAIT_FETCH_DATA' => 'Merci de patienter que les données soient récupérées…', - 'SHARING_ALBUM_USERS_NO_USERS' => 'Il n’y pas d’utilisateurs avec qui partager cet album', - 'SHARING_ALBUM_USERS_LONG_MESSAGE' => 'Sélectionner les utilisateurs avec qui partager cet album', - - 'DELETE_ALBUM_QUESTION' => 'Supprimer l’album et ses photos', - 'KEEP_ALBUM' => 'Garder l’album', - 'DELETE_ALBUM_CONFIRMATION_1' => 'Voulez-vous vraiment supprimer l’album ', - 'DELETE_ALBUM_CONFIRMATION_2' => 'et toutes les photos qu’il contient ? Cette action est irréversible !', - - 'DELETE_ALBUMS_QUESTION' => 'Supprimer les albums et leurs photos', - 'KEEP_ALBUMS' => 'Garder les albums', - 'DELETE_ALBUMS_CONFIRMATION_1' => 'Voulez-vous vraiment supprimer les ', - 'DELETE_ALBUMS_CONFIRMATION_2' => 'albums selectionnés et toutes leurs photos ? Cette action est irréversible !', - - 'DELETE_UNSORTED_CONFIRM' => 'Voulez-vous vraiment supprimer toutes les photos de «Non-triés» ?
Cette action est irréversible !', - 'CLEAR_UNSORTED' => 'Vider Non-triés', - 'KEEP_UNSORTED' => 'Garder Non-triés', - - 'EDIT_SHARING' => 'Éditer le partage', - 'MAKE_PRIVATE' => 'Rendre privé', - - 'CLOSE_ALBUM' => 'Fermer l’album', - 'CLOSE_PHOTO' => 'Fermer la photo', - 'CLOSE_MAP' => 'Fermer la carte', - - 'ADD' => 'Ajouter', - 'MOVE' => 'Déplacer', - 'MOVE_ALL' => 'Déplacer la sélection', - 'DUPLICATE' => 'Dupliquer', - 'DUPLICATE_ALL' => 'Dupliquer la sélection', - 'COPY_TO' => 'Copier vers…', - 'COPY_ALL_TO' => 'Copier la sélection vers…', - 'DELETE' => 'Supprimer', - 'DELETE_ALL' => 'Supprimer la sélection', - 'DOWNLOAD' => 'Télécharger', - 'DOWNLOAD_ALL' => 'Télécharger la sélection', - 'UPLOAD_PHOTO' => 'Ajouter une photo ou une vidéo', - 'IMPORT_LINK' => 'Importer depuis un lien', - 'IMPORT_DROPBOX' => 'Importer depuis Dropbox', - 'IMPORT_SERVER' => 'Importer depuis le serveur', - 'NEW_ALBUM' => 'Nouvel album', - 'NEW_TAG_ALBUM' => 'Nouvel album d’étiquette', - - 'TITLE_NEW_ALBUM' => 'Entrez le titre du nouvel album :', - 'UNTITLED' => 'Sans titre', - 'UNSORTED' => 'Non triés', - 'STARRED' => 'Favoris', - 'RECENT' => 'Récent', - 'PUBLIC' => 'Public', - 'NUM_PHOTOS' => 'Photos', - - 'CREATE_ALBUM' => 'Créer un album', - 'CREATE_TAG_ALBUM' => 'Créer un album d’étiquette', - - 'STAR_PHOTO' => 'Mettre en Favoris', - 'STAR' => 'Favori', - 'STAR_ALL' => 'Marquer la sélection comme favoris', - 'TAGS' => 'Tagger', - 'TAGS_ALL' => 'Tagger la sélection', - 'UNSTAR_PHOTO' => 'Retirer des Favoris', - 'SET_COVER' => 'Changer la pochette de l\'album', - 'REMOVE_COVER' => 'Supprimer la pochette de l\'album', - - 'FULL_PHOTO' => 'Ouvrir l’original', - 'ABOUT_PHOTO' => 'À propos de la photo', - 'DISPLAY_FULL_MAP' => 'Carte', - 'DIRECT_LINK' => 'Lien direct', - 'DIRECT_LINKS' => 'Liens directs', - - 'ALBUM_ABOUT' => 'À propos', - 'ALBUM_BASICS' => 'Informations de base', - 'ALBUM_TITLE' => 'Titre', - 'ALBUM_NEW_TITLE' => 'Entrez un nouveau titre pour cet album :', - 'ALBUMS_NEW_TITLE_1' => 'Entrez un titre pour les', - 'ALBUMS_NEW_TITLE_2' => 'albums sélectionnés :', - 'ALBUM_SET_TITLE' => 'Enregistrer le titre', - 'ALBUM_DESCRIPTION' => 'Description', - 'ALBUM_SHOW_TAGS' => 'Étiquettes à afficher', - 'ALBUM_NEW_DESCRIPTION' => 'Entrez une nouvelle description pour cet album :', - 'ALBUM_SET_DESCRIPTION' => 'Choisir une description', - 'ALBUM_NEW_SHOWTAGS' => 'Entrez les étiquettes des photos qui seront affichées dans cet album :', - 'ALBUM_SET_SHOWTAGS' => 'Afficher ces étiquettes', - 'ALBUM_ALBUM' => 'Album', - 'ALBUM_CREATED' => 'Créé', - 'ALBUM_IMAGES' => 'Images', - 'ALBUM_VIDEOS' => 'Videos', - 'ALBUM_SUBALBUMS' => 'Sous-albums', - 'ALBUM_SHARING' => 'Partager', - 'ALBUM_SHR_YES' => 'Oui', - 'ALBUM_SHR_NO' => 'Non', - 'ALBUM_PUBLIC' => 'Public', - 'ALBUM_PUBLIC_EXPL' => 'L’Album est visible publiquement avec les restrictions suivantes.', - 'ALBUM_HIDDEN' => 'Masqué', - 'ALBUM_FULL' => 'Originaux', - 'ALBUM_MARK_NSFW' => 'Mark album as sensitive', - 'ALBUM_UNMARK_NSFW' => 'Unmark album as sensitive', - 'ALBUM_NSFW' => 'Sensitive', - 'ALBUM_NSFW_EXPL' => 'Album is marked to contain sensitive content.', - 'ALBUM_FULL_EXPL' => 'Les images sont disponibles en complète résolution.', - 'ALBUM_HIDDEN_EXPL' => 'Seules les personnes avec le lien peuvent voir cet album.', - 'ALBUM_DOWNLOADABLE' => 'Téléchargeable', - 'ALBUM_DOWNLOADABLE_EXPL' => 'Les visiteurs peuvent télécharger cet album.', - 'ALBUM_SHARE_BUTTON_VISIBLE' => 'Visibilité du bouton de partage.', - 'ALBUM_SHARE_BUTTON_VISIBLE_EXPL' => 'Affiche les liens de partage sur les média sociaux.', - 'ALBUM_PASSWORD' => 'Mot de passe', - 'ALBUM_PASSWORD_PROT' => 'Protéger par un mot de passe.', - 'ALBUM_PASSWORD_PROT_EXPL' => 'Cet album est accessible avec un mot de passe.', - 'ALBUM_PASSWORD_REQUIRED' => 'Cet album est protégé par mot de passe. Entrez le mot de passe pour afficher les photos de cet album :', - 'ALBUM_MERGE_1' => 'Voulez-vous vraiment fusionner l’album', - 'ALBUM_MERGE_2' => 'dans l’album', // `dans` est important car il indique la direction du merge - 'ALBUMS_MERGE' => 'Voulez-vous vraiment fusionner les albums selectionnés avec l’album', - 'MERGE_ALBUM' => 'Fusionner les albums', - 'DONT_MERGE' => 'Ne pas fusionner.', - 'ALBUM_MOVE_1' => 'Voulez-vous vraiment déplacer l’album', - 'ALBUM_MOVE_2' => 'dans l’album', - 'ALBUMS_MOVE' => 'Voulez-vous vraiment déplacer les albums selectionnés dans l’album', - 'MOVE_ALBUMS' => 'Déplacer les albums', - 'NOT_MOVE_ALBUMS' => 'Ne pas déplacer', - 'ROOT' => 'Albums', - 'ALBUM_REUSE' => 'Reuse', - 'ALBUM_LICENSE' => 'License', - 'ALBUM_SET_LICENSE' => 'Selectioner une license', - 'ALBUM_LICENSE_HELP' => 'Un doute sur le choix ?', - 'ALBUM_LICENSE_NONE' => 'Aucune', - 'ALBUM_RESERVED' => 'Tous droits réservés', - - 'PHOTO_ABOUT' => 'À propos', - 'PHOTO_BASICS' => 'Informations de base', - 'PHOTO_TITLE' => 'Titre', - 'PHOTO_NEW_TITLE' => 'Entrer un nouveau titre pour cette photo :', - 'PHOTO_SET_TITLE' => 'Choisir un titre', - 'PHOTO_UPLOADED' => 'Uploadé', // Frenglish, but I don't care. Telecharge est ambigu en Francais... - 'PHOTO_DESCRIPTION' => 'Description', - 'PHOTO_NEW_DESCRIPTION' => 'Entrez une nouvelle description pour cette photo :', - 'PHOTO_SET_DESCRIPTION' => 'Choisir une description', - 'PHOTO_NEW_LICENSE' => 'Ajouter une License', - 'PHOTO_SET_LICENSE' => 'Sélectionner License', - 'PHOTO_LICENSE' => 'License', - 'PHOTO_REUSE' => 'Reuse', - 'PHOTO_LICENSE_NONE' => 'Aucune', - 'PHOTO_RESERVED' => 'Tous droits réservés', - 'PHOTO_LATITUDE' => 'Latitude', - 'PHOTO_LONGITUDE' => 'Longitude', - 'PHOTO_ALTITUDE' => 'Altitude', - 'PHOTO_IMGDIRECTION' => 'Direction', - 'PHOTO_LOCATION' => 'Location', - 'PHOTO_IMAGE' => 'Image', - 'PHOTO_VIDEO' => 'Vidéo', - 'PHOTO_SIZE' => 'Dimension', - 'PHOTO_FORMAT' => 'Format', - 'PHOTO_RESOLUTION' => 'Résolution', - 'PHOTO_DURATION' => 'Durée', - 'PHOTO_FPS' => 'Frame rate', - 'PHOTO_TAGS' => 'Étiquettes', - 'PHOTO_NOTAGS' => 'Pas d’étiquettes', - 'PHOTO_NEW_TAGS' => 'Entrez vos étiquettes pour cette photo. Vous pouvez ajouter plusieurs étiquettes en les séparant avec une virgule :', - 'PHOTO_NEW_TAGS_1' => 'Entrez vos étiquettes pour toutes les', - 'PHOTO_NEW_TAGS_2' => 'photos selectionnées. Les tags existants seront remplacés. Vous pouvez ajouter plusieurs tags en les séparant avec une virgule :', - 'PHOTO_SET_TAGS' => 'Établir les étiquettes', - 'PHOTO_CAMERA' => 'Appareil', - 'PHOTO_CAPTURED' => 'Date de prise de vue', - 'PHOTO_MAKE' => 'Marque', - 'PHOTO_TYPE' => 'Modèle', - 'PHOTO_LENS' => 'Objectif', - 'PHOTO_SHUTTER' => 'Durée d’exposition', - 'PHOTO_APERTURE' => 'Ouverture', - 'PHOTO_FOCAL' => 'Distance focale', - 'PHOTO_ISO' => 'ISO', - 'PHOTO_SHARING' => 'Partager', - 'PHOTO_SHR_PLUBLIC' => 'Publique', - 'PHOTO_SHR_ALB' => 'Oui (album)', - 'PHOTO_SHR_PHT' => 'Oui (photo)', - 'PHOTO_SHR_NO' => 'Non', - 'PHOTO_DELETE' => 'Supprimer la photo', - 'PHOTO_KEEP' => 'Garder la photo', - 'PHOTO_DELETE_1' => 'Voulez-vous vraiment supprimer la photo ? ', - 'PHOTO_DELETE_2' => 'Cette action est irréversible !', - 'PHOTO_DELETE_ALL_1' => 'Voulez-vous vraiment supprimer toutes les', - 'PHOTO_DELETE_ALL_2' => 'photos sélectionnées ? Cette action est irréversible !', - 'PHOTOS_NEW_TITLE_1' => 'Entrer un titre pour toutes les', - 'PHOTOS_NEW_TITLE_2' => 'photos sélectionnées :', - 'PHOTO_MAKE_PRIVATE_ALBUM' => 'Cette photo est située dans un album public. Pour rendre cette photo privée ou publique, modifiez la visibilité de l’album associé.', - 'PHOTO_SHOW_ALBUM' => 'Afficher l’album', - 'PHOTO_PUBLIC' => 'Public', - 'PHOTO_PUBLIC_EXPL' => 'La photo est visible publiquement avec les restrictions suivantes.', - 'PHOTO_FULL' => 'Originale', - 'PHOTO_FULL_EXPL' => 'La photo est disponible en résolution complète.', - 'PHOTO_HIDDEN' => 'Cachée.', - 'PHOTO_HIDDEN_EXPL' => 'Seul les personnes avec un lien peuvent voir cette photo.', - 'PHOTO_DOWNLOADABLE' => 'Téléchargeable.', - 'PHOTO_DOWNLOADABLE_EXPL' => 'Les visiteurs peuvent télécharger cette photo.', - 'PHOTO_SHARE_BUTTON_VISIBLE' => 'Visibilité du bouton de partage', - 'PHOTO_SHARE_BUTTON_VISIBLE_EXPL' => 'Affiche les liens de partage pour média sociaux.', - 'PHOTO_PASSWORD_PROT' => 'Protéger par un mot de passe.', - 'PHOTO_PASSWORD_PROT_EXPL' => 'Cette photo est accessible uniquement avec un mot de passe.', - 'PHOTO_EDIT_SHARING_TEXT' => 'Les propriété de partages de cette photo seront changé pour les suivantes:', - 'PHOTO_NO_EDIT_SHARING_TEXT' => 'Parce que cette photo est dans un album public, elle hérite des propriété de partage de l’album. Sa visibilité est montrée ci dessous pour votre information.', - 'PHOTO_EDIT_GLOBAL_SHARING_TEXT' => 'La visibilité de cette photo est ajustable avec les parametres generaux de Lychee. Sa visibilité est montrée ci dessous pour votre information.', - 'PHOTO_SHARING_CONFIRM' => 'Sauvegarder', - - 'LOADING' => 'Chargement en cours', - 'ERROR' => 'Erreur', - 'ERROR_TEXT' => 'Il semble qu’une erreur soit survenue. Veuillez rafraichir la page et réessayer !', - 'ERROR_DB_1' => 'Connexion impossible à la base de données car l’accès a été refusé. Vérifiez votre nom d’hôte, nom d’utilisateur et mot de passe, et assurez-vous que l’accès est autorisé à partir de votre emplacement actuel.', - 'ERROR_DB_2' => 'Impossible de creer la base de données. Verifiez votre nom d’hôte, nom d’utilisateur et mot de passe, et assurez-vous que l’utilisateur specifié est autorisé à modifier et ajouter du contenu dans la base de données.', - 'ERROR_CONFIG_FILE' => 'Impossible d’enregistrer cette configuration. Permission refusée dans «data/». Veuillez paramétrer les droits de lecture, d’ecriture et d’exécution pour les autres utilisateurs dans «data/» et «uploads/». Consultez le fichier Readme pour obtenir plus d’information.', - 'ERROR_UNKNOWN' => 'Une erreur inattendue est survenue. Veuillez réessayer et vérifier votre installation et votre serveur. Consultez le fichier Readme pour obtenir plus d’information.', - 'ERROR_LOGIN' => 'Impossible d’enregistrer les informations de connexion. Veuillez réessayer avec un autre nom d’utilisateur et mot de passe.', - 'ERROR_MAP_DEACTIVATED' => 'La carte a été désactivée dans les paramètres.', - 'ERROR_SEARCH_DEACTIVATED' => 'La recherche a été désactivée dans les paramètres.', - 'SUCCESS' => 'OK', - 'RETRY' => 'Réessayer', - - 'SETTINGS_SUCCESS_LOGIN' => 'Informations de connexions mise à jour.', - 'SETTINGS_SUCCESS_SORT' => 'Ordre d’affichage mis à jour.', - 'SETTINGS_SUCCESS_DROPBOX' => 'Clé Dropbox mise à updated.', - 'SETTINGS_SUCCESS_LANG' => 'Langage mis à jour.', - 'SETTINGS_SUCCESS_LAYOUT' => 'Affichage mis à jour.', - 'SETTINGS_SUCCESS_IMAGE_OVERLAY' => 'Overlay EXIF mis à jour.', - 'SETTINGS_SUCCESS_PUBLIC_SEARCH' => 'Recherche publique mise à jour.', - 'SETTINGS_SUCCESS_LICENSE' => 'License par défaut mise à jour.', - 'SETTINGS_SUCCESS_MAP_DISPLAY' => 'Parametres de la carte mis à jour.', - 'SETTINGS_SUCCESS_MAP_DISPLAY_PUBLIC' => 'Parametres de la carte pour les albums publics mis à jour.', - 'SETTINGS_SUCCESS_MAP_PROVIDER' => 'Fournisseur de la Carte mis à jour.', - - 'U2F_NOT_SUPPORTED' => 'U2F non suporté. Desolé.', - 'U2F_NOT_SECURE' => 'Environment non sécurisé. U2F non disponible.', - 'U2F_REGISTER_KEY' => 'Enregistrer une nouvelle clé.', - 'U2F_REGISTRATION_SUCCESS' => 'Enregistrement réussi!', - 'U2F_AUTHENTIFICATION_SUCCESS' => 'Authentication réussie!', - 'U2F_CREDENTIALS' => 'Clés', - 'U2F_CREDENTIALS_DELETED' => 'Clé supprimée!', - - 'NEW_PHOTOS_NOTIFICATION' => 'Send new photos notification emails.', - 'SETTINGS_SUCCESS_NEW_PHOTOS_NOTIFICATION' => 'New photos notification updated', - 'USER_EMAIL_INSTRUCTION' => 'Add your email below to enable receiving email notifications.
To stop receiving emails, simply remove your email below.', - - 'DB_INFO_TITLE' => 'Entrez vos identifiants de connexion à la base de données ci-dessous :', - 'DB_INFO_HOST' => 'Hôte de la base de donnees (facultatif)', - 'DB_INFO_USER' => 'Nom d’utilisateur pour la base de données', - 'DB_INFO_PASSWORD' => 'Mot de passe pour la base de données', - 'DB_INFO_TEXT' => 'Lychee va créer sa propre base de données. Si vous le souhaitez, vous pouvez entrer le nom d’une base de données existante à la place :', - 'DB_NAME' => 'Nom de la base de données (facultatif)', - 'DB_PREFIX' => 'Préfixe de la table (facultatif)', - 'DB_CONNECT' => 'Connexion', - - 'LOGIN_TITLE' => 'Entrez un nom d’utilisateur et un mot de passe pour votre installation :', - 'LOGIN_USERNAME' => 'Nouvel utilisateur', - 'LOGIN_PASSWORD' => 'Nouveau Mot de passe', - 'LOGIN_PASSWORD_CONFIRM' => 'Confirmez le mot de passe', - 'LOGIN_CREATE' => 'Créer les informations de connexion', - - 'PASSWORD_TITLE' => 'Entrez votre mot de passe existant :', - 'USERNAME_CURRENT' => 'Nom d’utilisateur actuel :', - 'PASSWORD_CURRENT' => 'Mot de passe existant :', - 'PASSWORD_TEXT' => 'Votre nom d’utilisateur et votre mot de passe seront modifiés comme suit :', - 'PASSWORD_CHANGE' => 'Modifier les informations de connexion', - - 'EDIT_SHARING_TITLE' => 'Modifier le partage', - 'EDIT_SHARING_TEXT' => 'Les propriétés de partage de cet album vont etre modifiées comme suit :', - 'SHARE_ALBUM_TEXT' => 'Cet album sera partagé avec les propriétés suivantes :', - 'ALBUM_SHARING_CONFIRM' => 'Sauvegarder', - - 'SORT_ALBUM_BY_1' => 'Trier les albums', - 'SORT_ALBUM_BY_2' => 'dans l’ordre', - 'SORT_ALBUM_BY_3' => '.', - - 'SORT_ALBUM_SELECT_1' => 'Heure de création', - 'SORT_ALBUM_SELECT_2' => 'Titre', - 'SORT_ALBUM_SELECT_3' => 'Description', - 'SORT_ALBUM_SELECT_4' => 'Public', - 'SORT_ALBUM_SELECT_5' => 'Prise de vue la plus récente', - 'SORT_ALBUM_SELECT_6' => 'Prise de vue la plus ancienne', - - 'SORT_PHOTO_BY_1' => 'Trier les photos', - 'SORT_PHOTO_BY_2' => 'dans l’ordre', - 'SORT_PHOTO_BY_3' => '.', - - 'SORT_PHOTO_SELECT_1' => 'Date d’upload', - 'SORT_PHOTO_SELECT_2' => 'Date de prise de vue', - 'SORT_PHOTO_SELECT_3' => 'Titre', - 'SORT_PHOTO_SELECT_4' => 'Description', - 'SORT_PHOTO_SELECT_5' => 'Public', - 'SORT_PHOTO_SELECT_6' => 'Favoris', - 'SORT_PHOTO_SELECT_7' => 'Format de la photo', - - 'SORT_ASCENDING' => 'Croissant', - 'SORT_DESCENDING' => 'Décroissant', - 'SORT_CHANGE' => 'Modifier le tri', - - 'DROPBOX_TITLE' => 'Définir une clé Dropbox', - 'DROPBOX_TEXT' => 'Pour pouvoir importer des photos à partir de votre Dropbox, vous aurez besoin d’une clé d’application «drop-ins» valide à créer sur leur site. Générez votre clé personnelle et puis entrez-la ci-dessous:', - - 'LANG_TEXT' => 'Remplacer la langue de Lychee par :', - 'LANG_TITLE' => 'Changer la langue', - 'PUBLIC_SEARCH_TEXT' => 'Recherche publique autorisée:', - 'OVERLAY_TYPE' => 'Informations à utiliser pour l’overlay:', - 'OVERLAY_NONE' => 'Pas d’overlay', - 'OVERLAY_EXIF' => 'Informations EXIF', - 'OVERLAY_DESCRIPTION' => 'Description de la photo', - 'OVERLAY_DATE' => 'Date de la photo', - 'MAP_DISPLAY_TEXT' => 'Activer les cartes (fourni par OpenStreetMap):', - 'MAP_DISPLAY_PUBLIC_TEXT' => 'Activer les cartes pour les albums publics (fourni par OpenStreetMap):', - 'MAP_PROVIDER' => 'Fournisseur de cartes OpenStreetMap:', - 'MAP_PROVIDER_WIKIMEDIA' => 'Wikimedia', - 'MAP_PROVIDER_OSM_ORG' => 'OpenStreetMap.org (no HiDPI)', - 'MAP_PROVIDER_OSM_DE' => 'OpenStreetMap.de (no HiDPI)', - 'MAP_PROVIDER_OSM_FR' => 'OpenStreetMap.fr (no HiDPI)', - 'MAP_PROVIDER_RRZE' => 'University de Erlangen, Allemagne (only HiDPI)', - 'MAP_INCLUDE_SUBALBUMS_TEXT' => 'Include les photos des sous-albums sur la carte :', - 'LOCATION_DECODING' => 'Convertir les informations GPS en nom de localisation', - 'LOCATION_SHOW' => 'Montrer le nom de la localisation', - 'LOCATION_SHOW_PUBLIC' => 'Montrer le nom de la localisation en mode public', - 'LAYOUT_TYPE' => 'Affichage des photos :', - 'LAYOUT_SQUARES' => 'Miniatures carrées', - 'LAYOUT_JUSTIFIED' => 'En proportions, justifiés', - 'LAYOUT_UNJUSTIFIED' => 'En proportions, non-justifiés', - 'SET_LAYOUT' => 'Changer l’affichage', - - 'NSFW_VISIBLE_TEXT_1' => 'Make Sensitive albums visible by default.', - 'NSFW_VISIBLE_TEXT_2' => 'If the album is public, it is still accessible, just hidden from the view and can be revealed by pressing H.', - 'SETTINGS_SUCCESS_NSFW_VISIBLE' => 'Default sensitive album visibility updated with success.', - - 'VIEW_NO_RESULT' => 'Aucun résultat', - 'VIEW_NO_PUBLIC_ALBUMS' => 'Aucun album public', - 'VIEW_NO_CONFIGURATION' => 'Aucune configuration', - 'VIEW_PHOTO_NOT_FOUND' => 'Photo introuvable', - - 'NO_TAGS' => 'Aucun tag', - - 'UPLOAD_MANAGE_NEW_PHOTOS' => 'Vous pouvez désormais gérer vos nouvelles photos.', - 'UPLOAD_COMPLETE' => 'Upload terminé', - 'UPLOAD_COMPLETE_FAILED' => 'L’Upload d’une ou plusieurs photos a échoué.', - 'UPLOAD_IMPORTING' => 'Importation', - 'UPLOAD_IMPORTING_URL' => 'Importation depuis l’URL', - 'UPLOAD_UPLOADING' => 'Upload en cours', - 'UPLOAD_FINISHED' => 'Terminé', - 'UPLOAD_PROCESSING' => 'Traitement', - 'UPLOAD_FAILED' => 'Échec', - 'UPLOAD_FAILED_ERROR' => 'Échec d’upload. Le serveur a retourné une erreur !', - 'UPLOAD_FAILED_WARNING' => 'Échec d’upload. Le serveur a retourné un avertissement !', - 'UPLOAD_CANCELLED' => 'Cancelled', - 'UPLOAD_SKIPPED' => 'Ignoré', - 'UPLOAD_UPDATED' => 'Updated', - 'UPLOAD_IMPORT_SKIPPED_DUPLICATE' => 'This photo has been skipped because it’s already in your library.', - 'UPLOAD_IMPORT_RESYNCED_DUPLICATE' => 'This photo has been skipped because it’s already in your library, but its metadata has been updated.', - 'UPLOAD_ERROR_CONSOLE' => 'Veuillez consulter la console de votre navigateur pour obtenir plus de détails.', - 'UPLOAD_UNKNOWN' => 'Le serveur a retourné une reponse inconnue. Veuillez consulter la console de votre navigateur pour obtenir plus de détails.', - 'UPLOAD_ERROR_UNKNOWN' => 'Échec de l’upload. Le serveur a retourné une erreur inconnue !', - 'UPLOAD_ERROR_POSTSIZE' => 'Upload failed. The PHP post_max_size may be too small! Otherwise check the FAQ.', - 'UPLOAD_ERROR_FILESIZE' => 'Upload failed. The PHP upload_max_filesize may be too small! Otherwise check the FAQ.', - 'UPLOAD_IN_PROGRESS' => 'Lychee est en cours de téléchargement !', - 'UPLOAD_IMPORT_WARN_ERR' => 'L’importation est terminée, mais des erreurs ou des avertissements ont été retournés. Veuillez consulter le fichier de Log (Paramètres -> Afficher les logs) pour obtenir plus de détails.', - 'UPLOAD_IMPORT_COMPLETE' => 'Importation terminée', - 'UPLOAD_IMPORT_INSTR' => 'Veuillez entrer un lien direct vers une photo pour l’importer :', - 'UPLOAD_IMPORT' => 'Importer', - 'UPLOAD_IMPORT_SERVER' => 'Importation à partir du serveur', - 'UPLOAD_IMPORT_SERVER_FOLD' => 'Dossier vide ou aucun fichier lisible à traiter. Veuillez consulter le journal (Paramètres -> Afficher le journal) pour obtenir plus de détails.', - 'UPLOAD_IMPORT_SERVER_INSTR' => 'Cette action importera toutes les photos ainsi que tous les dossiers et sous-dossiers situés dans le répertoire suivant.', - 'UPLOAD_ABSOLUTE_PATH' => 'Chemin absolu du répertoire', - 'UPLOAD_IMPORT_SERVER_EMPT' => 'Impossible de démarrer l’importation car le dossier était vide !', - 'UPLOAD_IMPORT_DELETE_ORIGINALS' => 'Supprimer les originaux', - 'UPLOAD_IMPORT_DELETE_ORIGINALS_EXPL' => 'Les fichiers originaux seront supprimés après l’importation lorsque cela est possible.', - 'UPLOAD_IMPORT_VIA_SYMLINK' => 'Symbolic links', - 'UPLOAD_IMPORT_VIA_SYMLINK_EXPL' => 'Import files using symbolic links to originals.', - 'UPLOAD_IMPORT_SKIP_DUPLICATES' => 'Skip duplicates', - 'UPLOAD_IMPORT_SKIP_DUPLICATES_EXPL' => 'Existing media files are skipped.', - 'UPLOAD_IMPORT_RESYNC_METADATA' => 'Re-sync metadata', - 'UPLOAD_IMPORT_RESYNC_METADATA_EXPL' => 'Update metadata of existing media files.', - 'UPLOAD_IMPORT_LOW_MEMORY' => 'Mémoire faible disponible !', - 'UPLOAD_IMPORT_LOW_MEMORY_EXPL' => 'Le processus d’importation du serveur approche la limite de la mémoire disponible et peut etre terminé prématurément.', - 'UPLOAD_WARNING' => 'Attention', - 'UPLOAD_IMPORT_NOT_A_DIRECTORY' => 'Le chemin fourni n’est pas un reportoire lisible !', - 'UPLOAD_IMPORT_PATH_RESERVED' => 'Le chemin fourni est reservé à Lychee !', - 'UPLOAD_IMPORT_UNREADABLE' => 'Impossible de lire le fichier !', - 'UPLOAD_IMPORT_FAILED' => 'Impossible d’importer le fichier !', - 'UPLOAD_IMPORT_UNSUPPORTED' => 'Type de fichier non supporté !', - 'UPLOAD_IMPORT_ALBUM_FAILED' => 'Impossible de créer l’album !', - 'UPLOAD_IMPORT_CANCELLED' => 'Import cancelled', - - 'ABOUT_SUBTITLE' => 'Hebergement personalisé de photo à votre façon !', - 'ABOUT_DESCRIPTION' => ' est une outil de gestion de gallerie gratuit qui fonctionne sur votre propre serveur. L’installation est rapide. Uploadez, gérez et partagez vos photos comme avec une application propre. Lychee vous fourni tout ce dont vous avez besoin et vos photos sont stockées en sécurité chez vous.', - 'FOOTER_COPYRIGHT' => 'Toutes les images de ce site Web sont protégées par le droit d’auteur par', - 'HOSTED_WITH_LYCHEE' => 'Herbergé avec Lychee', - - 'URL_COPY_TO_CLIPBOARD' => 'Copier dans le presse-papier', - 'URL_COPIED_TO_CLIPBOARD' => 'l’URL a été copiée dans le presse-papier !', - 'PHOTO_DIRECT_LINKS_TO_IMAGES' => 'Liens directs pour les fichier de l’image :', - 'PHOTO_MEDIUM' => 'Moyenne taille', - 'PHOTO_MEDIUM_HIDPI' => 'Moyenne taille HiDPI', - 'PHOTO_SMALL' => 'Petite taille', - 'PHOTO_SMALL_HIDPI' => 'Petite taille HiDPI', - 'PHOTO_THUMB' => 'Mignature carrée', - 'PHOTO_THUMB_HIDPI' => 'Mignature carrée HiDPI', - 'PHOTO_LIVE_VIDEO' => 'Partie vidéo d’une live-photo', - 'PHOTO_VIEW' => 'Vue photo de Lychee :', - - 'PHOTO_EDIT_ROTATECWISE' => 'Pivoter dans le sens des aiguilles d’une montre.', - 'PHOTO_EDIT_ROTATECCWISE' => 'Pivoter dans le sens contraire des aiguilles d’une montre.', - ]; - - return $locale; - } -} diff --git a/app/Locale/German.php b/app/Locale/German.php deleted file mode 100644 index da71c4bafe0..00000000000 --- a/app/Locale/German.php +++ /dev/null @@ -1,484 +0,0 @@ - 'Benutzername', - 'PASSWORD' => 'Kennwort', - 'ENTER' => 'Eingabe', - 'CANCEL' => 'Abbrechen', - 'SIGN_IN' => 'Anmelden', - 'CLOSE' => 'Schließen', - 'SETTINGS' => 'Einstellungen', - 'SEARCH' => 'Suchen ...', - 'MORE' => 'Mehr', - 'DEFAULT' => 'Standard', - - 'USERS' => 'Benutzer', - 'U2F' => 'U2F', - 'NOTIFICATIONS' => 'Notifications', - 'SHARING' => 'Freigabe', - 'CHANGE_LOGIN' => 'Anmeldung ändern', - 'CHANGE_SORTING' => 'Sortierung ändern', - 'SET_DROPBOX' => 'Dropbox einrichten', - 'ABOUT_LYCHEE' => 'Über Lychee', - 'DIAGNOSTICS' => 'Diagnose', - 'DIAGNOSTICS_GET_SIZE' => 'Speicherplatz-Nutzung abrufen', - 'LOGS' => 'Logs anzeigen', - 'SIGN_OUT' => 'Abmelden', - 'UPDATE_AVAILABLE' => 'Update verfügbar!', - 'MIGRATION_AVAILABLE' => 'Migration verfügbar!', - 'DEFAULT_LICENSE' => 'Standard-Lizenz für neue Uploads:', - 'SET_LICENSE' => 'Lizenz anwenden', - 'SET_OVERLAY_TYPE' => 'Setze Overlay', - 'SET_MAP_PROVIDER' => 'Speichere Provider für OpenStreetMap Karten', - - 'SAVE_RISK' => 'Änderungen speichern, ich kenne das Risiko!', - - 'SMART_ALBUMS' => 'Intelligente Alben', - 'SHARED_ALBUMS' => 'Freigegebene Alben', - 'ALBUMS' => 'Alben', - 'PHOTOS' => 'Bilder', - 'SEARCH_RESULTS' => 'Suchergebnisse', - - 'RENAME' => 'Umbenennen', - 'RENAME_ALL' => 'Ausgewählte umbenennen', - 'MERGE' => 'Zusammenführen', - 'MERGE_ALL' => 'Ausgewählte zusammenführen', - 'MAKE_PUBLIC' => 'Veröffentlichen', - 'SHARE_ALBUM' => 'Album freigeben', - 'SHARE_PHOTO' => 'Foto freigeben', - 'VISIBILITY_ALBUM' => 'Sichtbarkeit des Albums', - 'VISIBILITY_PHOTO' => 'Sichtbarkeit des Fotos', - 'DOWNLOAD_ALBUM' => 'Album herunterladen', - 'ABOUT_ALBUM' => 'Über dieses Album', - 'DELETE_ALBUM' => 'Album löschen', - 'MOVE_ALBUM' => 'Album verschieben', - 'FULLSCREEN_ENTER' => 'Vollbild', - 'FULLSCREEN_EXIT' => 'Vollbild beenden', - - 'SHARING_ALBUM_USERS' => 'Teile dieses Album mit Benutzern', - 'WAIT_FETCH_DATA' => 'Bitte warten Sie, während die Daten abgerufen werden...', - 'SHARING_ALBUM_USERS_NO_USERS' => 'Es sind keine Benutzer vorhanden, mit denen das Album geteilt werden kann', - 'SHARING_ALBUM_USERS_LONG_MESSAGE' => 'Wählen Sie die Benutzer aus, mit denen das Album geteilt werden soll', - - 'DELETE_ALBUM_QUESTION' => 'Album und Fotos löschen', - 'KEEP_ALBUM' => 'Album behalten', - 'DELETE_ALBUM_CONFIRMATION_1' => 'Sind Sie sicher, dass Sie das Album', - 'DELETE_ALBUM_CONFIRMATION_2' => 'und alle enthaltenen Fotos löschen wollen? Diese Aktion kann nicht rückgängig gemacht werden!', - - 'DELETE_ALBUMS_QUESTION' => 'Alben und Fotos löschen', - 'KEEP_ALBUMS' => 'Alben behalten', - 'DELETE_ALBUMS_CONFIRMATION_1' => 'Sind Sie sicher, dass Sie alle', - 'DELETE_ALBUMS_CONFIRMATION_2' => 'ausgewählten Alben und die enthaltenen Fotos löschen wollen? Diese Aktion kann nicht rückgängig gemacht werden!', - - 'DELETE_UNSORTED_CONFIRM' => 'Sind Sie sicher, dass Sie alle Fotos aus \'Unsortiert\' löschen wollen?
Diese Aktion kann nicht rückgängig gemacht werden!', - 'CLEAR_UNSORTED' => 'Unsortierte löschen', - 'KEEP_UNSORTED' => 'Unsortierte behalten', - - 'EDIT_SHARING' => 'Freigabe bearbeiten', - 'MAKE_PRIVATE' => 'Privat', - - 'CLOSE_ALBUM' => 'Album schließen', - 'CLOSE_PHOTO' => 'Foto schließen', - 'CLOSE_MAP' => 'Karte schließen', - - 'ADD' => 'Hinzufügen', - 'MOVE' => 'Verschieben', - 'MOVE_ALL' => 'Ausgewählte verschieben', - 'DUPLICATE' => 'Duplizieren', - 'DUPLICATE_ALL' => 'Ausgewählte duplizieren', - 'COPY_TO' => 'Kopieren nach...', - 'COPY_ALL_TO' => 'Ausgewählte kopieren nach...', - 'DELETE' => 'Löschen', - 'DELETE_ALL' => 'Ausgewählte löschen', - 'DOWNLOAD' => 'Herunterladen', - 'DOWNLOAD_ALL' => 'Ausgewählte herunterladen', - 'UPLOAD_PHOTO' => 'Foto hochladen', - 'IMPORT_LINK' => 'Aus Link importieren', - 'IMPORT_DROPBOX' => 'Aus Dropbox importieren', - 'IMPORT_SERVER' => 'Von Server importieren', - 'NEW_ALBUM' => 'Neues Album', - 'NEW_TAG_ALBUM' => 'Neues Tag-Album', - - 'TITLE_NEW_ALBUM' => 'Geben Sie einen Titel für das neue Album ein:', - 'UNTITLED' => 'Unbenannt', - 'UNSORTED' => 'Unsortiert', - 'STARRED' => 'Favoriten', - 'RECENT' => 'Zuletzt benutzt', - 'PUBLIC' => 'Öffentlich', - 'NUM_PHOTOS' => 'Fotos', - - 'CREATE_ALBUM' => 'Album erstellen', - 'CREATE_TAG_ALBUM' => 'Neues Tag-Album erstellen', - - 'STAR_PHOTO' => 'Foto als Favorit markieren', - 'STAR' => 'Als Favorit markieren', - 'STAR_ALL' => 'Ausgewählte als Favoriten markieren', - 'TAGS' => 'Taggen', - 'TAGS_ALL' => 'Ausgewählte Taggen', - 'UNSTAR_PHOTO' => 'Foto von Favoriten entfernen', - 'SET_COVER' => 'Setze Album Cover', - 'REMOVE_COVER' => 'Entferne Album Cover', - - 'FULL_PHOTO' => 'Original öffnen', - 'ABOUT_PHOTO' => 'Über dieses Foto', - 'DISPLAY_FULL_MAP' => 'Karte', - 'DIRECT_LINK' => 'Direkter Link', - 'DIRECT_LINKS' => 'Direkte Links', - - 'ALBUM_ABOUT' => 'Über', - 'ALBUM_BASICS' => 'Grundlegende Informationen', - 'ALBUM_TITLE' => 'Titel', - 'ALBUM_NEW_TITLE' => 'Geben Sie einen neuen Titel für dieses Album ein:', - 'ALBUMS_NEW_TITLE_1' => 'Geben Sie einen Titel für alle', - 'ALBUMS_NEW_TITLE_2' => 'ausgewählten Alben ein:', - 'ALBUM_SET_TITLE' => 'Titel speichern', - 'ALBUM_DESCRIPTION' => 'Beschreibung', - 'ALBUM_SHOW_TAGS' => 'Tags zum anzeigen', - 'ALBUM_NEW_DESCRIPTION' => 'Geben Sie eine neue Beschreibung für dieses Album ein:', - 'ALBUM_SET_DESCRIPTION' => 'Beschreibung speichern', - 'ALBUM_NEW_SHOWTAGS' => 'Gebe Tags der Bilder ein, die in diesem Album sichtbar sein sollen:', - 'ALBUM_SET_SHOWTAGS' => 'Setze Tags zum anschauen', - 'ALBUM_ALBUM' => 'Album', - 'ALBUM_CREATED' => 'Erstellt', - 'ALBUM_IMAGES' => 'Bilder', - 'ALBUM_VIDEOS' => 'Videos', - 'ALBUM_SUBALBUMS' => 'Unteralben', - 'ALBUM_SHARING' => 'Teilen', - 'ALBUM_SHR_YES' => 'Ja', - 'ALBUM_SHR_NO' => 'Nein', - 'ALBUM_PUBLIC' => 'Öffentlich', - 'ALBUM_PUBLIC_EXPL' => 'Dieses Album kann, abhängig von den Einstellungen unten, von anderen betrachtet werden.', - 'ALBUM_FULL' => 'Original', - 'ALBUM_FULL_EXPL' => 'Original Bilder sind verfügbar.', - 'ALBUM_HIDDEN' => 'Versteckt', - 'ALBUM_HIDDEN_EXPL' => 'Nur Personen mit dem direkten Link können dieses Album ansehen.', - 'ALBUM_MARK_NSFW' => 'Markiere Album als sensibel', - 'ALBUM_UNMARK_NSFW' => 'Entferne Markierung des Albums als sensibel', - 'ALBUM_NSFW' => 'Sensibel', - 'ALBUM_NSFW_EXPL' => 'Album enthält sensible Inhalte.', - 'ALBUM_DOWNLOADABLE' => 'Zum Herunterladen', - 'ALBUM_DOWNLOADABLE_EXPL' => 'Besucher können dieses Album herunterladen.', - 'ALBUM_SHARE_BUTTON_VISIBLE' => 'Teilen-Button ist sichtbar', - 'ALBUM_SHARE_BUTTON_VISIBLE_EXPL' => 'Zeige Links zum Teilen in sozialen Medien .', - 'ALBUM_PASSWORD' => 'Kennwort', - 'ALBUM_PASSWORD_PROT' => 'Kennwortgeschützt', - 'ALBUM_PASSWORD_PROT_EXPL' => 'Album nur einsehbar mit gültigem Kennwort.', - 'ALBUM_PASSWORD_REQUIRED' => 'Dieses Album ist mit einem Kennwort geschützt. Geben Sie unten das Kennwort ein, um das Album anzusehen:', - 'ALBUM_MERGE_1' => 'Sind Sie sicher, dass Sie das Album', - 'ALBUM_MERGE_2' => 'mit diesem Album zusammenführen wollen', - 'ALBUMS_MERGE' => 'Sind Sie sicher, dass Sie alle ausgewählten Alben mit diesem Album zusammenführen möchten?', - 'MERGE_ALBUM' => 'Alben zusammenführen', - 'DONT_MERGE' => 'Nicht zusammenführen', - 'ALBUM_MOVE_1' => 'Sind Sie sicher, dass Sie das Album', - 'ALBUM_MOVE_2' => 'in folgendes Album verschieben möchten?', - 'ALBUMS_MOVE' => 'Sind Sie sicher, dass Sie die ausgewählten Alben in folgendes Album verschieben wollen?', - 'MOVE_ALBUMS' => 'Alben verschieben', - 'NOT_MOVE_ALBUMS' => 'Nicht verschieben', - 'ROOT' => 'Alben', - 'ALBUM_REUSE' => 'Weiterverwendung', - 'ALBUM_LICENSE' => 'Lizenz', - 'ALBUM_SET_LICENSE' => 'Lizenz festlegen', - 'ALBUM_LICENSE_HELP' => 'Benötigen Sie Hilfe bei der Auswahl?', - 'ALBUM_LICENSE_NONE' => 'Keine', - 'ALBUM_RESERVED' => 'Alle Rechte vorbehalten', - 'ALBUM_SET_ORDER' => 'Reihenfolge festlegen', - 'ALBUM_ORDERING' => 'Sortieren nach', - - 'PHOTO_ABOUT' => 'Über', - 'PHOTO_BASICS' => 'Grundlegende Informationen', - 'PHOTO_TITLE' => 'Titel', - 'PHOTO_NEW_TITLE' => 'Geben Sie einen neuen Titel für dieses Foto ein:', - 'PHOTO_SET_TITLE' => 'Titel speichern', - 'PHOTO_UPLOADED' => 'Hochgeladen', - 'PHOTO_DESCRIPTION' => 'Beschreibung', - 'PHOTO_NEW_DESCRIPTION' => 'Geben Sie eine neue Beschreibung für dieses Foto ein:', - 'PHOTO_SET_DESCRIPTION' => 'Beschreibung speichern', - 'PHOTO_NEW_LICENSE' => 'Neue Lizenz hinzufügen', - 'PHOTO_SET_LICENSE' => 'Lizenz festlegen', - 'PHOTO_LICENSE' => 'Lizenz', - 'PHOTO_REUSE' => 'Weiterverwendung', - 'PHOTO_LICENSE_NONE' => 'Keine', - 'PHOTO_RESERVED' => 'Alle Rechte vorbehalten', - 'PHOTO_LATITUDE' => 'Breite', - 'PHOTO_LONGITUDE' => 'Länge', - 'PHOTO_ALTITUDE' => 'Höhe', - 'PHOTO_IMGDIRECTION' => 'Richtung', - 'PHOTO_LOCATION' => 'Ort', - 'PHOTO_IMAGE' => 'Bild', - 'PHOTO_VIDEO' => 'Video', - 'PHOTO_SIZE' => 'Größe', - 'PHOTO_FORMAT' => 'Format', - 'PHOTO_RESOLUTION' => 'Auflösung', - 'PHOTO_DURATION' => 'Dauer', - 'PHOTO_FPS' => 'Bilder pro Sekunde', - 'PHOTO_TAGS' => 'Tags', - 'PHOTO_NOTAGS' => 'Keine Tags', - 'PHOTO_NEW_TAGS' => 'Geben Sie die Tags für dieses Foto ein. Sie können mehrere Tags hinzufügen, indem Sie sie mit einem Komma trennen:', - 'PHOTO_NEW_TAGS_1' => 'Geben Sie die Tags für alle', - 'PHOTO_NEW_TAGS_2' => 'ausgewählten Fotos ein. Bestehende Tags werden überschrieben. Sie können mehrere Tags hinzufügen, indem Sie sie mit einem Komma trennen:', - 'PHOTO_SET_TAGS' => 'Tags speichern', - 'PHOTO_CAMERA' => 'Kamera', - 'PHOTO_CAPTURED' => 'Aufgenommen', - 'PHOTO_MAKE' => 'Marke', - 'PHOTO_TYPE' => 'Typ/Modell', - 'PHOTO_LENS' => 'Objektiv', - 'PHOTO_SHUTTER' => 'Verschlusszeit', - 'PHOTO_APERTURE' => 'Blende', - 'PHOTO_FOCAL' => 'Brennweite', - 'PHOTO_ISO' => 'ISO', - 'PHOTO_SHARING' => 'Teilen', - 'PHOTO_SHR_PLUBLIC' => 'Öffentlich', - 'PHOTO_SHR_ALB' => 'Ja (Album)', - 'PHOTO_SHR_PHT' => 'Ja (Foto)', - 'PHOTO_SHR_NO' => 'Nein', - 'PHOTO_DELETE' => 'Foto löschen', - 'PHOTO_KEEP' => 'Foto behalten', - 'PHOTO_DELETE_1' => 'Sind Sie sicher, dass Sie das Foto', - 'PHOTO_DELETE_2' => 'löschen wollen? Diese Aktion kann nicht rückgängig gemacht werden!', - 'PHOTO_DELETE_ALL_1' => 'Sind Sie sicher, dass Sie alle', - 'PHOTO_DELETE_ALL_2' => 'ausgewählten Fotos löschen wollen? Diese Aktion kann nicht rückgängig gemacht werden!', - 'PHOTOS_NEW_TITLE_1' => '', - 'PHOTOS_NEW_TITLE_2' => 'ausgewählten Fotos ein:', - 'PHOTO_MAKE_PRIVATE_ALBUM' => 'Dieses Foto befindet sich in einem öffentlichen Album. Um dieses Foto als privat oder öffentlich zu markieren, bearbeiten Sie die Sichtbarkeit des übergeordneten Albums.', - 'PHOTO_SHOW_ALBUM' => 'Album anzeigen', - 'PHOTO_PUBLIC' => 'Öffentlich', - 'PHOTO_PUBLIC_EXPL' => 'Fotos können, abhängig von den Einstellungen unten, von anderen betrachtet werden.', - 'PHOTO_FULL' => 'Original', - 'PHOTO_FULL_EXPL' => 'Original Bild verfügbar.', - 'PHOTO_HIDDEN' => 'Versteckt', - 'PHOTO_HIDDEN_EXPL' => 'Nur Besucher mit dem direkten Link können dieses Foto sehen.', - 'PHOTO_DOWNLOADABLE' => 'Herunterladbar', - 'PHOTO_DOWNLOADABLE_EXPL' => 'Besucher der Gallerie können dieses Foto herunterladen.', - 'PHOTO_SHARE_BUTTON_VISIBLE' => 'Teilen-Button ist sichtbar', - 'PHOTO_SHARE_BUTTON_VISIBLE_EXPL' => 'Zeige Links zum Teilen in sozialen Medien.', - 'PHOTO_PASSWORD_PROT' => 'Passwortgeschützt', - 'PHOTO_PASSWORD_PROT_EXPL' => 'Foto nur mit gültigen Passwort verfügbar.', - 'PHOTO_EDIT_SHARING_TEXT' => 'Die Einstellungen zum Teilen des Foto werden wie folgt angepasst:', - 'PHOTO_NO_EDIT_SHARING_TEXT' => 'Dieses Foto ist in einem öffentlichen Album und erbt deshalb die Sichtbarkeitseinstellungen des Albums. Die aktuellen Sichtbarkeitseinstellungen werden unten nur zur Info dargestellt.', - 'PHOTO_EDIT_GLOBAL_SHARING_TEXT' => 'Die Sichtbarkeit dieses Fotos kann über die globalen Lychee Einstellungen modifiziert werden. Die aktuellen Sichtbarkeitseinstellungen werden unten nur zur Info dargestellt.', - 'PHOTO_SHARING_CONFIRM' => 'Speichern', - - 'LOADING' => 'Laden', - 'ERROR' => 'Fehler', - 'ERROR_TEXT' => 'Hoppla, da ist etwas schiefgegangen. Bitte laden Sie die Seite erneut und probieren Sie es noch einmal!', - 'ERROR_DB_1' => 'Kann keine Verbindung zur Datenbank herstellen, weil der Zugriff verweigert wurde. Überprüfen Sie Host, Benutzername und Kennwort und stellen Sie sicher, dass der Zugriff von Ihrem momentanen Standort erlaubt ist.', - 'ERROR_DB_2' => 'Kann die Datenbank nicht erstellen. Überprüfen Sie Host, Benutzername und Kennwort und stellen Sie sicher, dass der angegebene Benutzer Inhalte zur Datenbank hinzufügen darf.', - 'ERROR_CONFIG_FILE' => "Kann diese Konfiguration nicht speichern. Zugriff verweigert auf 'data/'. Bitte setzen Sie die Schreibrechte auf 'data/' and 'uploads/'. Lesen Sie die README-Datei für mehr Informationen.", - 'ERROR_UNKNOWN' => 'Etwas Unerwartetes ist passiert. Bitte probieren Sie es erneut und überprüfen Sie die Installation und Ihren Server. Lesen Sie die README-Datei für mehr Informationen.', - 'ERROR_LOGIN' => 'Kann Login nicht speichern. Bitte versuchen Sie es erneut mit einem anderen Benutzernamen und Kennwort!', - 'ERROR_MAP_DEACTIVATED' => 'Karten sind unter Einstellungen deaktiviert worden.', - 'ERROR_SEARCH_DEACTIVATED' => 'Suchfunktion wurde unter Einstellungen deaktiviert.', - 'SUCCESS' => 'OK', - 'RETRY' => 'Noch einmal versuchen', - - 'SETTINGS_WARNING' => 'Ändern dieser erweiterten Einstellungen kann sich negativ auf die Stabilität, Sicherheit und Geschwindigkeit dieser Anwendung auswirken. Sie sollten sie nur ändern, wenn Sie genau wissen, was Sie tun.', - 'SETTINGS_SUCCESS_LOGIN' => 'Benutzerdaten aktualisiert', - 'SETTINGS_SUCCESS_SORT' => 'Sortierreihenfolge aktualisiert', - 'SETTINGS_SUCCESS_DROPBOX' => 'Dropbox-Schlüssel aktualisiert', - 'SETTINGS_SUCCESS_LANG' => 'Sprache aktualisiert', - 'SETTINGS_SUCCESS_LAYOUT' => 'Layout aktualisiert', - 'SETTINGS_SUCCESS_IMAGE_OVERLAY' => 'EXIF-Overlay-Einstellungen aktualisiert', - 'SETTINGS_SUCCESS_PUBLIC_SEARCH' => 'Öffentliche Suche geändert', - 'SETTINGS_SUCCESS_LICENSE' => 'Standard-Lizenz aktualisiert', - 'SETTINGS_SUCCESS_MAP_DISPLAY' => 'Karteneinstellungen erfolgreich aktualisiert', - 'SETTINGS_SUCCESS_MAP_DISPLAY_PUBLIC' => 'Karteneinstellungen für öffentlichen Alben erfolgreich aktualisiert', - 'SETTINGS_SUCCESS_MAP_PROVIDER' => 'Provider für Karten erfolgreich aktualisiert', - 'SETTINGS_SUCCESS_CSS' => 'CSS aktualisiert', - 'SETTINGS_SUCCESS_UPDATE' => 'Einstellungen erfolgreich aktualisiert', - - 'U2F_NOT_SUPPORTED' => 'U2F wird nicht unterstützt. Sorry.', - 'U2F_NOT_SECURE' => 'Umgebung ist nicht sicher. U2F ist nicht verfügbar.', - 'U2F_REGISTER_KEY' => 'Neues Gerät registrieren.', - 'U2F_REGISTRATION_SUCCESS' => 'Registrierung erfolgreich!', - 'U2F_AUTHENTIFICATION_SUCCESS' => 'Authentifizierung erfolgreich!', - 'U2F_CREDENTIALS' => 'Anmeldedaten', - 'U2F_CREDENTIALS_DELETED' => 'Anmeldedaten gelöscht!', - - 'NEW_PHOTOS_NOTIFICATION' => 'Send new photos notification emails.', - 'SETTINGS_SUCCESS_NEW_PHOTOS_NOTIFICATION' => 'New photos notification updated', - 'USER_EMAIL_INSTRUCTION' => 'Add your email below to enable receiving email notifications.
To stop receiving emails, simply remove your email below.', - - 'DB_INFO_TITLE' => 'Geben Sie die Informationen zu Ihrer Datenbankverbindung an:', - 'DB_INFO_HOST' => 'Name des Datenbankservers (optional)', - 'DB_INFO_USER' => 'Benutzername für die Datenbank', - 'DB_INFO_PASSWORD' => 'Kennwort für die Datenbank', - 'DB_INFO_TEXT' => 'Lychee wird seine eigene Datenbank erstellen. Falls erforderlich, können Sie stattdessen den Namen einer bestehenden Datenbank angeben:', - 'DB_NAME' => 'Name der Datenbank (optional)', - 'DB_PREFIX' => 'Präfix für den Tabellennamen (optional)', - 'DB_CONNECT' => 'Verbinden', - - 'LOGIN_TITLE' => 'Geben Sie Benutzername und Kennwort für Ihre Installation an:', - 'LOGIN_USERNAME' => 'Neuer Benutzername', - 'LOGIN_PASSWORD' => 'Neues Kennwort', - 'LOGIN_PASSWORD_CONFIRM' => 'Confirm Password', - 'LOGIN_CREATE' => 'Benutzer anlegen', - - 'PASSWORD_TITLE' => 'Geben Sie Ihr bestehendes Kennwort ein:', - 'USERNAME_CURRENT' => 'Bestehender Benutzername', - 'PASSWORD_CURRENT' => 'Bestehendes Kennwort', - 'PASSWORD_TEXT' => 'Ihr Benutzername und Passwort werden wie folgt geändert:', - 'PASSWORD_CHANGE' => 'Benutzer ändern', - - 'EDIT_SHARING_TITLE' => 'Freigabe bearbeiten', - 'EDIT_SHARING_TEXT' => 'Die Freigabeeinstellungen für dieses Album werden wie folgt geändert:', - 'SHARE_ALBUM_TEXT' => 'Dieses Album wird mit folgenden Einstellungen freigegeben:', - 'ALBUM_SHARING_CONFIRM' => 'Speichern', - - 'SORT_ALBUM_BY_1' => 'Alben sortieren nach', - 'SORT_ALBUM_BY_2' => 'in einer', - 'SORT_ALBUM_BY_3' => 'Reihenfolge.', - - 'SORT_ALBUM_SELECT_1' => 'Erstellungszeitpunkt', - 'SORT_ALBUM_SELECT_2' => 'Titel', - 'SORT_ALBUM_SELECT_3' => 'Beschreibung', - 'SORT_ALBUM_SELECT_4' => 'Öffentlich', - 'SORT_ALBUM_SELECT_5' => 'Neuestes Aufnahmedatum', - 'SORT_ALBUM_SELECT_6' => 'Ältestes Aufnahmedatum', - - 'SORT_PHOTO_BY_1' => 'Fotos sortieren nach', - 'SORT_PHOTO_BY_2' => 'in einer', - 'SORT_PHOTO_BY_3' => 'Reihenfolge.', - - 'SORT_PHOTO_SELECT_1' => 'Zeitpunkt des Hochladens', - 'SORT_PHOTO_SELECT_2' => 'Aufnahmedatum', - 'SORT_PHOTO_SELECT_3' => 'Titel', - 'SORT_PHOTO_SELECT_4' => 'Beschreibung', - 'SORT_PHOTO_SELECT_5' => 'Öffentlich', - 'SORT_PHOTO_SELECT_6' => 'Favorit', - 'SORT_PHOTO_SELECT_7' => 'Fotoformat', - - 'SORT_ASCENDING' => 'aufsteigenden', - 'SORT_DESCENDING' => 'absteigenden', - 'SORT_CHANGE' => 'Sortierung ändern', - - 'DROPBOX_TITLE' => 'Dropbox-Schlüssel festlegen', - 'DROPBOX_TEXT' => "Um Ihre Fotos von Dropbox zu importieren, brauchen Sie einen gültigen API-Key von der Dropbox-Webseite. Erstellen Sie einen persönlichen Schlüssel und geben Sie ihn darunter ein:", - - 'LANG_TEXT' => 'Sprache für Lychee ändern:', - 'LANG_TITLE' => 'Sprache festlegen', - - 'CSS_TEXT' => 'CSS personalisieren:', - 'CSS_TITLE' => 'CSS ändern', - 'PUBLIC_SEARCH_TEXT' => 'Öffentliche Suche erlauben:', - 'OVERLAY_TYPE' => 'Daten für Foto-Overlay:', - 'OVERLAY_NONE' => 'Kein Overlay', - 'OVERLAY_EXIF' => 'EXIF Daten des Fotos', - 'OVERLAY_DESCRIPTION' => 'Beschreibung des Fotos', - 'OVERLAY_DATE' => 'Erstellungsdatum des Fotos', - 'MAP_DISPLAY_TEXT' => 'Kartenfunktionalitäten aktivieren (OpenStreetMap):', - 'MAP_DISPLAY_PUBLIC_TEXT' => 'Kartenfunktionalität für öffentliche Alben aktivieren (OpenStreetMap):', - 'LOCATION_DECODING' => 'Ortsnamen mittels GPS Daten bestimmen', - 'LOCATION_SHOW' => 'Zeige Ortsnamen', - 'LOCATION_SHOW_PUBLIC' => 'Zeige Ortsnamen für öffentliche Alben', - 'MAP_PROVIDER' => 'Provider für OpenStreetMap Karten:', - 'MAP_PROVIDER_WIKIMEDIA' => 'Wikimedia', - 'MAP_PROVIDER_OSM_ORG' => 'OpenStreetMap.org (kein HiDPI)', - 'MAP_PROVIDER_OSM_DE' => 'OpenStreetMap.de (kein HiDPI)', - 'MAP_PROVIDER_OSM_FR' => 'OpenStreetMap.fr (kein HiDPI)', - 'MAP_PROVIDER_RRZE' => 'Universtät Erlangen, Deutschland (nur HiDPI)', - 'MAP_INCLUDE_SUBALBUMS_TEXT' => 'Fotos von Unterordnern für Karten berücksichtigen:', - - 'LAYOUT_TYPE' => 'Layout des Fotos:', - 'LAYOUT_SQUARES' => 'Quadratische Miniaturansichten', - 'LAYOUT_JUSTIFIED' => 'Seitenverhältnis beibehalten, Blocksatz', - 'LAYOUT_UNJUSTIFIED' => 'Seitenverhältnis beibehalten, Flattersatz', - 'SET_LAYOUT' => 'Ausgerichtetes Layout benutzen:', - - 'NSFW_VISIBLE_TEXT_1' => 'Setzte sensible Alben standardmäßig auf sichtbar.', - 'NSFW_VISIBLE_TEXT_2' => 'Wenn das Album öffentlich ist, kann weiterhin zugegriffen werden. Es wird nur ausgeblendet und kann durch Pressen der Taste H sichtbar gemacht werden..', - 'SETTINGS_SUCCESS_NSFW_VISIBLE' => 'Standardmäßige Sichtbarkeit wurde erfolgreich geändert.', - - 'VIEW_NO_RESULT' => 'Keine Ergebnisse', - 'VIEW_NO_PUBLIC_ALBUMS' => 'Keine öffentlichen Alben', - 'VIEW_NO_CONFIGURATION' => 'Keine Konfiguration', - 'VIEW_PHOTO_NOT_FOUND' => 'Foto nicht gefunden', - - 'NO_TAGS' => 'Keine Tags', - - 'UPLOAD_MANAGE_NEW_PHOTOS' => 'Sie können jetzt Ihre neuen Fotos verwalten.', - 'UPLOAD_COMPLETE' => 'Hochladen abgeschlossen', - 'UPLOAD_COMPLETE_FAILED' => 'Fehler beim Hochladen eines oder mehrerer Fotos.', - 'UPLOAD_IMPORTING' => 'Importieren', - 'UPLOAD_IMPORTING_URL' => 'URL importieren', - 'UPLOAD_UPLOADING' => 'Hochladen', - 'UPLOAD_FINISHED' => 'Beendet', - 'UPLOAD_PROCESSING' => 'Verarbeiten', - 'UPLOAD_FAILED' => 'Fehlgeschlagen', - 'UPLOAD_FAILED_ERROR' => 'Hochladen fehlgeschlagen. Der Server hat einen Fehler gemeldet!', - 'UPLOAD_FAILED_WARNING' => 'Hochladen fehlgeschlagen. Der Server hat eine Warnung ausgegeben!', - 'UPLOAD_CANCELLED' => 'Abgebrochen', - 'UPLOAD_SKIPPED' => 'Übersprungen', - 'UPLOAD_UPDATED' => 'Upgedatet', - 'UPLOAD_IMPORT_SKIPPED_DUPLICATE' => 'Dieses Foto wurde übersprungen, da es bereits in deiner Bibliothek vorhanden ist.', - 'UPLOAD_IMPORT_RESYNCED_DUPLICATE' => 'Dieses Foto wurde übersprungen, da es bereits in deiner Bibliothek vorhanden ist, jedoch wurden die Metadaten upgedatet.', - 'UPLOAD_ERROR_CONSOLE' => 'Bitte schauen Sie in die Konsole Ihres Browsers, um weiter Details zu erfahren.', - 'UPLOAD_UNKNOWN' => 'Der Server hat eine unbekannte Antwort gegeben. Bitte schauen Sie in die Konsole Ihres Browsers, um weiter Details zu erfahren.', - 'UPLOAD_ERROR_UNKNOWN' => 'Hochladen fehlgeschlagen. Der Server hat einen unbekannten Fehler gemeldet!', - 'UPLOAD_ERROR_POSTSIZE' => 'Upload fehlgeschlagen. Die PHP post_max_size limit ist zu klein!', - 'UPLOAD_ERROR_FILESIZE' => 'Upload fehlgeschlagen. The PHP upload_max_filesize limit ist zu klein!', - 'UPLOAD_IN_PROGRESS' => 'Lychee ist gerade beim Hochladen!', - 'UPLOAD_IMPORT_WARN_ERR' => 'Der Import ist fertig, hat aber Warnungen oder Fehler zurückgegeben. Schauen Sie bitte ins Protokoll (Einstellungen/Protokoll ansehen).', - 'UPLOAD_IMPORT_COMPLETE' => 'Import abgeschlossen', - 'UPLOAD_IMPORT_INSTR' => 'Geben Sie bitte den direkten Link ein, um ihn zu importieren:', - 'UPLOAD_IMPORT' => 'Importieren', - 'UPLOAD_IMPORT_SERVER' => 'Importieren von Server', - 'UPLOAD_IMPORT_SERVER_FOLD' => 'Der Ordner ist leer oder enthält keine lesbaren Dateien zum Verarbeiten. Schauen Sie bitte ins Protokoll (Einstellungen/Protokoll ansehen).', - 'UPLOAD_IMPORT_SERVER_INSTR' => 'Diese Aktion wird alle Fotos, Ordner und Unterordner importieren, die sich in folgendem Verzeichnis befinden.', - 'UPLOAD_ABSOLUTE_PATH' => 'Absoluter Pfad zum Verzeichnis', - 'UPLOAD_IMPORT_SERVER_EMPT' => 'Konnte Import nicht starten, weil der Ordner leer ist.', - 'UPLOAD_IMPORT_DELETE_ORIGINALS' => 'Originale löschen', - 'UPLOAD_IMPORT_DELETE_ORIGINALS_EXPL' => 'Die Originaldateien werden nach dem Import gelöscht, falls möglich.', - 'UPLOAD_IMPORT_VIA_SYMLINK' => 'Symbolischer Link', - 'UPLOAD_IMPORT_VIA_SYMLINK_EXPL' => 'Importiere Dateien durch symbolische Links zu den Originalen.', - 'UPLOAD_IMPORT_SKIP_DUPLICATES' => 'Überspringe Duplikate', - 'UPLOAD_IMPORT_SKIP_DUPLICATES_EXPL' => 'Bestehende Medien-Dateien wurden übersprungen.', - 'UPLOAD_IMPORT_RESYNC_METADATA' => 'Synchronisiere Metadaten erneut', - 'UPLOAD_IMPORT_RESYNC_METADATA_EXPL' => 'Update Metadaten der bestehenden Medien-Dateien.', - 'UPLOAD_IMPORT_LOW_MEMORY' => 'Sehr wenig Speicher!', - 'UPLOAD_IMPORT_LOW_MEMORY_EXPL' => 'Der Importprozess auf dem Server nähert sich dem Speicherlimit und wird eventuell vorzeitig beendet.', - 'UPLOAD_WARNING' => 'Warnung', - 'UPLOAD_IMPORT_NOT_A_DIRECTORY' => 'Der angegebene Pfad ist kein lesbares Verzeichnis!', - 'UPLOAD_IMPORT_PATH_RESERVED' => 'Der angegebene Pfad ist ein von Lychee reservierter Pfad!', - 'UPLOAD_IMPORT_UNREADABLE' => 'Die Datei konnte nicht gelesen werden!', - 'UPLOAD_IMPORT_FAILED' => 'The Datei konnte nicht importiert werden!', - 'UPLOAD_IMPORT_UNSUPPORTED' => 'Dateityp wird nicht unterstützt!', - 'UPLOAD_IMPORT_ALBUM_FAILED' => 'Album konnte nicht erstellt werden!', - 'UPLOAD_IMPORT_CANCELLED' => 'Import abgebrochen', - - 'ABOUT_SUBTITLE' => 'Selbst gehostetes Foto-Management, aber richtig!', - 'ABOUT_DESCRIPTION' => 'ist ein freies Foto-Management-Werkzeug, dass auf Ihrem Server oder Webspace läuft. Die Installation ist eine Sache von Sekunden. Hochladen, Organisieren und Teilen von Fotos funktioniert wie in einer nativen Anwendung. Lychee hält alles bereit, was Sie benötigen, und alle Bilder werden sicher abgespeichert.', - 'FOOTER_COPYRIGHT' => 'Alle Bilder auf dieser Website unterliegen dem Copyright von ', - 'HOSTED_WITH_LYCHEE' => 'Hosted with Lychee', - - 'URL_COPY_TO_CLIPBOARD' => 'In die Zwischenablage kopiert', - 'URL_COPIED_TO_CLIPBOARD' => 'URL in die Zwischenablage kopiert!', - 'PHOTO_DIRECT_LINKS_TO_IMAGES' => 'Direkte Links zu den Bilddateien:', - 'PHOTO_MEDIUM' => 'Mittlere Größe', - 'PHOTO_MEDIUM_HIDPI' => 'Mittlere Größe HiDPI', - 'PHOTO_SMALL' => 'Miniaturansicht', - 'PHOTO_SMALL_HIDPI' => 'Miniaturansicht HiDPI', - 'PHOTO_THUMB' => 'Quadratische Miniaturansicht', - 'PHOTO_THUMB_HIDPI' => 'Quadratische Miniaturansicht HiDPI', - 'PHOTO_LIVE_VIDEO' => 'Video des Live-Photos', - 'PHOTO_VIEW' => 'Lychee Foto Ansicht:', - - 'PHOTO_EDIT_ROTATECWISE' => 'Im Uhrzeigersinn drehen', - 'PHOTO_EDIT_ROTATECCWISE' => 'Gegen den Uhrzeigersinn drehen', - ]; - - return $locale; - } -} diff --git a/app/Locale/Greek.php b/app/Locale/Greek.php deleted file mode 100644 index 2dd2f89c13a..00000000000 --- a/app/Locale/Greek.php +++ /dev/null @@ -1,475 +0,0 @@ - 'όνομα χρήστη', - 'PASSWORD' => 'κωδικός πρόσβασης', - 'ENTER' => 'Είσοδος', - 'CANCEL' => 'Άκυρο', - 'SIGN_IN' => 'Συνδεθείτε', - 'CLOSE' => 'Κλείσιμο', - 'SETTINGS' => 'Ρυθμίσεις', - 'SEARCH' => 'Αναζήτηση ...', - 'MORE' => 'Περισσότερα', - 'DEFAULT' => 'Default', - - 'USERS' => 'Χρήστες', - 'U2F' => 'U2F', - 'NOTIFICATIONS' => 'Notifications', - 'SHARING' => 'Κοινή χρήση', - 'CHANGE_LOGIN' => 'Αλλαγή σύνδεσης', - 'CHANGE_SORTING' => 'Αλλαγή Ταξινόμησης', - 'SET_DROPBOX' => 'Ορίστε λογαριασμό Dropbox', - 'ABOUT_LYCHEE' => 'Περί Lychee', - 'DIAGNOSTICS' => 'Διαγνωστικά', - 'DIAGNOSTICS_GET_SIZE' => 'Request space usage', - 'LOGS' => 'Εμφάνιση Καταγραφών', - 'SIGN_OUT' => 'Αποσύνδεση', - 'UPDATE_AVAILABLE' => 'Διαθέσιμη Ενημέρωση!', - 'MIGRATION_AVAILABLE' => 'Migration available!', - 'DEFAULT_LICENSE' => 'Προεπιλεγμένη άδεια για τις νέες μεταφορτώσεις:', - 'SET_LICENSE' => 'Ορισμός Άδειας', - 'SET_OVERLAY_TYPE' => 'Ορισμός Τύπου Overlay', - 'SET_MAP_PROVIDER' => 'Set OpenStreetMap tiles provider', - - 'SMART_ALBUMS' => 'Έξυπνα λευκώματα', - 'SHARED_ALBUMS' => 'Κοινόχρηστα λευκώματα', - 'ALBUMS' => 'Λευκώματα', - 'PHOTOS' => 'Εικόνες', - 'SEARCH_RESULTS' => 'Search results', - - 'RENAME' => 'Μετονομασία', - 'RENAME_ALL' => 'Μετονομασία Επιλεγμένων', - 'MERGE' => 'Συγχώνευση', - 'MERGE_ALL' => 'Συγχώνευση Επιλεγμένων', - 'MAKE_PUBLIC' => 'Κάντε το Δημόσιο', - 'SHARE_ALBUM' => 'Κοινή χρήση Λευκώματος', - 'SHARE_PHOTO' => 'Κοινή χρήση Φωτογραφίας', - 'VISIBILITY_ALBUM' => 'Ορατότητα Λευκώματος', - 'VISIBILITY_PHOTO' => 'Ορατότητα Φωτογραφίας', - 'DOWNLOAD_ALBUM' => 'Λήψη Λευκώματος', - 'ABOUT_ALBUM' => 'Πληροφορίες Λευκώματος', - 'DELETE_ALBUM' => 'Διαγραφή Λευκώματος', - 'MOVE_ALBUM' => 'Μετακίνηση Λευκώματος', - 'FULLSCREEN_ENTER' => 'Εισέλθετε σε λειτουργία Πλήρης Οθόνης', - 'FULLSCREEN_EXIT' => 'Εξέλθετε από λειτουργία Πλήρης Οθόνης', - - 'SHARING_ALBUM_USERS' => 'Share this album with users', - 'WAIT_FETCH_DATA' => 'Please wait while we get the data...', - 'SHARING_ALBUM_USERS_NO_USERS' => 'There are no users to share the album with', - 'SHARING_ALBUM_USERS_LONG_MESSAGE' => 'Select the users to share this album with', - - 'DELETE_ALBUM_QUESTION' => 'Διαγραφή Λευκώματος και Φωτογραφιών', - 'KEEP_ALBUM' => 'Διατήρηση Λευκώματος', - 'DELETE_ALBUM_CONFIRMATION_1' => 'Είστε σίγουρη/ος πως θέλετε να διαγράψετε αυτό το λεύκωμα', - 'DELETE_ALBUM_CONFIRMATION_2' => 'και όλες τις φωτογραφίες που περιέχει; Αυτή η ενέργεια δεν μπορεί να αναιρεθεί!', - - 'DELETE_ALBUMS_QUESTION' => 'Διαγραφή Λευκωμάτων και Φωτογραφιών', - 'KEEP_ALBUMS' => 'Διατήρηση Λευκωμάτων', - 'DELETE_ALBUMS_CONFIRMATION_1' => 'Είστε σίγουρη/ος πως θέλετε να διαγράψετε όλα', - 'DELETE_ALBUMS_CONFIRMATION_2' => 'τα επιλεγμένα λευκώματα και όλες τις φωτογραφίες που περιέχουν; Αυτή η ενέργεια δεν μπορεί να αναιρεθεί!', - - 'DELETE_UNSORTED_CONFIRM' => 'Είστε σίγουρη/ος πως θέλετε να διαγράψετε όλες τις \'Μη Ταξινομημένες\' φωτογραφίες;
Αυτή η ενέργεια δεν μπορεί να αναιρεθεί!', - 'CLEAR_UNSORTED' => 'Καθαρισμός των μη ταξινομημένων φωτογραφιών', - 'KEEP_UNSORTED' => 'Διατήρηση των Μη Ταξινομημένων', - - 'EDIT_SHARING' => 'Επεξεργασία Κοινής Χρήσης', - 'MAKE_PRIVATE' => 'Κάντε το Ιδιωτικό', - - 'CLOSE_ALBUM' => 'Κλείσιμο Λευκώματος', - 'CLOSE_PHOTO' => 'Κλείσιμο Φωτογραφίας', - 'CLOSE_MAP' => 'Close Map', - - 'ADD' => 'Προσθήκη', - 'MOVE' => 'Μετακίνηση', - 'MOVE_ALL' => 'Μετακίνηση Επιλεγμένων', - 'DUPLICATE' => 'Κλώνοποίηση', - 'DUPLICATE_ALL' => 'Κλώνοποίηση Επιλεγμένων', - 'COPY_TO' => 'Αντιγραφή σε...', - 'COPY_ALL_TO' => 'Αντιγραφή Επιλεγμένων σε...', - 'DELETE' => 'Διαγραφή', - 'DELETE_ALL' => 'Διαγραφή Επιλεγμένων', - 'DOWNLOAD' => 'Λήψη', - 'DOWNLOAD_ALL' => 'Λήψη Επιλεγμένων', - 'UPLOAD_PHOTO' => 'Μεταφόρτωση Φωτογραφίας', - 'IMPORT_LINK' => 'Εισαγωγή από Σύνδεσμο', - 'IMPORT_DROPBOX' => 'Εισαγωγή από Dropbox', - 'IMPORT_SERVER' => 'Εισαγωγή από Εξυπηρετητή', - 'NEW_ALBUM' => 'Νέο Λεύκωμα', - 'NEW_TAG_ALBUM' => 'New Tag Album', - - 'TITLE_NEW_ALBUM' => 'Εισάγετε έναν τίτλο για το νέο λεύκωμα:', - 'UNTITLED' => 'Χωρίς Τίτλο', - 'UNSORTED' => 'Μη Ταξινομημένα', - 'STARRED' => 'Με Αστέρι', - 'RECENT' => 'Πρόσφατα', - 'PUBLIC' => 'Δημόσια', - 'NUM_PHOTOS' => 'Φωτογραφίες', - - 'CREATE_ALBUM' => 'Δημιουργία Λευκώματος', - 'CREATE_TAG_ALBUM' => 'Create Tag Album', - - 'STAR_PHOTO' => 'Βάλτε Αστέρι στη Φωτογραφία', - 'STAR' => 'Βάλτε Αστέρι', - 'STAR_ALL' => 'Βάλτε Αστέρι στα επιλεγμένα', - 'TAGS' => 'Ετικέτες', - 'TAGS_ALL' => 'Ετικέτες στα επιλεγμένα', - 'UNSTAR_PHOTO' => 'Αφαιρέστε Αστέρια από τη Φωτογραφία', - 'SET_COVER' => 'Set Album Cover', - 'REMOVE_COVER' => 'Remove Album Cover', - - 'FULL_PHOTO' => 'Πρωτότυπη Φωτογραφία', - 'ABOUT_PHOTO' => 'Πληροφορίες Φωτογραφίας', - 'DISPLAY_FULL_MAP' => 'Map', - 'DIRECT_LINK' => 'Απευθείας Σύνδεσμος', - 'DIRECT_LINKS' => 'Απευθείας Σύνδεσμοι', - - 'ALBUM_ABOUT' => 'Περί', - 'ALBUM_BASICS' => 'Βασικές Πληροφορίες', - 'ALBUM_TITLE' => 'Τίτλος', - 'ALBUM_NEW_TITLE' => 'Εισάγετε έναν νέο τίτλο για αυτό το Λεύκωμα:', - 'ALBUMS_NEW_TITLE_1' => 'Εισάγετε νέο τίτλο για όλα', - 'ALBUMS_NEW_TITLE_2' => 'τα επιλεγμένα λευκώματα:', - 'ALBUM_SET_TITLE' => 'Ορίστε Τίτλο', - 'ALBUM_DESCRIPTION' => 'Περιγραφή', - 'ALBUM_SHOW_TAGS' => 'Tags to show', - 'ALBUM_NEW_DESCRIPTION' => 'Εισάγετε μία νέα περιγραφή για αυτό το λεύκωμα:', - 'ALBUM_SET_DESCRIPTION' => 'Ορίστε Περιγραφή', - 'ALBUM_NEW_SHOWTAGS' => 'Enter tags of photos that will be visible in this album:', - 'ALBUM_SET_SHOWTAGS' => 'Set tags to show', - 'ALBUM_ALBUM' => 'Λεύκωμα', - 'ALBUM_CREATED' => 'Δημιουργήθηκε', - 'ALBUM_IMAGES' => 'Εικόνες', - 'ALBUM_VIDEOS' => 'Βίντεο', - 'ALBUM_SUBALBUMS' => 'Υπο-λευκώματα', - 'ALBUM_SHARING' => 'Κοινή Χρήση', - 'ALBUM_SHR_YES' => 'ΝΑΙ', - 'ALBUM_SHR_NO' => 'Όχι', - 'ALBUM_PUBLIC' => 'Δημόσιο', - 'ALBUM_PUBLIC_EXPL' => 'Το λεύκωμα είναι προσπελάσιμο από τρίτους, υπό τους ακόλουθους περιορισμούς.', - 'ALBUM_FULL' => 'Πρωτότυπο', - 'ALBUM_FULL_EXPL' => 'Οι εικόνες πλήρης ανάλυσης είναι διαθέσιμες.', - 'ALBUM_HIDDEN' => 'Κρυφό', - 'ALBUM_HIDDEN_EXPL' => 'Μόνο άτομα με τον απευθείας σύνδεσμο μπορούν να δουν αυτό το λεύκωμα.', - 'ALBUM_MARK_NSFW' => 'Mark album as sensitive', - 'ALBUM_UNMARK_NSFW' => 'Unmark album as sensitive', - 'ALBUM_NSFW' => 'Sensitive', - 'ALBUM_NSFW_EXPL' => 'Album is marked to contain sensitive content.', - 'ALBUM_DOWNLOADABLE' => 'Δυνατότητα Λήψης', - 'ALBUM_DOWNLOADABLE_EXPL' => 'Οι επισκέπτες της γκαλερί μπορούν να κατεβάσουν αυτό το λεύκωμα.', - 'ALBUM_SHARE_BUTTON_VISIBLE' => 'Share button is visible', - 'ALBUM_SHARE_BUTTON_VISIBLE_EXPL' => 'Display social media sharing links.', - 'ALBUM_PASSWORD' => 'Κωδικός Πρόσβασης', - 'ALBUM_PASSWORD_PROT' => 'Προστατεύεται με κωδικό πρόσβασης', - 'ALBUM_PASSWORD_PROT_EXPL' => 'Αυτό το λεύκωμα είναι μόνο προσβάσιμο με έναν έγκυρο κωδικό πρόσβασης.', - 'ALBUM_PASSWORD_REQUIRED' => 'Αυτό το λεύκωμα προστατεύεται με κωδικό πρόσβασης. Εισάγετε τον κωδικό πρόσβασης παρακάτω για να δείτε τις φωτογραφίες αυτού του λευκώματος:', - 'ALBUM_MERGE_1' => 'Είστε σίγουρη/ος πως θέλετε να συγχωνεύσετε αυτό το λεύκωμα', - 'ALBUM_MERGE_2' => 'σε αυτό το λεύκωμα', - 'ALBUMS_MERGE' => 'Είστε σίγουρη/ος πως θέλετε να συγχωνεύσετε όλα τα επιλεγμένα λευκώματα', - 'MERGE_ALBUM' => 'Συγχώνευση Λευκωμάτων', - 'DONT_MERGE' => 'Να μη γίνει συγχώνευση', - 'ALBUM_MOVE_1' => 'Είστε σίγουρη/ος πως θέλετε να μετακινήσετε το λεύκωμα', - 'ALBUM_MOVE_2' => 'σε αυτό το λεύκωμα', - 'ALBUMS_MOVE' => 'Είστε σίγουρη/ος πως θέλετε να μετακινήσετε όλα τα επιλεγμένα λευκώματα σε αυτό το λεύκωμα', - 'MOVE_ALBUMS' => 'Μετακίνηση Λευκωμάτων', - 'NOT_MOVE_ALBUMS' => 'Να μη γίνει μετακίνηση', - 'ROOT' => 'Λευκώματα', - 'ALBUM_REUSE' => 'Επαναχρησιμοποίηση', - 'ALBUM_LICENSE' => 'Άδεια', - 'ALBUM_SET_LICENSE' => 'Ορισμός Άδειας', - 'ALBUM_LICENSE_HELP' => 'Χρειάζεστε βοήθεια για την επιλογή άδειας;', - 'ALBUM_LICENSE_NONE' => 'Καμία', - 'ALBUM_RESERVED' => 'Με επιφύλαξη παντός δικαιώματος', - 'ALBUM_SET_ORDER' => 'Set Order', - 'ALBUM_ORDERING' => 'Order by', - - 'PHOTO_ABOUT' => 'Περί', - 'PHOTO_BASICS' => 'Βασικές Πληροφορίες', - 'PHOTO_TITLE' => 'Τίτλος', - 'PHOTO_NEW_TITLE' => 'Εισάγετε έναν νέο τίτλο για αυτή τη φωτογραφία:', - 'PHOTO_SET_TITLE' => 'Ορισμός Τίτλου', - 'PHOTO_UPLOADED' => 'Μεταφορτώθηκε', - 'PHOTO_DESCRIPTION' => 'Περιγραφή', - 'PHOTO_NEW_DESCRIPTION' => 'Εισάγετε μία νέα περιγραφή για αυτή τη φωτογραφία:', - 'PHOTO_SET_DESCRIPTION' => 'Ορισμός Περιγραφής', - 'PHOTO_NEW_LICENSE' => 'Προσθήκη Άδειας', - 'PHOTO_SET_LICENSE' => 'Ορισμός Άδειας', - 'PHOTO_LICENSE' => 'Άδεια', - 'PHOTO_REUSE' => 'Επαναχρησιμοποίηση', - 'PHOTO_LICENSE_NONE' => 'Καμία', - 'PHOTO_RESERVED' => 'Με επιφύλαξη παντός δικαιώματος', - 'PHOTO_LATITUDE' => 'Γεωγραφικό πλάτος', - 'PHOTO_LONGITUDE' => 'Γεωγραφικό μήκος', - 'PHOTO_ALTITUDE' => 'Υψόμετρο', - 'PHOTO_IMGDIRECTION' => 'Κατεύθυνση', - 'PHOTO_LOCATION' => 'Location', - 'PHOTO_IMAGE' => 'Εικόνα', - 'PHOTO_VIDEO' => 'Video', - 'PHOTO_SIZE' => 'Μέγεθος', - 'PHOTO_FORMAT' => 'Μορφή', - 'PHOTO_RESOLUTION' => 'Ανάλυση', - 'PHOTO_DURATION' => 'Duration', - 'PHOTO_FPS' => 'Ρυθμός καρέ', - 'PHOTO_TAGS' => 'Ετικέτες', - 'PHOTO_NOTAGS' => 'Χωρίς Ετικέτες', - 'PHOTO_NEW_TAGS' => 'Εισάγετε τις ετικέτες σας για αυτή τη φωτογραφία. Μπορείτε να προσθέσετε πολλαπλές ετικέτες χωρίζοντάς \'τες με ένα κόμμα:', - 'PHOTO_NEW_TAGS_1' => 'Εισάγετε τις ετικέτες σας για όλες', - 'PHOTO_NEW_TAGS_2' => 'τις επιλεγμένες φωγογραφίες. Υφιστάμενες ετικέτες θα αντικατασταθούν. Μπορείτε να προσθέσετε πολλαπλές ετικέτες χωρίζοντάς \'τες με ένα κόμμα:', - 'PHOTO_SET_TAGS' => 'Ορισμός Ετικετών', - 'PHOTO_CAMERA' => 'Κάμερα', - 'PHOTO_CAPTURED' => 'Φωτογραφήθηκε', - 'PHOTO_MAKE' => 'Έτος Κατασκευής', - 'PHOTO_TYPE' => 'Τύπος/Μοντέλο', - 'PHOTO_LENS' => 'Lens', - 'PHOTO_SHUTTER' => 'Ταχύτητα Κλείστρου', - 'PHOTO_APERTURE' => 'Διάφραγμα', - 'PHOTO_FOCAL' => 'Εστιακό μήκος', - 'PHOTO_ISO' => 'ISO', - 'PHOTO_SHARING' => 'Κοινή Χρήση', - 'PHOTO_SHR_PLUBLIC' => 'Δημόσια', - 'PHOTO_SHR_ALB' => 'Ναι (Λεύκωμα)', - 'PHOTO_SHR_PHT' => 'Ναι (Φωτογραφία)', - 'PHOTO_SHR_NO' => 'Όχι', - 'PHOTO_DELETE' => 'Διαγραφή Φωτογραφίας', - 'PHOTO_KEEP' => 'Να μη γίνει διαγραφή', - 'PHOTO_DELETE_1' => 'Είστε σίγουρη/ος πως θέλετε να διαγράψετε αυτή τη φωτογραφία', - 'PHOTO_DELETE_2' => '; Αυτή η ενέργεια δεν μπορεί να αναιρεθεί!', - 'PHOTO_DELETE_ALL_1' => 'Είστε σίγουρη/ος πως θέλετε να διαγράψετε όλες', - 'PHOTO_DELETE_ALL_2' => 'τις επιλεγμένες φωτογραφίες; Αυτή η ενέργεια δεν μπορεί να αναιρεθεί!', - 'PHOTOS_NEW_TITLE_1' => 'Εισάγετε νέο τίτλο για όλες', - 'PHOTOS_NEW_TITLE_2' => 'τις επιλεγμένες φωτογραφίες:', - 'PHOTO_MAKE_PRIVATE_ALBUM' => 'Αυτή η φωτογραφία βρίσκεται σε ένα δημόσιο λεύκωμα. Για να κάνετε αυτή τη φωτογραφία ιδιωτική ή δημόσια, επεξεργαστείτε τις ρυθμίσεις ορατότητας του συσχετιζόμενου Λευκώματος.', - 'PHOTO_SHOW_ALBUM' => 'Εμφάνιση Λευκώματος', - 'PHOTO_PUBLIC' => 'Δημόσια', - 'PHOTO_PUBLIC_EXPL' => 'Η φωτογραφία είναι προσπελάσιμο από τρίτους, υπό τους ακόλουθους περιορισμούς.', - 'PHOTO_FULL' => 'Πρωτότυπη', - 'PHOTO_FULL_EXPL' => 'Η εικόνα πλήρης ανάλυσης είναι διαθέσιμη.', - 'PHOTO_HIDDEN' => 'Hidden', - 'PHOTO_HIDDEN_EXPL' => 'Μόνο άτομα με τον απευθείας σύνδεσμο μπορούν να δουν αυτή τη φωτογραφία.', - 'PHOTO_DOWNLOADABLE' => 'Δυνατότητα Λήψης', - 'PHOTO_DOWNLOADABLE_EXPL' => 'Οι επισκέπτες της γκαλερί μπορούν να κατεβάσουν αυτή τη φωτογραφία.', - 'PHOTO_SHARE_BUTTON_VISIBLE' => 'Share button is visible', - 'PHOTO_SHARE_BUTTON_VISIBLE_EXPL' => 'Display social media sharing links.', - 'PHOTO_PASSWORD_PROT' => 'Προστατεύεται με κωδικό πρόσβασης', - 'PHOTO_PASSWORD_PROT_EXPL' => 'Αυτή η φωτογραφία είναι μόνο προσβάσιμη με έναν έγκυρο κωδικό πρόσβασης.', - 'PHOTO_EDIT_SHARING_TEXT' => 'Οι ιδιότητες κοινής χρήσης αυτής της φωτογραφίας θα αλλάξουν στις ακόλουθες:', - 'PHOTO_NO_EDIT_SHARING_TEXT' => 'Επειδή αυτή η φωτογραφία βρίσκεται σε ένα δημόσιο λεύκωμα, κληρονομεί τις ρυθμίσεις ορατότητας του λευκώματος στο οποίο ανήκει. Η τρέχουσα ορατότητά της φαίνεται παρακάτω για ενημερωτικούς λόγους μόνο.', - 'PHOTO_EDIT_GLOBAL_SHARING_TEXT' => 'Η ορατότητα αυτής της φωτογραφίας μπορεί να ρυθμιστεί με μεγαλύτερη λεπτομέρεια χρησιμοποιώντας τις γενικές ρυθμίσεις του Lychee. Η τρέχουσα ορατότητά της φαίνεται παρακάτω για ενημερωτικούς λόγους μόνο.', - 'PHOTO_SHARING_CONFIRM' => 'Αποθήκευση', - - 'LOADING' => 'Φορτώνει', - 'ERROR' => 'Σφάλμα', - 'ERROR_TEXT' => 'Ουπς, φαίνεται πως κάτι πήγε στραβά. Παρακαλούμε κάντε ανανέωση της σελίδας και προσπαθήστε ξανά!', - 'ERROR_DB_1' => 'Αδυναμία σύνδεσης στη βάση δεδομένων διότι η πρόσβαση απορρίφθηκε. Ελέγξτε ξανά τις ρυθμίσεις του εξυπηρετητή, όνομα χρήστη και κωδικό πρόσβασης και σιγουρευτείτε πως η πρόσβαση από την τρέχουσα τοποθεσία επιτρέπεται.', - 'ERROR_DB_2' => 'Αδυναμία δημιουργίας βάσης δεδομένων. Ελέγξτε ξανά τις ρυθμίσεις του εξυπηρετητή, όνομα χρήστη και κωδικό πρόσβασης και σιγουρευτείτε πως ο συγκεκριμένος χρήστης έχει τα δικαιώματα αλλαγής της βάσης δεδομένων και προσθήκης περιεχομένου σε αυτή.', - 'ERROR_CONFIG_FILE' => "Αδυναμία αποθήκευσης ρυθμίσεων. Η πρόσβαση δεν επιτρέπεται στον κατάλογο 'data/'. Παρακαλούμε ρυθμίστε τα δικαιώματα ανάγνωσης, γραφής και εκτέλεσης για άλλους (others) στον κατάλογο 'data/' και 'uploads/'. Ρίξτε μια ματιά στο αρχείο readme για περισσότερες πληροφορίες.", - 'ERROR_UNKNOWN' => 'Κάτι απρόσμενο συνέβη. Παρακαλούμε προσπαθείστε ξανά και ελέγξτε την εγκατάστασή σας και τον εξυπηρετητή. Ρίξτε μια ματιά στο αρχείο readme για περισσότερες πληροφορίες.', - 'ERROR_LOGIN' => 'Αδυναμία αποθήκευσης στοιχείων εισόδου. Παρακαλούμε δοκιμάστε ξανά με διαφορετικό όνομα χρήστη και κωδικό πρόσβασης!', - 'ERROR_MAP_DEACTIVATED' => 'Map functionality has been deactivated under settings.', - 'ERROR_SEARCH_DEACTIVATED' => 'Search functionality has been deactivated under settings.', - 'SUCCESS' => 'OK', - 'RETRY' => 'Προσπάθεια ξανά', - - 'SETTINGS_SUCCESS_LOGIN' => 'Τα στοιχεία εισόδου ενημερώθηκαν.', - 'SETTINGS_SUCCESS_SORT' => 'Η Ταξινόμηση ενημερώθηκε.', - 'SETTINGS_SUCCESS_DROPBOX' => 'Το κλειδί για το Dropbox ενημερώθηκε.', - 'SETTINGS_SUCCESS_LANG' => 'Η γλώσσα ενημερώθηκε', - 'SETTINGS_SUCCESS_LAYOUT' => 'Η διάταξη ενημερώθηκε', - 'SETTINGS_SUCCESS_IMAGE_OVERLAY' => 'Οι ρυθμίσεις επιφάνειας EXIF ενημερώθηκαν', - 'SETTINGS_SUCCESS_PUBLIC_SEARCH' => 'Η δημόσια αναζήτηση ενημερώθηκε', - 'SETTINGS_SUCCESS_LICENSE' => 'Η προεπιλεγμένη άδεια ενημερώθηκε', - 'SETTINGS_SUCCESS_MAP_DISPLAY' => 'Οι ρυθμίσεις εμφάνισης χάρτη ενημερώθηκαν', - 'SETTINGS_SUCCESS_MAP_DISPLAY_PUBLIC' => 'Map display settings for public albums updated', - 'SETTINGS_SUCCESS_MAP_PROVIDER' => 'Map provider settings updated', - - 'U2F_NOT_SUPPORTED' => 'U2F not supported. Sorry.', - 'U2F_NOT_SECURE' => 'Environment not secured. U2F not available.', - 'U2F_REGISTER_KEY' => 'Register new device.', - 'U2F_REGISTRATION_SUCCESS' => 'Registration successful!', - 'U2F_AUTHENTIFICATION_SUCCESS' => 'Authentication successful!', - 'U2F_CREDENTIALS' => 'Credentials', - 'U2F_CREDENTIALS_DELETED' => 'Credentials deleted!', - - 'NEW_PHOTOS_NOTIFICATION' => 'Send new photos notification emails.', - 'SETTINGS_SUCCESS_NEW_PHOTOS_NOTIFICATION' => 'New photos notification updated', - 'USER_EMAIL_INSTRUCTION' => 'Add your email below to enable receiving email notifications.
To stop receiving emails, simply remove your email below.', - - 'DB_INFO_TITLE' => 'Εισάγετε τις ρυθμίσεις της βάσης δεδομένων παρακάτω:', - 'DB_INFO_HOST' => 'Εξυπηρετητής Βάσης Δεδομένων (προαιρετικό)', - 'DB_INFO_USER' => 'Όνομα χρήστη Βάσης Δεδομένων', - 'DB_INFO_PASSWORD' => 'Κωδικός πρόσβασης Βάσης Δεδομένων', - 'DB_INFO_TEXT' => 'Το Lychee θα δημιουργήσει τη δικιά του βάση δεδομένων. Αν απατείται ωστόσο, μπορείτε να εισάγετε το όνομα μιας υπάρχουσας βάσης δεδομένων:', - 'DB_NAME' => 'Όνομα Βάσης Δεδομένων (προαιρετικό)', - 'DB_PREFIX' => 'Πρόθεμα στους πίνακες (προαιρετικό)', - 'DB_CONNECT' => 'Σύνδεση', - - 'LOGIN_TITLE' => 'Εισάγετε ένα όνομα χρήστη και κωδικό πρόσβασης για την εγκατάστασή σας:', - 'LOGIN_USERNAME' => 'Νέο όνομα χρήστη', - 'LOGIN_PASSWORD' => 'Νέος κωδικός πρόσβασης', - 'LOGIN_PASSWORD_CONFIRM' => 'Επιβεβαίωση κωδικού πρόσβασης', - 'LOGIN_CREATE' => 'Δημιουργία στοιχείων εισόδου', - - 'PASSWORD_TITLE' => 'Εισάγετε τον τρέχον κωδικό πρόσβασης:', - 'USERNAME_CURRENT' => 'Τρέχον Όνομα Χρήστη', - 'PASSWORD_CURRENT' => 'Τρέχον κωδικός πρόσβασης', - 'PASSWORD_TEXT' => 'Το όνομα χρήστη και ο κωδικός πρόσβασής σας θα αλλάξουν στα παρακάτω:', - 'PASSWORD_CHANGE' => 'Αλλαγή στοιχείων εισόδου', - - 'EDIT_SHARING_TITLE' => 'Επεξεργασία κοινής χρήσης', - 'EDIT_SHARING_TEXT' => 'Οι ιδιότητες κοινής χρήσης αυτού του λευκώματος θα αλλάξουν στις παρακάτω:', - 'SHARE_ALBUM_TEXT' => 'Αυτό το λεύκωμα θα κοινοποιείται με τις παρακάτω ιδιότητες:', - 'ALBUM_SHARING_CONFIRM' => 'Αποθήκευση', - - 'SORT_ALBUM_BY_1' => 'Ταξινόμηση λευκωμάτων κατά', - 'SORT_ALBUM_BY_2' => 'με', - 'SORT_ALBUM_BY_3' => 'σειρά.', - - 'SORT_ALBUM_SELECT_1' => 'Ημερομηνία Δημιουργίας', - 'SORT_ALBUM_SELECT_2' => 'Τίτλος', - 'SORT_ALBUM_SELECT_3' => 'Περιγραφή', - 'SORT_ALBUM_SELECT_4' => 'Δημόσιο', - 'SORT_ALBUM_SELECT_5' => 'Νεότερη Ημερομηνία Λήψης', - 'SORT_ALBUM_SELECT_6' => 'Παλαιότερη Ημερομηνία Λήψης', - - 'SORT_PHOTO_BY_1' => 'Ταξινόμηση Φωτογραφιών κατά', - 'SORT_PHOTO_BY_2' => 'με', - 'SORT_PHOTO_BY_3' => 'σειρά.', - - 'SORT_PHOTO_SELECT_1' => 'Ημερομηνία Μεταφόρτωσης', - 'SORT_PHOTO_SELECT_2' => 'Ημερομηνία Λήψης', - 'SORT_PHOTO_SELECT_3' => 'Τίτλος', - 'SORT_PHOTO_SELECT_4' => 'Περιγραφή', - 'SORT_PHOTO_SELECT_5' => 'Δημόσιο', - 'SORT_PHOTO_SELECT_6' => 'Αστέρια', - 'SORT_PHOTO_SELECT_7' => 'Μορφή Φωτογραφίας', - - 'SORT_ASCENDING' => 'Αύξουσα', - 'SORT_DESCENDING' => 'Φθίνουσα', - 'SORT_CHANGE' => 'Αλλαγή Ταξινόμησης', - - 'DROPBOX_TITLE' => 'Ορισμός Κλειδιού Dropbox', - 'DROPBOX_TEXT' => "Για να μπορέσουμε να εισάγουμε φωτογραφίες από το δικό σας Dropbox, θα χρειαστείτε ένα έγκυρο κλειδί drop-ins app από την ιστοσελίδα του Dropbox. Παράγετε ένα προσωπικό κλειδί και εισάγετέ το παρακάτω:", - - 'LANG_TEXT' => 'Αλλαγή γλώσσας του Lychee για:', - 'LANG_TITLE' => 'Αλλαγή Γλώσσας', - 'PUBLIC_SEARCH_TEXT' => 'Να επιτρέπεται η δημόσια αναζήτηση:', - 'OVERLAY_TYPE' => 'Δεδομένα που θα χρησιμοποιηθούν στο overlay εικόνας:', - 'OVERLAY_NONE' => 'None', - 'OVERLAY_EXIF' => 'EXIF δεδομένα φωτογραφίας', - 'OVERLAY_DESCRIPTION' => 'Περιγραφή φωτογραφίας', - 'OVERLAY_DATE' => 'Ημερομηνία λήψης της φωτογραφίας', - 'MAP_DISPLAY_TEXT' => 'Εμφάνιση συντεταγμένων στον χάρτη (OpenStreetMap):', - 'MAP_DISPLAY_PUBLIC_TEXT' => 'Enable maps for public albums (provided by OpenStreetMap):', - 'MAP_PROVIDER' => 'Provider of OpenStreetMap tiles:', - 'MAP_PROVIDER_WIKIMEDIA' => 'Wikimedia', - 'MAP_PROVIDER_OSM_ORG' => 'OpenStreetMap.org (no HiDPI)', - 'MAP_PROVIDER_OSM_DE' => 'OpenStreetMap.de (no HiDPI)', - 'MAP_PROVIDER_OSM_FR' => 'OpenStreetMap.fr (no HiDPI)', - 'MAP_PROVIDER_RRZE' => 'University of Erlangen, Germany (only HiDPI)', - 'MAP_INCLUDE_SUBALBUMS_TEXT' => 'Include photos of subalbums on map:', - 'LOCATION_DECODING' => 'Decode GPS data into location name', - 'LOCATION_SHOW' => 'Show location name', - 'LOCATION_SHOW_PUBLIC' => 'Show location name for public mode', - 'LAYOUT_TYPE' => 'Διάταξη φωτογραφιών:', - 'LAYOUT_SQUARES' => 'Τετράγωνες μικρογραφίες', - 'LAYOUT_JUSTIFIED' => 'Με ίσες αναλογίες', - 'LAYOUT_UNJUSTIFIED' => 'Με άνισες αναλογίες', - 'SET_LAYOUT' => 'Αλλαγή διάταξης', - - 'NSFW_VISIBLE_TEXT_1' => 'Make Sensitive albums visible by default.', - 'NSFW_VISIBLE_TEXT_2' => 'If the album is public, it is still accessible, just hidden from the view and can be revealed by pressing H.', - 'SETTINGS_SUCCESS_NSFW_VISIBLE' => 'Default sensitive album visibility updated with success.', - - 'VIEW_NO_RESULT' => 'Κανένα αποτέλεσμα', - 'VIEW_NO_PUBLIC_ALBUMS' => 'Κανένα δημόσιο λεύκωμα', - 'VIEW_NO_CONFIGURATION' => 'Καμία ρύθμιση', - 'VIEW_PHOTO_NOT_FOUND' => 'Η φωτογραφία δεν βρέθηκε', - - 'NO_TAGS' => 'Καμία ετικέτα', - - 'UPLOAD_MANAGE_NEW_PHOTOS' => 'Μπορείτε τώρα να διαχειριστείτε τις νέες φωτογραφίες σας.', - 'UPLOAD_COMPLETE' => 'Η μεταφόρτωση ολοκληρώθηκε', - 'UPLOAD_COMPLETE_FAILED' => 'Αποτυχία μεταφόρτωσης μιας ή περισσότερων φωτογραφιών.', - 'UPLOAD_IMPORTING' => 'Γίνεται εισαγωγή', - 'UPLOAD_IMPORTING_URL' => 'Εισαγωγή URL', - 'UPLOAD_UPLOADING' => 'Γίνεται μεταφόρτωση', - 'UPLOAD_FINISHED' => 'Ολοκληρώθηκε', - 'UPLOAD_PROCESSING' => 'Γίνεται επεξεργασία', - 'UPLOAD_FAILED' => 'Απέτυχε', - 'UPLOAD_FAILED_ERROR' => 'Η μεταφόρτωση απέτυχε. Ο εξυπηρετητής επέστρεψε ένα σφάλμα!', - 'UPLOAD_FAILED_WARNING' => 'Η μεταφόρτωση απέτυχε. Ο εξυπηρετητής επέστρεψε μία προειδοποίηση!', - 'UPLOAD_CANCELLED' => 'Cancelled', - 'UPLOAD_SKIPPED' => 'Παραλείφθηκε', - 'UPLOAD_UPDATED' => 'Updated', - 'UPLOAD_IMPORT_SKIPPED_DUPLICATE' => 'This photo has been skipped because it\'s already in your library.', - 'UPLOAD_IMPORT_RESYNCED_DUPLICATE' => 'This photo has been skipped because it\'s already in your library, but its metadata has been updated.', - 'UPLOAD_ERROR_CONSOLE' => 'Παρακαλούμε ρίξτε μια ματιά στην κονσόλα του περιηγητή σας για περισσότερες λεπτομέρειες.', - 'UPLOAD_UNKNOWN' => 'Ο εξυπηρετητής επέστρεψε μία άγνωστη απόκριση. Παρακαλούμε ρίξτε μια ματιά στην κονσόλα του περιηγητή σας για περισσότερες λεπτομέρειες.', - 'UPLOAD_ERROR_UNKNOWN' => 'Η μεταφόρτωση απέτυχε. Ο εξυπηρετητής επέστρεψε ένα άγνωστο σφάλμα!', - 'UPLOAD_ERROR_POSTSIZE' => 'Upload failed. The PHP post_max_size may be too small! Otherwise check the FAQ.', - 'UPLOAD_ERROR_FILESIZE' => 'Upload failed. The PHP upload_max_filesize may be too small! Otherwise check the FAQ.', - 'UPLOAD_IN_PROGRESS' => 'Το Lychee αυτή τη στιγμή μεταφορτώνει!', - 'UPLOAD_IMPORT_WARN_ERR' => 'Η εισαγωγή ολοκληρώθηκε, αλλά επέστρεψε προειδοποιήσεις ή σφάλματα. Παρακαλούμε ρίξτε μια ματία στις καταγραφές (Ρυθμίσεις -> Εμφάνιση Καταγραφών) για περισσότερες λεπτομέρειες.', - 'UPLOAD_IMPORT_COMPLETE' => 'Η εισαγωγή ολοκληρώθηκε', - 'UPLOAD_IMPORT_INSTR' => 'Παρακαλούμε εισάγετε τον απευθείας σύνδεσμο μιας φωτογραφίας για να την εισάγετε:', - 'UPLOAD_IMPORT' => 'Εισαγωγή', - 'UPLOAD_IMPORT_SERVER' => 'Γίνεται εισαγωγή από εξυπηρετητή', - 'UPLOAD_IMPORT_SERVER_FOLD' => 'Ο φάκελος είναι άδειος ή μη αναγνώσιμα αρχεία προς επεξεργασία. Παρακαλούμε ρίξτε μια ματία στις καταγραφές (Ρυθμίσεις -> Εμφάνιση Καταγραφών) για περισσότερες λεπτομέρειες.', - 'UPLOAD_IMPORT_SERVER_INSTR' => 'Αυτή η ενέργεια θα εισάγει όλες τις φωτογραφίες, φακέλους και υπο-φακέλους οι οποίοι βρίσκονται στον παρακάτω κατάλογο.', - 'UPLOAD_ABSOLUTE_PATH' => 'Απόλυτη διαδρομή του καταλόγου', - 'UPLOAD_IMPORT_SERVER_EMPT' => 'Δεν ήταν δυνατό να ξεκινήσει η διαδικασία εισαγωγής, διότι ο κατάλογος ήταν άδειος!', - 'UPLOAD_IMPORT_DELETE_ORIGINALS' => 'Διαγραφή πρωτότυπων', - 'UPLOAD_IMPORT_DELETE_ORIGINALS_EXPL' => 'Αν είναι εφικτό τα πρωτότυπα αρχεία θα διαγραφούν αφού ολοκληρωθεί η εισαγωγή τους.', - 'UPLOAD_IMPORT_VIA_SYMLINK' => 'Symbolic links', - 'UPLOAD_IMPORT_VIA_SYMLINK_EXPL' => 'Import files using symbolic links to originals.', - 'UPLOAD_IMPORT_SKIP_DUPLICATES' => 'Skip duplicates', - 'UPLOAD_IMPORT_SKIP_DUPLICATES_EXPL' => 'Existing media files are skipped.', - 'UPLOAD_IMPORT_RESYNC_METADATA' => 'Re-sync metadata', - 'UPLOAD_IMPORT_RESYNC_METADATA_EXPL' => 'Update metadata of existing media files.', - 'UPLOAD_IMPORT_LOW_MEMORY' => 'Κατάσταση χαμηλής μνήμης!', - 'UPLOAD_IMPORT_LOW_MEMORY_EXPL' => 'Η διεργασία εισαγωγής στον εξυπηρετητή πλησιάζει τα όρια μνήμης και μπορεί να τερματιστεί πρόωρα.', - 'UPLOAD_WARNING' => 'Προειδοποίηση', - 'UPLOAD_IMPORT_NOT_A_DIRECTORY' => 'Η δοθείσα διαδρομή δεν είναι ένας αναγνώσιμος κατάλογος!', - 'UPLOAD_IMPORT_PATH_RESERVED' => 'Η δοθείσα διαδρομή χρησιμοποιείται από το Lychee!', - 'UPLOAD_IMPORT_UNREADABLE' => 'Δεν ήταν δυνατή η ανάγνωση του αρχείου!', - 'UPLOAD_IMPORT_FAILED' => 'Δεν ήταν δυνατή η εισαγωγή του αρχείου!', - 'UPLOAD_IMPORT_UNSUPPORTED' => 'Μη υποστηριζόμενος τύπος αρχείου!', - 'UPLOAD_IMPORT_ALBUM_FAILED' => 'Δεν ήταν δυνατή η δημιουργία του λευκώματος!', - 'UPLOAD_IMPORT_CANCELLED' => 'Import cancelled', - - 'ABOUT_SUBTITLE' => 'Αυτό-φιλοξενούμενη διαχείριση φωτογραφιών καμωμένη σωστά', - 'ABOUT_DESCRIPTION' => 'είναι ένα δωρεάν εργαλείο διαχείρισης φωτογραφιών, το οποίο "τρέχει" στον δικό σας εξυπηρετητή ή δικτυακό-χώρο. Εγκαθίσταται σε μερικά δευτερόλεπτα. Μεταφορτώστε, διαχειριστείτε και κοινοποιήστε φωτογραφίες σαν από εγκατεστημένη εφαρμογή. Το Lychee παρέχεται με όλες τις λειτουργίες που χρειάζεστε και όλες οι φωτογραφίες σας είναι αποθηκευμένες με ασφάλεια.', - 'FOOTER_COPYRIGHT' => 'Όλες οι εικόνες σε αυτή την ιστοσελίδα υπόκεινται σε πνευματικά δικαιώματα από ', - 'HOSTED_WITH_LYCHEE' => 'Φιλοξενείται από το Lychee', - - 'URL_COPY_TO_CLIPBOARD' => 'Αντιγραφή στο πρόχειρο', - 'URL_COPIED_TO_CLIPBOARD' => 'Η διεύθυνση URL αντιγράφηκε στο πρόχειρο!', - 'PHOTO_DIRECT_LINKS_TO_IMAGES' => 'Απευθείας σύνδεσμοι στα αρχεία εικόνων:', - 'PHOTO_MEDIUM' => 'Μέτρια', - 'PHOTO_MEDIUM_HIDPI' => 'Μέτρια HiDPI', - 'PHOTO_SMALL' => 'Μικρογραφία', - 'PHOTO_SMALL_HIDPI' => 'Μικρογραφία HiDPI', - 'PHOTO_THUMB' => 'Τετράγωνη Μικρογραφία', - 'PHOTO_THUMB_HIDPI' => 'Τετράγωνη Μικρογραφία HiDPI', - 'PHOTO_LIVE_VIDEO' => 'Video part of live-photo', - 'PHOTO_VIEW' => 'Lychee Προβολή Φωτογραφιών:', - - 'PHOTO_EDIT_ROTATECWISE' => 'Rotate clockwise', - 'PHOTO_EDIT_ROTATECCWISE' => 'Rotate counter-clockwise', - ]; - - return $locale; - } -} diff --git a/app/Locale/Italian.php b/app/Locale/Italian.php deleted file mode 100644 index 14f27e819a1..00000000000 --- a/app/Locale/Italian.php +++ /dev/null @@ -1,480 +0,0 @@ - 'nome utente', - 'PASSWORD' => 'password', - 'ENTER' => 'Invia', - 'CANCEL' => 'Annulla', - 'SIGN_IN' => 'Entra', - 'CLOSE' => 'Chiudi', - 'SETTINGS' => 'Impostazioni', - 'SEARCH' => 'Cerca...', - 'MORE' => 'Altro', - 'DEFAULT' => 'Default', - - 'USERS' => 'Utenti', - 'U2F' => 'U2F', - 'NOTIFICATIONS' => 'Notifications', - 'SHARING' => 'Condivisione', - 'CHANGE_LOGIN' => 'Cambia Login', - 'CHANGE_SORTING' => 'Cambia Ordinamento', - 'SET_DROPBOX' => 'Imposta Dropbox', - 'ABOUT_LYCHEE' => 'Informazioni su Lychee', - 'DIAGNOSTICS' => 'Diagnostica', - 'DIAGNOSTICS_GET_SIZE' => 'Request space usage', - 'LOGS' => 'Visualizza Log', - 'SIGN_OUT' => 'Esci', - 'UPDATE_AVAILABLE' => 'Aggiornamento disponibile!', - 'MIGRATION_AVAILABLE' => 'Migration available!', - 'DEFAULT_LICENSE' => 'Licenza predefinita per i nuovi caricamenti:', - 'SET_LICENSE' => 'Imposta Licenza', - 'SET_OVERLAY_TYPE' => 'Imposta Filigrana', - 'SET_MAP_PROVIDER' => 'Set OpenStreetMap tiles provider', - - 'SMART_ALBUMS' => 'Album smart', - 'SHARED_ALBUMS' => 'Album condivisi', - 'ALBUMS' => 'Album', - 'PHOTOS' => 'Immagini', - 'SEARCH_RESULTS' => 'Search results', - - 'RENAME' => 'Rinomina', - 'RENAME_ALL' => 'Rinomina Tutto', - 'MERGE' => 'Unisci', - 'MERGE_ALL' => 'Unisci Tutto', - 'MAKE_PUBLIC' => 'Rendi Pubblico', - 'SHARE_ALBUM' => 'Condividi Album', - 'SHARE_PHOTO' => 'Condividi Photo', - 'VISIBILITY_ALBUM' => 'Album Visibility', - 'VISIBILITY_PHOTO' => 'Photo Visibility', - 'DOWNLOAD_ALBUM' => 'Scarica Album', - 'ABOUT_ALBUM' => 'Informazioni Album', - 'DELETE_ALBUM' => 'Elimina Album', - 'MOVE_ALBUM' => 'Move Album', - 'FULLSCREEN_ENTER' => 'Entra In Modalità Schermo Intero', - 'FULLSCREEN_EXIT' => 'Esci Dalla Modalità Schermo Intero', - - 'SHARING_ALBUM_USERS' => 'Share this album with users', - 'WAIT_FETCH_DATA' => 'Please wait while we get the data...', - 'SHARING_ALBUM_USERS_NO_USERS' => 'There are no users to share the album with', - 'SHARING_ALBUM_USERS_LONG_MESSAGE' => 'Select the users to share this album with', - - 'DELETE_ALBUM_QUESTION' => 'Elimina Album e Immagini', - 'KEEP_ALBUM' => 'Mantieni Album', - 'DELETE_ALBUM_CONFIRMATION_1' => 'Sei sicuro di voler eliminare l\' album', - 'DELETE_ALBUM_CONFIRMATION_2' => 'e tutte le immagini che contiene? Questa azione non può essere annullata successivamente!', - - 'DELETE_ALBUMS_QUESTION' => 'Elimina gli Album e le Immagini', - 'KEEP_ALBUMS' => 'Mantieni gli Album', - 'DELETE_ALBUMS_CONFIRMATION_1' => 'Sei sicuro di voler eliminare tutti', - 'DELETE_ALBUMS_CONFIRMATION_2' => 'gli album selezionati e le immagini contenute in essi? Questa azione non può essere annullata successivamente!', - - 'DELETE_UNSORTED_CONFIRM' => 'Sei sicuro di voler eliminare tutte le immagini da \'Non Catalogate\'?
Questa azione non può essere annullata successivamente!', - 'CLEAR_UNSORTED' => 'Rimuovi Immagini Non Catalogate', - 'KEEP_UNSORTED' => 'Mantieni Immagini Non Catalogate', - - 'EDIT_SHARING' => 'Modifica Condivisibilità', - 'MAKE_PRIVATE' => 'Rendi Privato', - - 'CLOSE_ALBUM' => 'Chiudi Album', - 'CLOSE_PHOTO' => 'Chiudi Foto', - 'CLOSE_MAP' => 'Close Map', - - 'ADD' => 'Aggiungi', - 'MOVE' => 'Sposta', - 'MOVE_ALL' => 'Sposta Tutto', - 'DUPLICATE' => 'Duplica', - 'DUPLICATE_ALL' => 'Duplica Tutto', - 'COPY_TO' => 'Copia in...', - 'COPY_ALL_TO' => 'Copia Tutto in...', - 'DELETE' => 'Elimina', - 'DELETE_ALL' => 'Elimina Tutto', - 'DOWNLOAD' => 'Scarica', - 'DOWNLOAD_ALL' => 'Scarica Tutto', - 'DOWNLOAD_MEDIUM' => 'Scarica a grandezza media', - 'DOWNLOAD_SMALL' => 'Scarica a grandezza piccola', - 'UPLOAD_PHOTO' => 'Carica Foto', - 'IMPORT_LINK' => 'Importa da Link', - 'IMPORT_DROPBOX' => 'Importa da Dropbox', - 'IMPORT_SERVER' => 'Importa da Server', - 'NEW_ALBUM' => 'Nuovo Album', - 'NEW_TAG_ALBUM' => 'New Tag Album', - - 'TITLE_NEW_ALBUM' => 'Inserire un titolo per il nuovo album:', - 'UNTITLED' => 'Senza Titolo', - 'UNSORTED' => 'Non Catalogate', - 'STARRED' => 'Speciali', - 'RECENT' => 'Recenti', - 'PUBLIC' => 'Pubbliche', - 'NUM_PHOTOS' => 'Foto', - - 'CREATE_ALBUM' => 'Crea Album', - 'CREATE_TAG_ALBUM' => 'Create Tag Album', - - 'STAR_PHOTO' => 'Contrassegna la Foto come Speciale', - 'STAR' => 'Contrassegna come Speciale', - 'STAR_ALL' => 'Contrassegna Tutto come Speciale', - 'TAGS' => 'Tag', - 'TAGS_ALL' => 'Tagga Tutto', - 'UNSTAR_PHOTO' => 'Rimuovi dalle Foto Speciali', - 'SET_COVER' => 'Set Album Cover', - 'REMOVE_COVER' => 'Remove Album Cover', - - 'FULL_PHOTO' => 'Open Original', - 'ABOUT_PHOTO' => 'Informazioni sulla Foto', - 'DISPLAY_FULL_MAP' => 'Map', - 'DIRECT_LINK' => 'Link Diretto', - 'DIRECT_LINKS' => 'Direct Links', - - 'ALBUM_ABOUT' => 'Informazioni', - 'ALBUM_BASICS' => 'Base', - 'ALBUM_TITLE' => 'Titolo', - 'ALBUM_NEW_TITLE' => 'Inserire un nuovo titolo per questo album:', - 'ALBUMS_NEW_TITLE_1' => 'Inserire un nuovo titolo per', - 'ALBUMS_NEW_TITLE_2' => 'gli album selezionati:', - 'ALBUM_SET_TITLE' => 'Imposta Titolo', - 'ALBUM_DESCRIPTION' => 'Descrizione', - 'ALBUM_SHOW_TAGS' => 'Tags to show', - 'ALBUM_NEW_DESCRIPTION' => 'Inserire una nuova descrizione per questo album:', - 'ALBUM_SET_DESCRIPTION' => 'Imposta Descrizione', - 'ALBUM_NEW_SHOWTAGS' => 'Enter tags of photos that will be visible in this album:', - 'ALBUM_SET_SHOWTAGS' => 'Set tags to show', - 'ALBUM_ALBUM' => 'Album', - 'ALBUM_CREATED' => 'Creato', - 'ALBUM_IMAGES' => 'Immagini', - 'ALBUM_VIDEOS' => 'Video', - 'ALBUM_SUBALBUMS' => 'Subalbums', - 'ALBUM_SHARING' => 'Condividi', - 'ALBUM_SHR_YES' => 'SI', - 'ALBUM_SHR_NO' => 'No', - 'ALBUM_PUBLIC' => 'Pubblico', - 'ALBUM_PUBLIC_EXPL' => 'Album can be viewed by others, subject to the restrictions below.', - 'ALBUM_FULL' => 'Original', - 'ALBUM_FULL_EXPL' => 'Full-resolution pictures are available.', - 'ALBUM_HIDDEN' => 'Nascosto', - 'ALBUM_HIDDEN_EXPL' => 'Solo le persone con il link diretto possono vedere questo album.', - 'ALBUM_MARK_NSFW' => 'Mark album as sensitive', - 'ALBUM_UNMARK_NSFW' => 'Unmark album as sensitive', - 'ALBUM_NSFW' => 'Sensitive', - 'ALBUM_NSFW_EXPL' => 'Album is marked to contain sensitive content.', - 'ALBUM_DOWNLOADABLE' => 'Scaricabile', - 'ALBUM_DOWNLOADABLE_EXPL' => 'I visitatori del tuo Lychee possono scaricare questo album.', - 'ALBUM_SHARE_BUTTON_VISIBLE' => 'Share button is visible', - 'ALBUM_SHARE_BUTTON_VISIBLE_EXPL' => 'Display social media sharing links.', - 'ALBUM_PASSWORD' => 'Password', - 'ALBUM_PASSWORD_PROT' => 'Password protetta', - 'ALBUM_PASSWORD_PROT_EXPL' => 'L\'album è accessibile soltanto con una password valida.', - 'ALBUM_PASSWORD_REQUIRED' => 'Questo album è protetto da password. Inserire la password sotto per vedere le foto di questo album:', - 'ALBUM_MERGE_1' => 'Sei sicuro di voler unire l\'album', - 'ALBUM_MERGE_2' => 'nell\' album', - 'ALBUMS_MERGE' => 'Sei sicuro di voler unire tutti gli album selezionati nell\'album', - 'MERGE_ALBUM' => 'Unisci Album', - 'DONT_MERGE' => 'Non Unire', - 'ALBUM_MOVE_1' => 'Sei sicuro di voler spostare l\' album', - 'ALBUM_MOVE_2' => 'nell\' album', - 'ALBUMS_MOVE' => 'Sei sicure di voler spostare tutti gli album selezionati nell\' album', - 'MOVE_ALBUMS' => 'Sposta Album', - 'NOT_MOVE_ALBUMS' => 'Non Spostare', - 'ROOT' => 'Radice', - 'ALBUM_REUSE' => 'Riutilizza', - 'ALBUM_LICENSE' => 'Licenza', - 'ALBUM_SET_LICENSE' => 'Imposta licenza', - 'ALBUM_LICENSE_HELP' => 'Hai bisogno di aiuto per scegliere?', - 'ALBUM_LICENSE_NONE' => 'Nessuna', - 'ALBUM_RESERVED' => 'Tutti i Diritti Riservati', - 'ALBUM_SET_ORDER' => 'Set Order', - 'ALBUM_ORDERING' => 'Order by', - - 'PHOTO_ABOUT' => 'Informazioni', - 'PHOTO_BASICS' => 'Base', - 'PHOTO_TITLE' => 'Titolo', - 'PHOTO_NEW_TITLE' => 'Inserire un nuovo titolo per questa foto:', - 'PHOTO_SET_TITLE' => 'Imposta Titolo', - 'PHOTO_UPLOADED' => 'Caricata', - 'PHOTO_DESCRIPTION' => 'Descrizione', - 'PHOTO_NEW_DESCRIPTION' => 'Inserire una nuova descrizione per questa foto:', - 'PHOTO_SET_DESCRIPTION' => 'Imposta Descrizione', - 'PHOTO_NEW_LICENSE' => 'Aggiungi una Licenze', - 'PHOTO_SET_LICENSE' => 'Imposta Licenza', - 'PHOTO_LICENSE' => 'Licenza', - 'PHOTO_REUSE' => 'Riutilizzo', - 'PHOTO_LICENSE_NONE' => 'Nessuna', - 'PHOTO_RESERVED' => 'Tutti i Diritti Riservati', - 'PHOTO_LATITUDE' => 'Latitude', - 'PHOTO_LONGITUDE' => 'Longitude', - 'PHOTO_ALTITUDE' => 'Altitude', - 'PHOTO_LOCATION' => 'Location', - 'PHOTO_IMGDIRECTION' => 'Direction', - 'PHOTO_IMAGE' => 'Immagine', - 'PHOTO_VIDEO' => 'Video', - 'PHOTO_SIZE' => 'Dimensioni', - 'PHOTO_FORMAT' => 'Formato', - 'PHOTO_RESOLUTION' => 'Risoluzione', - 'PHOTO_DURATION' => 'Durata', - 'PHOTO_FPS' => 'Frame rate', - 'PHOTO_TAGS' => 'Tag', - 'PHOTO_NOTAGS' => 'Nessun Tag', - 'PHOTO_NEW_TAGS' => 'Inserisci i tuoi tag per questa foto. Puoi aggiungere più tag separandoli con una virgola:', - 'PHOTO_NEW_TAGS_1' => 'Inserisci i tuoi tag per tutte', - 'PHOTO_NEW_TAGS_2' => 'le foto selezionate. I tag esistenti verrano sovrascritti. Puoi aggiungere più tag separandoli con una virgola:', - 'PHOTO_SET_TAGS' => 'Imposta Tag', - 'PHOTO_CAMERA' => 'Fotocamera', - 'PHOTO_CAPTURED' => 'Scattata', - 'PHOTO_MAKE' => 'Produttore', - 'PHOTO_TYPE' => 'Tipo/Modello', - 'PHOTO_LENS' => 'Lens', - 'PHOTO_SHUTTER' => 'Tempo di Esposizione', - 'PHOTO_APERTURE' => 'Apertura', - 'PHOTO_FOCAL' => 'Lunghezza Focale', - 'PHOTO_ISO' => 'ISO', - 'PHOTO_SHARING' => 'Condivisione', - 'PHOTO_SHR_PLUBLIC' => 'Pubblica', - 'PHOTO_SHR_ALB' => 'Si (Album)', - 'PHOTO_SHR_PHT' => 'Si (Foto)', - 'PHOTO_SHR_NO' => 'No', - 'PHOTO_DELETE' => 'Elimina Photo', - 'PHOTO_KEEP' => 'Mantieni Photo', - 'PHOTO_DELETE_1' => 'Sei sicuro di voler eliminare la foto', - 'PHOTO_DELETE_2' => '? Questa operazione non può essere annullata successivamente!', - 'PHOTO_DELETE_ALL_1' => 'Sei sicuro di voler eliminare tutte le', - 'PHOTO_DELETE_ALL_2' => 'foto selezionate? Questa operazione non può essere annullata successivamente!', - 'PHOTO_NEW_TITLE' => 'Inserisci un nuovo titolo per questa foto:', - 'PHOTOS_NEW_TITLE_1' => 'Inserisci un titolo per tutte le', - 'PHOTOS_NEW_TITLE_2' => 'foto selezionate:', - 'PHOTO_MAKE_PRIVATE_ALBUM' => 'Questa foto è all\'interno di un album pubblico. Per rendere questa foto privata o pubblica, modifica la visibilità dell\'album associato.', - 'PHOTO_SHOW_ALBUM' => 'Visualizza Album', - 'PHOTO_PUBLIC' => 'Public', - 'PHOTO_PUBLIC_EXPL' => 'Photo can be viewed by others, subject to the restrictions below.', - 'PHOTO_FULL' => 'Original', - 'PHOTO_FULL_EXPL' => 'Full-resolution picture is available.', - 'PHOTO_HIDDEN' => 'Hidden', - 'PHOTO_HIDDEN_EXPL' => 'Only people with the direct link can view this photo.', - 'PHOTO_DOWNLOADABLE' => 'Downloadable', - 'PHOTO_DOWNLOADABLE_EXPL' => 'Visitors of your gallery can download this photo.', - 'PHOTO_SHARE_BUTTON_VISIBLE' => 'Share button is visible', - 'PHOTO_SHARE_BUTTON_VISIBLE_EXPL' => 'Display social media sharing links.', - 'PHOTO_PASSWORD_PROT' => 'Password protected', - 'PHOTO_PASSWORD_PROT_EXPL' => 'Photo only accessible with a valid password.', - 'PHOTO_EDIT_SHARING_TEXT' => 'The sharing properties of this photo will be changed to the following:', - 'PHOTO_NO_EDIT_SHARING_TEXT' => 'Because this photo is located in a public album, it inherits that album\'s visibility settings. Its current visibility is shown below for informational purposes only.', - 'PHOTO_EDIT_GLOBAL_SHARING_TEXT' => 'The visibility of this photo can be fine-tuned using global Lychee settings. Its current visibility is shown below for informational purposes only.', - 'PHOTO_SHARING_CONFIRM' => 'Save', - - 'LOADING' => 'Caricamento', - 'ERROR' => 'Errore', - 'ERROR_TEXT' => 'Oops, sembra che qualcosa sia andato storto. Per favore ricarica il sito e prova di nuovo!', - 'ERROR_DB_1' => 'Impossibile connettersi al database host a causa di un accesso negato. Ricontrolla il tuo host, nome utente e password e assicurati che l\'accesso dalla tua posizione corrente sia permessa.', - 'ERROR_DB_2' => 'Impossibile creare il database. Ricontrolla il tuo host, nome utente e password e assicurati che that l\'utente specificato abbia i privilegi per modificare e aggiungere contenuto al database.', - 'ERROR_CONFIG_FILE' => "Impossibile salvare questa configurazione. Permessi negati in \'data/\'. Per favore imposta i diritti di lettura, scrittura ed esecuzione per gli utenti esterni in \'data/\' e \'uploads/\'. Controlla il readme per più informazioni.", - 'ERROR_UNKNOWN' => 'È successo qualcosa di inaspettato. Per favore prova di nuovo e controlla la tua installazione e il tuo server. Controlla il readme per più informazioni.', - 'ERROR_LOGIN' => 'Impossibile salvare il login. Per favore prova con altri nomi utenti e password!', - 'ERROR_MAP_DEACTIVATED' => 'Map functionality has been deactivated under settings.', - 'ERROR_SEARCH_DEACTIVATED' => 'Search functionality has been deactivated under settings.', - 'SUCCESS' => 'OK', - 'RETRY' => 'Riprova', - - 'SETTINGS_SUCCESS_LOGIN' => 'Informazioni di Login Aggiornate.', - 'SETTINGS_SUCCESS_SORT' => 'Modalità di ordinamento aggiornate.', - 'SETTINGS_SUCCESS_DROPBOX' => 'Chaive Dropbox aggiornata.', - 'SETTINGS_SUCCESS_LANG' => 'Lingua aggiornata', - 'SETTINGS_SUCCESS_LAYOUT' => 'Layout aggiornato', - 'SETTINGS_SUCCESS_IMAGE_OVERLAY' => 'Impostazioni filigrana EXIF aggiornate', - 'SETTINGS_SUCCESS_PUBLIC_SEARCH' => 'Ricerca pubblica aggiornata', - 'SETTINGS_SUCCESS_LICENSE' => 'Licenza predefinita aggiornata', - 'SETTINGS_SUCCESS_MAP_DISPLAY' => 'Map display settings updated', - 'SETTINGS_SUCCESS_MAP_DISPLAY_PUBLIC' => 'Map display settings for public albums updated', - 'SETTINGS_SUCCESS_MAP_PROVIDER' => 'Map provider settings updated', - - 'U2F_NOT_SUPPORTED' => 'U2F not supported. Sorry.', - 'U2F_NOT_SECURE' => 'Environment not secured. U2F not available.', - 'U2F_REGISTER_KEY' => 'Register new device.', - 'U2F_REGISTRATION_SUCCESS' => 'Registration successful!', - 'U2F_AUTHENTIFICATION_SUCCESS' => 'Authentication successful!', - 'U2F_CREDENTIALS' => 'Credentials', - 'U2F_CREDENTIALS_DELETED' => 'Credentials deleted!', - - 'NEW_PHOTOS_NOTIFICATION' => 'Send new photos notification emails.', - 'SETTINGS_SUCCESS_NEW_PHOTOS_NOTIFICATION' => 'New photos notification updated', - 'USER_EMAIL_INSTRUCTION' => 'Add your email below to enable receiving email notifications.
To stop receiving emails, simply remove your email below.', - - 'DB_INFO_TITLE' => 'Inserisci i dati per la connessione al database di seguito:', - 'DB_INFO_HOST' => 'Host Database (opzionale)', - 'DB_INFO_USER' => 'Nome Utente Database', - 'DB_INFO_PASSWORD' => 'Password Database', - 'DB_INFO_TEXT' => 'Lychee creerà il suo proprio database. Se necessario, puoi inserire il nome di un database esistente:', - 'DB_NAME' => 'Nome Database (opzionale)', - 'DB_PREFIX' => 'Prefisso Tabelle (opzionale)', - 'DB_CONNECT' => 'Connetti', - - 'LOGIN_TITLE' => 'Inserisci un nome utente e una password per la tua installazione:', - 'LOGIN_USERNAME' => 'Nuovo Nome Utente', - 'LOGIN_PASSWORD' => 'Nuova Password', - 'LOGIN_PASSWORD_CONFIRM' => 'Conferma Password', - 'LOGIN_CREATE' => 'Crea Login', - - 'PASSWORD_TITLE' => 'Inserisci la tua password attuale:', - 'USERNAME_CURRENT' => 'Nome Utente Attuale', - 'PASSWORD_CURRENT' => 'Password Attuale', - 'PASSWORD_TEXT' => 'Il tuo nome utente e password verrano cambiati nei seguenti:', - 'PASSWORD_CHANGE' => 'Cambia Login', - - 'EDIT_SHARING_TITLE' => 'Modifica Condivisibilità', - 'EDIT_SHARING_TEXT' => 'Le proprietà di condivisione di questo album verrano cambiate nelle seguenti:', - 'SHARE_ALBUM_TEXT' => 'Questo album verrà condiviso con le seguenti proprietà:', - 'ALBUM_SHARING_CONFIRM' => 'Save', - - 'SORT_ALBUM_BY_1' => 'Ordina album per', - 'SORT_ALBUM_BY_2' => 'in un ordine', - 'SORT_ALBUM_BY_3' => '.', - - 'SORT_ALBUM_SELECT_1' => 'Data di Creazione', - 'SORT_ALBUM_SELECT_2' => 'Titolo', - 'SORT_ALBUM_SELECT_3' => 'Descrizione', - 'SORT_ALBUM_SELECT_4' => 'Pubblico', - 'SORT_ALBUM_SELECT_5' => 'Ultima Aggiornamento', - 'SORT_ALBUM_SELECT_6' => 'Aggiornamento più vecchio', - - 'SORT_PHOTO_BY_1' => 'Ordina foto per', - 'SORT_PHOTO_BY_2' => 'in un ordine', - 'SORT_PHOTO_BY_3' => '.', - - 'SORT_PHOTO_SELECT_1' => 'Data di Upload', - 'SORT_PHOTO_SELECT_2' => 'Data di Creazione', - 'SORT_PHOTO_SELECT_3' => 'Titolo', - 'SORT_PHOTO_SELECT_4' => 'Descrizione', - 'SORT_PHOTO_SELECT_5' => 'Pubblico', - 'SORT_PHOTO_SELECT_6' => 'Speciale', - 'SORT_PHOTO_SELECT_7' => 'Formato Photo', - - 'SORT_ASCENDING' => 'Crescente', - 'SORT_DESCENDING' => 'Decrescente', - 'SORT_CHANGE' => 'Cambia Ordinamento', - - 'DROPBOX_TITLE' => 'Imposta Chiave Dropbox', - 'DROPBOX_TEXT' => "Per importare foto dal tuo Dropbox, ha bisogno di una chiave valida ottenibile da their website. Genera la tua chiave personale e inseriscila qui di seguito:", - - 'LANG_TEXT' => 'Cambia Lingua Lychee per:', - 'LANG_TITLE' => 'Cambia Lingua', - - 'LAYOUT_TYPE' => 'Layout delle foto:', - 'LAYOUT_SQUARES' => 'Miniature Quadrate', - 'LAYOUT_JUSTIFIED' => 'Relativo all\'aspetto, giustificate', - 'LAYOUT_UNJUSTIFIED' => 'Relativo all\'aspetto, non giustificate', - 'SET_LAYOUT' => 'Cambia layout', - - 'PUBLIC_SEARCH_TEXT' => 'Ricerca pubblica consentita:', - 'OVERLAY_TYPE' => 'Contenuto da utilizzare nella filigrana:', - 'OVERLAY_NONE' => 'None', - 'OVERLAY_EXIF' => 'Dati Foto EXIF', - 'OVERLAY_DESCRIPTION' => 'Descrizione della Foto', - 'OVERLAY_DATE' => 'Data di Creazione della Foto', - 'MAP_DISPLAY_TEXT' => 'Enable maps (provided by OpenStreetMap):', - 'MAP_DISPLAY_PUBLIC_TEXT' => 'Enable maps for public albums (provided by OpenStreetMap):', - 'MAP_PROVIDER' => 'Provider of OpenStreetMap tiles:', - 'MAP_PROVIDER_WIKIMEDIA' => 'Wikimedia', - 'MAP_PROVIDER_OSM_ORG' => 'OpenStreetMap.org (no HiDPI)', - 'MAP_PROVIDER_OSM_DE' => 'OpenStreetMap.de (no HiDPI)', - 'MAP_PROVIDER_OSM_FR' => 'OpenStreetMap.fr (no HiDPI)', - 'MAP_PROVIDER_RRZE' => 'University of Erlangen, Germany (only HiDPI)', - 'MAP_INCLUDE_SUBALBUMS_TEXT' => 'Include photos of subalbums on map:', - 'LOCATION_DECODING' => 'Decode GPS data into location name', - 'LOCATION_SHOW' => 'Show location name', - 'LOCATION_SHOW_PUBLIC' => 'Show location name for public mode', - - 'NSFW_VISIBLE_TEXT_1' => 'Make Sensitive albums visible by default.', - 'NSFW_VISIBLE_TEXT_2' => 'If the album is public, it is still accessible, just hidden from the view and can be revealed by pressing H.', - 'SETTINGS_SUCCESS_NSFW_VISIBLE' => 'Default sensitive album visibility updated with success.', - - 'VIEW_NO_RESULT' => 'Nessun risultato', - 'VIEW_NO_PUBLIC_ALBUMS' => 'Nessun album pubblico', - 'VIEW_NO_CONFIGURATION' => 'Nessuna configurazione', - 'VIEW_PHOTO_NOT_FOUND' => 'Foto non trovata', - - 'NO_TAGS' => 'Nessun Tag', - - 'UPLOAD_MANAGE_NEW_PHOTOS' => 'Adesso puoi gestire le tue nuove foto.', - 'UPLOAD_COMPLETE' => 'Caricamento completato', - 'UPLOAD_COMPLETE_FAILED' => 'Caricamento fallito per una o più foto.', - 'UPLOAD_IMPORTING' => 'Importazione', - 'UPLOAD_IMPORTING_URL' => 'Importazione URL', - 'UPLOAD_UPLOADING' => 'Caricamento', - 'UPLOAD_FINISHED' => 'Finito', - 'UPLOAD_PROCESSING' => 'In Elaborazione', - 'UPLOAD_FAILED' => 'Fallito', - 'UPLOAD_FAILED_ERROR' => 'Caricamento fallito. Il server ha restituito un errore!', - 'UPLOAD_FAILED_WARNING' => 'Caricamento fallito. Il server ha restituito un avviso!', - 'UPLOAD_CANCELLED' => 'Cancelled', - 'UPLOAD_SKIPPED' => 'Saltato', - 'UPLOAD_UPDATED' => 'Updated', - 'UPLOAD_IMPORT_SKIPPED_DUPLICATE' => 'This photo has been skipped because it\'s already in your library.', - 'UPLOAD_IMPORT_RESYNCED_DUPLICATE' => 'This photo has been skipped because it\'s already in your library, but its metadata has been updated.', - 'UPLOAD_ERROR_CONSOLE' => 'Per favore controlla la console del tuo browser per ulteriori dettagli.', - 'UPLOAD_UNKNOWN' => 'Il server ha restituito una risposta sconosciuta. Per favore controlla la console del tuo browser per ulteriori dettagli.', - 'UPLOAD_ERROR_UNKNOWN' => 'Caricamneto fallito. Il server ha restituito un errore sconosciuto!', - 'UPLOAD_ERROR_POSTSIZE' => 'Upload failed. The PHP post_max_size may be too small! Otherwise check the FAQ.', - 'UPLOAD_ERROR_FILESIZE' => 'Upload failed. The PHP upload_max_filesize may be too small! Otherwise check the FAQ.', - 'UPLOAD_IN_PROGRESS' => 'Lychee sta momentaneamente caricando!', - 'UPLOAD_IMPORT_WARN_ERR' => 'L\'importazione è finita, ma ha restituito errori o avvisi. Per favore controlla il log (Impostazioni -> Visualizza Log) per ulteriori dettagli.', - 'UPLOAD_IMPORT_COMPLETE' => 'Importazione completata', - 'UPLOAD_IMPORT_INSTR' => 'Per favore inserisci il link diretto alla foto per importarla:', - 'UPLOAD_IMPORT' => 'Importa', - 'UPLOAD_IMPORT_SERVER' => 'Importa da server', - 'UPLOAD_IMPORT_SERVER_FOLD' => 'Cartella vuota o nessun file leggibile da elaborare. Per favore controlla il log (Impostazioni -> Visualizza Log) per ulteriori dettagli.', - 'UPLOAD_IMPORT_SERVER_INSTR' => 'Questa azione importerà tutte le foto, cartelle e sottocartelle presenti nella seguente directory.', - 'UPLOAD_ABSOLUTE_PATH' => 'Percorso assoluto alla directory', - 'UPLOAD_IMPORT_SERVER_EMPT' => 'È stato impossibile avviare l\'importazione dato che la cartella era vuota!', - 'UPLOAD_IMPORT_DELETE_ORIGINALS' => 'Delete originals', - 'UPLOAD_IMPORT_DELETE_ORIGINALS_EXPL' => 'I file originali saranno eliminati dopo l\'importazione se possibile.', - 'UPLOAD_IMPORT_VIA_SYMLINK' => 'Symbolic links', - 'UPLOAD_IMPORT_VIA_SYMLINK_EXPL' => 'Import files using symbolic links to originals.', - 'UPLOAD_IMPORT_SKIP_DUPLICATES' => 'Skip duplicates', - 'UPLOAD_IMPORT_SKIP_DUPLICATES_EXPL' => 'Existing media files are skipped.', - 'UPLOAD_IMPORT_RESYNC_METADATA' => 'Re-sync metadata', - 'UPLOAD_IMPORT_RESYNC_METADATA_EXPL' => 'Update metadata of existing media files.', - 'UPLOAD_IMPORT_LOW_MEMORY' => 'Low memory condition!', - 'UPLOAD_IMPORT_LOW_MEMORY_EXPL' => 'The import process on the server is approaching the memory limit and may end up being terminated prematurely.', - 'UPLOAD_WARNING' => 'Warning', - 'UPLOAD_IMPORT_NOT_A_DIRECTORY' => 'The given path is not a readable directory!', - 'UPLOAD_IMPORT_PATH_RESERVED' => 'The given path is a reserved path of Lychee!', - 'UPLOAD_IMPORT_UNREADABLE' => 'Could not read the file!', - 'UPLOAD_IMPORT_FAILED' => 'Could not import the file!', - 'UPLOAD_IMPORT_UNSUPPORTED' => 'Unsupported file type!', - 'UPLOAD_IMPORT_ALBUM_FAILED' => 'Could not create the album!', - 'UPLOAD_IMPORT_CANCELLED' => 'Import cancelled', - - 'ABOUT_SUBTITLE' => 'Gestione propria delle foto fatta nel modo giusto', - 'ABOUT_DESCRIPTION' => 'è uno strumento gratuito di gestione delle foto, eseguito nel server o sul tuo spazio web. L\'installazione è questione di secondi. Carica, gestisci e condividi foto come in un\'applicazione nativa. Lychee offre tutto ciò di cui hai bisogno e tutte le tue foto vengono salvate in modo sicuro.', - 'FOOTER_COPYRIGHT' => 'Tutte le immagini su questo sito web sono soggette a Copyright di', - 'HOSTED_WITH_LYCHEE' => 'Hosted with Lychee', - - 'URL_COPY_TO_CLIPBOARD' => 'Copy to clipboard', - 'URL_COPIED_TO_CLIPBOARD' => 'Copied URL to clipboard!', - 'PHOTO_DIRECT_LINKS_TO_IMAGES' => 'Direct links to image files:', - 'PHOTO_MEDIUM' => 'Medium', - 'PHOTO_MEDIUM_HIDPI' => 'Medium HiDPI', - 'PHOTO_SMALL' => 'Thumb', - 'PHOTO_SMALL_HIDPI' => 'Thumb HiDPI', - 'PHOTO_THUMB' => 'Square thumb', - 'PHOTO_THUMB_HIDPI' => 'Square thumb HiDPI', - 'PHOTO_LIVE_VIDEO' => 'Video part of live-photo', - 'PHOTO_VIEW' => 'Lychee Photo View:', - - 'PHOTO_EDIT_ROTATECWISE' => 'Ruota in senso orario', - 'PHOTO_EDIT_ROTATECCWISE' => 'Ruota in senso anti-orario', - ]; - - return $locale; - } -} diff --git a/app/Locale/Lang.php b/app/Locale/Lang.php deleted file mode 100644 index fe37b089773..00000000000 --- a/app/Locale/Lang.php +++ /dev/null @@ -1,68 +0,0 @@ -langFactory = $langFactory; - - $this->code = Configs::get_value('lang', 'en'); - - $this->language = $langFactory->make($this->code); - } - - /** - * Quickly translate a string (used with the Facade). - */ - public function get(string $string) - { - return $this->language->get_locale()[$string]; - } - - /** - * Return code (mostly for HTML). - */ - public function get_code() - { - return $this->language->code(); - } - - /** - * Return the language array (AJAX initialization). - */ - public function get_lang() - { - return $this->language->get_locale(); - } - - /** - * Return the languages available (AJAX initialization & settings). - */ - public function get_lang_available() - { - return $this->langFactory->getCodes(); - } - - public function factory(): LangFactory - { - return $this->langFactory; - } -} diff --git a/app/Locale/NorwegianBokmal.php b/app/Locale/NorwegianBokmal.php deleted file mode 100644 index 960de388b49..00000000000 --- a/app/Locale/NorwegianBokmal.php +++ /dev/null @@ -1,476 +0,0 @@ - 'brukernavn', - 'PASSWORD' => 'passord', - 'ENTER' => 'Stig inn', - 'CANCEL' => 'Avbryt', - 'SIGN_IN' => 'Logg inn', - 'CLOSE' => 'Lukk', - 'SETTINGS' => 'Innstillinger', - 'SEARCH' => 'Søk ...', - 'MORE' => 'Mer', - 'DEFAULT' => 'Default', - - 'USERS' => 'Brukere', - 'U2F' => 'U2F', - 'NOTIFICATIONS' => 'Notifications', - 'SHARING' => 'Deling', - 'CHANGE_LOGIN' => 'Endre Bruker', - 'CHANGE_SORTING' => 'Endre sortering', - 'SET_DROPBOX' => 'Lagre Dropbox', - 'ABOUT_LYCHEE' => 'Om Lychee', - 'DIAGNOSTICS' => 'Diagnostikk', - 'DIAGNOSTICS_GET_SIZE' => 'Hent diskbruk', - 'LOGS' => 'Vis Logg', - 'SIGN_OUT' => 'Logg Ut', - 'UPDATE_AVAILABLE' => 'Oppdatering er tilgjengelig!', - 'MIGRATION_AVAILABLE' => 'Migrering er tilgjengelig!', - 'DEFAULT_LICENSE' => 'Standard lisens for nye opplastinger:', - 'SET_LICENSE' => 'Lagre Lisens', - 'SET_OVERLAY_TYPE' => 'Lagre overvisning', - 'SET_MAP_PROVIDER' => 'Lagre leverandør for OpenStreetMap fliser', - - 'SMART_ALBUMS' => 'Automatiske album', - 'SHARED_ALBUMS' => 'Delte album', - 'ALBUMS' => 'Album', - 'PHOTOS' => 'Bilder', - 'SEARCH_RESULTS' => 'Søkeresultater', - - 'RENAME' => 'Gi nytt navn', - 'RENAME_ALL' => 'Gi nytt navn til Valgte', - 'MERGE' => 'Slå sammen', - 'MERGE_ALL' => 'Slå sammen Valgte', - 'MAKE_PUBLIC' => 'Gjør Offentlig', - 'SHARE_ALBUM' => 'Del Albumet', - 'SHARE_PHOTO' => 'Del Bilde', - 'VISIBILITY_ALBUM' => 'Albumsynlighet', - 'VISIBILITY_PHOTO' => 'Bildesynlighet', - 'DOWNLOAD_ALBUM' => 'Last ned Albumet', - 'ABOUT_ALBUM' => 'Om Albumet', - 'DELETE_ALBUM' => 'Fjern Albumet', - 'MOVE_ALBUM' => 'Flytt Albumet', - 'FULLSCREEN_ENTER' => 'Gå i Fullskjermvisning', - 'FULLSCREEN_EXIT' => 'Slutt Fullskjermvisning', - - 'SHARING_ALBUM_USERS' => 'Share this album with users', - 'WAIT_FETCH_DATA' => 'Please wait while we get the data...', - 'SHARING_ALBUM_USERS_NO_USERS' => 'There are no users to share the album with', - 'SHARING_ALBUM_USERS_LONG_MESSAGE' => 'Select the users to share this album with', - - 'DELETE_ALBUM_QUESTION' => 'Fjern Album og Bilder', - 'KEEP_ALBUM' => 'Behold Album', - 'DELETE_ALBUM_CONFIRMATION_1' => 'Ønsker du virkelig å fjerne album', - 'DELETE_ALBUM_CONFIRMATION_2' => 'og alle bildene i det? Denne handlingen kan ikke angres!', - - 'DELETE_ALBUMS_QUESTION' => 'Fjern Album og Bilder', - 'KEEP_ALBUMS' => 'Behold Album', - 'DELETE_ALBUMS_CONFIRMATION_1' => 'Ønsker du virkelig å fjerne', - 'DELETE_ALBUMS_CONFIRMATION_2' => 'valgte album og alle bildene i disse? Denne handlingen kan ikke angres!', - - 'DELETE_UNSORTED_CONFIRM' => 'Ønsker du virkelig å fjerne alle bilder fra \'Usorterte\'?
Denne handlingen kan ikke angres!', - 'CLEAR_UNSORTED' => 'Fjern Usorterte', - 'KEEP_UNSORTED' => 'Behold Usorterte', - - 'EDIT_SHARING' => 'Endre Deling', - 'MAKE_PRIVATE' => 'Gjør Privat', - - 'CLOSE_ALBUM' => 'Lukk Album', - 'CLOSE_PHOTO' => 'Lukk Bilde', - 'CLOSE_MAP' => 'Lukk Kart', - - 'ADD' => 'Legg til', - 'MOVE' => 'Flytt', - 'MOVE_ALL' => 'Flytt Valgte', - 'DUPLICATE' => 'Dupliser', - 'DUPLICATE_ALL' => 'Dupliser Valgte', - 'COPY_TO' => 'Kopier til...', - 'COPY_ALL_TO' => 'Kopier Valgte til...', - 'DELETE' => 'Fjern', - 'DELETE_ALL' => 'Fjern Valgte', - 'DOWNLOAD' => 'Last ned', - 'DOWNLOAD_ALL' => 'Last ned Valgte', - 'UPLOAD_PHOTO' => 'Last opp Bilde', - 'IMPORT_LINK' => 'Importer fra Lenke', - 'IMPORT_DROPBOX' => 'Importer fra Dropbox', - 'IMPORT_SERVER' => 'Importer fra Serveren', - 'NEW_ALBUM' => 'Nytt Album', - 'NEW_TAG_ALBUM' => 'New Tag Album', - - 'TITLE_NEW_ALBUM' => 'Legg inn en tittel for det nye albumet:', - 'UNTITLED' => 'Uten Tittel', - 'UNSORTED' => 'Usorterte', - 'STARRED' => 'Favoritter', - 'RECENT' => 'Nylige', - 'PUBLIC' => 'Offentlige', - 'NUM_PHOTOS' => 'Bilder', - - 'CREATE_ALBUM' => 'Lag Album', - 'CREATE_TAG_ALBUM' => 'Create Tag Album', - - 'STAR_PHOTO' => 'Stjernemerk Bilde', - 'STAR' => 'Stjernemerk', - 'STAR_ALL' => 'Stjernemerk Valgte', - 'TAGS' => 'Tagg', - 'TAGS_ALL' => 'Tagg Valgte', - 'UNSTAR_PHOTO' => 'Fjern Stjernemerke', - 'SET_COVER' => 'Set Album Cover', - 'REMOVE_COVER' => 'Remove Album Cover', - - 'FULL_PHOTO' => 'Originalbildet', - 'ABOUT_PHOTO' => 'Om Bildet', - 'DISPLAY_FULL_MAP' => 'Kart', - 'DIRECT_LINK' => 'Direktelenke', - 'DIRECT_LINKS' => 'Direktelenker', - - 'ALBUM_ABOUT' => 'Om', - 'ALBUM_BASICS' => 'Grunnleggende', - 'ALBUM_TITLE' => 'Tittel', - 'ALBUM_NEW_TITLE' => 'Legg inn en ny tittel for Albumet:', - 'ALBUMS_NEW_TITLE_1' => 'Legg inn en ny tittel for', - 'ALBUMS_NEW_TITLE_2' => 'valgte album:', - 'ALBUM_SET_TITLE' => 'Lagre Tittel', - 'ALBUM_DESCRIPTION' => 'Beskrivelse', - 'ALBUM_SHOW_TAGS' => 'Tags to show', - 'ALBUM_NEW_DESCRIPTION' => 'Legg inn en ny beskrivelse for Albumet:', - 'ALBUM_SET_DESCRIPTION' => 'Lagre Beskrivelsen', - 'ALBUM_NEW_SHOWTAGS' => 'Enter tags of photos that will be visible in this album:', - 'ALBUM_SET_SHOWTAGS' => 'Set tags to show', - 'ALBUM_ALBUM' => 'Album', - 'ALBUM_CREATED' => 'Laget', - 'ALBUM_IMAGES' => 'Bilder', - 'ALBUM_VIDEOS' => 'Filmer', - 'ALBUM_SUBALBUMS' => 'Underalbum', - 'ALBUM_SHARING' => 'Deling', - 'ALBUM_SHR_YES' => 'JA', - 'ALBUM_SHR_NO' => 'Nei', - 'ALBUM_PUBLIC' => 'Offentlig', - 'ALBUM_PUBLIC_EXPL' => 'Album er synlige for andre, på betingelsene gitt under.', - 'ALBUM_FULL' => 'Original', - 'ALBUM_FULL_EXPL' => 'Fulloppløsningsbilder er tilgjengelig.', - 'ALBUM_HIDDEN' => 'Skjult', - 'ALBUM_HIDDEN_EXPL' => 'Bare folk med direkte lenke kan se Albumet', - 'ALBUM_MARK_NSFW' => 'Mark album as sensitive', - 'ALBUM_UNMARK_NSFW' => 'Unmark album as sensitive', - 'ALBUM_NSFW' => 'Sensitive', - 'ALBUM_NSFW_EXPL' => 'Album is marked to contain sensitive content.', - 'ALBUM_DOWNLOADABLE' => 'Nedlastbar', - 'ALBUM_DOWNLOADABLE_EXPL' => 'Besøkede av galleriet ditt kan laste ned Albumet.', - 'ALBUM_SHARE_BUTTON_VISIBLE' => 'Delingsknappen er synlig', - 'ALBUM_SHARE_BUTTON_VISIBLE_EXPL' => 'Vis lenker for deling på sosiale medier.', - 'ALBUM_PASSWORD' => 'Passord', - 'ALBUM_PASSWORD_PROT' => 'Passordbeskyttet', - 'ALBUM_PASSWORD_PROT_EXPL' => 'Albumet er bare tilgjengelig med et gyldig passord.', - 'ALBUM_PASSWORD_REQUIRED' => 'Albumet er beskyttet med et passord. Fyll inn passordet under for å se bildene i Albumet:', - 'ALBUM_MERGE_1' => 'Ønsker du virkelig slå sammen album', - 'ALBUM_MERGE_2' => 'med album', - 'ALBUMS_MERGE' => 'Ønsker du virkelig å slå sammen alle valge album til albumet', - 'MERGE_ALBUM' => 'Slå sammen Album', - 'DONT_MERGE' => 'Ikke slå sammen', - 'ALBUM_MOVE_1' => 'Ønsker du virkelig å flytte album', - 'ALBUM_MOVE_2' => 'inn i album', - 'ALBUMS_MOVE' => 'Ønsker du virkelig å flytte alle valge album inn i album', - 'MOVE_ALBUMS' => 'Flytt Album', - 'NOT_MOVE_ALBUMS' => 'Ikke flytt', - 'ROOT' => 'Album', - 'ALBUM_REUSE' => 'Bruk om igjen', - 'ALBUM_LICENSE' => 'Lisens', - 'ALBUM_SET_LICENSE' => 'Lagre Lisens', - 'ALBUM_LICENSE_HELP' => 'Trenger du hjelp for å velge?', - 'ALBUM_LICENSE_NONE' => 'Ingen', - 'ALBUM_RESERVED' => 'Alle Rettigheter Forbeholdt', - 'ALBUM_SET_ORDER' => 'Set Order', - 'ALBUM_ORDERING' => 'Order by', - - 'PHOTO_ABOUT' => 'Om', - 'PHOTO_BASICS' => 'Grunnleggende', - 'PHOTO_TITLE' => 'Tittel', - 'PHOTO_NEW_TITLE' => 'Fyll inn en ny tittel for bildet:', - 'PHOTO_SET_TITLE' => 'Lagre Tittelen', - 'PHOTO_UPLOADED' => 'Opplastet', - 'PHOTO_DESCRIPTION' => 'Beskrivelse', - 'PHOTO_NEW_DESCRIPTION' => 'Fyll inn en ny beskrivelse for dette bildet:', - 'PHOTO_SET_DESCRIPTION' => 'Lagre Beskrivelsen', - 'PHOTO_NEW_LICENSE' => 'Legg til en Lisens', - 'PHOTO_SET_LICENSE' => 'Lagre Lisens', - 'PHOTO_LICENSE' => 'Lisens', - 'PHOTO_REUSE' => 'Bruk om igjen', - 'PHOTO_LICENSE_NONE' => 'Ingen', - 'PHOTO_RESERVED' => 'Alle Rettigheter Forbeholdt', - 'PHOTO_LATITUDE' => 'Breddegrad', - 'PHOTO_LONGITUDE' => 'Lengdegrad', - 'PHOTO_ALTITUDE' => 'Høyde', - 'PHOTO_IMGDIRECTION' => 'Retning', - 'PHOTO_LOCATION' => 'Sted', - 'PHOTO_IMAGE' => 'Bilde', - 'PHOTO_VIDEO' => 'Film', - 'PHOTO_SIZE' => 'Størrelse', - 'PHOTO_FORMAT' => 'Format', - 'PHOTO_RESOLUTION' => 'Oppløsning', - 'PHOTO_DURATION' => 'Lengde', - 'PHOTO_FPS' => 'Bilderate', - 'PHOTO_TAGS' => 'Tagger', - 'PHOTO_NOTAGS' => 'Ingen Tagger', - 'PHOTO_NEW_TAGS' => 'Fyll inn tagger for dette bildet. Du kan legge inn flere tagger ved å dele de med komma', - 'PHOTO_NEW_TAGS_1' => 'Legg inn tagger for', - 'PHOTO_NEW_TAGS_2' => 'valgte bilder. Tagger vil bli overskrevet. Du kan legge inn flere tagger ved å dele de med komma', - 'PHOTO_SET_TAGS' => 'Lagre Tagger', - 'PHOTO_CAMERA' => 'Kamera', - 'PHOTO_CAPTURED' => 'Tatt', - 'PHOTO_MAKE' => 'Produsent', - 'PHOTO_TYPE' => 'Type/Modell', - 'PHOTO_LENS' => 'Linse', - 'PHOTO_SHUTTER' => 'Lukkertid', - 'PHOTO_APERTURE' => 'Blendertall', - 'PHOTO_FOCAL' => 'Brennvidde', - 'PHOTO_ISO' => 'ISO', - 'PHOTO_SHARING' => 'Deling', - 'PHOTO_SHR_PLUBLIC' => 'Offentlig', - 'PHOTO_SHR_ALB' => 'Ja (Album)', - 'PHOTO_SHR_PHT' => 'Ja (Bilde)', - 'PHOTO_SHR_NO' => 'Nei', - 'PHOTO_DELETE' => 'Fjern Bilde', - 'PHOTO_KEEP' => 'Behold Bilde', - 'PHOTO_DELETE_1' => 'Ønsker du virkelig å fjerne bilde', - 'PHOTO_DELETE_2' => '? Denne handlingen kan ikke angres!', - 'PHOTO_DELETE_ALL_1' => 'Ønsker du virkelig å fjerne', - 'PHOTO_DELETE_ALL_2' => 'valgte bilder? Denne handlingen kan ikke angres!', - 'PHOTOS_NEW_TITLE_1' => 'Fyll inn en tittel for', - 'PHOTOS_NEW_TITLE_2' => 'valgte bilder:', - 'PHOTO_MAKE_PRIVATE_ALBUM' => 'Bildet er i et offentlig album. Synligheten til bildet kan endres gjennom egenskapene for albumet.', - 'PHOTO_SHOW_ALBUM' => 'Vis Album', - 'PHOTO_PUBLIC' => 'Offentlig', - 'PHOTO_PUBLIC_EXPL' => 'Bildet kan bli sett av andre, på betingelsene gitt under.', - 'PHOTO_FULL' => 'Original', - 'PHOTO_FULL_EXPL' => 'Fullopløsningsbildet er tilgjengelig.', - 'PHOTO_HIDDEN' => 'Gjemt', - 'PHOTO_HIDDEN_EXPL' => 'Bare folk med riktig lenke kan se dette bildet.', - 'PHOTO_DOWNLOADABLE' => 'Kan lastes ned', - 'PHOTO_DOWNLOADABLE_EXPL' => 'Besøkende av galleriet kan laste ned dette bildet.', - 'PHOTO_SHARE_BUTTON_VISIBLE' => 'Delingsknappen er synlig', - 'PHOTO_SHARE_BUTTON_VISIBLE_EXPL' => 'Viser lenker for deling på sosiale meder.', - 'PHOTO_PASSWORD_PROT' => 'Passordbeskyttet', - 'PHOTO_PASSWORD_PROT_EXPL' => 'Bildet er bare tilgjengelig med gyldig passord.', - 'PHOTO_EDIT_SHARING_TEXT' => 'Innstillingene for deling av bildet vil bli endret til:', - 'PHOTO_NO_EDIT_SHARING_TEXT' => 'Dette bildet er i et offentlig album som arver synligheten til albumet. Nåværende synlighet er vist bare for informasjon.', - 'PHOTO_EDIT_GLOBAL_SHARING_TEXT' => 'Synligeten til bildet kan bli finjustert gjennom innstillingene til Lychee. Nåværende synlighet er vist bare for informasjon.', - 'PHOTO_SHARING_CONFIRM' => 'Lagre', - - 'LOADING' => 'Laster', - 'ERROR' => 'Feil', - 'ERROR_TEXT' => 'Oisann, her ser det ut som noe gikk galt. Vennligst last inn siden på nytt og prøv igjen!', - 'ERROR_DB_1' => 'Kan ikke koble til databasen siden tilgang nektes. Du bør kontrollere adressen, brukernavn, passord, og at du har tilgang fra din nåværende adresse.', - 'ERROR_DB_2' => 'Kan ikke lage databasen. Kontroller at adressen, brukernavn, passord, og at brukeren har rettigheter til å forandre og legge til innhold i databasen.', - 'ERROR_CONFIG_FILE' => "Kan ikke lagre innstillingene. Adgang nektes i 'data/''. Kontroller at andre har rettigheter til å lese, skrive, og execute i 'data/' og 'uploads/'. Se readme for mer informasjon.", - 'ERROR_UNKNOWN' => 'Noe uventet skjedde. Prøv på nytt og kontroller installasjonen av Lychee og serveren. Se readme for mer informasjon', - 'ERROR_LOGIN' => 'Kan ikke utføre innloggingen. Vennligst prøv med et annet brukernavn og passord!', - 'ERROR_MAP_DEACTIVATED' => 'Kartfunksjoner har blitt deaktivert under innstillinger', - 'ERROR_SEARCH_DEACTIVATED' => 'Søkefunksjoner har blitt deaktivert under innstillinger', - 'SUCCESS' => 'OK', - 'RETRY' => 'Prøv igjen', - - 'SETTINGS_SUCCESS_LOGIN' => 'Innlogging oppdatert.', - 'SETTINGS_SUCCESS_SORT' => 'Sorteringsrekkefølge oppdatert.', - 'SETTINGS_SUCCESS_DROPBOX' => 'Dropboxnøkkel oppdatert.', - 'SETTINGS_SUCCESS_LANG' => 'Språk oppdatert', - 'SETTINGS_SUCCESS_LAYOUT' => 'Oppsett oppdatert', - 'SETTINGS_SUCCESS_IMAGE_OVERLAY' => 'Instilling for EXIF overvisning oppdatert', - 'SETTINGS_SUCCESS_PUBLIC_SEARCH' => 'Offentlig søk oppdatert', - 'SETTINGS_SUCCESS_LICENSE' => 'Standard lisens oppdatert', - 'SETTINGS_SUCCESS_MAP_DISPLAY' => 'Innstillinger for Kartvisning oppdatert', - 'SETTINGS_SUCCESS_MAP_DISPLAY_PUBLIC' => 'Innstillinger for Kartvisning for offentlige album oppdatert', - 'SETTINGS_SUCCESS_MAP_PROVIDER' => 'Innstillinger for kartleverandør oppdatert', - - 'U2F_NOT_SUPPORTED' => 'U2F not supported. Sorry.', - 'U2F_NOT_SECURE' => 'Environment not secured. U2F not available.', - 'U2F_REGISTER_KEY' => 'Register new device.', - 'U2F_REGISTRATION_SUCCESS' => 'Registration successful!', - 'U2F_AUTHENTIFICATION_SUCCESS' => 'Authentication successful!', - 'U2F_CREDENTIALS' => 'Credentials', - 'U2F_CREDENTIALS_DELETED' => 'Credentials deleted!', - - 'NEW_PHOTOS_NOTIFICATION' => 'Send new photos notification emails.', - 'SETTINGS_SUCCESS_NEW_PHOTOS_NOTIFICATION' => 'New photos notification updated', - 'USER_EMAIL_INSTRUCTION' => 'Add your email below to enable receiving email notifications.
To stop receiving emails, simply remove your email below.', - - 'DB_INFO_TITLE' => 'Fyll inn detaljer om databaseforbindelsen under:', - 'DB_INFO_HOST' => 'Database Adresse (valgfritt)', - 'DB_INFO_USER' => 'Database Brukernavn', - 'DB_INFO_PASSWORD' => 'Database Passord', - 'DB_INFO_TEXT' => 'Lychee vil lage sin egen database. Hvis ønsket kan du isteden benytte navnet på en eksisterende database:', - 'DB_NAME' => 'Database Navn (valgfritt)', - 'DB_PREFIX' => 'Tabellprefix (valgfritt)', - 'DB_CONNECT' => 'Koble til', - - 'LOGIN_TITLE' => 'Fyll inn et brukernavn og passord for installasjonen:', - 'LOGIN_USERNAME' => 'Nytt Brukernavn', - 'LOGIN_PASSWORD' => 'Nytt Passord', - 'LOGIN_PASSWORD_CONFIRM' => 'Bekreft Passord', - 'LOGIN_CREATE' => 'Lag Innlogging', - - 'PASSWORD_TITLE' => 'Fyll inn ditt nåværende passord:', - 'USERNAME_CURRENT' => 'Nåværende Brukernavn', - 'PASSWORD_CURRENT' => 'Nåværende Passord', - 'PASSWORD_TEXT' => 'Brukernavnet og passordet ditt vil bli endret til det følgende:', - 'PASSWORD_CHANGE' => 'Lagre brukernavn og passord', - - 'EDIT_SHARING_TITLE' => 'Endre Deling', - 'EDIT_SHARING_TEXT' => 'Egenskapene for deling for dette albumet vil bli endret til følgende:', - 'SHARE_ALBUM_TEXT' => 'Albumet vil bli delt med følgende egenskaper:', - 'ALBUM_SHARING_CONFIRM' => 'Lagre', - - 'SORT_ALBUM_BY_1' => 'Sorter album etter', - 'SORT_ALBUM_BY_2' => 'i en', - 'SORT_ALBUM_BY_3' => 'rekkefølge.', - - 'SORT_ALBUM_SELECT_1' => 'Opprettelsestid', - 'SORT_ALBUM_SELECT_2' => 'Tittel', - 'SORT_ALBUM_SELECT_3' => 'Beskrivelse', - 'SORT_ALBUM_SELECT_4' => 'Offentlig', - 'SORT_ALBUM_SELECT_5' => 'Seneste fangstdato', - 'SORT_ALBUM_SELECT_6' => 'Eldste fangstdato', - - 'SORT_PHOTO_BY_1' => 'Sorter bilder etter', - 'SORT_PHOTO_BY_2' => 'i en', - 'SORT_PHOTO_BY_3' => 'rekkefølge.', - - 'SORT_PHOTO_SELECT_1' => 'Opplastingstid', - 'SORT_PHOTO_SELECT_2' => 'Fangsdato', - 'SORT_PHOTO_SELECT_3' => 'Tittel', - 'SORT_PHOTO_SELECT_4' => 'Beskrivelse', - 'SORT_PHOTO_SELECT_5' => 'Offentlig', - 'SORT_PHOTO_SELECT_6' => 'Stjernemerk', - 'SORT_PHOTO_SELECT_7' => 'Bildeformat', - - 'SORT_ASCENDING' => 'Stigende', - 'SORT_DESCENDING' => 'Fallende', - 'SORT_CHANGE' => 'Lagre Rekkefølge', - - 'DROPBOX_TITLE' => 'Lagre nøkkel for Dropbox', - 'DROPBOX_TEXT' => "For å importere bilder fra Dropbox trengs en gyldig applikasjonsnøkkel fra deres nettside. Lag en personlig nøkkel og fyll inn denne under:", - - 'LANG_TEXT' => 'Endre språk for Lychee til:', - 'LANG_TITLE' => 'Lagre innstilling for språk', - 'PUBLIC_SEARCH_TEXT' => 'Offentlig søk tillatt:', - 'OVERLAY_TYPE' => 'Data som skal brukes til overvisning:', - 'OVERLAY_NONE' => 'None', - 'OVERLAY_EXIF' => 'EXIF bildedata', - 'OVERLAY_DESCRIPTION' => 'Bildebeskrivelser', - 'OVERLAY_DATE' => 'Dato for når bildet ble tatt', - 'MAP_DISPLAY_TEXT' => 'Skru på kart (levert av OpenStreetMap):', - 'MAP_DISPLAY_PUBLIC_TEXT' => 'Skru på kart for offentlige album (levert av OpenStreetMap):', - 'LOCATION_DECODING' => 'Benytt GPS data for å fylle ut stedsnavn', - 'LOCATION_SHOW' => 'Vis stedsnavn', - 'LOCATION_SHOW_PUBLIC' => 'Vis stedsnavn i offentlig modus', - 'MAP_PROVIDER' => 'Leverandør av OpenStreetMap fliser:', - 'MAP_PROVIDER_WIKIMEDIA' => 'Wikimedia', - 'MAP_PROVIDER_OSM_ORG' => 'OpenStreetMap.org (ikke HiDPI)', - 'MAP_PROVIDER_OSM_DE' => 'OpenStreetMap.de (ikke HiDPI)', - 'MAP_PROVIDER_OSM_FR' => 'OpenStreetMap.fr (ikke HiDPI)', - 'MAP_PROVIDER_RRZE' => 'University of Erlangen, Germany (bare HiDPI)', - 'MAP_INCLUDE_SUBALBUMS_TEXT' => 'Inkluder bilder av underalbum på kart:', - - 'LAYOUT_TYPE' => 'Oppsett for bilder:', - 'LAYOUT_SQUARES' => 'Kvadratiske miniatyrbilder', - 'LAYOUT_JUSTIFIED' => 'Med aspektratio, justert', - 'LAYOUT_UNJUSTIFIED' => 'Med aspektratio, ikke justert', - 'SET_LAYOUT' => 'Lagre oppsett', - - 'NSFW_VISIBLE_TEXT_1' => 'Make Sensitive albums visible by default.', - 'NSFW_VISIBLE_TEXT_2' => 'If the album is public, it is still accessible, just hidden from the view and can be revealed by pressing H.', - 'SETTINGS_SUCCESS_NSFW_VISIBLE' => 'Default sensitive album visibility updated with success.', - - 'VIEW_NO_RESULT' => 'Ingen resultater', - 'VIEW_NO_PUBLIC_ALBUMS' => 'Ingen offentlige album', - 'VIEW_NO_CONFIGURATION' => 'Ingen innstillinger', - 'VIEW_PHOTO_NOT_FOUND' => 'Bildet ble ikke funnet', - - 'NO_TAGS' => 'Ingen Tagger', - - 'UPLOAD_MANAGE_NEW_PHOTOS' => 'Du kan nå håndtere de nye bildene.', - 'UPLOAD_COMPLETE' => 'Opplasting fullført', - 'UPLOAD_COMPLETE_FAILED' => 'Kunne ikke laste opp en eller flere av bildene.', - 'UPLOAD_IMPORTING' => 'Importerer', - 'UPLOAD_IMPORTING_URL' => 'Importerer lenke', - 'UPLOAD_UPLOADING' => 'Laster opp', - 'UPLOAD_FINISHED' => 'Fullført', - 'UPLOAD_PROCESSING' => 'Arbeider', - 'UPLOAD_FAILED' => 'Feilet', - 'UPLOAD_FAILED_ERROR' => 'Opplasting feilet. Serveren svarte med en feil!', - 'UPLOAD_FAILED_WARNING' => 'Opplasting feilet. Serveren svarte med en advarsel!', - 'UPLOAD_CANCELLED' => 'Cancelled', - 'UPLOAD_SKIPPED' => 'Hoppet over', - 'UPLOAD_UPDATED' => 'Updated', - 'UPLOAD_IMPORT_SKIPPED_DUPLICATE' => 'This photo has been skipped because it\'s already in your library.', - 'UPLOAD_IMPORT_RESYNCED_DUPLICATE' => 'This photo has been skipped because it\'s already in your library, but its metadata has been updated.', - 'UPLOAD_ERROR_CONSOLE' => 'Vennligst se konsollen i nettleseren for mer informasjon.', - 'UPLOAD_UNKNOWN' => 'Serveren svarte med en ukjent feilmelding. Vennlist se konsollen i nettleseren for mer informasjon.', - 'UPLOAD_ERROR_UNKNOWN' => 'Opplasting feilet. Serveren svarte med en ukjent feil!', - 'UPLOAD_ERROR_POSTSIZE' => 'Upload failed. The PHP post_max_size may be too small! Otherwise check the FAQ.', - 'UPLOAD_ERROR_FILESIZE' => 'Upload failed. The PHP upload_max_filesize may be too small! Otherwise check the FAQ.', - 'UPLOAD_IN_PROGRESS' => 'Lychee laster for tiden opp!', - 'UPLOAD_IMPORT_WARN_ERR' => 'Importeringen er ferdig, men advarsler eller feil ble returnert. Vennligst see loggen (Innstilinger -> Vis Logg) for mer informasjon.', - 'UPLOAD_IMPORT_COMPLETE' => 'Importering fullført', - 'UPLOAD_IMPORT_INSTR' => 'Vennlist fyll inn en direkte lenke til et bilde for å importere det:', - 'UPLOAD_IMPORT' => 'Importer', - 'UPLOAD_IMPORT_SERVER' => 'Importer fra server', - 'UPLOAD_IMPORT_SERVER_FOLD' => 'Mappen er tom eller inneholder ingen lesbare filer som kan behandles. Vennligst se loggen (Innstillinger -> Vis Logg) for mer informasjon.', - 'UPLOAD_IMPORT_SERVER_INSTR' => 'Denne handlingen vil importere alle bilder, mapper, og undermapper som er plassert i følgende mappe.', - 'UPLOAD_ABSOLUTE_PATH' => 'Full sti til mappen', - 'UPLOAD_IMPORT_SERVER_EMPT' => 'Kunne ikke starte importeringen siden mappen var tom!', - 'UPLOAD_IMPORT_DELETE_ORIGINALS' => 'Fjern originalene', - 'UPLOAD_IMPORT_DELETE_ORIGINALS_EXPL' => 'De opprinnelige filene vil bli fjernet etter importeringen når mulig.', - 'UPLOAD_IMPORT_VIA_SYMLINK' => 'Symbolic links', - 'UPLOAD_IMPORT_VIA_SYMLINK_EXPL' => 'Import files using symbolic links to originals.', - 'UPLOAD_IMPORT_SKIP_DUPLICATES' => 'Skip duplicates', - 'UPLOAD_IMPORT_SKIP_DUPLICATES_EXPL' => 'Existing media files are skipped.', - 'UPLOAD_IMPORT_RESYNC_METADATA' => 'Re-sync metadata', - 'UPLOAD_IMPORT_RESYNC_METADATA_EXPL' => 'Update metadata of existing media files.', - 'UPLOAD_IMPORT_LOW_MEMORY' => 'Lite minne gjenstår!', - 'UPLOAD_IMPORT_LOW_MEMORY_EXPL' => 'Importeringsprossesen på serverren nærmer seg grensen for hvor mye minne som kan brukes, og kan bli avbrutt før den er ferdig.', - 'UPLOAD_WARNING' => 'Advarsel', - 'UPLOAD_IMPORT_NOT_A_DIRECTORY' => 'Stien er ikke en lesbar mappe!', - 'UPLOAD_IMPORT_PATH_RESERVED' => 'Stien er en sti som er reservert for Lychee!', - 'UPLOAD_IMPORT_UNREADABLE' => 'Kan ikke lese filen!', - 'UPLOAD_IMPORT_FAILED' => 'Kan ikke importere filen!', - 'UPLOAD_IMPORT_UNSUPPORTED' => 'Filtypen er ikke støttet!', - 'UPLOAD_IMPORT_ALBUM_FAILED' => 'Kan ikke lage albumet!', - 'UPLOAD_IMPORT_CANCELLED' => 'Import cancelled', - - 'ABOUT_SUBTITLE' => 'Selvlevert bildehåndtering den riktige måten!', - 'ABOUT_DESCRIPTION' => 'er et gratis bildehåndteringsverktøy, som kjører på serveren eller en webhost som du eier og kontrollerer. Installasjon tar sekunder. Last opp, håndter, og del bilder som om det er din egen maskin. Lychee leverer alt du trenger, og alle bildene er trygt lagret.', - 'FOOTER_COPYRIGHT' => 'Alle bildene på denne nettsiden er bundet av opphavsrett fra ', - 'HOSTED_WITH_LYCHEE' => 'Levert av Lychee', - - 'URL_COPY_TO_CLIPBOARD' => 'Kopier til utklippstavlen', - 'URL_COPIED_TO_CLIPBOARD' => 'Kopierte lenke til utklippstavlen!', - 'PHOTO_DIRECT_LINKS_TO_IMAGES' => 'Direkte lenke til bildefiler:', - 'PHOTO_MEDIUM' => 'Medium', - 'PHOTO_MEDIUM_HIDPI' => 'Medium HiDPI', - 'PHOTO_SMALL' => 'Miniatyr', - 'PHOTO_SMALL_HIDPI' => 'Miniatyr HiDPI', - 'PHOTO_THUMB' => 'Kvadratisk miniatyr', - 'PHOTO_THUMB_HIDPI' => 'Kvadratisk miniatyr HiDPI', - 'PHOTO_LIVE_VIDEO' => 'Filmdel av livebilde', - 'PHOTO_VIEW' => 'Lychee Bildevisning:', - - 'PHOTO_EDIT_ROTATECWISE' => 'Roter med klokken', - 'PHOTO_EDIT_ROTATECCWISE' => 'Roter mot klokken', - ]; - - return $locale; - } -} diff --git a/app/Locale/Polish.php b/app/Locale/Polish.php deleted file mode 100644 index fe7071668e5..00000000000 --- a/app/Locale/Polish.php +++ /dev/null @@ -1,476 +0,0 @@ - 'nazwa użytkownika', - 'PASSWORD' => 'hasło', - 'ENTER' => 'Potwierdź', - 'CANCEL' => 'Anuluj', - 'SIGN_IN' => 'Zaloguj', - 'CLOSE' => 'Zamknij', - 'SETTINGS' => 'Ustawienia', - 'SEARCH' => 'Szukaj ...', - 'MORE' => 'Więcej', - 'DEFAULT' => 'Domyślne', - - 'USERS' => 'Użytkownicy', - 'U2F' => 'U2F', - 'NOTIFICATIONS' => 'Notifications', - 'SHARING' => 'Udostępnianie', - 'CHANGE_LOGIN' => 'Zmień login', - 'CHANGE_SORTING' => 'Zmień sortowanie', - 'SET_DROPBOX' => 'Zapisz', - 'ABOUT_LYCHEE' => 'O Lychee', - 'DIAGNOSTICS' => 'Informacje techniczne', - 'DIAGNOSTICS_GET_SIZE' => 'Analiza miejsca na dysku', - 'LOGS' => 'Logi', - 'SIGN_OUT' => 'Wyloguj', - 'UPDATE_AVAILABLE' => 'Dostępna aktualizacja!', - 'MIGRATION_AVAILABLE' => 'Dostępna migracja!', - 'DEFAULT_LICENSE' => 'Domyślna licencja dla nowych wrzutek:', - 'SET_LICENSE' => 'Zapisz', - 'SET_OVERLAY_TYPE' => 'Set Overlay', - 'SET_MAP_PROVIDER' => 'Set OpenStreetMap tiles provider', - - 'SMART_ALBUMS' => 'Inteligentne albumy', - 'SHARED_ALBUMS' => 'Udostępnione albumy', - 'ALBUMS' => 'Albumy', - 'PHOTOS' => 'Zdjęcia', - 'SEARCH_RESULTS' => 'Wyniki wyszukiwania', - - 'RENAME' => 'Zmień nazwę', - 'RENAME_ALL' => 'Zamień zaznaczone', - 'MERGE' => 'Połącz z...', - 'MERGE_ALL' => 'Połącz zaznaczone', - 'MAKE_PUBLIC' => 'Ustaw jako publiczne', - 'SHARE_ALBUM' => 'Udostępnij album', - 'SHARE_PHOTO' => 'Udostępnij zdjęcie', - 'VISIBILITY_ALBUM' => 'Widoczność', - 'VISIBILITY_PHOTO' => 'Widoczność', - 'DOWNLOAD_ALBUM' => 'Pobierz album', - 'ABOUT_ALBUM' => 'O albumie', - 'DELETE_ALBUM' => 'Usuń album', - 'MOVE_ALBUM' => 'Przenieś album', - 'FULLSCREEN_ENTER' => 'Włącz pełny ekran', - 'FULLSCREEN_EXIT' => 'Wyłącz pełny rkran', - - 'SHARING_ALBUM_USERS' => 'Udostępnij album', - 'WAIT_FETCH_DATA' => 'Proszę czekać, trwa pobieranie danych...', - 'SHARING_ALBUM_USERS_NO_USERS' => 'Brak użytkowników do udostępnienia albumu', - 'SHARING_ALBUM_USERS_LONG_MESSAGE' => 'Wybierz użytkowników aby udostępnić ten album', - - 'DELETE_ALBUM_QUESTION' => 'Usuń album i zdjęcia', - 'KEEP_ALBUM' => 'Zatrzymaj album', - 'DELETE_ALBUM_CONFIRMATION_1' => 'Czy na pewno chcesz usunąć album', - 'DELETE_ALBUM_CONFIRMATION_2' => 'razem z zawartością ? Ta akcja jest nieodwracalna!', - - 'DELETE_ALBUMS_QUESTION' => 'Usuń album wraz z zawartością', - 'KEEP_ALBUMS' => 'Zatrzymaj Albumy', - 'DELETE_ALBUMS_CONFIRMATION_1' => 'Czy na pewno usunąć', - 'DELETE_ALBUMS_CONFIRMATION_2' => 'zaznaczone albumy wraz z zawartością? Ta akcja jest nieodwracalna!', - - 'DELETE_UNSORTED_CONFIRM' => 'Czy na pewno usunąć wszystkie zdjęcia z \'Nieposortowane\'?
Ta operacja nie może zostać cofnięta!', - 'CLEAR_UNSORTED' => 'Wyczyść Nieposortowane', - 'KEEP_UNSORTED' => 'Zatrzymaj Nieposortowane', - - 'EDIT_SHARING' => 'Edytuj udostępnianie', - 'MAKE_PRIVATE' => 'Oznacz jako prywatne', - - 'CLOSE_ALBUM' => 'Zmaknij album', - 'CLOSE_PHOTO' => 'Zamknij zdjęcie', - 'CLOSE_MAP' => 'Zamknij mapę', - - 'ADD' => 'Dodaj', - 'MOVE' => 'Przenieś do...', - 'MOVE_ALL' => 'Przenieś zaznaczone', - 'DUPLICATE' => 'Kopiuj', - 'DUPLICATE_ALL' => 'Kopiuj zaznaczone', - 'COPY_TO' => 'Kopiuj do...', - 'COPY_ALL_TO' => 'Kopiuj zaznaczone do...', - 'DELETE' => 'Usuń', - 'DELETE_ALL' => 'Usuń zaznaczone', - 'DOWNLOAD' => 'Pobierz', - 'DOWNLOAD_ALL' => 'Pobierz zaznaczone', - 'UPLOAD_PHOTO' => 'Wgraj zdjęcie', - 'IMPORT_LINK' => 'Importuj z adresu', - 'IMPORT_DROPBOX' => 'Importuj z Dropbox', - 'IMPORT_SERVER' => 'Importuj z serwera', - 'NEW_ALBUM' => 'Dodaj album', - 'NEW_TAG_ALBUM' => 'Dodaj album z tagami', - - 'TITLE_NEW_ALBUM' => 'Wpisz tytuł dla nowego albumu:', - 'UNTITLED' => 'Bez nazwy', - 'UNSORTED' => 'Nieposortowane', - 'STARRED' => 'Oznaczone', - 'RECENT' => 'Ostatnie', - 'PUBLIC' => 'Publiczne', - 'NUM_PHOTOS' => 'Zdjęć', - - 'CREATE_ALBUM' => 'Utwórz album', - 'CREATE_TAG_ALBUM' => 'Utwórz album z tagami', - - 'STAR_PHOTO' => 'Oznacz', - 'STAR' => 'Oznacz', - 'STAR_ALL' => 'Oznacz zaznaczone', - 'TAGS' => 'Otaguj', - 'TAGS_ALL' => 'Otaguj zaznaczone', - 'UNSTAR_PHOTO' => 'Cofnij oznaczenie', - 'SET_COVER' => 'Ustaw jako okładkę albumu', - 'REMOVE_COVER' => 'Usuń okładkę albumu', - - 'FULL_PHOTO' => 'Otwórz oryginalne', - 'ABOUT_PHOTO' => 'Informacje o zdjęciu', - 'DISPLAY_FULL_MAP' => 'Mapa', - 'DIRECT_LINK' => 'Link bezpośredni', - 'DIRECT_LINKS' => 'Linki bezpośrednie', - - 'ALBUM_ABOUT' => 'Informacje o albumie', - 'ALBUM_BASICS' => 'Informacje podstawowe', - 'ALBUM_TITLE' => 'Tytuł', - 'ALBUM_NEW_TITLE' => 'Edytuj tytuł albumu:', - 'ALBUMS_NEW_TITLE_1' => 'Wpisz tytuł dla', - 'ALBUMS_NEW_TITLE_2' => 'wybranych albumów:', - 'ALBUM_SET_TITLE' => 'Zapisz', - 'ALBUM_DESCRIPTION' => 'Opis', - 'ALBUM_SHOW_TAGS' => 'Tagi do pokazania', - 'ALBUM_NEW_DESCRIPTION' => 'Edytuj opis albumu:', - 'ALBUM_SET_DESCRIPTION' => 'Zapisz', - 'ALBUM_NEW_SHOWTAGS' => 'Enter tags of photos that will be visible in this album:', - 'ALBUM_SET_SHOWTAGS' => 'Ustaw tagi do pokazania', - 'ALBUM_ALBUM' => 'Album', - 'ALBUM_CREATED' => 'Utworzone', - 'ALBUM_IMAGES' => 'Zdjęcia', - 'ALBUM_VIDEOS' => 'Filmy', - 'ALBUM_SUBALBUMS' => 'Albumy podrzędne', - 'ALBUM_SHARING' => 'Udostępnianie', - 'ALBUM_SHR_YES' => 'TAK', - 'ALBUM_SHR_NO' => 'Nie', - 'ALBUM_PUBLIC' => 'Publiczny', - 'ALBUM_PUBLIC_EXPL' => 'Album jest publicznie dostępny, włączając ograniczenia poniżej.', - 'ALBUM_FULL' => 'Oryginalne zdjęcia', - 'ALBUM_FULL_EXPL' => 'Dostepne są zdjęcia oryginalnej rozdzielczności.', - 'ALBUM_HIDDEN' => 'Ukryty', - 'ALBUM_HIDDEN_EXPL' => 'Album mogą zobaczyć TYLKO posiadający link bezpośredni.', - 'ALBUM_MARK_NSFW' => 'Oznacz album jako poufny', - 'ALBUM_UNMARK_NSFW' => 'Odznacz album jako poufny', - 'ALBUM_NSFW' => 'Poufny', - 'ALBUM_NSFW_EXPL' => 'Album zawiera poufne informacje.', - 'ALBUM_DOWNLOADABLE' => 'Pobieranie', - 'ALBUM_DOWNLOADABLE_EXPL' => 'Album można pobrać na dysk.', - 'ALBUM_SHARE_BUTTON_VISIBLE' => 'Udostępnianie', - 'ALBUM_SHARE_BUTTON_VISIBLE_EXPL' => 'Widoczne są dodatkowe ikony społecznościowe.', - 'ALBUM_PASSWORD' => 'Hasło', - 'ALBUM_PASSWORD_PROT' => 'Zabezpieczony', - 'ALBUM_PASSWORD_PROT_EXPL' => 'Dostęp do albumu zabezpiecza hasło.', - 'ALBUM_PASSWORD_REQUIRED' => 'Album chroniony jest hasłem. Wpisz hasło aby zobaczyć zawartość:', - 'ALBUM_MERGE_1' => 'Czy na pewno połączyć ten album', - 'ALBUM_MERGE_2' => 'z albumem', - 'ALBUMS_MERGE' => 'Czy na pewno połączyć zaznaczone albumy z', - 'MERGE_ALBUM' => 'Połącz albumy', - 'DONT_MERGE' => 'Anuluj', - 'ALBUM_MOVE_1' => 'Czy na pewno przenieść album', - 'ALBUM_MOVE_2' => 'do albumu', - 'ALBUMS_MOVE' => 'Czy na pewno przenieść zaznaczone albumy do albumu', - 'MOVE_ALBUMS' => 'Przenieś albumy', - 'NOT_MOVE_ALBUMS' => 'Anuluj', - 'ROOT' => 'Albumy', - 'ALBUM_REUSE' => 'Prawo do wykorzystania', - 'ALBUM_LICENSE' => 'Licencja', - 'ALBUM_SET_LICENSE' => 'Zapisz', - 'ALBUM_LICENSE_HELP' => 'Potrzebujesz pomocy?', - 'ALBUM_LICENSE_NONE' => 'Brak', - 'ALBUM_RESERVED' => 'Wszelkie prawa zastrzeżone', - 'ALBUM_SET_ORDER' => 'Zapisz', - 'ALBUM_ORDERING' => 'Sortowanie', - - 'PHOTO_ABOUT' => 'Informacje o zdjęciu', - 'PHOTO_BASICS' => 'Informacje podstawowe', - 'PHOTO_TITLE' => 'Tytuł', - 'PHOTO_NEW_TITLE' => 'Wpisz nowy tytuł:', - 'PHOTO_SET_TITLE' => 'Ustaw tytuł', - 'PHOTO_UPLOADED' => 'Wgrany', - 'PHOTO_DESCRIPTION' => 'Opis', - 'PHOTO_NEW_DESCRIPTION' => 'Wpisz nowy opis:', - 'PHOTO_SET_DESCRIPTION' => 'Ustaw opis', - 'PHOTO_NEW_LICENSE' => 'Dodaj licencję', - 'PHOTO_SET_LICENSE' => 'Ustaw licencję', - 'PHOTO_LICENSE' => 'Licencja', - 'PHOTO_REUSE' => 'Ponowne wykorzystanie', - 'PHOTO_LICENSE_NONE' => 'Brak', - 'PHOTO_RESERVED' => 'Wszelkie prawa zastrzeżone', - 'PHOTO_LATITUDE' => 'Długość geograficzna', - 'PHOTO_LONGITUDE' => 'Szerokość geograficzna', - 'PHOTO_ALTITUDE' => 'Wysokość', - 'PHOTO_IMGDIRECTION' => 'Kierunek', - 'PHOTO_LOCATION' => 'Lokalizacja', - 'PHOTO_IMAGE' => 'Zdjęcie', - 'PHOTO_VIDEO' => 'Film', - 'PHOTO_SIZE' => 'Rozmiar', - 'PHOTO_FORMAT' => 'Format', - 'PHOTO_RESOLUTION' => 'Rozdzielczość', - 'PHOTO_DURATION' => 'Czas trwania', - 'PHOTO_FPS' => 'Przepustowość klatek/sek', - 'PHOTO_TAGS' => 'Tagi', - 'PHOTO_NOTAGS' => 'Brak Tagów', - 'PHOTO_NEW_TAGS' => 'Wpisz tagi rozdzielając je przecinkiem:', - 'PHOTO_NEW_TAGS_1' => 'Wpisz tagi dla', - 'PHOTO_NEW_TAGS_2' => 'zaznaczonych zdjęć. Istniejące tagi zostaną nadpisane. Możesz wpisać więcej tagów rozdzielając je przecinkiem:', - 'PHOTO_SET_TAGS' => 'Zapisz', - 'PHOTO_CAMERA' => 'Aparat', - 'PHOTO_CAPTURED' => 'Zrzut', - 'PHOTO_MAKE' => 'Marka', - 'PHOTO_TYPE' => 'Typ/Model', - 'PHOTO_LENS' => 'Obiektyw', - 'PHOTO_SHUTTER' => 'Szybkość migawki', - 'PHOTO_APERTURE' => 'Przysłona', - 'PHOTO_FOCAL' => 'Ogniskowa', - 'PHOTO_ISO' => 'ISO', - 'PHOTO_SHARING' => 'Udostępnianie', - 'PHOTO_SHR_PLUBLIC' => 'Publiczne', - 'PHOTO_SHR_ALB' => 'Tak (album)', - 'PHOTO_SHR_PHT' => 'Tak (zdjęcie)', - 'PHOTO_SHR_NO' => 'Nie', - 'PHOTO_DELETE' => 'Usuń Zdjęcie', - 'PHOTO_KEEP' => 'Anuluj', - 'PHOTO_DELETE_1' => 'Czy na pewno usunąć zdjęcie', - 'PHOTO_DELETE_2' => '? Akcja jest nieodwracalna!', - 'PHOTO_DELETE_ALL_1' => 'Czy na pewno usunąć', - 'PHOTO_DELETE_ALL_2' => 'zaznaczone zdjęcia? Akcja jest nieodwracalna!', - 'PHOTOS_NEW_TITLE_1' => 'Wpisz tytuł dla', - 'PHOTOS_NEW_TITLE_2' => 'zaznaczonych zdjęć:', - 'PHOTO_MAKE_PRIVATE_ALBUM' => 'Zdjęcie znajduje się w albumie publicznym. Aby ustawić je jako prywatne/publiczne, edytuj ustawienie widoczności w albumie, w którym zdjęcie się znajduje.', - 'PHOTO_SHOW_ALBUM' => 'Pokaż album', - 'PHOTO_PUBLIC' => 'Publiczne', - 'PHOTO_PUBLIC_EXPL' => 'Zdjęcie jest dostępne publicznie, włączając ograniczenia poniżej.', - 'PHOTO_FULL' => 'Originalne zdjęcia', - 'PHOTO_FULL_EXPL' => 'Dostępne są zdjęcia w pełnej rozdzielczości.', - 'PHOTO_HIDDEN' => 'Ukryty', - 'PHOTO_HIDDEN_EXPL' => 'Album zobaczyć mogą TYLKO posiadający link bezpośredni.', - 'PHOTO_DOWNLOADABLE' => 'Pobieranie', - 'PHOTO_DOWNLOADABLE_EXPL' => 'Odwiedzający galerię mogą pobierać zdjęcie.', - 'PHOTO_SHARE_BUTTON_VISIBLE' => 'Udostępnianie', - 'PHOTO_SHARE_BUTTON_VISIBLE_EXPL' => 'Widoczne są dodatkowe ikony społecznościowe.', - 'PHOTO_PASSWORD_PROT' => 'Zabezpieczony', - 'PHOTO_PASSWORD_PROT_EXPL' => 'Dostęp do zdjęcia zabezpiecza hasło.', - 'PHOTO_EDIT_SHARING_TEXT' => 'Ustawienia udostępniania tego zdjęcia zostaną zmienione na następujące:', - 'PHOTO_NO_EDIT_SHARING_TEXT' => 'Ponieważ zdjęcie znajduje się w albumie publicznym, dzieli jego ustawienia widoczności. Aktualna wartość widoczna jest poniżej (tylko informacyjnie).', - 'PHOTO_EDIT_GLOBAL_SHARING_TEXT' => 'Widoczność zdjęcia można dostosować używając globalnych ustawień Lychee. Aktualna wartość widoczna jest poniżej (tylko informacyjnie).', - 'PHOTO_SHARING_CONFIRM' => 'Zapisz', - - 'LOADING' => 'Wczytywanie', - 'ERROR' => 'Błąd', - 'ERROR_TEXT' => 'Ups, wygląda na to że coś poszło nie tak. Odśwież stronę i spróbuj ponownie!', - 'ERROR_DB_1' => 'Brak dostępu do bazy danych. Sprawdź ponownie nazwę hosta, użytkownika oraz hasło, i upewnij się że Twoja aktualna lokalizacja pozwala na połączenie z bazą.', - 'ERROR_DB_2' => 'Nie można utworzyć bazy danych. Sprawdź ponownie nazwę hosta, użytkownika oraz hasło, i upewnij się że użytkownik posiada niezbędne prawa modyfikowania zawartości w bazie danych.', - 'ERROR_CONFIG_FILE' => "Nie można zapisać konfiguracji. Odmowa dostępu w 'data/'. Ustaw prawa do odczytu, zapisu i wykonania dla pozostałych użytkowników w 'data/' i 'uploads/'. Przejrzyj plik README dla bardziej szczegółowych informacji.", - 'ERROR_UNKNOWN' => 'Wystąpił nieoczekiwany błąd. Spróbuj ponownie i sprawdź swoją instalację oraz serwer. Przejrzyj plik README dla bardziej szczegółowych informacji.', - 'ERROR_LOGIN' => 'Nie można zapisać loginu. Spróbuj ponownie przy użyciu innej nazwy użytkownika oraz hasła!', - 'ERROR_MAP_DEACTIVATED' => 'Funkcja mapy została wyłączona w ustawieniach.', - 'ERROR_SEARCH_DEACTIVATED' => 'Funkcja wyszkukiwania została wyłączona w ustawieniach.', - 'SUCCESS' => 'OK', - 'RETRY' => 'Ponów', - - 'SETTINGS_SUCCESS_LOGIN' => 'Zaktualizowano informacje o loginie.', - 'SETTINGS_SUCCESS_SORT' => 'Zaktualizowano kolejność sortowania.', - 'SETTINGS_SUCCESS_DROPBOX' => 'Zaktualizowano klucz Dropbox.', - 'SETTINGS_SUCCESS_LANG' => 'Zaktualizowano język', - 'SETTINGS_SUCCESS_LAYOUT' => 'Zaktualizowano układ', - 'SETTINGS_SUCCESS_IMAGE_OVERLAY' => 'EXIF Overlay setting updated', - 'SETTINGS_SUCCESS_PUBLIC_SEARCH' => 'Zaktualizowano publiczne wyszukiwanie', - 'SETTINGS_SUCCESS_LICENSE' => 'Zaktualizowano domyślną licencję', - 'SETTINGS_SUCCESS_MAP_DISPLAY' => 'Zaktualizowano ustawienia wyświetlania mapy', - 'SETTINGS_SUCCESS_MAP_DISPLAY_PUBLIC' => 'Zaktualizowano ustawienia wyświetlania mapy dla albumów publicznych', - 'SETTINGS_SUCCESS_MAP_PROVIDER' => 'Zaktualizowano ustawienia dostawcy map', - - 'U2F_NOT_SUPPORTED' => 'Brak obsługi U2F. Przepraszamy.', - 'U2F_NOT_SECURE' => 'Środowisko nie zostało zabezpieczone. U2F nie jest dostępne.', - 'U2F_REGISTER_KEY' => 'Zarejestruj nowe urządzenie.', - 'U2F_REGISTRATION_SUCCESS' => 'Rejestracja pomyślna!', - 'U2F_AUTHENTIFICATION_SUCCESS' => 'Autoryzacja pomyślna!', - 'U2F_CREDENTIALS' => 'Dane uwierzytelniające', - 'U2F_CREDENTIALS_DELETED' => 'Usunięto dane uwierzytelniające!', - - 'NEW_PHOTOS_NOTIFICATION' => 'Send new photos notification emails.', - 'SETTINGS_SUCCESS_NEW_PHOTOS_NOTIFICATION' => 'New photos notification updated', - 'USER_EMAIL_INSTRUCTION' => 'Add your email below to enable receiving email notifications.
To stop receiving emails, simply remove your email below.', - - 'DB_INFO_TITLE' => 'Uzupełnij dane niezbędne do połączenia z bazą danych:', - 'DB_INFO_HOST' => 'Host (opcjonalnie)', - 'DB_INFO_USER' => 'Użytkownik', - 'DB_INFO_PASSWORD' => 'Hasło', - 'DB_INFO_TEXT' => 'Standardowo Lychee utworzy własną bazę. Zamiast tworzyć nową, wpisz dane istniejącej poniżej:', - 'DB_NAME' => 'Nazwa (opcjonalnie)', - 'DB_PREFIX' => 'Prefix tabeli (opcjonalnie)', - 'DB_CONNECT' => 'Połącz', - - 'LOGIN_TITLE' => 'Wpisz nazwę użytkownika oraz hasło dla swojej instalacji:', - 'LOGIN_USERNAME' => 'Nowa nazwa użytkownika', - 'LOGIN_PASSWORD' => 'Nowe hasło', - 'LOGIN_PASSWORD_CONFIRM' => 'Potwierdź nowe hasło', - 'LOGIN_CREATE' => 'Utwórz login', - - 'PASSWORD_TITLE' => 'Wpisz aktualne dane dostępowe:', - 'USERNAME_CURRENT' => 'Aktualna nazwa użytkownika', - 'PASSWORD_CURRENT' => 'Aktualne hasło', - 'PASSWORD_TEXT' => 'Twoja nazwa użytkownika oraz hasło zostaną zmienione na następujące:', - 'PASSWORD_CHANGE' => 'Zmień login', - - 'EDIT_SHARING_TITLE' => 'Edytuj udostępnianie', - 'EDIT_SHARING_TEXT' => 'Ustawienia udostępniania zostaną zmienione na następujące:', - 'SHARE_ALBUM_TEXT' => 'Album zostanie udostępniony z następującymi ustawieniami:', - 'ALBUM_SHARING_CONFIRM' => 'Zapisz', - - 'SORT_ALBUM_BY_1' => 'Sortuj albumy według pola', - 'SORT_ALBUM_BY_2' => 'w kolejności ', - 'SORT_ALBUM_BY_3' => '', - - 'SORT_ALBUM_SELECT_1' => 'data utworzenia', - 'SORT_ALBUM_SELECT_2' => 'tytuł', - 'SORT_ALBUM_SELECT_3' => 'opis', - 'SORT_ALBUM_SELECT_4' => 'publiczny', - 'SORT_ALBUM_SELECT_5' => 'Latest Take Date', - 'SORT_ALBUM_SELECT_6' => 'Oldest Take Date', - - 'SORT_PHOTO_BY_1' => 'Sortuj zdjęcia według pola', - 'SORT_PHOTO_BY_2' => '
w kolejności ', - 'SORT_PHOTO_BY_3' => '', - - 'SORT_PHOTO_SELECT_1' => 'data dodania', - 'SORT_PHOTO_SELECT_2' => 'Take Date', - 'SORT_PHOTO_SELECT_3' => 'tytuł', - 'SORT_PHOTO_SELECT_4' => 'opis', - 'SORT_PHOTO_SELECT_5' => 'publiczny', - 'SORT_PHOTO_SELECT_6' => 'oznaczony', - 'SORT_PHOTO_SELECT_7' => 'format', - - 'SORT_ASCENDING' => 'rosnącej', - 'SORT_DESCENDING' => 'malejącej', - 'SORT_CHANGE' => 'Zmień sortowanie', - - 'DROPBOX_TITLE' => 'Ustaw klucz Dropbox', - 'DROPBOX_TEXT' => "In order to import photos from your Dropbox, you need a valid drop-ins app key from their website. Generate yourself a personal key and enter it below:", - - 'LANG_TEXT' => 'Zmień język na:', - 'LANG_TITLE' => 'Zmień język', - 'PUBLIC_SEARCH_TEXT' => 'Public search allowed:', - 'OVERLAY_TYPE' => 'Photo overlay:', - 'OVERLAY_NONE' => 'None', - 'OVERLAY_EXIF' => 'EXIF data', - 'OVERLAY_DESCRIPTION' => 'Description', - 'OVERLAY_DATE' => 'Date taken', - 'MAP_DISPLAY_TEXT' => 'Enable maps (provided by OpenStreetMap):', - 'MAP_DISPLAY_PUBLIC_TEXT' => 'Enable maps for public albums (provided by OpenStreetMap):', - 'LOCATION_DECODING' => 'Decode GPS data into location name', - 'LOCATION_SHOW' => 'Pokaż lokalizację', - 'LOCATION_SHOW_PUBLIC' => 'Show location name for public mode', - 'MAP_PROVIDER' => 'Provider of OpenStreetMap tiles:', - 'MAP_PROVIDER_WIKIMEDIA' => 'Wikimedia', - 'MAP_PROVIDER_OSM_ORG' => 'OpenStreetMap.org (no HiDPI)', - 'MAP_PROVIDER_OSM_DE' => 'OpenStreetMap.de (no HiDPI)', - 'MAP_PROVIDER_OSM_FR' => 'OpenStreetMap.fr (no HiDPI)', - 'MAP_PROVIDER_RRZE' => 'University of Erlangen, Germany (only HiDPI)', - 'MAP_INCLUDE_SUBALBUMS_TEXT' => 'Include photos of subalbums on map:', - - 'LAYOUT_TYPE' => 'Układ zdjęć:', - 'LAYOUT_SQUARES' => 'Kwadratowe miniaturki', - 'LAYOUT_JUSTIFIED' => 'Aspekt, wyrównane', - 'LAYOUT_UNJUSTIFIED' => 'Aspekt, bez wyrównania', - 'SET_LAYOUT' => 'Zmień układ', - - 'NSFW_VISIBLE_TEXT_1' => 'Ustaw poufne albumy domyślnie widoczne.', - 'NSFW_VISIBLE_TEXT_2' => 'Jeśli album jest publiczny, wciąż jest dostępny, jedynie został ukryty do przeglądania i może zostać pokazany poprzez naciśnięcie H.', - 'SETTINGS_SUCCESS_NSFW_VISIBLE' => 'Domyślne ustawienie dotyczące widoczności albumów poufnych ostało zaktualizowane.', - - 'VIEW_NO_RESULT' => 'Brak wyników', - 'VIEW_NO_PUBLIC_ALBUMS' => 'Brak albumów publicznych', - 'VIEW_NO_CONFIGURATION' => 'Brak konfiguracji', - 'VIEW_PHOTO_NOT_FOUND' => 'Zdjęcie nie zostało znalezione', - - 'NO_TAGS' => 'Brak tagów', - - 'UPLOAD_MANAGE_NEW_PHOTOS' => 'Możesz już zarządzać nowymi zdjęciami.', - 'UPLOAD_COMPLETE' => 'Zakońcozno wgrywanie', - 'UPLOAD_COMPLETE_FAILED' => 'Nie udało się wgrać jednego lub więcej plików.', - 'UPLOAD_IMPORTING' => 'Importowanie', - 'UPLOAD_IMPORTING_URL' => 'Importowanie URL', - 'UPLOAD_UPLOADING' => 'Wgrywanie', - 'UPLOAD_FINISHED' => 'Ukończono', - 'UPLOAD_PROCESSING' => 'Przetwarzanie', - 'UPLOAD_FAILED' => 'Nie udało się', - 'UPLOAD_FAILED_ERROR' => 'Wgrywanie nie powiodło się. Serwer zwrócił błąd!', - 'UPLOAD_FAILED_WARNING' => 'Wgrywanie nie powiodło się. Serwer zwrócił ostrzeżenie!', - 'UPLOAD_CANCELLED' => 'Cancelled', - 'UPLOAD_SKIPPED' => 'Pominięto', - 'UPLOAD_UPDATED' => 'Updated', - 'UPLOAD_IMPORT_SKIPPED_DUPLICATE' => 'This photo has been skipped because it\'s already in your library.', - 'UPLOAD_IMPORT_RESYNCED_DUPLICATE' => 'This photo has been skipped because it\'s already in your library, but its metadata has been updated.', - 'UPLOAD_ERROR_CONSOLE' => 'Proszę przejrzeć konsolę błędów przeglądarki aby spradzić szczegóły.', - 'UPLOAD_UNKNOWN' => 'Server returned an unknown response. Please take a look at the console of your browser for further details.', - 'UPLOAD_ERROR_UNKNOWN' => 'Wgrywanie nie powiodło się. Server returned an unkown error!', - 'UPLOAD_ERROR_POSTSIZE' => 'Wgrywanie nie powiodło się. Wartość post_max_size w PHP jest zbyt niska!', - 'UPLOAD_ERROR_FILESIZE' => 'Wgrywanie nie powiodło się. Wartość upload_max_filesize w PHP jest zbyt niska!', - 'UPLOAD_IN_PROGRESS' => 'Lychee jest w trakcie wgrywania!', - 'UPLOAD_IMPORT_WARN_ERR' => 'The import has been finished, but returned warnings or errors. Please take a look at the log (Settings -> Show Log) for further details.', - 'UPLOAD_IMPORT_COMPLETE' => 'Import complete', - 'UPLOAD_IMPORT_INSTR' => 'Please enter the direct link to a photo to import it:', - 'UPLOAD_IMPORT' => 'Import', - 'UPLOAD_IMPORT_SERVER' => 'Importing from server', - 'UPLOAD_IMPORT_SERVER_FOLD' => 'Folder empty or no readable files to process. Please take a look at the log (Settings -> Show Log) for further details.', - 'UPLOAD_IMPORT_SERVER_INSTR' => 'This action will import all photos, folders and sub-folders which are located in the following directory.', - 'UPLOAD_ABSOLUTE_PATH' => 'Absolute path to directory', - 'UPLOAD_IMPORT_SERVER_EMPT' => 'Could not start import because the folder was empty!', - 'UPLOAD_IMPORT_DELETE_ORIGINALS' => 'Delete originals', - 'UPLOAD_IMPORT_DELETE_ORIGINALS_EXPL' => 'Original files will be deleted after the import when possible.', - 'UPLOAD_IMPORT_VIA_SYMLINK' => 'Symbolic links', - 'UPLOAD_IMPORT_VIA_SYMLINK_EXPL' => 'Import files using symbolic links to originals.', - 'UPLOAD_IMPORT_SKIP_DUPLICATES' => 'Skip duplicates', - 'UPLOAD_IMPORT_SKIP_DUPLICATES_EXPL' => 'Existing media files are skipped.', - 'UPLOAD_IMPORT_RESYNC_METADATA' => 'Re-sync metadata', - 'UPLOAD_IMPORT_RESYNC_METADATA_EXPL' => 'Update metadata of existing media files.', - 'UPLOAD_IMPORT_LOW_MEMORY' => 'Low memory condition!', - 'UPLOAD_IMPORT_LOW_MEMORY_EXPL' => 'The import process on the server is approaching the memory limit and may end up being terminated prematurely.', - 'UPLOAD_WARNING' => 'Warning', - 'UPLOAD_IMPORT_NOT_A_DIRECTORY' => 'The given path is not a readable directory!', - 'UPLOAD_IMPORT_PATH_RESERVED' => 'The given path is a reserved path of Lychee!', - 'UPLOAD_IMPORT_UNREADABLE' => 'Could not read the file!', - 'UPLOAD_IMPORT_FAILED' => 'Could not import the file!', - 'UPLOAD_IMPORT_UNSUPPORTED' => 'Unsupported file type!', - 'UPLOAD_IMPORT_ALBUM_FAILED' => 'Could not create the album!', - 'UPLOAD_IMPORT_CANCELLED' => 'Import cancelled', - - 'ABOUT_SUBTITLE' => 'Self-hosted photo-management done right', - 'ABOUT_DESCRIPTION' => 'is a free photo-management tool, which runs on your server or web-space. Installing is a matter of seconds. Upload, manage and share photos like from a native application. Lychee comes with everything you need and all your photos are stored securely.', - 'FOOTER_COPYRIGHT' => 'All images on this website are subject to Copyright by ', - 'HOSTED_WITH_LYCHEE' => 'Hosted with Lychee', - - 'URL_COPY_TO_CLIPBOARD' => 'Kopiuj do schowka', - 'URL_COPIED_TO_CLIPBOARD' => 'Skopiowano URL do schowka!', - 'PHOTO_DIRECT_LINKS_TO_IMAGES' => 'Direct links to image files:', - 'PHOTO_MEDIUM' => ' Średnie', - 'PHOTO_MEDIUM_HIDPI' => 'Medium HiDPI', - 'PHOTO_SMALL' => 'Miniaturka', - 'PHOTO_SMALL_HIDPI' => 'Thumb HiDPI', - 'PHOTO_THUMB' => 'Kwadratowa miniaturka', - 'PHOTO_THUMB_HIDPI' => 'Square thumb HiDPI', - 'PHOTO_LIVE_VIDEO' => 'Video part of live-photo', - 'PHOTO_VIEW' => 'Lychee Photo View:', - - 'PHOTO_EDIT_ROTATECWISE' => 'Obróć w prawo', - 'PHOTO_EDIT_ROTATECCWISE' => 'Obróć w lewo', - ]; - - return $locale; - } -} diff --git a/app/Locale/Portuguese.php b/app/Locale/Portuguese.php deleted file mode 100644 index 1972566244e..00000000000 --- a/app/Locale/Portuguese.php +++ /dev/null @@ -1,477 +0,0 @@ - 'nome de utilizador', - 'PASSWORD' => 'password', - 'ENTER' => 'Inserir', - 'CANCEL' => 'Cancelar', - 'SIGN_IN' => 'Iniciar Sessão', - 'CLOSE' => 'Fechar', - 'SETTINGS' => 'Configurações', - 'SEARCH' => 'Pesquisar ...', - 'MORE' => 'Mais', - 'DEFAULT' => 'Predefinição', - - 'USERS' => 'Utilizadores', - 'U2F' => 'U2F', - 'NOTIFICATIONS' => 'Notifications', - 'SHARING' => 'Partilha', - 'CHANGE_LOGIN' => 'Alterar Login', - 'CHANGE_SORTING' => 'Alterar Ordenação', - 'SET_DROPBOX' => 'Escolher Dropbox', - 'ABOUT_LYCHEE' => 'Acerca do Lychee', - 'DIAGNOSTICS' => 'Diagnosticos', - 'DIAGNOSTICS_GET_SIZE' => 'Pedir utilização de espaço', - 'LOGS' => 'Mostrar Logs', - 'SIGN_OUT' => 'Terminar Sessão', - 'UPDATE_AVAILABLE' => 'Atualização disponível!', - 'MIGRATION_AVAILABLE' => 'Migração disponível!', - 'DEFAULT_LICENSE' => 'Default licença for new uploads:', - 'SET_LICENSE' => 'Escolher Licença', - 'SET_OVERLAY_TYPE' => 'Escolher Overlay', - 'SET_MAP_PROVIDER' => 'Escolher OpenStreetMap tiles provider', - - 'SMART_ALBUMS' => 'Smart álbums', - 'SHARED_ALBUMS' => 'Álbums partilhados', - 'ALBUMS' => 'Álbums', - 'PHOTOS' => 'Fotos', - 'SEARCH_RESULTS' => 'Resultados da pesquisa', - - 'RENAME' => 'Renomear', - 'RENAME_ALL' => 'Renomear Seleção', - 'MERGE' => 'Unir', - 'MERGE_ALL' => 'Unir Seleção', - 'MAKE_PUBLIC' => 'Tornar Público', - 'SHARE_ALBUM' => 'Partilhar Álbum', - 'SHARE_PHOTO' => 'Partilhar Fotografia', - 'VISIBILITY_ALBUM' => 'Visibilidade do Álbum', - 'VISIBILITY_PHOTO' => 'Visibilidade da Fotografia', - 'DOWNLOAD_ALBUM' => 'Transferir Álbum', - 'ABOUT_ALBUM' => 'Acerca do Álbum', - 'DELETE_ALBUM' => 'Eliminar Álbum', - 'MOVE_ALBUM' => 'Moverr Álbum', - 'FULLSCREEN_ENTER' => 'Entrar em Tela Cheia', - 'FULLSCREEN_EXIT' => 'Sair da Tela Cheia', - - 'SHARING_ALBUM_USERS' => 'Partilhar este álbum com utilizadores', - 'WAIT_FETCH_DATA' => 'Por favor aguarde enquanto transferimos os dados...', - 'SHARING_ALBUM_USERS_NO_USERS' => 'Não há utilizadores com quem partilhar o álbum', - 'SHARING_ALBUM_USERS_LONG_MESSAGE' => 'Selecione os utilizadores com quem quer partilhar este álbum', - - 'DELETE_ALBUM_QUESTION' => 'Eliminar Álbum e Fotos', - 'KEEP_ALBUM' => 'Manter Álbum', - 'DELETE_ALBUM_CONFIRMATION_1' => 'De certeza que quer eliminar o álbum', - 'DELETE_ALBUM_CONFIRMATION_2' => 'e todas as fotografias contidas? Esta ação não pode ser desfeita!', - - 'DELETE_ALBUMS_QUESTION' => 'Eliminar Álbums e Fotos', - 'KEEP_ALBUMS' => 'Manter Álbums', - 'DELETE_ALBUMS_CONFIRMATION_1' => 'De certeza que quer eliminar todos', - 'DELETE_ALBUMS_CONFIRMATION_2' => 'os álbums e todas as fotografias neles contidas? Esta ação não pode ser desfeita!', - - 'DELETE_UNSORTED_CONFIRM' => 'De certeza que quer eliminar todas as fotografias de \'Desordenadas\'?
Esta ação não pode ser desfeita!', - 'CLEAR_UNSORTED' => 'Limpar Desordenadas', - 'KEEP_UNSORTED' => 'Manter Desordenadas', - - 'EDIT_SHARING' => 'Editar Sharing', - 'MAKE_PRIVATE' => 'Tornar Privado', - - 'CLOSE_ALBUM' => 'Fechar Álbum', - 'CLOSE_PHOTO' => 'Fechar Fotografia', - 'CLOSE_MAP' => 'Fechar Mapa', - - 'ADD' => 'Adicionar', - 'MOVE' => 'Mover', - 'MOVE_ALL' => 'Mover Selecionadas', - 'DUPLICATE' => 'Duplicar', - 'DUPLICATE_ALL' => 'Duplicar Selecionadas', - 'COPY_TO' => 'Copiar para...', - 'COPY_ALL_TO' => 'Copiar Selecionadas para...', - 'DELETE' => 'Eliminar', - 'DELETE_ALL' => 'Eliminar Selecionadas', - 'DOWNLOAD' => 'Transferir', - 'DOWNLOAD_ALL' => 'Transferir Selecionadas', - 'UPLOAD_PHOTO' => 'Enviar Fotografia', - 'IMPORT_LINK' => 'Importar a partir de um Link', - 'IMPORT_DROPBOX' => 'Importar do Dropbox', - 'IMPORT_SERVER' => 'Importar de um Servidor', - 'NEW_ALBUM' => 'Novo Álbum', - 'NEW_TAG_ALBUM' => 'Nova Etiqueta de Álbum', - - 'TITLE_NEW_ALBUM' => 'Insira um título para o novo álbum:', - 'UNTITLED' => 'Sem Título', - 'UNSORTED' => 'Desordenadas', - 'STARRED' => 'Favoritas', - 'RECENT' => 'Recentes', - 'PUBLIC' => 'Públicas', - 'NUM_PHOTOS' => 'Fotografias', - - 'CREATE_ALBUM' => 'Criar Álbum', - 'CREATE_TAG_ALBUM' => 'Criar Etiqueta Álbum', - - 'STAR_PHOTO' => 'Marcar como Favorita', - 'STAR' => 'Favorita', - 'STAR_ALL' => 'Marcar Selecionadas como Favoritas', - 'TAGS' => 'Etiqueta', - 'TAGS_ALL' => 'Etiquetar Selecionadas', - 'UNSTAR_PHOTO' => 'Desmarcar como Favorita', - 'SET_COVER' => 'Escolher para Capa de Álbum', - 'REMOVE_COVER' => 'Remover Capa de Álbum', - - 'FULL_PHOTO' => 'Abrir Original', - 'ABOUT_PHOTO' => 'Acerca da Fotografia', - 'DISPLAY_FULL_MAP' => 'Mapa', - 'DIRECT_LINK' => 'Link Direto', - 'DIRECT_LINKS' => 'Links Diretos', - - 'ALBUM_ABOUT' => 'Acerca de', - 'ALBUM_BASICS' => 'Básicos', - 'ALBUM_TITLE' => 'Título', - 'ALBUM_NEW_TITLE' => 'Inserir novo título para este álbum:', - 'ALBUMS_NEW_TITLE_1' => 'Inserir um título para todos', - 'ALBUMS_NEW_TITLE_2' => 'álbums selecionados:', - 'ALBUM_SET_TITLE' => 'Escolher Título', - 'ALBUM_DESCRIPTION' => 'Descrição', - 'ALBUM_SHOW_TAGS' => 'Etiquetas para mostrar', - 'ALBUM_NEW_DESCRIPTION' => 'Inserir uma nova descrição para este álbum:', - 'ALBUM_SET_DESCRIPTION' => 'Escolher Descrição', - 'ALBUM_NEW_SHOWTAGS' => 'Inserir etiquetas de fotografias que irão ficar visíveis neste álbum:', - 'ALBUM_SET_SHOWTAGS' => 'Escolher etiquetas a mostrar', - 'ALBUM_ALBUM' => 'Álbum', - 'ALBUM_CREATED' => 'Criado', - 'ALBUM_IMAGES' => 'Imagens', - 'ALBUM_VIDEOS' => 'Videos', - 'ALBUM_SUBALBUMS' => 'Subálbums', - 'ALBUM_SHARING' => 'Partilhar', - 'ALBUM_SHR_YES' => 'SIM', - 'ALBUM_SHR_NO' => 'Não', - 'ALBUM_PUBLIC' => 'Público', - 'ALBUM_PUBLIC_EXPL' => 'Álbum pode ser visto por outros, sujeito às restrições abaixo.', - 'ALBUM_FULL' => 'Original', - 'ALBUM_FULL_EXPL' => 'Imagens na resolução original estão disponíveis.', - 'ALBUM_HIDDEN' => 'Oculto', - 'ALBUM_HIDDEN_EXPL' => 'Apenas pessoas com o link direto verão este álbum.', - 'ALBUM_MARK_NSFW' => 'Marcar álbum como sensível', - 'ALBUM_UNMARK_NSFW' => 'Desmarcar álbum como sensível', - 'ALBUM_NSFW' => 'Sensível', - 'ALBUM_NSFW_EXPL' => 'Álbum está marcado como contendo conteúdo sensível.', - 'ALBUM_DOWNLOADABLE' => 'Transferível', - 'ALBUM_DOWNLOADABLE_EXPL' => 'Visitantes da tua galeria podem transferir este álbum.', - 'ALBUM_SHARE_BUTTON_VISIBLE' => 'Botão Partilhar está visível', - 'ALBUM_SHARE_BUTTON_VISIBLE_EXPL' => 'Mostrar links de partilha nas redes sociais.', - 'ALBUM_PASSWORD' => 'Password', - 'ALBUM_PASSWORD_PROT' => 'Protegido por password', - 'ALBUM_PASSWORD_PROT_EXPL' => 'Álbum apenas acessível com uma password válida.', - 'ALBUM_PASSWORD_REQUIRED' => 'Este álbum está protegido por password. Inserir a password para ver as fotografias deste álbum:', - 'ALBUM_MERGE_1' => 'De certeza que quer fundir o álbum', - 'ALBUM_MERGE_2' => 'com o álbum', - 'ALBUMS_MERGE' => 'De certeza que quer fundir todos os álbums selecionados com o álbum', - 'MERGE_ALBUM' => 'Fundir Álbums', - 'DONT_MERGE' => 'Não Fundir', - 'ALBUM_MOVE_1' => 'De certeza que quer mover o álbum', - 'ALBUM_MOVE_2' => 'para o álbum', - 'ALBUMS_MOVE' => 'De certeza que quer mover todos os álbums selecionados para o álbum', - 'MOVE_ALBUMS' => 'Mover Álbums', - 'NOT_MOVE_ALBUMS' => 'Não Mover', - 'ROOT' => 'Álbums', - 'ALBUM_REUSE' => 'Reutilizar', - 'ALBUM_LICENSE' => 'Licença', - 'ALBUM_SET_LICENSE' => 'Escolher Licença', - 'ALBUM_LICENSE_HELP' => 'Precisa de ajuda para escolher?', - 'ALBUM_LICENSE_NONE' => 'Nenhuma', - 'ALBUM_RESERVED' => 'Todos os Direitos Reservados', - 'ALBUM_SET_ORDER' => 'Escolher Ordem', - 'ALBUM_ORDERING' => 'Ordenar por', - - 'PHOTO_ABOUT' => 'Acerca de', - 'PHOTO_BASICS' => 'Básicos', - 'PHOTO_TITLE' => 'Título', - 'PHOTO_NEW_TITLE' => 'Inserir um novo título para esta fotografia:', - 'PHOTO_SET_TITLE' => 'Escolher Título', - 'PHOTO_UPLOADED' => 'Enviada', - 'PHOTO_DESCRIPTION' => 'Descrição', - 'PHOTO_NEW_DESCRIPTION' => 'Inserir uma nova descrição para esta fotografia:', - 'PHOTO_SET_DESCRIPTION' => 'Escolher Descrição', - 'PHOTO_NEW_LICENSE' => 'Adicionar uma Licença', - 'PHOTO_SET_LICENSE' => 'Escolher Licença', - 'PHOTO_LICENSE' => 'Licença', - 'PHOTO_REUSE' => 'Reutilizar', - 'PHOTO_LICENSE_NONE' => 'Nenhuma', - 'PHOTO_RESERVED' => 'Todos os Direitos Reservados', - 'PHOTO_LATITUDE' => 'Latitude', - 'PHOTO_LONGITUDE' => 'Longitude', - 'PHOTO_ALTITUDE' => 'Altitude', - 'PHOTO_IMGDIRECTION' => 'Direção', - 'PHOTO_LOCATION' => 'Localização', - 'PHOTO_IMAGE' => 'Imagem', - 'PHOTO_VIDEO' => 'Vídeo', - 'PHOTO_SIZE' => 'Tamanho', - 'PHOTO_FORMAT' => 'Formato', - 'PHOTO_RESOLUTION' => 'Resolução', - 'PHOTO_DURATION' => 'Duração', - 'PHOTO_FPS' => 'Frame rate', - 'PHOTO_TAGS' => 'Etiquetas', - 'PHOTO_NOTAGS' => 'Sem Etiquetas', - 'PHOTO_NEW_TAGS' => 'Inserir as suas etiquetas para esta fotografia. Pode adicionar várias etiquetas separando-as com uma vírgula:', - 'PHOTO_NEW_TAGS_1' => 'Inserir as suas etiquetas para todas', - 'PHOTO_NEW_TAGS_2' => 'as fotografias selecionadas. Etiquetas existentes vão ser substituídas. Pode adicionar várias etiquetas separando-as com uma vírgula:', - 'PHOTO_SET_TAGS' => 'Escolher Etiquetas', - 'PHOTO_CAMERA' => 'Camera', - 'PHOTO_CAPTURED' => 'Capturada', - 'PHOTO_MAKE' => 'Criada', - 'PHOTO_TYPE' => 'Tipo/Modelo', - 'PHOTO_LENS' => 'Lente', - 'PHOTO_SHUTTER' => 'Velocidade do Obturador', - 'PHOTO_APERTURE' => 'Abertura', - 'PHOTO_FOCAL' => 'Distância Focal', - 'PHOTO_ISO' => 'ISO', - 'PHOTO_SHARING' => 'Partilhada', - 'PHOTO_SHR_PLUBLIC' => 'Pública', - 'PHOTO_SHR_ALB' => 'Sim (Álbum)', - 'PHOTO_SHR_PHT' => 'Sim (Fotografia)', - 'PHOTO_SHR_NO' => 'Não', - 'PHOTO_DELETE' => 'Eliminar Fotografia', - 'PHOTO_KEEP' => 'Manter Fotografia', - 'PHOTO_DELETE_1' => 'De certeza que quer eliminar a fotografia', - 'PHOTO_DELETE_2' => '? Esta ação não pode ser desfeita!', - 'PHOTO_DELETE_ALL_1' => 'De certeza que quer eliminar todas', - 'PHOTO_DELETE_ALL_2' => 'as fotografias selecionadas? Esta ação não pode ser desfeita!', - 'PHOTOS_NEW_TITLE_1' => 'Inserir um título para todas', - 'PHOTOS_NEW_TITLE_2' => 'as fotografias selecionadas:', - 'PHOTO_MAKE_PRIVATE_ALBUM' => 'Esta fotografia está localizada num álbum público. Para tornar esta fotografia privada ou pública, edite a visibilidade do álbum associado.', - 'PHOTO_SHOW_ALBUM' => 'Mostrar Álbum', - 'PHOTO_PUBLIC' => 'Público', - 'PHOTO_PUBLIC_EXPL' => 'Fotografia pode ser vista por outros, sujeita às restrições abaixo.', - 'PHOTO_FULL' => 'Original', - 'PHOTO_FULL_EXPL' => 'Imagem em resolução original está disponível.', - 'PHOTO_HIDDEN' => 'Oculta', - 'PHOTO_HIDDEN_EXPL' => 'Apenas pessoas com o link direto podem ver esta fotografia.', - 'PHOTO_DOWNLOADABLE' => 'Transferível', - 'PHOTO_DOWNLOADABLE_EXPL' => 'Visitantes da sua galeria podem transferir esta fotografia.', - 'PHOTO_SHARE_BUTTON_VISIBLE' => 'Botão Partilhar está visível', - 'PHOTO_SHARE_BUTTON_VISIBLE_EXPL' => 'Mostrar links de partilha nas redes sociais.', - 'PHOTO_PASSWORD_PROT' => 'Protegido por password', - 'PHOTO_PASSWORD_PROT_EXPL' => 'Fotografia apenas acessível com uma password válida.', - 'PHOTO_EDIT_SHARING_TEXT' => 'As propriedades de partilha desta fotografia vão ser alteradas para o seguinte:', - 'PHOTO_NO_EDIT_SHARING_TEXT' => 'Porque esta fotografia está localizada num álbum público, herda as configurações de visibilidade desse álbum. A sua visibilidade atual é mostrada abaixo apenas como informação.', - 'PHOTO_EDIT_GLOBAL_SHARING_TEXT' => 'A visibilidade desta fotografia pode ser afinada através das configurações globais do Lychee. A sua visibilidade atual é mostrada abaixo apenas como informação.', - 'PHOTO_SHARING_CONFIRM' => 'Guardar', - - 'LOADING' => 'A carregar', - 'ERROR' => 'Erro', - 'ERROR_TEXT' => 'Whoops, parece que algo de errado aconteceu. Por favor, atualize a página e tente de novo!', - 'ERROR_DB_1' => 'Foi impossível conectar à base de dados do host porque o acesso foi negado. Verifique o host, username e password e garanta que o acesso através da localização atual é permitido.', - 'ERROR_DB_2' => 'Incapaz de criar a base de dados. Verifique o host, username e password e garanta que o utilizador especificado tem permissões para modificar e adicionar conteúdo à base de dados.', - 'ERROR_CONFIG_FILE' => "Incapaz de guardar esta configuração. Permissão negada em 'data/'. Por favor, estabeleça as permissões de leitura, escrita e execução para outros em 'data/' e 'uploads/'. Dê uma vista de olhos ao readme para mais informação.", - 'ERROR_UNKNOWN' => 'Algo de inesperado aconteceu. Por favor tente de novo e verifique a sua instalação e servidor. Dê uma vista de olhos ao readme para mais informação.', - 'ERROR_LOGIN' => 'Incapaz de guardar o login. Por favor tente de novo com outro nome de utilizador e password!', - 'ERROR_MAP_DEACTIVATED' => 'A funcionalidade do mapa foi desativada nas configurações.', - 'ERROR_SEARCH_DEACTIVATED' => 'A funcionalidade de procura foi desativada nas configurações.', - 'SUCCESS' => 'OK', - 'RETRY' => 'Tentar de novo', - - 'SETTINGS_SUCCESS_LOGIN' => 'Informação de Login atualizada.', - 'SETTINGS_SUCCESS_SORT' => 'Ordenação atualizada.', - 'SETTINGS_SUCCESS_DROPBOX' => 'Dropbox Key atualizada.', - 'SETTINGS_SUCCESS_LANG' => 'Linguagem atualizada', - 'SETTINGS_SUCCESS_LAYOUT' => 'Layout atualizado', - 'SETTINGS_SUCCESS_IMAGE_OVERLAY' => 'Configuração de Overlay EXIF atualizada', - 'SETTINGS_SUCCESS_PUBLIC_SEARCH' => 'Pesquisa pública atualizada', - 'SETTINGS_SUCCESS_LICENSE' => 'licença predefinida atualizada', - 'SETTINGS_SUCCESS_MAP_DISPLAY' => 'Configuração da janela do mapa atualizada', - 'SETTINGS_SUCCESS_MAP_DISPLAY_PUBLIC' => 'Configuração da janela do mapa para álbums públicos atualizada', - 'SETTINGS_SUCCESS_MAP_PROVIDER' => 'Provider do mapa atualizado', - - 'U2F_NOT_SUPPORTED' => 'U2F não suportado. Desculpe.', - 'U2F_NOT_SECURE' => 'Ambiente não seguro. U2F não disponível.', - 'U2F_REGISTER_KEY' => 'Registar novo dispositivo.', - 'U2F_REGISTRATION_SUCCESS' => 'Registo bem-sucedido!', - 'U2F_AUTHENTIFICATION_SUCCESS' => 'Authenticação bem-sucedida!', - 'U2F_CREDENTIALS' => 'Credenciais', - 'U2F_CREDENTIALS_DELETED' => 'Credenciais eliminadas!', - - 'NEW_PHOTOS_NOTIFICATION' => 'Send new photos notification emails.', - 'SETTINGS_SUCCESS_NEW_PHOTOS_NOTIFICATION' => 'New photos notification updated', - 'USER_EMAIL_INSTRUCTION' => 'Add your email below to enable receiving email notifications.
To stop receiving emails, simply remove your email below.', - - 'DB_INFO_TITLE' => 'Inserir os detalhes da conecção à base de dados abaixo:', - 'DB_INFO_HOST' => 'Host da Base de Dados (opcional)', - 'DB_INFO_USER' => 'Nome de Utilizador da Base de Dados', - 'DB_INFO_PASSWORD' => 'Password da Base de Dados', - 'DB_INFO_TEXT' => 'O Lychee vai criar a sua própria base de dados. Se for necessário, pode inserir o nome de uma base de dados existente em vez disso:', - 'DB_NAME' => 'Nome da Base de Dados (opcional)', - 'DB_PREFIX' => 'Prefixo das tabelas (opcional)', - 'DB_CONNECT' => 'Conectar', - - 'LOGIN_TITLE' => 'Inserir um nome de utilizador e uma password para a sua instalação:', - 'LOGIN_USERNAME' => 'Novo Nome de Utilizador', - 'LOGIN_PASSWORD' => 'Nova Password', - 'LOGIN_PASSWORD_CONFIRM' => 'Confirmar Password', - 'LOGIN_CREATE' => 'Criar Login', - - 'PASSWORD_TITLE' => 'Inserir a sua password atual:', - 'USERNAME_CURRENT' => 'Nome de Utilizador Atual', - 'PASSWORD_CURRENT' => 'Password Atual', - 'PASSWORD_TEXT' => 'O seu nome de utilizador e password vão ser alterados para o seguinte:', - 'PASSWORD_CHANGE' => 'Alterar Login', - - 'EDIT_SHARING_TITLE' => 'Editar Partilha', - 'EDIT_SHARING_TEXT' => 'As propriedades de partilha deste álbum vão ser alteradas para o seguinte:', - 'SHARE_ALBUM_TEXT' => 'Este álbum vai ser partilhado com as seguintes propriedades:', - 'ALBUM_SHARING_CONFIRM' => 'Guardar', - - 'SORT_ALBUM_BY_1' => 'Ordenar álbums por', - 'SORT_ALBUM_BY_2' => 'numa', - 'SORT_ALBUM_BY_3' => 'ordem.', - - 'SORT_ALBUM_SELECT_1' => 'Hora de Criação', - 'SORT_ALBUM_SELECT_2' => 'Título', - 'SORT_ALBUM_SELECT_3' => 'Descrição', - 'SORT_ALBUM_SELECT_4' => 'Público', - 'SORT_ALBUM_SELECT_5' => 'Data da Modificação Mais Recente', - 'SORT_ALBUM_SELECT_6' => 'Data da Modificação Mais Antiga', - - 'SORT_PHOTO_BY_1' => 'Ordenar fotografias por', - 'SORT_PHOTO_BY_2' => 'numa', - 'SORT_PHOTO_BY_3' => 'ordem.', - - 'SORT_PHOTO_SELECT_1' => 'Hora de Envio', - 'SORT_PHOTO_SELECT_2' => 'Data de Modificação', - 'SORT_PHOTO_SELECT_3' => 'Título', - 'SORT_PHOTO_SELECT_4' => 'Descrição', - 'SORT_PHOTO_SELECT_5' => 'Público', - 'SORT_PHOTO_SELECT_6' => 'Favorito', - 'SORT_PHOTO_SELECT_7' => 'Formato da Fotografia', - - 'SORT_ASCENDING' => 'Ascendente', - 'SORT_DESCENDING' => 'Descendente', - 'SORT_CHANGE' => 'Alterar Ordenação', - - 'DROPBOX_TITLE' => 'Escolher a Dropbox Key', - 'DROPBOX_TEXT' => "Para importar fotografias do seu Dropbox, precisa de uma key válida de drop-in do website deles. Crie uma key pessoal e insira-a abaixo:", - - 'LANG_TEXT' => 'Alterar língua do Lychee para:', - 'LANG_TITLE' => 'Alterar Linguagem', - 'PUBLIC_SEARCH_TEXT' => 'Pesquisa pública permitida:', - 'IMAGE_OVERLAY_TEXT' => 'Mostrar o overlay da data por defeito:', - 'OVERLAY_TYPE' => 'Data a usar no overlay da imagem:', - 'OVERLAY_NONE' => 'None', - 'OVERLAY_EXIF' => 'Informação EXIF da fotografia', - 'OVERLAY_DESCRIPTION' => 'Descrição da fotografia', - 'OVERLAY_DATE' => 'Data da Fotografia', - 'MAP_DISPLAY_TEXT' => 'Ligar mapas (fornecidos por OpenStreetMap):', - 'MAP_DISPLAY_PUBLIC_TEXT' => 'Permitir mapas para álbums públicos (fornecidos por OpenStreetMap):', - 'LOCATION_DECODING' => 'Transformar a informação GPS no nome da localização', - 'LOCATION_SHOW' => 'Mostrar nome da localização', - 'LOCATION_SHOW_PUBLIC' => 'Mostrar nome da localização no modo público', - 'MAP_PROVIDER' => 'Fornecedor das janelas OpenStreetMap:', - 'MAP_PROVIDER_WIKIMEDIA' => 'Wikimedia', - 'MAP_PROVIDER_OSM_ORG' => 'OpenStreetMap.org (sem HiDPI)', - 'MAP_PROVIDER_OSM_DE' => 'OpenStreetMap.de (sem HiDPI)', - 'MAP_PROVIDER_OSM_FR' => 'OpenStreetMap.fr (sem HiDPI)', - 'MAP_PROVIDER_RRZE' => 'Universidade de Erlangen, Alemanha (apenas HiDPI)', - 'MAP_INCLUDE_SUBALBUMS_TEXT' => 'Incluír fotografias de subálbums no mapa:', - - 'LAYOUT_TYPE' => 'Disposição das fotografias:', - 'LAYOUT_SQUARES' => 'Miniaturas quadradas', - 'LAYOUT_JUSTIFIED' => 'Com formatação, justificada', - 'LAYOUT_UNJUSTIFIED' => 'Com formatação, não justificada', - 'SET_LAYOUT' => 'Alterar disposição', - - 'NSFW_VISIBLE_TEXT_1' => 'Tornar Sensível os álbums visíveis por defeito.', - 'NSFW_VISIBLE_TEXT_2' => 'Se o álbum é público, continua acessível, apenas ocultado da visualização e pode ser mostrado pressionando H.', - 'SETTINGS_SUCCESS_NSFW_VISIBLE' => 'Sensibilidade predefinida do álbum visível atualizada com sucesso.', - - 'VIEW_NO_RESULT' => 'Sem resultados', - 'VIEW_NO_PUBLIC_ALBUMS' => 'Sem álbums públicos', - 'VIEW_NO_CONFIGURATION' => 'Sem configuração', - 'VIEW_PHOTO_NOT_FOUND' => 'Fotografia não encontrada', - - 'NO_TAGS' => 'Sem Etiquetas', - - 'UPLOAD_MANAGE_NEW_PHOTOS' => 'Pode agora gerir a(s) sua(s) nova(s) fotografia(s).', - 'UPLOAD_COMPLETE' => 'Envio completo', - 'UPLOAD_COMPLETE_FAILED' => 'Falha ao enviar uma ou mais fotografias.', - 'UPLOAD_IMPORTING' => 'A importar', - 'UPLOAD_IMPORTING_URL' => 'A importar URL', - 'UPLOAD_UPLOADING' => 'A enviar', - 'UPLOAD_FINISHED' => 'Acabado', - 'UPLOAD_PROCESSING' => 'A processar', - 'UPLOAD_FAILED' => 'Falhado', - 'UPLOAD_FAILED_ERROR' => 'Envio falhado. O servidor respondeu com um erro!', - 'UPLOAD_FAILED_WARNING' => 'Envio falhado. O servidor respondeu com um aviso!', - 'UPLOAD_CANCELLED' => 'Cancelado', - 'UPLOAD_SKIPPED' => 'Saltado', - 'UPLOAD_UPDATED' => 'Ignorado', - 'UPLOAD_IMPORT_SKIPPED_DUPLICATE' => 'Esta fotografia foi ignorada porque já está na sua livraria.', - 'UPLOAD_IMPORT_RESYNCED_DUPLICATE' => 'Esta fotografia foi ignorada porque já está na sua livraria, mas os seus metadados foram atualizados.', - 'UPLOAD_ERROR_CONSOLE' => 'Por favor dá uma vista de olhos na consola do teu navegador para mais detalhes.', - 'UPLOAD_UNKNOWN' => 'O servidor respondeu com uma mensagem desconhecida. Por favor dá uma vista de olhos na consola do teu navegador para mais detalhes.', - 'UPLOAD_ERROR_UNKNOWN' => 'Envio falhado. O servidor respondeu com um erro desconhecido!', - 'UPLOAD_ERROR_POSTSIZE' => 'Envio falhado. O limite post_max_size do PHP é demasiado pequeno!', - 'UPLOAD_ERROR_FILESIZE' => 'Envio falhado. O limite upload_max_filesize do PHP é demasiado pequeno!', - 'UPLOAD_IN_PROGRESS' => 'O Lychee está a enviar ficheiros de momento!', - 'UPLOAD_IMPORT_WARN_ERR' => 'A importação acabou, mas foi respondida com avisos ou erros. Por favor, dá uma vista de olhos no log (Configurações -> Mostrar Log) para mais detalhes.', - 'UPLOAD_IMPORT_COMPLETE' => 'Importação completa', - 'UPLOAD_IMPORT_INSTR' => 'Por favor insere o link direto da fotografia para a importar:', - 'UPLOAD_IMPORT' => 'Importar', - 'UPLOAD_IMPORT_SERVER' => 'A importar do servidor', - 'UPLOAD_IMPORT_SERVER_FOLD' => 'Pasta vazia ou sem ficheiros legíveis para processar. Por favor, dá uma vista de olhos no log (Configurações -> Mostrar Log) para mais detalhes.', - 'UPLOAD_IMPORT_SERVER_INSTR' => 'Importar todas as fotografias, pastas e sub-pastas localizadas na pasta com o seguinte caminho absoluto (no servidor):', - 'UPLOAD_ABSOLUTE_PATH' => 'Caminho absoluto para o diretório', - 'UPLOAD_IMPORT_SERVER_EMPT' => 'Não foi conseguido iniciar a importação porque a pasta estava vazia!', - 'UPLOAD_IMPORT_DELETE_ORIGINALS' => 'Eliminar originais', - 'UPLOAD_IMPORT_DELETE_ORIGINALS_EXPL' => 'Ficheiros originais vão ser eliminados depois da importação assim que possível.', - 'UPLOAD_IMPORT_VIA_SYMLINK' => 'Links Simbólicos', - 'UPLOAD_IMPORT_VIA_SYMLINK_EXPL' => 'Importar ficheiros utilizando links simbólicos dos links originais.', - 'UPLOAD_IMPORT_SKIP_DUPLICATES' => 'Saltar duplicados', - 'UPLOAD_IMPORT_SKIP_DUPLICATES_EXPL' => 'Ficheiros de multimédia existentes serão ignorados.', - 'UPLOAD_IMPORT_RESYNC_METADATA' => 'Re-sincronizar metadados', - 'UPLOAD_IMPORT_RESYNC_METADATA_EXPL' => 'Atualizar metadados dos ficheiros multimédia existentes.', - 'UPLOAD_IMPORT_LOW_MEMORY' => 'Condição de memória fraca!', - 'UPLOAD_IMPORT_LOW_MEMORY_EXPL' => 'O processo de importação no servidor está a chegar ao limite de memória e pode acabar por ser terminado de forma permatura.', - 'UPLOAD_WARNING' => 'Aviso', - 'UPLOAD_IMPORT_NOT_A_DIRECTORY' => 'O caminho indicado não é um diretório legível!', - 'UPLOAD_IMPORT_PATH_RESERVED' => 'O caminho indicado é um caminho reservado pelo Lychee!', - 'UPLOAD_IMPORT_UNREADABLE' => 'Não foi conseguido ler o ficheiro!', - 'UPLOAD_IMPORT_FAILED' => 'Não foi conseguido importar o ficheiro!', - 'UPLOAD_IMPORT_UNSUPPORTED' => 'Tipo de ficheiro não suportado!', - 'UPLOAD_IMPORT_ALBUM_FAILED' => 'Não foi conseguido criar o álbum!', - 'UPLOAD_IMPORT_CANCELLED' => 'Importação cancelada', - - 'ABOUT_SUBTITLE' => 'Gestão de fotografias auto-hospedada e bem feita', - 'ABOUT_DESCRIPTION' => 'é uma ferramenta gratuita de gestão de fotografias, que corre no teu servidor ou espaço web. A instalação demora segundos. Enviar, gerir e partilhar fotografias como uma aplicação nativa. O Lychee vem com tudo o que precisas e todas as tuas fotografias são guardadas de forma segura.', - 'FOOTER_COPYRIGHT' => 'Todas as imagens neste website estão sujeitas a direitos autorais por ', - 'HOSTED_WITH_LYCHEE' => 'Hospedado com Lychee', - - 'URL_COPY_TO_CLIPBOARD' => 'Copiar para o clipboard', - 'URL_COPIED_TO_CLIPBOARD' => 'URL copiado para o clipboard!', - 'PHOTO_DIRECT_LINKS_TO_IMAGES' => 'Links diretos para os ficheiros de imagem:', - 'PHOTO_MEDIUM' => 'Média', - 'PHOTO_MEDIUM_HIDPI' => 'Média HiDPI', - 'PHOTO_SMALL' => 'Pequena', - 'PHOTO_SMALL_HIDPI' => 'Pequena HiDPI', - 'PHOTO_THUMB' => 'Quadrada pequena', - 'PHOTO_THUMB_HIDPI' => 'Quadrada pequena HiDPI', - 'PHOTO_LIVE_VIDEO' => 'Video parte de fotografia ao vivo', - 'PHOTO_VIEW' => 'Vista de Fotografia Lychee:', - - 'PHOTO_EDIT_ROTATECWISE' => 'Rodar no sentido dos ponteiros do relógio', - 'PHOTO_EDIT_ROTATECCWISE' => 'Rodar contra o sentido dos ponteiros do relógio', - ]; - - return $locale; - } -} diff --git a/app/Locale/Russian.php b/app/Locale/Russian.php deleted file mode 100644 index aa9dd0cd930..00000000000 --- a/app/Locale/Russian.php +++ /dev/null @@ -1,476 +0,0 @@ - 'логин', - 'PASSWORD' => 'пароль', - 'ENTER' => 'Enter', - 'CANCEL' => 'Отмена', - 'SIGN_IN' => 'Вход', - 'CLOSE' => 'Закрыть', - 'SETTINGS' => 'Параметры', - 'SEARCH' => 'Search ...', - 'MORE' => 'More', - 'DEFAULT' => 'Default', - - 'USERS' => 'Пользователи', - 'U2F' => 'U2F', - 'NOTIFICATIONS' => 'Notifications', - 'SHARING' => 'Поделиться', - 'CHANGE_LOGIN' => 'Изменить логин', - 'CHANGE_SORTING' => 'Порядок сортировки', - 'SET_DROPBOX' => 'Подключить Dropbox', - 'ABOUT_LYCHEE' => 'О Lychee', - 'DIAGNOSTICS' => 'Диагностика', - 'DIAGNOSTICS_GET_SIZE' => 'Request space usage', - 'LOGS' => 'Логи', - 'SIGN_OUT' => 'Выход', - 'UPDATE_AVAILABLE' => 'Доступно обновление!', - 'MIGRATION_AVAILABLE' => 'Migration available!', - 'DEFAULT_LICENSE' => 'Лицензия для новых загрузок:', - 'SET_LICENSE' => 'Установить лицензию', - 'SET_OVERLAY_TYPE' => 'Установить оверлей', - 'SET_MAP_PROVIDER' => 'Set OpenStreetMap tiles provider', - - 'SMART_ALBUMS' => 'Метаальбомы', - 'SHARED_ALBUMS' => 'Общие альбомы', - 'ALBUMS' => 'Альбомы', - 'PHOTOS' => 'Фотографии', - 'SEARCH_RESULTS' => 'Search results', - - 'RENAME' => 'Переименовать', - 'RENAME_ALL' => 'Переименовать все', - 'MERGE' => 'Объединить', - 'MERGE_ALL' => 'Объединить все', - 'MAKE_PUBLIC' => 'Сделать публичным', - 'SHARE_ALBUM' => 'Поделиться альбомом', - 'SHARE_PHOTO' => 'Поделиться фото', - 'VISIBILITY_ALBUM' => 'Album Visibility', - 'VISIBILITY_PHOTO' => 'Photo Visibility', - 'DOWNLOAD_ALBUM' => 'Скачать альбом', - 'ABOUT_ALBUM' => 'Об альбоме', - 'DELETE_ALBUM' => 'Удалить альбом', - 'MOVE_ALBUM' => 'Move Album', - 'FULLSCREEN_ENTER' => 'Полный экран', - 'FULLSCREEN_EXIT' => 'Оконный режим', - - 'SHARING_ALBUM_USERS' => 'Share this album with users', - 'WAIT_FETCH_DATA' => 'Please wait while we get the data...', - 'SHARING_ALBUM_USERS_NO_USERS' => 'There are no users to share the album with', - 'SHARING_ALBUM_USERS_LONG_MESSAGE' => 'Select the users to share this album with', - - 'DELETE_ALBUM_QUESTION' => 'Удалить альбом и все фото', - 'KEEP_ALBUM' => 'Сохранить альбом', - 'DELETE_ALBUM_CONFIRMATION_1' => 'Вы точно хотите удалить альбом', - 'DELETE_ALBUM_CONFIRMATION_2' => 'со всеми фотографиями? Это действие необратимо!', - - 'DELETE_ALBUMS_QUESTION' => 'Удалить альбом и фотографии', - 'KEEP_ALBUMS' => 'Сохранить альбомы', - 'DELETE_ALBUMS_CONFIRMATION_1' => 'Вы точно хотите удалить альбомы', - 'DELETE_ALBUMS_CONFIRMATION_2' => 'со всеми фотографиями? Это действие необратимо!', - - 'DELETE_UNSORTED_CONFIRM' => 'Вы точно хотите удалить все фото из \'Unsorted\'?
Это действие необратимо!', - 'CLEAR_UNSORTED' => 'Очистить Unsorted', - 'KEEP_UNSORTED' => 'Сохранить Unsorted', - - 'EDIT_SHARING' => 'Доступ', - 'MAKE_PRIVATE' => 'Сделать личным', - - 'CLOSE_ALBUM' => 'Закрыть альбом', - 'CLOSE_PHOTO' => 'Закрыть фото', - 'CLOSE_MAP' => 'Close Map', - - 'ADD' => 'Добавить', - 'MOVE' => 'Переместить', - 'MOVE_ALL' => 'Переместить все', - 'DUPLICATE' => 'Скопировать', - 'DUPLICATE_ALL' => 'Скопировать все', - 'COPY_TO' => 'Скопировать в...', - 'COPY_ALL_TO' => 'Скопировать все в...', - 'DELETE' => 'Удалить', - 'DELETE_ALL' => 'Удалить все', - 'DOWNLOAD' => 'Скачать', - 'DOWNLOAD_ALL' => 'Download Selected', - 'UPLOAD_PHOTO' => 'Загрузить фото', - 'IMPORT_LINK' => 'Загрузить по ссылке', - 'IMPORT_DROPBOX' => 'Импортировать из Dropbox', - 'IMPORT_SERVER' => 'Импортировать с сервера', - 'NEW_ALBUM' => 'Создать альбом', - 'NEW_TAG_ALBUM' => 'New Tag Album', - - 'TITLE_NEW_ALBUM' => 'Название нового альбома:', - 'UNTITLED' => 'Безымянный', - 'UNSORTED' => 'Не отсортированные', - 'STARRED' => 'Отмеченные', - 'RECENT' => 'Последние', - 'PUBLIC' => 'Общие', - 'NUM_PHOTOS' => 'фотографий', - - 'CREATE_ALBUM' => 'Создать альбом', - 'CREATE_TAG_ALBUM' => 'Create Tag Album', - - 'STAR_PHOTO' => 'Отметить фото', - 'STAR' => 'Отметить', - 'STAR_ALL' => 'Отметить все', - 'TAGS' => 'Теги', - 'TAGS_ALL' => 'теги для всех', - 'UNSTAR_PHOTO' => 'Снять отметку', - 'SET_COVER' => 'Set Album Cover', - 'REMOVE_COVER' => 'Remove Album Cover', - - 'FULL_PHOTO' => 'Полный размер', - 'ABOUT_PHOTO' => 'О фотографии', - 'DISPLAY_FULL_MAP' => 'Map', - 'DIRECT_LINK' => 'Прямая ссылка', - 'DIRECT_LINKS' => 'Direct Links', - - 'ALBUM_ABOUT' => 'Об альбоме', - 'ALBUM_BASICS' => 'Основное', - 'ALBUM_TITLE' => 'Заголовок', - 'ALBUM_NEW_TITLE' => 'Новый заголовок альбома:', - 'ALBUMS_NEW_TITLE_1' => 'Введите заголовок для всех', - 'ALBUMS_NEW_TITLE_2' => 'выбранных альбомов:', - 'ALBUM_SET_TITLE' => 'Сохранить заголовок', - 'ALBUM_DESCRIPTION' => 'Описание', - 'ALBUM_SHOW_TAGS' => 'Tags to show', - 'ALBUM_NEW_DESCRIPTION' => 'Введите описание этого альбома:', - 'ALBUM_SET_DESCRIPTION' => 'Сохранить описание', - 'ALBUM_NEW_SHOWTAGS' => 'Enter tags of photos that will be visible in this album:', - 'ALBUM_SET_SHOWTAGS' => 'Set tags to show', - 'ALBUM_ALBUM' => 'Альбом', - 'ALBUM_CREATED' => 'Создано', - 'ALBUM_IMAGES' => 'Фотографий', - 'ALBUM_VIDEOS' => 'Videos', - 'ALBUM_SUBALBUMS' => 'Subalbums', - 'ALBUM_SHARING' => 'Общее', - 'ALBUM_SHR_YES' => 'Да', - 'ALBUM_SHR_NO' => 'Нет', - 'ALBUM_PUBLIC' => 'Доступ', - 'ALBUM_PUBLIC_EXPL' => 'Album can be viewed by others, subject to the restrictions below.', - 'ALBUM_FULL' => 'Original', - 'ALBUM_FULL_EXPL' => 'Full-resolution pictures are available.', - 'ALBUM_HIDDEN' => 'Скрытый', - 'ALBUM_HIDDEN_EXPL' => 'Альбом доступен только по прямой ссылке.', - 'ALBUM_MARK_NSFW' => 'Mark album as sensitive', - 'ALBUM_UNMARK_NSFW' => 'Unmark album as sensitive', - 'ALBUM_NSFW' => 'Sensitive', - 'ALBUM_NSFW_EXPL' => 'Album is marked to contain sensitive content.', - 'ALBUM_DOWNLOADABLE' => 'Скачивание разрешено', - 'ALBUM_DOWNLOADABLE_EXPL' => 'Гости фотогалереи могут скачать этот альбом.', - 'ALBUM_SHARE_BUTTON_VISIBLE' => 'Share button is visible', - 'ALBUM_SHARE_BUTTON_VISIBLE_EXPL' => 'Display social media sharing links.', - 'ALBUM_PASSWORD' => 'Пароль', - 'ALBUM_PASSWORD_PROT' => 'Защищено паролем', - 'ALBUM_PASSWORD_PROT_EXPL' => 'Альбом защищён паролем.', - 'ALBUM_PASSWORD_REQUIRED' => 'Этот альбом защищён паролем. Введите пароль для его просмотра:', - 'ALBUM_MERGE_1' => 'Вы точно хотите объединить альбом', - 'ALBUM_MERGE_2' => 'с альбомом', - 'ALBUMS_MERGE' => 'Вы точно хотите объединить все выбранные альбомы в альбом', - 'MERGE_ALBUM' => 'Объединить альбомы', - 'DONT_MERGE' => 'Не объединять', - 'ALBUM_MOVE_1' => 'Вы точно хотите переместить альбом', - 'ALBUM_MOVE_2' => 'в альбом', - 'ALBUMS_MOVE' => 'Вы точно хотите объединить все выбранные альбомы в альбом', - 'MOVE_ALBUMS' => 'Переместить альбомы', - 'NOT_MOVE_ALBUMS' => 'Не перемещать', - 'ROOT' => 'Root', - 'ALBUM_REUSE' => 'Авторские права', - 'ALBUM_LICENSE' => 'Лицензия', - 'ALBUM_SET_LICENSE' => 'Установить лицензию', - 'ALBUM_LICENSE_HELP' => 'Помочь выбрать?', - 'ALBUM_LICENSE_NONE' => 'Нет', - 'ALBUM_RESERVED' => 'Все права защищены', - 'ALBUM_SET_ORDER' => 'Set Order', - 'ALBUM_ORDERING' => 'Order by', - - 'PHOTO_ABOUT' => 'Описание', - 'PHOTO_BASICS' => 'Основное', - 'PHOTO_TITLE' => 'Заголовок', - 'PHOTO_NEW_TITLE' => 'Введите новый заголовок для этого фото:', - 'PHOTO_SET_TITLE' => 'Сохранить заголовок', - 'PHOTO_UPLOADED' => 'Загружено', - 'PHOTO_DESCRIPTION' => 'Описание', - 'PHOTO_NEW_DESCRIPTION' => 'Введите описание для этого фото:', - 'PHOTO_SET_DESCRIPTION' => 'Сохранить описание', - 'PHOTO_NEW_LICENSE' => 'Добавить лицензию', - 'PHOTO_SET_LICENSE' => 'Сохранить лицензию', - 'PHOTO_LICENSE' => 'Лицензия', - 'PHOTO_REUSE' => 'Авторские права', - 'PHOTO_LICENSE_NONE' => 'Нет', - 'PHOTO_RESERVED' => 'Все права защищены', - 'PHOTO_LATITUDE' => 'Latitude', - 'PHOTO_LONGITUDE' => 'Longitude', - 'PHOTO_ALTITUDE' => 'Altitude', - 'PHOTO_IMGDIRECTION' => 'Direction', - 'PHOTO_LOCATION' => 'Location', - 'PHOTO_IMAGE' => 'Изображение', - 'PHOTO_VIDEO' => 'Video', - 'PHOTO_SIZE' => 'Размер', - 'PHOTO_FORMAT' => 'Формат', - 'PHOTO_RESOLUTION' => 'Разрешение', - 'PHOTO_DURATION' => 'Duration', - 'PHOTO_FPS' => 'Frame rate', - 'PHOTO_TAGS' => 'Теги', - 'PHOTO_NOTAGS' => 'Тегов нет', - 'PHOTO_NEW_TAGS' => 'Укажите теги для этого фото. Можно добавить несколько тегов, разделяя их запятыми:', - 'PHOTO_NEW_TAGS_1' => 'Введите теги для всех', - 'PHOTO_NEW_TAGS_2' => 'выбранных фотографий. Имеющиеся теги будут перезаписаны. Вы можете добавить несколько тегов, разделяя их запятыми:', - 'PHOTO_SET_TAGS' => 'Сохранить теги', - 'PHOTO_CAMERA' => 'Камера', - 'PHOTO_CAPTURED' => 'Дата съёмки', - 'PHOTO_MAKE' => 'Производитель', - 'PHOTO_TYPE' => 'Тип/Модель', - 'PHOTO_LENS' => 'Оптика', - 'PHOTO_SHUTTER' => 'Скорость затвора', - 'PHOTO_APERTURE' => 'Диафрагма', - 'PHOTO_FOCAL' => 'Фокусное расстояние', - 'PHOTO_ISO' => 'ISO', - 'PHOTO_SHARING' => 'Доступность', - 'PHOTO_SHR_PLUBLIC' => 'Публичный', - 'PHOTO_SHR_ALB' => 'Да (альбом)', - 'PHOTO_SHR_PHT' => 'Да (фото)', - 'PHOTO_SHR_NO' => 'Нет', - 'PHOTO_DELETE' => 'Удалить фото', - 'PHOTO_KEEP' => 'Сохранить фото', - 'PHOTO_DELETE_1' => 'Вы точно хотите удалить фото', - 'PHOTO_DELETE_2' => '? Это действие необратимо!', - 'PHOTO_DELETE_ALL_1' => 'Вы точно хотите удалить все', - 'PHOTO_DELETE_ALL_2' => 'выбранные фото? Это действие необратимо!', - 'PHOTOS_NEW_TITLE_1' => 'Введите заголовок для всех', - 'PHOTOS_NEW_TITLE_2' => 'выбранных фото:', - 'PHOTO_MAKE_PRIVATE_ALBUM' => 'Это фото находится в публичном альбоме. Чтобы сделать его личным или общедоступным, измените параметры альбома.', - 'PHOTO_SHOW_ALBUM' => 'Перейти к альбому', - 'PHOTO_PUBLIC' => 'Public', - 'PHOTO_PUBLIC_EXPL' => 'Photo can be viewed by others, subject to the restrictions below.', - 'PHOTO_FULL' => 'Original', - 'PHOTO_FULL_EXPL' => 'Full-resolution picture is available.', - 'PHOTO_HIDDEN' => 'Hidden', - 'PHOTO_HIDDEN_EXPL' => 'Only people with the direct link can view this photo.', - 'PHOTO_DOWNLOADABLE' => 'Downloadable', - 'PHOTO_DOWNLOADABLE_EXPL' => 'Visitors of your gallery can download this photo.', - 'PHOTO_SHARE_BUTTON_VISIBLE' => 'Share button is visible', - 'PHOTO_SHARE_BUTTON_VISIBLE_EXPL' => 'Display social media sharing links.', - 'PHOTO_PASSWORD_PROT' => 'Password protected', - 'PHOTO_PASSWORD_PROT_EXPL' => 'Photo only accessible with a valid password.', - 'PHOTO_EDIT_SHARING_TEXT' => 'The sharing properties of this photo will be changed to the following:', - 'PHOTO_NO_EDIT_SHARING_TEXT' => 'Because this photo is located in a public album, it inherits that album\'s visibility settings. Its current visibility is shown below for informational purposes only.', - 'PHOTO_EDIT_GLOBAL_SHARING_TEXT' => 'The visibility of this photo can be fine-tuned using global Lychee settings. Its current visibility is shown below for informational purposes only.', - 'PHOTO_SHARING_CONFIRM' => 'Save', - - 'LOADING' => 'Загрузка', - 'ERROR' => 'Ошибка', - 'ERROR_TEXT' => 'Ой, что-то пошло не так... Пожалуйста, обновите страницу и попробуйте повторить.', - 'ERROR_DB_1' => 'Не могу подключиться к базе данных: доступ запрещён. Перепроверьте хост, логин и пароль и убедитесь, что корректно настроили доступ для хоста и пользователя фотогалереи.', - 'ERROR_DB_2' => 'Не удаётся создать базу данных. Перепроверьте хост, логин и пароль и убедитесь, что указанный пользователь имеет права на модификацию и добавление данных в БД.', - 'ERROR_CONFIG_FILE' => "Не удаётся сохранить конфигурацию. Доступ к 'data/' запрещён. Пожалуйста, задайте для 'data/' и 'uploads/' права на чтение, запись и выполнение файлов. За подробной информацией обратитесь к readme.", - 'ERROR_UNKNOWN' => 'Произошло нечто неожиданное... Пожалуйста, повторите и проверьте папку установки и параметры сервера. За подробной информацией обратитесь к readme.', - 'ERROR_LOGIN' => 'Не могу сохранить регистрационные данные. Попробуйте использовать другие логин и пароль.', - 'ERROR_MAP_DEACTIVATED' => 'Map functionality has been deactivated under settings.', - 'ERROR_SEARCH_DEACTIVATED' => 'Search functionality has been deactivated under settings.', - 'SUCCESS' => 'Ок', - 'RETRY' => 'Повторить', - - 'SETTINGS_SUCCESS_LOGIN' => 'Учётные данные обновлены.', - 'SETTINGS_SUCCESS_SORT' => 'Порядок сортировки обновлён.', - 'SETTINGS_SUCCESS_DROPBOX' => 'Ключ Dropbox обновлён.', - 'SETTINGS_SUCCESS_LANG' => 'Язык изменён', - 'SETTINGS_SUCCESS_LAYOUT' => 'Компоновка обновлена', - 'SETTINGS_SUCCESS_IMAGE_OVERLAY' => 'Данные наложения обновлены', - 'SETTINGS_SUCCESS_PUBLIC_SEARCH' => 'Public search updated', - 'SETTINGS_SUCCESS_LICENSE' => 'Лицензия по умолчанию установлена', - 'SETTINGS_SUCCESS_MAP_DISPLAY' => 'Map display settings updated', - 'SETTINGS_SUCCESS_MAP_DISPLAY_PUBLIC' => 'Map display settings for public albums updated', - 'SETTINGS_SUCCESS_MAP_PROVIDER' => 'Map provider settings updated', - - 'U2F_NOT_SUPPORTED' => 'U2F not supported. Sorry.', - 'U2F_NOT_SECURE' => 'Environment not secured. U2F not available.', - 'U2F_REGISTER_KEY' => 'Register new device.', - 'U2F_REGISTRATION_SUCCESS' => 'Registration successful!', - 'U2F_AUTHENTIFICATION_SUCCESS' => 'Authentication successful!', - 'U2F_CREDENTIALS' => 'Credentials', - 'U2F_CREDENTIALS_DELETED' => 'Credentials deleted!', - - 'NEW_PHOTOS_NOTIFICATION' => 'Send new photos notification emails.', - 'SETTINGS_SUCCESS_NEW_PHOTOS_NOTIFICATION' => 'New photos notification updated', - 'USER_EMAIL_INSTRUCTION' => 'Add your email below to enable receiving email notifications.
To stop receiving emails, simply remove your email below.', - - 'DB_INFO_TITLE' => 'Укажите данные для подключения к базе данных:', - 'DB_INFO_HOST' => 'Сервер баз данных (не обязательно)', - 'DB_INFO_USER' => 'Логин в БД', - 'DB_INFO_PASSWORD' => 'Пароль в БД', - 'DB_INFO_TEXT' => 'Lychee создаст базу данных. Если требуется, Вы можете указать существующую БД:', - 'DB_NAME' => 'Имя БД (не обязательно)', - 'DB_PREFIX' => 'Префикс таблиц (не обязательно)', - 'DB_CONNECT' => 'Подключиться', - - 'LOGIN_TITLE' => 'Введите логин и пароль для входа:', - 'LOGIN_USERNAME' => 'Новый логин', - 'LOGIN_PASSWORD' => 'Новый пароль', - 'LOGIN_PASSWORD_CONFIRM' => 'Повторите пароль', - 'LOGIN_CREATE' => 'Создать', - - 'PASSWORD_TITLE' => 'Введите текущий пароль:', - 'USERNAME_CURRENT' => 'Текущий логин', - 'PASSWORD_CURRENT' => 'Текущий пароль', - 'PASSWORD_TEXT' => 'Ваши логин и пароль будут изменены на следующие:', - 'PASSWORD_CHANGE' => 'Изменить данные', - - 'EDIT_SHARING_TITLE' => 'Параметры доступа', - 'EDIT_SHARING_TEXT' => 'Параметры доступа к выбранному альбому будут изменены на следующие:', - 'SHARE_ALBUM_TEXT' => 'Этот альбом будет доступен со следующими условиями:', - 'ALBUM_SHARING_CONFIRM' => 'Save', - - 'SORT_ALBUM_BY_1' => 'Сортировать альбомы', - 'SORT_ALBUM_BY_2' => 'в порядке', - 'SORT_ALBUM_BY_3' => '.', - - 'SORT_ALBUM_SELECT_1' => 'даты создания', - 'SORT_ALBUM_SELECT_2' => 'заголовка', - 'SORT_ALBUM_SELECT_3' => 'описания', - 'SORT_ALBUM_SELECT_4' => 'доступности', - 'SORT_ALBUM_SELECT_5' => 'свежайшего фото', - 'SORT_ALBUM_SELECT_6' => 'старейшего фото', - - 'SORT_PHOTO_BY_1' => 'Сортировать фотографии', - 'SORT_PHOTO_BY_2' => 'в порядке', - 'SORT_PHOTO_BY_3' => '.', - - 'SORT_PHOTO_SELECT_1' => 'загрузки', - 'SORT_PHOTO_SELECT_2' => 'съёмки', - 'SORT_PHOTO_SELECT_3' => 'заголовка', - 'SORT_PHOTO_SELECT_4' => 'описания', - 'SORT_PHOTO_SELECT_5' => 'доступности', - 'SORT_PHOTO_SELECT_6' => 'отметки', - 'SORT_PHOTO_SELECT_7' => 'формата', - - 'SORT_ASCENDING' => 'По возрастанию', - 'SORT_DESCENDING' => 'По убыванию', - 'SORT_CHANGE' => 'Сменить сортировку', - - 'DROPBOX_TITLE' => 'Задать ключ Dropbox', - 'DROPBOX_TEXT' => "Для загрузки фото из Dropbox Вам нужен ключ, который можно получить на специальной странице. Создайте личный ключ и вставьте здесь:", - - 'LANG_TEXT' => 'Change Lychee language to:', - 'LANG_TITLE' => 'Change Language', - 'PUBLIC_SEARCH_TEXT' => 'Разрешить публичный поиск:', - 'OVERLAY_TYPE' => 'Данные для наложения:', - 'OVERLAY_NONE' => 'None', - 'OVERLAY_EXIF' => 'EXIF данные', - 'OVERLAY_DESCRIPTION' => 'Описание фото', - 'OVERLAY_DATE' => 'Дата съёмки', - 'MAP_DISPLAY_TEXT' => 'Enable maps (provided by OpenStreetMap):', - 'MAP_DISPLAY_PUBLIC_TEXT' => 'Enable maps for public albums (provided by OpenStreetMap):', - 'LOCATION_DECODING' => 'Decode GPS data into location name', - 'LOCATION_SHOW' => 'Show location name', - 'LOCATION_SHOW_PUBLIC' => 'Show location name for public mode', - 'MAP_PROVIDER' => 'Provider of OpenStreetMap tiles:', - 'MAP_PROVIDER_WIKIMEDIA' => 'Wikimedia', - 'MAP_PROVIDER_OSM_ORG' => 'OpenStreetMap.org (no HiDPI)', - 'MAP_PROVIDER_OSM_DE' => 'OpenStreetMap.de (no HiDPI)', - 'MAP_PROVIDER_OSM_FR' => 'OpenStreetMap.fr (no HiDPI)', - 'MAP_PROVIDER_RRZE' => 'University of Erlangen, Germany (only HiDPI)', - 'MAP_INCLUDE_SUBALBUMS_TEXT' => 'Include photos of subalbums on map:', - - 'LAYOUT_TYPE' => 'Компоновка фото:', - 'LAYOUT_SQUARES' => 'Квадратные превью', - 'LAYOUT_JUSTIFIED' => 'По формату, выровнять', - 'LAYOUT_UNJUSTIFIED' => 'По формату, не выравнивать', - 'SET_LAYOUT' => 'Изменить компоновку', - - 'NSFW_VISIBLE_TEXT_1' => 'Make Sensitive albums visible by default.', - 'NSFW_VISIBLE_TEXT_2' => 'If the album is public, it is still accessible, just hidden from the view and can be revealed by pressing H.', - 'SETTINGS_SUCCESS_NSFW_VISIBLE' => 'Default sensitive album visibility updated with success.', - - 'VIEW_NO_RESULT' => 'Не найдено', - 'VIEW_NO_PUBLIC_ALBUMS' => 'Нет публичных альбомов', - 'VIEW_NO_CONFIGURATION' => 'Не настроено', - 'VIEW_PHOTO_NOT_FOUND' => 'Фото не найдено', - - 'NO_TAGS' => 'Тегов нет', - - 'UPLOAD_MANAGE_NEW_PHOTOS' => 'Теперь вы можете управлять новыми фото.', - 'UPLOAD_COMPLETE' => 'Загрузка завершена', - 'UPLOAD_COMPLETE_FAILED' => 'Ошибка загрузки одного или более фото.', - 'UPLOAD_IMPORTING' => 'Импорт', - 'UPLOAD_IMPORTING_URL' => 'импорт по URL', - 'UPLOAD_UPLOADING' => 'Выгрузка', - 'UPLOAD_FINISHED' => 'Завершено', - 'UPLOAD_PROCESSING' => 'Выполняется', - 'UPLOAD_FAILED' => 'Ошибка', - 'UPLOAD_FAILED_ERROR' => 'Загрузка не удалась: сервер вернул ошибку.', - 'UPLOAD_FAILED_WARNING' => 'Загрузка не удалась, сервер вернул предупреждение.', - 'UPLOAD_CANCELLED' => 'Cancelled', - 'UPLOAD_SKIPPED' => 'Пропущено', - 'UPLOAD_UPDATED' => 'Updated', - 'UPLOAD_IMPORT_SKIPPED_DUPLICATE' => 'This photo has been skipped because it\'s already in your library.', - 'UPLOAD_IMPORT_RESYNCED_DUPLICATE' => 'This photo has been skipped because it\'s already in your library, but its metadata has been updated.', - 'UPLOAD_ERROR_CONSOLE' => 'Подробности смотрите в консоли браузера.', - 'UPLOAD_UNKNOWN' => 'Сервер вернул непонятный ответ. Проверьте консоль браузера.', - 'UPLOAD_ERROR_UNKNOWN' => 'Загрузка не удалась: сервер вернул что-то непонятное!', - 'UPLOAD_ERROR_POSTSIZE' => 'Upload failed. The PHP post_max_size may be too small! Otherwise check the FAQ.', - 'UPLOAD_ERROR_FILESIZE' => 'Upload failed. The PHP upload_max_filesize may be too small! Otherwise check the FAQ.', - 'UPLOAD_IN_PROGRESS' => 'Lychee выполняет выгрузку.', - 'UPLOAD_IMPORT_WARN_ERR' => 'Импорт был завершён, но обнаружены ошибки или предупреждения. Пожалуйста, проверьте лог (Settings -> Логи).', - 'UPLOAD_IMPORT_COMPLETE' => 'Импорт завершён', - 'UPLOAD_IMPORT_INSTR' => 'Укажите прямую ссылку на фото для импорта:', - 'UPLOAD_IMPORT' => 'Импорт', - 'UPLOAD_IMPORT_SERVER' => 'Загрузка с сервера', - 'UPLOAD_IMPORT_SERVER_FOLD' => 'Каталог пуст или не содержит файлов, которые можно обработать. Пожалуйста, проверьте лог (Settings -> Логи).', - 'UPLOAD_IMPORT_SERVER_INSTR' => 'Будет выполнен импорт всех изображений из указанного каталога и всех его подкаталогов, после чего исходные файлы будут удалены, если это возможно.', - 'UPLOAD_ABSOLUTE_PATH' => 'Полный путь к каталогу', - 'UPLOAD_IMPORT_SERVER_EMPT' => 'Не могу импортировать: указанный каталог пуст!', - 'UPLOAD_IMPORT_DELETE_ORIGINALS' => 'Delete originals', - 'UPLOAD_IMPORT_DELETE_ORIGINALS_EXPL' => 'Original files will be deleted after the import when possible.', - 'UPLOAD_IMPORT_VIA_SYMLINK' => 'Symbolic links', - 'UPLOAD_IMPORT_VIA_SYMLINK_EXPL' => 'Import files using symbolic links to originals.', - 'UPLOAD_IMPORT_SKIP_DUPLICATES' => 'Skip duplicates', - 'UPLOAD_IMPORT_SKIP_DUPLICATES_EXPL' => 'Existing media files are skipped.', - 'UPLOAD_IMPORT_RESYNC_METADATA' => 'Re-sync metadata', - 'UPLOAD_IMPORT_RESYNC_METADATA_EXPL' => 'Update metadata of existing media files.', - 'UPLOAD_IMPORT_LOW_MEMORY' => 'Low memory condition!', - 'UPLOAD_IMPORT_LOW_MEMORY_EXPL' => 'The import process on the server is approaching the memory limit and may end up being terminated prematurely.', - 'UPLOAD_WARNING' => 'Warning', - 'UPLOAD_IMPORT_NOT_A_DIRECTORY' => 'The given path is not a readable directory!', - 'UPLOAD_IMPORT_PATH_RESERVED' => 'The given path is a reserved path of Lychee!', - 'UPLOAD_IMPORT_UNREADABLE' => 'Could not read the file!', - 'UPLOAD_IMPORT_FAILED' => 'Could not import the file!', - 'UPLOAD_IMPORT_UNSUPPORTED' => 'Unsupported file type!', - 'UPLOAD_IMPORT_ALBUM_FAILED' => 'Could not create the album!', - 'UPLOAD_IMPORT_CANCELLED' => 'Import cancelled', - - 'ABOUT_SUBTITLE' => 'Self-hosted photo-management done right', - 'ABOUT_DESCRIPTION' => "- это бесплатный фотоменеджер для Вашего сервера или хостинга. Установка занимает считанные секунды. Загружайте, редактируйте и делитесь фотографиями как в любимом приложении! Lychee обеспечит Вас всем необходимым, включая безопасность хранения Ваших фотографий!
На русский язык перевёл Евгений Лебедев. Пожалуйста, дайте мне знать, если заметите неточности.", - 'FOOTER_COPYRIGHT' => 'All images on this website are subject to Copyright by ', - 'HOSTED_WITH_LYCHEE' => 'Hosted with Lychee', - - 'URL_COPY_TO_CLIPBOARD' => 'Copy to clipboard', - 'URL_COPIED_TO_CLIPBOARD' => 'Copied URL to clipboard!', - 'PHOTO_DIRECT_LINKS_TO_IMAGES' => 'Direct links to image files:', - 'PHOTO_MEDIUM' => 'Medium', - 'PHOTO_MEDIUM_HIDPI' => 'Medium HiDPI', - 'PHOTO_SMALL' => 'Thumb', - 'PHOTO_SMALL_HIDPI' => 'Thumb HiDPI', - 'PHOTO_THUMB' => 'Square thumb', - 'PHOTO_THUMB_HIDPI' => 'Square thumb HiDPI', - 'PHOTO_LIVE_VIDEO' => 'Video part of live-photo', - 'PHOTO_VIEW' => 'Lychee Photo View:', - - 'PHOTO_EDIT_ROTATECWISE' => 'Rotate clockwise', - 'PHOTO_EDIT_ROTATECCWISE' => 'Rotate counter-clockwise', - ]; - - return $locale; - } -} diff --git a/app/Locale/Slovak.php b/app/Locale/Slovak.php deleted file mode 100644 index 14ea15f174b..00000000000 --- a/app/Locale/Slovak.php +++ /dev/null @@ -1,482 +0,0 @@ - 'Meno užívateľa', - 'PASSWORD' => 'Heslo', - 'ENTER' => 'Zadať', - 'CANCEL' => 'Prerušiť', - 'SIGN_IN' => 'Prihlásiť', - 'CLOSE' => 'Zatvoriť', - 'SETTINGS' => 'Nastavenia', - 'SEARCH' => 'Hľadaj ...', - 'MORE' => 'Viac', - 'DEFAULT' => 'Default', - - 'USERS' => 'Užívatelia', - 'U2F' => 'U2F', - 'NOTIFICATIONS' => 'Notifications', - 'SHARING' => 'Zdieľanie', - 'CHANGE_LOGIN' => 'Zmena prihlásenia', - 'CHANGE_SORTING' => 'Zmena zoraďovania', - 'SET_DROPBOX' => 'Dropbox nastaviť', - 'ABOUT_LYCHEE' => 'O Lychee', - 'DIAGNOSTICS' => 'Diagnostika', - 'DIAGNOSTICS_GET_SIZE' => 'Request space usage', - 'LOGS' => 'Protokoly', - 'SIGN_OUT' => 'Odhlásiť', - 'UPDATE_AVAILABLE' => 'Update je k dispozícii!', - 'MIGRATION_AVAILABLE' => 'Migration available!', - 'DEFAULT_LICENSE' => 'Predvolená licencia pre nové', - 'SET_LICENSE' => 'Použiť licenciu', - 'SET_OVERLAY_TYPE' => 'Nastaviť typ overlay', - 'SET_MAP_PROVIDER' => 'Set OpenStreetMap tiles provider', - 'SAVE_RISK' => 'Zmeny uložiť, riziko je známe!', - - 'SMART_ALBUMS' => 'Inteligentné albumy', - 'SHARED_ALBUMS' => 'Zdieľané albumy', - 'ALBUMS' => 'Albumy', - 'PHOTOS' => 'Obrázky', - 'SEARCH_RESULTS' => 'Search results', - - 'RENAME' => 'Premenovať', - 'RENAME_ALL' => 'Premenovať vybrané', - 'MERGE' => 'Zlúčiť', - 'MERGE_ALL' => 'Zlúčiť vybrané', - 'MAKE_PUBLIC' => 'Publikovať', - 'SHARE_ALBUM' => 'Album zdieľať', - 'SHARE_PHOTO' => 'Foto zdieľať', - 'VISIBILITY_ALBUM' => 'Album Visibility', - 'VISIBILITY_PHOTO' => 'Photo Visibility', - 'DOWNLOAD_ALBUM' => 'Album stiahnuť', - 'ABOUT_ALBUM' => 'O Albume', - 'DELETE_ALBUM' => 'Album zmazať', - 'MOVE_ALBUM' => 'Album presunúť', - 'FULLSCREEN_ENTER' => 'Celá obrazovka', - 'FULLSCREEN_EXIT' => 'Opustiť celú obrazovku', - - 'SHARING_ALBUM_USERS' => 'Share this album with users', - 'WAIT_FETCH_DATA' => 'Please wait while we get the data...', - 'SHARING_ALBUM_USERS_NO_USERS' => 'There are no users to share the album with', - 'SHARING_ALBUM_USERS_LONG_MESSAGE' => 'Select the users to share this album with', - - 'DELETE_ALBUM_QUESTION' => 'Album a obrázky zmazať', - 'KEEP_ALBUM' => 'Album ponechať', - 'DELETE_ALBUM_CONFIRMATION_1' => 'Ste si istý, že chcete album', - 'DELETE_ALBUM_CONFIRMATION_2' => 'a všetky obrázky v ňom zmazať? Táto akcia je nevratná!', - - 'DELETE_ALBUMS_QUESTION' => 'Všetky albumy a obrázky zmazať', - 'KEEP_ALBUMS' => 'Albumy ponechať', - 'DELETE_ALBUMS_CONFIRMATION_1' => 'Ste si istý, že chcete všetky', - 'DELETE_ALBUMS_CONFIRMATION_2' => 'vybrané albumy a všetky obrázky v nich zmazať? Táto akcia je nevratná!', - - 'DELETE_UNSORTED_CONFIRM' => 'Ste si istý, že chcete všetky obrázky z \'Netriedené\' zmazať?
Táto akcia je nevratná!', - 'CLEAR_UNSORTED' => 'Netriedené zmazať', - 'KEEP_UNSORTED' => 'Netriedené ponechať', - - 'EDIT_SHARING' => 'Upraviť zdieľanie', - 'MAKE_PRIVATE' => 'Súkromné', - - 'CLOSE_ALBUM' => 'Album zavrieť', - 'CLOSE_PHOTO' => 'Foto Zavrieť', - 'CLOSE_MAP' => 'Close Map', - - 'ADD' => 'Pridať', - 'MOVE' => 'Presunúť', - 'MOVE_ALL' => 'Presunúť vybrané', - 'DUPLICATE' => 'Duplikovať', - 'DUPLICATE_ALL' => 'Duplikovať vybrané', - 'COPY_TO' => 'Kopírovať do...', - 'COPY_ALL_TO' => 'Kopírovať vybrané do...', - 'DELETE' => 'Zmazať', - 'DELETE_ALL' => 'Zmazať vybrané', - 'DOWNLOAD' => 'Stiahnuť', - 'DOWNLOAD_ALL' => 'Stiahnuť vybrané', - 'UPLOAD_PHOTO' => 'Foto nahrať', - 'IMPORT_LINK' => 'Importovať z linku', - 'IMPORT_DROPBOX' => 'Importovať z Dropbox', - 'IMPORT_SERVER' => 'Importovať zo servera', - 'NEW_ALBUM' => 'Nový album', - 'NEW_TAG_ALBUM' => 'New Tag Album', - - 'TITLE_NEW_ALBUM' => 'Zadajte názov pre nový album:', - 'UNTITLED' => 'Bez názvu', - 'UNSORTED' => 'Netriedený', - 'STARRED' => 'Obľúbený', - 'RECENT' => 'Naposledy použitý', - 'PUBLIC' => 'Verejný', - 'NUM_PHOTOS' => 'obrázkov', - - 'CREATE_ALBUM' => 'Album vytvoriť', - 'CREATE_TAG_ALBUM' => 'Create Tag Album', - - 'STAR_PHOTO' => 'Obrázok označiť ako obľúbený', - 'STAR' => 'označiť ako obľúbené', - 'STAR_ALL' => 'všetky označiť ako obľúbené', - 'TAGS' => 'Štítky', - 'TAGS_ALL' => 'Štítky pre všetky', - 'UNSTAR_PHOTO' => 'Obrázok odstrániť z obľúbených', - 'SET_COVER' => 'Set Album Cover', - 'REMOVE_COVER' => 'Remove Album Cover', - - 'FULL_PHOTO' => 'Otvoriť originál', - 'ABOUT_PHOTO' => 'O tomto obrázku', - 'DISPLAY_FULL_MAP' => 'Map', - 'DIRECT_LINK' => 'Priamy link', - 'DIRECT_LINKS' => 'Priame linky', - - 'ALBUM_ABOUT' => 'O albume', - 'ALBUM_BASICS' => 'Základné informácie', - 'ALBUM_TITLE' => 'Názov', - 'ALBUM_NEW_TITLE' => 'Zadajte nový názov pre tento album:', - 'ALBUMS_NEW_TITLE_1' => 'Zadajte názov pre všetky', - 'ALBUMS_NEW_TITLE_2' => 'vybrané albumy:', - 'ALBUM_SET_TITLE' => 'Názov uložiť', - 'ALBUM_DESCRIPTION' => 'Popis', - 'ALBUM_SHOW_TAGS' => 'Tags to show', - 'ALBUM_NEW_DESCRIPTION' => 'Zadajte nový popis pre tento album:', - 'ALBUM_SET_DESCRIPTION' => 'Popis uložiť', - 'ALBUM_NEW_SHOWTAGS' => 'Enter tags of photos that will be visible in this album:', - 'ALBUM_SET_SHOWTAGS' => 'Set tags to show', - 'ALBUM_ALBUM' => 'Album', - 'ALBUM_CREATED' => 'Vytvorené', - 'ALBUM_IMAGES' => 'Obrázky', - 'ALBUM_VIDEOS' => 'Videá', - 'ALBUM_SUBALBUMS' => 'Subalbumy', - 'ALBUM_SHARING' => 'Zdieľanie', - 'ALBUM_SHR_YES' => 'Áno', - 'ALBUM_SHR_NO' => 'Nie', - 'ALBUM_PUBLIC' => 'Verejný', - 'ALBUM_PUBLIC_EXPL' => 'Album je viditeľný pre iných, podlieha nasledovným obmedzeniam.', - 'ALBUM_FULL' => 'Originál', - 'ALBUM_FULL_EXPL' => 'K dispozícii aj v plnom rozlíšení.', - 'ALBUM_HIDDEN' => 'Skrytý', - 'ALBUM_HIDDEN_EXPL' => 'Album viditeľný len cez priamy link.', - 'ALBUM_MARK_NSFW' => 'Mark album as sensitive', - 'ALBUM_UNMARK_NSFW' => 'Unmark album as sensitive', - 'ALBUM_NSFW' => 'Sensitive', - 'ALBUM_NSFW_EXPL' => 'Album is marked to contain sensitive content.', - 'ALBUM_DOWNLOADABLE' => 'Stiahnuteľný', - 'ALBUM_DOWNLOADABLE_EXPL' => 'Návštevníci môžu album stiahnuť.', - 'ALBUM_SHARE_BUTTON_VISIBLE' => 'Share button is visible', - 'ALBUM_SHARE_BUTTON_VISIBLE_EXPL' => 'Display social media sharing links.', - 'ALBUM_PASSWORD' => 'Heslo', - 'ALBUM_PASSWORD_PROT' => 'Chránené heslom', - 'ALBUM_PASSWORD_PROT_EXPL' => 'Album viditeľný len s platným heslom.', - 'ALBUM_PASSWORD_REQUIRED' => 'Tento album je chránený heslom. Zadajte heslo:', - 'ALBUM_MERGE_1' => 'Ste si istý, že chcete album', - 'ALBUM_MERGE_2' => 's týmto albumom zlúčiť?', - 'ALBUMS_MERGE' => 'Ste si istý, že chcete všetky vybrané albumy s týmto albumom zlúčiť?', - 'MERGE_ALBUM' => 'Albumy zlúčiť', - 'DONT_MERGE' => 'Nezlučovať', - 'ALBUM_MOVE_1' => 'Ste si istý, že chcete album', - 'ALBUM_MOVE_2' => 'presunúť do nasledujúceho albumu?', - 'ALBUMS_MOVE' => 'Ste si istý, že chcete všetky vybrané albumy presunúť do nasledovného albumu?', - 'MOVE_ALBUMS' => 'Albumy presunúť', - 'NOT_MOVE_ALBUMS' => 'Nepresúvať', - 'ROOT' => 'Albumy', - 'ALBUM_REUSE' => 'Použiť znova', - 'ALBUM_LICENSE' => 'Licencia', - 'ALBUM_SET_LICENSE' => 'Licenciu určiť', - 'ALBUM_LICENSE_HELP' => 'Potrebujete pomoc pri výbere?', - 'ALBUM_LICENSE_NONE' => 'Žiadna', - 'ALBUM_RESERVED' => 'Všetky práva vyhradené', - 'ALBUM_SET_ORDER' => 'Set Order', - 'ALBUM_ORDERING' => 'Order by', - - 'PHOTO_ABOUT' => 'O obrázku', - 'PHOTO_BASICS' => 'Základné informácie', - 'PHOTO_TITLE' => 'Názov', - 'PHOTO_NEW_TITLE' => 'Zadajte nový názov pre tento obrázok:', - 'PHOTO_SET_TITLE' => 'Názov uložiť', - 'PHOTO_UPLOADED' => 'Nahratie', - 'PHOTO_DESCRIPTION' => 'Popis', - 'PHOTO_NEW_DESCRIPTION' => 'Zadajte nový popis pre tento obrázok:', - 'PHOTO_SET_DESCRIPTION' => 'Popis uložiť', - 'PHOTO_NEW_LICENSE' => 'Pridať novú licenciu', - 'PHOTO_SET_LICENSE' => 'Určiť licenciu', - 'PHOTO_LICENSE' => 'Licencia', - 'PHOTO_REUSE' => 'Opakované použitie', - 'PHOTO_LICENSE_NONE' => 'žiadne', - 'PHOTO_RESERVED' => 'Všetky práva vyhradené', - 'PHOTO_LATITUDE' => 'Zemepisná šírka', - 'PHOTO_LONGITUDE' => 'Zemepisná dĺžka', - 'PHOTO_ALTITUDE' => 'Nadmorská výška', - 'PHOTO_IMGDIRECTION' => 'Smer', - 'PHOTO_LOCATION' => 'Location', - 'PHOTO_IMAGE' => 'Obrázok', - 'PHOTO_VIDEO' => 'Video', - 'PHOTO_SIZE' => 'Veľkosť', - 'PHOTO_FORMAT' => 'Formát', - 'PHOTO_RESOLUTION' => 'Rozlíšenie', - 'PHOTO_DURATION' => 'Trvanie', - 'PHOTO_FPS' => 'Počet snímkov/s', - 'PHOTO_TAGS' => 'Štítky', - 'PHOTO_NOTAGS' => 'Žiadne štítky', - 'PHOTO_NEW_TAGS' => 'Zadajte štítky pre tento obrázok. Jednotlivé štítky oddeľte čiarkou:', - 'PHOTO_NEW_TAGS_1' => 'Zadajte štítky pre všetky', - 'PHOTO_NEW_TAGS_2' => 'vybrané obrázky. Doterajšie štítky budú prepísané.Jednotlivé štítky oddeľte čiarkou:', - 'PHOTO_SET_TAGS' => 'Štítky uložiť', - 'PHOTO_CAMERA' => 'Kamera', - 'PHOTO_CAPTURED' => 'Zosnímané', - 'PHOTO_MAKE' => 'Značka', - 'PHOTO_TYPE' => 'Typ/Model', - 'PHOTO_LENS' => 'Objektív', - 'PHOTO_SHUTTER' => 'Uzávierka', - 'PHOTO_APERTURE' => 'Clona', - 'PHOTO_FOCAL' => 'Fokus', - 'PHOTO_ISO' => 'ISO', - 'PHOTO_SHARING' => 'Zdieľať', - 'PHOTO_SHR_PLUBLIC' => 'Verejný', - 'PHOTO_SHR_ALB' => 'Ano (album)', - 'PHOTO_SHR_PHT' => 'Ano (obrázok)', - 'PHOTO_SHR_NO' => 'Nie', - 'PHOTO_DELETE' => 'Zmazať obrázok', - 'PHOTO_KEEP' => 'Obrázok ponechať', - 'PHOTO_DELETE_1' => 'Ste si istý, že chcete obrázok', - 'PHOTO_DELETE_2' => 'zmazať? Táto akcia je nevratná!', - 'PHOTO_DELETE_ALL_1' => 'Ste si istý, že chcete všetky', - 'PHOTO_DELETE_ALL_2' => 'vybrané obrázky zmazať? Táto akcia je nevratná!', - 'PHOTOS_NEW_TITLE_1' => 'Zadaje nový názov pre všetky', - 'PHOTOS_NEW_TITLE_2' => 'vybrané obrázky:', - 'PHOTO_MAKE_PRIVATE_ALBUM' => 'Tento obrázok sa nachádza vo verejnom albume. Označenie obrázku ako verejný alebo súkromný musíte nastaviť na albume, v ktorom sa nachádza.', - 'PHOTO_SHOW_ALBUM' => 'Album zobraziť', - 'PHOTO_PUBLIC' => 'Verejný', - 'PHOTO_PUBLIC_EXPL' => 'Obrázok viditeľný iným, podlieha nasledovným obmedzeniam.', - 'PHOTO_FULL' => 'Originál', - 'PHOTO_FULL_EXPL' => 'K dispozícii je obrázok v plnom rozlíšení.', - 'PHOTO_HIDDEN' => 'Skrytý', - 'PHOTO_HIDDEN_EXPL' => 'Obrázok viditeľný len pomocou priameho linku.', - 'PHOTO_DOWNLOADABLE' => 'Stiahnuteľný', - 'PHOTO_DOWNLOADABLE_EXPL' => 'Návšetvníci vašej galérie môžu tento obrázok stiahnuť.', - 'PHOTO_SHARE_BUTTON_VISIBLE' => 'Share button is visible', - 'PHOTO_SHARE_BUTTON_VISIBLE_EXPL' => 'Display social media sharing links.', - 'PHOTO_PASSWORD_PROT' => 'Chránené heslom', - 'PHOTO_PASSWORD_PROT_EXPL' => 'Obrázok dostupný len s platným heslom.', - 'PHOTO_EDIT_SHARING_TEXT' => 'Vlastnosti zdieľania tejto fotografie sa zmenia na nasledujúce:', - 'PHOTO_NO_EDIT_SHARING_TEXT' => 'Pretože je táto fotografia umiestnená vo verejnom albume, zdedí nastavenie viditeľnosti daného albumu. Jeho aktuálna viditeľnosť je uvedená len na informačné účely.', - 'PHOTO_EDIT_GLOBAL_SHARING_TEXT' => 'Viditeľnosť tejto fotografie je možné doladiť pomocou globálnych nastavení. Jeho aktuálna viditeľnosť je uvedená len na informačné účely.', - 'PHOTO_SHARING_CONFIRM' => 'Uložiť', - - 'LOADING' => 'Nahráva sa', - 'ERROR' => 'Chyba', - 'ERROR_TEXT' => 'Asi sa niečo pokazilo. Obnovte stránku a skúste znova!', - 'ERROR_DB_1' => 'Nedá sa pripojiť na databázu, prístup zamietnutý. Preverte nastavenie Host, užívateľské meno a heslo a overte si prístup k databáze zo súčasnej lokality.', - 'ERROR_DB_2' => 'Nedá sa vytvoriť databáza. Preverte nastavenie Host, užívateľské meno a heslo a overte si, že daný užívateľ má právo modifikovať databázu.', - 'ERROR_CONFIG_FILE' => "Túto konfiguráciu nemožno uložiť. Prístup zamietnutý k 'data/'. Nastavte práva zápisu na 'data/' a 'uploads/'. Ďalšie informácie nájdete v súbore README.", - 'ERROR_UNKNOWN' => 'Vyskytla sa neočakávaná chyba. Skúste to znova a skontrolujte inštaláciu na vašom serveri. Ďalšie informácie nájdete v súbore README.', - 'ERROR_LOGIN' => 'Nemožno uložiť Login. Skúste to s iným menom a heslom!', - 'ERROR_MAP_DEACTIVATED' => 'Map functionality has been deactivated under settings.', - 'ERROR_SEARCH_DEACTIVATED' => 'Search functionality has been deactivated under settings.', - 'SUCCESS' => 'OK', - 'RETRY' => 'Opakovať', - - 'SETTINGS_WARNING' => 'Zmena rozšírených nastavení môže mať negatívny vplyv na stabilitu, bezpečnosť a rýchlosť tejto aplikácie. Upravujte len to, o čom presne viete, čo robíte.', - 'SETTINGS_SUCCESS_LOGIN' => 'Užívateľské dáta aktualizované', - 'SETTINGS_SUCCESS_SORT' => 'Triedenie aktualizované', - 'SETTINGS_SUCCESS_DROPBOX' => 'Kľúč Dropbox aktualizovaný', - 'SETTINGS_SUCCESS_LANG' => 'Jazyk aktualizovaný', - 'SETTINGS_SUCCESS_LAYOUT' => 'Layout aktualizovaný', - 'SETTINGS_SUCCESS_IMAGE_OVERLAY' => 'EXIF-Overlay nastavenia aktualizované', - 'SETTINGS_SUCCESS_PUBLIC_SEARCH' => 'Verejné vyhľadávanie bolo aktualizované', - 'SETTINGS_SUCCESS_LICENSE' => 'Prednastavená licencia aktualizovaná', - 'SETTINGS_SUCCESS_CSS' => 'CSS aktualizované', - 'SETTINGS_SUCCESS_UPDATE' => 'Nastavenia úspešne aktualizované', - 'SETTINGS_SUCCESS_MAP_DISPLAY' => 'Nastavenie zobrazenia mapy aktualizované', - 'SETTINGS_SUCCESS_MAP_DISPLAY_PUBLIC' => 'Map display settings for public albums updated', - 'SETTINGS_SUCCESS_MAP_PROVIDER' => 'Map provider settings updated', - - 'U2F_NOT_SUPPORTED' => 'U2F not supported. Sorry.', - 'U2F_NOT_SECURE' => 'Environment not secured. U2F not available.', - 'U2F_REGISTER_KEY' => 'Register new device.', - 'U2F_REGISTRATION_SUCCESS' => 'Registration successful!', - 'U2F_AUTHENTIFICATION_SUCCESS' => 'Authentication successful!', - 'U2F_CREDENTIALS' => 'Credentials', - 'U2F_CREDENTIALS_DELETED' => 'Credentials deleted!', - - 'NEW_PHOTOS_NOTIFICATION' => 'Send new photos notification emails.', - 'SETTINGS_SUCCESS_NEW_PHOTOS_NOTIFICATION' => 'New photos notification updated', - 'USER_EMAIL_INSTRUCTION' => 'Add your email below to enable receiving email notifications.
To stop receiving emails, simply remove your email below.', - - 'DB_INFO_TITLE' => 'Zadajte prístupové údaje k databáze:', - 'DB_INFO_HOST' => 'Názov databázového servera (voliteľné)', - 'DB_INFO_USER' => 'Užívateľ databázy', - 'DB_INFO_PASSWORD' => 'Heslo databázy', - 'DB_INFO_TEXT' => 'Lychee nastaví vlastnú databázu. Pokiaľ je potrebné, možno zadať názov existujúcej databázy:', - 'DB_NAME' => 'Názov databázy (voliteľné)', - 'DB_PREFIX' => 'Tabuľkový prefix (voliteľný)', - 'DB_CONNECT' => 'Pripojiť', - - 'LOGIN_TITLE' => 'Zadajte meno a heslo pre vašu inštaláciu:', - 'LOGIN_USERNAME' => 'Nový užívateľ', - 'LOGIN_PASSWORD' => 'Nové heslo', - 'LOGIN_PASSWORD_CONFIRM' => 'Heslo potvrdiť', - 'LOGIN_CREATE' => 'Založiť užívateľa', - - 'PASSWORD_TITLE' => 'Zadajte vaše heslo:', - 'USERNAME_CURRENT' => 'Vaše pôvodné meno', - 'PASSWORD_CURRENT' => 'Vaše pôvodné heslo', - 'PASSWORD_TEXT' => 'Vaše meno a heslo bolo zmenené nasledovne:', - 'PASSWORD_CHANGE' => 'Prihlásenie zmeniť', - - 'EDIT_SHARING_TITLE' => 'Zdieľanie spracovať', - 'EDIT_SHARING_TEXT' => 'Nastavenie zdieľania pre tento album bolo zmenené nasledovne:', - 'SHARE_ALBUM_TEXT' => 'Tento album bude zdieľaný s nasledovnými vlastnosťami:', - 'ALBUM_SHARING_CONFIRM' => 'Save', - - 'SORT_ALBUM_BY_1' => 'Triediť albumy podľa', - 'SORT_ALBUM_BY_2' => 'v', - 'SORT_ALBUM_BY_3' => 'rade.', - - 'SORT_ALBUM_SELECT_1' => 'Čas vytvorenia', - 'SORT_ALBUM_SELECT_2' => 'Titul', - 'SORT_ALBUM_SELECT_3' => 'Popis', - 'SORT_ALBUM_SELECT_4' => 'Verejný', - 'SORT_ALBUM_SELECT_5' => 'Najnovšia zmena', - 'SORT_ALBUM_SELECT_6' => 'Najstaršia zmena', - - 'SORT_PHOTO_BY_1' => 'Obrázky triediť podľa', - 'SORT_PHOTO_BY_2' => 'v', - 'SORT_PHOTO_BY_3' => 'rade.', - - 'SORT_PHOTO_SELECT_1' => 'Čas nahratia', - 'SORT_PHOTO_SELECT_2' => 'čas snímku', - 'SORT_PHOTO_SELECT_3' => 'Titul', - 'SORT_PHOTO_SELECT_4' => 'Popis', - 'SORT_PHOTO_SELECT_5' => 'Verejný', - 'SORT_PHOTO_SELECT_6' => 'Obľúbený', - 'SORT_PHOTO_SELECT_7' => 'Formát', - - 'SORT_ASCENDING' => 'vzostupnej', - 'SORT_DESCENDING' => 'zostupnej', - 'SORT_CHANGE' => 'Zmeniť triedenie', - - 'DROPBOX_TITLE' => 'Nastaviť kľúč Dropbox', - 'DROPBOX_TEXT' => "Aby ste mohli importovať obrázky z Dropbox, potrebujete platný API-Key zo stránky Dropbox. Vytvorte personal key a zadajte ho:", - - 'LANG_TEXT' => 'Zmeniť jazyk Lychee na:', - 'LANG_TITLE' => 'Zmena jazyka', - - 'CSS_TEXT' => 'Vlastné CSS:', - 'CSS_TITLE' => 'CSS zmeniť', - 'PUBLIC_SEARCH_TEXT' => 'Verejné vyhľadávanie povolené:', - 'OVERLAY_TYPE' => 'Dáta použité pre overlay:', - 'OVERLAY_NONE' => 'None', - 'OVERLAY_EXIF' => 'EXIF dáta obrázku', - 'OVERLAY_DESCRIPTION' => 'Popis obrázku', - 'OVERLAY_DATE' => 'Obrázok snímaný dňa', - 'MAP_DISPLAY_TEXT' => 'Enable maps (provided by OpenStreetMap):', - 'MAP_DISPLAY_PUBLIC_TEXT' => 'Enable maps for public albums (provided by OpenStreetMap):', - 'MAP_PROVIDER' => 'Provider of OpenStreetMap tiles:', - 'MAP_PROVIDER_WIKIMEDIA' => 'Wikimedia', - 'MAP_PROVIDER_OSM_ORG' => 'OpenStreetMap.org (no HiDPI)', - 'MAP_PROVIDER_OSM_DE' => 'OpenStreetMap.de (no HiDPI)', - 'MAP_PROVIDER_OSM_FR' => 'OpenStreetMap.fr (no HiDPI)', - 'MAP_PROVIDER_RRZE' => 'University of Erlangen, Germany (only HiDPI)', - 'MAP_INCLUDE_SUBALBUMS_TEXT' => 'Include photos of subalbums on map:', - 'LOCATION_DECODING' => 'Decode GPS data into location name', - 'LOCATION_SHOW' => 'Show location name', - 'LOCATION_SHOW_PUBLIC' => 'Show location name for public mode', - 'LAYOUT_TYPE' => 'Rozmiestnenie obrázkov:', - 'LAYOUT_SQUARES' => 'Štvorcové náhľady', - 'LAYOUT_JUSTIFIED' => 'Zachovaný pomer strán, zarovnané', - 'LAYOUT_UNJUSTIFIED' => 'Zachovaný pomer strán, nezarovnané', - 'SET_LAYOUT' => 'Zmeniť rozmiestnenie', - - 'NSFW_VISIBLE_TEXT_1' => 'Make Sensitive albums visible by default.', - 'NSFW_VISIBLE_TEXT_2' => 'If the album is public, it is still accessible, just hidden from the view and can be revealed by pressing H.', - 'SETTINGS_SUCCESS_NSFW_VISIBLE' => 'Default sensitive album visibility updated with success.', - - 'VIEW_NO_RESULT' => 'Žiadny výsledok', - 'VIEW_NO_PUBLIC_ALBUMS' => 'Žiadne verejné albumy', - 'VIEW_NO_CONFIGURATION' => 'Žiadna konfigurácia', - 'VIEW_PHOTO_NOT_FOUND' => 'Žiadny obrázok', - - 'NO_TAGS' => 'Žiadne štítky', - - 'UPLOAD_MANAGE_NEW_PHOTOS' => 'Môžete spravovať vaše nové obrázky.', - 'UPLOAD_COMPLETE' => 'Nahrávanie ukončené', - 'UPLOAD_COMPLETE_FAILED' => 'Chyba pri nahrávaní jedného alebo viacerých obrázkov.', - 'UPLOAD_IMPORTING' => 'Importovať', - 'UPLOAD_IMPORTING_URL' => 'Importovať URL', - 'UPLOAD_UPLOADING' => 'Nahrať', - 'UPLOAD_FINISHED' => 'Ukončené', - 'UPLOAD_PROCESSING' => 'Spracováva sa', - 'UPLOAD_FAILED' => 'Zlyhanie', - 'UPLOAD_FAILED_ERROR' => 'Nahrávanie zlyhalo. Server ohlásil chybu!', - 'UPLOAD_FAILED_WARNING' => 'Nahrávanie zlyhalo. Server ohlásil varovanie!', - 'UPLOAD_CANCELLED' => 'Cancelled', - 'UPLOAD_SKIPPED' => 'Preskočiť', - 'UPLOAD_UPDATED' => 'Updated', - 'UPLOAD_IMPORT_SKIPPED_DUPLICATE' => 'This photo has been skipped because it\'s already in your library.', - 'UPLOAD_IMPORT_RESYNCED_DUPLICATE' => 'This photo has been skipped because it\'s already in your library, but its metadata has been updated.', - 'UPLOAD_ERROR_CONSOLE' => 'Skontrolujte konzolu prehliadača, pre zistenie ďalších podrobností.', - 'UPLOAD_UNKNOWN' => 'Server vrátil neznámu odpoveď.Skontrolujte konzolu prehliadača, pre zistenie ďalších podrobností.', - 'UPLOAD_ERROR_UNKNOWN' => 'Nahrávanie zlyhalo. Server ohlásil neznámu chybu!', - 'UPLOAD_ERROR_POSTSIZE' => 'Upload failed. The PHP post_max_size may be too small! Otherwise check the FAQ.', - 'UPLOAD_ERROR_FILESIZE' => 'Upload failed. The PHP upload_max_filesize may be too small! Otherwise check the FAQ.', - 'UPLOAD_IN_PROGRESS' => 'Lychee práve nahráva!', - 'UPLOAD_IMPORT_WARN_ERR' => 'Import je hotový, vyskytli sa ale chyby alebo varovania. Skontrolujte protokoly (Nastavenia/ Protokoly).', - 'UPLOAD_IMPORT_COMPLETE' => 'Import hotový', - 'UPLOAD_IMPORT_INSTR' => 'Pre import zadajte priamy link:', - 'UPLOAD_IMPORT' => 'Import', - 'UPLOAD_IMPORT_SERVER' => 'Import zo servera', - 'UPLOAD_IMPORT_SERVER_FOLD' => 'Priečinok je prázdny alebo obsahuje nečitateľný obsah pre spracovanie. Skontrolujte protokoly (Nastavenia/ Protokoly) pre zistenie ďalších podrobností.', - 'UPLOAD_IMPORT_SERVER_INSTR' => 'Táto akcia naimportuje všetky obrázky, priečinky a podpriečinky, ktoré sa v danom adresári nachádzajú.', - 'UPLOAD_ABSOLUTE_PATH' => 'Absolútna cesta k adresáru', - 'UPLOAD_IMPORT_SERVER_EMPT' => 'Import sa nedá spustiť, priečinok je prázdny.', - 'UPLOAD_IMPORT_DELETE_ORIGINALS' => 'Zmazať originály', - 'UPLOAD_IMPORT_DELETE_ORIGINALS_EXPL' => 'Ak je možné, budú pôvodné súbory po importe zmazané.', - 'UPLOAD_IMPORT_VIA_SYMLINK' => 'Symbolic links', - 'UPLOAD_IMPORT_VIA_SYMLINK_EXPL' => 'Import files using symbolic links to originals.', - 'UPLOAD_IMPORT_SKIP_DUPLICATES' => 'Skip duplicates', - 'UPLOAD_IMPORT_SKIP_DUPLICATES_EXPL' => 'Existing media files are skipped.', - 'UPLOAD_IMPORT_RESYNC_METADATA' => 'Re-sync metadata', - 'UPLOAD_IMPORT_RESYNC_METADATA_EXPL' => 'Update metadata of existing media files.', - 'UPLOAD_IMPORT_LOW_MEMORY' => 'Málo pamäte!', - 'UPLOAD_IMPORT_LOW_MEMORY_EXPL' => 'Proces importu na serveri sa blíži k limitu pamäte a môže skončiť predčasným ukončením.', - 'UPLOAD_WARNING' => 'Varovanie', - 'UPLOAD_IMPORT_NOT_A_DIRECTORY' => 'Adresaár nie je čitateľný!', - 'UPLOAD_IMPORT_PATH_RESERVED' => 'Zadaná cesta je rezervovaná pre Lychee!', - 'UPLOAD_IMPORT_UNREADABLE' => 'Súbor sa nedá čítať!', - 'UPLOAD_IMPORT_FAILED' => 'Súbor sa nedá naimportovať!', - 'UPLOAD_IMPORT_UNSUPPORTED' => 'Nepodporovaný typ súboru!', - 'UPLOAD_IMPORT_ALBUM_FAILED' => 'Nemožno vytvoriť album!', - 'UPLOAD_IMPORT_CANCELLED' => 'Import cancelled', - - 'ABOUT_SUBTITLE' => 'Vlastný hostovaný manažment obrázkov!', - 'ABOUT_DESCRIPTION' => 'je open-source nástroj, bežiaci na vašom vlastnom serveri alebo v cloude. Inštalácia je otázkou sekúnd. Nahrať, spravovať a zdieľať obrázky ako v natívnej aplikácii. Lychee ponúka všetko čo potrebujete vy a vaše obrázky pre bezpečné uloženie.', - 'FOOTER_COPYRIGHT' => 'Všetky obrázky na tejto webovej stránke sú chránené autorským právom ', - 'HOSTED_WITH_LYCHEE' => 'Hostované s Lychee', - - 'URL_COPY_TO_CLIPBOARD' => 'Skopírovať do schránky', - 'URL_COPIED_TO_CLIPBOARD' => 'URL skopírované do schránky!', - 'PHOTO_DIRECT_LINKS_TO_IMAGES' => 'Priame linky k súborom obrázkov:', - 'PHOTO_MEDIUM' => 'Medium', - 'PHOTO_MEDIUM_HIDPI' => 'Medium HiDPI', - 'PHOTO_SMALL' => 'Náhľad', - 'PHOTO_SMALL_HIDPI' => 'Náhľad HiDPI', - 'PHOTO_THUMB' => 'Štvorcový náhľad', - 'PHOTO_THUMB_HIDPI' => 'Štvorcový náhľad HiDPI', - 'PHOTO_LIVE_VIDEO' => 'Video part of live-photo', - 'PHOTO_VIEW' => 'Zobrazenie foto Lychee:', - - 'PHOTO_EDIT_ROTATECWISE' => 'Rotate clockwise', - 'PHOTO_EDIT_ROTATECCWISE' => 'Rotate counter-clockwise', - ]; - - return $locale; - } -} diff --git a/app/Locale/Spanish.php b/app/Locale/Spanish.php deleted file mode 100644 index 5d41ed844b8..00000000000 --- a/app/Locale/Spanish.php +++ /dev/null @@ -1,475 +0,0 @@ - 'nombre de usuario', - 'PASSWORD' => 'contraseña', - 'ENTER' => 'Entrar', - 'CANCEL' => 'Cancelar', - 'SIGN_IN' => 'Iniciar sesión', - 'CLOSE' => 'Cerrar', - 'SETTINGS' => 'Configuraciones', - 'SEARCH' => 'Buscar...', - 'MORE' => 'Más', - 'DEFAULT' => 'Default', - - 'USERS' => 'Usuarios', - 'U2F' => 'U2F', - 'NOTIFICATIONS' => 'Notifications', - 'SHARING' => 'Compartir', - 'CHANGE_LOGIN' => 'Cambiar inicio de sesión', - 'CHANGE_SORTING' => 'Cambiar clasificación', - 'SET_DROPBOX' => 'Establecer Dropbox', - 'ABOUT_LYCHEE' => 'Acerca de Lychee', - 'DIAGNOSTICS' => 'Diagnóstico', - 'DIAGNOSTICS_GET_SIZE' => 'Request space usage', - 'LOGS' => 'Mostrar Registros', - 'SIGN_OUT' => 'Cerrar Sesión', - 'UPDATE_AVAILABLE' => '¡Actualización disponible!', - 'MIGRATION_AVAILABLE' => 'Migration available!', - 'DEFAULT_LICENSE' => 'Licencia predeterminada para nuevas cargas:', - 'SET_LICENSE' => 'Establecer Licencia', - 'SET_OVERLAY_TYPE' => 'Establecer Superposición', - 'SET_MAP_PROVIDER' => 'Set OpenStreetMap tiles provider', - - 'SMART_ALBUMS' => 'Álbumes inteligentes', - 'SHARED_ALBUMS' => 'Álbumes compartidos', - 'ALBUMS' => 'Álbumes', - 'PHOTOS' => 'Imágenes', - 'SEARCH_RESULTS' => 'Search results', - - 'RENAME' => 'Renombrar', - 'RENAME_ALL' => 'Renombrar Todo', - 'MERGE' => 'Unir', - 'MERGE_ALL' => 'Unir Todo', - 'MAKE_PUBLIC' => 'Hacer Público', - 'SHARE_ALBUM' => 'Compartir Álbum', - 'SHARE_PHOTO' => 'Compartir Foto', - 'VISIBILITY_ALBUM' => 'Visibilidad del Álbum', - 'VISIBILITY_PHOTO' => 'Visibilidad de la Foto', - 'DOWNLOAD_ALBUM' => 'Descargar Álbum', - 'ABOUT_ALBUM' => 'Acerca del Álbum', - 'DELETE_ALBUM' => 'Eliminar Álbum', - 'MOVE_ALBUM' => 'Mover Álbum', - 'FULLSCREEN_ENTER' => 'Ingreser a pantalla completa', - 'FULLSCREEN_EXIT' => 'Salir de pantalla completa', - - 'SHARING_ALBUM_USERS' => 'Share this album with users', - 'WAIT_FETCH_DATA' => 'Please wait while we get the data...', - 'SHARING_ALBUM_USERS_NO_USERS' => 'There are no users to share the album with', - 'SHARING_ALBUM_USERS_LONG_MESSAGE' => 'Select the users to share this album with', - - 'DELETE_ALBUM_QUESTION' => 'Eliminar Álbum y Fotos', - 'KEEP_ALBUM' => 'Mantener Álbum', - 'DELETE_ALBUM_CONFIRMATION_1' => '¿Estás seguro de que deseas eliminar el álbum?', - 'DELETE_ALBUM_CONFIRMATION_2' => '¿Ha seleccionado un álbum y todas las fotos que contiene? ¡Esta acción no se puede deshacer!', - - 'DELETE_ALBUMS_QUESTION' => 'Eliminar Álbumes y Fotos', - 'KEEP_ALBUMS' => 'Mantener Álbumes', - 'DELETE_ALBUMS_CONFIRMATION_1' => '¿Está seguro de que desea eliminar todo', - 'DELETE_ALBUMS_CONFIRMATION_2' => 'Ha seleccionado álbumes y todas las fotos que contienen? ¡Esta acción no se puede deshacer!', - - 'DELETE_UNSORTED_CONFIRM' => '¿Estás seguro de que deseas eliminar todas las fotos de \'Sin clasificar\'?
¡Esta acción no se puede deshacer!', - 'CLEAR_UNSORTED' => 'Borrar \'Sin Clasificar\'', - 'KEEP_UNSORTED' => 'Mantener \'Sin Clasificar\'', - - 'EDIT_SHARING' => 'Editar Compartido', - 'MAKE_PRIVATE' => 'Hazlo Privado', - - 'CLOSE_ALBUM' => 'Cerrar Álbum', - 'CLOSE_PHOTO' => 'Cerrar Foto', - 'CLOSE_MAP' => 'Cerrar Mapa', - - 'ADD' => 'Añadir', - 'MOVE' => 'Mover', - 'MOVE_ALL' => 'Muever Todo', - 'DUPLICATE' => 'Duplicar', - 'DUPLICATE_ALL' => 'Duplicar Todo', - 'COPY_TO' => 'Copiar a...', - 'COPY_ALL_TO' => 'Copiar Todo a ...', - 'DELETE' => 'Eliminar', - 'DELETE_ALL' => 'Eliminar Todos', - 'DOWNLOAD' => 'Descargar', - 'DOWNLOAD_ALL' => 'Descargar Seleccionados', - 'UPLOAD_PHOTO' => 'Subir Foto', - 'IMPORT_LINK' => 'Importar desde Enlace', - 'IMPORT_DROPBOX' => 'Importar desde Dropbox', - 'IMPORT_SERVER' => 'Importar desde Servidor', - 'NEW_ALBUM' => 'Nuevo Álbum', - 'NEW_TAG_ALBUM' => 'New Tag Album', - - 'TITLE_NEW_ALBUM' => 'Ingrese un título para el nuevo álbum:', - 'UNTITLED' => 'Sin Título', - 'UNSORTED' => 'Sin Clasificar', - 'STARRED' => 'Destacado', - 'RECENT' => 'Reciente', - 'PUBLIC' => 'Público', - 'NUM_PHOTOS' => 'Fotos', - - 'CREATE_ALBUM' => 'Crear Álbum', - 'CREATE_TAG_ALBUM' => 'Create Tag Album', - - 'STAR_PHOTO' => 'Destacar Photo', - 'STAR' => 'Destacar', - 'STAR_ALL' => 'Destacar Todo', - 'TAGS' => 'Etiquetar', - 'TAGS_ALL' => 'Etiquetar Todo', - 'UNSTAR_PHOTO' => 'Desetiquetar Foto', - 'SET_COVER' => 'Set Album Cover', - 'REMOVE_COVER' => 'Remove Album Cover', - - 'FULL_PHOTO' => 'Foto Completa', - 'ABOUT_PHOTO' => 'Acerca de la Foto', - 'DISPLAY_FULL_MAP' => 'Mapa', - 'DIRECT_LINK' => 'Enlace Directo', - 'DIRECT_LINKS' => 'Enlaces Directos', - - 'ALBUM_ABOUT' => 'Acerca de', - 'ALBUM_BASICS' => 'Basico', - 'ALBUM_TITLE' => 'Título', - 'ALBUM_NEW_TITLE' => 'Ingrese un nuevo título para este álbum:', - 'ALBUMS_NEW_TITLE_1' => 'Ingrese un título para todos', - 'ALBUMS_NEW_TITLE_2' => 'álbumes seleccionados:', - 'ALBUM_SET_TITLE' => 'Establecer Título', - 'ALBUM_DESCRIPTION' => 'Descripción', - 'ALBUM_SHOW_TAGS' => 'Tags to show', - 'ALBUM_NEW_DESCRIPTION' => 'Ingrese una nueva descripción para este álbum:', - 'ALBUM_SET_DESCRIPTION' => 'Establecer Descripción', - 'ALBUM_NEW_SHOWTAGS' => 'Enter tags of photos that will be visible in this album:', - 'ALBUM_SET_SHOWTAGS' => 'Set tags to show', - 'ALBUM_ALBUM' => 'Álbum', - 'ALBUM_CREATED' => 'Creado', - 'ALBUM_IMAGES' => 'Imágenes', - 'ALBUM_VIDEOS' => 'Videos', - 'ALBUM_SUBALBUMS' => 'Subalbums', - 'ALBUM_SHARING' => 'Compartir', - 'ALBUM_SHR_YES' => 'SI', - 'ALBUM_SHR_NO' => 'No', - 'ALBUM_PUBLIC' => 'Público', - 'ALBUM_PUBLIC_EXPL' => 'Otros pueden ver el álbum, sujeto a las restricciones a continuación.', - 'ALBUM_FULL' => 'Original', - 'ALBUM_FULL_EXPL' => 'Imágenes de resolución completa disponibles', - 'ALBUM_HIDDEN' => 'Oculto', - 'ALBUM_HIDDEN_EXPL' => 'Solo las personas con el enlace directo pueden ver este álbum.', - 'ALBUM_MARK_NSFW' => 'Mark album as sensitive', - 'ALBUM_UNMARK_NSFW' => 'Unmark album as sensitive', - 'ALBUM_NSFW' => 'Sensitive', - 'ALBUM_NSFW_EXPL' => 'Album is marked to contain sensitive content.', - 'ALBUM_DOWNLOADABLE' => 'Descargable', - 'ALBUM_DOWNLOADABLE_EXPL' => 'Los visitantes de su Lychee pueden descargar este álbum.', - 'ALBUM_SHARE_BUTTON_VISIBLE' => 'Share button is visible', - 'ALBUM_SHARE_BUTTON_VISIBLE_EXPL' => 'Display social media sharing links.', - 'ALBUM_PASSWORD' => 'Contraseña', - 'ALBUM_PASSWORD_PROT' => 'Contraseña protegida', - 'ALBUM_PASSWORD_PROT_EXPL' => 'Álbum solo accesible con una contraseña válida', - 'ALBUM_PASSWORD_REQUIRED' => 'Este álbum está protegido por una contraseña. Ingrese la contraseña a continuación para ver las fotos de este álbum:', - 'ALBUM_MERGE_1' => '¿Estás seguro de que quieres fusionar el álbum', - 'ALBUM_MERGE_2' => 'en el álbum', - 'ALBUMS_MERGE' => '¿Está seguro de que desea fusionar todos los álbumes seleccionados en el álbum', - 'MERGE_ALBUM' => 'Fusionar álbumes', - 'DONT_MERGE' => 'No combinar', - 'ALBUM_MOVE_1' => '¿Estás seguro de que quieres mover el álbum', - 'ALBUM_MOVE_2' => 'detro del álbum', - 'ALBUMS_MOVE' => '¿Está seguro de que desea mover todos los álbumes seleccionados al álbum', - 'MOVE_ALBUMS' => 'Mover álbumes', - 'NOT_MOVE_ALBUMS' => 'No te muevas', - 'ROOT' => 'Inicio', - 'ALBUM_REUSE' => 'Reutilizar', - 'ALBUM_LICENSE' => 'Licencia', - 'ALBUM_SET_LICENSE' => 'Establecer licencia', - 'ALBUM_LICENSE_HELP' => '¿Necesitas ayuda para elegir?', - 'ALBUM_LICENSE_NONE' => 'Ninguna', - 'ALBUM_RESERVED' => 'Todos los derechos reservados', - 'ALBUM_SET_ORDER' => 'Set Order', - 'ALBUM_ORDERING' => 'Order by', - - 'PHOTO_ABOUT' => 'Acerca de', - 'PHOTO_BASICS' => 'Basico', - 'PHOTO_TITLE' => 'Título', - 'PHOTO_NEW_TITLE' => 'Ingrese un nuevo título para esta foto:', - 'PHOTO_SET_TITLE' => 'Establecer título', - 'PHOTO_UPLOADED' => 'Subido', - 'PHOTO_DESCRIPTION' => 'Descripción', - 'PHOTO_NEW_DESCRIPTION' => 'Ingrese una nueva descripción para esta foto:', - 'PHOTO_SET_DESCRIPTION' => 'Establecer descripción', - 'PHOTO_NEW_LICENSE' => 'Agregar una licencia', - 'PHOTO_SET_LICENSE' => 'Establecer licencia', - 'PHOTO_LICENSE' => 'Licencia', - 'PHOTO_REUSE' => 'Reutilizar', - 'PHOTO_LICENSE_NONE' => 'Ninguna', - 'PHOTO_RESERVED' => 'Todos los derechos reservados', - 'PHOTO_LATITUDE' => 'Latitud', - 'PHOTO_LONGITUDE' => 'Longitud', - 'PHOTO_ALTITUDE' => 'Altitud', - 'PHOTO_IMGDIRECTION' => 'Dirección', - 'PHOTO_LOCATION' => 'Location', - 'PHOTO_IMAGE' => 'Imagen', - 'PHOTO_VIDEO' => 'Vídeo', - 'PHOTO_SIZE' => 'Tamaño', - 'PHOTO_FORMAT' => 'Formato', - 'PHOTO_RESOLUTION' => 'Resolución', - 'PHOTO_DURATION' => 'Duración', - 'PHOTO_FPS' => 'Cuadros por segundo', - 'PHOTO_TAGS' => 'Etiquetas', - 'PHOTO_NOTAGS' => 'Sin etiquetas', - 'PHOTO_NEW_TAGS' => 'Ingrese sus etiquetas para esta foto. Puede agregar varias etiquetas separándolas con una coma:', - 'PHOTO_NEW_TAGS_1' => 'Ingrese sus etiquetas para todos', - 'PHOTO_NEW_TAGS_2' => 'fotos seleccionadas. Las etiquetas existentes se sobrescribirán. Puede agregar varias etiquetas separándolas con una coma:', - 'PHOTO_SET_TAGS' => 'Establecer etiquetas', - 'PHOTO_CAMERA' => 'Cámara', - 'PHOTO_CAPTURED' => 'Capturado', - 'PHOTO_MAKE' => 'Hacer', - 'PHOTO_TYPE' => 'Tipo / Modelo', - 'PHOTO_LENS' => 'Lens', - 'PHOTO_SHUTTER' => 'Velocidad de obturación', - 'PHOTO_APERTURE' => 'Abertura', - 'PHOTO_FOCAL' => 'Longitud focal', - 'PHOTO_ISO' => 'ISO', - 'PHOTO_SHARING' => 'Compartir', - 'PHOTO_SHR_PLUBLIC' => 'Público', - 'PHOTO_SHR_ALB' => 'Sí (Álbum)', - 'PHOTO_SHR_PHT' => 'Sí (Foto)', - 'PHOTO_SHR_NO' => 'No', - 'PHOTO_DELETE' => 'Borrar Foto', - 'PHOTO_KEEP' => 'Mantener Foto', - 'PHOTO_DELETE_1' => '¿Estás seguro de que deseas eliminar la foto', - 'PHOTO_DELETE_2' => '? ¡Esta acción no se puede deshacer!', - 'PHOTO_DELETE_ALL_1' => '¿Está seguro de que desea eliminar todo', - 'PHOTO_DELETE_ALL_2' => 'foto seleccionada? ¡Esta acción no se puede deshacer!', - 'PHOTOS_NEW_TITLE_1' => 'Ingrese un título para todos', - 'PHOTOS_NEW_TITLE_2' => 'fotos seleccionadas:', - 'PHOTO_MAKE_PRIVATE_ALBUM' => 'Esta foto se encuentra en un álbum público. Para que esta foto sea privada o pública, edite la visibilidad del álbum asociado.', - 'PHOTO_SHOW_ALBUM' => 'Mostrar álbum', - 'PHOTO_PUBLIC' => 'Público', - 'PHOTO_PUBLIC_EXPL' => 'Otros pueden ver la foto, sujeto a las restricciones a continuación', - 'PHOTO_FULL' => 'Original', - 'PHOTO_FULL_EXPL' => 'La imagen a resolución completa está disponible', - 'PHOTO_HIDDEN' => 'Oculto', - 'PHOTO_HIDDEN_EXPL' => 'Solo las personas con el enlace directo pueden ver esta foto', - 'PHOTO_DOWNLOADABLE' => 'Descargable', - 'PHOTO_DOWNLOADABLE_EXPL' => 'Los visitantes de su galería pueden descargar esta foto', - 'PHOTO_SHARE_BUTTON_VISIBLE' => 'Share button is visible', - 'PHOTO_SHARE_BUTTON_VISIBLE_EXPL' => 'Display social media sharing links.', - 'PHOTO_PASSWORD_PROT' => 'Contraseña protegida', - 'PHOTO_PASSWORD_PROT_EXPL' => 'Foto solo accesible con una contraseña válida', - 'PHOTO_EDIT_SHARING_TEXT' => 'Las propiedades para compartir de esta foto se cambiarán a lo siguiente:', - 'PHOTO_NO_EDIT_SHARING_TEXT' => 'Debido a que esta foto se encuentra en un álbum público, hereda la configuración de visibilidad de ese álbum. Su visibilidad actual se muestra a continuación solo con fines informativos.', - 'PHOTO_EDIT_GLOBAL_SHARING_TEXT' => 'La visibilidad de esta foto se puede ajustar utilizando la configuración global de Lychee. Su visibilidad actual se muestra a continuación solo con fines informativos.', - 'PHOTO_SHARING_CONFIRM' => 'Salvar', - - 'LOADING' => 'Cargando', - 'ERROR' => 'Error', - 'ERROR_TEXT' => 'Vaya, parece que algo salió mal. ¡Vuelva a cargar el sitio e intente nuevamente!', - 'ERROR_DB_1' => 'No se puede conectar a la base de datos del host porque se denegó el acceso. Vuelva a verificar su host, nombre de usuario y contraseña y asegúrese de que se permita el acceso desde su ubicación actual.', - 'ERROR_DB_2' => 'No se puede crear la base de datos. Vuelva a verificar su host, nombre de usuario y contraseña y asegúrese de que el usuario especificado tenga los derechos para modificar y agregar contenido a la base de datos.', - 'ERROR_CONFIG_FILE' => 'No se puede guardar esta configuración. Permiso denegado en \'datos\'. Establezca los derechos de lectura, escritura y ejecución para otros en \'datos\'y\'uploads\'. Consulte el archivo Léame para obtener más información', - 'ERROR_UNKNOWN' => 'Algo inesperado sucedió. Intente nuevamente y verifique su instalación y servidor. Eche un vistazo al archivo Léame para obtener más información.', - 'ERROR_LOGIN' => 'No se puede guardar el inicio de sesión. ¡Inténtalo de nuevo con otro nombre de usuario y contraseña!', - 'ERROR_MAP_DEACTIVATED' => 'La funcionalidad del mapa se ha desactivado en la configuración.', - 'ERROR_SEARCH_DEACTIVATED' => 'Search functionality has been deactivated under settings.', - 'SUCCESS' => 'OK', - 'RETRY' => 'Procesar de nuevo', - - 'SETTINGS_SUCCESS_LOGIN' => 'Información de inicio de sesión actualizada', - 'SETTINGS_SUCCESS_SORT' => 'Orden de clasificación actualizado', - 'SETTINGS_SUCCESS_DROPBOX' => 'Dropbox Key updated', - 'SETTINGS_SUCCESS_LANG' => 'Idioma actualizado', - 'SETTINGS_SUCCESS_LAYOUT' => 'Diseño actualizado', - 'SETTINGS_SUCCESS_IMAGE_OVERLAY' => 'Configuración de superposición EXIF ​​actualizada', - 'SETTINGS_SUCCESS_PUBLIC_SEARCH' => 'Búsqueda pública actualizada', - 'SETTINGS_SUCCESS_LICENSE' => 'Licencia predeterminada actualizada', - 'SETTINGS_SUCCESS_MAP_DISPLAY' => 'Configuración de visualización del mapa actualizada', - 'SETTINGS_SUCCESS_MAP_DISPLAY_PUBLIC' => 'Map display settings for public albums updated', - 'SETTINGS_SUCCESS_MAP_PROVIDER' => 'Map provider settings updated', - - 'U2F_NOT_SUPPORTED' => 'U2F not supported. Sorry.', - 'U2F_NOT_SECURE' => 'Environment not secured. U2F not available.', - 'U2F_REGISTER_KEY' => 'Register new device.', - 'U2F_REGISTRATION_SUCCESS' => 'Registration successful!', - 'U2F_AUTHENTIFICATION_SUCCESS' => 'Authentication successful!', - 'U2F_CREDENTIALS' => 'Credentials', - 'U2F_CREDENTIALS_DELETED' => 'Credentials deleted!', - - 'NEW_PHOTOS_NOTIFICATION' => 'Send new photos notification emails.', - 'SETTINGS_SUCCESS_NEW_PHOTOS_NOTIFICATION' => 'New photos notification updated', - 'USER_EMAIL_INSTRUCTION' => 'Add your email below to enable receiving email notifications.
To stop receiving emails, simply remove your email below.', - - 'DB_INFO_TITLE' => 'Ingrese los detalles de conexión de su base de datos a continuación:', - 'DB_INFO_HOST' => 'Host de base de datos (opcional)', - 'DB_INFO_USER' => 'Nombre de usuario de la base de datos', - 'DB_INFO_PASSWORD' => 'Contraseña de la base de datos', - 'DB_INFO_TEXT' => 'Lychee creará su propia base de datos. Si es necesario, puede ingresar el nombre de una base de datos existente en su lugar:', - 'DB_NAME' => 'Nombre de la base de datos (opcional)', - 'DB_PREFIX' => 'Prefijo de tabla (opcional)', - 'DB_CONNECT' => 'Conectar', - - 'LOGIN_TITLE' => 'Ingrese un nombre de usuario y contraseña para su instalación:', - 'LOGIN_USERNAME' => 'Nuevo nombre de usuario', - 'LOGIN_PASSWORD' => 'Nueva contraseña', - 'LOGIN_PASSWORD_CONFIRM' => 'Confirmar contraseña', - 'LOGIN_CREATE' => 'Crear inicio de sesión', - - 'PASSWORD_TITLE' => 'Introduce tu contraseña actual:', - 'USERNAME_CURRENT' => 'Nombre de usuario actual', - 'PASSWORD_CURRENT' => 'Contraseña actual', - 'PASSWORD_TEXT' => 'Su nombre de usuario y contraseña se cambiarán a lo siguiente:', - 'PASSWORD_CHANGE' => 'Cambiar inicio de sesión', - - 'EDIT_SHARING_TITLE' => 'Editar compartir', - 'EDIT_SHARING_TEXT' => 'Las propiedades para compartir de este álbum se cambiarán a lo siguiente:', - 'SHARE_ALBUM_TEXT' => 'Este álbum se compartirá con las siguientes propiedades:', - 'ALBUM_SHARING_CONFIRM' => 'Salvar', - - 'SORT_ALBUM_BY_1' => 'Ordenar álbumes por', - 'SORT_ALBUM_BY_2' => 'en un', - 'SORT_ALBUM_BY_3' => 'orden.', - - 'SORT_ALBUM_SELECT_1' => 'Tiempo de creación', - 'SORT_ALBUM_SELECT_2' => 'Título', - 'SORT_ALBUM_SELECT_3' => 'Descripción', - 'SORT_ALBUM_SELECT_4' => 'Público', - 'SORT_ALBUM_SELECT_5' => 'Última fecha de toma', - 'SORT_ALBUM_SELECT_6' => 'La fecha de toma más antigua', - - 'SORT_PHOTO_BY_1' => 'Ordenar fotos por', - 'SORT_PHOTO_BY_2' => 'en un', - 'SORT_PHOTO_BY_3' => 'orden.', - - 'SORT_PHOTO_SELECT_1' => 'Tiempo de carga', - 'SORT_PHOTO_SELECT_2' => 'Take Date', - 'SORT_PHOTO_SELECT_3' => 'Título', - 'SORT_PHOTO_SELECT_4' => 'Descripción', - 'SORT_PHOTO_SELECT_5' => 'Público', - 'SORT_PHOTO_SELECT_6' => 'Estrella', - 'SORT_PHOTO_SELECT_7' => 'Formato de foto', - - 'SORT_ASCENDING' => 'Ascendente', - 'SORT_DESCENDING' => 'Descendente', - 'SORT_CHANGE' => 'Cambiar clasificación', - - 'DROPBOX_TITLE' => 'Establecer clave de Dropbox', - 'DROPBOX_TEXT' => 'Para importar fotos desde su Dropbox, necesita una clave de aplicación válida desde su sitio web . Generar usted mismo una clave personal e ingrésela a continuación:', - - 'LANG_TEXT' => 'Cambiar el idioma Lychee para:', - 'LANG_TITLE' => 'Cambiar idioma', - 'PUBLIC_SEARCH_TEXT' => 'Búsqueda pública permitida:', - 'OVERLAY_TYPE' => 'Datos para usar en la superposición de imágenes:', - 'OVERLAY_NONE' => 'None', - 'OVERLAY_EXIF' => 'Datos EXIF de fotos', - 'OVERLAY_DESCRIPTION' => 'Descripción de la foto', - 'OVERLAY_DATE' => 'Fecha de foto tomada', - 'MAP_DISPLAY_TEXT' => 'Habilitar mapas (OpenStreetMap):', - 'MAP_DISPLAY_PUBLIC_TEXT' => 'Enable maps for public albums (provided by OpenStreetMap):', - 'MAP_PROVIDER' => 'Provider of OpenStreetMap tiles:', - 'MAP_PROVIDER_WIKIMEDIA' => 'Wikimedia', - 'MAP_PROVIDER_OSM_ORG' => 'OpenStreetMap.org (no HiDPI)', - 'MAP_PROVIDER_OSM_DE' => 'OpenStreetMap.de (no HiDPI)', - 'MAP_PROVIDER_OSM_FR' => 'OpenStreetMap.fr (no HiDPI)', - 'MAP_PROVIDER_RRZE' => 'University of Erlangen, Germany (only HiDPI)', - 'MAP_INCLUDE_SUBALBUMS_TEXT' => 'Include photos of subalbums on map:', - 'LOCATION_DECODING' => 'Decode GPS data into location name', - 'LOCATION_SHOW' => 'Show location name', - 'LOCATION_SHOW_PUBLIC' => 'Show location name for public mode', - 'LAYOUT_TYPE' => 'Diseño de fotos:', - 'LAYOUT_SQUARES' => 'Miniaturas cuadradas', - 'LAYOUT_JUSTIFIED' => 'Con aspecto justificado', - 'LAYOUT_UNJUSTIFIED' => 'Con aspecto, injustificado', - 'SET_LAYOUT' => 'Cambia el diseño', - - 'NSFW_VISIBLE_TEXT_1' => 'Make Sensitive albums visible by default.', - 'NSFW_VISIBLE_TEXT_2' => 'If the album is public, it is still accessible, just hidden from the view and can be revealed by pressing H.', - 'SETTINGS_SUCCESS_NSFW_VISIBLE' => 'Default sensitive album visibility updated with success.', - - 'VIEW_NO_RESULT' => 'No hay resultados', - 'VIEW_NO_PUBLIC_ALBUMS' => 'Sin álbumes públicos', - 'VIEW_NO_CONFIGURATION' => 'Sin configuración', - 'VIEW_PHOTO_NOT_FOUND' => 'Foto no encontrada', - - 'NO_TAGS' => 'No etiquetas', - - 'UPLOAD_MANAGE_NEW_PHOTOS' => 'Ahora puede administrar sus nuevas fotos.', - 'UPLOAD_COMPLETE' => 'Carga completa', - 'UPLOAD_COMPLETE_FAILED' => 'No se pudo cargar una o más fotos.', - 'UPLOAD_IMPORTING' => 'Importador', - 'UPLOAD_IMPORTING_URL' => 'Importando URL', - 'UPLOAD_UPLOADING' => 'Subiendo', - 'UPLOAD_FINISHED' => 'Terminado', - 'UPLOAD_PROCESSING' => 'Tratamiento', - 'UPLOAD_FAILED' => 'Ha fallado', - 'UPLOAD_FAILED_ERROR' => 'Subida fallida. ¡El servidor devolvió un error!', - 'UPLOAD_FAILED_WARNING' => 'Subida fallida. ¡El servidor devolvió una advertencia!', - 'UPLOAD_CANCELLED' => 'Cancelled', - 'UPLOAD_SKIPPED' => 'Saltado', - 'UPLOAD_UPDATED' => 'Updated', - 'UPLOAD_IMPORT_SKIPPED_DUPLICATE' => 'This photo has been skipped because it\'s already in your library.', - 'UPLOAD_IMPORT_RESYNCED_DUPLICATE' => 'This photo has been skipped because it\'s already in your library, but its metadata has been updated.', - 'UPLOAD_ERROR_CONSOLE' => 'Por favor, eche un vistazo a la consola de su navegador para más detalles.', - 'UPLOAD_UNKNOWN' => 'El servidor devolvió una respuesta desconocida. Por favor, eche un vistazo a la consola de su navegador para más detalles.', - 'UPLOAD_ERROR_UNKNOWN' => 'Subida fallida. ¡El servidor devolvió un error desconocido!', - 'UPLOAD_ERROR_POSTSIZE' => 'Upload failed. The PHP post_max_size may be too small! Otherwise check the FAQ.', - 'UPLOAD_ERROR_FILESIZE' => 'Upload failed. The PHP upload_max_filesize may be too small! Otherwise check the FAQ.', - 'UPLOAD_IN_PROGRESS' => '¡Lychee está subiendo actualmente!', - 'UPLOAD_IMPORT_WARN_ERR' => 'La importación ha finalizado, pero devolvió advertencias o errores. Por favor, eche un vistazo al registro (Configuración -> Mostrar registro) para obtener más detalles.', - 'UPLOAD_IMPORT_COMPLETE' => 'Importación completa', - 'UPLOAD_IMPORT_INSTR' => 'Ingrese el enlace directo a una foto para importarla:', - 'UPLOAD_IMPORT' => 'Importar', - 'UPLOAD_IMPORT_SERVER' => 'Importando desde el servidor', - 'UPLOAD_IMPORT_SERVER_FOLD' => 'Carpeta vacía o no hay archivos legibles para procesar. Por favor, eche un vistazo al registro (Configuración -> Mostrar registro) para obtener más detalles.', - 'UPLOAD_IMPORT_SERVER_INSTR' => 'Esta acción importará todas las fotos, carpetas y subcarpetas que se encuentran en el siguiente directorio. Los archivos originales se eliminarán después de la importación cuando sea posible.', - 'UPLOAD_ABSOLUTE_PATH' => 'Ruta absoluta al directorio', - 'UPLOAD_IMPORT_SERVER_EMPT' => 'No se pudo iniciar la importación porque la carpeta estaba vacía', - 'UPLOAD_IMPORT_DELETE_ORIGINALS' => 'Eliminar originales', - 'UPLOAD_IMPORT_DELETE_ORIGINALS_EXPL' => 'Los archivos originales se eliminarán después de la importación cuando sea posible', - 'UPLOAD_IMPORT_VIA_SYMLINK' => 'Symbolic links', - 'UPLOAD_IMPORT_VIA_SYMLINK_EXPL' => 'Import files using symbolic links to originals.', - 'UPLOAD_IMPORT_SKIP_DUPLICATES' => 'Skip duplicates', - 'UPLOAD_IMPORT_SKIP_DUPLICATES_EXPL' => 'Existing media files are skipped.', - 'UPLOAD_IMPORT_RESYNC_METADATA' => 'Re-sync metadata', - 'UPLOAD_IMPORT_RESYNC_METADATA_EXPL' => 'Update metadata of existing media files.', - 'UPLOAD_IMPORT_LOW_MEMORY' => '¡Condición de memoria baja!', - 'UPLOAD_IMPORT_LOW_MEMORY_EXPL' => 'El proceso de importación en el servidor se acerca al límite de memoria y puede terminar antes de tiempo.', - 'UPLOAD_WARNING' => 'Advertencia', - 'UPLOAD_IMPORT_NOT_A_DIRECTORY' => '¡La ruta dada no es un directorio legible!', - 'UPLOAD_IMPORT_PATH_RESERVED' => '¡El camino dado es un camino reservado de Lychee!', - 'UPLOAD_IMPORT_UNREADABLE' => '¡No se pudo leer el archivo!', - 'UPLOAD_IMPORT_FAILED' => '¡No se pudo importar el archivo!', - 'UPLOAD_IMPORT_UNSUPPORTED' => '¡Tipo de archivo no soportado!', - 'UPLOAD_IMPORT_ALBUM_FAILED' => '¡No se pudo crear el álbum!', - 'UPLOAD_IMPORT_CANCELLED' => 'Import cancelled', - - 'ABOUT_SUBTITLE' => 'Un auto-hosteado gestor de imagenes, bien hecho', - 'ABOUT_DESCRIPTION' => 'es una herramienta gratuita de gestión de fotos, que se ejecuta en su servidor o espacio web. La instalación es cuestión de segundos. Cargue, administre y comparta fotos como desde una aplicación nativa. Lychee viene con todo lo que necesitas y todas tus fotos se almacenan de forma segura.', - 'FOOTER_COPYRIGHT' => 'Todas las imágenes de este sitio web están sujetas a Copyright por', - 'HOSTED_WITH_LYCHEE' => 'Alojado con Lychee', - - 'URL_COPY_TO_CLIPBOARD' => 'Copiar al portapapeles', - 'URL_COPIED_TO_CLIPBOARD' => '¡URL copiada al portapapeles!', - 'PHOTO_DIRECT_LINKS_TO_IMAGES' => 'Enlaces directos a archivos de imagen:', - 'PHOTO_MEDIUM' => 'Mediana', - 'PHOTO_MEDIUM_HIDPI' => 'Mediana HiDPI', - 'PHOTO_SMALL' => 'Miniatura', - 'PHOTO_SMALL_HIDPI' => 'Miniatura HiDPI', - 'PHOTO_THUMB' => 'Cuadrado de Miniatura', - 'PHOTO_THUMB_HIDPI' => 'Cuadrado de Miniatura HiDPI', - 'PHOTO_LIVE_VIDEO' => 'Video part of live-photo', - 'PHOTO_VIEW' => 'Vista de Foto de Lychee', - - 'PHOTO_EDIT_ROTATECWISE' => 'Rotate clockwise', - 'PHOTO_EDIT_ROTATECCWISE' => 'Rotate counter-clockwise', - ]; - - return $locale; - } -} diff --git a/app/Locale/Swedish.php b/app/Locale/Swedish.php deleted file mode 100644 index c72730b0289..00000000000 --- a/app/Locale/Swedish.php +++ /dev/null @@ -1,475 +0,0 @@ - 'användarnamn', - 'PASSWORD' => 'lösenord', - 'ENTER' => 'Stig in', - 'CANCEL' => 'Avbryt', - 'SIGN_IN' => 'Logga in', - 'CLOSE' => 'Stäng', - 'SETTINGS' => 'Settings', - 'SEARCH' => 'Search ...', - 'MORE' => 'More', - 'DEFAULT' => 'Default', - - 'USERS' => 'Users', - 'U2F' => 'U2F', - 'NOTIFICATIONS' => 'Notifications', - 'SHARING' => 'Sharing', - 'CHANGE_LOGIN' => 'Ändra inloggning', - 'CHANGE_SORTING' => 'Ändra sortering', - 'SET_DROPBOX' => 'Ställ in Dropbox', - 'ABOUT_LYCHEE' => 'Om Lychee', - 'DIAGNOSTICS' => 'Diagnostik', - 'DIAGNOSTICS_GET_SIZE' => 'Request space usage', - 'LOGS' => 'Visa logfilen', - 'SIGN_OUT' => 'Logga ut', - 'UPDATE_AVAILABLE' => 'En uppdatering finns!', - 'MIGRATION_AVAILABLE' => 'Migration available!', - 'DEFAULT_LICENSE' => 'Default license for new uploads:', - 'SET_LICENSE' => 'Set License', - 'SET_OVERLAY_TYPE' => 'Set Overlay', - 'SET_MAP_PROVIDER' => 'Set OpenStreetMap tiles provider', - - 'SMART_ALBUMS' => 'Smarta album', - 'SHARED_ALBUMS' => 'Shared albums', - 'ALBUMS' => 'Album', - 'PHOTOS' => 'Pictures', - 'SEARCH_RESULTS' => 'Search results', - - 'RENAME' => 'Ändra namnet', - 'RENAME_ALL' => 'Byt namn på vald', - 'MERGE' => 'Slå ihop', - 'MERGE_ALL' => 'Sammanfoga vald', - 'MAKE_PUBLIC' => 'Gör publika', - 'SHARE_ALBUM' => 'Dela album', - 'SHARE_PHOTO' => 'Dela fotografi', - 'VISIBILITY_ALBUM' => 'Album Visibility', - 'VISIBILITY_PHOTO' => 'Photo Visibility', - 'DOWNLOAD_ALBUM' => 'Ladda ner album', - 'ABOUT_ALBUM' => 'Om albumet', - 'DELETE_ALBUM' => 'Radera albumet', - 'MOVE_ALBUM' => 'Move Album', - 'FULLSCREEN_ENTER' => 'Enter Fullscreen', - 'FULLSCREEN_EXIT' => 'Exit Fullscreen', - - 'SHARING_ALBUM_USERS' => 'Share this album with users', - 'WAIT_FETCH_DATA' => 'Please wait while we get the data...', - 'SHARING_ALBUM_USERS_NO_USERS' => 'There are no users to share the album with', - 'SHARING_ALBUM_USERS_LONG_MESSAGE' => 'Select the users to share this album with', - - 'DELETE_ALBUM_QUESTION' => 'Radera album och fotografier', - 'KEEP_ALBUM' => 'Behåll albumet', - 'DELETE_ALBUM_CONFIRMATION_1' => 'Är du säker på att du vill radera albumet', - 'DELETE_ALBUM_CONFIRMATION_2' => 'och alla fotografier det innehåller? Raderingen går inte att ångra!', - - 'DELETE_ALBUMS_QUESTION' => 'Radera album och fotografier', - 'KEEP_ALBUMS' => 'Behåll album', - 'DELETE_ALBUMS_CONFIRMATION_1' => 'Är du säker på att du vill radera alla', - 'DELETE_ALBUMS_CONFIRMATION_2' => 'valda album och alla fotografier de innehåller? Raderingen går inte att ångra!', - - 'DELETE_UNSORTED_CONFIRM' => 'Är du säker på att du vill radera alla fotografier från \'Osorterat\'?
Det här går inte att ångra!', - 'CLEAR_UNSORTED' => 'Rensa osorterade', - 'KEEP_UNSORTED' => 'Behåll osorterade', - - 'EDIT_SHARING' => 'Redrigera delning', - 'MAKE_PRIVATE' => 'Gör privat', - - 'CLOSE_ALBUM' => 'Stäng albumet', - 'CLOSE_PHOTO' => 'Stäng fotografiet', - 'CLOSE_MAP' => 'Close Map', - - 'ADD' => 'Lägg till', - 'MOVE' => 'Flytta', - 'MOVE_ALL' => 'Flytta valda', - 'DUPLICATE' => 'Kopiera', - 'DUPLICATE_ALL' => 'Kopiera valda', - 'COPY_TO' => 'Kopiera till ...', - 'COPY_ALL_TO' => 'Valda kopia till ...', - 'DELETE' => 'Radera', - 'DELETE_ALL' => 'Radera vald', - 'DOWNLOAD' => 'Ladda ner', - 'DOWNLOAD_ALL' => 'Download Selected', - 'UPLOAD_PHOTO' => 'Ladda upp fotografi', - 'IMPORT_LINK' => 'Importera från länk', - 'IMPORT_DROPBOX' => 'Importera från Dropbox', - 'IMPORT_SERVER' => 'Importera från server', - 'NEW_ALBUM' => 'Nytt album', - 'NEW_TAG_ALBUM' => 'New Tag Album', - - 'TITLE_NEW_ALBUM' => 'Skriv en titel för det nya albumet:', - 'UNTITLED' => 'Saknar titel', - 'UNSORTED' => 'Osorterat', - 'STARRED' => 'Stjärnmärkta', - 'RECENT' => 'Nyligen', - 'PUBLIC' => 'Publika', - 'NUM_PHOTOS' => 'Fotografier', - - 'CREATE_ALBUM' => 'Skapa album', - 'CREATE_TAG_ALBUM' => 'Create Tag Album', - - 'STAR_PHOTO' => 'Stjärnmärk fotografi', - 'STAR' => 'Stjärnmärk', - 'STAR_ALL' => 'Markera valda som favoriter', - 'TAGS' => 'Tag', - 'TAGS_ALL' => 'Vald taggen', - 'UNSTAR_PHOTO' => 'Ta bort stjärnmärke', - 'SET_COVER' => 'Set Album Cover', - 'REMOVE_COVER' => 'Remove Album Cover', - - 'FULL_PHOTO' => 'Originalfotografi', - 'ABOUT_PHOTO' => 'Om fotografiet', - 'DISPLAY_FULL_MAP' => 'Map', - 'DIRECT_LINK' => 'Direktlänk', - 'DIRECT_LINKS' => 'Direct Links', - - 'ALBUM_ABOUT' => 'Om', - 'ALBUM_BASICS' => 'Grundläggande', - 'ALBUM_TITLE' => 'Titel', - 'ALBUM_NEW_TITLE' => 'Skriv en ny titel för det här albumet:', - 'ALBUMS_NEW_TITLE_1' => 'Skriv en ny titel för alla', - 'ALBUMS_NEW_TITLE_2' => 'valda album:', - 'ALBUM_SET_TITLE' => 'Spara titel', - 'ALBUM_DESCRIPTION' => 'Beskrivning', - 'ALBUM_SHOW_TAGS' => 'Tags to show', - 'ALBUM_NEW_DESCRIPTION' => 'Ny beskrivning för detta album:', - 'ALBUM_SET_DESCRIPTION' => 'Spara beskrivningen', - 'ALBUM_NEW_SHOWTAGS' => 'Enter tags of photos that will be visible in this album:', - 'ALBUM_SET_SHOWTAGS' => 'Set tags to show', - 'ALBUM_ALBUM' => 'Album', - 'ALBUM_CREATED' => 'Skapad', - 'ALBUM_IMAGES' => 'Fotografier', - 'ALBUM_VIDEOS' => 'Videos', - 'ALBUM_SUBALBUMS' => 'Subalbums', - 'ALBUM_SHARING' => 'Dela', - 'ALBUM_SHR_YES' => 'Ja', - 'ALBUM_SHR_NO' => 'Nej', - 'ALBUM_PUBLIC' => 'Publikt', - 'ALBUM_PUBLIC_EXPL' => 'Album can be viewed by others, subject to the restrictions below.', - 'ALBUM_FULL' => 'Original', - 'ALBUM_FULL_EXPL' => 'Full-resolution pictures are available.', - 'ALBUM_HIDDEN' => 'Dold', - 'ALBUM_HIDDEN_EXPL' => 'Bara personer med korrekt länk kan se detta album.', - 'ALBUM_MARK_NSFW' => 'Mark album as sensitive', - 'ALBUM_UNMARK_NSFW' => 'Unmark album as sensitive', - 'ALBUM_NSFW' => 'Sensitive', - 'ALBUM_NSFW_EXPL' => 'Album is marked to contain sensitive content.', - 'ALBUM_DOWNLOADABLE' => 'Nedladdningsbart', - 'ALBUM_DOWNLOADABLE_EXPL' => 'Besökare till din Lychee kan ladda ner detta album.', - 'ALBUM_SHARE_BUTTON_VISIBLE' => 'Share button is visible', - 'ALBUM_SHARE_BUTTON_VISIBLE_EXPL' => 'Display social media sharing links.', - 'ALBUM_PASSWORD' => 'Lösenord', - 'ALBUM_PASSWORD_PROT' => 'Lösenordsskyddad', - 'ALBUM_PASSWORD_PROT_EXPL' => 'Albumet kan bara ses med giltigt lösenord.', - 'ALBUM_PASSWORD_REQUIRED' => 'Albumet är skyddat med ett lösenord. Ange lösenordet nedan för att se fotografierna i albumet:', - 'ALBUM_MERGE_1' => 'Är du säker på att du vill sammanfoga albumet', - 'ALBUM_MERGE_2' => 'med albumet', - 'ALBUMS_MERGE' => 'Är du säker på att du vill sammanfoga samtliga valda album till albumet', - 'MERGE_ALBUM' => 'Sammanfoga album', - 'DONT_MERGE' => 'Sammanfoga inte', - 'ALBUM_MOVE_1' => 'Are you sure you want to move the album', - 'ALBUM_MOVE_2' => 'into the album', - 'ALBUMS_MOVE' => 'Are you sure you want to move all selected albums into the album', - 'MOVE_ALBUMS' => 'Move Albums', - 'NOT_MOVE_ALBUMS' => "Don't Move", - 'ROOT' => 'Album', - 'ALBUM_REUSE' => 'Reuse', - 'ALBUM_LICENSE' => 'License', - 'ALBUM_SET_LICENSE' => 'Set License', - 'ALBUM_LICENSE_HELP' => 'Need help choosing?', - 'ALBUM_LICENSE_NONE' => 'None', - 'ALBUM_RESERVED' => 'All Rights Reserved', - 'ALBUM_SET_ORDER' => 'Set Order', - 'ALBUM_ORDERING' => 'Order by', - - 'PHOTO_ABOUT' => 'Om', - 'PHOTO_BASICS' => 'Grundläggande', - 'PHOTO_TITLE' => 'Titel', - 'PHOTO_NEW_TITLE' => 'Skriv in en ny tital för det hör fotografiet:', - 'PHOTO_SET_TITLE' => 'Spara titeln', - 'PHOTO_UPLOADED' => 'Uppladdat', - 'PHOTO_DESCRIPTION' => 'Beskrivning', - 'PHOTO_NEW_DESCRIPTION' => 'Skriv en ny beskrivning för detta fotografi:', - 'PHOTO_SET_DESCRIPTION' => 'Spara beskrivningen', - 'PHOTO_NEW_LICENSE' => 'Add a License', - 'PHOTO_SET_LICENSE' => 'Set License', - 'PHOTO_LICENSE' => 'License', - 'PHOTO_REUSE' => 'Reuse', - 'PHOTO_LICENSE_NONE' => 'None', - 'PHOTO_RESERVED' => 'All Rights Reserved', - 'PHOTO_LATITUDE' => 'Latitude', - 'PHOTO_LONGITUDE' => 'Longitude', - 'PHOTO_ALTITUDE' => 'Altitude', - 'PHOTO_IMGDIRECTION' => 'Direction', - 'PHOTO_LOCATION' => 'Location', - 'PHOTO_IMAGE' => 'Fotografi', - 'PHOTO_VIDEO' => 'Video', - 'PHOTO_SIZE' => 'Storlek', - 'PHOTO_FORMAT' => 'Filformat', - 'PHOTO_DURATION' => 'Duration', - 'PHOTO_FPS' => 'Frame rate', - 'PHOTO_RESOLUTION' => 'Mått', - 'PHOTO_TAGS' => 'Kategori', - 'PHOTO_NOTAGS' => 'Inga kategorier', - 'PHOTO_NEW_TAGS' => 'Skriv in din kategori för det här fotografier. Du kan ange flera kategori genom att separera dem med ett kommatecken:', - 'PHOTO_NEW_TAGS_1' => 'Ange kategori för samtliga valda bilder', - 'PHOTO_NEW_TAGS_2' => 'Befintliga kategorier kommer att raderas. Du kan lägga till nya kategorier genom att separera dem med kommatecken:', - 'PHOTO_SET_TAGS' => 'Spara kategori', - 'PHOTO_CAMERA' => 'Kamera', - 'PHOTO_CAPTURED' => 'Digitaliserad', - 'PHOTO_MAKE' => 'Tillverkare', - 'PHOTO_TYPE' => 'Typ/Modell', - 'PHOTO_LENS' => 'Lens', - 'PHOTO_SHUTTER' => 'Slutartid', - 'PHOTO_APERTURE' => 'Bländartal', - 'PHOTO_FOCAL' => 'Brännvidd', - 'PHOTO_ISO' => 'ISO', - 'PHOTO_SHARING' => 'Delning', - 'PHOTO_SHR_PLUBLIC' => 'Publik', - 'PHOTO_SHR_ALB' => 'Ja (Album)', - 'PHOTO_SHR_PHT' => 'Ja (Fotografi)', - 'PHOTO_SHR_NO' => 'Nej', - 'PHOTO_DELETE' => 'Radera fotografi', - 'PHOTO_KEEP' => 'Spara fotografi', - 'PHOTO_DELETE_1' => 'Är du säker på att du vill radera det här fotografiet?', - 'PHOTO_DELETE_2' => 'Raderingen går inte att ångra!', - 'PHOTO_DELETE_ALL_1' => 'Är du säker på att du vill radera alla', - 'PHOTO_DELETE_ALL_2' => 'valda fotografier? Raderingen går inte att ångra!', - 'PHOTOS_NEW_TITLE_1' => 'Ange en tital för alla', - 'PHOTOS_NEW_TITLE_2' => 'valda fotografier:', - 'PHOTO_MAKE_PRIVATE_ALBUM' => 'Det här fotografier finns i ett publikt album. Du kan ändra fotografiets synlighet genom att redigera egenskapen för albumet.', - 'PHOTO_SHOW_ALBUM' => 'Visa album', - 'PHOTO_PUBLIC' => 'Public', - 'PHOTO_PUBLIC_EXPL' => 'Photo can be viewed by others, subject to the restrictions below.', - 'PHOTO_FULL' => 'Original', - 'PHOTO_FULL_EXPL' => 'Full-resolution picture is available.', - 'PHOTO_HIDDEN' => 'Hidden', - 'PHOTO_HIDDEN_EXPL' => 'Only people with the direct link can view this photo.', - 'PHOTO_DOWNLOADABLE' => 'Downloadable', - 'PHOTO_DOWNLOADABLE_EXPL' => 'Visitors of your gallery can download this photo.', - 'PHOTO_SHARE_BUTTON_VISIBLE' => 'Share button is visible', - 'PHOTO_SHARE_BUTTON_VISIBLE_EXPL' => 'Display social media sharing links.', - 'PHOTO_PASSWORD_PROT' => 'Password protected', - 'PHOTO_PASSWORD_PROT_EXPL' => 'Photo only accessible with a valid password.', - 'PHOTO_EDIT_SHARING_TEXT' => 'The sharing properties of this photo will be changed to the following:', - 'PHOTO_NO_EDIT_SHARING_TEXT' => 'Because this photo is located in a public album, it inherits that album\'s visibility settings. Its current visibility is shown below for informational purposes only.', - 'PHOTO_EDIT_GLOBAL_SHARING_TEXT' => 'The visibility of this photo can be fine-tuned using global Lychee settings. Its current visibility is shown below for informational purposes only.', - 'PHOTO_SHARING_CONFIRM' => 'Save', - - 'LOADING' => 'Laddar', - 'ERROR' => 'Fel', - 'ERROR_TEXT' => 'Ojsan, något verkar ha gått lite fel. Prova att ladda om sidan och försök igen!', - 'ERROR_DB_1' => 'Kan inte ansluta till databasen då åtkomst nekades. Kontrollera adressen, användarnamnet och löseordet samt säkerställ att du har behörighet att ansluta mot databasen från din nuvarande adress.', - 'ERROR_DB_2' => 'Kunde inte skapa databasen. Kontrollera adressen, användarnamnet och löseordet samt säkerställ att du har behörighet att användarnamnet har behörighet att förändra databasen.', - 'ERROR_CONFIG_FILE' => "Kunde inte spara konfigureringen. Åtkomst nekades i 'data/'. Kontrollera rättigheterna för läsning, skrivning och exekvering för andra i 'data/' och 'uploads/'. För mera information läs dokumentet 'readme'.", - 'ERROR_UNKNOWN' => 'Något oväntat inträffade. Vänligen försök igen och kontrollera installationen av Lychee och din server. För mera information läs dokumentet readme.', - 'ERROR_LOGIN' => 'Kunde inte spara inloggningsuppgifterna. Vänligen prova med ett annat användarnamn och lösenord!', - 'ERROR_MAP_DEACTIVATED' => 'Map functionality has been deactivated under settings.', - 'ERROR_SEARCH_DEACTIVATED' => 'Search functionality has been deactivated under settings.', - 'SUCCESS' => 'OK', - 'RETRY' => 'Försök igen', - - 'SETTINGS_SUCCESS_LOGIN' => 'Login Info updated.', - 'SETTINGS_SUCCESS_SORT' => 'Sorting order updated.', - 'SETTINGS_SUCCESS_DROPBOX' => 'Dropbox Key updated.', - 'SETTINGS_SUCCESS_LANG' => 'Language updated', - 'SETTINGS_SUCCESS_LAYOUT' => 'Layout updated', - 'SETTINGS_SUCCESS_IMAGE_OVERLAY' => 'EXIF Overlay setting updated', - 'SETTINGS_SUCCESS_PUBLIC_SEARCH' => 'Offentlig sökning uppdaterad', - 'SETTINGS_SUCCESS_LICENSE' => 'Default license updated', - 'SETTINGS_SUCCESS_MAP_DISPLAY' => 'Map display settings updated', - 'SETTINGS_SUCCESS_MAP_DISPLAY_PUBLIC' => 'Map display settings for public albums updated', - 'SETTINGS_SUCCESS_MAP_PROVIDER' => 'Map provider settings updated', - - 'U2F_NOT_SUPPORTED' => 'U2F not supported. Sorry.', - 'U2F_NOT_SECURE' => 'Environment not secured. U2F not available.', - 'U2F_REGISTER_KEY' => 'Register new device.', - 'U2F_REGISTRATION_SUCCESS' => 'Registration successful!', - 'U2F_AUTHENTIFICATION_SUCCESS' => 'Authentication successful!', - 'U2F_CREDENTIALS' => 'Credentials', - 'U2F_CREDENTIALS_DELETED' => 'Credentials deleted!', - - 'NEW_PHOTOS_NOTIFICATION' => 'Send new photos notification emails.', - 'SETTINGS_SUCCESS_NEW_PHOTOS_NOTIFICATION' => 'New photos notification updated', - 'USER_EMAIL_INSTRUCTION' => 'Add your email below to enable receiving email notifications.
To stop receiving emails, simply remove your email below.', - - 'DB_INFO_TITLE' => 'Ange dina uppgifter för databasanslutninge nedan:', - 'DB_INFO_HOST' => 'Databasens adress (valfritt)', - 'DB_INFO_USER' => 'Databasens användarnamn', - 'DB_INFO_PASSWORD' => 'Databasens lösenord', - 'DB_INFO_TEXT' => 'Lychee kommer nu att skapa sin egen databas. Vill du istället ansluta till en befintlig databas anger du namnet på den:', - 'DB_NAME' => 'Databasens namn (valfritt)', - 'DB_PREFIX' => 'Tabell prefix (valfritt)', - 'DB_CONNECT' => 'Anslut', - - 'LOGIN_TITLE' => 'Ange ett användarnamn och lösenord för din installation:', - 'LOGIN_USERNAME' => 'Nytt användarnamn', - 'LOGIN_PASSWORD' => 'Nytt lösenord', - 'LOGIN_PASSWORD_CONFIRM' => 'Confirm Password', - 'LOGIN_CREATE' => 'Skapa inloggning', - - 'PASSWORD_TITLE' => 'Ange dina befintliga inloggningsuppgifter:', - 'USERNAME_CURRENT' => 'Befintligt användarnamn', - 'PASSWORD_CURRENT' => 'Befintligt lösenord', - 'PASSWORD_TEXT' => 'Ditt inloggningsuppgifter kommer att ändras till:', - 'PASSWORD_CHANGE' => 'Spara ändringar av inloggningsuppgifter', - - 'EDIT_SHARING_TITLE' => 'Redigera delning', - 'EDIT_SHARING_TEXT' => 'Albumets egenskaper för delning kommer att ändras till:', - 'SHARE_ALBUM_TEXT' => 'Det här albumet kommer att delas ut med dessa egenskaper::', - 'ALBUM_SHARING_CONFIRM' => 'Save', - - 'SORT_ALBUM_BY_1' => 'Sortera album efter', - 'SORT_ALBUM_BY_2' => 'i en', - 'SORT_ALBUM_BY_3' => 'ordning.', - - 'SORT_ALBUM_SELECT_1' => 'skapelsetid', - 'SORT_ALBUM_SELECT_2' => 'titel', - 'SORT_ALBUM_SELECT_3' => 'beskrivning', - 'SORT_ALBUM_SELECT_4' => 'publikt', - 'SORT_ALBUM_SELECT_5' => 'senaste datum', - 'SORT_ALBUM_SELECT_6' => 'äldsta datum', - - 'SORT_PHOTO_BY_1' => 'Sortera fotografier efter', - 'SORT_PHOTO_BY_2' => 'i en', - 'SORT_PHOTO_BY_3' => 'ordning.', - - 'SORT_PHOTO_SELECT_1' => 'Uppladdningstid', - 'SORT_PHOTO_SELECT_2' => 'Fotograferingsdatum', - 'SORT_PHOTO_SELECT_3' => 'Titel', - 'SORT_PHOTO_SELECT_4' => 'Beskrivning', - 'SORT_PHOTO_SELECT_5' => 'Publikt', - 'SORT_PHOTO_SELECT_6' => 'Stjärnmärkning', - 'SORT_PHOTO_SELECT_7' => 'Bildformat', - - 'SORT_ASCENDING' => 'stigande', - 'SORT_DESCENDING' => 'fallande', - 'SORT_CHANGE' => 'Spara ändringar av sorteringsföljden', - - 'DROPBOX_TITLE' => 'Spara nyckeln för Dropbox', - 'DROPBOX_TEXT' => "För att kunna importera fotografier från ditt Dropboxkonto behöver du en godkänd applikationsnyckel från Dropbox.\n Skapa en personlig nyckel och ange den sedan här nedan:", - - 'LANG_TEXT' => 'Ändra språket i Lychee till:', - 'LANG_TITLE' => 'Spara ändringen av språket', - 'PUBLIC_SEARCH_TEXT' => 'Offentlig sökning tillåts:', - 'OVERLAY_TYPE' => 'Photo overlay:', - 'OVERLAY_NONE' => 'None', - 'OVERLAY_EXIF' => 'EXIF data', - 'OVERLAY_DESCRIPTION' => 'Description', - 'OVERLAY_DATE' => 'Date taken', - 'MAP_DISPLAY_TEXT' => 'Enable maps (provided by OpenStreetMap):', - 'MAP_DISPLAY_PUBLIC_TEXT' => 'Enable maps for public albums (provided by OpenStreetMap):', - 'MAP_PROVIDER' => 'Provider of OpenStreetMap tiles:', - 'MAP_PROVIDER_WIKIMEDIA' => 'Wikimedia', - 'MAP_PROVIDER_OSM_ORG' => 'OpenStreetMap.org (no HiDPI)', - 'MAP_PROVIDER_OSM_DE' => 'OpenStreetMap.de (no HiDPI)', - 'MAP_PROVIDER_OSM_FR' => 'OpenStreetMap.fr (no HiDPI)', - 'MAP_PROVIDER_RRZE' => 'University of Erlangen, Germany (only HiDPI)', - 'MAP_INCLUDE_SUBALBUMS_TEXT' => 'Include photos of subalbums on map:', - 'LOCATION_DECODING' => 'Decode GPS data into location name', - 'LOCATION_SHOW' => 'Show location name', - 'LOCATION_SHOW_PUBLIC' => 'Show location name for public mode', - 'LAYOUT_TYPE' => 'Layout of photos:', - 'LAYOUT_SQUARES' => 'Square thumbnails', - 'LAYOUT_JUSTIFIED' => 'With aspect, justified', - 'LAYOUT_UNJUSTIFIED' => 'With aspect, unjustified', - 'SET_LAYOUT' => 'Change layout', - - 'NSFW_VISIBLE_TEXT_1' => 'Make Sensitive albums visible by default.', - 'NSFW_VISIBLE_TEXT_2' => 'If the album is public, it is still accessible, just hidden from the view and can be revealed by pressing H.', - 'SETTINGS_SUCCESS_NSFW_VISIBLE' => 'Default sensitive album visibility updated with success.', - - 'VIEW_NO_RESULT' => 'Inget resultat', - 'VIEW_NO_PUBLIC_ALBUMS' => 'Inga publika album', - 'VIEW_NO_CONFIGURATION' => 'Ingen konfigurering', - 'VIEW_PHOTO_NOT_FOUND' => 'Fotografiet hittade inte', - - 'NO_TAGS' => 'Inga kategorier', - - 'UPLOAD_MANAGE_NEW_PHOTOS' => 'Du kan nu hantera de nya bilderna.', - 'UPLOAD_COMPLETE' => 'Uppladdning klar', - 'UPLOAD_COMPLETE_FAILED' => 'Kunde inte ladda upp en eller flera bilder.', - 'UPLOAD_IMPORTING' => 'Importering', - 'UPLOAD_IMPORTING_URL' => 'Adress för importering', - 'UPLOAD_UPLOADING' => 'Uppladdning', - 'UPLOAD_FINISHED' => 'Klar', - 'UPLOAD_PROCESSING' => 'Bearbetning', - 'UPLOAD_FAILED' => 'Misslyckades', - 'UPLOAD_FAILED_ERROR' => 'Uppladdning misslyckades. Servern svarade med ett felmeddelande!', - 'UPLOAD_FAILED_WARNING' => 'Uppladdning misslyckades. Servern svarade med en varning!', - 'UPLOAD_CANCELLED' => 'Cancelled', - 'UPLOAD_SKIPPED' => 'Ignorerade', - 'UPLOAD_UPDATED' => 'Updated', - 'UPLOAD_IMPORT_SKIPPED_DUPLICATE' => 'This photo has been skipped because it\'s already in your library.', - 'UPLOAD_IMPORT_RESYNCED_DUPLICATE' => 'This photo has been skipped because it\'s already in your library, but its metadata has been updated.', - 'UPLOAD_ERROR_CONSOLE' => 'Kontrollera din webbläsares konsoll för ytterligare information.', - 'UPLOAD_UNKNOWN' => 'Servern returnerade ett oklart svar. Kontrollera din webbläsares konsoll för ytterligare information.', - 'UPLOAD_ERROR_UNKNOWN' => 'Uppladdning misslyckades. Servern returnerade ett oklart fel!', - 'UPLOAD_ERROR_POSTSIZE' => 'Upload failed. The PHP post_max_size may be too small! Otherwise check the FAQ.', - 'UPLOAD_ERROR_FILESIZE' => 'Upload failed. The PHP upload_max_filesize may be too small! Otherwise check the FAQ.', - 'UPLOAD_IN_PROGRESS' => 'Lychee laddar för tillfället upp material!', - 'UPLOAD_IMPORT_WARN_ERR' => 'Importeringen är avslutad, men processen gav felmeddelanden. Kontrollera logfilen (Inställningar -> Visa logfilen) för ytterligare detaljer.', - 'UPLOAD_IMPORT_COMPLETE' => 'Importeringen klar', - 'UPLOAD_IMPORT_INSTR' => 'Ange den exakta länken till fotografiet du vill importera.', - 'UPLOAD_IMPORT' => 'Importera', - 'UPLOAD_IMPORT_SERVER' => 'Importera från server', - 'UPLOAD_IMPORT_SERVER_FOLD' => 'Mappen du angav var tom eller saknade läsbara filer. Kontrollera logfilen (Inställningar -> Visa logfilen) för ytterligare detaljer', - 'UPLOAD_IMPORT_SERVER_INSTR' => 'Den här processen kommer att importera alla fotografier, inklusive alla mappar och undermappar från platse du angav.', - 'UPLOAD_ABSOLUTE_PATH' => 'Exakt sökväg till mappen', - 'UPLOAD_IMPORT_SERVER_EMPT' => 'Kunde inte påbörja importeringen då mappen saknade innehåll!', - 'UPLOAD_IMPORT_DELETE_ORIGINALS' => 'Delete originals', - 'UPLOAD_IMPORT_DELETE_ORIGINALS_EXPL' => 'Originalfotografierna kommer att raderas efter att importering genomförts.', - 'UPLOAD_IMPORT_VIA_SYMLINK' => 'Symbolic links', - 'UPLOAD_IMPORT_VIA_SYMLINK_EXPL' => 'Import files using symbolic links to originals.', - 'UPLOAD_IMPORT_SKIP_DUPLICATES' => 'Skip duplicates', - 'UPLOAD_IMPORT_SKIP_DUPLICATES_EXPL' => 'Existing media files are skipped.', - 'UPLOAD_IMPORT_RESYNC_METADATA' => 'Re-sync metadata', - 'UPLOAD_IMPORT_RESYNC_METADATA_EXPL' => 'Update metadata of existing media files.', - 'UPLOAD_IMPORT_LOW_MEMORY' => 'Low memory condition!', - 'UPLOAD_IMPORT_LOW_MEMORY_EXPL' => 'The import process on the server is approaching the memory limit and may end up being terminated prematurely.', - 'UPLOAD_WARNING' => 'Warning', - 'UPLOAD_IMPORT_NOT_A_DIRECTORY' => 'The given path is not a readable directory!', - 'UPLOAD_IMPORT_PATH_RESERVED' => 'The given path is a reserved path of Lychee!', - 'UPLOAD_IMPORT_UNREADABLE' => 'Could not read the file!', - 'UPLOAD_IMPORT_FAILED' => 'Could not import the file!', - 'UPLOAD_IMPORT_UNSUPPORTED' => 'Unsupported file type!', - 'UPLOAD_IMPORT_ALBUM_FAILED' => 'Could not create the album!', - 'UPLOAD_IMPORT_CANCELLED' => 'Import cancelled', - - 'ABOUT_SUBTITLE' => 'Self-hosted photo-management done right', - 'ABOUT_DESCRIPTION' => 'is a free photo-management tool, which runs on your server or web-space. Installing is a matter of seconds. Upload, manage and share photos like from a native application. Lychee comes with everything you need and all your photos are stored securely.', - 'FOOTER_COPYRIGHT' => 'Alla bilder på denna webbplats är föremål för upphovsrätt från', - 'HOSTED_WITH_LYCHEE' => 'Hosted with Lychee', - - 'URL_COPY_TO_CLIPBOARD' => 'Copy to clipboard', - 'URL_COPIED_TO_CLIPBOARD' => 'Copied URL to clipboard!', - 'PHOTO_DIRECT_LINKS_TO_IMAGES' => 'Direct links to image files:', - 'PHOTO_MEDIUM' => 'Medium', - 'PHOTO_MEDIUM_HIDPI' => 'Medium HiDPI', - 'PHOTO_SMALL' => 'Thumb', - 'PHOTO_SMALL_HIDPI' => 'Thumb HiDPI', - 'PHOTO_THUMB' => 'Square thumb', - 'PHOTO_THUMB_HIDPI' => 'Square thumb HiDPI', - 'PHOTO_LIVE_VIDEO' => 'Video part of live-photo', - 'PHOTO_VIEW' => 'Lychee Photo View:', - - 'PHOTO_EDIT_ROTATECWISE' => 'Rotate clockwise', - 'PHOTO_EDIT_ROTATECCWISE' => 'Rotate counter-clockwise', - ]; - - return $locale; - } -} diff --git a/app/Mail/PhotosAdded.php b/app/Mail/PhotosAdded.php index c08ff7c1fe1..2ef70fed5a6 100644 --- a/app/Mail/PhotosAdded.php +++ b/app/Mail/PhotosAdded.php @@ -1,5 +1,11 @@ >|string>> */ + protected array $photos; + protected string $title; /** * Create a new message instance. * + * @param array>|string>> $photos + * * @return void */ - public function __construct($photos) + public function __construct(array $photos) { $this->photos = $photos; - $this->settings = Configs::get(); + $this->title = Configs::getValueAsString('site_title'); } /** @@ -30,10 +40,11 @@ public function __construct($photos) * * @return $this */ - public function build() + public function build(): self { return $this->markdown('emails.photos-added', [ - 'title' => $this->settings['site_title'], + 'title' => $this->title, + 'photos' => $this->photos, ]); } } diff --git a/app/Metadata/DiskUsage.php b/app/Metadata/DiskUsage.php index e49b4568a3d..42f59eb0190 100644 --- a/app/Metadata/DiskUsage.php +++ b/app/Metadata/DiskUsage.php @@ -1,7 +1,20 @@ is_win()) { + if (!$this->is_win() && Helpers::isExecAvailable()) { $command = "ls -ltrR {$dir} |awk '{print $5}'|awk 'BEGIN{sum=0} {sum=sum+$1} END {print sum}' 2>&1"; exec($command, $output); $size = $output[0] ?? 0; @@ -58,13 +53,11 @@ public function getTotalSize($dir) else { if (extension_loaded('com_dotnet')) { $obj = new \COM('scripting.filesystemobject'); - if (is_object($obj)) { - $ref = $obj->getfolder($dir); - $totalSize = $ref->size; - $obj = null; + $ref = $obj->getfolder($dir); + $totalSize = $ref->size; + $obj = null; - return $totalSize; - } + return $totalSize; } } @@ -84,12 +77,12 @@ public function getTotalSize($dir) * * @return string */ - public function get_total_space() + public function get_total_space(): string { - //TODO : FIX TO USE STORAGE FACADE => uploads may not be in public/uploads + // TODO : FIX TO USE STORAGE FACADE => uploads may not be in public/uploads $dts = disk_total_space(base_path('')); - return $this->getSymbolByQuantity($dts); + return Helpers::getSymbolByQuantity($dts); } /** @@ -97,12 +90,12 @@ public function get_total_space() * * @return string */ - public function get_free_space() + public function get_free_space(): string { - //TODO : FIX TO USE STORAGE FACADE => uploads may not be in public/uploads + // TODO : FIX TO USE STORAGE FACADE => uploads may not be in public/uploads $dfs = disk_free_space(base_path('')); - return $this->getSymbolByQuantity($dfs); + return Helpers::getSymbolByQuantity($dfs); } /** @@ -110,9 +103,9 @@ public function get_free_space() * * @return string */ - public function get_free_percent() + public function get_free_percent(): string { - //TODO : FIX TO USE STORAGE FACADE => uploads may not be in public/uploads + // TODO : FIX TO USE STORAGE FACADE => uploads may not be in public/uploads $dts = disk_total_space(base_path('')); $dfs = disk_free_space(base_path('')); @@ -124,11 +117,11 @@ public function get_free_percent() * * @return string */ - public function get_lychee_space() + public function get_lychee_space(): string { $ds = $this->getTotalSize(base_path('')); - return $this->getSymbolByQuantity($ds); + return Helpers::getSymbolByQuantity($ds); } /** @@ -136,11 +129,10 @@ public function get_lychee_space() * * @return string */ - public function get_lychee_upload_space() + public function get_lychee_upload_space(): string { - //TODO : FIX TO USE STORAGE FACADE => uploads may not be in public/uploads - $ds = $this->getTotalSize(base_path('public/uploads/')); + $ds = $this->getTotalSize(Storage::disk('images')->path('')); - return $this->getSymbolByQuantity($ds); + return Helpers::getSymbolByQuantity($ds); } } diff --git a/app/Metadata/Extractor.php b/app/Metadata/Extractor.php index 9d1eee140b1..5082d043e24 100644 --- a/app/Metadata/Extractor.php +++ b/app/Metadata/Extractor.php @@ -1,352 +1,345 @@ '', - 'width' => 0, - 'height' => 0, - 'title' => '', - 'description' => '', - 'orientation' => '', - 'iso' => '', - 'aperture' => '', - 'make' => '', - 'model' => '', - 'shutter' => '', - 'focal' => '', - 'taken_at' => null, - 'lens' => '', - 'tags' => '', - 'position' => '', - 'latitude' => null, - 'longitude' => null, - 'altitude' => null, - 'imgDirection' => null, - 'location' => null, - 'filesize' => 0, - 'livePhotoContentID' => null, - 'livePhotoStillImageTime' => null, - 'MicroVideoOffset' => null, - ]; - } + public const SUFFIX_MM_UNIT = ' mm'; + public const SUFFIX_SEC_UNIT = ' s'; + public const ABSOLUTE_ALTITUDE_BOUNDS = 999999.9999; + public const MAX_LOCATION_STRING_LENGTH = 255; - /** - * Returns the size of a file in bytes. - * - * @param string $path The relative file path - * - * @return int The file size in bytes or 0 in case of a failure - */ - public function filesize(string $path): int - { - return (int) filesize($path); - /*$size = $filesize_raw / 1024; - if ($size >= 1024) { - $metadata['filesize'] = round($size / 1024, 1) . ' MB'; - } else { - $metadata['filesize'] = round($size, 1) . ' KB'; - }*/ - } + public ?string $type = null; + public int $width = 0; + public int $height = 0; + public ?string $title = null; + public ?string $description = null; + public int $orientation = 1; + public ?string $iso = null; + public ?string $aperture = null; + public ?string $make = null; + public ?string $model = null; + public ?string $shutter = null; + public ?string $focal = null; + public ?Carbon $taken_at = null; + public ?string $lens = null; + /** @var string[] */ + public array $tags = []; + /** @var string|null TODO: What is the difference to {@link Extractor::$location}? */ + public ?string $position = null; + public ?float $latitude = null; + public ?float $longitude = null; + public ?float $altitude = null; + public ?float $imgDirection = null; + /** @var string|null TODO: What is the difference to {@link Extractor::$position}? */ + public ?string $location = null; + public ?string $livePhotoContentID = null; + public int $microVideoOffset = 0; /** - * Extracts metadata from an image file. + * Extracts metadata from a file. + * + * @param NativeLocalFile $file the file + * @param int $fileLastModifiedTime the timestamp to use if there's no creation date in Exif * - * @param string $filename - * @param string file kind + * @return Extractor * - * @return array + * @throws ExternalComponentMissingException + * @throws MediaFileOperationException */ - public function extract(string $filename, string $kind): array + public static function createFromFile(NativeLocalFile $file, int $fileLastModifiedTime): self { - $reader = null; + $metadata = new self(); + $isSupportedVideo = $file->isSupportedVideo(); - // Get kind of file (photo, video, raw) - $extension = Helpers::getExtension($filename, false); - - // check raw files - $is_raw = false; - $raw_formats = strtolower(Configs::get_value('raw_formats', '')); - if (in_array(strtolower($extension), explode('|', $raw_formats), true)) { - $is_raw = true; - } + try { + // Priority of EXIF data readers is + // 1. FFMpeg (only for videos) + // 2. Exiftool + // 3. Imagick (not for videos, i.e. for supported photos and accepted raw files only) + // 4. Native PHP exif reader (last resort) + $reader = match (true) { + (Configs::hasFFmpeg() && $isSupportedVideo) => Reader::factory(ReaderType::FFPROBE, Configs::getValueAsString('ffprobe_path')), + Configs::hasExiftool() => Reader::factory(ReaderType::EXIFTOOL, Configs::getValueAsString('exiftool_path')), + (Configs::hasImagick() && !$isSupportedVideo) => Reader::factory(ReaderType::IMAGICK), + default => Reader::factory(ReaderType::NATIVE), + }; - if ($kind !== 'video') { - // It's a photo - if (Configs::hasExiftool()) { - // reader with Exiftool adapter - $reader = Reader::factory(Reader::TYPE_EXIFTOOL); - } elseif (Configs::hasImagick() && $is_raw) { - // Use imagick as exif reader for raw files (broader support) - $reader = Reader::factory(Reader::TYPE_IMAGICK); - } else { + // this can throw an exception in the case of Exiftool adapter! + // TODO: This may fail for files without an extension. + // In particular, PHPExif uses another method again to determine + // the MIME type of a file. + // For example, the adapter `PHPExif\Adapter\FFprobe` uses + // `mime_content_type`, but our upload controller uses the + // Symfony MIME utilities. + // The adapter `PHPExif\Adapter\FFprobe` has already been equipped + // with a work-around for MP4 videos which are wrongly classified + // as `application/octet-stream`, but this work-around only + // succeeds if the file has a recognized extension. + $exif = $reader->read($file->getRealPath()); + // @codeCoverageIgnoreStart + } catch (PhpExifReaderException $e) { + // thrown by $reader->read if EXIF could not be extracted, + // don't give up yet, only log the event + Handler::reportSafely($e); + try { + Log::notice(__METHOD__ . ':' . __LINE__ . ' Falling back to native adapter.'); // Use Php native tools - $reader = Reader::factory(Reader::TYPE_NATIVE); - } - } else { - // Let's try to use FFmpeg; if not available, let's try Exiftool - if (Configs::hasFFmpeg()) { - // It's a video -> use FFProbe - $reader = Reader::factory(Reader::TYPE_FFPROBE); - } elseif (Configs::hasExiftool()) { - // reader with Exiftool adapter - $reader = Reader::factory(Reader::TYPE_EXIFTOOL); - } else { - // Use Php native tools to extract at least MimeType and Filesize - // For all other properties, it will not return anything - $reader = Reader::factory(Reader::TYPE_NATIVE); - Logs::notice(__METHOD__, __LINE__, 'FFmpeg and Exiftool not being available; Extraction of metadata limited to mime type and file size.'); + $reader = Reader::factory(ReaderType::NATIVE); + $exif = $reader->read($file->getRealPath()); + } catch (PhpExifReaderException $e) { + // thrown by $reader->read if EXIF could not be extracted, + // even with the native adapter, now we give up + throw new MediaFileOperationException('Could not even extract basic EXIF data with the native adapter', $e); } } - - try { - // this can throw an exception in the case of Exiftool adapter! - $exif = $reader->read($filename); - } catch (\Exception $e) { - Logs::error(__METHOD__, __LINE__, $e->getMessage()); - $exif = false; - } - - if ($exif === false) { - Logs::notice(__METHOD__, __LINE__, 'Falling back to native adapter.'); - // Use Php native tools - $reader = Reader::factory(Reader::TYPE_NATIVE); - $exif = $reader->read($filename); - } + // @codeCoverageIgnoreEnd // Attempt to get sidecar metadata if it exists, make sure to check 'real' path in case of symlinks $sidecarData = []; - // readlink fails if it's not a link -> we need to separate it - $realFile = $filename; - if (is_link($filename)) { - try { - // if readlink($filename) == False then $realFile = $filename. - // if readlink($filename) != False then $realFile = readlink($filename) - $realFile = readlink($filename) ?: $filename; - } catch (\Exception $e) { - Logs::error(__METHOD__, __LINE__, $e->getMessage()); - } - } - if (Configs::hasExiftool() && file_exists($realFile . '.xmp')) { + $sidecarFile = new NativeLocalFile($file->getPath() . '.xmp'); + + if (Configs::hasExiftool() && $sidecarFile->exists()) { + // @codeCoverageIgnoreStart try { // Don't use the same reader as the file in case it's a video - $sidecarReader = Reader::factory(Reader::TYPE_EXIFTOOL); - $sidecarData = $sidecarReader->read($realFile . '.xmp')->getData(); + $sidecarReader = Reader::factory(ReaderType::EXIFTOOL); + $sideCarExifData = $sidecarReader->read($sidecarFile->getRealPath()); + $sidecarData = $sideCarExifData->getData(); // We don't want to overwrite the media's type with the mimetype of the sidecar file unset($sidecarData['MimeType']); - if (Configs::get_value('prefer_available_xmp_metadata', '0') == '1') { + if (Configs::getValueAsBool('prefer_available_xmp_metadata')) { $exif->setData(array_merge($exif->getData(), $sidecarData)); } else { $exif->setData(array_merge($sidecarData, $exif->getData())); } } catch (\Exception $e) { - Logs::error(__METHOD__, __LINE__, $e->getMessage()); + Handler::reportSafely($e); } + // @codeCoverageIgnoreEnd } - $metadata = $this->bare(); - $metadata['type'] = ($exif->getMimeType() !== false) ? $exif->getMimeType() : ''; - $metadata['width'] = ($exif->getWidth() !== false) ? $exif->getWidth() : 0; - $metadata['height'] = ($exif->getHeight() !== false) ? $exif->getHeight() : 0; - $metadata['title'] = ($exif->getTitle() !== false) ? $exif->getTitle() : ''; - $metadata['description'] = ($exif->getDescription() !== false) ? $exif->getDescription() : ''; - $metadata['orientation'] = ($exif->getOrientation() !== false) ? $exif->getOrientation() : ''; - $metadata['iso'] = ($exif->getIso() !== false) ? $exif->getIso() : ''; - $metadata['make'] = ($exif->getMake() !== false) ? $exif->getMake() : ''; - $metadata['model'] = ($exif->getCamera() !== false) ? $exif->getCamera() : ''; - $metadata['shutter'] = ($exif->getExposure() !== false) ? $exif->getExposure() : ''; - $metadata['lens'] = ($exif->getLens() !== false) ? $exif->getLens() : ''; - $metadata['tags'] = ($exif->getKeywords() !== false) ? (is_array($exif->getKeywords()) ? implode(',', $exif->getKeywords()) : $exif->getKeywords()) : ''; - $metadata['latitude'] = ($exif->getLatitude() !== false) ? $exif->getLatitude() : null; - $metadata['longitude'] = ($exif->getLongitude() !== false) ? $exif->getLongitude() : null; - $metadata['altitude'] = ($exif->getAltitude() !== false) ? $exif->getAltitude() : null; - $metadata['imgDirection'] = ($exif->getImgDirection() !== false) ? $exif->getImgDirection() : null; - $metadata['filesize'] = ($exif->getFileSize() !== false) ? $exif->getFileSize() : 0; - $metadata['livePhotoContentID'] = ($exif->getContentIdentifier() !== false) ? $exif->getContentIdentifier() : null; - $metadata['MicroVideoOffset'] = ($exif->getMicroVideoOffset() !== false) ? $exif->getMicroVideoOffset() : null; + $metadata->type = ($exif->getMimeType() !== false) ? $exif->getMimeType() : $file->getMimeType(); + $metadata->width = ($exif->getWidth() !== false) ? (int) $exif->getWidth() : 0; + $metadata->height = ($exif->getHeight() !== false) ? (int) $exif->getHeight() : 0; + $metadata->title = ($exif->getTitle() !== false) ? $exif->getTitle() : null; + $metadata->description = ($exif->getDescription() !== false) ? $exif->getDescription() : null; + $metadata->orientation = ($exif->getOrientation() !== false) ? (int) $exif->getOrientation() : 1; + $metadata->iso = ($exif->getIso() !== false) ? $exif->getIso() : null; + $metadata->make = ($exif->getMake() !== false) ? $exif->getMake() : null; + $metadata->model = ($exif->getCamera() !== false) ? $exif->getCamera() : null; + $metadata->shutter = ($exif->getExposure() !== false) ? $exif->getExposure() : null; + $metadata->lens = ($exif->getLens() !== false) ? $exif->getLens() : null; + $metadata->tags = ($exif->getKeywords() !== false) ? $exif->getKeywords() : []; + $metadata->latitude = ($exif->getLatitude() !== false) ? $exif->getLatitude() : null; + $metadata->longitude = ($exif->getLongitude() !== false) ? $exif->getLongitude() : null; + $metadata->altitude = ($exif->getAltitude() !== false) ? $exif->getAltitude() : null; + $metadata->imgDirection = ($exif->getImgDirection() !== false) ? $exif->getImgDirection() : null; + $metadata->livePhotoContentID = ($exif->getContentIdentifier() !== false) ? $exif->getContentIdentifier() : null; + $metadata->microVideoOffset = ($exif->getMicroVideoOffset() !== false) ? (int) $exif->getMicroVideoOffset() : 0; $taken_at = $exif->getCreationDate(); + + if ($taken_at === false && + Configs::getValueAsBool('use_last_modified_date_when_no_exif_date')) { + $taken_at = DateTime::createFromFormat('U', "$fileLastModifiedTime"); + } + if ($taken_at !== false) { - // There are three different timezone which needs to considered: - // - // a) The original timezone of the location where the photo has - // been taken - // b) The timezone of the server which is running the Lychee - // backend - // c) The timezone of the beholder who is looking at the photo - // with his/her/their web browser - // - // **Notes about a):** - // - // For best human interaction with photos the date/time when the - // photo has been taken should be based on the local timezone of - // the location where the photo has been taken. - // This matches the beholder's expectation; e.g. a photo of a - // sunset should show a "wall time" around the early evening, - // while a breakfast photo should show a "wall time" in the - // morning. - // Contrary, for handling photos programmatically, timestamps - // (in UTC) are best. - // Unfortunately, the EXIF specification prior to version 2.31 - // did not consider timezone information and only defined - // tag #9003 "DateTimeOriginal" which uses the string format - // "YYYY-MM-DD hh:mm:ss" _without_ timezone information. - // Moreover, the specification left open, if this string should - // represent a "wall time" relative to the local timezone of the - // location where the photo has been taken or a UTC-based time. - // As most cameras for still photography have just a dumb - // timezone-unaware clock, they simply store that time. - // This time is most probably the "wall time" in the local - // timezone assuming that the owner of the camera has set the - // correct time. - // For videos the situation is a little bit different. - // Some video cameras store creation time in local time while - // others use UTC and it's often impossible to tell, especially - // since the metadata extractors are not consistent either. - // Since 2016 and EXIF 2.31 the situation has improved. - // Next to the old tag "DateTimeOriginal" EXIF 2.31 also includes - // GPS datetime information and GPS time offset. - // On top, there is XMP which has been created by Adobe but is - // now an ISO standard and always included timezone information - // as part of the specification. - // - // Here, we rely here on a simple filetype-based heuristics and, - // for a timestamp we suspect to be in UTC, we convert it to the - // application's default timezone. - // All other timestamps are not altered, but used "as is": - // - // i) Either the meta-data extractor was able to properly - // extract a timezone information (good case), or - // ii) the meta-data extractor returned a \DateTime object which - // uses the application's default timezone due to the EXIF - // date lacking an explicit timezone (bad case). - // - // In the "bad case", the shown "wall time" relative to the - // application's default timezone matches the EXIF time. - // This approach implicitly assumes that the beholder of the photo - // in front of the GUI uses the same timezone as the backend - // and thus sees the correct "wall time" which is consistent to - // content of the photo. - // - // Other possible approaches would include deriving the original - // timezone from the file name or from other objects in the same - // album, as well as extracting the timezone from the location - // data if present. - // The latter is what the "big players" like Google Photo or - // Apple do. - // TODO: Implement timezone derivation from location data. - // See [this StackOverflow answer](https://stackoverflow.com/a/16086964/2690527) - // for a fairly comprehensive overview of available options. - // The [Geo-Timezone PHP Library](https://github.com/minube/geo-timezone) - // seems to be the most accurate one and does not depend on an - // external web-service. - // Unfortunately, it is not an simple PHP library which can be - // pulled in as a Composer dependencies, but requires a binary - // PHP extension (`geos.so`). - // - // **Notes about b):** - // - // With respect to the beholder, b) is irrelevant. - // However, please be aware that there is not necessarily a single - // server timezone, but actually three. - // The timezone of the server OS, the configured timezone of the - // PHP application and the timezone of SQL connection to the SQL - // server. - // Those three timezone are not necessarily identical, especially - // not, if the Lychee application and the SQL server are running - // on different machines. - // {@link App\Models\PatchedBaseModel} takes care that - // all timestamps are (de-)hydrated as UTC timestamps. - // Moreover, {@link App\Models\Photo} ensures that the original - // timezone information of the datetime when the photo has been - // taken is stored. - // - // **Notes about c):** - // - // The datetime is sent from the web backend to the client using - // the JSON (aka ISO 8601) format incl. the correct time-offset - // (e.g. 20210519T211643+02:00). - // On top, the original timezone is sent to the client as - // the string attribute `taken_at_orig_tz` which either is - // - // - a named timezone like "Europe/Paris" (most accurate), - // - a timezone abbreviation like "CEST" (central european summer - // time, less accurate), or - // - a time offset like "+02:00" (least accurate), - // - // whatever the metadata extractor was able to extract from the - // media file. - // In theory, this give the GUI to show the datetime of creation - // either - // - // a) relative to the original timezone (probably the most - // useful option), - // b) relative to UTC, or - // c) relative to the beholder's own, local timezone. - // - // Note 1: At the moment, the "original timezone" typically is not - // the "true" original timezone, but the configured - // default timezone of the PHP application (see notes - // about a). - // Note 2: We do not set the the attribute `taken_at_orig_tz` - // here. - // This is the responsibility of {@link App\Models\Photo}. - // At the layer of the "business logic" we only use - // the attribute `taken_at` which extends - // \DateTimeInterface and stores the timezone. - if ($kind === 'video') { - $locals = strtolower(Configs::get_value('local_takestamp_video_formats', '')); - if (!in_array(strtolower($extension), explode('|', $locals), true)) { - // This is a video format where we expect the takestamp - // to be provided in UTC. - if ($taken_at->getTimezone()->getName() === date_default_timezone_get()) { - // Most likely the time zone info was missing so the - // system default was used instead, which is wrong, - // because the recording time is actually in UTC. - // This will trigger, e.g., for mp4 files with the - // Exiftool extractor. - // We recreate the recording time as a UTC timestamp - // and _then_ change the timezone to the application's - // default timezone. - // Note: This assumes that the application's default - // timezone is the same as the timezone of the - // location where the video has been recorded and that - // the beholder (of the video) expects to observe - // that timezone. - $taken_at = new \DateTime( - $taken_at->format('Y-m-d H:i:s'), - new \DateTimeZone('UTC') - ); - $taken_at->setTimezone(new \DateTimeZone(date_default_timezone_get())); - } elseif ($taken_at->getTimezone()->getName() === 'Z') { - // This one is correctly in Zulu (UTC). - // We change the timezone to the application's default - // timezone and convert the time. - // Note: This assumes that the application's default - // timezone is the same as the timezone of the - // location where the video has been recorded and that - // the beholder (of the video) expects to observe - // that timezone. - $taken_at->setTimezone(new \DateTimeZone(date_default_timezone_get())); - } + try { + $taken_at = Carbon::instance($taken_at); + // There are three different timezone which needs to considered: + // + // a) The original timezone of the location where the photo has + // been taken + // b) The timezone of the server which is running the Lychee + // backend + // c) The timezone of the beholder who is looking at the photo + // with his/her/their web browser + // + // **Notes about a):** + // + // For best human interaction with photos the date/time when the + // photo has been taken should be based on the local timezone of + // the location where the photo has been taken. + // This matches the beholder's expectation; e.g. a photo of a + // sunset should show a "wall time" around the early evening, + // while a breakfast photo should show a "wall time" in the + // morning. + // Contrary, for handling photos programmatically, timestamps + // (in UTC) are best. + // Unfortunately, the EXIF specification prior to version 2.31 + // did not consider timezone information and only defined + // tag #9003 "DateTimeOriginal" which uses the string format + // "YYYY-MM-DD hh:mm:ss" _without_ timezone information. + // Moreover, the specification left open, if this string should + // represent a "wall time" relative to the local timezone of the + // location where the photo has been taken or a UTC-based time. + // As most cameras for still photography have just a dumb + // timezone-unaware clock, they simply store that time. + // This time is most probably the "wall time" in the local + // timezone assuming that the owner of the camera has set the + // correct time. + // For videos the situation is a little bit different. + // Some video cameras store creation time in local time while + // others use UTC and it's often impossible to tell, especially + // since the metadata extractors are not consistent either. + // Since 2016 and EXIF 2.31 the situation has improved. + // Next to the old tag "DateTimeOriginal" EXIF 2.31 also includes + // GPS datetime information and GPS time offset. + // On top, there is XMP which has been created by Adobe but is + // now an ISO standard and always included timezone information + // as part of the specification. + // + // Here, we rely here on a simple filetype-based heuristics and, + // for a timestamp we suspect to be in UTC, we convert it to the + // application's default timezone. For a timestamp we suspect to + // be local, we ensure that the extractor didn't erroneously mark + // it as UTC. + // All other timestamps are not altered, but used "as is": + // + // i) Either the meta-data extractor was able to properly + // extract a timezone information (good case), or + // ii) the meta-data extractor returned a \DateTime object which + // uses the application's default timezone due to the EXIF + // date lacking an explicit timezone (bad case). + // + // In the "bad case", the shown "wall time" relative to the + // application's default timezone matches the EXIF time. + // This approach implicitly assumes that the beholder of the photo + // in front of the GUI uses the same timezone as the backend + // and thus sees the correct "wall time" which is consistent to + // content of the photo. + // + // Other possible approaches would include deriving the original + // timezone from the file name or from other objects in the same + // album, as well as extracting the timezone from the location + // data if present. + // The latter is what the "big players" like Google Photo or + // Apple do. + // TODO: Implement timezone derivation from location data. + // See [this StackOverflow answer](https://stackoverflow.com/a/16086964/2690527) + // for a fairly comprehensive overview of available options. + // The [Geo-Timezone PHP Library](https://github.com/minube/geo-timezone) + // seems to be the most accurate one and does not depend on an + // external web-service. + // Unfortunately, it is not an simple PHP library which can be + // pulled in as a Composer dependencies, but requires a binary + // PHP extension (`geos.so`). + // + // **Notes about b):** + // + // With respect to the beholder, b) is irrelevant. + // However, please be aware that there is not necessarily a single + // server timezone, but actually three. + // The timezone of the server OS, the configured timezone of the + // PHP application and the timezone of SQL connection to the SQL + // server. + // Those three timezone are not necessarily identical, especially + // not, if the Lychee application and the SQL server are running + // on different machines. + // {@link App\Models\PatchedBaseModel} takes care that + // all timestamps are (de-)hydrated as UTC timestamps. + // Moreover, {@link App\Models\Photo} ensures that the original + // timezone information of the datetime when the photo has been + // taken is stored. + // + // **Notes about c):** + // + // The datetime is sent from the web backend to the client using + // the JSON (aka ISO 8601) format incl. the correct time-offset + // (e.g. 20210519T211643+02:00). + // On top, the original timezone is sent to the client as + // the string attribute `taken_at_orig_tz` which either is + // + // - a named timezone like "Europe/Paris" (most accurate), + // - a timezone abbreviation like "CEST" (central european summer + // time, less accurate), or + // - a time offset like "+02:00" (least accurate), + // + // whatever the metadata extractor was able to extract from the + // media file. + // In theory, this give the GUI to show the datetime of creation + // either + // + // a) relative to the original timezone (probably the most + // useful option), + // b) relative to UTC, or + // c) relative to the beholder's own, local timezone. + // + // Note 1: At the moment, the "original timezone" typically is not + // the "true" original timezone, but the configured + // default timezone of the PHP application (see notes + // about a). + // Note 2: We do not set the the attribute `taken_at_orig_tz` + // here. + // This is the responsibility of {@link App\Models\Photo}. + // At the layer of the "business logic" we only use + // the attribute `taken_at` which extends + // \DateTimeInterface and stores the timezone. + if ($isSupportedVideo) { + $locals = strtolower(Configs::getValueAsString('local_takestamp_video_formats')); + if (!in_array(strtolower($file->getExtension()), explode('|', $locals), true)) { + // This is a video format where we expect the takestamp + // to be provided in UTC. + if ($taken_at->getTimezone()->getName() === date_default_timezone_get()) { + // Most likely the time zone info was missing so the + // system default was used instead, which is wrong, + // because the recording time is actually in UTC. + // This will trigger, e.g., for mp4 files with the + // Exiftool extractor. + // We recreate the recording time as a UTC timestamp + // and _then_ change the timezone to the application's + // default timezone. + // Note: This assumes that the application's default + // timezone is the same as the timezone of the + // location where the video has been recorded and that + // the beholder (of the video) expects to observe + // that timezone. + $taken_at = new Carbon( + $taken_at->format('Y-m-d H:i:s'), + new \DateTimeZone('UTC') + ); + $taken_at->setTimezone(new \DateTimeZone(date_default_timezone_get())); + } elseif ($taken_at->getTimezone()->getName() === 'Z') { + // This one is correctly in Zulu (UTC). + // We change the timezone to the application's default + // timezone and convert the time. + // Note: This assumes that the application's default + // timezone is the same as the timezone of the + // location where the video has been recorded and that + // the beholder (of the video) expects to observe + // that timezone. + $taken_at->setTimezone(new \DateTimeZone(date_default_timezone_get())); + } // In the remaining cases the timezone information was // extracted and the recording time is assumed exhibit // to original timezone of the location where the video @@ -356,42 +349,57 @@ public function extract(string $filename, string $kind): array // The only known example are the mov files from Apple // devices; the time zone will be formatted as "+01:00" // so neither of the two conditions above should trigger. + } elseif ($taken_at->getTimezone()->getName() === 'Z') { + // This is a video format where we expect the takestamp + // to be provided in local time but the timezone is + // (erroneously) set to Zulu (UTC). This will trigger, + // e.g., for mov files with the FFprobe extractor. + // We recreate the recording time as a timestamp in the + // application's default timezone. + // Note: This assumes that the application's default + // timezone is the same as the timezone of the + // location where the video has been recorded and that + // the beholder (of the video) expects to observe + // that timezone. + $taken_at = new Carbon( + $taken_at->format('Y-m-d H:i:s'), + new \DateTimeZone(date_default_timezone_get()) + ); + } } + $metadata->taken_at = $taken_at; + } catch (InvalidTimeZoneException|InvalidFormatException $e) { + throw new MediaFileOperationException('Could not even extract date/time from EXIF data', $e); } - $metadata['taken_at'] = Carbon::instance($taken_at); } else { - $metadata['taken_at'] = null; + $metadata->taken_at = null; } // We need to make sure, latitude is between -90/90 and longitude is between -180/180 // We set values to null in case we're out of bounds - if ($metadata['latitude'] !== null || $metadata['longitude'] !== null) { - $latitude = $metadata['latitude']; - $longitude = $metadata['longitude']; - if ($latitude < -90 || $latitude > 90 || $longitude < -180 || $longitude > 180) { - $metadata['latitude'] = null; - $metadata['longitude'] = null; - Logs::notice(__METHOD__, __LINE__, 'Latitude/Longitude (' . $latitude . '/' . $longitude . ') out of bounds (needs to be between -90/90 and -180/180)'); + if ($metadata->latitude !== null || $metadata->longitude !== null) { + if ($metadata->latitude < -90 || $metadata->latitude > 90 || $metadata->longitude < -180 || $metadata->longitude > 180) { + Log::notice(__METHOD__ . ':' . __LINE__ . 'Latitude/Longitude (' . $metadata->latitude . '/' . $metadata->longitude . ') out of bounds (needs to be between -90/90 and -180/180)'); + $metadata->latitude = null; + $metadata->longitude = null; } } // We need to make sure, altitude is between -999999.9999 and 999999.9999 // We set values to null in case we're out of bounds - if ($metadata['altitude'] !== null) { - $altitude = $metadata['altitude']; - if ($altitude < -999999.9999 || $altitude > 999999.9999) { - $metadata['altitude'] = null; - Logs::notice(__METHOD__, __LINE__, 'Altitude (' . $altitude . ') out of bounds for database (needs to be between -999999.9999 and 999999.9999)'); + if ($metadata->altitude !== null) { + if ($metadata->altitude < -self::ABSOLUTE_ALTITUDE_BOUNDS || $metadata->altitude > self::ABSOLUTE_ALTITUDE_BOUNDS) { + Log::notice(__METHOD__ . ':' . __LINE__ . 'Altitude (' . $metadata->altitude . ') out of bounds for database (needs to be between -999999.9999 and 999999.9999)'); + $metadata->altitude = null; } } // We need to make sure, imgDirection is between 0 and 360 // We set values to null in case we're out of bounds - if ($metadata['imgDirection'] !== null) { - $imgDirection = $metadata['imgDirection']; - if ($imgDirection < 0 || $imgDirection > 360) { - $metadata['imgDirection'] = null; - Logs::notice(__METHOD__, __LINE__, 'GPSImgDirection (' . $imgDirection . ') out of bounds (needs to be between 0 and 360)'); + if ($metadata->imgDirection !== null) { + if ($metadata->imgDirection < 0 || $metadata->imgDirection > 360) { + Log::notice(__METHOD__ . ':' . __LINE__ . 'GPSImgDirection (' . $metadata->imgDirection . ') out of bounds (needs to be between 0 and 360)'); + $metadata->imgDirection = null; } } @@ -409,36 +417,47 @@ public function extract(string $filename, string $kind): array if ($exif->getCountry() !== false) { $fields[] = trim($exif->getCountry()); } - if (!empty($fields)) { - $metadata['position'] = implode(', ', $fields); + if (count($fields) !== 0) { + $metadata->position = implode(', ', $fields); } - if ($kind !== 'video') { - $metadata['aperture'] = ($exif->getAperture() !== false) ? $exif->getAperture() : ''; - $metadata['focal'] = ($exif->getFocalLength() !== false) ? $exif->getFocalLength() : ''; - if ($metadata['focal'] !== '') { - $metadata['focal'] = round($metadata['focal']) . ' mm'; + if (!$isSupportedVideo) { + // Media is either a supported photo or an accepted raw file: + // properly format aperture and focal + $metadata->aperture = ($exif->getAperture() !== false) ? $exif->getAperture() : null; + $metadata->focal = ($exif->getFocalLength() !== false) ? $exif->getFocalLength() : null; + if ($metadata->focal !== null) { + $metadata->focal = round(floatval($metadata->focal)) . self::SUFFIX_MM_UNIT; } } else { - // Video -> reuse fields - $metadata['aperture'] = ($exif->getDuration() !== false) ? $exif->getDuration() : ''; - $metadata['focal'] = ($exif->getFramerate() !== false) ? $exif->getFramerate() : ''; + // Media is a video: Reuse (exploit) fields aperture and focal for duration and framerate + $metadata->aperture = ($exif->getDuration() !== false) ? $exif->getDuration() : null; + $metadata->focal = ($exif->getFramerate() !== false) ? $exif->getFramerate() : null; } - if ($metadata['title'] == '') { - $metadata['title'] = ($exif->getHeadline() !== false) ? $exif->getHeadline() : ''; + if ($metadata->title === null || $metadata->title === '') { + $metadata->title = ($exif->getHeadline() !== false) ? $exif->getHeadline() : null; } - if ($metadata['shutter'] !== '') { - $metadata['shutter'] = $metadata['shutter'] . ' s'; + if ($metadata->shutter !== null && $metadata->shutter !== '') { + // TODO: If we add the suffix " s" here, we should also normalize the fraction here. + // It does not make any sense to strip-off the suffix again in Photo and re-add it again. + $metadata->shutter .= self::SUFFIX_SEC_UNIT; } // Decode location data, it can be longer than is acceptable for DB that's the reason for substr // but only if return value is not null (= function has been disabled) - $metadata['location'] = Geodecoder::decodeLocation($metadata['latitude'], $metadata['longitude']); - if (is_null($metadata['location']) == false) { - $metadata['location'] = substr($metadata['location'], 0, 255); + try { + $metadata->location = Geodecoder::decodeLocation($metadata->latitude, $metadata->longitude); + if ($metadata->location !== null) { + $metadata->location = substr($metadata->location, 0, self::MAX_LOCATION_STRING_LENGTH); + } + // @codeCoverageIgnoreStart + } catch (ExternalComponentFailedException|StringsException $e) { + Handler::reportSafely($e); + $metadata->location = null; } + // @codeCoverageIgnoreEnd return $metadata; } diff --git a/app/Metadata/Geodecoder.php b/app/Metadata/Geodecoder.php index 95eb8e8a477..6afa807e295 100644 --- a/app/Metadata/Geodecoder.php +++ b/app/Metadata/Geodecoder.php @@ -1,56 +1,73 @@ push(RateLimiterMiddleware::perSecond(1)); + try { + $stack = HandlerStack::create(); + $stack->push(RateLimiterMiddleware::perSecond(1)); - $httpClient = new \GuzzleHttp\Client([ - 'handler' => $stack, - 'timeout' => Configs::get_value('location_decoding_timeout'), - ]); + $httpClient = new \GuzzleHttp\Client([ + 'handler' => $stack, + 'timeout' => Configs::getValueAsInt('location_decoding_timeout'), + ]); - $httpAdapter = new \Http\Adapter\Guzzle7\Client($httpClient); + $httpAdapter = new \Http\Adapter\Guzzle7\Client($httpClient); - $provider = new Nominatim($httpAdapter, 'https://nominatim.openstreetmap.org', config('app.name')); + $provider = new Nominatim($httpAdapter, 'https://nominatim.openstreetmap.org', config('app.name')); - return new ProviderCache($provider, app('cache.store')); + return new ProviderCache($provider, app('cache.store')); + } catch (GeocoderException|GuzzleException|\RuntimeException|BindingResolutionException|\InvalidArgumentException $e) { + throw new ExternalComponentFailedException('Could not create geocoder provider', $e); + } } /** * Decode GPS coordinates into location. * - * @return string location + * @param ?float $latitude + * @param ?float $longitude + * + * @return ?string location + * + * @throws ExternalComponentFailedException */ - public static function decodeLocation($latitude, $longitude) + public static function decodeLocation(?float $latitude, ?float $longitude): ?string { // User does not want to decode location data - if (Configs::get_value('location_decoding') == false) { + if (!Configs::getValueAsBool('location_decoding')) { return null; } - if ($latitude == null || $longitude == null) { + if ($latitude === null || $longitude === null) { return null; } @@ -62,42 +79,31 @@ public static function decodeLocation($latitude, $longitude) /** * Wrapper to decode GPS coordinates into location. * - * @return string location + * @param float $latitude + * @param float $longitude + * @param ProviderCache $cachedProvider + * + * @return ?string location + * + * @throws LocationDecodingFailed */ - public static function decodeLocation_core($latitude, $longitude, $cachedProvider) + public static function decodeLocation_core(float $latitude, float $longitude, ProviderCache $cachedProvider): ?string { - $geocoder = new StatefulGeocoder($cachedProvider, Configs::get_value('lang')); + $lang = Configs::getValueAsString('lang'); + $geocoder = new StatefulGeocoder($cachedProvider, $lang); try { $result_list = $geocoder->reverseQuery(ReverseQuery::fromCoordinates($latitude, $longitude)); // If no result has been returned -> return null if ($result_list->isEmpty()) { - Logs::warning(__METHOD__, __LINE__, 'Location (' . $latitude . ', ' . $longitude . ') could not be decoded.'); - - return null; + throw new LocationDecodingFailed('Location (' . $latitude . ', ' . $longitude . ') could not be decoded.'); } return $result_list->first()->getDisplayName(); // @codeCoverageIgnoreStart - } catch (\Exception $exception) { - Logs::warning(__METHOD__, __LINE__, 'Decoding of location failed!'); - Logs::warning(__METHOD__, __LINE__, $exception->getMessage()); - - return null; + } catch (GeocoderException $e) { + throw new LocationDecodingFailed('Location (' . $latitude . ', ' . $longitude . ') could not be decoded.', $e); } // @codeCoverageIgnoreEnd } } - -class RateLimiterStore implements Store -{ - public function get(): array - { - return Cache::get('rate-limiter', []); - } - - public function push(int $timestamp, int $limit) - { - Cache::put('rate-limiter', array_merge($this->get(), [$timestamp])); - } -} diff --git a/app/Metadata/GitHubFunctions.php b/app/Metadata/GitHubFunctions.php deleted file mode 100644 index b19d6960123..00000000000 --- a/app/Metadata/GitHubFunctions.php +++ /dev/null @@ -1,258 +0,0 @@ -gitRequest = $gitRequest; - try { - $this->branch = $this->get_current_branch(); - $this->head = $this->get_current_commit(); - // @codeCoverageIgnoreStart - // when testing on master branch this is not covered. - } catch (Exception $e) { - $this->branch = false; - $this->head = false; - } - // @codeCoverageIgnoreEnd - } - - /** - * Given a commit id, return the 7 first characters (7 hex digits) and trim it to remove \n. - * - * @param $commit_id - * - * @return string - */ - private function trim($commit_id): string - { - return trim(substr($commit_id, 0, 7)); - } - - /** - * look at .git/HEAD and return the current branch. - * Return false if the file is not readable. - * - * @return false|string - */ - public function get_current_branch() - { - // @codeCoverageIgnoreStart - $head_file = base_path('.git/HEAD'); - $branch_ = file_get_contents($head_file); - //separate out by the "/" in the string - $branch_ = explode('/', $branch_, 3); - - return trim($branch_[2]); - // @codeCoverageIgnoreEnd - } - - /** - * Return the current commit id (7 hex digits). - * - * @return false|string - */ - public function get_current_commit() - { - $file = base_path('.git/refs/heads/' . $this->branch); - $head_ = file_get_contents($file); - - return $this->trim($head_); - } - - /** - * return the list of the last 30 commits on the master branch. - * - * @param bool $cached - * - * @return bool|array - * - * @throws NotInCacheException - */ - private function get_commits(bool $cached = true) - { - return $this->gitRequest->get_json($cached); - } - - /** - * Count the number of commits between current version and master/HEAD. - * Throws NotMaster if the branch is not ... master - * Throws NotInCache if the commits are not cached - * Returns between 0 and 30 if we can find the value - * Returns false if more than 30 commits behind. - * - * @param bool $cached - * - * @return bool|int - * - * @throws NotInCacheException - * @throws NotMasterException - */ - public function count_behind(bool $cached = true) - { - if ($this->branch != 'master') { - // @codeCoverageIgnoreStart - throw new NotMasterException(); - // @codeCoverageIgnoreEnd - } - - $commits = $this->get_commits($cached); - - $i = 0; - while ($i < count($commits)) { - if ($this->trim($commits[$i]->sha) == $this->head) { - break; - } - // @codeCoverageIgnoreStart - // when testing on master branch this is not covered: we are up to date. - $i++; - // @codeCoverageIgnoreEnd - } - - return ($i == count($commits)) ? false : $i; - } - - /** - * return the commit id (7 hex digits) of the had if found. - * - * @return string - */ - // @codeCoverageIgnoreStart - public function get_github_head(): string - { - try { - $commits = $this->get_commits(); - - return ' (' . $this->trim($commits[0]->sha) . ')'; - } catch (Exception $e) { - return ''; - } - } - - // @codeCoverageIgnoreEnd - - /** - * Return a string indicating whether we are up to date (used in Diagnostics). - * - * This function should not throw exceptions ! - * - * @return string - */ - public function get_behind_text(): string - { - try { - $count = $this->count_behind(); // NotInCache or NotMaster - } catch (Exception $e) { - return ' - ' . $e->getMessage(); - } - - $last_update = $this->gitRequest->get_age_text(); - - if ($count === 0) { - return sprintf(' - Up to date (%s).', $last_update); - } - // @codeCoverageIgnoreStart - if ($count != false) { - return sprintf( - ' - %s commits behind master %s (%s)', - $count, - $this->get_github_head(), - $last_update - ); - } - - return ' - Probably more than 30 commits behind master'; - // @codeCoverageIgnoreEnd - } - - /** - * Check if the repo is up to date, throw an exception if fails. - * - * @param bool $cached - * - * @return bool - * - * @throws NotMasterException - * @throws NotInCacheException - */ - public function is_up_to_date(bool $cached = true): bool - { - $count = $this->count_behind($cached); - if ($count === 0) { - return true; - } - - // @codeCoverageIgnoreStart - return false; - // @codeCoverageIgnoreEnd - } - - /** - * Simple check if git is usable or not. - * - * @return bool - */ - public function has_permissions(): bool - { - if (!$this->branch) { - // @codeCoverageIgnoreStart - return false; - // @codeCoverageIgnoreEnd - } else { - return Helpers::hasFullPermissions(base_path('.git')) && Helpers::hasPermissions(base_path('.git/refs/heads/' . $this->branch)); - } - } - - /** - * Check for updates (old). - * - * @param $return - */ - public function checkUpdates(&$return): void - { - // add a setting to do this check only once per day ? - if (Configs::get_value('check_for_updates', '0') == '1') { - $json = new JsonRequestFunctions(Config::get('urls.update.json')); - $json = $json->get_json(); - if ($json != false) { - /* @noinspection PhpUndefinedFieldInspection */ - $return['update_json'] = $json->lychee->version; - $return['update_available'] - = ((intval(Configs::get_value('version', '40000'))) - < $return['update_json']); - } - } - } - - /** - * Return true if the current branch is master. - * This is used to avoid running git pulls on development branches during tests. - * - * @return bool - */ - public function is_master_branch(): bool - { - return $this->branch === 'master'; - } -} diff --git a/app/Metadata/GitRequest.php b/app/Metadata/GitRequest.php deleted file mode 100644 index 87cff7273cf..00000000000 --- a/app/Metadata/GitRequest.php +++ /dev/null @@ -1,22 +0,0 @@ -init( + Config::get('urls.update.git.commits'), + Configs::getValueAsInt('update_check_every_days') + ); + } +} diff --git a/app/Metadata/Json/JsonRequestFunctions.php b/app/Metadata/Json/JsonRequestFunctions.php new file mode 100644 index 00000000000..6da7288c561 --- /dev/null +++ b/app/Metadata/Json/JsonRequestFunctions.php @@ -0,0 +1,141 @@ +url = $url; + $this->decodedJson = null; + $this->ttl = $ttl; + } + + /** + * {@inheritDoc} + * + * @codeCoverageIgnore + */ + public function clear_cache(): void + { + $this->decodedJson = null; + Cache::forget($this->url); + Cache::forget($this->url . '_age'); + } + + /** + * {@inheritDoc} + */ + public function get_age_text(): string + { + $age = Cache::get($this->url . '_age'); + if (!$age instanceof \DateTimeInterface) { + // @codeCoverageIgnoreStart + return 'unknown'; + // @codeCoverageIgnoreEnd + } + try { + $text = match (0) { + (int) now()->diffInMinutes($age) => (int) -now()->diffInSeconds($age) . ' seconds', + (int) now()->diffInHours($age) => (int) -now()->diffInMinutes($age) . ' minutes', + (int) now()->diffInDays($age) => (int) -now()->diffInHours($age) . ' hours', + (int) now()->diffInWeeks($age) => (int) -now()->diffInDays($age) . ' days', + (int) now()->diffInMonths($age) => (int) -now()->diffInWeeks($age) . ' weeks', + (int) now()->diffInYears($age) => (int) -now()->diffInMonths($age) . ' months', + default => now()->diffInYears($age) . ' years', + }; + + return $text . ' ago'; + // @codeCoverageIgnoreStart + } catch (\Throwable) { + return 'unknown'; + } + // @codeCoverageIgnoreEnd + } + + /** + * {@inheritDoc} + */ + public function get_json(bool $useCache = false): mixed + { + try { + if ($this->decodedJson === null || !$useCache) { + $rawResponse = $useCache ? (string) Cache::get($this->url) : ''; + if ($rawResponse === '') { + $rawResponse = $this->fetchFromServer(); + Cache::put($this->url, $rawResponse, now()->addDays($this->ttl)); + Cache::put($this->url . '_age', now(), now()->addDays($this->ttl)); + } + + $this->decodedJson = json_decode($rawResponse, false, 512, JSON_THROW_ON_ERROR); + } + + return $this->decodedJson; + // @codeCoverageIgnoreStart + } catch (JsonRequestFailedException $e) { + Log::error(__METHOD__ . ':' . __LINE__ . ' ' . $e->getMessage()); + } catch (\JsonException $e) { + Log::error(__METHOD__ . ':' . __LINE__ . ' ' . $e->getMessage()); + } + $this->clear_cache(); + + return null; + // @codeCoverageIgnoreEnd + } + + /** + * Runs the HTTP query and returns the result. + * + * @return string the plain JSON-encoded response + * + * @throws JsonRequestFailedException + */ + private function fetchFromServer(): string + { + try { + $opts = [ + 'http' => [ + 'method' => 'GET', + 'timeout' => 1, + 'header' => [ + 'User-Agent: ' . ini_get('user_agent'), + ], + ], + ]; + $context = stream_context_create($opts); + + $raw = file_get_contents($this->url, false, $context); + if ($raw === '') { + // @codeCoverageIgnoreStart + throw new JsonRequestFailedException('file_get_contents() failed'); + // @codeCoverageIgnoreEnd + } + + return $raw; + // @codeCoverageIgnoreStart + } catch (\Throwable $e) { + throw new JsonRequestFailedException('Could not fetch ' . $this->url, $e); + } + // @codeCoverageIgnoreEnd + } +} diff --git a/app/Metadata/Json/TagsRequest.php b/app/Metadata/Json/TagsRequest.php new file mode 100644 index 00000000000..afcfaf32e43 --- /dev/null +++ b/app/Metadata/Json/TagsRequest.php @@ -0,0 +1,27 @@ +init( + Config::get('urls.update.git.tags'), + Configs::getValueAsInt('update_check_every_days') + ); + } +} diff --git a/app/Metadata/Json/UpdateRequest.php b/app/Metadata/Json/UpdateRequest.php new file mode 100644 index 00000000000..c42f1f6bd37 --- /dev/null +++ b/app/Metadata/Json/UpdateRequest.php @@ -0,0 +1,27 @@ +init( + Config::get('urls.update.json'), + Configs::getValueAsInt('update_check_every_days') + ); + } +} diff --git a/app/Metadata/Laminas/Unicode.php b/app/Metadata/Laminas/Unicode.php new file mode 100644 index 00000000000..e201edaa878 --- /dev/null +++ b/app/Metadata/Laminas/Unicode.php @@ -0,0 +1,158 @@ +_uniChar(0x250C); + } + + /** + * {@inheritDoc} + */ + public function getTopRight(): string + { + return $this->_uniChar(0x2510); + } + + /** + * {@inheritDoc} + */ + public function getBottomLeft(): string + { + return $this->_uniChar(0x2514); + } + + /** + * {@inheritDoc} + */ + public function getBottomRight(): string + { + return $this->_uniChar(0x2518); + } + + /** + * {@inheritDoc} + */ + public function getVertical(): string + { + return $this->_uniChar(0x2502); + } + + /** + * {@inheritDoc} + */ + public function getHorizontal(): string + { + return $this->_uniChar(0x2500); + } + + /** + * {@inheritDoc} + */ + public function getCross(): string + { + return $this->_uniChar(0x253C); + } + + /** + * {@inheritDoc} + */ + public function getVerticalRight(): string + { + return $this->_uniChar(0x251C); + } + + /** + * {@inheritDoc} + */ + public function getVerticalLeft(): string + { + return $this->_uniChar(0x2524); + } + + /** + * {@inheritDoc} + */ + public function getHorizontalDown(): string + { + return $this->_uniChar(0x252C); + } + + /** + * {@inheritDoc} + */ + public function getHorizontalUp(): string + { + return $this->_uniChar(0x2534); + } + + /** + * Convert am unicode character code to a character. + * + * @param int $code + */ + // @codingStandardsIgnoreStart + protected function _uniChar(int $code): string + { + // @codingStandardsIgnoreEnd + if ($code <= 0x7F) { + return \chr($code); + } + if ($code <= 0x7FF) { + return \chr(0xC0 | $code >> 6) + . \chr(0x80 | $code & 0x3F); + } + if ($code <= 0xFFFF) { + return \chr(0xE0 | $code >> 12) + . \chr(0x80 | $code >> 6 & 0x3F) + . \chr(0x80 | $code & 0x3F); + } + if ($code <= 0x10FFFF) { + return \chr(0xF0 | $code >> 18) + . \chr(0x80 | $code >> 12 & 0x3F) + . \chr(0x80 | $code >> 6 & 0x3F) + . \chr(0x80 | $code & 0x3F); + } + + throw new LycheeLogicException('Code point requested outside of Unicode range'); + } +} diff --git a/app/Metadata/LycheeVersion.php b/app/Metadata/LycheeVersion.php deleted file mode 100644 index 1f3fbc2e7be..00000000000 --- a/app/Metadata/LycheeVersion.php +++ /dev/null @@ -1,138 +0,0 @@ -gitHubFunctions = $githubFunctions; - $this->isRelease = $this->fetchReleaseInfo(); - $this->phpUnit = $this->fetchComposerInfo(); - } - - /** - * Returns true if we are using the release channel - * Returns false if we are using the git channel. - */ - private function fetchReleaseInfo() - { - return !file_exists(base_path('.git')); - } - - /** - * Returns true if we are using the release channel - * Returns false if we are using the git channel. - */ - private function fetchComposerInfo() - { - return file_exists(base_path('vendor/bin/phpunit')); - } - - /** - * Return asked information. - * - * @return array - */ - public function get() - { - $versions = []; - $versions['channel'] = $this->isRelease ? 'release' : 'git'; - $versions['composer'] = $this->phpUnit ? 'dev' : '--no-dev'; - $versions['DB'] = $this->getDBVersion(); - $versions['Lychee'] = $this->getLycheeVersion(); - - return $versions; - } - - /** - * Format the version : number (commit id). - */ - public function format(array $info) - { - $ret = $info['version']; - $ret .= (isset($info['commit']) ? ' (' . $info['commit'] . ')' : ''); - $ret .= $info['additional'] ?? ''; - - return $ret; - } - - /** - * @param string $version in the shape of xxyyzz - * - * @return string xx.yy.zz - */ - public function format_version(string $version) - { - return implode('.', array_map('intval', str_split($version, 2))); - } - - /** - * Return the info about the database. - * - * @return array - */ - public function getDBVersion() - { - return ['version' => $this->format_version(Configs::get_value('version', '040000'))]; - } - - /** - * Return the info about the version.md file. - * - * @return array - */ - public function getFileVersion() - { - return ['version' => rtrim(@file_get_contents(base_path('version.md')))]; - } - - /** - * Return the information with respect to Lychee. - * - * @return array - */ - private function getLycheeVersion() - { - if ($this->isRelease) { - // @codeCoverageIgnoreStart - return $this->getFileVersion(); - // @codeCoverageIgnoreEnd - } - - $branch = $this->gitHubFunctions->branch; - $commit = $this->gitHubFunctions->head; - if (!$commit && !$branch) { - // @codeCoverageIgnoreStart - return ['version' => 'No git data found.']; - // @codeCoverageIgnoreEnd - } - - return ['version' => $branch, 'commit' => $commit, 'additional' => $this->gitHubFunctions->get_behind_text()]; - } -} diff --git a/app/Metadata/Versions/FileVersion.php b/app/Metadata/Versions/FileVersion.php new file mode 100644 index 00000000000..582b1642295 --- /dev/null +++ b/app/Metadata/Versions/FileVersion.php @@ -0,0 +1,79 @@ +version = Version::createFromString( + File::get(base_path('version.md')) + ); + } + + /** + * {@inheritDoc} + */ + public function hydrate(bool $withRemote = true, bool $useCache = true): void + { + if ($withRemote && Schema::hasTable('configs')) { + $updateRequest = resolve(UpdateRequest::class); + $json = $updateRequest->get_json($useCache); + + if ($json !== null) { + $this->remoteVersion = Version::createFromString($json->lychee->version); + } + } + } + + /** + * {@inheritDoc} + */ + public function getVersion(): Version + { + return $this->version; + } + + /** + * {@inheritDoc} + */ + public function isUpToDate(): bool + { + return $this->remoteVersion === null || $this->remoteVersion->toInteger() <= $this->version->toInteger(); + } +} \ No newline at end of file diff --git a/app/Metadata/Versions/GitHubVersion.php b/app/Metadata/Versions/GitHubVersion.php new file mode 100644 index 00000000000..c1a8ec684fa --- /dev/null +++ b/app/Metadata/Versions/GitHubVersion.php @@ -0,0 +1,247 @@ +isGit()) { + // @codeCoverageIgnoreStart + return; + // @codeCoverageIgnoreEnd + } + + // Let's fetch the HEAD & branch if available. + $this->hydrateLocalBranch(); + $this->hydrateLocalHead(); // Only if GitCommits + + if ($withRemote) { + $this->hydrateRemote($useCache); + } + } + + /** + * We are a release if the localBranch is a tag. + * + * @return bool + */ + public function isRelease(): bool + { + return $this->remote instanceof GitTags && $this->localBranch !== null; + } + + /** + * {@inheritDoc} + */ + public function isMasterBranch(): bool + { + return $this->remote instanceof GitTags || ($this->remote instanceof GitCommits && $this->localBranch === self::MASTER); + } + + /** + * {@inheritDoc} + */ + public function isUpToDate(): bool + { + return $this->countBehind === 0 || $this->countBehind === false; + } + + /** + * {@inheritDoc} + */ + public function getBehindTest(): string + { + return match ($this->countBehind) { + // @codeCoverageIgnoreStart + false => 'Could not compare.', + 0 => sprintf('Up to date (%s).', $this->remote->getAgeText() ?? '??'), + 30 => sprintf('More than 30 %s behind (%s).', + $this->remote->getType(), + $this->remote->getAgeText() ?? '??'), + // @codeCoverageIgnoreEnd + default => sprintf('%d %s behind %s (%s)', + $this->countBehind, + $this->remote->getType(), + $this->remote->getHead() ?? '??', + $this->remote->getAgeText() ?? '??'), + }; + } + + /** + * {@inheritDoc} + */ + public function hasPermissions(): bool + { + return Helpers::hasFullPermissions(base_path('.git')) && + $this->remote instanceof GitCommits && + Helpers::hasPermissions(base_path('.git/refs/heads/' . $this->localBranch) + ); + } + + /** + * Set current mode. + * We determines if we are in commit mode or in tags. + */ + private function isGit(): bool + { + // We get the branch name + $branch_path = base_path('.git/HEAD'); + if (!File::exists($branch_path) && + !File::isReadable($branch_path)) { + // @codeCoverageIgnoreStart + Log::warning(__METHOD__ . ':' . __LINE__ . ' Could not read ' . $branch_path); + + return false; + // @codeCoverageIgnoreEnd + } + + $branch = File::get($branch_path); + // Init remote request + if (Str::startsWith($branch, 'ref:')) { + $this->remote = resolve(GitCommits::class); + } else { + // @codeCoverageIgnoreStart + $this->remote = resolve(GitTags::class); + // @codeCoverageIgnoreEnd + } + + return true; + } + + /** + * We fetch the branch head. + * This will return false in the case of : + * - .git not accessible + * - release. + * + * @return void + */ + private function hydrateLocalBranch(): void + { + // Remote is not set: exit early + if ($this->remote === null) { + // @codeCoverageIgnoreStart + return; + // @codeCoverageIgnoreEnd + } + + // We get the branch name + $branch_path = base_path('.git/HEAD'); + $branchOrCommit = File::get($branch_path); + + if ($this->remote instanceof GitCommits) { + // This is "normal" behaviour + $branch = explode('/', $branchOrCommit, 3); + $this->localBranch = trim($branch[2]); + } else { + // @codeCoverageIgnoreStart + // This is tagged/CICD behaviour + // we leave localBranch as null so that we know that we are not on master + $this->localHead = $this->trim($branchOrCommit); + // @codeCoverageIgnoreEnd + } + } + + /** + * We fetch the commit head. + * This will return false in the case of : + * - .git not accessible + * - release. + * + * @return void + */ + private function hydrateLocalHead(): void + { + // Remote is not set: exit early + if ( + $this->remote === null || + $this->remote instanceof GitTags || + $this->localBranch === null + ) { + // @codeCoverageIgnoreStart + return; + // @codeCoverageIgnoreEnd + } + + // We get the branch commit ID + $commit_path = base_path('.git/refs/heads/' . $this->localBranch); + if (!File::exists($commit_path) && + !File::isReadable($commit_path)) { + // @codeCoverageIgnoreStart + Log::warning(__METHOD__ . ':' . __LINE__ . ' Could not read ' . $commit_path); + + return; + // @codeCoverageIgnoreEnd + } + $commitID = File::get($commit_path); + $this->localHead = $this->trim($commitID); + } + + /** + * Fetch the commits on master branch. + * + * @param bool $useCache + * + * @return void + * + * @codeCoverageIgnore the code path here depends whether you are on a PR or on master... + */ + private function hydrateRemote(bool $useCache): void + { + // We do not fetch when local branch is not master. + // We do not fetch when the localHead is not set. + if ($this->remote === null || $this->localHead === null || !$this->isMasterBranch()) { + return; + } + + $data = $this->remote->fetchRemote($useCache); + $this->countBehind = $this->remote->countBehind($data, $this->localHead); + + if ($this->remote instanceof GitTags) { + $this->localBranch = $this->remote->getTagName($data, $this->localHead); + } + } +} diff --git a/app/Metadata/Versions/InstalledVersion.php b/app/Metadata/Versions/InstalledVersion.php new file mode 100644 index 00000000000..ebf76054153 --- /dev/null +++ b/app/Metadata/Versions/InstalledVersion.php @@ -0,0 +1,79 @@ +isGit = File::exists(base_path('.git')); + $this->phpUnit = File::exists(base_path('vendor/bin/phpunit')); + } + + /** + * Return true if we are using a Release version of Lychee. + */ + public function isRelease(): bool + { + return !$this->isGit; + } + + /** + * Return true of the dev dependencies are installed. + */ + public function isDev(): bool + { + return $this->phpUnit; + } + + /** + * {@inheritDoc} + * + * @throws ConfigurationKeyMissingException + */ + public function getVersion(): Version + { + if (!Schema::hasTable('configs')) { + // @codeCoverageIgnoreStart + return Version::createFromInt(10000); + // @codeCoverageIgnoreEnd + } + + return Version::createFromInt(Configs::getValueAsInt('version')); + } +} diff --git a/app/Metadata/Versions/Remote/AbstractGitRemote.php b/app/Metadata/Versions/Remote/AbstractGitRemote.php new file mode 100644 index 00000000000..fcf3a015ac9 --- /dev/null +++ b/app/Metadata/Versions/Remote/AbstractGitRemote.php @@ -0,0 +1,117 @@ +getRequest(); + + // We fetch the commits + $data = $request->get_json($useCache); + if (!is_array($data) || count($data) === 0) { + // if $gitData is null we already logged the problem + // @codeCoverageIgnoreStart + return []; + // @codeCoverageIgnoreEnd + } + + $this->head = $this->dataToName($data[0]); + $this->headSha = $this->dataToSha($data[0]); + $this->age = $request->get_age_text(); + + return $data; + } + + /** + * {@inheritDoc} + */ + public function countBehind(array $data, string $needle): int|false + { + if (count($data) === 0) { + return false; + } + + $i = 0; + while ($i < count($data)) { + if ($this->dataToSha($data[$i]) === $needle) { + return $i; + } + $i++; + } + + return $i; + } + + /** + * {@inheritDoc} + * + * @codeCoverageIgnore + */ + public function getAgeText(): string + { + return $this->age; + } + + /** + * {@inheritDoc} + * + * @codeCoverageIgnore + */ + public function getHead(): ?string + { + return $this->head; + } + + /** + * {@inheritDoc} + * + * @codeCoverageIgnore + */ + public function getHeadSha(): ?string + { + return $this->headSha; + } +} \ No newline at end of file diff --git a/app/Metadata/Versions/Remote/GitCommits.php b/app/Metadata/Versions/Remote/GitCommits.php new file mode 100644 index 00000000000..52159231f96 --- /dev/null +++ b/app/Metadata/Versions/Remote/GitCommits.php @@ -0,0 +1,73 @@ +trim($data->sha); // @phpstan-ignore-line : Access to an undefined property object::$sha + } + + /** + * {@inheritDoc} + */ + protected function dataToSha(object $data): string + { + return $this->trim($data->sha); // @phpstan-ignore-line : Access to an undefined property object::$sha + } +} \ No newline at end of file diff --git a/app/Metadata/Versions/Remote/GitTags.php b/app/Metadata/Versions/Remote/GitTags.php new file mode 100644 index 00000000000..7d3629873c0 --- /dev/null +++ b/app/Metadata/Versions/Remote/GitTags.php @@ -0,0 +1,87 @@ +name; // @phpstan-ignore-line : Access to an undefined property object::$name + } + + /** + * {@inheritDoc} + */ + protected function dataToSha(object $data): string + { + // In this specific case we + return $this->trim($data->commit->sha); // @phpstan-ignore-line : Access to an undefined property object::$commit + } + + /** + * Given array and sha returns the name of the tag associated to the sha. + * + * @param object[] $data + * @param string $sha + * + * @return string + */ + public function getTagName(array $data, string $sha): string + { + foreach ($data as $d) { + if ($this->dataToSha($d) === $sha) { + return $this->dataToName($d); + } + } + + return ''; + } +} \ No newline at end of file diff --git a/app/Metadata/Versions/Trimable.php b/app/Metadata/Versions/Trimable.php new file mode 100644 index 00000000000..4d85342ed45 --- /dev/null +++ b/app/Metadata/Versions/Trimable.php @@ -0,0 +1,24 @@ + Configs::get_value('landing_owner'), - 'title' => Configs::get_value('landing_title'), - 'subtitle' => Configs::get_value('landing_subtitle'), - 'facebook' => Configs::get_value('landing_facebook'), - 'flickr' => Configs::get_value('landing_flickr'), - 'twitter' => Configs::get_value('landing_twitter'), - 'instagram' => Configs::get_value('landing_instagram'), - 'youtube' => Configs::get_value('landing_youtube'), - 'background' => Configs::get_value('landing_background'), - 'copyright_enable' => Configs::get_value('site_copyright_enable'), - 'copyright_year' => Configs::get_value('site_copyright_begin'), - 'additional_footer_text' => Configs::get_value('additional_footer_text'), - ]; - if (Configs::get_value('site_copyright_begin') != Configs::get_value('site_copyright_end')) { - $infos['copyright_year'] = Configs::get_value('site_copyright_begin') . '-' . Configs::get_value('site_copyright_end'); - } - - return $infos; - } - - /** - * Returns the public settings of Lychee (served to diagnostics). - * - * @return array - */ - public function min_info() - { - // Execute query - return Configs::info()->orderBy('id', 'ASC')->get()->pluck('value', 'key'); - } - - /** - * Returns the public settings of Lychee (served to the user). - * - * @return array - */ - public function public() - { - // Execute query - $return = Configs::public()->pluck('value', 'key')->all(); - $return['sorting_Albums'] = 'ORDER BY ' . $return['sorting_Albums_col'] . ' ' . $return['sorting_Albums_order']; - - return $return; - } - - /** - * Returns the admin settings of Lychee. - * - * @return array - */ - public function admin() - { - // Execute query - $return = Configs::admin()->pluck('value', 'key')->all(); - $return['sorting_Photos'] = 'ORDER BY ' . $return['sorting_Photos_col'] . ' ' . $return['sorting_Photos_order']; - $return['sorting_Albums'] = 'ORDER BY ' . $return['sorting_Albums_col'] . ' ' . $return['sorting_Albums_order']; - - $return['lang_available'] = Lang::get_lang_available(); - - return $return; - } - - /** - * Sanity check of the config. - * - * @param array $return - */ - public function sanity(array &$return) - { - try { - $configs = Configs::all(['key', 'value', 'type_range']); - - foreach ($configs as $config) { - $message = $config->sanity($config->value); - if ($message != '') { - $return[] = $message; - } - } - } catch (QueryException $e) { - $return[] = 'Error: ' . $e->getMessage(); - } - } - - public function get_config_device(string $device) - { - $true = true; - $false = false; - - // we just flip the values in the television case - if ($device == 'television') { - // @codeCoverageIgnoreStart - $true = false; - $false = true; - // @codeCoverageIgnoreEnd - } - - return [ - 'header_auto_hide' => $true, - 'active_focus_on_page_load' => $false, - 'enable_button_visibility' => $true, - 'enable_button_share' => $true, - 'enable_button_archive' => $true, - 'enable_button_move' => $true, - 'enable_button_trash' => $true, - 'enable_button_fullscreen' => $true, - 'enable_button_download' => $true, - 'enable_button_add' => $true, - 'enable_button_more' => $true, - 'enable_button_rotate' => $true, - 'enable_close_tab_on_esc' => $false, - 'enable_contextmenu_header' => $true, - 'hide_content_during_imgview' => $false, - 'enable_tabindex' => $false, - 'device_type' => $device, - ]; - } -} diff --git a/app/ModelFunctions/JsonRequestFunctions.php b/app/ModelFunctions/JsonRequestFunctions.php deleted file mode 100644 index 65bc9fc2b06..00000000000 --- a/app/ModelFunctions/JsonRequestFunctions.php +++ /dev/null @@ -1,149 +0,0 @@ -url = $url; - $this->json = json_decode(Cache::get($url)); - $this->ttl = $ttl; - } - - /** - * Cache the result of the request. - */ - private function cache() - { - try { - Cache::put($this->url, $this->raw, now()->addDays($this->ttl)); - Cache::put($this->url . '_age', now(), now()->addDays($this->ttl)); - } catch (InvalidArgumentException $e) { - Logs::error( - __METHOD__, - __LINE__, - 'Could not set in the cache' - ); - } - } - - /** - * Remove elements from the cache. - */ - public function clear_cache() - { - Cache::forget($this->url); - Cache::forget($this->url . '_age'); - $this->json = null; - $this->raw = null; - } - - /** - * return the age of the last query to url. - * - * @return mixed - */ - public function get_age() - { - return Cache::get($this->url . '_age'); - } - - /** - * Return the age of the last query in days/hours/minutes. - * - * @return string - */ - public function get_age_text() - { - $age = $this->get_age(); - if (!$age) { - $last = 'unknown'; - $end = ''; - } else { - $last = now()->diffInDays($age); - $end = $last > 0 ? ' days' : ''; - $last = ($last == 0 && $end = ' hours') - ? now()->diffInHours($age) : $last; - $last = ($last == 0 && $end = ' minutes') - ? now()->diffInMinutes($age) : $last; - $last = ($last == 0 && $end = ' seconds') - ? now()->diffInSeconds($age) : $last; - $end = $end . ' ago'; - } - - return $last . $end; - } - - /** - * make the query and cache the result. - * - * @return false|array - */ - private function get() - { - $opts = [ - 'http' => [ - 'method' => 'GET', - 'timeout' => 1, - 'header' => [ - 'User-Agent: PHP', - ], - ], - ]; - $context = stream_context_create($opts); - - /* @var string|false $json */ - $this->raw = @file_get_contents($this->url, false, $context); - - if ($this->raw != false) { - $this->cache(); - $this->json = json_decode($this->raw); - - return $this->json; - } - // @codeCoverageIgnoreStart - Logs::notice(__METHOD__, __LINE__, 'Could not access: ' . $this->url); - $this->raw = null; - $this->json = null; - - return false; - // @codeCoverageIgnoreEnd - } - - /** - * Return the JSON. - * - * @param bool $cached - * - * @return false|json - */ - public function get_json(bool $cached = false) - { - if ($cached) { - if (!$this->json) { - throw new NotInCacheException(); - } - - return $this->json; - } - - return $this->get(); - } -} diff --git a/app/ModelFunctions/LogFunctions.php b/app/ModelFunctions/LogFunctions.php deleted file mode 100644 index 0887c2cbba1..00000000000 --- a/app/ModelFunctions/LogFunctions.php +++ /dev/null @@ -1,62 +0,0 @@ - $val) { - // check that the value can be cast to string - if ($this->is_stringable($val)) { - $replace['{' . $key . '}'] = $val; - } - } - - // interpolate replacement values into the message and return - return strtr($message, $replace); - } - - /** - * Implements log so that AbstractLogger works. - */ - public function log($loglevel, $message, $context = []) - { - $dbt = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3); - // debug_backtrace return the backtrace of all the function calls - // we want to know who called us, because log is being called by the AbstractLogger, - // we need to go one step further - $fun = $dbt[2]['function'] ?? $dbt[1]['function'] ?? __METHOD__; - $line = $dbt[2]['line'] ?? $dbt[1]['line'] ?? __LINE__; - if ($this->is_stringable($message)) { - $text = $this->interpolate($message, $context); - } else { - $text = 'argument is not stringable!'; - } - - $log = Logs::create([ - 'type' => $loglevel, - 'function' => $fun, - 'line' => $line, - 'text' => $text, - ]); - $log->save(); - } -} diff --git a/app/ModelFunctions/MOVFormat.php b/app/ModelFunctions/MOVFormat.php index 2661042ef0a..2125de972d6 100644 --- a/app/ModelFunctions/MOVFormat.php +++ b/app/ModelFunctions/MOVFormat.php @@ -1,38 +1,78 @@ setAudioCodec($audioCodec) - ->setVideoCodec($videoCodec); + try { + $this + ->setAudioCodec(self::FFMPEG_AUDIO_CODEC_ID) + ->setVideoCodec(self::FFMPEG_VIDEO_CODEC_ID); + } catch (InvalidArgumentException $e) { + throw LycheeAssertionError::createFromUnexpectedException($e); + } } - public function supportBFrames() + /** + * @codeCoverageIgnore + */ + public function supportBFrames(): bool { return false; } - public function getExtraParams() + /** + * Returns the extra parameters to be added to the FFMpeg command line. + * + * Here we force FFMpeg to use the Quicktime Container format for output. + * Natively, a Google Motion Picture uses the video codec AVC (H.264) + * in an MP4 (MPEG-4 Part 1) container. + * But the JS package `livephotoskit/livephotoskit` which handles + * live photos on the frontend only supports Quicktime containers + * (at least this was the case in 2019, see + * [comment in issue #378](https://github.com/LycheeOrg/Lychee/issues/378#issuecomment-548687276) + * and the [related pull request #172](https://github.com/LycheeOrg/Lychee-front/pull/172)). + * Hence, we re-packetize the video stream into a Quicktime container. + * + * @return string[] + */ + public function getExtraParams(): array { - return ['-f', 'mov']; + return ['-f', self::FFMPEG_CONTAINER_ID]; } - public function getAvailableAudioCodecs() + /** + * @return string[] + */ + public function getAvailableAudioCodecs(): array { - return ['copy']; + return [self::FFMPEG_AUDIO_CODEC_ID]; } - public function getAvailableVideoCodecs() + /** + * @return string[] + */ + public function getAvailableVideoCodecs(): array { - return ['copy']; + return [self::FFMPEG_VIDEO_CODEC_ID]; } } diff --git a/app/ModelFunctions/SessionFunctions.php b/app/ModelFunctions/SessionFunctions.php deleted file mode 100644 index babc8178e59..00000000000 --- a/app/ModelFunctions/SessionFunctions.php +++ /dev/null @@ -1,250 +0,0 @@ -is_logged_in() && ($this->id() == 0 || $this->user()->upload); - } - - /** - * Return the current ID of the user - * what happens when UserID is not set? :p. - * - * @return int - */ - public function id(): int - { - if (!Session::get('login')) { - throw new NotLoggedInException(); - } - - return Session::get('UserID'); - } - - /** - * Return User object given a positive ID. - */ - private function accessUserData(): User - { - $id = $this->id(); - $this->user_data = User::find($id); - - return $this->user_data; - } - - /** - * Return User object and cache the result. - */ - public function user(): User - { - return $this->user_data ?? $this->accessUserData(); - } - - /** - * Return true if the currently logged in user is the one provided - * (or if that user is Admin). - * - * @param int userId - * - * @return bool - */ - public function is_current_user(int $userId): bool - { - return Session::get('login') && (Session::get('UserID') === $userId || Session::get('UserID') === 0); - } - - /** - * Given a user, login. - */ - public function login(User $user): void - { - $this->user_data = $user; - Session::put('login', true); - Session::put('UserID', $user->id); - } - - /** - * Sets the session values when no there is no username and password in the database. - * - * @return bool returns true when no login was found - */ - public function noLogin(): bool - { - $adminUser = User::find(0); - if ($adminUser !== null && $adminUser->password === '' && $adminUser->username === '') { - $this->user_data = $adminUser; - Session::put('login', true); - Session::put('UserID', 0); - - return true; - } - - return Legacy::noLogin(); - } - - /** - * Given a username, password and ip (for logging), try to log the user. - * returns true if succeed - * returns false if fail. - * - * @param string $username - * @param string $password - * @param string $ip - * - * @return bool - */ - public function log_as_user(string $username, string $password, string $ip): bool - { - // We select the NON ADMIN user - $user = User::where('username', '=', $username)->where('id', '>', '0')->first(); - - if ($user != null && Hash::check($password, $user->password)) { - $this->user_data = $user; - Session::put('login', true); - Session::put('UserID', $user->id); - Logs::notice(__METHOD__, __LINE__, 'User (' . $username . ') has logged in from ' . $ip); - - return true; - } - - return false; - } - - /** - * Given a username, password and ip (for logging), try to log the user as admin. - * returns true if succeed - * returns false if fail. - * - * @param string $username - * @param string $password - * @param string $ip - * - * @return bool - */ - public function log_as_admin(string $username, string $password, string $ip): bool - { - $AdminUser = User::find(0); - - if ($AdminUser !== null) { - // Admin User exist, so we check against it. - if (Hash::check($username, $AdminUser->username) && Hash::check($password, $AdminUser->password)) { - $this->user_data = $AdminUser; - Session::put('login', true); - Session::put('UserID', 0); - Logs::notice(__METHOD__, __LINE__, 'User (' . $username . ') has logged in from ' . $ip); - - return true; - } - - return false; - } - // Admin User does not exist yet, so we use the Legacy. - - return Legacy::log_as_admin($username, $password, $ip); - } - - /** - * Given an albumID, check if it exists in the visible_albums session variable. - * - * @param $albumID - * - * @return bool - */ - public function has_visible_album($albumID): bool - { - if (!Session::has('visible_albums')) { - return false; - } - - $visible_albums = Session::get('visible_albums'); - $visible_albums = explode('|', $visible_albums); - - return in_array($albumID, $visible_albums); - } - - /** - * Add new album to the visible_albums session variable. - * - * @param $albumIDs - */ - public function add_visible_albums($albumIDs): void - { - $visible_albums = []; - if (Session::has('visible_albums')) { - $visible_albums = Session::get('visible_albums'); - $visible_albums = explode('|', $visible_albums); - } - - foreach ($albumIDs as $albumID) { - if (!in_array($albumID, $visible_albums)) { - $visible_albums[] = $albumID; - } - } - - $visible_albums = implode('|', $visible_albums); - Session::put('visible_albums', $visible_albums); - } - - public function get_visible_albums(): array - { - if (Session::has('visible_albums')) { - return explode('|', Session::get('visible_albums')); - } - - return []; - } - - /** - * Log out the current user. - */ - public function logout() - { - $this->user_data = null; - Session::flush(); - } -} diff --git a/app/ModelFunctions/SymLinkFunctions.php b/app/ModelFunctions/SymLinkFunctions.php index 8d2e3bde3d1..26b04dec502 100644 --- a/app/ModelFunctions/SymLinkFunctions.php +++ b/app/ModelFunctions/SymLinkFunctions.php @@ -1,106 +1,47 @@ id) - ->orderBy('created_at', 'DESC') - ->first(); - if ($sym == null) { - $sym = new SymLink(); - $sym->set($photo); - $sym->save(); - } - - return $sym; - } - - /** - * Get URLS of pictures. - * - * This method modifies the serialization of a photo such that the original URLs are replaced by symlinks. - * *Attention:* The passed $photo and the passed array $return which represents the serialization of the photo must - * match. - * It is the caller's responsibility to ensure that $return equals $photo->toReturnArray(). - * - * @param Photo $photo The photo that is going to be serialized - * @param array $return The serialization of the passed photo as returned by Photo#toReturnArray() - */ - public function getUrl( - Photo $photo, - array &$return - ) { - $sym = $this->find($photo); - if ($sym != null) { - $sym->override($return); - } - } - /** * Clear the table of existing SymLinks. * - * @return string + * @return void * - * @throws \Exception + * @throws ModelDBException */ - public function clearSymLink() + public function clearSymLink(): void { - $symlinks = SymLink::all(); - $no_error = true; - foreach ($symlinks as $symlink) { - $no_error &= $symlink->delete(); + $symLinks = SymLink::all(); + /** @var SymLink $symLink */ + foreach ($symLinks as $symLink) { + $symLink->delete(); } - - return $no_error ? 'true' : 'false'; } /** * Remove outdated SymLinks. * - * @return bool + * @return void + * + * @throws ModelDBException */ - public function remove_outdated() + public function remove_outdated(): void { - $symlinks = SymLink::query() - ->where('created_at', '<', now()->subDays(intval(Configs::get_value('SL_life_time_days', '3')))->toDateTimeString()) - ->get(); - $success = true; - foreach ($symlinks as $symlink) { - // it may be faster to just do the unlink and then one query for all the delete. - $success &= $symlink->delete(); + $symLinks = SymLink::expired()->get(); + /** @var SymLink $symLink */ + foreach ($symLinks as $symLink) { + $symLink->delete(); } - - return $success; } } diff --git a/app/Models/AccessPermission.php b/app/Models/AccessPermission.php new file mode 100644 index 00000000000..edc7a973de9 --- /dev/null +++ b/app/Models/AccessPermission.php @@ -0,0 +1,182 @@ + */ + use HasFactory; + + protected $casts = [ + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + APC::USER_ID => 'integer', + APC::IS_LINK_REQUIRED => 'boolean', + APC::GRANTS_FULL_PHOTO_ACCESS => 'boolean', + APC::GRANTS_DOWNLOAD => 'boolean', + APC::GRANTS_UPLOAD => 'boolean', + APC::GRANTS_EDIT => 'boolean', + APC::GRANTS_DELETE => 'boolean', + ]; + + /** + * allow these properties to be mass assigned. + */ + protected $fillable = [ + APC::USER_ID, + APC::BASE_ALBUM_ID, + APC::IS_LINK_REQUIRED, + APC::GRANTS_FULL_PHOTO_ACCESS, + APC::GRANTS_DOWNLOAD, + APC::GRANTS_UPLOAD, + APC::GRANTS_EDIT, + APC::GRANTS_DELETE, + APC::PASSWORD, + ]; + + /** + * @param $query + * + * @return AccessPermissionBuilder + */ + public function newEloquentBuilder($query): AccessPermissionBuilder + { + return new AccessPermissionBuilder($query); + } + + /** + * Returns the relationship between an AccessPermission and its associated album. + * + * @return BelongsTo + */ + public function album(): BelongsTo + { + return $this->belongsTo(BaseAlbumImpl::class, 'base_album_id', 'id'); + } + + /** + * Returns the relationship between an AccessPermission and its applied User. + * + * @return BelongsTo + */ + public function user(): BelongsTo + { + return $this->belongsTo(User::class, 'user_id', 'id'); + } + + /** + * Given an AccessPermission, duplicate its reccord. + * - Password is NOT transfered + * - base_album_id is NOT transfered. + * + * @param AccessPermission $accessPermission + * + * @return AccessPermission + */ + public static function ofAccessPermission(AccessPermission $accessPermission): self + { + return $accessPermission->replicate([APC::PASSWORD, APC::BASE_ALBUM_ID]); + } + + /** + * Return a new Public sharing permission with defaults. + * + * @return AccessPermission + * + * @throws ConfigurationKeyMissingException + */ + public static function ofPublic(): self + { + return new AccessPermission([ + APC::IS_LINK_REQUIRED => false, + APC::GRANTS_FULL_PHOTO_ACCESS => Configs::getValueAsBool('grants_full_photo_access'), + APC::GRANTS_DOWNLOAD => Configs::getValueAsBool('grants_download'), + APC::GRANTS_UPLOAD => false, + APC::GRANTS_EDIT => false, + APC::GRANTS_DELETE => false, + APC::PASSWORD => null, + ]); + } + + /** + * Return a new permission set associated to a specific userId. + * + * @param int $userId + * + * @return AccessPermission + */ + public static function withGrantFullPermissionsToUser(int $userId): self + { + return new AccessPermission([ + APC::USER_ID => $userId, + APC::GRANTS_FULL_PHOTO_ACCESS => true, + APC::GRANTS_DOWNLOAD => true, + APC::GRANTS_UPLOAD => true, + APC::GRANTS_EDIT => true, + APC::GRANTS_DELETE => true, + ]); + } +} \ No newline at end of file diff --git a/app/Models/Album.php b/app/Models/Album.php index dd147e84027..1ae001a3a7e 100644 --- a/app/Models/Album.php +++ b/app/Models/Album.php @@ -1,297 +1,507 @@ $children + * @property int $num_children The number of children. + * @property Collection $all_photos + * @property int $num_photos The number of photos in this album (excluding photos in subalbums). + * @property LicenseType $license + * @property string|null $cover_id + * @property Photo|null $cover + * @property string|null $header_id + * @property Photo|null $header + * @property string|null $track_short_path + * @property string|null $track_url + * @property AspectRatioType|null $album_thumb_aspect_ratio + * @property TimelineAlbumGranularity $album_timeline + * @property int $_lft + * @property int $_rgt + * @property BaseAlbumImpl $base_class + * @property User|null $owner * - * @property int $id - * @property string $title - * @property int $owner_id - * @property int|null $parent_id - * @property string $description - * @property Carbon|null $min_taken_at - * @property Carbon|null $max_taken_at - * @property int $public - * @property int $full_photo - * @property int $viewable - * @property int $downloadable - * @property int $share_button_visible - * @property string|null $password - * @property string $license - * @property bool $smart - * @property text $showtags - * @property Carbon $created_at - * @property Carbon $updated_at - * @property Collection[Album] $children - * @property User $owner - * @property Album $parent - * @property Collection[Photo] $photos + * @method static AlbumBuilder|Album query() Begin querying the model. + * @method static AlbumBuilder|Album with(array|string $relations) Begin querying the model with eager loading. + * @method AlbumBuilder|Album newModelQuery() Get a new, "pure" query builder for the model's table without any scopes, eager loading, etc. + * @method AlbumBuilder|Album newQuery() Get a new query builder for the model's table. * - * @method static Builder|Album newModelQuery() - * @method static Builder|Album newQuery() - * @method static Builder|Album query() - * @method static Builder|Album whereCreatedAt($value) - * @method static Builder|Album whereDescription($value) - * @method static Builder|Album whereDownloadable($value) - * @method static Builder|Album whereShareButtonVisible($value) - * @method static Builder|Album whereId($value) - * @method static Builder|Album whereLicense($value) - * @method static Builder|Album whereMaxTakestamp($value) - * @method static Builder|Album whereMinTakestamp($value) - * @method static Builder|Album whereOwnerId($value) - * @method static Builder|Album whereParentId($value) - * @method static Builder|Album wherePassword($value) - * @method static Builder|Album wherePublic($value) - * @method static Builder|Album whereTitle($value) - * @method static Builder|Album whereUpdatedAt($value) - * @method static Builder|Album whereVisibleHidden($value) - * @method static Builder|Album whereSmart($value) - * @mixin Eloquent + * @property Collection $access_permissions + * @property int|null $access_permissions_count + * @property AccessPermission|null $current_user_permissions + * @property AccessPermission|null $public_permissions + * @property Collection $shared_with + * @property int|null $shared_with_count * - * @property Collection|User[] $shared_with + * @method static AlbumBuilder|Album addSelect($column) + * @method static NSCollection all($columns = ['*']) + * @method static AlbumBuilder|Album ancestorsAndSelf($id, array $columns = []) + * @method static AlbumBuilder|Album ancestorsOf($id, array $columns = []) + * @method static AlbumBuilder|Album applyNestedSetScope(?string $table = null) + * @method static AlbumBuilder|Album countErrors() + * @method static AlbumBuilder|Album d() + * @method static AlbumBuilder|Album defaultOrder(string $dir = 'asc') + * @method static AlbumBuilder|Album descendantsAndSelf($id, array $columns = []) + * @method static AlbumBuilder|Album descendantsOf($id, array $columns = [], $andSelf = false) + * @method static AlbumBuilder|Album fixSubtree($root) + * @method static AlbumBuilder|Album fixTree($root = null) + * @method static NSCollection get($columns = ['*']) + * @method static AlbumBuilder|Album getNodeData($id, $required = false) + * @method static AlbumBuilder|Album getPlainNodeData($id, $required = false) + * @method static AlbumBuilder|Album getTotalErrors() + * @method static AlbumBuilder|Album hasChildren() + * @method static AlbumBuilder|Album hasParent() + * @method static AlbumBuilder|Album isBroken() + * @method static AlbumBuilder|Album join(string $table, string $first, string $operator = null, string $second = null, string $type = 'inner', string $where = false) + * @method static AlbumBuilder|Album joinSub($query, $as, $first, $operator = null, $second = null, $type = 'inner', $where = false) + * @method static AlbumBuilder|Album leaves(array $columns = []) + * @method static AlbumBuilder|Album leftJoin(string $table, string $first, string $operator = null, string $second = null) + * @method static AlbumBuilder|Album makeGap(int $cut, int $height) + * @method static AlbumBuilder|Album moveNode($key, $position) + * @method static AlbumBuilder|Album orWhereAncestorOf(bool $id, bool $andSelf = false) + * @method static AlbumBuilder|Album orWhereDescendantOf($id) + * @method static AlbumBuilder|Album orWhereNodeBetween($values) + * @method static AlbumBuilder|Album orWhereNotDescendantOf($id) + * @method static AlbumBuilder|Album orderBy($column, $direction = 'asc') + * @method static AlbumBuilder|Album rebuildSubtree($root, array $data, $delete = false) + * @method static AlbumBuilder|Album rebuildTree(array $data, $delete = false, $root = null) + * @method static AlbumBuilder|Album reversed() + * @method static AlbumBuilder|Album root(array $columns = []) + * @method static AlbumBuilder|Album select($columns = []) + * @method static AlbumBuilder|Album whereAncestorOf($id, $andSelf = false, $boolean = 'and') + * @method static AlbumBuilder|Album whereAncestorOrSelf($id) + * @method static AlbumBuilder|Album whereCoverId($value) + * @method static AlbumBuilder|Album whereDescendantOf($id, $boolean = 'and', $not = false, $andSelf = false) + * @method static AlbumBuilder|Album whereDescendantOrSelf(string $id, string $boolean = 'and', string $not = false) + * @method static AlbumBuilder|Album whereId($value) + * @method static AlbumBuilder|Album whereIn(string $column, string $values, string $boolean = 'and', string $not = false) + * @method static AlbumBuilder|Album whereIsAfter($id, $boolean = 'and') + * @method static AlbumBuilder|Album whereIsBefore($id, $boolean = 'and') + * @method static AlbumBuilder|Album whereIsLeaf() + * @method static AlbumBuilder|Album whereIsRoot() + * @method static AlbumBuilder|Album whereLft($value) + * @method static AlbumBuilder|Album whereLicense($value) + * @method static AlbumBuilder|Album whereNodeBetween($values, $boolean = 'and', $not = false) + * @method static AlbumBuilder|Album whereNotDescendantOf($id) + * @method static AlbumBuilder|Album whereNotIn(string $column, string $values, string $boolean = 'and') + * @method static AlbumBuilder|Album whereParentId($value) + * @method static AlbumBuilder|Album whereRgt($value) + * @method static AlbumBuilder|Album whereTrackShortPath($value) + * @method static AlbumBuilder|Album withDepth(string $as = 'depth') + * @method static AlbumBuilder|Album withoutRoot() + * + * // * @mixin \Eloquent + * + * @implements Node */ -class Album extends Model implements AlbumInterface +class Album extends BaseAlbum implements Node { + /** @phpstan-use NodeTrait */ use NodeTrait; - use AlbumBooleans; - use AlbumStringify; - use AlbumGetters; - use AlbumCast; - use AlbumSetters; - use CustomSort; - use UTCBasedTimes; - - protected $casts - = [ - 'public' => 'int', - 'nsfw' => 'int', - 'viewable' => 'int', - 'downloadable' => 'int', - 'share_button_visible' => 'int', - 'created_at' => 'datetime', - 'updated_at' => 'datetime', + use ToArrayThrowsNotImplemented; + /** @phpstan-use HasFactory<\Database\Factories\AlbumFactory> */ + use HasFactory; + + /** + * The model's attributes. + * + * We must list all attributes explicitly here, otherwise the attributes + * of a new model will accidentally be set on the parent class. + * The trait {@link \App\Models\Extensions\ForwardsToParentImplementation} + * only works properly, if it knows which attributes belong to the parent + * class and which attributes belong to the child class. + * + * @var array + */ + protected $attributes = [ + 'id' => null, + 'parent_id' => null, + 'album_timeline' => null, + 'license' => 'none', + 'cover_id' => null, + 'header_id' => null, + 'album_thumb_aspect_ratio' => null, + '_lft' => null, + '_rgt' => null, + 'album_sorting_col' => null, + 'album_sorting_order' => null, + ]; + + /** + * @var array + */ + protected $casts = [ 'min_taken_at' => 'datetime', 'max_taken_at' => 'datetime', + 'num_children' => 'integer', + 'num_photos' => 'integer', + 'album_thumb_aspect_ratio' => AspectRatioType::class, + 'album_timeline' => TimelineAlbumGranularity::class, + '_lft' => 'integer', + '_rgt' => 'integer', ]; /** * The relationships that should always be eagerly loaded by default. */ - protected $with = ['owner', 'cover']; + protected $with = ['cover', 'cover.size_variants', 'thumb']; /** - * This method is called by the framework after the model has been - * booted. + * Return the relationship between this album and photos which are + * direct children of this album. * - * This method alters the default query builder for this model and - * adds a "scope" to the query builder in order to add the "virtual" - * columns `max_taken_at` and `min_taken_at` to every query. + * @return HasManyChildPhotos */ - protected static function booted() + public function photos(): HasManyChildPhotos // @phpstan-ignore-line { - parent::booted(); - // Normally "scopes" are used to restrict the result of the query - // to a particular subset through adding additional WHERE-clauses - // to the default query. - // However, "scopes" can be used to manipulate the query in any way. - // Here we add to additional "virtual" columns to the query. - static::addGlobalScope('add_minmax_taken_at', function (Builder $builder) { - $builder->addSelect([ - 'max_taken_at' => Photo::query() - ->select('taken_at') - ->leftJoin('albums as a', 'a.id', '=', 'album_id') - ->whereColumn('a._lft', '>=', 'albums._lft') - ->whereColumn('a._rgt', '<=', 'albums._rgt') - ->whereNotNull('taken_at') - ->orderBy('taken_at', 'desc') - ->limit(1), - 'min_taken_at' => Photo::query() - ->select('taken_at') - ->leftJoin('albums as a', 'a.id', '=', 'album_id') - ->whereColumn('a._lft', '>=', 'albums._lft') - ->whereColumn('a._rgt', '<=', 'albums._rgt') - ->whereNotNull('taken_at') - ->orderBy('taken_at', 'asc') - ->limit(1), - ]); - }); + return new HasManyChildPhotos($this); } /** - * Return the relationship between Photos and their Album. + * Returns the relationship between this album and all photos incl. + * photos which are recursive children of this album. * - * @return HasMany + * @return HasManyPhotosRecursively */ - public function photos(): HasMany + public function all_photos(): HasManyPhotosRecursively + { + return new HasManyPhotosRecursively($this); + } + + public function thumb(): HasAlbumThumb { - return $this->hasMany('App\Models\Photo', 'album_id', 'id'); + return new HasAlbumThumb($this); } /** - * Return the relationship between an album and its owner. + * Return the relationship between an album and its sub-albums. * - * @return BelongsTo + * @return HasManyChildAlbums */ - public function owner(): BelongsTo + public function children(): HasManyChildAlbums { - return $this->belongsTo('App\Models\User', 'owner_id', 'id'); + return new HasManyChildAlbums($this); } /** - * Return the relationship between an album and its sub albums. - * - * Note: Actually, the return type should be non-nullable. - * However, {@link \App\SmartAlbums\BareSmartAlbum} extends this class and - * {@link \App\SmartAlbums\SmartAlbum::children()} cannot return an - * correctly instantiated object of `HasMany` but must return `null`, - * because a `SmartAlbum` is not a real Eloquent model and does not exist - * as a database entity. - * TODO: Refactor the inheritance relationships of all album types. - * A `SmartAlbum` (which cannot have sub-albums} should not inherit from - * `Album`. - * Instead both kind of albums should share an interface. - * Then the return type of this method could be repaired. - * - * @return ?HasMany + * Get query for descendants of the node. + * + * @return DescendantsRelation + * + * @throws QueryBuilderException */ - public function children(): ?HasMany + public function descendants(): DescendantsRelation { - return $this->hasMany('App\Models\Album', 'parent_id', 'id'); + try { + /** @var DescendantsRelation */ + return new DescendantsRelation($this->newQuery(), $this); + // @codeCoverageIgnoreStart + } catch (\Throwable $e) { + throw new QueryBuilderException($e); + } + // @codeCoverageIgnoreEnd } /** * Return the relationship between an album and its cover. * - * @return HasOne + * @return HasOne */ public function cover(): HasOne { - return $this->hasOne('App\Models\Photo', 'id', 'cover_id'); + return $this->hasOne(Photo::class, 'id', 'cover_id'); } /** - * Return the relationship between an album and its parent. + * Return the relationship between an album and its header. * - * @return BelongsTo + * @return HasOne */ - public function parent(): BelongsTo + public function header(): HasOne { - return $this->belongsTo('App\Models\Album', 'parent_id', 'id'); + return $this->hasOne(Photo::class, 'id', 'header_id'); } /** - * @return BelongsToMany + * Return the License used by the album. + * + * @param string|LicenseType|null $value + * + * @return LicenseType + * + * @throws ConfigurationKeyMissingException */ - public function shared_with(): BelongsToMany + protected function getLicenseAttribute(string|LicenseType|null $value): LicenseType { - return $this->belongsToMany( - 'App\Models\User', - 'user_album', - 'album_id', - 'user_id' - ); + if ($value === null || $value === 'none' || $value === LicenseType::NONE) { + return Configs::getValueAsEnum('default_license', LicenseType::class); + } + + if (is_string($value)) { + return LicenseType::from($value); + } + + return $value; + } + + /** + * {@inheritDoc} + * + * @throws ModelDBException + * @throws MediaFileOperationException + */ + public function performDeleteOnModel(): void + { + $fileDeleter = (new Delete())->do([$this->id]); + $this->exists = false; + $fileDeleter->do(); } /** - * Before calling delete() to remove the album from the database - * we need to go through each sub album and delete it. - * Idem we also delete each pictures inside an album (recursively). + * This method is a no-op. * - * @return bool|null + * This method is originally defined by {@link NodeTrait::deleteDescendants()} + * and called as part of the event listener for the 'deleting' event. + * The event listener is installed by {@link NodeTrait::bootNodeTrait()}. * - * @throws Exception + * For efficiency reasons all descendants are deleted by + * {@link Delete::do()}. + * Hence, we must avoid any attempt to delete the descendants twice. + * + * @return void + * + * @codeCoverageIgnore */ - public function predelete() + protected function deleteDescendants(): void { - $no_error = true; - $photos = $this->get_all_photos()->get(); - foreach ($photos as $photo) { - $no_error &= $photo->predelete(); - $no_error &= $photo->delete(); - } + // deliberately a no op + } - return $no_error; + /** + * Sets the ownership of all child albums and child photos to the owner + * of this album. + * + * ANSI SQL does not allow a `JOIN`-clause in the table reference + * of `UPDATE` statements. + * MySQL and PostgreSQL have their proprietary but different + * extension for that, SQLite does not support it at all. + * Hence, we must use a (slightly) less efficient, but + * SQL-compatible `WHERE EXIST` condition instead of a `JOIN`. + * This also means that we cannot use the succinct statements + * + * $this->descendants()->update(['owner_id' => $this->owner_id]) + * $this->all_photos()->update(['owner_id' => $this->owner_id]) + * + * because these method return queries which use `JOINS`. + * So, we need to build the queries from scratch. + * + * @return void + */ + public function fixOwnershipOfChildren(): void + { + $this->refreshNode(); + $lft = $this->_lft; + $rgt = $this->_rgt; + + BaseAlbumImpl::query() + ->whereExists(function (BaseBuilder $q) use ($lft, $rgt) { + $q + ->from('albums') + ->whereColumn('base_albums.id', '=', 'albums.id') + ->whereBetween('albums._lft', [$lft + 1, $rgt - 1]); + }) + ->update(['owner_id' => $this->owner_id]); + Photo::query() + ->whereExists(function (BaseBuilder $q) use ($lft, $rgt) { + $q + ->from('albums') + ->whereColumn('photos.album_id', '=', 'albums.id') + ->whereBetween('albums._lft', [$lft, $rgt]); + }) + ->update(['owner_id' => $this->owner_id]); } /** - * Return the full path of the album consisting of all its parents' titles. + * Create a new Eloquent query builder for the model. * - * @return string + * @param BaseBuilder $query + * + * @return AlbumBuilder */ - public static function getFullPath($album) + public function newEloquentBuilder($query): AlbumBuilder { - $title = [$album->title]; - $parentId = $album->parent_id; - while ($parentId) { - $parent = Album::find($parentId); - array_unshift($title, $parent->title); - $parentId = $parent->parent_id; - } + return new AlbumBuilder($query); + } + + /** + * Defines accessor for the Aspect Ratio. + * + * @return AspectRatioType|null + */ + protected function getAlbumThumbAspectRatioAttribute(): ?AspectRatioType + { + return AspectRatioType::tryFrom($this->attributes['album_thumb_aspect_ratio'] ?? '1/1'); + } + + /** + * Defines setter for Aspect Ratio. + * + * @param AspectRatioType|null $aspectRatio + * + * @return void + */ + protected function setAlbumThumbAspectRatioAttribute(?AspectRatioType $aspectRatio): void + { + $this->attributes['album_thumb_aspect_ratio'] = $aspectRatio?->value; + } - return implode('/', $title); + /** + * Defines accessor for the Album Timeline. + * + * @return TimelineAlbumGranularity|null + */ + protected function getAlbumTimelineAttribute(): ?TimelineAlbumGranularity + { + return TimelineAlbumGranularity::tryFrom($this->attributes['album_timeline']); } /** - * Setter/Mutator for attribute `min_taken_at`. + * Defines setter for Album Timeline. * - * Actually, this method should be a no-op and throw an exception. - * The attribute `min_taken_at` is a transient attribute of the model - * and cannot be persisted to database. - * It is calculated by the DB back-end upon fetching the model. - * Hence, it wrong to try to set this attribute. - * However, {@link AlbumCast::toTagAlbum()} does it nonetheless, so we - * don't throw an exception until that method is fixed. + * @param TimelineAlbumGranularity|null $album_timeline + * + * @return void + */ + protected function setAlbumTimelineAttribute(?TimelineAlbumGranularity $album_timeline): void + { + $this->attributes['album_timeline'] = $album_timeline?->value; + } + + /** + * Accessor for the "virtual" attribute {@link Album::$track_url}. * - * TODO: Fix {@link AlbumCast::toTagAlbum()}. + * This is a convenient method which wraps + * {@link Album::$track_short_path} into + * {@link \Illuminate\Support\Facades\Storage::url()}. * - * @param Carbon|null $value + * @return string|null the url of the track */ - protected function setMinTakenAtAttribute(?Carbon $value): void + public function getTrackUrlAttribute(): ?string { - // Uncomment the following line, after AlbumCast::toTagAlbum() has been fixed - //throw new \BadMethodCallException('Attribute "min_taken_at" must not be set as it is a virtual attribute'); + return $this->track_short_path !== null && $this->track_short_path !== '' ? + Storage::url($this->track_short_path) : null; } /** - * Setter/Mutator for attribute `max_taken_at`. + * Set the GPX track for the album. * - * Actually, this method should be a no-op and throw an exception. - * The attribute `max_taken_at` is a transient attribute of the model - * and cannot be persisted to database. - * It is calculated by the DB back-end upon fetching the model. - * Hence, it wrong to try to set this attribute. - * However, {@link AlbumCast::toTagAlbum()} does it nonetheless, so we - * don't throw an exception until that method is fixed. + * @param UploadedFile $file the GPX track file to be set * - * TODO: Fix {@link AlbumCast::toTagAlbum()}. + * @return void + * + * @throws ModelDBException + * @throws MediaFileOperationException + */ + public function setTrack(UploadedFile $file): void + { + try { + if ($this->track_short_path !== null) { + Storage::delete($this->track_short_path); + } + + $new_track_name = strtr(base64_encode(random_bytes(18)), '+/', '-_') . '.xml'; + Storage::putFileAs('tracks/', $file, $new_track_name); + $this->track_short_path = 'tracks/' . $new_track_name; + $this->save(); + } catch (ModelDBException $e) { + throw $e; + } catch (\Exception $e) { + throw new MediaFileOperationException('Could not save track file', $e); + } + } + + /** + * Delete the track of the album. + * + * @return void + * + * @throws ModelDBException + */ + public function deleteTrack(): void + { + if ($this->track_short_path === null) { + return; + } + Storage::delete($this->track_short_path); + $this->track_short_path = null; + $this->save(); + } + + protected function getAlbumSortingAttribute(): ?AlbumSortingCriterion + { + $sortingColumn = $this->attributes['album_sorting_col']; + $sortingOrder = $this->attributes['album_sorting_order']; + + return ($sortingColumn === null || $sortingOrder === null) ? + null : + new AlbumSortingCriterion( + ColumnSortingType::from($sortingColumn), + OrderSortingType::from($sortingOrder)); + } + + protected function setAlbumSortingAttribute(?AlbumSortingCriterion $sorting): void + { + $this->attributes['album_sorting_col'] = $sorting?->column->value; + $this->attributes['album_sorting_order'] = $sorting?->order->value; + } + + /** + * Returns the criterion acc. to which **albums** inside the album shall be sorted. * - * @param Carbon|null $value + * @return AlbumSortingCriterion */ - protected function setMaxTakenAtAttribute(?Carbon $value): void + public function getEffectiveAlbumSorting(): AlbumSortingCriterion { - // Uncomment the following line, after AlbumCast::toTagAlbum() has been fixed - //throw new \BadMethodCallException('Attribute "max_taken_at" must not be set as it is a virtual attribute'); + return $this->getAlbumSortingAttribute() ?? AlbumSortingCriterion::createDefault(); } } diff --git a/app/Models/BaseAlbumImpl.php b/app/Models/BaseAlbumImpl.php new file mode 100644 index 00000000000..beec5db3ff3 --- /dev/null +++ b/app/Models/BaseAlbumImpl.php @@ -0,0 +1,339 @@ +> | + * +---------+ | BaseAlbum | + * ^ ^ ^ +-----------------+ + * | | \ ^ ^ + * | | \ | | + * | \ \-----------------|------\ | + * | \----------------\ | \ | + * | +-------+ \ | + * | | Album | | | + * +---------------+ <---X +-------+ +----------+ + * | BaseAlbumImpl | | TagAlbum | + * +---------------+ <----------------X +----------+ + * + * (Note: A sideways arrow with an X, i.e. <-----X, shall denote a composite.) + * All child classes and this class extend + * {@link \Illuminate\Database\Eloquent\Model}, because they map to a single + * DB table. + * All methods and properties which are common to any sort of persistable + * album is declared in the interface {@link \App\Contracts\BaseAlbum} + * and thus {@link \App\Models\Album} and {@link \App\Models\TagAlbum} + * realize it. + * However, for any method which is implemented identically for all + * child classes and thus would normally be defined in a true parent class, + * the child classes forward the call to this class via the composite. + * For this reason, this class is called `BaseAlbumImpl` like _implementation_. + * Also note, that this class does not realize + * {@link \App\Contracts\BaseAlbum} intentionally. + * The interface {@link \App\Contracts\BaseAlbum} requires methods from + * albums which this class cannot implement reasonably, because the + * implementation depends on the specific sub-type of album and thus must + * be implemented by the child classes. + * For example, every album contains photos and thus must provide + * {@link \App\Contracts\Models\AbstractAlbum::$photos}, but the way how an album + * defines its collection of photos is specific for the album. + * Normally, a proper parent class would use abstract methods for these cases, + * but this class is not a proper parent class (it just provides an + * implementation of it) and we need this class to be instantiable. + * + * @property string $id + * @property int $legacy_id + * @property Carbon $created_at + * @property Carbon $updated_at + * @property string $title + * @property string|null $description + * @property PhotoLayoutType|null $photo_layout + * @property TimelinePhotoGranularity $photo_timeline + * @property int $owner_id + * @property User $owner + * @property bool $is_nsfw + * @property Collection $shared_with + * @property int|null $shared_with_count + * @property PhotoSortingCriterion|null $photo_sorting + * @property string|null $sorting_col + * @property string|null $sorting_order + * @property Collection $access_permissions + * @property int|null $access_permissions_count + * @property string|null $copyright + * + * @method static BaseAlbumImplBuilder|BaseAlbumImpl addSelect($column) + * @method static BaseAlbumImplBuilder|BaseAlbumImpl join(string $table, string $first, string $operator = null, string $second = null, string $type = 'inner', string $where = false) + * @method static BaseAlbumImplBuilder|BaseAlbumImpl joinSub($query, $as, $first, $operator = null, $second = null, $type = 'inner', $where = false) + * @method static BaseAlbumImplBuilder|BaseAlbumImpl leftJoin(string $table, string $first, string $operator = null, string $second = null) + * @method static BaseAlbumImplBuilder|BaseAlbumImpl newModelQuery() + * @method static BaseAlbumImplBuilder|BaseAlbumImpl newQuery() + * @method static BaseAlbumImplBuilder|BaseAlbumImpl orderBy($column, $direction = 'asc') + * @method static BaseAlbumImplBuilder|BaseAlbumImpl query() + * @method static BaseAlbumImplBuilder|BaseAlbumImpl select($columns = []) + * @method static BaseAlbumImplBuilder|BaseAlbumImpl whereCreatedAt($value) + * @method static BaseAlbumImplBuilder|BaseAlbumImpl whereDescription($value) + * @method static BaseAlbumImplBuilder|BaseAlbumImpl whereId($value) + * @method static BaseAlbumImplBuilder|BaseAlbumImpl whereIn(string $column, string $values, string $boolean = 'and', string $not = false) + * @method static BaseAlbumImplBuilder|BaseAlbumImpl whereIsNsfw($value) + * @method static BaseAlbumImplBuilder|BaseAlbumImpl whereLegacyId($value) + * @method static BaseAlbumImplBuilder|BaseAlbumImpl whereNotIn(string $column, string $values, string $boolean = 'and') + * @method static BaseAlbumImplBuilder|BaseAlbumImpl whereOwnerId($value) + * @method static BaseAlbumImplBuilder|BaseAlbumImpl whereSortingCol($value) + * @method static BaseAlbumImplBuilder|BaseAlbumImpl whereSortingOrder($value) + * @method static BaseAlbumImplBuilder|BaseAlbumImpl whereTitle($value) + * @method static BaseAlbumImplBuilder|BaseAlbumImpl whereUpdatedAt($value) + * + * @mixin \Eloquent + */ +class BaseAlbumImpl extends Model implements HasRandomID +{ + use HasAttributesPatch; + /** @phpstan-use HasRandomIDAndLegacyTimeBasedID */ + use HasRandomIDAndLegacyTimeBasedID; + use ThrowsConsistentExceptions; + use UTCBasedTimes; + use HasBidirectionalRelationships; + use ToArrayThrowsNotImplemented; + + protected $table = 'base_albums'; + + /** + * @var string The type of the primary key + */ + protected $keyType = RandomID::ID_TYPE; + + /** + * Indicates if the model's primary key is auto-incrementing. + * + * @var bool + */ + public $incrementing = false; + + /** + * The model's attributes. + * + * We must list all attributes explicitly here, otherwise the attributes + * of a new model will accidentally be set on the child class. + * The trait {@link \App\Models\Extensions\ForwardsToParentImplementation} + * only works properly, if it knows which attributes belong to the parent + * class and which attributes belong to the child class. + * + * @var array + */ + protected $attributes = [ + 'id' => null, + RandomID::LEGACY_ID_NAME => null, + 'created_at' => null, + 'updated_at' => null, + 'title' => null, // Sic! `title` is actually non-nullable, but using `null` here forces the caller to actually set a title before saving. + 'description' => null, + 'owner_id' => 0, + 'sorting_col' => null, + 'sorting_order' => null, + 'copyright' => null, + // Special visibility attributes + 'is_nsfw' => false, + 'photo_layout' => null, + ]; + + /** + * @var array + */ + protected $casts = [ + 'id' => RandomID::ID_TYPE, + RandomID::LEGACY_ID_NAME => RandomID::LEGACY_ID_TYPE, + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'is_nsfw' => 'boolean', + 'owner_id' => 'integer', + 'photo_layout' => PhotoLayoutType::class, + ]; + + /** + * The relationships that should always be eagerly loaded by default. + */ + protected $with = ['owner', 'access_permissions']; + + /** + * @param $query + * + * @return BaseAlbumImplBuilder + */ + public function newEloquentBuilder($query): BaseAlbumImplBuilder + { + return new BaseAlbumImplBuilder($query); + } + + /** + * Returns the relationship between an album and its owner. + * + * @return BelongsTo + */ + public function owner(): BelongsTo + { + return $this->belongsTo(User::class, 'owner_id', 'id'); + } + + /** + * Returns the relationship between an album and all users with whom + * this album is shared. + * + * @return BelongsToMany + */ + public function shared_with(): BelongsToMany + { + return $this->belongsToMany( + User::class, + APC::ACCESS_PERMISSIONS, + APC::BASE_ALBUM_ID, + APC::USER_ID + )->wherePivotNotNull('user_id'); + } + + /** + * Returns the relationship between an album and its associated permissions. + * + * @return hasMany + */ + public function access_permissions(): hasMany + { + return $this->hasMany(AccessPermission::class, APC::BASE_ALBUM_ID, 'id'); + } + + /** + * Returns the relationship between an album and its associated current user permissions. + * + * @return ?AccessPermission + */ + public function current_user_permissions(): AccessPermission|null + { + return $this->access_permissions->first(fn (AccessPermission $p) => $p->user_id !== null && $p->user_id === Auth::id()); + } + + /** + * Returns the relationship between an album and its associated public permissions. + * + * @return ?AccessPermission + */ + public function public_permissions(): AccessPermission|null + { + return $this->access_permissions->first(fn (AccessPermission $p) => $p->user_id === null); + } + + protected function getPhotoSortingAttribute(): ?PhotoSortingCriterion + { + $sortingColumn = $this->attributes['sorting_col']; + $sortingOrder = $this->attributes['sorting_order']; + + return ($sortingColumn === null || $sortingOrder === null) ? + null : + new PhotoSortingCriterion( + ColumnSortingType::from($sortingColumn), + OrderSortingType::from($sortingOrder)); + } + + protected function setPhotoSortingAttribute(?PhotoSortingCriterion $sorting): void + { + $this->attributes['sorting_col'] = $sorting?->column->value; + $this->attributes['sorting_order'] = $sorting?->order->value; + } + + /** + * Defines accessor for the Aspect Ratio. + * + * @return PhotoLayoutType|null + */ + protected function getPhotoLayoutAttribute(): ?PhotoLayoutType + { + return PhotoLayoutType::tryFrom($this->attributes['photo_layout']); + } + + /** + * Defines setter for Aspect Ratio. + * + * @param PhotoLayoutType|null $aspectRatio + * + * @return void + */ + protected function setPhotoLayoutAttribute(?PhotoLayoutType $aspectRatio): void + { + $this->attributes['photo_layout'] = $aspectRatio?->value; + } + + /** + * Defines accessor for the Photo Timeline. + * + * @return TimelinePhotoGranularity|null + */ + protected function getPhotoTimelineAttribute(): ?TimelinePhotoGranularity + { + return TimelinePhotoGranularity::tryFrom($this->attributes['photo_timeline']); + } + + /** + * Defines setter for Photo Timeline. + * + * @param TimelinePhotoGranularity|null $photo_timeline + * + * @return void + */ + protected function setPhotoTimelineAttribute(?TimelinePhotoGranularity $photo_timeline): void + { + $this->attributes['photo_timeline'] = $photo_timeline?->value; + } +} diff --git a/app/Models/Builders/AccessPermissionBuilder.php b/app/Models/Builders/AccessPermissionBuilder.php new file mode 100644 index 00000000000..33c11c8689e --- /dev/null +++ b/app/Models/Builders/AccessPermissionBuilder.php @@ -0,0 +1,20 @@ + + */ +class AccessPermissionBuilder extends FixedQueryBuilder +{ +} \ No newline at end of file diff --git a/app/Models/Builders/AlbumBuilder.php b/app/Models/Builders/AlbumBuilder.php new file mode 100644 index 00000000000..d0e5a8aca2e --- /dev/null +++ b/app/Models/Builders/AlbumBuilder.php @@ -0,0 +1,288 @@ + + */ +class AlbumBuilder extends NSQueryBuilder +{ + /** @phpstan-use FixedQueryBuilderTrait */ + use FixedQueryBuilderTrait; + + /** + * Get the hydrated models without eager loading. + * + * Adds the "virtual" columns min_taken_at, max_taken_at as well as + * num_children and num_photos to the query, if a "full" model is + * requested, i.e. if the selected columns are `*` or not given at all. + * + * @param string[]|string $columns + * + * @return array + * + * @throws InternalLycheeException + */ + public function getModels($columns = ['*']): array + { + $albumQueryPolicy = resolve(AlbumQueryPolicy::class); + $baseQuery = $this->getQuery(); + + if ( + ($columns === ['*'] || $columns === ['albums.*']) && + ($baseQuery->columns === ['*'] || $baseQuery->columns === ['albums.*'] || $baseQuery->columns === null) + ) { + $countChildren = DB::table('albums', 'a') + ->selectRaw('COUNT(*)') + ->whereColumn('a.parent_id', '=', 'albums.id'); + + $countPhotos = DB::table('photos', 'p') + ->selectRaw('COUNT(*)') + ->whereColumn('p.album_id', '=', 'albums.id'); + + $this->addSelect([ + 'min_taken_at' => $this->getTakenAtSQL()->selectRaw('MIN(taken_at)'), + 'max_taken_at' => $this->getTakenAtSQL()->selectRaw('MAX(taken_at)'), + 'num_children' => $this->applyVisibilityConditioOnSubalbums($countChildren, $albumQueryPolicy), + 'num_photos' => $this->applyVisibilityConditioOnPhotos($countPhotos, $albumQueryPolicy), + ]); + } + + // The parent method returns a `Model[]`, but we must return + // `Album[]` and we know that this is indeed the case as we have + // queried for albums + return parent::getModels($columns); + } + + /** + * Get statistics of errors of the tree. + * + * @return array{oddness:int,duplicates:int,wrong_parent:int,missing_parent:int} + * + * @throws QueryBuilderException + */ + public function countErrors(): array + { + try { + return parent::countErrors(); + } catch (\Throwable $e) { + throw new QueryBuilderException($e); + } + } + + /** + * Generate a query which tie the taken_at attribute from photos to the albums. + * This makes use of nested set, which means that ALL the sub albums are considered. + * Do note that no visibility filters are applied. + * + * @return Builder + * + * @throws \InvalidArgumentException + */ + private function getTakenAtSQL(): Builder + { + // Note: + // 1. The order of JOINS is important. + // Although `JOIN` is cumulative, i.e. + // `photos JOIN albums` and `albums JOIN photos` + // should be identical, it is not with respect to the + // MySQL query optimizer. + // For an efficient query it is paramount, that the + // query first filters out all child albums and then + // selects the most/least recent photo within the child + // albums. + // If the JOIN starts with photos, MySQL first selects + // all photos of the entire gallery. + // 2. The query must use the aggregation functions + // `MIN`/`MAX`, we must not use `ORDER BY ... LIMIT 1`. + // Otherwise, the MySQL optimizer first selects the + // photos and then joins with albums (i.e. the same + // effect as above). + // The background is rather difficult to explain, but is + // due to MySQL's "Limit Query Optimization" + // (https://dev.mysql.com/doc/refman/8.0/en/limit-optimization.html). + // Basically, if MySQL sees an `ORDER BY ... LIMIT ...` + // construction and has an applicable index for that, + // MySQL's built-in heuristic chooses that index with high + // priority and does not consider any alternatives. + // In this specific case, this heuristic fails splendidly. + // + // Further note, that PostgreSQL's optimizer is not affected + // by any of these tricks. + // The optimized query plan for PostgreSQL is always the same. + // Good PosgreSQL :-) + // + // We must not use `Album::query->` to start the query, but + // use a non-Eloquent query here to avoid an infinite loop + // with this query builder. + return DB::table('albums', 'a') + ->join('photos', 'album_id', '=', 'a.id') + ->whereColumn('a._lft', '>=', 'albums._lft') + ->whereColumn('a._rgt', '<=', 'albums._rgt') + ->whereNotNull('taken_at'); + } + + /** + * Apply Visibiltiy conditions. + * This a simplified version of AlbumQueryPolicy::applyVisibilityFilter(). + * + * @param Builder $countQuery + * + * @return Builder Query with the visibility requirements applied + */ + private function applyVisibilityConditioOnSubalbums(Builder $countQuery, AlbumQueryPolicy $albumQueryPolicy): Builder + { + if (Auth::user()?->may_administrate === true) { + return $countQuery; + } + + $userID = Auth::id(); + + // Only join with base_album (used to get owner_id) when user is logged in + $countQuery->when(Auth::check(), + fn ($q) => $albumQueryPolicy->joinBaseAlbumOwnerId( + query: $q, + second: 'a.id', + full: false, + ) + ); + + // We must left join with `conputed_access_permissions`. + // We must restrict the `LEFT JOIN` to the user ID which + // is also used in the outer `WHERE`-clause. + // See `applyVisibilityFilter` and `appendAccessibilityConditions` + // in AlbumQueryPolicy. + $albumQueryPolicy->joinSubComputedAccessPermissions( + query: $countQuery, + second: 'a.id', + type: 'left', + ); + + // We must wrap everything into an outer query to avoid any undesired + // effects in case that the original query already contains an + // "OR"-clause. + // The sub-query only uses properties (i.e. columns) which are + // defined on the common base model for all albums. + $visibilitySubQuery = function (Builder $query2) use ($userID) { + $query2 + // We laverage that IS_LINK_REQUIRED is NULL if the album is NOT shared publically (left join). + ->where(APC::COMPUTED_ACCESS_PERMISSIONS . '.' . APC::IS_LINK_REQUIRED, '=', false) + ->when($userID !== null, + // Current user is the owner of the album + fn ($q) => $q + ->orWhere('base_albums.owner_id', '=', $userID)); + }; + + return $countQuery->where($visibilitySubQuery); + } + + /** + * Apply Visibiltiy conditions. + * This a simplified version of PhotoQueryPolicy::applyVisibilityFilter(). + * + * @param Builder $countQuery + * + * @return Builder Query with the visibility requirements applied + */ + private function applyVisibilityConditioOnPhotos(Builder $countQuery, AlbumQueryPolicy $albumQueryPolicy): Builder + { + if (Auth::user()?->may_administrate === true) { + return $countQuery; + } + + $userID = Auth::id(); + + // Only join with base_album (used to get owner_id) when user is logged in + $countQuery->when($userID !== null, + fn ($q) => $albumQueryPolicy->joinBaseAlbumOwnerId( + query: $q, + second: 'p.album_id', + full: false, + ) + ); + + $albumQueryPolicy->joinSubComputedAccessPermissions( + query: $countQuery, + second: 'p.album_id', + type: 'left', + ); + + // We must wrap everything into an outer query to avoid any undesired + // effects in case that the original query already contains an + // "OR"-clause. + $visibilitySubQuery = function ($query2) use ($userID) { + $query2 + ->where(APC::COMPUTED_ACCESS_PERMISSIONS . '.' . APC::IS_LINK_REQUIRED, '=', false) + ->when($userID !== null, + fn ($query) => $query + ->orWhere('base_albums.owner_id', '=', $userID) + ->orWhere('p.owner_id', '=', $userID) + ); + }; + + return $countQuery->where($visibilitySubQuery); + } + + /** + * Scope limits query to select just root node. + * + * @return AlbumBuilder + */ + public function whereIsRoot(): AlbumBuilder + { + $this->query->whereNull($this->model->getParentIdName()); + + return $this; + } +} diff --git a/app/Models/Builders/BaseAlbumImplBuilder.php b/app/Models/Builders/BaseAlbumImplBuilder.php new file mode 100644 index 00000000000..d862c633c0b --- /dev/null +++ b/app/Models/Builders/BaseAlbumImplBuilder.php @@ -0,0 +1,20 @@ + + */ +class BaseAlbumImplBuilder extends FixedQueryBuilder +{ +} \ No newline at end of file diff --git a/app/Models/Builders/ConfigsBuilder.php b/app/Models/Builders/ConfigsBuilder.php new file mode 100644 index 00000000000..39242bf859c --- /dev/null +++ b/app/Models/Builders/ConfigsBuilder.php @@ -0,0 +1,20 @@ + + */ +class ConfigsBuilder extends FixedQueryBuilder +{ +} \ No newline at end of file diff --git a/app/Models/Builders/JobHistoryBuilder.php b/app/Models/Builders/JobHistoryBuilder.php new file mode 100644 index 00000000000..f3b66de5db6 --- /dev/null +++ b/app/Models/Builders/JobHistoryBuilder.php @@ -0,0 +1,20 @@ + + */ +class JobHistoryBuilder extends FixedQueryBuilder +{ +} \ No newline at end of file diff --git a/app/Models/Builders/OauthCredentialBuilder.php b/app/Models/Builders/OauthCredentialBuilder.php new file mode 100644 index 00000000000..7b7a9a0e1c5 --- /dev/null +++ b/app/Models/Builders/OauthCredentialBuilder.php @@ -0,0 +1,20 @@ + + */ +class OauthCredentialBuilder extends FixedQueryBuilder +{ +} \ No newline at end of file diff --git a/app/Models/Builders/PhotoBuilder.php b/app/Models/Builders/PhotoBuilder.php new file mode 100644 index 00000000000..2d6fc5af74e --- /dev/null +++ b/app/Models/Builders/PhotoBuilder.php @@ -0,0 +1,20 @@ + + */ +class PhotoBuilder extends FixedQueryBuilder +{ +} \ No newline at end of file diff --git a/app/Models/Builders/SizeVariantBuilder.php b/app/Models/Builders/SizeVariantBuilder.php new file mode 100644 index 00000000000..462a9c72251 --- /dev/null +++ b/app/Models/Builders/SizeVariantBuilder.php @@ -0,0 +1,20 @@ + + */ +class SizeVariantBuilder extends FixedQueryBuilder +{ +} \ No newline at end of file diff --git a/app/Models/Builders/SymLinkBuilder.php b/app/Models/Builders/SymLinkBuilder.php new file mode 100644 index 00000000000..f11b200cc55 --- /dev/null +++ b/app/Models/Builders/SymLinkBuilder.php @@ -0,0 +1,20 @@ + + */ +class SymLinkBuilder extends FixedQueryBuilder +{ +} \ No newline at end of file diff --git a/app/Models/Builders/TagAlbumBuilder.php b/app/Models/Builders/TagAlbumBuilder.php new file mode 100644 index 00000000000..d22b86a97ab --- /dev/null +++ b/app/Models/Builders/TagAlbumBuilder.php @@ -0,0 +1,60 @@ + + */ +class TagAlbumBuilder extends FixedQueryBuilder +{ + /** + * Get the hydrated models without eager loading. + * + * @param array|string $columns + * + * @return array + * + * @throws QueryBuilderException + */ + public function getModels($columns = ['*']): array + { + $baseQuery = $this->getQuery(); + if (($baseQuery->columns === null || count($baseQuery->columns) === 0) && is_string($baseQuery->from)) { + $this->select([$baseQuery->from . '.*']); + } + + if ( + ($columns === ['*'] || $columns === ['tag_albums.*']) && + ($baseQuery->columns === ['*'] || $baseQuery->columns === ['tag_albums.*']) + ) { + $this->addSelect([ + DB::raw('null as max_taken_at'), + DB::raw('null as min_taken_at'), + ]); + } + + return parent::getModels($columns); + } +} diff --git a/app/Models/Builders/UserBuilder.php b/app/Models/Builders/UserBuilder.php new file mode 100644 index 00000000000..abb699b6b86 --- /dev/null +++ b/app/Models/Builders/UserBuilder.php @@ -0,0 +1,20 @@ + + */ +class UserBuilder extends FixedQueryBuilder +{ +} \ No newline at end of file diff --git a/app/Models/Configs.php b/app/Models/Configs.php index 810d92a5759..7de84662897 100644 --- a/app/Models/Configs.php +++ b/app/Models/Configs.php @@ -1,13 +1,29 @@ */ - protected $fillable = ['key', 'value', 'cat', 'type_range', 'confidentiality', 'description']; + protected $fillable = ['key', 'value', 'cat', 'type_range', 'is_secret', 'description', 'level']; /** * this is a parameter for Laravel to indicate that there is no created_at, updated_at columns. */ public $timestamps = false; - /** We define this as a singleton */ - private static $cache = null; + /** + * We define this as a singleton. + * + * @var array + */ + private static array $cache = []; + + /** + * @param $query + * + * @return ConfigsBuilder + */ + public function newEloquentBuilder($query): ConfigsBuilder + { + return new ConfigsBuilder($query); + } /** * Sanity check. * - * @param $value + * @param string|null $candidateValue + * @param string|null $message_template * * @return string */ - public function sanity($value) + public function sanity(?string $candidateValue, ?string $message_template = null): string { - if (!defined('INT')) { - define('INT', 'int'); - define('STRING', 'string'); - define('STRING_REQ', 'string_required'); - define('BOOL', '0|1'); - define('TERNARY', '0|1|2'); - define('DISABLED', ''); - define('LICENSE', 'license'); - } - $message = ''; - $val_range = [BOOL => explode('|', BOOL), TERNARY => explode('|', TERNARY)]; + $val_range = [ + ConfigType::BOOL->value => explode('|', ConfigType::BOOL->value), + ConfigType::TERNARY->value => explode('|', ConfigType::TERNARY->value), + ]; + $message_template ??= 'Error: Wrong property for ' . $this->key . ', expected %s, got ' . ($candidateValue ?? 'NULL') . '.'; switch ($this->type_range) { - case STRING: - case DISABLED: + case ConfigType::STRING->value: + case ConfigType::DISABLED->value: break; - case STRING_REQ: - if ($value == '') { - $message = 'Error: ' . $this->key . ' empty or not set in database'; + case ConfigType::STRING_REQ->value: + if ($candidateValue === '' || $candidateValue === null) { + $message = 'Error: ' . $this->key . ' empty or not set'; } break; - case INT: + case ConfigType::INT->value: // we make sure that we only have digits in the chosen value. - if (!ctype_digit(strval($value))) { - $message = 'Error: Wrong property for ' . $this->key . ' in database, expected positive integer.'; + if (!ctype_digit(strval($candidateValue))) { + $message = sprintf($message_template, 'positive integer or 0'); + } + break; + case ConfigType::POSTIIVE->value: + if (!ctype_digit(strval($candidateValue)) || intval($candidateValue, 10) === 0) { + $message = sprintf($message_template, 'strictly positive integer'); + } + break; + case ConfigType::BOOL->value: + case ConfigType::TERNARY->value: + if (!in_array($candidateValue, $val_range[$this->type_range], true)) { // BOOL or TERNARY + $message = sprintf($message_template, implode(' or ', $val_range[$this->type_range])); } break; - case BOOL: - case TERNARY: - if (!in_array($value, $val_range[$this->type_range])) { // BOOL or TERNARY - $message = 'Error: Wrong property for ' . $this->key - . ' in database, expected ' . implode( - ' or ', - $val_range[$this->type_range] - ) . ', got ' . ($value ? $value : 'NULL'); + case ConfigType::LICENSE->value: + if (LicenseType::tryFrom($candidateValue) === null) { + $message = sprintf($message_template, 'a valid license'); } break; - case LICENSE: - if (!in_array($value, Helpers::get_all_licenses())) { - $message = 'Error: Wrong property for ' . $this->key - . ' in database, expected a valide license, got ' . ($value ? $value : 'NULL'); + case ConfigType::MAP_PROVIDER->value: + if (MapProviders::tryFrom($candidateValue) === null) { + $message = sprintf($message_template, 'a valid map provider'); } break; default: $values = explode('|', $this->type_range); - if (!in_array($value, $values)) { - $message = 'Error: Wrong property for ' . $this->key - . ' in database, expected ' . implode(' or ', $values) - . ', got ' . ($value ? $value : 'NULL'); + if (!in_array($candidateValue, $values, true)) { + $message = sprintf($message_template, implode(' or ', $values)); } break; } @@ -121,157 +160,177 @@ public function sanity($value) /** * Cache and return the current settings of this Lychee installation. * - * @return array + * @return array */ - public static function get() + public static function get(): array { - if (self::$cache) { + if (count(self::$cache) > 0) { return self::$cache; } try { - $query = Configs::select([ - 'key', - 'value', - ]); - $return = $query->pluck('value', 'key')->all(); - - $return['sorting_Photos'] = 'ORDER BY ' . $return['sorting_Photos_col'] . ' ' . $return['sorting_Photos_order']; - $return['sorting_Albums'] = 'ORDER BY ' . $return['sorting_Albums_col'] . ' ' . $return['sorting_Albums_order']; - - self::$cache = $return; - } catch (Exception $e) { - self::$cache = null; - - return null; + self::$cache = Configs::query() + ->select(['key', 'value']) + ->pluck('value', 'key') + ->all(); + // @codeCoverageIgnoreStart + } catch (\Throwable) { + self::$cache = []; } + // @codeCoverageIgnoreEnd - return $return; + return self::$cache; } /** * The best way to request a value from the config... * * @param string $key - * @param mixed $default * - * @return int|bool|string + * @return int|bool|string|null + * + * @throws ConfigurationKeyMissingException if a key does not exist */ - public static function get_value(string $key, $default = null) + public static function getValue(string $key): int|bool|string|null { - if (!self::$cache) { - /* - * try is here because when composer does the package discovery it - * looks at AppServiceProvider which register a singleton with: - * $compressionQuality = Configs::get_value('compression_quality', 90); - * - * this will fail for sure as the config table does not exist yet - */ - try { - self::get(); - } catch (QueryException $e) { - return $default; - } + if (count(self::$cache) === 0) { + self::get(); } - if (!isset(self::$cache[$key])) { + if (!array_key_exists($key, self::$cache)) { /* * For some reason the $default is not returned above... */ - try { - Logs::notice(__METHOD__, __LINE__, $key . ' does not exist in config (local) !'); - } catch (Exception $e) { - // yeah we do nothing because we cannot do anything in that case ... :p - } + // @codeCoverageIgnoreStart + Log::critical(__METHOD__ . ':' . __LINE__ . ' ' . $key . ' does not exist in config (local) !'); - return $default; + throw new ConfigurationKeyMissingException($key . ' does not exist in config!'); + // @codeCoverageIgnoreEnd } return self::$cache[$key]; } /** - * Update Lychee configuration - * Note that we must invalidate the cache now. + * Get string configuration value. * * @param string $key - * @param $value * - * @return bool returns true when successful + * @return string + * + * @throws ConfigurationKeyMissingException */ - public static function set(string $key, $value) + public static function getValueAsString(string $key): string { - $config = Configs::where('key', '=', $key)->first(); - - // first() may return null, fixup 'Creating default object from empty value' error - // we also log a warning - if ($config == null) { - Logs::warning(__METHOD__, __LINE__, 'key ' . $key . ' not found!'); - - return true; - } - - /** - * Sanity check. :). - */ - $message = $config->sanity($value); - if ($message != '') { - Logs::error(__METHOD__, __LINE__, $message); - - return false; - } - - $config->value = $value; - - try { - $config->save(); - } catch (Exception $e) { - Logs::error(__METHOD__, __LINE__, $e->getMessage()); - - return false; - } - - // invalidate cache. - self::$cache = null; - - return true; + return strval(self::getValue($key)); } /** - * Define scopes. + * Get string configuration value. + * + * @param string $key + * + * @return int + * + * @throws ConfigurationKeyMissingException */ + public static function getValueAsInt(string $key): int + { + return intval(self::getValue($key)); + } /** - * @param $query + * Get bool configuration value. * - * @return mixed + * @param string $key + * + * @return bool + * + * @throws ConfigurationKeyMissingException */ - public function scopePublic(Builder $query) + public static function getValueAsBool(string $key): bool { - return $query->where('confidentiality', '=', 0); + return self::getValue($key) === '1'; } /** - * Logged user can see. + * @template T of BackedEnum * - * @param $query + * @param string $key + * @param class-string $type * - * @return mixed + * @return T|null */ - public function scopeInfo(Builder $query) + public static function getValueAsEnum(string $key, string $type): \BackedEnum|null { - return $query->where('confidentiality', '<=', 2); + if (!function_exists('enum_exists') || !enum_exists($type) || !method_exists($type, 'tryFrom')) { + // @codeCoverageIgnoreStart + throw new UnexpectedException(); + // @codeCoverageIgnoreEnd + } + + return $type::tryFrom(self::getValue($key)); } /** - * Only admin can see. + * Update Lychee configuration + * Note that we must invalidate the cache now. * - * @param $query + * @param string $key + * @param string|int|bool $value * - * @return mixed + * @return void + * + * @throws InvalidConfigOption + * @throws QueryBuilderException + */ + public static function set(string $key, string|int|bool|\BackedEnum $value): void + { + try { + /** @var Configs $config */ + $config = Configs::query() + ->where('key', '=', $key) + ->firstOrFail(); + + // For BackEnm we take the value. In theory this is no longer necessary because we enforce at the column type. + if ($value instanceof \BackedEnum) { + $value = $value->value; + } + + $strValue = match (gettype($value)) { + 'boolean' => $value === true ? '1' : '0', + 'integer', 'string' => strval($value), + // @codeCoverageIgnoreStart + default => throw new LycheeAssertionError('Unexpected type'), + // @codeCoverageIgnoreEnd + }; + + /** + * Sanity check. :). + */ + $message = $config->sanity($strValue); + if ($message !== '') { + throw new InvalidConfigOption($message); + } + $config->value = $strValue; + $config->save(); + // @codeCoverageIgnoreStart + } catch (ModelNotFoundException $e) { + throw new InvalidConfigOption('key ' . $key . ' not found!', $e); + } catch (ModelDBException $e) { + throw new InvalidConfigOption('Could not save configuration', $e); + // @codeCoverageIgnoreEnd + } finally { + // invalidate cache. + self::$cache = []; + } + } + + /** + * Reset the cache. */ - public function scopeAdmin(Builder $query) + public static function invalidateCache(): void { - return $query->where('confidentiality', '<=', 3); + self::$cache = []; } } diff --git a/app/Models/Extensions/AbstractBaseConfigMigration.php b/app/Models/Extensions/AbstractBaseConfigMigration.php new file mode 100644 index 00000000000..f7a7c7529fe --- /dev/null +++ b/app/Models/Extensions/AbstractBaseConfigMigration.php @@ -0,0 +1,34 @@ + + */ + abstract public function getConfigs(): array; + + /** + * Run the migrations. + */ + abstract public function up(): void; + + /** + * Reverse the migrations. + */ + abstract public function down(): void; +} diff --git a/app/Models/Extensions/AlbumBooleans.php b/app/Models/Extensions/AlbumBooleans.php deleted file mode 100644 index 6b7961e8f9c..00000000000 --- a/app/Models/Extensions/AlbumBooleans.php +++ /dev/null @@ -1,70 +0,0 @@ -public == 1; - } - - /** - * Return whether or not public users will see the full photo. - * - * @return bool - */ - public function is_full_photo_visible() - { - if ($this->is_public()) { - return $this->full_photo == 1; - } else { - return Configs::get_value('full_photo', '1') === '1'; - } - } - - /** - * Return whether or not public users can download photos. - * - * @return bool - */ - public function is_downloadable(): bool - { - if ($this->is_public()) { - return $this->downloadable == 1; - } else { - return Configs::get_value('downloadable', '0') === '1'; - } - } - - /** - * Return whether or not display share button. - * - * @return bool - */ - public function is_share_button_visible(): bool - { - if ($this->is_public()) { - return $this->share_button_visible == 1; - } else { - return Configs::get_value('share_button_visible', '0') === '1'; - } - } - - public function is_smart() - { - return $this->smart; - } - - public function is_tag_album() - { - return $this->smart && !empty($this->showtags); - } -} diff --git a/app/Models/Extensions/AlbumCast.php b/app/Models/Extensions/AlbumCast.php deleted file mode 100644 index 650df22963b..00000000000 --- a/app/Models/Extensions/AlbumCast.php +++ /dev/null @@ -1,99 +0,0 @@ - strval($this->id), - 'title' => $this->title, - 'public' => Helpers::str_of_bool($this->is_public()), - 'full_photo' => Helpers::str_of_bool($this->is_full_photo_visible()), - 'visible' => strval($this->viewable), - 'nsfw' => strval($this->nsfw), - 'parent_id' => $this->str_parent_id(), - 'cover_id' => strval($this->cover_id), - 'description' => strval($this->description), - - 'downloadable' => Helpers::str_of_bool($this->is_downloadable()), - 'share_button_visible' => Helpers::str_of_bool($this->is_share_button_visible()), - - 'created_at' => $this->created_at->format(\DateTimeInterface::ATOM), - 'updated_at' => $this->updated_at->format(\DateTimeInterface::ATOM), - 'min_taken_at' => $this->min_taken_at !== null ? $this->min_taken_at->format(\DateTimeInterface::ATOM) : null, - 'max_taken_at' => $this->max_taken_at !== null ? $this->max_taken_at->format(\DateTimeInterface::ATOM) : null, - - // Parse password - 'password' => Helpers::str_of_bool($this->password != ''), - 'license' => $this->get_license(), - - // Parse Ordering - 'sorting_col' => $this->sorting_col, - 'sorting_order' => $this->sorting_order, - - 'thumb' => optional($this->get_thumb())->toArray(), - 'has_albums' => Helpers::str_of_bool($this->isLeaf() === false), - ]; - - if ($this->is_tag_album()) { - $return['tag_album'] = '1'; - $return['show_tags'] = $this->showtags; - } - - if (!empty($this->showtags) || !$this->smart) { - if (AccessControl::is_logged_in()) { - $return['owner'] = $this->owner->name(); - } - } - - return $return; - } - - public function toTagAlbum(): TagAlbum - { - /** - * ! DO NOT USE ->save() on this object! - * It is convenient to quickly convert, but if you want to ->save(), - * this will create conflict in the database as NestedTree thinks it - * is a new object and not an already existing one. - */ - $tag_album = resolve(TagAlbum::class); - $tag_album->id = $this->id; - $tag_album->title = $this->title; - $tag_album->owner_id = $this->owner_id; - $tag_album->parent_id = $this->parent_id; - $tag_album->_lft = $this->_lft; - $tag_album->_rgt = $this->_rgt; - $tag_album->description = $this->description ?? ''; - $tag_album->min_taken_at = $this->min_taken_at; - $tag_album->max_taken_at = $this->max_taken_at; - $tag_album->public = $this->public; - $tag_album->full_photo = $this->full_photo; - $tag_album->viewable = $this->viewable; - $tag_album->nsfw = $this->nsfw; - $tag_album->downloadable = $this->downloadable; - $tag_album->password = $this->password; - $tag_album->license = $this->license; - $tag_album->created_at = $this->created_at; - $tag_album->updated_at = $this->updated_at; - $tag_album->share_button_visible = $this->share_button_visible; - $tag_album->smart = $this->smart; - $tag_album->showtags = $this->showtags; - - return $tag_album; - } -} diff --git a/app/Models/Extensions/AlbumGetters.php b/app/Models/Extensions/AlbumGetters.php deleted file mode 100644 index 45ebb7f5dab..00000000000 --- a/app/Models/Extensions/AlbumGetters.php +++ /dev/null @@ -1,121 +0,0 @@ -sorting_col == null || $this->sorting_col == '') { - $sort_col = Configs::get_value('sorting_Photos_col'); - $sort_order = Configs::get_value('sorting_Photos_order'); - } else { - $sort_col = $this->sorting_col; - $sort_order = $this->sorting_order; - } - - return [$sort_col, $sort_order]; - } - - /** - * Return the Album license or the default one. - * - * @return string - */ - public function get_license(): string - { - if ($this->license == 'none') { - return Configs::get_value('default_license'); - } - - return $this->license; - } - - /** - * Return a query builder or an SQL relation for the list of photos. - * - * See comment in {@link \App\SmartAlbums\BareSmartAlbum} why we need - * an ambitious return type here. - * - * @return Builder|HasMany - */ - public function get_photos() - { - return $this->photos(); - } - - /** - * Return a Query with all the subsequent pictures. - * - * @return Builder - */ - public function get_all_photos(): Builder - { - return Photo::query() - ->leftJoin('albums', 'photos.album_id', '=', 'albums.id') - ->select('photos.*') - ->where('albums._lft', '>=', $this->_lft) - ->where('albums._rgt', '<=', $this->_rgt); - } - - public function get_thumb(): ?Thumb - { - if ($this->cover != null) { - $cover = $this->cover; - } else { - [$sort_col, $sort_order] = $this->get_sort(); - - /* @var Builder|HasMany $sql */ - if ($this->is_smart()) { - $sql = $this->get_photos(); - } else { - $sql = $this->get_all_photos(); - } - - //? apply safety filter : Do not leak pictures which are not ours - $forbiddenID = resolve(PublicIds::class)->getNotAccessible(); - - if ($forbiddenID != null && !$forbiddenID->isEmpty()) { - $sql = $sql->where( - fn ($q) => $q->whereNull('album_id') - ->orWhereNotIn('album_id', $forbiddenID) - ); - } - - $cover = $sql->orderBy('star', 'DESC') - ->orderBy($sort_col, $sort_order) - ->orderBy('photos.id', 'ASC') - ->limit(1) - ->first(); - } - - return optional($cover)->toThumb(); - } - - public function get_children() - { - $sortingCol = Configs::get_value('sorting_Albums_col'); - $sortingOrder = Configs::get_value('sorting_Albums_order'); - - $sql = self::query()->where('parent_id', '=', $this->id); - //? apply safety filter : Do not leak albums which are not visible - $sql = $this->publicViewable($sql); - - return $this->customSort($sql, $sortingCol, $sortingOrder); - } -} diff --git a/app/Models/Extensions/AlbumSetters.php b/app/Models/Extensions/AlbumSetters.php deleted file mode 100644 index 2200acc4c46..00000000000 --- a/app/Models/Extensions/AlbumSetters.php +++ /dev/null @@ -1,11 +0,0 @@ -toArray(); - } -} diff --git a/app/Models/Extensions/AlbumStringify.php b/app/Models/Extensions/AlbumStringify.php deleted file mode 100644 index ba32ce3e5aa..00000000000 --- a/app/Models/Extensions/AlbumStringify.php +++ /dev/null @@ -1,16 +0,0 @@ -parent_id == null ? '' : strval($this->parent_id); - } -} diff --git a/app/Models/Extensions/BaseAlbum.php b/app/Models/Extensions/BaseAlbum.php new file mode 100644 index 00000000000..ea1604d9767 --- /dev/null +++ b/app/Models/Extensions/BaseAlbum.php @@ -0,0 +1,143 @@ + $access_permissions + * @property Carbon|null $min_taken_at + * @property Carbon|null $max_taken_at + * @property PhotoSortingCriterion|null $photo_sorting + * @property BaseAlbumImpl $base_class + */ +abstract class BaseAlbum extends Model implements AbstractAlbum, HasRandomID +{ + use HasBidirectionalRelationships; + use ForwardsToParentImplementation, ThrowsConsistentExceptions { + ForwardsToParentImplementation::delete insteadof ThrowsConsistentExceptions; + } + + /** + * @var string The type of the primary key + */ + protected $keyType = RandomID::ID_TYPE; + + /** + * Indicates if the model's primary key is auto-incrementing. + * + * @var bool + */ + public $incrementing = false; + + /** + * @return BelongsTo + */ + public function base_class(): BelongsTo + { + return $this->belongsTo(BaseAlbumImpl::class, 'id', 'id'); + } + + /** + * Returns the relationship between an album and its owner. + * + * @return BelongsTo + */ + public function owner(): BelongsTo + { + return $this->base_class->owner(); + } + + /** + * Returns the relationship between an album and all users with whom + * this album is shared. + * + * @return BelongsToMany + */ + public function shared_with(): BelongsToMany + { + return $this->base_class->shared_with(); + } + + /** + * Returns the relationship between an album and its associated permissions. + * + * @return HasMany + */ + public function access_permissions(): HasMany + { + return $this->base_class->access_permissions(); + } + + /** + * Returns the relationship between an album and its associated current user permissions. + * + * @return AccessPermission|null + */ + public function current_user_permissions(): AccessPermission|null + { + return $this->base_class->current_user_permissions(); + } + + /** + * Returns the relationship between an album and its associated public permissions. + * + * @return AccessPermission|null + */ + public function public_permissions(): AccessPermission|null + { + return $this->base_class->public_permissions(); + } + + /** + * @return Relation<\App\Models\Photo,AbstractAlbum&Model,Collection> + */ + abstract public function photos(): Relation; + + /** + * Returns the criterion acc. to which **photos** inside the album shall be sorted. + * + * @return PhotoSortingCriterion the attribute acc. to which **photos** inside the album shall be sorted + */ + public function getEffectivePhotoSorting(): PhotoSortingCriterion + { + return $this->photo_sorting ?? PhotoSortingCriterion::createDefault(); + } +} diff --git a/app/Models/Extensions/BaseConfigMigration.php b/app/Models/Extensions/BaseConfigMigration.php new file mode 100644 index 00000000000..cb04f780ad9 --- /dev/null +++ b/app/Models/Extensions/BaseConfigMigration.php @@ -0,0 +1,31 @@ +insert($this->getConfigs()); + } + + /** + * Reverse the migrations. + */ + final public function down(): void + { + $keys = collect($this->getConfigs())->map(fn ($v) => $v['key'])->all(); + DB::table('configs')->whereIn('key', $keys)->delete(); + } +} diff --git a/app/Models/Extensions/BaseConfigMigrationReversed.php b/app/Models/Extensions/BaseConfigMigrationReversed.php new file mode 100644 index 00000000000..980826cd927 --- /dev/null +++ b/app/Models/Extensions/BaseConfigMigrationReversed.php @@ -0,0 +1,31 @@ +getConfigs())->map(fn ($v) => $v['key'])->all(); + DB::table('configs')->whereIn('key', $keys)->delete(); + } + + /** + * Reverse the migrations. + */ + final public function down(): void + { + DB::table('configs')->insert($this->getConfigs()); + } +} diff --git a/app/Models/Extensions/ConfigsHas.php b/app/Models/Extensions/ConfigsHas.php index 6ffa345a745..0b54bbc0ae0 100644 --- a/app/Models/Extensions/ConfigsHas.php +++ b/app/Models/Extensions/ConfigsHas.php @@ -1,100 +1,105 @@ let's see if exiftool is available - if ($has_exiftool == 2) { - try { - $path = exec('command -v exiftool'); - if ($path == '') { - self::set('has_exiftool', 0); - $has_exiftool = false; - } else { - self::set('has_exiftool', 1); - $has_exiftool = true; + if ($has_exiftool === 2) { + if (Helpers::isExecAvailable()) { + try { + $cmd_output = exec('command -v exiftool'); + } catch (\Exception $e) { + $cmd_output = false; + Handler::reportSafely(new ExternalComponentMissingException('could not find exiftool; `has_exiftool` will be set to 0', $e)); } - } catch (Exception $e) { - self::set('has_exiftool', 0); - $has_exiftool = false; - Logs::warning(__METHOD__, __LINE__, 'exec is disabled, has_exiftool has been set to 0.'); + $path = $cmd_output === false ? '' : $cmd_output; + $has_exiftool = $path === '' ? 0 : 1; + } else { + $has_exiftool = 0; + } + + try { + self::set('has_exiftool', $has_exiftool); + } catch (InvalidConfigOption|QueryBuilderException $e) { + // If we could not save the detected setting, still proceed + Handler::reportSafely($e); } - } elseif ($has_exiftool == 1) { - $has_exiftool = true; - } else { - $has_exiftool = false; } - return $has_exiftool; + return $has_exiftool === 1; } /** - * @return bool returns the Exiftool setting + * @return bool returns the FFMpeg setting */ - public static function hasFFmpeg() + public static function hasFFmpeg(): bool { // has_ffmpeg has the following values: // 0: No ffmpeg // 1: ffmpeg is available // 2: Not yet tested if ffmpeg is available - $has_ffmpeg = intval(self::get_value('has_ffmpeg')); + $has_ffmpeg = self::getValueAsInt('has_ffmpeg'); // value not yet set -> let's see if ffmpeg is available - if ($has_ffmpeg == 2) { - try { - $path = exec('command -v ffmpeg'); - if ($path == '') { - self::set('has_ffmpeg', 0); - $has_ffmpeg = false; - } else { - self::set('has_ffmpeg', 1); - $has_ffmpeg = true; + if ($has_ffmpeg === 2) { + if (Helpers::isExecAvailable()) { + try { + $cmd_output = exec('command -v ffmpeg'); + } catch (\Exception $e) { + $cmd_output = false; + Handler::reportSafely(new ExternalComponentMissingException('could not find ffmpeg; `has_ffmpeg` will be set to 0', $e)); } - } catch (Exception $e) { - self::set('has_ffmpeg', 0); - $has_ffmpeg = false; - Logs::warning(__METHOD__, __LINE__, 'exec is disabled, set_ffmpeg has been set to 0.'); + $path = $cmd_output === false ? '' : $cmd_output; + $has_ffmpeg = $path === '' ? 0 : 1; + } else { + $has_ffmpeg = 0; + } + + try { + self::set('has_ffmpeg', $has_ffmpeg); + } catch (InvalidConfigOption|QueryBuilderException $e) { + // If we could not save the detected setting, still proceed + Handler::reportSafely($e); } - } elseif ($has_ffmpeg == 1) { - $has_ffmpeg = true; - } else { - $has_ffmpeg = false; } - return $has_ffmpeg; + return $has_ffmpeg === 1; } } diff --git a/app/Models/Extensions/CustomSort.php b/app/Models/Extensions/CustomSort.php deleted file mode 100644 index c683aa43fbd..00000000000 --- a/app/Models/Extensions/CustomSort.php +++ /dev/null @@ -1,28 +0,0 @@ -orderBy($sortingCol, $sortingOrder) - ->get(); - } else { - return $query - ->get() - ->sortBy($sortingCol, SORT_NATURAL | SORT_FLAG_CASE, $sortingOrder === 'DESC'); - } - } -} diff --git a/app/Models/Extensions/ForwardsToParentImplementation.php b/app/Models/Extensions/ForwardsToParentImplementation.php new file mode 100644 index 00000000000..f229a8b16e5 --- /dev/null +++ b/app/Models/Extensions/ForwardsToParentImplementation.php @@ -0,0 +1,518 @@ +touches = array_diff($this->touches, ['base_class']); + $this->appends = array_diff($this->appends, ['base_class']); + $this->makeHidden('base_class'); + $this->with[] = 'base_class'; + $this->timestamps = false; + $this->incrementing = false; + } + + /** + * Perform a model insert operation. + * + * @param Builder $query + * + * @return bool + * + * @throws FailedModelAssumptionException + */ + protected function performInsert(Builder $query): bool + { + if (!$this->relationLoaded('base_class')) { + throw new FailedModelAssumptionException('cannot create a child class whose base class is not loaded'); + } + /** @var Model $base_class */ + $base_class = $this->getRelation('base_class'); + if ($base_class->exists) { + throw new FailedModelAssumptionException('cannot create a child class whose base class already exists'); + } + // Save and therewith create the base class + if (!$base_class->save()) { + return false; + } + // Inherit the key of the base class + $this->attributes[$this->getKeyName()] = $base_class->getKey(); + + return parent::performInsert($query); + } + + /** + * Perform a model update operation. + * + * @param Builder $query + * + * @return bool + */ + protected function performUpdate(Builder $query): bool + { + /** @var Model */ + $base_class = $this->base_class; + // touch() also indirectly saves the base_class hence any other + // attributes which require an update are also saved + if (!$base_class->touch()) { + return false; + } + + return parent::performUpdate($query); + } + + /** + * Delete the model from the database. + * + * @return bool always returns true + * + * @throws ModelDBException thrown on failure + */ + public function delete(): bool + { + /** @var ?Model $baseClass */ + $baseClass = $this->base_class; + + $parentException = null; + try { + // Sic! Don't use `!$parentDelete` in condition, because we also + // need to proceed if `$parentDelete === null` . + // If Eloquent returns `null` (instead of `true`), this also + // indicates a success, and we must go on. + // Eloquent, I love you .... not. + $parentResult = parent::delete(); + if ($parentResult === false) { + $parentException = new \RuntimeException('Eloquent\Model::delete() returned false'); + } + } catch (\Throwable $e) { + $parentException = $e; + } + if ($parentException !== null) { + throw ModelDBException::create($this->friendlyModelName(), 'deleting', $parentException); + } + + // We must explicitly check if the base_class still exists in order + // to avoid an infinite recursion, as the base class will also call + // delete() on this class + if ($baseClass !== null && $baseClass->exists) { + $baseException = null; + try { + $baseResult = $baseClass->delete(); + // Same stupidity as above, if Eloquent returns `null`, + // this also indicates a good case. + if ($baseResult === false) { + $baseException = new \RuntimeException('Eloquent\Model::delete() returned false'); + } + } catch (\Throwable $e) { + $baseException = $e; + } + if ($baseException !== null) { + throw ModelDBException::create($this->friendlyModelName(), 'deleting', $baseException); + } + } + + return true; + } + + /** + * Indicates whether the model has timestamps. + * + * Returns always false, because the child model uses the timestamps of + * its parent model + * + * @return bool always false + */ + public function usesTimestamps(): bool + { + return false; + } + + /** + * Indicates whether the ID of the model is incrementing. + * + * Returns always false, because the child model inherits the ID of its + * parent model. + * + * @return bool always false + */ + public function getIncrementing(): bool + { + return false; + } + + /** + * Determine if the model or any of the given attribute(s) have been modified. + * + * Inspired by {@link \Illuminate\Database\Eloquent\Concerns\HasAttributes::isDirty()}. + * + * @param string[]|string|null $attributes + * + * @return bool + */ + public function isDirty($attributes = null): bool + { + $baseIsDirty = $this->relationLoaded('base_class') && $this->getRelation('base_class')->isDirty(); + + return $baseIsDirty || $this->hasChanges( + $this->getDirty(), + is_array($attributes) ? $attributes : func_get_args() + ); + } + + /** + * Convert the model instance to an array. + * + * @return array + */ + public function toArray(): array + { + return array_merge(parent::toArray(), $this->base_class->toArray()); + } + + /** + * Get an attribute from the model. + * + * This method is heavily inspired by + * {@link \Illuminate\Database\Concerns\HasAttributes::getAttribute()}. + * This method is modified in three ways: + * + * 1. A preliminary check if the requested attribute equals `'base_class'`. + * This is necessary to avoid infinite loops in combination with 2). + * 2. A final call which forwards to the implementation of the base class + * at the end, if the default code of + * {@link \Illuminate\Database\Concerns\HasAttributes::getAttribute()} + * would have fallen through. + * 3. While the middle part is basically a copy of the original code, + * we had to tweak it slightly. + * The original code calls `getRelationValue`, if the `$key` is not + * an attribute, but we had to inline the code of `getRelationValue` + * here due to two reasons: + * + * 1. This trait also overwrites `getRelationValue` such that + * `getRelationValue` checks for a relation on both the child + * and the parent model. + * But here, we only must check on the child model, so we must + * not call `getRelationValue`. + * 2. If `getRelationValue` returns `null` it is impossible to + * distinguish, if `null` has been returned because the relation + * exists and equals null or if no relation of that name exists + * at all. + * However, only in the latter case we want to forward the call to + * the parent. + * In the former case, we must return null directly. + * + * @param string $key the name of the queried attribute or relation + * + * @return mixed the value of the attribute or relation + * + * @throws \LogicException + * @throws InvalidCastException + */ + public function getAttribute($key): mixed + { + if ($key === '') { + return null; + } + + // If the primary key is requested, we must use a shortcut. + // If the primary key of the model is not yet set as it might be the + // case for new models, the implementation otherwise would fall + // through until the end and try to forward the call to the base class. + // However, asking for the primary key of the base class is + // + // 1. insane, because it should be identical to the primary key of + // this class, and + // 2. does not work, because we cannot load the base class without + // knowing the primary key. + if ($key === $this->getKeyName()) { + // Sic! + // Don't use `$this->getKey()` because this would call + // `getAttribute` again, and we would end up in an infinite loop. + // Just get the attribute directly. + return $this->getAttributeValue($key); + } + + // Avoid infinite loops, see below + if ($key === 'base_class') { + return $this->getRelationValue($key); + } + + // If the attribute exists in the attribute array or has a "get" + // mutator we will get the attribute's value. + // Otherwise, we will proceed as if the developers + // are asking for a relationship's value. This covers both types of values. + if ( + array_key_exists($key, $this->attributes) || + array_key_exists($key, $this->casts) || + $this->hasGetMutator($key) || + $this->isClassCastable($key) + ) { + return $this->getAttributeValue($key); + } + + // Here we will determine if the model base class itself contains this given key + // since we don't want to treat any of those methods as relationships because + // they are all intended as helper methods and none of these are relations. + if (method_exists(Model::class, $key)) { + return null; + } + + // If the key already exists in the relationships array, it just means the + // relationship has already been loaded, so we'll just return it out of + // here because there is no need to query within the relations twice. + if ($this->relationLoaded($key)) { + return $this->relations[$key]; + } + + // If the "attribute" exists as a method on the model, we will just assume + // it is a relationship and will load and return results from the query + // and hydrate the relationship's value on the "relationships" array. + /** @disregard */ + if ( + method_exists($this, $key) || + (static::$relationResolvers[get_class($this)][$key] ?? null) + ) { + return $this->getRelationshipFromMethod($key); + } + + // If we have fallen through until here, the using "child" class has + // no matching property nor relation. + // So we try the implementation of the "parent" class. + // Note, that his will load the relation of the parent class, if it + // has not been loaded yet. + // To avoid infinite loops, we had to check for "base_class" early in + // this method. + return $this->base_class->getAttribute($key); + } + + /** + * Get the value of a relationship. + * + * This method is heavily inspired by + * {@link \Illuminate\Database\Eloquent\Concerns\HasAttributes::getRelationValue()}. + * + * @param string $key the name of the queried relation + * + * @return mixed the value of the relation if it could be loaded + * + * @throws InternalLycheeException + */ + public function getRelationValue($key): mixed + { + // If the key already exists in the relationships array, this means the + // relationship has already been loaded, so we'll just return it out of + // here because there is no need to query the relations twice. + if ($this->relationLoaded($key)) { + return $this->getRelation($key); + } + + // Avoid infinite loops + // Here we assume that the using class provides a relation `base_class` + // (no check if such a method exists) and we rely on the fact that + // `getRelationshipFromMethod` throws an exception if no such method + // exists. + // Bailing out with an exception prevents the infinite loop. + if ($key === 'base_class') { + // If this is a newly created model, then we cannot resolve the + // relation to the base class from the database, because no such + // entity exists. + // In particular, calling the relation requires that this instance + // of a model already has a valid primary key which does not exist + // for a freshly created model. + $primaryKey = $this->getKey(); + if (!$this->exists) { + if ($primaryKey !== null) { + throw new FailedModelAssumptionException('the primary key must not be set if the model does not exist'); + } + $baseModel = $this->base_class()->getRelated()->newInstance(); + $this->setRelation('base_class', $baseModel); + + return $baseModel; + } else { + // This model exists, but the relation to the base class + // has not yet been loaded. + // Load it now. + if ($primaryKey === null) { + throw new FailedModelAssumptionException('the model allegedly exists, but we don\'t have a primary key, cannot load base model'); + } + + return $this->getRelationshipFromMethod('base_class'); + } + } + + // If the "attribute" exists as a method on the model, we will just assume + // it is a relationship and will load and return results from the query + // and hydrate the relationship's value on the "relationships" array. + /** @disregard */ + if ( + method_exists($this, $key) || + (static::$relationResolvers[get_class($this)][$key] ?? null) + ) { + return $this->getRelationshipFromMethod($key); + } + + // If we have fallen through until here, the using "child" class has + // no matching property nor relation. + // So we try the implementation of the "parent" class. + // Note, that his will load the relation of the parent class, if it + // has not been loaded yet. + // To avoid infinite loops, we had to check for "base_class" early in + // this method. + return $this->base_class->getRelationValue($key); + } + + /** + * Set a given attribute on the model. + * + * This method is heavily inspired by + * {@link \Illuminate\Database\Concerns\HasAttributes::setAttribute()}. + * + * @param string $key + * @param mixed $value + * + * @return mixed + * + * @throws InvalidCastException + * @throws JsonEncodingException + * @throws EncryptException + * @throws \InvalidArgumentException + * @throws NotImplementedException + */ + public function setAttribute($key, $value): mixed + { + // First we will check for the presence of a mutator for the set operation + // which simply lets the developers tweak the attribute as it is set on + // this model, such as "json_encoding" a listing of data for storage. + if ($this->hasSetMutator($key)) { + return $this->setMutatedAttributeValue($key, $value); + } + + // If an attribute is listed as a "date", we'll convert it from a DateTime + // instance into a form proper for storage in the database tables using + // the connection grammar's date format. We will auto set the values. + elseif ($this->isDateAttribute($key)) { + $value = $this->fromDateTime($value); + } + + if ($this->isClassCastable($key)) { + $this->setClassCastableAttribute($key, $value); + + return $this; + } + + if (!is_null($value) && $this->isJsonCastable($key)) { + $value = $this->castAttributeAsJson($key, $value); + } + + // If this attribute contains a JSON ->, we'll set the proper value in the + // attribute's underlying array. This takes care of properly nesting an + // attribute in the array's value in the case of deeply nested items. + if (Str::contains($key, '->')) { + return $this->fillJsonAttribute($key, $value); + } + + if (!is_null($value) && $this->isEncryptedCastable($key)) { + $value = $this->castAttributeAsEncryptedString($key, $value); + } + + // If we have fallen through until here, we first check if the parent + // class provides an attribute of that name and then set the attribute + // on the parent class. + // Only if the parent class does not provide such an attribute either, + // we write it to the child class. + $baseClass = $this->base_class; + if ( + array_key_exists($key, $baseClass->getAttributes()) || + $baseClass->hasSetMutator($key) + ) { + $baseClass->setAttribute($key, $value); + } else { + $this->attributes[$key] = $value; + } + + return $this; + } + + /** + * Unset the value for a given offset. + * + * @param mixed $offset + * + * @return void + */ + public function offsetUnset($offset): void + { + // Prevent that the base model is unset from the set of relations + if ($offset === 'base_class') { + return; + } + parent::offsetUnset($offset); + if ($this->relationLoaded('base_class')) { + $this->base_class->offsetUnset($offset); + } + } +} diff --git a/app/Models/Extensions/HasAttributesPatch.php b/app/Models/Extensions/HasAttributesPatch.php new file mode 100644 index 00000000000..5df5cd12944 --- /dev/null +++ b/app/Models/Extensions/HasAttributesPatch.php @@ -0,0 +1,35 @@ +hasGetMutator($key)) { + $value = $this->mutateAttribute($key, $value); + } + if ($this->isClassCastable($key)) { + $value = $this->getClassCastableAttributeValue($key, $value); + } + + return $value instanceof Arrayable ? $value->toArray() : $value; + } +} diff --git a/app/Models/Extensions/HasBidirectionalRelationships.php b/app/Models/Extensions/HasBidirectionalRelationships.php new file mode 100644 index 00000000000..789c075a63b --- /dev/null +++ b/app/Models/Extensions/HasBidirectionalRelationships.php @@ -0,0 +1,131 @@ +$method(); // @phpstan-ignore-line, PhpStan does not like variadic calls + + if (!$relation instanceof Relation) { + if (is_null($relation)) { + throw new \LogicException(sprintf('%s::%s must return a relationship instance, but "null" was returned. Was the "return" keyword used?', static::class, $method)); + } + throw new \LogicException(sprintf('%s::%s must return a relationship instance.', static::class, $method)); + } + + $result = $relation->getResults(); + $this->setRelation($method, $result); + + // Now the additional code + // We also set the reverse direction of the relation, i.e. each + // hydrated model points back to this model + + if ($relation instanceof BidirectionalRelation) { + if ($result instanceof Collection) { + /** @var Model $model */ + foreach ($result as $model) { + $model->setRelation($relation->getForeignMethodName(), $this); + } + } elseif ($result instanceof Model) { + $result->setRelation($relation->getForeignMethodName(), $this); + } else { + throw new \LogicException(sprintf('$result must either be a collection of models or a model, but got %s', is_object($result) ? get_class($result) : gettype($result))); + } + } + + return $result; + } + + /** + * Define a one-to-many relationship. + * + * Inspired by {@link \Illuminate\Database\Eloquent\Concerns\HasRelationships::hasMany}. + * + * @template TRelatedModel of \Illuminate\Database\Eloquent\Model + * + * @param class-string $related + * @param string|null $foreignKey + * @param string|null $localKey + * @param string|null $foreignMethodName + * + * @return HasManyBidirectionally + */ + public function hasManyBidirectionally(string $related, ?string $foreignKey = null, ?string $localKey = null, ?string $foreignMethodName = null): HasManyBidirectionally + { + /** @var TRelatedModel $instance */ + $instance = $this->newRelatedInstance($related); + + $foreignKey = $foreignKey ?? $this->getForeignKey(); + + $localKey = $localKey ?? $this->getKeyName(); + + $foreignMethodName = $foreignMethodName ?? $this->getForeignProperty(); + + /** @phpstan-ignore-next-line */ + return $this->newHasManyBidirectionally( + $instance->newQuery(), + $this, + $instance->getTable() . '.' . $foreignKey, + $localKey, + $foreignMethodName + ); + } + + /** + * Instantiate a new HasManyBidirectionally relationship. + * + * Inspired by {@link \Illuminate\Database\Eloquent\Concerns\HasRelationships::newHasMany}. + * + * @template TRelatedModel of \Illuminate\Database\Eloquent\Model + * @template TParentModel of \Illuminate\Database\Eloquent\Model + * + * @param Builder $query + * @param TParentModel $parent + * @param string $foreignKey + * @param string $localKey + * @param string $foreignMethodName + * + * @return HasManyBidirectionally + */ + protected function newHasManyBidirectionally(Builder $query, Model $parent, string $foreignKey, string $localKey, string $foreignMethodName): HasManyBidirectionally + { + return new HasManyBidirectionally($query, $parent, $foreignKey, $localKey, $foreignMethodName); + } + + /** + * Get the default foreign method name for this model. + * + * @return string + */ + public function getForeignProperty(): string + { + return Str::snake(class_basename($this)); + } +} diff --git a/app/Models/Extensions/HasRandomIDAndLegacyTimeBasedID.php b/app/Models/Extensions/HasRandomIDAndLegacyTimeBasedID.php new file mode 100644 index 00000000000..3da6dfc0559 --- /dev/null +++ b/app/Models/Extensions/HasRandomIDAndLegacyTimeBasedID.php @@ -0,0 +1,191 @@ +getKeyName()) { + throw new NotImplementedException('must not set primary key explicitly, primary key will be set on first insert'); + } + if ($key === RandomID::LEGACY_ID_NAME) { + throw new NotImplementedException('must not set legacy key explicitly, legacy key will be set on first insert'); + } + + return parent::setAttribute($key, $value); + } + + /** + * Performs the `INSERT` operation of the model. + * + * This method also tries to create a unique, time-based ID. + * The method is mostly copied & pasted from {@link \Illuminate\Database\Eloquent\Model::performInsert()} + * with adoptions regarding key generation. + * + * @param Builder $query + * + * @return bool true on success + * + * @throws TimeBasedIdException + * @throws InsufficientEntropyException + */ + protected function performInsert(Builder $query): bool + { + if ($this->fireModelEvent('creating') === false) { + return false; + } + + // First we'll need to create a fresh query instance and touch the creation and + // update timestamps on this model, which are maintained by us for developer + // convenience. After, we will just continue saving these model instances. + if ($this->usesTimestamps()) { + $this->updateTimestamps(); + } + + $result = false; + $retryCounter = 5; + $lastException = null; + + do { + $retry = false; + try { + $retryCounter--; + $this->generateKey(); + $attributes = $this->getAttributesForInsert(); + $result = $query->insert($attributes); + } catch (QueryException $e) { + $lastException = $e; + $errorCode = $e->getCode(); + if ($errorCode === 23000 || $errorCode === 23505 || $errorCode === '23000' || $errorCode === '23505') { + // houston, we have a duplicate entry problem + // Our ids are based on current system time, so + // wait randomly up to 1s before retrying. + usleep(rand(0, 1000000)); + $retry = true; + } else { + throw $e; + } + } + } while ($retry && $retryCounter > 0); + + if ($retryCounter === 0) { + throw new TimeBasedIdException('unable to persist model to DB after 5 unsuccessful attempts', $lastException); + } + + // We will go ahead and set the exists property to true, so that it is set when + // the created event is fired, just in case the developer tries to update it + // during the event. This will allow them to do so and run an update here. + $this->exists = true; + $this->wasRecentlyCreated = true; + $this->fireModelEvent('created', false); + + return $result; + } + + /** + * Generates a primary key and a legacy key. + * + * The primary key are 144bit true randomness, encoded as Base64. + * The legacy key is based on the current micro-time. + * + * @return void + * + * @throws InsufficientEntropyException + */ + private function generateKey(): void + { + // URl-compatible variant of base64 encoding + // `+` and `/` are replaced by `-` and `_`, resp. + // The other characters (a-z, A-Z, 0-9) are legal within an URL. + // As the number of bytes is divisible by 3, no trailing `=` occurs. + try { + $id = strtr(base64_encode(random_bytes(3 * RandomID::ID_LENGTH / 4)), '+/', '-_'); + // Last character whould not be a - for some version of android. + // this will reduce the entropy and induce a slight bias but we are still + // above the birthday bounds. + if ($id[23] === '-') { + $id[23] = '0'; + } + } catch (\Exception $e) { + throw new InsufficientEntropyException($e); + } + + if ( + PHP_INT_MAX === 2147483647 || + Configs::getValueAsBool('force_32bit_ids') + ) { + // For 32-bit installations, we can only afford to store the + // full seconds in id. The calling code needs to be able to + // handle duplicate ids. Note that this also exposes us to + // the year 2038 problem. + $legacyID = sprintf('%010d', microtime(true)); + } else { + // Ensure 4 digits after the decimal point, 15 characters + // total (including the decimal point), 0-padded on the + // left if needed (shouldn't be needed unless we move back in + // time :-) ) + $legacyID = sprintf('%015.4f', microtime(true)); + $legacyID = str_replace('.', '', $legacyID); + } + $this->attributes[$this->getKeyName()] = $id; + $this->attributes[RandomID::LEGACY_ID_NAME] = intval($legacyID); + } +} diff --git a/app/Models/Extensions/NodeTrait.php b/app/Models/Extensions/NodeTrait.php deleted file mode 100644 index 4f58afe862f..00000000000 --- a/app/Models/Extensions/NodeTrait.php +++ /dev/null @@ -1,65 +0,0 @@ -newQueryWithoutScopes(), $this); - } - - /** - * Get query ancestors of the node. - * - * @return AncestorsRelation - */ - public function ancestors() - { - return new AncestorsRelation($this->newQueryWithoutScopes(), $this); - } - - /** - * Get a new base query that includes deleted nodes. - * - * @since 1.1 - * - * @return QueryBuilder - */ - public function newNestedSetQuery($table = null) - { - return $this->applyNestedSetScope($this->newQueryWithoutScopes(), $table); - } - - /** - * @param string $table - * - * @return QueryBuilder - */ - public function newScopedQuery($table = null) - { - return $this->applyNestedSetScope($this->newQueryWithoutScopes(), $table); - } -} \ No newline at end of file diff --git a/app/Models/Extensions/PhotoBooleans.php b/app/Models/Extensions/PhotoBooleans.php deleted file mode 100644 index 473db427c0d..00000000000 --- a/app/Models/Extensions/PhotoBooleans.php +++ /dev/null @@ -1,44 +0,0 @@ -where(function ($q) use ($checksum) { - $q->where('checksum', '=', $checksum) - ->orWhere('livePhotoChecksum', '=', $checksum); - }); - if (isset($photoID)) { - $sql = $sql->where('id', '<>', $photoID); - } - - return $sql->first() ?? false; - } - - /** - * We are checking if the beginning of the type string is - * video. - * - * type contains the mime informations - */ - public function isVideo(): bool - { - return $this->isValidVideoType($this->type); - } -} diff --git a/app/Models/Extensions/PhotoCast.php b/app/Models/Extensions/PhotoCast.php deleted file mode 100644 index f719611abd0..00000000000 --- a/app/Models/Extensions/PhotoCast.php +++ /dev/null @@ -1,155 +0,0 @@ -isVideo()) { - $filename = $this->thumbUrl; - } elseif ($this->type == 'raw') { - // It's a raw file -> we also use jpeg as extension - $filename = $this->thumbUrl; - } else { - $filename = $this->url; - } - $filename2x = ($filename !== '') ? Helpers::ex2x($filename) : ''; - $thumbFileName2x = $this->thumb2x === 1 ? Helpers::ex2x($this->thumbUrl) : null; - - // The original size is not stored in this sub-array but on the root level of the JSON response - // TODO: Maybe harmonize and put original variant into this array, too? This would also avoid an ugly if branch in SymLink#override. - $sizeVariants = [ - Photo::VARIANT_THUMB => $this->serializeSizeVariant( - Photo::VARIANT_THUMB, $this->thumbUrl, Photo::THUMBNAIL_DIM, Photo::THUMBNAIL_DIM - ), - Photo::VARIANT_THUMB2X => $this->serializeSizeVariant( - Photo::VARIANT_THUMB2X, $thumbFileName2x, Photo::THUMBNAIL2X_DIM, Photo::THUMBNAIL2X_DIM - ), - Photo::VARIANT_SMALL => $this->serializeSizeVariant( - Photo::VARIANT_SMALL, $filename, $this->small_width, $this->small_height - ), - Photo::VARIANT_SMALL2X => $this->serializeSizeVariant( - Photo::VARIANT_SMALL2X, $filename2x, $this->small2x_width, $this->small2x_height - ), - Photo::VARIANT_MEDIUM => $this->serializeSizeVariant( - Photo::VARIANT_MEDIUM, $filename, $this->medium_width, $this->medium_height - ), - Photo::VARIANT_MEDIUM2X => $this->serializeSizeVariant( - Photo::VARIANT_MEDIUM2X, $filename2x, $this->medium2x_width, $this->medium2x_height - ), - ]; - - return [ - 'id' => strval($this->id), - 'title' => $this->title, - 'description' => $this->description == null ? '' : $this->description, - 'tags' => $this->tags, - 'star' => Helpers::str_of_bool($this->star), - 'public' => $this->get_public(), - 'album' => $this->album_id !== null ? strval($this->album_id) : null, - 'url' => ($this->type == 'raw') ? Storage::url('raw/' . $this->url) : Storage::url('big/' . $this->url), - 'width' => $this->width !== null ? $this->width : 0, - 'height' => $this->height !== null ? $this->height : 0, - 'type' => $this->type, - 'filesize' => $this->filesize, - 'iso' => $this->iso, - 'aperture' => $this->aperture, - 'make' => $this->make, - 'model' => $this->model, - 'shutter' => $this->get_shutter_str(), - // We need to format the framerate (stored as focal) -> max 2 decimal digits - 'focal' => (strpos($this->type, 'video') === 0) ? round(floatval($this->focal), 2) : $this->focal, - 'lens' => $this->lens, - 'latitude' => $this->latitude, - 'longitude' => $this->longitude, - 'altitude' => $this->altitude, - 'imgDirection' => $this->imgDirection, - 'location' => $this->location, - 'livePhotoContentID' => $this->livePhotoContentID, - 'livePhotoUrl' => (!empty($this->livePhotoUrl)) ? Storage::url('big/' . $this->livePhotoUrl) : null, - 'created_at' => $this->created_at->format(\DateTimeInterface::ATOM), - 'updated_at' => $this->updated_at->format(\DateTimeInterface::ATOM), - 'taken_at' => (!empty($this->taken_at)) ? $this->taken_at->format(\DateTimeInterface::ATOM) : null, - 'taken_at_orig_tz' => $this->taken_at_orig_tz, - 'license' => $this->license, - 'sizeVariants' => $sizeVariants, - ]; - } - - /** - * Returns a front-end friendly array which describes a particular size variant of a media file. - * - * @param string $sizeVariant The name of the size variant which is being serialized; used to determine the correct path prefix - * @param string|null $fileName The filename - * @param int|null $width The width of this variant - * @param int|null $height The height of this variant - * - * @return array|null An associative array with the following attributes "url", "width" and "height" or null, if - * any of the parameters is null - */ - protected function serializeSizeVariant(string $sizeVariant, ?string $fileName, ?int $width, ?int $height): ?array - { - if ($width === null || $height === null || $fileName === null || $fileName === '') { - return null; - } else { - return [ - 'url' => Storage::url(Photo::VARIANT_2_PATH_PREFIX[$sizeVariant] . '/' . $fileName), - 'width' => $width, - 'height' => $height, - ]; - } - } - - /** - * Given a Photo, returns the thumb version. - */ - public function toThumb(): Thumb - { - /* @var $symLinkFunctions ?SymLinkFunctions */ - $symLinkFunctions = resolve(SymLinkFunctions::class); - - $thumb = new Thumb($this->type, $this->id); - // maybe refactor? - $sym = $symLinkFunctions->find($this); - if ($sym !== null) { - $thumb->thumb = $sym->get(Photo::VARIANT_THUMB); - // default is '' so if thumb2x does not exist we just reply '' which is the behaviour we want - $thumb->thumb2x = $sym->get(Photo::VARIANT_THUMB2X); - } else { - $thumb->thumb = Storage::url( - Photo::VARIANT_2_PATH_PREFIX[Photo::VARIANT_THUMB] . '/' . $this->thumbUrl - ); - if ($this->thumb2x === 1) { - $thumb->set_thumb2x(); - } - } - - return $thumb; - } - - /** - * Downgrade the quality of the pictures. - * - * @param array $return - */ - public function downgrade(array &$return) - { - if ( - $this->isVideo() === false && - ($return['sizeVariants']['medium2x'] !== null || $return['sizeVariants']['medium'] !== null) - ) { - $return['url'] = ''; - } - } -} diff --git a/app/Models/Extensions/PhotoGetters.php b/app/Models/Extensions/PhotoGetters.php deleted file mode 100644 index 13fc65abb8d..00000000000 --- a/app/Models/Extensions/PhotoGetters.php +++ /dev/null @@ -1,78 +0,0 @@ -shutter; - // shutter speed needs to be processed. It is stored as a string `a/b s` - if ($shutter != '' && substr($shutter, 0, 2) != '1/') { - preg_match('/(\d+)\/(\d+) s/', $shutter, $matches); - if ($matches) { - $a = intval($matches[1]); - $b = intval($matches[2]); - if ($b != 0) { - try { - $gcd = Helpers::gcd($a, $b); - $a = $a / $gcd; - $b = $b / $gcd; - } catch (Exception $e) { - // this should not happen as we covered the case $b = 0; - } - if ($a == 1) { - $shutter = '1/' . $b . ' s'; - } else { - $shutter = ($a / $b) . ' s'; - } - } - } - } - - if ($shutter == '1/1 s') { - $shutter = '1 s'; - } - - return $shutter; - } - - /** - * Get the public value of a picture - * if 0 : picture is private - * if 1 : picture is public alone. - * - * @return string - */ - public function get_public(): string - { - return $this->public == 1 ? '1' : '0'; - } - - /** - * Return the Album license or the default one. - * - * @param string $license = album License - * - * @return string - */ - public function get_license(string $license = 'none'): string - { - if ($this->license != 'none') { - return $this->license; - } - - if ($license != 'none') { - return $license; - } - - return Configs::get_value('default_license'); - } -} diff --git a/app/Models/Extensions/SizeVariants.php b/app/Models/Extensions/SizeVariants.php new file mode 100644 index 00000000000..e6d99dc393f --- /dev/null +++ b/app/Models/Extensions/SizeVariants.php @@ -0,0 +1,346 @@ +|null> + */ +class SizeVariants extends AbstractDTO +{ + /** @var Photo the parent object this object is tied to */ + private Photo $photo; + + private ?SizeVariant $original = null; + private ?SizeVariant $medium2x = null; + private ?SizeVariant $medium = null; + private ?SizeVariant $small2x = null; + private ?SizeVariant $small = null; + private ?SizeVariant $thumb2x = null; + private ?SizeVariant $thumb = null; + private ?SizeVariant $placeholder = null; + + /** + * SizeVariants constructor. + * + * @param Photo $photo the parent object + * this object is tied to + * @param Collection|null $sizeVariants a collection of size + * variants + * + * @throws LycheeInvalidArgumentException thrown if the photo and the + * collection of size variants don't + * belong together + */ + public function __construct(Photo $photo, ?Collection $sizeVariants = null) + { + $this->photo = $photo; + if ($sizeVariants !== null) { + /** @var SizeVariant $sizeVariant */ + foreach ($sizeVariants as $sizeVariant) { + $this->add($sizeVariant); + } + } + } + + /** + * @param SizeVariant $sizeVariant + * + * @return void + * + * @throws LycheeInvalidArgumentException thrown if ID of owning photo + * does not match + */ + public function add(SizeVariant $sizeVariant): void + { + if ($sizeVariant->photo_id !== $this->photo->id) { + throw new LycheeInvalidArgumentException('ID of owning photo does not match'); + } + $sizeVariant->setRelation('photo', $this->photo); + $candidate = $this->getSizeVariant($sizeVariant->type); + + if ($candidate !== null && $candidate->id !== $sizeVariant->id) { + throw new LycheeInvalidArgumentException('Another size variant of the same type has already been added'); + } + + match ($sizeVariant->type) { + SizeVariantType::ORIGINAL => $this->original = $sizeVariant, + SizeVariantType::MEDIUM2X => $this->medium2x = $sizeVariant, + SizeVariantType::MEDIUM => $this->medium = $sizeVariant, + SizeVariantType::SMALL2X => $this->small2x = $sizeVariant, + SizeVariantType::SMALL => $this->small = $sizeVariant, + SizeVariantType::THUMB2X => $this->thumb2x = $sizeVariant, + SizeVariantType::THUMB => $this->thumb = $sizeVariant, + SizeVariantType::PLACEHOLDER => $this->placeholder = $sizeVariant, + }; + } + + /** + * Serializes this object into an array. + * + * @return array|null> The serialized properties of this object + */ + public function toArray(): array + { + return [ + SizeVariantType::ORIGINAL->name() => $this->original?->toArray(), + SizeVariantType::MEDIUM2X->name() => $this->medium2x?->toArray(), + SizeVariantType::MEDIUM->name() => $this->medium?->toArray(), + SizeVariantType::SMALL2X->name() => $this->small2x?->toArray(), + SizeVariantType::SMALL->name() => $this->small?->toArray(), + SizeVariantType::THUMB2X->name() => $this->thumb2x?->toArray(), + SizeVariantType::THUMB->name() => $this->thumb?->toArray(), + SizeVariantType::PLACEHOLDER->name() => $this->placeholder?->toArray(), + ]; + } + + /** + * Return all SizeVariants as a collection. + * + * @return BaseCollection + */ + public function toCollection(): BaseCollection + { + return collect([ + $this->original, + $this->medium2x, + $this->medium, + $this->small2x, + $this->small, + $this->thumb2x, + $this->thumb, + $this->placeholder, + ]); + } + + /** + * Returns the requested size variant of the photo. + * + * @param SizeVariantType $sizeVariantType the type of the size variant + * + * @return SizeVariant|null The size variant + * + * @throws InvalidSizeVariantException + */ + public function getSizeVariant(SizeVariantType $sizeVariantType): ?SizeVariant + { + return match ($sizeVariantType) { + SizeVariantType::ORIGINAL => $this->original, + SizeVariantType::MEDIUM2X => $this->medium2x, + SizeVariantType::MEDIUM => $this->medium, + SizeVariantType::SMALL2X => $this->small2x, + SizeVariantType::SMALL => $this->small, + SizeVariantType::THUMB2X => $this->thumb2x, + SizeVariantType::THUMB => $this->thumb, + SizeVariantType::PLACEHOLDER => $this->placeholder, + }; + } + + public function getOriginal(): ?SizeVariant + { + return $this->original; + } + + /** + * Get Medium2x or fallback to Medium. + * + * @return SizeVariant|null + */ + public function getMedium2x(): ?SizeVariant + { + return $this->medium2x; + } + + /** + * get Medium or fallback to Original. + * + * @return SizeVariant|null + */ + public function getMedium(): ?SizeVariant + { + return $this->medium; + } + + /** + * Get Small2x or fallback to Small. + * + * @return SizeVariant|null + */ + public function getSmall2x(): ?SizeVariant + { + return $this->small2x; + } + + public function getSmall(): ?SizeVariant + { + return $this->small; + } + + public function getThumb2x(): ?SizeVariant + { + return $this->thumb2x; + } + + public function getThumb(): ?SizeVariant + { + return $this->thumb; + } + + public function getPlaceholder(): ?SizeVariant + { + return $this->placeholder; + } + + /** + * Creates a new instance of {@link \App\Models\SizeVariant} for the + * associated photo and persists it to DB. + * + * @param SizeVariantType $sizeVariantType the type of the desired size variant; + * @param string $shortPath the short path of the media file this + * size variant shall point to + * @param ImageDimension $dim the width of the size variant + * @param int $filesize the filesize of the size variant + * + * @return SizeVariant The newly created and persisted size variant + * + * @throws IllegalOrderOfOperationException + * @throws ModelDBException + */ + public function create(SizeVariantType $sizeVariantType, string $shortPath, ImageDimension $dim, int $filesize): SizeVariant + { + if (!$this->photo->exists) { + throw new IllegalOrderOfOperationException('Cannot create a size variant for a photo whose id is not yet persisted to DB'); + } + try { + $result = SizeVariant::create([ + 'photo_id' => $this->photo->id, + 'storage_disk' => StorageDiskType::LOCAL, + 'type' => $sizeVariantType, + 'short_path' => $shortPath, + 'width' => $dim->width, + 'height' => $dim->height, + 'filesize' => $filesize, + 'ratio' => $dim->getRatio(), + ]); + $this->add($result); + + return $result; + } catch (LycheeInvalidArgumentException $e) { + // thrown by ::add(), if $result->photo_id !== $this->photo->id, + // but we know that we assert that + throw LycheeAssertionError::createFromUnexpectedException($e); + } + } + + /** + * Deletes all size variants incl. the files from storage. + * + * @return void + * + * @throws ModelDBException + * @throws MediaFileOperationException + */ + public function deleteAll(): void + { + $ids = [ + $this->original?->id, + $this->medium2x?->id, + $this->medium?->id, + $this->small2x?->id, + $this->small?->id, + $this->thumb2x?->id, + $this->thumb?->id, + $this->placeholder?->id, + ]; + + $this->original = null; + $this->medium2x = null; + $this->medium = null; + $this->small2x = null; + $this->small = null; + $this->thumb2x = null; + $this->thumb = null; + $this->placeholder = null; + + (new Delete())->do(array_diff($ids, [null]))->do(); + } + + /** + * @throws ModelDBException + * @throws IllegalOrderOfOperationException + */ + public function replicate(Photo $duplicatePhoto): SizeVariants + { + $duplicate = new SizeVariants($duplicatePhoto); + self::replicateSizeVariant($duplicate, $this->original); + self::replicateSizeVariant($duplicate, $this->medium2x); + self::replicateSizeVariant($duplicate, $this->medium); + self::replicateSizeVariant($duplicate, $this->small2x); + self::replicateSizeVariant($duplicate, $this->small); + self::replicateSizeVariant($duplicate, $this->thumb2x); + self::replicateSizeVariant($duplicate, $this->thumb); + self::replicateSizeVariant($duplicate, $this->placeholder); + + return $duplicate; + } + + /** + * @throws ModelDBException + * @throws IllegalOrderOfOperationException + */ + private static function replicateSizeVariant(SizeVariants $duplicate, ?SizeVariant $sizeVariant): void + { + if ($sizeVariant !== null) { + $duplicate->create( + $sizeVariant->type, + $sizeVariant->short_path, + new ImageDimension($sizeVariant->width, $sizeVariant->height), + $sizeVariant->filesize + ); + } + } + + /** + * Returns true if at least one version of medium is not null. + * + * @return bool + */ + public function hasMedium(): bool + { + return $this->medium !== null || $this->medium2x !== null; + } + + /** + * We don't need to check if small2x or medium2x exists. + * small2x implies small, and same for medium2x, but the opposite is not true! + * + * @return bool + */ + public function hasMediumOrSmall(): bool + { + return $this->small !== null || $this->medium !== null; + } +} diff --git a/app/Models/Extensions/SortingDecorator.php b/app/Models/Extensions/SortingDecorator.php new file mode 100644 index 00000000000..217e02a1d1a --- /dev/null +++ b/app/Models/Extensions/SortingDecorator.php @@ -0,0 +1,189 @@ + + */ + protected Builder $baseBuilder; + + /** + * @param Builder $baseBuilder + * + * @return void + */ + public function __construct(Builder $baseBuilder) + { + $this->baseBuilder = $baseBuilder; + } + + /** + * The list of all sorting criteria in descending priority. + * + * The sorting criterion at index 0 is the most significant criterion; + * the sorting criterion at index `length-1` is the least significant + * criterion. + * + * If everything can be sorted on the SQL layer, then the SQL basically + * has to look like that: + * + * $query->orderBy($orderBy[0])->orderBy($orderBy[1])->...->orderBy($orderBy[length-1]) + * + * For SQL the most significant order criterion has to be put first. + * + * If everything needs to be sorted on the software layer (i.e. with + * Laravel Collections), then the criteria must be applied in reverse + * order like this + * + * $collection->sortBy($orderBy[length-1])->...->sortBy($orderBy[1])->sortBy($orderBy[0]) + * + * The reason is that each `sortBy` immediately executes a _stable_ sort + * and thus the last one "wins". + * + * The mixed case with some pre-sorting on the SQL layer and final sorting + * on the software layer is more complicated. + * + * @var array + */ + protected array $orderBy = []; + + /** + * The index for {@link SortingDecorator::$orderBy} at which we must + * switch from SQL sorting to PHP sorting. + * + * Criteria between `0` ... `$pivotIdx` are sorted on the software layer + * (in reverse order). + * Criteria between `$pivotIdx+1` ... `length-1` are sorted on the SQL + * layer. + * + * If `$pivotIdx === -1`, then everything is sorted on the SQL layer. + * `$pivotIdx` is only set to a different value, if a sorting criteria + * which must be postponed (see {@link SortingDecorator::POSTPONE_COLUMNS}) + * is added. + * Then `$pivotIdx` points to that with the least priority, because from + * there on everything must be sorted in software. + * + * @var int + */ + protected int $pivotIdx = -1; + + /** + * @param ColumnSortingType $column the column acc. to which the result shall be sorted + * @param OrderSortingType $direction the order direction + * + * @return SortingDecorator + * + * @throws InvalidOrderDirectionException + */ + public function orderBy(ColumnSortingType $column, OrderSortingType $direction): SortingDecorator + { + $this->orderBy[] = [ + 'column' => $column->value, + 'direction' => $direction->value, + ]; + + if (in_array($column, self::POSTPONE_COLUMNS, true)) { + $this->pivotIdx = sizeof($this->orderBy) - 1; + } + + return $this; + } + + /** + * Some sorting are done at the photo level, however because we enforce more strictly the type on column + * we are now prefixing the column by `photos.`. + * + * @param ColumnSortingType $column the column acc. to which the result shall be sorted + * @param OrderSortingType $direction the order direction + * + * @return SortingDecorator + * + * @throws InvalidOrderDirectionException + */ + public function orderPhotosBy(ColumnSortingType $column, OrderSortingType $direction): SortingDecorator + { + $this->orderBy[] = [ + 'column' => 'photos.' . $column->value, + 'direction' => $direction->value, + ]; + + if (in_array($column, self::POSTPONE_COLUMNS, true)) { + $this->pivotIdx = sizeof($this->orderBy) - 1; + } + + return $this; + } + + /** + * Gets the result collection. + * + * @param string[] $columns + * + * @return Collection + * + * @throws InvalidOrderDirectionException + */ + public function get(array $columns = ['*']): Collection + { + // Sort as much as we can on the SQL layer, i.e. everything with a + // lower significance than the least significant criterion which + // requires natural sorting. + try { + for ($i = $this->pivotIdx + 1; $i < sizeof($this->orderBy); $i++) { + $this->baseBuilder->orderBy($this->orderBy[$i]['column'], $this->orderBy[$i]['direction']); + } + } catch (\InvalidArgumentException) { + // Sic! In theory, `\InvalidArgumentException` should be thrown + // if the *type* of argument differs from the expected type + // (e.g. a method gets pass an integer, but requires a string). + // If the *value* is invalid, the method should throw a + // `\InvalidDomainException`. + // But Eloquent throws `\InvalidArgumentException` if the + // direction does neither equal "asc" nor "desc". + throw new InvalidOrderDirectionException(); + } + + /** @var Collection $result */ + $result = $this->baseBuilder->get($columns); + + // Sort with PHP for the remaining criteria in reverse order. + for ($i = $this->pivotIdx; $i >= 0; $i--) { + $column = $this->orderBy[$i]['column']; + + // This conversion is necessary + $columnSortingName = str_replace('photos.', '', $column); + $columnSortingType = ColumnSortingType::tryFrom($columnSortingName) ?? ColumnSortingType::CREATED_AT; + + $options = in_array($columnSortingType, self::POSTPONE_COLUMNS, true) ? SORT_NATURAL | SORT_FLAG_CASE : SORT_REGULAR; + $result = $result->sortBy( + $columnSortingName, + $options, + $this->orderBy[$i]['direction'] === OrderSortingType::DESC->value + )->values(); + } + + return $result; + } +} diff --git a/app/Models/Extensions/ThrowsConsistentExceptions.php b/app/Models/Extensions/ThrowsConsistentExceptions.php new file mode 100644 index 00000000000..d399ef73be3 --- /dev/null +++ b/app/Models/Extensions/ThrowsConsistentExceptions.php @@ -0,0 +1,160 @@ + $options + * + * @return bool always return true + * + * @throws ModelDBException thrown on failure + * + * @noinspection PhpMultipleClassDeclarationsInspection + */ + public function save(array $options = []): bool + { + $parentException = null; + try { + // Note, `Model::save` may also return `null` which also indicates a success + if (parent::save($options) === false) { + $parentException = new \RuntimeException('Eloquent\Model::save() returned false'); + } + } catch (\Throwable $e) { + $parentException = $e; + } + if ($parentException !== null) { + throw ModelDBException::create($this->friendlyModelName(), $this->wasRecentlyCreated ? 'creating' : 'updating', $parentException); + } + + return true; + } + + /** + * @return bool always return true + * + * @throws ModelDBException thrown on failure + * + * @noinspection PhpMultipleClassDeclarationsInspection + */ + public function delete(): bool + { + $parentException = null; + try { + // Sic! Don't use `!$parentDelete` in condition, because we also + // need to proceed if `$parentDelete === null` . + // If Eloquent returns `null` (instead of `true`), this also + // indicates a success, and we must go on. + // Eloquent, I love you .... not. + $result = parent::delete(); + if ($result === false) { + $parentException = new \RuntimeException('Eloquent\Model::delete() returned false'); + } + } catch (\Throwable $e) { + $parentException = $e; + } + if ($parentException !== null) { + throw ModelDBException::create($this->friendlyModelName(), 'deleting', $parentException); + } + + return true; + } + + /** + * Serializes this object into an array. + * + * @return array The serialized properties of this object + * + * @throws \JsonException + * + * @see ThrowsConsistentExceptions::toArray() + */ + public function jsonSerialize(): array + { + try { + return $this->toArray(); + } catch (\Exception $e) { + throw new \JsonException(get_class($this) . '::toArray() failed', 0, $e); + } + } + + /** + * Convert the model instance to JSON. + * + * The error message is inspired by {@link JsonEncodingException::forModel()}. + * + * @param int $options + * + * @return string + * + * @throws JsonEncodingException + */ + public function toJson($options = 0): string + { + try { + // Note, we must not use the option `JSON_THROW_ON_ERROR` here, + // because this does not clear `json_last_error()` from any + // previous, stalled error message. + // But `\Illuminate\Http\JsonResponse::setData()` falsy assumes + // that this method does so. + // Hence, we call `json_encode` _without_ specifying + // `JSON_THROW_ON_ERROR` and then mimic that behaviour. + // TODO: VERIFY THIS + $json = json_encode($this->jsonSerialize(), $options); + if (json_last_error() !== JSON_ERROR_NONE) { + throw new \JsonException(json_last_error_msg(), json_last_error()); + } + + return $json; + } catch (\JsonException $e) { + throw new JsonEncodingException('Error encoding model [' . get_class($this) . '] to JSON', 0, $e); + } + } +} diff --git a/app/Models/Extensions/Thumb.php b/app/Models/Extensions/Thumb.php index 130df697a0b..fbb4617a556 100644 --- a/app/Models/Extensions/Thumb.php +++ b/app/Models/Extensions/Thumb.php @@ -1,36 +1,181 @@ + */ +class Thumb extends AbstractDTO { - public $thumb = ''; - public $type = ''; - public $thumb2x = ''; - public $id = null; + public string $id; + public string $type; + public ?string $thumbUrl; + public ?string $thumb2xUrl; + public ?string $placeholderUrl; - public function __construct(string $type, int $id) + protected function __construct(string $id, string $type, string $thumbUrl, ?string $thumb2xUrl = null, ?string $placeholderUrl = null) { - $this->type = $type; $this->id = $id; + $this->type = $type; + $this->thumbUrl = $thumbUrl; + $this->thumb2xUrl = $thumb2xUrl; + $this->placeholderUrl = $placeholderUrl; + } + + /** + * Restricts the given relation for size variants such that only the + * necessary variants for a thumbnail are selected. + * + * @param HasMany $relation + * + * @return HasMany + */ + public static function sizeVariantsFilter(HasMany $relation): HasMany // @phpstan-ignore-line + { + $svAlbumThumbs = [SizeVariantType::THUMB, SizeVariantType::THUMB2X, SizeVariantType::PLACEHOLDER]; + if (Features::active('vuejs')) { + $svAlbumThumbs = [SizeVariantType::SMALL, SizeVariantType::SMALL2X, SizeVariantType::THUMB, SizeVariantType::THUMB2X, SizeVariantType::PLACEHOLDER]; + } + + return $relation->whereIn('type', $svAlbumThumbs); + } + + /** + * Creates a thumb by using the best rated photo from the given queryable. + * + * Note, this method assumes that the relation is already restricted + * such that it only returns photos which the current user may see. + * + * @template TDeclaringModel of \Illuminate\Database\Eloquent\Model + * @template TResult + * + * @param Relation|Builder $photoQueryable the relation to or query for {@link Photo} which is used to pick a thumb + * @param SortingCriterion $sorting the sorting criterion + * + * @return Thumb|null the created thumbnail; null if the relation is empty + * + * @throws InvalidPropertyException thrown, if $sortingOrder neither + * equals `desc` nor `asc` + */ + public static function createFromQueryable(Relation|Builder $photoQueryable, SortingCriterion $sorting): ?Thumb + { + try { + /** @var Photo|null $cover */ + $cover = $photoQueryable + ->withOnly(['size_variants' => (fn ($r) => self::sizeVariantsFilter($r))]) + ->orderBy('photos.' . ColumnSortingPhotoType::IS_STARRED->value, OrderSortingType::DESC->value) + ->orderBy('photos.' . $sorting->column->value, $sorting->order->value) + ->select(['photos.id', 'photos.type']) + ->first(); + + return self::createFromPhoto($cover); + } catch (\InvalidArgumentException $e) { + throw new InvalidPropertyException('Sorting order invalid', $e); + } + } + + /** + * Creates a thumb by using the best rated photo from the given queryable. + * In other words, same as above but this time we pick a random image instead. + * + * Note, this method assumes that the relation is already restricted + * such that it only returns photos which the current user may see. + * + * @template TDeclaringModel of \Illuminate\Database\Eloquent\Model + * @template TResult + * + * @param Relation|Builder $photoQueryable the relation to or query for {@link Photo} which is used to pick a thumb + * + * @return Thumb|null the created thumbnail; null if the relation is empty + * + * @throws InvalidPropertyException thrown, if $sortingOrder neither + * equals `desc` nor `asc` + */ + public static function createFromRandomQueryable(Relation|Builder $photoQueryable): ?Thumb + { + try { + /** @var Photo|null $cover */ + $cover = $photoQueryable + ->withOnly(['size_variants' => (fn ($r) => self::sizeVariantsFilter($r))]) + ->inRandomOrder() + ->select(['photos.id', 'photos.type']) + ->first(); + + return self::createFromPhoto($cover); + } catch (\InvalidArgumentException $e) { + throw new InvalidPropertyException('Sorting order invalid', $e); + } } - public function set_thumb2x(): void + /** + * Creates a thumbnail from the given photo. + * On Livewire it will use by default small and small2x if available, thumb and thumb2x if not. + * On Legacy it will use thumb and thumb2x. + * + * @param Photo|null $photo the photo + * + * @return Thumb|null the created thumbnail or null if null has been passed + */ + public static function createFromPhoto(?Photo $photo): ?Thumb { - $this->thumb2x = Helpers::ex2x($this->thumb); + if ($photo === null) { + return null; + } + + $thumb = $photo->size_variants->getSmall() ?? $photo->size_variants->getThumb(); + if ($thumb === null) { + return null; + } + + $thumb2x = $photo->size_variants->getSmall() !== null + ? $photo->size_variants->getSmall2x() + : $photo->size_variants->getThumb2x(); + + $placeholder = (Configs::getValueAsBool('low_quality_image_placeholder')) + ? $photo->size_variants->getPlaceholder() + : null; + + return new self( + $photo->id, + $photo->type, + $thumb->url, + $thumb2x?->url, + $placeholder?->url, + ); } + /** + * Serializes this object into an array. + * + * @return array The serialized properties of this object + */ public function toArray(): array { return [ - 'id' => strval($this->id), + 'id' => $this->id, 'type' => $this->type, - 'thumb' => $this->thumb, - 'thumb2x' => $this->thumb2x, + 'thumb' => $this->thumbUrl, + 'thumb2x' => $this->thumb2xUrl, + 'placeholder' => $this->placeholderUrl, ]; } } diff --git a/app/Models/Extensions/ToArrayThrowsNotImplemented.php b/app/Models/Extensions/ToArrayThrowsNotImplemented.php new file mode 100644 index 00000000000..8bca5a40ee9 --- /dev/null +++ b/app/Models/Extensions/ToArrayThrowsNotImplemented.php @@ -0,0 +1,36 @@ + + * + * @throws NotImplementedException + */ + final public function toArray(): array + { + $details = Route::getCurrentRoute()?->getName() ?? ''; + $details .= ($details !== '' ? ':' : '') . get_called_class(); + throw new NotImplementedException($details . '->toArray() is deprecated, use Resources instead.'); + } +} \ No newline at end of file diff --git a/app/Models/Extensions/UTCBasedTimes.php b/app/Models/Extensions/UTCBasedTimes.php index 92f62462851..6920ae49128 100644 --- a/app/Models/Extensions/UTCBasedTimes.php +++ b/app/Models/Extensions/UTCBasedTimes.php @@ -1,11 +1,20 @@ '+00:00'` and the configuration for PostgreSQL * should explicitly include the option `'timezone => 'UTC'`. - * Otherwise those RDBM system interpret a SQL datetime string without an + * Otherwise, those RDBM systems interpret an SQL datetime string without an * explicit timezone relative to their own default timezone. - * The default timezone of the database connection might or might or might not + * The default timezone of the database connection might or might not * be UTC and might or might not be equal to the default timezone of the PHP * application. * Hence, it is always a good thing to set the timezone of the database @@ -40,7 +49,7 @@ trait UTCBasedTimes { private static string $DB_TIMEZONE_NAME = 'UTC'; - private static string $DB_DATETIME_FORMAT = 'Y-m-d H:i:s'; + private static string $DB_DATETIME_FORMAT = 'Y-m-d H:i:s.u'; private static string $STANDARD_DATE_PATTERN = '/^(\d{4})-(\d{1,2})-(\d{1,2})$/'; /** @@ -70,17 +79,20 @@ trait UTCBasedTimes * * @param mixed $value * - * @return ?string + * @return string|null + * + * @throws InvalidTimeZoneException */ public function fromDateTime($value): ?string { // If $value is already an instance of Carbon, the method returns a - // deep copy, hence it is save to change the timezone below without + // deep copy, hence it is safe to change the timezone below without // altering the original object - $carbonTime = $this->asDateTime($value); - if (empty($carbonTime)) { + if ($value === null || $value === '') { return null; } + + $carbonTime = $this->asDateTime($value); $carbonTime->setTimezone(self::$DB_TIMEZONE_NAME); return $carbonTime->format(self::$DB_DATETIME_FORMAT); @@ -122,12 +134,14 @@ public function fromDateTime($value): ?string * * @param mixed $value * - * @return Carbon|null + * @return Carbon + * + * @throws InvalidTimeZoneException */ - public function asDateTime($value): ?Carbon + public function asDateTime($value): Carbon { - if (empty($value)) { - return null; + if ($value === null || $value === '') { + throw new LycheeLogicException('asDateTime called on null or empty string'); } // If this value is already a Carbon instance, we shall just return it as is. @@ -142,7 +156,8 @@ public function asDateTime($value): ?Carbon // when checking the field. We will just return the DateTime right away. if ($value instanceof \DateTimeInterface) { return Date::parse( - $value->format('Y-m-d H:i:s.u'), $value->getTimezone() + $value->format('Y-m-d H:i:s.u'), + $value->getTimezone() ); } @@ -163,31 +178,30 @@ public function asDateTime($value): ?Carbon // Applied patch: The standard date format Y-m-d _without_ a timezone // is interpreted relative to UTC and _then_ set to the // application's default timezone. - if (preg_match(self::$STANDARD_DATE_PATTERN, $value)) { - $result = Date::createFromFormat( - 'Y-m-d', $value, self::$DB_TIMEZONE_NAME - )->startOfDay(); - $result->setTimezone(date_default_timezone_get()); + if (preg_match(self::$STANDARD_DATE_PATTERN, $value) === 1) { + $date = Date::createFromFormat('Y-m-d', $value, self::$DB_TIMEZONE_NAME); + $date = $date !== false ? $date : null; + $result = $date?->startOfDay(); + $result?->setTimezone(date_default_timezone_get()); return $result; } // Finally, we will just assume this date is in the format used by default on // the database connection and use that format to create the Carbon object - // that is returned back out to the developers after we convert it here. + // that is returned to the caller after we convert it here. // Applied patch: Use 'UTC' as the default timezone for string // formats which do not include timezone information. // Note that the timezone parameter is ignored for formats which // include explicit timezone information. try { - $result = Date::createFromFormat( - self::$DB_DATETIME_FORMAT, $value, self::$DB_TIMEZONE_NAME - ); - if ($result->getTimezone()->getName() === self::$DB_TIMEZONE_NAME) { + $result = Date::createFromFormat(self::$DB_DATETIME_FORMAT, $value, self::$DB_TIMEZONE_NAME); + $result = $result !== false ? $result : null; + if ($result?->getTimezone()?->getName() === self::$DB_TIMEZONE_NAME) { // If the timezone is different to UTC, we don't set it, because then // the timezone came from the input string. // If the timezone equals UTC, then we assume that no explicit timezone - // information has been given and we set it to the application's + // information has been given, and we set it to the application's // default time zone. // This is a no-op, if the application's default timezone equals 'UTC' // anyway. @@ -197,9 +211,9 @@ public function asDateTime($value): ?Carbon } return $result; - } catch (InvalidArgumentException $e) { + } catch (\InvalidArgumentException) { // If the specified format did not mach, don't throw an exception, - // but try to parse the value using an best-effort approach, see below + // but try to parse the value using a best-effort approach, see below } // Might throw an InvalidArgumentException if no recognized format is found, @@ -216,7 +230,7 @@ public function asDateTime($value): ?Carbon * Prepares a date for array/JSON serialization. * * In contrast to the original implementation, this one serializes the - * timezone "as is". + * timezone "as is" and includes fractions of seconds. * * @param \DateTimeInterface $date * @@ -224,6 +238,6 @@ public function asDateTime($value): ?Carbon */ protected function serializeDate(\DateTimeInterface $date): string { - return $date->format(\DateTimeInterface::ATOM); + return $date->format('Y-m-d\TH:i:sP'); } } diff --git a/app/Models/JobHistory.php b/app/Models/JobHistory.php new file mode 100644 index 00000000000..f1ca203d669 --- /dev/null +++ b/app/Models/JobHistory.php @@ -0,0 +1,89 @@ + + */ + protected $casts = [ + 'status' => JobStatus::class, + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'owner_id' => 'integer', + ]; + + /** + * The relationships that should always be eagerly loaded by default. + */ + protected $with = ['owner']; + + /** + * Returns the relationship between an Job and its owner. + * + * @return BelongsTo + */ + public function owner(): BelongsTo + { + return $this->belongsTo(User::class, 'owner_id', 'id'); + } +} diff --git a/app/Models/Logs.php b/app/Models/Logs.php deleted file mode 100644 index 44b84650668..00000000000 --- a/app/Models/Logs.php +++ /dev/null @@ -1,132 +0,0 @@ - 'emergency', - self::SEVERITY_ALERT => 'alert', - self::SEVERITY_CRITICAL => 'critical', - self::SEVERITY_ERROR => 'error', - self::SEVERITY_WARNING => 'warning', - self::SEVERITY_NOTICE => 'notice', - self::SEVERITY_INFO => 'info', - self::SEVERITY_DEBUG => 'debug', - ]; - - const MAX_METHOD_LENGTH = 100; - - /** - * allow these properties to be mass assigned. - */ - protected $fillable = [ - 'type', - 'function', - 'line', - 'text', - ]; - - /** - * Logs a notification. - * - * @param string $method the name of the method which triggers the log - * (use the magic constant `__METHOD__`, neither - * `__FUNCTION__` nor `__FILE__`) - * @param int $line the line which triggers the log - * @param string $msg the message to log - */ - public static function notice(string $method, int $line, string $msg): void - { - self::log(self::SEVERITY_NOTICE, $method, $line, $msg); - } - - /** - * Logs a warning. - * - * @param string $method the name of the method which triggers the log - * (use the magic constant `__METHOD__`, neither - * `__FUNCTION__` nor `__FILE__`) - * @param int $line the line which triggers the log - * @param string $msg the message to log - */ - public static function warning(string $method, int $line, string $msg): void - { - self::log(self::SEVERITY_WARNING, $method, $line, $msg); - } - - /** - * Logs an error. - * - * @param string $method the name of the method which triggers the log - * (use the magic constant `__METHOD__`, neither - * `__FUNCTION__` nor `__FILE__`) - * @param int $line the line which triggers the log - * @param string $msg the message to log - */ - public static function error(string $method, int $line, string $msg): void - { - self::log(self::SEVERITY_ERROR, $method, $line, $msg); - } - - /** - * Writes a log entry. - * - * @param int $severity the severity of the incident, must be one out - * of {@link Logs::SEVERITY_EMERGENCY}, - * {@link Logs::SEVERITY_ALERT}, - * {@link Logs::SEVERITY_CRITICAL}, - * {@link Logs::SEVERITY_ERROR}, - * {@link Logs::SEVERITY_WARNING}, - * {@link Logs::SEVERITY_NOTICE}, - * {@link Logs::SEVERITY_INFO} or - * {@link Logs::SEVERITY_DEBUG} - * @param string $method the name of the method which triggers the log - * (use the magic constant `__METHOD__`, neither - * `__FUNCTION__` nor `__FILE__`) - * @param int $line the line which triggers the log - * @param string $msg the message to log - */ - public static function log(int $severity, string $method, int $line, string $msg): void - { - try { - if (strlen($method) > self::MAX_METHOD_LENGTH) { - $method = '...' . substr($method, 3, self::MAX_METHOD_LENGTH - 3); - } - $log = new static([ - 'type' => self::SEVERITY_2_STRING[$severity], - 'function' => $method, - 'line' => $line, - 'text' => $msg, - ]); - $log->save(); - } catch (\Throwable $ignored) { - } - } -} diff --git a/app/Models/OauthCredential.php b/app/Models/OauthCredential.php new file mode 100644 index 00000000000..af35ffcb5cf --- /dev/null +++ b/app/Models/OauthCredential.php @@ -0,0 +1,72 @@ + 'datetime', + 'updated_at' => 'datetime', + 'user_id' => 'integer', + 'provider' => OauthProvidersType::class, + ]; + + protected $hidden = [ + 'token_id', + ]; + + /** + * Return the relationship between a Photo and its Album. + * + * @return BelongsTo + */ + public function user(): BelongsTo + { + return $this->belongsTo(User::class, 'user_id', 'id'); + } + + /** + * @param $query + * + * @return OauthCredentialBuilder + */ + public function newEloquentBuilder($query): OauthCredentialBuilder + { + return new OauthCredentialBuilder($query); + } +} diff --git a/app/Models/Page.php b/app/Models/Page.php deleted file mode 100644 index 2a7d8ddebb1..00000000000 --- a/app/Models/Page.php +++ /dev/null @@ -1,80 +0,0 @@ -hasMany('App\Models\PageContent', 'page_id', 'id')->orderBy('order', 'ASC'); - } - - /** - * Define some scopes. - */ - - /** - * @param $query - * - * @return mixed - */ - public function scopeMenu(Builder $query) - { - return $query->where('in_menu', true)->where('enabled', true)->orderBy('order', 'ASC'); - } - - /** - * @param $query - * - * @return mixed - */ - public function scopeEnabled(Builder $query) - { - return $query->where('enabled', true)->orderBy('order', 'ASC'); - } -} diff --git a/app/Models/PageContent.php b/app/Models/PageContent.php deleted file mode 100644 index 8411c45076c..00000000000 --- a/app/Models/PageContent.php +++ /dev/null @@ -1,58 +0,0 @@ - create a img tag, `content` is the url of the image - * It can be a div -> create a div tag, `content` is then compiled from Markdown to HTML. - * - * @return string - */ - public function get_content() - { - $return = ''; - if ($this->type == 'img') { - $return = '
image
'; - } elseif ($this->type == 'div') { - $return = '
'; - $return .= Markdown::convertToHtml($this->content); - $return .= '
'; - } - - return $return; - } -} diff --git a/app/Models/Photo.php b/app/Models/Photo.php index ee650e7b2ad..17028ea7285 100644 --- a/app/Models/Photo.php +++ b/app/Models/Photo.php @@ -1,350 +1,458 @@ */ + use HasFactory; use UTCBasedTimes; - const THUMBNAIL_DIM = 200; - const THUMBNAIL2X_DIM = 400; - const VARIANT_THUMB = 'thumb'; - const VARIANT_THUMB2X = 'thumb2x'; - const VARIANT_SMALL = 'small'; - const VARIANT_SMALL2X = 'small2x'; - const VARIANT_MEDIUM = 'medium'; - const VARIANT_MEDIUM2X = 'medium2x'; - const VARIANT_ORIGINAL = 'original'; + use HasAttributesPatch; + /** @phpstan-use HasRandomIDAndLegacyTimeBasedID */ + use HasRandomIDAndLegacyTimeBasedID; + use ThrowsConsistentExceptions; + /** @phpstan-use HasBidirectionalRelationships */ + use HasBidirectionalRelationships; + use ToArrayThrowsNotImplemented; /** - * Maps a size variant to the path prefix (directory) where the file for that size variant is stored. - * Use this array to avoid the anti-pattern "magic constants" throughout the whole code. + * @var string The type of the primary key */ - const VARIANT_2_PATH_PREFIX = [ - self::VARIANT_THUMB => 'thumb', - self::VARIANT_THUMB2X => 'thumb', - self::VARIANT_SMALL => 'small', - self::VARIANT_SMALL2X => 'small', - self::VARIANT_MEDIUM => 'medium', - self::VARIANT_MEDIUM2X => 'medium', - self::VARIANT_ORIGINAL => 'big', - ]; + protected $keyType = 'string'; + + /** + * Indicates if the model's primary key is auto-incrementing. + * + * @var bool + */ + public $incrementing = false; protected $casts = [ - 'public' => 'int', - 'star' => 'int', - 'downloadable' => 'int', - 'share_button_visible' => 'int', + RandomID::LEGACY_ID_NAME => RandomID::LEGACY_ID_TYPE, 'created_at' => 'datetime', 'updated_at' => 'datetime', 'taken_at' => DateTimeWithTimezoneCast::class, + 'live_photo_url' => MustNotSetCast::class . ':live_photo_short_path', + 'owner_id' => 'integer', + 'is_starred' => 'boolean', + 'tags' => ArrayCast::class, + 'latitude' => 'float', + 'longitude' => 'float', + 'altitude' => 'float', + 'img_direction' => 'float', + ]; + + /** + * @var array The list of attributes which exist as columns of the DB + * relation but shall not be serialized to JSON + */ + protected $hidden = [ + RandomID::LEGACY_ID_NAME, + 'album', // do not serialize relation in order to avoid infinite loops + 'owner', // do not serialize relation + 'owner_id', + 'live_photo_short_path', // serialize live_photo_url instead ]; + /** + * @param $query + * + * @return PhotoBuilder + */ + public function newEloquentBuilder($query): PhotoBuilder + { + return new PhotoBuilder($query); + } + /** * Return the relationship between a Photo and its Album. * - * @return BelongsTo + * @return BelongsTo */ public function album(): BelongsTo { - return $this->belongsTo('App\Models\Album', 'album_id', 'id'); + return $this->belongsTo(Album::class, 'album_id', 'id'); } /** * Return the relationship between a Photo and its Owner. * - * @return BelongsTo + * @return BelongsTo */ public function owner(): BelongsTo { - return $this->belongsTo('App\Models\User', 'owner_id', 'id'); + return $this->belongsTo(User::class, 'owner_id', 'id'); + } + + public function size_variants(): HasManySizeVariants + { + return new HasManySizeVariants($this); } /** - * Before calling the delete() method which will remove the entry from the database, we need to remove the files. + * Accessor for attribute {@link Photo::$shutter}. + * + * This accessor ensures that the returned string is either formatted as + * a unit fraction or a decimal number irrespective of what is stored + * in the database. * - * @param bool $keep_original + * Actually it would be much more efficient to write a mutator which + * ensures that the string is stored correctly formatted at the DB right + * from the beginning and then simply return the stored string instead of + * re-format the string on every fetch. + * TODO: Refactor this. * - * @return bool True on success, false otherwise + * @param string|null $shutter the value from the database passed in by + * the Eloquent framework + * + * @return ?string A properly formatted shutter value */ - public function predelete(bool $keep_original = false): bool + protected function getShutterAttribute(?string $shutter): ?string { - if ($this->isDuplicate($this->checksum, $this->id)) { - Logs::notice(__METHOD__, __LINE__, $this->id . ' is a duplicate!'); - // it is a duplicate, we do not delete! - return true; - } - - $error = false; - $path_prefix = $this->type == 'raw' ? 'raw/' : 'big/'; - if ($keep_original === false) { - // quick check... - if (!Storage::exists($path_prefix . $this->url)) { - Logs::error(__METHOD__, __LINE__, 'Could not find file in ' . Storage::path($path_prefix . $this->url)); - $error = true; - } elseif (!Storage::delete($path_prefix . $this->url)) { - Logs::error(__METHOD__, __LINE__, 'Could not delete file in ' . Storage::path($path_prefix . $this->url)); - $error = true; + try { + if ($shutter === null || $shutter === '') { + return null; } - } - - if ((strpos($this->type, 'video') === 0) || ($this->type == 'raw')) { - $photoName = $this->thumbUrl; - } else { - $photoName = $this->url; - } - if ($photoName !== '') { - $photoName2x = Helpers::ex2x($photoName); - - // Delete Live Photo Video file - // TODO: USE STORAGE FOR DELETE - // check first if livePhotoUrl is available - if ($this->livePhotoUrl !== null) { - if (!Storage::exists('big/' . $this->livePhotoUrl)) { - Logs::error(__METHOD__, __LINE__, 'Could not find file in ' . Storage::path('big/' . $this->livePhotoUrl)); - $error = true; - } elseif (!Storage::delete('big/' . $this->livePhotoUrl)) { - Logs::error(__METHOD__, __LINE__, 'Could not delete file in ' . Storage::path('big/' . $this->livePhotoUrl)); - $error = true; + // shutter speed needs to be processed. It is stored as a string `a/b s` + if (!str_starts_with($shutter, '1/')) { + preg_match('/(\d+)\/(\d+) s/', $shutter, $matches); + if ($matches) { + $a = intval($matches[1]); + $b = intval($matches[2]); + if ($b !== 0) { + $gcd = Helpers::gcd($a, $b); + $a /= $gcd; + $b /= $gcd; + if ($a === 1) { + $shutter = '1/' . $b . ' s'; + } else { + $shutter = ($a / $b) . ' s'; + } + } } } - // Delete medium - // TODO: USE STORAGE FOR DELETE - if (Storage::exists('medium/' . $photoName) && !unlink(Storage::path('medium/' . $photoName))) { - Logs::error(__METHOD__, __LINE__, 'Could not delete photo in uploads/medium/'); - $error = true; - } - - // TODO: USE STORAGE FOR DELETE - if (Storage::exists('medium/' . $photoName2x) && !unlink(Storage::path('medium/' . $photoName2x))) { - Logs::error(__METHOD__, __LINE__, 'Could not delete high-res photo in uploads/medium/'); - $error = true; + if ($shutter === '1/1 s') { + $shutter = '1 s'; } - // Delete small - // TODO: USE STORAGE FOR DELETE - if (Storage::exists('small/' . $photoName) && !unlink(Storage::path('small/' . $photoName))) { - Logs::error(__METHOD__, __LINE__, 'Could not delete photo in uploads/small/'); - $error = true; - } + return $shutter; + } catch (ZeroModuloException $e) { + // this should not happen as we covered the case $b = 0; + throw LycheeAssertionError::createFromUnexpectedException($e); + } + } - // TODO: USE STORAGE FOR DELETE - if (Storage::exists('small/' . $photoName2x) && !unlink(Storage::path('small/' . $photoName2x))) { - Logs::error(__METHOD__, __LINE__, 'Could not delete high-res photo in uploads/small/'); - $error = true; - } + /** + * Accessor for attribute `license`. + * + * If the photo has an explicitly set license, that license is returned. + * Else, either the licence of the album is returned (if the photo is + * part of an album) or the default license of the application-wide + * setting is returned. + * + * @param ?string $license the value from the database passed in by + * the Eloquent framework + * + * @return LicenseType + */ + protected function getLicenseAttribute(?string $license): LicenseType + { + if ($license === null) { + return Configs::getValueAsEnum('default_license', LicenseType::class); } - if ($this->thumbUrl != '') { - // Get retina thumb url - $thumbUrl2x = Helpers::ex2x($this->thumbUrl); - // Delete thumb - // TODO: USE STORAGE FOR DELETE - if (Storage::exists('thumb/' . $this->thumbUrl) && !unlink(Storage::path('thumb/' . $this->thumbUrl))) { - Logs::error(__METHOD__, __LINE__, 'Could not delete photo in uploads/thumb/'); - $error = true; - } + if (LicenseType::tryFrom($license) !== null && LicenseType::tryFrom($license) !== LicenseType::NONE) { + return LicenseType::from($license); + } - // Delete thumb@2x - // TODO: USE STORAGE FOR DELETE - if (Storage::exists('thumb/' . $thumbUrl2x) && !unlink(Storage::path('thumb/' . $thumbUrl2x))) { - Logs::error(__METHOD__, __LINE__, 'Could not delete high-res photo in uploads/thumb/'); - $error = true; - } + if ($this->album_id !== null && $this->relationLoaded('album')) { + return $this->album->license; } - return !$error; + return Configs::getValueAsEnum('default_license', LicenseType::class); } /** - * @param $query + * Accessor for attribute `focal`. * - * @return mixed + * In case the photo is a video (why it is called a photo then, btw?), the + * attribute `focal` is exploited to store the framerate and rounded + * to two decimal digits. + * + * Again, we probably should do that when the value is set and stored, + * not every time when it is read from the database. + * TODO: Refactor this. + * + * @param string|null $focal the value from the database passed in by the + * Eloquent framework + * + * @return ?string + * + * @throws IllegalOrderOfOperationException */ - public static function set_order(Builder $query) + protected function getFocalAttribute(?string $focal): ?string { - $sortingCol = Configs::get_value('sorting_Photos_col'); - if ($sortingCol !== 'title' && $sortingCol !== 'description') { - $query = $query->orderBy($sortingCol, Configs::get_value('sorting_Photos_order')); + if ($focal === null || $focal === '') { + return null; } - return $query->orderBy('photos.id', 'ASC'); + // We need to format the framerate (stored as focal) -> max 2 decimal digits + return $this->isVideo() ? (string) round(floatval($focal), 2) : $focal; } /** - * Define scopes which we can directly use e.g. Photo::stars()->all(). - */ - - /** - * @param $query + * Accessor for the "virtual" attribute {@see Photo::$live_photo_url}. * - * @return mixed + * Returns the URL of the live photo as it is seen from a client's + * point of view. + * This is a convenient method and wraps + * {@link Photo::$live_photo_short_path} into + * {@link \Illuminate\Support\Facades\Storage::url()}. + * + * @return ?string the url of the file */ - public function scopeStars($query) + protected function getLivePhotoUrlAttribute(): ?string { - return $query->where('star', '=', 1); + $path = $this->live_photo_short_path; + $disk_name = $this->size_variants->getOriginal()?->storage_disk?->value ?? StorageDiskType::LOCAL->value; + + /** @disregard P1013 */ + return ($path === null || $path === '') ? null : Storage::disk($disk_name)->url($path); } /** - * @param $query + * Accessor for the virtual attribute $aspect_ratio. + * + * Returns the correct aspect ratio for + * - photos + * - and videos where small or medium exists + * Otherwise returns 1 (square) * - * @return mixed + * @return float aspect ratio to use in display mode */ - public function scopePublic($query) + protected function getAspectRatioAttribute(): float { - return $query->where('public', '=', 1); + if ($this->isVideo() && + $this->size_variants->getSmall() === null && + $this->size_variants->getMedium() === null) { + return 1; + } + + return $this->size_variants->getOriginal()?->ratio ?? + $this->size_variants->getMedium()?->ratio ?? + $this->size_variants->getSmall()?->ratio ?? 1; } /** - * @param $query + * Checks if the photo represents a (real) photo (as opposed to video or raw). * - * @return mixed + * @return bool + * + * @throws IllegalOrderOfOperationException */ - public function scopeRecent($query) + public function isPhoto(): bool { - return $query->where('created_at', '>=', Carbon::now()->subDays(intval(Configs::get_value('recent_age', '1')))->toDateTimeString()); + if ($this->type === null || $this->type === '') { + // @codeCoverageIgnoreStart + throw new IllegalOrderOfOperationException('Photo::isPhoto() must not be called before Photo::$type has been set'); + // @codeCoverageIgnoreEnd + } + + return BaseMediaFile::isSupportedImageMimeType($this->type); } /** - * @param $query + * Checks if the photo represents a video. + * + * @return bool * - * @return mixed + * @throws IllegalOrderOfOperationException */ - public function scopeUnsorted($query) + public function isVideo(): bool { - return $query->where('album_id', '=', null); + if ($this->type === null || $this->type === '') { + // @codeCoverageIgnoreStart + throw new IllegalOrderOfOperationException('Photo::isVideo() must not be called before Photo::$type has been set'); + // @codeCoverageIgnoreEnd + } + + return BaseMediaFile::isSupportedVideoMimeType($this->type); } /** - * @param $query - * @param $id + * Checks if the photo represents a raw media. + * + * The media record is "raw" if it is neither of a supported photo nor + * video type. + * + * @return bool * - * @return mixed + * @throws IllegalOrderOfOperationException */ - public function scopeOwnedBy(Builder $query, $id) + public function isRaw(): bool { - return $id == 0 ? $query : $query->where('owner_id', '=', $id); + return !$this->isPhoto() && !$this->isVideo(); } - public function withTags($tags) + /** + * @param string[] $except + */ + public function replicate(?array $except = null): Photo { - $sql = $this; - foreach ($tags as $tag) { - $sql = $sql->where('tags', 'like', '%' . $tag . '%'); + $duplicate = parent::replicate($except); + // A photo has the following relations: (parent) album, owner and + // size_variants. + // While the duplicate may keep the relation to the same album and + // each photo requires an individual set of size variants. + // Se we unset the relation and explicitly duplicate the size variants. + $duplicate->unsetRelation('size_variants'); + // save duplicate so that the photo gets an ID + $duplicate->save(); + + $areSizeVariantsOriginallyLoaded = $this->relationLoaded('size_variants'); + // Duplicate the size variants of this instance for the duplicate + $duplicatedSizeVariants = $this->size_variants->replicate($duplicate); + if ($areSizeVariantsOriginallyLoaded) { + $duplicate->setRelation('size_variants', $duplicatedSizeVariants); } - return ($sql->count() == 0) ? false : $sql->first(); + return $duplicate; + } + + /** + * {@inheritDoc} + * + * @throws ModelDBException + * @throws MediaFileOperationException + */ + protected function performDeleteOnModel(): void + { + $fileDeleter = (new Delete())->do([$this->id]); + $this->exists = false; + $fileDeleter->do(); } } diff --git a/app/Models/SizeVariant.php b/app/Models/SizeVariant.php new file mode 100644 index 00000000000..abf11b648e8 --- /dev/null +++ b/app/Models/SizeVariant.php @@ -0,0 +1,284 @@ + $sym_links + * + * @method static SizeVariantBuilder|SizeVariant addSelect($column) + * @method static SizeVariantBuilder|SizeVariant join(string $table, string $first, string $operator = null, string $second = null, string $type = 'inner', string $where = false) + * @method static SizeVariantBuilder|SizeVariant joinSub($query, $as, $first, $operator = null, $second = null, $type = 'inner', $where = false) + * @method static SizeVariantBuilder|SizeVariant leftJoin(string $table, string $first, string $operator = null, string $second = null) + * @method static SizeVariantBuilder|SizeVariant newModelQuery() + * @method static SizeVariantBuilder|SizeVariant newQuery() + * @method static SizeVariantBuilder|SizeVariant orderBy($column, $direction = 'asc') + * @method static SizeVariantBuilder|SizeVariant query() + * @method static SizeVariantBuilder|SizeVariant select($columns = []) + * @method static SizeVariantBuilder|SizeVariant whereFilesize($value) + * @method static SizeVariantBuilder|SizeVariant whereHeight($value) + * @method static SizeVariantBuilder|SizeVariant whereId($value) + * @method static SizeVariantBuilder|SizeVariant whereIn(string $column, string $values, string $boolean = 'and', string $not = false) + * @method static SizeVariantBuilder|SizeVariant whereNotIn(string $column, string $values, string $boolean = 'and') + * @method static SizeVariantBuilder|SizeVariant wherePhotoId($value) + * @method static SizeVariantBuilder|SizeVariant whereShortPath($value) + * @method static SizeVariantBuilder|SizeVariant whereType($value) + * @method static SizeVariantBuilder|SizeVariant whereWidth($value) + * + * @mixin \Eloquent + */ +class SizeVariant extends Model +{ + use UTCBasedTimes; + use HasAttributesPatch; + use HasBidirectionalRelationships; + use ThrowsConsistentExceptions; + use ToArrayThrowsNotImplemented; + /** @phpstan-use HasFactory<\Database\Factories\SizeVariantFactory> */ + use HasFactory; + + /** + * This model has no own timestamps as it is inseparably bound to its + * parent {@link \App\Models\Photo} and uses the same timestamps. + * + * @var bool + */ + public $timestamps = false; + + /** + * @var array + */ + protected $casts = [ + 'id' => 'integer', + 'type' => SizeVariantType::class, + 'url' => MustNotSetCast::class . ':short_path', + 'width' => 'integer', + 'height' => 'integer', + 'filesize' => 'integer', + 'ratio' => 'float', + 'storage_disk' => StorageDiskType::class, + ]; + + /** + * @var array The list of attributes which exist as columns of the DB + * relation but shall not be serialized to JSON + */ + protected $hidden = [ + 'id', // irrelevant, because a size variant is always serialized as an embedded object of its photo + 'photo', // see above and otherwise infinite loops will occur + 'photo_id', // see above + 'short_path', // serialize url instead + 'sym_links', // don't serialize relation of symlinks + ]; + + /** + * @var array The list of "virtual" attributes which do not exist as + * columns of the DB relation but which shall be appended to + * JSON from accessors + */ + protected $appends = [ + 'url', + ]; + + /** + * The attributes that are mass assignable. + * + * @var array + */ + protected $fillable = ['photo_id', 'storage_disk', 'type', 'short_path', 'width', 'height', 'filesize', 'ratio']; + + /** + * @param $query + * + * @return SizeVariantBuilder + */ + public function newEloquentBuilder($query): SizeVariantBuilder + { + return new SizeVariantBuilder($query); + } + + /** + * Returns the association to the photo which this size variant belongs + * to. + * + * @return BelongsTo + */ + public function photo(): BelongsTo + { + return $this->belongsTo(Photo::class); + } + + /** + * Returns the association to the symbolics links which point to this + * size variant. + * + * @return HasManyBidirectionally + */ + public function sym_links(): HasManyBidirectionally + { + return $this->hasManyBidirectionally(SymLink::class); + } + + /** + * Accessor for the "virtual" attribute {@link SizeVariant::$url}. + * + * This is more than a simple convenient method which wraps + * {@link SizeVariant::$short_path} into + * {@link \Illuminate\Support\Facades\Storage::url()}. + * Based on the current application settings and the authenticated user, + * this method returns a URL to a short-living symbolic link instead of a + * direct URL to the actual size variant, if the underlying storage + * provides symbolic links. + * + * @return string the url of the size variant + * + * @throws ConfigurationException + */ + public function getUrlAttribute(): string + { + $imageDisk = Storage::disk($this->storage_disk->value); + + if ($this->type === SizeVariantType::PLACEHOLDER) { + return 'data:image/webp;base64,' . $this->short_path; + } + + if ( + !Configs::getValueAsBool('SL_enable') || + (!Configs::getValueAsBool('SL_for_admin') && Auth::user()?->may_administrate === true) + ) { + /** @disregard P1013 */ + return $imageDisk->url($this->short_path); + } + + /** @disregard P1013 */ + $storageAdapter = $imageDisk->getAdapter(); + if ($storageAdapter instanceof AwsS3V3Adapter) { + // @codeCoverageIgnoreStart + return $this->getAwsUrl(); + // @codeCoverageIgnoreEnd + } + + if ($storageAdapter instanceof LocalFilesystemAdapter) { + return $this->getSymLinkUrl(); + } + + throw new ConfigurationException('the chosen storage adapter "' . get_class($storageAdapter) . '" does not support the symbolic linking feature'); + } + + /** + * Retrieve the tempary url from AWS if possible. + * + * @return string + * + * @codeCoverageIgnore + */ + private function getAwsUrl(): string + { + // In order to allow a grace period, we create a new symbolic link, + $maxLifetime = Configs::getValueAsInt('SL_life_time_days') * 24 * 60 * 60; + $imageDisk = Storage::disk($this->storage_disk->value); + + // Return the public URL in case the S3 bucket is set to public, otherwise generate a temporary URL + $visibility = config('filesystems.disks.s3.visibility', 'private'); + if ($visibility === 'public') { + /** @disregard P1013 */ + return $imageDisk->url($this->short_path); + } + + /** @disregard P1013 */ + return $imageDisk->temporaryUrl($this->short_path, now()->addSeconds($maxLifetime)); + } + + /** + * Get the symlink url if possible. + * + * @return string + */ + private function getSymLinkUrl(): string + { + // In order to allow a grace period, we create a new symbolic link, + // if the most recent existing link has reached 2/3 of its lifetime + $maxLifetime = Configs::getValueAsInt('SL_life_time_days') * 24 * 60 * 60; + $gracePeriod = $maxLifetime / 3; + + /** @var ?SymLink $symLink */ + $symLink = $this->sym_links()->latest()->first(); + if ($symLink === null || $symLink->created_at->isBefore(now()->subSeconds($gracePeriod))) { + /** @var SymLink $symLink */ + $symLink = $this->sym_links()->create(); + } + + return $symLink->url; + } + + public function getFile(): FlysystemFile + { + return new FlysystemFile( + Storage::disk($this->storage_disk->value), + $this->short_path + ); + } + + /** + * {@inheritDoc} + * + * @throws ModelDBException + * @throws MediaFileOperationException + */ + protected function performDeleteOnModel(): void + { + $fileDeleter = (new Delete())->do([$this->id]); + $this->exists = false; + $fileDeleter->do(); + } + + public function toResource(bool $noUrl = false): SizeVariantResource + { + return new SizeVariantResource($this, noUrl: $noUrl); + } +} diff --git a/app/Models/SymLink.php b/app/Models/SymLink.php index 60782a6a85b..df878d7c94b 100644 --- a/app/Models/SymLink.php +++ b/app/Models/SymLink.php @@ -1,208 +1,199 @@ 'thumbUrl', - Photo::VARIANT_THUMB2X => 'thumbUrl', - Photo::VARIANT_SMALL => 'url', - Photo::VARIANT_SMALL2X => 'url', - Photo::VARIANT_MEDIUM => 'url', - Photo::VARIANT_MEDIUM2X => 'url', - Photo::VARIANT_ORIGINAL => 'url', - ]; + public const DISK_NAME = 'symbolic'; - /** - * Maps a size variant to the name of an attribute (field) of the class App\Models\Photo which may be exploited - * as an indicator whether this size variant exist. - */ - const VARIANT_2_INDICATOR_FIELD = [ - Photo::VARIANT_THUMB => 'thumbUrl', // type: string|null - Photo::VARIANT_THUMB2X => 'thumb2x', // type: integer, either 0 or 1 - Photo::VARIANT_SMALL => 'small_width', // type: int|null - Photo::VARIANT_SMALL2X => 'small2x_width', // type: int|null - Photo::VARIANT_MEDIUM => 'medium_width', // type: int|null - Photo::VARIANT_MEDIUM2X => 'medium2x_width', // type: int|null - Photo::VARIANT_ORIGINAL => 'url', // type: string|null + protected $casts = [ + 'id' => 'integer', + 'size_variant_id' => 'integer', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'url' => MustNotSetCast::class, ]; /** - * Maps a size variant to the name of the attribute (field) of this class/database table which stores the - * symlinked path. - * (Despite some attributes being named "url" they actually store relative paths). + * @var array The list of attributes which exist as columns of the DB + * relation but shall not be serialized to JSON */ - const VARIANT_2_SYM_PATH_FIELD = [ - Photo::VARIANT_THUMB => 'thumbUrl', - Photo::VARIANT_THUMB2X => 'thumb2x', - Photo::VARIANT_SMALL => 'small', - Photo::VARIANT_SMALL2X => 'small2x', - Photo::VARIANT_MEDIUM => 'medium', - Photo::VARIANT_MEDIUM2X => 'medium2x', - Photo::VARIANT_ORIGINAL => 'url', + protected $hidden = [ + 'size_variant', // see above and otherwise infinite loops will occur + 'size_variant_id', // see above ]; /** - * Generate a sym link. - * The salt is important in order to remove the deterministic side of the address. + * @param $query * - * @param Photo $photo The original photo - * @param string $sizeVariant An enum-like attribute which indicates what size variant shall be sym-linked. - * Allowed values are defined as constants in class Photo. - * @param string $salt + * @return SymLinkBuilder */ - private function create(Photo $photo, string $sizeVariant, string $salt) + public function newEloquentBuilder($query): SymLinkBuilder { - // in case of video and raw we always need to use the field 'thumbUrl' for anything which is not the original size - $originalFieldName = ($sizeVariant != Photo::VARIANT_ORIGINAL && ($photo->isVideo() || $photo->type == 'raw')) ? - self::VARIANT_2_ORIGINAL_FILENAME_FIELD[Photo::VARIANT_THUMB] : - self::VARIANT_2_ORIGINAL_FILENAME_FIELD[$sizeVariant]; - $originalFileName = (substr($sizeVariant, -2, 2) == '2x') ? Helpers::ex2x($photo->$originalFieldName) : $photo->$originalFieldName; - - if ($photo->type == 'raw' && $sizeVariant == Photo::VARIANT_ORIGINAL) { - $originalPath = Storage::path('raw/' . $originalFileName); - } else { - $originalPath = Storage::path(Photo::VARIANT_2_PATH_PREFIX[$sizeVariant] . '/' . $originalFileName); - } - $extension = Helpers::getExtension($originalPath); - $symFilename = hash('sha256', $salt . '|' . $originalPath) . $extension; - $symPath = Storage::drive('symbolic')->path($symFilename); + return new SymLinkBuilder($query); + } - try { - // in theory we should be safe... - symlink($originalPath, $symPath); - } catch (Exception $exception) { - unlink($symPath); - symlink($originalPath, $symPath); - } - $this->{self::VARIANT_2_SYM_PATH_FIELD[$sizeVariant]} = $symFilename; + /** + * @return BelongsTo + */ + public function size_variant(): BelongsTo + { + return $this->belongsTo(SizeVariant::class); } /** - * Set up a link. + * Scopes the passed query to all outdated symlinks. * - * @param Photo $photo + * @param Builder $query the unscoped query + * + * @return Builder the scoped query + * + * @throws InvalidTimeZoneException */ - public function set(Photo $photo) + public function scopeExpired(Builder $query): Builder { - $this->photo_id = $photo->id; - $this->timestamps = false; - // we set up the created_at - $now = now(); - $this->created_at = $now; - $this->updated_at = $now; - - foreach (self::VARIANT_2_INDICATOR_FIELD as $variant => $indicator_field) { - if ($photo->{$indicator_field} !== null && $photo->{$indicator_field} !== 0 && $photo->{$indicator_field} !== '') { - $this->create($photo, $variant, strval($now)); - } - } + $expiration = now()->subDays(Configs::getValueAsInt('SL_life_time_days')); + + return $query->where('created_at', '<', $this->fromDateTime($expiration)); } /** - * Given the return array of a photo, override the link provided. + * Accessor for the "virtual" attribute {@link SymLink::$url}. * - * @param array $return The serialization of a photo as returned by Photo#toReturnArray() + * Returns the URL to the symbolic link from the perspective of a + * web client. + * This is a convenient method and wraps {@link SymLink::$short_path} + * into {@link \Illuminate\Support\Facades\Storage::url()}. + * + * @return string the URL to the symbolic link + * + * @throws FrameworkException */ - public function override(array &$return) + protected function getUrlAttribute(): string { - foreach (self::VARIANT_2_SYM_PATH_FIELD as $variant => $field) { - if ($this->$field != '') { - // TODO: This could be avoided, if the original variant was also serialized into the sub-array 'sizeVariants', see comment in PhotoCast#toReturnArray - if ($variant == Photo::VARIANT_ORIGINAL) { - $return['url'] = Storage::drive('symbolic')->url($this->$field); - } else { - $return['sizeVariants'][$variant]['url'] = Storage::drive('symbolic')->url($this->$field); - } - } + try { + /** @disregard P1013 */ + return Storage::disk(self::DISK_NAME)->url($this->short_path); + } catch (\RuntimeException $e) { + throw new FrameworkException('Laravel\'s storage component', $e); } } /** - * Returns the relative symlinked path of a particular size variant, if it exists. + * Performs the `INSERT` operation of the model and creates an actual + * symbolic link on disk. * - * @param string $sizeVariant An enum-like attribute which indicates what size variant shall be sym-linked. - * Allowed values are defined as constants in class Photo. + * If this method cannot create the symbolic link, then this method + * cancels the insert operation. * - * @return string Relative path to symbolic link or the empty string ('') + * @param Builder $query + * + * @return bool + * + * @throws MediaFileOperationException */ - public function get(string $sizeVariant): string + protected function performInsert(Builder $query): bool { - $field = self::VARIANT_2_SYM_PATH_FIELD[$sizeVariant]; - if ($this->$field != '') { - return Storage::drive('symbolic')->url($this->$field); - } else { - return ''; + $file = $this->size_variant->getFile()->toLocalFile(); + $origRealPath = $file->getRealPath(); + $extension = $file->getExtension(); + $symShortPath = hash('sha256', random_bytes(32) . '|' . $origRealPath) . $extension; + /** @disregard P1013 */ + $symAbsolutePath = Storage::disk(SymLink::DISK_NAME)->path($symShortPath); + try { + if (is_link($symAbsolutePath)) { + unlink($symAbsolutePath); + } + symlink($origRealPath, $symAbsolutePath); + } catch (FilesystemException $e) { + throw new MediaFileOperationException($e->getMessage(), $e); } + $this->short_path = $symShortPath; + + return parent::performInsert($query); } /** - * before deleting we actually unlink the symlinks. + * Deletes the model from the database and the symbolic link from storage. * - * @return bool|null + * If this method cannot delete the symbolic link, then this method + * cancels the delete operation. + * + * @return bool always returns true + * + * @throws MediaFileOperationException + * @throws ModelDBException */ - public function delete() + public function delete(): bool { - foreach (self::VARIANT_2_SYM_PATH_FIELD as $variant => $field) { - if ($this->$field != '') { - $path = Storage::drive('symbolic')->path($this->$field); - try { - unlink($path); - } catch (Exception $e) { - Logs::error(__METHOD__, __LINE__, 'could not unlink ' . $path); - } - } - } + // Laravel and Flysystem does not support symbolic links. + // So we must convert it to a local file + $flyFile = new FlysystemFile(Storage::disk(self::DISK_NAME), $this->short_path); + $symLink = $flyFile->toLocalFile(); + $symLink->delete(); - return parent::delete(); + return $this->internalDelete(); } } diff --git a/app/Models/TagAlbum.php b/app/Models/TagAlbum.php new file mode 100644 index 00000000000..7493a0903fd --- /dev/null +++ b/app/Models/TagAlbum.php @@ -0,0 +1,155 @@ + $shared_with + * @property int|null $shared_with_count + * @property Collection $access_permissions + * @property int|null $access_permissions_count + * @property AccessPermission|null $current_user_permissions + * @property AccessPermission|null $public_permissions + * @property Collection $shared_with + * + * @method static TagAlbumBuilder|TagAlbum addSelect($column) + * @method static TagAlbumBuilder|TagAlbum join(string $table, string $first, string $operator = null, string $second = null, string $type = 'inner', string $where = false) + * @method static TagAlbumBuilder|TagAlbum joinSub($query, $as, $first, $operator = null, $second = null, $type = 'inner', $where = false) + * @method static TagAlbumBuilder|TagAlbum leftJoin(string $table, string $first, string $operator = null, string $second = null) + * @method static TagAlbumBuilder|TagAlbum newModelQuery() + * @method static TagAlbumBuilder|TagAlbum newQuery() + * @method static TagAlbumBuilder|TagAlbum orderBy($column, $direction = 'asc') + * @method static TagAlbumBuilder|TagAlbum select($columns = []) + * @method static TagAlbumBuilder|TagAlbum whereId($value) + * @method static TagAlbumBuilder|TagAlbum whereIn(string $column, string $values, string $boolean = 'and', string $not = false) + * @method static TagAlbumBuilder|TagAlbum whereNotIn(string $column, string $values, string $boolean = 'and') + * @method static TagAlbumBuilder|TagAlbum whereShowTags($value) + * + * @mixin \Eloquent + */ +class TagAlbum extends BaseAlbum +{ + use ToArrayThrowsNotImplemented; + /** @phpstan-use HasFactory<\Database\Factories\TagAlbumFactory> */ + use HasFactory; + + /** + * The model's attributes. + * + * We must list all attributes explicitly here, otherwise the attributes + * of a new model will accidentally be set on the parent class. + * The trait {@link \App\Models\Extensions\ForwardsToParentImplementation} + * only works properly, if it knows which attributes belong to the parent + * class and which attributes belong to the child class. + * + * @var array + */ + protected $attributes = [ + 'id' => null, + 'show_tags' => null, + ]; + + /** + * @var array + */ + protected $casts = [ + 'min_taken_at' => 'datetime', + 'max_taken_at' => 'datetime', + 'show_tags' => ArrayCast::class, + ]; + + /** + * @var array The list of attributes which exist as columns of the DB + * relation but shall not be serialized to JSON + */ + protected $hidden = [ + 'base_class', // don't serialize base class as a relation, the attributes of the base class are flatly merged into the JSON result + ]; + + /** + * @var array The list of "virtual" attributes which do not exist as + * columns of the DB relation but which shall be appended to + * JSON from accessors + */ + protected $appends = [ + 'thumb', + ]; + + public function photos(): HasManyPhotosByTag // @phpstan-ignore-line + { + return new HasManyPhotosByTag($this); + } + + /** + * Returns the value for the virtual attribute {@link TagAlbum::$thumb}. + * + * Note, opposed to {@link Album} the thumbnail of a tag album cannot be + * converted into a proper relation (cp. {@link Album::thumb()}). + * However, doing so would enable to eagerly load all thumbs of all + * tag albums at once (using a single query) and cache the result. + * This would speed up rendering the root album. + * The main obstacle is the way how tags of photos and tags of albums + * are matched to each other. + * At the moment this requires string operations on the PHP level and + * the SQL query for each tag album has an individual number of + * `WHERE`-clauses which is specific for the particular + * tag album (cp. {@link HasManyPhotosByTag::addEagerConstraints()}). + * Hence, it is not possible to construct a single SQL query which fetches + * the photos for multiple tag albums. + * However, this would be possible if we had a proper `tags` table and + * two n:m-relations between photos and tags and tags and albums. + * This would allow to create a single `JOIN`-query for all tag albums. + * + * @return Thumb|null + * + * @throws InvalidPropertyException + */ + protected function getThumbAttribute(): ?Thumb + { + // Note, `photos()` already applies a "security filter" and + // only returns photos which are accessible by the current + // user + return Thumb::createFromQueryable( + $this->photos(), + $this->getEffectivePhotoSorting() + ); + } + + /** + * Create a new Eloquent query builder for the model. + * + * @param BaseBuilder $query + * + * @return TagAlbumBuilder + */ + public function newEloquentBuilder($query): TagAlbumBuilder + { + return new TagAlbumBuilder($query); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 49e1d3734cf..0ed0b28e3cc 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -1,58 +1,102 @@ $albums + * @property Collection $oauthCredentials * @property DatabaseNotificationCollection|DatabaseNotification[] $notifications - * @property Collection|Album[] $shared + * @property Collection $shared + * @property Collection $photos + * @property int|null $photos_count + * @property Collection $webAuthnCredentials + * @property int|null $web_authn_credentials_count * - * @method static Builder|User newModelQuery() - * @method static Builder|User newQuery() - * @method static Builder|User query() - * @method static Builder|User whereCreatedAt($value) - * @method static Builder|User whereId($value) - * @method static Builder|User whereLock($value) - * @method static Builder|User wherePassword($value) - * @method static Builder|User whereRememberToken($value) - * @method static Builder|User whereUpdatedAt($value) - * @method static Builder|User whereUpload($value) - * @method static Builder|User whereUsername($value) - * @mixin Eloquent + * @method static UserBuilder|User addSelect($column) + * @method static UserBuilder|User join(string $table, string $first, string $operator = null, string $second = null, string $type = 'inner', string $where = false) + * @method static UserBuilder|User joinSub($query, $as, $first, $operator = null, $second = null, $type = 'inner', $where = false) + * @method static UserBuilder|User leftJoin(string $table, string $first, string $operator = null, string $second = null) + * @method static UserBuilder|User newModelQuery() + * @method static UserBuilder|User newQuery() + * @method static UserBuilder|User orderBy($column, $direction = 'asc') + * @method static UserBuilder|User query() + * @method static UserBuilder|User select($columns = []) + * @method static UserBuilder|User whereCreatedAt($value) + * @method static UserBuilder|User whereEmail($value) + * @method static UserBuilder|User whereId($value) + * @method static UserBuilder|User whereIn(string $column, string $values, string $boolean = 'and', string $not = false) + * @method static UserBuilder|User whereMayAdministrate($value) + * @method static UserBuilder|User whereMayEditOwnSettings($value) + * @method static UserBuilder|User whereMayUpload($value) + * @method static UserBuilder|User whereNotIn(string $column, string $values, string $boolean = 'and') + * @method static UserBuilder|User wherePassword($value) + * @method static UserBuilder|User whereRememberToken($value) + * @method static UserBuilder|User whereToken($value) + * @method static UserBuilder|User whereUpdatedAt($value) + * @method static UserBuilder|User whereUsername($value) + * + * @mixin \Eloquent */ class User extends Authenticatable implements WebAuthnAuthenticatable { + /** @phpstan-use HasFactory<\Database\Factories\UserFactory> */ + use HasFactory; use Notifiable; use WebAuthnAuthentication; use UTCBasedTimes; + use ThrowsConsistentExceptions { + delete as parentDelete; + } + use ToArrayThrowsNotImplemented; /** - * The attributes that are mass assignable. + * @var array the attributes that are mass assignable */ protected $fillable = [ 'username', @@ -61,59 +105,141 @@ class User extends Authenticatable implements WebAuthnAuthenticatable ]; /** - * The attributes that should be hidden for arrays. + * @var array */ - protected $hidden = [ - 'password', - 'remember_token', - 'created_at', - 'updated_at', - ]; - protected $casts = [ - 'upload' => 'int', - 'lock' => 'int', + 'id' => 'integer', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'may_administrate' => 'boolean', + 'may_upload' => 'boolean', + 'may_edit_own_settings' => 'boolean', + 'quota_kb' => 'integer', ]; + protected $hidden = []; + + /** + * Create a new Eloquent query builder for the model. + * + * @param BaseBuilder $query + * + * @return UserBuilder + */ + public function newEloquentBuilder($query): UserBuilder + { + return new UserBuilder($query); + } + /** * Return the albums owned by the user. * - * @return HasMany + * @return HasMany */ - public function albums() + public function albums(): HasMany { - return $this->hasMany('App\Models\Album', 'owner_id', 'id'); + /** @phpstan-ignore-next-line */ + return $this->hasMany(BaseAlbumImpl::class, 'owner_id', 'id'); } /** - * Return the albums shared to the user. + * Return the photos owned by the user. * - * @return BelongsToMany + * @return HasMany */ - public function shared() + public function photos(): HasMany { - return $this->belongsToMany('App\Models\Album', 'user_album', 'user_id', 'album_id'); + return $this->hasMany(Photo::class, 'owner_id', 'id'); } - public function is_admin(): bool + /** + * Return the albums shared to the user. + * + * @return BelongsToMany + */ + public function shared(): BelongsToMany { - return $this->id == 0; + return $this->belongsToMany( + BaseAlbumImpl::class, + APC::ACCESS_PERMISSIONS, + APC::USER_ID, + APC::BASE_ALBUM_ID + ); } - public function can_upload(): bool + /** + * Return the Oauth credentials owned by the user. + * + * @return HasMany + */ + public function oauthCredentials(): HasMany { - return $this->id == 0 || $this->upload; + return $this->hasMany(OauthCredential::class, 'user_id', 'id'); } - // ! Used by Larapass + /** + * Used by Larapass. + * + * @return string + */ public function username(): string { - return utf8_encode($this->username); + // @phpstan-ignore-next-line This is temporary and should hopefully be fixed soon by Safe with proper type hinting. + return mb_convert_encoding($this->username, 'UTF-8'); } - // ! Used by Larapass - public function name(): string + /** + * Used by Larapass since 2022-09-21. + * + * @return string + */ + public function getNameAttribute(): string { - return ($this->id == 0) ? 'Admin' : $this->username; + // If strings starts by '$2y$', it is very likely that it's a blowfish hash. + return substr($this->username, 0, 4) === '$2y$' ? 'Admin' : $this->username; + } + + /** + * Deletes a user from the DB and re-assigns ownership of albums and photos + * to the currently authenticated user. + * + * For efficiency reasons the methods performs a mass-update without + * hydrating the actual models. + * + * @return bool always true + * + * @throws ModelDBException + * @throws InvalidFormatException + * @throws UnauthenticatedException + */ + public function delete(): bool + { + /** @var HasMany[] $ownershipRelations */ + $ownershipRelations = [$this->photos(), $this->albums()]; + $hasAny = false; + + foreach ($ownershipRelations as $relation) { + $hasAny = $hasAny || $relation->count() > 0; + } + + if ($hasAny) { + // only try update relations if there are any to allow deleting users from migrations (relations are moved before deleting) + $now = Carbon::now(); + $newOwnerID = Auth::id() ?? throw new UnauthenticatedException(); + + foreach ($ownershipRelations as $relation) { + // We must also update the `updated_at` column of the related + // models in case clients have cached these models. + $relation->update([ + $relation->getForeignKeyName() => $newOwnerID, + $relation->getRelated()->getUpdatedAtColumn() => $relation->getRelated()->fromDateTime($now), + ]); + } + } + + AccessPermission::query()->where(APC::USER_ID, '=', $this->id)->delete(); + WebAuthnCredential::query()->where('authenticatable_id', '=', $this->id)->delete(); + + return $this->parentDelete(); } } diff --git a/app/Notifications/PhotoAdded.php b/app/Notifications/PhotoAdded.php index 32fe4c08f47..1b36c00eede 100644 --- a/app/Notifications/PhotoAdded.php +++ b/app/Notifications/PhotoAdded.php @@ -1,5 +1,11 @@ */ public function via($notifiable) { @@ -37,7 +45,7 @@ public function via($notifiable) * * @param mixed $notifiable * - * @return array + * @return array */ public function toArray($notifiable) { diff --git a/app/Policies/AlbumPolicy.php b/app/Policies/AlbumPolicy.php new file mode 100644 index 00000000000..34d442a2590 --- /dev/null +++ b/app/Policies/AlbumPolicy.php @@ -0,0 +1,578 @@ +owner_id === $user->id; + } + + /** + * Checks whether the currentuser can see said album. + * + * Note, at the moment this check is only needed for built-in smart + * albums. + * Hence, the method is only provided for them. + * + * @param User|null $user + * @param BaseSmartAlbum $smartAlbum + * + * @return bool true, if the album is visible + */ + public function canSee(?User $user, BaseSmartAlbum $smartAlbum): bool + { + return ($user?->may_upload === true) || + $smartAlbum->public_permissions() !== null; + } + + /** + * Checks whether current user can access the album. + * + * A real albums (i.e. albums that are stored in the DB) is called + * _accessible_ if the current user is allowed to browse into it, i.e. if + * the current user may open it and see its content. + * An album is _accessible_ if any of the following conditions hold + * (OR-clause) + * + * - the user is an admin + * - the user is the owner of the album + * - the album is shared with the user + * - the album is public AND no password is set + * - the album is public AND has been unlocked + * + * In other cases, the following holds: + * - the root album is accessible by everybody + * - the built-in smart albums are accessible, if + * - the user is authenticated and is granted the right of uploading, or + * - the album is public + * + * @param User|null $user + * @param AbstractAlbum|null $album + * + * @return bool + */ + public function canAccess(?User $user, ?AbstractAlbum $album): bool + { + if ($album === null) { + return true; + } + + if (!$album instanceof BaseAlbum) { + /** @var BaseSmartAlbum $album */ + return $this->canSee($user, $album); + } + + if ($this->isOwner($user, $album)) { + return true; + } + + if ($album->current_user_permissions() !== null) { + return true; + } + + if ( + $album->public_permissions() !== null && + ($album->public_permissions()->password === null || + $this->isUnlocked($album)) + ) { + return true; + } + + return false; + } + + /** + * Check if user can access the map. + * Note that this is not used to determine the visibility of the header button because + * 1. Admin will always return true. + * 2. We also check if there are pictures with location data to be display in the album. + * + * @param User|null $user + * @param AbstractAlbum|null $album + * + * @return bool + */ + public function canAccessMap(?User $user, ?AbstractAlbum $album): bool + { + if (!Configs::getValueAsBool('map_display')) { + return false; + } + + if ($user === null && !Configs::getValueAsBool('map_display_public')) { + return false; + } + + return $this->canAccess($user, $album); + } + + /** + * Check if current user can download the album. + * + * @param User|null $user + * @param AbstractAlbum|null $abstractAlbum + * + * @return bool + * + * @throws ConfigurationKeyMissingException + */ + public function canDownload(?User $user, ?AbstractAlbum $abstractAlbum): bool + { + // The root album always uses the global setting + if ($abstractAlbum === null) { + return Configs::getValueAsBool('grants_download'); + } + + // User is logged in + // Or User can download. + if (!$abstractAlbum instanceof BaseAlbum) { + return $user !== null || $abstractAlbum->public_permissions()?->grants_download === true; + } + + return $this->isOwner($user, $abstractAlbum) || + $abstractAlbum->current_user_permissions()?->grants_download === true || + $abstractAlbum->public_permissions()?->grants_download === true; + } + + /** + * Check if user is allowed to upload in current albumn. + * + * @param User $user + * @param AbstractAlbum|null $abstractAlbum + * + * @return bool + * + * @throws ConfigurationKeyMissingException + */ + public function canUpload(User $user, ?AbstractAlbum $abstractAlbum = null): bool + { + // The upload right on the root album is directly determined by the user's capabilities. + if ($abstractAlbum === null || !$abstractAlbum instanceof BaseAlbum) { + return $user->may_upload; + } + + return $this->isOwner($user, $abstractAlbum) || + $abstractAlbum->current_user_permissions()?->grants_upload === true || + $abstractAlbum->public_permissions()?->grants_upload === true; + } + + /** + * Checks whether the album is editable by the current user. + * + * An album is called _editable_ if the current user is allowed to edit + * the album's properties. + * This also covers adding new photos to an album. + * An album is _editable_ if any of the following conditions hold + * (OR-clause) + * + * - the user is an admin + * - the user has the upload privilege and is the owner of the album + * + * Note about built-in smart albums: + * The built-in smart albums (starred, public, recent, unsorted) do not + * have any editable properties. + * Hence, it is pointless whether a smart album is editable or not. + * In order to silently ignore/skip this condition for smart albums, + * this method always returns `true` for a smart album. + * + * @param User $user + * @param AbstractAlbum|null $album the album; `null` designates the root album + * + * @return bool + */ + public function canEdit(User $user, AbstractAlbum|null $album): bool + { + // The root album and smart albums get a pass + if ($album === null || $album instanceof BaseSmartAlbum) { + return $user->may_upload; + } + + if ($album instanceof BaseAlbum) { + return ($this->isOwner($user, $album) && $user->may_upload) || + $album->current_user_permissions()?->grants_edit === true || + $album->public_permissions()?->grants_edit === true; + } + + return false; + } + + /** + * Check if user is allowed to USE delete in current albumn. + * + * @param User $user + * @param AbstractAlbum|null $abstractAlbum + * + * @return bool + * + * @throws ConfigurationKeyMissingException + */ + public function canDelete(User $user, ?AbstractAlbum $abstractAlbum = null): bool + { + if ($abstractAlbum instanceof BaseSmartAlbum) { + return $user->may_upload; + } + + if (!$abstractAlbum instanceof Album) { + return $user->may_upload; + } + + if ($this->isOwner($user, $abstractAlbum)) { + return true; + } + + /** @var Album $abstractAlbum */ + if ( + AccessPermission::query() + ->where(APC::BASE_ALBUM_ID, '=', $abstractAlbum->parent_id) + ->where(APC::USER_ID, '=', $user->id) + ->where(APC::GRANTS_DELETE, '=', true) + ->count() === 1 + ) { + return true; + } + + return false; + } + + /** + * Check if user is allowed to USE transfer in current album. + * + * @param User $user + * @param AbstractAlbum|null $baseAlbum + * + * @return bool + * + * @throws ConfigurationKeyMissingException + */ + public function canTransfer(User $user, ?AbstractAlbum $baseAlbum = null): bool + { + if (!$baseAlbum instanceof BaseAlbum) { + return false; + } + + return $this->isOwner($user, $baseAlbum); + } + + /** + * Checks whether the album-user has the full photo access. + * + * @param User|null $user + * @param AbstractAlbum|null $abstractAlbum + * + * @return bool + */ + public function canAccessFullPhoto(?User $user, ?AbstractAlbum $abstractAlbum): bool + { + if ($abstractAlbum === null || $abstractAlbum instanceof BaseSmartAlbum) { + return Configs::getValueAsBool('grants_full_photo_access'); + } + + /** @var BaseAlbum $abstractAlbum */ + if ($this->isOwner($user, $abstractAlbum)) { + return true; + } + + return $abstractAlbum->public_permissions()?->grants_full_photo_access === true || + $abstractAlbum->current_user_permissions()?->grants_full_photo_access === true; + } + + /** + * Checks whether the designated albums are editable by the current user. + * + * See {@link AlbumQueryPolicy::isEditable()} for the definition + * when an album is editable. + * + * This method is mostly only useful during deletion of albums, when no + * album models are loaded for efficiency reasons. + * If an album model is required anyway (because it shall be edited), + * then first load the album once and use + * {@link AlbumQueryPolicy::isEditable()} + * instead in order to avoid several DB requests. + * + * @param User $user + * @param array $albumIDs + * + * @return bool + * + * @throws QueryBuilderException + */ + public function canEditById(User $user, array $albumIDs): bool + { + $albumIDs = $this->uniquify($albumIDs); + $num_albums = count($albumIDs); + + if ($num_albums === 0) { + return $user->may_upload; + } + + if ( + BaseAlbumImpl::query() + ->whereIn('id', $albumIDs) + ->where('owner_id', '=', $user->id) + ->count() === $num_albums + ) { + return $user->may_upload; + } + + if ( + AccessPermission::query() + ->whereIn(APC::BASE_ALBUM_ID, $albumIDs) + ->where(APC::USER_ID, '=', $user->id) + ->where(APC::GRANTS_EDIT, '=', true) + ->count() === $num_albums + ) { + return true; + } + + return false; + } + + /** + * Checks whether the designated albums are editable by the current user. + * + * See {@link AlbumQueryPolicy::isEditable()} for the definition + * when an album is editable. + * + * This method is mostly only useful during deletion of albums, when no + * album models are loaded for efficiency reasons. + * If an album model is required anyway (because it shall be edited), + * then first load the album once and use + * {@link AlbumQueryPolicy::isEditable()} + * instead in order to avoid several DB requests. + * + * @param User $user + * @param array $albumIDs + * + * @return bool + * + * @throws QueryBuilderException + */ + public function canDeleteById(User $user, array $albumIDs): bool + { + $albumIDs = $this->uniquify($albumIDs); + $num_albums = count($albumIDs); + + if ($num_albums === 0) { + return $user->may_upload; + } + + if ( + BaseAlbumImpl::query() + ->whereIn('id', $albumIDs) + ->where('owner_id', '=', $user->id) + ->count() === $num_albums + ) { + return $user->may_upload; + } + + if ( + AccessPermission::query() + ->whereIn(APC::BASE_ALBUM_ID, $albumIDs) + ->where(APC::USER_ID, '=', $user->id) + ->where(APC::GRANTS_DELETE, '=', true) + ->count() === $num_albums + ) { + return true; + } + + return false; + } + + /** + * Check if user can share selected album with to public. + * + * @param User $user + * @param ?AbstractAlbum $abstractAlbum + * + * @return bool + */ + public function canShare(?User $user, ?AbstractAlbum $abstractAlbum): bool + { + // should not be the case, but well. + if ($abstractAlbum === null) { + return true; + } + + if (Configs::getValueAsBool('share_button_visible')) { + return true; + } + + if (!$abstractAlbum instanceof BaseAlbum) { + return false; + } + + return $this->isOwner($user, $abstractAlbum); + } + + /** + * Check if user can share selected album with another user. + * + * @param User $user + * @param AbstractAlbum|BaseAlbumImpl|null $abstractAlbum + * + * @return bool + */ + public function canShareWithUsers(User $user, AbstractAlbum|BaseAlbumImpl|null $abstractAlbum): bool + { + if ($user->may_upload !== true) { + return false; + } + + // If this is null, this means that we are looking at the list. + if ($abstractAlbum === null) { + return true; + } + + if (!$abstractAlbum instanceof BaseAlbum && !$abstractAlbum instanceof BaseAlbumImpl) { + return false; + } + + return $this->isOwner($user, $abstractAlbum); + } + + /** + * Check if user can share selected albums with other users. + * Only owner can share. + * + * @param User $user + * @param array $albumIDs + * + * @return bool + * + * @throws ConfigurationKeyMissingException + */ + public function canShareById(User $user, array $albumIDs): bool + { + if (!$user->may_upload) { + return false; + } + + $albumIDs = $this->uniquify($albumIDs); + $num_albums = count($albumIDs); + + if ($num_albums === 0) { + return false; + } + + if ( + BaseAlbumImpl::query() + ->whereIn('id', $albumIDs) + ->where('owner_id', '=', $user->id) + ->count() === $num_albums + ) { + return true; + } + + return false; + } + + /** + * Check whether user can import from server. + * Only Admin can do that. + * + * @param User|null $user + * + * @return bool + */ + public function canImportFromServer(?User $user): bool + { + return false; + } + + // The following methods are not to be called by Gate. + + /** + * Pushes an album onto the stack of unlocked albums. + * + * @param BaseAlbum|BaseAlbumImpl $album + */ + public function unlock(BaseAlbum|BaseAlbumImpl $album): void + { + Session::push(AlbumPolicy::UNLOCKED_ALBUMS_SESSION_KEY, $album->id); + } + + /** + * Check whether the given album has previously been unlocked. + * + * @param BaseAlbum|BaseAlbumImpl $album + * + * @return bool + */ + public function isUnlocked(BaseAlbum|BaseAlbumImpl $album): bool + { + return in_array($album->id, self::getUnlockedAlbumIDs(), true); + } + + /** + * @return string[] + */ + public static function getUnlockedAlbumIDs(): array + { + return Session::get(self::UNLOCKED_ALBUMS_SESSION_KEY, []); + } + + /** + * Remove root and smart albums, as they get a pass. + * Make IDs unique as otherwise count will fail. + * + * @param array $albumIDs + * + * @return array + */ + private function uniquify(array $albumIDs): array + { + return array_diff( + array_unique($albumIDs), + array_keys(SmartAlbumType::values()), + [null] + ); + } +} diff --git a/app/Policies/AlbumQueryPolicy.php b/app/Policies/AlbumQueryPolicy.php new file mode 100644 index 00000000000..be273473742 --- /dev/null +++ b/app/Policies/AlbumQueryPolicy.php @@ -0,0 +1,658 @@ +|FixedQueryBuilder $query + * + * @return AlbumBuilder|FixedQueryBuilder|FixedQueryBuilder|TagAlbumBuilder + * + * @throws InternalLycheeException + */ + public function applyVisibilityFilter(AlbumBuilder|FixedQueryBuilder $query): AlbumBuilder|TagAlbumBuilder|FixedQueryBuilder + { + $this->prepareModelQueryOrFail($query); + + if (Auth::user()?->may_administrate === true) { + return $query; + } + + $userID = Auth::id(); + + // We must wrap everything into an outer query to avoid any undesired + // effects in case that the original query already contains an + // "OR"-clause. + // The sub-query only uses properties (i.e. columns) which are + // defined on the common base model for all albums. + $visibilitySubQuery = function (AlbumBuilder|TagAlbumBuilder $query2) use ($userID) { + $query2 + // We laverage that IS_LINK_REQUIRED is NULL if the album is NOT shared publically (left join). + ->where(APC::COMPUTED_ACCESS_PERMISSIONS . '.' . APC::IS_LINK_REQUIRED, '=', false) + // Current user is the owner of the album + // This is the case when is_link_required is NULL + ->when( + $userID !== null, + fn ($q) => $q->orWhere('base_albums.owner_id', '=', $userID) + ); + }; + + return $query->where($visibilitySubQuery); + } + + /** + * Adds the conditions of an accessible album to the query. + * + * **Attention:** This method is only meant for internal use by + * this class or {@link PhotoQueryPolicy}. + * + * This method adds the WHERE conditions without any further pre-cautions. + * The method silently assumes that the SELECT clause contains the tables + * + * - **`base_albums`** and + * - **`computed_access_permissions`**. + * + * Moreover, the raw OR-clauses are added. + * They are not wrapped into a nesting braces `()`. + * + * Note this makes use of the fact that when an album is NOT shared nor public, the value of is_link_required is NULL. + * + * @param BaseBuilder $query + * + * @return BaseBuilder + * + * @throws InternalLycheeException + */ + public function appendAccessibilityConditions(BaseBuilder $query): BaseBuilder + { + $unlockedAlbumIDs = AlbumPolicy::getUnlockedAlbumIDs(); + $userID = Auth::id(); + + try { + $query + ->orWhere( + // Album is public/shared (visible or not => IS_LINK_REQUIRED NOT NULL) + // and NOT protected by a password + fn (BaseBuilder $q) => $q + ->whereNotNull(APC::COMPUTED_ACCESS_PERMISSIONS . '.' . APC::IS_LINK_REQUIRED) + ->whereNull(APC::COMPUTED_ACCESS_PERMISSIONS . '.' . APC::PASSWORD) + ) + ->orWhere( + // Album is public/shared (visible or not) and protected by a password and unlocked + fn (BaseBuilder $q) => $q + ->whereNotNull(APC::COMPUTED_ACCESS_PERMISSIONS . '.' . APC::IS_LINK_REQUIRED) + ->whereNotNull(APC::COMPUTED_ACCESS_PERMISSIONS . '.' . APC::PASSWORD) + ->whereIn(APC::COMPUTED_ACCESS_PERMISSIONS . '.' . APC::BASE_ALBUM_ID, $unlockedAlbumIDs) + ) + ->when( + $userID !== null, + // TODO: move the owner to ACCESS PERMISSIONS so that we do not need to join base_album anymore + // Current user is the owner of the album + fn (BaseBuilder $q) => $q->orWhere('base_albums.owner_id', '=', $userID) + ); + + return $query; + } catch (\Throwable $e) { + throw new QueryBuilderException($e); + } + } + + /** + * Restricts an album query to _reachable_ albums. + * + * An album is called _reachable_, if it is _visible_ and _accessible_ simultaneously. + * An album is reachable, if the user is able to see the album + * within its parent album and has the privilege to enter it. + * + * + * The combination of both sets of conditions yields that an album is + * _reachable_, if any of the following conditions hold + * (OR-clause) + * + * - the user is the admin, or + * - the user is the owner, or + * - the album is shared with the user, or + * - the album does not require a direct link, is public and has no password set, or + * - the album does not require a direct link, is public and has been unlocked + * + * @param AlbumBuilder $query + * + * @return AlbumBuilder + * + * @throws QueryBuilderException + * @throws InvalidQueryModelException + */ + public function applyReachabilityFilter(AlbumBuilder $query): AlbumBuilder + { + $this->prepareModelQueryOrFail($query); + + if (Auth::user()?->may_administrate === true) { + return $query; + } + + $unlockedAlbumIDs = AlbumPolicy::getUnlockedAlbumIDs(); + $userID = Auth::id(); + + // We must wrap everything into an outer query to avoid any undesired + // effects in case that the original query already contains an + // "OR"-clause. + // The sub-query only uses properties (i.e. columns) which are + // defined on the common base model for all albums. + $reachabilitySubQuery = function (Builder $query2) use ($unlockedAlbumIDs, $userID) { + $query2 + ->where( + // Album is visible and not password protected. + fn (Builder $q) => $q + ->where(APC::COMPUTED_ACCESS_PERMISSIONS . '.' . APC::IS_LINK_REQUIRED, '=', false) + ->whereNull(APC::COMPUTED_ACCESS_PERMISSIONS . '.' . APC::PASSWORD) + ) + ->orWhere( + // Album is visible and password protected and unlocked + fn (Builder $q) => $q + ->where(APC::COMPUTED_ACCESS_PERMISSIONS . '.' . APC::IS_LINK_REQUIRED, '=', false) + ->whereNotNull(APC::COMPUTED_ACCESS_PERMISSIONS . '.' . APC::PASSWORD) + ->whereIn(APC::COMPUTED_ACCESS_PERMISSIONS . '.' . APC::BASE_ALBUM_ID, $unlockedAlbumIDs) + ) + ->when( + $userID !== null, + // User is owner of the album + fn (Builder $q) => $q->orWhere('base_albums.owner_id', '=', $userID) + ); + }; + + return $query->where($reachabilitySubQuery); + } + + /** + * Restricts an album query to _browsable_ albums. + * + * Intuitively, an album is browsable if users can find a path to the + * album by "clicking around". + * An album is called _browsable_, if + * + * 1. there is a path from the origin to the album, and + * 2. all albums on the path are _reachable_ + * + * See {@link AlbumQueryPolicy::applyReachabilityFilter()} + * for the definition of reachability. + * Note, while _reachability_ (as well as _visibility_ and _accessibility_) + * are a _local_ properties, _browsability_ is a _global_ property. + * + * **Attention**: + * For efficiency reasons this method does not check if `$origin` itself + * is reachable. + * The method simply assumes that the user has already legitimately + * accessed the origin album, if the caller provides an album model. + * + * Due to constraints in the SQL syntax, the query actually checks that + * + * 1. there is a path from the origin to the album, and + * 2. no album on that path is unreachable + * + * Note that the worst case efficiency of this query is O(n²), if n is + * the number of query results. + * The query does not "know" that albums are organized in a tree structure + * and thus re-examines the entire path for each album in the result and + * does not take a short-cut for sub-paths which have already been examined + * previously. + * In other words for a flat tree (all result nodes are direct children + * of the origin), the runtime is O(n), but for a high tree (the nodes are + * basically a sequence), the runtime is O(n²). + * + * @param AlbumBuilder $query the album query which shall be restricted + * + * @return AlbumBuilder the restricted album query + * + * @throws InternalLycheeException + */ + public function applyBrowsabilityFilter(AlbumBuilder $query): AlbumBuilder + { + $table = $query->getQuery()->from; + if (!($query->getModel() instanceof Album) || $table !== 'albums') { + throw new LycheeInvalidArgumentException('the given query does not query for albums'); + } + + if (Auth::user()?->may_administrate === true) { + return $query; + } + + // Ensures that only those albums of the original query are + // returned for which a path from the origin to the album exist + // such that there are no blocked albums on the path to the album. + return $query->whereNotExists(function (BaseBuilder $q) { + $this->appendUnreachableAlbumsCondition( + $q, + null, + null, + ); + }); + } + + /** + * Adds the conditions of an unreachable album to the query. + * + * An album is called _unreachable_, if it is + * - _invisible_ + * - or not _accessible_ + * + * It is the opposite of "reachable", if the user is not able to see the album + * within its parent album or does not have the privilege to enter it. + * + * **Attention:** This method is only meant for internal use by + * this class or {@link PhotoQueryPolicy}. + * Use {@link AlbumQueryPolicy::applyBrowsabilityFilter()} + * if called from other places instead. + * + * This method adds the WHERE conditions without any further pre-cautions. + * The method silently assumes that the passed query builder is used + * within an outer query whose SELECT clause contains the table + * + * - **`albums`**. + * + * Moreover, the raw clauses are added. + * They are not wrapped into a nesting braces `()`. + * + * @param BaseBuilder $builder the album query which shall be + * restricted + * @param int|string|null $originLeft optionally constraints the search + * base; an integer value is + * interpreted a raw left bound of the + * search base; a string value is + * interpreted as a reference to a + * column which shall be used as a + * left bound + * @param int|string|null $originRight like `$originLeft` but for the + * right bound + * + * @return BaseBuilder + * + * @throws InternalLycheeException + */ + public function appendUnreachableAlbumsCondition(BaseBuilder $builder, int|string|null $originLeft, int|string|null $originRight): BaseBuilder + { + if (gettype($originLeft) !== gettype($originRight)) { + throw new LycheeInvalidArgumentException('$originLeft and $originRight must simultaneously either be integers, strings or null'); + } + + $unlockedAlbumIDs = AlbumPolicy::getUnlockedAlbumIDs(); + $userID = Auth::id(); + + try { + // There are inner albums ... + $builder + ->from('albums', 'inner') + ->when( + Auth::check(), + fn ($q) => $this->joinBaseAlbumOwnerId($q, 'inner.id', 'inner_', false) + ); + + // WE MUST JOIN LEFT HERE + $this->joinSubComputedAccessPermissions($builder, 'inner.id', 'left', 'inner_'); + + // ... on the path from the origin ... + if (is_int($originLeft)) { + // (We must exclude the origin as an inner node + // because the origin might have set "require_link", but + // we do not care, because the user has already got + // somehow into the origin) + $builder + ->where('inner._lft', '>', $originLeft) + ->where('inner._rgt', '<', $originRight); + } elseif (is_string($originLeft) && is_string($originRight)) { + $builder + ->whereColumn('inner._lft', '>', $originLeft) + ->whereColumn('inner._rgt', '<', $originRight); + } + // ... to the target ... + $builder + // (We must include the target into the list of inner nodes, + // because we must also check whether the target is unreachable.) + ->whereColumn('inner._lft', '<=', 'albums._lft') + ->whereColumn('inner._rgt', '>=', 'albums._rgt'); + // ... which are unreachable. + + /** + * | Link required <> false | Password required = true + * -----------------------+------------------------+-------------------------- + * Link required <> false | Not reachable ✓ | Not reachable ✓ + * Id not Unlocked | Not reachable ✓ | Not reachable ✓. + */ + $builder + ->where( + fn (BaseBuilder $q) => $q + ->where('inner_' . APC::COMPUTED_ACCESS_PERMISSIONS . '.' . APC::IS_LINK_REQUIRED, '=', true) + ->orWhereNull('inner_' . APC::COMPUTED_ACCESS_PERMISSIONS . '.' . APC::IS_LINK_REQUIRED) + ->orWhereNotNull('inner_' . APC::COMPUTED_ACCESS_PERMISSIONS . '.' . APC::PASSWORD) + ) + ->where( + fn (BaseBuilder $q) => $q + ->where('inner_' . APC::COMPUTED_ACCESS_PERMISSIONS . '.' . APC::IS_LINK_REQUIRED, '=', true) + ->orWhereNull('inner_' . APC::COMPUTED_ACCESS_PERMISSIONS . '.' . APC::IS_LINK_REQUIRED) + ->orWhereNotIn('inner_' . APC::COMPUTED_ACCESS_PERMISSIONS . '.' . APC::BASE_ALBUM_ID, $unlockedAlbumIDs) + ) + ->when( + $userID !== null, + fn (BaseBuilder $q) => $q + ->where('inner_base_albums.owner_id', '<>', $userID) + ); + + return $builder; + } catch (\InvalidArgumentException $e) { + throw new QueryBuilderException($e); + } + } + + /** + * Adds the conditions of a sensitive album by recursion to the query. + * + * An album is called _recursive sensitive_, if it is marked as sensitive or contains a sensitive parent. + * + * **Attention:** This method is only meant for internal use by + * this class or {@link PhotoQueryPolicy}. + * + * This method adds the WHERE conditions without any further pre-cautions. + * The method silently assumes that the passed query builder is used + * within an outer query whose SELECT clause contains the table + * + * - **`albums`**. + * + * Moreover, the raw clauses are added. + * They are not wrapped into a nesting braces `()`. + * + * @param BaseBuilder $builder the album query which shall be + * restricted + * @param int|string|null $originLeft optionally constrains the search + * base; an integer value is + * interpreted a raw left bound of the + * search base; a string value is + * interpreted as a reference to a + * column which shall be used as a + * left bound + * @param int|string|null $originRight like `$originLeft` but for the + * right bound + * + * @return BaseBuilder + * + * @throws InternalLycheeException + */ + public function appendRecursiveSensitiveAlbumsCondition(BaseBuilder $builder, int|string|null $originLeft, int|string|null $originRight): BaseBuilder + { + if (gettype($originLeft) !== gettype($originRight)) { + throw new LycheeInvalidArgumentException('$originLeft and $originRight must simultaneously either be integers, strings or null'); + } + + try { + // There are outers albums ... + // WE MUST JOIN LEFT HERE + $builder->from('albums', 'outers'); + $this->joinBaseAlbumSensitive($builder, 'outers.id', 'outers_'); + + // ... on the path from the origin ... + if (is_int($originLeft)) { + // (We must exclude the origin as an outer node + // because the origin might have set as is_nsfw, but + // we do not care, because the user has already got + // somehow into the origin) + $builder + ->where('outers._lft', '>', $originLeft) + ->where('outers._rgt', '<', $originRight); + } elseif (is_string($originLeft) && is_string($originRight)) { + $builder + ->whereColumn('outers._lft', '>', $originLeft) + ->whereColumn('outers._rgt', '<', $originRight); + } + + // ... to the target ... + $builder + // (We must include the target into the list of outer nodes, + // because we must also check whether the target is unreachable.) + ->whereColumn('outers._lft', '<=', 'albums._lft') + ->whereColumn('outers._rgt', '>=', 'albums._rgt'); + // ... which are unreachable. + + $builder->where('outers_base_albums.is_nsfw', '=', true); + + return $builder; + } catch (\InvalidArgumentException $e) { + throw new QueryBuilderException($e); + } + } + + /** + * Throws an exception if the given query does not query for an album. + * + * @param AlbumBuilder|FixedQueryBuilder|FixedQueryBuilder $query + * + * @throws QueryBuilderException + * @throws InvalidQueryModelException + */ + private function prepareModelQueryOrFail(AlbumBuilder|FixedQueryBuilder $query): void + { + $model = $query->getModel(); + $table = $query->getQuery()->from; + if ( + !($model instanceof Album || + $model instanceof TagAlbum || + $model instanceof BaseAlbumImpl + ) || + $table !== $model->getTable() + ) { + throw new InvalidQueryModelException('album'); + } + + // Ensure that only columns of the targeted model are selected, + // if no specific columns are yet set. + // Otherwise, we cannot add a JOIN clause below + // without accidentally adding all columns of the join, too. + $baseQuery = $query->getQuery(); + if ($baseQuery->columns === null || count($baseQuery->columns) === 0) { + $query->select([$table . '.*']); + } + + // We MUST do a full join because we are also sorting on created_at, title and description. + // Those are stored in the base_albums. + if ($model instanceof Album || $model instanceof TagAlbum) { + $this->joinBaseAlbumOwnerId($query, $table . '.id'); + } + + // We MUST use left here because otherwise we are preventing any non shared album to be visible + $this->joinSubComputedAccessPermissions($query, $table . '.id', 'left'); + } + + /** + * Generate the computed property for the possibly logged-in user. + * + * This produces a sub table with base_album_id where we compute: + * - base_album_id so that we can link those computed property to the base_album table. + * - is_link_required => MIN as we want to ensure that a logged in user can see the shared album + * - grants_full_photo_access => MAX as the public setting takes priority + * - grants_download => MAX as the public setting takes priority + * - grants_upload => MAX as the shared setting takes priority + * - grants_edit => MAX as the shared setting takes priority + * - grants_delete => MAX as the shared setting takes priority + * + * @return BaseBuilder + */ + private function getComputedAccessPermissionSubQuery(bool $full = false): BaseBuilder + { + $select = [ + APC::BASE_ALBUM_ID, + APC::IS_LINK_REQUIRED, + APC::PASSWORD, + ]; + + if ($full) { + $select[] = APC::GRANTS_DELETE; + $select[] = APC::GRANTS_EDIT; + $select[] = APC::GRANTS_DOWNLOAD; + $select[] = APC::GRANTS_FULL_PHOTO_ACCESS; + $select[] = APC::GRANTS_UPLOAD; + $select[] = APC::USER_ID; + } + $userId = Auth::id(); + + return DB::table('access_permissions', APC::COMPUTED_ACCESS_PERMISSIONS)->select($select) + ->when( + Auth::check(), + fn ($q1) => $q1 + ->where(APC::USER_ID, '=', $userId) + ->orWhere( + fn ($q2) => $q2->whereNull(APC::USER_ID) + ->whereNotIn( + APC::COMPUTED_ACCESS_PERMISSIONS . '.' . APC::BASE_ALBUM_ID, + fn ($q3) => $q3->select('acc_per.' . APC::BASE_ALBUM_ID) + ->from('access_permissions', 'acc_per') + ->where(APC::USER_ID, '=', $userId) + ) + ) + ) + ->when( + !Auth::check(), + fn ($q1) => $q1->whereNull(APC::USER_ID) + ); + } + + /** + * Helper to join the the computed property for the possibly logged-in user. + * + * @param AlbumBuilder|FixedQueryBuilder|FixedQueryBuilder|FixedQueryBuilder<\App\Models\Photo>|BaseBuilder $query query to join to + * @param string $second id to link with + * @param string $prefix prefix in the future queries + * @param string $type left|inner + * @param bool $full Select most columns instead of just restricted + * + * @return void + * + * @throws \InvalidArgumentException + */ + public function joinSubComputedAccessPermissions( + AlbumBuilder|FixedQueryBuilder|BaseBuilder $query, + string $second = 'base_albums.id', + string $type = 'left', + string $prefix = '', + bool $full = false, + ): void { + $query->joinSub( + query: $this->getComputedAccessPermissionSubQuery($full), + as: $prefix . APC::COMPUTED_ACCESS_PERMISSIONS, + first: $prefix . APC::COMPUTED_ACCESS_PERMISSIONS . '.' . APC::BASE_ALBUM_ID, + operator: '=', + second: $second, + type: $type + ); + } + + /** + * Join BaseAlbum for ownership and more. + * This aim to give lighter sub selection to make the queries run faster. + * + * @param AlbumBuilder|FixedQueryBuilder|FixedQueryBuilder|BaseBuilder $query + * @param string $second + * @param string $prefix + * @param bool $full + * + * @return void + * + * @throws \InvalidArgumentException + */ + public function joinBaseAlbumOwnerId( + AlbumBuilder|FixedQueryBuilder|BaseBuilder $query, + string $second = 'inner.id', + string $prefix = '', + bool $full = true, + ): void { + $columns = [ + $prefix . 'base_albums.id', + $prefix . 'base_albums.owner_id', + ]; + + if ($full) { + $columns[] = $prefix . 'base_albums.title'; + $columns[] = $prefix . 'base_albums.created_at'; + $columns[] = $prefix . 'base_albums.description'; + } + + $query->joinSub( + query: DB::table('base_albums', $prefix . 'base_albums') + ->select($columns), + as: $prefix . 'base_albums', + first: $prefix . 'base_albums.id', + operator: '=', + second: $second, + type: 'left' + ); + } + + /** + * Join BaseAlbum for sensitivity only. + * This aim to give lighter sub selection to make the queries run faster. + * + * @param AlbumBuilder|FixedQueryBuilder|FixedQueryBuilder|BaseBuilder $query + * @param string $second + * @param string $prefix + * + * @return void + * + * @throws \InvalidArgumentException + */ + public function joinBaseAlbumSensitive( + AlbumBuilder|FixedQueryBuilder|BaseBuilder $query, + string $second = 'inner.id', + string $prefix = '', + ): void { + $columns = [ + $prefix . 'base_albums.id', + $prefix . 'base_albums.is_nsfw', + ]; + + $query->joinSub( + query: DB::table('base_albums', $prefix . 'base_albums') + ->select($columns), + as: $prefix . 'base_albums', + first: $prefix . 'base_albums.id', + operator: '=', + second: $second, + type: 'left' + ); + } +} diff --git a/app/Policies/BasePolicy.php b/app/Policies/BasePolicy.php new file mode 100644 index 00000000000..be8fa8d6f10 --- /dev/null +++ b/app/Policies/BasePolicy.php @@ -0,0 +1,32 @@ +may_administrate === true) { + return true; + } + } +} \ No newline at end of file diff --git a/app/Policies/PhotoPolicy.php b/app/Policies/PhotoPolicy.php new file mode 100644 index 00000000000..e1951dccc18 --- /dev/null +++ b/app/Policies/PhotoPolicy.php @@ -0,0 +1,228 @@ +albumPolicy = resolve(AlbumPolicy::class); + } catch (BindingResolutionException $e) { + throw new FrameworkException('Laravel\'s provider component', $e); + } + } + + /** + * This ensures that current photo is owned by current user. + * + * @param User|null $user + * @param Photo $photo + * + * @return bool + */ + private function isOwner(?User $user, Photo $photo): bool + { + return $user !== null && $photo->owner_id === $user->id; + } + + /** + * Defines whether the photo is visible to the current user. + * + * @param User|null $user + * @param Photo $photo + * + * @return bool + */ + public function canSee(?User $user, Photo $photo): bool + { + if ($this->isOwner($user, $photo)) { + return true; + } + + return $photo->album !== null && $this->albumPolicy->canAccess($user, $photo->album); + } + + /** + * Checks whether the photo may be downloaded by the current user. + * + * @param User|null $user + * @param Photo $photo + * + * @return bool + */ + public function canDownload(?User $user, Photo $photo): bool + { + if ($this->isOwner($user, $photo)) { + return true; + } + + return $this->canSee($user, $photo) && $this->albumPolicy->canDownload($user, $photo->album); + } + + /** + * Checks whether the photo is editable by the current user. + * + * A photo is called _editable_ if the current user is allowed to edit + * the photo's properties. + * A photo is _editable_ if any of the following conditions hold + * (OR-clause) + * + * - the user is an admin + * - the user is the owner of the photo + * + * @param Photo $photo + * + * @return bool + */ + public function canEdit(User $user, Photo $photo) + { + if ($this->isOwner($user, $photo)) { + return true; + } + + return $this->canSee($user, $photo) && $this->albumPolicy->canEdit($user, $photo->album); + } + + /** + * Checks whether the designated photos are editable by the current user. + * + * @param User $user + * @param string[] $photoIDs + * + * @return bool + * + * @throws QueryBuilderException + */ + public function canEditById(User $user, array $photoIDs): bool + { + // Make IDs unique as otherwise count will fail. + $photoIDs = array_unique($photoIDs); + + if ( + $user->may_upload && + Photo::query() + ->whereIn('id', $photoIDs) + ->where('owner_id', $user->id) + ->count() === count($photoIDs) + ) { + return true; + } + + $parents_id = Photo::query() + ->select('album_id') + ->whereIn('id', $photoIDs) + ->groupBy('album_id') + ->pluck('album_id')->all(); + + return $this->albumPolicy->canEditById($user, $parents_id); + } + + /** + * Checks whether the photo may be seen full resolution by the current user. + * + * @param User|null $user + * @param Photo $photo + * + * @return bool + * + * @throws ConfigurationKeyMissingException + */ + public function canAccessFullPhoto(?User $user, Photo $photo): bool + { + if ($this->isOwner($user, $photo)) { + return true; + } + + if (!$this->canSee($user, $photo)) { + return false; + } + + return $this->albumPolicy->canAccessFullPhoto($user, $photo->album); + } + + /** + * Checks whether the photo is deletable le by the current user. + * + * @param Photo $photo + * + * @return bool + */ + public function canDelete(User $user, Photo $photo) + { + if ($this->isOwner($user, $photo)) { + return true; + } + + return $this->canSee($user, $photo) && $this->albumPolicy->canDelete($user, $photo->album); + } + + /** + * Checks whether the designated photos are deletable by the current user. + * + * @param User $user + * @param string[] $photoIDs + * + * @return bool + * + * @throws QueryBuilderException + */ + public function canDeleteById(User $user, array $photoIDs): bool + { + // Make IDs unique as otherwise count will fail. + $photoIDs = array_unique($photoIDs); + + if ( + $user->may_upload && + Photo::query() + ->whereIn('id', $photoIDs) + ->where('owner_id', $user->id) + ->count() === count($photoIDs) + ) { + return true; + } + + // If there are any photos which are not in albums at this point, we fail. + if (Photo::query() + ->whereNull('album_id') + ->whereIn('id', $photoIDs) + ->count() > 0 + ) { + return false; + } + + $parentIDs = Photo::query() + ->select('album_id') + ->whereIn('id', $photoIDs) + ->groupBy('album_id') + ->pluck('album_id')->all(); + + return $this->albumPolicy->canDeleteById($user, $parentIDs); + } +} diff --git a/app/Policies/PhotoQueryPolicy.php b/app/Policies/PhotoQueryPolicy.php new file mode 100644 index 00000000000..ac3076d7d78 --- /dev/null +++ b/app/Policies/PhotoQueryPolicy.php @@ -0,0 +1,299 @@ +albumQueryPolicy = resolve(AlbumQueryPolicy::class); + } + + /** + * Restricts a photo query to _visible_ photos. + * + * A photo is called _visible_ if the current user is allowed to see the + * photo. + * A photo is _visible_ if any of the following conditions hold + * (OR-clause): + * + * - the user is the admin + * - the user is the owner of the photo + * - the photo is part of an album which the user is allowed to access + * (cp. {@link AlbumQueryPolicy::isAccessible()}). + * - the photo is public + * + * @param FixedQueryBuilder $query + * + * @return FixedQueryBuilder + * + * @throws InternalLycheeException + */ + public function applyVisibilityFilter(FixedQueryBuilder $query): FixedQueryBuilder + { + $this->prepareModelQueryOrFail($query, false, true); + + if (Auth::user()?->may_administrate === true) { + return $query; + } + + $userId = Auth::id(); + + // We must wrap everything into an outer query to avoid any undesired + // effects in case that the original query already contains an + // "OR"-clause. + $visibilitySubQuery = function (FixedQueryBuilder $query2) use ($userId) { + $this->albumQueryPolicy->appendAccessibilityConditions($query2->getQuery()); + if ($userId !== null) { + $query2->orWhere('photos.owner_id', '=', $userId); + } + }; + + return $query->where($visibilitySubQuery); + } + + /** + * Restricts a photo query to _searchable_ photos. + * + * A photo is _searchable_ if at least one of the following conditions + * hold: + * + * - the photo is part of an album which the user is allowed to browse + * - the user is the owner of the photo + * - the photo is public and searching through public photos is enabled + * + * See {@link AlbumQueryPolicy::applyBrowsabilityFilter()} + * for a definition of a browsable album. + * + * The search result is restricted to photos in albums which are below + * `$origin`. + * + * **Attention**: + * For efficiency reasons this method does not check if `$origin` itself + * is accessible. + * The method simply assumes that the user has already legitimately + * accessed the origin album, if the caller provides an album model. + * + * @param FixedQueryBuilder $query the photo query which shall be restricted + * @param Album|null $origin the optional top album which is used as a search base + * @param bool $include_nsfw include also the photos in sensitive albums + * + * @return FixedQueryBuilder the restricted photo query + * + * @throws InternalLycheeException + */ + public function applySearchabilityFilter(FixedQueryBuilder $query, ?Album $origin = null, bool $include_nsfw = true): FixedQueryBuilder + { + $this->prepareModelQueryOrFail($query, true, false); + + // If origin is set, also restrict the search result for admin + // to photos which are in albums below origin. + // This is not a security filter, but simply functional. + if ($origin !== null) { + $query + ->where('albums._lft', '>=', $origin->_lft) + ->where('albums._rgt', '<=', $origin->_rgt); + } + + if (!$include_nsfw) { + $query->where(fn (Builder $query) => $this->appendSensitivityConditions($query->getQuery(), $origin?->_lft, $origin?->_rgt)); + } + + if (Auth::user()?->may_administrate === true) { + return $query; + } + + return $query->where(function (Builder $query) use ($origin) { + $this->appendSearchabilityConditions( + $query->getQuery(), + $origin?->_lft, + $origin?->_rgt + ); + }); + } + + /** + * Adds the conditions of _searchable_ photos to the query. + * + * **Attention:** This method is only meant for internal use. + * Use {@link PhotoQueryPolicy::applySearchabilityFilter()} + * if called from other places instead. + * + * This method adds the WHERE conditions without any further pre-cautions. + * The method silently assumes that the SELECT clause contains the tables + * + * - **`albums`**. + * + * See {@link AlbumQueryPolicy::applySearchabilityFilter()} + * for a definition of a searchable photo. + * + * Moreover, the raw clauses are added. + * They are not wrapped into a nesting braces `()`. + * + * @param BaseBuilder $query the photo query which shall be + * restricted + * @param int|string|null $originLeft optionally constraints the search + * base; an integer value is + * interpreted a raw left bound of the + * search base; a string value is + * interpreted as a reference to a + * column which shall be used as a + * left bound + * @param int|string|null $originRight like `$originLeft` but for the + * right bound + * + * @return BaseBuilder the restricted photo query + * + * @throws QueryBuilderException + */ + public function appendSearchabilityConditions(BaseBuilder $query, int|string|null $originLeft, int|string|null $originRight): BaseBuilder + { + $userId = Auth::id(); + + try { + // there must be no unreachable album between the origin and the photo + $query->whereNotExists(function (BaseBuilder $q) use ($originLeft, $originRight) { + $this->albumQueryPolicy->appendUnreachableAlbumsCondition($q, $originLeft, $originRight); + }); + + // Special care needs to be taken for unsorted photo, i.e. photos on + // the root level: + // The condition for "no unreachable albums along the path" fails for + // root album due to two reasons: + // a) the path of albums between to the root album is empty; hence, + // there are never any unreachable albums in between + // b) while all users (even unauthenticated users) may access the + // root album, they must only see their own photos or public + // photos (this is different to any other album: if users are + // allowed to access an album, they may also see its content) + $query->whereNotNull('photos.album_id'); + + if ($userId !== null) { + $query->orWhere('photos.owner_id', '=', $userId); + } + } catch (\Throwable $e) { + throw new QueryBuilderException($e); + } + + return $query; + } + + /** + * Adds the conditions of _sensitive_ photos to the query. + * + * **Attention:** This method is only meant for internal use. + * Use {@link PhotoQueryPolicy::applySearchabilityFilter()} + * if called from other places instead. + * + * This method adds the WHERE conditions without any further pre-cautions. + * The method silently assumes that the SELECT clause contains the tables + * + * - **`albums`**. + * + * Moreover, the raw clauses are added. + * They are not wrapped into a nesting braces `()`. + * + * @param BaseBuilder $query the photo query which shall be + * restricted + * + * @return BaseBuilder the restricted photo query + * + * @throws QueryBuilderException + */ + public function appendSensitivityConditions(BaseBuilder $query, int|string|null $originLeft, int|string|null $originRight): BaseBuilder + { + $userId = Auth::id(); + + try { + // there must be no unreachable album between the origin and the photo + $query->whereNotExists(function (BaseBuilder $q) use ($originLeft, $originRight) { + $this->albumQueryPolicy->appendRecursiveSensitiveAlbumsCondition($q, $originLeft, $originRight); + }); + + // Special care needs to be taken for unsorted photo, i.e. photos on + // the root level: + // The condition for "no unreachable albums along the path" fails for + // root album due to two reasons: + // a) the path of albums between to the root album is empty; hence, + // there are never any unreachable albums in between + // b) while all users (even unauthenticated users) may access the + // root album, they must only see their own photos or public + // photos (this is different to any other album: if users are + // allowed to access an album, they may also see its content) + $query->orWhere( + fn ($q) => $q + ->whereNull('photos.album_id') + ->where('photos.owner_id', '=', $userId) + ); + } catch (\Throwable $e) { + throw new QueryBuilderException($e); + } + + return $query; + } + + /** + * Throws an exception if the given query does not query for a photo. + * + * @param FixedQueryBuilder $query the query to prepare + * @param bool $addAlbums if true, joins photo query with (parent) albums + * @param bool $addBaseAlbums if true, joins photos query with (parent) base albums + * + * @throws InternalLycheeException + */ + private function prepareModelQueryOrFail(FixedQueryBuilder $query, bool $addAlbums, bool $addBaseAlbums): void + { + $model = $query->getModel(); + $table = $query->getQuery()->from; + if (!($model instanceof Photo && $table === 'photos')) { + throw new InvalidQueryModelException('photo'); + } + + // Ensure that only columns of the photos are selected, + // if no specific columns are yet set. + // Otherwise, we cannot add a JOIN clause below + // without accidentally adding all columns of the join, too. + $baseQuery = $query->getQuery(); + if ($baseQuery->columns === null || count($baseQuery->columns) === 0) { + $query->select(['photos.*']); + } + if ($addAlbums) { + $query->leftJoin( + table: 'albums', + first: 'albums.id', + operator: '=', + second: 'photos.album_id'); + } + if ($addBaseAlbums) { + $query->leftJoin( + table: 'base_albums', + first: 'base_albums.id', + operator: '=', + second: 'photos.album_id'); + } + + // Necessary to apply the visibiliy/search conditions + $this->albumQueryPolicy->joinSubComputedAccessPermissions( + query: $query, + second: 'photos.album_id' + ); + } +} diff --git a/app/Policies/SettingsPolicy.php b/app/Policies/SettingsPolicy.php new file mode 100644 index 00000000000..7c476594dad --- /dev/null +++ b/app/Policies/SettingsPolicy.php @@ -0,0 +1,116 @@ +may_administrate === true; + } + + /** + * This function returns false as it is bypassed by the before() + * which directly checks for admin rights. + * + * @param User $user + * + * @return bool + */ + public function canClearLogs(User $user): bool + { + return false; + } + + /** + * This function returns false as it is bypassed by the before() + * which directly checks for admin rights. + * + * @param User $user + * + * @return bool + */ + public function canSeeDiagnostics(User $user): bool + { + return false; + } + + /** + * This function returns false as it is bypassed by the before() + * which directly checks for admin rights. + * + * @param User $user + * + * @return bool + */ + public function canSeeStatistics(User $user): bool + { + return true; + } + + /** + * This function returns false as it is bypassed by the before() + * which directly checks for admin rights. + * + * @param User $user + * + * @return bool + */ + public function canUpdate(User $user): bool + { + return false; + } + + /** + * This function returns false as it is bypassed by the before() + * which directly checks for admin rights. + * + * @param User $user + * + * @return bool + */ + public function canAccessDevTools(User $user): bool + { + return false; + } +} diff --git a/app/Policies/UserPolicy.php b/app/Policies/UserPolicy.php new file mode 100644 index 00000000000..9d17610a9ac --- /dev/null +++ b/app/Policies/UserPolicy.php @@ -0,0 +1,47 @@ +may_upload; + } + + /** + * This defines if user can edit their settings. + * + * @param User $user + * + * @return bool + */ + public function canEdit(User $user): bool + { + return $user->may_edit_own_settings; + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 1513f7fc905..98eb8c5c125 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -1,43 +1,90 @@ SymLinkFunctions::class, - ConfigFunctions::class => ConfigFunctions::class, - LangFactory::class => LangFactory::class, - Lang::class => Lang::class, - Helpers::class => Helpers::class, - PublicIds::class => PublicIds::class, - SessionFunctions::class => SessionFunctions::class, - GitRequest::class => GitRequest::class, - GitHubFunctions::class => GitHubFunctions::class, - LycheeVersion::class => LycheeVersion::class, - CheckUpdate::class => CheckUpdate::class, - ApplyUpdate::class => ApplyUpdate::class, - SmartFactory::class => SmartFactory::class, - ]; + /** + * Defines which queries to ignore when doing explain. + * + * @var string[] + */ + private array $ignore_log_SQL = + [ + 'information_schema', // Not interesting + 'migrations', + + // We do not want infinite loops + 'EXPLAIN', + + // Way too noisy + 'configs', + ]; + + /** @var array */ + public array $singletons = + [ + SymLinkFunctions::class => SymLinkFunctions::class, + Helpers::class => Helpers::class, + CheckUpdate::class => CheckUpdate::class, + AlbumFactory::class => AlbumFactory::class, + AlbumQueryPolicy::class => AlbumQueryPolicy::class, + PhotoQueryPolicy::class => PhotoQueryPolicy::class, + + // Versioning + InstalledVersion::class => InstalledVersion::class, + GitHubVersion::class => GitHubVersion::class, + FileVersion::class => FileVersion::class, + + // Json requests. + CommitsRequest::class => CommitsRequest::class, + UpdateRequest::class => UpdateRequest::class, + + // JsonParsers + GitCommits::class => GitCommits::class, + GitTags::class => GitTags::class, + ]; /** * Bootstrap any application services. @@ -46,6 +93,71 @@ class AppServiceProvider extends ServiceProvider */ public function boot() { + // Prohibits: db:wipe, migrate:fresh, migrate:refresh, and migrate:reset + DB::prohibitDestructiveCommands(config('app.env', 'production') !== 'dev'); + + /** + * By default resources are wrapping results in a 'data' attribute. + * We disable that. + */ + JsonResource::withoutWrapping(); + + /** + * We force URL to HTTPS if requested in .env via APP_FORCE_HTTPS. + */ + if (config('features.force_https') === true) { + URL::forceScheme('https'); + } + + if (config('database.db_log_sql', false) === true) { + DB::listen(fn ($q) => $this->logSQL($q)); + } + + try { + $lang = Configs::getValueAsString('lang'); + /** @disregard P1013 Undefined method setLocale() (stupid intelephense) */ + app()->setLocale($lang); + } catch (\Throwable $e) { + /** Ignore. + * This is necessary so that we can continue: + * - if Configs table do not exists (no install), + * - if the value does not exists in configs (no install),. + */ + } + + /** + * We enforce strict mode + * this has the following effect: + * - lazy loading is disabled + * - non-fillable attributes on creation of model are not discarded but throw an error + * - prevents accessing missing attributes. + */ + Model::shouldBeStrict(); + + try { + stream_filter_register( + StreamStatFilter::REGISTERED_NAME, + StreamStatFilter::class + ); + } catch (StreamException) { + // We ignore any error here, because Laravel calls the `boot` + // method several times and any subsequent attempt to register a + // filter for the same name anew will fail. + } + + /** + * Set up the Authorization layer for accessing Logs in LogViewer. + */ + LogViewer::auth(function ($request) { + // Allow to bypass when debug is ON and when env is dev + // At this point, it is no longer our fault if the Lychee admin have their logs publically accessible. + if (config('app.debug', false) === true && config('app.env', 'production') === 'dev') { + return true; + } + + // return true to allow viewing the Log Viewer. + return Auth::authenticate() !== null && Gate::check(SettingsPolicy::CAN_SEE_LOGS, Configs::class); + }); } /** @@ -55,22 +167,64 @@ public function boot() */ public function register() { - $this->app->singleton(Image\ImageHandlerInterface::class, function ($app) { - $compressionQuality = Configs::get_value('compression_quality', 90); - - return new ImageHandler($compressionQuality); + $this->app->bind('Helpers', function () { + return resolve(Helpers::class); }); - $this->app->bind('AccessControl', function () { - return resolve(SessionFunctions::class); - }); + $this->app->bind( + AbstractSizeVariantNamingStrategy::class, + SizeVariantGroupedWithRandomSuffixNamingStrategy::class + ); - $this->app->bind('lang', function () { - return resolve(Lang::class); - }); + $this->app->bind( + SizeVariantFactory::class, + SizeVariantDefaultFactory::class + ); + } - $this->app->bind('Helpers', function () { - return resolve(Helpers::class); - }); + private function logSQL(QueryExecuted $query): void + { + // Quick exit + if ( + Str::contains(request()->getRequestUri(), 'logs', true) || + Str::contains($query->sql, $this->ignore_log_SQL) + ) { + return; + } + + // Get message with binding outside. + $msg = '(' . $query->time . 'ms) ' . $query->sql . ' [' . implode(', ', $query->bindings) . ']'; + + // For pgsql and sqlite we log the query and exit early + if (config('database.default', 'mysql') !== 'mysql' || + config('database.explain', false) === false || + !Str::contains($query->sql, 'select') + ) { + Log::debug($msg); + + return; + } + + // For mysql we perform an explain as this is usually the one being slower... + $bindings = collect($query->bindings)->map(function ($q) { + return match (gettype($q)) { + 'NULL' => "''", + 'string' => "'{$q}'", + 'boolean' => $q ? '1' : '0', + default => $q, + }; + })->all(); + + $sql_with_bindings = Str::replaceArray('?', $bindings, $query->sql); + + $explain = DB::select('EXPLAIN ' . $query->sql, $query->bindings); + $renderer = new ArrayToTextTable(); + $renderer->setIgnoredKeys(['possible_keys', 'key_len', 'ref']); + + $msg .= PHP_EOL; + $msg .= Str::repeat('-', 20) . PHP_EOL; + $msg .= $sql_with_bindings . PHP_EOL; + $msg .= $renderer->getTable($explain); + Log::debug($msg); } } diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 33fcad01d45..c3ad41e0f8a 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -1,18 +1,48 @@ */ protected $policies = [ - // 'App\Model' => 'App\Policies\ModelPolicy', + User::class => UserPolicy::class, + + Photo::class => PhotoPolicy::class, + + // This ensures that all the kinds of albums are covered in the Gate mapping. + BaseSmartAlbum::class => AlbumPolicy::class, + BaseAlbum::class => AlbumPolicy::class, + Album::class => AlbumPolicy::class, + AbstractAlbum::class => AlbumPolicy::class, + + Configs::class => SettingsPolicy::class, ]; /** @@ -20,8 +50,12 @@ class AuthServiceProvider extends ServiceProvider * * @return void */ - public function boot() + public function boot(): void { $this->registerPolicies(); + // The identifier "session-or-token" is used in config/auth.php. + Auth::extend('session-or-token', function (Application $app, string $name, array $config) { + return SessionOrTokenGuard::createGuard($app, $name, $config); + }); } } diff --git a/app/Providers/BroadcastServiceProvider.php b/app/Providers/BroadcastServiceProvider.php index 0546ead6034..779dc35aee7 100644 --- a/app/Providers/BroadcastServiceProvider.php +++ b/app/Providers/BroadcastServiceProvider.php @@ -1,5 +1,11 @@ > */ protected $listen = [ Registered::class => [ // SendEmailVerificationNotification::class, ], + SocialiteWasCalled::class => [ + AmazonExtendSocialite::class . '@handle', + AppleExtendSocialite::class . '@handle', + AuthentikExtendSocialite::class . '@handle', + FacebookExtendSocialite::class . '@handle', + GitHubExtendSocialite::class . '@handle', + GoogleExtendSocialite::class . '@handle', + // Mastodon is provided directly. + MicrosoftExtendSocialite::class . '@handle', + NextcloudExtendSocialite::class . '@handle', + KeycloakExtendSocialite::class . '@handle', + ], ]; /** @@ -25,7 +51,7 @@ class EventServiceProvider extends ServiceProvider * * @return void */ - public function boot() + public function boot(): void { } } diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 97e78e11ff6..b6d84e39e6b 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -1,7 +1,14 @@ configureRateLimiting(); + // Note: `web.php` must be registered last, because it contains a + // "catch all" route and the routes are considered in a "first match" + // fashion. $this->routes(function () { - Route::middleware('install') - ->group(base_path('routes/install.php')); + Features::when('vuejs', fn () => $this->getLycheeV6Routes(), fn () => $this->getLegacyRoutes()); + }); + } - Route::middleware('web-admin') - ->group(base_path('routes/admin.php')); + private function getLycheeV6Routes(): void + { + Route::middleware('web-admin')->group(base_path('routes/web-admin-v2.php')); + Route::middleware('api')->prefix('api/v2')->group(base_path('routes/api_v2.php')); + Route::middleware('web-install')->group(base_path('routes/web-install.php')); - Route::prefix('api') - ->middleware('api') - ->group(base_path('routes/api.php')); + if (Features::active('legacy_api')) { + Route::middleware('api')->prefix('api')->group(base_path('routes/api_v1.php')); + } - Route::middleware('web') - ->group(base_path('routes/livewire.php')); + Route::middleware('web')->group(base_path('routes/web_v2.php')); + } - Route::middleware('web') - ->group(base_path('routes/web.php')); - }); + private function getLegacyRoutes(): void + { + Route::middleware('web-install')->group(base_path('routes/web-install.php')); + Route::middleware('api')->prefix('api')->group(base_path('routes/api_v1.php')); + Route::middleware('web-admin')->group(base_path('routes/web-admin-v1.php')); + Route::middleware('web')->group(base_path('routes/web_v1.php')); } /** diff --git a/app/Redirections/Redirection.php b/app/Redirections/Redirection.php deleted file mode 100644 index 082040bc5d0..00000000000 --- a/app/Redirections/Redirection.php +++ /dev/null @@ -1,8 +0,0 @@ - 'no-cache, must-revalidate', - ]); - } -} - diff --git a/app/Redirections/ToInstall.php b/app/Redirections/ToInstall.php deleted file mode 100644 index b1c8cbd528b..00000000000 --- a/app/Redirections/ToInstall.php +++ /dev/null @@ -1,17 +0,0 @@ - 'no-cache, must-revalidate', - ]); - } -} - diff --git a/app/Relations/BaseHasManyPhotos.php b/app/Relations/BaseHasManyPhotos.php new file mode 100644 index 00000000000..663bb653172 --- /dev/null +++ b/app/Relations/BaseHasManyPhotos.php @@ -0,0 +1,153 @@ +> + */ +abstract class BaseHasManyPhotos extends Relation +{ + protected PhotoQueryPolicy $photoQueryPolicy; + + /** + * @param TagAlbum|Album $owningAlbum + */ + public function __construct(TagAlbum|Album $owningAlbum) + { + // Sic! We must initialize attributes of this class before we call + // the parent constructor. + // The parent constructor calls `addConstraints` and thus our own + // attributes must be initialized by then + $this->photoQueryPolicy = resolve(PhotoQueryPolicy::class); + // This is a hack. + // The abstract class + // {@link \Illuminate\Database\Eloquent\Relations\Relation} + // stores a pointer to the parent and assumes that the parent is + // an instance of {@link Illuminate\Database\Eloquent\Model}. + // However, we cannot guarantee this, because we have smart albums + // which do not exist on the DB and therefore do not extend + // `Model`. + // Actually, it is sufficient if the owning side implements the + // method which are provided by `HasRelations`. + // Unfortunately, the constructor of `Relation` demands a true model + // and does not only ask for something which implements `HasRelations`. + // Luckily, `Relation` itself does not do anything with the passed + // model but only stores a reference in `Relation::$parent` to be + // used by child classes. + // Moreover, it is impossible to pass `null`. + // As a work-around we store the owning album in our own attribute + // `$owningAlbum` and always use that instead of `$parent`. + /** @var Album|TagAlbum $owningAlbum */ + parent::__construct( + // Sic! We also must load the album eagerly. + // This relation is not used by albums which own the queried + // photos, but by albums which only include the photos due to some + // indirect condition. + // Hence, the actually owning albums of the photos are not + // necessarily loaded. + Photo::query()->with(['album', 'size_variants', 'size_variants.sym_links']), + // @phpstan-ignore-next-line + $owningAlbum + ); + } + + /** + * @return FixedQueryBuilder + */ + protected function getRelationQuery(): FixedQueryBuilder + { + /** + * We know that the internal query is of type `FixedQueryBuilder`, + * because it was set in the constructor as `Photo::query()`. + * + * @noinspection PhpIncompatibleReturnTypeInspection + * + * @phpstan-ignore-next-line + */ + return $this->query; + } + + public function getParent(): BaseAlbum + { + /** + * We know that the parent is of type `BaseAlbum`, + * because it was set in the constructor as `$owningAlbum`. + * + * @noinspection PhpIncompatibleReturnTypeInspection + */ + return $this->parent; + } + + /** + * Initializes the given owning models with a default value of this + * relation. + * + * In this case, the default value is an empty collection of + * {@link \App\Models\Photo}. + * + * @param array $models a list of owning models, i.e. a list of albums + * @param string $relation the name of the relation on the owning models + * + * @return array always returns $models + */ + public function initRelation(array $models, $relation): array + { + /** @var TagAlbum|Album $model */ + foreach ($models as $model) { + $model->setRelation($relation, $this->related->newCollection()); + } + + return $models; + } + + /** + * Returns the collection of photos for a single owning parent (aka + * "album"). + * + * This method also takes care of proper sorting. + * For most columns this method performs sorting on the DB layer for + * improved performance. + * But for some columns which require "natural" and locale-dependent + * sorting, the collection is sorted after is has been fetched from + * the DB. + * + * @return Collection + * + * @throws InvalidOrderDirectionException + */ + public function getResults(): Collection + { + /** @var BaseAlbum */ + $parent = $this->parent; + /** @var SortingCriterion $sorting */ + $sorting = $parent->getEffectivePhotoSorting(); + + return (new SortingDecorator($this->getRelationQuery())) + ->orderPhotosBy($sorting->column, $sorting->order) + ->get(); + } +} diff --git a/app/Relations/BidirectionalRelationTrait.php b/app/Relations/BidirectionalRelationTrait.php new file mode 100644 index 00000000000..ae98c652d4f --- /dev/null +++ b/app/Relations/BidirectionalRelationTrait.php @@ -0,0 +1,19 @@ +foreignMethodName; + } +} diff --git a/app/Relations/HasAlbumThumb.php b/app/Relations/HasAlbumThumb.php new file mode 100644 index 00000000000..0a9334d9f50 --- /dev/null +++ b/app/Relations/HasAlbumThumb.php @@ -0,0 +1,321 @@ + + * + * @extends Relation + * + * @disregard P1037 + */ +class HasAlbumThumb extends Relation +{ + protected AlbumQueryPolicy $albumQueryPolicy; + protected PhotoQueryPolicy $photoQueryPolicy; + protected PhotoSortingCriterion $sorting; + + public function __construct(Album $parent) + { + // Sic! We must initialize attributes of this class before we call + // the parent constructor. + // The parent constructor calls `addConstraints` and thus our own + // attributes must be initialized by then + $this->albumQueryPolicy = resolve(AlbumQueryPolicy::class); + $this->photoQueryPolicy = resolve(PhotoQueryPolicy::class); + $this->sorting = PhotoSortingCriterion::createDefault(); + parent::__construct( + Photo::query()->with(['size_variants' => (fn ($r) => Thumb::sizeVariantsFilter($r))]), + $parent + ); + } + + /** + * @return FixedQueryBuilder + */ + protected function getRelationQuery(): FixedQueryBuilder + { + /** + * We know that the internal query is of type `FixedQueryBuilder`, + * because it was set in the constructor as `Photo::query()`. + * + * @noinspection PhpIncompatibleReturnTypeInspection + * + * @phpstan-ignore-next-line + */ + return $this->query; + } + + /** + * Adds the constraints for a single album. + * + * If the album has set an explicit cover, then we simply search for that + * photo. + * Else, we search for all photos which are (recursive) descendants of the + * given album. + */ + public function addConstraints(): void + { + if (static::$constraints) { + /** @var Album $album */ + $album = $this->parent; + if ($album->cover_id !== null) { + $this->where('photos.id', '=', $album->cover_id); + } else { + $this->photoQueryPolicy + ->applySearchabilityFilter( + query: $this->getRelationQuery(), + origin: $album, + include_nsfw: $album->is_nsfw); + } + } + } + + /** + * Builds a query to eagerly load the thumbnails of a sequence of albums. + * + * Note, the query is not as efficient as it could be, but it is the + * best query we can construct which is portable to MySQL, PostgreSQl and + * SQLite. + * The inefficiency comes from the inner, correlated value sub-query + * `bestPhotoIDSelect`. + * This value query refers the outer query through `covered_albums` and + * thus needs to be executed for every result. + * Moreover, the temporary query table `$album2Cover` is an in-memory + * table and thus does not provide any indexes. + * + * A faster approach would be to first JOIN the tables, then sort the + * result and finally pick the first result of each group based on + * identical `covered_album_id`. + * The approach "join first (with everything), filter last" is faster, + * because the DBMS can use its indexes. + * + * For PostgreSQL we could use the `DISTINCT ON`-clause to achieve the + * result: + * + * SELECT DISTINCT ON (covered_album_id) + * covered_albums.id AS covered_album_id, + * photos.id AS id, + * photos.type AS type + * FROM covered_albums + * LEFT JOIN + * ( + * photos + * LEFT JOIN albums + * ON (albums.id = photos.album_id) + * ) + * ON ( + * albums._lft >= covered_albums._lft AND + * albums._rgt <= covered_albums._rgt AND + * "complicated searchability filter goes here" + * ) + * WHERE covered_albums.id IN $albumKeys + * ORDER BY album_id ASC, photos.is_starred DESC, photos.created_at DESC + * + * For PostgreSQL see ["SELECT - DISTINCT Clause"](https://www.postgresql.org/docs/13/sql-select.html#SQL-DISTINCT). + * + * But `DISTINCT ON` is provided by neither MySQL nor SQLite. + * For the latter two, the following non-SQL-conformant query could be + * used: + * + * SELECT + * covered_albums.id AS covered_album_id, + * photos.id AS id, + * photos.type AS type + * FROM covered_albums + * LEFT JOIN + * ( + * photos + * LEFT JOIN albums + * ON (albums.id = photos.album_id) + * ) + * ON ( + * albums._lft >= covered_albums._lft AND + * albums._rgt <= covered_albums._rgt AND + * "complicated seachability filter goes here" + * ) + * WHERE covered_albums.id IN $albumKeys + * ORDER BY album_id ASC, photos.is_starred DESC, photos.created_at DESC + * GROUP BY album_id + * + * Instead of enforcing distinct results for `covered_album_id`, the result + * is grouped by `covered_album_id`. + * Note that this is not SQL-compliant, because the `SELECT` clause + * contains two columns (`photo.id` and `photo.type`) which are neither + * part of the `GROUP BY`-clause nor aggregates. + * However, MySQL and SQLite relax this constraint and return the + * column values of the first row of a group. + * This is exactly the specified behaviour of `DISTINCT ON`. + * For SQLite see "[Quirks, Caveats, and Gotchas In SQLite, Sec. 6](https://www.sqlite.org/quirks.html)" + * + * TODO: If the following query is too slow for large installation, we must write two separate implementations for PostgreSQL and MySQL/SQLite as outlined above. + * + * @param array $models + */ + public function addEagerConstraints(array $models): void + { + // We only use those `Album` models which have not set an explicit + // cover. + // Albums with explicit covers are treated separately in + // method `match`. + $albumKeys = collect($models) + ->whereNull('cover_id') + ->unique('id', true) + ->sortBy('id') + ->map(fn (Album $album) => $album->getKey()) + ->values(); + + $bestPhotoIDSelect = Photo::query() + ->select(['photos.id AS photo_id']) + ->join('albums', 'albums.id', '=', 'photos.album_id') + ->whereColumn('albums._lft', '>=', 'covered_albums._lft') + ->whereColumn('albums._rgt', '<=', 'covered_albums._rgt') + ->orderBy('photos.' . ColumnSortingPhotoType::IS_STARRED->value, OrderSortingType::DESC->value) + ->orderBy('photos.' . $this->sorting->column->value, $this->sorting->order->value) + ->limit(1); + + if (Auth::user()?->may_administrate !== true) { + $bestPhotoIDSelect->where(function (Builder $query2) { + $this->photoQueryPolicy->appendSearchabilityConditions( + $query2->getQuery(), + 'covered_albums._lft', + 'covered_albums._rgt' + ); + }); + } + + $album2Cover = function (BaseBuilder $builder) use ($bestPhotoIDSelect, $albumKeys) { + $builder + ->from('albums as covered_albums') + ->join('base_albums', 'base_albums.id', '=', 'covered_albums.id'); + + $this->albumQueryPolicy->joinSubComputedAccessPermissions( + query: $builder, + second: 'base_albums.id' + ); + + $builder->select(['covered_albums.id AS album_id']) + ->addSelect(['photo_id' => $bestPhotoIDSelect]) + ->whereIn('covered_albums.id', $albumKeys); + if (Auth::user()?->may_administrate !== true) { + $builder->where(function (BaseBuilder $q) { + $this->albumQueryPolicy->appendAccessibilityConditions($q); + }); + } + }; + + $this->getRelationQuery() + ->select([ + 'covers.id as id', + 'covers.type as type', + 'album_2_cover.album_id as covered_album_id', + ]) + ->from($album2Cover, 'album_2_cover') + ->join( + 'photos as covers', + 'covers.id', + '=', + 'album_2_cover.photo_id' + ); + } + + /** + * @param array $models an array of albums models whose thumbnails shall be initialized + * @param string $relation the name of the relation from the parent to the child models + * + * @return array the array of album models + */ + public function initRelation(array $models, $relation): array + { + foreach ($models as $model) { + $model->setRelation($relation, null); + } + + return $models; + } + + /** + * Match the eagerly loaded results to their parents. + * + * @param array $models an array of parent models + * @param Collection $results the unified collection of all child models of all parent models + * @param string $relation the name of the relation from the parent to the child models + * + * @return array + */ + public function match(array $models, Collection $results, $relation): array + { + $dictionary = $results->mapToDictionary(function ($result) { + /** @phpstan-ignore-next-line undefied property */ + return [$result->covered_album_id => $result]; + })->all(); + + // Once we have the dictionary we can simply spin through the parent models to + // link them up with their children using the keyed dictionary to make the + // matching very convenient and easy work. Then we'll just return them. + /** @var Album $album */ + foreach ($models as $album) { + $albumID = $album->id; + if ($album->cover_id !== null) { + // We do not execute a query, if `cover_id` is set, because + // `Album`always eagerly loads its cover and hence, we already + // have it. + // See {@link Album::with} + $album->setRelation($relation, Thumb::createFromPhoto($album->cover)); + } elseif (isset($dictionary[$albumID])) { + /** @var Photo $cover */ + $cover = reset($dictionary[$albumID]); + $album->setRelation($relation, Thumb::createFromPhoto($cover)); + } else { + $album->setRelation($relation, null); + } + } + + return $models; + } + + public function getResults(): ?Thumb + { + /** @var Album|null $album */ + $album = $this->parent; + if ($album === null || !Gate::check(AlbumPolicy::CAN_ACCESS, $album)) { + return null; + } + + // We do not execute a query, if `cover_id` is set, because `Album` + // is always eagerly loaded with its cover and hence, we already + // have it. + // See {@link Album::with} + if ($album->cover_id !== null) { + return Thumb::createFromPhoto($album->cover); + } else { + return Thumb::createFromQueryable( + $this->getRelationQuery(), + $this->sorting + ); + } + } +} diff --git a/app/Relations/HasManyBidirectionally.php b/app/Relations/HasManyBidirectionally.php new file mode 100644 index 00000000000..3fd4118eaed --- /dev/null +++ b/app/Relations/HasManyBidirectionally.php @@ -0,0 +1,80 @@ + + */ +class HasManyBidirectionally extends HasMany implements BidirectionalRelation +{ + use BidirectionalRelationTrait; + + /** + * @param Builder $query + * @param TDeclaringModel $parent + * @param string $foreignKey + * @param string $localKey + * @param string $foreignMethodName + * + * @return void + */ + public function __construct(Builder $query, Model $parent, string $foreignKey, string $localKey, string $foreignMethodName) + { + parent::__construct($query, $parent, $foreignKey, $localKey); + $this->foreignMethodName = $foreignMethodName; + } + + /** + * Match the eagerly loaded results to their parents. + * + * This method is identical to + * {@link \Illuminate\Database\Eloquent\Relations\HasOneOrMany::matchOneOrMany} + * but additionally sets the reverse association of the child object + * back to its parent object. + * + * @param TDeclaringModel[] $models an array of parent models + * @param Collection $results the unified collection of all child models of all parent models + * @param string $relation the name of the relation from the parent to the child models + * + * @return array + */ + public function match(array $models, Collection $results, $relation): array + { + $dictionary = $this->buildDictionary($results); + + // Once we have the dictionary we can simply spin through the parent models to + // link them up with their children using the keyed dictionary to make the + // matching very convenient and easy work. Then we'll just return them. + foreach ($models as $model) { + if (isset($dictionary[$key = $this->getDictionaryKey($model->getAttribute($this->localKey))])) { + /** @var Collection $childrenOfModel */ + $childrenOfModel = $this->getRelationValue($dictionary, $key, 'many'); + $model->setRelation($relation, $childrenOfModel); + // This is the newly added code which sets this method apart + // from the original method and additionally sets the + // reverse link + /** @var Model $childModel */ + foreach ($childrenOfModel as $childModel) { + $childModel->setRelation($this->foreignMethodName, $model); + } + } + } + + return $models; + } +} diff --git a/app/Relations/HasManyChildAlbums.php b/app/Relations/HasManyChildAlbums.php new file mode 100644 index 00000000000..ee223c80873 --- /dev/null +++ b/app/Relations/HasManyChildAlbums.php @@ -0,0 +1,141 @@ + + */ +class HasManyChildAlbums extends HasManyBidirectionally +{ + protected AlbumQueryPolicy $albumQueryPolicy; + + public function __construct(Album $owningAlbum) + { + // Sic! We must initialize attributes of this class before we call + // the parent constructor. + // The parent constructor calls `addConstraints` and thus our own + // attributes must be initialized by then + $this->albumQueryPolicy = resolve(AlbumQueryPolicy::class); + + parent::__construct( + $owningAlbum->newQuery(), + $owningAlbum, + 'parent_id', + 'id', + 'parent' + ); + } + + protected function getRelationQuery(): AlbumBuilder + { + /** + * We know that the internal query is of type `AlbumBuilder`, + * because it was set in the constructor as `$owningAlbum->newQuery()`. + * + * @noinspection PhpIncompatibleReturnTypeInspection + * + * @phpstan-ignore-next-line + */ + return $this->query; + } + + /** + * @throws InternalLycheeException + */ + public function addConstraints() + { + if (static::$constraints) { + parent::addConstraints(); + $this->albumQueryPolicy->applyVisibilityFilter($this->getRelationQuery()); + } + } + + /** + * @param Album[] $models + * + * @throws InternalLycheeException + */ + public function addEagerConstraints(array $models) + { + parent::addEagerConstraints($models); + $this->albumQueryPolicy->applyVisibilityFilter($this->getRelationQuery()); + } + + /** + * @return Collection + * + * @throws InvalidOrderDirectionException + */ + public function getResults(): Collection + { + if (is_null($this->getParentKey())) { + /** @var Collection */ + return $this->related->newCollection(); + } + + $albumSorting = $this->getParent()->getEffectiveAlbumSorting(); + + /** @var SortingDecorator */ + $sortingDecorator = new SortingDecorator($this->query); + + return $sortingDecorator + ->orderBy( + $albumSorting->column, + $albumSorting->order) + ->get(); + } + + /** + * Match the eagerly loaded results to their parents. + * + * @param Album[] $models an array of parent models + * @param Collection $results the unified collection of all child models of all parent models + * @param string $relation the name of the relation from the parent to the child models + * + * @return array + */ + public function match(array $models, Collection $results, $relation): array + { + $dictionary = $this->buildDictionary($results); + + // Once we have the dictionary we can simply spin through the parent models to + // link them up with their children using the keyed dictionary to make the + // matching very convenient and easy work. Then we'll just return them. + foreach ($models as $model) { + if (isset($dictionary[$key = $this->getDictionaryKey($model->getAttribute($this->localKey))])) { + /** @var Collection $childrenOfModel */ + $childrenOfModel = $this->getRelationValue($dictionary, $key, 'many'); + $sorting = $model->getEffectiveAlbumSorting(); + $childrenOfModel = $childrenOfModel + ->sortBy($sorting->column->value, SORT_NATURAL | SORT_FLAG_CASE, $sorting->order === OrderSortingType::DESC) + ->values(); + $model->setRelation($relation, $childrenOfModel); + // This is the newly added code which sets this method apart + // from the original method and additionally sets the + // reverse link + /** @var Model $childModel */ + foreach ($childrenOfModel as $childModel) { + $childModel->setRelation($this->foreignMethodName, $model); + } + } + } + + return $models; + } +} diff --git a/app/Relations/HasManyChildPhotos.php b/app/Relations/HasManyChildPhotos.php new file mode 100644 index 00000000000..1f464cf5a10 --- /dev/null +++ b/app/Relations/HasManyChildPhotos.php @@ -0,0 +1,164 @@ + + */ +class HasManyChildPhotos extends HasManyBidirectionally +{ + protected PhotoQueryPolicy $photoQueryPolicy; + + public function __construct(Album $owningAlbum) + { + // Sic! We must initialize attributes of this class before we call + // the parent constructor. + // The parent constructor calls `addConstraints` and thus our own + // attributes must be initialized by then + $this->photoQueryPolicy = resolve(PhotoQueryPolicy::class); + parent::__construct( + Photo::query(), + $owningAlbum, + 'album_id', + 'id', + 'album' + ); + } + + /** + * @return FixedQueryBuilder + */ + protected function getRelationQuery(): FixedQueryBuilder + { + /** + * We know that the internal query is of type `FixedQueryBuilder`, + * because it was set in the constructor as `Photo::query()`. + * + * @noinspection PhpIncompatibleReturnTypeInspection + * + * @phpstan-ignore-next-line + */ + return $this->query; + } + + public function getParent(): Album + { + /** + * We know that the internal query is of type `Album`, + * because it was set in the constructor as `$owningAlbum`. + * + * @noinspection PhpIncompatibleReturnTypeInspection + */ + return $this->parent; + } + + /** + * @throws InternalLycheeException + */ + public function addConstraints() + { + if (static::$constraints) { + parent::addConstraints(); + $this->photoQueryPolicy->applyVisibilityFilter($this->getRelationQuery()); + } + } + + /** + * @param Album[] $models + * + * @throws InternalLycheeException + */ + public function addEagerConstraints(array $models) + { + parent::addEagerConstraints($models); + $this->photoQueryPolicy->applyVisibilityFilter($this->getRelationQuery()); + } + + /** + * @return Collection + * + * @throws InvalidOrderDirectionException + */ + public function getResults(): Collection + { + if (is_null($this->getParentKey())) { + return $this->related->newCollection(); + } + + $albumSorting = $this->getParent()->getEffectivePhotoSorting(); + + /** @var SortingDecorator */ + $sortingDecorator = new SortingDecorator($this->query); + + return $sortingDecorator + ->orderPhotosBy( + $albumSorting->column, + $albumSorting->order + ) + ->get(); + } + + /** + * Match the eagerly loaded results to their parents. + * + * @param Album[] $models an array of parent models + * @param Collection $results the unified collection of all child models of all parent models + * @param string $relation the name of the relation from the parent to the child models + * + * @return array + * + * @throws \LogicException + * @throws InvalidCastException + */ + public function match(array $models, Collection $results, $relation): array + { + $dictionary = $this->buildDictionary($results); + + // Once we have the dictionary we can simply spin through the parent models to + // link them up with their children using the keyed dictionary to make the + // matching very convenient and easy work. Then we'll just return them. + /** @var Album $model */ + foreach ($models as $model) { + if (isset($dictionary[$key = $this->getDictionaryKey($model->getAttribute($this->localKey))])) { + /** @var Collection $childrenOfModel */ + $childrenOfModel = $this->getRelationValue($dictionary, $key, 'many'); + $sorting = $model->getEffectivePhotoSorting(); + $childrenOfModel = $childrenOfModel + ->sortBy( + $sorting->column->value, + in_array($sorting->column, SortingDecorator::POSTPONE_COLUMNS, true) ? SORT_NATURAL | SORT_FLAG_CASE : SORT_REGULAR, + $sorting->order === OrderSortingType::DESC + ) + ->values(); + $model->setRelation($relation, $childrenOfModel); + // This is the newly added code which sets this method apart + // from the original method and additionally sets the + // reverse link + /** @var Model $childModel */ + foreach ($childrenOfModel as $childModel) { + $childModel->setRelation($this->foreignMethodName, $model); + } + } + } + + return $models; + } +} diff --git a/app/Relations/HasManyPhotosByTag.php b/app/Relations/HasManyPhotosByTag.php new file mode 100644 index 00000000000..cbc15d050de --- /dev/null +++ b/app/Relations/HasManyPhotosByTag.php @@ -0,0 +1,118 @@ + + */ +class HasManyPhotosByTag extends BaseHasManyPhotos +{ + public function __construct(TagAlbum $owningAlbum) + { + parent::__construct($owningAlbum); + } + + /** + * Adds the constraints for single owning album to the base query. + * + * This method is called by the framework, if the photos of a + * single tag albums are fetched. + * + * @return void + * + * @throws InternalLycheeException + */ + public function addConstraints(): void + { + if (static::$constraints) { + $this->addEagerConstraints([$this->parent]); + } + } + + /** + * Adds the constraints for a list of owning album to the base query. + * + * This method is called by the framework, if the related photos of a + * list of owning albums are fetched. + * The unified result of the query is mapped to the specific albums + * by {@link HasManyPhotosByTag::match()}. + * + * @param TagAlbum[] $albums an array of {@link \App\Models\TagAlbum} whose photos are loaded + * + * @return void + * + * @throws InternalLycheeException + */ + public function addEagerConstraints(array $albums): void + { + if (count($albums) !== 1) { + throw new NotImplementedException('eagerly fetching all photos of an album is not implemented for multiple albums'); + } + /** @var TagAlbum $album */ + $album = $albums[0]; + $tags = $album->show_tags; + + $this->photoQueryPolicy + ->applySearchabilityFilter( + $this->getRelationQuery(), + origin: null, + include_nsfw: !Configs::getValueAsBool('hide_nsfw_in_smart_albums') + ) + ->where(function (Builder $q) use ($tags) { + // Filter for requested tags + foreach ($tags as $tag) { + $q->where('tags', 'like', '%' . trim($tag) . '%'); + } + }); + } + + /** + * Maps a collection of eagerly fetched photos to the given owning albums. + * + * This method is called by the framework after the unified result of + * photos has been fetched by {@link HasManyPhotosByTag::addEagerConstraints()}. + * + * @param TagAlbum[] $albums the list of owning albums + * @param Collection $photos collection of {@link Photo} models which needs to be mapped to the albums + * @param string $relation the name of the relation + * + * @return array + * + * @throws NotImplementedException + */ + public function match(array $albums, Collection $photos, $relation): array + { + if (count($albums) !== 1) { + throw new NotImplementedException('eagerly fetching all photos of an album is not implemented for multiple albums'); + } + /** @var TagAlbum $album */ + $album = $albums[0]; + $sorting = $album->getEffectivePhotoSorting(); + + $photos = $photos->sortBy( + $sorting->column->value, + in_array($sorting->column, SortingDecorator::POSTPONE_COLUMNS, true) ? SORT_NATURAL | SORT_FLAG_CASE : SORT_REGULAR, + $sorting->order === OrderSortingType::DESC + )->values(); + $album->setRelation($relation, $photos); + + return $albums; + } +} diff --git a/app/Relations/HasManyPhotosRecursively.php b/app/Relations/HasManyPhotosRecursively.php new file mode 100644 index 00000000000..a8c7b8d4df8 --- /dev/null +++ b/app/Relations/HasManyPhotosRecursively.php @@ -0,0 +1,144 @@ + + */ +class HasManyPhotosRecursively extends BaseHasManyPhotos +{ + protected AlbumQueryPolicy $albumQueryPolicy; + + public function __construct(Album $owningAlbum) + { + // Sic! We must initialize attributes of this class before we call + // the parent constructor. + // The parent constructor calls `addConstraints` and thus our own + // attributes must be initialized by then + $this->albumQueryPolicy = resolve(AlbumQueryPolicy::class); + parent::__construct($owningAlbum); + } + + public function getParent(): Album + { + /** + * We know that the parent is of type `Album`, + * because it was set in the constructor as `$owningAlbum`. + * + * @noinspection PhpIncompatibleReturnTypeInspection + */ + return $this->parent; + } + + /** + * Adds the constraints for single owning album to the base query. + * + * This method is called by the framework, if the related photos of a + * single albums are fetched. + * + * @throws InternalLycheeException + */ + public function addConstraints(): void + { + if (static::$constraints) { + $this->addEagerConstraints([$this->getParent()]); + } + } + + /** + * Adds the constraints for a list of owning album to the base query. + * + * This method is called by the framework, if the related photos of a + * list of owning albums are fetched. + * The unified result of the query is mapped to the specific albums + * by {@link HasManyPhotosRecursively::match()}. + * + * @param Album[] $albums an array of {@link \App\Models\Album} whose photos are loaded + * + * @return void + * + * @throws InternalLycheeException + */ + public function addEagerConstraints(array $albums): void + { + if (count($albums) !== 1) { + throw new NotImplementedException('eagerly fetching all photos of an album is not implemented for multiple albums'); + } + + $this->photoQueryPolicy + ->applySearchabilityFilter( + query: $this->getRelationQuery(), + origin: $albums[0], + include_nsfw: true + ); + } + + /** + * @return Collection + */ + public function getResults(): Collection + { + /** @var Album|null $album */ + $album = $this->parent; + if ($album === null || !Gate::check(AlbumPolicy::CAN_ACCESS, $album)) { + return $this->related->newCollection(); + } else { + return parent::getResults(); + } + } + + /** + * Maps a collection of eagerly fetched photos to the given owning albums. + * + * This method is called by the framework after the unified result of + * photos has been fetched by {@link HasManyPhotosRecursively::addEagerConstraints()}. + * + * @param Album[] $albums the list of owning albums + * @param Collection $photos collection of {@link Photo} models which needs to be mapped to the albums + * @param string $relation the name of the relation + * + * @return array + * + * @throws NotImplementedException + */ + public function match(array $albums, Collection $photos, $relation): array + { + if (count($albums) !== 1) { + throw new NotImplementedException('eagerly fetching all photos of an album is not implemented for multiple albums'); + } + /** @var Album $album */ + $album = $albums[0]; + + if (!Gate::check(AlbumPolicy::CAN_ACCESS, $album)) { + $album->setRelation($relation, $this->related->newCollection()); + } else { + $sorting = $album->getEffectivePhotoSorting(); + $photos = $photos->sortBy( + $sorting->column->value, + in_array($sorting->column, SortingDecorator::POSTPONE_COLUMNS, true) ? SORT_NATURAL | SORT_FLAG_CASE : SORT_REGULAR, + $sorting->order === OrderSortingType::DESC + )->values(); + $album->setRelation($relation, $photos); + } + + return $albums; + } +} diff --git a/app/Relations/HasManySizeVariants.php b/app/Relations/HasManySizeVariants.php new file mode 100644 index 00000000000..a856a5412c5 --- /dev/null +++ b/app/Relations/HasManySizeVariants.php @@ -0,0 +1,141 @@ + + */ +class HasManySizeVariants extends HasMany +{ + public function __construct(Photo $owningPhoto) + { + parent::__construct( + SizeVariant::query(), + $owningPhoto, + 'photo_id', + 'id' + ); + } + + /** + * Get the results of the relationship. + * + * @internal The parent class allows to return `mixed`, hence it is + * perfectly fine to return `SizeVariants` acc. to Liskov's substitution + * principle. + * However, the buggy `larastan` ruleset lies to PhpStan about the return + * type. + * Hence, we must ignore the false positive here. + * + * @return SizeVariants + * + * @phpstan-ignore-next-line + */ + public function getResults(): SizeVariants + { + /** @var Photo $parent */ + $parent = $this->parent; + + return new SizeVariants($parent, + is_null($this->getParentKey()) ? + $this->related->newCollection() : + $this->query->get() + ); + } + + /** + * Initialize the relation on a set of models. + * + * @param Photo[] $models + * @param string $relation + * + * @return array + */ + public function initRelation(array $models, $relation): array + { + /** @var Photo $model */ + foreach ($models as $model) { + $model->setRelation( + $relation, + new SizeVariants($model, $this->related->newCollection()) + ); + } + + return $models; + } + + /** + * Match the eagerly loaded results to their parents. + * + * This method is identical to + * {@link \Illuminate\Database\Eloquent\Relations\HasOneOrMany::matchOneOrMany} + * but additionally sets the reverse association of the child object + * back to its parent object. + * + * @param Photo[] $models an array of parent models + * @param Collection $results the unified collection of all child models of all parent models + * @param string $relation the name of the relation from the parent to the child models + * + * @return array + */ + public function match(array $models, Collection $results, $relation): array + { + $dictionary = $this->buildDictionary($results); + + // Once we have the dictionary we can simply spin through the parent models to + // link them up with their children using the keyed dictionary to make the + // matching very convenient and easy work. Then we'll just return them. + /** @var Photo $model */ + foreach ($models as $model) { + if (isset($dictionary[$key = $this->getDictionaryKey($model->getAttribute($this->localKey))])) { + /** @var Collection $childrenOfModel */ + $childrenOfModel = $this->getRelationValue($dictionary, $key, 'many'); + $model->setRelation($relation, new SizeVariants($model, $childrenOfModel)); + } + } + + return $models; + } + + /** + * Set the foreign ID for creating a related model. + * + * @param Model $model + * + * @return void + * + * @throws InternalLycheeException + */ + protected function setForeignAttributesForCreate(Model $model) + { + try { + if (!($model instanceof SizeVariant)) { + throw new LycheeInvalidArgumentException('model must be an instance of SizeVariant'); + } + $model->setAttribute('photo_id', $this->getParentKey()); + $model->setRelation('photo', $this->parent); + } catch (EncryptException|InvalidCastException|JsonEncodingException $e) { + // thrown by Eloquent\Model::setAttribute + throw new FrameworkException('Eloquent\'s model', $e); + } + } +} diff --git a/app/Response.php b/app/Response.php deleted file mode 100644 index 88e7ff75ec5..00000000000 --- a/app/Response.php +++ /dev/null @@ -1,49 +0,0 @@ -passes('', $albumID); + } + + return $success; + } + + /** + * {@inheritDoc} + */ + public function message(): string + { + return ':attribute must be a comma-separated string of strings with either ' . + RandomID::ID_LENGTH . ' characters each or one of the built-in IDs ' . + implode(', ', SmartAlbumType::values()); + } +} diff --git a/app/Rules/AlbumIDRule.php b/app/Rules/AlbumIDRule.php new file mode 100644 index 00000000000..bd510072fbb --- /dev/null +++ b/app/Rules/AlbumIDRule.php @@ -0,0 +1,47 @@ +isNullable = $isNullable; + } + + /** + * {@inheritDoc} + */ + public function passes(string $attribute, mixed $value): bool + { + return + ($value === null && $this->isNullable) || + strlen($value) === RandomID::ID_LENGTH || + SmartAlbumType::tryFrom($value) !== null; + } + + /** + * {@inheritDoc} + */ + public function message(): string + { + return ':attribute must be' . + ($this->isNullable ? ' either null, or' : '') . + ' a string with ' . RandomID::ID_LENGTH . ' characters or one of the built-in IDs ' . + implode(', ', SmartAlbumType::values()); + } +} diff --git a/app/Rules/BooleanRequireSupportRule.php b/app/Rules/BooleanRequireSupportRule.php new file mode 100644 index 00000000000..5769c097a31 --- /dev/null +++ b/app/Rules/BooleanRequireSupportRule.php @@ -0,0 +1,44 @@ +verify = $verify; + $this->expected = $expected; + } + + /** + * {@inheritDoc} + */ + public function validate(string $attribute, mixed $value, \Closure $fail): void + { + $value = filter_var($value, FILTER_VALIDATE_BOOLEAN); + if ($value === $this->expected) { + return; + } + + if ($this->verify->is_supporter()) { + return; + } + + $fail('Error: This functionality is only available in the Supporter Edition of Lychee. See here: https://lycheeorg.dev/get-supporter-edition/'); + } +} diff --git a/app/Rules/ConfigKeyRequireSupportRule.php b/app/Rules/ConfigKeyRequireSupportRule.php new file mode 100644 index 00000000000..4102895dfbf --- /dev/null +++ b/app/Rules/ConfigKeyRequireSupportRule.php @@ -0,0 +1,50 @@ +verify = $verify; + } + + /** + * {@inheritDoc} + */ + public function validate(string $attribute, mixed $value, \Closure $fail): void + { + if (is_string($value) === false) { + // This is taken care of in ConfigKeyRule + return; + } + + /** @var string $value */ + if (!array_key_exists($value, Configs::get())) { + // This is taken care of in ConfigKeyRule + return; + } + + /** @var string $value */ + $config = Configs::where('key', '=', $value)->firstOrFail(); + if ($config->level === 1 && !$this->verify->is_supporter()) { + $fail('Error: This functionality is only available in the Supporter Edition of Lychee. See here: https://lycheeorg.dev/get-supporter-edition/'); + + return; + } + } +} diff --git a/app/Rules/ConfigKeyRule.php b/app/Rules/ConfigKeyRule.php new file mode 100644 index 00000000000..cb795f7b32b --- /dev/null +++ b/app/Rules/ConfigKeyRule.php @@ -0,0 +1,34 @@ + */ + private Collection $configs; + + /** + * All of the data under validation. + * + * @var array + */ + protected $data = []; + + public function __construct() + { + $this->configs = Configs::all(); + } + + /** + * Set the data under validation. + * + * @param array $data + * + * @phpstan-ignore-next-line + */ + public function setData(array $data): static + { + $this->data = $data; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function passes(string $attribute, mixed $value): bool + { + $path = explode('.', $attribute); + if (count($path) !== 3) { + throw new LycheeLogicException('ConfigValueRule: attribute must be in the form of "xxx.*.value"'); + } + + $template = 'Error: Expected %s, got ' . ($value ?? 'NULL') . '.'; + $array_key = $this->data[$path[0]][intval($path[1])]['key']; + + return '' === $this->configs->first(fn (Configs $c) => $c->key === $array_key)->sanity($value, $template); + } + + /** + * {@inheritDoc} + */ + public function message(): string + { + return ':attribute is not a valid configuration value.'; + } +} diff --git a/app/Rules/CopyrightRule.php b/app/Rules/CopyrightRule.php new file mode 100644 index 00000000000..372d3955459 --- /dev/null +++ b/app/Rules/CopyrightRule.php @@ -0,0 +1,17 @@ +password); + } + + /** + * {@inheritDoc} + */ + public function message(): string + { + return ':attribute is invalid.'; + } +} diff --git a/app/Rules/DescriptionRule.php b/app/Rules/DescriptionRule.php new file mode 100644 index 00000000000..b563ea407a1 --- /dev/null +++ b/app/Rules/DescriptionRule.php @@ -0,0 +1,17 @@ + This is usually a container of allowed values for backed enum */ + protected array $expected; + + /** + * Create a new rule instance. + * + * @param class-string $type + * @param array $expected + * @param VerifyInterface $verify + * + * @return void + */ + public function __construct(mixed $type, array $expected, VerifyInterface $verify) + { + $this->type = $type; + $this->verify = $verify; + $this->expected = $expected; + } + + /** + * {@inheritDoc} + */ + public function validate(string $attribute, mixed $value, \Closure $fail): void + { + if ($value === null || !enum_exists($this->type) || !method_exists($this->type, 'tryFrom')) { + return; + } + + try { + // Enum version + $value = $this->type::tryFrom($value); + + if ($value !== null && $this->isDesirable($value)) { + return; + } + } catch (\TypeError) { + return; + } + + if ($this->verify->is_supporter()) { + return; + } + + $fail('Error: This functionality is only available in the Supporter Edition of Lychee. See here: https://lycheeorg.dev/get-supporter-edition/'); + } + + /** + * Determine if the given case is a valid case based on the only / except values. + * + * @param mixed $value + * + * @return bool + */ + protected function isDesirable($value) + { + return in_array(needle: $value, haystack: $this->expected, strict: true); + } +} diff --git a/app/Rules/ExtensionRule.php b/app/Rules/ExtensionRule.php new file mode 100644 index 00000000000..2949c027d26 --- /dev/null +++ b/app/Rules/ExtensionRule.php @@ -0,0 +1,102 @@ + + */ + protected $data = []; + + /** + * Set the data under validation. + * + * @param array $data + * + * @phpstan-ignore-next-line + */ + public function setData(array $data): static + { + $this->data = $data; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function validate(string $attribute, mixed $value, \Closure $fail): void + { + $value = $value === '' ? null : $value; + + if ($attribute !== 'extension') { + throw new LycheeLogicException('ExtensionRule: attribute must be "extension"'); + } + + $chunk_number = intval($this->data['chunk_number'] ?? null); + if ($chunk_number === 0) { + return; // we are going to fail elsewhere. + } + + if ($chunk_number === 1 && $value === null) { + return; // it is normal that it is not set yet. + } + + if ($chunk_number === 1 && $value !== null) { + $fail('Error: Expected NULL in :attribute , got ' . $value . '.'); + + return; // it is not normal that it is set. + } + + if (is_string($value) === false) { + $fail(':attribute is not a string.'); + + return; + } + + if (Str::of($value)->isMatch('/^\.[a-zA-Z0-9]*$/') === false) { + $fail(':attribute is not a valid extension.'); + + return; + } + + $file_name = $this->data['file_name'] ?? null; + if ($file_name === null) { + return; // we are going to fail elsewhere + } + + $extension = '.' . pathinfo($file_name, PATHINFO_EXTENSION); + if ($value !== $extension) { + $fail('Error: Expected ' . $extension . ' in :attribute, got ' . $value . '.'); + + return; + } + + $file_name = $this->data['uuid_name'] ?? null; + if ($file_name === null) { + return; // we are going to fail elsewhere if chunk is not 1. + } + + $extension = '.' . pathinfo($this->data['uuid_name'] ?? '', PATHINFO_EXTENSION); + if ($value !== $extension) { + $fail('Error: Expected ' . $extension . ' in :attribute, got ' . $value . '.'); + } + } +} diff --git a/app/Rules/FileUuidRule.php b/app/Rules/FileUuidRule.php new file mode 100644 index 00000000000..90995b9dae1 --- /dev/null +++ b/app/Rules/FileUuidRule.php @@ -0,0 +1,94 @@ + + */ + protected $data = []; + + /** + * Set the data under validation. + * + * @param array $data + * + * @phpstan-ignore-next-line + */ + public function setData(array $data): static + { + $this->data = $data; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function validate(string $attribute, mixed $value, \Closure $fail): void + { + $value = $value === '' ? null : $value; + + if ($attribute !== 'uuid_name') { + throw new LycheeLogicException('FileUuidRule: attribute must be "uuid_name"'); + } + + $chunk_number = intval($this->data['chunk_number'] ?? null); + if ($chunk_number === 0) { + return; // we are going to fail elsewhere. + } + + if ($chunk_number === 1 && $value === null) { + return; // it is normal that it is not set yet. + } + + if ($chunk_number === 1 && $value !== null) { + $fail('Error: Expected NULL in :attribute , got ' . $value . '.'); + + return; // it is not normal that it is set. + } + + if (is_string($value) === false) { + $fail(':attribute is not a string.'); + + return; + } + + $file_name = $this->data['file_name'] ?? null; + if ($file_name === null) { + return; // we are going to fail elsewhere + } + + $extension = pathinfo($file_name, PATHINFO_EXTENSION); + + $pattern = '/[a-zA-Z0-9-_]{16}\.' . $extension . '/'; + if (Str::of($value)->isMatch($pattern) === false) { + $fail(':attribute is not a valid random string.'); + + return; + } + + if (!Storage::disk(PhotoController::DISK_NAME)->exists($value)) { + $fail(':attribute is not a valid target file.'); + } + } +} diff --git a/app/Rules/IntegerIDRule.php b/app/Rules/IntegerIDRule.php new file mode 100644 index 00000000000..b3f74413948 --- /dev/null +++ b/app/Rules/IntegerIDRule.php @@ -0,0 +1,54 @@ +isNullable = $isNullable; + $this->isRelaxed = $isRelaxed; + } + + /** + * {@inheritDoc} + */ + public function passes(string $attribute, mixed $value): bool + { + return + ( + $value === null && + $this->isNullable + ) || ( + $this->isRelaxed && + filter_var($value, FILTER_VALIDATE_INT) !== false && + intval($value) > 0 + ) || ( + is_int($value) && + intval($value) > 0 + ); + } + + /** + * {@inheritDoc} + */ + public function message(): string + { + return ':attribute must be' . + ($this->isNullable ? ' either null or' : '') . + ' a non-zero, positive integer'; + } +} diff --git a/app/Rules/IntegerRequireSupportRule.php b/app/Rules/IntegerRequireSupportRule.php new file mode 100644 index 00000000000..5b106cd472d --- /dev/null +++ b/app/Rules/IntegerRequireSupportRule.php @@ -0,0 +1,43 @@ +verify = $verify; + $this->expected = $expected; + } + + /** + * {@inheritDoc} + */ + public function validate(string $attribute, mixed $value, \Closure $fail): void + { + if (is_int($value) && intval($value) === $this->expected) { + return; + } + + if ($this->verify->is_supporter()) { + return; + } + + $fail('Error: This functionality is only available in the Supporter Edition of Lychee. See here: https://lycheeorg.dev/get-supporter-edition/'); + } +} diff --git a/app/Rules/PasswordRule.php b/app/Rules/PasswordRule.php new file mode 100644 index 00000000000..5f0db6a623d --- /dev/null +++ b/app/Rules/PasswordRule.php @@ -0,0 +1,17 @@ +passes('', $randomID); + } + + return $success; + } + + /** + * {@inheritDoc} + */ + public function message(): string + { + return ':attribute must be a comma-separated string of strings with ' . RandomID::ID_LENGTH . ' characters each.'; + } +} diff --git a/app/Rules/RandomIDRule.php b/app/Rules/RandomIDRule.php new file mode 100644 index 00000000000..c72a4931f7e --- /dev/null +++ b/app/Rules/RandomIDRule.php @@ -0,0 +1,47 @@ +isNullable = $isNullable; + } + + /** + * {@inheritDoc} + */ + public function passes(string $attribute, mixed $value): bool + { + return + ( + $value === null && + $this->isNullable + ) || preg_match('/^[-_a-zA-Z0-9]{' . RandomID::ID_LENGTH . '}$/', $value) === 1; + } + + /** + * {@inheritDoc} + */ + public function message(): string + { + return ':attribute must be' . + ($this->isNullable ? ' either null or' : '') . + ' a string in Base64-encoding with ' . RandomID::ID_LENGTH . ' characters'; + } +} diff --git a/app/Rules/StringRequireSupportRule.php b/app/Rules/StringRequireSupportRule.php new file mode 100644 index 00000000000..5fd9c19b169 --- /dev/null +++ b/app/Rules/StringRequireSupportRule.php @@ -0,0 +1,44 @@ +verify = $verify; + $this->expected = $expected === '' ? null : $expected; + } + + /** + * {@inheritDoc} + */ + public function validate(string $attribute, mixed $value, \Closure $fail): void + { + $value = $value === '' ? null : $value; + if ($value === $this->expected) { + return; + } + + if ($this->verify->is_supporter()) { + return; + } + + $fail('Error: This functionality is only available in the Supporter Edition of Lychee. See here: https://lycheeorg.dev/get-supporter-edition/'); + } +} diff --git a/app/Rules/StringRule.php b/app/Rules/StringRule.php new file mode 100644 index 00000000000..5bf78d0f6a8 --- /dev/null +++ b/app/Rules/StringRule.php @@ -0,0 +1,55 @@ +isNullable = $isNullable; + $this->limit = $limit; + } + + /** + * {@inheritDoc} + */ + public function passes(string $attribute, mixed $value): bool + { + return ($value === null && + $this->isNullable + ) || (is_string($value) && + strlen($value) !== 0 && + ($this->limit === 0 || strlen($value) <= $this->limit) + ); + } + + /** + * {@inheritDoc} + */ + public function message(): string + { + return ':attribute must be' . + ($this->isNullable ? ' either null or' : '') . + ' a non-empty string' . + ($this->limit !== 0 ? ' with at most ' . $this->limit . ' characters' : ''); + } +} diff --git a/app/Rules/TitleRule.php b/app/Rules/TitleRule.php new file mode 100644 index 00000000000..c4e2d14aa11 --- /dev/null +++ b/app/Rules/TitleRule.php @@ -0,0 +1,17 @@ + 10. + */ +trait ValidateTrait +{ + /** + * {@inheritDoc} + */ + public function validate(string $attribute, mixed $value, \Closure $fail): void + { + if (!$this->passes($attribute, $value)) { + $fail($this->message()); + } + } +} diff --git a/app/Services/Auth/SessionOrTokenGuard.php b/app/Services/Auth/SessionOrTokenGuard.php new file mode 100644 index 00000000000..fd8802590d8 --- /dev/null +++ b/app/Services/Auth/SessionOrTokenGuard.php @@ -0,0 +1,357 @@ + $config + * + * @throws BindingResolutionException + */ + public static function createGuard(Application $app, string $name, array $config): self + { + $userProvider = Auth::createUserProvider($config['provider']); + $guard = new self($name, $userProvider, $app->make('session.store')); + $guard->setCookieJar($app->make('cookie')); + $guard->setDispatcher($app->make('events')); + /** @disregard P1013 */ + $guard->setRequest($app->refresh('request', $guard, 'setRequest')); + if (isset($config['remember'])) { + $guard->setRememberDuration($config['remember']); + } + + return $guard; + } + + /** + * Returns the user of the current request. + * + * This method is a merger of + * {@link SessionGuard::user} and {@link \Illuminate\Auth\TokenGuard::user}. + * This method is the main "working horse" and behaves as described in + * the class comment. + * + * @return ?Authenticatable + * + * @throws BadRequestHeaderException + * @throws \RuntimeException + */ + public function user(): Authenticatable|null + { + // If we've already retrieved the user for the current request we can just + // return it back immediately. We do not want to fetch the user data on + // every call to this method because that would be tremendously slow. + if ($this->user !== null) { + return $this->user; + } + + // First, try to get a user by token. + $userByToken = $this->getUserByToken(); + + // Second, try to get a user by stored user ID on the session. + $userIdBySession = $this->session->get($this->getName()); + $userBySession = $userIdBySession !== null ? $this->provider->retrieveById($userIdBySession) : null; + + // Third, if `$userBySession` is null, but we decrypt a "recaller" + // cookie we attempt to pull the user data from that cookie which + // serves as a remember-me cookie + $recaller = $userBySession === null ? $this->recaller() : null; + $userByRecaller = $recaller !== null ? $this->userFromRecaller($recaller) : null; + + // We step through the different combinations which may happen, + // because we use a combination of token and session. + if ($userBySession !== null) { + if ($userByToken === null || $userBySession->getAuthIdentifier() === $userByToken->getAuthIdentifier()) { + // We are good, no contradiction! + // We call the parent method here to skip the additional token + // check added by the overwritten method of this class. + parent::setUser($userBySession); + // `setUser()` sets `authState` to stateless, but here we + // used the user from a previous session _without_ logging in + // again, hence we must set `authState` explicitly. + $this->authState = self::AUTH_STATE_STATEFUL; + } else { + throw new BadRequestHeaderException('Token- and session-based user mismatch'); + } + } elseif ($userByToken !== null) { + // A token-based authentication is considered stateless, so we + // call `setUser` and not `login`. + // We call the parent method here to skip the additional token + // check added by the overwritten method of this class. + parent::setUser($userByToken); + // As we called the parent method `setUser`, we must set the + // new authentication state explicitly. + $this->authState = self::AUTH_STATE_STATELESS; + } elseif ($userByRecaller !== null) { + $this->login($userByRecaller, true); + } else { + // In the other cases, `$this->user` has implicitly been set by + // `parent::setUser` or `$this->login`. + $this->authState = self::AUTH_STATE_UNAUTHENTICATED; + $this->user = null; + } + + return $this->user; + } + + /** + * Get the ID for the currently authenticated user. + * + * This is a fixed variant of {@link \Illuminate\Auth\TokenGuard::id} + * which uses PHP 8 syntax and ensures that a value is always returned. + * We don't use the complicated variant of {@link SessionGuard::id}, + * because {@link SessionOrTokenGuard::user()} ensures that + * {@link SessionOrTokenGuard::$loggedOut} and + * {@link SessionOrTokenGuard::$user} are always consistent. + * + * @return int|string|null + * + * @throws BadRequestHeaderException + * @throws \RuntimeException + */ + public function id(): int|string|null + { + return $this->user()?->getAuthIdentifier(); + } + + /** + * Sets the given user without changing the session. + * + * If an API token is given, setting another user than the user given by + * the API token is considered an error. + * + * If the method succeeds, {@link SessionOrTokenGuard::$authState} equals + * {@link SessionOrTokenGuard::AUTH_STATE_STATELESS} afterwards. + * + * @return $this + * + * @throws BadRequestHeaderException + */ + public function setUser(Authenticatable $user): static + { + $userByToken = $this->getUserByToken(); + if ($userByToken !== null && $user->getAuthIdentifier() !== $userByToken->getAuthIdentifier()) { + throw new BadRequestHeaderException('Cannot set another user than the one provided by the API token'); + } + parent::setUser($user); + $this->authState = self::AUTH_STATE_STATELESS; + + return $this; + } + + /** + * Logs-in the given user stateful. + * + * If an API token is given, logging in another user than the user + * given by the API token is considered an error. + * + * If the method succeeds, {@link SessionOrTokenGuard::$authState} equals + * {@link SessionOrTokenGuard::AUTH_STATE_STATEFUL} afterwards. + * + * @param AuthenticatableContract $user + * @param bool $remember + * + * @return void + * + * @throws BadRequestHeaderException + * @throws \RuntimeException + */ + public function login(AuthenticatableContract $user, $remember = false): void + { + parent::login($user, $remember); + $this->authState = self::AUTH_STATE_STATEFUL; + } + + /** + * Logs out the current stateful user. + * + * If the method succeeds, {@link SessionOrTokenGuard::$authState} equals + * {@link SessionOrTokenGuard::AUTH_STATE_STATELESS} or + * {@link SessionOrTokenGuard::AUTH_STATE_UNAUTHENTICATED} afterwards, + * depending on whether a token is given in the request or not. + * + * @return void + * + * @throws BadRequestHeaderException + * @throws \RuntimeException + */ + public function logout(): void + { + parent::logout(); + $this->authState = self::AUTH_STATE_UNAUTHENTICATED; + + // Re-authenticate as token-based user if given. + $userByToken = $this->getUserByToken(); + if ($userByToken !== null) { + // A token-based authentication is considered stateless, so we + // call `setUser` and not `login`. + // We call the parent method here to skip the additional token + // check added by the overwritten method of this class. + parent::setUser($userByToken); + // As we called the parent method `setUser`, we must set the + // new authentication state explicitly. + $this->authState = self::AUTH_STATE_STATELESS; + } + } + + /** + * Returns the user denoted by the token in the HTTP header. + * + * @return ?Authenticatable The user denoted by the HTTP header or `null` + * if HTTP header is not set + * + * @throws BadRequestHeaderException thrown if the HTTP header with a + * token is set, but no corresponding + * user can be found + */ + protected function getUserByToken(): ?Authenticatable + { + $token = $this->request->headers->get(self::HTTP_TOKEN_HEADER); + + // Skip if token is not found. + if ($token === null || !is_string($token) || $token === '') { + return null; + } + + // Skip if token starts with Basic: it is not related to Lychee. + if (Str::startsWith('Basic', $token)) { + return null; + } + + // Check if token starts with Bearer + $hasBearer = Str::startsWith('Bearer', $token); + /** @var bool $configLog */ + $configLog = config('auth.token_guard.log_warn_no_scheme_bearer'); + /** @var bool $configThrow */ + $configThrow = config('auth.token_guard.fail_bearer_authenticable_not_found', true); + + // If Token does not start with Bearer + if (!$hasBearer && $configLog) { + Log::warning('Auth token found, but Bearer prefix not provided.'); + } + + // Remove prefix and fetch authenticable. + $token = trim(Str::remove('Bearer', $token)); + $authenticable = $this->provider->retrieveByCredentials([ + self::TOKEN_COLUMN_NAME => hash(self::TOKEN_HASH_METHOD, $token), + ]); + + return match (true) { + $authenticable !== null => $authenticable, + $hasBearer && $configThrow => throw new BadRequestHeaderException('Invalid token'), + $hasBearer => null, + default => throw new BadRequestHeaderException('Invalid token'), + }; + } +} diff --git a/app/SmartAlbums/BareSmartAlbum.php b/app/SmartAlbums/BareSmartAlbum.php deleted file mode 100644 index d3583d0bcc6..00000000000 --- a/app/SmartAlbums/BareSmartAlbum.php +++ /dev/null @@ -1,94 +0,0 @@ -albumIds = new BaseCollection(); - $this->created_at = new Carbon(); - $this->updated_at = new Carbon(); - $this->smart = true; - } - - /** - * Set a restriction on the available albums. - * - * @param BaseCollection[int] $albumIds - * - * @return void - */ - public function setAlbumIDs(BaseCollection $albumIds): void - { - $this->albumIds = $albumIds; - } - - public function filter($query) - { - if (AccessControl::is_admin()) { - return $query; - } - - if (AccessControl::is_logged_in()) { - $query = $query->where('owner_id', '=', AccessControl::id()) - ->orWhere( - fn ($q) => $q->whereNotNull('album_id') - ->whereIn('album_id', $this->albumIds) - ); - } else { - $query = $query->whereIn('album_id', $this->albumIds); - } - - if (Configs::get_value('public_photos_hidden', '1') === '0') { - $query = $query->orWhere('public', '=', 1); - } - - return $query; - } - - /*------------------------- STRINGS --------------------------------- */ - public function str_parent_id() - { - return ''; - } - - public function get_license(): string - { - return 'none'; - } -} diff --git a/app/SmartAlbums/BaseSmartAlbum.php b/app/SmartAlbums/BaseSmartAlbum.php new file mode 100644 index 00000000000..38b8955cfe8 --- /dev/null +++ b/app/SmartAlbums/BaseSmartAlbum.php @@ -0,0 +1,174 @@ + */ + protected ?Collection $photos = null; + protected \Closure $smartPhotoCondition; + protected AccessPermission|null $publicPermissions; + + /** + * @throws ConfigurationKeyMissingException + * @throws FrameworkException + */ + protected function __construct(SmartAlbumType $id, \Closure $smartCondition) + { + try { + $this->photoQueryPolicy = resolve(PhotoQueryPolicy::class); + $this->id = $id->value; + $this->title = __('gallery.smart_album.' . strtolower($id->name)) ?? $id->name; + $this->smartPhotoCondition = $smartCondition; + /** @var AccessPermission|null $perm */ + $perm = AccessPermission::query()->where('base_album_id', '=', $id->value)->first(); + $this->publicPermissions = $perm; + } catch (BindingResolutionException $e) { + throw new FrameworkException('Laravel\'s service container', $e); + } + } + + /** + * @return \App\Eloquent\FixedQueryBuilder + * + * @throws InternalLycheeException + */ + public function photos(): Builder + { + $query = $this->photoQueryPolicy + ->applySearchabilityFilter( + query: Photo::query()->with(['album', 'size_variants', 'size_variants.sym_links']), + origin: null, + include_nsfw: !Configs::getValueAsBool('hide_nsfw_in_smart_albums') + )->where($this->smartPhotoCondition); + + return $query; + } + + /** + * @return Collection + * + * @throws InvalidOrderDirectionException + * @throws InvalidQueryModelException + */ + protected function getPhotosAttribute(): Collection + { + // Cache query result for later use + // (this mimics the behaviour of relations of true Eloquent models) + if ($this->photos === null) { + $sorting = PhotoSortingCriterion::createDefault(); + + /** @var \Illuminate\Database\Eloquent\Collection&iterable $photos */ + $photos = (new SortingDecorator($this->photos())) + ->orderPhotosBy($sorting->column, $sorting->order) + ->get(); + $this->photos = $photos; + } + + return $this->photos; + } + + /** + * Similar to the function above. + * The big difference is that we do not check if it is null or not. + * + * @return Collection|null + */ + public function getPhotos(): ?Collection + { + return $this->photos; + } + + /** + * @throws InvalidPropertyException + * @throws InvalidQueryModelException + */ + protected function getThumbAttribute(): ?Thumb + { + /* + * Note, `photos()` already applies a "security filter" and + * only returns photos which are accessible by the current + * user. + */ + $this->thumb ??= Configs::getValueAsBool('SA_random_thumbs') + ? Thumb::createFromRandomQueryable($this->photos()) + : $this->thumb = Thumb::createFromQueryable( + $this->photos(), + PhotoSortingCriterion::createDefault() + ); + + return $this->thumb; + } + + public function public_permissions(): ?AccessPermission + { + return $this->publicPermissions; + } + + public function setPublic(): void + { + if ($this->publicPermissions !== null) { + return; + } + + $this->publicPermissions = AccessPermission::ofPublic(); + $this->publicPermissions->base_album_id = $this->id; + $this->publicPermissions->save(); + } + + public function setPrivate(): void + { + if ($this->publicPermissions === null) { + return; + } + + $perm = $this->publicPermissions; + $this->publicPermissions = null; + $perm->delete(); + } +} diff --git a/app/SmartAlbums/OnThisDayAlbum.php b/app/SmartAlbums/OnThisDayAlbum.php new file mode 100644 index 00000000000..390884e19f9 --- /dev/null +++ b/app/SmartAlbums/OnThisDayAlbum.php @@ -0,0 +1,53 @@ +value; + + /** + * @throws InvalidFormatException + * @throws InvalidTimeZoneException + * @throws ConfigurationKeyMissingException + * @throws FrameworkException + */ + protected function __construct() + { + $today = Carbon::today(); + + parent::__construct( + SmartAlbumType::ON_THIS_DAY, + function (Builder $query) use ($today) { + $query->where(fn (Builder $q) => $q + ->whereMonth('photos.taken_at', '=', $today->month) + ->whereDay('photos.taken_at', '=', $today->day)) + ->orWhere(fn (Builder $q) => $q + ->whereNull('photos.taken_at') + ->whereYear('photos.created_at', '<', $today->year) + ->whereMonth('photos.created_at', '=', $today->month) + ->whereDay('photos.created_at', '=', $today->day)); + } + ); + } + + public static function getInstance(): self + { + return self::$instance ??= new self(); + } +} diff --git a/app/SmartAlbums/PublicAlbum.php b/app/SmartAlbums/PublicAlbum.php deleted file mode 100644 index 67d41a4f4b3..00000000000 --- a/app/SmartAlbums/PublicAlbum.php +++ /dev/null @@ -1,23 +0,0 @@ -title = 'public'; - } - - public function get_photos(): Builder - { - return Photo::public()->where(fn ($q) => $this->filter($q)); - } -} diff --git a/app/SmartAlbums/RecentAlbum.php b/app/SmartAlbums/RecentAlbum.php index 407945c902e..586a9521e39 100644 --- a/app/SmartAlbums/RecentAlbum.php +++ b/app/SmartAlbums/RecentAlbum.php @@ -1,25 +1,49 @@ value; - public function __construct() + /** + * @throws InvalidFormatException + * @throws InvalidTimeZoneException + * @throws ConfigurationKeyMissingException + * @throws FrameworkException + */ + protected function __construct() { - parent::__construct(); + $strRecent = $this->fromDateTime( + Carbon::now()->subDays(Configs::getValueAsInt('recent_age')) + ); - $this->title = 'recent'; - $this->public = Configs::get_value('public_recent', '0') === '1'; + parent::__construct( + SmartAlbumType::RECENT, + function (Builder $query) use ($strRecent) { + $query->where('photos.created_at', '>=', $strRecent); + } + ); } - public function get_photos(): Builder + public static function getInstance(): self { - return Photo::recent()->where(fn ($q) => $this->filter($q)); + return self::$instance ??= new self(); } } diff --git a/app/SmartAlbums/SmartAlbum.php b/app/SmartAlbums/SmartAlbum.php deleted file mode 100644 index a3ae555ae03..00000000000 --- a/app/SmartAlbums/SmartAlbum.php +++ /dev/null @@ -1,69 +0,0 @@ -value; - public function __construct() + /** + * @throws ConfigurationKeyMissingException + * @throws FrameworkException + */ + protected function __construct() { - parent::__construct(); - - $this->title = 'starred'; - $this->public = Configs::get_value('public_starred', '0') === '1'; + parent::__construct( + SmartAlbumType::STARRED, + fn (Builder $q) => $q->where('photos.is_starred', '=', true) + ); } - public function get_photos(): Builder + public static function getInstance(): self { - return Photo::stars()->where(fn ($q) => $this->filter($q)); + return self::$instance ??= new self(); } } diff --git a/app/SmartAlbums/TagAlbum.php b/app/SmartAlbums/TagAlbum.php deleted file mode 100644 index 8cc1439c875..00000000000 --- a/app/SmartAlbums/TagAlbum.php +++ /dev/null @@ -1,23 +0,0 @@ -showtags); - foreach ($tags as $tag) { - $sql = $sql->where('tags', 'like', '%' . trim($tag) . '%'); - } - - return $sql->where(fn ($q) => $this->filter($q)); - } -} diff --git a/app/SmartAlbums/UnsortedAlbum.php b/app/SmartAlbums/UnsortedAlbum.php index 40eadf3b080..7d62de9a936 100644 --- a/app/SmartAlbums/UnsortedAlbum.php +++ b/app/SmartAlbums/UnsortedAlbum.php @@ -1,24 +1,54 @@ value; + /** + * @throws ConfigurationKeyMissingException + * @throws FrameworkException + */ public function __construct() { - parent::__construct(); + parent::__construct( + SmartAlbumType::UNSORTED, + fn (Builder $q) => $q->whereNull('photos.album_id') + ); + } - $this->title = 'unsorted'; - $this->public = false; + public static function getInstance(): self + { + return self::$instance ??= new self(); } - public function get_photos(): Builder + /** + * In the case of unsorted, we cannot determine whether the photo is visible or not from its parent. + * If the Unsorted album is made public, then all the pictures in it are visible (including pictures which are not owned by the current user). + * + * @return \App\Eloquent\FixedQueryBuilder + */ + public function photos(): Builder { - return Photo::unsorted()->where(fn ($q) => $this->filter($q)); + if ($this->publicPermissions !== null) { + return Photo::query()->with(['album', 'size_variants', 'size_variants.sym_links']) + ->where($this->smartPhotoCondition); + } + + return parent::photos(); } } diff --git a/app/SmartAlbums/Utils/MimicModel.php b/app/SmartAlbums/Utils/MimicModel.php new file mode 100644 index 00000000000..d2fa6f4aa80 --- /dev/null +++ b/app/SmartAlbums/Utils/MimicModel.php @@ -0,0 +1,67 @@ +{$getter}(); + } elseif (property_exists($this, $key)) { + /** @phpstan-ignore-next-line PhpStan does not like variadic calls */ + return $this->{$key}; + } elseif (property_exists($this, $studlyKey)) { + /** @phpstan-ignore-next-line PhpStan does not like variadic calls */ + return $this->{$studlyKey}; + } else { + throw new LycheeInvalidArgumentException('neither property nor getter method exist for [' . $getter . '/' . $key . '/' . $studlyKey . ']'); + } + } + + /** + * Determine if the given relation is loaded. + * + * @param string $key + * + * @return bool + */ + public function relationLoaded($key) + { + return $key === 'photos' && $this->photos !== null; + } +} diff --git a/app/View/Components/Album/Thumbimg.php b/app/View/Components/Album/Thumbimg.php deleted file mode 100644 index dfca83c7efa..00000000000 --- a/app/View/Components/Album/Thumbimg.php +++ /dev/null @@ -1,44 +0,0 @@ -isVideo = Str::contains($type, 'video'); - $this->thumb = $thumb; - $this->thumb2x = $thumb2x; - $this->type = $type; - } - - /** - * Get the view / contents that represent the component. - * - * @return \Illuminate\Contracts\View\View|\Closure|string - */ - public function render() - { - if ($this->thumb == 'uploads/thumb/' && $this->isVideo) { - return view('components.album.thumb-play'); - } - if ($this->thumb == 'uploads/thumb/' && Str::contains($this->type, 'raw')) { - return view('components.album.thumb-placeholder'); - } - - return view('components.album.thumbimg'); - } -} diff --git a/app/View/Components/Icon.php b/app/View/Components/Icon.php deleted file mode 100644 index c6fdca9853b..00000000000 --- a/app/View/Components/Icon.php +++ /dev/null @@ -1,32 +0,0 @@ -class = $class; - $this->icon = $icon; - } - - /** - * Get the view / contents that represent the component. - * - * @return \Illuminate\Contracts\View\View|\Closure|string - */ - public function render() - { - return view('components.icon'); - } -} diff --git a/app/View/Components/Iconic.php b/app/View/Components/Iconic.php deleted file mode 100644 index 2c2b3908b08..00000000000 --- a/app/View/Components/Iconic.php +++ /dev/null @@ -1,32 +0,0 @@ -class = $class; - $this->icon = $icon; - } - - /** - * Get the view / contents that represent the component. - * - * @return \Illuminate\Contracts\View\View|\Closure|string - */ - public function render() - { - return view('components.iconic'); - } -} diff --git a/app/View/Components/Meta.php b/app/View/Components/Meta.php new file mode 100644 index 00000000000..b7ab26c37b7 --- /dev/null +++ b/app/View/Components/Meta.php @@ -0,0 +1,126 @@ +siteOwner = Configs::getValueAsString('site_owner'); + $this->pageUrl = url()->current(); + $this->rssEnable = Configs::getValueAsBool('rss_enable'); + $this->userCssUrl = self::getUserCustomFiles('user.css'); + $this->userJsUrl = self::getUserCustomFiles('custom.js'); + $this->baseUrl = url('/'); + + $this->pageTitle = Configs::getValueAsString('site_title'); + $this->pageDescription = ''; + $this->imageUrl = Configs::getValueAsString('landing_background'); + + // processing photo and album data + if (session()->has('access')) { + $this->access = session()->get('access'); + session()->forget('access'); + } + if (session()->has('album')) { + $this->album = session()->get('album'); + session()->forget('album'); + } + if (session()->has('photo')) { + $this->photo = session()->get('photo'); + session()->forget('photo'); + } + + if ($this->access === false) { + return; + } + + if ($this->album !== null) { + $this->pageTitle = $this->album->title; + if ($this->album instanceof BaseAlbum) { + $this->pageDescription = $this->album->description ?? Configs::getValueAsString('site_title'); + } + $this->imageUrl = $this->getHeaderUrl($this->album) ?? $this->imageUrl; + } + + if ($this->photo !== null) { + $this->pageTitle = $this->photo->title; + $this->pageDescription = $this->photo->description ?? Configs::getValueAsString('site_title'); + $this->imageUrl = $this->photo->size_variants->getSmall()->url; + } + } + + /** + * Render component. + * + * @return View + * + * @throws BindingResolutionException + */ + public function render(): View + { + return view('components.meta'); + } + + /** + * Returns user.css url with cache busting if file has been updated. + * + * @param string $fileName + * + * @return string + */ + public static function getUserCustomFiles(string $fileName): string + { + $cssCacheBusting = ''; + /** @disregard P1013 */ + if (Storage::disk('dist')->fileExists($fileName)) { + $cssCacheBusting = '?' . Storage::disk('dist')->lastModified($fileName); + } + + /** @disregard P1013 */ + return Storage::disk('dist')->url($fileName) . $cssCacheBusting; + } +} \ No newline at end of file diff --git a/app/View/Components/Photo.php b/app/View/Components/Photo.php deleted file mode 100644 index 3e61a24ddb5..00000000000 --- a/app/View/Components/Photo.php +++ /dev/null @@ -1,126 +0,0 @@ -album_id = $data['album']; - $this->photo_id = $data['id']; - $this->title = $data['title']; - $this->takedate = $data['taken_at']; - $this->created_at = $data['created_at']; - $this->star = $data['star'] == '1'; - $this->public = $data['public'] == '1'; - - $isVideo = Str::contains($data['type'], 'video'); - $isRaw = Str::contains($data['type'], 'raw'); - $isLivePhoto = filled($data['livePhotoUrl']); - - $this->class = ''; - $this->class .= $isVideo ? ' video' : ''; - $this->class .= $isLivePhoto ? ' livephoto' : ''; - - $this->layout = Configs::get_value('layout', '0') == '0'; - - if ($data['sizeVariants']['thumb']['url'] == 'uploads/thumb/') { - $this->show_live = $isLivePhoto; - $this->show_play = $isVideo; - $this->show_placeholder = $isRaw; - } - - $dim = ''; - $dim2x = ''; - $thumb2x = ''; - - // TODO: The class Photo for the database model does not anymore contain the attributes `small`, `small_dim`, etc. - // Probably this code needs some fix/refactoring, too. However, where is this method invoked and - // what is the structure of the passed `data` array? (Could find any invocation.) - if ($this->layout) { - $thumb = $data['sizeVariants']['thumb']['url']; - $thumb2x = $data['sizeVariants']['thumb2x']['url']; - } elseif ($data['sizeVariants']['small'] !== null) { - $thumb = $data['sizeVariants']['small']['url']; - $thumb2x = $data['sizeVariants']['small2x']['url'] ?? ''; - $this->_w = $data['sizeVariants']['small']['width']; - $this->_h = $data['sizeVariants']['small']['height']; - $dim = $data['sizeVariants']['small']['width']; - $dim2x = $data['sizeVariants']['small2x']['width'] ?? 0; - } elseif ($data['sizeVariants']['medium'] !== null) { - $thumb = $data['sizeVariants']['medium']['url']; - $thumb2x = $data['sizeVariants']['medium2x']['url'] ?? ''; - $this->_w = $data['sizeVariants']['medium']['width']; - $this->_h = $data['sizeVariants']['medium']['height']; - $dim = $data['sizeVariants']['medium']['width']; - $dim2x = $data['sizeVariants']['medium2x']['width'] ?? 0; - } elseif (!$isVideo) { - // Fallback for images with no small or medium. - $thumb = $data['url']; - $this->_w = $data['width']; - $this->_h = $data['height']; - } else { - // Fallback for videos with no small (the case of no thumb is handled else where). - $this->class = 'video'; - $thumb = $data['sizeVariants']['thumb']['url']; - $thumb2x = $data['sizeVariants']['thumb2x']['url']; - $dim = (string) PhotoModel::THUMBNAIL_DIM; - $dim2x = (string) PhotoModel::THUMBNAIL2X_DIM; - } - - $this->src = "src='" . URL::asset('img/placeholder.png') . "'"; - $this->srcset = "data-src='" . URL::asset($thumb) . "'"; - $thumb2x_src = ''; - - if ($this->layout) { - $thumb2x_src = URL::asset($thumb2x) . ' 2x'; - } else { - $thumb2x_src = URL::asset($thumb) . ' ' . $dim . 'w, '; - $thumb2x_src .= URL::asset($thumb2x) . ' ' . $dim2x . 'w'; - } - - $this->srcset2x = $thumb2x != '' ? "data-srcset='" . $thumb2x_src . "'" : ''; - } - - /** - * Get the view / contents that represent the component. - * - * @return \Illuminate\Contracts\View\View|\Closure|string - */ - public function render() - { - return view('components.photo'); - } -} diff --git a/bootstrap/PanicAttack.php b/bootstrap/PanicAttack.php index 205f905554c..067d69c39be 100644 --- a/bootstrap/PanicAttack.php +++ b/bootstrap/PanicAttack.php @@ -1,5 +1,16 @@ title = 'ROOT'; $this->code = 403; - $this->message = 'This is the root directory and it MUST NOT BE PUBLICALLY ACCESSIBLE.
+ $this->message = 'This is the root directory and MUST NOT BE PUBLICLY ACCESSIBLE.
To access Lychee, go here.'; $this->displaySimpleError(); } @@ -128,12 +139,5 @@ public function handle(string $error_message) $this->$fun(); } } - // var_dump($error_message); - // if nothing has been catched so far - // $this->title = 'Error !'; - // $this->code = 500; - // $this->message = 'Oups, something went wrong !'; - // $this->message = $last_error['message']; - // $this->displaySimpleError(); } } diff --git a/bootstrap/app.php b/bootstrap/app.php index 9276cf557b6..e50165580c5 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -1,5 +1,23 @@ handle($message); diff --git a/codecov.yml b/codecov.yml index 5b98383158d..d7a7bcf6ca8 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,53 +1,45 @@ -{ - "codecov": { - "require_ci_to_pass": true - }, - "comment": { - "behavior": "default", - "require_base": false, - "require_changes": true, - "require_head": true - - }, - "github_checks": { - "annotations": false - }, - "coverage": { - "precision": 2, - "round": "down", - "status": { - "changes": false, - "patch": { - "default": {} - }, - "project": { - "default": { - "target": 50% - } - } - } - }, - "ignore": [ - "^app/Console/.*", - "^app/Exceptions/.*", - "^app/Http/ControllersFunctions/Diagnostics/.*", - "^app/Http/Controllers/Auth/.*", - "^app/Http/Middleware/LoginCheck.*", - "^app/Http/Middleware/VerifyCsrfToken.*", - "^app/Http/Middleware/RedirectIfAuthenticated.*", - "^app/Providers/BroadcastServiceProvider.*" - ], - "parsers": { - "gcov": { - "branch_detection": { - "conditional": true, - "loop": true, - "macro": false, - "method": false - } - }, - "javascript": { - "enable_partials": false - } - } -} +--- +codecov: + require_ci_to_pass: true + notify: + after_n_builds: 9 + wait_for_ci: true +comment: + behavior: default + require_base: false + require_changes: true + require_head: true +github_checks: + annotations: false +coverage: + precision: 2 + round: down + status: + changes: false + patch: off + project: + default: + target: 80% +ignore: +- "^app/Console/.*" +- "^app/Exceptions/.*" +- "^app/Http/Middleware/VerifyCsrfToken.*" +- "^app/Http/Middleware/VerifyCsrfToken.*" +- "^app/Providers/BroadcastServiceProvider.*" +- "^app/Jobs/UploadSizeVariantToS3Job.php" +# We do not test those +- "^app/Mail/.*" +- "^app/Notifications/.*" +# Legacy shit +- "^app/Legacy/Actions/Photo/.*" +- "^app/Livewire/.*" +- "^app/View/.*" +parsers: + gcov: + branch_detection: + conditional: true + loop: true + macro: false + method: false + javascript: + enable_partials: false diff --git a/composer-cache/.gitignore b/composer-cache/.gitignore old mode 100755 new mode 100644 diff --git a/composer.json b/composer.json index 119f5c3d7d9..6129d00f342 100644 --- a/composer.json +++ b/composer.json @@ -1,57 +1,109 @@ { - "name": "lycheeorg/lychee-laravel", + "name": "lychee-org/lychee", "description": "A great looking and easy-to-use photo-management-system you can run on your server, to manage and share photos.", - "homepage": "https://lycheeorg.github.io/", + "homepage": "https://lycheeorg.dev/", + "readme": "README.md", + "support": { + "source": "https://github.com/LycheeOrg/Lychee", + "issues": "https://github.com/LycheeOrg/Lychee/issues", + "docs": "https://lycheeorg.dev/docs/", + "chat": "https://gitter.im/LycheeOrg/Lobby" + }, "keywords": [ - "framework", - "laravel" + "photos", + "gallery", + "photo management", + "album software", + "image organizer" + ], + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/LycheeOrg/phpstan-lychee" + }, + { + "type": "vcs", + "url": "https://github.com/LycheeOrg/php-exif" + }, + { + "type": "vcs", + "url": "https://github.com/LycheeOrg/log-viewer" + }, + { + "type": "vcs", + "url": "https://github.com/LycheeOrg/verify" + } ], - "repositories": [{ - "type": "vcs", - "url": "https://github.com/LycheeOrg/Larapass" - }], "license": "MIT", "type": "project", "require": { - "php": "^7.4.0|^8.0", + "php": "^8.3", "ext-bcmath": "*", + "ext-ctype": "*", "ext-exif": "*", + "ext-fileinfo": "*", "ext-gd": "*", "ext-json": "*", - "bepsvpt/secure-headers": "^6.3", - "darkghosthunter/larapass": "dev-LycheeSpecial", - "doctrine/dbal": "^2.9", - "fideloper/proxy": "^4.2", - "geocoder-php/cache-provider": "^4.1", - "geocoder-php/nominatim-provider": "^5.1", - "graham-campbell/markdown": "^13.1", - "kalnoy/nestedset": "^5.0", - "laravel/framework": "^8.0", - "livewire/livewire": "^2.3", - "lychee-org/php-exif": "dev-master", - "maennchen/zipstream-php": "^1.2.0", - "php-ffmpeg/php-ffmpeg": "^0.17.0", - "php-http/guzzle7-adapter": "^0.1.1", - "php-http/message": "^1.8", + "ext-mbstring": "*", + "ext-openssl": "*", + "ext-pdo": "*", + "ext-tokenizer": "*", + "ext-xml": "*", + "bepsvpt/secure-headers": "^8.0", + "dedoc/scramble": "^0.11.30", + "doctrine/dbal": "^3.1", + "geocoder-php/cache-provider": "^4.3", + "geocoder-php/nominatim-provider": "^5.5", + "graham-campbell/markdown": "^15.0", + "laragear/webauthn": "^3.1", + "laravel/framework": "^11.0", + "laravel/socialite": "^5.11", + "league/flysystem-aws-s3-v3": "^3.22", + "lychee-org/lycheeverify": "^1.0.2", + "lychee-org/nestedset": "^9.0", + "lychee-org/php-exif": "^1.0.4", + "maennchen/zipstream-php": "^3.1", + "mavinoo/laravel-batch": "^2.4", + "opcodesio/log-viewer": "dev-lycheeOrg", + "php-ffmpeg/php-ffmpeg": "^1.0", + "php-http/guzzle7-adapter": "^1.0", + "php-http/message": "^1.12", + "revolution/socialite-mastodon": "^1.4", + "socialiteproviders/amazon": "^4.1", + "socialiteproviders/apple": "^5.6", + "socialiteproviders/authentik": "^5.2", + "socialiteproviders/facebook": "^4.1", + "socialiteproviders/github": "^4.1", + "socialiteproviders/google": "^4.1", + "socialiteproviders/keycloak": "^5.3", + "socialiteproviders/microsoft": "^4.2", + "socialiteproviders/nextcloud": "^4.0", + "spatie/enum": "^3.13", "spatie/guzzle-rate-limiter-middleware": "^2.0", - "spatie/laravel-feed": ">=3.1", - "spatie/laravel-image-optimizer": "^1.6.2", - "symfony/cache": "^5.1", - "whichbrowser/parser": "^2.0" + "spatie/laravel-data": "^4.7", + "spatie/laravel-feed": "^4.0", + "spatie/laravel-image-optimizer": "^1.8", + "spatie/laravel-typescript-transformer": "^2.4", + "symfony/cache": "^v7.1.5", + "thecodingmachine/safe": "^2.4" }, "require-dev": { "ext-imagick": "*", - "barryvdh/laravel-debugbar": "^3.3", - "barryvdh/laravel-ide-helper": "^2.6", + "ext-posix": "*", + "ext-zip": "*", + "barryvdh/laravel-debugbar": "^3.13", + "barryvdh/laravel-ide-helper": "^3.0", + "brianium/paratest": "^7.4", + "fakerphp/faker": "^1.23.0", "filp/whoops": "^2.5", - "friendsofphp/php-cs-fixer": "^2.16", - "itsgoingd/clockwork": "^5.0", - "laravel/homestead": "^11.4", - "nunomaduro/collision": "^5.0", - "phpunit/phpunit": "^9" - }, - "conflict": { - "doctrine/dbal": "2.10.3" + "friendsofphp/php-cs-fixer": "^3.3", + "itsgoingd/clockwork": "^5.1", + "larastan/larastan": "^2.0", + "lychee-org/phpstan-lychee": "^v1.0.1", + "mockery/mockery": "^1.5", + "nunomaduro/collision": "^8.3", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpunit/phpunit": "^10.0" }, "autoload": { "classmap": [ @@ -59,12 +111,14 @@ "database/factories" ], "psr-4": { - "App\\": "app/" + "App\\": "app/", + "Scripts\\": "scripts/" } }, "autoload-dev": { "psr-4": { - "Tests\\": "tests/" + "Tests\\": "tests/", + "PHPStan\\": "phpstan/" } }, "extra": { @@ -73,25 +127,31 @@ } }, "scripts": { - "post-root-package-install": [ - "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" - ], - "post-create-project-cmd": [ - "@php artisan key:generate" + "post-install-cmd": [ + "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"", + "@php artisan key:generate --no-override", + "@php artisan vendor:publish --tag=log-viewer-assets" ], "post-autoload-dump": [ "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", + "@php artisan optimize --clever --dont-confirm=assume-no", "@php artisan package:discover", "@install_files" ], "install_files": [ - "sh scripts/install_files.sh" + "@php scripts/install_files.php" ] }, "config": { + "platform": { + "php": "8.3" + }, "preferred-install": "dist", "sort-packages": true, - "optimize-autoloader": true + "optimize-autoloader": true, + "allow-plugins": { + "php-http/discovery": false + } }, "minimum-stability": "dev", "prefer-stable": true diff --git a/composer.lock b/composer.lock index bdef3009795..7f4884f20ec 100644 --- a/composer.lock +++ b/composer.lock @@ -4,35 +4,40 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "cd0eed1c62072ca998c7e7f2647e5648", + "content-hash": "b612dc095eb23dbb82b9efc3d06acc04", "packages": [ { - "name": "alchemy/binary-driver", - "version": "v5.2.0", + "name": "amphp/amp", + "version": "v3.0.2", "source": { "type": "git", - "url": "https://github.com/alchemy-fr/BinaryDriver.git", - "reference": "e0615cdff315e6b4b05ada67906df6262a020d22" + "url": "https://github.com/amphp/amp.git", + "reference": "138801fb68cfc9c329da8a7b39d01ce7291ee4b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/alchemy-fr/BinaryDriver/zipball/e0615cdff315e6b4b05ada67906df6262a020d22", - "reference": "e0615cdff315e6b4b05ada67906df6262a020d22", + "url": "https://api.github.com/repos/amphp/amp/zipball/138801fb68cfc9c329da8a7b39d01ce7291ee4b0", + "reference": "138801fb68cfc9c329da8a7b39d01ce7291ee4b0", "shasum": "" }, "require": { - "evenement/evenement": "^3.0|^2.0|^1.0", - "php": ">=5.5", - "psr/log": "^1.0", - "symfony/process": "^2.3|^3.0|^4.0|^5.0" + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" }, "require-dev": { - "phpunit/phpunit": "^4.0|^5.0" + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "5.23.1" }, "type": "library", "autoload": { - "psr-0": { - "Alchemy": "src" + "files": [ + "src/functions.php", + "src/Future/functions.php", + "src/Internal/functions.php" + ], + "psr-4": { + "Amp\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -41,141 +46,153 @@ ], "authors": [ { - "name": "Nicolas Le Goff", - "email": "legoff.n@gmail.com" + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" }, { - "name": "Romain Neutron", - "email": "imprec@gmail.com", - "homepage": "http://www.lickmychip.com/" + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" }, { - "name": "Phraseanet Team", - "email": "info@alchemy.fr", - "homepage": "http://www.phraseanet.com/" + "name": "Niklas Keller", + "email": "me@kelunik.com" }, { - "name": "Jens Hausdorf", - "email": "mail@jens-hausdorf.de", - "homepage": "https://jens-hausdorf.de", - "role": "Maintainer" + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" } ], - "description": "A set of tools to build binary drivers", + "description": "A non-blocking concurrency framework for PHP applications.", + "homepage": "https://amphp.org/amp", "keywords": [ - "binary", - "driver" + "async", + "asynchronous", + "awaitable", + "concurrency", + "event", + "event-loop", + "future", + "non-blocking", + "promise" ], "support": { - "issues": "https://github.com/alchemy-fr/BinaryDriver/issues", - "source": "https://github.com/alchemy-fr/BinaryDriver/tree/master" + "issues": "https://github.com/amphp/amp/issues", + "source": "https://github.com/amphp/amp/tree/v3.0.2" }, - "time": "2020-02-12T19:35:11+00:00" + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-05-10T21:37:46+00:00" }, { - "name": "beberlei/assert", - "version": "v3.3.1", + "name": "amphp/byte-stream", + "version": "v2.1.1", "source": { "type": "git", - "url": "https://github.com/beberlei/assert.git", - "reference": "5e721d7e937ca3ba2cdec1e1adf195f9e5188372" + "url": "https://github.com/amphp/byte-stream.git", + "reference": "daa00f2efdbd71565bf64ffefa89e37542addf93" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/beberlei/assert/zipball/5e721d7e937ca3ba2cdec1e1adf195f9e5188372", - "reference": "5e721d7e937ca3ba2cdec1e1adf195f9e5188372", + "url": "https://api.github.com/repos/amphp/byte-stream/zipball/daa00f2efdbd71565bf64ffefa89e37542addf93", + "reference": "daa00f2efdbd71565bf64ffefa89e37542addf93", "shasum": "" }, "require": { - "ext-ctype": "*", - "ext-json": "*", - "ext-mbstring": "*", - "ext-simplexml": "*", - "php": "^7.0 || ^8.0" + "amphp/amp": "^3", + "amphp/parser": "^1.1", + "amphp/pipeline": "^1", + "amphp/serialization": "^1", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2.3" }, "require-dev": { - "friendsofphp/php-cs-fixer": "*", - "phpstan/phpstan": "*", - "phpunit/phpunit": ">=6.0.0", - "yoast/phpunit-polyfills": "^0.1.0" - }, - "suggest": { - "ext-intl": "Needed to allow Assertion::count(), Assertion::isCountable(), Assertion::minCount(), and Assertion::maxCount() to operate on ResourceBundles" + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "5.22.1" }, "type": "library", "autoload": { - "psr-4": { - "Assert\\": "lib/Assert" - }, "files": [ - "lib/Assert/functions.php" - ] + "src/functions.php", + "src/Internal/functions.php" + ], + "psr-4": { + "Amp\\ByteStream\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-2-Clause" + "MIT" ], "authors": [ { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de", - "role": "Lead Developer" + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" }, { - "name": "Richard Quadling", - "email": "rquadling@gmail.com", - "role": "Collaborator" + "name": "Niklas Keller", + "email": "me@kelunik.com" } ], - "description": "Thin assertion library for input validation in business models.", + "description": "A stream abstraction to make working with non-blocking I/O simple.", + "homepage": "https://amphp.org/byte-stream", "keywords": [ - "assert", - "assertion", - "validation" + "amp", + "amphp", + "async", + "io", + "non-blocking", + "stream" ], "support": { - "issues": "https://github.com/beberlei/assert/issues", - "source": "https://github.com/beberlei/assert/tree/v3.3.1" + "issues": "https://github.com/amphp/byte-stream/issues", + "source": "https://github.com/amphp/byte-stream/tree/v2.1.1" }, - "time": "2021-04-18T20:11:03+00:00" + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-02-17T04:49:38+00:00" }, { - "name": "bepsvpt/secure-headers", - "version": "6.3.0", + "name": "amphp/cache", + "version": "v2.0.1", "source": { "type": "git", - "url": "https://github.com/bepsvpt/secure-headers.git", - "reference": "be5948516c10dab75a863a98dabcc3cb151711aa" + "url": "https://github.com/amphp/cache.git", + "reference": "46912e387e6aa94933b61ea1ead9cf7540b7797c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bepsvpt/secure-headers/zipball/be5948516c10dab75a863a98dabcc3cb151711aa", - "reference": "be5948516c10dab75a863a98dabcc3cb151711aa", + "url": "https://api.github.com/repos/amphp/cache/zipball/46912e387e6aa94933b61ea1ead9cf7540b7797c", + "reference": "46912e387e6aa94933b61ea1ead9cf7540b7797c", "shasum": "" }, "require": { - "illuminate/support": "~5.1|~6.0|~7.0|~8.0", - "php": "^7.0|^8.0" + "amphp/amp": "^3", + "amphp/serialization": "^1", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" }, "require-dev": { - "ext-json": "*", - "ext-xdebug": "*", - "friendsofphp/php-cs-fixer": "~2.2", - "orchestra/testbench": "~3.1|~4.0|~5.0|~6.0", - "phpstan/phpstan": "~0.7", - "phpunit/phpunit": "~5.7|~6.5|~7.5|~8.4|~9.0" + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.4" }, "type": "library", - "extra": { - "laravel": { - "providers": [ - "Bepsvpt\\SecureHeaders\\SecureHeadersServiceProvider" - ] - } - }, "autoload": { "psr-4": { - "Bepsvpt\\SecureHeaders\\": "src/" + "Amp\\Cache\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -184,120 +201,165 @@ ], "authors": [ { - "name": "bepsvpt", - "email": "og7lsrszah6y3lz@infinitefa.email" + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" } ], - "description": "Add security related headers to HTTP response. The package includes Service Providers for easy Laravel integration.", - "homepage": "https://github.com/bepsvpt/secure-headers", - "keywords": [ - "clear-site-data", - "content-security-policy", - "csp", - "except-ct", - "feature-policy", - "header", - "hsts", - "https", - "laravel", - "referrer-policy" - ], + "description": "A fiber-aware cache API based on Amp and Revolt.", + "homepage": "https://amphp.org/cache", "support": { - "issues": "https://github.com/bepsvpt/secure-headers/issues", - "source": "https://github.com/bepsvpt/secure-headers/tree/6.3.0" + "issues": "https://github.com/amphp/cache/issues", + "source": "https://github.com/amphp/cache/tree/v2.0.1" }, "funding": [ { - "url": "https://opencollective.com/secure-headers", - "type": "open_collective" + "url": "https://github.com/amphp", + "type": "github" } ], - "time": "2020-09-07T10:43:49+00:00" + "time": "2024-04-19T03:38:06+00:00" }, { - "name": "brick/math", - "version": "0.9.2", + "name": "amphp/dns", + "version": "v2.3.0", "source": { "type": "git", - "url": "https://github.com/brick/math.git", - "reference": "dff976c2f3487d42c1db75a3b180e2b9f0e72ce0" + "url": "https://github.com/amphp/dns.git", + "reference": "166c43737cef1b77782c648a9d9ed11ee0c9859f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/dff976c2f3487d42c1db75a3b180e2b9f0e72ce0", - "reference": "dff976c2f3487d42c1db75a3b180e2b9f0e72ce0", + "url": "https://api.github.com/repos/amphp/dns/zipball/166c43737cef1b77782c648a9d9ed11ee0c9859f", + "reference": "166c43737cef1b77782c648a9d9ed11ee0c9859f", "shasum": "" }, "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/cache": "^2", + "amphp/parser": "^1", + "amphp/process": "^2", + "daverandom/libdns": "^2.0.2", + "ext-filter": "*", "ext-json": "*", - "php": "^7.1 || ^8.0" + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" }, "require-dev": { - "php-coveralls/php-coveralls": "^2.2", - "phpunit/phpunit": "^7.5.15 || ^8.5 || ^9.0", - "vimeo/psalm": "4.3.2" + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "5.20" }, "type": "library", "autoload": { + "files": [ + "src/functions.php" + ], "psr-4": { - "Brick\\Math\\": "src/" + "Amp\\Dns\\": "src" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "Arbitrary-precision arithmetic library", + "authors": [ + { + "name": "Chris Wright", + "email": "addr@daverandom.com" + }, + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + }, + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + } + ], + "description": "Async DNS resolution for Amp.", + "homepage": "https://github.com/amphp/dns", "keywords": [ - "Arbitrary-precision", - "BigInteger", - "BigRational", - "arithmetic", - "bigdecimal", - "bignum", - "brick", - "math" + "amp", + "amphp", + "async", + "client", + "dns", + "resolve" ], "support": { - "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.9.2" + "issues": "https://github.com/amphp/dns/issues", + "source": "https://github.com/amphp/dns/tree/v2.3.0" }, "funding": [ { - "url": "https://tidelift.com/funding/github/packagist/brick/math", - "type": "tidelift" + "url": "https://github.com/amphp", + "type": "github" } ], - "time": "2021-01-20T22:51:39+00:00" + "time": "2024-12-21T01:15:34+00:00" }, { - "name": "clue/stream-filter", - "version": "v1.5.0", + "name": "amphp/parallel", + "version": "v2.3.1", "source": { "type": "git", - "url": "https://github.com/clue/stream-filter.git", - "reference": "aeb7d8ea49c7963d3b581378955dbf5bc49aa320" + "url": "https://github.com/amphp/parallel.git", + "reference": "5113111de02796a782f5d90767455e7391cca190" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/clue/stream-filter/zipball/aeb7d8ea49c7963d3b581378955dbf5bc49aa320", - "reference": "aeb7d8ea49c7963d3b581378955dbf5bc49aa320", + "url": "https://api.github.com/repos/amphp/parallel/zipball/5113111de02796a782f5d90767455e7391cca190", + "reference": "5113111de02796a782f5d90767455e7391cca190", "shasum": "" }, "require": { - "php": ">=5.3" + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/cache": "^2", + "amphp/parser": "^1", + "amphp/pipeline": "^1", + "amphp/process": "^2", + "amphp/serialization": "^1", + "amphp/socket": "^2", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1" }, "require-dev": { - "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.36" + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.18" }, "type": "library", "autoload": { - "psr-4": { - "Clue\\StreamFilter\\": "src/" - }, "files": [ - "src/functions_include.php" - ] + "src/Context/functions.php", + "src/Context/Internal/functions.php", + "src/Ipc/functions.php", + "src/Worker/functions.php" + ], + "psr-4": { + "Amp\\Parallel\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -305,162 +367,130 @@ ], "authors": [ { - "name": "Christian Lück", - "email": "christian@clue.engineering" + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Stephen Coakley", + "email": "me@stephencoakley.com" } ], - "description": "A simple and modern approach to stream filtering in PHP", - "homepage": "https://github.com/clue/php-stream-filter", + "description": "Parallel processing component for Amp.", + "homepage": "https://github.com/amphp/parallel", "keywords": [ - "bucket brigade", - "callback", - "filter", - "php_user_filter", - "stream", - "stream_filter_append", - "stream_filter_register" + "async", + "asynchronous", + "concurrent", + "multi-processing", + "multi-threading" ], "support": { - "issues": "https://github.com/clue/stream-filter/issues", - "source": "https://github.com/clue/stream-filter/tree/v1.5.0" + "issues": "https://github.com/amphp/parallel/issues", + "source": "https://github.com/amphp/parallel/tree/v2.3.1" }, "funding": [ { - "url": "https://clue.engineering/support", - "type": "custom" - }, - { - "url": "https://github.com/clue", + "url": "https://github.com/amphp", "type": "github" } ], - "time": "2020-10-02T12:38:20+00:00" + "time": "2024-12-21T01:56:09+00:00" }, { - "name": "darkghosthunter/larapass", - "version": "dev-LycheeSpecial", + "name": "amphp/parser", + "version": "v1.1.1", "source": { "type": "git", - "url": "https://github.com/LycheeOrg/Larapass.git", - "reference": "0d03a9ad17f32b5cdbad5667c8312078e5a57f49" + "url": "https://github.com/amphp/parser.git", + "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/LycheeOrg/Larapass/zipball/0d03a9ad17f32b5cdbad5667c8312078e5a57f49", - "reference": "0d03a9ad17f32b5cdbad5667c8312078e5a57f49", + "url": "https://api.github.com/repos/amphp/parser/zipball/3cf1f8b32a0171d4b1bed93d25617637a77cded7", + "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7", "shasum": "" }, "require": { - "ext-json": "*", - "illuminate/support": "^8.0", - "nyholm/psr7": "^1.3", - "php": ">=7.4.0", - "ramsey/uuid": "^4.0", - "symfony/psr-http-message-bridge": "^2.0", - "thecodingmachine/safe": "^1.3.3", - "web-auth/webauthn-lib": "^3.3" + "php": ">=7.4" }, "require-dev": { - "laravel/framework": "8.*", - "orchestra/testbench": "^6.7.2", - "phpunit/phpunit": "^9.0" + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.4" }, "type": "library", - "extra": { - "laravel": { - "providers": [ - "DarkGhostHunter\\Larapass\\LarapassServiceProvider" - ] - } - }, "autoload": { "psr-4": { - "DarkGhostHunter\\Larapass\\": "src/" - } - }, - "autoload-dev": { - "psr-4": { - "Tests\\": "tests/" + "Amp\\Parser\\": "src" } }, - "scripts": { - "test": [ - "vendor/bin/phpunit" - ], - "test-coverage": [ - "vendor/bin/phpunit --coverage-html coverage" - ] - }, + "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "authors": [ { - "name": "Italo Israel Baeza Cabrera", - "email": "darkghosthunter@gmail.com", - "role": "Developer" + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" } ], - "description": "Authenticate users with just their device, fingerprint or biometric data. Goodbye passwords!", - "homepage": "https://github.com/darkghosthunter/larapass", + "description": "A generator parser to make streaming parsers simple.", + "homepage": "https://github.com/amphp/parser", "keywords": [ - "darkghosthunter", - "laravel", - "webauthn" + "async", + "non-blocking", + "parser", + "stream" ], "support": { - "source": "https://github.com/LycheeOrg/Larapass/tree/LycheeSpecial" + "issues": "https://github.com/amphp/parser/issues", + "source": "https://github.com/amphp/parser/tree/v1.1.1" }, "funding": [ { - "type": "ko_fi", - "url": "https://ko-fi.com/DarkGhostHunter" - }, - { - "type": "custom", - "url": "https://paypal.me/darkghosthunter" + "url": "https://github.com/amphp", + "type": "github" } ], - "time": "2021-01-11T20:00:11+00:00" + "time": "2024-03-21T19:16:53+00:00" }, { - "name": "doctrine/cache", - "version": "1.11.2", + "name": "amphp/pipeline", + "version": "v1.2.1", "source": { "type": "git", - "url": "https://github.com/doctrine/cache.git", - "reference": "9c53086695937c50c47936ed86d96150ffbcf60d" + "url": "https://github.com/amphp/pipeline.git", + "reference": "66c095673aa5b6e689e63b52d19e577459129ab3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/cache/zipball/9c53086695937c50c47936ed86d96150ffbcf60d", - "reference": "9c53086695937c50c47936ed86d96150ffbcf60d", + "url": "https://api.github.com/repos/amphp/pipeline/zipball/66c095673aa5b6e689e63b52d19e577459129ab3", + "reference": "66c095673aa5b6e689e63b52d19e577459129ab3", "shasum": "" }, "require": { - "php": "~7.1 || ^8.0" - }, - "conflict": { - "doctrine/common": ">2.2,<2.4", - "psr/cache": ">=3" + "amphp/amp": "^3", + "php": ">=8.1", + "revolt/event-loop": "^1" }, "require-dev": { - "alcaeus/mongo-php-adapter": "^1.1", - "cache/integration-tests": "dev-master", - "doctrine/coding-standard": "^8.0", - "mongodb/mongodb": "^1.1", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", - "predis/predis": "~1.0", - "psr/cache": "^1.0 || ^2.0", - "symfony/cache": "^4.4 || ^5.2" - }, - "suggest": { - "alcaeus/mongo-php-adapter": "Required to use legacy MongoDB driver" + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.18" }, "type": "library", "autoload": { "psr-4": { - "Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache" + "Amp\\Pipeline\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -469,99 +499,70 @@ ], "authors": [ { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com" + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" }, { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" + "name": "Niklas Keller", + "email": "me@kelunik.com" } ], - "description": "PHP Doctrine Cache library is a popular cache implementation that supports many different drivers such as redis, memcache, apc, mongodb and others.", - "homepage": "https://www.doctrine-project.org/projects/cache.html", + "description": "Asynchronous iterators and operators.", + "homepage": "https://amphp.org/pipeline", "keywords": [ - "abstraction", - "apcu", - "cache", - "caching", - "couchdb", - "memcached", - "php", - "redis", - "xcache" + "amp", + "amphp", + "async", + "io", + "iterator", + "non-blocking" ], "support": { - "issues": "https://github.com/doctrine/cache/issues", - "source": "https://github.com/doctrine/cache/tree/1.11.2" + "issues": "https://github.com/amphp/pipeline/issues", + "source": "https://github.com/amphp/pipeline/tree/v1.2.1" }, "funding": [ { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcache", - "type": "tidelift" + "url": "https://github.com/amphp", + "type": "github" } ], - "time": "2021-05-20T14:57:29+00:00" + "time": "2024-07-04T00:56:47+00:00" }, { - "name": "doctrine/dbal", - "version": "2.13.1", + "name": "amphp/process", + "version": "v2.0.3", "source": { "type": "git", - "url": "https://github.com/doctrine/dbal.git", - "reference": "c800380457948e65bbd30ba92cc17cda108bf8c9" + "url": "https://github.com/amphp/process.git", + "reference": "52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/c800380457948e65bbd30ba92cc17cda108bf8c9", - "reference": "c800380457948e65bbd30ba92cc17cda108bf8c9", + "url": "https://api.github.com/repos/amphp/process/zipball/52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d", + "reference": "52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d", "shasum": "" }, "require": { - "doctrine/cache": "^1.0", - "doctrine/deprecations": "^0.5.3", - "doctrine/event-manager": "^1.0", - "ext-pdo": "*", - "php": "^7.1 || ^8" + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" }, "require-dev": { - "doctrine/coding-standard": "8.2.0", - "jetbrains/phpstorm-stubs": "2020.2", - "phpstan/phpstan": "0.12.81", - "phpunit/phpunit": "^7.5.20|^8.5|9.5.0", - "squizlabs/php_codesniffer": "3.6.0", - "symfony/console": "^2.0.5|^3.0|^4.0|^5.0", - "vimeo/psalm": "4.6.4" - }, - "suggest": { - "symfony/console": "For helpful console commands such as SQL execution and import of files." + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.4" }, - "bin": [ - "bin/doctrine-dbal" - ], "type": "library", "autoload": { + "files": [ + "src/functions.php" + ], "psr-4": { - "Doctrine\\DBAL\\": "lib/Doctrine/DBAL" + "Amp\\Process\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -570,141 +571,131 @@ ], "authors": [ { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" }, { - "name": "Roman Borschel", - "email": "roman@code-factory.org" + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" }, { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A fiber-aware process manager based on Amp and Revolt.", + "homepage": "https://amphp.org/process", + "support": { + "issues": "https://github.com/amphp/process/issues", + "source": "https://github.com/amphp/process/tree/v2.0.3" + }, + "funding": [ { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com" + "url": "https://github.com/amphp", + "type": "github" } ], - "description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.", - "homepage": "https://www.doctrine-project.org/projects/dbal.html", - "keywords": [ - "abstraction", - "database", - "db2", - "dbal", - "mariadb", - "mssql", - "mysql", - "oci8", - "oracle", - "pdo", - "pgsql", - "postgresql", - "queryobject", - "sasql", - "sql", - "sqlanywhere", - "sqlite", - "sqlserver", - "sqlsrv" - ], - "support": { - "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/2.13.1" - }, - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdbal", - "type": "tidelift" - } - ], - "time": "2021-04-17T17:30:19+00:00" + "time": "2024-04-19T03:13:44+00:00" }, { - "name": "doctrine/deprecations", - "version": "v0.5.3", + "name": "amphp/serialization", + "version": "v1.0.0", "source": { "type": "git", - "url": "https://github.com/doctrine/deprecations.git", - "reference": "9504165960a1f83cc1480e2be1dd0a0478561314" + "url": "https://github.com/amphp/serialization.git", + "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/deprecations/zipball/9504165960a1f83cc1480e2be1dd0a0478561314", - "reference": "9504165960a1f83cc1480e2be1dd0a0478561314", + "url": "https://api.github.com/repos/amphp/serialization/zipball/693e77b2fb0b266c3c7d622317f881de44ae94a1", + "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1", "shasum": "" }, "require": { - "php": "^7.1|^8.0" + "php": ">=7.1" }, "require-dev": { - "doctrine/coding-standard": "^6.0|^7.0|^8.0", - "phpunit/phpunit": "^7.0|^8.0|^9.0", - "psr/log": "^1.0" - }, - "suggest": { - "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + "amphp/php-cs-fixer-config": "dev-master", + "phpunit/phpunit": "^9 || ^8 || ^7" }, "type": "library", "autoload": { + "files": [ + "src/functions.php" + ], "psr-4": { - "Doctrine\\Deprecations\\": "lib/Doctrine/Deprecations" + "Amp\\Serialization\\": "src" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", - "homepage": "https://www.doctrine-project.org/", + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Serialization tools for IPC and data storage in PHP.", + "homepage": "https://github.com/amphp/serialization", + "keywords": [ + "async", + "asynchronous", + "serialization", + "serialize" + ], "support": { - "issues": "https://github.com/doctrine/deprecations/issues", - "source": "https://github.com/doctrine/deprecations/tree/v0.5.3" + "issues": "https://github.com/amphp/serialization/issues", + "source": "https://github.com/amphp/serialization/tree/master" }, - "time": "2021-03-21T12:59:47+00:00" + "time": "2020-03-25T21:39:07+00:00" }, { - "name": "doctrine/event-manager", - "version": "1.1.1", + "name": "amphp/socket", + "version": "v2.3.1", "source": { "type": "git", - "url": "https://github.com/doctrine/event-manager.git", - "reference": "41370af6a30faa9dc0368c4a6814d596e81aba7f" + "url": "https://github.com/amphp/socket.git", + "reference": "58e0422221825b79681b72c50c47a930be7bf1e1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/event-manager/zipball/41370af6a30faa9dc0368c4a6814d596e81aba7f", - "reference": "41370af6a30faa9dc0368c4a6814d596e81aba7f", + "url": "https://api.github.com/repos/amphp/socket/zipball/58e0422221825b79681b72c50c47a930be7bf1e1", + "reference": "58e0422221825b79681b72c50c47a930be7bf1e1", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" - }, - "conflict": { - "doctrine/common": "<2.9@dev" + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/dns": "^2", + "ext-openssl": "*", + "kelunik/certificate": "^1.1", + "league/uri": "^6.5 | ^7", + "league/uri-interfaces": "^2.3 | ^7", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" }, "require-dev": { - "doctrine/coding-standard": "^6.0", - "phpunit/phpunit": "^7.0" + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "amphp/process": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "5.20" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, "autoload": { + "files": [ + "src/functions.php", + "src/Internal/functions.php", + "src/SocketAddress/functions.php" + ], "psr-4": { - "Doctrine\\Common\\": "lib/Doctrine/Common" + "Amp\\Socket\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -713,92 +704,75 @@ ], "authors": [ { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com" + "name": "Daniel Lowrey", + "email": "rdlowrey@gmail.com" }, { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" }, { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com" + "name": "Niklas Keller", + "email": "me@kelunik.com" } ], - "description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.", - "homepage": "https://www.doctrine-project.org/projects/event-manager.html", + "description": "Non-blocking socket connection / server implementations based on Amp and Revolt.", + "homepage": "https://github.com/amphp/socket", "keywords": [ - "event", - "event dispatcher", - "event manager", - "event system", - "events" + "amp", + "async", + "encryption", + "non-blocking", + "sockets", + "tcp", + "tls" ], "support": { - "issues": "https://github.com/doctrine/event-manager/issues", - "source": "https://github.com/doctrine/event-manager/tree/1.1.x" + "issues": "https://github.com/amphp/socket/issues", + "source": "https://github.com/amphp/socket/tree/v2.3.1" }, "funding": [ { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fevent-manager", - "type": "tidelift" + "url": "https://github.com/amphp", + "type": "github" } ], - "time": "2020-05-29T18:28:51+00:00" + "time": "2024-04-21T14:33:03+00:00" }, { - "name": "doctrine/inflector", - "version": "2.0.3", + "name": "amphp/sync", + "version": "v2.3.0", "source": { "type": "git", - "url": "https://github.com/doctrine/inflector.git", - "reference": "9cf661f4eb38f7c881cac67c75ea9b00bf97b210" + "url": "https://github.com/amphp/sync.git", + "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/inflector/zipball/9cf661f4eb38f7c881cac67c75ea9b00bf97b210", - "reference": "9cf661f4eb38f7c881cac67c75ea9b00bf97b210", + "url": "https://api.github.com/repos/amphp/sync/zipball/217097b785130d77cfcc58ff583cf26cd1770bf1", + "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0" + "amphp/amp": "^3", + "amphp/pipeline": "^1", + "amphp/serialization": "^1", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" }, "require-dev": { - "doctrine/coding-standard": "^7.0", - "phpstan/phpstan": "^0.11", - "phpstan/phpstan-phpunit": "^0.11", - "phpstan/phpstan-strict-rules": "^0.11", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "5.23" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, "autoload": { + "files": [ + "src/functions.php" + ], "psr-4": { - "Doctrine\\Inflector\\": "lib/Doctrine/Inflector" + "Amp\\Sync\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -807,237 +781,231 @@ ], "authors": [ { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" }, { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com" + "name": "Niklas Keller", + "email": "me@kelunik.com" }, { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" + "name": "Stephen Coakley", + "email": "me@stephencoakley.com" } ], - "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", - "homepage": "https://www.doctrine-project.org/projects/inflector.html", + "description": "Non-blocking synchronization primitives for PHP based on Amp and Revolt.", + "homepage": "https://github.com/amphp/sync", "keywords": [ - "inflection", - "inflector", - "lowercase", - "manipulation", - "php", - "plural", - "singular", - "strings", - "uppercase", - "words" + "async", + "asynchronous", + "mutex", + "semaphore", + "synchronization" ], "support": { - "issues": "https://github.com/doctrine/inflector/issues", - "source": "https://github.com/doctrine/inflector/tree/2.0.x" + "issues": "https://github.com/amphp/sync/issues", + "source": "https://github.com/amphp/sync/tree/v2.3.0" }, "funding": [ { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", - "type": "tidelift" + "url": "https://github.com/amphp", + "type": "github" } ], - "time": "2020-05-29T15:13:26+00:00" + "time": "2024-08-03T19:31:26+00:00" }, { - "name": "doctrine/lexer", - "version": "1.2.1", + "name": "aws/aws-crt-php", + "version": "v1.2.7", "source": { "type": "git", - "url": "https://github.com/doctrine/lexer.git", - "reference": "e864bbf5904cb8f5bb334f99209b48018522f042" + "url": "https://github.com/awslabs/aws-crt-php.git", + "reference": "d71d9906c7bb63a28295447ba12e74723bd3730e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/lexer/zipball/e864bbf5904cb8f5bb334f99209b48018522f042", - "reference": "e864bbf5904cb8f5bb334f99209b48018522f042", + "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/d71d9906c7bb63a28295447ba12e74723bd3730e", + "reference": "d71d9906c7bb63a28295447ba12e74723bd3730e", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0" + "php": ">=5.5" }, "require-dev": { - "doctrine/coding-standard": "^6.0", - "phpstan/phpstan": "^0.11.8", - "phpunit/phpunit": "^8.2" + "phpunit/phpunit": "^4.8.35||^5.6.3||^9.5", + "yoast/phpunit-polyfills": "^1.0" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.2.x-dev" - } + "suggest": { + "ext-awscrt": "Make sure you install awscrt native extension to use any of the functionality." }, + "type": "library", "autoload": { - "psr-4": { - "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "Apache-2.0" ], "authors": [ { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" + "name": "AWS SDK Common Runtime Team", + "email": "aws-sdk-common-runtime@amazon.com" } ], - "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", - "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "description": "AWS Common Runtime for PHP", + "homepage": "https://github.com/awslabs/aws-crt-php", "keywords": [ - "annotations", - "docblock", - "lexer", - "parser", - "php" + "amazon", + "aws", + "crt", + "sdk" ], "support": { - "issues": "https://github.com/doctrine/lexer/issues", - "source": "https://github.com/doctrine/lexer/tree/1.2.1" + "issues": "https://github.com/awslabs/aws-crt-php/issues", + "source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.7" }, - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", - "type": "tidelift" - } - ], - "time": "2020-05-25T17:44:05+00:00" + "time": "2024-10-18T22:15:13+00:00" }, { - "name": "dragonmantank/cron-expression", - "version": "v3.1.0", + "name": "aws/aws-sdk-php", + "version": "3.336.9", "source": { "type": "git", - "url": "https://github.com/dragonmantank/cron-expression.git", - "reference": "7a8c6e56ab3ffcc538d05e8155bb42269abf1a0c" + "url": "https://github.com/aws/aws-sdk-php.git", + "reference": "bbc76138ed66f593dc2ae529c95fe1f794e6d77f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/7a8c6e56ab3ffcc538d05e8155bb42269abf1a0c", - "reference": "7a8c6e56ab3ffcc538d05e8155bb42269abf1a0c", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/bbc76138ed66f593dc2ae529c95fe1f794e6d77f", + "reference": "bbc76138ed66f593dc2ae529c95fe1f794e6d77f", "shasum": "" }, "require": { - "php": "^7.2|^8.0", - "webmozart/assert": "^1.7.0" - }, - "replace": { - "mtdowling/cron-expression": "^1.0" + "aws/aws-crt-php": "^1.2.3", + "ext-json": "*", + "ext-pcre": "*", + "ext-simplexml": "*", + "guzzlehttp/guzzle": "^6.5.8 || ^7.4.5", + "guzzlehttp/promises": "^1.4.0 || ^2.0", + "guzzlehttp/psr7": "^1.9.1 || ^2.4.5", + "mtdowling/jmespath.php": "^2.6", + "php": ">=7.2.5", + "psr/http-message": "^1.0 || ^2.0" }, "require-dev": { - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^0.12", - "phpstan/phpstan-webmozart-assert": "^0.12.7", - "phpunit/phpunit": "^7.0|^8.0|^9.0" + "andrewsville/php-token-reflection": "^1.4", + "aws/aws-php-sns-message-validator": "~1.0", + "behat/behat": "~3.0", + "composer/composer": "^1.10.22", + "dms/phpunit-arraysubset-asserts": "^0.4.0", + "doctrine/cache": "~1.4", + "ext-dom": "*", + "ext-openssl": "*", + "ext-pcntl": "*", + "ext-sockets": "*", + "nette/neon": "^2.3", + "paragonie/random_compat": ">= 2", + "phpunit/phpunit": "^5.6.3 || ^8.5 || ^9.5", + "psr/cache": "^1.0 || ^2.0 || ^3.0", + "psr/simple-cache": "^1.0 || ^2.0 || ^3.0", + "sebastian/comparator": "^1.2.3 || ^4.0", + "yoast/phpunit-polyfills": "^1.0" + }, + "suggest": { + "aws/aws-php-sns-message-validator": "To validate incoming SNS notifications", + "doctrine/cache": "To use the DoctrineCacheAdapter", + "ext-curl": "To send requests using cURL", + "ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages", + "ext-sockets": "To use client-side monitoring" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, "autoload": { + "files": [ + "src/functions.php" + ], "psr-4": { - "Cron\\": "src/Cron/" - } + "Aws\\": "src/" + }, + "exclude-from-classmap": [ + "src/data/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "Apache-2.0" ], "authors": [ { - "name": "Chris Tankersley", - "email": "chris@ctankersley.com", - "homepage": "https://github.com/dragonmantank" + "name": "Amazon Web Services", + "homepage": "http://aws.amazon.com" } ], - "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due", + "description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project", + "homepage": "http://aws.amazon.com/sdkforphp", "keywords": [ - "cron", - "schedule" + "amazon", + "aws", + "cloud", + "dynamodb", + "ec2", + "glacier", + "s3", + "sdk" ], "support": { - "issues": "https://github.com/dragonmantank/cron-expression/issues", - "source": "https://github.com/dragonmantank/cron-expression/tree/v3.1.0" + "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", + "issues": "https://github.com/aws/aws-sdk-php/issues", + "source": "https://github.com/aws/aws-sdk-php/tree/3.336.9" }, - "funding": [ - { - "url": "https://github.com/dragonmantank", - "type": "github" - } - ], - "time": "2020-11-24T19:55:57+00:00" + "time": "2025-01-06T19:06:42+00:00" }, { - "name": "egulias/email-validator", - "version": "2.1.25", + "name": "bepsvpt/secure-headers", + "version": "8.0.0", "source": { "type": "git", - "url": "https://github.com/egulias/EmailValidator.git", - "reference": "0dbf5d78455d4d6a41d186da50adc1122ec066f4" + "url": "https://github.com/bepsvpt/secure-headers.git", + "reference": "d2a520195221eac497c753893eb03c81572ab330" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/0dbf5d78455d4d6a41d186da50adc1122ec066f4", - "reference": "0dbf5d78455d4d6a41d186da50adc1122ec066f4", + "url": "https://api.github.com/repos/bepsvpt/secure-headers/zipball/d2a520195221eac497c753893eb03c81572ab330", + "reference": "d2a520195221eac497c753893eb03c81572ab330", "shasum": "" }, "require": { - "doctrine/lexer": "^1.0.1", - "php": ">=5.5", - "symfony/polyfill-intl-idn": "^1.10" + "ext-json": "*", + "php": "^7.0 || ^8.0" }, "require-dev": { - "dominicsayers/isemail": "^3.0.7", - "phpunit/phpunit": "^4.8.36|^7.5.15", - "satooshi/php-coveralls": "^1.0.1" - }, - "suggest": { - "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + "ergebnis/composer-normalize": "^2.42", + "ext-xdebug": "*", + "laravel/pint": "^1.14", + "orchestra/testbench": "^3.1 || ^4.18 || ^5.20 || ^6.43 || ^7.41 || ^8.22 || ^9.0", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.5 || ^9.6 || ^10.5" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "2.1.x-dev" + "laravel": { + "providers": [ + "Bepsvpt\\SecureHeaders\\SecureHeadersServiceProvider" + ] } }, "autoload": { + "files": [ + "src/helpers.php" + ], "psr-4": { - "Egulias\\EmailValidator\\": "src" + "Bepsvpt\\SecureHeaders\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -1046,113 +1014,125 @@ ], "authors": [ { - "name": "Eduardo Gulias Davis" + "name": "bepsvpt", + "email": "6ibrl@cpp.tw" } ], - "description": "A library for validating emails against several RFCs", - "homepage": "https://github.com/egulias/EmailValidator", + "description": "Add security related headers to HTTP response. The package includes Service Providers for easy Laravel integration.", + "homepage": "https://github.com/bepsvpt/secure-headers", "keywords": [ - "email", - "emailvalidation", - "emailvalidator", - "validation", - "validator" + "clear-site-data", + "content-security-policy", + "csp", + "except-ct", + "feature-policy", + "header", + "hsts", + "https", + "laravel", + "referrer-policy" ], "support": { - "issues": "https://github.com/egulias/EmailValidator/issues", - "source": "https://github.com/egulias/EmailValidator/tree/2.1.25" + "issues": "https://github.com/bepsvpt/secure-headers/issues", + "source": "https://github.com/bepsvpt/secure-headers/tree/8.0.0" }, "funding": [ { - "url": "https://github.com/egulias", - "type": "github" + "url": "https://opencollective.com/secure-headers", + "type": "open_collective" } ], - "time": "2020-12-29T14:50:06+00:00" + "time": "2024-10-13T11:50:22+00:00" }, { - "name": "evenement/evenement", - "version": "v3.0.1", + "name": "brick/math", + "version": "0.12.1", "source": { "type": "git", - "url": "https://github.com/igorw/evenement.git", - "reference": "531bfb9d15f8aa57454f5f0285b18bec903b8fb7" + "url": "https://github.com/brick/math.git", + "reference": "f510c0a40911935b77b86859eb5223d58d660df1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/igorw/evenement/zipball/531bfb9d15f8aa57454f5f0285b18bec903b8fb7", - "reference": "531bfb9d15f8aa57454f5f0285b18bec903b8fb7", + "url": "https://api.github.com/repos/brick/math/zipball/f510c0a40911935b77b86859eb5223d58d660df1", + "reference": "f510c0a40911935b77b86859eb5223d58d660df1", "shasum": "" }, "require": { - "php": ">=7.0" + "php": "^8.1" }, "require-dev": { - "phpunit/phpunit": "^6.0" + "php-coveralls/php-coveralls": "^2.2", + "phpunit/phpunit": "^10.1", + "vimeo/psalm": "5.16.0" }, "type": "library", "autoload": { - "psr-0": { - "Evenement": "src" + "psr-4": { + "Brick\\Math\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ - { - "name": "Igor Wiedler", - "email": "igor@wiedler.ch" - } - ], - "description": "Événement is a very simple event dispatching library for PHP", + "description": "Arbitrary-precision arithmetic library", "keywords": [ - "event-dispatcher", - "event-emitter" + "Arbitrary-precision", + "BigInteger", + "BigRational", + "arithmetic", + "bigdecimal", + "bignum", + "bignumber", + "brick", + "decimal", + "integer", + "math", + "mathematics", + "rational" ], "support": { - "issues": "https://github.com/igorw/evenement/issues", - "source": "https://github.com/igorw/evenement/tree/master" + "issues": "https://github.com/brick/math/issues", + "source": "https://github.com/brick/math/tree/0.12.1" }, - "time": "2017-07-23T21:35:13+00:00" + "funding": [ + { + "url": "https://github.com/BenMorel", + "type": "github" + } + ], + "time": "2023-11-29T23:19:16+00:00" }, { - "name": "fgrosse/phpasn1", - "version": "v2.3.0", + "name": "carbonphp/carbon-doctrine-types", + "version": "2.1.0", "source": { "type": "git", - "url": "https://github.com/fgrosse/PHPASN1.git", - "reference": "20299033c35f4300eb656e7e8e88cf52d1d6694e" + "url": "https://github.com/CarbonPHP/carbon-doctrine-types.git", + "reference": "99f76ffa36cce3b70a4a6abce41dba15ca2e84cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/fgrosse/PHPASN1/zipball/20299033c35f4300eb656e7e8e88cf52d1d6694e", - "reference": "20299033c35f4300eb656e7e8e88cf52d1d6694e", + "url": "https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/99f76ffa36cce3b70a4a6abce41dba15ca2e84cb", + "reference": "99f76ffa36cce3b70a4a6abce41dba15ca2e84cb", "shasum": "" }, "require": { - "php": ">=7.0.0" + "php": "^7.4 || ^8.0" }, - "require-dev": { - "phpunit/phpunit": "~6.3", - "satooshi/php-coveralls": "~2.0" + "conflict": { + "doctrine/dbal": "<3.7.0 || >=4.0.0" }, - "suggest": { - "ext-bcmath": "BCmath is the fallback extension for big integer calculations", - "ext-curl": "For loading OID information from the web if they have not bee defined statically", - "ext-gmp": "GMP is the preferred extension for big integer calculations", - "phpseclib/bcmath_compat": "BCmath polyfill for servers where neither GMP nor BCmath is available" + "require-dev": { + "doctrine/dbal": "^3.7.0", + "nesbot/carbon": "^2.71.0 || ^3.0.0", + "phpunit/phpunit": "^10.3" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, "autoload": { "psr-4": { - "FG\\": "lib/" + "Carbon\\Doctrine\\": "src/Carbon/Doctrine/" } }, "notification-url": "https://packagist.org/downloads/", @@ -1161,71 +1141,65 @@ ], "authors": [ { - "name": "Friedrich Große", - "email": "friedrich.grosse@gmail.com", - "homepage": "https://github.com/FGrosse", - "role": "Author" - }, - { - "name": "All contributors", - "homepage": "https://github.com/FGrosse/PHPASN1/contributors" + "name": "KyleKatarn", + "email": "kylekatarnls@gmail.com" } ], - "description": "A PHP Framework that allows you to encode and decode arbitrary ASN.1 structures using the ITU-T X.690 Encoding Rules.", - "homepage": "https://github.com/FGrosse/PHPASN1", + "description": "Types to use Carbon in Doctrine", "keywords": [ - "DER", - "asn.1", - "asn1", - "ber", - "binary", - "decoding", - "encoding", - "x.509", - "x.690", - "x509", - "x690" + "carbon", + "date", + "datetime", + "doctrine", + "time" ], "support": { - "issues": "https://github.com/fgrosse/PHPASN1/issues", - "source": "https://github.com/fgrosse/PHPASN1/tree/v2.3.0" + "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues", + "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/2.1.0" }, - "time": "2021-04-24T19:01:55+00:00" + "funding": [ + { + "url": "https://github.com/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon", + "type": "tidelift" + } + ], + "time": "2023-12-11T17:09:12+00:00" }, { - "name": "fideloper/proxy", - "version": "4.4.1", + "name": "clue/stream-filter", + "version": "v1.7.0", "source": { "type": "git", - "url": "https://github.com/fideloper/TrustedProxy.git", - "reference": "c073b2bd04d1c90e04dc1b787662b558dd65ade0" + "url": "https://github.com/clue/stream-filter.git", + "reference": "049509fef80032cb3f051595029ab75b49a3c2f7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/fideloper/TrustedProxy/zipball/c073b2bd04d1c90e04dc1b787662b558dd65ade0", - "reference": "c073b2bd04d1c90e04dc1b787662b558dd65ade0", + "url": "https://api.github.com/repos/clue/stream-filter/zipball/049509fef80032cb3f051595029ab75b49a3c2f7", + "reference": "049509fef80032cb3f051595029ab75b49a3c2f7", "shasum": "" }, "require": { - "illuminate/contracts": "^5.0|^6.0|^7.0|^8.0|^9.0", - "php": ">=5.4.0" + "php": ">=5.3" }, "require-dev": { - "illuminate/http": "^5.0|^6.0|^7.0|^8.0|^9.0", - "mockery/mockery": "^1.0", - "phpunit/phpunit": "^6.0" + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" }, "type": "library", - "extra": { - "laravel": { - "providers": [ - "Fideloper\\Proxy\\TrustedProxyServiceProvider" - ] - } - }, "autoload": { + "files": [ + "src/functions_include.php" + ], "psr-4": { - "Fideloper\\Proxy\\": "src/" + "Clue\\StreamFilter\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -1234,122 +1208,126 @@ ], "authors": [ { - "name": "Chris Fidao", - "email": "fideloper@gmail.com" + "name": "Christian Lück", + "email": "christian@clue.engineering" } ], - "description": "Set trusted proxies for Laravel", + "description": "A simple and modern approach to stream filtering in PHP", + "homepage": "https://github.com/clue/stream-filter", "keywords": [ - "load balancing", - "proxy", - "trusted proxy" + "bucket brigade", + "callback", + "filter", + "php_user_filter", + "stream", + "stream_filter_append", + "stream_filter_register" ], "support": { - "issues": "https://github.com/fideloper/TrustedProxy/issues", - "source": "https://github.com/fideloper/TrustedProxy/tree/4.4.1" + "issues": "https://github.com/clue/stream-filter/issues", + "source": "https://github.com/clue/stream-filter/tree/v1.7.0" }, - "time": "2020-10-22T13:48:01+00:00" + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2023-12-20T15:40:13+00:00" }, { - "name": "geocoder-php/cache-provider", - "version": "4.3.0", + "name": "daverandom/libdns", + "version": "v2.1.0", "source": { "type": "git", - "url": "https://github.com/geocoder-php/cache-provider.git", - "reference": "094e272069b4dffda18f10e75f7143a9548da53c" + "url": "https://github.com/DaveRandom/LibDNS.git", + "reference": "b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/geocoder-php/cache-provider/zipball/094e272069b4dffda18f10e75f7143a9548da53c", - "reference": "094e272069b4dffda18f10e75f7143a9548da53c", + "url": "https://api.github.com/repos/DaveRandom/LibDNS/zipball/b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a", + "reference": "b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a", "shasum": "" }, "require": { - "php": "^7.3 || ^8.0", - "psr/simple-cache": "^1.0", - "willdurand/geocoder": "^4.0.0" - }, - "provide": { - "geocoder-php/provider-implementation": "1.0" + "ext-ctype": "*", + "php": ">=7.1" }, - "require-dev": { - "phpunit/phpunit": "^9.5" + "suggest": { + "ext-intl": "Required for IDN support" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.0-dev" - } - }, "autoload": { + "files": [ + "src/functions.php" + ], "psr-4": { - "Geocoder\\Provider\\Cache\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "LibDNS\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ - { - "name": "Tobias Nyholm", - "email": "tobias.nyholm@gmail.com" - } + "description": "DNS protocol implementation written in pure PHP", + "keywords": [ + "dns" ], - "description": "Cache the result of a provider", - "homepage": "http://geocoder-php.org/Geocoder/", "support": { - "source": "https://github.com/geocoder-php/cache-provider/tree/4.3.0" + "issues": "https://github.com/DaveRandom/LibDNS/issues", + "source": "https://github.com/DaveRandom/LibDNS/tree/v2.1.0" }, - "time": "2020-12-21T16:41:18+00:00" + "time": "2024-04-12T12:12:48+00:00" }, { - "name": "geocoder-php/common-http", - "version": "4.4.0", + "name": "dedoc/scramble", + "version": "v0.11.33", "source": { "type": "git", - "url": "https://github.com/geocoder-php/php-common-http.git", - "reference": "9f44a006d4b45d01dd31ea9b38ee7fb5724cd73e" + "url": "https://github.com/dedoc/scramble.git", + "reference": "3c44e2ec517045590cb36b165967283fd5798edc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/geocoder-php/php-common-http/zipball/9f44a006d4b45d01dd31ea9b38ee7fb5724cd73e", - "reference": "9f44a006d4b45d01dd31ea9b38ee7fb5724cd73e", + "url": "https://api.github.com/repos/dedoc/scramble/zipball/3c44e2ec517045590cb36b165967283fd5798edc", + "reference": "3c44e2ec517045590cb36b165967283fd5798edc", "shasum": "" }, "require": { - "php": "^7.3 || ^8.0", - "php-http/client-implementation": "^1.0", - "php-http/discovery": "^1.6", - "php-http/httplug": "^1.0 || ^2.0", - "php-http/message-factory": "^1.0.2", - "psr/http-message": "^1.0", - "psr/http-message-implementation": "^1.0", - "willdurand/geocoder": "^4.0" + "illuminate/contracts": "^10.0|^11.0", + "myclabs/deep-copy": "^1.12", + "nikic/php-parser": "^5.0", + "php": "^8.1", + "phpstan/phpdoc-parser": "^1.0|^2.0", + "spatie/laravel-package-tools": "^1.9.2" }, "require-dev": { - "nyholm/psr7": "^1.0", - "php-http/message": "^1.0", - "php-http/mock-client": "^1.0", - "phpunit/phpunit": "^9.5", - "symfony/stopwatch": "~2.5" + "laravel/pint": "^v1.1.0", + "nunomaduro/collision": "^7.0|^8.0", + "orchestra/testbench": "^8.0|^9.0", + "pestphp/pest": "^2.34", + "pestphp/pest-plugin-laravel": "^2.3", + "phpunit/phpunit": "^10.5", + "spatie/laravel-permission": "^6.10", + "spatie/pest-plugin-snapshots": "^2.1" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "4.0-dev" + "laravel": { + "providers": [ + "Dedoc\\Scramble\\ScrambleServiceProvider" + ] } }, "autoload": { "psr-4": { - "Geocoder\\Http\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "Dedoc\\Scramble\\": "src", + "Dedoc\\Scramble\\Database\\Factories\\": "database/factories" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1357,61 +1335,64 @@ ], "authors": [ { - "name": "Tobias Nyholm", - "email": "tobias.nyholm@gmail.com" + "name": "Roman Lytvynenko", + "email": "litvinenko95@gmail.com", + "role": "Developer" } ], - "description": "Common files for HTTP based Geocoders", - "homepage": "http://geocoder-php.org", + "description": "Automatic generation of API documentation for Laravel applications.", + "homepage": "https://github.com/dedoc/scramble", "keywords": [ - "http geocoder" + "documentation", + "laravel", + "openapi" ], "support": { - "source": "https://github.com/geocoder-php/php-common-http/tree/4.4.0" + "issues": "https://github.com/dedoc/scramble/issues", + "source": "https://github.com/dedoc/scramble/tree/v0.11.33" }, - "time": "2020-12-21T09:30:01+00:00" + "funding": [ + { + "url": "https://github.com/romalytvynenko", + "type": "github" + } + ], + "time": "2025-01-08T07:53:14+00:00" }, { - "name": "geocoder-php/nominatim-provider", - "version": "5.4.0", + "name": "dflydev/dot-access-data", + "version": "v3.0.3", "source": { "type": "git", - "url": "https://github.com/geocoder-php/nominatim-provider.git", - "reference": "80f39ce41bcd0e4d9de3e83c40caf92d089fecf2" + "url": "https://github.com/dflydev/dflydev-dot-access-data.git", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/geocoder-php/nominatim-provider/zipball/80f39ce41bcd0e4d9de3e83c40caf92d089fecf2", - "reference": "80f39ce41bcd0e4d9de3e83c40caf92d089fecf2", + "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f", "shasum": "" }, "require": { - "geocoder-php/common-http": "^4.1", - "php": "^7.3 || ^8.0", - "willdurand/geocoder": "^4.0" - }, - "provide": { - "geocoder-php/provider-implementation": "1.0" + "php": "^7.1 || ^8.0" }, "require-dev": { - "geocoder-php/provider-integration-tests": "^1.0", - "php-http/curl-client": "^2.2", - "php-http/message": "^1.0", - "phpunit/phpunit": "^9.5" + "phpstan/phpstan": "^0.12.42", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.3", + "scrutinizer/ocular": "1.6.0", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.0.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "3.x-dev" } }, "autoload": { "psr-4": { - "Geocoder\\Provider\\Nominatim\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "Dflydev\\DotAccessData\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1419,56 +1400,72 @@ ], "authors": [ { - "name": "William Durand", - "email": "william.durand1@gmail.com" + "name": "Dragonfly Development Inc.", + "email": "info@dflydev.com", + "homepage": "http://dflydev.com" + }, + { + "name": "Beau Simensen", + "email": "beau@dflydev.com", + "homepage": "http://beausimensen.com" + }, + { + "name": "Carlos Frutos", + "email": "carlos@kiwing.it", + "homepage": "https://github.com/cfrutos" + }, + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com" } ], - "description": "Geocoder Nominatim adapter", - "homepage": "http://geocoder-php.org/Geocoder/", + "description": "Given a deep data structure, access data by dot notation.", + "homepage": "https://github.com/dflydev/dflydev-dot-access-data", + "keywords": [ + "access", + "data", + "dot", + "notation" + ], "support": { - "source": "https://github.com/geocoder-php/nominatim-provider/tree/5.4.0" + "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues", + "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.3" }, - "time": "2020-12-21T16:41:18+00:00" + "time": "2024-07-08T12:26:09+00:00" }, { - "name": "graham-campbell/markdown", - "version": "v13.1.1", + "name": "doctrine/cache", + "version": "2.2.0", "source": { "type": "git", - "url": "https://github.com/GrahamCampbell/Laravel-Markdown.git", - "reference": "d25b873e5c5870edc4de7d980808f1a8e092a9c7" + "url": "https://github.com/doctrine/cache.git", + "reference": "1ca8f21980e770095a31456042471a57bc4c68fb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/GrahamCampbell/Laravel-Markdown/zipball/d25b873e5c5870edc4de7d980808f1a8e092a9c7", - "reference": "d25b873e5c5870edc4de7d980808f1a8e092a9c7", + "url": "https://api.github.com/repos/doctrine/cache/zipball/1ca8f21980e770095a31456042471a57bc4c68fb", + "reference": "1ca8f21980e770095a31456042471a57bc4c68fb", "shasum": "" }, "require": { - "illuminate/contracts": "^6.0 || ^7.0 || ^8.0", - "illuminate/filesystem": "^6.0 || ^7.0 || ^8.0", - "illuminate/support": "^6.0 || ^7.0 || ^8.0", - "illuminate/view": "^6.0 || ^7.0 || ^8.0", - "league/commonmark": "^1.5", - "php": "^7.2.5 || ^8.0" + "php": "~7.1 || ^8.0" + }, + "conflict": { + "doctrine/common": ">2.2,<2.4" }, "require-dev": { - "graham-campbell/analyzer": "^3.0", - "graham-campbell/testbench": "^5.4", - "mockery/mockery": "^1.3.1", - "phpunit/phpunit": "^8.5.8 || ^9.3.7" + "cache/integration-tests": "dev-master", + "doctrine/coding-standard": "^9", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "psr/cache": "^1.0 || ^2.0 || ^3.0", + "symfony/cache": "^4.4 || ^5.4 || ^6", + "symfony/var-exporter": "^4.4 || ^5.4 || ^6" }, "type": "library", - "extra": { - "laravel": { - "providers": [ - "GrahamCampbell\\Markdown\\MarkdownServiceProvider" - ] - } - }, "autoload": { "psr-4": { - "GrahamCampbell\\Markdown\\": "src/" + "Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache" } }, "notification-url": "https://packagist.org/downloads/", @@ -1477,68 +1474,106 @@ ], "authors": [ { - "name": "Graham Campbell", - "email": "graham@alt-three.com" + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" } ], - "description": "Markdown Is A CommonMark Wrapper For Laravel", + "description": "PHP Doctrine Cache library is a popular cache implementation that supports many different drivers such as redis, memcache, apc, mongodb and others.", + "homepage": "https://www.doctrine-project.org/projects/cache.html", "keywords": [ - "Graham Campbell", - "GrahamCampbell", - "Laravel Markdown", - "Laravel-Markdown", - "common mark", - "commonmark", - "framework", - "laravel", - "markdown" + "abstraction", + "apcu", + "cache", + "caching", + "couchdb", + "memcached", + "php", + "redis", + "xcache" ], "support": { - "issues": "https://github.com/GrahamCampbell/Laravel-Markdown/issues", - "source": "https://github.com/GrahamCampbell/Laravel-Markdown/tree/v13.1.1" + "issues": "https://github.com/doctrine/cache/issues", + "source": "https://github.com/doctrine/cache/tree/2.2.0" }, "funding": [ { - "url": "https://github.com/GrahamCampbell", - "type": "github" + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" }, { - "url": "https://tidelift.com/funding/github/packagist/graham-campbell/markdown", + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcache", "type": "tidelift" } ], - "time": "2020-08-22T14:18:21+00:00" + "time": "2022-05-20T20:07:39+00:00" }, { - "name": "graham-campbell/result-type", - "version": "v1.0.1", + "name": "doctrine/dbal", + "version": "3.9.3", "source": { "type": "git", - "url": "https://github.com/GrahamCampbell/Result-Type.git", - "reference": "7e279d2cd5d7fbb156ce46daada972355cea27bb" + "url": "https://github.com/doctrine/dbal.git", + "reference": "61446f07fcb522414d6cfd8b1c3e5f9e18c579ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/7e279d2cd5d7fbb156ce46daada972355cea27bb", - "reference": "7e279d2cd5d7fbb156ce46daada972355cea27bb", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/61446f07fcb522414d6cfd8b1c3e5f9e18c579ba", + "reference": "61446f07fcb522414d6cfd8b1c3e5f9e18c579ba", "shasum": "" }, "require": { - "php": "^7.0|^8.0", - "phpoption/phpoption": "^1.7.3" + "composer-runtime-api": "^2", + "doctrine/cache": "^1.11|^2.0", + "doctrine/deprecations": "^0.5.3|^1", + "doctrine/event-manager": "^1|^2", + "php": "^7.4 || ^8.0", + "psr/cache": "^1|^2|^3", + "psr/log": "^1|^2|^3" }, "require-dev": { - "phpunit/phpunit": "^6.5|^7.5|^8.5|^9.0" + "doctrine/coding-standard": "12.0.0", + "fig/log-test": "^1", + "jetbrains/phpstorm-stubs": "2023.1", + "phpstan/phpstan": "1.12.6", + "phpstan/phpstan-strict-rules": "^1.6", + "phpunit/phpunit": "9.6.20", + "psalm/plugin-phpunit": "0.18.4", + "slevomat/coding-standard": "8.13.1", + "squizlabs/php_codesniffer": "3.10.2", + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/console": "^4.4|^5.4|^6.0|^7.0", + "vimeo/psalm": "4.30.0" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } + "suggest": { + "symfony/console": "For helpful console commands such as SQL execution and import of files." }, + "bin": [ + "bin/doctrine-dbal" + ], + "type": "library", "autoload": { "psr-4": { - "GrahamCampbell\\ResultType\\": "src/" + "Doctrine\\DBAL\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1547,170 +1582,140 @@ ], "authors": [ { - "name": "Graham Campbell", - "email": "graham@alt-three.com" + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" } ], - "description": "An Implementation Of The Result Type", + "description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.", + "homepage": "https://www.doctrine-project.org/projects/dbal.html", "keywords": [ - "Graham Campbell", - "GrahamCampbell", - "Result Type", - "Result-Type", - "result" + "abstraction", + "database", + "db2", + "dbal", + "mariadb", + "mssql", + "mysql", + "oci8", + "oracle", + "pdo", + "pgsql", + "postgresql", + "queryobject", + "sasql", + "sql", + "sqlite", + "sqlserver", + "sqlsrv" ], "support": { - "issues": "https://github.com/GrahamCampbell/Result-Type/issues", - "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.0.1" + "issues": "https://github.com/doctrine/dbal/issues", + "source": "https://github.com/doctrine/dbal/tree/3.9.3" }, "funding": [ { - "url": "https://github.com/GrahamCampbell", - "type": "github" + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" }, { - "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type", + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdbal", "type": "tidelift" } ], - "time": "2020-04-13T13:17:36+00:00" + "time": "2024-10-10T17:56:43+00:00" }, { - "name": "guzzlehttp/guzzle", - "version": "7.3.0", + "name": "doctrine/deprecations", + "version": "1.1.4", "source": { "type": "git", - "url": "https://github.com/guzzle/guzzle.git", - "reference": "7008573787b430c1c1f650e3722d9bba59967628" + "url": "https://github.com/doctrine/deprecations.git", + "reference": "31610dbb31faa98e6b5447b62340826f54fbc4e9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7008573787b430c1c1f650e3722d9bba59967628", - "reference": "7008573787b430c1c1f650e3722d9bba59967628", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/31610dbb31faa98e6b5447b62340826f54fbc4e9", + "reference": "31610dbb31faa98e6b5447b62340826f54fbc4e9", "shasum": "" }, "require": { - "ext-json": "*", - "guzzlehttp/promises": "^1.4", - "guzzlehttp/psr7": "^1.7 || ^2.0", - "php": "^7.2.5 || ^8.0", - "psr/http-client": "^1.0" - }, - "provide": { - "psr/http-client-implementation": "1.0" + "php": "^7.1 || ^8.0" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.4.1", - "ext-curl": "*", - "php-http/client-integration-tests": "^3.0", - "phpunit/phpunit": "^8.5.5 || ^9.3.5", - "psr/log": "^1.1" + "doctrine/coding-standard": "^9 || ^12", + "phpstan/phpstan": "1.4.10 || 2.0.3", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "psr/log": "^1 || ^2 || ^3" }, "suggest": { - "ext-curl": "Required for CURL handler support", - "ext-intl": "Required for Internationalized Domain Name (IDN) support", - "psr/log": "Required for using the Log middleware" + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "7.3-dev" - } - }, "autoload": { "psr-4": { - "GuzzleHttp\\": "src/" - }, - "files": [ - "src/functions_include.php" - ] + "Doctrine\\Deprecations\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - }, - { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com", - "homepage": "https://sagikazarmark.hu" - } - ], - "description": "Guzzle is a PHP HTTP client library", - "homepage": "http://guzzlephp.org/", - "keywords": [ - "client", - "curl", - "framework", - "http", - "http client", - "psr-18", - "psr-7", - "rest", - "web service" - ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", "support": { - "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.3.0" + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.4" }, - "funding": [ - { - "url": "https://github.com/GrahamCampbell", - "type": "github" - }, - { - "url": "https://github.com/Nyholm", - "type": "github" - }, - { - "url": "https://github.com/alexeyshockov", - "type": "github" - }, - { - "url": "https://github.com/gmponos", - "type": "github" - } - ], - "time": "2021-03-23T11:33:13+00:00" + "time": "2024-12-07T21:18:45+00:00" }, { - "name": "guzzlehttp/promises", - "version": "1.4.1", + "name": "doctrine/event-manager", + "version": "2.0.1", "source": { "type": "git", - "url": "https://github.com/guzzle/promises.git", - "reference": "8e7d04f1f6450fef59366c399cfad4b9383aa30d" + "url": "https://github.com/doctrine/event-manager.git", + "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/8e7d04f1f6450fef59366c399cfad4b9383aa30d", - "reference": "8e7d04f1f6450fef59366c399cfad4b9383aa30d", + "url": "https://api.github.com/repos/doctrine/event-manager/zipball/b680156fa328f1dfd874fd48c7026c41570b9c6e", + "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e", "shasum": "" }, "require": { - "php": ">=5.5" + "php": "^8.1" + }, + "conflict": { + "doctrine/common": "<2.9" }, "require-dev": { - "symfony/phpunit-bridge": "^4.4 || ^5.1" + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.8.8", + "phpunit/phpunit": "^10.5", + "vimeo/psalm": "^5.24" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.4-dev" - } - }, "autoload": { "psr-4": { - "GuzzleHttp\\Promise\\": "src/" - }, - "files": [ - "src/functions_include.php" - ] + "Doctrine\\Common\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1718,63 +1723,3730 @@ ], "authors": [ { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - } - ], - "description": "Guzzle promises library", - "keywords": [ - "promise" - ], - "support": { - "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/1.4.1" - }, - "time": "2021-03-07T09:25:29+00:00" - }, - { - "name": "guzzlehttp/psr7", - "version": "1.8.2", - "source": { - "type": "git", - "url": "https://github.com/guzzle/psr7.git", - "reference": "dc960a912984efb74d0a90222870c72c87f10c91" - }, + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.", + "homepage": "https://www.doctrine-project.org/projects/event-manager.html", + "keywords": [ + "event", + "event dispatcher", + "event manager", + "event system", + "events" + ], + "support": { + "issues": "https://github.com/doctrine/event-manager/issues", + "source": "https://github.com/doctrine/event-manager/tree/2.0.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fevent-manager", + "type": "tidelift" + } + ], + "time": "2024-05-22T20:47:39+00:00" + }, + { + "name": "doctrine/inflector", + "version": "2.0.10", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/5817d0659c5b50c9b950feb9af7b9668e2c436bc", + "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^11.0", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.3", + "phpunit/phpunit": "^8.5 || ^9.5", + "vimeo/psalm": "^4.25 || ^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Inflector\\": "lib/Doctrine/Inflector" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", + "keywords": [ + "inflection", + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" + ], + "support": { + "issues": "https://github.com/doctrine/inflector/issues", + "source": "https://github.com/doctrine/inflector/tree/2.0.10" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" + } + ], + "time": "2024-02-18T20:23:39+00:00" + }, + { + "name": "doctrine/lexer", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5", + "psalm/plugin-phpunit": "^0.18.3", + "vimeo/psalm": "^5.21" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/3.0.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2024-02-05T11:56:58+00:00" + }, + { + "name": "dragonmantank/cron-expression", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/dragonmantank/cron-expression.git", + "reference": "8c784d071debd117328803d86b2097615b457500" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/8c784d071debd117328803d86b2097615b457500", + "reference": "8c784d071debd117328803d86b2097615b457500", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0", + "webmozart/assert": "^1.0" + }, + "replace": { + "mtdowling/cron-expression": "^1.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^1.0", + "phpunit/phpunit": "^7.0|^8.0|^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Cron\\": "src/Cron/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Tankersley", + "email": "chris@ctankersley.com", + "homepage": "https://github.com/dragonmantank" + } + ], + "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due", + "keywords": [ + "cron", + "schedule" + ], + "support": { + "issues": "https://github.com/dragonmantank/cron-expression/issues", + "source": "https://github.com/dragonmantank/cron-expression/tree/v3.4.0" + }, + "funding": [ + { + "url": "https://github.com/dragonmantank", + "type": "github" + } + ], + "time": "2024-10-09T13:47:03+00:00" + }, + { + "name": "egulias/email-validator", + "version": "4.0.3", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "b115554301161fa21467629f1e1391c1936de517" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/b115554301161fa21467629f1e1391c1936de517", + "reference": "b115554301161fa21467629f1e1391c1936de517", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^2.0 || ^3.0", + "php": ">=8.1", + "symfony/polyfill-intl-idn": "^1.26" + }, + "require-dev": { + "phpunit/phpunit": "^10.2", + "vimeo/psalm": "^5.12" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eduardo Gulias Davis" + } + ], + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": [ + "email", + "emailvalidation", + "emailvalidator", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/egulias/EmailValidator/issues", + "source": "https://github.com/egulias/EmailValidator/tree/4.0.3" + }, + "funding": [ + { + "url": "https://github.com/egulias", + "type": "github" + } + ], + "time": "2024-12-27T00:36:43+00:00" + }, + { + "name": "evenement/evenement", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/igorw/evenement.git", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^9 || ^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Evenement\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + } + ], + "description": "Événement is a very simple event dispatching library for PHP", + "keywords": [ + "event-dispatcher", + "event-emitter" + ], + "support": { + "issues": "https://github.com/igorw/evenement/issues", + "source": "https://github.com/igorw/evenement/tree/v3.0.2" + }, + "time": "2023-08-08T05:53:35+00:00" + }, + { + "name": "firebase/php-jwt", + "version": "v6.10.2", + "source": { + "type": "git", + "url": "https://github.com/firebase/php-jwt.git", + "reference": "30c19ed0f3264cb660ea496895cfb6ef7ee3653b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/30c19ed0f3264cb660ea496895cfb6ef7ee3653b", + "reference": "30c19ed0f3264cb660ea496895cfb6ef7ee3653b", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^7.4", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "psr/cache": "^2.0||^3.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0" + }, + "suggest": { + "ext-sodium": "Support EdDSA (Ed25519) signatures", + "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" + }, + "type": "library", + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/firebase/php-jwt", + "keywords": [ + "jwt", + "php" + ], + "support": { + "issues": "https://github.com/firebase/php-jwt/issues", + "source": "https://github.com/firebase/php-jwt/tree/v6.10.2" + }, + "time": "2024-11-24T11:22:49+00:00" + }, + { + "name": "fruitcake/php-cors", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/fruitcake/php-cors.git", + "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/3d158f36e7875e2f040f37bc0573956240a5a38b", + "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0", + "symfony/http-foundation": "^4.4|^5.4|^6|^7" + }, + "require-dev": { + "phpstan/phpstan": "^1.4", + "phpunit/phpunit": "^9", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "Fruitcake\\Cors\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fruitcake", + "homepage": "https://fruitcake.nl" + }, + { + "name": "Barryvdh", + "email": "barryvdh@gmail.com" + } + ], + "description": "Cross-origin resource sharing library for the Symfony HttpFoundation", + "homepage": "https://github.com/fruitcake/php-cors", + "keywords": [ + "cors", + "laravel", + "symfony" + ], + "support": { + "issues": "https://github.com/fruitcake/php-cors/issues", + "source": "https://github.com/fruitcake/php-cors/tree/v1.3.0" + }, + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2023-10-12T05:21:21+00:00" + }, + { + "name": "fylax/forceutf8", + "version": "v3.0.3", + "source": { + "type": "git", + "url": "https://github.com/Fylax/forceutf8.git", + "reference": "efaa4ec353ce35931ef469632fece63df4c5a301" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Fylax/forceutf8/zipball/efaa4ec353ce35931ef469632fece63df4c5a301", + "reference": "efaa4ec353ce35931ef469632fece63df4c5a301", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.6" + }, + "suggest": { + "ext-iconv": "To convert non-UTF-8 strings to UTF-8", + "ext-mbstring": "To convert non-UTF-8 strings to UTF-8" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/neitanod/forceutf8", + "name": "neitanod/forceutf8" + } + }, + "autoload": { + "psr-4": { + "ForceUTF8\\": "ForceUTF8" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nico Caprioli", + "email": "nico.caprioli@gmail.com", + "role": "Developer" + } + ], + "description": "PHP Class Encoding featuring popular Encoding::toUTF8() function --formerly known as forceUTF8()-- that fixes mixed encoded strings.", + "homepage": "https://github.com/Fylax/forceutf8", + "support": { + "issues": "https://github.com/Fylax/forceutf8/issues", + "source": "https://github.com/Fylax/forceutf8/tree/v3.0.3" + }, + "time": "2023-06-06T09:49:33+00:00" + }, + { + "name": "geocoder-php/cache-provider", + "version": "4.4.0", + "source": { + "type": "git", + "url": "https://github.com/geocoder-php/cache-provider.git", + "reference": "0b03ae7c3108c1e48d6615cc52cac3d52bcff797" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/geocoder-php/cache-provider/zipball/0b03ae7c3108c1e48d6615cc52cac3d52bcff797", + "reference": "0b03ae7c3108c1e48d6615cc52cac3d52bcff797", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "willdurand/geocoder": "^4.0.0" + }, + "provide": { + "geocoder-php/provider-implementation": "1.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "psr-4": { + "Geocoder\\Provider\\Cache\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + } + ], + "description": "Cache the result of a provider", + "homepage": "http://geocoder-php.org/Geocoder/", + "support": { + "source": "https://github.com/geocoder-php/cache-provider/tree/4.4.0" + }, + "time": "2022-07-30T12:09:30+00:00" + }, + { + "name": "geocoder-php/common-http", + "version": "4.6.0", + "source": { + "type": "git", + "url": "https://github.com/geocoder-php/php-common-http.git", + "reference": "d8c22a66120daed35ba8017467bc1ebfec28a63e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/geocoder-php/php-common-http/zipball/d8c22a66120daed35ba8017467bc1ebfec28a63e", + "reference": "d8c22a66120daed35ba8017467bc1ebfec28a63e", + "shasum": "" + }, + "require": { + "php": "^8.0", + "php-http/discovery": "^1.17", + "psr/http-client-implementation": "^1.0", + "psr/http-factory-implementation": "^1.0", + "willdurand/geocoder": "^4.0" + }, + "require-dev": { + "nyholm/psr7": "^1.0", + "php-http/message": "^1.0", + "php-http/mock-client": "^1.0", + "phpunit/phpunit": "^9.5", + "symfony/stopwatch": "~2.5 || ~5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "psr-4": { + "Geocoder\\Http\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + } + ], + "description": "Common files for HTTP based Geocoders", + "homepage": "http://geocoder-php.org", + "keywords": [ + "http geocoder" + ], + "support": { + "source": "https://github.com/geocoder-php/php-common-http/tree/4.6.0" + }, + "time": "2023-12-28T10:51:54+00:00" + }, + { + "name": "geocoder-php/nominatim-provider", + "version": "5.7.0", + "source": { + "type": "git", + "url": "https://github.com/geocoder-php/nominatim-provider.git", + "reference": "a50486161f6babad7b1ed7ee7bf86147b0844413" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/geocoder-php/nominatim-provider/zipball/a50486161f6babad7b1ed7ee7bf86147b0844413", + "reference": "a50486161f6babad7b1ed7ee7bf86147b0844413", + "shasum": "" + }, + "require": { + "geocoder-php/common-http": "^4.1", + "php": "^7.4 || ^8.0", + "willdurand/geocoder": "^4.0" + }, + "provide": { + "geocoder-php/provider-implementation": "1.0" + }, + "require-dev": { + "geocoder-php/provider-integration-tests": "^1.0", + "php-http/curl-client": "^2.2", + "php-http/message": "^1.0", + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "psr-4": { + "Geocoder\\Provider\\Nominatim\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "William Durand", + "email": "william.durand1@gmail.com" + } + ], + "description": "Geocoder Nominatim adapter", + "homepage": "http://geocoder-php.org/Geocoder/", + "support": { + "source": "https://github.com/geocoder-php/nominatim-provider/tree/5.7.0" + }, + "time": "2023-03-01T10:18:27+00:00" + }, + { + "name": "graham-campbell/markdown", + "version": "v15.2.0", + "source": { + "type": "git", + "url": "https://github.com/GrahamCampbell/Laravel-Markdown.git", + "reference": "d594fc197b9068de5e234a890be361807a1ab34f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GrahamCampbell/Laravel-Markdown/zipball/d594fc197b9068de5e234a890be361807a1ab34f", + "reference": "d594fc197b9068de5e234a890be361807a1ab34f", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^8.75 || ^9.0 || ^10.0 || ^11.0", + "illuminate/filesystem": "^8.75 || ^9.0 || ^10.0 || ^11.0", + "illuminate/support": "^8.75 || ^9.0 || ^10.0 || ^11.0", + "illuminate/view": "^8.75 || ^9.0 || ^10.0 || ^11.0", + "league/commonmark": "^2.4.2", + "php": "^7.4.15 || ^8.0.2" + }, + "require-dev": { + "graham-campbell/analyzer": "^4.1", + "graham-campbell/testbench": "^6.1", + "mockery/mockery": "^1.6.6", + "phpunit/phpunit": "^9.6.17 || ^10.5.13" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "GrahamCampbell\\Markdown\\MarkdownServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "GrahamCampbell\\Markdown\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "Markdown Is A CommonMark Wrapper For Laravel", + "keywords": [ + "Graham Campbell", + "GrahamCampbell", + "Laravel Markdown", + "Laravel-Markdown", + "common mark", + "commonmark", + "framework", + "laravel", + "markdown" + ], + "support": { + "issues": "https://github.com/GrahamCampbell/Laravel-Markdown/issues", + "source": "https://github.com/GrahamCampbell/Laravel-Markdown/tree/v15.2.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/graham-campbell/markdown", + "type": "tidelift" + } + ], + "time": "2024-03-17T23:07:39+00:00" + }, + { + "name": "graham-campbell/result-type", + "version": "v1.1.3", + "source": { + "type": "git", + "url": "https://github.com/GrahamCampbell/Result-Type.git", + "reference": "3ba905c11371512af9d9bdd27d99b782216b6945" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/3ba905c11371512af9d9bdd27d99b782216b6945", + "reference": "3ba905c11371512af9d9bdd27d99b782216b6945", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.3" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28" + }, + "type": "library", + "autoload": { + "psr-4": { + "GrahamCampbell\\ResultType\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "An Implementation Of The Result Type", + "keywords": [ + "Graham Campbell", + "GrahamCampbell", + "Result Type", + "Result-Type", + "result" + ], + "support": { + "issues": "https://github.com/GrahamCampbell/Result-Type/issues", + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type", + "type": "tidelift" + } + ], + "time": "2024-07-20T21:45:45+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.9.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "d281ed313b989f213357e3be1a179f02196ac99b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/d281ed313b989f213357e3be1a179f02196ac99b", + "reference": "d281ed313b989f213357e3be1a179f02196ac99b", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.5.3 || ^2.0.3", + "guzzlehttp/psr7": "^2.7.0", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "3.0.2", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.9.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2024-07-24T11:22:20+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/f9c436286ab2892c7db7be8c8da4ef61ccf7b455", + "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2024-10-17T10:06:22+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.7.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/a70f5c95fb43bc83f07c9c948baa0dc1829bf201", + "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "0.9.0", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.7.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2024-07-18T11:15:46+00:00" + }, + { + "name": "guzzlehttp/uri-template", + "version": "v1.0.3", + "source": { + "type": "git", + "url": "https://github.com/guzzle/uri-template.git", + "reference": "ecea8feef63bd4fef1f037ecb288386999ecc11c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/ecea8feef63bd4fef1f037ecb288386999ecc11c", + "reference": "ecea8feef63bd4fef1f037ecb288386999ecc11c", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/polyfill-php80": "^1.24" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.36 || ^9.6.15", + "uri-template/tests": "1.0.0" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\UriTemplate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + } + ], + "description": "A polyfill class for uri_template of PHP", + "keywords": [ + "guzzlehttp", + "uri-template" + ], + "support": { + "issues": "https://github.com/guzzle/uri-template/issues", + "source": "https://github.com/guzzle/uri-template/tree/v1.0.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/uri-template", + "type": "tidelift" + } + ], + "time": "2023-12-03T19:50:20+00:00" + }, + { + "name": "kelunik/certificate", + "version": "v1.1.3", + "source": { + "type": "git", + "url": "https://github.com/kelunik/certificate.git", + "reference": "7e00d498c264d5eb4f78c69f41c8bd6719c0199e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/kelunik/certificate/zipball/7e00d498c264d5eb4f78c69f41c8bd6719c0199e", + "reference": "7e00d498c264d5eb4f78c69f41c8bd6719c0199e", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "php": ">=7.0" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^6 | 7 | ^8 | ^9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Kelunik\\Certificate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Access certificate details and transform between different formats.", + "keywords": [ + "DER", + "certificate", + "certificates", + "openssl", + "pem", + "x509" + ], + "support": { + "issues": "https://github.com/kelunik/certificate/issues", + "source": "https://github.com/kelunik/certificate/tree/v1.1.3" + }, + "time": "2023-02-03T21:26:53+00:00" + }, + { + "name": "laragear/meta-model", + "version": "v1.1.0", + "source": { + "type": "git", + "url": "https://github.com/Laragear/MetaModel.git", + "reference": "86aa8bbd0e1b9d03467a0257f0cd5815b6836a34" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Laragear/MetaModel/zipball/86aa8bbd0e1b9d03467a0257f0cd5815b6836a34", + "reference": "86aa8bbd0e1b9d03467a0257f0cd5815b6836a34", + "shasum": "" + }, + "require": { + "illuminate/database": "10.*|11.*", + "php": "^8.1" + }, + "require-dev": { + "mockery/mockery": "^1.6", + "phpunit/phpunit": "^10.5|11.*" + }, + "type": "library", + "autoload": { + "psr-4": { + "Laragear\\MetaModel\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Italo Israel Baeza Cabrera", + "email": "DarkGhostHunter@Gmail.com", + "homepage": "https://github.com/sponsors/DarkGhostHunter", + "role": "Developer" + } + ], + "description": "Let other developers customize your package model and migrations", + "keywords": [ + "database", + "eloquent", + "laravel", + "model" + ], + "support": { + "issues": "https://github.com/Laragear/MetaModel/issues", + "source": "https://github.com/Laragear/MetaModel" + }, + "funding": [ + { + "url": "https://github.com/sponsors/DarkGhostHunter", + "type": "Github Sponsorship" + }, + { + "url": "https://paypal.me/darkghosthunter", + "type": "Paypal" + } + ], + "time": "2024-03-15T23:27:56+00:00" + }, + { + "name": "laragear/webauthn", + "version": "v3.1.0", + "source": { + "type": "git", + "url": "https://github.com/Laragear/WebAuthn.git", + "reference": "5136b8fee90abb5c0b50b644594fb3142191f3b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Laragear/WebAuthn/zipball/5136b8fee90abb5c0b50b644594fb3142191f3b9", + "reference": "5136b8fee90abb5c0b50b644594fb3142191f3b9", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-openssl": "*", + "illuminate/auth": "10.*|11.*", + "illuminate/config": "10.*|11.*", + "illuminate/database": "10.*|11.*", + "illuminate/encryption": "10.*|11.*", + "illuminate/http": "10.*|11.*", + "illuminate/session": "10.*|11.*", + "illuminate/support": "10.*|11.*", + "laragear/meta-model": "^1.1", + "php": "^8.1" + }, + "require-dev": { + "ext-sodium": "*", + "orchestra/testbench": "8.*|9.*" + }, + "suggest": { + "paragonie/sodium_compat": "To enable EdDSA 25519 keys from authenticators, if `ext-sodium` is unavailable." + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laragear\\WebAuthn\\WebAuthnServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laragear\\WebAuthn\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lukas Buchs", + "role": "Original developer" + }, + { + "name": "Italo Israel Baeza Cabrera", + "email": "DarkGhostHunter@Gmail.com", + "homepage": "https://github.com/sponsors/DarkGhostHunter", + "role": "Developer" + } + ], + "description": "Authenticate users with Passkeys: fingerprints, patterns and biometric data.", + "homepage": "https://github.com/laragear/webauthn", + "keywords": [ + "Authentication", + "faceid", + "laravel", + "passkeys", + "touchid", + "webauthn", + "windows hello" + ], + "support": { + "issues": "https://github.com/Laragear/WebAuthn/issues", + "source": "https://github.com/Laragear/WebAuthn" + }, + "funding": [ + { + "url": "https://github.com/sponsors/DarkGhostHunter", + "type": "Github Sponsorship" + }, + { + "url": "https://paypal.me/darkghosthunter", + "type": "Paypal" + } + ], + "time": "2024-08-15T07:50:06+00:00" + }, + { + "name": "laravel/framework", + "version": "v11.38.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/framework.git", + "reference": "5d964a15efc8fffcb175aed3976796c0420bcf99" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/framework/zipball/5d964a15efc8fffcb175aed3976796c0420bcf99", + "reference": "5d964a15efc8fffcb175aed3976796c0420bcf99", + "shasum": "" + }, + "require": { + "brick/math": "^0.9.3|^0.10.2|^0.11|^0.12", + "composer-runtime-api": "^2.2", + "doctrine/inflector": "^2.0.5", + "dragonmantank/cron-expression": "^3.4", + "egulias/email-validator": "^3.2.1|^4.0", + "ext-ctype": "*", + "ext-filter": "*", + "ext-hash": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "ext-session": "*", + "ext-tokenizer": "*", + "fruitcake/php-cors": "^1.3", + "guzzlehttp/guzzle": "^7.8.2", + "guzzlehttp/uri-template": "^1.0", + "laravel/prompts": "^0.1.18|^0.2.0|^0.3.0", + "laravel/serializable-closure": "^1.3|^2.0", + "league/commonmark": "^2.6", + "league/flysystem": "^3.25.1", + "league/flysystem-local": "^3.25.1", + "league/uri": "^7.5.1", + "monolog/monolog": "^3.0", + "nesbot/carbon": "^2.72.2|^3.4", + "nunomaduro/termwind": "^2.0", + "php": "^8.2", + "psr/container": "^1.1.1|^2.0.1", + "psr/log": "^1.0|^2.0|^3.0", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "ramsey/uuid": "^4.7", + "symfony/console": "^7.0.3", + "symfony/error-handler": "^7.0.3", + "symfony/finder": "^7.0.3", + "symfony/http-foundation": "^7.2.0", + "symfony/http-kernel": "^7.0.3", + "symfony/mailer": "^7.0.3", + "symfony/mime": "^7.0.3", + "symfony/polyfill-php83": "^1.31", + "symfony/process": "^7.0.3", + "symfony/routing": "^7.0.3", + "symfony/uid": "^7.0.3", + "symfony/var-dumper": "^7.0.3", + "tijsverkoyen/css-to-inline-styles": "^2.2.5", + "vlucas/phpdotenv": "^5.6.1", + "voku/portable-ascii": "^2.0.2" + }, + "conflict": { + "tightenco/collect": "<5.5.33" + }, + "provide": { + "psr/container-implementation": "1.1|2.0", + "psr/log-implementation": "1.0|2.0|3.0", + "psr/simple-cache-implementation": "1.0|2.0|3.0" + }, + "replace": { + "illuminate/auth": "self.version", + "illuminate/broadcasting": "self.version", + "illuminate/bus": "self.version", + "illuminate/cache": "self.version", + "illuminate/collections": "self.version", + "illuminate/concurrency": "self.version", + "illuminate/conditionable": "self.version", + "illuminate/config": "self.version", + "illuminate/console": "self.version", + "illuminate/container": "self.version", + "illuminate/contracts": "self.version", + "illuminate/cookie": "self.version", + "illuminate/database": "self.version", + "illuminate/encryption": "self.version", + "illuminate/events": "self.version", + "illuminate/filesystem": "self.version", + "illuminate/hashing": "self.version", + "illuminate/http": "self.version", + "illuminate/log": "self.version", + "illuminate/macroable": "self.version", + "illuminate/mail": "self.version", + "illuminate/notifications": "self.version", + "illuminate/pagination": "self.version", + "illuminate/pipeline": "self.version", + "illuminate/process": "self.version", + "illuminate/queue": "self.version", + "illuminate/redis": "self.version", + "illuminate/routing": "self.version", + "illuminate/session": "self.version", + "illuminate/support": "self.version", + "illuminate/testing": "self.version", + "illuminate/translation": "self.version", + "illuminate/validation": "self.version", + "illuminate/view": "self.version", + "spatie/once": "*" + }, + "require-dev": { + "ably/ably-php": "^1.0", + "aws/aws-sdk-php": "^3.322.9", + "ext-gmp": "*", + "fakerphp/faker": "^1.24", + "guzzlehttp/promises": "^2.0.3", + "guzzlehttp/psr7": "^2.4", + "league/flysystem-aws-s3-v3": "^3.25.1", + "league/flysystem-ftp": "^3.25.1", + "league/flysystem-path-prefixing": "^3.25.1", + "league/flysystem-read-only": "^3.25.1", + "league/flysystem-sftp-v3": "^3.25.1", + "mockery/mockery": "^1.6.10", + "orchestra/testbench-core": "^9.6", + "pda/pheanstalk": "^5.0.6", + "php-http/discovery": "^1.15", + "phpstan/phpstan": "^1.11.5", + "phpunit/phpunit": "^10.5.35|^11.3.6", + "predis/predis": "^2.3", + "resend/resend-php": "^0.10.0", + "symfony/cache": "^7.0.3", + "symfony/http-client": "^7.0.3", + "symfony/psr-http-message-bridge": "^7.0.3", + "symfony/translation": "^7.0.3" + }, + "suggest": { + "ably/ably-php": "Required to use the Ably broadcast driver (^1.0).", + "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage, and SES mail driver (^3.322.9).", + "brianium/paratest": "Required to run tests in parallel (^7.0|^8.0).", + "ext-apcu": "Required to use the APC cache driver.", + "ext-fileinfo": "Required to use the Filesystem class.", + "ext-ftp": "Required to use the Flysystem FTP driver.", + "ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image().", + "ext-memcached": "Required to use the memcache cache driver.", + "ext-pcntl": "Required to use all features of the queue worker and console signal trapping.", + "ext-pdo": "Required to use all database features.", + "ext-posix": "Required to use all features of the queue worker.", + "ext-redis": "Required to use the Redis cache and queue drivers (^4.0|^5.0|^6.0).", + "fakerphp/faker": "Required to use the eloquent factory builder (^1.9.1).", + "filp/whoops": "Required for friendly error pages in development (^2.14.3).", + "laravel/tinker": "Required to use the tinker console command (^2.0).", + "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.25.1).", + "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.25.1).", + "league/flysystem-path-prefixing": "Required to use the scoped driver (^3.25.1).", + "league/flysystem-read-only": "Required to use read-only disks (^3.25.1)", + "league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.25.1).", + "mockery/mockery": "Required to use mocking (^1.6).", + "pda/pheanstalk": "Required to use the beanstalk queue driver (^5.0).", + "php-http/discovery": "Required to use PSR-7 bridging features (^1.15).", + "phpunit/phpunit": "Required to use assertions and run tests (^10.5|^11.0).", + "predis/predis": "Required to use the predis connector (^2.3).", + "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", + "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0).", + "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0).", + "symfony/cache": "Required to PSR-6 cache bridge (^7.0).", + "symfony/filesystem": "Required to enable support for relative symbolic links (^7.0).", + "symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.0).", + "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^7.0).", + "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^7.0).", + "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.0)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "11.x-dev" + } + }, + "autoload": { + "files": [ + "src/Illuminate/Collections/functions.php", + "src/Illuminate/Collections/helpers.php", + "src/Illuminate/Events/functions.php", + "src/Illuminate/Filesystem/functions.php", + "src/Illuminate/Foundation/helpers.php", + "src/Illuminate/Log/functions.php", + "src/Illuminate/Support/functions.php", + "src/Illuminate/Support/helpers.php" + ], + "psr-4": { + "Illuminate\\": "src/Illuminate/", + "Illuminate\\Support\\": [ + "src/Illuminate/Macroable/", + "src/Illuminate/Collections/", + "src/Illuminate/Conditionable/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Laravel Framework.", + "homepage": "https://laravel.com", + "keywords": [ + "framework", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-01-14T14:53:42+00:00" + }, + { + "name": "laravel/prompts", + "version": "v0.3.3", + "source": { + "type": "git", + "url": "https://github.com/laravel/prompts.git", + "reference": "749395fcd5f8f7530fe1f00dfa84eb22c83d94ea" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/prompts/zipball/749395fcd5f8f7530fe1f00dfa84eb22c83d94ea", + "reference": "749395fcd5f8f7530fe1f00dfa84eb22c83d94ea", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.2", + "ext-mbstring": "*", + "php": "^8.1", + "symfony/console": "^6.2|^7.0" + }, + "conflict": { + "illuminate/console": ">=10.17.0 <10.25.0", + "laravel/framework": ">=10.17.0 <10.25.0" + }, + "require-dev": { + "illuminate/collections": "^10.0|^11.0", + "mockery/mockery": "^1.5", + "pestphp/pest": "^2.3|^3.4", + "phpstan/phpstan": "^1.11", + "phpstan/phpstan-mockery": "^1.1" + }, + "suggest": { + "ext-pcntl": "Required for the spinner to be animated." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "0.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Laravel\\Prompts\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Add beautiful and user-friendly forms to your command-line applications.", + "support": { + "issues": "https://github.com/laravel/prompts/issues", + "source": "https://github.com/laravel/prompts/tree/v0.3.3" + }, + "time": "2024-12-30T15:53:31+00:00" + }, + { + "name": "laravel/serializable-closure", + "version": "v2.0.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/serializable-closure.git", + "reference": "613b2d4998f85564d40497e05e89cb6d9bd1cbe8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/613b2d4998f85564d40497e05e89cb6d9bd1cbe8", + "reference": "613b2d4998f85564d40497e05e89cb6d9bd1cbe8", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "illuminate/support": "^10.0|^11.0", + "nesbot/carbon": "^2.67|^3.0", + "pestphp/pest": "^2.36", + "phpstan/phpstan": "^2.0", + "symfony/var-dumper": "^6.2.0|^7.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\SerializableClosure\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "nuno@laravel.com" + } + ], + "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", + "keywords": [ + "closure", + "laravel", + "serializable" + ], + "support": { + "issues": "https://github.com/laravel/serializable-closure/issues", + "source": "https://github.com/laravel/serializable-closure" + }, + "time": "2024-12-16T15:26:28+00:00" + }, + { + "name": "laravel/socialite", + "version": "v5.16.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/socialite.git", + "reference": "4e5be83c0b3ecf81b2ffa47092e917d1f79dce71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/socialite/zipball/4e5be83c0b3ecf81b2ffa47092e917d1f79dce71", + "reference": "4e5be83c0b3ecf81b2ffa47092e917d1f79dce71", + "shasum": "" + }, + "require": { + "ext-json": "*", + "firebase/php-jwt": "^6.4", + "guzzlehttp/guzzle": "^6.0|^7.0", + "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", + "illuminate/http": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", + "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", + "league/oauth1-client": "^1.11", + "php": "^7.2|^8.0", + "phpseclib/phpseclib": "^3.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "orchestra/testbench": "^4.0|^5.0|^6.0|^7.0|^8.0|^9.0", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^8.0|^9.3|^10.4" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Socialite": "Laravel\\Socialite\\Facades\\Socialite" + }, + "providers": [ + "Laravel\\Socialite\\SocialiteServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Socialite\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel wrapper around OAuth 1 & OAuth 2 libraries.", + "homepage": "https://laravel.com", + "keywords": [ + "laravel", + "oauth" + ], + "support": { + "issues": "https://github.com/laravel/socialite/issues", + "source": "https://github.com/laravel/socialite" + }, + "time": "2024-12-11T16:43:51+00:00" + }, + { + "name": "lcobucci/clock", + "version": "3.3.1", + "source": { + "type": "git", + "url": "https://github.com/lcobucci/clock.git", + "reference": "db3713a61addfffd615b79bf0bc22f0ccc61b86b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lcobucci/clock/zipball/db3713a61addfffd615b79bf0bc22f0ccc61b86b", + "reference": "db3713a61addfffd615b79bf0bc22f0ccc61b86b", + "shasum": "" + }, + "require": { + "php": "~8.2.0 || ~8.3.0 || ~8.4.0", + "psr/clock": "^1.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "infection/infection": "^0.29", + "lcobucci/coding-standard": "^11.1.0", + "phpstan/extension-installer": "^1.3.1", + "phpstan/phpstan": "^1.10.25", + "phpstan/phpstan-deprecation-rules": "^1.1.3", + "phpstan/phpstan-phpunit": "^1.3.13", + "phpstan/phpstan-strict-rules": "^1.5.1", + "phpunit/phpunit": "^11.3.6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Lcobucci\\Clock\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Luís Cobucci", + "email": "lcobucci@gmail.com" + } + ], + "description": "Yet another clock abstraction", + "support": { + "issues": "https://github.com/lcobucci/clock/issues", + "source": "https://github.com/lcobucci/clock/tree/3.3.1" + }, + "funding": [ + { + "url": "https://github.com/lcobucci", + "type": "github" + }, + { + "url": "https://www.patreon.com/lcobucci", + "type": "patreon" + } + ], + "time": "2024-09-24T20:45:14+00:00" + }, + { + "name": "lcobucci/jwt", + "version": "5.4.2", + "source": { + "type": "git", + "url": "https://github.com/lcobucci/jwt.git", + "reference": "ea1ce71cbf9741e445a5914e2f67cdbb484ff712" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lcobucci/jwt/zipball/ea1ce71cbf9741e445a5914e2f67cdbb484ff712", + "reference": "ea1ce71cbf9741e445a5914e2f67cdbb484ff712", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "ext-sodium": "*", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0", + "psr/clock": "^1.0" + }, + "require-dev": { + "infection/infection": "^0.29", + "lcobucci/clock": "^3.2", + "lcobucci/coding-standard": "^11.0", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.2", + "phpstan/phpstan": "^1.10.7", + "phpstan/phpstan-deprecation-rules": "^1.1.3", + "phpstan/phpstan-phpunit": "^1.3.10", + "phpstan/phpstan-strict-rules": "^1.5.0", + "phpunit/phpunit": "^11.1" + }, + "suggest": { + "lcobucci/clock": ">= 3.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Lcobucci\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Luís Cobucci", + "email": "lcobucci@gmail.com", + "role": "Developer" + } + ], + "description": "A simple library to work with JSON Web Token and JSON Web Signature", + "keywords": [ + "JWS", + "jwt" + ], + "support": { + "issues": "https://github.com/lcobucci/jwt/issues", + "source": "https://github.com/lcobucci/jwt/tree/5.4.2" + }, + "funding": [ + { + "url": "https://github.com/lcobucci", + "type": "github" + }, + { + "url": "https://www.patreon.com/lcobucci", + "type": "patreon" + } + ], + "time": "2024-11-07T12:54:35+00:00" + }, + { + "name": "league/commonmark", + "version": "2.6.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/commonmark.git", + "reference": "d990688c91cedfb69753ffc2512727ec646df2ad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/d990688c91cedfb69753ffc2512727ec646df2ad", + "reference": "d990688c91cedfb69753ffc2512727ec646df2ad", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "league/config": "^1.1.1", + "php": "^7.4 || ^8.0", + "psr/event-dispatcher": "^1.0", + "symfony/deprecation-contracts": "^2.1 || ^3.0", + "symfony/polyfill-php80": "^1.16" + }, + "require-dev": { + "cebe/markdown": "^1.0", + "commonmark/cmark": "0.31.1", + "commonmark/commonmark.js": "0.31.1", + "composer/package-versions-deprecated": "^1.8", + "embed/embed": "^4.4", + "erusev/parsedown": "^1.0", + "ext-json": "*", + "github/gfm": "0.29.0", + "michelf/php-markdown": "^1.4 || ^2.0", + "nyholm/psr7": "^1.5", + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0", + "scrutinizer/ocular": "^1.8.1", + "symfony/finder": "^5.3 | ^6.0 | ^7.0", + "symfony/process": "^5.4 | ^6.0 | ^7.0", + "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0", + "unleashedtech/php-coding-standard": "^3.1.1", + "vimeo/psalm": "^4.24.0 || ^5.0.0" + }, + "suggest": { + "symfony/yaml": "v2.3+ required if using the Front Matter extension" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "League\\CommonMark\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM)", + "homepage": "https://commonmark.thephpleague.com", + "keywords": [ + "commonmark", + "flavored", + "gfm", + "github", + "github-flavored", + "markdown", + "md", + "parser" + ], + "support": { + "docs": "https://commonmark.thephpleague.com/", + "forum": "https://github.com/thephpleague/commonmark/discussions", + "issues": "https://github.com/thephpleague/commonmark/issues", + "rss": "https://github.com/thephpleague/commonmark/releases.atom", + "source": "https://github.com/thephpleague/commonmark" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/commonmark", + "type": "tidelift" + } + ], + "time": "2024-12-29T14:10:59+00:00" + }, + { + "name": "league/config", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/config.git", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/config/zipball/754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "shasum": "" + }, + "require": { + "dflydev/dot-access-data": "^3.0.1", + "nette/schema": "^1.2", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.5", + "scrutinizer/ocular": "^1.8.1", + "unleashedtech/php-coding-standard": "^3.1", + "vimeo/psalm": "^4.7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Config\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Define configuration arrays with strict schemas and access values with dot notation", + "homepage": "https://config.thephpleague.com", + "keywords": [ + "array", + "config", + "configuration", + "dot", + "dot-access", + "nested", + "schema" + ], + "support": { + "docs": "https://config.thephpleague.com/", + "issues": "https://github.com/thephpleague/config/issues", + "rss": "https://github.com/thephpleague/config/releases.atom", + "source": "https://github.com/thephpleague/config" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + } + ], + "time": "2022-12-11T20:36:23+00:00" + }, + { + "name": "league/flysystem", + "version": "3.29.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem.git", + "reference": "edc1bb7c86fab0776c3287dbd19b5fa278347319" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/edc1bb7c86fab0776c3287dbd19b5fa278347319", + "reference": "edc1bb7c86fab0776c3287dbd19b5fa278347319", + "shasum": "" + }, + "require": { + "league/flysystem-local": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "conflict": { + "async-aws/core": "<1.19.0", + "async-aws/s3": "<1.14.0", + "aws/aws-sdk-php": "3.209.31 || 3.210.0", + "guzzlehttp/guzzle": "<7.0", + "guzzlehttp/ringphp": "<1.1.1", + "phpseclib/phpseclib": "3.0.15", + "symfony/http-client": "<5.2" + }, + "require-dev": { + "async-aws/s3": "^1.5 || ^2.0", + "async-aws/simple-s3": "^1.1 || ^2.0", + "aws/aws-sdk-php": "^3.295.10", + "composer/semver": "^3.0", + "ext-fileinfo": "*", + "ext-ftp": "*", + "ext-mongodb": "^1.3", + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.5", + "google/cloud-storage": "^1.23", + "guzzlehttp/psr7": "^2.6", + "microsoft/azure-storage-blob": "^1.1", + "mongodb/mongodb": "^1.2", + "phpseclib/phpseclib": "^3.0.36", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.5.11|^10.0", + "sabre/dav": "^4.6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "File storage abstraction for PHP", + "keywords": [ + "WebDAV", + "aws", + "cloud", + "file", + "files", + "filesystem", + "filesystems", + "ftp", + "s3", + "sftp", + "storage" + ], + "support": { + "issues": "https://github.com/thephpleague/flysystem/issues", + "source": "https://github.com/thephpleague/flysystem/tree/3.29.1" + }, + "time": "2024-10-08T08:58:34+00:00" + }, + { + "name": "league/flysystem-aws-s3-v3", + "version": "3.29.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git", + "reference": "c6ff6d4606e48249b63f269eba7fabdb584e76a9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/c6ff6d4606e48249b63f269eba7fabdb584e76a9", + "reference": "c6ff6d4606e48249b63f269eba7fabdb584e76a9", + "shasum": "" + }, + "require": { + "aws/aws-sdk-php": "^3.295.10", + "league/flysystem": "^3.10.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "conflict": { + "guzzlehttp/guzzle": "<7.0", + "guzzlehttp/ringphp": "<1.1.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\AwsS3V3\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "AWS S3 filesystem adapter for Flysystem.", + "keywords": [ + "Flysystem", + "aws", + "file", + "files", + "filesystem", + "s3", + "storage" + ], + "support": { + "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.29.0" + }, + "time": "2024-08-17T13:10:48+00:00" + }, + { + "name": "league/flysystem-local", + "version": "3.29.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-local.git", + "reference": "e0e8d52ce4b2ed154148453d321e97c8e931bd27" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/e0e8d52ce4b2ed154148453d321e97c8e931bd27", + "reference": "e0e8d52ce4b2ed154148453d321e97c8e931bd27", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "league/flysystem": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\Local\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Local filesystem adapter for Flysystem.", + "keywords": [ + "Flysystem", + "file", + "files", + "filesystem", + "local" + ], + "support": { + "source": "https://github.com/thephpleague/flysystem-local/tree/3.29.0" + }, + "time": "2024-08-09T21:24:39+00:00" + }, + { + "name": "league/mime-type-detection", + "version": "1.16.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/mime-type-detection.git", + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/2d6702ff215bf922936ccc1ad31007edc76451b9", + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "phpstan/phpstan": "^0.12.68", + "phpunit/phpunit": "^8.5.8 || ^9.3 || ^10.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\MimeTypeDetection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Mime-type detection for Flysystem", + "support": { + "issues": "https://github.com/thephpleague/mime-type-detection/issues", + "source": "https://github.com/thephpleague/mime-type-detection/tree/1.16.0" + }, + "funding": [ + { + "url": "https://github.com/frankdejonge", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/flysystem", + "type": "tidelift" + } + ], + "time": "2024-09-21T08:32:55+00:00" + }, + { + "name": "league/oauth1-client", + "version": "v1.11.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/oauth1-client.git", + "reference": "f9c94b088837eb1aae1ad7c4f23eb65cc6993055" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/oauth1-client/zipball/f9c94b088837eb1aae1ad7c4f23eb65cc6993055", + "reference": "f9c94b088837eb1aae1ad7c4f23eb65cc6993055", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-openssl": "*", + "guzzlehttp/guzzle": "^6.0|^7.0", + "guzzlehttp/psr7": "^1.7|^2.0", + "php": ">=7.1||>=8.0" + }, + "require-dev": { + "ext-simplexml": "*", + "friendsofphp/php-cs-fixer": "^2.17", + "mockery/mockery": "^1.3.3", + "phpstan/phpstan": "^0.12.42", + "phpunit/phpunit": "^7.5||9.5" + }, + "suggest": { + "ext-simplexml": "For decoding XML-based responses." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev", + "dev-develop": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "League\\OAuth1\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Corlett", + "email": "bencorlett@me.com", + "homepage": "http://www.webcomm.com.au", + "role": "Developer" + } + ], + "description": "OAuth 1.0 Client Library", + "keywords": [ + "Authentication", + "SSO", + "authorization", + "bitbucket", + "identity", + "idp", + "oauth", + "oauth1", + "single sign on", + "trello", + "tumblr", + "twitter" + ], + "support": { + "issues": "https://github.com/thephpleague/oauth1-client/issues", + "source": "https://github.com/thephpleague/oauth1-client/tree/v1.11.0" + }, + "time": "2024-12-10T19:59:05+00:00" + }, + { + "name": "league/uri", + "version": "7.5.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri.git", + "reference": "81fb5145d2644324614cc532b28efd0215bda430" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/81fb5145d2644324614cc532b28efd0215bda430", + "reference": "81fb5145d2644324614cc532b28efd0215bda430", + "shasum": "" + }, + "require": { + "league/uri-interfaces": "^7.5", + "php": "^8.1" + }, + "conflict": { + "league/uri-schemes": "^1.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-fileinfo": "to create Data URI from file contennts", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain", + "league/uri-components": "Needed to easily manipulate URI objects components", + "php-64bit": "to improve IPV4 host parsing", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "URI manipulation library", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "middleware", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "uri-template", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri/tree/7.5.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2024-12-08T08:40:02+00:00" + }, + { + "name": "league/uri-interfaces", + "version": "7.5.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri-interfaces.git", + "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", + "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^8.1", + "psr/http-factory": "^1", + "psr/http-message": "^1.1 || ^2.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "php-64bit": "to improve IPV4 host parsing", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "Common interfaces and classes for URI representation and interaction", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.5.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2024-12-08T08:18:47+00:00" + }, + { + "name": "lychee-org/lycheeverify", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/LycheeOrg/verify.git", + "reference": "d09d003452f70b05ddbe2affbef856b5f31ea705" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/LycheeOrg/verify/zipball/d09d003452f70b05ddbe2affbef856b5f31ea705", + "reference": "d09d003452f70b05ddbe2affbef856b5f31ea705", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^11.0", + "php": "^8.2", + "thecodingmachine/safe": "^2.5" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.3", + "larastan/larastan": "^2.9", + "lychee-org/phpstan-lychee": "^v1.0.1", + "nunomaduro/collision": "^8.3", + "orchestra/testbench": "^9.0.0||^8.22.0", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "LycheeVerify\\VerifyServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "LycheeVerify\\": "src/", + "LycheeVerify\\Database\\Factories\\": "database/factories/" + } + }, + "autoload-dev": { + "psr-4": { + "LycheeVerify\\Tests\\": "tests/", + "Workbench\\App\\": "workbench/app/" + } + }, + "scripts": { + "post-autoload-dump": [ + "@composer run prepare" + ], + "clear": [ + "@php vendor/bin/testbench package:purge-lycheeverify --ansi" + ], + "prepare": [ + "@php vendor/bin/testbench package:discover --ansi" + ], + "build": [ + "@composer run prepare", + "@php vendor/bin/testbench workbench:build --ansi" + ], + "start": [ + "Composer\\Config::disableProcessTimeout", + "@composer run build", + "@php vendor/bin/testbench serve" + ], + "analyse": [ + "vendor/bin/phpstan analyse" + ], + "test": [ + "vendor/bin/phpunit" + ] + }, + "license": [ + "MIT" + ], + "description": "Verification package for Lychee", + "homepage": "https://github.com/LycheeOrg/verify", + "support": { + "source": "https://github.com/LycheeOrg/verify/tree/1.0.2", + "issues": "https://github.com/LycheeOrg/verify/issues" + }, + "funding": [ + { + "type": "github", + "url": "https://github.com/LycheeOrg" + } + ], + "time": "2024-10-15T11:41:02+00:00" + }, + { + "name": "lychee-org/nestedset", + "version": "v9.0.1", + "source": { + "type": "git", + "url": "https://github.com/LycheeOrg/laravel-nestedset.git", + "reference": "dcd86fa1dfcc7343a4b0ceddbda32ba55b2b440a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/LycheeOrg/laravel-nestedset/zipball/dcd86fa1dfcc7343a4b0ceddbda32ba55b2b440a", + "reference": "dcd86fa1dfcc7343a4b0ceddbda32ba55b2b440a", + "shasum": "" + }, + "require": { + "illuminate/database": "^11.0", + "illuminate/events": "^11.0", + "illuminate/support": "^11.0", + "php": "^8.2" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.3", + "larastan/larastan": "^2.0", + "lychee-org/phpstan-lychee": "^1.0.4", + "orchestra/testbench": "^9.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Kalnoy\\Nestedset\\NestedSetServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "v8.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Kalnoy\\Nestedset\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alexander Kalnoy", + "email": "lazychaser@gmail.com" + } + ], + "description": "Nested Set Model for Laravel 10.0 and up (fork with patches for Lychee)", + "keywords": [ + "database", + "hierarchy", + "laravel", + "nested sets", + "nsm" + ], + "support": { + "source": "https://github.com/LycheeOrg/laravel-nestedset/tree/v9.0.1" + }, + "time": "2024-10-23T21:46:18+00:00" + }, + { + "name": "lychee-org/php-exif", + "version": "v1.0.4", + "source": { + "type": "git", + "url": "https://github.com/LycheeOrg/php-exif.git", + "reference": "b9535df9fa455ae0e6820b327a031a63121de890" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/LycheeOrg/php-exif/zipball/b9535df9fa455ae0e6820b327a031a63121de890", + "reference": "b9535df9fa455ae0e6820b327a031a63121de890", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "fylax/forceutf8": "^3.0.3", + "php": "^8.2", + "php-ffmpeg/php-ffmpeg": "^1.2", + "thecodingmachine/safe": "^2.5" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.51", + "infection/infection": "^0.27.10", + "lychee-org/phpstan-lychee": "^1.0.4", + "php-parallel-lint/php-parallel-lint": "^1.3.2", + "phpmd/phpmd": "^2.15", + "phpunit/phpunit": "^9.5.10", + "squizlabs/php_codesniffer": "^3.9", + "thecodingmachine/phpstan-safe-rule": "^1.2" + }, + "suggest": { + "FFmpeg": "Use FFmpeg/FFprobe as adapter", + "ext-exif": "Use exif PHP extension as adapter", + "ext-imagick": "Use ImageMagick as adapter", + "ext-mbstring": "Support UTC-16 characters in EXIF data with exif PHP extension", + "lib-exiftool": "Use perl lib exiftool as adapter" + }, + "type": "library", + "autoload": { + "psr-0": { + "PHPExif": "lib/" + } + }, + "scripts": { + "check-code-style": [ + "vendor/bin/phpcs --standard=PSR2 ./lib/" + ], + "run-tests": [ + "vendor/bin/phpunit -c phpunit.xml.dist", + "vendor/bin/phpunit --coverage-clover=coverage.xml" + ], + "validate-files": [ + "vendor/bin/parallel-lint --exclude vendor --exclude imagick ." + ], + "phpstan": [ + "vendor/bin/phpstan analyze" + ], + "fix-code-style": [ + "vendor/bin/phpcbf --standard=PSR2 ./lib/" + ] + }, + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tom Van Herreweghe", + "homepage": "http://theanalogguy.be", + "role": "Developer" + } + ], + "description": "Object-Oriented EXIF parsing", + "keywords": [ + "EXIF", + "FFmpeg", + "FFprobe", + "IPTC", + "ImageMagick", + "Imagick", + "exiftool", + "jpeg", + "tiff" + ], + "support": { + "source": "https://github.com/LycheeOrg/php-exif/tree/v1.0.4", + "issues": "https://github.com/LycheeOrg/php-exif/issues" + }, + "time": "2024-07-01T21:08:54+00:00" + }, + { + "name": "maennchen/zipstream-php", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/maennchen/ZipStream-PHP.git", + "reference": "6187e9cc4493da94b9b63eb2315821552015fca9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/6187e9cc4493da94b9b63eb2315821552015fca9", + "reference": "6187e9cc4493da94b9b63eb2315821552015fca9", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "ext-zlib": "*", + "php-64bit": "^8.1" + }, + "require-dev": { + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.16", + "guzzlehttp/guzzle": "^7.5", + "mikey179/vfsstream": "^1.6", + "php-coveralls/php-coveralls": "^2.5", + "phpunit/phpunit": "^10.0", + "vimeo/psalm": "^5.0" + }, + "suggest": { + "guzzlehttp/psr7": "^2.4", + "psr/http-message": "^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "ZipStream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paul Duncan", + "email": "pabs@pablotron.org" + }, + { + "name": "Jonatan Männchen", + "email": "jonatan@maennchen.ch" + }, + { + "name": "Jesse Donat", + "email": "donatj@gmail.com" + }, + { + "name": "András Kolesár", + "email": "kolesar@kolesar.hu" + } + ], + "description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.", + "keywords": [ + "stream", + "zip" + ], + "support": { + "issues": "https://github.com/maennchen/ZipStream-PHP/issues", + "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.1.1" + }, + "funding": [ + { + "url": "https://github.com/maennchen", + "type": "github" + } + ], + "time": "2024-10-10T12:33:01+00:00" + }, + { + "name": "mavinoo/laravel-batch", + "version": "v2.4.1", + "source": { + "type": "git", + "url": "https://github.com/mavinoo/laravelBatch.git", + "reference": "8f85ba2923b63bfad8b9181ef210094eba47b5ec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mavinoo/laravelBatch/zipball/8f85ba2923b63bfad8b9181ef210094eba47b5ec", + "reference": "8f85ba2923b63bfad8b9181ef210094eba47b5ec", + "shasum": "" + }, + "require": { + "ext-json": "*" + }, + "require-dev": { + "phpunit/phpunit": "^9.3@dev" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Batch": "Mavinoo\\Batch\\BatchFacade" + }, + "providers": [ + "Mavinoo\\Batch\\BatchServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/Common/Helpers.php" + ], + "psr-4": { + "Mavinoo\\Batch\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mohammad Ghanbari", + "email": "mavin.developer@gmail.com" + } + ], + "description": "Insert and update batch (bulk) in laravel", + "support": { + "issues": "https://github.com/mavinoo/laravelBatch/issues", + "source": "https://github.com/mavinoo/laravelBatch/tree/v2.4.1" + }, + "time": "2024-09-17T11:09:20+00:00" + }, + { + "name": "monolog/monolog", + "version": "3.8.1", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "aef6ee73a77a66e404dd6540934a9ef1b3c855b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/aef6ee73a77a66e404dd6540934a9ef1b3c855b4", + "reference": "aef6ee73a77a66e404dd6540934a9ef1b3c855b4", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2.0", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "php-console/php-console": "^3.1.8", + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^10.5.17 || ^11.0.7", + "predis/predis": "^1.1 || ^2", + "rollbar/rollbar": "^4.0", + "ruflin/elastica": "^7 || ^8", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/3.8.1" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2024-12-05T17:15:07+00:00" + }, + { + "name": "mtdowling/jmespath.php", + "version": "2.8.0", + "source": { + "type": "git", + "url": "https://github.com/jmespath/jmespath.php.git", + "reference": "a2a865e05d5f420b50cc2f85bb78d565db12a6bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/a2a865e05d5f420b50cc2f85bb78d565db12a6bc", + "reference": "a2a865e05d5f420b50cc2f85bb78d565db12a6bc", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/polyfill-mbstring": "^1.17" + }, + "require-dev": { + "composer/xdebug-handler": "^3.0.3", + "phpunit/phpunit": "^8.5.33" + }, + "bin": [ + "bin/jp.php" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.8-dev" + } + }, + "autoload": { + "files": [ + "src/JmesPath.php" + ], + "psr-4": { + "JmesPath\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Declaratively specify how to extract elements from a JSON document", + "keywords": [ + "json", + "jsonpath" + ], + "support": { + "issues": "https://github.com/jmespath/jmespath.php/issues", + "source": "https://github.com/jmespath/jmespath.php/tree/2.8.0" + }, + "time": "2024-09-04T18:46:31+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.12.1", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/123267b2c49fbf30d78a7b2d333f6be754b94845", + "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.12.1" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2024-11-08T17:47:46+00:00" + }, + { + "name": "nesbot/carbon", + "version": "3.8.4", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon.git", + "reference": "129700ed449b1f02d70272d2ac802357c8c30c58" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/129700ed449b1f02d70272d2ac802357c8c30c58", + "reference": "129700ed449b1f02d70272d2ac802357c8c30c58", + "shasum": "" + }, + "require": { + "carbonphp/carbon-doctrine-types": "<100.0", + "ext-json": "*", + "php": "^8.1", + "psr/clock": "^1.0", + "symfony/clock": "^6.3 || ^7.0", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation": "^4.4.18 || ^5.2.1|| ^6.0 || ^7.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "doctrine/dbal": "^3.6.3 || ^4.0", + "doctrine/orm": "^2.15.2 || ^3.0", + "friendsofphp/php-cs-fixer": "^3.57.2", + "kylekatarnls/multi-tester": "^2.5.3", + "ondrejmirtes/better-reflection": "^6.25.0.4", + "phpmd/phpmd": "^2.15.0", + "phpstan/extension-installer": "^1.3.1", + "phpstan/phpstan": "^1.11.2", + "phpunit/phpunit": "^10.5.20", + "squizlabs/php_codesniffer": "^3.9.0" + }, + "bin": [ + "bin/carbon" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Carbon\\Laravel\\ServiceProvider" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev", + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Carbon\\": "src/Carbon/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Nesbitt", + "email": "brian@nesbot.com", + "homepage": "https://markido.com" + }, + { + "name": "kylekatarnls", + "homepage": "https://github.com/kylekatarnls" + } + ], + "description": "An API extension for DateTime that supports 281 different languages.", + "homepage": "https://carbon.nesbot.com", + "keywords": [ + "date", + "datetime", + "time" + ], + "support": { + "docs": "https://carbon.nesbot.com/docs", + "issues": "https://github.com/briannesbitt/Carbon/issues", + "source": "https://github.com/briannesbitt/Carbon" + }, + "funding": [ + { + "url": "https://github.com/sponsors/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon#sponsor", + "type": "opencollective" + }, + { + "url": "https://tidelift.com/subscription/pkg/packagist-nesbot-carbon?utm_source=packagist-nesbot-carbon&utm_medium=referral&utm_campaign=readme", + "type": "tidelift" + } + ], + "time": "2024-12-27T09:25:35+00:00" + }, + { + "name": "nette/schema", + "version": "v1.3.2", + "source": { + "type": "git", + "url": "https://github.com/nette/schema.git", + "reference": "da801d52f0354f70a638673c4a0f04e16529431d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/schema/zipball/da801d52f0354f70a638673c4a0f04e16529431d", + "reference": "da801d52f0354f70a638673c4a0f04e16529431d", + "shasum": "" + }, + "require": { + "nette/utils": "^4.0", + "php": "8.1 - 8.4" + }, + "require-dev": { + "nette/tester": "^2.5.2", + "phpstan/phpstan-nette": "^1.0", + "tracy/tracy": "^2.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "📐 Nette Schema: validating data structures against a given Schema.", + "homepage": "https://nette.org", + "keywords": [ + "config", + "nette" + ], + "support": { + "issues": "https://github.com/nette/schema/issues", + "source": "https://github.com/nette/schema/tree/v1.3.2" + }, + "time": "2024-10-06T23:10:23+00:00" + }, + { + "name": "nette/utils", + "version": "v4.0.5", + "source": { + "type": "git", + "url": "https://github.com/nette/utils.git", + "reference": "736c567e257dbe0fcf6ce81b4d6dbe05c6899f96" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/utils/zipball/736c567e257dbe0fcf6ce81b4d6dbe05c6899f96", + "reference": "736c567e257dbe0fcf6ce81b4d6dbe05c6899f96", + "shasum": "" + }, + "require": { + "php": "8.0 - 8.4" + }, + "conflict": { + "nette/finder": "<3", + "nette/schema": "<1.2.2" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "dev-master", + "nette/tester": "^2.5", + "phpstan/phpstan": "^1.0", + "tracy/tracy": "^2.9" + }, + "suggest": { + "ext-gd": "to use Image", + "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()", + "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", + "ext-json": "to use Nette\\Utils\\Json", + "ext-mbstring": "to use Strings::lower() etc...", + "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", + "homepage": "https://nette.org", + "keywords": [ + "array", + "core", + "datetime", + "images", + "json", + "nette", + "paginator", + "password", + "slugify", + "string", + "unicode", + "utf-8", + "utility", + "validation" + ], + "support": { + "issues": "https://github.com/nette/utils/issues", + "source": "https://github.com/nette/utils/tree/v4.0.5" + }, + "time": "2024-08-07T15:39:19+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.4.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "447a020a1f875a434d62f2a401f53b82a396e494" + }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/dc960a912984efb74d0a90222870c72c87f10c91", - "reference": "dc960a912984efb74d0a90222870c72c87f10c91", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", + "reference": "447a020a1f875a434d62f2a401f53b82a396e494", "shasum": "" }, "require": { - "php": ">=5.4.0", - "psr/http-message": "~1.0", - "ralouphie/getallheaders": "^2.0.5 || ^3.0.0" - }, - "provide": { - "psr/http-message-implementation": "1.0" + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" }, "require-dev": { - "ext-zlib": "*", - "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.10" - }, - "suggest": { - "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" }, + "bin": [ + "bin/php-parse" + ], "type": "library", "extra": { "branch-alias": { - "dev-master": "1.7-dev" + "dev-master": "5.0-dev" } }, "autoload": { "psr-4": { - "GuzzleHttp\\Psr7\\": "src/" + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0" + }, + "time": "2024-12-30T11:07:19+00:00" + }, + { + "name": "nunomaduro/termwind", + "version": "v2.3.0", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/termwind.git", + "reference": "52915afe6a1044e8b9cee1bcff836fb63acf9cda" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/52915afe6a1044e8b9cee1bcff836fb63acf9cda", + "reference": "52915afe6a1044e8b9cee1bcff836fb63acf9cda", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^8.2", + "symfony/console": "^7.1.8" + }, + "require-dev": { + "illuminate/console": "^11.33.2", + "laravel/pint": "^1.18.2", + "mockery/mockery": "^1.6.12", + "pestphp/pest": "^2.36.0", + "phpstan/phpstan": "^1.12.11", + "phpstan/phpstan-strict-rules": "^1.6.1", + "symfony/var-dumper": "^7.1.8", + "thecodingmachine/phpstan-strict-rules": "^1.0.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Termwind\\Laravel\\TermwindServiceProvider" + ] }, + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { "files": [ - "src/functions_include.php" - ] + "src/Functions.php" + ], + "psr-4": { + "Termwind\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1782,120 +5454,349 @@ ], "authors": [ { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Its like Tailwind CSS, but for the console.", + "keywords": [ + "cli", + "console", + "css", + "package", + "php", + "style" + ], + "support": { + "issues": "https://github.com/nunomaduro/termwind/issues", + "source": "https://github.com/nunomaduro/termwind/tree/v2.3.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" }, { - "name": "Tobias Schultze", - "homepage": "https://github.com/Tobion" + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://github.com/xiCO2k", + "type": "github" } ], - "description": "PSR-7 message implementation that also provides common utility methods", + "time": "2024-11-21T10:39:51+00:00" + }, + { + "name": "opcodesio/log-viewer", + "version": "dev-lycheeOrg", + "source": { + "type": "git", + "url": "https://github.com/LycheeOrg/log-viewer.git", + "reference": "cd0de2bf122db91447abd83b7fbfb148228641eb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/LycheeOrg/log-viewer/zipball/cd0de2bf122db91447abd83b7fbfb148228641eb", + "reference": "cd0de2bf122db91447abd83b7fbfb148228641eb", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^8.0|^9.0|^10.0|^11.0", + "opcodesio/mail-parser": "^0.1.6", + "php": "^8.0" + }, + "conflict": { + "arcanedev/log-viewer": "^8.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^7.2", + "itsgoingd/clockwork": "^5.1", + "laravel/pint": "^1.0", + "nunomaduro/collision": "^7.0|^8.0", + "orchestra/testbench": "^7.6|^8.0|^9.0", + "pestphp/pest": "^2.0", + "pestphp/pest-plugin-laravel": "^2.0", + "spatie/test-time": "^1.3" + }, + "suggest": { + "guzzlehttp/guzzle": "Required for multi-host support. ^7.2" + }, + "default-branch": true, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Opcodes\\LogViewer\\LogViewerServiceProvider" + ], + "aliases": { + "LogViewer": "Opcodes\\LogViewer\\Facades\\LogViewer" + } + } + }, + "autoload": { + "psr-4": { + "Opcodes\\LogViewer\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Opcodes\\LogViewer\\Tests\\": "tests" + } + }, + "scripts": { + "analyse": [ + "echo \"Static analysis not configured yet.\" && exit 0" + ], + "test": [ + "vendor/bin/pest --order-by random" + ], + "test-coverage": [ + "echo \"Test coverage not configured yet.\" && exit 0" + ], + "format": [ + "vendor/bin/pint" + ] + }, + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Arunas Skirius", + "email": "arukomp@gmail.com", + "role": "Developer" + } + ], + "description": "Fast and easy-to-use log viewer for your Laravel application", + "homepage": "https://github.com/opcodesio/log-viewer", "keywords": [ - "http", - "message", - "psr-7", - "request", - "response", - "stream", - "uri", - "url" + "arukompas", + "better-log-viewer", + "laravel", + "log viewer", + "logs", + "opcodesio" ], "support": { - "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/1.8.2" + "source": "https://github.com/LycheeOrg/log-viewer/tree/lycheeOrg" }, - "time": "2021-04-26T09:17:50+00:00" + "funding": [ + { + "type": "github", + "url": "https://github.com/arukompas" + }, + { + "type": "custom", + "url": "https://www.buymeacoffee.com/arunas" + } + ], + "time": "2024-06-11T20:59:36+00:00" }, { - "name": "hamcrest/hamcrest-php", - "version": "v2.0.1", + "name": "opcodesio/mail-parser", + "version": "v0.1.6", "source": { "type": "git", - "url": "https://github.com/hamcrest/hamcrest-php.git", - "reference": "8c3d0a3f6af734494ad8f6fbbee0ba92422859f3" + "url": "https://github.com/opcodesio/mail-parser.git", + "reference": "639ef31cbd146a63416283e75afce152e13233ea" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opcodesio/mail-parser/zipball/639ef31cbd146a63416283e75afce152e13233ea", + "reference": "639ef31cbd146a63416283e75afce152e13233ea", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "pestphp/pest": "^2.16", + "symfony/var-dumper": "^6.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Opcodes\\MailParser\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Arunas Skirius", + "email": "arukomp@gmail.com", + "role": "Developer" + } + ], + "description": "Parse emails without the mailparse extension", + "keywords": [ + "arukompas", + "email", + "email parser", + "mail", + "opcodesio", + "php" + ], + "support": { + "issues": "https://github.com/opcodesio/mail-parser/issues", + "source": "https://github.com/opcodesio/mail-parser/tree/v0.1.6" + }, + "time": "2023-11-19T08:47:43+00:00" + }, + { + "name": "paragonie/constant_time_encoding", + "version": "v3.0.0", + "source": { + "type": "git", + "url": "https://github.com/paragonie/constant_time_encoding.git", + "reference": "df1e7fde177501eee2037dd159cf04f5f301a512" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/df1e7fde177501eee2037dd159cf04f5f301a512", + "reference": "df1e7fde177501eee2037dd159cf04f5f301a512", + "shasum": "" + }, + "require": { + "php": "^8" + }, + "require-dev": { + "phpunit/phpunit": "^9", + "vimeo/psalm": "^4|^5" + }, + "type": "library", + "autoload": { + "psr-4": { + "ParagonIE\\ConstantTime\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com", + "role": "Maintainer" + }, + { + "name": "Steve 'Sc00bz' Thomas", + "email": "steve@tobtu.com", + "homepage": "https://www.tobtu.com", + "role": "Original Developer" + } + ], + "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", + "keywords": [ + "base16", + "base32", + "base32_decode", + "base32_encode", + "base64", + "base64_decode", + "base64_encode", + "bin2hex", + "encoding", + "hex", + "hex2bin", + "rfc4648" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/constant_time_encoding/issues", + "source": "https://github.com/paragonie/constant_time_encoding" + }, + "time": "2024-05-08T12:36:18+00:00" + }, + { + "name": "paragonie/random_compat", + "version": "v9.99.100", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/8c3d0a3f6af734494ad8f6fbbee0ba92422859f3", - "reference": "8c3d0a3f6af734494ad8f6fbbee0ba92422859f3", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", "shasum": "" }, "require": { - "php": "^5.3|^7.0|^8.0" - }, - "replace": { - "cordoval/hamcrest-php": "*", - "davedevelopment/hamcrest-php": "*", - "kodova/hamcrest-php": "*" + "php": ">= 7" }, "require-dev": { - "phpunit/php-file-iterator": "^1.4 || ^2.0", - "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.1-dev" - } + "phpunit/phpunit": "4.*|5.*", + "vimeo/psalm": "^1" }, - "autoload": { - "classmap": [ - "hamcrest" - ] + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." }, + "type": "library", "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], - "description": "This is the PHP port of Hamcrest Matchers", + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", "keywords": [ - "test" + "csprng", + "polyfill", + "pseudorandom", + "random" ], "support": { - "issues": "https://github.com/hamcrest/hamcrest-php/issues", - "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.0.1" + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/random_compat/issues", + "source": "https://github.com/paragonie/random_compat" }, - "time": "2020-07-09T08:09:16+00:00" + "time": "2020-10-15T08:29:30+00:00" }, { - "name": "kalnoy/nestedset", - "version": "v5.0.3", + "name": "php-ffmpeg/php-ffmpeg", + "version": "v1.3.1", "source": { "type": "git", - "url": "https://github.com/lazychaser/laravel-nestedset.git", - "reference": "789a70bce94a7c3bd206fb05fa4b747cf27acbe2" + "url": "https://github.com/PHP-FFMpeg/PHP-FFMpeg.git", + "reference": "0fbbc4c6a6336155679adc800616001ae3328c7a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/lazychaser/laravel-nestedset/zipball/789a70bce94a7c3bd206fb05fa4b747cf27acbe2", - "reference": "789a70bce94a7c3bd206fb05fa4b747cf27acbe2", + "url": "https://api.github.com/repos/PHP-FFMpeg/PHP-FFMpeg/zipball/0fbbc4c6a6336155679adc800616001ae3328c7a", + "reference": "0fbbc4c6a6336155679adc800616001ae3328c7a", "shasum": "" }, "require": { - "illuminate/database": "~5.7.0|~5.8.0|^6.0|^7.0|^8.0", - "illuminate/events": "~5.7.0|~5.8.0|^6.0|^7.0|^8.0", - "illuminate/support": "~5.7.0|~5.8.0|^6.0|^7.0|^8.0", - "php": ">=7.1.3" + "evenement/evenement": "^3.0", + "php": "^8.0 || ^8.1 || ^8.2 || ^8.3 || ^8.4", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "spatie/temporary-directory": "^2.0", + "symfony/cache": "^5.4 || ^6.0 || ^7.0", + "symfony/process": "^5.4 || ^6.0 || ^7.0" }, "require-dev": { - "phpunit/phpunit": "7.*" + "mockery/mockery": "^1.5", + "phpunit/phpunit": "^9.5.10 || ^10.0" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "v5.0.x-dev" - }, - "laravel": { - "providers": [ - "Kalnoy\\Nestedset\\NestedSetServiceProvider" - ] - } + "suggest": { + "php-ffmpeg/extras": "A compilation of common audio & video drivers for PHP-FFMpeg" }, + "type": "library", "autoload": { "psr-4": { - "Kalnoy\\Nestedset\\": "src/" + "FFMpeg\\": "src/FFMpeg", + "Alchemy\\BinaryDriver\\": "src/Alchemy/BinaryDriver" } }, "notification-url": "https://packagist.org/downloads/", @@ -1904,169 +5805,103 @@ ], "authors": [ { - "name": "Alexander Kalnoy", - "email": "lazychaser@gmail.com" + "name": "Romain Neutron", + "email": "imprec@gmail.com", + "homepage": "http://www.lickmychip.com/" + }, + { + "name": "Phraseanet Team", + "email": "info@alchemy.fr", + "homepage": "http://www.phraseanet.com/" + }, + { + "name": "Patrik Karisch", + "email": "patrik@karisch.guru", + "homepage": "http://www.karisch.guru" + }, + { + "name": "Romain Biard", + "email": "romain.biard@gmail.com", + "homepage": "https://www.strime.io/" + }, + { + "name": "Jens Hausdorf", + "email": "hello@jens-hausdorf.de", + "homepage": "https://jens-hausdorf.de" + }, + { + "name": "Pascal Baljet", + "email": "pascal@protone.media", + "homepage": "https://protone.media" } ], - "description": "Nested Set Model for Laravel 5.7 and up", + "description": "FFMpeg PHP, an Object Oriented library to communicate with AVconv / ffmpeg", "keywords": [ - "database", - "hierarchy", - "laravel", - "nested sets", - "nsm" + "audio", + "audio processing", + "avconv", + "avprobe", + "ffmpeg", + "ffprobe", + "video", + "video processing" ], "support": { - "issues": "https://github.com/lazychaser/laravel-nestedset/issues", - "source": "https://github.com/lazychaser/laravel-nestedset/tree/v5.0.3" + "issues": "https://github.com/PHP-FFMpeg/PHP-FFMpeg/issues", + "source": "https://github.com/PHP-FFMpeg/PHP-FFMpeg/tree/v1.3.1" }, - "time": "2020-12-07T05:59:45+00:00" + "time": "2025-01-10T20:23:57+00:00" }, { - "name": "laravel/framework", - "version": "v8.42.1", + "name": "php-http/discovery", + "version": "1.20.0", "source": { "type": "git", - "url": "https://github.com/laravel/framework.git", - "reference": "41ec4897a70eb8729cf0ff34a8354413c54e42a6" + "url": "https://github.com/php-http/discovery.git", + "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/41ec4897a70eb8729cf0ff34a8354413c54e42a6", - "reference": "41ec4897a70eb8729cf0ff34a8354413c54e42a6", + "url": "https://api.github.com/repos/php-http/discovery/zipball/82fe4c73ef3363caed49ff8dd1539ba06044910d", + "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d", "shasum": "" }, "require": { - "doctrine/inflector": "^1.4|^2.0", - "dragonmantank/cron-expression": "^3.0.2", - "egulias/email-validator": "^2.1.10", - "ext-json": "*", - "ext-mbstring": "*", - "ext-openssl": "*", - "league/commonmark": "^1.3", - "league/flysystem": "^1.1", - "monolog/monolog": "^2.0", - "nesbot/carbon": "^2.31", - "opis/closure": "^3.6", - "php": "^7.3|^8.0", - "psr/container": "^1.0", - "psr/simple-cache": "^1.0", - "ramsey/uuid": "^4.0", - "swiftmailer/swiftmailer": "^6.0", - "symfony/console": "^5.1.4", - "symfony/error-handler": "^5.1.4", - "symfony/finder": "^5.1.4", - "symfony/http-foundation": "^5.1.4", - "symfony/http-kernel": "^5.1.4", - "symfony/mime": "^5.1.4", - "symfony/process": "^5.1.4", - "symfony/routing": "^5.1.4", - "symfony/var-dumper": "^5.1.4", - "tijsverkoyen/css-to-inline-styles": "^2.2.2", - "vlucas/phpdotenv": "^5.2", - "voku/portable-ascii": "^1.4.8" + "composer-plugin-api": "^1.0|^2.0", + "php": "^7.1 || ^8.0" }, "conflict": { - "tightenco/collect": "<5.5.33" + "nyholm/psr7": "<1.0", + "zendframework/zend-diactoros": "*" }, "provide": { - "psr/container-implementation": "1.0" - }, - "replace": { - "illuminate/auth": "self.version", - "illuminate/broadcasting": "self.version", - "illuminate/bus": "self.version", - "illuminate/cache": "self.version", - "illuminate/collections": "self.version", - "illuminate/config": "self.version", - "illuminate/console": "self.version", - "illuminate/container": "self.version", - "illuminate/contracts": "self.version", - "illuminate/cookie": "self.version", - "illuminate/database": "self.version", - "illuminate/encryption": "self.version", - "illuminate/events": "self.version", - "illuminate/filesystem": "self.version", - "illuminate/hashing": "self.version", - "illuminate/http": "self.version", - "illuminate/log": "self.version", - "illuminate/macroable": "self.version", - "illuminate/mail": "self.version", - "illuminate/notifications": "self.version", - "illuminate/pagination": "self.version", - "illuminate/pipeline": "self.version", - "illuminate/queue": "self.version", - "illuminate/redis": "self.version", - "illuminate/routing": "self.version", - "illuminate/session": "self.version", - "illuminate/support": "self.version", - "illuminate/testing": "self.version", - "illuminate/translation": "self.version", - "illuminate/validation": "self.version", - "illuminate/view": "self.version" + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "*", + "psr/http-factory-implementation": "*", + "psr/http-message-implementation": "*" }, "require-dev": { - "aws/aws-sdk-php": "^3.155", - "doctrine/dbal": "^2.6|^3.0", - "filp/whoops": "^2.8", - "guzzlehttp/guzzle": "^6.5.5|^7.0.1", - "league/flysystem-cached-adapter": "^1.0", - "mockery/mockery": "^1.4.2", - "orchestra/testbench-core": "^6.8", - "pda/pheanstalk": "^4.0", - "phpunit/phpunit": "^8.5.8|^9.3.3", - "predis/predis": "^1.1.1", - "symfony/cache": "^5.1.4" - }, - "suggest": { - "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage and SES mail driver (^3.155).", - "brianium/paratest": "Required to run tests in parallel (^6.0).", - "doctrine/dbal": "Required to rename columns and drop SQLite columns (^2.6|^3.0).", - "ext-ftp": "Required to use the Flysystem FTP driver.", - "ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image().", - "ext-memcached": "Required to use the memcache cache driver.", - "ext-pcntl": "Required to use all features of the queue worker.", - "ext-posix": "Required to use all features of the queue worker.", - "ext-redis": "Required to use the Redis cache and queue drivers (^4.0|^5.0).", - "fakerphp/faker": "Required to use the eloquent factory builder (^1.9.1).", - "filp/whoops": "Required for friendly error pages in development (^2.8).", - "guzzlehttp/guzzle": "Required to use the HTTP Client, Mailgun mail driver and the ping methods on schedules (^6.5.5|^7.0.1).", - "laravel/tinker": "Required to use the tinker console command (^2.0).", - "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^1.0).", - "league/flysystem-cached-adapter": "Required to use the Flysystem cache (^1.0).", - "league/flysystem-sftp": "Required to use the Flysystem SFTP driver (^1.0).", - "mockery/mockery": "Required to use mocking (^1.4.2).", - "nyholm/psr7": "Required to use PSR-7 bridging features (^1.2).", - "pda/pheanstalk": "Required to use the beanstalk queue driver (^4.0).", - "phpunit/phpunit": "Required to use assertions and run tests (^8.5.8|^9.3.3).", - "predis/predis": "Required to use the predis connector (^1.1.2).", - "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", - "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^4.0|^5.0|^6.0).", - "symfony/cache": "Required to PSR-6 cache bridge (^5.1.4).", - "symfony/filesystem": "Required to enable support for relative symbolic links (^5.1.4).", - "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^2.0).", - "wildbit/swiftmailer-postmark": "Required to use Postmark mail driver (^3.0)." + "composer/composer": "^1.0.2|^2.0", + "graham-campbell/phpspec-skip-example-extension": "^5.0", + "php-http/httplug": "^1.0 || ^2.0", + "php-http/message-factory": "^1.0", + "phpspec/phpspec": "^5.1 || ^6.1 || ^7.3", + "sebastian/comparator": "^3.0.5 || ^4.0.8", + "symfony/phpunit-bridge": "^6.4.4 || ^7.0.1" }, - "type": "library", + "type": "composer-plugin", "extra": { - "branch-alias": { - "dev-master": "8.x-dev" - } + "class": "Http\\Discovery\\Composer\\Plugin", + "plugin-optional": true }, "autoload": { - "files": [ - "src/Illuminate/Collections/helpers.php", - "src/Illuminate/Events/functions.php", - "src/Illuminate/Foundation/helpers.php", - "src/Illuminate/Support/helpers.php" - ], "psr-4": { - "Illuminate\\": "src/Illuminate/", - "Illuminate\\Support\\": [ - "src/Illuminate/Macroable/", - "src/Illuminate/Collections/" - ] - } + "Http\\Discovery\\": "src/" + }, + "exclude-from-classmap": [ + "src/Composer/Plugin.php" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -2074,173 +5909,115 @@ ], "authors": [ { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" } ], - "description": "The Laravel Framework.", - "homepage": "https://laravel.com", + "description": "Finds and installs PSR-7, PSR-17, PSR-18 and HTTPlug implementations", + "homepage": "http://php-http.org", "keywords": [ - "framework", - "laravel" + "adapter", + "client", + "discovery", + "factory", + "http", + "message", + "psr17", + "psr7" ], "support": { - "issues": "https://github.com/laravel/framework/issues", - "source": "https://github.com/laravel/framework" + "issues": "https://github.com/php-http/discovery/issues", + "source": "https://github.com/php-http/discovery/tree/1.20.0" }, - "time": "2021-05-19T13:03:18+00:00" + "time": "2024-10-02T11:20:13+00:00" }, { - "name": "league/commonmark", - "version": "1.6.2", + "name": "php-http/guzzle7-adapter", + "version": "1.1.0", "source": { "type": "git", - "url": "https://github.com/thephpleague/commonmark.git", - "reference": "7d70d2f19c84bcc16275ea47edabee24747352eb" + "url": "https://github.com/php-http/guzzle7-adapter.git", + "reference": "03a415fde709c2f25539790fecf4d9a31bc3d0eb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/7d70d2f19c84bcc16275ea47edabee24747352eb", - "reference": "7d70d2f19c84bcc16275ea47edabee24747352eb", + "url": "https://api.github.com/repos/php-http/guzzle7-adapter/zipball/03a415fde709c2f25539790fecf4d9a31bc3d0eb", + "reference": "03a415fde709c2f25539790fecf4d9a31bc3d0eb", "shasum": "" }, "require": { - "ext-mbstring": "*", - "php": "^7.1 || ^8.0" + "guzzlehttp/guzzle": "^7.0", + "php": "^7.3 | ^8.0", + "php-http/httplug": "^2.0", + "psr/http-client": "^1.0" }, - "conflict": { - "scrutinizer/ocular": "1.7.*" + "provide": { + "php-http/async-client-implementation": "1.0", + "php-http/client-implementation": "1.0", + "psr/http-client-implementation": "1.0" }, "require-dev": { - "cebe/markdown": "~1.0", - "commonmark/commonmark.js": "0.29.2", - "erusev/parsedown": "~1.0", - "ext-json": "*", - "github/gfm": "0.29.0", - "michelf/php-markdown": "~1.4", - "mikehaertl/php-shellcommand": "^1.4", - "phpstan/phpstan": "^0.12", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.2", - "scrutinizer/ocular": "^1.5", - "symfony/finder": "^4.2" + "php-http/client-integration-tests": "^3.0", + "php-http/message-factory": "^1.1", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^8.0|^9.3" }, - "bin": [ - "bin/commonmark" - ], "type": "library", "autoload": { "psr-4": { - "League\\CommonMark\\": "src" + "Http\\Adapter\\Guzzle7\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Colin O'Dell", - "email": "colinodell@gmail.com", - "homepage": "https://www.colinodell.com", - "role": "Lead Developer" - } - ], - "description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and Github-Flavored Markdown (GFM)", - "homepage": "https://commonmark.thephpleague.com", - "keywords": [ - "commonmark", - "flavored", - "gfm", - "github", - "github-flavored", - "markdown", - "md", - "parser" - ], - "support": { - "docs": "https://commonmark.thephpleague.com/", - "issues": "https://github.com/thephpleague/commonmark/issues", - "rss": "https://github.com/thephpleague/commonmark/releases.atom", - "source": "https://github.com/thephpleague/commonmark" - }, - "funding": [ - { - "url": "https://enjoy.gitstore.app/repositories/thephpleague/commonmark", - "type": "custom" - }, - { - "url": "https://www.colinodell.com/sponsor", - "type": "custom" - }, - { - "url": "https://www.paypal.me/colinpodell/10.00", - "type": "custom" - }, - { - "url": "https://github.com/colinodell", - "type": "github" - }, - { - "url": "https://www.patreon.com/colinodell", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/league/commonmark", - "type": "tidelift" + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" } ], - "time": "2021-05-12T11:39:41+00:00" + "description": "Guzzle 7 HTTP Adapter", + "homepage": "http://httplug.io", + "keywords": [ + "Guzzle", + "http" + ], + "support": { + "issues": "https://github.com/php-http/guzzle7-adapter/issues", + "source": "https://github.com/php-http/guzzle7-adapter/tree/1.1.0" + }, + "time": "2024-11-26T11:14:36+00:00" }, { - "name": "league/flysystem", - "version": "1.1.4", + "name": "php-http/httplug", + "version": "2.4.1", "source": { "type": "git", - "url": "https://github.com/thephpleague/flysystem.git", - "reference": "f3ad69181b8afed2c9edf7be5a2918144ff4ea32" + "url": "https://github.com/php-http/httplug.git", + "reference": "5cad731844891a4c282f3f3e1b582c46839d22f4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/f3ad69181b8afed2c9edf7be5a2918144ff4ea32", - "reference": "f3ad69181b8afed2c9edf7be5a2918144ff4ea32", + "url": "https://api.github.com/repos/php-http/httplug/zipball/5cad731844891a4c282f3f3e1b582c46839d22f4", + "reference": "5cad731844891a4c282f3f3e1b582c46839d22f4", "shasum": "" }, "require": { - "ext-fileinfo": "*", - "league/mime-type-detection": "^1.3", - "php": "^7.2.5 || ^8.0" - }, - "conflict": { - "league/flysystem-sftp": "<1.0.6" + "php": "^7.1 || ^8.0", + "php-http/promise": "^1.1", + "psr/http-client": "^1.0", + "psr/http-message": "^1.0 || ^2.0" }, "require-dev": { - "phpspec/prophecy": "^1.11.1", - "phpunit/phpunit": "^8.5.8" - }, - "suggest": { - "ext-ftp": "Allows you to use FTP server storage", - "ext-openssl": "Allows you to use FTPS server storage", - "league/flysystem-aws-s3-v2": "Allows you to use S3 storage with AWS SDK v2", - "league/flysystem-aws-s3-v3": "Allows you to use S3 storage with AWS SDK v3", - "league/flysystem-azure": "Allows you to use Windows Azure Blob storage", - "league/flysystem-cached-adapter": "Flysystem adapter decorator for metadata caching", - "league/flysystem-eventable-filesystem": "Allows you to use EventableFilesystem", - "league/flysystem-rackspace": "Allows you to use Rackspace Cloud Files", - "league/flysystem-sftp": "Allows you to use SFTP server storage via phpseclib", - "league/flysystem-webdav": "Allows you to use WebDAV storage", - "league/flysystem-ziparchive": "Allows you to use ZipArchive adapter", - "spatie/flysystem-dropbox": "Allows you to use Dropbox storage", - "srmklive/flysystem-dropbox-v2": "Allows you to use Dropbox storage for PHP 5 applications" + "friends-of-phpspec/phpspec-code-coverage": "^4.1 || ^5.0 || ^6.0", + "phpspec/phpspec": "^5.1 || ^6.0 || ^7.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.1-dev" - } - }, "autoload": { "psr-4": { - "League\\Flysystem\\": "src/" + "Http\\Client\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -2249,69 +6026,71 @@ ], "authors": [ { - "name": "Frank de Jonge", - "email": "info@frenky.net" + "name": "Eric GELOEN", + "email": "geloen.eric@gmail.com" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" } ], - "description": "Filesystem abstraction: Many filesystems, one API.", + "description": "HTTPlug, the HTTP client abstraction for PHP", + "homepage": "http://httplug.io", "keywords": [ - "Cloud Files", - "WebDAV", - "abstraction", - "aws", - "cloud", - "copy.com", - "dropbox", - "file systems", - "files", - "filesystem", - "filesystems", - "ftp", - "rackspace", - "remote", - "s3", - "sftp", - "storage" + "client", + "http" ], "support": { - "issues": "https://github.com/thephpleague/flysystem/issues", - "source": "https://github.com/thephpleague/flysystem/tree/1.1.4" + "issues": "https://github.com/php-http/httplug/issues", + "source": "https://github.com/php-http/httplug/tree/2.4.1" }, - "funding": [ - { - "url": "https://offset.earth/frankdejonge", - "type": "other" - } - ], - "time": "2021-06-23T21:56:05+00:00" + "time": "2024-09-23T11:39:58+00:00" }, { - "name": "league/mime-type-detection", - "version": "1.7.0", + "name": "php-http/message", + "version": "1.16.2", "source": { "type": "git", - "url": "https://github.com/thephpleague/mime-type-detection.git", - "reference": "3b9dff8aaf7323590c1d2e443db701eb1f9aa0d3" + "url": "https://github.com/php-http/message.git", + "reference": "06dd5e8562f84e641bf929bfe699ee0f5ce8080a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/3b9dff8aaf7323590c1d2e443db701eb1f9aa0d3", - "reference": "3b9dff8aaf7323590c1d2e443db701eb1f9aa0d3", + "url": "https://api.github.com/repos/php-http/message/zipball/06dd5e8562f84e641bf929bfe699ee0f5ce8080a", + "reference": "06dd5e8562f84e641bf929bfe699ee0f5ce8080a", "shasum": "" }, "require": { - "ext-fileinfo": "*", - "php": "^7.2 || ^8.0" + "clue/stream-filter": "^1.5", + "php": "^7.2 || ^8.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "provide": { + "php-http/message-factory-implementation": "1.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^2.18", - "phpstan/phpstan": "^0.12.68", - "phpunit/phpunit": "^8.5.8 || ^9.3" + "ergebnis/composer-normalize": "^2.6", + "ext-zlib": "*", + "guzzlehttp/psr7": "^1.0 || ^2.0", + "laminas/laminas-diactoros": "^2.0 || ^3.0", + "php-http/message-factory": "^1.0.2", + "phpspec/phpspec": "^5.1 || ^6.3 || ^7.1", + "slim/slim": "^3.0" + }, + "suggest": { + "ext-zlib": "Used with compressor/decompressor streams", + "guzzlehttp/psr7": "Used with Guzzle PSR-7 Factories", + "laminas/laminas-diactoros": "Used with Diactoros Factories", + "slim/slim": "Used with Slim Framework PSR-7 implementation" }, "type": "library", "autoload": { + "files": [ + "src/filters.php" + ], "psr-4": { - "League\\MimeTypeDetection\\": "src" + "Http\\Message\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -2320,73 +6099,48 @@ ], "authors": [ { - "name": "Frank de Jonge", - "email": "info@frankdejonge.nl" + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" } ], - "description": "Mime-type detection for Flysystem", + "description": "HTTP Message related tools", + "homepage": "http://php-http.org", + "keywords": [ + "http", + "message", + "psr-7" + ], "support": { - "issues": "https://github.com/thephpleague/mime-type-detection/issues", - "source": "https://github.com/thephpleague/mime-type-detection/tree/1.7.0" + "issues": "https://github.com/php-http/message/issues", + "source": "https://github.com/php-http/message/tree/1.16.2" }, - "funding": [ - { - "url": "https://github.com/frankdejonge", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/league/flysystem", - "type": "tidelift" - } - ], - "time": "2021-01-18T20:58:21+00:00" + "time": "2024-10-02T11:34:13+00:00" }, { - "name": "league/uri", - "version": "6.4.0", + "name": "php-http/promise", + "version": "1.3.1", "source": { "type": "git", - "url": "https://github.com/thephpleague/uri.git", - "reference": "09da64118eaf4c5d52f9923a1e6a5be1da52fd9a" + "url": "https://github.com/php-http/promise.git", + "reference": "fc85b1fba37c169a69a07ef0d5a8075770cc1f83" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri/zipball/09da64118eaf4c5d52f9923a1e6a5be1da52fd9a", - "reference": "09da64118eaf4c5d52f9923a1e6a5be1da52fd9a", + "url": "https://api.github.com/repos/php-http/promise/zipball/fc85b1fba37c169a69a07ef0d5a8075770cc1f83", + "reference": "fc85b1fba37c169a69a07ef0d5a8075770cc1f83", "shasum": "" }, "require": { - "ext-json": "*", - "league/uri-interfaces": "^2.1", - "php": ">=7.2", - "psr/http-message": "^1.0" - }, - "conflict": { - "league/uri-schemes": "^1.0" + "php": "^7.1 || ^8.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^2.16", - "phpstan/phpstan": "^0.12", - "phpstan/phpstan-phpunit": "^0.12", - "phpstan/phpstan-strict-rules": "^0.12", - "phpunit/phpunit": "^8.0 || ^9.0", - "psr/http-factory": "^1.0" - }, - "suggest": { - "ext-fileinfo": "Needed to create Data URI from a filepath", - "ext-intl": "Needed to improve host validation", - "league/uri-components": "Needed to easily manipulate URI objects", - "psr/http-factory": "Needed to use the URI factory" + "friends-of-phpspec/phpspec-code-coverage": "^4.3.2 || ^6.3", + "phpspec/phpspec": "^5.1.2 || ^6.2 || ^7.4" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "6.x-dev" - } - }, "autoload": { "psr-4": { - "League\\Uri\\": "src" + "Http\\Promise\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -2395,161 +6149,118 @@ ], "authors": [ { - "name": "Ignace Nyamagana Butera", - "email": "nyamsprod@gmail.com", - "homepage": "https://nyamsprod.com" + "name": "Joel Wurtz", + "email": "joel.wurtz@gmail.com" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" } ], - "description": "URI manipulation library", - "homepage": "http://uri.thephpleague.com", + "description": "Promise used for asynchronous HTTP requests", + "homepage": "http://httplug.io", "keywords": [ - "data-uri", - "file-uri", - "ftp", - "hostname", - "http", - "https", - "middleware", - "parse_str", - "parse_url", - "psr-7", - "query-string", - "querystring", - "rfc3986", - "rfc3987", - "rfc6570", - "uri", - "uri-template", - "url", - "ws" + "promise" ], "support": { - "docs": "https://uri.thephpleague.com", - "forum": "https://thephpleague.slack.com", - "issues": "https://github.com/thephpleague/uri/issues", - "source": "https://github.com/thephpleague/uri/tree/6.4.0" + "issues": "https://github.com/php-http/promise/issues", + "source": "https://github.com/php-http/promise/tree/1.3.1" }, - "funding": [ - { - "url": "https://github.com/sponsors/nyamsprod", - "type": "github" - } - ], - "time": "2020-11-22T14:29:11+00:00" + "time": "2024-03-15T13:55:21+00:00" }, { - "name": "league/uri-interfaces", - "version": "2.2.0", + "name": "phpdocumentor/reflection", + "version": "6.1.0", "source": { "type": "git", - "url": "https://github.com/thephpleague/uri-interfaces.git", - "reference": "667f150e589d65d79c89ffe662e426704f84224f" + "url": "https://github.com/phpDocumentor/Reflection.git", + "reference": "bb4dea805a645553d6d989b23dad9f8041f39502" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/667f150e589d65d79c89ffe662e426704f84224f", - "reference": "667f150e589d65d79c89ffe662e426704f84224f", + "url": "https://api.github.com/repos/phpDocumentor/Reflection/zipball/bb4dea805a645553d6d989b23dad9f8041f39502", + "reference": "bb4dea805a645553d6d989b23dad9f8041f39502", "shasum": "" }, "require": { - "ext-json": "*", - "php": "^7.1 || ^8.0" + "nikic/php-parser": "~4.18 || ^5.0", + "php": "8.1.*|8.2.*|8.3.*|8.4.*", + "phpdocumentor/reflection-common": "^2.1", + "phpdocumentor/reflection-docblock": "^5", + "phpdocumentor/type-resolver": "^1.2", + "symfony/polyfill-php80": "^1.28", + "webmozart/assert": "^1.7" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^2.0", - "phpstan/phpstan": "^0.12", - "phpstan/phpstan-phpunit": "^0.12", - "phpstan/phpstan-strict-rules": "^0.12" + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "doctrine/coding-standard": "^12.0", + "mikey179/vfsstream": "~1.2", + "mockery/mockery": "~1.6.0", + "phpspec/prophecy-phpunit": "^2.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-webmozart-assert": "^1.2", + "phpunit/phpunit": "^10.0", + "psalm/phar": "^5.24", + "rector/rector": "^1.0.0", + "squizlabs/php_codesniffer": "^3.8" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.x-dev" + "dev-5.x": "5.3.x-dev", + "dev-6.x": "6.0.x-dev" } }, "autoload": { "psr-4": { - "League\\Uri\\": "src/" + "phpDocumentor\\": "src/phpDocumentor" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ - { - "name": "Ignace Nyamagana Butera", - "email": "nyamsprod@gmail.com", - "homepage": "https://nyamsprod.com" - } - ], - "description": "Common interface for URI representation", - "homepage": "http://github.com/thephpleague/uri-interfaces", + "description": "Reflection library to do Static Analysis for PHP Projects", + "homepage": "http://www.phpdoc.org", "keywords": [ - "rfc3986", - "rfc3987", - "uri", - "url" + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" ], "support": { - "issues": "https://github.com/thephpleague/uri-interfaces/issues", - "source": "https://github.com/thephpleague/uri-interfaces/tree/2.2.0" + "issues": "https://github.com/phpDocumentor/Reflection/issues", + "source": "https://github.com/phpDocumentor/Reflection/tree/6.1.0" }, - "funding": [ - { - "url": "https://github.com/sponsors/nyamsprod", - "type": "github" - } - ], - "time": "2020-10-31T13:45:51+00:00" + "time": "2024-11-22T15:11:54+00:00" }, { - "name": "livewire/livewire", - "version": "v2.4.4", + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", "source": { "type": "git", - "url": "https://github.com/livewire/livewire.git", - "reference": "33101c83b75728651b9e668a4559f97def7c9138" + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/livewire/livewire/zipball/33101c83b75728651b9e668a4559f97def7c9138", - "reference": "33101c83b75728651b9e668a4559f97def7c9138", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", "shasum": "" }, "require": { - "illuminate/database": "^7.0|^8.0", - "illuminate/support": "^7.0|^8.0", - "illuminate/validation": "^7.0|^8.0", - "php": "^7.2.5|^8.0", - "symfony/http-kernel": "^5.0" - }, - "require-dev": { - "calebporzio/sushi": "^2.1", - "laravel/framework": "^7.0|^8.0", - "mockery/mockery": "^1.3.1", - "orchestra/testbench": "^5.0|^6.0", - "orchestra/testbench-dusk": "^5.2|^6.0", - "phpunit/phpunit": "^8.4|^9.0", - "psy/psysh": "@stable" + "php": "^7.2 || ^8.0" }, "type": "library", "extra": { - "laravel": { - "providers": [ - "Livewire\\LivewireServiceProvider" - ], - "aliases": { - "Livewire": "Livewire\\Livewire" - } + "branch-alias": { + "dev-2.x": "2.x-dev" } }, "autoload": { - "files": [ - "src/helpers.php" - ], "psr-4": { - "Livewire\\": "src/" + "phpDocumentor\\Reflection\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -2558,61 +6269,66 @@ ], "authors": [ { - "name": "Caleb Porzio", - "email": "calebporzio@gmail.com" + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" } ], - "description": "A front-end framework for Laravel.", + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], "support": { - "issues": "https://github.com/livewire/livewire/issues", - "source": "https://github.com/livewire/livewire/tree/v2.4.4" + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" }, - "funding": [ - { - "url": "https://github.com/calebporzio", - "type": "github" - } - ], - "time": "2021-04-28T15:31:15+00:00" + "time": "2020-06-27T09:03:43+00:00" }, { - "name": "lychee-org/php-exif", - "version": "dev-master", + "name": "phpdocumentor/reflection-docblock", + "version": "5.6.1", "source": { "type": "git", - "url": "https://github.com/LycheeOrg/php-exif.git", - "reference": "4fd2b9e325982f476fdd8bd356cb51d7fa312021" + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "e5e784149a09bd69d9a5e3b01c5cbd2e2bd653d8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/LycheeOrg/php-exif/zipball/4fd2b9e325982f476fdd8bd356cb51d7fa312021", - "reference": "4fd2b9e325982f476fdd8bd356cb51d7fa312021", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/e5e784149a09bd69d9a5e3b01c5cbd2e2bd653d8", + "reference": "e5e784149a09bd69d9a5e3b01c5cbd2e2bd653d8", "shasum": "" }, "require": { - "neitanod/forceutf8": "^2.0.4", - "php": ">=7.2", - "php-ffmpeg/php-ffmpeg": "^0.17.0" + "doctrine/deprecations": "^1.1", + "ext-filter": "*", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^1.7", + "phpstan/phpdoc-parser": "^1.7|^2.0", + "webmozart/assert": "^1.9.1" }, "require-dev": { - "friendsofphp/php-cs-fixer": ">=2.17", - "php-parallel-lint/php-parallel-lint": "^1.2", - "phpmd/phpmd": "^2.9", - "phpunit/phpunit": "^9.3", - "sebastian/phpcpd": ">=4.1", - "squizlabs/php_codesniffer": "^3.5" + "mockery/mockery": "~1.3.5 || ~1.6.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-webmozart-assert": "^1.2", + "phpunit/phpunit": "^9.5", + "psalm/phar": "^5.26" }, - "suggest": { - "FFmpeg": "Use FFmpeg/FFprobe as adapter", - "ImageMagick": "Use ImageMagick as adapter", - "ext-exif": "Use exif PHP extension as adapter", - "lib-exiftool": "Use perl lib exiftool as adapter" + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } }, - "default-branch": true, - "type": "library", "autoload": { - "psr-0": { - "PHPExif": "lib/" + "psr-4": { + "phpDocumentor\\Reflection\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -2621,59 +6337,60 @@ ], "authors": [ { - "name": "Tom Van Herreweghe", - "homepage": "http://theanalogguy.be", - "role": "Developer" + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" } ], - "description": "Object-Oriented EXIF parsing", - "keywords": [ - "IPTC", - "ImageMagick", - "exif", - "exiftool", - "ffmpeg", - "ffprobe", - "imagick", - "jpeg", - "tiff" - ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", "support": { - "issues": "https://github.com/LycheeOrg/php-exif/issues", - "source": "https://github.com/LycheeOrg/php-exif/tree/master" + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.1" }, - "time": "2021-05-23T11:47:45+00:00" + "time": "2024-12-07T09:39:29+00:00" }, { - "name": "maennchen/zipstream-php", - "version": "1.2.0", + "name": "phpdocumentor/type-resolver", + "version": "1.10.0", "source": { "type": "git", - "url": "https://github.com/maennchen/ZipStream-PHP.git", - "reference": "6373eefe0b3274d7b702d81f2c99aa977ff97dc2" + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/6373eefe0b3274d7b702d81f2c99aa977ff97dc2", - "reference": "6373eefe0b3274d7b702d81f2c99aa977ff97dc2", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/679e3ce485b99e84c775d28e2e96fade9a7fb50a", + "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a", "shasum": "" }, "require": { - "ext-mbstring": "*", - "myclabs/php-enum": "^1.5", - "php": ">= 7.1", - "psr/http-message": "^1.0" + "doctrine/deprecations": "^1.0", + "php": "^7.3 || ^8.0", + "phpdocumentor/reflection-common": "^2.0", + "phpstan/phpdoc-parser": "^1.18|^2.0" }, "require-dev": { - "ext-zip": "*", - "guzzlehttp/guzzle": ">= 6.3", - "mikey179/vfsstream": "^1.6", - "phpunit/phpunit": ">= 7.5" + "ext-tokenizer": "*", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpunit/phpunit": "^9.5", + "rector/rector": "^0.13.9", + "vimeo/psalm": "^4.25" }, "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + } + }, "autoload": { "psr-4": { - "ZipStream\\": "src/" + "phpDocumentor\\Reflection\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -2682,165 +6399,128 @@ ], "authors": [ { - "name": "Paul Duncan", - "email": "pabs@pablotron.org" - }, - { - "name": "Jesse Donat", - "email": "donatj@gmail.com" - }, - { - "name": "Jonatan Männchen", - "email": "jonatan@maennchen.ch" - }, - { - "name": "András Kolesár", - "email": "kolesar@kolesar.hu" + "name": "Mike van Riel", + "email": "me@mikevanriel.com" } ], - "description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.", - "keywords": [ - "stream", - "zip" - ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", "support": { - "issues": "https://github.com/maennchen/ZipStream-PHP/issues", - "source": "https://github.com/maennchen/ZipStream-PHP/tree/master" + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.10.0" }, - "time": "2019-07-17T11:01:58+00:00" + "time": "2024-11-09T15:12:26+00:00" }, { - "name": "mockery/mockery", - "version": "1.4.3", + "name": "phpoption/phpoption", + "version": "1.9.3", "source": { "type": "git", - "url": "https://github.com/mockery/mockery.git", - "reference": "d1339f64479af1bee0e82a0413813fe5345a54ea" + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mockery/mockery/zipball/d1339f64479af1bee0e82a0413813fe5345a54ea", - "reference": "d1339f64479af1bee0e82a0413813fe5345a54ea", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/e3fac8b24f56113f7cb96af14958c0dd16330f54", + "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54", "shasum": "" }, "require": { - "hamcrest/hamcrest-php": "^2.0.1", - "lib-pcre": ">=7.0", - "php": "^7.3 || ^8.0" - }, - "conflict": { - "phpunit/phpunit": "<8.0" + "php": "^7.2.5 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "^8.5 || ^9.3" + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28" }, "type": "library", "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, "branch-alias": { - "dev-master": "1.4.x-dev" + "dev-master": "1.9-dev" } }, "autoload": { - "psr-0": { - "Mockery": "library/" + "psr-4": { + "PhpOption\\": "src/PhpOption/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "Apache-2.0" ], "authors": [ { - "name": "Pádraic Brady", - "email": "padraic.brady@gmail.com", - "homepage": "http://blog.astrumfutura.com" + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh" }, { - "name": "Dave Marshall", - "email": "dave.marshall@atstsolutions.co.uk", - "homepage": "http://davedevelopment.co.uk" + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" } ], - "description": "Mockery is a simple yet flexible PHP mock object framework", - "homepage": "https://github.com/mockery/mockery", + "description": "Option Type for PHP", "keywords": [ - "BDD", - "TDD", - "library", - "mock", - "mock objects", - "mockery", - "stub", - "test", - "test double", - "testing" + "language", + "option", + "php", + "type" ], "support": { - "issues": "https://github.com/mockery/mockery/issues", - "source": "https://github.com/mockery/mockery/tree/1.4.3" + "issues": "https://github.com/schmittjoh/php-option/issues", + "source": "https://github.com/schmittjoh/php-option/tree/1.9.3" }, - "time": "2021-02-24T09:51:49+00:00" + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2024-07-20T21:41:07+00:00" }, { - "name": "monolog/monolog", - "version": "2.2.0", + "name": "phpseclib/phpseclib", + "version": "3.0.43", "source": { "type": "git", - "url": "https://github.com/Seldaek/monolog.git", - "reference": "1cb1cde8e8dd0f70cc0fe51354a59acad9302084" + "url": "https://github.com/phpseclib/phpseclib.git", + "reference": "709ec107af3cb2f385b9617be72af8cf62441d02" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/1cb1cde8e8dd0f70cc0fe51354a59acad9302084", - "reference": "1cb1cde8e8dd0f70cc0fe51354a59acad9302084", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/709ec107af3cb2f385b9617be72af8cf62441d02", + "reference": "709ec107af3cb2f385b9617be72af8cf62441d02", "shasum": "" }, "require": { - "php": ">=7.2", - "psr/log": "^1.0.1" - }, - "provide": { - "psr/log-implementation": "1.0.0" + "paragonie/constant_time_encoding": "^1|^2|^3", + "paragonie/random_compat": "^1.4|^2.0|^9.99.99", + "php": ">=5.6.1" }, "require-dev": { - "aws/aws-sdk-php": "^2.4.9 || ^3.0", - "doctrine/couchdb": "~1.0@dev", - "elasticsearch/elasticsearch": "^7", - "graylog2/gelf-php": "^1.4.2", - "mongodb/mongodb": "^1.8", - "php-amqplib/php-amqplib": "~2.4", - "php-console/php-console": "^3.1.3", - "phpspec/prophecy": "^1.6.1", - "phpstan/phpstan": "^0.12.59", - "phpunit/phpunit": "^8.5", - "predis/predis": "^1.1", - "rollbar/rollbar": "^1.3", - "ruflin/elastica": ">=0.90 <7.0.1", - "swiftmailer/swiftmailer": "^5.3|^6.0" + "phpunit/phpunit": "*" }, "suggest": { - "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", - "doctrine/couchdb": "Allow sending log messages to a CouchDB server", - "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", - "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", - "ext-mbstring": "Allow to work properly with unicode symbols", - "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", - "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", - "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", - "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", - "php-console/php-console": "Allow sending log messages to Google Chrome", - "rollbar/rollbar": "Allow sending log messages to Rollbar", - "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + "ext-dom": "Install the DOM extension to load XML formatted public keys.", + "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", + "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.", + "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.", + "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations." }, "type": "library", - "extra": { - "branch-alias": { - "dev-main": "2.x-dev" - } - }, "autoload": { + "files": [ + "phpseclib/bootstrap.php" + ], "psr-4": { - "Monolog\\": "src/Monolog" + "phpseclib3\\": "phpseclib/" } }, "notification-url": "https://packagist.org/downloads/", @@ -2849,61 +6529,145 @@ ], "authors": [ { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "https://seld.be" + "name": "Jim Wigginton", + "email": "terrafrost@php.net", + "role": "Lead Developer" + }, + { + "name": "Patrick Monnerat", + "email": "pm@datasphere.ch", + "role": "Developer" + }, + { + "name": "Andreas Fischer", + "email": "bantu@phpbb.com", + "role": "Developer" + }, + { + "name": "Hans-Jürgen Petrich", + "email": "petrich@tronic-media.com", + "role": "Developer" + }, + { + "name": "Graham Campbell", + "email": "graham@alt-three.com", + "role": "Developer" } ], - "description": "Sends your logs to files, sockets, inboxes, databases and various web services", - "homepage": "https://github.com/Seldaek/monolog", + "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.", + "homepage": "http://phpseclib.sourceforge.net", "keywords": [ - "log", - "logging", - "psr-3" + "BigInteger", + "aes", + "asn.1", + "asn1", + "blowfish", + "crypto", + "cryptography", + "encryption", + "rsa", + "security", + "sftp", + "signature", + "signing", + "ssh", + "twofish", + "x.509", + "x509" ], "support": { - "issues": "https://github.com/Seldaek/monolog/issues", - "source": "https://github.com/Seldaek/monolog/tree/2.2.0" + "issues": "https://github.com/phpseclib/phpseclib/issues", + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.43" }, "funding": [ { - "url": "https://github.com/Seldaek", + "url": "https://github.com/terrafrost", "type": "github" }, { - "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "url": "https://www.patreon.com/phpseclib", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpseclib/phpseclib", "type": "tidelift" } ], - "time": "2020-12-14T13:15:25+00:00" + "time": "2024-12-14T21:12:59+00:00" }, { - "name": "myclabs/php-enum", - "version": "1.8.0", + "name": "phpstan/phpdoc-parser", + "version": "2.0.0", "source": { "type": "git", - "url": "https://github.com/myclabs/php-enum.git", - "reference": "46cf3d8498b095bd33727b13fd5707263af99421" + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "c00d78fb6b29658347f9d37ebe104bffadf36299" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/php-enum/zipball/46cf3d8498b095bd33727b13fd5707263af99421", - "reference": "46cf3d8498b095bd33727b13fd5707263af99421", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/c00d78fb6b29658347f9d37ebe104bffadf36299", + "reference": "c00d78fb6b29658347f9d37ebe104bffadf36299", "shasum": "" }, "require": { - "ext-json": "*", - "php": "^7.3 || ^8.0" + "php": "^7.4 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "^9.5", - "squizlabs/php_codesniffer": "1.*", - "vimeo/psalm": "^4.5.1" + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^5.3.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.0.0" + }, + "time": "2024-10-13T11:29:49+00:00" + }, + { + "name": "psr/cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, "autoload": { "psr-4": { - "MyCLabs\\Enum\\": "src/" + "Psr\\Cache\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -2912,125 +6676,95 @@ ], "authors": [ { - "name": "PHP Enum contributors", - "homepage": "https://github.com/myclabs/php-enum/graphs/contributors" + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" } ], - "description": "PHP Enum implementation", - "homepage": "http://github.com/myclabs/php-enum", + "description": "Common interface for caching libraries", "keywords": [ - "enum" + "cache", + "psr", + "psr-6" ], "support": { - "issues": "https://github.com/myclabs/php-enum/issues", - "source": "https://github.com/myclabs/php-enum/tree/1.8.0" + "source": "https://github.com/php-fig/cache/tree/3.0.0" }, - "funding": [ - { - "url": "https://github.com/mnapoli", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/myclabs/php-enum", - "type": "tidelift" - } - ], - "time": "2021-02-15T16:11:48+00:00" + "time": "2021-02-03T23:26:27+00:00" }, { - "name": "neitanod/forceutf8", - "version": "v2.0.4", + "name": "psr/clock", + "version": "1.0.0", "source": { "type": "git", - "url": "https://github.com/neitanod/forceutf8.git", - "reference": "c1fbe70bfb5ad41b8ec5785056b0e308b40d4831" + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/neitanod/forceutf8/zipball/c1fbe70bfb5ad41b8ec5785056b0e308b40d4831", - "reference": "c1fbe70bfb5ad41b8ec5785056b0e308b40d4831", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": "^7.0 || ^8.0" }, "type": "library", "autoload": { - "psr-0": { - "ForceUTF8\\": "src/" + "psr-4": { + "Psr\\Clock\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastián Grignoli", - "email": "grignoli@gmail.com" + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" } ], - "description": "PHP Class Encoding featuring popular Encoding::toUTF8() function --formerly known as forceUTF8()-- that fixes mixed encoded strings.", - "homepage": "https://github.com/neitanod/forceutf8", + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], "support": { - "issues": "https://github.com/neitanod/forceutf8/issues", - "source": "https://github.com/neitanod/forceutf8/tree/master" + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" }, - "time": "2019-12-10T14:09:14+00:00" + "time": "2022-11-25T14:36:26+00:00" }, { - "name": "nesbot/carbon", - "version": "2.48.0", + "name": "psr/container", + "version": "2.0.2", "source": { "type": "git", - "url": "https://github.com/briannesbitt/Carbon.git", - "reference": "d3c447f21072766cddec3522f9468a5849a76147" + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/d3c447f21072766cddec3522f9468a5849a76147", - "reference": "d3c447f21072766cddec3522f9468a5849a76147", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", "shasum": "" }, - "require": { - "ext-json": "*", - "php": "^7.1.8 || ^8.0", - "symfony/polyfill-mbstring": "^1.0", - "symfony/translation": "^3.4 || ^4.0 || ^5.0" - }, - "require-dev": { - "doctrine/orm": "^2.7", - "friendsofphp/php-cs-fixer": "^2.14 || ^3.0", - "kylekatarnls/multi-tester": "^2.0", - "phpmd/phpmd": "^2.9", - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^0.12.54", - "phpunit/phpunit": "^7.5.20 || ^8.5.14", - "squizlabs/php_codesniffer": "^3.4" + "require": { + "php": ">=7.4.0" }, - "bin": [ - "bin/carbon" - ], "type": "library", "extra": { "branch-alias": { - "dev-master": "2.x-dev", - "dev-3.x": "3.x-dev" - }, - "laravel": { - "providers": [ - "Carbon\\Laravel\\ServiceProvider" - ] - }, - "phpstan": { - "includes": [ - "extension.neon" - ] + "dev-master": "2.0.x-dev" } }, "autoload": { "psr-4": { - "Carbon\\": "src/Carbon/" + "Psr\\Container\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -3039,63 +6773,51 @@ ], "authors": [ { - "name": "Brian Nesbitt", - "email": "brian@nesbot.com", - "homepage": "http://nesbot.com" - }, - { - "name": "kylekatarnls", - "homepage": "http://github.com/kylekatarnls" + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" } ], - "description": "An API extension for DateTime that supports 281 different languages.", - "homepage": "http://carbon.nesbot.com", + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", "keywords": [ - "date", - "datetime", - "time" + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" ], "support": { - "issues": "https://github.com/briannesbitt/Carbon/issues", - "source": "https://github.com/briannesbitt/Carbon" + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" }, - "funding": [ - { - "url": "https://opencollective.com/Carbon", - "type": "open_collective" - }, - { - "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon", - "type": "tidelift" - } - ], - "time": "2021-05-07T10:08:30+00:00" + "time": "2021-11-05T16:47:00+00:00" }, { - "name": "neutron/temporary-filesystem", - "version": "3.0", + "name": "psr/event-dispatcher", + "version": "1.0.0", "source": { "type": "git", - "url": "https://github.com/romainneutron/Temporary-Filesystem.git", - "reference": "60e79adfd16f42f4b888e351ad49f9dcb959e3c2" + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/romainneutron/Temporary-Filesystem/zipball/60e79adfd16f42f4b888e351ad49f9dcb959e3c2", - "reference": "60e79adfd16f42f4b888e351ad49f9dcb959e3c2", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", "shasum": "" }, "require": { - "php": ">=5.6", - "symfony/filesystem": "^2.3 || ^3.0 || ^4.0 || ^5.0" - }, - "require-dev": { - "symfony/phpunit-bridge": "^5.0.4" + "php": ">=7.2.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, "autoload": { - "psr-0": { - "Neutron": "src" + "psr-4": { + "Psr\\EventDispatcher\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -3104,56 +6826,49 @@ ], "authors": [ { - "name": "Romain Neutron", - "email": "imprec@gmail.com" + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" } ], - "description": "Symfony filesystem extension to handle temporary files", + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], "support": { - "issues": "https://github.com/romainneutron/Temporary-Filesystem/issues", - "source": "https://github.com/romainneutron/Temporary-Filesystem/tree/3.0" + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" }, - "time": "2020-07-27T14:00:33+00:00" + "time": "2019-01-08T18:20:26+00:00" }, { - "name": "nyholm/psr7", - "version": "1.4.0", + "name": "psr/http-client", + "version": "1.0.3", "source": { "type": "git", - "url": "https://github.com/Nyholm/psr7.git", - "reference": "23ae1f00fbc6a886cbe3062ca682391b9cc7c37b" + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Nyholm/psr7/zipball/23ae1f00fbc6a886cbe3062ca682391b9cc7c37b", - "reference": "23ae1f00fbc6a886cbe3062ca682391b9cc7c37b", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", "shasum": "" }, "require": { - "php": ">=7.1", - "php-http/message-factory": "^1.0", - "psr/http-factory": "^1.0", - "psr/http-message": "^1.0" - }, - "provide": { - "psr/http-factory-implementation": "1.0", - "psr/http-message-implementation": "1.0" - }, - "require-dev": { - "http-interop/http-factory-tests": "^0.8", - "php-http/psr7-integration-tests": "^1.0", - "phpunit/phpunit": "^7.5 || 8.5 || 9.4", - "symfony/error-handler": "^4.4" + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4-dev" + "dev-master": "1.0.x-dev" } }, "autoload": { "psr-4": { - "Nyholm\\Psr7\\": "src/" + "Psr\\Http\\Client\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -3162,70 +6877,51 @@ ], "authors": [ { - "name": "Tobias Nyholm", - "email": "tobias.nyholm@gmail.com" - }, - { - "name": "Martijn van der Ven", - "email": "martijn@vanderven.se" + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" } ], - "description": "A fast PHP7 implementation of PSR-7", - "homepage": "https://tnyholm.se", + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", "keywords": [ - "psr-17", - "psr-7" + "http", + "http-client", + "psr", + "psr-18" ], "support": { - "issues": "https://github.com/Nyholm/psr7/issues", - "source": "https://github.com/Nyholm/psr7/tree/1.4.0" + "source": "https://github.com/php-fig/http-client" }, - "funding": [ - { - "url": "https://github.com/Zegnat", - "type": "github" - }, - { - "url": "https://github.com/nyholm", - "type": "github" - } - ], - "time": "2021-02-18T15:41:32+00:00" + "time": "2023-09-23T14:17:50+00:00" }, { - "name": "opis/closure", - "version": "3.6.2", + "name": "psr/http-factory", + "version": "1.1.0", "source": { "type": "git", - "url": "https://github.com/opis/closure.git", - "reference": "06e2ebd25f2869e54a306dda991f7db58066f7f6" + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opis/closure/zipball/06e2ebd25f2869e54a306dda991f7db58066f7f6", - "reference": "06e2ebd25f2869e54a306dda991f7db58066f7f6", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", "shasum": "" }, "require": { - "php": "^5.4 || ^7.0 || ^8.0" - }, - "require-dev": { - "jeremeamia/superclosure": "^2.0", - "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0" + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.6.x-dev" + "dev-master": "1.0.x-dev" } }, "autoload": { "psr-4": { - "Opis\\Closure\\": "src/" - }, - "files": [ - "functions.php" - ] + "Psr\\Http\\Message\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -3233,68 +6929,52 @@ ], "authors": [ { - "name": "Marius Sarca", - "email": "marius.sarca@gmail.com" - }, - { - "name": "Sorin Sarca", - "email": "sarca_sorin@hotmail.com" + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" } ], - "description": "A library that can be used to serialize closures (anonymous functions) and arbitrary objects.", - "homepage": "https://opis.io/closure", + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", "keywords": [ - "anonymous functions", - "closure", - "function", - "serializable", - "serialization", - "serialize" + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" ], "support": { - "issues": "https://github.com/opis/closure/issues", - "source": "https://github.com/opis/closure/tree/3.6.2" + "source": "https://github.com/php-fig/http-factory" }, - "time": "2021-04-09T13:42:10+00:00" + "time": "2024-04-15T12:06:14+00:00" }, { - "name": "php-ffmpeg/php-ffmpeg", - "version": "v0.17.0", + "name": "psr/http-message", + "version": "2.0", "source": { "type": "git", - "url": "https://github.com/PHP-FFMpeg/PHP-FFMpeg.git", - "reference": "a5147d1ae041e78e7870bf2443d4e2dfa7635856" + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-FFMpeg/PHP-FFMpeg/zipball/a5147d1ae041e78e7870bf2443d4e2dfa7635856", - "reference": "a5147d1ae041e78e7870bf2443d4e2dfa7635856", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", "shasum": "" }, "require": { - "alchemy/binary-driver": "^1.5 || ~2.0.0 || ^5.0", - "doctrine/cache": "^1.0", - "evenement/evenement": "^3.0 || ^2.0 || ^1.0", - "neutron/temporary-filesystem": "^2.1.1 || ^3.0", - "php": ">=5.3.9" - }, - "require-dev": { - "silex/silex": "~1.0", - "symfony/phpunit-bridge": "^5.0.4", - "symfony/process": "2.8 || 3.3" - }, - "suggest": { - "php-ffmpeg/extras": "A compilation of common audio & video drivers for PHP-FFMpeg" + "php": "^7.2 || ^8.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "0.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { - "psr-0": { - "FFMpeg": "src" + "psr-4": { + "Psr\\Http\\Message\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -3303,88 +6983,51 @@ ], "authors": [ { - "name": "Romain Neutron", - "email": "imprec@gmail.com", - "homepage": "http://www.lickmychip.com/" - }, - { - "name": "Phraseanet Team", - "email": "info@alchemy.fr", - "homepage": "http://www.phraseanet.com/" - }, - { - "name": "Patrik Karisch", - "email": "patrik@karisch.guru", - "homepage": "http://www.karisch.guru" - }, - { - "name": "Romain Biard", - "email": "romain.biard@gmail.com", - "homepage": "https://www.strime.io/" - }, - { - "name": "Jens Hausdorf", - "email": "hello@jens-hausdorf.de", - "homepage": "https://jens-hausdorf.de" + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" } ], - "description": "FFMpeg PHP, an Object Oriented library to communicate with AVconv / ffmpeg", + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", "keywords": [ - "audio", - "audio processing", - "avconv", - "avprobe", - "ffmpeg", - "ffprobe", - "video", - "video processing" + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" ], "support": { - "issues": "https://github.com/PHP-FFMpeg/PHP-FFMpeg/issues", - "source": "https://github.com/PHP-FFMpeg/PHP-FFMpeg/tree/v0.17.0" + "source": "https://github.com/php-fig/http-message/tree/2.0" }, - "time": "2020-12-18T14:31:34+00:00" + "time": "2023-04-04T09:54:51+00:00" }, { - "name": "php-http/discovery", - "version": "1.13.0", + "name": "psr/log", + "version": "3.0.2", "source": { "type": "git", - "url": "https://github.com/php-http/discovery.git", - "reference": "788f72d64c43dc361e7fcc7464c3d947c64984a7" + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/discovery/zipball/788f72d64c43dc361e7fcc7464c3d947c64984a7", - "reference": "788f72d64c43dc361e7fcc7464c3d947c64984a7", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" - }, - "conflict": { - "nyholm/psr7": "<1.0" - }, - "require-dev": { - "graham-campbell/phpspec-skip-example-extension": "^5.0", - "php-http/httplug": "^1.0 || ^2.0", - "php-http/message-factory": "^1.0", - "phpspec/phpspec": "^5.1 || ^6.1", - "puli/composer-plugin": "1.0.0-beta10" - }, - "suggest": { - "php-http/message": "Allow to use Guzzle, Diactoros or Slim Framework factories", - "puli/composer-plugin": "Sets up Puli which is recommended for Discovery to work. Check http://docs.php-http.org/en/latest/discovery.html for more details." + "php": ">=8.0.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.9-dev" + "dev-master": "3.x-dev" } }, "autoload": { "psr-4": { - "Http\\Discovery\\": "src/" + "Psr\\Log\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -3393,65 +7036,48 @@ ], "authors": [ { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com" + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" } ], - "description": "Finds installed HTTPlug implementations and PSR-7 message factories", - "homepage": "http://php-http.org", + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", "keywords": [ - "adapter", - "client", - "discovery", - "factory", - "http", - "message", - "psr7" + "log", + "psr", + "psr-3" ], "support": { - "issues": "https://github.com/php-http/discovery/issues", - "source": "https://github.com/php-http/discovery/tree/1.13.0" + "source": "https://github.com/php-fig/log/tree/3.0.2" }, - "time": "2020-11-27T14:49:42+00:00" + "time": "2024-09-11T13:17:53+00:00" }, { - "name": "php-http/guzzle7-adapter", - "version": "0.1.1", + "name": "psr/simple-cache", + "version": "3.0.0", "source": { "type": "git", - "url": "https://github.com/php-http/guzzle7-adapter.git", - "reference": "1967de656b9679a2a6a66d0e4e16fa99bbed1ad1" + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/guzzle7-adapter/zipball/1967de656b9679a2a6a66d0e4e16fa99bbed1ad1", - "reference": "1967de656b9679a2a6a66d0e4e16fa99bbed1ad1", - "shasum": "" - }, - "require": { - "guzzlehttp/guzzle": "^7.0", - "php": "^7.2 | ^8.0", - "php-http/httplug": "^2.0", - "psr/http-client": "^1.0" - }, - "provide": { - "php-http/async-client-implementation": "1.0", - "php-http/client-implementation": "1.0", - "psr/http-client-implementation": "1.0" - }, - "require-dev": { - "php-http/client-integration-tests": "^3.0", - "phpunit/phpunit": "^8.0|^9.3" + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "0.2.x-dev" + "dev-master": "3.0.x-dev" } }, "autoload": { "psr-4": { - "Http\\Adapter\\Guzzle7\\": "src/" + "Psr\\SimpleCache\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -3460,56 +7086,49 @@ ], "authors": [ { - "name": "Tobias Nyholm", - "email": "tobias.nyholm@gmail.com" + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" } ], - "description": "Guzzle 7 HTTP Adapter", - "homepage": "http://httplug.io", + "description": "Common interfaces for simple caching", "keywords": [ - "Guzzle", - "http" + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" ], "support": { - "issues": "https://github.com/php-http/guzzle7-adapter/issues", - "source": "https://github.com/php-http/guzzle7-adapter/tree/0.1.1" + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" }, - "time": "2020-10-21T17:30:51+00:00" + "time": "2021-10-29T13:26:27+00:00" }, { - "name": "php-http/httplug", - "version": "2.2.0", + "name": "ralouphie/getallheaders", + "version": "3.0.3", "source": { "type": "git", - "url": "https://github.com/php-http/httplug.git", - "reference": "191a0a1b41ed026b717421931f8d3bd2514ffbf9" + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/httplug/zipball/191a0a1b41ed026b717421931f8d3bd2514ffbf9", - "reference": "191a0a1b41ed026b717421931f8d3bd2514ffbf9", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0", - "php-http/promise": "^1.1", - "psr/http-client": "^1.0", - "psr/http-message": "^1.0" + "php": ">=5.6" }, "require-dev": { - "friends-of-phpspec/phpspec-code-coverage": "^4.1", - "phpspec/phpspec": "^5.1 || ^6.0" + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.x-dev" - } - }, "autoload": { - "psr-4": { - "Http\\Client\\": "src/" - } + "files": [ + "src/getallheaders.php" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -3517,77 +7136,69 @@ ], "authors": [ { - "name": "Eric GELOEN", - "email": "geloen.eric@gmail.com" - }, - { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com", - "homepage": "https://sagikazarmark.hu" + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" } ], - "description": "HTTPlug, the HTTP client abstraction for PHP", - "homepage": "http://httplug.io", - "keywords": [ - "client", - "http" - ], + "description": "A polyfill for getallheaders.", "support": { - "issues": "https://github.com/php-http/httplug/issues", - "source": "https://github.com/php-http/httplug/tree/master" + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" }, - "time": "2020-07-13T15:43:23+00:00" + "time": "2019-03-08T08:55:37+00:00" }, { - "name": "php-http/message", - "version": "1.11.0", + "name": "ramsey/collection", + "version": "2.0.0", "source": { "type": "git", - "url": "https://github.com/php-http/message.git", - "reference": "fb0dbce7355cad4f4f6a225f537c34d013571f29" + "url": "https://github.com/ramsey/collection.git", + "reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/message/zipball/fb0dbce7355cad4f4f6a225f537c34d013571f29", - "reference": "fb0dbce7355cad4f4f6a225f537c34d013571f29", + "url": "https://api.github.com/repos/ramsey/collection/zipball/a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5", + "reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5", "shasum": "" }, "require": { - "clue/stream-filter": "^1.5", - "php": "^7.1 || ^8.0", - "php-http/message-factory": "^1.0.2", - "psr/http-message": "^1.0" - }, - "provide": { - "php-http/message-factory-implementation": "1.0" + "php": "^8.1" }, "require-dev": { - "ergebnis/composer-normalize": "^2.6", - "ext-zlib": "*", - "guzzlehttp/psr7": "^1.0", - "laminas/laminas-diactoros": "^2.0", - "phpspec/phpspec": "^5.1 || ^6.3", - "slim/slim": "^3.0" - }, - "suggest": { - "ext-zlib": "Used with compressor/decompressor streams", - "guzzlehttp/psr7": "Used with Guzzle PSR-7 Factories", - "laminas/laminas-diactoros": "Used with Diactoros Factories", - "slim/slim": "Used with Slim Framework PSR-7 implementation" + "captainhook/plugin-composer": "^5.3", + "ergebnis/composer-normalize": "^2.28.3", + "fakerphp/faker": "^1.21", + "hamcrest/hamcrest-php": "^2.0", + "jangregor/phpstan-prophecy": "^1.0", + "mockery/mockery": "^1.5", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpcsstandards/phpcsutils": "^1.0.0-rc1", + "phpspec/prophecy-phpunit": "^2.0", + "phpstan/extension-installer": "^1.2", + "phpstan/phpstan": "^1.9", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5", + "psalm/plugin-mockery": "^1.1", + "psalm/plugin-phpunit": "^0.18.4", + "ramsey/coding-standard": "^2.0.3", + "ramsey/conventional-commits": "^1.3", + "vimeo/psalm": "^5.4" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "1.10-dev" + "captainhook": { + "force-install": true + }, + "ramsey/conventional-commits": { + "configFile": "conventional-commits.json" } }, "autoload": { "psr-4": { - "Http\\Message\\": "src/" - }, - "files": [ - "src/filters.php" - ] + "Ramsey\\Collection\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -3595,107 +7206,160 @@ ], "authors": [ { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com" + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" } ], - "description": "HTTP Message related tools", - "homepage": "http://php-http.org", + "description": "A PHP library for representing and manipulating collections.", "keywords": [ - "http", - "message", - "psr-7" + "array", + "collection", + "hash", + "map", + "queue", + "set" ], "support": { - "issues": "https://github.com/php-http/message/issues", - "source": "https://github.com/php-http/message/tree/1.11.0" + "issues": "https://github.com/ramsey/collection/issues", + "source": "https://github.com/ramsey/collection/tree/2.0.0" }, - "time": "2021-02-01T08:54:58+00:00" + "funding": [ + { + "url": "https://github.com/ramsey", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/ramsey/collection", + "type": "tidelift" + } + ], + "time": "2022-12-31T21:50:55+00:00" }, { - "name": "php-http/message-factory", - "version": "v1.0.2", + "name": "ramsey/uuid", + "version": "4.7.6", "source": { "type": "git", - "url": "https://github.com/php-http/message-factory.git", - "reference": "a478cb11f66a6ac48d8954216cfed9aa06a501a1" + "url": "https://github.com/ramsey/uuid.git", + "reference": "91039bc1faa45ba123c4328958e620d382ec7088" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/message-factory/zipball/a478cb11f66a6ac48d8954216cfed9aa06a501a1", - "reference": "a478cb11f66a6ac48d8954216cfed9aa06a501a1", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/91039bc1faa45ba123c4328958e620d382ec7088", + "reference": "91039bc1faa45ba123c4328958e620d382ec7088", "shasum": "" }, "require": { - "php": ">=5.4", - "psr/http-message": "^1.0" + "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12", + "ext-json": "*", + "php": "^8.0", + "ramsey/collection": "^1.2 || ^2.0" + }, + "replace": { + "rhumsaa/uuid": "self.version" + }, + "require-dev": { + "captainhook/captainhook": "^5.10", + "captainhook/plugin-composer": "^5.3", + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", + "doctrine/annotations": "^1.8", + "ergebnis/composer-normalize": "^2.15", + "mockery/mockery": "^1.3", + "paragonie/random-lib": "^2", + "php-mock/php-mock": "^2.2", + "php-mock/php-mock-mockery": "^1.3", + "php-parallel-lint/php-parallel-lint": "^1.1", + "phpbench/phpbench": "^1.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-phpunit": "^1.1", + "phpunit/phpunit": "^8.5 || ^9", + "ramsey/composer-repl": "^1.4", + "slevomat/coding-standard": "^8.4", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.9" + }, + "suggest": { + "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", + "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", + "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", + "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "1.0-dev" + "captainhook": { + "force-install": true } }, "autoload": { + "files": [ + "src/functions.php" + ], "psr-4": { - "Http\\Message\\": "src/" + "Ramsey\\Uuid\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ - { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com" - } - ], - "description": "Factory interfaces for PSR-7 HTTP Message", - "homepage": "http://php-http.org", + "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", "keywords": [ - "factory", - "http", - "message", - "stream", - "uri" + "guid", + "identifier", + "uuid" ], "support": { - "issues": "https://github.com/php-http/message-factory/issues", - "source": "https://github.com/php-http/message-factory/tree/master" + "issues": "https://github.com/ramsey/uuid/issues", + "source": "https://github.com/ramsey/uuid/tree/4.7.6" }, - "time": "2015-12-19T14:08:53+00:00" + "funding": [ + { + "url": "https://github.com/ramsey", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/ramsey/uuid", + "type": "tidelift" + } + ], + "time": "2024-04-27T21:32:50+00:00" }, { - "name": "php-http/promise", - "version": "1.1.0", + "name": "revolt/event-loop", + "version": "v1.0.6", "source": { "type": "git", - "url": "https://github.com/php-http/promise.git", - "reference": "4c4c1f9b7289a2ec57cde7f1e9762a5789506f88" + "url": "https://github.com/revoltphp/event-loop.git", + "reference": "25de49af7223ba039f64da4ae9a28ec2d10d0254" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/promise/zipball/4c4c1f9b7289a2ec57cde7f1e9762a5789506f88", - "reference": "4c4c1f9b7289a2ec57cde7f1e9762a5789506f88", + "url": "https://api.github.com/repos/revoltphp/event-loop/zipball/25de49af7223ba039f64da4ae9a28ec2d10d0254", + "reference": "25de49af7223ba039f64da4ae9a28ec2d10d0254", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" + "php": ">=8.1" }, "require-dev": { - "friends-of-phpspec/phpspec-code-coverage": "^4.3.2", - "phpspec/phpspec": "^5.1.2 || ^6.2" + "ext-json": "*", + "jetbrains/phpstorm-stubs": "^2019.3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.15" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1-dev" + "dev-main": "1.x-dev" } }, "autoload": { "psr-4": { - "Http\\Promise\\": "src/" + "Revolt\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -3704,120 +7368,118 @@ ], "authors": [ { - "name": "Joel Wurtz", - "email": "joel.wurtz@gmail.com" + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" }, { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com" + "name": "Cees-Jan Kiewiet", + "email": "ceesjank@gmail.com" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" } ], - "description": "Promise used for asynchronous HTTP requests", - "homepage": "http://httplug.io", + "description": "Rock-solid event loop for concurrent PHP applications.", "keywords": [ - "promise" + "async", + "asynchronous", + "concurrency", + "event", + "event-loop", + "non-blocking", + "scheduler" ], "support": { - "issues": "https://github.com/php-http/promise/issues", - "source": "https://github.com/php-http/promise/tree/1.1.0" + "issues": "https://github.com/revoltphp/event-loop/issues", + "source": "https://github.com/revoltphp/event-loop/tree/v1.0.6" }, - "time": "2020-07-07T09:29:14+00:00" + "time": "2023-11-30T05:34:44+00:00" }, { - "name": "phpoption/phpoption", - "version": "1.7.5", + "name": "revolution/socialite-mastodon", + "version": "1.5.2", "source": { "type": "git", - "url": "https://github.com/schmittjoh/php-option.git", - "reference": "994ecccd8f3283ecf5ac33254543eb0ac946d525" + "url": "https://github.com/kawax/socialite-mastodon.git", + "reference": "2013943f9258b247215dc342504c016c33535e8e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/994ecccd8f3283ecf5ac33254543eb0ac946d525", - "reference": "994ecccd8f3283ecf5ac33254543eb0ac946d525", + "url": "https://api.github.com/repos/kawax/socialite-mastodon/zipball/2013943f9258b247215dc342504c016c33535e8e", + "reference": "2013943f9258b247215dc342504c016c33535e8e", "shasum": "" }, "require": { - "php": "^5.5.9 || ^7.0 || ^8.0" + "ext-json": "*", + "laravel/socialite": "*", + "php": "^8.0" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.4.1", - "phpunit/phpunit": "^4.8.35 || ^5.7.27 || ^6.5.6 || ^7.0 || ^8.0 || ^9.0" + "orchestra/testbench": "^9.0", + "revolution/laravel-mastodon-api": "^3.1" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "1.7-dev" + "laravel": { + "providers": [ + "Revolution\\Socialite\\Mastodon\\MastodonServiceProvider" + ] } }, "autoload": { "psr-4": { - "PhpOption\\": "src/PhpOption/" + "Revolution\\Socialite\\Mastodon\\": "src" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "Apache-2.0" + "MIT" ], "authors": [ { - "name": "Johannes M. Schmitt", - "email": "schmittjoh@gmail.com" - }, - { - "name": "Graham Campbell", - "email": "graham@alt-three.com" + "name": "kawax", + "email": "kawaxbiz@gmail.com" } ], - "description": "Option Type for PHP", + "description": "Socialite for Mastodon", "keywords": [ - "language", - "option", - "php", - "type" + "laravel", + "mastodon", + "socialite" ], "support": { - "issues": "https://github.com/schmittjoh/php-option/issues", - "source": "https://github.com/schmittjoh/php-option/tree/1.7.5" + "source": "https://github.com/kawax/socialite-mastodon/tree/1.5.2" }, - "funding": [ - { - "url": "https://github.com/GrahamCampbell", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", - "type": "tidelift" - } - ], - "time": "2020-07-20T17:29:33+00:00" + "time": "2024-10-02T08:20:07+00:00" }, { - "name": "psr/cache", - "version": "1.0.1", + "name": "socialiteproviders/amazon", + "version": "4.1.0", "source": { "type": "git", - "url": "https://github.com/php-fig/cache.git", - "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8" + "url": "https://github.com/SocialiteProviders/Amazon.git", + "reference": "717c8c78d42fdffced7ccba218092384ce473e1d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8", - "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8", + "url": "https://api.github.com/repos/SocialiteProviders/Amazon/zipball/717c8c78d42fdffced7ccba218092384ce473e1d", + "reference": "717c8c78d42fdffced7ccba218092384ce473e1d", "shasum": "" }, "require": { - "php": ">=5.3.0" + "ext-json": "*", + "php": "^7.2 || ^8.0", + "socialiteproviders/manager": "~4.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, "autoload": { "psr-4": { - "Psr\\Cache\\": "src/" + "SocialiteProviders\\Amazon\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -3826,42 +7488,47 @@ ], "authors": [ { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "name": "atymic", + "email": "atymicq@gmail.com", + "homepage": "https://atymic.dev" } ], - "description": "Common interface for caching libraries", - "keywords": [ - "cache", - "psr", - "psr-6" - ], + "description": "Amazon OAuth2 Provider for Laravel Socialite", "support": { - "source": "https://github.com/php-fig/cache/tree/master" + "source": "https://github.com/SocialiteProviders/Amazon/tree/4.1.0" }, - "time": "2016-08-06T20:24:11+00:00" + "time": "2020-12-01T23:10:59+00:00" }, { - "name": "psr/container", - "version": "1.1.1", + "name": "socialiteproviders/apple", + "version": "5.6.1", "source": { "type": "git", - "url": "https://github.com/php-fig/container.git", - "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf" + "url": "https://github.com/SocialiteProviders/Apple.git", + "reference": "e00ff7c06e4df297aaeace4e454b2054d6bebe95" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf", - "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf", + "url": "https://api.github.com/repos/SocialiteProviders/Apple/zipball/e00ff7c06e4df297aaeace4e454b2054d6bebe95", + "reference": "e00ff7c06e4df297aaeace4e454b2054d6bebe95", "shasum": "" }, "require": { - "php": ">=7.2.0" + "ext-json": "*", + "ext-openssl": "*", + "firebase/php-jwt": "^6.8", + "lcobucci/clock": "^2.0 || ^3.0", + "lcobucci/jwt": "^4.1.5 || ^5.0.0", + "php": "^8.0", + "socialiteproviders/manager": "^4.4" + }, + "suggest": { + "ahilmurugesan/socialite-apple-helper": "Automatic Apple client key generation and management." }, "type": "library", "autoload": { "psr-4": { - "Psr\\Container\\": "src/" + "SocialiteProviders\\Apple\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -3870,51 +7537,63 @@ ], "authors": [ { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" + "name": "Ahilan", + "email": "ahilmurugesan@gmail.com", + "role": "Developer" + }, + { + "name": "Vamsi Krishna V", + "email": "vamsi@vonectech.com", + "homepage": "https://vonectech.com/", + "role": "Farmer" } ], - "description": "Common Container Interface (PHP FIG PSR-11)", - "homepage": "https://github.com/php-fig/container", + "description": "Apple OAuth2 Provider for Laravel Socialite", "keywords": [ - "PSR-11", - "container", - "container-interface", - "container-interop", - "psr" + "apple", + "apple client key", + "apple sign in", + "client key generator", + "client key refresh", + "laravel", + "laravel apple", + "laravel socialite", + "oauth", + "provider", + "sign in with apple", + "socialite", + "socialite apple" ], "support": { - "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/1.1.1" + "docs": "https://socialiteproviders.com/apple", + "issues": "https://github.com/socialiteproviders/providers/issues", + "source": "https://github.com/socialiteproviders/providers" }, - "time": "2021-03-05T17:36:06+00:00" + "time": "2023-12-06T14:43:17+00:00" }, { - "name": "psr/event-dispatcher", - "version": "1.0.0", + "name": "socialiteproviders/authentik", + "version": "5.2.0", "source": { "type": "git", - "url": "https://github.com/php-fig/event-dispatcher.git", - "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + "url": "https://github.com/SocialiteProviders/Authentik.git", + "reference": "4cf129cf04728a38e0531c54454464b162f0fa66" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", - "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "url": "https://api.github.com/repos/SocialiteProviders/Authentik/zipball/4cf129cf04728a38e0531c54454464b162f0fa66", + "reference": "4cf129cf04728a38e0531c54454464b162f0fa66", "shasum": "" }, "require": { - "php": ">=7.2.0" + "ext-json": "*", + "php": "^8.0", + "socialiteproviders/manager": "^4.4" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, "autoload": { "psr-4": { - "Psr\\EventDispatcher\\": "src/" + "SocialiteProviders\\Authentik\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -3923,49 +7602,48 @@ ], "authors": [ { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "name": "rf152", + "email": "git@rf152.co.uk" } ], - "description": "Standard interfaces for event handling.", + "description": "Authentik OAuth2 Provider for Laravel Socialite", "keywords": [ - "events", - "psr", - "psr-14" + "authentik", + "laravel", + "oauth", + "provider", + "socialite" ], "support": { - "issues": "https://github.com/php-fig/event-dispatcher/issues", - "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + "docs": "https://socialiteproviders.com/authentik", + "issues": "https://github.com/socialiteproviders/providers/issues", + "source": "https://github.com/socialiteproviders/providers" }, - "time": "2019-01-08T18:20:26+00:00" + "time": "2023-11-07T22:21:16+00:00" }, { - "name": "psr/http-client", - "version": "1.0.1", + "name": "socialiteproviders/facebook", + "version": "4.1.0", "source": { "type": "git", - "url": "https://github.com/php-fig/http-client.git", - "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621" + "url": "https://github.com/SocialiteProviders/Facebook.git", + "reference": "9b94a9334b5d0f61de8f5a20928d63d4d8f4e00d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-client/zipball/2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", - "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", + "url": "https://api.github.com/repos/SocialiteProviders/Facebook/zipball/9b94a9334b5d0f61de8f5a20928d63d4d8f4e00d", + "reference": "9b94a9334b5d0f61de8f5a20928d63d4d8f4e00d", "shasum": "" }, "require": { - "php": "^7.0 || ^8.0", - "psr/http-message": "^1.0" + "ext-json": "*", + "php": "^7.2 || ^8.0", + "socialiteproviders/manager": "~4.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, "autoload": { "psr-4": { - "Psr\\Http\\Client\\": "src/" + "SocialiteProviders\\Facebook\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -3974,50 +7652,39 @@ ], "authors": [ { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "name": "Oleksandr Prypkhan (Alex Wells)", + "email": "autaut03@googlemail.com" } ], - "description": "Common interface for HTTP clients", - "homepage": "https://github.com/php-fig/http-client", - "keywords": [ - "http", - "http-client", - "psr", - "psr-18" - ], + "description": "Facebook (facebook.com) OAuth2 Provider for Laravel Socialite", "support": { - "source": "https://github.com/php-fig/http-client/tree/master" + "source": "https://github.com/SocialiteProviders/Facebook/tree/4.1.0" }, - "time": "2020-06-29T06:28:15+00:00" + "time": "2020-12-01T23:10:59+00:00" }, { - "name": "psr/http-factory", - "version": "1.0.1", + "name": "socialiteproviders/github", + "version": "4.1.0", "source": { "type": "git", - "url": "https://github.com/php-fig/http-factory.git", - "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be" + "url": "https://github.com/SocialiteProviders/GitHub.git", + "reference": "25fc481721d74b829b43bff3519e7103d5339e19" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-factory/zipball/12ac7fcd07e5b077433f5f2bee95b3a771bf61be", - "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "url": "https://api.github.com/repos/SocialiteProviders/GitHub/zipball/25fc481721d74b829b43bff3519e7103d5339e19", + "reference": "25fc481721d74b829b43bff3519e7103d5339e19", "shasum": "" }, "require": { - "php": ">=7.0.0", - "psr/http-message": "^1.0" + "ext-json": "*", + "php": "^7.2 || ^8.0", + "socialiteproviders/manager": "~4.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, "autoload": { "psr-4": { - "Psr\\Http\\Message\\": "src/" + "SocialiteProviders\\GitHub\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -4026,52 +7693,39 @@ ], "authors": [ { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "name": "Anton Komarev", + "email": "ell@cybercog.su" } ], - "description": "Common interfaces for PSR-7 HTTP message factories", - "keywords": [ - "factory", - "http", - "message", - "psr", - "psr-17", - "psr-7", - "request", - "response" - ], + "description": "GitHub OAuth2 Provider for Laravel Socialite", "support": { - "source": "https://github.com/php-fig/http-factory/tree/master" + "source": "https://github.com/SocialiteProviders/GitHub/tree/4.1.0" }, - "time": "2019-04-30T12:38:16+00:00" + "time": "2020-12-01T23:10:59+00:00" }, { - "name": "psr/http-message", - "version": "1.0.1", + "name": "socialiteproviders/google", + "version": "4.1.0", "source": { "type": "git", - "url": "https://github.com/php-fig/http-message.git", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + "url": "https://github.com/SocialiteProviders/Google-Plus.git", + "reference": "1cb8f6fb2c0dd0fc8b34e95f69865663fdf0b401" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "url": "https://api.github.com/repos/SocialiteProviders/Google-Plus/zipball/1cb8f6fb2c0dd0fc8b34e95f69865663fdf0b401", + "reference": "1cb8f6fb2c0dd0fc8b34e95f69865663fdf0b401", "shasum": "" }, "require": { - "php": ">=5.3.0" + "ext-json": "*", + "php": "^7.2 || ^8.0", + "socialiteproviders/manager": "~4.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, "autoload": { "psr-4": { - "Psr\\Http\\Message\\": "src/" + "SocialiteProviders\\Google\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -4080,51 +7734,39 @@ ], "authors": [ { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "name": "xstoop", + "email": "myenglishnameisx@gmail.com" } ], - "description": "Common interface for HTTP messages", - "homepage": "https://github.com/php-fig/http-message", - "keywords": [ - "http", - "http-message", - "psr", - "psr-7", - "request", - "response" - ], + "description": "Google OAuth2 Provider for Laravel Socialite", "support": { - "source": "https://github.com/php-fig/http-message/tree/master" + "source": "https://github.com/SocialiteProviders/Google-Plus/tree/4.1.0" }, - "time": "2016-08-06T14:39:51+00:00" + "time": "2020-12-01T23:10:59+00:00" }, { - "name": "psr/log", - "version": "1.1.4", + "name": "socialiteproviders/keycloak", + "version": "5.3.0", "source": { "type": "git", - "url": "https://github.com/php-fig/log.git", - "reference": "d49695b909c3b7628b6289db5479a1c204601f11" + "url": "https://github.com/SocialiteProviders/Keycloak.git", + "reference": "87d13f8a411a6f8f5010ecbaff9aedd4494863e4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", - "reference": "d49695b909c3b7628b6289db5479a1c204601f11", + "url": "https://api.github.com/repos/SocialiteProviders/Keycloak/zipball/87d13f8a411a6f8f5010ecbaff9aedd4494863e4", + "reference": "87d13f8a411a6f8f5010ecbaff9aedd4494863e4", "shasum": "" }, "require": { - "php": ">=5.3.0" + "ext-json": "*", + "php": "^7.4 || ^8.0", + "socialiteproviders/manager": "~4.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.1.x-dev" - } - }, "autoload": { "psr-4": { - "Psr\\Log\\": "Psr/Log/" + "SocialiteProviders\\Keycloak\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -4133,48 +7775,59 @@ ], "authors": [ { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" + "name": "Oleg Kuchumov", + "email": "voenniy@gmail.com" } ], - "description": "Common interface for logging libraries", - "homepage": "https://github.com/php-fig/log", + "description": "Keycloak OAuth2 Provider for Laravel Socialite", "keywords": [ - "log", - "psr", - "psr-3" + "keycloak", + "laravel", + "oauth", + "provider", + "socialite" ], "support": { - "source": "https://github.com/php-fig/log/tree/1.1.4" + "docs": "https://socialiteproviders.com/keycloak", + "issues": "https://github.com/socialiteproviders/providers/issues", + "source": "https://github.com/socialiteproviders/providers" }, - "time": "2021-05-03T11:20:27+00:00" + "time": "2023-04-10T05:50:49+00:00" }, { - "name": "psr/simple-cache", - "version": "1.0.1", + "name": "socialiteproviders/manager", + "version": "v4.8.0", "source": { "type": "git", - "url": "https://github.com/php-fig/simple-cache.git", - "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b" + "url": "https://github.com/SocialiteProviders/Manager.git", + "reference": "e93acc38f8464cc775a2b8bf09df311d1fdfefcb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", - "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "url": "https://api.github.com/repos/SocialiteProviders/Manager/zipball/e93acc38f8464cc775a2b8bf09df311d1fdfefcb", + "reference": "e93acc38f8464cc775a2b8bf09df311d1fdfefcb", "shasum": "" }, "require": { - "php": ">=5.3.0" + "illuminate/support": "^8.0 || ^9.0 || ^10.0 || ^11.0", + "laravel/socialite": "^5.5", + "php": "^8.1" + }, + "require-dev": { + "mockery/mockery": "^1.2", + "phpunit/phpunit": "^9.0" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" + "laravel": { + "providers": [ + "SocialiteProviders\\Manager\\ServiceProvider" + ] } }, "autoload": { "psr-4": { - "Psr\\SimpleCache\\": "src/" + "SocialiteProviders\\Manager\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -4183,49 +7836,62 @@ ], "authors": [ { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "name": "Andy Wendt", + "email": "andy@awendt.com" + }, + { + "name": "Anton Komarev", + "email": "a.komarev@cybercog.su" + }, + { + "name": "Miguel Piedrafita", + "email": "soy@miguelpiedrafita.com" + }, + { + "name": "atymic", + "email": "atymicq@gmail.com", + "homepage": "https://atymic.dev" } ], - "description": "Common interfaces for simple caching", + "description": "Easily add new or override built-in providers in Laravel Socialite.", + "homepage": "https://socialiteproviders.com", "keywords": [ - "cache", - "caching", - "psr", - "psr-16", - "simple-cache" + "laravel", + "manager", + "oauth", + "providers", + "socialite" ], "support": { - "source": "https://github.com/php-fig/simple-cache/tree/master" + "issues": "https://github.com/socialiteproviders/manager/issues", + "source": "https://github.com/socialiteproviders/manager" }, - "time": "2017-10-23T01:57:42+00:00" + "time": "2025-01-03T09:40:37+00:00" }, { - "name": "ralouphie/getallheaders", - "version": "3.0.3", + "name": "socialiteproviders/microsoft", + "version": "4.6.0", "source": { "type": "git", - "url": "https://github.com/ralouphie/getallheaders.git", - "reference": "120b605dfeb996808c31b6477290a714d356e822" + "url": "https://github.com/SocialiteProviders/Microsoft.git", + "reference": "76d51308ef53d1425a5ce3c3b094b0925cd82951" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", - "reference": "120b605dfeb996808c31b6477290a714d356e822", + "url": "https://api.github.com/repos/SocialiteProviders/Microsoft/zipball/76d51308ef53d1425a5ce3c3b094b0925cd82951", + "reference": "76d51308ef53d1425a5ce3c3b094b0925cd82951", "shasum": "" }, "require": { - "php": ">=5.6" - }, - "require-dev": { - "php-coveralls/php-coveralls": "^2.1", - "phpunit/phpunit": "^5 || ^6.5" + "ext-json": "*", + "php": "^8.0", + "socialiteproviders/manager": "^4.4" }, "type": "library", "autoload": { - "files": [ - "src/getallheaders.php" - ] + "psr-4": { + "SocialiteProviders\\Microsoft\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -4233,56 +7899,48 @@ ], "authors": [ { - "name": "Ralph Khattar", - "email": "ralph.khattar@gmail.com" + "name": "Brian Faust", + "email": "hello@brianfaust.de" } ], - "description": "A polyfill for getallheaders.", + "description": "Microsoft OAuth2 Provider for Laravel Socialite", + "keywords": [ + "laravel", + "microsoft", + "oauth", + "provider", + "socialite" + ], "support": { - "issues": "https://github.com/ralouphie/getallheaders/issues", - "source": "https://github.com/ralouphie/getallheaders/tree/develop" + "docs": "https://socialiteproviders.com/microsoft", + "issues": "https://github.com/socialiteproviders/providers/issues", + "source": "https://github.com/socialiteproviders/providers" }, - "time": "2019-03-08T08:55:37+00:00" + "time": "2024-11-07T21:57:40+00:00" }, { - "name": "ramsey/collection", - "version": "1.1.3", + "name": "socialiteproviders/nextcloud", + "version": "4.0.0", "source": { "type": "git", - "url": "https://github.com/ramsey/collection.git", - "reference": "28a5c4ab2f5111db6a60b2b4ec84057e0f43b9c1" + "url": "https://github.com/SocialiteProviders/Nextcloud.git", + "reference": "94b3cef64410994bdd480230a7aa78294564cc44" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/collection/zipball/28a5c4ab2f5111db6a60b2b4ec84057e0f43b9c1", - "reference": "28a5c4ab2f5111db6a60b2b4ec84057e0f43b9c1", + "url": "https://api.github.com/repos/SocialiteProviders/Nextcloud/zipball/94b3cef64410994bdd480230a7aa78294564cc44", + "reference": "94b3cef64410994bdd480230a7aa78294564cc44", "shasum": "" }, "require": { - "php": "^7.2 || ^8" - }, - "require-dev": { - "captainhook/captainhook": "^5.3", - "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", - "ergebnis/composer-normalize": "^2.6", - "fakerphp/faker": "^1.5", - "hamcrest/hamcrest-php": "^2", - "jangregor/phpstan-prophecy": "^0.8", - "mockery/mockery": "^1.3", - "phpstan/extension-installer": "^1", - "phpstan/phpstan": "^0.12.32", - "phpstan/phpstan-mockery": "^0.12.5", - "phpstan/phpstan-phpunit": "^0.12.11", - "phpunit/phpunit": "^8.5 || ^9", - "psy/psysh": "^0.10.4", - "slevomat/coding-standard": "^6.3", - "squizlabs/php_codesniffer": "^3.5", - "vimeo/psalm": "^4.4" + "ext-json": "*", + "php": "^7.2 || ^8.0", + "socialiteproviders/manager": "~4.0" }, "type": "library", "autoload": { "psr-4": { - "Ramsey\\Collection\\": "src/" + "SocialiteProviders\\Nextcloud\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -4291,140 +7949,104 @@ ], "authors": [ { - "name": "Ben Ramsey", - "email": "ben@benramsey.com", - "homepage": "https://benramsey.com" + "name": "Artur Shaik", + "email": "artur@shaik.link" } ], - "description": "A PHP 7.2+ library for representing and manipulating collections.", - "keywords": [ - "array", - "collection", - "hash", - "map", - "queue", - "set" - ], + "description": "Nextcloud OAuth2 Provider for Laravel Socialite", "support": { - "issues": "https://github.com/ramsey/collection/issues", - "source": "https://github.com/ramsey/collection/tree/1.1.3" + "source": "https://github.com/SocialiteProviders/Nextcloud/tree/4.0.0" }, - "funding": [ - { - "url": "https://github.com/ramsey", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/ramsey/collection", - "type": "tidelift" - } - ], - "time": "2021-01-21T17:40:04+00:00" + "time": "2021-02-27T23:17:54+00:00" }, { - "name": "ramsey/uuid", - "version": "4.1.1", + "name": "spatie/enum", + "version": "3.13.0", "source": { "type": "git", - "url": "https://github.com/ramsey/uuid.git", - "reference": "cd4032040a750077205918c86049aa0f43d22947" + "url": "https://github.com/spatie/enum.git", + "reference": "f1a0f464ba909491a53e60a955ce84ad7cd93a2c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/cd4032040a750077205918c86049aa0f43d22947", - "reference": "cd4032040a750077205918c86049aa0f43d22947", + "url": "https://api.github.com/repos/spatie/enum/zipball/f1a0f464ba909491a53e60a955ce84ad7cd93a2c", + "reference": "f1a0f464ba909491a53e60a955ce84ad7cd93a2c", "shasum": "" }, "require": { - "brick/math": "^0.8 || ^0.9", "ext-json": "*", - "php": "^7.2 || ^8", - "ramsey/collection": "^1.0", - "symfony/polyfill-ctype": "^1.8" - }, - "replace": { - "rhumsaa/uuid": "self.version" + "php": "^8.0" }, "require-dev": { - "codeception/aspect-mock": "^3", - "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7.0", - "doctrine/annotations": "^1.8", - "goaop/framework": "^2", - "mockery/mockery": "^1.3", - "moontoast/math": "^1.1", - "paragonie/random-lib": "^2", - "php-mock/php-mock-mockery": "^1.3", - "php-mock/php-mock-phpunit": "^2.5", - "php-parallel-lint/php-parallel-lint": "^1.1", - "phpbench/phpbench": "^0.17.1", - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^0.12", - "phpstan/phpstan-mockery": "^0.12", - "phpstan/phpstan-phpunit": "^0.12", - "phpunit/phpunit": "^8.5", - "psy/psysh": "^0.10.0", - "slevomat/coding-standard": "^6.0", - "squizlabs/php_codesniffer": "^3.5", - "vimeo/psalm": "3.9.4" + "fakerphp/faker": "^1.9.1", + "larapack/dd": "^1.1", + "phpunit/phpunit": "^9.0", + "vimeo/psalm": "^4.3" }, "suggest": { - "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", - "ext-ctype": "Enables faster processing of character classification using ctype functions.", - "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", - "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", - "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", - "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + "fakerphp/faker": "To use the enum faker provider", + "phpunit/phpunit": "To use the enum assertions" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.x-dev" - } - }, "autoload": { "psr-4": { - "Ramsey\\Uuid\\": "src/" - }, - "files": [ - "src/functions.php" - ] + "Spatie\\Enum\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", - "homepage": "https://github.com/ramsey/uuid", - "keywords": [ - "guid", - "identifier", - "uuid" + "authors": [ + { + "name": "Brent Roose", + "email": "brent@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + }, + { + "name": "Tom Witkowski", + "email": "dev@gummibeer.de", + "homepage": "https://gummibeer.de", + "role": "Developer" + } + ], + "description": "PHP Enums", + "homepage": "https://github.com/spatie/enum", + "keywords": [ + "enum", + "enumerable", + "spatie" ], "support": { - "issues": "https://github.com/ramsey/uuid/issues", - "rss": "https://github.com/ramsey/uuid/releases.atom", - "source": "https://github.com/ramsey/uuid" + "docs": "https://docs.spatie.be/enum", + "issues": "https://github.com/spatie/enum/issues", + "source": "https://github.com/spatie/enum" }, "funding": [ { - "url": "https://github.com/ramsey", + "url": "https://spatie.be/open-source/support-us", + "type": "custom" + }, + { + "url": "https://github.com/spatie", "type": "github" } ], - "time": "2020-08-18T17:17:46+00:00" + "time": "2022-04-22T08:51:55+00:00" }, { "name": "spatie/guzzle-rate-limiter-middleware", - "version": "2.0.1", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/spatie/guzzle-rate-limiter-middleware.git", - "reference": "8679f7a22e46edc182046f18b83bacbc627b0600" + "reference": "096966730d24a5c056a69465ae458f9339e1945a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/guzzle-rate-limiter-middleware/zipball/8679f7a22e46edc182046f18b83bacbc627b0600", - "reference": "8679f7a22e46edc182046f18b83bacbc627b0600", + "url": "https://api.github.com/repos/spatie/guzzle-rate-limiter-middleware/zipball/096966730d24a5c056a69465ae458f9339e1945a", + "reference": "096966730d24a5c056a69465ae458f9339e1945a", "shasum": "" }, "require": { @@ -4461,33 +8083,34 @@ ], "support": { "issues": "https://github.com/spatie/guzzle-rate-limiter-middleware/issues", - "source": "https://github.com/spatie/guzzle-rate-limiter-middleware/tree/2.0.1" + "source": "https://github.com/spatie/guzzle-rate-limiter-middleware/tree/2.1.0" }, - "time": "2020-12-19T18:47:06+00:00" + "time": "2024-12-02T08:42:25+00:00" }, { "name": "spatie/image-optimizer", - "version": "1.4.0", + "version": "1.8.0", "source": { "type": "git", "url": "https://github.com/spatie/image-optimizer.git", - "reference": "c22202fdd57856ed18a79cfab522653291a6e96a" + "reference": "4fd22035e81d98fffced65a8c20d9ec4daa9671c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/image-optimizer/zipball/c22202fdd57856ed18a79cfab522653291a6e96a", - "reference": "c22202fdd57856ed18a79cfab522653291a6e96a", + "url": "https://api.github.com/repos/spatie/image-optimizer/zipball/4fd22035e81d98fffced65a8c20d9ec4daa9671c", + "reference": "4fd22035e81d98fffced65a8c20d9ec4daa9671c", "shasum": "" }, "require": { "ext-fileinfo": "*", - "php": "^7.2|^8.0", - "psr/log": "^1.0", - "symfony/process": "^4.2|^5.0" + "php": "^7.3|^8.0", + "psr/log": "^1.0 | ^2.0 | ^3.0", + "symfony/process": "^4.2|^5.0|^6.0|^7.0" }, "require-dev": { - "phpunit/phpunit": "^8.0|^9.0", - "symfony/var-dumper": "^4.2|^5.0" + "pestphp/pest": "^1.21", + "phpunit/phpunit": "^8.5.21|^9.4.4", + "symfony/var-dumper": "^4.2|^5.0|^6.0|^7.0" }, "type": "library", "autoload": { @@ -4515,35 +8138,119 @@ ], "support": { "issues": "https://github.com/spatie/image-optimizer/issues", - "source": "https://github.com/spatie/image-optimizer/tree/1.4.0" + "source": "https://github.com/spatie/image-optimizer/tree/1.8.0" + }, + "time": "2024-11-04T08:24:54+00:00" + }, + { + "name": "spatie/laravel-data", + "version": "4.11.1", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-data.git", + "reference": "df5b58baebae34475ca35338b4e9a131c9e2a8e0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-data/zipball/df5b58baebae34475ca35338b4e9a131c9e2a8e0", + "reference": "df5b58baebae34475ca35338b4e9a131c9e2a8e0", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^10.0|^11.0", + "php": "^8.1", + "phpdocumentor/reflection": "^6.0", + "spatie/laravel-package-tools": "^1.9.0", + "spatie/php-structure-discoverer": "^2.0" + }, + "require-dev": { + "fakerphp/faker": "^1.14", + "friendsofphp/php-cs-fixer": "^3.0", + "inertiajs/inertia-laravel": "^1.2", + "livewire/livewire": "^3.0", + "mockery/mockery": "^1.6", + "nesbot/carbon": "^2.63", + "nunomaduro/larastan": "^2.0", + "orchestra/testbench": "^8.0|^9.0", + "pestphp/pest": "^2.31", + "pestphp/pest-plugin-laravel": "^2.0", + "pestphp/pest-plugin-livewire": "^2.1", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.1", + "phpunit/phpunit": "^10.0", + "spatie/invade": "^1.0", + "spatie/laravel-typescript-transformer": "^2.5", + "spatie/pest-plugin-snapshots": "^2.1", + "spatie/test-time": "^1.2" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Spatie\\LaravelData\\LaravelDataServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Spatie\\LaravelData\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ruben Van Assche", + "email": "ruben@spatie.be", + "role": "Developer" + } + ], + "description": "Create unified resources and data transfer objects", + "homepage": "https://github.com/spatie/laravel-data", + "keywords": [ + "laravel", + "laravel-data", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/laravel-data/issues", + "source": "https://github.com/spatie/laravel-data/tree/4.11.1" }, - "time": "2021-04-22T06:17:27+00:00" + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2024-10-23T07:14:53+00:00" }, { "name": "spatie/laravel-feed", - "version": "3.2.0", + "version": "4.4.0", "source": { "type": "git", "url": "https://github.com/spatie/laravel-feed.git", - "reference": "1a9cbeb21de25b8a4f6a30ddc53e038fb5ae1651" + "reference": "8cd283bbb998beb3ae220e71bae162e52dfbd1d9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-feed/zipball/1a9cbeb21de25b8a4f6a30ddc53e038fb5ae1651", - "reference": "1a9cbeb21de25b8a4f6a30ddc53e038fb5ae1651", + "url": "https://api.github.com/repos/spatie/laravel-feed/zipball/8cd283bbb998beb3ae220e71bae162e52dfbd1d9", + "reference": "8cd283bbb998beb3ae220e71bae162e52dfbd1d9", "shasum": "" }, "require": { - "illuminate/contracts": "^7.0|^8.0", - "illuminate/http": "^7.0|^8.0", - "illuminate/support": "^7.0|^8.0", - "php": "^7.4|^8.0", - "spatie/laravel-package-tools": "^1.5" + "illuminate/contracts": "^10.0|^11.0", + "illuminate/http": "^10.0|^11.0", + "illuminate/support": "^10.0|^11.0", + "php": "^8.0", + "spatie/laravel-package-tools": "^1.15" }, "require-dev": { - "orchestra/testbench": "^5.0|^6.0", - "phpunit/phpunit": "^9.3", - "spatie/phpunit-snapshot-assertions": "^4.2", + "orchestra/testbench": "^8.0|^9.0", + "pestphp/pest": "^2.0", + "spatie/pest-plugin-snapshots": "^2.0", "spatie/test-time": "^1.2" }, "type": "library", @@ -4581,6 +8288,11 @@ "email": "sebastian@spatie.be", "homepage": "https://spatie.be", "role": "Developer" + }, + { + "name": "Patrick Organ", + "homepage": "https://github.com/patinthehat", + "role": "Developer" } ], "description": "Generate rss feeds", @@ -4592,49 +8304,52 @@ "spatie" ], "support": { - "issues": "https://github.com/spatie/laravel-feed/issues", - "source": "https://github.com/spatie/laravel-feed/tree/3.2.0" + "source": "https://github.com/spatie/laravel-feed/tree/4.4.0" }, "funding": [ + { + "url": "https://spatie.be/open-source/support-us", + "type": "custom" + }, { "url": "https://github.com/spatie", "type": "github" } ], - "time": "2021-05-17T21:00:38+00:00" + "time": "2024-03-01T10:20:32+00:00" }, { "name": "spatie/laravel-image-optimizer", - "version": "1.6.4", + "version": "1.8.0", "source": { "type": "git", "url": "https://github.com/spatie/laravel-image-optimizer.git", - "reference": "c39e9ea77dee6b6eddfc26800adb1aa06a624294" + "reference": "024752cba691fee3cd1800000b6aa3da3b8b2474" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-image-optimizer/zipball/c39e9ea77dee6b6eddfc26800adb1aa06a624294", - "reference": "c39e9ea77dee6b6eddfc26800adb1aa06a624294", + "url": "https://api.github.com/repos/spatie/laravel-image-optimizer/zipball/024752cba691fee3cd1800000b6aa3da3b8b2474", + "reference": "024752cba691fee3cd1800000b6aa3da3b8b2474", "shasum": "" }, "require": { - "laravel/framework": "^6.0|^7.0|^8.0", - "php": "^7.2|^8.0", + "laravel/framework": "^8.0|^9.0|^10.0|^11.0", + "php": "^8.0", "spatie/image-optimizer": "^1.2.0" }, "require-dev": { - "orchestra/testbench": "^4.0|^5.0|^6.0", - "phpunit/phpunit": "^9.0" + "orchestra/testbench": "^6.23|^7.0|^8.0|^9.0", + "phpunit/phpunit": "^9.4|^10.5" }, "type": "library", "extra": { "laravel": { - "providers": [ - "Spatie\\LaravelImageOptimizer\\ImageOptimizerServiceProvider" - ], "aliases": { "ImageOptimizer": "Spatie\\LaravelImageOptimizer\\Facades\\ImageOptimizer" - } + }, + "providers": [ + "Spatie\\LaravelImageOptimizer\\ImageOptimizerServiceProvider" + ] } }, "autoload": { @@ -4661,8 +8376,7 @@ "spatie" ], "support": { - "issues": "https://github.com/spatie/laravel-image-optimizer/issues", - "source": "https://github.com/spatie/laravel-image-optimizer/tree/1.6.4" + "source": "https://github.com/spatie/laravel-image-optimizer/tree/1.8.0" }, "funding": [ { @@ -4670,37 +8384,37 @@ "type": "custom" } ], - "time": "2020-11-27T18:27:06+00:00" + "time": "2024-02-29T10:55:08+00:00" }, { "name": "spatie/laravel-package-tools", - "version": "1.8.0", + "version": "1.18.0", "source": { "type": "git", "url": "https://github.com/spatie/laravel-package-tools.git", - "reference": "1150930205dbe32f4e594ba2805cd56563120145" + "reference": "8332205b90d17164913244f4a8e13ab7e6761d29" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/1150930205dbe32f4e594ba2805cd56563120145", - "reference": "1150930205dbe32f4e594ba2805cd56563120145", + "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/8332205b90d17164913244f4a8e13ab7e6761d29", + "reference": "8332205b90d17164913244f4a8e13ab7e6761d29", "shasum": "" }, "require": { - "illuminate/contracts": "^7.0|^8.0", - "mockery/mockery": "^1.4", - "php": "^7.4|^8.0" + "illuminate/contracts": "^9.28|^10.0|^11.0", + "php": "^8.0" }, "require-dev": { - "orchestra/testbench": "^5.0|^6.0", - "phpunit/phpunit": "^9.3", - "spatie/test-time": "^1.2" + "mockery/mockery": "^1.5", + "orchestra/testbench": "^7.7|^8.0|^9.0", + "pestphp/pest": "^1.22|^2", + "phpunit/phpunit": "^9.5.24|^10.5", + "spatie/pest-plugin-test-time": "^1.1|^2.2" }, "type": "library", "autoload": { "psr-4": { - "Spatie\\LaravelPackageTools\\": "src", - "Spatie\\LaravelPackageTools\\Database\\Factories\\": "database/factories" + "Spatie\\LaravelPackageTools\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -4722,7 +8436,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-package-tools/issues", - "source": "https://github.com/spatie/laravel-package-tools/tree/1.8.0" + "source": "https://github.com/spatie/laravel-package-tools/tree/1.18.0" }, "funding": [ { @@ -4730,37 +8444,53 @@ "type": "github" } ], - "time": "2021-05-22T09:13:53+00:00" + "time": "2024-12-30T13:13:39+00:00" }, { - "name": "spomky-labs/base64url", - "version": "v2.0.4", + "name": "spatie/laravel-typescript-transformer", + "version": "2.5.0", "source": { "type": "git", - "url": "https://github.com/Spomky-Labs/base64url.git", - "reference": "7752ce931ec285da4ed1f4c5aa27e45e097be61d" + "url": "https://github.com/spatie/laravel-typescript-transformer.git", + "reference": "cdf82498b7e02f89f5a3c0eeed78ac0d633a212b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Spomky-Labs/base64url/zipball/7752ce931ec285da4ed1f4c5aa27e45e097be61d", - "reference": "7752ce931ec285da4ed1f4c5aa27e45e097be61d", + "url": "https://api.github.com/repos/spatie/laravel-typescript-transformer/zipball/cdf82498b7e02f89f5a3c0eeed78ac0d633a212b", + "reference": "cdf82498b7e02f89f5a3c0eeed78ac0d633a212b", "shasum": "" }, "require": { - "php": ">=7.1" + "illuminate/console": "^8.83|^9.30|^10.0|^11.0", + "php": "^8.1", + "spatie/laravel-package-tools": "^1.12", + "spatie/typescript-transformer": "^2.4" }, "require-dev": { - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^0.11|^0.12", - "phpstan/phpstan-beberlei-assert": "^0.11|^0.12", - "phpstan/phpstan-deprecation-rules": "^0.11|^0.12", - "phpstan/phpstan-phpunit": "^0.11|^0.12", - "phpstan/phpstan-strict-rules": "^0.11|^0.12" + "friendsofphp/php-cs-fixer": "^3.0", + "mockery/mockery": "^1.4", + "nesbot/carbon": "^2.63", + "orchestra/testbench": "^6.0|^7.0|^8.0|^9.0", + "pestphp/pest": "^1.22|^2.0", + "phpunit/phpunit": "^9.0|^10.0|^11.0", + "spatie/data-transfer-object": "^2.0", + "spatie/enum": "^3.0", + "spatie/laravel-model-states": "^1.6|^2.0", + "spatie/pest-plugin-snapshots": "^1.1|^2.0", + "spatie/phpunit-snapshot-assertions": "^4.2|^5.0", + "spatie/temporary-directory": "^1.2" }, "type": "library", + "extra": { + "laravel": { + "providers": [ + "Spatie\\LaravelTypeScriptTransformer\\TypeScriptTransformerServiceProvider" + ] + } + }, "autoload": { "psr-4": { - "Base64Url\\": "src/" + "Spatie\\LaravelTypeScriptTransformer\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -4769,67 +8499,81 @@ ], "authors": [ { - "name": "Florent Morselli", - "homepage": "https://github.com/Spomky-Labs/base64url/contributors" + "name": "Ruben Van Assche", + "email": "ruben@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" } ], - "description": "Base 64 URL Safe Encoding/Decoding PHP Library", - "homepage": "https://github.com/Spomky-Labs/base64url", + "description": "Transform your PHP structures to TypeScript types", + "homepage": "https://github.com/spatie/typescript-transformer", "keywords": [ - "base64", - "rfc4648", - "safe", - "url" + "spatie", + "typescript-transformer" ], "support": { - "issues": "https://github.com/Spomky-Labs/base64url/issues", - "source": "https://github.com/Spomky-Labs/base64url/tree/v2.0.4" + "issues": "https://github.com/spatie/laravel-typescript-transformer/issues", + "source": "https://github.com/spatie/laravel-typescript-transformer/tree/2.5.0" }, "funding": [ { - "url": "https://github.com/Spomky", - "type": "github" + "url": "https://spatie.be/open-source/support-us", + "type": "custom" }, { - "url": "https://www.patreon.com/FlorentMorselli", - "type": "patreon" + "url": "https://github.com/spatie", + "type": "github" } ], - "time": "2020-11-03T09:10:25+00:00" + "time": "2024-10-04T13:26:07+00:00" }, { - "name": "spomky-labs/cbor-php", - "version": "v2.0.1", + "name": "spatie/php-structure-discoverer", + "version": "2.2.1", "source": { "type": "git", - "url": "https://github.com/Spomky-Labs/cbor-php.git", - "reference": "9776578000be884cd7864eeb7c37a4ac92d8c995" + "url": "https://github.com/spatie/php-structure-discoverer.git", + "reference": "e2b39ba0baaf05d1300c5467e7ee8a6439324827" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Spomky-Labs/cbor-php/zipball/9776578000be884cd7864eeb7c37a4ac92d8c995", - "reference": "9776578000be884cd7864eeb7c37a4ac92d8c995", + "url": "https://api.github.com/repos/spatie/php-structure-discoverer/zipball/e2b39ba0baaf05d1300c5467e7ee8a6439324827", + "reference": "e2b39ba0baaf05d1300c5467e7ee8a6439324827", "shasum": "" }, "require": { - "brick/math": "^0.8.15|^0.9.0", - "php": ">=7.3" + "amphp/amp": "^v3.0", + "amphp/parallel": "^2.2", + "illuminate/collections": "^10.0|^11.0", + "php": "^8.1", + "spatie/laravel-package-tools": "^1.4.3", + "symfony/finder": "^6.0|^7.0" }, "require-dev": { - "phpstan/phpstan": "^0.12", - "phpstan/phpstan-beberlei-assert": "^0.12", - "phpstan/phpstan-deprecation-rules": "^0.12", - "phpstan/phpstan-phpunit": "^0.12", - "phpstan/phpstan-strict-rules": "^0.12" - }, - "suggest": { - "ext-bcmath": "GMP or BCMath extensions will drastically improve the library performance. BCMath extension needed to handle the Big Float and Decimal Fraction Tags", - "ext-gmp": "GMP or BCMath extensions will drastically improve the library performance" + "illuminate/console": "^10.0|^11.0", + "laravel/pint": "^1.0", + "nunomaduro/collision": "^7.0|^8.0", + "nunomaduro/larastan": "^2.0.1", + "orchestra/testbench": "^7.0|^8.0|^9.0", + "pestphp/pest": "^2.0", + "pestphp/pest-plugin-laravel": "^2.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^9.5|^10.0", + "spatie/laravel-ray": "^1.26" }, "type": "library", + "extra": { + "laravel": { + "providers": [ + "Spatie\\StructureDiscoverer\\StructureDiscovererServiceProvider" + ] + } + }, "autoload": { "psr-4": { - "CBOR\\": "src/" + "Spatie\\StructureDiscoverer\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -4838,70 +8582,56 @@ ], "authors": [ { - "name": "Florent Morselli", - "homepage": "https://github.com/Spomky" - }, - { - "name": "All contributors", - "homepage": "https://github.com/Spomky-Labs/cbor-php/contributors" + "name": "Ruben Van Assche", + "email": "ruben@spatie.be", + "role": "Developer" } ], - "description": "CBOR Encoder/Decoder for PHP", + "description": "Automatically discover structures within your PHP application", + "homepage": "https://github.com/spatie/php-structure-discoverer", "keywords": [ - "Concise Binary Object Representation", - "RFC7049", - "cbor" + "discover", + "laravel", + "php", + "php-structure-discoverer" ], "support": { - "issues": "https://github.com/Spomky-Labs/cbor-php/issues", - "source": "https://github.com/Spomky-Labs/cbor-php/tree/v2.0.1" + "issues": "https://github.com/spatie/php-structure-discoverer/issues", + "source": "https://github.com/spatie/php-structure-discoverer/tree/2.2.1" }, "funding": [ { - "url": "https://www.patreon.com/FlorentMorselli", - "type": "patreon" + "url": "https://github.com/LaravelAutoDiscoverer", + "type": "github" } ], - "time": "2020-08-31T20:08:03+00:00" + "time": "2024-12-16T13:29:18+00:00" }, { - "name": "swiftmailer/swiftmailer", - "version": "v6.2.7", + "name": "spatie/temporary-directory", + "version": "2.3.0", "source": { "type": "git", - "url": "https://github.com/swiftmailer/swiftmailer.git", - "reference": "15f7faf8508e04471f666633addacf54c0ab5933" + "url": "https://github.com/spatie/temporary-directory.git", + "reference": "580eddfe9a0a41a902cac6eeb8f066b42e65a32b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/15f7faf8508e04471f666633addacf54c0ab5933", - "reference": "15f7faf8508e04471f666633addacf54c0ab5933", + "url": "https://api.github.com/repos/spatie/temporary-directory/zipball/580eddfe9a0a41a902cac6eeb8f066b42e65a32b", + "reference": "580eddfe9a0a41a902cac6eeb8f066b42e65a32b", "shasum": "" }, "require": { - "egulias/email-validator": "^2.0|^3.1", - "php": ">=7.0.0", - "symfony/polyfill-iconv": "^1.0", - "symfony/polyfill-intl-idn": "^1.10", - "symfony/polyfill-mbstring": "^1.0" + "php": "^8.0" }, "require-dev": { - "mockery/mockery": "^1.0", - "symfony/phpunit-bridge": "^4.4|^5.0" - }, - "suggest": { - "ext-intl": "Needed to support internationalized email addresses" + "phpunit/phpunit": "^9.5" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "6.2-dev" - } - }, "autoload": { - "files": [ - "lib/swift_required.php" - ] + "psr-4": { + "Spatie\\TemporaryDirectory\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -4909,88 +8639,162 @@ ], "authors": [ { - "name": "Chris Corbyn" + "name": "Alex Vanderbist", + "email": "alex@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "Easily create, use and destroy temporary directories", + "homepage": "https://github.com/spatie/temporary-directory", + "keywords": [ + "php", + "spatie", + "temporary-directory" + ], + "support": { + "issues": "https://github.com/spatie/temporary-directory/issues", + "source": "https://github.com/spatie/temporary-directory/tree/2.3.0" + }, + "funding": [ + { + "url": "https://spatie.be/open-source/support-us", + "type": "custom" }, { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2025-01-13T13:04:43+00:00" + }, + { + "name": "spatie/typescript-transformer", + "version": "2.4.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/typescript-transformer.git", + "reference": "130c2447e0aa83f8d8d0ff590bc5bc402b17d641" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/typescript-transformer/zipball/130c2447e0aa83f8d8d0ff590bc5bc402b17d641", + "reference": "130c2447e0aa83f8d8d0ff590bc5bc402b17d641", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18|^5.0", + "php": "^8.0", + "phpdocumentor/type-resolver": "^1.6.2", + "symfony/process": "^5.2|^6.0|^7.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.40", + "larapack/dd": "^1.1", + "myclabs/php-enum": "^1.7", + "pestphp/pest": "^1.22", + "phpstan/extension-installer": "^1.1", + "phpunit/phpunit": "^9.0", + "spatie/data-transfer-object": "^2.0", + "spatie/enum": "^3.0", + "spatie/pest-plugin-snapshots": "^1.1", + "spatie/temporary-directory": "^1.2|^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\TypeScriptTransformer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ruben Van Assche", + "email": "ruben@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" } ], - "description": "Swiftmailer, free feature-rich PHP mailer", - "homepage": "https://swiftmailer.symfony.com", + "description": "Transform your PHP structures to TypeScript types", + "homepage": "https://github.com/spatie/typescript-transformer", "keywords": [ - "email", - "mail", - "mailer" + "spatie", + "typescript-transformer" ], "support": { - "issues": "https://github.com/swiftmailer/swiftmailer/issues", - "source": "https://github.com/swiftmailer/swiftmailer/tree/v6.2.7" + "issues": "https://github.com/spatie/typescript-transformer/issues", + "source": "https://github.com/spatie/typescript-transformer/tree/2.4.0" }, "funding": [ { - "url": "https://github.com/fabpot", - "type": "github" + "url": "https://spatie.be/open-source/support-us", + "type": "custom" }, { - "url": "https://tidelift.com/funding/github/packagist/swiftmailer/swiftmailer", - "type": "tidelift" + "url": "https://github.com/spatie", + "type": "github" } ], - "time": "2021-03-09T12:30:35+00:00" + "time": "2024-10-04T13:13:08+00:00" }, { "name": "symfony/cache", - "version": "v5.2.9", + "version": "v7.2.1", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "17a6d585603fade3838bc692548b619d97ded67e" + "reference": "e7e983596b744c4539f31e79b0350a6cf5878a20" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/17a6d585603fade3838bc692548b619d97ded67e", - "reference": "17a6d585603fade3838bc692548b619d97ded67e", + "url": "https://api.github.com/repos/symfony/cache/zipball/e7e983596b744c4539f31e79b0350a6cf5878a20", + "reference": "e7e983596b744c4539f31e79b0350a6cf5878a20", "shasum": "" }, "require": { - "php": ">=7.2.5", - "psr/cache": "^1.0|^2.0", - "psr/log": "^1.1", - "symfony/cache-contracts": "^1.1.7|^2", - "symfony/polyfill-php80": "^1.15", - "symfony/service-contracts": "^1.1|^2", - "symfony/var-exporter": "^4.4|^5.0" + "php": ">=8.2", + "psr/cache": "^2.0|^3.0", + "psr/log": "^1.1|^2|^3", + "symfony/cache-contracts": "^2.5|^3", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/var-exporter": "^6.4|^7.0" }, "conflict": { - "doctrine/dbal": "<2.10", - "symfony/dependency-injection": "<4.4", - "symfony/http-kernel": "<4.4", - "symfony/var-dumper": "<4.4" + "doctrine/dbal": "<3.6", + "symfony/dependency-injection": "<6.4", + "symfony/http-kernel": "<6.4", + "symfony/var-dumper": "<6.4" }, "provide": { - "psr/cache-implementation": "1.0|2.0", - "psr/simple-cache-implementation": "1.0", - "symfony/cache-implementation": "1.0|2.0" + "psr/cache-implementation": "2.0|3.0", + "psr/simple-cache-implementation": "1.0|2.0|3.0", + "symfony/cache-implementation": "1.1|2.0|3.0" }, "require-dev": { "cache/integration-tests": "dev-master", - "doctrine/cache": "^1.6|^2.0", - "doctrine/dbal": "^2.10|^3.0", - "predis/predis": "^1.1", - "psr/simple-cache": "^1.0", - "symfony/config": "^4.4|^5.0", - "symfony/dependency-injection": "^4.4|^5.0", - "symfony/filesystem": "^4.4|^5.0", - "symfony/http-kernel": "^4.4|^5.0", - "symfony/messenger": "^4.4|^5.0", - "symfony/var-dumper": "^4.4|^5.0" + "doctrine/dbal": "^3.6|^4", + "predis/predis": "^1.1|^2.0", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "symfony/clock": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/filesystem": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" }, "type": "library", "autoload": { "psr-4": { "Symfony\\Component\\Cache\\": "" }, + "classmap": [ + "Traits/ValueWrapper.php" + ], "exclude-from-classmap": [ "/Tests/" ] @@ -5009,14 +8813,90 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Provides an extended PSR-6, PSR-16 (and tags) implementation", + "description": "Provides extended PSR-6, PSR-16 (and tags) implementations", "homepage": "https://symfony.com", "keywords": [ "caching", "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v5.2.9" + "source": "https://github.com/symfony/cache/tree/v7.2.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-07T08:08:50+00:00" + }, + { + "name": "symfony/cache-contracts", + "version": "v3.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache-contracts.git", + "reference": "15a4f8e5cd3bce9aeafc882b1acab39ec8de2c1b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/15a4f8e5cd3bce9aeafc882b1acab39ec8de2c1b", + "reference": "15a4f8e5cd3bce9aeafc882b1acab39ec8de2c1b", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/cache": "^3.0" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Cache\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to caching", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/cache-contracts/tree/v3.5.1" }, "funding": [ { @@ -5032,43 +8912,41 @@ "type": "tidelift" } ], - "time": "2021-05-17T19:35:40+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { - "name": "symfony/cache-contracts", - "version": "v2.4.0", + "name": "symfony/clock", + "version": "v7.2.0", "source": { "type": "git", - "url": "https://github.com/symfony/cache-contracts.git", - "reference": "c0446463729b89dd4fa62e9aeecc80287323615d" + "url": "https://github.com/symfony/clock.git", + "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/c0446463729b89dd4fa62e9aeecc80287323615d", - "reference": "c0446463729b89dd4fa62e9aeecc80287323615d", + "url": "https://api.github.com/repos/symfony/clock/zipball/b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", + "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", "shasum": "" }, "require": { - "php": ">=7.2.5", - "psr/cache": "^1.0|^2.0|^3.0" + "php": ">=8.2", + "psr/clock": "^1.0", + "symfony/polyfill-php83": "^1.28" }, - "suggest": { - "symfony/cache-implementation": "" + "provide": { + "psr/clock-implementation": "1.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-main": "2.4-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" - } - }, "autoload": { + "files": [ + "Resources/now.php" + ], "psr-4": { - "Symfony\\Contracts\\Cache\\": "" - } + "Symfony\\Component\\Clock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -5084,18 +8962,15 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Generic abstractions related to caching", + "description": "Decouples applications from the system clock", "homepage": "https://symfony.com", "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" + "clock", + "psr20", + "time" ], "support": { - "source": "https://github.com/symfony/cache-contracts/tree/v2.4.0" + "source": "https://github.com/symfony/clock/tree/v7.2.0" }, "funding": [ { @@ -5111,56 +8986,50 @@ "type": "tidelift" } ], - "time": "2021-03-23T23:28:01+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { "name": "symfony/console", - "version": "v5.3.7", + "version": "v7.2.1", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "8b1008344647462ae6ec57559da166c2bfa5e16a" + "reference": "fefcc18c0f5d0efe3ab3152f15857298868dc2c3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/8b1008344647462ae6ec57559da166c2bfa5e16a", - "reference": "8b1008344647462ae6ec57559da166c2bfa5e16a", + "url": "https://api.github.com/repos/symfony/console/zipball/fefcc18c0f5d0efe3ab3152f15857298868dc2c3", + "reference": "fefcc18c0f5d0efe3ab3152f15857298868dc2c3", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1", + "php": ">=8.2", "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php73": "^1.8", - "symfony/polyfill-php80": "^1.16", - "symfony/service-contracts": "^1.1|^2", - "symfony/string": "^5.1" + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^6.4|^7.0" }, "conflict": { - "psr/log": ">=3", - "symfony/dependency-injection": "<4.4", - "symfony/dotenv": "<5.1", - "symfony/event-dispatcher": "<4.4", - "symfony/lock": "<4.4", - "symfony/process": "<4.4" + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" }, "provide": { - "psr/log-implementation": "1.0|2.0" + "psr/log-implementation": "1.0|2.0|3.0" }, "require-dev": { - "psr/log": "^1|^2", - "symfony/config": "^4.4|^5.0", - "symfony/dependency-injection": "^4.4|^5.0", - "symfony/event-dispatcher": "^4.4|^5.0", - "symfony/lock": "^4.4|^5.0", - "symfony/process": "^4.4|^5.0", - "symfony/var-dumper": "^4.4|^5.0" - }, - "suggest": { - "psr/log": "For using the console logger", - "symfony/event-dispatcher": "", - "symfony/lock": "", - "symfony/process": "" + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -5189,12 +9058,12 @@ "homepage": "https://symfony.com", "keywords": [ "cli", - "command line", + "command-line", "console", "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.3.7" + "source": "https://github.com/symfony/console/tree/v7.2.1" }, "funding": [ { @@ -5210,24 +9079,24 @@ "type": "tidelift" } ], - "time": "2021-08-25T20:02:16+00:00" + "time": "2024-12-11T03:49:26+00:00" }, { "name": "symfony/css-selector", - "version": "v5.2.9", + "version": "v7.2.0", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "5d5f97809015102116208b976eb2edb44b689560" + "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/5d5f97809015102116208b976eb2edb44b689560", - "reference": "5d5f97809015102116208b976eb2edb44b689560", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/601a5ce9aaad7bf10797e3663faefce9e26c24e2", + "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2", "shasum": "" }, "require": { - "php": ">=7.2.5" + "php": ">=8.2" }, "type": "library", "autoload": { @@ -5259,7 +9128,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v5.2.9" + "source": "https://github.com/symfony/css-selector/tree/v7.2.0" }, "funding": [ { @@ -5275,33 +9144,33 @@ "type": "tidelift" } ], - "time": "2021-05-16T13:07:46+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v2.4.0", + "version": "v3.5.1", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "5f38c8804a9e97d23e0c8d63341088cd8a22d627" + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5f38c8804a9e97d23e0c8d63341088cd8a22d627", - "reference": "5f38c8804a9e97d23e0c8d63341088cd8a22d627", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=8.1" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "2.4-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" } }, "autoload": { @@ -5326,7 +9195,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v2.4.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" }, "funding": [ { @@ -5342,33 +9211,39 @@ "type": "tidelift" } ], - "time": "2021-03-23T23:28:01+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/error-handler", - "version": "v5.2.8", + "version": "v7.2.1", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "1416bc16317a8188aabde251afef7618bf4687ac" + "reference": "6150b89186573046167796fa5f3f76601d5145f8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/1416bc16317a8188aabde251afef7618bf4687ac", - "reference": "1416bc16317a8188aabde251afef7618bf4687ac", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/6150b89186573046167796fa5f3f76601d5145f8", + "reference": "6150b89186573046167796fa5f3f76601d5145f8", "shasum": "" }, "require": { - "php": ">=7.2.5", - "psr/log": "^1.0", - "symfony/polyfill-php80": "^1.15", - "symfony/var-dumper": "^4.4|^5.0" + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/var-dumper": "^6.4|^7.0" + }, + "conflict": { + "symfony/deprecation-contracts": "<2.5", + "symfony/http-kernel": "<6.4" }, "require-dev": { - "symfony/deprecation-contracts": "^2.1", - "symfony/http-kernel": "^4.4|^5.0", - "symfony/serializer": "^4.4|^5.0" + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0" }, + "bin": [ + "Resources/bin/patch-type-declarations" + ], "type": "library", "autoload": { "psr-4": { @@ -5395,7 +9270,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v5.2.8" + "source": "https://github.com/symfony/error-handler/tree/v7.2.1" }, "funding": [ { @@ -5411,48 +9286,43 @@ "type": "tidelift" } ], - "time": "2021-05-07T13:42:21+00:00" + "time": "2024-12-07T08:50:44+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v5.2.4", + "version": "v7.2.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "d08d6ec121a425897951900ab692b612a61d6240" + "reference": "910c5db85a5356d0fea57680defec4e99eb9c8c1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/d08d6ec121a425897951900ab692b612a61d6240", - "reference": "d08d6ec121a425897951900ab692b612a61d6240", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/910c5db85a5356d0fea57680defec4e99eb9c8c1", + "reference": "910c5db85a5356d0fea57680defec4e99eb9c8c1", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1", - "symfony/event-dispatcher-contracts": "^2", - "symfony/polyfill-php80": "^1.15" + "php": ">=8.2", + "symfony/event-dispatcher-contracts": "^2.5|^3" }, "conflict": { - "symfony/dependency-injection": "<4.4" + "symfony/dependency-injection": "<6.4", + "symfony/service-contracts": "<2.5" }, "provide": { "psr/event-dispatcher-implementation": "1.0", - "symfony/event-dispatcher-implementation": "2.0" + "symfony/event-dispatcher-implementation": "2.0|3.0" }, "require-dev": { - "psr/log": "~1.0", - "symfony/config": "^4.4|^5.0", - "symfony/dependency-injection": "^4.4|^5.0", - "symfony/error-handler": "^4.4|^5.0", - "symfony/expression-language": "^4.4|^5.0", - "symfony/http-foundation": "^4.4|^5.0", - "symfony/service-contracts": "^1.1|^2", - "symfony/stopwatch": "^4.4|^5.0" - }, - "suggest": { - "symfony/dependency-injection": "", - "symfony/http-kernel": "" + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/error-handler": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -5480,7 +9350,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v5.2.4" + "source": "https://github.com/symfony/event-dispatcher/tree/v7.2.0" }, "funding": [ { @@ -5496,37 +9366,34 @@ "type": "tidelift" } ], - "time": "2021-02-18T17:12:37+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v2.4.0", + "version": "v3.5.1", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "69fee1ad2332a7cbab3aca13591953da9cdb7a11" + "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/69fee1ad2332a7cbab3aca13591953da9cdb7a11", - "reference": "69fee1ad2332a7cbab3aca13591953da9cdb7a11", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/7642f5e970b672283b7823222ae8ef8bbc160b9f", + "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f", "shasum": "" }, "require": { - "php": ">=7.2.5", + "php": ">=8.1", "psr/event-dispatcher": "^1" }, - "suggest": { - "symfony/event-dispatcher-implementation": "" - }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "2.4-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" } }, "autoload": { @@ -5559,7 +9426,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v2.4.0" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.1" }, "funding": [ { @@ -5575,31 +9442,32 @@ "type": "tidelift" } ], - "time": "2021-03-23T23:28:01+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { - "name": "symfony/filesystem", - "version": "v5.3.4", + "name": "symfony/finder", + "version": "v7.2.2", "source": { "type": "git", - "url": "https://github.com/symfony/filesystem.git", - "reference": "343f4fe324383ca46792cae728a3b6e2f708fb32" + "url": "https://github.com/symfony/finder.git", + "reference": "87a71856f2f56e4100373e92529eed3171695cfb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/343f4fe324383ca46792cae728a3b6e2f708fb32", - "reference": "343f4fe324383ca46792cae728a3b6e2f708fb32", + "url": "https://api.github.com/repos/symfony/finder/zipball/87a71856f2f56e4100373e92529eed3171695cfb", + "reference": "87a71856f2f56e4100373e92529eed3171695cfb", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-php80": "^1.16" + "php": ">=8.2" + }, + "require-dev": { + "symfony/filesystem": "^6.4|^7.0" }, "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\Filesystem\\": "" + "Symfony\\Component\\Finder\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -5619,10 +9487,10 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Provides basic utilities for the filesystem", + "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v5.3.4" + "source": "https://github.com/symfony/finder/tree/v7.2.2" }, "funding": [ { @@ -5638,30 +9506,46 @@ "type": "tidelift" } ], - "time": "2021-07-21T12:40:44+00:00" + "time": "2024-12-30T19:00:17+00:00" }, { - "name": "symfony/finder", - "version": "v5.3.7", + "name": "symfony/http-foundation", + "version": "v7.2.2", "source": { "type": "git", - "url": "https://github.com/symfony/finder.git", - "reference": "a10000ada1e600d109a6c7632e9ac42e8bf2fb93" + "url": "https://github.com/symfony/http-foundation.git", + "reference": "62d1a43796ca3fea3f83a8470dfe63a4af3bc588" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/a10000ada1e600d109a6c7632e9ac42e8bf2fb93", - "reference": "a10000ada1e600d109a6c7632e9ac42e8bf2fb93", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/62d1a43796ca3fea3f83a8470dfe63a4af3bc588", + "reference": "62d1a43796ca3fea3f83a8470dfe63a4af3bc588", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/polyfill-php80": "^1.16" + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/polyfill-mbstring": "~1.1", + "symfony/polyfill-php83": "^1.27" + }, + "conflict": { + "doctrine/dbal": "<3.6", + "symfony/cache": "<6.4.12|>=7.0,<7.1.5" + }, + "require-dev": { + "doctrine/dbal": "^3.6|^4", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^6.4.12|^7.1.5", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/mime": "^6.4|^7.0", + "symfony/rate-limiter": "^6.4|^7.0" }, "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\Finder\\": "" + "Symfony\\Component\\HttpFoundation\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -5681,10 +9565,10 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Finds files and directories via an intuitive fluent interface", + "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v5.3.7" + "source": "https://github.com/symfony/http-foundation/tree/v7.2.2" }, "funding": [ { @@ -5700,119 +9584,82 @@ "type": "tidelift" } ], - "time": "2021-08-04T21:20:46+00:00" + "time": "2024-12-30T19:00:17+00:00" }, { - "name": "symfony/http-client-contracts", - "version": "v2.4.0", + "name": "symfony/http-kernel", + "version": "v7.2.2", "source": { "type": "git", - "url": "https://github.com/symfony/http-client-contracts.git", - "reference": "7e82f6084d7cae521a75ef2cb5c9457bbda785f4" + "url": "https://github.com/symfony/http-kernel.git", + "reference": "3c432966bd8c7ec7429663105f5a02d7e75b4306" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/7e82f6084d7cae521a75ef2cb5c9457bbda785f4", - "reference": "7e82f6084d7cae521a75ef2cb5c9457bbda785f4", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/3c432966bd8c7ec7429663105f5a02d7e75b4306", + "reference": "3c432966bd8c7ec7429663105f5a02d7e75b4306", "shasum": "" }, "require": { - "php": ">=7.2.5" - }, - "suggest": { - "symfony/http-client-implementation": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "2.4-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Contracts\\HttpClient\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Generic abstractions related to HTTP clients", - "homepage": "https://symfony.com", - "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" - ], - "support": { - "source": "https://github.com/symfony/http-client-contracts/tree/v2.4.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2021-04-11T23:07:08+00:00" - }, - { - "name": "symfony/http-foundation", - "version": "v5.2.8", - "source": { - "type": "git", - "url": "https://github.com/symfony/http-foundation.git", - "reference": "e8fbbab7c4a71592985019477532629cb2e142dc" + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/polyfill-ctype": "^1.8" }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/e8fbbab7c4a71592985019477532629cb2e142dc", - "reference": "e8fbbab7c4a71592985019477532629cb2e142dc", - "shasum": "" + "conflict": { + "symfony/browser-kit": "<6.4", + "symfony/cache": "<6.4", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/doctrine-bridge": "<6.4", + "symfony/form": "<6.4", + "symfony/http-client": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/mailer": "<6.4", + "symfony/messenger": "<6.4", + "symfony/translation": "<6.4", + "symfony/translation-contracts": "<2.5", + "symfony/twig-bridge": "<6.4", + "symfony/validator": "<6.4", + "symfony/var-dumper": "<6.4", + "twig/twig": "<3.12" }, - "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1", - "symfony/polyfill-mbstring": "~1.1", - "symfony/polyfill-php80": "^1.15" + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" }, "require-dev": { - "predis/predis": "~1.0", - "symfony/cache": "^4.4|^5.0", - "symfony/expression-language": "^4.4|^5.0", - "symfony/mime": "^4.4|^5.0" - }, - "suggest": { - "symfony/mime": "To use the file extension guesser" + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^6.4|^7.0", + "symfony/clock": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/css-selector": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/dom-crawler": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/http-client-contracts": "^2.5|^3", + "symfony/process": "^6.4|^7.0", + "symfony/property-access": "^7.1", + "symfony/routing": "^6.4|^7.0", + "symfony/serializer": "^7.1", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/translation": "^6.4|^7.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^6.4|^7.0", + "symfony/validator": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0", + "symfony/var-exporter": "^6.4|^7.0", + "twig/twig": "^3.12" }, "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\HttpFoundation\\": "" + "Symfony\\Component\\HttpKernel\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -5832,10 +9679,10 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Defines an object-oriented layer for the HTTP specification", + "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v5.2.8" + "source": "https://github.com/symfony/http-kernel/tree/v7.2.2" }, "funding": [ { @@ -5851,80 +9698,48 @@ "type": "tidelift" } ], - "time": "2021-05-07T13:41:16+00:00" + "time": "2024-12-31T14:59:40+00:00" }, { - "name": "symfony/http-kernel", - "version": "v5.2.9", + "name": "symfony/mailer", + "version": "v7.2.0", "source": { "type": "git", - "url": "https://github.com/symfony/http-kernel.git", - "reference": "eb540ef6870dbf33c92e372cfb869ebf9649e6cb" + "url": "https://github.com/symfony/mailer.git", + "reference": "e4d358702fb66e4c8a2af08e90e7271a62de39cc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/eb540ef6870dbf33c92e372cfb869ebf9649e6cb", - "reference": "eb540ef6870dbf33c92e372cfb869ebf9649e6cb", + "url": "https://api.github.com/repos/symfony/mailer/zipball/e4d358702fb66e4c8a2af08e90e7271a62de39cc", + "reference": "e4d358702fb66e4c8a2af08e90e7271a62de39cc", "shasum": "" }, "require": { - "php": ">=7.2.5", - "psr/log": "~1.0", - "symfony/deprecation-contracts": "^2.1", - "symfony/error-handler": "^4.4|^5.0", - "symfony/event-dispatcher": "^5.0", - "symfony/http-client-contracts": "^1.1|^2", - "symfony/http-foundation": "^4.4|^5.0", - "symfony/polyfill-ctype": "^1.8", - "symfony/polyfill-php73": "^1.9", - "symfony/polyfill-php80": "^1.15" + "egulias/email-validator": "^2.1.10|^3|^4", + "php": ">=8.2", + "psr/event-dispatcher": "^1", + "psr/log": "^1|^2|^3", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/mime": "^7.2", + "symfony/service-contracts": "^2.5|^3" }, "conflict": { - "symfony/browser-kit": "<4.4", - "symfony/cache": "<5.0", - "symfony/config": "<5.0", - "symfony/console": "<4.4", - "symfony/dependency-injection": "<5.1.8", - "symfony/doctrine-bridge": "<5.0", - "symfony/form": "<5.0", - "symfony/http-client": "<5.0", - "symfony/mailer": "<5.0", - "symfony/messenger": "<5.0", - "symfony/translation": "<5.0", - "symfony/twig-bridge": "<5.0", - "symfony/validator": "<5.0", - "twig/twig": "<2.13" - }, - "provide": { - "psr/log-implementation": "1.0" + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<6.4", + "symfony/messenger": "<6.4", + "symfony/mime": "<6.4", + "symfony/twig-bridge": "<6.4" }, "require-dev": { - "psr/cache": "^1.0|^2.0|^3.0", - "symfony/browser-kit": "^4.4|^5.0", - "symfony/config": "^5.0", - "symfony/console": "^4.4|^5.0", - "symfony/css-selector": "^4.4|^5.0", - "symfony/dependency-injection": "^5.1.8", - "symfony/dom-crawler": "^4.4|^5.0", - "symfony/expression-language": "^4.4|^5.0", - "symfony/finder": "^4.4|^5.0", - "symfony/process": "^4.4|^5.0", - "symfony/routing": "^4.4|^5.0", - "symfony/stopwatch": "^4.4|^5.0", - "symfony/translation": "^4.4|^5.0", - "symfony/translation-contracts": "^1.1|^2", - "twig/twig": "^2.13|^3.0.4" - }, - "suggest": { - "symfony/browser-kit": "", - "symfony/config": "", - "symfony/console": "", - "symfony/dependency-injection": "" + "symfony/console": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/twig-bridge": "^6.4|^7.0" }, "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\HttpKernel\\": "" + "Symfony\\Component\\Mailer\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -5944,10 +9759,10 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Provides a structured process for converting a Request into a Response", + "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v5.2.9" + "source": "https://github.com/symfony/mailer/tree/v7.2.0" }, "funding": [ { @@ -5963,42 +9778,43 @@ "type": "tidelift" } ], - "time": "2021-05-19T12:23:45+00:00" + "time": "2024-11-25T15:21:05+00:00" }, { "name": "symfony/mime", - "version": "v5.2.9", + "version": "v7.2.1", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "64258e870f8cc75c3dae986201ea2df58c210b52" + "reference": "7f9617fcf15cb61be30f8b252695ed5e2bfac283" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/64258e870f8cc75c3dae986201ea2df58c210b52", - "reference": "64258e870f8cc75c3dae986201ea2df58c210b52", + "url": "https://api.github.com/repos/symfony/mime/zipball/7f9617fcf15cb61be30f8b252695ed5e2bfac283", + "reference": "7f9617fcf15cb61be30f8b252695ed5e2bfac283", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1", + "php": ">=8.2", "symfony/polyfill-intl-idn": "^1.10", - "symfony/polyfill-mbstring": "^1.0", - "symfony/polyfill-php80": "^1.15" + "symfony/polyfill-mbstring": "^1.0" }, "conflict": { "egulias/email-validator": "~3.0.0", "phpdocumentor/reflection-docblock": "<3.2.2", "phpdocumentor/type-resolver": "<1.4.0", - "symfony/mailer": "<4.4" + "symfony/mailer": "<6.4", + "symfony/serializer": "<6.4.3|>7.0,<7.0.3" }, "require-dev": { - "egulias/email-validator": "^2.1.10|^3.1", + "egulias/email-validator": "^2.1.10|^3.1|^4", + "league/html-to-markdown": "^5.0", "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", - "symfony/dependency-injection": "^4.4|^5.0", - "symfony/property-access": "^4.4|^5.1", - "symfony/property-info": "^4.4|^5.1", - "symfony/serializer": "^5.2" + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/property-access": "^6.4|^7.0", + "symfony/property-info": "^6.4|^7.0", + "symfony/serializer": "^6.4.3|^7.0.3" }, "type": "library", "autoload": { @@ -6030,7 +9846,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v5.2.9" + "source": "https://github.com/symfony/mime/tree/v7.2.1" }, "funding": [ { @@ -6046,45 +9862,45 @@ "type": "tidelift" } ], - "time": "2021-05-16T13:07:46+00:00" + "time": "2024-12-07T08:50:44+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.23.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "46cd95797e9df938fdd2b03693b5fca5e64b01ce" + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/46cd95797e9df938fdd2b03693b5fca5e64b01ce", - "reference": "46cd95797e9df938fdd2b03693b5fca5e64b01ce", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" }, "suggest": { "ext-ctype": "For best performance" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.23-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - }, "files": [ "bootstrap.php" - ] + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -6109,87 +9925,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.23.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2021-02-19T12:13:01+00:00" - }, - { - "name": "symfony/polyfill-iconv", - "version": "v1.22.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-iconv.git", - "reference": "06fb361659649bcfd6a208a0f1fcaf4e827ad342" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/06fb361659649bcfd6a208a0f1fcaf4e827ad342", - "reference": "06fb361659649bcfd6a208a0f1fcaf4e827ad342", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "suggest": { - "ext-iconv": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.22-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Iconv\\": "" - }, - "files": [ - "bootstrap.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for the Iconv extension", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "iconv", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-iconv/tree/v1.22.1" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" }, "funding": [ { @@ -6205,45 +9941,42 @@ "type": "tidelift" } ], - "time": "2021-01-22T09:19:47+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.23.1", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "16880ba9c5ebe3642d1995ab866db29270b36535" + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/16880ba9c5ebe3642d1995ab866db29270b36535", - "reference": "16880ba9c5ebe3642d1995ab866db29270b36535", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "suggest": { "ext-intl": "For best performance" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.23-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Intl\\Grapheme\\": "" - }, "files": [ "bootstrap.php" - ] + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -6270,7 +10003,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.23.1" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" }, "funding": [ { @@ -6286,47 +10019,43 @@ "type": "tidelift" } ], - "time": "2021-05-27T12:26:48+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.22.1", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "2d63434d922daf7da8dd863e7907e67ee3031483" + "reference": "c36586dcf89a12315939e00ec9b4474adcb1d773" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/2d63434d922daf7da8dd863e7907e67ee3031483", - "reference": "2d63434d922daf7da8dd863e7907e67ee3031483", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/c36586dcf89a12315939e00ec9b4474adcb1d773", + "reference": "c36586dcf89a12315939e00ec9b4474adcb1d773", "shasum": "" }, "require": { - "php": ">=7.1", - "symfony/polyfill-intl-normalizer": "^1.10", - "symfony/polyfill-php72": "^1.10" + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" }, "suggest": { "ext-intl": "For best performance" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.22-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Intl\\Idn\\": "" - }, "files": [ "bootstrap.php" - ] + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -6357,7 +10086,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.22.1" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.31.0" }, "funding": [ { @@ -6373,45 +10102,42 @@ "type": "tidelift" } ], - "time": "2021-01-22T09:19:47+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.23.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8" + "reference": "3833d7255cc303546435cb650316bff708a1c75c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8590a5f561694770bdcd3f9b5c69dde6945028e8", - "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "suggest": { "ext-intl": "For best performance" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.23-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Intl\\Normalizer\\": "" - }, "files": [ "bootstrap.php" ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, "classmap": [ "Resources/stubs" ] @@ -6441,7 +10167,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.23.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" }, "funding": [ { @@ -6457,45 +10183,45 @@ "type": "tidelift" } ], - "time": "2021-02-19T12:13:01+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.23.1", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "9174a3d80210dca8daa7f31fec659150bbeabfc6" + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9174a3d80210dca8daa7f31fec659150bbeabfc6", - "reference": "9174a3d80210dca8daa7f31fec659150bbeabfc6", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" }, "suggest": { "ext-mbstring": "For best performance" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.23-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - }, "files": [ "bootstrap.php" - ] + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -6521,7 +10247,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.23.1" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" }, "funding": [ { @@ -6537,41 +10263,41 @@ "type": "tidelift" } ], - "time": "2021-05-27T12:26:48+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { - "name": "symfony/polyfill-php72", - "version": "v1.22.1", + "name": "symfony/polyfill-php80", + "version": "v1.31.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "cc6e6f9b39fe8075b3dabfbaf5b5f645ae1340c9" + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/cc6e6f9b39fe8075b3dabfbaf5b5f645ae1340c9", - "reference": "cc6e6f9b39fe8075b3dabfbaf5b5f645ae1340c9", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.22-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php72\\": "" - }, "files": [ "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" ] }, "notification-url": "https://packagist.org/downloads/", @@ -6579,6 +10305,10 @@ "MIT" ], "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, { "name": "Nicolas Grekas", "email": "p@tchwork.com" @@ -6588,7 +10318,7 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ "compatibility", @@ -6597,7 +10327,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php72/tree/v1.22.1" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" }, "funding": [ { @@ -6613,42 +10343,39 @@ "type": "tidelift" } ], - "time": "2021-01-07T16:49:33+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { - "name": "symfony/polyfill-php73", - "version": "v1.23.0", + "name": "symfony/polyfill-php83", + "version": "v1.31.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "fba8933c384d6476ab14fb7b8526e5287ca7e010" + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/fba8933c384d6476ab14fb7b8526e5287ca7e010", - "reference": "fba8933c384d6476ab14fb7b8526e5287ca7e010", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491", + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.23-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php73\\": "" - }, "files": [ "bootstrap.php" ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, "classmap": [ "Resources/stubs" ] @@ -6667,7 +10394,7 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ "compatibility", @@ -6676,7 +10403,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.23.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.31.0" }, "funding": [ { @@ -6692,45 +10419,45 @@ "type": "tidelift" } ], - "time": "2021-02-19T12:13:01+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { - "name": "symfony/polyfill-php80", - "version": "v1.23.1", + "name": "symfony/polyfill-uuid", + "version": "v1.31.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "1100343ed1a92e3a38f9ae122fc0eb21602547be" + "url": "https://github.com/symfony/polyfill-uuid.git", + "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/1100343ed1a92e3a38f9ae122fc0eb21602547be", - "reference": "1100343ed1a92e3a38f9ae122fc0eb21602547be", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/21533be36c24be3f4b1669c4725c7d1d2bab4ae2", + "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" + }, + "provide": { + "ext-uuid": "*" + }, + "suggest": { + "ext-uuid": "For best performance" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.23-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php80\\": "" - }, "files": [ "bootstrap.php" ], - "classmap": [ - "Resources/stubs" - ] + "psr-4": { + "Symfony\\Polyfill\\Uuid\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -6738,28 +10465,24 @@ ], "authors": [ { - "name": "Ion Bazan", - "email": "ion.bazan@gmail.com" - }, - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "description": "Symfony polyfill for uuid functions", "homepage": "https://symfony.com", "keywords": [ "compatibility", "polyfill", "portable", - "shim" + "uuid" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.23.1" + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.31.0" }, "funding": [ { @@ -6775,25 +10498,24 @@ "type": "tidelift" } ], - "time": "2021-07-28T13:41:28+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/process", - "version": "v5.3.7", + "version": "v7.2.0", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "38f26c7d6ed535217ea393e05634cb0b244a1967" + "reference": "d34b22ba9390ec19d2dd966c40aa9e8462f27a7e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/38f26c7d6ed535217ea393e05634cb0b244a1967", - "reference": "38f26c7d6ed535217ea393e05634cb0b244a1967", + "url": "https://api.github.com/repos/symfony/process/zipball/d34b22ba9390ec19d2dd966c40aa9e8462f27a7e", + "reference": "d34b22ba9390ec19d2dd966c40aa9e8462f27a7e", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/polyfill-php80": "^1.16" + "php": ">=8.2" }, "type": "library", "autoload": { @@ -6821,95 +10543,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v5.3.7" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2021-08-04T21:20:46+00:00" - }, - { - "name": "symfony/psr-http-message-bridge", - "version": "v2.1.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/psr-http-message-bridge.git", - "reference": "81db2d4ae86e9f0049828d9343a72b9523884e5d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/81db2d4ae86e9f0049828d9343a72b9523884e5d", - "reference": "81db2d4ae86e9f0049828d9343a72b9523884e5d", - "shasum": "" - }, - "require": { - "php": ">=7.1", - "psr/http-message": "^1.0", - "symfony/http-foundation": "^4.4 || ^5.0" - }, - "require-dev": { - "nyholm/psr7": "^1.1", - "psr/log": "^1.1", - "symfony/browser-kit": "^4.4 || ^5.0", - "symfony/config": "^4.4 || ^5.0", - "symfony/event-dispatcher": "^4.4 || ^5.0", - "symfony/framework-bundle": "^4.4 || ^5.0", - "symfony/http-kernel": "^4.4 || ^5.0", - "symfony/phpunit-bridge": "^4.4.19 || ^5.2" - }, - "suggest": { - "nyholm/psr7": "For a super lightweight PSR-7/17 implementation" - }, - "type": "symfony-bridge", - "extra": { - "branch-alias": { - "dev-main": "2.1-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Bridge\\PsrHttpMessage\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "http://symfony.com/contributors" - } - ], - "description": "PSR HTTP message bridge", - "homepage": "http://symfony.com", - "keywords": [ - "http", - "http-message", - "psr-17", - "psr-7" - ], - "support": { - "issues": "https://github.com/symfony/psr-http-message-bridge/issues", - "source": "https://github.com/symfony/psr-http-message-bridge/tree/v2.1.0" + "source": "https://github.com/symfony/process/tree/v7.2.0" }, "funding": [ { @@ -6925,46 +10559,38 @@ "type": "tidelift" } ], - "time": "2021-02-17T10:35:25+00:00" + "time": "2024-11-06T14:24:19+00:00" }, { "name": "symfony/routing", - "version": "v5.2.9", + "version": "v7.2.0", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "4a7b2bf5e1221be1902b6853743a9bb317f6925e" + "reference": "e10a2450fa957af6c448b9b93c9010a4e4c0725e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/4a7b2bf5e1221be1902b6853743a9bb317f6925e", - "reference": "4a7b2bf5e1221be1902b6853743a9bb317f6925e", + "url": "https://api.github.com/repos/symfony/routing/zipball/e10a2450fa957af6c448b9b93c9010a4e4c0725e", + "reference": "e10a2450fa957af6c448b9b93c9010a4e4c0725e", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1", - "symfony/polyfill-php80": "^1.15" + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" }, "conflict": { - "symfony/config": "<5.0", - "symfony/dependency-injection": "<4.4", - "symfony/yaml": "<4.4" + "symfony/config": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/yaml": "<6.4" }, "require-dev": { - "doctrine/annotations": "^1.10.4", - "psr/log": "~1.0", - "symfony/config": "^5.0", - "symfony/dependency-injection": "^4.4|^5.0", - "symfony/expression-language": "^4.4|^5.0", - "symfony/http-foundation": "^4.4|^5.0", - "symfony/yaml": "^4.4|^5.0" - }, - "suggest": { - "symfony/config": "For using the all-in-one router or any loader", - "symfony/expression-language": "For using expression matching", - "symfony/http-foundation": "For using a Symfony Request object", - "symfony/yaml": "For using the YAML loader" + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/yaml": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -6998,7 +10624,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v5.2.9" + "source": "https://github.com/symfony/routing/tree/v7.2.0" }, "funding": [ { @@ -7014,43 +10640,47 @@ "type": "tidelift" } ], - "time": "2021-05-16T13:07:46+00:00" + "time": "2024-11-25T11:08:51+00:00" }, { "name": "symfony/service-contracts", - "version": "v2.4.0", + "version": "v3.5.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb" + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb", - "reference": "f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", "shasum": "" }, "require": { - "php": ">=7.2.5", - "psr/container": "^1.1" + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" }, - "suggest": { - "symfony/service-implementation": "" + "conflict": { + "ext-psr": "<1.1|>=2" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "2.4-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" } }, "autoload": { "psr-4": { "Symfony\\Contracts\\Service\\": "" - } + }, + "exclude-from-classmap": [ + "/Test/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -7077,7 +10707,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v2.4.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.5.1" }, "funding": [ { @@ -7093,44 +10723,48 @@ "type": "tidelift" } ], - "time": "2021-04-01T10:43:52+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/string", - "version": "v5.3.7", + "version": "v7.2.0", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "8d224396e28d30f81969f083a58763b8b9ceb0a5" + "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/8d224396e28d30f81969f083a58763b8b9ceb0a5", - "reference": "8d224396e28d30f81969f083a58763b8b9ceb0a5", + "url": "https://api.github.com/repos/symfony/string/zipball/446e0d146f991dde3e73f45f2c97a9faad773c82", + "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82", "shasum": "" }, "require": { - "php": ">=7.2.5", + "php": ">=8.2", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-intl-grapheme": "~1.0", "symfony/polyfill-intl-normalizer": "~1.0", - "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php80": "~1.15" + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" }, "require-dev": { - "symfony/error-handler": "^4.4|^5.0", - "symfony/http-client": "^4.4|^5.0", - "symfony/translation-contracts": "^1.1|^2", - "symfony/var-exporter": "^4.4|^5.0" + "symfony/emoji": "^7.1", + "symfony/error-handler": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.0" }, "type": "library", "autoload": { - "psr-4": { - "Symfony\\Component\\String\\": "" - }, "files": [ "Resources/functions.php" ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, "exclude-from-classmap": [ "/Tests/" ] @@ -7160,7 +10794,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.3.7" + "source": "https://github.com/symfony/string/tree/v7.2.0" }, "funding": [ { @@ -7176,53 +10810,55 @@ "type": "tidelift" } ], - "time": "2021-08-26T08:00:08+00:00" + "time": "2024-11-13T13:31:26+00:00" }, { "name": "symfony/translation", - "version": "v5.2.9", + "version": "v7.2.2", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "61af68dba333e2d376a325a29c2a3f2a605b4876" + "reference": "e2674a30132b7cc4d74540d6c2573aa363f05923" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/61af68dba333e2d376a325a29c2a3f2a605b4876", - "reference": "61af68dba333e2d376a325a29c2a3f2a605b4876", + "url": "https://api.github.com/repos/symfony/translation/zipball/e2674a30132b7cc4d74540d6c2573aa363f05923", + "reference": "e2674a30132b7cc4d74540d6c2573aa363f05923", "shasum": "" }, "require": { - "php": ">=7.2.5", + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php80": "^1.15", - "symfony/translation-contracts": "^2.3" + "symfony/translation-contracts": "^2.5|^3.0" }, "conflict": { - "symfony/config": "<4.4", - "symfony/dependency-injection": "<5.0", - "symfony/http-kernel": "<5.0", - "symfony/twig-bundle": "<5.0", - "symfony/yaml": "<4.4" + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<6.4", + "symfony/service-contracts": "<2.5", + "symfony/twig-bundle": "<6.4", + "symfony/yaml": "<6.4" }, "provide": { - "symfony/translation-implementation": "2.3" + "symfony/translation-implementation": "2.3|3.0" }, "require-dev": { - "psr/log": "~1.0", - "symfony/config": "^4.4|^5.0", - "symfony/console": "^4.4|^5.0", - "symfony/dependency-injection": "^5.0", - "symfony/finder": "^4.4|^5.0", - "symfony/http-kernel": "^5.0", - "symfony/intl": "^4.4|^5.0", - "symfony/service-contracts": "^1.1.2|^2", - "symfony/yaml": "^4.4|^5.0" - }, - "suggest": { - "psr/log-implementation": "To use logging capability in translator", - "symfony/config": "", - "symfony/yaml": "" + "nikic/php-parser": "^4.18|^5.0", + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/http-client-contracts": "^2.5|^3.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/routing": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -7253,7 +10889,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v5.2.9" + "source": "https://github.com/symfony/translation/tree/v7.2.2" }, "funding": [ { @@ -7269,42 +10905,42 @@ "type": "tidelift" } ], - "time": "2021-05-16T13:07:46+00:00" + "time": "2024-12-07T08:18:10+00:00" }, { "name": "symfony/translation-contracts", - "version": "v2.4.0", + "version": "v3.5.1", "source": { "type": "git", "url": "https://github.com/symfony/translation-contracts.git", - "reference": "95c812666f3e91db75385749fe219c5e494c7f95" + "reference": "4667ff3bd513750603a09c8dedbea942487fb07c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/95c812666f3e91db75385749fe219c5e494c7f95", - "reference": "95c812666f3e91db75385749fe219c5e494c7f95", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/4667ff3bd513750603a09c8dedbea942487fb07c", + "reference": "4667ff3bd513750603a09c8dedbea942487fb07c", "shasum": "" }, "require": { - "php": ">=7.2.5" - }, - "suggest": { - "symfony/translation-implementation": "" + "php": ">=8.1" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "2.4-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" } }, "autoload": { "psr-4": { "Symfony\\Contracts\\Translation\\": "" - } + }, + "exclude-from-classmap": [ + "/Test/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -7331,7 +10967,81 @@ "standards" ], "support": { - "source": "https://github.com/symfony/translation-contracts/tree/v2.4.0" + "source": "https://github.com/symfony/translation-contracts/tree/v3.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:20:29+00:00" + }, + { + "name": "symfony/uid", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/uid.git", + "reference": "2d294d0c48df244c71c105a169d0190bfb080426" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/uid/zipball/2d294d0c48df244c71c105a169d0190bfb080426", + "reference": "2d294d0c48df244c71c105a169d0190bfb080426", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-uuid": "^1.15" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Uid\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to generate and represent UIDs", + "homepage": "https://symfony.com", + "keywords": [ + "UID", + "ulid", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/uid/tree/v7.2.0" }, "funding": [ { @@ -7347,41 +11057,36 @@ "type": "tidelift" } ], - "time": "2021-03-23T23:28:01+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { "name": "symfony/var-dumper", - "version": "v5.2.8", + "version": "v7.2.0", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "d693200a73fae179d27f8f1b16b4faf3e8569eba" + "reference": "c6a22929407dec8765d6e2b6ff85b800b245879c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/d693200a73fae179d27f8f1b16b4faf3e8569eba", - "reference": "d693200a73fae179d27f8f1b16b4faf3e8569eba", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/c6a22929407dec8765d6e2b6ff85b800b245879c", + "reference": "c6a22929407dec8765d6e2b6ff85b800b245879c", "shasum": "" }, - "require": { - "php": ">=7.2.5", - "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php80": "^1.15" + "require": { + "php": ">=8.2", + "symfony/polyfill-mbstring": "~1.0" }, "conflict": { - "phpunit/phpunit": "<5.4.3", - "symfony/console": "<4.4" + "symfony/console": "<6.4" }, "require-dev": { "ext-iconv": "*", - "symfony/console": "^4.4|^5.0", - "symfony/process": "^4.4|^5.0", - "twig/twig": "^2.13|^3.0.4" - }, - "suggest": { - "ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).", - "ext-intl": "To show region name in time zone dump", - "symfony/console": "To use the ServerDumpCommand and/or the bin/var-dump-server script" + "symfony/console": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/uid": "^6.4|^7.0", + "twig/twig": "^3.12" }, "bin": [ "Resources/bin/var-dump-server" @@ -7419,7 +11124,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v5.2.8" + "source": "https://github.com/symfony/var-dumper/tree/v7.2.0" }, "funding": [ { @@ -7435,28 +11140,29 @@ "type": "tidelift" } ], - "time": "2021-05-07T13:42:21+00:00" + "time": "2024-11-08T15:48:14+00:00" }, { "name": "symfony/var-exporter", - "version": "v5.2.8", + "version": "v7.2.0", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "d26db2d2b2d7eb2c1adb8545179f8803998b8237" + "reference": "1a6a89f95a46af0f142874c9d650a6358d13070d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/d26db2d2b2d7eb2c1adb8545179f8803998b8237", - "reference": "d26db2d2b2d7eb2c1adb8545179f8803998b8237", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/1a6a89f95a46af0f142874c9d650a6358d13070d", + "reference": "1a6a89f95a46af0f142874c9d650a6358d13070d", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/polyfill-php80": "^1.15" + "php": ">=8.2" }, "require-dev": { - "symfony/var-dumper": "^4.4.9|^5.0.9" + "symfony/property-access": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -7489,10 +11195,12 @@ "export", "hydrate", "instantiate", + "lazy-loading", + "proxy", "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v5.3.0-BETA3" + "source": "https://github.com/symfony/var-exporter/tree/v7.2.0" }, "funding": [ { @@ -7508,50 +11216,50 @@ "type": "tidelift" } ], - "time": "2021-05-07T13:42:21+00:00" + "time": "2024-10-18T07:58:17+00:00" }, { "name": "thecodingmachine/safe", - "version": "v1.3.3", + "version": "v2.5.0", "source": { "type": "git", "url": "https://github.com/thecodingmachine/safe.git", - "reference": "a8ab0876305a4cdaef31b2350fcb9811b5608dbc" + "reference": "3115ecd6b4391662b4931daac4eba6b07a2ac1f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/a8ab0876305a4cdaef31b2350fcb9811b5608dbc", - "reference": "a8ab0876305a4cdaef31b2350fcb9811b5608dbc", + "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/3115ecd6b4391662b4931daac4eba6b07a2ac1f0", + "reference": "3115ecd6b4391662b4931daac4eba6b07a2ac1f0", "shasum": "" }, "require": { - "php": ">=7.2" + "php": "^8.0" }, "require-dev": { - "phpstan/phpstan": "^0.12", + "phpstan/phpstan": "^1.5", + "phpunit/phpunit": "^9.5", "squizlabs/php_codesniffer": "^3.2", - "thecodingmachine/phpstan-strict-rules": "^0.12" + "thecodingmachine/phpstan-strict-rules": "^1.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "0.1-dev" + "dev-master": "2.2.x-dev" } }, "autoload": { - "psr-4": { - "Safe\\": [ - "lib/", - "deprecated/", - "generated/" - ] - }, "files": [ "deprecated/apc.php", + "deprecated/array.php", + "deprecated/datetime.php", "deprecated/libevent.php", + "deprecated/misc.php", + "deprecated/password.php", "deprecated/mssql.php", "deprecated/stats.php", + "deprecated/strings.php", "lib/special_cases.php", + "deprecated/mysqli.php", "generated/apache.php", "generated/apcu.php", "generated/array.php", @@ -7572,6 +11280,7 @@ "generated/fpm.php", "generated/ftp.php", "generated/funchand.php", + "generated/gettext.php", "generated/gmp.php", "generated/gnupg.php", "generated/hash.php", @@ -7581,7 +11290,6 @@ "generated/image.php", "generated/imap.php", "generated/info.php", - "generated/ingres-ii.php", "generated/inotify.php", "generated/json.php", "generated/ldap.php", @@ -7590,20 +11298,14 @@ "generated/mailparse.php", "generated/mbstring.php", "generated/misc.php", - "generated/msql.php", "generated/mysql.php", - "generated/mysqli.php", - "generated/mysqlndMs.php", - "generated/mysqlndQc.php", "generated/network.php", "generated/oci8.php", "generated/opcache.php", "generated/openssl.php", "generated/outcontrol.php", - "generated/password.php", "generated/pcntl.php", "generated/pcre.php", - "generated/pdf.php", "generated/pgsql.php", "generated/posix.php", "generated/ps.php", @@ -7614,7 +11316,6 @@ "generated/sem.php", "generated/session.php", "generated/shmop.php", - "generated/simplexml.php", "generated/sockets.php", "generated/sodium.php", "generated/solr.php", @@ -7636,6 +11337,13 @@ "generated/yaz.php", "generated/zip.php", "generated/zlib.php" + ], + "classmap": [ + "lib/DateTime.php", + "lib/DateTimeImmutable.php", + "lib/Exceptions/", + "deprecated/Exceptions/", + "generated/Exceptions/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -7645,37 +11353,39 @@ "description": "PHP core functions that throw exceptions instead of returning FALSE on error", "support": { "issues": "https://github.com/thecodingmachine/safe/issues", - "source": "https://github.com/thecodingmachine/safe/tree/v1.3.3" + "source": "https://github.com/thecodingmachine/safe/tree/v2.5.0" }, - "time": "2020-10-28T17:51:34+00:00" + "time": "2023-04-05T11:54:14+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", - "version": "2.2.3", + "version": "v2.3.0", "source": { "type": "git", "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", - "reference": "b43b05cf43c1b6d849478965062b6ef73e223bb5" + "reference": "0d72ac1c00084279c1816675284073c5a337c20d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/b43b05cf43c1b6d849478965062b6ef73e223bb5", - "reference": "b43b05cf43c1b6d849478965062b6ef73e223bb5", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/0d72ac1c00084279c1816675284073c5a337c20d", + "reference": "0d72ac1c00084279c1816675284073c5a337c20d", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", - "php": "^5.5 || ^7.0 || ^8.0", - "symfony/css-selector": "^2.7 || ^3.0 || ^4.0 || ^5.0" + "php": "^7.4 || ^8.0", + "symfony/css-selector": "^5.4 || ^6.0 || ^7.0" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0 || ^7.5" + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^8.5.21 || ^9.5.10" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.2.x-dev" + "dev-master": "2.x-dev" } }, "autoload": { @@ -7698,45 +11408,49 @@ "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", "support": { "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues", - "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/2.2.3" + "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.3.0" }, - "time": "2020-07-13T06:12:54+00:00" + "time": "2024-12-21T16:25:41+00:00" }, { "name": "vlucas/phpdotenv", - "version": "v5.3.0", + "version": "v5.6.1", "source": { "type": "git", "url": "https://github.com/vlucas/phpdotenv.git", - "reference": "b3eac5c7ac896e52deab4a99068e3f4ab12d9e56" + "reference": "a59a13791077fe3d44f90e7133eb68e7d22eaff2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/b3eac5c7ac896e52deab4a99068e3f4ab12d9e56", - "reference": "b3eac5c7ac896e52deab4a99068e3f4ab12d9e56", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/a59a13791077fe3d44f90e7133eb68e7d22eaff2", + "reference": "a59a13791077fe3d44f90e7133eb68e7d22eaff2", "shasum": "" }, "require": { "ext-pcre": "*", - "graham-campbell/result-type": "^1.0.1", - "php": "^7.1.3 || ^8.0", - "phpoption/phpoption": "^1.7.4", - "symfony/polyfill-ctype": "^1.17", - "symfony/polyfill-mbstring": "^1.17", - "symfony/polyfill-php80": "^1.17" + "graham-campbell/result-type": "^1.1.3", + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.3", + "symfony/polyfill-ctype": "^1.24", + "symfony/polyfill-mbstring": "^1.24", + "symfony/polyfill-php80": "^1.24" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.4.1", + "bamarni/composer-bin-plugin": "^1.8.2", "ext-filter": "*", - "phpunit/phpunit": "^7.5.20 || ^8.5.14 || ^9.5.1" + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" }, "suggest": { "ext-filter": "Required to use the boolean validator." }, "type": "library", "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, "branch-alias": { - "dev-master": "5.3-dev" + "dev-master": "5.6-dev" } }, "autoload": { @@ -7751,13 +11465,13 @@ "authors": [ { "name": "Graham Campbell", - "email": "graham@alt-three.com", - "homepage": "https://gjcampbell.co.uk/" + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" }, { "name": "Vance Lucas", "email": "vance@vancelucas.com", - "homepage": "https://vancelucas.com/" + "homepage": "https://github.com/vlucas" } ], "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", @@ -7768,7 +11482,7 @@ ], "support": { "issues": "https://github.com/vlucas/phpdotenv/issues", - "source": "https://github.com/vlucas/phpdotenv/tree/v5.3.0" + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.1" }, "funding": [ { @@ -7780,20 +11494,20 @@ "type": "tidelift" } ], - "time": "2021-01-20T15:23:13+00:00" + "time": "2024-07-20T21:52:34+00:00" }, { "name": "voku/portable-ascii", - "version": "1.5.6", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/voku/portable-ascii.git", - "reference": "80953678b19901e5165c56752d087fc11526017c" + "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/voku/portable-ascii/zipball/80953678b19901e5165c56752d087fc11526017c", - "reference": "80953678b19901e5165c56752d087fc11526017c", + "url": "https://api.github.com/repos/voku/portable-ascii/zipball/b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", + "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", "shasum": "" }, "require": { @@ -7818,7 +11532,7 @@ "authors": [ { "name": "Lars Moelleken", - "homepage": "http://www.moelleken.org/" + "homepage": "https://www.moelleken.org/" } ], "description": "Portable ASCII library - performance optimized (ascii) string functions for php.", @@ -7830,7 +11544,7 @@ ], "support": { "issues": "https://github.com/voku/portable-ascii/issues", - "source": "https://github.com/voku/portable-ascii/tree/1.5.6" + "source": "https://github.com/voku/portable-ascii/tree/2.0.3" }, "funding": [ { @@ -7854,34 +11568,42 @@ "type": "tidelift" } ], - "time": "2020-11-12T00:07:28+00:00" + "time": "2024-11-21T01:49:47+00:00" }, { - "name": "web-auth/cose-lib", - "version": "v3.3.9", + "name": "webmozart/assert", + "version": "1.11.0", "source": { "type": "git", - "url": "https://github.com/web-auth/cose-lib.git", - "reference": "ed172d2dc1a6b87b5c644c07c118cd30c1b3819b" + "url": "https://github.com/webmozarts/assert.git", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/web-auth/cose-lib/zipball/ed172d2dc1a6b87b5c644c07c118cd30c1b3819b", - "reference": "ed172d2dc1a6b87b5c644c07c118cd30c1b3819b", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", "shasum": "" }, "require": { - "beberlei/assert": "^3.2", - "ext-json": "*", - "ext-mbstring": "*", - "ext-openssl": "*", - "fgrosse/phpasn1": "^2.1", - "php": ">=7.2" + "ext-ctype": "*", + "php": "^7.2 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<4.6.1 || 4.6.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.13" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, "autoload": { "psr-4": { - "Cose\\": "src/" + "Webmozart\\Assert\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -7890,66 +11612,134 @@ ], "authors": [ { - "name": "Florent Morselli", - "homepage": "https://github.com/Spomky" - }, - { - "name": "All contributors", - "homepage": "https://github.com/web-auth/cose/contributors" + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" } ], - "description": "CBOR Object Signing and Encryption (COSE) For PHP", - "homepage": "https://github.com/web-auth", + "description": "Assertions to validate method input/output with nice error messages.", "keywords": [ - "COSE", - "RFC8152" + "assert", + "check", + "validate" ], "support": { - "source": "https://github.com/web-auth/cose-lib/tree/v3.3.9" + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/1.11.0" }, - "funding": [ - { - "url": "https://github.com/Spomky", - "type": "github" + "time": "2022-06-03T18:03:27+00:00" + }, + { + "name": "willdurand/geocoder", + "version": "4.6.0", + "source": { + "type": "git", + "url": "https://github.com/geocoder-php/php-common.git", + "reference": "be3d9ed0fddf8c698ee079d8a07ae9520b4a49a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/geocoder-php/php-common/zipball/be3d9ed0fddf8c698ee079d8a07ae9520b4a49a1", + "reference": "be3d9ed0fddf8c698ee079d8a07ae9520b4a49a1", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "nyholm/nsa": "^1.1", + "phpunit/phpunit": "^9.5", + "symfony/stopwatch": "~2.5" + }, + "suggest": { + "symfony/stopwatch": "If you want to use the TimedGeocoder" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "psr-4": { + "Geocoder\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ { - "url": "https://www.patreon.com/FlorentMorselli", - "type": "patreon" + "name": "William Durand", + "email": "william.durand1@gmail.com" } ], - "time": "2021-05-02T19:57:09+00:00" - }, + "description": "Common files for PHP Geocoder", + "homepage": "http://geocoder-php.org", + "keywords": [ + "abstraction", + "geocoder", + "geocoding", + "geoip" + ], + "support": { + "source": "https://github.com/geocoder-php/php-common/tree/4.6.0" + }, + "time": "2022-07-30T11:09:43+00:00" + } + ], + "packages-dev": [ { - "name": "web-auth/metadata-service", - "version": "v3.3.9", + "name": "barryvdh/laravel-debugbar", + "version": "v3.14.10", "source": { "type": "git", - "url": "https://github.com/web-auth/webauthn-metadata-service.git", - "reference": "8488d3a832a38cc81c670fce05de1e515c6e64b1" + "url": "https://github.com/barryvdh/laravel-debugbar.git", + "reference": "56b9bd235e3fe62e250124804009ce5bab97cc63" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/web-auth/webauthn-metadata-service/zipball/8488d3a832a38cc81c670fce05de1e515c6e64b1", - "reference": "8488d3a832a38cc81c670fce05de1e515c6e64b1", + "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/56b9bd235e3fe62e250124804009ce5bab97cc63", + "reference": "56b9bd235e3fe62e250124804009ce5bab97cc63", "shasum": "" }, "require": { - "beberlei/assert": "^3.2", - "ext-json": "*", - "league/uri": "^6.0", - "php": ">=7.2", - "psr/http-client": "^1.0", - "psr/http-factory": "^1.0", - "psr/log": "^1.1" + "illuminate/routing": "^9|^10|^11", + "illuminate/session": "^9|^10|^11", + "illuminate/support": "^9|^10|^11", + "maximebf/debugbar": "~1.23.0", + "php": "^8.0", + "symfony/finder": "^6|^7" }, - "suggest": { - "web-token/jwt-key-mgmt": "Mandatory for fetching Metadata Statement from distant sources", - "web-token/jwt-signature-algorithm-ecdsa": "Mandatory for fetching Metadata Statement from distant sources" + "require-dev": { + "mockery/mockery": "^1.3.3", + "orchestra/testbench-dusk": "^5|^6|^7|^8|^9", + "phpunit/phpunit": "^9.6|^10.5", + "squizlabs/php_codesniffer": "^3.5" }, "type": "library", + "extra": { + "laravel": { + "aliases": { + "Debugbar": "Barryvdh\\Debugbar\\Facades\\Debugbar" + }, + "providers": [ + "Barryvdh\\Debugbar\\ServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "3.14-dev" + } + }, "autoload": { + "files": [ + "src/helpers.php" + ], "psr-4": { - "Webauthn\\MetadataService\\": "src/" + "Barryvdh\\Debugbar\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -7958,80 +11748,87 @@ ], "authors": [ { - "name": "Florent Morselli", - "homepage": "https://github.com/Spomky" - }, - { - "name": "All contributors", - "homepage": "https://github.com/web-auth/metadata-service/contributors" + "name": "Barry vd. Heuvel", + "email": "barryvdh@gmail.com" } ], - "description": "Metadata Service for FIDO2/Webauthn", - "homepage": "https://github.com/web-auth", + "description": "PHP Debugbar integration for Laravel", "keywords": [ - "FIDO2", - "fido", - "webauthn" + "debug", + "debugbar", + "laravel", + "profiler", + "webprofiler" ], "support": { - "source": "https://github.com/web-auth/webauthn-metadata-service/tree/v3.3.9" + "issues": "https://github.com/barryvdh/laravel-debugbar/issues", + "source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.14.10" }, "funding": [ { - "url": "https://github.com/Spomky", - "type": "github" + "url": "https://fruitcake.nl", + "type": "custom" }, { - "url": "https://www.patreon.com/FlorentMorselli", - "type": "patreon" + "url": "https://github.com/barryvdh", + "type": "github" } ], - "time": "2021-01-09T13:31:01+00:00" + "time": "2024-12-23T10:10:42+00:00" }, { - "name": "web-auth/webauthn-lib", - "version": "v3.3.9", + "name": "barryvdh/laravel-ide-helper", + "version": "v3.5.4", "source": { "type": "git", - "url": "https://github.com/web-auth/webauthn-lib.git", - "reference": "04b98ee3d39cb79dad68a7c15c297c085bf66bfe" + "url": "https://github.com/barryvdh/laravel-ide-helper.git", + "reference": "980a87e250fc2a7558bc46e07f61c7594500ea53" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/web-auth/webauthn-lib/zipball/04b98ee3d39cb79dad68a7c15c297c085bf66bfe", - "reference": "04b98ee3d39cb79dad68a7c15c297c085bf66bfe", + "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/980a87e250fc2a7558bc46e07f61c7594500ea53", + "reference": "980a87e250fc2a7558bc46e07f61c7594500ea53", "shasum": "" }, "require": { - "beberlei/assert": "^3.2", + "barryvdh/reflection-docblock": "^2.3", + "composer/class-map-generator": "^1.0", "ext-json": "*", - "ext-mbstring": "*", - "ext-openssl": "*", - "fgrosse/phpasn1": "^2.1", - "php": ">=7.2", - "psr/http-client": "^1.0", - "psr/http-factory": "^1.0", - "psr/http-message": "^1.0", - "psr/log": "^1.1", - "ramsey/uuid": "^3.8|^4.0", - "spomky-labs/base64url": "^2.0", - "spomky-labs/cbor-php": "^1.1|^2.0", - "symfony/process": "^3.0|^4.0|^5.0", - "thecodingmachine/safe": "^1.1", - "web-auth/cose-lib": "self.version", - "web-auth/metadata-service": "self.version" + "illuminate/console": "^11.15", + "illuminate/database": "^11.15", + "illuminate/filesystem": "^11.15", + "illuminate/support": "^11.15", + "php": "^8.2" + }, + "require-dev": { + "ext-pdo_sqlite": "*", + "friendsofphp/php-cs-fixer": "^3", + "illuminate/config": "^11.15", + "illuminate/view": "^11.15", + "mockery/mockery": "^1.4", + "orchestra/testbench": "^9.2", + "phpunit/phpunit": "^10.5", + "spatie/phpunit-snapshot-assertions": "^4 || ^5", + "vimeo/psalm": "^5.4", + "vlucas/phpdotenv": "^5" }, "suggest": { - "psr/log-implementation": "Recommended to receive logs from the library", - "web-token/jwt-key-mgmt": "Mandatory for the AndroidSafetyNet Attestation Statement support", - "web-token/jwt-signature-algorithm-ecdsa": "Recommended for the AndroidSafetyNet Attestation Statement support", - "web-token/jwt-signature-algorithm-eddsa": "Recommended for the AndroidSafetyNet Attestation Statement support", - "web-token/jwt-signature-algorithm-rsa": "Mandatory for the AndroidSafetyNet Attestation Statement support" + "illuminate/events": "Required for automatic helper generation (^6|^7|^8|^9|^10|^11)." }, "type": "library", + "extra": { + "laravel": { + "providers": [ + "Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "3.5-dev" + } + }, "autoload": { "psr-4": { - "Webauthn\\": "src/" + "Barryvdh\\LaravelIdeHelper\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -8040,70 +11837,74 @@ ], "authors": [ { - "name": "Florent Morselli", - "homepage": "https://github.com/Spomky" - }, - { - "name": "All contributors", - "homepage": "https://github.com/web-auth/webauthn-library/contributors" + "name": "Barry vd. Heuvel", + "email": "barryvdh@gmail.com" } ], - "description": "FIDO2/Webauthn Support For PHP", - "homepage": "https://github.com/web-auth", + "description": "Laravel IDE Helper, generates correct PHPDocs for all Facade classes, to improve auto-completion.", "keywords": [ - "FIDO2", - "fido", - "webauthn" + "autocomplete", + "codeintel", + "dev", + "helper", + "ide", + "laravel", + "netbeans", + "phpdoc", + "phpstorm", + "sublime" ], "support": { - "source": "https://github.com/web-auth/webauthn-lib/tree/v3.3.9" + "issues": "https://github.com/barryvdh/laravel-ide-helper/issues", + "source": "https://github.com/barryvdh/laravel-ide-helper/tree/v3.5.4" }, "funding": [ { - "url": "https://github.com/Spomky", - "type": "github" + "url": "https://fruitcake.nl", + "type": "custom" }, { - "url": "https://www.patreon.com/FlorentMorselli", - "type": "patreon" + "url": "https://github.com/barryvdh", + "type": "github" } ], - "time": "2021-04-19T20:22:20+00:00" + "time": "2025-01-14T09:07:00+00:00" }, { - "name": "webmozart/assert", - "version": "1.10.0", + "name": "barryvdh/reflection-docblock", + "version": "v2.3.0", "source": { "type": "git", - "url": "https://github.com/webmozarts/assert.git", - "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25" + "url": "https://github.com/barryvdh/ReflectionDocBlock.git", + "reference": "818be8de6af4d16ef3ad51ea9234b3d37026ee5f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/6964c76c7804814a842473e0c8fd15bab0f18e25", - "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25", + "url": "https://api.github.com/repos/barryvdh/ReflectionDocBlock/zipball/818be8de6af4d16ef3ad51ea9234b3d37026ee5f", + "reference": "818be8de6af4d16ef3ad51ea9234b3d37026ee5f", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0", - "symfony/polyfill-ctype": "^1.8" - }, - "conflict": { - "phpstan/phpstan": "<0.12.20", - "vimeo/psalm": "<4.6.1 || 4.6.2" + "php": ">=7.1" }, "require-dev": { - "phpunit/phpunit": "^8.5.13" + "phpunit/phpunit": "^8.5.14|^9" + }, + "suggest": { + "dflydev/markdown": "~1.0", + "erusev/parsedown": "~1.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.10-dev" + "dev-master": "2.3.x-dev" } }, "autoload": { - "psr-4": { - "Webmozart\\Assert\\": "src/" + "psr-0": { + "Barryvdh": [ + "src/" + ] } }, "notification-url": "https://packagist.org/downloads/", @@ -8112,58 +11913,65 @@ ], "authors": [ { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" + "name": "Mike van Riel", + "email": "mike.vanriel@naenius.com" } ], - "description": "Assertions to validate method input/output with nice error messages.", - "keywords": [ - "assert", - "check", - "validate" - ], "support": { - "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.10.0" + "source": "https://github.com/barryvdh/ReflectionDocBlock/tree/v2.3.0" }, - "time": "2021-03-09T10:59:23+00:00" + "time": "2024-12-30T10:35:04+00:00" }, { - "name": "whichbrowser/parser", - "version": "v2.1.2", + "name": "brianium/paratest", + "version": "v7.4.8", "source": { "type": "git", - "url": "https://github.com/WhichBrowser/Parser-PHP.git", - "reference": "bcf642a1891032de16a5ab976fd352753dd7f9a0" + "url": "https://github.com/paratestphp/paratest.git", + "reference": "cf16fcbb9b8107a7df6b97e497fc91e819774d8b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/WhichBrowser/Parser-PHP/zipball/bcf642a1891032de16a5ab976fd352753dd7f9a0", - "reference": "bcf642a1891032de16a5ab976fd352753dd7f9a0", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/cf16fcbb9b8107a7df6b97e497fc91e819774d8b", + "reference": "cf16fcbb9b8107a7df6b97e497fc91e819774d8b", "shasum": "" }, "require": { - "php": ">=5.4.0", - "psr/cache": "^1.0" - }, - "require-dev": { - "cache/array-adapter": "^1.1", - "icomefromthenet/reverse-regex": "0.0.6.3", - "php-coveralls/php-coveralls": "^2.0", - "phpunit/php-code-coverage": "^5.0 || ^7.0", - "phpunit/phpunit": "^6.0 || ^8.0", - "squizlabs/php_codesniffer": "^3.5", - "symfony/yaml": "~3.4 || ~4.0" - }, - "suggest": { - "cache/array-adapter": "Allows testing of the caching functionality" + "ext-dom": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-simplexml": "*", + "fidry/cpu-core-counter": "^1.2.0", + "jean85/pretty-package-versions": "^2.0.6", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0", + "phpunit/php-code-coverage": "^10.1.16", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-timer": "^6.0.0", + "phpunit/phpunit": "^10.5.36", + "sebastian/environment": "^6.1.0", + "symfony/console": "^6.4.7 || ^7.1.5", + "symfony/process": "^6.4.7 || ^7.1.5" + }, + "require-dev": { + "doctrine/coding-standard": "^12.0.0", + "ext-pcov": "*", + "ext-posix": "*", + "phpstan/phpstan": "^1.12.6", + "phpstan/phpstan-deprecation-rules": "^1.2.1", + "phpstan/phpstan-phpunit": "^1.4.0", + "phpstan/phpstan-strict-rules": "^1.6.1", + "squizlabs/php_codesniffer": "^3.10.3", + "symfony/filesystem": "^6.4.3 || ^7.1.5" }, + "bin": [ + "bin/paratest", + "bin/paratest_for_phpstorm" + ], "type": "library", "autoload": { "psr-4": { - "WhichBrowser\\": [ - "src/", - "tests/src/" + "ParaTest\\": [ + "src/" ] } }, @@ -8173,63 +11981,67 @@ ], "authors": [ { - "name": "Niels Leenheer", - "email": "niels@leenheer.nl", + "name": "Brian Scaturro", + "email": "scaturrob@gmail.com", + "role": "Developer" + }, + { + "name": "Filippo Tessarotto", + "email": "zoeslam@gmail.com", "role": "Developer" } ], - "description": "Useragent sniffing library for PHP", - "homepage": "http://whichbrowser.net", + "description": "Parallel testing for PHP", + "homepage": "https://github.com/paratestphp/paratest", "keywords": [ - "browser", - "sniffing", - "ua", - "useragent" + "concurrent", + "parallel", + "phpunit", + "testing" ], "support": { - "issues": "https://github.com/WhichBrowser/Parser-PHP/issues", - "source": "https://github.com/WhichBrowser/Parser-PHP/tree/v2.1.2" + "issues": "https://github.com/paratestphp/paratest/issues", + "source": "https://github.com/paratestphp/paratest/tree/v7.4.8" }, - "time": "2021-05-10T10:18:11+00:00" + "funding": [ + { + "url": "https://github.com/sponsors/Slamdunk", + "type": "github" + }, + { + "url": "https://paypal.me/filippotessarotto", + "type": "paypal" + } + ], + "time": "2024-10-15T12:45:19+00:00" }, { - "name": "willdurand/geocoder", - "version": "4.4.0", + "name": "clue/ndjson-react", + "version": "v1.3.0", "source": { "type": "git", - "url": "https://github.com/geocoder-php/php-common.git", - "reference": "3e86f5b10ab0cef1cf03f979fe8e34b6476daff0" + "url": "https://github.com/clue/reactphp-ndjson.git", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/geocoder-php/php-common/zipball/3e86f5b10ab0cef1cf03f979fe8e34b6476daff0", - "reference": "3e86f5b10ab0cef1cf03f979fe8e34b6476daff0", + "url": "https://api.github.com/repos/clue/reactphp-ndjson/zipball/392dc165fce93b5bb5c637b67e59619223c931b0", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0", "shasum": "" }, "require": { - "php": "^7.3 || ^8.0" + "php": ">=5.3", + "react/stream": "^1.2" }, "require-dev": { - "nyholm/nsa": "^1.1", - "phpunit/phpunit": "^9.5", - "symfony/stopwatch": "~2.5" - }, - "suggest": { - "symfony/stopwatch": "If you want to use the TimedGeocoder" + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35", + "react/event-loop": "^1.2" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.1-dev" - } - }, "autoload": { "psr-4": { - "Geocoder\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "Clue\\React\\NDJson\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -8237,75 +12049,73 @@ ], "authors": [ { - "name": "William Durand", - "email": "william.durand1@gmail.com" + "name": "Christian Lück", + "email": "christian@clue.engineering" } ], - "description": "Common files for PHP Geocoder", - "homepage": "http://geocoder-php.org", + "description": "Streaming newline-delimited JSON (NDJSON) parser and encoder for ReactPHP.", + "homepage": "https://github.com/clue/reactphp-ndjson", "keywords": [ - "abstraction", - "geocoder", - "geocoding", - "geoip" + "NDJSON", + "json", + "jsonlines", + "newline", + "reactphp", + "streaming" ], "support": { - "source": "https://github.com/geocoder-php/php-common/tree/4.4.0" + "issues": "https://github.com/clue/reactphp-ndjson/issues", + "source": "https://github.com/clue/reactphp-ndjson/tree/v1.3.0" }, - "time": "2020-12-21T09:30:01+00:00" - } - ], - "packages-dev": [ + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2022-12-23T10:58:28+00:00" + }, { - "name": "barryvdh/laravel-debugbar", - "version": "v3.5.7", + "name": "composer/class-map-generator", + "version": "1.5.0", "source": { "type": "git", - "url": "https://github.com/barryvdh/laravel-debugbar.git", - "reference": "88fd9cfa144b06b2549e9d487fdaec68265e791e" + "url": "https://github.com/composer/class-map-generator.git", + "reference": "4b0a223cf5be7c9ee7e0ef1bc7db42b4a97c9915" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/88fd9cfa144b06b2549e9d487fdaec68265e791e", - "reference": "88fd9cfa144b06b2549e9d487fdaec68265e791e", + "url": "https://api.github.com/repos/composer/class-map-generator/zipball/4b0a223cf5be7c9ee7e0ef1bc7db42b4a97c9915", + "reference": "4b0a223cf5be7c9ee7e0ef1bc7db42b4a97c9915", "shasum": "" }, "require": { - "illuminate/routing": "^6|^7|^8", - "illuminate/session": "^6|^7|^8", - "illuminate/support": "^6|^7|^8", - "maximebf/debugbar": "^1.16.3", - "php": ">=7.2", - "symfony/debug": "^4.3|^5", - "symfony/finder": "^4.3|^5" + "composer/pcre": "^2.1 || ^3.1", + "php": "^7.2 || ^8.0", + "symfony/finder": "^4.4 || ^5.3 || ^6 || ^7" }, "require-dev": { - "mockery/mockery": "^1.3.3", - "orchestra/testbench-dusk": "^4|^5|^6", - "phpunit/phpunit": "^8.5|^9.0", - "squizlabs/php_codesniffer": "^3.5" + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-deprecation-rules": "^1 || ^2", + "phpstan/phpstan-phpunit": "^1 || ^2", + "phpstan/phpstan-strict-rules": "^1.1 || ^2", + "phpunit/phpunit": "^8", + "symfony/filesystem": "^5.4 || ^6" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.5-dev" - }, - "laravel": { - "providers": [ - "Barryvdh\\Debugbar\\ServiceProvider" - ], - "aliases": { - "Debugbar": "Barryvdh\\Debugbar\\Facade" - } + "dev-main": "1.x-dev" } }, "autoload": { "psr-4": { - "Barryvdh\\Debugbar\\": "src/" - }, - "files": [ - "src/helpers.php" - ] + "Composer\\ClassMapGenerator\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -8313,84 +12123,74 @@ ], "authors": [ { - "name": "Barry vd. Heuvel", - "email": "barryvdh@gmail.com" + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" } ], - "description": "PHP Debugbar integration for Laravel", + "description": "Utilities to scan PHP code and generate class maps.", "keywords": [ - "debug", - "debugbar", - "laravel", - "profiler", - "webprofiler" + "classmap" ], "support": { - "issues": "https://github.com/barryvdh/laravel-debugbar/issues", - "source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.5.7" + "issues": "https://github.com/composer/class-map-generator/issues", + "source": "https://github.com/composer/class-map-generator/tree/1.5.0" }, "funding": [ { - "url": "https://github.com/barryvdh", + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" } ], - "time": "2021-05-13T20:18:35+00:00" + "time": "2024-11-25T16:11:06+00:00" }, { - "name": "barryvdh/laravel-ide-helper", - "version": "v2.10.0", + "name": "composer/pcre", + "version": "3.3.2", "source": { "type": "git", - "url": "https://github.com/barryvdh/laravel-ide-helper.git", - "reference": "73b1012b927633a1b4cd623c2e6b1678e6faef08" + "url": "https://github.com/composer/pcre.git", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/73b1012b927633a1b4cd623c2e6b1678e6faef08", - "reference": "73b1012b927633a1b4cd623c2e6b1678e6faef08", + "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", "shasum": "" }, "require": { - "barryvdh/reflection-docblock": "^2.0.6", - "composer/composer": "^1.6 || ^2", - "doctrine/dbal": "^2.6 || ^3", - "ext-json": "*", - "illuminate/console": "^8", - "illuminate/filesystem": "^8", - "illuminate/support": "^8", - "nikic/php-parser": "^4.7", - "php": "^7.3 || ^8.0", - "phpdocumentor/type-resolver": "^1.1.0" + "php": "^7.4 || ^8.0" }, - "require-dev": { - "ext-pdo_sqlite": "*", - "friendsofphp/php-cs-fixer": "^2", - "illuminate/config": "^8", - "illuminate/view": "^8", - "mockery/mockery": "^1.4", - "orchestra/testbench": "^6", - "phpunit/phpunit": "^8.5 || ^9", - "spatie/phpunit-snapshot-assertions": "^3 || ^4", - "vimeo/psalm": "^3.12" + "conflict": { + "phpstan/phpstan": "<1.11.10" }, - "suggest": { - "illuminate/events": "Required for automatic helper generation (^6|^7|^8)." + "require-dev": { + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-strict-rules": "^1 || ^2", + "phpunit/phpunit": "^8 || ^9" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "2.9-dev" - }, - "laravel": { - "providers": [ - "Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider" + "phpstan": { + "includes": [ + "extension.neon" ] + }, + "branch-alias": { + "dev-main": "3.x-dev" } }, "autoload": { "psr-4": { - "Barryvdh\\LaravelIdeHelper\\": "src" + "Composer\\Pcre\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -8399,69 +12199,68 @@ ], "authors": [ { - "name": "Barry vd. Heuvel", - "email": "barryvdh@gmail.com" + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" } ], - "description": "Laravel IDE Helper, generates correct PHPDocs for all Facade classes, to improve auto-completion.", + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", "keywords": [ - "autocomplete", - "codeintel", - "helper", - "ide", - "laravel", - "netbeans", - "phpdoc", - "phpstorm", - "sublime" + "PCRE", + "preg", + "regex", + "regular expression" ], "support": { - "issues": "https://github.com/barryvdh/laravel-ide-helper/issues", - "source": "https://github.com/barryvdh/laravel-ide-helper/tree/v2.10.0" + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.3.2" }, "funding": [ { - "url": "https://github.com/barryvdh", + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" } ], - "time": "2021-04-09T06:17:55+00:00" + "time": "2024-11-12T16:29:46+00:00" }, { - "name": "barryvdh/reflection-docblock", - "version": "v2.0.6", + "name": "composer/semver", + "version": "3.4.3", "source": { "type": "git", - "url": "https://github.com/barryvdh/ReflectionDocBlock.git", - "reference": "6b69015d83d3daf9004a71a89f26e27d27ef6a16" + "url": "https://github.com/composer/semver.git", + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/ReflectionDocBlock/zipball/6b69015d83d3daf9004a71a89f26e27d27ef6a16", - "reference": "6b69015d83d3daf9004a71a89f26e27d27ef6a16", + "url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": "^5.3.2 || ^7.0 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "~4.0,<4.5" - }, - "suggest": { - "dflydev/markdown": "~1.0", - "erusev/parsedown": "~1.0" + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-main": "3.x-dev" } }, "autoload": { - "psr-0": { - "Barryvdh": [ - "src/" - ] + "psr-4": { + "Composer\\Semver\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -8470,49 +12269,77 @@ ], "authors": [ { - "name": "Mike van Riel", - "email": "mike.vanriel@naenius.com" + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" } ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], "support": { - "source": "https://github.com/barryvdh/ReflectionDocBlock/tree/v2.0.6" + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.3" }, - "time": "2018-12-13T10:34:14+00:00" + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-09-19T14:15:21+00:00" }, { - "name": "composer/ca-bundle", - "version": "1.2.11", + "name": "composer/xdebug-handler", + "version": "3.0.5", "source": { "type": "git", - "url": "https://github.com/composer/ca-bundle.git", - "reference": "0b072d51c5a9c6f3412f7ea3ab043d6603cb2582" + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/0b072d51c5a9c6f3412f7ea3ab043d6603cb2582", - "reference": "0b072d51c5a9c6f3412f7ea3ab043d6603cb2582", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", "shasum": "" }, "require": { - "ext-openssl": "*", - "ext-pcre": "*", - "php": "^5.3.2 || ^7.0 || ^8.0" + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" }, "require-dev": { - "phpstan/phpstan": "^0.12.55", - "psr/log": "^1.0", - "symfony/phpunit-bridge": "^4.2 || ^5", - "symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0 || ^6.0" + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" }, "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.x-dev" - } - }, "autoload": { "psr-4": { - "Composer\\CaBundle\\": "src" + "Composer\\XdebugHandler\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -8521,23 +12348,19 @@ ], "authors": [ { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" } ], - "description": "Lets you find a path to the system CA bundle, and includes a fallback to the Mozilla CA bundle.", + "description": "Restarts a process without Xdebug.", "keywords": [ - "cabundle", - "cacert", - "certificate", - "ssl", - "tls" + "Xdebug", + "performance" ], "support": { - "irc": "irc://irc.freenode.org/composer", - "issues": "https://github.com/composer/ca-bundle/issues", - "source": "https://github.com/composer/ca-bundle/tree/1.2.11" + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" }, "funding": [ { @@ -8553,60 +12376,103 @@ "type": "tidelift" } ], - "time": "2021-09-25T20:32:43+00:00" + "time": "2024-05-06T16:37:16+00:00" }, { - "name": "composer/composer", - "version": "2.1.9", + "name": "fakerphp/faker", + "version": "v1.24.1", "source": { "type": "git", - "url": "https://github.com/composer/composer.git", - "reference": "e558c88f28d102d497adec4852802c0dc14c7077" + "url": "https://github.com/FakerPHP/Faker.git", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/e558c88f28d102d497adec4852802c0dc14c7077", - "reference": "e558c88f28d102d497adec4852802c0dc14c7077", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", "shasum": "" }, "require": { - "composer/ca-bundle": "^1.0", - "composer/metadata-minifier": "^1.0", - "composer/semver": "^3.0", - "composer/spdx-licenses": "^1.2", - "composer/xdebug-handler": "^2.0", - "justinrainbow/json-schema": "^5.2.11", - "php": "^5.3.2 || ^7.0 || ^8.0", - "psr/log": "^1.0", - "react/promise": "^1.2 || ^2.7", - "seld/jsonlint": "^1.4", - "seld/phar-utils": "^1.0", - "symfony/console": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0", - "symfony/filesystem": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0", - "symfony/finder": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0", - "symfony/process": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0" + "php": "^7.4 || ^8.0", + "psr/container": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "conflict": { + "fzaninotto/faker": "*" }, "require-dev": { - "phpspec/prophecy": "^1.10", - "symfony/phpunit-bridge": "^4.2 || ^5.0 || ^6.0" + "bamarni/composer-bin-plugin": "^1.4.1", + "doctrine/persistence": "^1.3 || ^2.0", + "ext-intl": "*", + "phpunit/phpunit": "^9.5.26", + "symfony/phpunit-bridge": "^5.4.16" }, "suggest": { - "ext-openssl": "Enabling the openssl extension allows you to access https URLs for repositories and packages", - "ext-zip": "Enabling the zip extension allows you to unzip archives", - "ext-zlib": "Allow gzip compression of HTTP requests" + "doctrine/orm": "Required to use Faker\\ORM\\Doctrine", + "ext-curl": "Required by Faker\\Provider\\Image to download images.", + "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.", + "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.", + "ext-mbstring": "Required for multibyte Unicode string functionality." }, - "bin": [ - "bin/composer" - ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.1-dev" + "autoload": { + "psr-4": { + "Faker\\": "src/Faker/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "François Zaninotto" } + ], + "description": "Faker is a PHP library that generates fake data for you.", + "keywords": [ + "data", + "faker", + "fixtures" + ], + "support": { + "issues": "https://github.com/FakerPHP/Faker/issues", + "source": "https://github.com/FakerPHP/Faker/tree/v1.24.1" + }, + "time": "2024-11-21T13:46:39+00:00" + }, + { + "name": "fidry/cpu-core-counter", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/theofidry/cpu-core-counter.git", + "reference": "8520451a140d3f46ac33042715115e290cf5785f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/8520451a140d3f46ac33042715115e290cf5785f", + "reference": "8520451a140d3f46ac33042715115e290cf5785f", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "fidry/makefile": "^0.2.0", + "fidry/php-cs-fixer-config": "^1.1.2", + "phpstan/extension-installer": "^1.2.0", + "phpstan/phpstan": "^1.9.2", + "phpstan/phpstan-deprecation-rules": "^1.0.0", + "phpstan/phpstan-phpunit": "^1.2.2", + "phpstan/phpstan-strict-rules": "^1.4.4", + "phpunit/phpunit": "^8.5.31 || ^9.5.26", + "webmozarts/strict-phpunit": "^7.5" }, + "type": "library", "autoload": { "psr-4": { - "Composer\\": "src/Composer" + "Fidry\\CpuCoreCounter\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -8615,75 +12481,63 @@ ], "authors": [ { - "name": "Nils Adermann", - "email": "naderman@naderman.de", - "homepage": "https://www.naderman.de" - }, - { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "https://seld.be" + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com" } ], - "description": "Composer helps you declare, manage and install dependencies of PHP projects. It ensures you have the right stack everywhere.", - "homepage": "https://getcomposer.org/", + "description": "Tiny utility to get the number of CPU cores.", "keywords": [ - "autoload", - "dependency", - "package" + "CPU", + "core" ], "support": { - "irc": "ircs://irc.libera.chat:6697/composer", - "issues": "https://github.com/composer/composer/issues", - "source": "https://github.com/composer/composer/tree/2.1.9" + "issues": "https://github.com/theofidry/cpu-core-counter/issues", + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.2.0" }, "funding": [ { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", + "url": "https://github.com/theofidry", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" } ], - "time": "2021-10-05T07:47:38+00:00" + "time": "2024-08-06T10:04:20+00:00" }, { - "name": "composer/metadata-minifier", - "version": "1.0.0", + "name": "filp/whoops", + "version": "2.16.0", "source": { "type": "git", - "url": "https://github.com/composer/metadata-minifier.git", - "reference": "c549d23829536f0d0e984aaabbf02af91f443207" + "url": "https://github.com/filp/whoops.git", + "reference": "befcdc0e5dce67252aa6322d82424be928214fa2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/metadata-minifier/zipball/c549d23829536f0d0e984aaabbf02af91f443207", - "reference": "c549d23829536f0d0e984aaabbf02af91f443207", + "url": "https://api.github.com/repos/filp/whoops/zipball/befcdc0e5dce67252aa6322d82424be928214fa2", + "reference": "befcdc0e5dce67252aa6322d82424be928214fa2", "shasum": "" }, "require": { - "php": "^5.3.2 || ^7.0 || ^8.0" + "php": "^7.1 || ^8.0", + "psr/log": "^1.0.1 || ^2.0 || ^3.0" }, "require-dev": { - "composer/composer": "^2", - "phpstan/phpstan": "^0.12.55", - "symfony/phpunit-bridge": "^4.2 || ^5" + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.3", + "symfony/var-dumper": "^4.0 || ^5.0" + }, + "suggest": { + "symfony/var-dumper": "Pretty print complex values better with var-dumper available", + "whoops/soap": "Formats errors as SOAP responses" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "1.x-dev" + "dev-master": "2.7-dev" } }, "autoload": { "psr-4": { - "Composer\\MetadataMinifier\\": "src" + "Whoops\\": "src/Whoops/" } }, "notification-url": "https://packagist.org/downloads/", @@ -8692,67 +12546,102 @@ ], "authors": [ { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" + "name": "Filipe Dobreira", + "homepage": "https://github.com/filp", + "role": "Developer" } ], - "description": "Small utility library that handles metadata minification and expansion.", + "description": "php error handling for cool kids", + "homepage": "https://filp.github.io/whoops/", "keywords": [ - "composer", - "compression" + "error", + "exception", + "handling", + "library", + "throwable", + "whoops" ], "support": { - "issues": "https://github.com/composer/metadata-minifier/issues", - "source": "https://github.com/composer/metadata-minifier/tree/1.0.0" + "issues": "https://github.com/filp/whoops/issues", + "source": "https://github.com/filp/whoops/tree/2.16.0" }, "funding": [ { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", + "url": "https://github.com/denis-sokolov", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" } ], - "time": "2021-04-07T13:37:33+00:00" + "time": "2024-09-25T12:00:00+00:00" }, { - "name": "composer/semver", - "version": "3.2.5", + "name": "friendsofphp/php-cs-fixer", + "version": "v3.68.0", "source": { "type": "git", - "url": "https://github.com/composer/semver.git", - "reference": "31f3ea725711245195f62e54ffa402d8ef2fdba9" + "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", + "reference": "73f78d8b2b34a0dd65fedb434a602ee4c2c8ad4c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/31f3ea725711245195f62e54ffa402d8ef2fdba9", - "reference": "31f3ea725711245195f62e54ffa402d8ef2fdba9", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/73f78d8b2b34a0dd65fedb434a602ee4c2c8ad4c", + "reference": "73f78d8b2b34a0dd65fedb434a602ee4c2c8ad4c", "shasum": "" }, "require": { - "php": "^5.3.2 || ^7.0 || ^8.0" - }, - "require-dev": { - "phpstan/phpstan": "^0.12.54", - "symfony/phpunit-bridge": "^4.2 || ^5" + "clue/ndjson-react": "^1.0", + "composer/semver": "^3.4", + "composer/xdebug-handler": "^3.0.3", + "ext-filter": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "fidry/cpu-core-counter": "^1.2", + "php": "^7.4 || ^8.0", + "react/child-process": "^0.6.5", + "react/event-loop": "^1.0", + "react/promise": "^2.0 || ^3.0", + "react/socket": "^1.0", + "react/stream": "^1.0", + "sebastian/diff": "^4.0 || ^5.1 || ^6.0", + "symfony/console": "^5.4 || ^6.4 || ^7.0", + "symfony/event-dispatcher": "^5.4 || ^6.4 || ^7.0", + "symfony/filesystem": "^5.4 || ^6.4 || ^7.0", + "symfony/finder": "^5.4 || ^6.4 || ^7.0", + "symfony/options-resolver": "^5.4 || ^6.4 || ^7.0", + "symfony/polyfill-mbstring": "^1.31", + "symfony/polyfill-php80": "^1.31", + "symfony/polyfill-php81": "^1.31", + "symfony/process": "^5.4 || ^6.4 || ^7.2", + "symfony/stopwatch": "^5.4 || ^6.4 || ^7.0" + }, + "require-dev": { + "facile-it/paraunit": "^1.3.1 || ^2.4", + "infection/infection": "^0.29.8", + "justinrainbow/json-schema": "^5.3 || ^6.0", + "keradus/cli-executor": "^2.1", + "mikey179/vfsstream": "^1.6.12", + "php-coveralls/php-coveralls": "^2.7", + "php-cs-fixer/accessible-object": "^1.1", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.5", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.5", + "phpunit/phpunit": "^9.6.22 || ^10.5.40 || ^11.5.2", + "symfony/var-dumper": "^5.4.48 || ^6.4.15 || ^7.2.0", + "symfony/yaml": "^5.4.45 || ^6.4.13 || ^7.2.0" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.x-dev" - } + "suggest": { + "ext-dom": "For handling output formats in XML", + "ext-mbstring": "For handling non-UTF8 characters." }, + "bin": [ + "php-cs-fixer" + ], + "type": "application", "autoload": { "psr-4": { - "Composer\\Semver\\": "src" - } + "PhpCsFixer\\": "src/" + }, + "exclude-from-classmap": [ + "src/Fixer/Internal/*" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -8760,154 +12649,124 @@ ], "authors": [ { - "name": "Nils Adermann", - "email": "naderman@naderman.de", - "homepage": "http://www.naderman.de" - }, - { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { - "name": "Rob Bast", - "email": "rob.bast@gmail.com", - "homepage": "http://robbast.nl" + "name": "Dariusz Rumiński", + "email": "dariusz.ruminski@gmail.com" } ], - "description": "Semver library that offers utilities, version constraint parsing and validation.", + "description": "A tool to automatically fix PHP code style", "keywords": [ - "semantic", - "semver", - "validation", - "versioning" + "Static code analysis", + "fixer", + "standards", + "static analysis" ], "support": { - "irc": "irc://irc.freenode.org/composer", - "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/3.2.5" + "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.68.0" }, "funding": [ { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", + "url": "https://github.com/keradus", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" } ], - "time": "2021-05-24T12:41:47+00:00" + "time": "2025-01-13T17:01:01+00:00" }, { - "name": "composer/spdx-licenses", - "version": "1.5.5", + "name": "hamcrest/hamcrest-php", + "version": "v2.0.1", "source": { "type": "git", - "url": "https://github.com/composer/spdx-licenses.git", - "reference": "de30328a7af8680efdc03e396aad24befd513200" + "url": "https://github.com/hamcrest/hamcrest-php.git", + "reference": "8c3d0a3f6af734494ad8f6fbbee0ba92422859f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/de30328a7af8680efdc03e396aad24befd513200", - "reference": "de30328a7af8680efdc03e396aad24befd513200", + "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/8c3d0a3f6af734494ad8f6fbbee0ba92422859f3", + "reference": "8c3d0a3f6af734494ad8f6fbbee0ba92422859f3", "shasum": "" }, "require": { - "php": "^5.3.2 || ^7.0 || ^8.0" + "php": "^5.3|^7.0|^8.0" + }, + "replace": { + "cordoval/hamcrest-php": "*", + "davedevelopment/hamcrest-php": "*", + "kodova/hamcrest-php": "*" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || 6.5 - 7" + "phpunit/php-file-iterator": "^1.4 || ^2.0", + "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "1.x-dev" + "dev-master": "2.1-dev" } }, "autoload": { - "psr-4": { - "Composer\\Spdx\\": "src" - } + "classmap": [ + "hamcrest" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nils Adermann", - "email": "naderman@naderman.de", - "homepage": "http://www.naderman.de" - }, - { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" - }, - { - "name": "Rob Bast", - "email": "rob.bast@gmail.com", - "homepage": "http://robbast.nl" - } + "BSD-3-Clause" ], - "description": "SPDX licenses list and validation library.", + "description": "This is the PHP port of Hamcrest Matchers", "keywords": [ - "license", - "spdx", - "validator" + "test" ], "support": { - "irc": "irc://irc.freenode.org/composer", - "issues": "https://github.com/composer/spdx-licenses/issues", - "source": "https://github.com/composer/spdx-licenses/tree/1.5.5" + "issues": "https://github.com/hamcrest/hamcrest-php/issues", + "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.0.1" }, - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" - } - ], - "time": "2020-12-03T16:04:16+00:00" + "time": "2020-07-09T08:09:16+00:00" }, { - "name": "composer/xdebug-handler", - "version": "2.0.2", + "name": "itsgoingd/clockwork", + "version": "v5.3.3", "source": { "type": "git", - "url": "https://github.com/composer/xdebug-handler.git", - "reference": "84674dd3a7575ba617f5a76d7e9e29a7d3891339" + "url": "https://github.com/itsgoingd/clockwork.git", + "reference": "f5b14e5c871f5b5552ec8f8c146794f0aaae8b4c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/84674dd3a7575ba617f5a76d7e9e29a7d3891339", - "reference": "84674dd3a7575ba617f5a76d7e9e29a7d3891339", + "url": "https://api.github.com/repos/itsgoingd/clockwork/zipball/f5b14e5c871f5b5552ec8f8c146794f0aaae8b4c", + "reference": "f5b14e5c871f5b5552ec8f8c146794f0aaae8b4c", "shasum": "" }, "require": { - "php": "^5.3.2 || ^7.0 || ^8.0", - "psr/log": "^1 || ^2 || ^3" + "ext-json": "*", + "php": ">=7.1" }, - "require-dev": { - "phpstan/phpstan": "^0.12.55", - "symfony/phpunit-bridge": "^4.2 || ^5" + "suggest": { + "ext-pdo": "Needed in order to use a SQL database for metadata storage", + "ext-pdo_mysql": "Needed in order to use MySQL for metadata storage", + "ext-pdo_postgres": "Needed in order to use Postgres for metadata storage", + "ext-pdo_sqlite": "Needed in order to use a SQLite for metadata storage", + "ext-redis": "Needed in order to use Redis for metadata storage", + "php-http/discovery": "Vanilla integration - required for the middleware zero-configuration setup" }, "type": "library", + "extra": { + "laravel": { + "aliases": { + "Clockwork": "Clockwork\\Support\\Laravel\\Facade" + }, + "providers": [ + "Clockwork\\Support\\Laravel\\ClockworkServiceProvider" + ] + } + }, "autoload": { "psr-4": { - "Composer\\XdebugHandler\\": "src" + "Clockwork\\": "Clockwork/" } }, "notification-url": "https://packagist.org/downloads/", @@ -8916,138 +12775,147 @@ ], "authors": [ { - "name": "John Stevenson", - "email": "john-stevenson@blueyonder.co.uk" + "name": "itsgoingd", + "email": "itsgoingd@luzer.sk", + "homepage": "https://twitter.com/itsgoingd" } ], - "description": "Restarts a process without Xdebug.", + "description": "php dev tools in your browser", + "homepage": "https://underground.works/clockwork", "keywords": [ - "Xdebug", - "performance" + "Devtools", + "debugging", + "laravel", + "logging", + "lumen", + "profiling", + "slim" ], "support": { - "irc": "irc://irc.freenode.org/composer", - "issues": "https://github.com/composer/xdebug-handler/issues", - "source": "https://github.com/composer/xdebug-handler/tree/2.0.2" + "issues": "https://github.com/itsgoingd/clockwork/issues", + "source": "https://github.com/itsgoingd/clockwork/tree/v5.3.3" }, "funding": [ { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", + "url": "https://github.com/itsgoingd", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" } ], - "time": "2021-07-31T17:03:58+00:00" + "time": "2025-01-12T15:58:56+00:00" }, { - "name": "doctrine/annotations", - "version": "1.13.1", + "name": "jean85/pretty-package-versions", + "version": "2.1.0", "source": { "type": "git", - "url": "https://github.com/doctrine/annotations.git", - "reference": "e6e7b7d5b45a2f2abc5460cc6396480b2b1d321f" + "url": "https://github.com/Jean85/pretty-package-versions.git", + "reference": "3c4e5f62ba8d7de1734312e4fff32f67a8daaf10" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/e6e7b7d5b45a2f2abc5460cc6396480b2b1d321f", - "reference": "e6e7b7d5b45a2f2abc5460cc6396480b2b1d321f", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/3c4e5f62ba8d7de1734312e4fff32f67a8daaf10", + "reference": "3c4e5f62ba8d7de1734312e4fff32f67a8daaf10", "shasum": "" }, "require": { - "doctrine/lexer": "1.*", - "ext-tokenizer": "*", - "php": "^7.1 || ^8.0", - "psr/cache": "^1 || ^2 || ^3" + "composer-runtime-api": "^2.1.0", + "php": "^7.4|^8.0" }, "require-dev": { - "doctrine/cache": "^1.11 || ^2.0", - "doctrine/coding-standard": "^6.0 || ^8.1", - "phpstan/phpstan": "^0.12.20", - "phpunit/phpunit": "^7.5 || ^8.0 || ^9.1.5", - "symfony/cache": "^4.4 || ^5.2" + "friendsofphp/php-cs-fixer": "^3.2", + "jean85/composer-provided-replaced-stub-package": "^1.0", + "phpstan/phpstan": "^1.4", + "phpunit/phpunit": "^7.5|^8.5|^9.6", + "vimeo/psalm": "^4.3 || ^5.0" }, "type": "library", - "autoload": { - "psr-4": { - "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com" - }, + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Jean85\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" + "name": "Alessandro Lai", + "email": "alessandro.lai85@gmail.com" } ], - "description": "Docblock Annotations Parser", - "homepage": "https://www.doctrine-project.org/projects/annotations.html", + "description": "A library to get pretty versions strings of installed dependencies", "keywords": [ - "annotations", - "docblock", - "parser" + "composer", + "package", + "release", + "versions" ], "support": { - "issues": "https://github.com/doctrine/annotations/issues", - "source": "https://github.com/doctrine/annotations/tree/1.13.1" + "issues": "https://github.com/Jean85/pretty-package-versions/issues", + "source": "https://github.com/Jean85/pretty-package-versions/tree/2.1.0" }, - "time": "2021-05-16T18:07:53+00:00" + "time": "2024-11-18T16:19:46+00:00" }, { - "name": "doctrine/instantiator", - "version": "1.4.0", + "name": "larastan/larastan", + "version": "v2.9.12", "source": { "type": "git", - "url": "https://github.com/doctrine/instantiator.git", - "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b" + "url": "https://github.com/larastan/larastan.git", + "reference": "19012b39fbe4dede43dbe0c126d9681827a5e908" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/d56bf6102915de5702778fe20f2de3b2fe570b5b", - "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b", + "url": "https://api.github.com/repos/larastan/larastan/zipball/19012b39fbe4dede43dbe0c126d9681827a5e908", + "reference": "19012b39fbe4dede43dbe0c126d9681827a5e908", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" + "ext-json": "*", + "illuminate/console": "^9.52.16 || ^10.28.0 || ^11.16", + "illuminate/container": "^9.52.16 || ^10.28.0 || ^11.16", + "illuminate/contracts": "^9.52.16 || ^10.28.0 || ^11.16", + "illuminate/database": "^9.52.16 || ^10.28.0 || ^11.16", + "illuminate/http": "^9.52.16 || ^10.28.0 || ^11.16", + "illuminate/pipeline": "^9.52.16 || ^10.28.0 || ^11.16", + "illuminate/support": "^9.52.16 || ^10.28.0 || ^11.16", + "php": "^8.0.2", + "phpmyadmin/sql-parser": "^5.9.0", + "phpstan/phpstan": "^1.12.11" + }, + "require-dev": { + "doctrine/coding-standard": "^12.0", + "laravel/framework": "^9.52.16 || ^10.28.0 || ^11.16", + "mockery/mockery": "^1.5.1", + "nikic/php-parser": "^4.19.1", + "orchestra/canvas": "^7.11.1 || ^8.11.0 || ^9.0.2", + "orchestra/testbench-core": "^7.33.0 || ^8.13.0 || ^9.0.9", + "phpstan/phpstan-deprecation-rules": "^1.2", + "phpunit/phpunit": "^9.6.13 || ^10.5.16" }, - "require-dev": { - "doctrine/coding-standard": "^8.0", - "ext-pdo": "*", - "ext-phar": "*", - "phpbench/phpbench": "^0.13 || 1.0.0-alpha2", - "phpstan/phpstan": "^0.12", - "phpstan/phpstan-phpunit": "^0.12", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + "suggest": { + "orchestra/testbench": "Using Larastan for analysing a package needs Testbench" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-master": "2.0-dev" + } }, - "type": "library", "autoload": { "psr-4": { - "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + "Larastan\\Larastan\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -9056,126 +12924,125 @@ ], "authors": [ { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com", - "homepage": "https://ocramius.github.io/" + "name": "Can Vural", + "email": "can9119@gmail.com" + }, + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" } ], - "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", - "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "description": "Larastan - Discover bugs in your code without running it. A phpstan/phpstan extension for Laravel", "keywords": [ - "constructor", - "instantiate" + "PHPStan", + "code analyse", + "code analysis", + "larastan", + "laravel", + "package", + "php", + "static analysis" ], "support": { - "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/1.4.0" + "issues": "https://github.com/larastan/larastan/issues", + "source": "https://github.com/larastan/larastan/tree/v2.9.12" }, "funding": [ { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", - "type": "tidelift" + "url": "https://github.com/canvural", + "type": "github" } ], - "time": "2020-11-10T18:47:58+00:00" + "time": "2024-11-26T23:09:02+00:00" }, { - "name": "facade/ignition-contracts", - "version": "1.0.2", + "name": "lychee-org/phpstan-lychee", + "version": "v1.0.5", "source": { "type": "git", - "url": "https://github.com/facade/ignition-contracts.git", - "reference": "3c921a1cdba35b68a7f0ccffc6dffc1995b18267" + "url": "https://github.com/LycheeOrg/phpstan-lychee.git", + "reference": "fcdd430577c26178da59809db4e1dd0f0f12fc5e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/facade/ignition-contracts/zipball/3c921a1cdba35b68a7f0ccffc6dffc1995b18267", - "reference": "3c921a1cdba35b68a7f0ccffc6dffc1995b18267", + "url": "https://api.github.com/repos/LycheeOrg/phpstan-lychee/zipball/fcdd430577c26178da59809db4e1dd0f0f12fc5e", + "reference": "fcdd430577c26178da59809db4e1dd0f0f12fc5e", "shasum": "" }, "require": { - "php": "^7.3|^8.0" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^v2.15.8", - "phpunit/phpunit": "^9.3.11", - "vimeo/psalm": "^3.17.1" + "friendsofphp/php-cs-fixer": "^3.3", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan": "^1.9", + "phpstan/phpstan-deprecation-rules": "^1.2", + "phpstan/phpstan-strict-rules": "^1.6", + "slam/phpstan-extensions": "^6.3", + "squizlabs/php_codesniffer": "^3.5", + "symplify/phpstan-rules": "12.7.0", + "thecodingmachine/phpstan-safe-rule": "^1.2" }, "type": "library", "autoload": { "psr-4": { - "Facade\\IgnitionContracts\\": "src" + "Lycheeorg\\PHPStan\\": "phpstan/" } }, - "notification-url": "https://packagist.org/downloads/", + "scripts": { + "check-code-style": [ + "./vendor/bin/php-cs-fixer fix -v --config=.php-cs-fixer.php" + ], + "validate-files": [ + "vendor/bin/parallel-lint --exclude vendor ." + ] + }, "license": [ "MIT" ], - "authors": [ - { - "name": "Freek Van der Herten", - "email": "freek@spatie.be", - "homepage": "https://flareapp.io", - "role": "Developer" - } - ], - "description": "Solution contracts for Ignition", - "homepage": "https://github.com/facade/ignition-contracts", - "keywords": [ - "contracts", - "flare", - "ignition" - ], + "description": "Set of rules for all Lychee related php repo", "support": { - "issues": "https://github.com/facade/ignition-contracts/issues", - "source": "https://github.com/facade/ignition-contracts/tree/1.0.2" + "source": "https://github.com/LycheeOrg/phpstan-lychee/tree/v1.0.5", + "issues": "https://github.com/LycheeOrg/phpstan-lychee/issues" }, - "time": "2020-10-16T08:27:54+00:00" + "time": "2024-10-23T21:13:51+00:00" }, { - "name": "filp/whoops", - "version": "2.12.1", + "name": "maximebf/debugbar", + "version": "v1.23.5", "source": { "type": "git", - "url": "https://github.com/filp/whoops.git", - "reference": "c13c0be93cff50f88bbd70827d993026821914dd" + "url": "https://github.com/php-debugbar/php-debugbar.git", + "reference": "eeabd61a1f19ba5dcd5ac4585a477130ee03ce25" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filp/whoops/zipball/c13c0be93cff50f88bbd70827d993026821914dd", - "reference": "c13c0be93cff50f88bbd70827d993026821914dd", + "url": "https://api.github.com/repos/php-debugbar/php-debugbar/zipball/eeabd61a1f19ba5dcd5ac4585a477130ee03ce25", + "reference": "eeabd61a1f19ba5dcd5ac4585a477130ee03ce25", "shasum": "" }, "require": { - "php": "^5.5.9 || ^7.0 || ^8.0", - "psr/log": "^1.0.1" + "php": "^7.2|^8", + "psr/log": "^1|^2|^3", + "symfony/var-dumper": "^4|^5|^6|^7" }, "require-dev": { - "mockery/mockery": "^0.9 || ^1.0", - "phpunit/phpunit": "^4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.3", - "symfony/var-dumper": "^2.6 || ^3.0 || ^4.0 || ^5.0" + "dbrekelmans/bdi": "^1", + "phpunit/phpunit": "^8|^9", + "symfony/panther": "^1|^2.1", + "twig/twig": "^1.38|^2.7|^3.0" }, "suggest": { - "symfony/var-dumper": "Pretty print complex values better with var-dumper available", - "whoops/soap": "Formats errors as SOAP responses" + "kriswallsmith/assetic": "The best way to manage assets", + "monolog/monolog": "Log using Monolog", + "predis/predis": "Redis storage" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.7-dev" + "dev-master": "1.23-dev" } }, "autoload": { "psr-4": { - "Whoops\\": "src/Whoops/" + "DebugBar\\": "src/DebugBar/" } }, "notification-url": "https://packagist.org/downloads/", @@ -9184,175 +13051,162 @@ ], "authors": [ { - "name": "Filipe Dobreira", - "homepage": "https://github.com/filp", - "role": "Developer" + "name": "Maxime Bouroumeau-Fuseau", + "email": "maxime.bouroumeau@gmail.com", + "homepage": "http://maximebf.com" + }, + { + "name": "Barry vd. Heuvel", + "email": "barryvdh@gmail.com" } ], - "description": "php error handling for cool kids", - "homepage": "https://filp.github.io/whoops/", + "description": "Debug bar in the browser for php application", + "homepage": "https://github.com/maximebf/php-debugbar", "keywords": [ - "error", - "exception", - "handling", - "library", - "throwable", - "whoops" + "debug", + "debugbar" ], "support": { - "issues": "https://github.com/filp/whoops/issues", - "source": "https://github.com/filp/whoops/tree/2.12.1" + "issues": "https://github.com/php-debugbar/php-debugbar/issues", + "source": "https://github.com/php-debugbar/php-debugbar/tree/v1.23.5" }, - "funding": [ - { - "url": "https://github.com/denis-sokolov", - "type": "github" - } - ], - "time": "2021-04-25T12:00:00+00:00" + "time": "2024-12-15T19:20:42+00:00" }, { - "name": "friendsofphp/php-cs-fixer", - "version": "v2.19.0", + "name": "mockery/mockery", + "version": "1.6.12", "source": { "type": "git", - "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", - "reference": "d5b8a9d852b292c2f8a035200fa6844b1f82300b" + "url": "https://github.com/mockery/mockery.git", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/d5b8a9d852b292c2f8a035200fa6844b1f82300b", - "reference": "d5b8a9d852b292c2f8a035200fa6844b1f82300b", + "url": "https://api.github.com/repos/mockery/mockery/zipball/1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699", "shasum": "" }, "require": { - "composer/semver": "^1.4 || ^2.0 || ^3.0", - "composer/xdebug-handler": "^1.2 || ^2.0", - "doctrine/annotations": "^1.2", - "ext-json": "*", - "ext-tokenizer": "*", - "php": "^5.6 || ^7.0 || ^8.0", - "php-cs-fixer/diff": "^1.3", - "symfony/console": "^3.4.43 || ^4.1.6 || ^5.0", - "symfony/event-dispatcher": "^3.0 || ^4.0 || ^5.0", - "symfony/filesystem": "^3.0 || ^4.0 || ^5.0", - "symfony/finder": "^3.0 || ^4.0 || ^5.0", - "symfony/options-resolver": "^3.0 || ^4.0 || ^5.0", - "symfony/polyfill-php70": "^1.0", - "symfony/polyfill-php72": "^1.4", - "symfony/process": "^3.0 || ^4.0 || ^5.0", - "symfony/stopwatch": "^3.0 || ^4.0 || ^5.0" - }, - "require-dev": { - "justinrainbow/json-schema": "^5.0", - "keradus/cli-executor": "^1.4", - "mikey179/vfsstream": "^1.6", - "php-coveralls/php-coveralls": "^2.4.2", - "php-cs-fixer/accessible-object": "^1.0", - "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.2", - "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.2.1", - "phpspec/prophecy-phpunit": "^1.1 || ^2.0", - "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.13 || ^9.5", - "phpunitgoodpractices/polyfill": "^1.5", - "phpunitgoodpractices/traits": "^1.9.1", - "sanmai/phpunit-legacy-adapter": "^6.4 || ^8.2.1", - "symfony/phpunit-bridge": "^5.2.1", - "symfony/yaml": "^3.0 || ^4.0 || ^5.0" + "hamcrest/hamcrest-php": "^2.0.1", + "lib-pcre": ">=7.0", + "php": ">=7.3" }, - "suggest": { - "ext-dom": "For handling output formats in XML", - "ext-mbstring": "For handling non-UTF8 characters.", - "php-cs-fixer/phpunit-constraint-isidenticalstring": "For IsIdenticalString constraint.", - "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "For XmlMatchesXsd constraint.", - "symfony/polyfill-mbstring": "When enabling `ext-mbstring` is not possible." + "conflict": { + "phpunit/phpunit": "<8.0" }, - "bin": [ - "php-cs-fixer" - ], - "type": "application", - "extra": { - "branch-alias": { - "dev-master": "2.19-dev" - } + "require-dev": { + "phpunit/phpunit": "^8.5 || ^9.6.17", + "symplify/easy-coding-standard": "^12.1.14" }, + "type": "library", "autoload": { + "files": [ + "library/helpers.php", + "library/Mockery.php" + ], "psr-4": { - "PhpCsFixer\\": "src/" - }, - "classmap": [ - "tests/Test/AbstractFixerTestCase.php", - "tests/Test/AbstractIntegrationCaseFactory.php", - "tests/Test/AbstractIntegrationTestCase.php", - "tests/Test/Assert/AssertTokensTrait.php", - "tests/Test/IntegrationCase.php", - "tests/Test/IntegrationCaseFactory.php", - "tests/Test/IntegrationCaseFactoryInterface.php", - "tests/Test/InternalIntegrationCaseFactory.php", - "tests/Test/IsIdenticalConstraint.php", - "tests/Test/TokensWithObservedTransformers.php", - "tests/TestCase.php" - ] + "Mockery\\": "library/Mockery" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Pádraic Brady", + "email": "padraic.brady@gmail.com", + "homepage": "https://github.com/padraic", + "role": "Author" }, { - "name": "Dariusz Rumiński", - "email": "dariusz.ruminski@gmail.com" + "name": "Dave Marshall", + "email": "dave.marshall@atstsolutions.co.uk", + "homepage": "https://davedevelopment.co.uk", + "role": "Developer" + }, + { + "name": "Nathanael Esayeas", + "email": "nathanael.esayeas@protonmail.com", + "homepage": "https://github.com/ghostwriter", + "role": "Lead Developer" } ], - "description": "A tool to automatically fix PHP code style", + "description": "Mockery is a simple yet flexible PHP mock object framework", + "homepage": "https://github.com/mockery/mockery", + "keywords": [ + "BDD", + "TDD", + "library", + "mock", + "mock objects", + "mockery", + "stub", + "test", + "test double", + "testing" + ], "support": { - "issues": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/issues", - "source": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/tree/v2.19.0" + "docs": "https://docs.mockery.io/", + "issues": "https://github.com/mockery/mockery/issues", + "rss": "https://github.com/mockery/mockery/releases.atom", + "security": "https://github.com/mockery/mockery/security/advisories", + "source": "https://github.com/mockery/mockery" }, - "funding": [ - { - "url": "https://github.com/keradus", - "type": "github" - } - ], - "time": "2021-05-03T21:43:24+00:00" + "time": "2024-05-16T03:13:13+00:00" }, { - "name": "itsgoingd/clockwork", - "version": "v5.0.8", + "name": "nunomaduro/collision", + "version": "v8.5.0", "source": { "type": "git", - "url": "https://github.com/itsgoingd/clockwork.git", - "reference": "01686ebbf75d8e121dfb1b60e52f334858793830" + "url": "https://github.com/nunomaduro/collision.git", + "reference": "f5c101b929c958e849a633283adff296ed5f38f5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/itsgoingd/clockwork/zipball/01686ebbf75d8e121dfb1b60e52f334858793830", - "reference": "01686ebbf75d8e121dfb1b60e52f334858793830", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/f5c101b929c958e849a633283adff296ed5f38f5", + "reference": "f5c101b929c958e849a633283adff296ed5f38f5", "shasum": "" }, "require": { - "ext-json": "*", - "php": ">=5.6", - "psr/log": "1.*" + "filp/whoops": "^2.16.0", + "nunomaduro/termwind": "^2.1.0", + "php": "^8.2.0", + "symfony/console": "^7.1.5" + }, + "conflict": { + "laravel/framework": "<11.0.0 || >=12.0.0", + "phpunit/phpunit": "<10.5.1 || >=12.0.0" + }, + "require-dev": { + "larastan/larastan": "^2.9.8", + "laravel/framework": "^11.28.0", + "laravel/pint": "^1.18.1", + "laravel/sail": "^1.36.0", + "laravel/sanctum": "^4.0.3", + "laravel/tinker": "^2.10.0", + "orchestra/testbench-core": "^9.5.3", + "pestphp/pest": "^2.36.0 || ^3.4.0", + "sebastian/environment": "^6.1.0 || ^7.2.0" }, "type": "library", "extra": { "laravel": { "providers": [ - "Clockwork\\Support\\Laravel\\ClockworkServiceProvider" - ], - "aliases": { - "Clockwork": "Clockwork\\Support\\Laravel\\Facade" - } + "NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider" + ] + }, + "branch-alias": { + "dev-8.x": "8.x-dev" } }, "autoload": { + "files": [ + "./src/Adapters/Phpunit/Autoload.php" + ], "psr-4": { - "Clockwork\\": "Clockwork/" + "NunoMaduro\\Collision\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -9361,499 +13215,505 @@ ], "authors": [ { - "name": "itsgoingd", - "email": "itsgoingd@luzer.sk", - "homepage": "https://twitter.com/itsgoingd" + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" } ], - "description": "php dev tools in your browser", - "homepage": "https://underground.works/clockwork", + "description": "Cli error handling for console/command-line PHP applications.", "keywords": [ - "Devtools", - "debugging", + "artisan", + "cli", + "command-line", + "console", + "error", + "handling", "laravel", - "logging", - "lumen", - "profiling", - "slim" + "laravel-zero", + "php", + "symfony" ], "support": { - "issues": "https://github.com/itsgoingd/clockwork/issues", - "source": "https://github.com/itsgoingd/clockwork/tree/v5.0.8" + "issues": "https://github.com/nunomaduro/collision/issues", + "source": "https://github.com/nunomaduro/collision" }, "funding": [ { - "url": "https://github.com/itsgoingd", + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" } ], - "time": "2021-04-27T22:00:25+00:00" + "time": "2024-10-15T16:06:32+00:00" }, { - "name": "justinrainbow/json-schema", - "version": "5.2.11", + "name": "phar-io/manifest", + "version": "2.0.4", "source": { "type": "git", - "url": "https://github.com/justinrainbow/json-schema.git", - "reference": "2ab6744b7296ded80f8cc4f9509abbff393399aa" + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/2ab6744b7296ded80f8cc4f9509abbff393399aa", - "reference": "2ab6744b7296ded80f8cc4f9509abbff393399aa", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", "shasum": "" }, "require": { - "php": ">=5.3.3" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1", - "json-schema/json-schema-test-suite": "1.2.0", - "phpunit/phpunit": "^4.8.35" + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" }, - "bin": [ - "bin/validate-json" - ], "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { - "psr-4": { - "JsonSchema\\": "src/JsonSchema/" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Bruno Prieto Reis", - "email": "bruno.p.reis@gmail.com" - }, - { - "name": "Justin Rainbow", - "email": "justin.rainbow@gmail.com" + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" }, { - "name": "Igor Wiedler", - "email": "igor@wiedler.ch" + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" }, { - "name": "Robert Schönthal", - "email": "seroscho@googlemail.com" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" } ], - "description": "A library to validate a json schema.", - "homepage": "https://github.com/justinrainbow/json-schema", - "keywords": [ - "json", - "schema" - ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", "support": { - "issues": "https://github.com/justinrainbow/json-schema/issues", - "source": "https://github.com/justinrainbow/json-schema/tree/5.2.11" + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" }, - "time": "2021-07-22T09:24:00+00:00" + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" }, { - "name": "laravel/homestead", - "version": "v11.4.0", + "name": "phar-io/version", + "version": "3.2.1", "source": { "type": "git", - "url": "https://github.com/laravel/homestead.git", - "reference": "41a628deed9b601ee80689cf3ae0815195b5040f" + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/homestead/zipball/41a628deed9b601ee80689cf3ae0815195b5040f", - "reference": "41a628deed9b601ee80689cf3ae0815195b5040f", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", "shasum": "" }, "require": { - "php": "^7.4 || ^8.0", - "symfony/console": "~5.1", - "symfony/process": "~5.1", - "symfony/yaml": "~5.1" - }, - "require-dev": { - "dms/phpunit-arraysubset-asserts": "^0.2.0", - "phpunit/phpunit": "^9" + "php": "^7.2 || ^8.0" }, - "bin": [ - "bin/homestead" - ], "type": "library", "autoload": { - "psr-4": { - "Laravel\\Homestead\\": "src/" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" } ], - "description": "A virtual machine for web artisans.", + "description": "Library for handling version information and constraints", "support": { - "issues": "https://github.com/laravel/homestead/issues", - "source": "https://github.com/laravel/homestead/tree/v11.4.0" + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" }, - "time": "2020-11-24T17:59:52+00:00" + "time": "2022-02-21T01:04:05+00:00" }, { - "name": "maximebf/debugbar", - "version": "v1.16.5", + "name": "php-parallel-lint/php-parallel-lint", + "version": "v1.4.0", "source": { "type": "git", - "url": "https://github.com/maximebf/php-debugbar.git", - "reference": "6d51ee9e94cff14412783785e79a4e7ef97b9d62" + "url": "https://github.com/php-parallel-lint/PHP-Parallel-Lint.git", + "reference": "6db563514f27e19595a19f45a4bf757b6401194e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/6d51ee9e94cff14412783785e79a4e7ef97b9d62", - "reference": "6d51ee9e94cff14412783785e79a4e7ef97b9d62", + "url": "https://api.github.com/repos/php-parallel-lint/PHP-Parallel-Lint/zipball/6db563514f27e19595a19f45a4bf757b6401194e", + "reference": "6db563514f27e19595a19f45a4bf757b6401194e", "shasum": "" }, "require": { - "php": "^7.1|^8", - "psr/log": "^1.0", - "symfony/var-dumper": "^2.6|^3|^4|^5" + "ext-json": "*", + "php": ">=5.3.0" + }, + "replace": { + "grogy/php-parallel-lint": "*", + "jakub-onderka/php-parallel-lint": "*" }, "require-dev": { - "phpunit/phpunit": "^7.5.20 || ^9.4.2" + "nette/tester": "^1.3 || ^2.0", + "php-parallel-lint/php-console-highlighter": "0.* || ^1.0", + "squizlabs/php_codesniffer": "^3.6" }, "suggest": { - "kriswallsmith/assetic": "The best way to manage assets", - "monolog/monolog": "Log using Monolog", - "predis/predis": "Redis storage" + "php-parallel-lint/php-console-highlighter": "Highlight syntax in code snippet" }, + "bin": [ + "parallel-lint" + ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.16-dev" - } - }, "autoload": { - "psr-4": { - "DebugBar\\": "src/DebugBar/" - } + "classmap": [ + "./src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-2-Clause" ], "authors": [ { - "name": "Maxime Bouroumeau-Fuseau", - "email": "maxime.bouroumeau@gmail.com", - "homepage": "http://maximebf.com" - }, - { - "name": "Barry vd. Heuvel", - "email": "barryvdh@gmail.com" + "name": "Jakub Onderka", + "email": "ahoj@jakubonderka.cz" } ], - "description": "Debug bar in the browser for php application", - "homepage": "https://github.com/maximebf/php-debugbar", + "description": "This tool checks the syntax of PHP files about 20x faster than serial check.", + "homepage": "https://github.com/php-parallel-lint/PHP-Parallel-Lint", "keywords": [ - "debug", - "debugbar" + "lint", + "static analysis" ], "support": { - "issues": "https://github.com/maximebf/php-debugbar/issues", - "source": "https://github.com/maximebf/php-debugbar/tree/v1.16.5" + "issues": "https://github.com/php-parallel-lint/PHP-Parallel-Lint/issues", + "source": "https://github.com/php-parallel-lint/PHP-Parallel-Lint/tree/v1.4.0" }, - "time": "2020-12-07T11:07:24+00:00" + "time": "2024-03-27T12:14:49+00:00" }, { - "name": "myclabs/deep-copy", - "version": "1.10.2", + "name": "phpmyadmin/sql-parser", + "version": "5.10.2", "source": { "type": "git", - "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220" + "url": "https://github.com/phpmyadmin/sql-parser.git", + "reference": "72afbce7e4b421593b60d2eb7281e37a50734df8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/776f831124e9c62e1a2c601ecc52e776d8bb7220", - "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220", + "url": "https://api.github.com/repos/phpmyadmin/sql-parser/zipball/72afbce7e4b421593b60d2eb7281e37a50734df8", + "reference": "72afbce7e4b421593b60d2eb7281e37a50734df8", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" + "php": "^7.2 || ^8.0", + "symfony/polyfill-mbstring": "^1.3", + "symfony/polyfill-php80": "^1.16" }, - "replace": { - "myclabs/deep-copy": "self.version" + "conflict": { + "phpmyadmin/motranslator": "<3.0" }, "require-dev": { - "doctrine/collections": "^1.0", - "doctrine/common": "^2.6", - "phpunit/phpunit": "^7.1" + "phpbench/phpbench": "^1.1", + "phpmyadmin/coding-standard": "^3.0", + "phpmyadmin/motranslator": "^4.0 || ^5.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.9.12", + "phpstan/phpstan-phpunit": "^1.3.3", + "phpunit/phpunit": "^8.5 || ^9.6", + "psalm/plugin-phpunit": "^0.16.1", + "vimeo/psalm": "^4.11", + "zumba/json-serializer": "~3.0.2" + }, + "suggest": { + "ext-mbstring": "For best performance", + "phpmyadmin/motranslator": "Translate messages to your favorite locale" }, + "bin": [ + "bin/highlight-query", + "bin/lint-query", + "bin/sql-parser", + "bin/tokenize-query" + ], "type": "library", "autoload": { "psr-4": { - "DeepCopy\\": "src/DeepCopy/" - }, - "files": [ - "src/DeepCopy/deep_copy.php" - ] + "PhpMyAdmin\\SqlParser\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "GPL-2.0-or-later" ], - "description": "Create deep copies (clones) of your objects", + "authors": [ + { + "name": "The phpMyAdmin Team", + "email": "developers@phpmyadmin.net", + "homepage": "https://www.phpmyadmin.net/team/" + } + ], + "description": "A validating SQL lexer and parser with a focus on MySQL dialect.", + "homepage": "https://github.com/phpmyadmin/sql-parser", "keywords": [ - "clone", - "copy", - "duplicate", - "object", - "object graph" + "analysis", + "lexer", + "parser", + "query linter", + "sql", + "sql lexer", + "sql linter", + "sql parser", + "sql syntax highlighter", + "sql tokenizer" ], "support": { - "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.10.2" + "issues": "https://github.com/phpmyadmin/sql-parser/issues", + "source": "https://github.com/phpmyadmin/sql-parser" }, "funding": [ { - "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", - "type": "tidelift" + "url": "https://www.phpmyadmin.net/donate/", + "type": "other" } ], - "time": "2020-11-13T09:40:50+00:00" + "time": "2024-12-05T15:04:09+00:00" }, { - "name": "nikic/php-parser", - "version": "v4.10.5", + "name": "phpstan/phpstan", + "version": "1.12.15", "source": { "type": "git", - "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "4432ba399e47c66624bc73c8c0f811e5c109576f" + "url": "https://github.com/phpstan/phpstan.git", + "reference": "c91d4e8bc056f46cf653656e6f71004b254574d1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/4432ba399e47c66624bc73c8c0f811e5c109576f", - "reference": "4432ba399e47c66624bc73c8c0f811e5c109576f", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/c91d4e8bc056f46cf653656e6f71004b254574d1", + "reference": "c91d4e8bc056f46cf653656e6f71004b254574d1", "shasum": "" }, "require": { - "ext-tokenizer": "*", - "php": ">=7.0" + "php": "^7.2|^8.0" }, - "require-dev": { - "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + "conflict": { + "phpstan/phpstan-shim": "*" }, "bin": [ - "bin/php-parse" + "phpstan", + "phpstan.phar" ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.9-dev" - } - }, "autoload": { - "psr-4": { - "PhpParser\\": "lib/PhpParser" - } + "files": [ + "bootstrap.php" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Nikita Popov" - } + "MIT" ], - "description": "A PHP parser written in PHP", + "description": "PHPStan - PHP Static Analysis Tool", "keywords": [ - "parser", - "php" + "dev", + "static analysis" ], "support": { - "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.10.5" + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" }, - "time": "2021-05-03T19:11:20+00:00" + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2025-01-05T16:40:22+00:00" }, { - "name": "nunomaduro/collision", - "version": "v5.4.0", + "name": "phpstan/phpstan-deprecation-rules", + "version": "1.2.1", "source": { "type": "git", - "url": "https://github.com/nunomaduro/collision.git", - "reference": "41b7e9999133d5082700d31a1d0977161df8322a" + "url": "https://github.com/phpstan/phpstan-deprecation-rules.git", + "reference": "f94d246cc143ec5a23da868f8f7e1393b50eaa82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/collision/zipball/41b7e9999133d5082700d31a1d0977161df8322a", - "reference": "41b7e9999133d5082700d31a1d0977161df8322a", + "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/f94d246cc143ec5a23da868f8f7e1393b50eaa82", + "reference": "f94d246cc143ec5a23da868f8f7e1393b50eaa82", "shasum": "" }, "require": { - "facade/ignition-contracts": "^1.0", - "filp/whoops": "^2.7.2", - "php": "^7.3 || ^8.0", - "symfony/console": "^5.0" + "php": "^7.2 || ^8.0", + "phpstan/phpstan": "^1.12" }, "require-dev": { - "brianium/paratest": "^6.1", - "fideloper/proxy": "^4.4.1", - "friendsofphp/php-cs-fixer": "^2.17.3", - "fruitcake/laravel-cors": "^2.0.3", - "laravel/framework": "^9.0", - "nunomaduro/larastan": "^0.6.2", - "nunomaduro/mock-final-classes": "^1.0", - "orchestra/testbench": "^7.0", - "phpstan/phpstan": "^0.12.64", - "phpunit/phpunit": "^9.5.0" + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^9.5" }, - "type": "library", + "type": "phpstan-extension", "extra": { - "laravel": { - "providers": [ - "NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider" + "phpstan": { + "includes": [ + "rules.neon" ] } }, "autoload": { "psr-4": { - "NunoMaduro\\Collision\\": "src/" + "PHPStan\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ - { - "name": "Nuno Maduro", - "email": "enunomaduro@gmail.com" - } - ], - "description": "Cli error handling for console/command-line PHP applications.", - "keywords": [ - "artisan", - "cli", - "command-line", - "console", - "error", - "handling", - "laravel", - "laravel-zero", - "php", - "symfony" - ], + "description": "PHPStan rules for detecting usage of deprecated classes, methods, properties, constants and traits.", "support": { - "issues": "https://github.com/nunomaduro/collision/issues", - "source": "https://github.com/nunomaduro/collision" + "issues": "https://github.com/phpstan/phpstan-deprecation-rules/issues", + "source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/1.2.1" }, - "funding": [ - { - "url": "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=66BYDWAT92N6L", - "type": "custom" - }, - { - "url": "https://github.com/nunomaduro", - "type": "github" - }, - { - "url": "https://www.patreon.com/nunomaduro", - "type": "patreon" - } - ], - "time": "2021-04-09T13:38:32+00:00" + "time": "2024-09-11T15:52:35+00:00" }, { - "name": "phar-io/manifest", - "version": "2.0.1", + "name": "phpstan/phpstan-strict-rules", + "version": "1.6.1", "source": { "type": "git", - "url": "https://github.com/phar-io/manifest.git", - "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133" + "url": "https://github.com/phpstan/phpstan-strict-rules.git", + "reference": "daeec748b53de80a97498462513066834ec28f8b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/85265efd3af7ba3ca4b2a2c34dbfc5788dd29133", - "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133", + "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/daeec748b53de80a97498462513066834ec28f8b", + "reference": "daeec748b53de80a97498462513066834ec28f8b", "shasum": "" }, "require": { - "ext-dom": "*", - "ext-phar": "*", - "ext-xmlwriter": "*", - "phar-io/version": "^3.0.1", - "php": "^7.2 || ^8.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] + "php": "^7.2 || ^8.0", + "phpstan/phpstan": "^1.12.4" }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - }, - { - "name": "Sebastian Heuer", - "email": "sebastian@phpeople.de", - "role": "Developer" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "Developer" + "require-dev": { + "nikic/php-parser": "^4.13.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-deprecation-rules": "^1.1", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^9.5" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" ], - "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "description": "Extra strict and opinionated rules for PHPStan", "support": { - "issues": "https://github.com/phar-io/manifest/issues", - "source": "https://github.com/phar-io/manifest/tree/master" + "issues": "https://github.com/phpstan/phpstan-strict-rules/issues", + "source": "https://github.com/phpstan/phpstan-strict-rules/tree/1.6.1" }, - "time": "2020-06-27T14:33:11+00:00" + "time": "2024-09-20T14:04:44+00:00" }, { - "name": "phar-io/version", - "version": "3.1.0", + "name": "phpunit/php-code-coverage", + "version": "10.1.16", "source": { "type": "git", - "url": "https://github.com/phar-io/version.git", - "reference": "bae7c545bef187884426f042434e561ab1ddb182" + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/version/zipball/bae7c545bef187884426f042434e561ab1ddb182", - "reference": "bae7c545bef187884426f042434e561ab1ddb182", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7e308268858ed6baedc8704a304727d20bc07c77", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0" + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.19.1 || ^5.1.0", + "php": ">=8.1", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-text-template": "^3.0.1", + "sebastian/code-unit-reverse-lookup": "^3.0.0", + "sebastian/complexity": "^3.2.0", + "sebastian/environment": "^6.1.0", + "sebastian/lines-of-code": "^2.0.2", + "sebastian/version": "^4.0.1", + "theseer/tokenizer": "^1.2.3" + }, + "require-dev": { + "phpunit/phpunit": "^10.1" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" }, "type": "library", + "extra": { + "branch-alias": { + "dev-main": "10.1.x-dev" + } + }, "autoload": { "classmap": [ "src/" @@ -9864,51 +13724,58 @@ "BSD-3-Clause" ], "authors": [ - { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - }, - { - "name": "Sebastian Heuer", - "email": "sebastian@phpeople.de", - "role": "Developer" - }, { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de", - "role": "Developer" + "role": "lead" } ], - "description": "Library for handling version information and constraints", + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], "support": { - "issues": "https://github.com/phar-io/version/issues", - "source": "https://github.com/phar-io/version/tree/3.1.0" + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.16" }, - "time": "2021-02-23T14:00:09+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-08-22T04:31:57+00:00" }, { - "name": "php-cs-fixer/diff", - "version": "v1.3.1", + "name": "phpunit/php-file-iterator", + "version": "4.1.0", "source": { "type": "git", - "url": "https://github.com/PHP-CS-Fixer/diff.git", - "reference": "dbd31aeb251639ac0b9e7e29405c1441907f5759" + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/diff/zipball/dbd31aeb251639ac0b9e7e29405c1441907f5759", - "reference": "dbd31aeb251639ac0b9e7e29405c1441907f5759", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a95037b6d9e608ba092da1b23931e537cadc3c3c", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0 || ^8.0" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^5.7.23 || ^6.4.3 || ^7.0", - "symfony/process": "^3.3" + "phpunit/phpunit": "^10.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, "autoload": { "classmap": [ "src/" @@ -9921,747 +13788,862 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" - }, - { - "name": "SpacePossum" + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "sebastian/diff v2 backport support for PHP5.6", - "homepage": "https://github.com/PHP-CS-Fixer", + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", "keywords": [ - "diff" + "filesystem", + "iterator" ], "support": { - "issues": "https://github.com/PHP-CS-Fixer/diff/issues", - "source": "https://github.com/PHP-CS-Fixer/diff/tree/v1.3.1" + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/4.1.0" }, - "time": "2020-10-14T08:39:05+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-31T06:24:48+00:00" }, { - "name": "phpdocumentor/reflection-common", - "version": "2.2.0", + "name": "phpunit/php-invoker", + "version": "4.0.0", "source": { "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0" + "php": ">=8.1" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-pcntl": "*" }, "type": "library", "extra": { "branch-alias": { - "dev-2.x": "2.x-dev" + "dev-main": "4.0-dev" } }, "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src/" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Jaap van Otterdijk", - "email": "opensource@ijaap.nl" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Common reflection classes used by phpdocumentor to reflect the code structure", - "homepage": "http://www.phpdoc.org", + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", "keywords": [ - "FQSEN", - "phpDocumentor", - "phpdoc", - "reflection", - "static analysis" + "process" ], "support": { - "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", - "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/4.0.0" }, - "time": "2020-06-27T09:03:43+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:56:09+00:00" }, { - "name": "phpdocumentor/reflection-docblock", - "version": "5.2.2", + "name": "phpunit/php-text-template", + "version": "3.0.1", "source": { "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556" + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/069a785b2141f5bcf49f3e353548dc1cce6df556", - "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748", "shasum": "" }, "require": { - "ext-filter": "*", - "php": "^7.2 || ^8.0", - "phpdocumentor/reflection-common": "^2.2", - "phpdocumentor/type-resolver": "^1.3", - "webmozart/assert": "^1.9.1" + "php": ">=8.1" }, "require-dev": { - "mockery/mockery": "~1.3.2" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.x-dev" + "dev-main": "3.0-dev" } }, "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - }, - { - "name": "Jaap van Otterdijk", - "email": "account@ijaap.nl" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], "support": { - "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/master" + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/3.0.1" }, - "time": "2020-09-03T19:13:55+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-31T14:07:24+00:00" }, { - "name": "phpdocumentor/type-resolver", - "version": "1.4.0", + "name": "phpunit/php-timer", + "version": "6.0.0", "source": { "type": "git", - "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0" + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", - "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0", - "phpdocumentor/reflection-common": "^2.0" + "php": ">=8.1" }, "require-dev": { - "ext-tokenizer": "*" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-1.x": "1.x-dev" + "dev-main": "6.0-dev" } }, "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], "support": { - "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.4.0" + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/6.0.0" }, - "time": "2020-09-17T18:55:26+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:57:52+00:00" }, { - "name": "phpspec/prophecy", - "version": "1.13.0", + "name": "phpunit/phpunit", + "version": "10.5.40", "source": { "type": "git", - "url": "https://github.com/phpspec/prophecy.git", - "reference": "be1996ed8adc35c3fd795488a653f4b518be70ea" + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "e6ddda95af52f69c1e0c7b4f977cccb58048798c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/be1996ed8adc35c3fd795488a653f4b518be70ea", - "reference": "be1996ed8adc35c3fd795488a653f4b518be70ea", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e6ddda95af52f69c1e0c7b4f977cccb58048798c", + "reference": "e6ddda95af52f69c1e0c7b4f977cccb58048798c", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.2", - "php": "^7.2 || ~8.0, <8.1", - "phpdocumentor/reflection-docblock": "^5.2", - "sebastian/comparator": "^3.0 || ^4.0", - "sebastian/recursion-context": "^3.0 || ^4.0" + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.12.1", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.1", + "phpunit/php-code-coverage": "^10.1.16", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-invoker": "^4.0.0", + "phpunit/php-text-template": "^3.0.1", + "phpunit/php-timer": "^6.0.0", + "sebastian/cli-parser": "^2.0.1", + "sebastian/code-unit": "^2.0.0", + "sebastian/comparator": "^5.0.3", + "sebastian/diff": "^5.1.1", + "sebastian/environment": "^6.1.0", + "sebastian/exporter": "^5.1.2", + "sebastian/global-state": "^6.0.2", + "sebastian/object-enumerator": "^5.0.0", + "sebastian/recursion-context": "^5.0.0", + "sebastian/type": "^4.0.0", + "sebastian/version": "^4.0.1" }, - "require-dev": { - "phpspec/phpspec": "^6.0", - "phpunit/phpunit": "^8.0 || ^9.0" + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files" }, + "bin": [ + "phpunit" + ], "type": "library", "extra": { "branch-alias": { - "dev-master": "1.11.x-dev" + "dev-main": "10.5-dev" } }, "autoload": { - "psr-4": { - "Prophecy\\": "src/Prophecy" - } + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Konstantin Kudryashov", - "email": "ever.zet@gmail.com", - "homepage": "http://everzet.com" - }, - { - "name": "Marcello Duarte", - "email": "marcello.duarte@gmail.com" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Highly opinionated mocking framework for PHP 5.3+", - "homepage": "https://github.com/phpspec/prophecy", + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", "keywords": [ - "Double", - "Dummy", - "fake", - "mock", - "spy", - "stub" + "phpunit", + "testing", + "xunit" ], "support": { - "issues": "https://github.com/phpspec/prophecy/issues", - "source": "https://github.com/phpspec/prophecy/tree/1.13.0" + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.40" }, - "time": "2021-03-17T13:42:18+00:00" + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2024-12-21T05:49:06+00:00" }, { - "name": "phpunit/php-code-coverage", - "version": "9.2.6", + "name": "react/cache", + "version": "v1.2.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "f6293e1b30a2354e8428e004689671b83871edde" + "url": "https://github.com/reactphp/cache.git", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f6293e1b30a2354e8428e004689671b83871edde", - "reference": "f6293e1b30a2354e8428e004689671b83871edde", + "url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b", "shasum": "" }, "require": { - "ext-dom": "*", - "ext-libxml": "*", - "ext-xmlwriter": "*", - "nikic/php-parser": "^4.10.2", - "php": ">=7.3", - "phpunit/php-file-iterator": "^3.0.3", - "phpunit/php-text-template": "^2.0.2", - "sebastian/code-unit-reverse-lookup": "^2.0.2", - "sebastian/complexity": "^2.0", - "sebastian/environment": "^5.1.2", - "sebastian/lines-of-code": "^1.0.3", - "sebastian/version": "^3.0.1", - "theseer/tokenizer": "^1.2.0" + "php": ">=5.3.0", + "react/promise": "^3.0 || ^2.0 || ^1.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "suggest": { - "ext-pcov": "*", - "ext-xdebug": "*" + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "9.2-dev" - } - }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "React\\Cache\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" } ], - "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", - "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "description": "Async, Promise-based cache interface for ReactPHP", "keywords": [ - "coverage", - "testing", - "xunit" + "cache", + "caching", + "promise", + "reactphp" ], "support": { - "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.6" + "issues": "https://github.com/reactphp/cache/issues", + "source": "https://github.com/reactphp/cache/tree/v1.2.0" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", - "type": "github" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "time": "2021-03-28T07:26:59+00:00" + "time": "2022-11-30T15:59:55+00:00" }, { - "name": "phpunit/php-file-iterator", - "version": "3.0.5", + "name": "react/child-process", + "version": "v0.6.6", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "aa4be8575f26070b100fccb67faabb28f21f66f8" + "url": "https://github.com/reactphp/child-process.git", + "reference": "1721e2b93d89b745664353b9cfc8f155ba8a6159" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/aa4be8575f26070b100fccb67faabb28f21f66f8", - "reference": "aa4be8575f26070b100fccb67faabb28f21f66f8", + "url": "https://api.github.com/repos/reactphp/child-process/zipball/1721e2b93d89b745664353b9cfc8f155ba8a6159", + "reference": "1721e2b93d89b745664353b9cfc8f155ba8a6159", "shasum": "" }, "require": { - "php": ">=7.3" + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/event-loop": "^1.2", + "react/stream": "^1.4" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/socket": "^1.16", + "sebastian/environment": "^5.0 || ^3.0 || ^2.0 || ^1.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0-dev" - } - }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "React\\ChildProcess\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" } ], - "description": "FilterIterator implementation that filters files based on a list of suffixes.", - "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "description": "Event-driven library for executing child processes with ReactPHP.", "keywords": [ - "filesystem", - "iterator" + "event-driven", + "process", + "reactphp" ], "support": { - "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.5" + "issues": "https://github.com/reactphp/child-process/issues", + "source": "https://github.com/reactphp/child-process/tree/v0.6.6" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", - "type": "github" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "time": "2020-09-28T05:57:25+00:00" + "time": "2025-01-01T16:37:48+00:00" }, { - "name": "phpunit/php-invoker", - "version": "3.1.1", + "name": "react/dns", + "version": "v1.13.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + "url": "https://github.com/reactphp/dns.git", + "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", - "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "url": "https://api.github.com/repos/reactphp/dns/zipball/eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", + "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=5.3.0", + "react/cache": "^1.0 || ^0.6 || ^0.5", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.7 || ^1.2.1" }, "require-dev": { - "ext-pcntl": "*", - "phpunit/phpunit": "^9.3" - }, - "suggest": { - "ext-pcntl": "*" + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3 || ^2", + "react/promise-timer": "^1.11" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.1-dev" - } - }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "React\\Dns\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" } ], - "description": "Invoke callables with a timeout", - "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "description": "Async DNS resolver for ReactPHP", "keywords": [ - "process" + "async", + "dns", + "dns-resolver", + "reactphp" ], "support": { - "issues": "https://github.com/sebastianbergmann/php-invoker/issues", - "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + "issues": "https://github.com/reactphp/dns/issues", + "source": "https://github.com/reactphp/dns/tree/v1.13.0" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", - "type": "github" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "time": "2020-09-28T05:58:55+00:00" + "time": "2024-06-13T14:18:03+00:00" }, { - "name": "phpunit/php-text-template", - "version": "2.0.4", + "name": "react/event-loop", + "version": "v1.5.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + "url": "https://github.com/reactphp/event-loop.git", + "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", - "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", + "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=5.3.0" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } + "suggest": { + "ext-pcntl": "For signal handling support when using the StreamSelectLoop" }, + "type": "library", "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "React\\EventLoop\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" } ], - "description": "Simple template engine.", - "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", "keywords": [ - "template" + "asynchronous", + "event-loop" ], "support": { - "issues": "https://github.com/sebastianbergmann/php-text-template/issues", - "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + "issues": "https://github.com/reactphp/event-loop/issues", + "source": "https://github.com/reactphp/event-loop/tree/v1.5.0" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", - "type": "github" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "time": "2020-10-26T05:33:50+00:00" + "time": "2023-11-13T13:48:05+00:00" }, { - "name": "phpunit/php-timer", - "version": "5.0.3", + "name": "react/promise", + "version": "v3.2.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + "url": "https://github.com/reactphp/promise.git", + "reference": "8a164643313c71354582dc850b42b33fa12a4b63" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", - "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "url": "https://api.github.com/repos/reactphp/promise/zipball/8a164643313c71354582dc850b42b33fa12a4b63", + "reference": "8a164643313c71354582dc850b42b33fa12a4b63", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=7.1.0" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpstan/phpstan": "1.10.39 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.0-dev" - } - }, "autoload": { - "classmap": [ - "src/" - ] + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" } ], - "description": "Utility class for timing", - "homepage": "https://github.com/sebastianbergmann/php-timer/", + "description": "A lightweight implementation of CommonJS Promises/A for PHP", "keywords": [ - "timer" + "promise", + "promises" ], "support": { - "issues": "https://github.com/sebastianbergmann/php-timer/issues", - "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/v3.2.0" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", - "type": "github" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "time": "2020-10-26T13:16:10+00:00" + "time": "2024-05-24T10:39:05+00:00" }, { - "name": "phpunit/phpunit", - "version": "9.5.4", + "name": "react/socket", + "version": "v1.16.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "c73c6737305e779771147af66c96ca6a7ed8a741" + "url": "https://github.com/reactphp/socket.git", + "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c73c6737305e779771147af66c96ca6a7ed8a741", - "reference": "c73c6737305e779771147af66c96ca6a7ed8a741", + "url": "https://api.github.com/repos/reactphp/socket/zipball/23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", + "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.3.1", - "ext-dom": "*", - "ext-json": "*", - "ext-libxml": "*", - "ext-mbstring": "*", - "ext-xml": "*", - "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.10.1", - "phar-io/manifest": "^2.0.1", - "phar-io/version": "^3.0.2", - "php": ">=7.3", - "phpspec/prophecy": "^1.12.1", - "phpunit/php-code-coverage": "^9.2.3", - "phpunit/php-file-iterator": "^3.0.5", - "phpunit/php-invoker": "^3.1.1", - "phpunit/php-text-template": "^2.0.3", - "phpunit/php-timer": "^5.0.2", - "sebastian/cli-parser": "^1.0.1", - "sebastian/code-unit": "^1.0.6", - "sebastian/comparator": "^4.0.5", - "sebastian/diff": "^4.0.3", - "sebastian/environment": "^5.1.3", - "sebastian/exporter": "^4.0.3", - "sebastian/global-state": "^5.0.1", - "sebastian/object-enumerator": "^4.0.3", - "sebastian/resource-operations": "^3.0.3", - "sebastian/type": "^2.3", - "sebastian/version": "^3.0.2" - }, - "require-dev": { - "ext-pdo": "*", - "phpspec/prophecy-phpunit": "^2.0.1" + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/dns": "^1.13", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.6 || ^1.2.1", + "react/stream": "^1.4" }, - "suggest": { - "ext-soap": "*", - "ext-xdebug": "*" + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3.3 || ^2", + "react/promise-stream": "^1.4", + "react/promise-timer": "^1.11" }, - "bin": [ - "phpunit" - ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "9.5-dev" - } - }, "autoload": { - "classmap": [ - "src/" - ], - "files": [ - "src/Framework/Assert/Functions.php" - ] + "psr-4": { + "React\\Socket\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" } ], - "description": "The PHP Unit Testing framework.", - "homepage": "https://phpunit.de/", + "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", "keywords": [ - "phpunit", - "testing", - "xunit" + "Connection", + "Socket", + "async", + "reactphp", + "stream" ], "support": { - "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.4" + "issues": "https://github.com/reactphp/socket/issues", + "source": "https://github.com/reactphp/socket/tree/v1.16.0" }, "funding": [ { - "url": "https://phpunit.de/donate.html", - "type": "custom" - }, - { - "url": "https://github.com/sebastianbergmann", - "type": "github" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "time": "2021-03-23T07:16:29+00:00" + "time": "2024-07-26T10:38:09+00:00" }, { - "name": "react/promise", - "version": "v2.8.0", + "name": "react/stream", + "version": "v1.4.0", "source": { "type": "git", - "url": "https://github.com/reactphp/promise.git", - "reference": "f3cff96a19736714524ca0dd1d4130de73dbbbc4" + "url": "https://github.com/reactphp/stream.git", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/promise/zipball/f3cff96a19736714524ca0dd1d4130de73dbbbc4", - "reference": "f3cff96a19736714524ca0dd1d4130de73dbbbc4", + "url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d", "shasum": "" }, "require": { - "php": ">=5.4.0" + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.8", + "react/event-loop": "^1.2" }, "require-dev": { - "phpunit/phpunit": "^7.0 || ^6.5 || ^5.7 || ^4.8.36" + "clue/stream-filter": "~1.2", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" }, "type": "library", "autoload": { - "psr-4": { - "React\\Promise\\": "src/" - }, - "files": [ - "src/functions_include.php" - ] + "psr-4": { + "React\\Stream\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, { "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com" + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" } ], - "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", "keywords": [ - "promise", - "promises" + "event-driven", + "io", + "non-blocking", + "pipe", + "reactphp", + "readable", + "stream", + "writable" ], "support": { - "issues": "https://github.com/reactphp/promise/issues", - "source": "https://github.com/reactphp/promise/tree/v2.8.0" + "issues": "https://github.com/reactphp/stream/issues", + "source": "https://github.com/reactphp/stream/tree/v1.4.0" }, - "time": "2020-05-12T15:16:56+00:00" + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-11T12:45:25+00:00" }, { "name": "sebastian/cli-parser", - "version": "1.0.1", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-main": "2.0-dev" } }, "autoload": { @@ -10684,7 +14666,8 @@ "homepage": "https://github.com/sebastianbergmann/cli-parser", "support": { "issues": "https://github.com/sebastianbergmann/cli-parser/issues", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1" + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/2.0.1" }, "funding": [ { @@ -10692,32 +14675,32 @@ "type": "github" } ], - "time": "2020-09-28T06:08:49+00:00" + "time": "2024-03-02T07:12:49+00:00" }, { "name": "sebastian/code-unit", - "version": "1.0.8", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", - "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/a81fee9eef0b7a76af11d121767abc44c104e503", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-main": "2.0-dev" } }, "autoload": { @@ -10740,7 +14723,7 @@ "homepage": "https://github.com/sebastianbergmann/code-unit", "support": { "issues": "https://github.com/sebastianbergmann/code-unit/issues", - "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + "source": "https://github.com/sebastianbergmann/code-unit/tree/2.0.0" }, "funding": [ { @@ -10748,32 +14731,32 @@ "type": "github" } ], - "time": "2020-10-26T13:08:54+00:00" + "time": "2023-02-03T06:58:43+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", - "version": "2.0.3", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "3.0-dev" } }, "autoload": { @@ -10795,7 +14778,7 @@ "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", "support": { "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", - "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/3.0.0" }, "funding": [ { @@ -10803,34 +14786,36 @@ "type": "github" } ], - "time": "2020-09-28T05:30:19+00:00" + "time": "2023-02-03T06:59:15+00:00" }, { "name": "sebastian/comparator", - "version": "4.0.6", + "version": "5.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "55f4261989e546dc112258c7a75935a81a7ce382" + "reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55f4261989e546dc112258c7a75935a81a7ce382", - "reference": "55f4261989e546dc112258c7a75935a81a7ce382", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e", + "reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/diff": "^4.0", - "sebastian/exporter": "^4.0" + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/diff": "^5.0", + "sebastian/exporter": "^5.0" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -10869,7 +14854,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.6" + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.3" }, "funding": [ { @@ -10877,33 +14863,33 @@ "type": "github" } ], - "time": "2020-10-26T15:49:45+00:00" + "time": "2024-10-18T14:56:07+00:00" }, { "name": "sebastian/complexity", - "version": "2.0.2", + "version": "3.2.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88" + "reference": "68ff824baeae169ec9f2137158ee529584553799" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/68ff824baeae169ec9f2137158ee529584553799", + "reference": "68ff824baeae169ec9f2137158ee529584553799", "shasum": "" }, "require": { - "nikic/php-parser": "^4.7", - "php": ">=7.3" + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "3.2-dev" } }, "autoload": { @@ -10926,7 +14912,8 @@ "homepage": "https://github.com/sebastianbergmann/complexity", "support": { "issues": "https://github.com/sebastianbergmann/complexity/issues", - "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2" + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/3.2.0" }, "funding": [ { @@ -10934,33 +14921,33 @@ "type": "github" } ], - "time": "2020-10-26T15:52:27+00:00" + "time": "2023-12-21T08:37:17+00:00" }, { "name": "sebastian/diff", - "version": "4.0.4", + "version": "5.1.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d" + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/c41e007b4b62af48218231d6c2275e4c9b975b2e", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3", - "symfony/process": "^4.2 || ^5" + "phpunit/phpunit": "^10.0", + "symfony/process": "^6.4" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "5.1-dev" } }, "autoload": { @@ -10992,7 +14979,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.4" + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/5.1.1" }, "funding": [ { @@ -11000,27 +14988,27 @@ "type": "github" } ], - "time": "2020-10-26T13:10:38+00:00" + "time": "2024-03-02T07:15:17+00:00" }, { "name": "sebastian/environment", - "version": "5.1.3", + "version": "6.1.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "388b6ced16caa751030f6a69e588299fa09200ac" + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/388b6ced16caa751030f6a69e588299fa09200ac", - "reference": "388b6ced16caa751030f6a69e588299fa09200ac", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/8074dbcd93529b357029f5cc5058fd3e43666984", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "suggest": { "ext-posix": "*" @@ -11028,7 +15016,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.1-dev" + "dev-main": "6.1-dev" } }, "autoload": { @@ -11047,7 +15035,7 @@ } ], "description": "Provides functionality to handle HHVM/PHP environments", - "homepage": "http://www.github.com/sebastianbergmann/environment", + "homepage": "https://github.com/sebastianbergmann/environment", "keywords": [ "Xdebug", "environment", @@ -11055,7 +15043,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", - "source": "https://github.com/sebastianbergmann/environment/tree/5.1.3" + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/6.1.0" }, "funding": [ { @@ -11063,34 +15052,34 @@ "type": "github" } ], - "time": "2020-09-28T05:52:38+00:00" + "time": "2024-03-23T08:47:14+00:00" }, { "name": "sebastian/exporter", - "version": "4.0.3", + "version": "5.1.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "d89cc98761b8cb5a1a235a6b703ae50d34080e65" + "reference": "955288482d97c19a372d3f31006ab3f37da47adf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/d89cc98761b8cb5a1a235a6b703ae50d34080e65", - "reference": "d89cc98761b8cb5a1a235a6b703ae50d34080e65", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/955288482d97c19a372d3f31006ab3f37da47adf", + "reference": "955288482d97c19a372d3f31006ab3f37da47adf", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/recursion-context": "^4.0" + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/recursion-context": "^5.0" }, "require-dev": { - "ext-mbstring": "*", - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "5.1-dev" } }, "autoload": { @@ -11125,14 +15114,15 @@ } ], "description": "Provides the functionality to export PHP variables for visualization", - "homepage": "http://www.github.com/sebastianbergmann/exporter", + "homepage": "https://www.github.com/sebastianbergmann/exporter", "keywords": [ "export", "exporter" ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.3" + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.2" }, "funding": [ { @@ -11140,38 +15130,35 @@ "type": "github" } ], - "time": "2020-09-28T05:24:23+00:00" + "time": "2024-03-02T07:17:12+00:00" }, { "name": "sebastian/global-state", - "version": "5.0.2", + "version": "6.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "a90ccbddffa067b51f574dea6eb25d5680839455" + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/a90ccbddffa067b51f574dea6eb25d5680839455", - "reference": "a90ccbddffa067b51f574dea6eb25d5680839455", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/object-reflector": "^2.0", - "sebastian/recursion-context": "^4.0" + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" }, "require-dev": { "ext-dom": "*", - "phpunit/phpunit": "^9.3" - }, - "suggest": { - "ext-uopz": "*" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -11190,13 +15177,14 @@ } ], "description": "Snapshotting of global state", - "homepage": "http://www.github.com/sebastianbergmann/global-state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", "keywords": [ "global state" ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.2" + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/6.0.2" }, "funding": [ { @@ -11204,33 +15192,33 @@ "type": "github" } ], - "time": "2020-10-26T15:55:19+00:00" + "time": "2024-03-02T07:19:19+00:00" }, { "name": "sebastian/lines-of-code", - "version": "1.0.3", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc" + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/856e7f6a75a84e339195d48c556f23be2ebf75d0", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0", "shasum": "" }, "require": { - "nikic/php-parser": "^4.6", - "php": ">=7.3" + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-main": "2.0-dev" } }, "autoload": { @@ -11253,7 +15241,8 @@ "homepage": "https://github.com/sebastianbergmann/lines-of-code", "support": { "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3" + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/2.0.2" }, "funding": [ { @@ -11261,34 +15250,34 @@ "type": "github" } ], - "time": "2020-11-28T06:42:11+00:00" + "time": "2023-12-21T08:38:20+00:00" }, { "name": "sebastian/object-enumerator", - "version": "4.0.4", + "version": "5.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", - "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/202d0e344a580d7f7d04b3fafce6933e59dae906", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/object-reflector": "^2.0", - "sebastian/recursion-context": "^4.0" + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -11310,7 +15299,7 @@ "homepage": "https://github.com/sebastianbergmann/object-enumerator/", "support": { "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", - "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/5.0.0" }, "funding": [ { @@ -11318,32 +15307,32 @@ "type": "github" } ], - "time": "2020-10-26T13:12:34+00:00" + "time": "2023-02-03T07:08:32+00:00" }, { "name": "sebastian/object-reflector", - "version": "2.0.4", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", - "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/24ed13d98130f0e7122df55d06c5c4942a577957", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "3.0-dev" } }, "autoload": { @@ -11365,7 +15354,7 @@ "homepage": "https://github.com/sebastianbergmann/object-reflector/", "support": { "issues": "https://github.com/sebastianbergmann/object-reflector/issues", - "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + "source": "https://github.com/sebastianbergmann/object-reflector/tree/3.0.0" }, "funding": [ { @@ -11373,32 +15362,32 @@ "type": "github" } ], - "time": "2020-10-26T13:14:26+00:00" + "time": "2023-02-03T07:06:18+00:00" }, { "name": "sebastian/recursion-context", - "version": "4.0.4", + "version": "5.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172" + "reference": "05909fb5bc7df4c52992396d0116aed689f93712" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/cd9d8cf3c5804de4341c283ed787f099f5506172", - "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/05909fb5bc7df4c52992396d0116aed689f93712", + "reference": "05909fb5bc7df4c52992396d0116aed689f93712", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -11425,65 +15414,10 @@ } ], "description": "Provides functionality to recursively process PHP variables", - "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.4" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-10-26T13:17:30+00:00" - }, - { - "name": "sebastian/resource-operations", - "version": "3.0.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", - "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides a list of PHP built-in functions that operate on resources", - "homepage": "https://www.github.com/sebastianbergmann/resource-operations", - "support": { - "issues": "https://github.com/sebastianbergmann/resource-operations/issues", - "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.0" }, "funding": [ { @@ -11491,32 +15425,32 @@ "type": "github" } ], - "time": "2020-09-28T06:45:17+00:00" + "time": "2023-02-03T07:05:40+00:00" }, { "name": "sebastian/type", - "version": "2.3.1", + "version": "4.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "81cd61ab7bbf2de744aba0ea61fae32f721df3d2" + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/81cd61ab7bbf2de744aba0ea61fae32f721df3d2", - "reference": "81cd61ab7bbf2de744aba0ea61fae32f721df3d2", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/462699a16464c3944eefc02ebdd77882bd3925bf", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-main": "4.0-dev" } }, "autoload": { @@ -11539,7 +15473,7 @@ "homepage": "https://github.com/sebastianbergmann/type", "support": { "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/2.3.1" + "source": "https://github.com/sebastianbergmann/type/tree/4.0.0" }, "funding": [ { @@ -11547,29 +15481,29 @@ "type": "github" } ], - "time": "2020-10-26T13:18:59+00:00" + "time": "2023-02-03T07:10:45+00:00" }, { "name": "sebastian/version", - "version": "3.0.2", + "version": "4.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/version.git", - "reference": "c6c1022351a901512170118436c764e473f6de8c" + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", - "reference": "c6c1022351a901512170118436c764e473f6de8c", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-main": "4.0-dev" } }, "autoload": { @@ -11592,7 +15526,7 @@ "homepage": "https://github.com/sebastianbergmann/version", "support": { "issues": "https://github.com/sebastianbergmann/version/issues", - "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + "source": "https://github.com/sebastianbergmann/version/tree/4.0.1" }, "funding": [ { @@ -11600,35 +15534,45 @@ "type": "github" } ], - "time": "2020-09-28T06:39:44+00:00" + "time": "2023-02-07T11:34:05+00:00" }, { - "name": "seld/jsonlint", - "version": "1.8.3", + "name": "slam/phpstan-extensions", + "version": "v6.5.0", "source": { "type": "git", - "url": "https://github.com/Seldaek/jsonlint.git", - "reference": "9ad6ce79c342fbd44df10ea95511a1b24dee5b57" + "url": "https://github.com/Slamdunk/phpstan-extensions.git", + "reference": "dd0d6fd86a714ea533382d2b2ddaf44472669e5c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/9ad6ce79c342fbd44df10ea95511a1b24dee5b57", - "reference": "9ad6ce79c342fbd44df10ea95511a1b24dee5b57", + "url": "https://api.github.com/repos/Slamdunk/phpstan-extensions/zipball/dd0d6fd86a714ea533382d2b2ddaf44472669e5c", + "reference": "dd0d6fd86a714ea533382d2b2ddaf44472669e5c", "shasum": "" }, "require": { - "php": "^5.3 || ^7.0 || ^8.0" + "php": "~8.3.0 || ~8.4.0", + "phpstan/phpstan": "^1.12.4" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + "nette/di": "^3.2.2", + "nette/neon": "^3.4.3", + "nikic/php-parser": "^4.19.2 || ^5.2.0", + "phpstan/phpstan-phpunit": "^1.4.0", + "phpunit/phpunit": "^11.3.6", + "slam/php-cs-fixer-extensions": "^3.12.0" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "conf/slam-rules.neon" + ] + } }, - "bin": [ - "bin/jsonlint" - ], - "type": "library", "autoload": { "psr-4": { - "Seld\\JsonLint\\": "src/Seld/JsonLint/" + "SlamPhpStan\\": "lib/" } }, "notification-url": "https://packagist.org/downloads/", @@ -11637,110 +15581,134 @@ ], "authors": [ { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" + "name": "Filippo Tessarotto", + "email": "zoeslam@gmail.com", + "role": "Developer" } ], - "description": "JSON Linter", - "keywords": [ - "json", - "linter", - "parser", - "validator" - ], + "description": "Slam extension of phpstan", "support": { - "issues": "https://github.com/Seldaek/jsonlint/issues", - "source": "https://github.com/Seldaek/jsonlint/tree/1.8.3" + "issues": "https://github.com/Slamdunk/phpstan-extensions/issues", + "source": "https://github.com/Slamdunk/phpstan-extensions/tree/v6.5.0" }, "funding": [ { - "url": "https://github.com/Seldaek", - "type": "github" + "url": "https://paypal.me/filippotessarotto", + "type": "custom" }, { - "url": "https://tidelift.com/funding/github/packagist/seld/jsonlint", - "type": "tidelift" + "url": "https://github.com/Slamdunk", + "type": "github" } ], - "time": "2020-11-11T09:19:24+00:00" + "time": "2024-09-25T06:39:09+00:00" }, { - "name": "seld/phar-utils", - "version": "1.1.2", + "name": "squizlabs/php_codesniffer", + "version": "3.11.2", "source": { "type": "git", - "url": "https://github.com/Seldaek/phar-utils.git", - "reference": "749042a2315705d2dfbbc59234dd9ceb22bf3ff0" + "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", + "reference": "1368f4a58c3c52114b86b1abe8f4098869cb0079" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/phar-utils/zipball/749042a2315705d2dfbbc59234dd9ceb22bf3ff0", - "reference": "749042a2315705d2dfbbc59234dd9ceb22bf3ff0", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/1368f4a58c3c52114b86b1abe8f4098869cb0079", + "reference": "1368f4a58c3c52114b86b1abe8f4098869cb0079", "shasum": "" }, "require": { - "php": ">=5.3" + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" }, + "bin": [ + "bin/phpcbf", + "bin/phpcs" + ], "type": "library", "extra": { "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Seld\\PharUtils\\": "src/" + "dev-master": "3.x-dev" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be" + "name": "Greg Sherwood", + "role": "Former lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "Current lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" } ], - "description": "PHAR file format utilities, for when PHP phars you up", + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer", "keywords": [ - "phar" + "phpcs", + "standards", + "static analysis" ], "support": { - "issues": "https://github.com/Seldaek/phar-utils/issues", - "source": "https://github.com/Seldaek/phar-utils/tree/1.1.2" + "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues", + "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy", + "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki" }, - "time": "2021-08-19T21:01:38+00:00" + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + } + ], + "time": "2024-12-11T16:04:26+00:00" }, { - "name": "symfony/debug", - "version": "v4.4.31", + "name": "symfony/filesystem", + "version": "v7.2.0", "source": { "type": "git", - "url": "https://github.com/symfony/debug.git", - "reference": "43ede438d4cb52cd589ae5dc070e9323866ba8e0" + "url": "https://github.com/symfony/filesystem.git", + "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/debug/zipball/43ede438d4cb52cd589ae5dc070e9323866ba8e0", - "reference": "43ede438d4cb52cd589ae5dc070e9323866ba8e0", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/b8dce482de9d7c9fe2891155035a7248ab5c7fdb", + "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb", "shasum": "" }, "require": { - "php": ">=7.1.3", - "psr/log": "^1|^2|^3" - }, - "conflict": { - "symfony/http-kernel": "<3.4" + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" }, "require-dev": { - "symfony/http-kernel": "^3.4|^4.0|^5.0" + "symfony/process": "^6.4|^7.0" }, "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\Debug\\": "" + "Symfony\\Component\\Filesystem\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -11760,10 +15728,10 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Provides tools to ease debugging PHP code", + "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/debug/tree/v4.4.31" + "source": "https://github.com/symfony/filesystem/tree/v7.2.0" }, "funding": [ { @@ -11779,27 +15747,25 @@ "type": "tidelift" } ], - "time": "2021-09-24T13:30:14+00:00" + "time": "2024-10-25T15:15:23+00:00" }, { "name": "symfony/options-resolver", - "version": "v5.2.4", + "version": "v7.2.0", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "5d0f633f9bbfcf7ec642a2b5037268e61b0a62ce" + "reference": "7da8fbac9dcfef75ffc212235d76b2754ce0cf50" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/5d0f633f9bbfcf7ec642a2b5037268e61b0a62ce", - "reference": "5d0f633f9bbfcf7ec642a2b5037268e61b0a62ce", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/7da8fbac9dcfef75ffc212235d76b2754ce0cf50", + "reference": "7da8fbac9dcfef75ffc212235d76b2754ce0cf50", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1", - "symfony/polyfill-php73": "~1.0", - "symfony/polyfill-php80": "^1.15" + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" }, "type": "library", "autoload": { @@ -11832,7 +15798,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v5.2.4" + "source": "https://github.com/symfony/options-resolver/tree/v7.2.0" }, "funding": [ { @@ -11848,35 +15814,43 @@ "type": "tidelift" } ], - "time": "2021-01-27T12:56:27+00:00" + "time": "2024-11-20T11:17:29+00:00" }, { - "name": "symfony/polyfill-php70", - "version": "v1.20.0", + "name": "symfony/polyfill-php81", + "version": "v1.31.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php70.git", - "reference": "5f03a781d984aae42cebd18e7912fa80f02ee644" + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/5f03a781d984aae42cebd18e7912fa80f02ee644", - "reference": "5f03a781d984aae42cebd18e7912fa80f02ee644", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, - "type": "metapackage", + "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.20-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php81\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" @@ -11891,7 +15865,7 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 7.0+ features to lower PHP versions", + "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ "compatibility", @@ -11900,7 +15874,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php70/tree/v1.20.0" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0" }, "funding": [ { @@ -11916,25 +15890,25 @@ "type": "tidelift" } ], - "time": "2020-10-23T14:02:19+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/stopwatch", - "version": "v5.2.7", + "version": "v7.2.2", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "d99310c33e833def36419c284f60e8027d359678" + "reference": "e46690d5b9d7164a6d061cab1e8d46141b9f49df" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/d99310c33e833def36419c284f60e8027d359678", - "reference": "d99310c33e833def36419c284f60e8027d359678", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/e46690d5b9d7164a6d061cab1e8d46141b9f49df", + "reference": "e46690d5b9d7164a6d061cab1e8d46141b9f49df", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/service-contracts": "^1.0|^2" + "php": ">=8.2", + "symfony/service-contracts": "^2.5|^3" }, "type": "library", "autoload": { @@ -11962,7 +15936,7 @@ "description": "Provides a way to profile code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/stopwatch/tree/v5.3.0-BETA1" + "source": "https://github.com/symfony/stopwatch/tree/v7.2.2" }, "funding": [ { @@ -11978,95 +15952,131 @@ "type": "tidelift" } ], - "time": "2021-03-29T15:28:41+00:00" + "time": "2024-12-18T14:28:33+00:00" }, { - "name": "symfony/yaml", - "version": "v5.2.9", + "name": "symplify/phpstan-rules", + "version": "12.7.0", "source": { "type": "git", - "url": "https://github.com/symfony/yaml.git", - "reference": "d23115e4a3d50520abddccdbec9514baab1084c8" + "url": "https://github.com/symplify/phpstan-rules.git", + "reference": "14f506143ae7d6548da88326331cc536686e224f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/d23115e4a3d50520abddccdbec9514baab1084c8", - "reference": "d23115e4a3d50520abddccdbec9514baab1084c8", + "url": "https://api.github.com/repos/symplify/phpstan-rules/zipball/14f506143ae7d6548da88326331cc536686e224f", + "reference": "14f506143ae7d6548da88326331cc536686e224f", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1", - "symfony/polyfill-ctype": "~1.8" - }, - "conflict": { - "symfony/console": "<4.4" - }, - "require-dev": { - "symfony/console": "^4.4|^5.0" + "nette/utils": "^3.2.9 || ^4.0", + "php": "^7.2|^8.0", + "phpstan/phpstan": "^1.10.30", + "webmozart/assert": "^1.11" }, - "suggest": { - "symfony/console": "For validating YAML files using the lint command" + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "config/services/services.neon" + ] + } }, - "bin": [ - "Resources/bin/yaml-lint" - ], - "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\Yaml\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "Symplify\\PHPStanRules\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Loads and dumps YAML files", - "homepage": "https://symfony.com", + "description": "Set of Symplify rules for PHPStan", "support": { - "source": "https://github.com/symfony/yaml/tree/v5.2.9" + "issues": "https://github.com/symplify/phpstan-rules/issues", + "source": "https://github.com/symplify/phpstan-rules/tree/12.7.0" }, "funding": [ { - "url": "https://symfony.com/sponsor", + "url": "https://www.paypal.me/rectorphp", "type": "custom" }, { - "url": "https://github.com/fabpot", + "url": "https://github.com/tomasvotruba", "type": "github" + } + ], + "time": "2024-05-25T15:32:40+00:00" + }, + { + "name": "thecodingmachine/phpstan-safe-rule", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/thecodingmachine/phpstan-safe-rule.git", + "reference": "8a7b88e0d54f209a488095085f183e9174c40e1e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thecodingmachine/phpstan-safe-rule/zipball/8a7b88e0d54f209a488095085f183e9174c40e1e", + "reference": "8a7b88e0d54f209a488095085f183e9174c40e1e", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "phpstan/phpstan": "^1.0", + "thecodingmachine/safe": "^1.0 || ^2.0" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^7.5.2 || ^8.0", + "squizlabs/php_codesniffer": "^3.4" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "phpstan-safe-rule.neon" + ] }, + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-4": { + "TheCodingMachine\\Safe\\PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" + "name": "David Négrier", + "email": "d.negrier@thecodingmachine.com" } ], - "time": "2021-05-16T13:07:46+00:00" + "description": "A PHPStan rule to detect safety issues. Must be used in conjunction with thecodingmachine/safe", + "support": { + "issues": "https://github.com/thecodingmachine/phpstan-safe-rule/issues", + "source": "https://github.com/thecodingmachine/phpstan-safe-rule/tree/v1.2.0" + }, + "time": "2022-01-17T10:12:29+00:00" }, { "name": "theseer/tokenizer", - "version": "1.2.0", + "version": "1.2.3", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "75a63c33a8577608444246075ea0af0d052e452a" + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/75a63c33a8577608444246075ea0af0d052e452a", - "reference": "75a63c33a8577608444246075ea0af0d052e452a", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", "shasum": "" }, "require": { @@ -12095,7 +16105,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/master" + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" }, "funding": [ { @@ -12103,26 +16113,37 @@ "type": "github" } ], - "time": "2020-07-12T23:59:07+00:00" + "time": "2024-03-03T12:36:25+00:00" } ], "aliases": [], "minimum-stability": "dev", "stability-flags": { - "darkghosthunter/larapass": 20, - "lychee-org/php-exif": 20 + "opcodesio/log-viewer": 20 }, "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": "^7.4.0|^8.0", + "php": "^8.3", "ext-bcmath": "*", + "ext-ctype": "*", "ext-exif": "*", + "ext-fileinfo": "*", "ext-gd": "*", - "ext-json": "*" + "ext-json": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "ext-pdo": "*", + "ext-tokenizer": "*", + "ext-xml": "*" }, "platform-dev": { - "ext-imagick": "*" + "ext-imagick": "*", + "ext-posix": "*", + "ext-zip": "*" + }, + "platform-overrides": { + "php": "8.3" }, - "plugin-api-version": "2.1.0" + "plugin-api-version": "2.3.0" } diff --git a/config/app.php b/config/app.php index f1305d32273..8900e974ab2 100644 --- a/config/app.php +++ b/config/app.php @@ -1,5 +1,24 @@ env('APP_ENV', 'production'), - /* - |-------------------------------------------------------------------------- - | Application Environment - |-------------------------------------------------------------------------- - | - | This value determines whether livewire front-end is enabled as it is - | currently under development. - | - */ - - 'livewire' => (bool) env('LIVEWIRE_ENABLED', false), - /* |-------------------------------------------------------------------------- | Application Debug Mode @@ -61,11 +68,17 @@ | the Artisan command line tool. You should set this to the root of | your application so that it is used when running Artisan tasks. | + | url : the base url of your Lychee install up to the tld (end '/' will be trimmed) + | dir_url : the path of your Lychee install from the tld (will be prefixed by '/' and end '/' will be trimmed) + | + | asset_url : should be left to default (null). */ - 'url' => env('APP_URL', 'http://localhost'), + 'url' => renv('APP_URL', 'http://localhost'), - 'asset_url' => env('ASSET_URL', null), + 'dir_url' => env('APP_DIR', '') === '' ? '' : ('/' . trim((string) (env('APP_DIR') ?? ''), '/')), + + 'asset_url' => null, /* |-------------------------------------------------------------------------- @@ -78,7 +91,7 @@ | */ - 'timezone' => env('TIMEZONE', 'UTC'), + 'timezone' => env('TIMEZONE', date('e')), /* |-------------------------------------------------------------------------- @@ -106,6 +119,40 @@ 'fallback_locale' => 'en', + /* + |-------------------------------------------------------------------------- + | Application Avilable Locale + |-------------------------------------------------------------------------- + | + | List of locale supported by Lychee. + | ['cz', 'de', 'el', 'en', 'es', 'fr', 'it', 'nl', 'no', 'pl', 'pt', 'ru', 'sk', 'sv', 'vi', 'zh_CN', 'zh_TW'] + */ + + 'supported_locale' => array_diff(scandir(base_path('lang')), ['..', '.']), + + /* + |-------------------------------------------------------------------------- + | Faker Locale + |-------------------------------------------------------------------------- + | + | This locale will be used by the Faker PHP library when generating fake + | data for your database seeds. For example, this will be used to get + | localized telephone numbers, street address information and more. + | + */ + + 'faker_locale' => 'en_US', + + /* + |-------------------------------------------------------------------------- + | Skip diagnostics checks + |-------------------------------------------------------------------------- + | + | Allows to define class names of diagnostics checks that will be skipped. + | + */ + 'skip_diagnostics_checks' => explode(',', (string) env('SKIP_DIAGNOSTICS_CHECKS', '')), + /* |-------------------------------------------------------------------------- | Encryption Key @@ -119,7 +166,25 @@ 'key' => env('APP_KEY'), - 'cipher' => 'AES-256-CBC', + 'cipher' => env('APP_CIPHER', 'AES-256-CBC'), + + /* + |-------------------------------------------------------------------------- + | Maintenance Mode Driver + |-------------------------------------------------------------------------- + | + | These configuration options determine the driver used to determine and + | manage Laravel's "maintenance mode" status. The "cache" driver will + | allow maintenance mode to be controlled across multiple machines. + | + | Supported drivers: "file", "cache" + | + */ + + 'maintenance' => [ + 'driver' => 'file', + // 'store' => 'redis', + ], /* |-------------------------------------------------------------------------- @@ -153,7 +218,6 @@ Illuminate\Pipeline\PipelineServiceProvider::class, Illuminate\Queue\QueueServiceProvider::class, Illuminate\Redis\RedisServiceProvider::class, - // Illuminate\Auth\Passwords\PasswordResetServiceProvider::class, Illuminate\Session\SessionServiceProvider::class, Illuminate\Translation\TranslationServiceProvider::class, Illuminate\Validation\ValidationServiceProvider::class, @@ -163,18 +227,18 @@ * Package Service Providers... */ + \SocialiteProviders\Manager\ServiceProvider::class, // Barryvdh\Debugbar\ServiceProvider::class, + Mavinoo\Batch\BatchServiceProvider::class, /* * Application Service Providers... */ App\Providers\AppServiceProvider::class, App\Providers\AuthServiceProvider::class, - // App\Providers\BroadcastServiceProvider::class, App\Providers\EventServiceProvider::class, App\Providers\RouteServiceProvider::class, - // App\Providers\LangServiceProvider::class, - // App\Providers\AccessControlServiceProvider::class, + LycheeVerify\VerifyServiceProvider::class, ], /* @@ -188,47 +252,19 @@ | */ - 'aliases' => [ - 'App' => Illuminate\Support\Facades\App::class, - 'AccessControl' => App\Facades\AccessControl::class, - 'Arr' => Illuminate\Support\Arr::class, - 'Artisan' => Illuminate\Support\Facades\Artisan::class, - 'Auth' => Illuminate\Support\Facades\Auth::class, - 'Blade' => Illuminate\Support\Facades\Blade::class, - 'Broadcast' => Illuminate\Support\Facades\Broadcast::class, - 'Bus' => Illuminate\Support\Facades\Bus::class, - 'Cache' => Illuminate\Support\Facades\Cache::class, - 'Config' => Illuminate\Support\Facades\Config::class, - 'Cookie' => Illuminate\Support\Facades\Cookie::class, - 'Crypt' => Illuminate\Support\Facades\Crypt::class, - 'DB' => Illuminate\Support\Facades\DB::class, - 'DebugBar' => Barryvdh\Debugbar\Facade::class, - 'Eloquent' => Illuminate\Database\Eloquent\Model::class, - 'Event' => Illuminate\Support\Facades\Event::class, - 'File' => Illuminate\Support\Facades\File::class, - 'Gate' => Illuminate\Support\Facades\Gate::class, - 'Hash' => Illuminate\Support\Facades\Hash::class, - 'Http' => Illuminate\Support\Facades\Http::class, - // 'Lang' => Illuminate\Support\Facades\Lang::class, + 'aliases' => Facade::defaultAliases()->merge([ + 'DebugBar' => Barryvdh\Debugbar\Facades\Debugbar::class, 'Helpers' => App\Facades\Helpers::class, - 'Lang' => App\Facades\Lang::class, - 'Log' => Illuminate\Support\Facades\Log::class, - 'Mail' => Illuminate\Support\Facades\Mail::class, - 'Markdown' => GrahamCampbell\Markdown\Facades\Markdown::class, - 'Notification' => Illuminate\Support\Facades\Notification::class, - // 'Password' => Illuminate\Support\Facades\Password::class, - 'Queue' => Illuminate\Support\Facades\Queue::class, - 'Redirect' => Illuminate\Support\Facades\Redirect::class, - 'Redis' => Illuminate\Support\Facades\Redis::class, - 'Request' => Illuminate\Support\Facades\Request::class, - 'Response' => Illuminate\Support\Facades\Response::class, - 'Route' => Illuminate\Support\Facades\Route::class, - 'Schema' => Illuminate\Support\Facades\Schema::class, - 'Session' => Illuminate\Support\Facades\Session::class, - 'Storage' => Illuminate\Support\Facades\Storage::class, - 'Str' => Illuminate\Support\Str::class, - 'URL' => Illuminate\Support\Facades\URL::class, - 'Validator' => Illuminate\Support\Facades\Validator::class, - 'View' => Illuminate\Support\Facades\View::class, - ], + 'Features' => App\Assets\Features::class, + // Aliases for easier access in the blade templates + 'Configs' => App\Models\Configs::class, + 'AlbumPolicy' => App\Policies\AlbumPolicy::class, + 'PhotoPolicy' => App\Policies\PhotoPolicy::class, + 'SettingsPolicy' => App\Policies\SettingsPolicy::class, + 'UserPolicy' => App\Policies\UserPolicy::class, + 'User' => App\Models\User::class, + 'SizeVariantType' => App\Enum\SizeVariantType::class, + 'FileStatus' => App\Enum\FileStatus::class, + 'PhotoLayoutType' => \App\Enum\PhotoLayoutType::class, + ])->toArray(), ]; diff --git a/config/auth.php b/config/auth.php index fd9b016e7b3..4face21e4e1 100644 --- a/config/auth.php +++ b/config/auth.php @@ -13,7 +13,7 @@ */ 'defaults' => [ - 'guard' => 'web', + 'guard' => 'lychee', 'passwords' => 'users', ], @@ -32,19 +32,16 @@ | | Supported: "session", "token" | + | The custom identifier "session-or-token" is registered in + | App\Providers\AuthServiceProvider and resolves to + | App\Services\Auth\SessionOrTokenGuard. */ 'guards' => [ - 'web' => [ - 'driver' => 'session', + 'lychee' => [ + 'driver' => env('ENABLE_BEARER_TOKEN_AUTH', env('ENABLE_TOKEN_AUTH', true)) ? 'session-or-token' : 'session', // @phpstan-ignore-line 'provider' => 'users', ], - - 'api' => [ - 'driver' => 'token', - 'provider' => 'users', - 'hash' => false, - ], ], /* @@ -66,9 +63,9 @@ 'providers' => [ 'users' => [ - 'driver' => 'eloquent', - // 'driver' => 'eloquent-webauthn', + 'driver' => 'eloquent-webauthn', 'model' => App\Models\User::class, + 'password_fallback' => true, ], // 'users' => [ @@ -113,4 +110,23 @@ */ 'password_timeout' => 10800, + + /* + |-------------------------------------------------------------------------- + | Hard fail on bearer token + |-------------------------------------------------------------------------- + | + | When a bearer token is found, we fail hard by throwing an exception when the + | associated authenticable (user) is not found. + | + | This is only used if ENABLE_BEARER_TOKEN_AUTH = true + */ + + 'token_guard' => [ + // Hard fail if bearer token is provided but no authenticable user is found + 'fail_bearer_authenticable_not_found' => (bool) env('FAIL_NO_AUTHENTICABLE_BEARER_TOKEN', true), + + // Log if token is provided but no bearer prefix. + 'log_warn_no_scheme_bearer' => (bool) env('LOG_WARN_NO_BEARER_TOKEN', true), + ], ]; diff --git a/config/broadcasting.php b/config/broadcasting.php index a5284122a67..64dbf68581a 100644 --- a/config/broadcasting.php +++ b/config/broadcasting.php @@ -34,9 +34,20 @@ 'secret' => env('PUSHER_APP_SECRET'), 'app_id' => env('PUSHER_APP_ID'), 'options' => [ - 'cluster' => env('PUSHER_APP_CLUSTER'), - 'useTLS' => true, + 'host' => env('PUSHER_HOST', 'api-' . env('PUSHER_APP_CLUSTER', 'mt1') . '.pusher.com') ?: 'api-' . env('PUSHER_APP_CLUSTER', 'mt1') . '.pusher.com', // @phpstan-ignore-line + 'port' => env('PUSHER_PORT', 443), + 'scheme' => env('PUSHER_SCHEME', 'https'), + 'encrypted' => true, + 'useTLS' => env('PUSHER_SCHEME', 'https') === 'https', ], + 'client_options' => [ + // Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html + ], + ], + + 'ably' => [ + 'driver' => 'ably', + 'key' => env('ABLY_KEY'), ], 'redis' => [ diff --git a/config/cache.php b/config/cache.php index 2b3507ab58b..2e68fee947a 100644 --- a/config/cache.php +++ b/config/cache.php @@ -28,6 +28,9 @@ | well as their drivers. You may even define multiple stores for the | same cache driver to group types of items stored in your caches. | + | Supported drivers: "apc", "array", "database", "file", + | "memcached", "redis", "dynamodb", "octane", "null" + | */ 'stores' => [ @@ -43,7 +46,8 @@ 'database' => [ 'driver' => 'database', 'table' => 'cache', - 'connection' => null, + 'connection' => env('DB_CONNECTION'), + 'lock_connection' => null, ], 'file' => [ @@ -58,9 +62,6 @@ env('MEMCACHED_USERNAME'), env('MEMCACHED_PASSWORD'), ], - 'options' => [ - // Memcached::OPT_CONNECT_TIMEOUT => 2000, - ], 'servers' => [ [ 'host' => env('MEMCACHED_HOST', '127.0.0.1'), @@ -73,6 +74,7 @@ 'redis' => [ 'driver' => 'redis', 'connection' => 'cache', + 'lock_connection' => 'default', ], 'dynamodb' => [ @@ -83,6 +85,10 @@ 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), 'endpoint' => env('DYNAMODB_ENDPOINT'), ], + + 'octane' => [ + 'driver' => 'octane', + ], ], /* @@ -96,8 +102,5 @@ | */ - 'prefix' => env( - 'CACHE_PREFIX', - Str::slug(env('APP_NAME', 'laravel'), '_') . '_cache' - ), + 'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'Lychee'), '_') . '_cache_'), ]; diff --git a/config/cors.php b/config/cors.php new file mode 100644 index 00000000000..8583af2e347 --- /dev/null +++ b/config/cors.php @@ -0,0 +1,32 @@ + ['api/*', 'sanctum/csrf-cookie'], + + 'allowed_methods' => ['*'], + + 'allowed_origins' => ['*'], + + 'allowed_origins_patterns' => [], + + 'allowed_headers' => ['*'], + + 'exposed_headers' => [], + + 'max_age' => 0, + + 'supports_credentials' => false, +]; \ No newline at end of file diff --git a/config/data.php b/config/data.php new file mode 100644 index 00000000000..eee755ef956 --- /dev/null +++ b/config/data.php @@ -0,0 +1,201 @@ + DATE_ATOM, + + /** + * When transforming or casting dates, the following timezone will be used to + * convert the date to the correct timezone. If set to null no timezone will + * be passed. + */ + 'date_timezone' => null, + + /** + * It is possible to enable certain features of the package, these would otherwise + * be breaking changes, and thus they are disabled by default. In the next major + * version of the package, these features will be enabled by default. + */ + 'features' => [ + 'cast_and_transform_iterables' => false, + + /** + * When trying to set a computed property value, the package will throw an exception. + * You can disable this behaviour by setting this option to true, which will then just + * ignore the value being passed into the computed property and recalculate it. + */ + 'ignore_exception_when_trying_to_set_computed_property_value' => false, + ], + + /** + * Global transformers will take complex types and transform them into simple + * types. + */ + 'transformers' => [ + DateTimeInterface::class => DateTimeInterfaceTransformer::class, + \Illuminate\Contracts\Support\Arrayable::class => ArrayableTransformer::class, + BackedEnum::class => EnumTransformer::class, + ], + + /** + * Global casts will cast values into complex types when creating a data + * object from simple types. + */ + 'casts' => [ + DateTimeInterface::class => DateTimeInterfaceCast::class, + BackedEnum::class => EnumCast::class, + // Enumerable::class => Spatie\LaravelData\Casts\EnumerableCast::class, + ], + + /** + * Rule inferrers can be configured here. They will automatically add + * validation rules to properties of a data object based upon + * the type of the property. + */ + 'rule_inferrers' => [ + SometimesRuleInferrer::class, + NullableRuleInferrer::class, + RequiredRuleInferrer::class, + BuiltInTypesRuleInferrer::class, + AttributesRuleInferrer::class, + ], + + /** + * Normalizers return an array representation of the payload, or null if + * it cannot normalize the payload. The normalizers below are used for + * every data object, unless overridden in a specific data object class. + */ + 'normalizers' => [ + ModelNormalizer::class, + // FormRequestNormalizer::class, + ArrayableNormalizer::class, + ObjectNormalizer::class, + ArrayNormalizer::class, + JsonNormalizer::class, + ], + + /** + * Data objects can be wrapped into a key like 'data' when used as a resource, + * this key can be set globally here for all data objects. You can pass in + * `null` if you want to disable wrapping. + */ + 'wrap' => null, + + /** + * Adds a specific caster to the Symphony VarDumper component which hides + * some properties from data objects and collections when being dumped + * by `dump` or `dd`. Can be 'enabled', 'disabled' or 'development' + * which will only enable the caster locally. + */ + 'var_dumper_caster_mode' => 'development', + + /** + * It is possible to skip the PHP reflection analysis of data objects + * when running in production. This will speed up the package. You + * can configure where data objects are stored and which cache + * store should be used. + * + * Structures are cached forever as they'll become stale when your + * application is deployed with changes. You can set a duration + * in seconds if you want the cache to clear after a certain + * timeframe. + */ + 'structure_caching' => [ + 'enabled' => true, + 'directories' => [app_path('Data')], + 'cache' => [ + 'store' => env('CACHE_STORE', env('CACHE_DRIVER', 'file')), + 'prefix' => 'laravel-data', + 'duration' => null, + ], + 'reflection_discovery' => [ + 'enabled' => true, + 'base_path' => base_path(), + 'root_namespace' => null, + ], + ], + + /** + * A data object can be validated when created using a factory or when calling the from + * method. By default, only when a request is passed the data is being validated. This + * behaviour can be changed to always validate or to completely disable validation. + */ + 'validation_strategy' => \Spatie\LaravelData\Support\Creation\ValidationStrategy::OnlyRequests->value, + + /** + * When using an invalid include, exclude, only or except partial, the package will + * throw an exception. You can disable this behaviour by setting this option to true. + */ + 'ignore_invalid_partials' => false, + + /** + * When transforming a nested chain of data objects, the package can end up in an infinite + * loop when including a recursive relationship. The max transformation depth can be + * set as a safety measure to prevent this from happening. When set to null, the + * package will not enforce a maximum depth. + */ + 'max_transformation_depth' => null, + + /** + * When the maximum transformation depth is reached, the package will throw an exception. + * You can disable this behaviour by setting this option to true which will return an + * empty array. + */ + 'throw_when_max_transformation_depth_reached' => true, + + /** + * When using the `make:data` command, the package will use these settings to generate + * the data classes. You can override these settings by passing options to the command. + */ + 'commands' => [ + /** + * Provides default configuration for the `make:data` command. These settings can be overridden with options + * passed directly to the `make:data` command for generating single Data classes, or if not set they will + * automatically fall back to these defaults. See `php artisan make:data --help` for more information. + */ + 'make' => [ + /** + * The default namespace for generated Data classes. This exists under the application's root namespace, + * so the default 'Data` will end up as '\App\Data', and generated Data classes will be placed in the + * app/Data/ folder. Data classes can live anywhere, but this is where `make:data` will put them. + */ + 'namespace' => 'Data', + + /** + * This suffix will be appended to all data classes generated by make:data, so that they are less likely + * to conflict with other related classes, controllers or models with a similar name without resorting + * to adding an alias for the Data object. Set to a blank string (not null) to disable. + */ + 'suffix' => 'Resource', + ], + ], + + /** + * When using Livewire, the package allows you to enable or disable the synths + * these synths will automatically handle the data objects and their + * properties when used in a Livewire component. + */ + 'livewire' => [ + 'enable_synths' => false, + ], +]; diff --git a/config/database.php b/config/database.php index 4252313fe14..da0afd5aa7b 100644 --- a/config/database.php +++ b/config/database.php @@ -16,6 +16,21 @@ 'default' => env('DB_CONNECTION', 'mysql'), + /* + |-------------------------------------------------------------------------- + | Log DB SQL statements + |-------------------------------------------------------------------------- + | + | If set to true, all SQL statements will be logged to a text file below + | storage. + | Only use it for debugging and development purposes as it slows down + | the performance of the application + | + */ + + 'db_log_sql' => (bool) env('DB_LOG_SQL', false), + 'explain' => (bool) env('DB_LOG_SQL_EXPLAIN', false), + /* |-------------------------------------------------------------------------- | Database Connections @@ -38,7 +53,7 @@ 'url' => env('DATABASE_URL'), 'database' => env('DB_DATABASE', database_path('database.sqlite')), 'prefix' => '', - 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), + 'foreign_key_constraints' => true, ], 'mysql' => [ @@ -66,7 +81,40 @@ 'engine' => 'InnoDB ROW_FORMAT=DYNAMIC', 'options' => extension_loaded('pdo_mysql') ? array_filter([ PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), - ]) : [], + ], + fn ($elem) => ($elem !== null && $elem !== ''), + ) : [], + // Ensure a deterministic SQL mode for MySQL/MariaDB. + // Don't rely on accidentally correct, system-wide settings of the + // DB service. + 'modes' => [ + // If strict mode is not enable, MySQL "cleverly" converts + // invalid data on INSERT/UPDATE/etc. to something which MySQL + // believes you wanted. + // Overflow/underflow of values is silently ignored. + // We want strict mode, because any error probably indicates + // a bug in Lychee which should be fixed. + 'STRICT_ALL_TABLES', + // same as above, but for transactional storage engines, like InnoDB + 'STRICT_TRANS_TABLES', + // Nomen est omen, for some versions of MySQL not included in + // `STRICT_ALL_TABLES` and hence must be set separately. + 'ERROR_FOR_DIVISION_BY_ZERO', + // don't accept 00.00.0000 as a date + 'NO_ZERO_DATE', + // don't accept dates as valid whose month or day component is + // zero, i.e. refuse 00.05.2021 or 13.00.2021 as invalid + 'NO_ZERO_IN_DATE', + // Disable the probably most stupid feature of MySQL. + // If one INSERTS a DB row with id=0, then MySQL replaces the + // ID with latest auto-increment value plus one. WTF?! + // As our admin user has ID=0, we want 0 to be 0 when we + // insert 0 and not some "auto-magical" replacement. + 'NO_AUTO_VALUE_ON_ZERO', + // Don't silently use another DB engine, if the selected + // DB engin (InnoDB) is not available. + 'NO_ENGINE_SUBSTITUTION ', + ], ], 'pgsql' => [ @@ -88,7 +136,7 @@ 'timezone' => 'UTC', 'prefix' => '', 'prefix_indexes' => true, - 'schema' => 'public', + 'search_path' => 'public', 'sslmode' => 'prefer', ], @@ -131,22 +179,26 @@ */ 'redis' => [ - 'client' => env('REDIS_CLIENT', 'phpredis'), + 'client' => 'phpredis', 'options' => [ 'cluster' => env('REDIS_CLUSTER', 'redis'), - 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_') . '_database_'), + 'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'Lychee'), '_') . '_database_'), ], 'default' => [ + 'scheme' => env('REDIS_SCHEME', 'tcp'), + 'path' => env('REDIS_PATH', null), 'url' => env('REDIS_URL'), 'host' => env('REDIS_HOST', '127.0.0.1'), - 'password' => env('REDIS_PASSWORD', null), + 'password' => env('REDIS_PASSWORD'), 'port' => env('REDIS_PORT', '6379'), 'database' => env('REDIS_DB', '0'), ], 'cache' => [ + 'scheme' => env('REDIS_SCHEME', 'tcp'), + 'path' => env('REDIS_PATH', null), 'url' => env('REDIS_URL'), 'host' => env('REDIS_HOST', '127.0.0.1'), 'password' => env('REDIS_PASSWORD', null), @@ -154,4 +206,7 @@ 'database' => env('REDIS_CACHE_DB', '1'), ], ], + + // Only list fk keys in debug mode. + 'list_foreign_keys' => (bool) env('DB_LIST_FOREIGN_KEYS', false) && (bool) env('APP_DEBUG', false), ]; diff --git a/config/debugbar.php b/config/debugbar.php index 0b32abb98db..274c8285deb 100644 --- a/config/debugbar.php +++ b/config/debugbar.php @@ -135,26 +135,26 @@ 'options' => [ 'auth' => [ - 'show_name' => true, // Also show the users name/email in the debugbar + 'show_name' => true, // Also show the users name/email in the debugbar ], 'db' => [ - 'with_params' => true, // Render SQL with the parameters substituted - 'backtrace' => true, // Use a backtrace to find the origin of the query in your files. - 'timeline' => false, // Add the queries to the timeline - 'explain' => [ // Show EXPLAIN output on queries + 'with_params' => true, // Render SQL with the parameters substituted + 'backtrace' => true, // Use a backtrace to find the origin of the query in your files. + 'timeline' => false, // Add the queries to the timeline + 'explain' => [ // Show EXPLAIN output on queries 'enabled' => false, - 'types' => ['SELECT'], // // workaround ['SELECT'] only. https://github.com/barryvdh/laravel-debugbar/issues/888 ['SELECT', 'INSERT', 'UPDATE', 'DELETE']; for MySQL 5.6.3+ + 'types' => ['SELECT'], // workaround ['SELECT'] only. https://github.com/barryvdh/laravel-debugbar/issues/888 ['SELECT', 'INSERT', 'UPDATE', 'DELETE']; for MySQL 5.6.3+ ], - 'hints' => true, // Show hints for common mistakes + 'hints' => true, // Show hints for common mistakes ], 'mail' => [ 'full_log' => false, ], 'views' => [ - 'data' => false, //Note: Can slow down the application, because the data can be quite large.. + 'data' => false, // Note: Can slow down the application, because the data can be quite large.. ], 'route' => [ - 'label' => true, // show complete route on bar + 'label' => true, // show complete route on bar ], 'logs' => [ 'file' => null, diff --git a/config/defines.php b/config/defines.php deleted file mode 100644 index d28337a9f9b..00000000000 --- a/config/defines.php +++ /dev/null @@ -1,14 +0,0 @@ - [ - 'LYCHEE_STATUS_NOCONFIG' => 0, - 'LYCHEE_STATUS_LOGGEDOUT' => 1, - 'LYCHEE_STATUS_LOGGEDIN' => 2, - ], - - 'defaults' => [ - 'SITE_TITLE' => 'Lychee v4', - ], -]; \ No newline at end of file diff --git a/config/features.php b/config/features.php new file mode 100644 index 00000000000..37ee4cdc43f --- /dev/null +++ b/config/features.php @@ -0,0 +1,85 @@ + (bool) env('VUEJS_ENABLED', true), + + /* + |-------------------------------------------------------------------------- + | Use Legacy API + |-------------------------------------------------------------------------- + | + | This value determines whether the Legacy API is still available. + | + */ + 'legacy_api' => (bool) env('LEGACY_API_ENABLED', !(bool) env('VUEJS_ENABLED', true)), + + /* + |-------------------------------------------------------------------------- + | Force HTTPS + |-------------------------------------------------------------------------- + | + | When running behind a proxy, it may be necessary for the urls to be + | set as https for the reverse translation. You should set this if you + | want to force the https scheme. + */ + 'force_https' => (bool) env('APP_FORCE_HTTPS', false), + + /* + |-------------------------------------------------------------------------- + | Enable v4 redirections + |-------------------------------------------------------------------------- + | + | When using new front-end old links to /#albumID/PhotoID are broken. + | This provides here a way to avoid those. + */ + 'legacy_v4_redirect' => (bool) env('LEGACY_V4_REDIRECT', false), + + /* + |-------------------------------------------------------------------------- + | Log Viewer + |-------------------------------------------------------------------------- + | + | Log Viewer can be disabled, so it's no longer accessible via browser. + */ + 'log-viewer' => (bool) env('LOG_VIEWER_ENABLED', true), + + /* + |-------------------------------------------------------------------------- + | Use new code path when importing photos + |-------------------------------------------------------------------------- + | + | Use pipeline design pattern instead of hardcoded Strategies. + */ + 'create-photo-via-pipes' => (bool) env('PHOTO_PIPES', true), + + /* + |-------------------------------------------------------------------------- + | Use S3 buckets instead of local hosting. + |-------------------------------------------------------------------------- + | + | Put images on AWS instead of locally to save space. + */ + 'use-s3' => (env('AWS_ACCESS_KEY_ID', '') !== '') && (bool) env('S3_ENABLED', false), + + /* + |-------------------------------------------------------------------------- + | Hide Lychee SE from config to allow for easier video + |-------------------------------------------------------------------------- + */ + 'hide-lychee-SE' => (bool) env('HIDE_LYCHEE_SE_CONFIG', false), +]; \ No newline at end of file diff --git a/config/feed.php b/config/feed.php index ca61afe0262..b66cc4748d3 100644 --- a/config/feed.php +++ b/config/feed.php @@ -6,12 +6,12 @@ /* * Here you can specify which class and method will return * the items that should appear in the feed. For example: - * 'App\Model@getAllFeedItems' + * [App\Model::class, 'getAllFeedItems'] * - * You can also pass an argument to that method: - * ['App\Model@getAllFeedItems', 'argument'] + * You can also pass an argument to that method. Note that their key must be the name of the parameter: * + * [App\Model::class, 'getAllFeedItems', 'parameterName' => 'argument'] */ - 'items' => 'App\Http\Controllers\RSSController@getRSS', + 'items' => [App\Http\Controllers\RSSController::class, 'getRSS'], /* * The feed will be available on this url. @@ -19,16 +19,39 @@ // ! This is due to Spacie fucking up... See here: spatie/laravel-feed#151 // Hopefully this will be fixed soon... // ? Correct value should be '/feed' - 'url' => '', + 'url' => '/feed', 'title' => 'Latest pictures', 'description' => 'Latest added pictures.', 'language' => 'en-US', + /* + * The image to display for the feed. For Atom feeds, this is displayed as + * a banner/logo; for RSS and JSON feeds, it's displayed as an icon. + * An empty value omits the image attribute from the feed. + */ + 'image' => '', + + /* + * The format of the feed. Acceptable values are 'rss', 'atom', or 'json'. + */ + 'format' => 'rss', + /* * The view that will render the feed. */ 'view' => 'feed::rss', - ], + + /* + * The mime type to be used in the tag. Set to an empty string to automatically + * determine the correct value. + */ + 'type' => '', + + /* + * The content type for the feed response. Set to an empty string to automatically + * determine the correct value. + */ + 'contentType' => '', ], ], ]; diff --git a/config/filesystems.php b/config/filesystems.php index c61dca7eb4e..b79f3149d55 100644 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -1,5 +1,34 @@ env('FILESYSTEM_DRIVER', 'images'), + 'default' => 'images', /* |-------------------------------------------------------------------------- @@ -41,62 +70,92 @@ */ 'disks' => [ - 'local' => [ + // Lychee uses the disk "images" to store the media files + 'images' => [ 'driver' => 'local', - 'root' => storage_path('app'), + 'root' => env('LYCHEE_UPLOADS', public_path((string) env('LYCHEE_UPLOADS_DIR', 'uploads/'))), + 'url' => env('LYCHEE_UPLOADS_URL', '') !== '' ? renv('LYCHEE_UPLOADS_URL') + : (renv('APP_URL', '') . renv_cond('APP_DIR') . '/' . + renv('LYCHEE_UPLOADS_DIR', 'uploads')), + 'visibility' => env('LYCHEE_IMAGE_VISIBILITY', 'public'), + 'directory_visibility' => env('LYCHEE_IMAGE_VISIBILITY', 'public'), + 'permissions' => [ + 'file' => [ + 'world' => 00666, + 'public' => 00664, + 'private' => 00660, + ], + 'dir' => [ + 'world' => 02777, + 'public' => 02775, + 'private' => 02770, + ], + ], ], - 'images' => [ - 'driver' => 'local', - 'root' => env('LYCHEE_UPLOADS', public_path('uploads/')), - 'url' => env('LYCHEE_UPLOADS_URL', 'uploads/'), - 'visibility' => 'public', + 's3' => [ + 'driver' => 's3', + 'key' => env('AWS_ACCESS_KEY_ID', ''), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'bucket' => env('AWS_BUCKET'), + 'url' => env('AWS_URL'), + 'endpoint' => env('AWS_ENDPOINT'), + 'visibility' => env('AWS_IMAGE_VISIBILITY', 'public'), + 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), + 'throw' => true, ], + // Lychee uses this disk to store the customized CSS file provided by the user + // ATTENTION: This disk MUST ALWAYS point to the local `./public/dist` directory. + // TODO: Maybe we should drop this Flysystem disk, because neither the driver nor the root must be changed and hence the whole point of using the Flysystem abstraction is gone. 'dist' => [ 'driver' => 'local', 'root' => env('LYCHEE_DIST', public_path('dist/')), - 'url' => env('LYCHEE_DIST_URL', 'dist/'), + 'url' => env('LYCHEE_DIST_URL', renv_cond('APP_DIR') . '/dist/'), 'visibility' => 'public', ], + // Lychee uses this disk to create ephemeral, symbolic links to photos, + // if the feature is enabled. + // For this feature to work, the "images" disk must use the "local" driver. + // ATTENTION: This disk MUST ALWAYS use the "local" driver, because + // Flysystem does not support symbolic links. 'symbolic' => [ 'driver' => 'local', - 'root' => public_path('sym'), - 'url' => 'sym', + 'root' => env('LYCHEE_SYM', public_path('sym')), + 'url' => env('LYCHEE_SYM_URL', '') !== '' ? renv('LYCHEE_SYM_URL') : + (renv('APP_URL', 'http://localhost') . renv_cond('APP_DIR') . '/sym'), 'visibility' => 'public', ], - 'public' => [ + // We use this space to temporarily store images when uploading. + // Mostly chunks and incomplete images are placed here + 'image-upload' => [ 'driver' => 'local', - 'root' => storage_path('app/public'), - 'url' => env('APP_URL') . '/storage', - 'visibility' => 'public', + 'root' => env('LYCHEE_TMP_UPLOAD', storage_path('tmp/uploads')), + 'visibility' => 'private', ], - 's3' => [ - 'driver' => 's3', - 'key' => env('AWS_ACCESS_KEY_ID'), - 'secret' => env('AWS_SECRET_ACCESS_KEY'), - 'region' => env('AWS_DEFAULT_REGION'), - 'bucket' => env('AWS_BUCKET'), - 'url' => env('AWS_URL'), - 'endpoint' => env('AWS_ENDPOINT'), + // We use this space to process the images, + 'image-jobs' => [ + 'driver' => 'local', + 'root' => env('LYCHEE_IMAGE_JOBS', storage_path('tmp/jobs')), + 'visibility' => 'private', ], - ], - /* - |-------------------------------------------------------------------------- - | Symbolic Links - |-------------------------------------------------------------------------- - | - | Here you may configure the symbolic links that will be created when the - | `storage:link` Artisan command is executed. The array keys should be - | the locations of the links and the values should be their targets. - | - */ + // This is where we extract zip files before importing them. + 'extract-jobs' => [ + 'driver' => 'local', + 'root' => env('LYCHEE_EXTRACT_JOBS', storage_path('tmp/extract')), + 'visibility' => 'private', + ], - 'links' => [ - public_path('storage') => storage_path('app/public'), + // For tests purposes + 'tmp-for-tests' => [ + 'driver' => 'local', + 'root' => storage_path('tmp/uploads'), + 'visibility' => 'private', + ], ], ]; diff --git a/config/hashing.php b/config/hashing.php index 0725a9d0ba8..1741309541d 100644 --- a/config/hashing.php +++ b/config/hashing.php @@ -14,7 +14,7 @@ | */ - 'driver' => 'bcrypt', + 'driver' => env('HASHING_ALGORITHM', 'bcrypt'), /* |-------------------------------------------------------------------------- @@ -43,8 +43,8 @@ */ 'argon' => [ - 'memory' => 1024, - 'threads' => 2, - 'time' => 2, + 'memory' => env('ARGON_MEMORY', 1024), + 'threads' => env('ARGON_THREADS', 2), + 'time' => env('ARGON_TIME', 2), ], ]; diff --git a/config/honeypot.php b/config/honeypot.php new file mode 100644 index 00000000000..138727dd0b3 --- /dev/null +++ b/config/honeypot.php @@ -0,0 +1,125 @@ + true, + + /** + * Honey. + * + * Set of possible path. + * Those will be concatenated into a regex. + */ + 'paths' => [ + '.env', + '.git/config', + '.git/HEAD', + '.well-known/security.txt', + '.well-known/traffic-advice', + + 'readme.txt', + 'pools', + 'pools/default/buckets', + '__Additional', + + 'CSS/Miniweb.css', + 'wp-login.php', + 'wp-content/plugins/core-plugin/include.php', + 'wp-content/plugins/woocommerce/readme.txt', + 'Portal/Portal.mwsl', + 'Portal0000.htm', + + 'ads.txt', + 'aQQY', + 'UEPs', + 'HNAP1', + 'nmaplowercheck1686252089', + 'sdk', + + 'backup', + 'bc', + 'bk', + 'blog', + 'home', + 'main', + 'new', + 'newsite', + 'old', + 'test', + 'testing', + 'wordpress', + 'wp-admin/install.php', + 'wp-admin/setup-config.php', + 'wp', + 'xmlrpc.php', + + '.vscode/sftp.json', + 'aws.json', + 'awsconfig.json', + 'AwsConfig.json', + 'client_secrets.json', + 'conf.json', + 'config/config.json', + 'credentials/config.json', + 'database-config.json', + 'db.json', + 'env.json', + 'smtp.json', + 'ssh-config.json', + 'user-config.json', + ], + + /** + * Because of all the combinations, it is more interesting to do a cross product. + */ + 'xpaths' => [ + [ // admin, main default etc. + 'prefix' => [ + 'admin', + 'base', + 'default', + 'home', + 'indice', + 'inicio', + 'localstart', + 'main', + 'menu', + 'start', + ], + 'suffix' => [ + '.asp', + '.aspx', + '.cgi', + '.cfm', + '.html', + '.jhtml', + '.inc', + '.jsa', + '.jsp', + '.php', + '.pl', + '.shtml', + ], + ], + [ // phpinfo sets + 'prefix' => [ + '', + '_', + '__', + 'html/', + ], + 'suffix' => [ + 'info.php', + 'phpinfo.php', + ], + ], + ], +]; \ No newline at end of file diff --git a/config/image-optimizer.php b/config/image-optimizer.php index f6810052b29..76f3a873181 100644 --- a/config/image-optimizer.php +++ b/config/image-optimizer.php @@ -1,6 +1,5 @@ LogFunctions::class, + 'log_optimizer_activity' => true, ]; diff --git a/config/larapass.php b/config/larapass.php deleted file mode 100644 index eff41614676..00000000000 --- a/config/larapass.php +++ /dev/null @@ -1,155 +0,0 @@ - [ - 'name' => env('WEBAUTHN_NAME', env('APP_NAME', 'Lychee')), - 'id' => env('WEBAUTHN_ID'), - 'icon' => env('WEBAUTHN_ICON'), - ], - - /* - |-------------------------------------------------------------------------- - | Challenge configuration - |-------------------------------------------------------------------------- - | - | When making challenges your application needs to push at least 16 bytes - | of randomness. Since we need to later check them, we'll also store the - | bytes for a sensible amount of seconds inside your default app cache. - | - */ - - 'bytes' => 16, - 'timeout' => 60, - 'cache' => env('WEBAUTHN_CACHE'), - - /* - |-------------------------------------------------------------------------- - | Algorithms - |-------------------------------------------------------------------------- - | - | Here are default algorithms to use when asking to create sign and encrypt - | binary objects like a public key and a challenge. These works almost in - | any device, but you can add or change these depending on your devices. - | - | @see https://www.iana.org/assignments/cose/cose.xhtml#algorithms - | - */ - - 'algorithms' => [ - \Cose\Algorithm\Signature\ECDSA\ES256::class, // ECDSA with SHA-256 - \Cose\Algorithm\Signature\EdDSA\Ed25519::class, // EdDSA - \Cose\Algorithm\Signature\ECDSA\ES384::class, // ECDSA with SHA-384 - \Cose\Algorithm\Signature\ECDSA\ES512::class, // ECDSA with SHA-512 - \Cose\Algorithm\Signature\RSA\RS256::class, // RSASSA-PKCS1-v1_5 with SHA-256 - ], - - /* - |-------------------------------------------------------------------------- - | Credentials Attachment. - |-------------------------------------------------------------------------- - | - | Authentication can be tied to the current device (like when using Windows - | Hello or Touch ID) or a cross-platform device (like USB Key). When this - | is "null" the user will decide where to store their authentication info. - | - | Supported: "null", "cross-platform", "platform". - | - */ - - 'attachment' => 'null', - - /* - |-------------------------------------------------------------------------- - | Attestation Conveyance - |-------------------------------------------------------------------------- - | - | The attestation is the data about the device and the public key used to - | sign. Using "none" means the data is meaningless, "indirect" allows to - | receive anonymized data, and "direct" means to receive the real data. - | - | Supported: "none", "indirect", "direct". - | - */ - - 'conveyance' => 'none', - - /* - |-------------------------------------------------------------------------- - | User presence and verification - |-------------------------------------------------------------------------- - | - | Most authenticators and smartphones will ask the user to actively verify - | themselves for log in. Use "required" to always ask verify, "preferred" - | to ask when possible, and "discouraged" to just ask for user presence. - | - | Supported: "required", "preferred", "discouraged". - | - */ - - 'login_verify' => 'discouraged', - - /* - |-------------------------------------------------------------------------- - | Userless (One touch, Typeless) login - |-------------------------------------------------------------------------- - | - | By default the user must input its username to receive which credentials - | can use to login. If this is activated, and the device supports it, the - | public key and ID can be stored inside the device for one-touch login. - | - | Supported: "null", "required", "preferred", "discouraged". - | - */ - - 'userless' => 'discouraged', - - /* - |-------------------------------------------------------------------------- - | Credential limit - |-------------------------------------------------------------------------- - | - | Authenticators can have multiple credentials for the same user account. - | To limit one device per user account, you can set this to true. This - | will force the attest to fail when registering another credential. - | - */ - - 'unique' => false, - - /* - |-------------------------------------------------------------------------- - | Password Fallback - |-------------------------------------------------------------------------- - | - | When using the `eloquent-webauthn´ user provider you will be able to use - | the same user provider to authenticate users using their password. When - | disabling this, users will be strictly authenticated only by WebAuthn. - | - */ - - 'fallback' => false, - - /* - |-------------------------------------------------------------------------- - | Device Confirmation - |-------------------------------------------------------------------------- - | - | If you're using the "webauthn.confirm" middleware in your routes you may - | want to adjust the time the confirmation is remembered in the browser. - | This is measured in seconds, but it can be overridden in the route. - | - */ - - 'confirm_timeout' => 10800, // 3 hours -]; diff --git a/config/log-viewer.php b/config/log-viewer.php new file mode 100644 index 00000000000..00118ba58f3 --- /dev/null +++ b/config/log-viewer.php @@ -0,0 +1,244 @@ + env('LOG_VIEWER_ENABLED', true), + + /* + |-------------------------------------------------------------------------- + | Log Viewer Domain + |-------------------------------------------------------------------------- + | You may change the domain where Log Viewer should be active. + | If the domain is empty, all domains will be valid. + | + */ + + 'route_domain' => null, + + /* + |-------------------------------------------------------------------------- + | Log Viewer Route + |-------------------------------------------------------------------------- + | Log Viewer will be available under this URL. + | + */ + + 'route_path' => 'Logs', + + /* + |-------------------------------------------------------------------------- + | Back to system URL + |-------------------------------------------------------------------------- + | When set, displays a link to easily get back to this URL. + | Set to `null` to hide this link. + | + | Optional label to display for the above URL. + | + */ + + 'back_to_system_url' => renv('APP_URL', 'http://localhost') . renv_cond('APP_DIR') . '/gallery', + + 'back_to_system_label' => null, // Displayed by default: "Back to {{ app.name }}" + + /* + |-------------------------------------------------------------------------- + | Log Viewer time zone. + |-------------------------------------------------------------------------- + | The time zone in which to display the times in the UI. Defaults to + | the application's timezone defined in config/app.php. + | + */ + + 'timezone' => null, + + /* + |-------------------------------------------------------------------------- + | Log Viewer route middleware. + |-------------------------------------------------------------------------- + | Optional middleware to use when loading the initial Log Viewer page. + | + */ + + 'middleware' => [ + 'web-admin', + \App\Http\Middleware\FixStatusCode::class, + \Illuminate\Http\Middleware\TrustProxies::class, + \Opcodes\LogViewer\Http\Middleware\AuthorizeLogViewer::class, + ], + + /* + |-------------------------------------------------------------------------- + | Log Viewer API middleware. + |-------------------------------------------------------------------------- + | Optional middleware to use on every API request. The same API is also + | used from within the Log Viewer user interface. + | + */ + + 'api_middleware' => [ + \App\Http\Middleware\FixStatusCode::class, + \Illuminate\Http\Middleware\TrustProxies::class, + \Opcodes\LogViewer\Http\Middleware\EnsureFrontendRequestsAreStateful::class, + \Illuminate\Cookie\Middleware\EncryptCookies::class, + \Illuminate\Session\Middleware\StartSession::class, + \Illuminate\Session\Middleware\AuthenticateSession::class, + \App\Http\Middleware\VerifyCsrfToken::class, + \Opcodes\LogViewer\Http\Middleware\AuthorizeLogViewer::class, + ], + + /* + |-------------------------------------------------------------------------- + | Log Viewer Remote hosts. + |-------------------------------------------------------------------------- + | Log Viewer supports viewing Laravel logs from remote hosts. They must + | be running Log Viewer as well. Below you can define the hosts you + | would like to show in this Log Viewer instance. + | + */ + + 'hosts' => [ + 'local' => [ + 'name' => ucfirst((string) env('APP_ENV', 'local')), + ], + + // 'staging' => [ + // 'name' => 'Staging', + // 'host' => 'https://staging.example.com/log-viewer', + // 'auth' => [ // Example of HTTP Basic auth + // 'username' => 'username', + // 'password' => 'password', + // ], + // ], + // + // 'production' => [ + // 'name' => 'Production', + // 'host' => 'https://example.com/log-viewer', + // 'auth' => [ // Example of Bearer token auth + // 'token' => env('LOG_VIEWER_PRODUCTION_TOKEN'), + // ], + // 'headers' => [ + // 'X-Foo' => 'Bar', + // ], + // ], + ], + + /* + |-------------------------------------------------------------------------- + | Include file patterns + |-------------------------------------------------------------------------- + | + */ + + 'include_files' => [ + '*.log', + '**/*.log', + // '/absolute/paths/supported', + ], + + /* + |-------------------------------------------------------------------------- + | Exclude file patterns. + |-------------------------------------------------------------------------- + | This will take precedence over included files. + | + */ + + 'exclude_files' => [ + // 'my_secret.log' + ], + + /* + |-------------------------------------------------------------------------- + | Shorter stack trace filters. + |-------------------------------------------------------------------------- + | Lines containing any of these strings will be excluded from the full log. + | This setting is only active when the function is enabled via the user interface. + | + */ + + 'shorter_stack_trace_excludes' => [ + '/vendor/symfony/', + '/vendor/laravel/framework/', + '/vendor/barryvdh/laravel-debugbar/', + ], + + /* + |-------------------------------------------------------------------------- + | Log matching patterns + |-------------------------------------------------------------------------- + | Regexes for matching log files + | + */ + + 'patterns' => [ + 'laravel' => [ + 'log_matching_regex' => '/^\[(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}\.?(\d{6}([\+-]\d\d:\d\d)?)?)\].*/', + + /** + * This pattern, used for processing Laravel logs, returns these results: + * $matches[0] - the full log line being tested. + * $matches[1] - full timestamp between the square brackets (includes microseconds and timezone offset) + * $matches[2] - timestamp microseconds, if available + * $matches[3] - timestamp timezone offset, if available + * $matches[4] - contents between timestamp and the severity level + * $matches[5] - environment (local, production, etc) + * $matches[6] - log severity (info, debug, error, etc) + * $matches[7] - the log text, the rest of the text. + */ + 'log_parsing_regex' => '/^\[(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}\.?(\d{6}([\+-]\d\d:\d\d)?)?)\](.*?(\w+)\.|.*?)(' + . implode('|', array_filter(LevelClass::caseValues(), fn ($elem) => ($elem !== null && $elem !== ''))) + . ')?: (.*?)( in [\/].*?:[0-9]+)?$/is', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Cache driver + |-------------------------------------------------------------------------- + | Cache driver to use for storing the log indices. Indices are used to speed up + | log navigation. Defaults to your application's default cache driver. + | + */ + + 'cache_driver' => env('LOG_VIEWER_CACHE_DRIVER', null), + + /* + |-------------------------------------------------------------------------- + | Chunk size when scanning log files lazily + |-------------------------------------------------------------------------- + | The size in MB of files to scan before updating the progress bar when searching across all files. + | + */ + + 'lazy_scan_chunk_size_in_mb' => 50, +]; diff --git a/config/logging.php b/config/logging.php index c22f0dc0e13..58712e9490e 100644 --- a/config/logging.php +++ b/config/logging.php @@ -1,9 +1,5 @@ env('LOG_CHANNEL', 'stack'), + 'default' => 'stack', + + /* + |-------------------------------------------------------------------------- + | Deprecations Log Channel + |-------------------------------------------------------------------------- + | + | This option controls the log channel that should be used to log warnings + | regarding deprecated PHP and library features. This allows you to get + | your application ready for upcoming major versions of dependencies. + | + */ + + 'deprecations' => [ + 'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'), + 'trace' => false, + ], /* |-------------------------------------------------------------------------- @@ -36,67 +48,45 @@ 'channels' => [ 'stack' => [ 'driver' => 'stack', - 'channels' => ['single'], - 'ignore_exceptions' => false, - ], - - 'single' => [ - 'driver' => 'single', - 'path' => storage_path('logs/laravel.log'), - 'level' => 'debug', + 'channels' => ['debug-daily', 'error', 'warning', 'notice'], ], - 'daily' => [ + // Whatever debug log is needed + // Mostly SQL requests + 'debug-daily' => [ + 'path' => storage_path('logs/daily.log'), 'driver' => 'daily', - 'path' => storage_path('logs/laravel.log'), 'level' => 'debug', - 'days' => 14, ], - 'slack' => [ - 'driver' => 'slack', - 'url' => env('LOG_SLACK_WEBHOOK_URL'), - 'username' => 'Laravel Log', - 'emoji' => ':boom:', - 'level' => 'critical', - ], - - 'papertrail' => [ - 'driver' => 'monolog', - 'level' => 'debug', - 'handler' => SyslogUdpHandler::class, - 'handler_with' => [ - 'host' => env('PAPERTRAIL_URL'), - 'port' => env('PAPERTRAIL_PORT'), - ], - ], - - 'stderr' => [ - 'driver' => 'monolog', - 'handler' => StreamHandler::class, - 'formatter' => env('LOG_STDERR_FORMATTER'), - 'with' => [ - 'stream' => 'php://stderr', - ], - ], - - 'syslog' => [ - 'driver' => 'syslog', - 'level' => 'debug', + // Something went wrong + 'error' => [ + 'path' => storage_path('logs/errors.log'), + 'driver' => 'single', + 'level' => 'error', + 'bubble' => false, ], - 'errorlog' => [ - 'driver' => 'errorlog', - 'level' => 'debug', + // Something may have gone wrong + 'warning' => [ + 'path' => storage_path('logs/warning.log'), + 'driver' => 'single', + 'level' => 'warning', + 'bubble' => false, ], - 'null' => [ - 'driver' => 'monolog', - 'handler' => NullHandler::class, + // By the way... + 'notice' => [ + 'path' => storage_path('logs/notice.log'), + 'driver' => 'daily', + 'level' => 'notice', ], - 'emergency' => [ - 'path' => storage_path('logs/laravel.log'), + // Specific channel to check who is accessing Lychee + 'login' => [ + 'path' => storage_path('logs/login.log'), + 'driver' => 'single', + 'level' => 'info', ], ], ]; diff --git a/config/mail.php b/config/mail.php index ceefe4c1c25..aa5712ae9fb 100644 --- a/config/mail.php +++ b/config/mail.php @@ -12,7 +12,7 @@ | */ - 'default' => env('MAIL_MAILER', 'smtp'), + 'default' => env('MAIL_DRIVER', 'smtp'), /* |-------------------------------------------------------------------------- @@ -35,13 +35,13 @@ 'mailers' => [ 'smtp' => [ 'transport' => 'smtp', - 'host' => env('MAIL_HOST', 'smtp.mailgun.org'), + 'host' => env('MAIL_HOST'), 'port' => env('MAIL_PORT', 587), 'encryption' => env('MAIL_ENCRYPTION', 'tls'), 'username' => env('MAIL_USERNAME'), 'password' => env('MAIL_PASSWORD'), 'timeout' => null, - 'auth_mode' => null, + 'local_domain' => env('MAIL_EHLO_DOMAIN'), ], 'ses' => [ @@ -58,17 +58,20 @@ 'sendmail' => [ 'transport' => 'sendmail', - 'path' => '/usr/sbin/sendmail -bs', - ], - - 'log' => [ - 'transport' => 'log', - 'channel' => env('MAIL_LOG_CHANNEL'), + 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'), ], 'array' => [ 'transport' => 'array', ], + + 'failover' => [ + 'transport' => 'failover', + 'mailers' => [ + 'smtp', + 'log', + ], + ], ], /* @@ -83,8 +86,8 @@ */ 'from' => [ - 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), - 'name' => env('MAIL_FROM_NAME', 'Example'), + 'address' => env('MAIL_FROM_ADDRESS'), + 'name' => env('MAIL_FROM_NAME', env('APP_NAME', 'Lychee')), ], /* diff --git a/config/markdown.php b/config/markdown.php index 48257c88511..01ac337615b 100644 --- a/config/markdown.php +++ b/config/markdown.php @@ -5,154 +5,150 @@ /* * This file is part of Laravel Markdown. * - * (c) Graham Campbell + * (c) Graham Campbell * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ return [ - - /* - |-------------------------------------------------------------------------- - | Enable View Integration - |-------------------------------------------------------------------------- - | - | This option specifies if the view integration is enabled so you can write - | markdown views and have them rendered as html. The following extensions - | are currently supported: ".md", ".md.php", and ".md.blade.php". You may - | disable this integration if it is conflicting with another package. - | - | Default: true - | - */ - - 'views' => true, - - /* - |-------------------------------------------------------------------------- - | CommonMark Extensions - |-------------------------------------------------------------------------- - | - | This option specifies what extensions will be automatically enabled. - | Simply provide your extension class names here. - | - | Default: [] - | - */ - - 'extensions' => [], - - /* - |-------------------------------------------------------------------------- - | Renderer Configuration - |-------------------------------------------------------------------------- - | - | This option specifies an array of options for rendering HTML. - | - | Default: [ - | 'block_separator' => "\n", - | 'inner_separator' => "\n", - | 'soft_break' => "\n", - | ] - | - */ - - 'renderer' => [ - 'block_separator' => "\n", - 'inner_separator' => "\n", - 'soft_break' => "\n", - ], - - /* - |-------------------------------------------------------------------------- - | Enable Em Tag Parsing - |-------------------------------------------------------------------------- - | - | This option specifies if `` parsing is enabled. - | - | Default: true - | - */ - - 'enable_em' => true, - - /* - |-------------------------------------------------------------------------- - | Enable Strong Tag Parsing - |-------------------------------------------------------------------------- - | - | This option specifies if `` parsing is enabled. - | - | Default: true - | - */ - - 'enable_strong' => true, - - /* - |-------------------------------------------------------------------------- - | Enable Asterisk Parsing - |-------------------------------------------------------------------------- - | - | This option specifies if `*` should be parsed for emphasis. - | - | Default: true - | - */ - - 'use_asterisk' => true, - - /* - |-------------------------------------------------------------------------- - | Enable Underscore Parsing - |-------------------------------------------------------------------------- - | - | This option specifies if `_` should be parsed for emphasis. - | - | Default: true - | - */ - - 'use_underscore' => true, - - /* - |-------------------------------------------------------------------------- - | HTML Input - |-------------------------------------------------------------------------- - | - | This option specifies how to handle untrusted HTML input. - | - | Default: 'strip' - | - */ - - 'html_input' => 'strip', - - /* - |-------------------------------------------------------------------------- - | Allow Unsafe Links - |-------------------------------------------------------------------------- - | - | This option specifies whether to allow risky image URLs and links. - | - | Default: true - | - */ - - 'allow_unsafe_links' => true, - - /* - |-------------------------------------------------------------------------- - | Maximum Nesting Level - |-------------------------------------------------------------------------- - | - | This option specifies the maximum permitted block nesting level. - | - | Default: INF - | - */ - - 'max_nesting_level' => INF, - -]; + /* + |-------------------------------------------------------------------------- + | Enable View Integration + |-------------------------------------------------------------------------- + | + | This option specifies if the view integration is enabled so you can write + | markdown views and have them rendered as html. The following extensions + | are currently supported: ".md", ".md.php", and ".md.blade.php". You may + | disable this integration if it is conflicting with another package. + | + | Default: true + | + */ + + 'views' => true, + + /* + |-------------------------------------------------------------------------- + | CommonMark Extensions + |-------------------------------------------------------------------------- + | + | This option specifies what extensions will be automatically enabled. + | Simply provide your extension class names here. + | + | Default: [ + | League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension::class, + | League\CommonMark\Extension\Table\TableExtension::class, + | ] + | + */ + + 'extensions' => [ + League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension::class, + League\CommonMark\Extension\Table\TableExtension::class, + ], + + /* + |-------------------------------------------------------------------------- + | Renderer Configuration + |-------------------------------------------------------------------------- + | + | This option specifies an array of options for rendering HTML. + | + | Default: [ + | 'block_separator' => "\n", + | 'inner_separator' => "\n", + | 'soft_break' => "\n", + | ] + | + */ + + 'renderer' => [ + 'block_separator' => "\n", + 'inner_separator' => "\n", + 'soft_break' => "\n", + ], + + /* + |-------------------------------------------------------------------------- + | Commonmark Configuration + |-------------------------------------------------------------------------- + | + | This option specifies an array of options for commonmark. + | + | Default: [ + | 'enable_em' => true, + | 'enable_strong' => true, + | 'use_asterisk' => true, + | 'use_underscore' => true, + | 'unordered_list_markers' => ['-', '+', '*'], + | ] + | + */ + + 'commonmark' => [ + 'enable_em' => true, + 'enable_strong' => true, + 'use_asterisk' => true, + 'use_underscore' => true, + 'unordered_list_markers' => ['-', '+', '*'], + ], + + /* + |-------------------------------------------------------------------------- + | HTML Input + |-------------------------------------------------------------------------- + | + | This option specifies how to handle untrusted HTML input. + | + | Default: 'strip' + | + */ + + 'html_input' => 'strip', + + /* + |-------------------------------------------------------------------------- + | Allow Unsafe Links + |-------------------------------------------------------------------------- + | + | This option specifies whether to allow risky image URLs and links. + | + | Default: true + | + */ + + 'allow_unsafe_links' => true, + + /* + |-------------------------------------------------------------------------- + | Maximum Nesting Level + |-------------------------------------------------------------------------- + | + | This option specifies the maximum permitted block nesting level. + | + | Default: PHP_INT_MAX + | + */ + + 'max_nesting_level' => PHP_INT_MAX, + + /* + |-------------------------------------------------------------------------- + | Slug Normalizer + |-------------------------------------------------------------------------- + | + | This option specifies an array of options for slug normalization. + | + | Default: [ + | 'max_length' => 255, + | 'unique' => 'document', + | ] + | + */ + + 'slug_normalizer' => [ + 'max_length' => 255, + 'unique' => 'document', + ], +]; \ No newline at end of file diff --git a/config/queue.php b/config/queue.php index 628b39e8835..4064a5d5cfb 100644 --- a/config/queue.php +++ b/config/queue.php @@ -37,6 +37,7 @@ 'table' => 'jobs', 'queue' => 'default', 'retry_after' => 90, + 'after_commit' => false, ], 'beanstalkd' => [ @@ -45,6 +46,7 @@ 'queue' => 'default', 'retry_after' => 90, 'block_for' => 0, + 'after_commit' => false, ], 'sqs' => [ @@ -52,9 +54,10 @@ 'key' => env('AWS_ACCESS_KEY_ID'), 'secret' => env('AWS_SECRET_ACCESS_KEY'), 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), - 'queue' => env('SQS_QUEUE', 'your-queue-name'), + 'queue' => env('SQS_QUEUE', 'default'), 'suffix' => env('SQS_SUFFIX'), 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'after_commit' => false, ], 'redis' => [ @@ -63,6 +66,7 @@ 'queue' => env('REDIS_QUEUE', 'default'), 'retry_after' => 90, 'block_for' => null, + 'after_commit' => false, ], ], diff --git a/config/scramble.php b/config/scramble.php new file mode 100644 index 00000000000..34d6074ab40 --- /dev/null +++ b/config/scramble.php @@ -0,0 +1,89 @@ + 'api/v2', + + /* + * Your API domain. By default, app domain is used. This is also a part of the default API routes + * matcher, so when implementing your own, make sure you use this config if needed. + */ + 'api_domain' => null, + + /* + * The path where your OpenAPI specification will be exported. + */ + 'export_path' => 'api.json', + + 'info' => [ + /* + * API version. + */ + 'version' => env('API_VERSION', '2.0.0'), + + /* + * Description rendered on the home page of the API documentation (`/docs/api`). + */ + 'description' => '', + ], + + /* + * Customize Stoplight Elements UI + */ + 'ui' => [ + /* + * Define the title of the documentation's website. App name is used when this config is `null`. + */ + 'title' => null, + + /* + * Define the theme of the documentation. Available options are `light` and `dark`. + */ + 'theme' => 'light', + + /* + * Hide the `Try It` feature. Enabled by default. + */ + 'hide_try_it' => false, + + /* + * URL to an image that displays as a small square logo next to the title, above the table of contents. + */ + 'logo' => '', + + /* + * Use to fetch the credential policy for the Try It feature. Options are: omit, include (default), and same-origin + */ + 'try_it_credentials_policy' => 'include', + ], + + /* + * The list of servers of the API. By default, when `null`, server URL will be created from + * `scramble.api_path` and `scramble.api_domain` config variables. When providing an array, you + * will need to specify the local server URL manually (if needed). + * + * Example of non-default config (final URLs are generated using Laravel `url` helper): + * + * ```php + * 'servers' => [ + * 'Live' => 'api', + * 'Prod' => 'https://scramble.dedoc.co/api', + * ], + * ``` + */ + 'servers' => null, + + 'middleware' => [ + 'web', + // RestrictedDocsAccess::class, + ], + + 'extensions' => [ + DataToResponse::class, + ], +]; diff --git a/config/secure-headers.php b/config/secure-headers.php index 5dff77337ca..ca74e438eda 100644 --- a/config/secure-headers.php +++ b/config/secure-headers.php @@ -1,5 +1,7 @@ ((bool) env('DEBUGBAR_ENABLED', false)) ? '' : null, - 'csp' => [ + 'enable' => true, + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only 'report-only' => false, - 'report-uri' => null, + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/report-to + 'report-to' => '', + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/report-uri + 'report-uri' => [ + // uri + ], + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/block-all-mixed-content 'block-all-mixed-content' => false, + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/upgrade-insecure-requests 'upgrade-insecure-requests' => false, - /* - * Please references script-src directive for available values, only `script-src` and `style-src` - * supports `add-generated-nonce`. - * - * Note: when directive value is empty, it will use `none` for that directive. - */ + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/base-uri + 'base-uri' => [ + ], - 'script-src' => [ - 'allow' => [ - 'https://www.dropbox.com/static/api/1/dropins.js', - // 'url', - ], - 'hashes' => [ - 'sha256' => [ - '8bLztrDF3NUpheSuvAzpebgX1DpPJEfhmUHKTwGF4qA=', + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/child-src + 'child-src' => [ + 'allow' => explode(',', (string) env('SECURITY_HEADER_CSP_CHILD_SRC', '')), + ], + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/connect-src + 'connect-src' => array_merge( + ['https://lycheeorg.dev/update.json'], + explode(',', (string) env('SECURITY_HEADER_CSP_CONNECT_SRC', '')) + ), + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/default-src + 'default-src' => [ + 'self' => false, + ], + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/font-src + 'font-src' => [ + 'allow' => explode(',', (string) env('SECURITY_HEADER_CSP_FONT_SRC', '')), + ], + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/form-action + 'form-action' => [ + 'self' => true, + 'allow' => explode(',', (string) env('SECURITY_HEADER_CSP_FORM_ACTION', '')), + ], + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-ancestors + 'frame-ancestors' => [ + ], + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-src + 'frame-src' => [ + 'allow' => explode(',', (string) env('SECURITY_HEADER_CSP_FRAME_SRC', '')), + ], + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/img-src + 'img-src' => [ + 'self' => true, + // Allow OpenStreetMap tile images to be fetched from the different provides + // Allow image to be directly encoded at the img source parameter + 'allow' => array_merge( + [ + 'https://maps.wikimedia.org/osm-intl/', + 'https://tile.openstreetmap.org/', + 'https://tile.openstreetmap.de/', + 'https://a.tile.openstreetmap.fr/osmfr/', + 'https://b.tile.openstreetmap.fr/osmfr/', + 'https://c.tile.openstreetmap.fr/osmfr/', + 'https://a.osm.rrze.fau.de/osmhd/', + 'https://b.osm.rrze.fau.de/osmhd/', + 'https://c.osm.rrze.fau.de/osmhd/', + 'data:', // required by openstreetmap + 'blob:', // required for "live" photos ], - ], - 'nonces' => [ - // 'base64-encoded', - ], + // Add the S3 URL to the list of allowed image sources + env('AWS_ACCESS_KEY_ID', '') === '' ? [] : + [ + // @phpstan-ignore-next-line + str_replace(parse_url(env('AWS_URL'), PHP_URL_PATH), '', env('AWS_URL')), + ], + explode(',', (string) env('SECURITY_HEADER_CSP_IMG_SRC', '')) + ), + ], + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/manifest-src + 'manifest-src' => [ + ], + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/media-src + 'media-src' => [ + 'self' => true, + 'allow' => array_merge( + [ + 'blob:', // required for "live" photos + ], + // Add the S3 URL to the list of allowed media sources + env('AWS_ACCESS_KEY_ID', '') === '' ? [] : + [ + // @phpstan-ignore-next-line + str_replace(parse_url(env('AWS_URL'), PHP_URL_PATH), '', env('AWS_URL')), + ], + explode(',', (string) env('SECURITY_HEADER_CSP_MEDIA_SRC', '')) + ), + ], + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/navigate-to + 'navigate-to' => [ + 'unsafe-allow-redirects' => false, + ], + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/object-src + 'object-src' => [ + 'none' => true, + ], + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/plugin-types + 'plugin-types' => [ + // 'application/pdf', + ], + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/prefetch-src + 'prefetch-src' => [ + ], + + // https://w3c.github.io/webappsec-trusted-types/dist/spec/#integration-with-content-security-policy + 'require-trusted-types-for' => [ + 'script' => false, + ], + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/sandbox + 'sandbox' => [ + 'enable' => false, + + 'allow-downloads-without-user-activation' => false, + + 'allow-forms' => false, + + 'allow-modals' => false, + + 'allow-orientation-lock' => false, + + 'allow-pointer-lock' => false, + + 'allow-popups' => false, + + 'allow-popups-to-escape-sandbox' => false, + + 'allow-presentation' => false, + + 'allow-same-origin' => false, + + 'allow-scripts' => false, + + 'allow-storage-access-by-user-activation' => false, + + 'allow-top-navigation' => false, + + 'allow-top-navigation-by-user-activation' => false, + ], + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src + 'script-src' => [ + 'none' => false, + + 'self' => true, + + // https://www.chromestatus.com/feature/5792234276388864 + 'report-sample' => true, + + 'allow' => array_merge( + ['https://www.dropbox.com/static/api/1/dropins.js'], + explode(',', (string) env('SECURITY_HEADER_SCRIPT_SRC_ALLOW', '')) + ), + 'schemes' => [ + // 'data:', // 'https:', ], - 'self' => true, + + /* followings are only work for `script` and `style` related directives */ 'unsafe-inline' => false, + 'unsafe-eval' => false, + + // https://www.w3.org/TR/CSP3/#unsafe-hashes-usage + 'unsafe-hashes' => true, + + // Enable `strict-dynamic` will *ignore* `self`, `unsafe-inline`, + // `allow` and `schemes`. You can find more information from: + // https://www.w3.org/TR/CSP3/#strict-dynamic-usage 'strict-dynamic' => false, - 'unsafe-hashed-attributes' => false, - // https://www.chromestatus.com/feature/5792234276388864 - 'report-sample' => true, - 'add-generated-nonce' => false, - ], - 'style-src' => [ - 'allow' => [ - 'https://fonts.googleapis.com', - ], 'hashes' => [ - // 'sha256' => [ - // 'hash-value', - // ], + 'sha256' => [ + // 'sha256-hash-value-with-base64-encode', + + // lychee.startDrag(event) + 'FdKE+KVp/tkYM5hwGXGeKZ1EmS4DJ8kbnsKo5YymNrc=', + + // lychee.endDrag(event) + 'bY67+0U7yUmtjaisfHv+mZXHsAptKwcV1a4EacCUL5M=', + + // lychee.overDrag(event) + 'fwPcZ6SFcvBLfJYjzlBRZfKzcidwsD4GPcmkVECbSKM=', + + // lychee.leaveDrag(event) + 'FCPseLYJ4+r0Mbp93zyaq/x4zQEEPLgEectDgkA/V3A=', + + // lychee.finishDrag(event) + 'T0Fzr5h5zkZyE3QOpQ9anSTcWp19WQ14eO86qdlSdvA=', + + // upload.check() + 'CL4mGy9ZhHM+PkLDZsWVuM25kEFBv3FXlmWe/O9Unmc=', + + /* + const hashMatch = document.location.hash.replace("#", "").split("/"); + const albumID = hashMatch[0] ?? ''; + const photoID = hashMatch[1] ?? ''; + const elem = document.getElementById('redirectData'); + const gallery = elem.dataset.gallery; + const base = elem.dataset.redirect; + + if (photoID !== '') { + window.location = gallery + '/' + albumID + '/' + photoID; + } else if (albumID !== '') { + window.location = gallery + '/' + albumID; + } else { + window.location = base; + } + */ + 'okzzdI+OgeNYCr3oJXDZ/rPI5WwGyiU5V/RwOQrv5zE=', + + /* + document.addEventListener("DOMContentLoaded", function(event) { + document.querySelector("form").addEventListener("submit", function(e){ + document.querySelector("form").hidden = true; + var text = document.createElement("div"); + text.innerHTML = "Migration started. DO NOT REFRESH THE PAGE."; + document.querySelector(".form").appendChild(text); + // e.preventDefault(); //stop form from submitting + }); + }); + +*/ + 'hHvKTS0wUaMuiFMar2j4TbjYjlLQMR/c5b0bA9DLi6g=', + ], + + 'sha384' => [ + // 'sha384-hash-value-with-base64-encode', + ], + + 'sha512' => [ + // 'sha512-hash-value-with-base64-encode', + ], ], - 'nonces' => [], - 'schemes' => [ - // 'https:', + + 'nonces' => [ + // 'base64-encoded', ], - 'self' => true, - 'unsafe-inline' => true, - 'report-sample' => true, + + 'unsafe-hashed-attributes' => false, + 'add-generated-nonce' => false, ], - 'img-src' => [ - 'self' => true, - // Allow OpenStreetMap tile images to be fetched from the different provides - // Allow image to be directly encoded at the img source parameter - 'allow' => [ - 'https://maps.wikimedia.org/osm-intl/', - 'https://a.tile.osm.org/', - 'https://b.tile.osm.org/', - 'https://c.tile.osm.org/', - 'https://a.tile.openstreetmap.de/', - 'https://b.tile.openstreetmap.de/', - 'https://c.tile.openstreetmap.de/', - 'https://a.tile.openstreetmap.fr/osmfr/', - 'https://b.tile.openstreetmap.fr/osmfr/', - 'https://c.tile.openstreetmap.fr/osmfr/', - 'https://a.osm.rrze.fau.de/osmhd/', - 'https://b.osm.rrze.fau.de/osmhd/', - 'https://c.osm.rrze.fau.de/osmhd/', - env('LYCHEE_UPLOADS_URL', 'https://lycheeorg.github.io/'), - 'data:', - 'blob:', - ], + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src-attr + 'script-src-attr' => [ ], - 'default-src' => [ - 'self' => true, - 'allow' => [ - 'blob:', - ], + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src-elem + 'script-src-elem' => [ ], - 'base-uri' => [], - 'connect-src' => [ - 'allow' => [ - 'http://lycheeorg.github.io/update.json', - 'blob:', - ], + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/style-src + 'style-src' => [ 'self' => true, + 'unsafe-inline' => true, // We need this one due to direct styles (not just style classes) applied by JavaScript ], - 'font-src' => [ - 'allow' => [ - 'https://fonts.gstatic.com', - ], - 'self' => true, + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/style-src-attr + 'style-src-attr' => [ ], - 'form-action' => [ - 'self' => true, + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/style-src-elem + 'style-src-elem' => [ ], - 'frame-ancestors' => [], - 'frame-src' => [], - 'manifest-src' => [], - 'media-src' => [ - 'self' => true, - 'allow' => [ - 'blob:', + + // https://w3c.github.io/webappsec-trusted-types/dist/spec/#trusted-types-csp-directive + 'trusted-types' => [ + 'enable' => false, + + 'allow-duplicates' => false, + + 'default' => false, + + 'policies' => [ ], ], - 'object-src' => [], - 'worker-src' => [], - 'plugin-types' => [ - // 'application/x-shockwave-flash', + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/worker-src + 'worker-src' => [ ], - 'require-sri-for' => '', - 'sandbox' => '', ], ]; diff --git a/config/services.php b/config/services.php index dff256b7506..d5cb618de83 100644 --- a/config/services.php +++ b/config/services.php @@ -28,4 +28,80 @@ 'secret' => env('AWS_SECRET_ACCESS_KEY'), 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), ], + + /* + |-------------------------------------------------------------------------- + | Oauth services + |-------------------------------------------------------------------------- + */ + 'amazon' => [ + 'client_id' => env('AMAZON_SIGNIN_CLIENT_ID'), + 'client_secret' => env('AMAZON_SIGNIN_SECRET'), + 'redirect' => env('AMAZON_SIGNIN_REDIRECT_URI', '/auth/amazon/redirect'), + ], + + // https://developer.okta.com/blog/2019/06/04/what-the-heck-is-sign-in-with-apple + // Note: the client secret used for "Sign In with Apple" is a JWT token that can have a maximum lifetime of 6 months. + // The article above explains how to generate the client secret on demand and you'll need to update this every 6 months. + // To generate the client secret for each request, see Generating A Client Secret For Sign In With Apple On Each Request. + // https://bannister.me/blog/generating-a-client-secret-for-sign-in-with-apple-on-each-request + 'apple' => [ + 'client_id' => env('APPLE_CLIENT_ID'), + 'client_secret' => env('APPLE_CLIENT_SECRET'), + 'redirect' => env('APPLE_REDIRECT_URI', '/auth/apple/redirect'), + ], + + 'authentik' => [ + 'client_id' => env('AUTHENTIK_CLIENT_ID'), + 'client_secret' => env('AUTHENTIK_CLIENT_SECRET'), + 'redirect' => env('AUTHENTIK_REDIRECT_URI'), + 'base_url' => env('AUTHENTIK_BASE_URL'), + ], + + 'facebook' => [ + 'client_id' => env('FACEBOOK_CLIENT_ID'), + 'client_secret' => env('FACEBOOK_CLIENT_SECRET'), + 'redirect' => env('FACEBOOK_REDIRECT_URI', '/auth/facebook/redirect'), + ], + + 'github' => [ + 'client_id' => env('GITHUB_CLIENT_ID'), + 'client_secret' => env('GITHUB_CLIENT_SECRET'), + 'redirect' => env('GITHUB_REDIRECT_URI', '/auth/github/redirect'), + ], + + 'google' => [ + 'client_id' => env('GOOGLE_CLIENT_ID'), + 'client_secret' => env('GOOGLE_CLIENT_SECRET'), + 'redirect' => env('GOOGLE_REDIRECT_URI', '/auth/google/redirect'), + ], + + 'mastodon' => [ + 'domain' => env('MASTODON_DOMAIN'), + 'client_id' => env('MASTODON_ID'), + 'client_secret' => env('MASTODON_SECRET'), + 'redirect' => env('MASTODON_REDIRECT_URI', '/auth/mastodon/redirect'), + // 'read', 'write', 'follow' + 'scope' => ['read'], + ], + + 'microsoft' => [ + 'client_id' => env('MICROSOFT_CLIENT_ID'), + 'client_secret' => env('MICROSOFT_CLIENT_SECRET'), + 'redirect' => env('MICROSOFT_REDIRECT_URI', '/auth/microsoft/redirect'), + ], + + 'nextcloud' => [ + 'client_id' => env('NEXTCLOUD_CLIENT_ID'), + 'client_secret' => env('NEXTCLOUD_CLIENT_SECRET'), + 'redirect' => env('NEXTCLOUD_REDIRECT_URI', '/auth/nextcloud/redirect'), + 'instance_uri' => env('NEXTCLOUD_BASE_URI'), + ], + 'keycloak' => [ + 'client_id' => env('KEYCLOAK_CLIENT_ID'), + 'client_secret' => env('KEYCLOAK_CLIENT_SECRET'), + 'redirect' => env('KEYCLOAK_REDIRECT_URI'), + 'base_url' => env('KEYCLOAK_BASE_URL'), + 'realms' => env('KEYCLOAK_REALM'), + ], ]; diff --git a/config/session.php b/config/session.php index e6f777e8b31..7aa69178ad4 100644 --- a/config/session.php +++ b/config/session.php @@ -71,20 +71,8 @@ | */ - 'connection' => null, - - /* - |-------------------------------------------------------------------------- - | Session Database Connection - |-------------------------------------------------------------------------- - | - | When using the "database" or "redis" session drivers, you may specify a - | connection that should be used to manage these sessions. This should - | correspond to a connection in your database configuration options. - | - */ - - 'connection' => env('SESSION_CONNECTION', null), + 'connection' => env('SESSION_DRIVER') === 'database' ? + env('DB_CONNECTION', 'mysql') : 'cache', /* |-------------------------------------------------------------------------- @@ -112,7 +100,7 @@ | */ - 'store' => env('SESSION_STORE', null), + 'store' => env('SESSION_STORE'), /* |-------------------------------------------------------------------------- @@ -140,7 +128,7 @@ 'cookie' => env( 'SESSION_COOKIE', - Str::slug(env('APP_NAME', 'lychee'), '_') . '_session' + Str::slug((string) env('APP_NAME', 'Lychee'), '_') . '_session' ), /* @@ -167,7 +155,7 @@ | */ - 'domain' => env('SESSION_DOMAIN', null), + 'domain' => env('SESSION_DOMAIN'), /* |-------------------------------------------------------------------------- diff --git a/config/trustedproxy.php b/config/trustedproxy.php new file mode 100644 index 00000000000..85b7b273280 --- /dev/null +++ b/config/trustedproxy.php @@ -0,0 +1,41 @@ + env('TRUSTED_PROXIES'), // [,], '*' + + /* + * Which headers to use to detect proxy related data (For, Host, Proto, Port) + * + * Options include: + * + * - Illuminate\Http\Request::HEADER_X_FORWARDED_ALL (use all x-forwarded-* headers to establish trust) + * - Illuminate\Http\Request::HEADER_FORWARDED (use the FORWARDED header to establish trust) + * - Illuminate\Http\Request::HEADER_X_FORWARDED_AWS_ELB (If you are using AWS Elastic Load Balancer) + * + * - 'HEADER_X_FORWARDED_ALL' (use all x-forwarded-* headers to establish trust) + * - 'HEADER_FORWARDED' (use the FORWARDED header to establish trust) + * - 'HEADER_X_FORWARDED_AWS_ELB' (If you are using AWS Elastic Load Balancer) + * + * @link https://symfony.com/doc/current/deployment/proxies.html + */ + 'headers' => Request::HEADER_X_FORWARDED_FOR | + Request::HEADER_X_FORWARDED_HOST | + Request::HEADER_X_FORWARDED_PORT | + Request::HEADER_X_FORWARDED_PROTO | + Request::HEADER_X_FORWARDED_AWS_ELB, +]; diff --git a/config/typescript-transformer.php b/config/typescript-transformer.php new file mode 100644 index 00000000000..fbe6cb1119a --- /dev/null +++ b/config/typescript-transformer.php @@ -0,0 +1,86 @@ + [ + app_path(), + ], + + /* + * Collectors will search for classes in the `auto_discover_types` paths and choose the correct + * transformer to transform them. By default, we include a DefaultCollector which will search + * for @typescript annotated and #[TypeScript] attributed classes to transform. + */ + + 'collectors' => [ + DefaultCollector::class, + EnumCollector::class, + ], + + /* + * Transformers take PHP classes(e.g., enums) as an input and will output + * a TypeScript representation of the PHP class. + */ + + 'transformers' => [ + SpatieStateTransformer::class, + EnumTransformer::class, + SpatieEnumTransformer::class, + DtoTransformer::class, + ], + + /* + * In your classes, you sometimes have types that should always be replaced + * by the same TypeScript representations. For example, you can replace a + * Datetime always with a string. You define these replacements here. + */ + + 'default_type_replacements' => [ + DateTime::class => 'string', + DateTimeImmutable::class => 'string', + Carbon\CarbonInterface::class => 'string', + Carbon\CarbonImmutable::class => 'string', + Carbon\Carbon::class => 'string', + ], + + /* + * The package will write the generated TypeScript to this file. + */ + + 'output_file' => resource_path('js/lychee.d.ts'), + + /* + * When the package is writing types to the output file, a writer is used to + * determine the format. By default, this is the `TypeDefinitionWriter`. + * But you can also use the `ModuleWriter` or implement your own. + */ + + 'writer' => TypeDefinitionWriter::class, + + /* + * The generated TypeScript file can be formatted. We ship a Prettier formatter + * out of the box: `PrettierFormatter` but you can also implement your own one. + * The generated TypeScript will not be formatted when no formatter was set. + */ + + 'formatter' => null, + + /* + * Enums can be transformed into types or native TypeScript enums, by default + * the package will transform them to types. + */ + + 'transform_to_native_enums' => false, +]; diff --git a/config/urls.php b/config/urls.php index 035ec6040cb..f8cddca904a 100644 --- a/config/urls.php +++ b/config/urls.php @@ -3,8 +3,11 @@ return [ 'update' => [ // we need this in case the URL of the project changes - 'git' => 'http://api.github.com/repos/LycheeOrg/Lychee/commits', - 'json' => 'https://lycheeorg.github.io/update.json', + 'git' => [ + 'commits' => 'https://api.github.com/repos/LycheeOrg/Lychee/commits', + 'tags' => 'https://api.github.com/repos/LycheeOrg/Lychee/tags', + ], + 'json' => 'https://lycheeorg.dev/update.json', ], 'git' => [ 'pull' => 'https://github.com/LycheeOrg/Lychee.git', diff --git a/config/view.php b/config/view.php index d7836a521cc..4576eb17a9d 100644 --- a/config/view.php +++ b/config/view.php @@ -27,8 +27,5 @@ | */ - 'compiled' => env( - 'VIEW_COMPILED_PATH', - realpath(storage_path('framework/views')) - ), + 'compiled' => realpath(storage_path('framework/views')), // @phpstan-ignore-line ]; diff --git a/config/webauthn.php b/config/webauthn.php new file mode 100644 index 00000000000..2cab26d088c --- /dev/null +++ b/config/webauthn.php @@ -0,0 +1,36 @@ + [ + 'name' => env('WEBAUTHN_NAME', env('APP_NAME', 'Lychee')), + 'id' => env('WEBAUTHN_ID'), + ], + + /* + |-------------------------------------------------------------------------- + | Challenge configuration + |-------------------------------------------------------------------------- + | + | When making challenges your application needs to push at least 16 bytes + | of randomness. Since we need to later check them, we'll also store the + | bytes for a small amount of time inside this current request session. + | + */ + + 'challenge' => [ + 'bytes' => 16, + 'timeout' => 60, + 'key' => '_webauthn', + ], +]; diff --git a/database/factories/AccessPermissionFactory.php b/database/factories/AccessPermissionFactory.php new file mode 100644 index 00000000000..6f3886f7e70 --- /dev/null +++ b/database/factories/AccessPermissionFactory.php @@ -0,0 +1,140 @@ + + */ +class AccessPermissionFactory extends Factory +{ + /** + * The name of the factory's corresponding model. + * + * @var string + */ + protected $model = AccessPermission::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return + [ + 'is_link_required' => true, + 'grants_full_photo_access' => false, + 'grants_download' => false, + 'grants_upload' => false, + 'grants_edit' => false, + 'grants_delete' => false, + ]; + } + + public function public() + { + return $this->state(function (array $attributes) { + return [ + 'user_id' => null, + ]; + }); + } + + public function locked() + { + return $this->state(function (array $attributes) { + return [ + 'password' => Hash::make('password'), + ]; + }); + } + + public function for_user(User $user) + { + return $this->state(function (array $attributes) use ($user) { + return [ + 'user_id' => $user->id, + ]; + })->afterCreating(function (AccessPermission $perm) { + $perm->load('album', 'user'); + }); + } + + public function grants_edit() + { + return $this->state(function (array $attributes) { + return [ + 'grants_edit' => true, + ]; + }); + } + + public function grants_delete() + { + return $this->state(function (array $attributes) { + return [ + 'grants_delete' => true, + ]; + }); + } + + public function grants_upload() + { + return $this->state(function (array $attributes) { + return [ + 'grants_upload' => true, + ]; + }); + } + + public function grants_download() + { + return $this->state(function (array $attributes) { + return [ + 'grants_download' => true, + ]; + }); + } + + public function grants_full_photo() + { + return $this->state(function (array $attributes) { + return [ + 'grants_full_photo_access' => true, + ]; + }); + } + + public function visible() + { + return $this->state(function (array $attributes) { + return [ + 'is_link_required' => false, + ]; + }); + } + + public function for_album(Album $album) + { + return $this->state(function (array $attributes) use ($album) { + return [ + 'base_album_id' => $album->id, + ]; + })->afterCreating(function (AccessPermission $perm) { + $perm->load('album', 'user'); + }); + } +} \ No newline at end of file diff --git a/database/factories/AlbumFactory.php b/database/factories/AlbumFactory.php new file mode 100644 index 00000000000..4fbba143fe4 --- /dev/null +++ b/database/factories/AlbumFactory.php @@ -0,0 +1,71 @@ + + */ +class AlbumFactory extends Factory +{ + use OwnedBy; + + /** + * The name of the factory's corresponding model. + * + * @var string + */ + protected $model = Album::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'title' => fake()->country() . ' ' . fake()->year(), + 'owner_id' => 1, + ]; + } + + /** + * Defines the parent of the create album. + * + * @param Album $parent + * + * @return self + */ + public function children_of(Album $parent): Factory + { + return $this->afterMaking( + fn (Album $album) => $parent->appendNode($album) + ) + ->afterCreating(function (Album $album) use ($parent) { + $parent->load('children'); + $parent->fixOwnershipOfChildren(); + }); + } + + /** + * Make the album root. + * + * @return self + */ + public function as_root(): self + { + return $this->afterMaking(function (Album $album) { + $album->makeRoot(); + }); + } +} diff --git a/database/factories/PhotoFactory.php b/database/factories/PhotoFactory.php new file mode 100644 index 00000000000..71be09427ef --- /dev/null +++ b/database/factories/PhotoFactory.php @@ -0,0 +1,151 @@ + + */ +class PhotoFactory extends Factory +{ + use OwnedBy; + + private bool $with_size_variants = true; + + /** + * The name of the factory's corresponding model. + * + * @var string + */ + protected $model = Photo::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'title' => 'CR_' . fake()->numerify('####'), + 'description' => null, + 'tags' => '', + 'owner_id' => 1, + 'type' => 'image/jpeg', + 'iso' => '100', + 'aperture' => 'f/2', + 'make' => 'Canon', + 'model' => 'Canon EOS R', + 'lens' => 'EF200mm f/2L IS', + 'shutter' => '1/320 s', + 'focal' => '200mm', + 'taken_at' => now(), + 'taken_at_orig_tz' => null, + 'is_starred' => false, + 'album_id' => null, + 'checksum' => sha1(rand()), + 'original_checksum' => sha1(rand()), + 'license' => 'none', + 'created_at' => now(), + 'updated_at' => now(), + ]; + } + + /** + * Indicate that the user is suspended. + */ + public function without_size_variants(): Factory + { + $this->with_size_variants = false; + + return $this; + } + + /** + * Set a bunch of GPS coordinates (in Netherlands). + * + * @return array + */ + public function with_GPS_coordinates(): self + { + return $this->state(function (array $attributes) { + return [ + 'latitude' => '51.81738000', + 'longitude' => '5.86694306', + 'altitude' => '83.1000', + ]; + }); + } + + /** + * Set a bunch of GPS coordinates (in Netherlands). + * + * @return array + */ + public function with_subGPS_coordinates(): self + { + return $this->state(function (array $attributes) { + return [ + 'latitude' => '-51.81738000', + 'longitude' => '-5.86694306', + 'altitude' => '83.1000', + ]; + }); + } + + /** define tags for that picture */ + public function with_tags(string $tags): self + { + return $this->state(function (array $attributes) use ($tags) { + return [ + 'tags' => $tags, + ]; + }); + } + + /** + * Set a bunch of GPS coordinates (in Netherlands). + * + * @return self + */ + public function in(Album $album): self + { + return $this->state(function (array $attributes) use ($album) { + return [ + 'album_id' => $album->id, + ]; + })->afterCreating(function (Photo $photo) { + $photo->load('album', 'owner'); + }); + } + + /** + * Configure the model factory. + * We create 7 random Size Variants. + */ + public function configure(): static + { + return $this->afterCreating(function (Photo $photo) { + // Creates the size variants + if ($this->with_size_variants) { + SizeVariant::factory()->count(7)->allSizeVariants()->create(['photo_id' => $photo->id]); + $photo->fresh(); + $photo->load('size_variants'); + } + + // Reset the value if it was disabled. + $this->with_size_variants = true; + }); + } +} diff --git a/database/factories/SizeVariantFactory.php b/database/factories/SizeVariantFactory.php new file mode 100644 index 00000000000..bc65539619e --- /dev/null +++ b/database/factories/SizeVariantFactory.php @@ -0,0 +1,63 @@ + + */ +class SizeVariantFactory extends Factory +{ + private const H = 360; + private const W = 540; + private const FS = 141011; + + /** + * The name of the factory's corresponding model. + * + * @var string + */ + protected $model = SizeVariant::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + $hash = fake()->sha1(); + $url = substr($hash, 0, 2) . '/' . substr($hash, 2, 2) . '/' . substr($hash, 4) . '.jpg'; + + return ['type' => SizeVariantType::ORIGINAL, 'short_path' => SizeVariantType::ORIGINAL->name() . '/' . $url, 'ratio' => 1.5, 'height' => self::H * 8, 'width' => self::W * 8, 'filesize' => 64 * self::FS, 'storage_disk' => 'images']; + } + + /** + * Creates 7 size variant with correct type and size,. + */ + public function allSizeVariants(): Factory + { + $hash = fake()->sha1(); + $url = substr($hash, 0, 2) . '/' . substr($hash, 2, 2) . '/' . substr($hash, 4) . '.jpg'; + + return $this->state(new Sequence( + ['type' => SizeVariantType::ORIGINAL, 'short_path' => SizeVariantType::ORIGINAL->name() . '/' . $url, 'ratio' => 1.5, 'height' => self::H * 8, 'width' => self::W * 8, 'filesize' => 64 * self::FS, 'storage_disk' => 'images'], + ['type' => SizeVariantType::MEDIUM2X, 'short_path' => SizeVariantType::MEDIUM2X->name() . '/' . $url, 'ratio' => 1.5, 'height' => self::H * 6, 'width' => self::W * 6, 'filesize' => 36 * self::FS, 'storage_disk' => 'images'], + ['type' => SizeVariantType::MEDIUM, 'short_path' => SizeVariantType::MEDIUM->name() . '/' . $url, 'ratio' => 1.5, 'height' => self::H * 3, 'width' => self::W * 3, 'filesize' => 9 * self::FS, 'storage_disk' => 'images'], + ['type' => SizeVariantType::SMALL2X, 'short_path' => SizeVariantType::SMALL2X->name() . '/' . $url, 'ratio' => 1.5, 'height' => self::H * 2, 'width' => self::W * 2, 'filesize' => 4 * self::FS, 'storage_disk' => 'images'], + ['type' => SizeVariantType::SMALL, 'short_path' => SizeVariantType::SMALL->name() . '/' . $url, 'ratio' => 1.5, 'height' => self::H, 'width' => self::W, 'filesize' => self::FS, 'storage_disk' => 'images'], + ['type' => SizeVariantType::THUMB2X, 'short_path' => SizeVariantType::THUMB2X->name() . '/' . $url, 'ratio' => 1.5, 'height' => 400, 'width' => 400, 'filesize' => 160_000, 'storage_disk' => 'images'], + ['type' => SizeVariantType::THUMB, 'short_path' => SizeVariantType::THUMB->name() . '/' . $url, 'ratio' => 1.5, 'height' => 200, 'width' => 200, 'filesize' => 40_000, 'storage_disk' => 'images'], + )); + } +} diff --git a/database/factories/TagAlbumFactory.php b/database/factories/TagAlbumFactory.php new file mode 100644 index 00000000000..375bbad694e --- /dev/null +++ b/database/factories/TagAlbumFactory.php @@ -0,0 +1,50 @@ + + */ +class TagAlbumFactory extends Factory +{ + use OwnedBy; + + /** + * The name of the factory's corresponding model. + * + * @var string + */ + protected $model = TagAlbum::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'title' => 'Tag Album ' . fake()->year(), + 'owner_id' => 1, + ]; + } + + public function of_tags(string $tags): self + { + return $this->state(function (array $attributes) use ($tags) { + return [ + 'show_tags' => $tags, + ]; + }); + } +} diff --git a/database/factories/Traits/OwnedBy.php b/database/factories/Traits/OwnedBy.php new file mode 100644 index 00000000000..fe3bf0a3aed --- /dev/null +++ b/database/factories/Traits/OwnedBy.php @@ -0,0 +1,32 @@ +state(function (array $attributes) use ($user) { + return [ + 'owner_id' => $user->id, + ]; + }); + } +} \ No newline at end of file diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index 36125fd748e..6f56892196e 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -1,11 +1,19 @@ + */ class UserFactory extends Factory { /** @@ -18,16 +26,59 @@ class UserFactory extends Factory /** * Define the model's default state. * - * @return array + * @return array */ - public function definition() + public function definition(): array { return [ - 'name' => $this->faker->name, - 'email' => $this->faker->unique()->safeEmail, - 'email_verified_at' => now(), + 'username' => fake()->name(), 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password - 'remember_token' => Str::random(10), + 'may_administrate' => false, + 'may_upload' => false, + 'email' => fake()->email(), + 'token' => null, + 'remember_token' => null, + 'may_edit_own_settings' => true, ]; } + + /** + * Indicate that user is Admin. + * + * @return Factory + */ + public function may_administrate(): Factory + { + return $this->state(function (array $attributes) { + return [ + 'may_administrate' => true, + ]; + }); + } + + /** + * Indicate that the user has upload rights. + * + * @return Factory + */ + public function may_upload(): Factory + { + return $this->state(function (array $attributes) { + return [ + 'may_upload' => true, + ]; + }); + } + + /** + * Indicates the user is locked. + * + * @return Factory + */ + public function locked(): Factory + { + return $this->state(function (array $attributes) { + return ['may_edit_own_settings' => false]; + }); + } } diff --git a/database/migrations/2014_10_12_000000_create_users_table.php b/database/migrations/2014_10_12_000000_create_users_table.php index ade449d940a..45c9c29236b 100644 --- a/database/migrations/2014_10_12_000000_create_users_table.php +++ b/database/migrations/2014_10_12_000000_create_users_table.php @@ -1,17 +1,20 @@ show columns from lychee_albums; -//+--------------+---------------------+------+-----+---------+-------+ -//| Field | Type | Null | Key | Default | Extra | -//+--------------+---------------------+------+-----+---------+-------+ -//| id | bigint(14) unsigned | NO | PRI | NULL | | -//| title | varchar(100) | NO | | | | -//| description | varchar(1000) | YES | | | | -//| sysstamp | int(11) | NO | | NULL | | -//| public | tinyint(1) | NO | | 0 | | -//| visible | tinyint(1) | NO | | 1 | | -//| downloadable | tinyint(1) | NO | | 0 | | -//| password | varchar(100) | YES | | NULL | | -//+--------------+---------------------+------+-----+---------+-------+ +// MariaDB [lychee]> show columns from lychee_albums; +// +--------------+---------------------+------+-----+---------+-------+ +// | Field | Type | Null | Key | Default | Extra | +// +--------------+---------------------+------+-----+---------+-------+ +// | id | bigint(14) unsigned | NO | PRI | NULL | | +// | title | varchar(100) | NO | | | | +// | description | varchar(1000) | YES | | | | +// | sysstamp | int(11) | NO | | NULL | | +// | public | tinyint(1) | NO | | 0 | | +// | visible | tinyint(1) | NO | | 1 | | +// | downloadable | tinyint(1) | NO | | 0 | | +// | password | varchar(100) | YES | | NULL | | +// +--------------+---------------------+------+-----+---------+-------+ -class CreateAlbumsTable extends Migration -{ +return new class() extends Migration { /** * Run the migrations. - * - * @return void */ - public function up() + public function up(): void { Schema::dropIfExists('albums'); Schema::create('albums', function (Blueprint $table) { @@ -49,11 +52,9 @@ public function up() /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { Schema::dropIfExists('albums'); } -} +}; diff --git a/database/migrations/2018_08_03_110936_create_photos_table.php b/database/migrations/2018_08_03_110936_create_photos_table.php index ac7493b87da..af265a2ae62 100644 --- a/database/migrations/2018_08_03_110936_create_photos_table.php +++ b/database/migrations/2018_08_03_110936_create_photos_table.php @@ -1,44 +1,47 @@ show columns from lychee_photos; -//+-------------+---------------------+------+-----+---------+-------+ -//| Field | Type | Null | Key | Default | Extra | -//+-------------+---------------------+------+-----+---------+-------+ -//| id | bigint(14) unsigned | NO | PRI | NULL | | -//| title | varchar(100) | NO | | | | -//| description | varchar(1000) | YES | | | | -//| url | varchar(100) | NO | | NULL | | -//| tags | varchar(1000) | NO | | | | -//| public | tinyint(1) | NO | | NULL | | -//| type | varchar(10) | NO | | NULL | | -//| width | int(11) | NO | | NULL | | -//| height | int(11) | NO | | NULL | | -//| size | varchar(20) | NO | | NULL | | -//| iso | varchar(15) | NO | | NULL | | -//| aperture | varchar(20) | NO | | NULL | | -//| make | varchar(50) | NO | | NULL | | -//| model | varchar(50) | NO | | NULL | | -//| shutter | varchar(30) | NO | | NULL | | -//| focal | varchar(20) | NO | | NULL | | -//| takestamp | int(11) | YES | | NULL | | -//| star | tinyint(1) | NO | MUL | NULL | | -//| thumbUrl | char(37) | NO | | NULL | | -//| album | bigint(14) unsigned | NO | MUL | NULL | | -//| checksum | char(40) | YES | | NULL | | -//| medium | tinyint(1) | NO | | 0 | | +// MariaDB [lychee]> show columns from lychee_photos; +// +-------------+---------------------+------+-----+---------+-------+ +// | Field | Type | Null | Key | Default | Extra | +// +-------------+---------------------+------+-----+---------+-------+ +// | id | bigint(14) unsigned | NO | PRI | NULL | | +// | title | varchar(100) | NO | | | | +// | description | varchar(1000) | YES | | | | +// | url | varchar(100) | NO | | NULL | | +// | tags | varchar(1000) | NO | | | | +// | public | tinyint(1) | NO | | NULL | | +// | type | varchar(10) | NO | | NULL | | +// | width | int(11) | NO | | NULL | | +// | height | int(11) | NO | | NULL | | +// | size | varchar(20) | NO | | NULL | | +// | iso | varchar(15) | NO | | NULL | | +// | aperture | varchar(20) | NO | | NULL | | +// | make | varchar(50) | NO | | NULL | | +// | model | varchar(50) | NO | | NULL | | +// | shutter | varchar(30) | NO | | NULL | | +// | focal | varchar(20) | NO | | NULL | | +// | takestamp | int(11) | YES | | NULL | | +// | star | tinyint(1) | NO | MUL | NULL | | +// | thumbUrl | char(37) | NO | | NULL | | +// | album | bigint(14) unsigned | NO | MUL | NULL | | +// | checksum | char(40) | YES | | NULL | | +// | medium | tinyint(1) | NO | | 0 | | -class CreatePhotosTable extends Migration -{ +return new class() extends Migration { /** * Run the migrations. - * - * @return void */ - public function up() + public function up(): void { Schema::dropIfExists('photos'); Schema::create('photos', function (Blueprint $table) { @@ -81,11 +84,9 @@ public function up() /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { Schema::dropIfExists('photos'); } -} +}; diff --git a/database/migrations/2018_08_03_110942_create_configs_table.php b/database/migrations/2018_08_03_110942_create_configs_table.php index c2227e81780..fd05283d8dc 100644 --- a/database/migrations/2018_08_03_110942_create_configs_table.php +++ b/database/migrations/2018_08_03_110942_create_configs_table.php @@ -1,5 +1,11 @@ show columns from lychee_log; -//+----------+--------------+------+-----+---------+----------------+ -//| Field | Type | Null | Key | Default | Extra | -//+----------+--------------+------+-----+---------+----------------+ -//| id | int(11) | NO | PRI | NULL | auto_increment | -//| time | int(11) | NO | | NULL | | -//| type | varchar(11) | NO | | NULL | | -//| function | varchar(100) | NO | | NULL | | -//| line | int(11) | NO | | NULL | | -//| text | text | YES | | NULL | | -//+----------+--------------+------+-----+---------+----------------+ +// MariaDB [lychee]> show columns from lychee_log; +// +----------+--------------+------+-----+---------+----------------+ +// | Field | Type | Null | Key | Default | Extra | +// +----------+--------------+------+-----+---------+----------------+ +// | id | int(11) | NO | PRI | NULL | auto_increment | +// | time | int(11) | NO | | NULL | | +// | type | varchar(11) | NO | | NULL | | +// | function | varchar(100) | NO | | NULL | | +// | line | int(11) | NO | | NULL | | +// | text | text | YES | | NULL | | +// +----------+--------------+------+-----+---------+----------------+ -class CreateLogsTable extends Migration -{ +return new class() extends Migration { /** * Run the migrations. - * - * @return void */ - public function up() + public function up(): void { Schema::dropIfExists('logs'); Schema::create('logs', function (Blueprint $table) { @@ -38,11 +41,9 @@ public function up() /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { Schema::dropIfExists('logs'); } -} +}; diff --git a/database/migrations/2018_08_10_134924_move_settings.php b/database/migrations/2018_08_10_134924_move_settings.php index 783ad68b213..f59df381742 100644 --- a/database/migrations/2018_08_10_134924_move_settings.php +++ b/database/migrations/2018_08_10_134924_move_settings.php @@ -1,23 +1,25 @@ count() == 0) { + if (DB::table('configs')->where('key', '=', 'check_for_updates')->count() === 0) { $results = DB::table(env('DB_OLD_LYCHEE_PREFIX', '') . 'lychee_settings')->select('*')->orderBy('key', 'asc')->get(); foreach ($results as $result) { @@ -53,29 +55,27 @@ public function up() | version | update_030216 | +---------------------+--------------------------------------------------------------+ */ - if (in_array($result->key, ['sortingAlbums', 'sortingPhotos'])) { + if (in_array($result->key, ['sortingAlbums', 'sortingPhotos'], true)) { $order_by = explode(' ', $result->value); - Configs::where('key', '=', $result->key . '_col')->update(['value' => $order_by[2] ?? 'id']); - Configs::where('key', '=', $result->key . '_order')->update(['value' => $order_by[3] ?? 'DESC']); - } elseif (!in_array($result->key, ['checkForUpdates', 'hide_version_number', 'identifier', 'php_script_limit', 'plugins', 'public_search', 'useExiftool', 'version'])) { - Configs::where('key', '=', $result->key)->update(['value' => $result->value ?? '']); + DB::table('configs')->where('key', '=', $result->key . '_col')->update(['value' => $order_by[2] ?? 'id']); + DB::table('configs')->where('key', '=', $result->key . '_order')->update(['value' => $order_by[3] ?? 'DESC']); + } elseif (!in_array($result->key, ['checkForUpdates', 'hide_version_number', 'identifier', 'php_script_limit', 'plugins', 'public_search', 'useExiftool', 'version'], true)) { + DB::table('configs')->where('key', '=', $result->key)->update(['value' => $result->value ?? '']); } } } else { - Logs::notice(__METHOD__, __LINE__, 'We are already passed migration point, ' . __CLASS__ . ' will not be applied.'); + Log::notice(__METHOD__ . ':' . __LINE__ . ' We are already passed migration point, ' . __CLASS__ . ' will not be applied.'); } } else { - Logs::notice(__FUNCTION__, __LINE__, env('DB_OLD_LYCHEE_PREFIX', '') . 'lychee_settings does not exist!'); + Log::notice(__FUNCTION__ . ':' . __LINE__ . ' ' . env('DB_OLD_LYCHEE_PREFIX', '') . 'lychee_settings does not exist!'); } } /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { - Logs::warning(__METHOD__, __LINE__, 'There is no going back for ' . __CLASS__ . '! HUE HUE HUE'); + Log::warning(__METHOD__ . ':' . __LINE__ . ' There is no going back for ' . __CLASS__ . '! HUE HUE HUE'); } -} +}; diff --git a/database/migrations/2018_08_15_102039_move_albums.php b/database/migrations/2018_08_15_102039_move_albums.php index 86a8fd12b39..50a9585a9d4 100644 --- a/database/migrations/2018_08_15_102039_move_albums.php +++ b/database/migrations/2018_08_15_102039_move_albums.php @@ -1,27 +1,37 @@ withoutGlobalScopes()->get()) == 0) { + if (DB::table('albums')->count('id') === 0) { if (Schema::hasTable(env('DB_OLD_LYCHEE_PREFIX', '') . 'lychee_albums')) { $results = DB::table(env('DB_OLD_LYCHEE_PREFIX', '') . 'lychee_albums')->select('*')->orderBy('id', 'asc')->get(); $id = 0; foreach ($results as $result) { - $id = Helpers::trancateIf32($result->id, $id); + $id = Helpers::trancateIf32($result->id, (int) $id); + try { + $date = date('Y-m-d H:i:s', $result->sysstamp); + } catch (DatetimeException) { + $date = date('Y-m-d H:i:s'); + } + DB::table('albums')->insert([ 'id' => $id, 'title' => $result->title, @@ -30,28 +40,26 @@ public function up() 'visible_hidden' => $result->visible, 'password' => $result->password, 'license' => $result->license ?? 'none', - 'created_at' => date('Y-m-d H:i:s', $result->sysstamp), + 'created_at' => $date, ]); } } else { - Logs::notice(__FUNCTION__, __LINE__, env('DB_OLD_LYCHEE_PREFIX', '') . 'lychee_albums does not exist!'); + Log::notice(__METHOD__ . ':' . __LINE__ . ' ' . env('DB_OLD_LYCHEE_PREFIX', '') . 'lychee_albums does not exist!'); } } else { - Logs::notice(__FUNCTION__, __LINE__, 'albums is not empty.'); + Log::notice(__METHOD__ . ':' . __LINE__ . ' albums is not empty.'); } } /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { if (Schema::hasTable('lychee_albums')) { Schema::disableForeignKeyConstraints(); - Album::truncate(); + DB::table('albums')->truncate(); Schema::enableForeignKeyConstraints(); } } -} +}; diff --git a/database/migrations/2018_08_15_103716_move_photos.php b/database/migrations/2018_08_15_103716_move_photos.php index c5035e6cf2e..82e533e5d46 100644 --- a/database/migrations/2018_08_15_103716_move_photos.php +++ b/database/migrations/2018_08_15_103716_move_photos.php @@ -1,100 +1,121 @@ select('*')->orderBy('id', 'asc')->orderBy('album', 'asc')->get(); - $id = 0; - foreach ($results as $result) { - $photo = new Photo(); - $id = Helpers::trancateIf32($result->id, $id); - $photo->id = $id; - if ($result->album == 0) { - $photo->album_id = null; - } else { - $photo->album_id = Helpers::trancateIf32($result->album, 0); - } - $photo->title = $result->title; - $photo->description = $result->description; - $photo->url = $result->url; - $photo->tags = $result->tags; - $photo->public = $result->public; - $photo->type = $result->type; - $photo->width = $result->width; - $photo->height = $result->height; - $photo->size = $result->size; - $photo->iso = $result->iso; - $photo->aperture = $result->aperture; - $photo->make = $result->make; - $photo->lens = $result->lens ?? ''; - $photo->model = $result->model; - $photo->shutter = $result->shutter; - $photo->focal = $result->focal; - $photo->takestamp = ($result->takestamp == 0 || $result->takestamp == null) ? null : date('Y-m-d H:i:s', $result->takestamp); - $photo->star = $result->star; - $photo->thumbUrl = $result->thumbUrl; - $thumbUrl2x = explode('.', $result->thumbUrl); - if (count($thumbUrl2x) < 2) { - $photo->thumb2x = 0; + // only do if photos is empty and + // if there is a table to import from + if ( + MovePhotos_Photo::count() === 0 && + Schema::hasTable(env('DB_OLD_LYCHEE_PREFIX', '') . 'lychee_photos') + ) { + $results = DB::table(env('DB_OLD_LYCHEE_PREFIX', '') . 'lychee_photos')->select('*')->orderBy('id', 'asc')->orderBy('album', 'asc')->get(); + $id = 0; + foreach ($results as $result) { + $photoAttributes = []; + $id = Helpers::trancateIf32($result->id, (int) $id); + $photoAttributes['id'] = $id; + if ($result->album === 0) { + $photoAttributes['album_id'] = null; + } else { + $albumID = Helpers::trancateIf32($result->album, 0); + $exists = DB::table('albums')->select('id')->where('id', '=', $albumID)->count() > 0; + $photoAttributes['album_id'] = $exists ? $albumID : null; + } + $photoAttributes['title'] = $result->title; + $photoAttributes['description'] = $result->description; + $photoAttributes['url'] = $result->url; + $photoAttributes['tags'] = $result->tags; + $photoAttributes['public'] = $result->public; + $photoAttributes['type'] = $result->type; + $photoAttributes['width'] = $result->width; + $photoAttributes['height'] = $result->height; + $photoAttributes['size'] = $result->size; + $photoAttributes['iso'] = $result->iso; + $photoAttributes['aperture'] = $result->aperture; + $photoAttributes['make'] = $result->make; + $photoAttributes['lens'] = $result->lens ?? ''; + $photoAttributes['model'] = $result->model; + $photoAttributes['shutter'] = $result->shutter; + $photoAttributes['focal'] = $result->focal; + try { + $date = date('Y-m-d H:i:s', $result->takestamp); + } catch (DatetimeException) { + $date = null; + } + $photoAttributes['takestamp'] = ($result->takestamp === 0 || $result->takestamp === null) ? null : $date; + $photoAttributes['star'] = $result->star; + $photoAttributes['thumbUrl'] = $result->thumbUrl; + $thumbUrl2x = explode('.', $result->thumbUrl); + if (count($thumbUrl2x) < 2) { + $photoAttributes['thumb2x'] = 0; + } else { + /** @var string $thumbUrl2x */ + $thumbUrl2x = $thumbUrl2x[0] . '@2x.' . $thumbUrl2x[1]; + if (!Storage::exists('thumb/' . $thumbUrl2x)) { + $photoAttributes['thumb2x'] = 0; } else { - $thumbUrl2x = $thumbUrl2x[0] . '@2x.' . $thumbUrl2x[1]; - if (!Storage::exists('thumb/' . $thumbUrl2x)) { - $photo->thumb2x = 0; - } else { - $photo->thumb2x = 1; - } + $photoAttributes['thumb2x'] = 1; } - $photo->checksum = $result->checksum; - if (Storage::exists('medium/' . $photo->url)) { - list($width, $height) = getimagesize(Storage::path('medium/' . $photo->url)); - $photo->medium = $width . 'x' . $height; - } else { - $photo->medium = ''; + } + $photoAttributes['checksum'] = $result->checksum; + if (Storage::exists('medium/' . $photoAttributes['url'])) { + try { + list($width, $height) = getimagesize(Storage::path('medium/' . $photoAttributes['url'])); + $photoAttributes['medium'] = $width . 'x' . $height; + } catch (ImageException) { + $photoAttributes['medium'] = ''; } - if (Storage::exists('small/' . $photo->url)) { - list($width, $height) = getimagesize(Storage::path('small/' . $photo->url)); - $result->small = $width . 'x' . $height; - } else { - $result->small = ''; + } else { + $photoAttributes['medium'] = ''; + } + if (Storage::exists('small/' . $photoAttributes['url'])) { + try { + list($width, $height) = getimagesize(Storage::path('small/' . $photoAttributes['url'])); + $photoAttributes['small'] = $width . 'x' . $height; + } catch (ImageException) { + $photoAttributes['small'] = ''; } - $photo->license = $result->license ?? 'none'; - $photo->save(); + } else { + $photoAttributes['small'] = ''; } - } else { - Logs::notice(__FUNCTION__, __LINE__, env('DB_OLD_LYCHEE_PREFIX', '') . 'lychee_photos does not exist!'); + $photoAttributes['license'] = $result->license ?? 'none'; + + $photoModel = new MovePhotos_Photo(); + $photoModel->setRawAttributes($photoAttributes); + $photoModel->save(); } - } else { - Logs::notice(__FUNCTION__, __LINE__, 'photos is not empty.'); } } /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { if (Schema::hasTable('lychee_photos')) { - Photo::truncate(); + MovePhotos_Photo::query()->truncate(); } } -} +}; diff --git a/database/migrations/2018_10_30_135411_sharing.php b/database/migrations/2018_10_30_135411_sharing.php index 5f30f94a5d7..c5c1963ebee 100644 --- a/database/migrations/2018_10_30_135411_sharing.php +++ b/database/migrations/2018_10_30_135411_sharing.php @@ -1,17 +1,20 @@ insert([ - // [ - // 'title' => 'contact', - // 'menu_title' => 'contact', - // 'in_menu' => true, - // 'link' => '/contact', - // 'enabled' => true, - // 'order' => 0 - // ], - // [ - // 'title' => 'about', - // 'menu_title' => 'about', - // 'in_menu' => true, - // 'link' => '/about', - // 'enabled' => true, - // 'order' => 1 - // ], [ 'title' => 'gallery', 'menu_title' => 'gallery', @@ -52,24 +38,14 @@ public function up() 'enabled' => true, 'order' => 2, ], - // [ - // 'title' => 'portfolio', - // 'menu_title' => 'portfolio', - // 'in_menu' => true, - // 'link' => '/portfolio', - // 'enabled' => true, - // 'order' => 3 - // ], ]); } /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { Schema::dropIfExists('pages'); } -} +}; diff --git a/database/migrations/2019_02_21_114408_create_page_contents_table.php b/database/migrations/2019_02_21_114408_create_page_contents_table.php index c7540c95056..3735f7a9f95 100644 --- a/database/migrations/2019_02_21_114408_create_page_contents_table.php +++ b/database/migrations/2019_02_21_114408_create_page_contents_table.php @@ -1,18 +1,20 @@ update(['key' => 'layout']); - Configs::where('key', '=', 'checkForUpdates')->update(['key' => 'check_for_updates']); - Configs::where('key', '=', 'sortingPhotos_col')->update(['key' => 'sorting_Photos_col']); - Configs::where('key', '=', 'sortingPhotos_order')->update(['key' => 'sorting_Photos_order']); - Configs::where('key', '=', 'sortingAlbums_col')->update(['key' => 'sorting_Albums_col']); - Configs::where('key', '=', 'sortingAlbums_order')->update(['key' => 'sorting_Albums_order']); - Configs::where('key', '=', 'skipDuplicates')->update(['key' => 'skip_duplicates']); - Configs::where('key', '=', 'deleteImported')->update(['key' => 'delete_imported']); - Configs::where('key', '=', 'dropboxKey')->update(['key' => 'dropbox_key']); + DB::table('configs')->where('key', '=', 'justified_layout')->update(['key' => 'layout']); + DB::table('configs')->where('key', '=', 'checkForUpdates')->update(['key' => 'check_for_updates']); + DB::table('configs')->where('key', '=', 'sortingPhotos_col')->update(['key' => 'sorting_Photos_col']); + DB::table('configs')->where('key', '=', 'sortingPhotos_order')->update(['key' => 'sorting_Photos_order']); + DB::table('configs')->where('key', '=', 'sortingAlbums_col')->update(['key' => 'sorting_Albums_col']); + DB::table('configs')->where('key', '=', 'sortingAlbums_order')->update(['key' => 'sorting_Albums_order']); + DB::table('configs')->where('key', '=', 'skipDuplicates')->update(['key' => 'skip_duplicates']); + DB::table('configs')->where('key', '=', 'deleteImported')->update(['key' => 'delete_imported']); + DB::table('configs')->where('key', '=', 'dropboxKey')->update(['key' => 'dropbox_key']); } /** * Cleaning up entries which do not exists anymore. * - * @param array $values + * @param array{key:string,value:string,cat:string,type_range:string,confidentiality:string}[] $values */ - private function cleanup(array &$values) + private function cleanup(array &$values): void { - function get_key($v) - { - return $v['key']; - } - - $keys = array_map('get_key', $values); + $keys = array_map(fn ($v) => $v['key'], $values); try { - Configs::whereNotIn('key', $keys)->delete(); + DB::table('configs')->whereNotIn('key', $keys)->delete(); } catch (Exception $e) { - Logs::warning(__FUNCTION__, __LINE__, 'Something weird happened.'); + Log::warning(__FUNCTION__ . ':' . __LINE__ . ' Something weird happened.'); } } /** * Add potentially missing columns. */ - private function missing_columns() + private function missing_columns(): void { if (!Schema::hasColumn('configs', 'cat')) { Schema::table('configs', function (Blueprint $table) { @@ -87,33 +94,24 @@ private function missing_columns() /** * Update the fields which are missing, set up the correct values for columns. * - * @param array $default_values + * @param array{key:string,value:string,cat:string,type_range:string,confidentiality:string}[] $default_values */ - private function update_missing_fields(array &$default_values) + private function update_missing_fields(array &$default_values): void { foreach ($default_values as $value) { - $c = Configs::where('key', $value['key'])->count(); - $config = Configs::updateOrCreate( - ['key' => $value['key']], - [ - 'cat' => $value['cat'], - 'type_range' => $value['type_range'], - 'confidentiality' => $value['confidentiality'], - ] - ); - if ($c == 0) { - $config->value = $value['value']; - $config->save(); + $c = DB::table('configs')->where('key', '=', $value['key'])->count(); + if ($c === 0) { + DB::table('configs')->insert($value); + } else { // $c === 1 + DB::table('configs')->where('key', '=', $value['key'])->update($value); } } } /** * Run the migrations. - * - * @return void */ - public function up() + public function up(): void { defined('INT') or define('INT', 'int'); defined('STRING') or define('STRING', 'string'); @@ -122,431 +120,432 @@ public function up() defined('TERNARY') or define('TERNARY', '0|1|2'); defined('DISABLED') or define('DISABLED', ''); + /** @var array{key:string,value:string,cat:string,type_range:string,confidentiality:string}[] */ $default_values = [ [ 'key' => 'version', 'value' => '040000', - 'cat' => 'Admin', + 'cat' => self::ADMIN, 'type_range' => INT, 'confidentiality' => '0', ], [ 'key' => 'username', 'value' => '', - 'cat' => 'Admin', + 'cat' => self::ADMIN, 'type_range' => STRING_REQ, 'confidentiality' => '4', ], [ 'key' => 'password', 'value' => '', - 'cat' => 'Admin', + 'cat' => self::ADMIN, 'type_range' => STRING_REQ, 'confidentiality' => '4', ], [ 'key' => 'check_for_updates', 'value' => '0', - 'cat' => 'Admin', + 'cat' => self::ADMIN, 'type_range' => BOOL, 'confidentiality' => '0', ], [ 'key' => 'sorting_Photos_col', 'value' => 'takestamp', - 'cat' => 'Gallery', + 'cat' => self::GALLERY, 'type_range' => 'id|takestamp|title|description|public|star|type', 'confidentiality' => '2', ], [ 'key' => 'sorting_Photos_order', 'value' => 'ASC', - 'cat' => 'Gallery', + 'cat' => self::GALLERY, 'type_range' => 'ASC|DESC', 'confidentiality' => '2', ], [ 'key' => 'sorting_Albums_col', 'value' => 'max_takestamp', - 'cat' => 'Gallery', + 'cat' => self::GALLERY, 'type_range' => 'id|title|description|public|max_takestamp|min_takestamp|created_at', 'confidentiality' => '2', ], [ 'key' => 'sorting_Albums_order', 'value' => 'ASC', - 'cat' => 'Gallery', + 'cat' => self::GALLERY, 'type_range' => 'ASC|DESC', 'confidentiality' => '2', ], [ 'key' => 'imagick', 'value' => '1', - 'cat' => 'Image Processing', + 'cat' => self::IMAGE_PROCESSING, 'type_range' => BOOL, 'confidentiality' => '2', ], [ 'key' => 'dropbox_key', 'value' => '', - 'cat' => 'Admin', + 'cat' => self::ADMIN, 'type_range' => STRING, 'confidentiality' => '3', ], [ 'key' => 'skip_duplicates', 'value' => '0', - 'cat' => 'Image Processing', + 'cat' => self::IMAGE_PROCESSING, 'type_range' => BOOL, 'confidentiality' => '2', ], [ 'key' => 'lang', 'value' => 'en', - 'cat' => 'Gallery', + 'cat' => self::GALLERY, 'type_range' => DISABLED, 'confidentiality' => '0', ], [ 'key' => 'layout', 'value' => '1', - 'cat' => 'Gallery', + 'cat' => self::GALLERY, 'type_range' => TERNARY, 'confidentiality' => '0', ], [ 'key' => 'image_overlay', 'value' => '1', - 'cat' => 'Gallery', + 'cat' => self::GALLERY, 'type_range' => BOOL, 'confidentiality' => '0', ], [ 'key' => 'default_license', 'value' => 'none', - 'cat' => 'Gallery', + 'cat' => self::GALLERY, 'type_range' => STRING_REQ, 'confidentiality' => '2', ], [ 'key' => 'small_max_width', 'value' => '0', - 'cat' => 'Image Processing', + 'cat' => self::IMAGE_PROCESSING, 'type_range' => INT, 'confidentiality' => '2', ], [ 'key' => 'small_max_height', 'value' => '360', - 'cat' => 'Image Processing', + 'cat' => self::IMAGE_PROCESSING, 'type_range' => INT, 'confidentiality' => '2', ], [ 'key' => 'medium_max_width', 'value' => '1920', - 'cat' => 'Image Processing', + 'cat' => self::IMAGE_PROCESSING, 'type_range' => INT, 'confidentiality' => '2', ], [ 'key' => 'medium_max_height', 'value' => '1080', - 'cat' => 'Image Processing', + 'cat' => self::IMAGE_PROCESSING, 'type_range' => INT, 'confidentiality' => '2', ], [ 'key' => 'full_photo', 'value' => '1', - 'cat' => 'Gallery', + 'cat' => self::GALLERY, 'type_range' => BOOL, 'confidentiality' => '0', ], [ 'key' => 'delete_imported', 'value' => '0', - 'cat' => 'Image Processing', + 'cat' => self::IMAGE_PROCESSING, 'type_range' => BOOL, 'confidentiality' => '2', ], [ 'key' => 'Mod_Frame', 'value' => '1', - 'cat' => 'Mod Frame', + 'cat' => self::MOD_FRAME, 'type_range' => BOOL, 'confidentiality' => '0', ], [ 'key' => 'Mod_Frame_refresh', 'value' => '30000', - 'cat' => 'Mod Frame', + 'cat' => self::MOD_FRAME, 'type_range' => INT, 'confidentiality' => '0', ], [ 'key' => 'image_overlay_type', 'value' => 'desc', - 'cat' => 'Gallery', + 'cat' => self::GALLERY, 'type_range' => 'exif|desc|takedate', 'confidentiality' => '0', ], [ 'key' => 'compression_quality', 'value' => '90', - 'cat' => 'Image Processing', + 'cat' => self::IMAGE_PROCESSING, 'type_range' => INT, 'confidentiality' => '2', ], [ 'key' => 'landing_page_enable', 'value' => '0', - 'cat' => 'Mod Welcome', + 'cat' => self::MOD_WELCOME, 'type_range' => BOOL, 'confidentiality' => '0', ], [ 'key' => 'landing_owner', 'value' => 'John Smith', - 'cat' => 'Mod Welcome', + 'cat' => self::MOD_WELCOME, 'type_range' => STRING, 'confidentiality' => '2', ], [ 'key' => 'landing_title', 'value' => 'John Smith', - 'cat' => 'Mod Welcome', + 'cat' => self::MOD_WELCOME, 'type_range' => STRING, 'confidentiality' => '2', ], [ 'key' => 'landing_subtitle', 'value' => 'Cats, Dogs & Humans Photography', - 'cat' => 'Mod Welcome', + 'cat' => self::MOD_WELCOME, 'type_range' => STRING, 'confidentiality' => '2', ], [ 'key' => 'landing_facebook', 'value' => 'https://www.facebook.com/JohnSmith', - 'cat' => 'Mod Welcome', + 'cat' => self::MOD_WELCOME, 'type_range' => STRING, 'confidentiality' => '2', ], [ 'key' => 'landing_flickr', 'value' => 'https://www.flickr.com/JohnSmith', - 'cat' => 'Mod Welcome', + 'cat' => self::MOD_WELCOME, 'type_range' => STRING, 'confidentiality' => '2', ], [ 'key' => 'landing_twitter', 'value' => 'https://www.twitter.com/JohnSmith', - 'cat' => 'Mod Welcome', + 'cat' => self::MOD_WELCOME, 'type_range' => STRING, 'confidentiality' => '2', ], [ 'key' => 'landing_instagram', 'value' => 'https://instagram.com/JohnSmith', - 'cat' => 'Mod Welcome', + 'cat' => self::MOD_WELCOME, 'type_range' => STRING, 'confidentiality' => '2', ], [ 'key' => 'landing_youtube', 'value' => 'https://www.youtube.com/JohnSmith', - 'cat' => 'Mod Welcome', + 'cat' => self::MOD_WELCOME, 'type_range' => STRING, 'confidentiality' => '2', ], [ 'key' => 'landing_background', 'value' => 'dist/cat.jpg', - 'cat' => 'Mod Welcome', + 'cat' => self::MOD_WELCOME, 'type_range' => STRING, 'confidentiality' => '2', ], [ 'key' => 'thumb_2x', 'value' => '1', - 'cat' => 'Image Processing', + 'cat' => self::IMAGE_PROCESSING, 'type_range' => BOOL, 'confidentiality' => '2', ], [ 'key' => 'small_2x', 'value' => '1', - 'cat' => 'Image Processing', + 'cat' => self::IMAGE_PROCESSING, 'type_range' => BOOL, 'confidentiality' => '2', ], [ 'key' => 'medium_2x', 'value' => '1', - 'cat' => 'Image Processing', + 'cat' => self::IMAGE_PROCESSING, 'type_range' => BOOL, 'confidentiality' => '2', ], [ 'key' => 'site_title', 'value' => 'Lychee v4', - 'cat' => 'config', + 'cat' => self::CONFIG, 'type_range' => STRING, 'confidentiality' => '0', ], [ 'key' => 'site_copyright_enable', 'value' => '1', - 'cat' => 'config', + 'cat' => self::CONFIG, 'type_range' => BOOL, 'confidentiality' => '2', ], [ 'key' => 'site_copyright_begin', 'value' => '2019', - 'cat' => 'config', + 'cat' => self::CONFIG, 'type_range' => INT, 'confidentiality' => '2', ], [ 'key' => 'site_copyright_end', 'value' => '2019', - 'cat' => 'config', + 'cat' => self::CONFIG, 'type_range' => INT, 'confidentiality' => '2', ], [ 'key' => 'api_key', 'value' => '', - 'cat' => 'Admin', + 'cat' => self::ADMIN, 'type_range' => STRING, 'confidentiality' => '3', ], [ 'key' => 'allow_online_git_pull', 'value' => '1', - 'cat' => 'Admin', + 'cat' => self::ADMIN, 'type_range' => BOOL, 'confidentiality' => '3', ], [ 'key' => 'force_migration_in_production', 'value' => '0', - 'cat' => 'Admin', + 'cat' => self::ADMIN, 'type_range' => BOOL, 'confidentiality' => '3', ], [ 'key' => 'additional_footer_text', 'value' => '', - 'cat' => 'config', + 'cat' => self::CONFIG, 'type_range' => STRING, 'confidentiality' => '2', ], [ 'key' => 'display_social_in_gallery', 'value' => '0', - 'cat' => 'config', + 'cat' => self::CONFIG, 'type_range' => BOOL, 'confidentiality' => '2', ], [ 'key' => 'public_search', 'value' => '0', - 'cat' => 'config', + 'cat' => self::CONFIG, 'type_range' => BOOL, 'confidentiality' => '0', ], [ 'key' => 'gen_demo_js', 'value' => '0', - 'cat' => 'Admin', + 'cat' => self::ADMIN, 'type_range' => BOOL, 'confidentiality' => '3', ], [ 'key' => 'hide_version_number', 'value' => '0', - 'cat' => 'config', + 'cat' => self::CONFIG, 'type_range' => BOOL, 'confidentiality' => '3', ], [ 'key' => 'SL_enable', 'value' => '0', - 'cat' => 'Symbolic Link', + 'cat' => self::SYMBOLIC_LINK, 'type_range' => BOOL, 'confidentiality' => '3', ], [ 'key' => 'SL_for_admin', 'value' => '0', - 'cat' => 'Symbolic Link', + 'cat' => self::SYMBOLIC_LINK, 'type_range' => BOOL, 'confidentiality' => '3', ], [ 'key' => 'SL_life_time_days', 'value' => '7', - 'cat' => 'Symbolic Link', + 'cat' => self::SYMBOLIC_LINK, 'type_range' => INT, 'confidentiality' => '3', ], [ 'key' => 'public_recent', 'value' => '0', - 'cat' => 'Smart Albums', + 'cat' => self::SMART_ALBUMS, 'type_range' => BOOL, 'confidentiality' => '0', ], [ 'key' => 'recent_age', 'value' => '1', - 'cat' => 'Smart Albums', + 'cat' => self::SMART_ALBUMS, 'type_range' => INT, 'confidentiality' => '2', ], [ 'key' => 'public_starred', 'value' => '0', - 'cat' => 'Smart Albums', + 'cat' => self::SMART_ALBUMS, 'type_range' => BOOL, 'confidentiality' => '0', ], [ 'key' => 'downloadable', 'value' => '0', - 'cat' => 'config', + 'cat' => self::CONFIG, 'type_range' => BOOL, 'confidentiality' => '0', ], [ 'key' => 'photos_wraparound', 'value' => '1', - 'cat' => 'Gallery', + 'cat' => self::GALLERY, 'type_range' => BOOL, 'confidentiality' => '0', ], [ 'key' => 'raw_formats', 'value' => '.tex', - 'cat' => 'config', + 'cat' => self::CONFIG, 'type_range' => DISABLED, 'confidentiality' => '3', ], [ 'key' => 'map_display', 'value' => '0', - 'cat' => 'Gallery', + 'cat' => self::GALLERY, 'type_range' => BOOL, 'confidentiality' => '0', ], [ 'key' => 'zip64', 'value' => '1', - 'cat' => 'config', + 'cat' => self::CONFIG, 'type_range' => BOOL, 'confidentiality' => '0', ], @@ -561,11 +560,9 @@ public function up() /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { - Logs::warning(__METHOD__, __LINE__, 'There is no going back for ' . __CLASS__ . '! HUE HUE HUE'); + Log::warning(__METHOD__ . ':' . __LINE__ . 'There is no going back for ' . __CLASS__ . '! HUE HUE HUE'); } -} +}; diff --git a/database/migrations/2019_09_28_190822_photos_fix.php b/database/migrations/2019_09_28_190822_photos_fix.php index ca3cd7d1364..fa451286a48 100644 --- a/database/migrations/2019_09_28_190822_photos_fix.php +++ b/database/migrations/2019_09_28_190822_photos_fix.php @@ -1,19 +1,21 @@ where('thumbUrl', '=', '') ->where('thumb2x', '=', '1') ->update([ 'thumb2x' => '0', @@ -23,7 +25,7 @@ private function fix_thumbs() }); } - private function image_direction() + private function image_direction(): void { // migration from imageDirection if (!Schema::hasColumn('photos', 'imgDirection')) { @@ -36,10 +38,8 @@ private function image_direction() /** * Run the migrations. - * - * @return void */ - public function up() + public function up(): void { $this->fix_thumbs(); $this->image_direction(); @@ -47,11 +47,8 @@ public function up() /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { - Logs::warning(__FUNCTION__, __LINE__, 'There is no going back! HUE HUE HUE'); } -} +}; diff --git a/database/migrations/2019_10_01_add_livephoto_cols.php b/database/migrations/2019_10_01_add_livephoto_cols.php index a86d545e4b2..addcb4e710e 100644 --- a/database/migrations/2019_10_01_add_livephoto_cols.php +++ b/database/migrations/2019_10_01_add_livephoto_cols.php @@ -1,17 +1,20 @@ string('livePhotoUrl')->default(null)->after('thumbURL')->nullable(); @@ -28,10 +31,8 @@ public function up() /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { Schema::table('photos', function (Blueprint $table) { $table->dropColumn('livePhotoContentID'); @@ -43,4 +44,4 @@ public function down() $table->dropColumn('livePhotoChecksum'); }); } -} +}; diff --git a/database/migrations/2019_10_02_1400_config_map_display_public.php b/database/migrations/2019_10_02_1400_config_map_display_public.php index ccd0aa22f1b..eae0a403bf4 100644 --- a/database/migrations/2019_10_02_1400_config_map_display_public.php +++ b/database/migrations/2019_10_02_1400_config_map_display_public.php @@ -1,40 +1,25 @@ insert([ + return [ [ 'key' => 'map_display_public', 'value' => '0', - 'confidentiality' => 0, + 'confidentiality' => '0', 'cat' => 'Mod Map', - 'type_range' => BOOL, + 'type_range' => self::BOOL, + 'description' => '', ], - ]); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Configs::where('key', '=', 'map_display_public')->delete(); + ]; } -} +}; diff --git a/database/migrations/2019_10_03_214750_frame_refresh_in_sec.php b/database/migrations/2019_10_03_214750_frame_refresh_in_sec.php index 4cd9261c05c..5d923cf5f13 100644 --- a/database/migrations/2019_10_03_214750_frame_refresh_in_sec.php +++ b/database/migrations/2019_10_03_214750_frame_refresh_in_sec.php @@ -1,37 +1,46 @@ update( - [ - 'value' => Configs::get_value('Mod_Frame_refresh') / 1000, - ] - ); + $value = DB::table('configs') + ->where('key', '=', 'Mod_Frame_refresh') + ->value('value'); + if (is_numeric($value)) { + DB::table('configs') + ->where('key', '=', 'Mod_Frame_refresh') + ->update(['value' => strval(intval(floatval($value) / 1000.0))]); + } } /** * Reverse the migrations. * - * @return void + * @throws InvalidArgumentException */ - public function down() + public function down(): void { - Configs::where('key', 'Mod_Frame_refresh') - ->update( - [ - 'value' => Configs::get_value('Mod_Frame_refresh') * 1000, - ] - ); + $value = DB::table('configs') + ->where('key', '=', 'Mod_Frame_refresh') + ->value('value'); + if (is_numeric($value)) { + DB::table('configs') + ->where('key', '=', 'Mod_Frame_refresh') + ->update(['value' => strval(intval(floatval($value) * 1000.0))]); + } } -} +}; diff --git a/database/migrations/2019_10_06_1400_config_map_providers.php b/database/migrations/2019_10_06_1400_config_map_providers.php index 4e92878840e..5f49e2ce1c1 100644 --- a/database/migrations/2019_10_06_1400_config_map_providers.php +++ b/database/migrations/2019_10_06_1400_config_map_providers.php @@ -1,40 +1,25 @@ insert([ + return [ [ 'key' => 'map_provider', 'value' => 'Wikimedia', - 'confidentiality' => 0, + 'confidentiality' => '0', 'cat' => 'Mod Map', - 'type_range' => MAP_PROVIDERS, + 'type_range' => 'Wikimedia|OpenStreetMap.org|OpenStreetMap.de|OpenStreetMap.fr|RRZE', + 'description' => '', ], - ]); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Configs::where('key', '=', 'map_provider')->delete(); + ]; } -} +}; diff --git a/database/migrations/2019_10_06_152017_add_force_32bit_ids.php b/database/migrations/2019_10_06_152017_add_force_32bit_ids.php index 5cd0ba44edc..47545340818 100644 --- a/database/migrations/2019_10_06_152017_add_force_32bit_ids.php +++ b/database/migrations/2019_10_06_152017_add_force_32bit_ids.php @@ -1,37 +1,25 @@ insert([ +return new class() extends BaseConfigMigration { + public function getConfigs(): array + { + return [ [ 'key' => 'force_32bit_ids', 'value' => '0', - 'cat' => 'config', - 'type_range' => BOOL, 'confidentiality' => '0', + 'cat' => 'config', + 'type_range' => self::BOOL, + 'description' => '', ], - ]); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Configs::where('key', '=', 'force_32bit_ids')->delete(); + ]; } -} +}; diff --git a/database/migrations/2019_10_07_0900_config_map_include_sub_albums.php b/database/migrations/2019_10_07_0900_config_map_include_sub_albums.php index 3f0117e0e73..d2595ad3706 100644 --- a/database/migrations/2019_10_07_0900_config_map_include_sub_albums.php +++ b/database/migrations/2019_10_07_0900_config_map_include_sub_albums.php @@ -1,40 +1,25 @@ insert([ + return [ [ 'key' => 'map_include_subalbums', 'value' => '0', - 'confidentiality' => 0, + 'confidentiality' => '0', 'cat' => 'Mod Map', - 'type_range' => BOOL, + 'type_range' => self::BOOL, + 'description' => '', ], - ]); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Configs::where('key', '=', 'map_include_subalbums')->delete(); + ]; } -} +}; diff --git a/database/migrations/2019_10_09_233402_config_map_mod.php b/database/migrations/2019_10_09_233402_config_map_mod.php index 4ea2a8643f7..3577f2f2eb2 100644 --- a/database/migrations/2019_10_09_233402_config_map_mod.php +++ b/database/migrations/2019_10_09_233402_config_map_mod.php @@ -1,27 +1,28 @@ update(['cat' => 'Mod Map']); + DB::table('configs')->where('key', '=', 'map_display')->update(['cat' => 'Mod Map']); } /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { - Configs::where('key', '=', 'map_display')->update(['cat' => 'config']); + DB::table('configs')->where('key', '=', 'map_display')->update(['cat' => 'config']); } -} +}; diff --git a/database/migrations/2019_10_11_093442_config_check_update_every.php b/database/migrations/2019_10_11_093442_config_check_update_every.php index eabf6ca1a8a..6a4781977fe 100644 --- a/database/migrations/2019_10_11_093442_config_check_update_every.php +++ b/database/migrations/2019_10_11_093442_config_check_update_every.php @@ -1,40 +1,25 @@ insert([ + return [ [ 'key' => 'update_check_every_days', 'value' => '3', - 'confidentiality' => 2, + 'confidentiality' => '2', 'cat' => 'Config', - 'type_range' => INT, + 'type_range' => self::INT, + 'description' => '', ], - ]); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Configs::where('key', '=', 'update_check_every_days')->delete(); + ]; } -} +}; diff --git a/database/migrations/2019_12_02_2100_config_exiftool.php b/database/migrations/2019_12_02_2100_config_exiftool.php index b3fe4c4c79f..6d2061e2dd3 100644 --- a/database/migrations/2019_12_02_2100_config_exiftool.php +++ b/database/migrations/2019_12_02_2100_config_exiftool.php @@ -1,42 +1,25 @@ insert([ + return [ [ 'key' => 'has_exiftool', - 'value' => null, - 'confidentiality' => 2, + 'value' => '', + 'confidentiality' => '2', 'cat' => 'Image Processing', - 'type_range' => BOOL, + 'type_range' => self::BOOL, + 'description' => '', ], - ]); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - if (env('DB_DROP_CLEAR_TABLES_ON_ROLLBACK', false)) { - Configs::where('key', '=', 'has_exiftool')->delete(); - } + ]; } -} +}; diff --git a/database/migrations/2019_12_15_0700_add_share_button_visible_option.php b/database/migrations/2019_12_15_0700_add_share_button_visible_option.php index eeecff08cf3..ee78ae6350b 100644 --- a/database/migrations/2019_12_15_0700_add_share_button_visible_option.php +++ b/database/migrations/2019_12_15_0700_add_share_button_visible_option.php @@ -1,19 +1,21 @@ boolean('share_button_visible')->after('downloadable')->default(false); }); - Album::query() - ->withoutGlobalScopes() + DB::table('albums') ->where('public', '=', 1) ->update([ 'share_button_visible' => true, @@ -39,15 +40,10 @@ public function up() /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { - Schema::table('albums', function (Blueprint $table) { - $table->dropColumn('share_button_visible'); - }); - + Schema::dropColumns('albums', ['share_button_visible']); DB::table('configs')->where('key', 'share_button_visible')->delete(); } -} +}; diff --git a/database/migrations/2019_12_15_1000_config_check_update_every_cat_fix.php b/database/migrations/2019_12_15_1000_config_check_update_every_cat_fix.php index 9798cc71657..a9443fbe510 100644 --- a/database/migrations/2019_12_15_1000_config_check_update_every_cat_fix.php +++ b/database/migrations/2019_12_15_1000_config_check_update_every_cat_fix.php @@ -1,29 +1,28 @@ update(['cat' => 'config']); + DB::table('configs')->where('key', 'update_check_every_days')->update(['cat' => 'config']); } /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { - Configs::where('key', 'update_check_every_days')->update(['cat' => 'Config']); + DB::table('configs')->where('key', 'update_check_every_days')->update(['cat' => 'Config']); } -} +}; diff --git a/database/migrations/2019_12_25_0600_config_exiftool_ternary.php b/database/migrations/2019_12_25_0600_config_exiftool_ternary.php index 9489b0e8d49..8328c7fe0e6 100644 --- a/database/migrations/2019_12_25_0600_config_exiftool_ternary.php +++ b/database/migrations/2019_12_25_0600_config_exiftool_ternary.php @@ -1,59 +1,62 @@ where('key', '=', 'has_exiftool') ->update( [ 'value' => $has_exiftool, - 'type_range' => TERNARY, + 'type_range' => self::TERNARY, ] ); } /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { - defined('BOOL') or define('BOOL', '0|1'); - - Configs::where('key', '=', 'has_exiftool') + DB::table('configs')->where('key', '=', 'has_exiftool') ->update( [ 'value' => null, - 'type_range' => BOOL, + 'type_range' => self::BOOL, ] ); } -} +}; diff --git a/database/migrations/2020_01_018_2300_config_import_via_symlink.php b/database/migrations/2020_01_018_2300_config_import_via_symlink.php index cc6a0752281..fadc5148267 100644 --- a/database/migrations/2020_01_018_2300_config_import_via_symlink.php +++ b/database/migrations/2020_01_018_2300_config_import_via_symlink.php @@ -1,40 +1,25 @@ insert([ + return [ [ 'key' => 'import_via_symlink', - 'value' => 0, - 'confidentiality' => 2, + 'value' => '0', + 'confidentiality' => '2', 'cat' => 'Image Processing', - 'type_range' => BOOL, + 'type_range' => self::BOOL, + 'description' => '', ], - ]); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Configs::where('key', '=', 'import_via_symlink')->delete(); + ]; } -} +}; diff --git a/database/migrations/2020_01_04_1200_config_has_ffmpeg.php b/database/migrations/2020_01_04_1200_config_has_ffmpeg.php index f243b7738d8..0785889405e 100644 --- a/database/migrations/2020_01_04_1200_config_has_ffmpeg.php +++ b/database/migrations/2020_01_04_1200_config_has_ffmpeg.php @@ -1,35 +1,41 @@ insert([ @@ -38,18 +44,16 @@ public function up() 'value' => $has_ffmpeg, 'confidentiality' => 2, 'cat' => 'Image Processing', - 'type_range' => TERNARY, + 'type_range' => self::TERNARY, ], ]); } /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { - Configs::where('key', '=', 'has_ffmpeg')->delete(); + DB::table('configs')->where('key', '=', 'has_ffmpeg')->delete(); } -} +}; diff --git a/database/migrations/2020_01_26_1200_config_public_sorting.php b/database/migrations/2020_01_26_1200_config_public_sorting.php index c50041a9c21..51f180dcfe7 100644 --- a/database/migrations/2020_01_26_1200_config_public_sorting.php +++ b/database/migrations/2020_01_26_1200_config_public_sorting.php @@ -1,31 +1,30 @@ update(['confidentiality' => '0']); - Configs::where('key', 'sorting_Albums_order')->update(['confidentiality' => '0']); + DB::table('configs')->where('key', 'sorting_Albums_col')->update(['confidentiality' => '0']); + DB::table('configs')->where('key', 'sorting_Albums_order')->update(['confidentiality' => '0']); } /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { - Configs::where('key', 'sorting_Albums_col')->update(['confidentiality' => '2']); - Configs::where('key', 'sorting_Albums_order')->update(['confidentiality' => '2']); + DB::table('configs')->where('key', 'sorting_Albums_col')->update(['confidentiality' => '2']); + DB::table('configs')->where('key', 'sorting_Albums_order')->update(['confidentiality' => '2']); } -} +}; diff --git a/database/migrations/2020_01_28_133201_composer_update.php b/database/migrations/2020_01_28_133201_composer_update.php index 86c62aab97d..55dd742fd5e 100644 --- a/database/migrations/2020_01_28_133201_composer_update.php +++ b/database/migrations/2020_01_28_133201_composer_update.php @@ -1,40 +1,25 @@ insert([ + return [ [ 'key' => 'apply_composer_update', 'value' => '0', 'confidentiality' => '3', 'cat' => 'Admin', - 'type_range' => BOOL, + 'type_range' => self::BOOL, + 'description' => '', ], - ]); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Configs::where('key', '=', 'apply_composer_update')->delete(); + ]; } -} +}; diff --git a/database/migrations/2020_02_14_0600_location_decoding.php b/database/migrations/2020_02_14_0600_location_decoding.php index fbb19fe68cb..7aae359b7ad 100644 --- a/database/migrations/2020_02_14_0600_location_decoding.php +++ b/database/migrations/2020_02_14_0600_location_decoding.php @@ -1,21 +1,21 @@ delete(); - Configs::where('key', '=', 'location_decoding_timeout')->delete(); - Configs::where('key', '=', 'location_show')->delete(); - Configs::where('key', '=', 'location_show_public')->delete(); + DB::table('configs')->where('key', '=', 'location_decoding')->delete(); + DB::table('configs')->where('key', '=', 'location_decoding_timeout')->delete(); + DB::table('configs')->where('key', '=', 'location_show')->delete(); + DB::table('configs')->where('key', '=', 'location_show_public')->delete(); Schema::table('photos', function (Blueprint $table) { $table->dropColumn('location'); }); } -} +}; diff --git a/database/migrations/2020_03_11_124417_increase_length_photo_type.php b/database/migrations/2020_03_11_124417_increase_length_photo_type.php index cc2266b1087..fd7f027d260 100644 --- a/database/migrations/2020_03_11_124417_increase_length_photo_type.php +++ b/database/migrations/2020_03_11_124417_increase_length_photo_type.php @@ -1,31 +1,32 @@ string('type', 30)->change(); + $table->string('type', 30)->default('')->change(); }); } /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { - Logs::warning(__FUNCTION__, __LINE__, 'There is no going back for ' . __CLASS__ . '!'); + Log::warning(__FUNCTION__ . ':' . __LINE__ . ' There is no going back for ' . __CLASS__ . '!'); } -} +}; diff --git a/database/migrations/2020_03_17_200000_unhide_configs.php b/database/migrations/2020_03_17_200000_unhide_configs.php index b56c3afec1f..5753ff19a4d 100644 --- a/database/migrations/2020_03_17_200000_unhide_configs.php +++ b/database/migrations/2020_03_17_200000_unhide_configs.php @@ -1,31 +1,30 @@ update(['confidentiality' => '2']); - Configs::where('key', 'SL_for_admin')->update(['confidentiality' => '2']); + DB::table('configs')->where('key', 'SL_enable')->update(['confidentiality' => '2']); + DB::table('configs')->where('key', 'SL_for_admin')->update(['confidentiality' => '2']); } /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { - Configs::where('key', 'SL_enable')->update(['confidentiality' => '0']); - Configs::where('key', 'SL_for_admin')->update(['confidentiality' => '0']); + DB::table('configs')->where('key', 'SL_enable')->update(['confidentiality' => '0']); + DB::table('configs')->where('key', 'SL_for_admin')->update(['confidentiality' => '0']); } -} +}; diff --git a/database/migrations/2020_04_19_122905_bump_version.php b/database/migrations/2020_04_19_122905_bump_version.php index d0e873987f9..e77b3c5acc5 100644 --- a/database/migrations/2020_04_19_122905_bump_version.php +++ b/database/migrations/2020_04_19_122905_bump_version.php @@ -1,27 +1,28 @@ update(['value' => '040001']); + DB::table('configs')->where('key', 'version')->update(['value' => '040001']); } /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { - Configs::where('key', 'version')->update(['value' => '040000']); + DB::table('configs')->where('key', 'version')->update(['value' => '040000']); } -} +}; diff --git a/database/migrations/2020_04_22_155712_bump_version040002.php b/database/migrations/2020_04_22_155712_bump_version040002.php index 2b03edb329f..293ee68c3cd 100644 --- a/database/migrations/2020_04_22_155712_bump_version040002.php +++ b/database/migrations/2020_04_22_155712_bump_version040002.php @@ -1,27 +1,28 @@ update(['value' => '040002']); + DB::table('configs')->where('key', 'version')->update(['value' => '040002']); } /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { - Configs::where('key', 'version')->update(['value' => '040001']); + DB::table('configs')->where('key', 'version')->update(['value' => '040001']); } -} +}; diff --git a/database/migrations/2020_04_29_000250_bump_version040003.php b/database/migrations/2020_04_29_000250_bump_version040003.php index 77f84d26614..e202d56203c 100644 --- a/database/migrations/2020_04_29_000250_bump_version040003.php +++ b/database/migrations/2020_04_29_000250_bump_version040003.php @@ -1,27 +1,28 @@ update(['value' => '040003']); + DB::table('configs')->where('key', 'version')->update(['value' => '040003']); } /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { - Configs::where('key', 'version')->update(['value' => '040002']); + DB::table('configs')->where('key', 'version')->update(['value' => '040002']); } -} +}; diff --git a/database/migrations/2020_05_12_114228_rss.php b/database/migrations/2020_05_12_114228_rss.php index 0fe0f19a28c..55e75930a82 100644 --- a/database/migrations/2020_05_12_114228_rss.php +++ b/database/migrations/2020_05_12_114228_rss.php @@ -1,55 +1,41 @@ insert([ +return new class() extends BaseConfigMigration { + public function getConfigs(): array + { + return [ [ 'key' => 'rss_enable', 'value' => '0', 'confidentiality' => '0', 'cat' => 'Mod RSS', - 'type_range' => BOOL, + 'type_range' => self::BOOL, + 'description' => '', ], [ 'key' => 'rss_recent_days', 'value' => '7', 'confidentiality' => '0', 'cat' => 'Mod RSS', - 'type_range' => INT, + 'type_range' => self::INT, + 'description' => '', ], [ 'key' => 'rss_max_items', 'value' => '100', 'confidentiality' => '0', 'cat' => 'Mod RSS', - 'type_range' => INT, + 'type_range' => self::INT, + 'description' => '', ], - ]); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Configs::where('key', '=', 'rss_enable')->delete(); - Configs::where('key', '=', 'rss_recent_days')->delete(); - Configs::where('key', '=', 'rss_max_items')->delete(); + ]; } -} +}; diff --git a/database/migrations/2020_05_12_161427_bump_version040005.php b/database/migrations/2020_05_12_161427_bump_version040005.php index 8047338ddab..af8a67f569d 100644 --- a/database/migrations/2020_05_12_161427_bump_version040005.php +++ b/database/migrations/2020_05_12_161427_bump_version040005.php @@ -1,27 +1,28 @@ update(['value' => '040005']); + DB::table('configs')->where('key', 'version')->update(['value' => '040005']); } /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { - Configs::where('key', 'version')->update(['value' => '040004']); + DB::table('configs')->where('key', 'version')->update(['value' => '040004']); } -} +}; diff --git a/database/migrations/2020_05_19_174233_config_prefer_available_xmp_metadata.php b/database/migrations/2020_05_19_174233_config_prefer_available_xmp_metadata.php index 1ab801b1cd2..3871f7e2f71 100644 --- a/database/migrations/2020_05_19_174233_config_prefer_available_xmp_metadata.php +++ b/database/migrations/2020_05_19_174233_config_prefer_available_xmp_metadata.php @@ -1,38 +1,25 @@ insert([ +return new class() extends BaseConfigMigration { + public function getConfigs(): array + { + return [ [ 'key' => 'prefer_available_xmp_metadata', 'value' => '0', 'confidentiality' => '2', 'cat' => 'Image Processing', - 'type_range' => BOOL, + 'type_range' => self::BOOL, + 'description' => '', ], - ]); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Configs::where('key', '=', 'prefer_available_xmp_metadata')->delete(); + ]; } -} +}; diff --git a/database/migrations/2020_05_26_135052_bump_version040006.php b/database/migrations/2020_05_26_135052_bump_version040006.php index e758573ab97..84d262efb77 100644 --- a/database/migrations/2020_05_26_135052_bump_version040006.php +++ b/database/migrations/2020_05_26_135052_bump_version040006.php @@ -1,27 +1,28 @@ update(['value' => '040006']); + DB::table('configs')->where('key', 'version')->update(['value' => '040006']); } /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { - Configs::where('key', 'version')->update(['value' => '040005']); + DB::table('configs')->where('key', 'version')->update(['value' => '040005']); } -} +}; diff --git a/database/migrations/2020_06_04_104605_config_editor_enabled.php b/database/migrations/2020_06_04_104605_config_editor_enabled.php index ead9a4914cd..7cbc1b31ebc 100644 --- a/database/migrations/2020_06_04_104605_config_editor_enabled.php +++ b/database/migrations/2020_06_04_104605_config_editor_enabled.php @@ -1,36 +1,25 @@ insert([ + return [ [ 'key' => 'editor_enabled', 'value' => '1', 'confidentiality' => '2', 'cat' => 'Image Processing', - 'type_range' => BOOL, + 'type_range' => self::BOOL, + 'description' => '', ], - ]); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Configs::where('key', '=', 'editor_enabled')->delete(); + ]; } -} +}; diff --git a/database/migrations/2020_07_11_104605_config_lossless_optimization.php b/database/migrations/2020_07_11_104605_config_lossless_optimization.php index dda147977db..01ec8b90fd3 100644 --- a/database/migrations/2020_07_11_104605_config_lossless_optimization.php +++ b/database/migrations/2020_07_11_104605_config_lossless_optimization.php @@ -1,36 +1,25 @@ insert([ + return [ [ 'key' => 'lossless_optimization', 'value' => '0', 'confidentiality' => '2', 'cat' => 'Image Processing', - 'type_range' => BOOL, + 'type_range' => self::BOOL, + 'description' => '', ], - ]); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Configs::where('key', '=', 'lossless_optimization')->delete(); + ]; } -} +}; diff --git a/database/migrations/2020_07_11_184605_update_licences.php b/database/migrations/2020_07_11_184605_update_licences.php index 978a65a438b..cfc0a68c2bb 100644 --- a/database/migrations/2020_07_11_184605_update_licences.php +++ b/database/migrations/2020_07_11_184605_update_licences.php @@ -1,20 +1,25 @@ updateOrInsert( ['key' => $value['key']], [ 'cat' => $value['cat'], @@ -27,10 +32,8 @@ private function update_fields(array &$default_values) /** * Run the migrations. - * - * @return void */ - public function up() + public function up(): void { defined('LICENSE') or define('LICENSE', 'license'); @@ -47,32 +50,30 @@ public function up() $this->update_fields($default_values); // Get all CC licences - $photos = Photo::where('license', 'like', 'CC-%')->get(); - if (count($photos) == 0) { - return false; + /** @var Collection $photos */ + $photos = DB::table('photos')->where('license', 'like', 'CC-%')->get(); + if ($photos->isEmpty()) { + return; } foreach ($photos as $photo) { - $photo->license = $photo->license . '-4.0'; - $photo->save(); + DB::table('photos')->where('id', '=', $photo->id)->update(['license' => $photo->license . '-4.0']); } } /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { // Get all CC licences - $photos = Photo::where('license', 'like', 'CC-%')->get(); - if (count($photos) == 0) { - return false; + /** @var Collection $photos */ + $photos = DB::table('photos')->where('license', 'like', 'CC-%')->get(); + if ($photos->isEmpty()) { + return; } foreach ($photos as $photo) { // Delete version - $photo->license = substr($photo->license, 0, -4); - $photo->save(); + DB::table('photos')->where('id', '=', $photo->id)->update(['license' => substr($photo->license, 0, -4)]); } } -} +}; diff --git a/database/migrations/2020_07_26_085322_config_swipe_tolerance.php b/database/migrations/2020_07_26_085322_config_swipe_tolerance.php index 3a17c29e940..24ddba7ec58 100644 --- a/database/migrations/2020_07_26_085322_config_swipe_tolerance.php +++ b/database/migrations/2020_07_26_085322_config_swipe_tolerance.php @@ -1,44 +1,33 @@ insert([ + return [ [ 'key' => 'swipe_tolerance_x', 'value' => '150', - 'confidentiality' => 0, + 'confidentiality' => '0', 'cat' => 'Gallery', - 'type_range' => INT, + 'type_range' => self::INT, + 'description' => '', ], [ 'key' => 'swipe_tolerance_y', 'value' => '250', - 'confidentiality' => 0, + 'confidentiality' => '0', 'cat' => 'Gallery', - 'type_range' => INT, + 'type_range' => self::INT, + 'description' => '', ], - ]); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Configs::where('key', '=', 'swipe_tolerance_x')->delete(); - Configs::where('key', '=', 'swipe_tolerance_y')->delete(); + ]; } -} +}; diff --git a/database/migrations/2020_07_29_132731_config_local_takestamp.php b/database/migrations/2020_07_29_132731_config_local_takestamp.php index 89560099479..3efb35bf25b 100644 --- a/database/migrations/2020_07_29_132731_config_local_takestamp.php +++ b/database/migrations/2020_07_29_132731_config_local_takestamp.php @@ -1,37 +1,25 @@ insert([ +return new class() extends BaseConfigMigration { + public function getConfigs(): array + { + return [ [ 'key' => 'local_takestamp_video_formats', 'value' => '.avi|.mov', 'confidentiality' => '2', 'cat' => 'Image Processing', - 'type_range' => DISABLED, + 'type_range' => '', + 'description' => '', ], - ]); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Configs::where('key', '=', 'local_takestamp_video_formats')->delete(); + ]; } -} +}; diff --git a/database/migrations/2020_08_21_123622_add_smart_tag_album_cols.php b/database/migrations/2020_08_21_123622_add_smart_tag_album_cols.php index 36d9dcd3700..2def957039f 100644 --- a/database/migrations/2020_08_21_123622_add_smart_tag_album_cols.php +++ b/database/migrations/2020_08_21_123622_add_smart_tag_album_cols.php @@ -1,21 +1,24 @@ boolean(self::SMART_COLUMN_NAME)->default(false)->after('license'); @@ -27,10 +30,8 @@ public function up() /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { Schema::table(self::ALBUM, function (Blueprint $table) { $table->dropColumn(self::SMART_COLUMN_NAME); @@ -39,4 +40,4 @@ public function down() $table->dropColumn(self::SHOWTAGS_COLUMN_NAME); }); } -} \ No newline at end of file +}; \ No newline at end of file diff --git a/database/migrations/2020_10_09_130043_bump_version040007.php b/database/migrations/2020_10_09_130043_bump_version040007.php index 2b2ee70bac2..66fd0bddedd 100644 --- a/database/migrations/2020_10_09_130043_bump_version040007.php +++ b/database/migrations/2020_10_09_130043_bump_version040007.php @@ -1,27 +1,28 @@ update(['value' => '040007']); + DB::table('configs')->where('key', 'version')->update(['value' => '040007']); } /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { - Configs::where('key', 'version')->update(['value' => '040006']); + DB::table('configs')->where('key', 'version')->update(['value' => '040006']); } -} +}; diff --git a/database/migrations/2020_10_15_104504_add_log_max_num_line.php b/database/migrations/2020_10_15_104504_add_log_max_num_line.php index a1d6c45048f..79d61d87dfe 100644 --- a/database/migrations/2020_10_15_104504_add_log_max_num_line.php +++ b/database/migrations/2020_10_15_104504_add_log_max_num_line.php @@ -1,37 +1,25 @@ insert([ +return new class() extends BaseConfigMigration { + public function getConfigs(): array + { + return [ [ 'key' => 'log_max_num_line', 'value' => '1000', 'confidentiality' => '2', 'cat' => 'Admin', - 'type_range' => INT, + 'type_range' => self::INT, + 'description' => '', ], - ]); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Configs::where('key', '=', 'log_max_num_line')->delete(); + ]; } -} +}; diff --git a/database/migrations/2020_10_15_161346_sort_image_per_album.php b/database/migrations/2020_10_15_161346_sort_image_per_album.php index 6ea7a320b3f..daee9e1791d 100644 --- a/database/migrations/2020_10_15_161346_sort_image_per_album.php +++ b/database/migrations/2020_10_15_161346_sort_image_per_album.php @@ -1,21 +1,24 @@ string(self::SORT_COLUMN_NAME, 30)->default('')->after('license'); @@ -27,10 +30,8 @@ public function up() /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { Schema::table(self::ALBUM, function (Blueprint $table) { $table->dropColumn(self::SORT_COLUMN_NAME); @@ -39,4 +40,4 @@ public function down() $table->dropColumn(self::SORT_COLUMN_ORDER); }); } -} +}; diff --git a/database/migrations/2020_11_12_183345_config_password_url_param_for_smart_album.php b/database/migrations/2020_11_12_183345_config_password_url_param_for_smart_album.php index b51884737ba..be25e9d98f9 100644 --- a/database/migrations/2020_11_12_183345_config_password_url_param_for_smart_album.php +++ b/database/migrations/2020_11_12_183345_config_password_url_param_for_smart_album.php @@ -1,36 +1,25 @@ insert([ + return [ [ 'key' => 'unlock_password_photos_with_url_param', - 'value' => 0, - 'confidentiality' => 2, + 'value' => '0', + 'confidentiality' => '2', 'cat' => 'Smart Albums', - 'type_range' => BOOL, + 'type_range' => self::BOOL, + 'description' => '', ], - ]); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Configs::where('key', '=', 'unlock_password_photos_with_url_param')->delete(); + ]; } -} +}; diff --git a/database/migrations/2020_11_19_231553_bump_version040008.php b/database/migrations/2020_11_19_231553_bump_version040008.php index 861f403d475..e52f245928f 100644 --- a/database/migrations/2020_11_19_231553_bump_version040008.php +++ b/database/migrations/2020_11_19_231553_bump_version040008.php @@ -1,27 +1,28 @@ update(['value' => '040008']); + DB::table('configs')->where('key', 'version')->update(['value' => '040008']); } /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { - Configs::where('key', 'version')->update(['value' => '040007']); + DB::table('configs')->where('key', 'version')->update(['value' => '040007']); } -} +}; diff --git a/database/migrations/2020_12_12_203153_migrate_admin_user.php b/database/migrations/2020_12_12_203153_migrate_admin_user.php index 47b606201b5..9fa37146bd0 100644 --- a/database/migrations/2020_12_12_203153_migrate_admin_user.php +++ b/database/migrations/2020_12_12_203153_migrate_admin_user.php @@ -1,41 +1,45 @@ username = Configs::get_value('username', ''); - $user->password = Configs::get_value('password', ''); - $user->save(); + $username = DB::table('configs')->select('value')->where('key', 'username')->first(); + $password = DB::table('configs')->select('value')->where('key', 'password')->first(); - // user will have a id which is NOT 0. - // we want this user to have an ID of 0 as it is the ADMIN ID. - $user->id = 0; - $user->save(); + DB::table('users')->updateOrInsert(['id' => 0], + [ + 'username' => $username?->value ?? '', + 'password' => $password?->value ?? '', + ]); } /** * Reverse the migrations. * - * @return void + * @throws InvalidArgumentException */ - public function down() + public function down(): void { if (Schema::hasTable('users')) { - $user = User::find(0); - if ($user != null) { - $user->delete(); - } + DB::table('users') + ->where('id', '=', 0) + ->delete(); } } -} +}; diff --git a/database/migrations/2020_12_12_203831_create_web_authn_tables.php b/database/migrations/2020_12_12_203831_create_web_authn_tables.php index 62de4aac9d3..ea349cdc59c 100644 --- a/database/migrations/2020_12_12_203831_create_web_authn_tables.php +++ b/database/migrations/2020_12_12_203831_create_web_authn_tables.php @@ -1,19 +1,23 @@ uuid('user_handle')->nullable(); $table->timestamps(); - $table->softDeletes(WebAuthnCredential::DELETED_AT); - Configs::where('key', '=', 'username')->orWhere('key', '=', 'password')->update(['type_range' => STRING]); + $table->softDeletes(self::DELETED_AT); + DB::table('configs')->where('key', '=', 'username')->orWhere('key', '=', 'password')->update(['type_range' => STRING]); $table->primary(['id', 'user_id']); }); @@ -47,16 +51,14 @@ public function up() /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { defined('STRING_REQ') or define('STRING_REQ', 'string_required'); if (Schema::hasTable('configs')) { - Configs::where('key', '=', 'username')->orWhere('key', '=', 'password')->update(['type_range' => STRING_REQ]); + DB::table('configs')->where('key', '=', 'username')->orWhere('key', '=', 'password')->update(['type_range' => STRING_REQ]); } Schema::dropIfExists('web_authn_credentials'); } -} +}; diff --git a/database/migrations/2020_12_18_162100_bump_version040009.php b/database/migrations/2020_12_18_162100_bump_version040009.php index 7cd9399f4d8..69068faf47f 100644 --- a/database/migrations/2020_12_18_162100_bump_version040009.php +++ b/database/migrations/2020_12_18_162100_bump_version040009.php @@ -1,27 +1,28 @@ update(['value' => '040009']); + DB::table('configs')->where('key', 'version')->update(['value' => '040009']); } /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { - Configs::where('key', 'version')->update(['value' => '040008']); + DB::table('configs')->where('key', 'version')->update(['value' => '040008']); } -} +}; diff --git a/database/migrations/2020_12_18_162155_add_nsfw_album.php b/database/migrations/2020_12_18_162155_add_nsfw_album.php index 5c19d454b0f..3d5562e668f 100644 --- a/database/migrations/2020_12_18_162155_add_nsfw_album.php +++ b/database/migrations/2020_12_18_162155_add_nsfw_album.php @@ -1,12 +1,17 @@ dropColumn(self::NSFW_COLUMN_NAME); @@ -53,6 +54,6 @@ public function down() $table->renameColumn(self::VIEWABLE, self::VISIBLE_HIDDEN); }); - Configs::where('cat', '=', 'Mod NSFW')->delete(); + DB::table('configs')->where('cat', '=', 'Mod NSFW')->delete(); } -} +}; diff --git a/database/migrations/2020_12_18_163800_bump_version040010.php b/database/migrations/2020_12_18_163800_bump_version040010.php index 910bf66e3d3..3b925b46658 100644 --- a/database/migrations/2020_12_18_163800_bump_version040010.php +++ b/database/migrations/2020_12_18_163800_bump_version040010.php @@ -1,27 +1,28 @@ update(['value' => '040010']); + DB::table('configs')->where('key', 'version')->update(['value' => '040010']); } /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { - Configs::where('key', 'version')->update(['value' => '040009']); + DB::table('configs')->where('key', 'version')->update(['value' => '040009']); } -} +}; diff --git a/database/migrations/2020_12_24_022307_bump_version040100.php b/database/migrations/2020_12_24_022307_bump_version040100.php index 2b5c28c4f52..2c79a71e4e5 100644 --- a/database/migrations/2020_12_24_022307_bump_version040100.php +++ b/database/migrations/2020_12_24_022307_bump_version040100.php @@ -1,27 +1,28 @@ update(['value' => '040100']); + DB::table('configs')->where('key', 'version')->update(['value' => '040100']); } /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { - Configs::where('key', 'version')->update(['value' => '040010']); + DB::table('configs')->where('key', 'version')->update(['value' => '040010']); } -} +}; diff --git a/database/migrations/2020_12_26_153220_nested_set_for_albums.php b/database/migrations/2020_12_26_153220_nested_set_for_albums.php index c8486c3e9c1..4845f4162b0 100644 --- a/database/migrations/2020_12_26_153220_nested_set_for_albums.php +++ b/database/migrations/2020_12_26_153220_nested_set_for_albums.php @@ -1,22 +1,26 @@ unsignedBigInteger(self::LEFT)->nullable()->default(null)->after('parent_id'); @@ -28,15 +32,13 @@ public function up() $table->index([self::LEFT, self::RIGHT]); }); - Album::fixTree(); + NestedSetForAlbums_AlbumModel::query()->fixTree(); } /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { Schema::table(self::ALBUMS, function (Blueprint $table) { $table->dropColumn(self::LEFT); @@ -45,4 +47,5 @@ public function down() $table->dropColumn(self::RIGHT); }); } -} +}; + diff --git a/database/migrations/2021_01_09_163715_remove_max_min_takestamps.php b/database/migrations/2021_01_09_163715_remove_max_min_takestamps.php index 6a7a5e70753..fa2a7edd807 100644 --- a/database/migrations/2021_01_09_163715_remove_max_min_takestamps.php +++ b/database/migrations/2021_01_09_163715_remove_max_min_takestamps.php @@ -1,22 +1,25 @@ dropColumn(self::MIN); @@ -28,10 +31,8 @@ public function up() /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { Schema::table(self::ALBUMS, function ($table) { $table->timestamp(self::MIN)->nullable()->after('description'); @@ -40,11 +41,34 @@ public function down() $table->timestamp(self::MAX)->nullable()->after(self::MIN); }); - $albums = Album::query()->withoutGlobalScopes()->get(); - foreach ($albums as $_album) { - $_album->min_takestamp = $_album->get_all_photos()->whereNotNull('takestamp')->min('takestamp'); - $_album->max_takestamp = $_album->get_all_photos()->whereNotNull('takestamp')->max('takestamp'); - $_album->save(); + $albums = DB::table('albums') + ->select(['id']) + ->addSelect([ + 'min_takestamp' => DB::table('photos') + ->select('takestamp') + ->join('albums as a', 'a.id', '=', 'album_id') + ->whereColumn('a._lft', '>=', 'albums._lft') + ->whereColumn('a._rgt', '<=', 'albums._rgt') + ->whereNotNull('takestamp') + ->orderBy('takestamp', 'asc') + ->limit(1), + 'max_takestamp' => DB::table('photos') + ->select('takestamp') + ->join('albums as a', 'a.id', '=', 'album_id') + ->whereColumn('a._lft', '>=', 'albums._lft') + ->whereColumn('a._rgt', '<=', 'albums._rgt') + ->whereNotNull('takestamp') + ->orderBy('takestamp', 'desc') + ->limit(1), + ]) + ->get(); + foreach ($albums as $album) { + DB::table('albums') + ->where('id', '=', $album->id) + ->update([ + 'min_takestamp' => $album->min_takestamp, + 'max_takestamp' => $album->max_takestamp, + ]); } } -} +}; diff --git a/database/migrations/2021_01_12_122546_bump_version040200.php b/database/migrations/2021_01_12_122546_bump_version040200.php index f94164d36e6..271541cd8a7 100644 --- a/database/migrations/2021_01_12_122546_bump_version040200.php +++ b/database/migrations/2021_01_12_122546_bump_version040200.php @@ -1,27 +1,28 @@ update(['value' => '040200']); + DB::table('configs')->where('key', 'version')->update(['value' => '040200']); } /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { - Configs::where('key', 'version')->update(['value' => '040100']); + DB::table('configs')->where('key', 'version')->update(['value' => '040100']); } -} +}; diff --git a/database/migrations/2021_01_18_103729_add_album_cover.php b/database/migrations/2021_01_18_103729_add_album_cover.php index 316f89601c0..ee8d9d1a3ce 100644 --- a/database/migrations/2021_01_18_103729_add_album_cover.php +++ b/database/migrations/2021_01_18_103729_add_album_cover.php @@ -1,20 +1,23 @@ bigInteger(self::COVER)->unsigned()->nullable()->default(null); @@ -23,13 +26,11 @@ public function up() /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { Schema::table(self::ALBUMS, function (Blueprint $table) { $table->dropColumn(self::COVER); }); } -} +}; diff --git a/database/migrations/2021_01_20_113912_bump_version040201.php b/database/migrations/2021_01_20_113912_bump_version040201.php index 56f5d141fc2..f9a9a7dc23f 100644 --- a/database/migrations/2021_01_20_113912_bump_version040201.php +++ b/database/migrations/2021_01_20_113912_bump_version040201.php @@ -1,27 +1,28 @@ update(['value' => '040201']); + DB::table('configs')->where('key', 'version')->update(['value' => '040201']); } /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { - Configs::where('key', 'version')->update(['value' => '040200']); + DB::table('configs')->where('key', 'version')->update(['value' => '040200']); } -} +}; diff --git a/database/migrations/2021_01_24_231904_fix-rotation.php b/database/migrations/2021_01_24_231904_fix-rotation.php index f6835f4560b..9b5d613f90c 100644 --- a/database/migrations/2021_01_24_231904_fix-rotation.php +++ b/database/migrations/2021_01_24_231904_fix-rotation.php @@ -1,30 +1,31 @@ update(['small' => '']); - Photo::where('small2x', 'x')->update(['small2x' => '']); - Photo::where('medium', 'x')->update(['medium' => '']); - Photo::where('medium2x', 'x')->update(['medium2x' => '']); + DB::table('photos')->where('small', 'x')->update(['small' => '']); + DB::table('photos')->where('small2x', 'x')->update(['small2x' => '']); + DB::table('photos')->where('medium', 'x')->update(['medium' => '']); + DB::table('photos')->where('medium2x', 'x')->update(['medium2x' => '']); } /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { // There is no undo } -} +}; diff --git a/database/migrations/2021_01_27_085903_config_map_display_direction.php b/database/migrations/2021_01_27_085903_config_map_display_direction.php index 924daabacc5..5ac893c310c 100644 --- a/database/migrations/2021_01_27_085903_config_map_display_direction.php +++ b/database/migrations/2021_01_27_085903_config_map_display_direction.php @@ -1,40 +1,25 @@ insert([ + return [ [ 'key' => 'map_display_direction', 'value' => '1', - 'confidentiality' => 0, + 'confidentiality' => '0', 'cat' => 'Mod Map', - 'type_range' => BOOL, + 'type_range' => self::BOOL, + 'description' => '', ], - ]); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Configs::where('key', '=', 'map_display_direction')->delete(); + ]; } -} +}; diff --git a/database/migrations/2021_01_30_111736_display_takedate.php b/database/migrations/2021_01_30_111736_display_takedate.php index 55c5a8ad0a4..bc86e20a01c 100644 --- a/database/migrations/2021_01_30_111736_display_takedate.php +++ b/database/migrations/2021_01_30_111736_display_takedate.php @@ -1,35 +1,25 @@ insert([ + return [ [ 'key' => 'album_subtitle_type', 'value' => 'oldstyle', 'confidentiality' => '0', 'cat' => 'Gallery', 'type_range' => 'description|takedate|creation|oldstyle', + 'description' => '', ], - ]); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Configs::where('key', '=', 'album_subtitle_type')->delete(); + ]; } -} +}; diff --git a/database/migrations/2021_02_12_222948_config_upload_processing_limit.php b/database/migrations/2021_02_12_222948_config_upload_processing_limit.php index 5d43297ef85..bb369a84a7c 100644 --- a/database/migrations/2021_02_12_222948_config_upload_processing_limit.php +++ b/database/migrations/2021_02_12_222948_config_upload_processing_limit.php @@ -1,38 +1,25 @@ insert([ +return new class() extends BaseConfigMigration { + public function getConfigs(): array + { + return [ [ 'key' => 'upload_processing_limit', 'value' => '4', - 'confidentiality' => 2, + 'confidentiality' => '2', 'cat' => 'Image Processing', - 'type_range' => INT, + 'type_range' => self::INT, + 'description' => '', ], - ]); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Configs::where('key', '=', 'upload_processing_limit')->delete(); + ]; } -} +}; diff --git a/database/migrations/2021_02_13_132245_bump_version040202.php b/database/migrations/2021_02_13_132245_bump_version040202.php index 7da82e22992..87678c8376e 100644 --- a/database/migrations/2021_02_13_132245_bump_version040202.php +++ b/database/migrations/2021_02_13_132245_bump_version040202.php @@ -1,27 +1,28 @@ update(['value' => '040202']); + DB::table('configs')->where('key', 'version')->update(['value' => '040202']); } /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { - Configs::where('key', 'version')->update(['value' => '040201']); + DB::table('configs')->where('key', 'version')->update(['value' => '040201']); } -} +}; diff --git a/database/migrations/2021_02_18_232639_config_public_photos_hidden.php b/database/migrations/2021_02_18_232639_config_public_photos_hidden.php index 11f20ed5c06..2577ac97463 100644 --- a/database/migrations/2021_02_18_232639_config_public_photos_hidden.php +++ b/database/migrations/2021_02_18_232639_config_public_photos_hidden.php @@ -1,38 +1,25 @@ insert([ +return new class() extends BaseConfigMigration { + public function getConfigs(): array + { + return [ [ 'key' => 'public_photos_hidden', 'value' => '1', - 'confidentiality' => 2, + 'confidentiality' => '2', 'cat' => 'config', - 'type_range' => BOOL, + 'type_range' => self::BOOL, + 'description' => '', ], - ]); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Configs::where('key', '=', 'public_photos_hidden')->delete(); + ]; } -} +}; diff --git a/database/migrations/2021_03_03_175555_config_remove_image_overlay.php b/database/migrations/2021_03_03_175555_config_remove_image_overlay.php index 30c1a7e29ae..e76e15ad33d 100644 --- a/database/migrations/2021_03_03_175555_config_remove_image_overlay.php +++ b/database/migrations/2021_03_03_175555_config_remove_image_overlay.php @@ -1,32 +1,32 @@ update(['type_range' => 'exif|desc|date|none']); - Configs::where('key', '=', 'image_overlay')->delete(); + DB::table('configs')->where('key', '=', 'image_overlay_type')->update(['type_range' => 'exif|desc|date|none']); + DB::table('configs')->where('key', '=', 'image_overlay')->delete(); } /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { defined('BOOL') or define('BOOL', '0|1'); - Configs::where('key', '=', 'image_overlay_type')->update(['type_range' => 'exif|desc|takedate']); + DB::table('configs')->where('key', '=', 'image_overlay_type')->update(['type_range' => 'exif|desc|takedate']); DB::table('configs')->insert([ [ @@ -38,4 +38,4 @@ public function down() ], ]); } -} +}; diff --git a/database/migrations/2021_04_17_135924_bump_version040300.php b/database/migrations/2021_04_17_135924_bump_version040300.php index 81e7ccdbdc8..52a7d75571e 100644 --- a/database/migrations/2021_04_17_135924_bump_version040300.php +++ b/database/migrations/2021_04_17_135924_bump_version040300.php @@ -1,27 +1,28 @@ update(['value' => '040300']); + DB::table('configs')->where('key', 'version')->update(['value' => '040300']); } /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { - Configs::where('key', 'version')->update(['value' => '040202']); + DB::table('configs')->where('key', 'version')->update(['value' => '040202']); } -} +}; diff --git a/database/migrations/2021_05_02_174300_add_filesize_raw_col.php b/database/migrations/2021_05_02_174300_add_filesize_raw_col.php index 478de6cf0d3..2074d029a3f 100644 --- a/database/migrations/2021_05_02_174300_add_filesize_raw_col.php +++ b/database/migrations/2021_05_02_174300_add_filesize_raw_col.php @@ -1,12 +1,17 @@ unsignedBigInteger(self::NEW_COL_NAME)->default(0)->after(self::OLD_COL_NAME); @@ -60,10 +63,8 @@ public function up() /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { Schema::table(self::TABLE_NAME, function (Blueprint $table) { $table->string(self::OLD_COL_NAME, 20)->default('')->after(self::NEW_COL_NAME); @@ -94,4 +95,4 @@ public function down() $table->dropColumn(self::NEW_COL_NAME); }); } -} +}; diff --git a/database/migrations/2021_05_12_185726_bump_version040301.php b/database/migrations/2021_05_12_185726_bump_version040301.php index d27977c9b88..c93e384c4ef 100644 --- a/database/migrations/2021_05_12_185726_bump_version040301.php +++ b/database/migrations/2021_05_12_185726_bump_version040301.php @@ -1,27 +1,28 @@ update(['value' => '040301']); + DB::table('configs')->where('key', 'version')->update(['value' => '040301']); } /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { - Configs::where('key', 'version')->update(['value' => '040300']); + DB::table('configs')->where('key', 'version')->update(['value' => '040300']); } -} +}; diff --git a/database/migrations/2021_05_13_140700_refactor_size_variants.php b/database/migrations/2021_05_13_140700_refactor_size_variants.php index dee5c6bc515..bbe988cac5a 100644 --- a/database/migrations/2021_05_13_140700_refactor_size_variants.php +++ b/database/migrations/2021_05_13_140700_refactor_size_variants.php @@ -1,12 +1,17 @@ integer(self::SMALL_WIDTH_COL_NAME)->unsigned()->nullable()->default(null); @@ -50,11 +53,10 @@ public function up() ])->lazyById(); foreach ($photos as $photo) { - $this->convertUp($photo->{self::SMALL_COL_NAME}, $smallWidth, $smallHeight); - $this->convertUp($photo->{self::SMALL2X_COL_NAME}, $small2xWidth, $small2xHeight); - $this->convertUp($photo->{self::MEDIUM_COL_NAME}, $mediumWidth, $mediumHeight); - $this->convertUp($photo->{self::MEDIUM2X_COL_NAME}, $medium2xWidth, $medium2xHeight); - + $this->convertUp($photo->{self::SMALL_COL_NAME}, $smallWidth, $smallHeight); /** @phpstan-ignore-line */ + $this->convertUp($photo->{self::SMALL2X_COL_NAME}, $small2xWidth, $small2xHeight); /** @phpstan-ignore-line */ + $this->convertUp($photo->{self::MEDIUM_COL_NAME}, $mediumWidth, $mediumHeight); /** @phpstan-ignore-line */ + $this->convertUp($photo->{self::MEDIUM2X_COL_NAME}, $medium2xWidth, $medium2xHeight); /** @phpstan-ignore-line */ DB::table(self::PHOTOS_TABLE_NAME)->where(self::ID_COL_NAME, '=', $photo->id)->update([ self::SMALL_WIDTH_COL_NAME => $smallWidth, self::SMALL_HEIGHT_COL_NAME => $smallHeight, @@ -82,16 +84,14 @@ public function up() protected function convertUp(string $sizeString, ?int &$width, ?int &$height): void { $size = explode('x', $sizeString); - $width = ($size !== false && count($size) === 2) ? (int) ($size[0]) : null; - $height = ($size !== false && count($size) === 2) ? (int) ($size[1]) : null; + $width = count($size) === 2 ? (int) ($size[0]) : null; + $height = count($size) === 2 ? (int) ($size[1]) : null; } /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { Schema::table(self::PHOTOS_TABLE_NAME, function (Blueprint $table) { $table->string(self::SMALL_COL_NAME)->default(''); @@ -114,9 +114,13 @@ public function down() ])->lazyById(); foreach ($photos as $photo) { + /** @phpstan-ignore-next-line */ $smallSize = $this->convertDown($photo->{self::SMALL_WIDTH_COL_NAME}, $photo->{self::SMALL_HEIGHT_COL_NAME}); + /** @phpstan-ignore-next-line */ $small2xSize = $this->convertDown($photo->{self::SMALL2X_WIDTH_COL_NAME}, $photo->{self::SMALL2X_HEIGHT_COL_NAME}); + /** @phpstan-ignore-next-line */ $mediumSize = $this->convertDown($photo->{self::MEDIUM_WIDTH_COL_NAME}, $photo->{self::MEDIUM_HEIGHT_COL_NAME}); + /** @phpstan-ignore-next-line */ $medium2xSize = $this->convertDown($photo->{self::MEDIUM2X_WIDTH_COL_NAME}, $photo->{self::MEDIUM2X_HEIGHT_COL_NAME}); DB::table(self::PHOTOS_TABLE_NAME)->where(self::ID_COL_NAME, '=', $photo->id)->update([ @@ -147,4 +151,4 @@ protected function convertDown(?int $width, ?int $height): string { return ($width !== null) ? $width . 'x' . $height : ''; } -} +}; diff --git a/database/migrations/2021_05_16_171615_bump_version040302.php b/database/migrations/2021_05_16_171615_bump_version040302.php index 035931f264c..f3c95d40686 100644 --- a/database/migrations/2021_05_16_171615_bump_version040302.php +++ b/database/migrations/2021_05_16_171615_bump_version040302.php @@ -1,27 +1,28 @@ update(['value' => '040302']); + DB::table('configs')->where('key', 'version')->update(['value' => '040302']); } /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { - Configs::where('key', 'version')->update(['value' => '040301']); + DB::table('configs')->where('key', 'version')->update(['value' => '040301']); } -} +}; diff --git a/database/migrations/2021_05_25_160600_post_revert_fixes.php b/database/migrations/2021_05_25_160600_post_revert_fixes.php index 8a75a574dc2..d196ed7ecbb 100644 --- a/database/migrations/2021_05_25_160600_post_revert_fixes.php +++ b/database/migrations/2021_05_25_160600_post_revert_fixes.php @@ -1,20 +1,24 @@ 'taken_at']; + // private const CONFIG_PS_VALUE_OLD2NEW = ['takestamp' => 'taken_at']; private const CONFIG_PS_VALUE_NEW2OLD = ['taken_at' => 'takestamp']; private const CONFIG_PS_RANGE_OLD = 'id|takestamp|title|description|public|star|type'; - private const CONFIG_PS_RANGE_NEW = 'id|taken_at|title|description|public|star|type'; // All constants related to the configuration of Album sorting (AS) private const CONFIG_AS_KEY = 'sorting_Albums_col'; - private const CONFIG_AS_VALUE_OLD2NEW = [ - 'min_takestamp' => 'min_taken_at', - 'max_takestamp' => 'max_taken_at', - ]; private const CONFIG_AS_VALUE_NEW2OLD = [ 'min_taken_at' => 'min_takestamp', 'max_taken_at' => 'max_takestamp', ]; private const CONFIG_AS_RANGE_OLD = 'id|title|description|public|max_takestamp|min_takestamp|created_at'; - private const CONFIG_AS_RANGE_NEW = 'id|title|description|public|max_taken_at|min_taken_at|created_at'; private const DB_TIMEZONE_NAME = 'UTC'; /** * Run the migration. */ - public function up() + public function up(): void { - if (Configs::get_value('version') !== '040303') { + $version = DB::table('configs')->select(['value'])->where('key', '=', 'version')->first()?->value; + if ($version !== '040303') { return; } @@ -108,13 +107,13 @@ public function up() ]); }); - Configs::where('key', 'version')->update(['value' => '040302']); + DB::table('configs')->where('key', 'version')->update(['value' => '040302']); } /** * Reverse the migration. */ - public function down() + public function down(): void { } @@ -214,6 +213,7 @@ protected function getConfiguration(string $key): string ->where(self::CONFIG_KEY_COL_NAME, '=', $key) ->first(); + /** @phpstan-ignore-next-line */ // Access to an undefined property object::$value // Variable property access on object|null. return $config->{self::CONFIG_VALUE_COL_NAME}; } @@ -241,9 +241,9 @@ protected function setConfiguration(string $key, string $value, string $range): * If the current value of the configuration option is not included in * $map, then the value is not altered. * - * @param string $key the key (aka name) of the configuration option - * @param array $map a mapping from old-to-new configuration values - * @param string $range the new range for the configuration option + * @param string $key the key (aka name) of the configuration option + * @param array $map a mapping from old-to-new configuration values + * @param string $range the new range for the configuration option */ protected function convertConfiguration(string $key, array $map, string $range): void { @@ -253,4 +253,4 @@ protected function convertConfiguration(string $key, array $map, string $range): } $this->setConfiguration($key, $value, $range); } -} +}; diff --git a/database/migrations/2021_05_31_201000_convert_filesize_to_bigint.php b/database/migrations/2021_05_31_201000_convert_filesize_to_bigint.php index ffcca27e2ce..10d76158276 100644 --- a/database/migrations/2021_05_31_201000_convert_filesize_to_bigint.php +++ b/database/migrations/2021_05_31_201000_convert_filesize_to_bigint.php @@ -1,23 +1,28 @@ = 4GB can be represented. */ - public function up() + public function up(): void { Schema::table('photos', function (Blueprint $table) { $table->unsignedBigInteger('filesize')->nullable(false)->default(0)->change(); }); } - public function down() + public function down(): void { // no-op by intention } -} \ No newline at end of file +}; \ No newline at end of file diff --git a/database/migrations/2021_06_01_181900_refactor_timestamps_anew.php b/database/migrations/2021_06_01_181900_refactor_timestamps_anew.php index 5841264d48a..992931c1e78 100644 --- a/database/migrations/2021_06_01_181900_refactor_timestamps_anew.php +++ b/database/migrations/2021_06_01_181900_refactor_timestamps_anew.php @@ -1,5 +1,11 @@ fixPagesTable(); @@ -74,7 +79,7 @@ public function up() /** * Reverse the migration. */ - public function down() + public function down(): void { try { $this->downgradeORMSystemTimes(); @@ -134,6 +139,8 @@ protected function upgradeORMSystemTimes(): void */ protected function upgradeORMSystemTimesByTable(string $tableName): void { + $nowString = Carbon::now(self::SQL_TIMEZONE_NAME)->format(self::SQL_DATETIME_FORMAT); + // We must use three single calls to work around an SQLite limitation Schema::table($tableName, function (Blueprint $table) { $table->renameColumn(self::CREATED_AT_COL_NAME, self::CREATED_AT_COL_NAME . '_tmp'); @@ -163,14 +170,23 @@ protected function upgradeORMSystemTimesByTable(string $tableName): void $created_at = $entity->{self::CREATED_AT_COL_NAME . '_tmp'}; $updated_at = $entity->{self::UPDATED_AT_COL_NAME . '_tmp'}; if ($needsConversion) { - $created_at = $this->upgradeDatetime($created_at); - $updated_at = $this->upgradeDatetime($updated_at); + $created_at = $this->upgradeDatetime($created_at) ?? $nowString; + $updated_at = $this->upgradeDatetime($updated_at) ?? $nowString; } DB::table($tableName)->where(self::ID_COL_NAME, '=', $entity->id)->update([ self::CREATED_AT_COL_NAME => $created_at, self::UPDATED_AT_COL_NAME => $updated_at, ]); } + DB::table($tableName) + ->whereNull(self::CREATED_AT_COL_NAME) + ->update([ + self::CREATED_AT_COL_NAME => $nowString, + self::UPDATED_AT_COL_NAME => $nowString, + ]); + DB::table($tableName) + ->whereNull(self::UPDATED_AT_COL_NAME) + ->update([self::UPDATED_AT_COL_NAME => $nowString]); DB::commit(); // Make the new columns non-nullable Schema::table($tableName, function (Blueprint $table) { @@ -450,10 +466,11 @@ protected function convertDatetime(?string $sqlDatetime, ?string $oldTz, ?string return null; } $result = Carbon::createFromFormat( - self::SQL_DATETIME_FORMAT, + self::SQL_DATETIME_FORMAT . '+', $sqlDatetime, $oldTz ); + $result->setTimezone($newTz); return $result->format(self::SQL_DATETIME_FORMAT); @@ -473,7 +490,7 @@ protected function getConfiguration(string $key): string ->where(self::CONFIG_KEY_COL_NAME, '=', $key) ->first(); - return $config->{self::CONFIG_VALUE_COL_NAME}; + return $config?->{self::CONFIG_VALUE_COL_NAME} ?? ''; } /** @@ -500,9 +517,9 @@ protected function setConfiguration(string $key, string $value, string $range): * If the current value of the configuration option is not included in * $map, then the value is not altered. * - * @param string $key the key (aka name) of the configuration option - * @param array $map a mapping from old-to-new configuration values - * @param string $range the new range for the configuration option + * @param string $key the key (aka name) of the configuration option + * @param array $map a mapping from old-to-new configuration values + * @param string $range the new range for the configuration option */ protected function convertConfiguration(string $key, array $map, string $range): void { @@ -522,15 +539,13 @@ protected function convertConfiguration(string $key, array $map, string $range): protected function needsConversion(): bool { $dbConnType = Config::get('database.default'); - switch ($dbConnType) { - case 'mysql': - return false; - case 'sqlite': - case 'pgsql': - return true; - default: - // What is about sqlsrv? Is this actually used? - throw new InvalidArgumentException('Unsupported DB system: ' . $dbConnType); - } + + return match ($dbConnType) { + 'mysql' => false, + 'sqlite', + 'pgsql' => true, + // What is about sqlsrv? Is this actually used? + default => throw new InvalidArgumentException('Unsupported DB system: ' . $dbConnType), + }; } -} +}; diff --git a/database/migrations/2021_06_01_182000_bump_version040304.php b/database/migrations/2021_06_01_182000_bump_version040304.php index b5032a01e34..7a95d9572b9 100644 --- a/database/migrations/2021_06_01_182000_bump_version040304.php +++ b/database/migrations/2021_06_01_182000_bump_version040304.php @@ -1,27 +1,28 @@ update(['value' => '040304']); + DB::table('configs')->where('key', 'version')->update(['value' => '040304']); } /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { - Configs::where('key', 'version')->update(['value' => '040302']); + DB::table('configs')->where('key', 'version')->update(['value' => '040302']); } -} +}; diff --git a/database/migrations/2021_06_06_151613_fix-takedate.php b/database/migrations/2021_06_06_151613_fix-takedate.php index 7dcb32ff7b8..a5c295c19bc 100644 --- a/database/migrations/2021_06_06_151613_fix-takedate.php +++ b/database/migrations/2021_06_06_151613_fix-takedate.php @@ -1,33 +1,33 @@ update(['value' => self::TAKEN_AT]); - Album::where('sorting_col', '=', self::TAKESTAMP)->update(['sorting_col' => self::TAKEN_AT]); + DB::table('configs')->where('value', '=', self::TAKESTAMP)->update(['value' => self::TAKEN_AT]); + DB::table('albums')->where('sorting_col', '=', self::TAKESTAMP)->update(['sorting_col' => self::TAKEN_AT]); } /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { - Configs::where('value', '=', self::TAKEN_AT)->update(['value' => self::TAKESTAMP]); - Album::where('sorting_col', '=', self::TAKEN_AT)->update(['sorting_col' => self::TAKESTAMP]); + DB::table('configs')->where('value', '=', self::TAKEN_AT)->update(['value' => self::TAKESTAMP]); + DB::table('albums')->where('sorting_col', '=', self::TAKEN_AT)->update(['sorting_col' => self::TAKESTAMP]); } -} +}; diff --git a/database/migrations/2021_06_23_105939_create_notifications_table.php b/database/migrations/2021_06_23_105939_create_notifications_table.php index 557aebbbbd4..2f0202dc7f8 100644 --- a/database/migrations/2021_06_23_105939_create_notifications_table.php +++ b/database/migrations/2021_06_23_105939_create_notifications_table.php @@ -1,17 +1,20 @@ uuid('id')->primary(); @@ -25,11 +28,9 @@ public function up() /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { Schema::dropIfExists('notifications'); } -} +}; diff --git a/database/migrations/2021_06_30_121651_add_email_to_users_table.php b/database/migrations/2021_06_30_121651_add_email_to_users_table.php index 89f66732b29..1838b4a9a9e 100644 --- a/database/migrations/2021_06_30_121651_add_email_to_users_table.php +++ b/database/migrations/2021_06_30_121651_add_email_to_users_table.php @@ -1,17 +1,29 @@ optimize = new OptimizeTables(); + } + /** * Run the migrations. - * - * @return void */ - public function up() + public function up(): void { Schema::table('users', function (Blueprint $table) { $table->char('email', 100)->after('password')->nullable(); @@ -20,13 +32,15 @@ public function up() /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { Schema::table('users', function (Blueprint $table) { + Schema::table('users', function (Blueprint $table) { + $this->optimize->dropUniqueIfExists($table, 'users_email_unique'); + }); + $table->dropColumn('email'); }); } -} +}; diff --git a/database/migrations/2021_06_30_122229_config_new_photos_notification.php b/database/migrations/2021_06_30_122229_config_new_photos_notification.php index 169bf0bd97b..ef9a5208ef8 100644 --- a/database/migrations/2021_06_30_122229_config_new_photos_notification.php +++ b/database/migrations/2021_06_30_122229_config_new_photos_notification.php @@ -1,38 +1,25 @@ insert([ +return new class() extends BaseConfigMigration { + public function getConfigs(): array + { + return [ [ 'key' => 'new_photos_notification', 'value' => '0', - 'confidentiality' => 0, + 'confidentiality' => '0', 'cat' => 'config', - 'type_range' => BOOL, + 'type_range' => self::BOOL, + 'description' => '', ], - ]); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Configs::where('key', '=', 'new_photos_notification')->delete(); + ]; } -} +}; diff --git a/database/migrations/2021_07_19_134617_bump_version040305.php b/database/migrations/2021_07_19_134617_bump_version040305.php index 0ea8a2b8832..4dcc57053f7 100644 --- a/database/migrations/2021_07_19_134617_bump_version040305.php +++ b/database/migrations/2021_07_19_134617_bump_version040305.php @@ -1,27 +1,28 @@ update(['value' => '040305']); + DB::table('configs')->where('key', 'version')->update(['value' => '040305']); } /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { - Configs::where('key', 'version')->update(['value' => '040304']); + DB::table('configs')->where('key', 'version')->update(['value' => '040304']); } -} +}; diff --git a/database/migrations/2021_10_27_133121_fix_confidentiality.php b/database/migrations/2021_10_27_133121_fix_confidentiality.php index a2aa93da175..27fa6d3377c 100644 --- a/database/migrations/2021_10_27_133121_fix_confidentiality.php +++ b/database/migrations/2021_10_27_133121_fix_confidentiality.php @@ -1,31 +1,32 @@ update(['confidentiality' => '0']); - Configs::where('key', 'upload_processing_limit')->update(['confidentiality' => '0']); - Configs::where('key', 'public_photos_hidden')->update(['confidentiality' => '0']); + DB::table('configs')->where('key', 'editor_enabled')->update(['confidentiality' => '0']); + DB::table('configs')->where('key', 'upload_processing_limit')->update(['confidentiality' => '0']); + DB::table('configs')->where('key', 'public_photos_hidden')->update(['confidentiality' => '0']); } /** * Reverse the migrations. - * - * @return void */ - public function down() + public function down(): void { - Configs::where('key', 'editor_enabled')->update(['confidentiality' => '2']); - Configs::where('key', 'upload_processing_limit')->update(['confidentiality' => '2']); - Configs::where('key', 'public_photos_hidden')->update(['confidentiality' => '2']); + DB::table('configs')->where('key', 'editor_enabled')->update(['confidentiality' => '2']); + DB::table('configs')->where('key', 'upload_processing_limit')->update(['confidentiality' => '2']); + DB::table('configs')->where('key', 'public_photos_hidden')->update(['confidentiality' => '2']); } -} +}; diff --git a/database/migrations/2021_11_16_162058_bump_version040306.php b/database/migrations/2021_11_16_162058_bump_version040306.php new file mode 100644 index 00000000000..7b5683aecab --- /dev/null +++ b/database/migrations/2021_11_16_162058_bump_version040306.php @@ -0,0 +1,28 @@ +where('key', 'version')->update(['value' => '040306']); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '040305']); + } +}; diff --git a/database/migrations/2021_12_03_201242_bump_version040400.php b/database/migrations/2021_12_03_201242_bump_version040400.php new file mode 100644 index 00000000000..e70e2dd4318 --- /dev/null +++ b/database/migrations/2021_12_03_201242_bump_version040400.php @@ -0,0 +1,28 @@ +where('key', 'version')->update(['value' => '040400']); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '040306']); + } +}; diff --git a/database/migrations/2021_12_04_181200_refactor_models.php b/database/migrations/2021_12_04_181200_refactor_models.php new file mode 100644 index 00000000000..01be49d9b93 --- /dev/null +++ b/database/migrations/2021_12_04_181200_refactor_models.php @@ -0,0 +1,1997 @@ +foreign('local_column')->references('foreign_column')->on('foreign_table'); + * }); + * + * does not work, but + * + * Schema::create('my_table', function (Blueprint $table) { + * $table->foreign('local_column')->references('foreign_column')->on('foreign_table'); + * }); + * + * works. + * + * I also noticed that some foreign constrains that should actually + * exist are already missing for SQLite. + * I guess that former migrations have already run into that trap + * without noticing, because Laravel does not throw an error, if + * a foreign constraint cannot be created. + * I checked with my PostgreSQL installation and my SQLite + * installation and found missing constraints. + * However, I did not check the actual code of past migrations. + * + * As we alter the table `albums` the foreign constraint from + * `photos` to `albums` via the column `album_id` vanishes. + * Hence, we must re-create the table `photos`. + * This has a cascading effect on `size_variants` and in turn on + * `sym_links`. + * In other words, we have to re-create the whole database more or + * less. + * (At least, if we want to keep foreign constraints in SQLite.) + * Yikes! :-( + */ +return new class() extends Migration { + private ConsoleOutput $output; + /** @var ProgressBar[] */ + private array $progressBars; + private ConsoleSectionOutput $msgSection; + private OptimizeTables $optimize; + + private const SQL_TIMEZONE_NAME = 'UTC'; + private const SQL_DATETIME_FORMAT = 'Y-m-d H:i:s'; + + public const THUMBNAIL_DIM = 200; + public const THUMBNAIL2X_DIM = 400; + + public const VARIANT_ORIGINAL = 0; + public const VARIANT_MEDIUM2X = 1; + public const VARIANT_MEDIUM = 2; + public const VARIANT_SMALL2X = 3; + public const VARIANT_SMALL = 4; + public const VARIANT_THUMB2X = 5; + public const VARIANT_THUMB = 6; + + /** + * 2013-11-01 in seconds since epoch. + */ + public const BIRTH_OF_LYCHEE = 1383264000; + public const MAX_SIGNED_32BIT_INT = 2147483647; + + public const RANDOM_ID_LENGTH = 24; + + /** + * Maps a size variant (0...6) to the path prefix (directory) where the + * file for that size variant is stored. + */ + public const VARIANT_2_PATH_PREFIX = [ + 'big', + 'medium', + 'medium', + 'small', + 'small', + 'thumb', + 'thumb', + ]; + + public const VALID_VIDEO_TYPES = [ + 'video/mp4', + 'video/mpeg', + 'image/x-tga', // mpg; will be corrected by the metadata extractor + 'video/ogg', + 'video/webm', + 'video/quicktime', + 'video/x-ms-asf', // wmv file + 'video/x-ms-wmv', // wmv file + 'video/x-msvideo', // Avi + 'video/x-m4v', // Avi + 'application/octet-stream', // Some mp4 files; will be corrected by the metadata extractor + ]; + + /** + * Maps a size variant (0...4) to the name of the (old) attribute which + * stores the width of that size variant. + * Note: No attribute is defined for the size variants 5 and 6 (`thumb2x` + * and `thumb`), because their width is not stored as an attribute but + * hard-coded. + * See {@link RefactorModels::THUMBNAIL2X_DIM} and + * {@link RefactorModels::THUMBNAIL_DIM}. + */ + public const VARIANT_2_WIDTH_ATTRIBUTE = [ + 'width', + 'medium2x_width', + 'medium_width', + 'small2x_width', + 'small_width', + ]; + + /** + * Maps a size variant (0...4) to the name of the (old) attribute which + * stores the height of that size variant. + * Note: No attribute is defined for the size variants 5 and 6 (`thumb2x` + * and `thumb`), because their width is not stored as an attribute but + * hard-coded. + * See {@link RefactorModels::THUMBNAIL2X_DIM} and + * {@link RefactorModels::THUMBNAIL_DIM}. + */ + public const VARIANT_2_HEIGHT_ATTRIBUTE = [ + 'height', + 'medium2x_height', + 'medium_height', + 'small2x_height', + 'small_height', + ]; + + /** + * Translates album IDs. + * + * During upgrade the array maps legacy, time-based IDs to new, random IDs. + * During downgrade the array maps random IDs to legacy, time-based IDs. + * + * @var array + */ + private array $albumIDCache = []; + + /** + * Translates photo IDs. + * + * During upgrade the array maps legacy, time-based IDs to new, random IDs. + * During downgrade the array maps random IDs to legacy, time-based IDs. + * + * @var array + */ + private array $photoIDCache = []; + + /** + * @throws DBALException + */ + public function __construct() + { + $this->output = new ConsoleOutput(); + $this->progressBars = []; + $this->msgSection = $this->output->section(); + $this->optimize = new OptimizeTables(); + } + + /** + * Outputs an error message. + * + * @param string $msg the message + */ + private function printError(string $msg): void + { + $this->msgSection->writeln('Error: ' . $msg); + } + + /** + * Outputs a warning. + * + * @param string $msg the message + */ + private function printWarning(string $msg): void + { + $this->msgSection->writeln('Warning: ' . $msg); + } + + /** + * Outputs an informational message. + * + * @param string $msg the message + */ + private function printInfo(string $msg): void + { + $this->msgSection->writeln('Info: ' . $msg); + } + + /** + * Gets the progress bar for the given table. + * + * The method always returns the same instance of the progress bar for + * the same table. + * The method creates a new progress bar, when it is called for a new + * table name the first time. + * + * @param string $tableName + * + * @return ProgressBar + */ + private function getProgressBar(string $tableName): ProgressBar + { + if (!key_exists($tableName, $this->progressBars)) { + // Also start a new message section **above** the new progress bar + // This way the progress bar remains on the bottom in case too + // many warning/errors are spit out. + $this->msgSection = $this->output->section(); + $this->progressBars[$tableName] = new ProgressBar($this->output->section()); + $this->progressBars[$tableName]->setFormat('Table \'' . $tableName . '\' %current%/%max% [%bar%] %percent:3s%%'); + } + + return $this->progressBars[$tableName]; + } + + /** + * @throws InvalidArgumentException + * @throws RuntimeException + */ + public function up(): void + { + $this->printInfo('Checking consistency of DB'); + $this->ensureDBConsistency(); + + Schema::drop('sym_links'); + + // Step 1 + // Create tables in correct order so that foreign keys can + // be created immediately. + $this->printInfo('Renaming existing tables'); + $this->renameTables(); + $this->printInfo('Creating new tables'); + $this->createUsersTableUp(); + $this->createBaseAlbumTable(); + $this->createAlbumTableUp(); + $this->createTagAlbumTable(); + $this->createUserBaseAlbumTableUp(); + $this->createPhotoTableUp(); + $this->createSizeVariantTableUp(); + $this->createSymLinkTableUp(); + $this->createRemainingForeignConstraints(); + $this->createWebAuthnTableUp(); + $this->createPageTableUp(); + $this->createPageContentTableUp(); + $this->createLogTableUp(); + + // Step 2 + // Happy copying :( + DB::beginTransaction(); + $this->printInfo('Start copying ...'); + $this->upgradeCopy(); + $this->copyStructurallyUnchangedTables(); + $this->printInfo('Finished copying'); + $this->printInfo('Upgrading configuration'); + $this->upgradeConfig(); + DB::commit(); + + // Step 3 + $this->printInfo('Dropping old tables'); + $this->dropTemporaryTablesUp(); + } + + /** + * @throws InvalidArgumentException + */ + public function down(): void + { + Schema::drop('sym_links'); + + // Step 1 + // Create tables in correct order so that foreign keys can + // be created immediately. + $this->printInfo('Renaming existing tables'); + $this->renameTables(); + $this->printInfo('Creating new tables'); + $this->createUsersTableDown(); + $this->createAlbumTableDown(); + $this->createUserAlbumTableDown(); + $this->createPhotoTableDown(); + $this->createSymLinkTableDown(); + $this->createWebAuthnTableDown(); + $this->createPageTableDown(); + $this->createPageContentTableDown(); + $this->createLogTableDown(); + + // Step 2 + // Happy copying :( + DB::beginTransaction(); + $this->printInfo('Start copying ...'); + $this->downgradeCopy(); + $this->copyStructurallyUnchangedTables(); + $this->printInfo('Finished copying'); + $this->printInfo('Downgrading configuration'); + $this->downgradeConfig(); + DB::commit(); + + // Step 3 + $this->printInfo('Dropping old tables'); + $this->dropTemporaryTablesDown(); + } + + /** + * Renames some tables to a temporary name so that we get them out of + * out way. + * + * In case of SQLite, this already destroys foreign constraints, but + * it does not destroy any other indexes. + * Again, we are facing funny differences how the schema abstraction of + * Laravel handles SQLite on the on hand side and MySQL/PostgreSQL on the + * other hand side. + * Hence, we remove all indexes in advance before we rename the table, + * so that we can re-create them later without failing. + */ + private function renameTables(): void + { + Schema::table('albums', function (Blueprint $table) { + $this->optimize->dropForeignIfExists($table, 'albums_owner_id_foreign'); + // We must remove any foreign link from `albums` to `photos` to + // break up circular dependencies. + $this->optimize->dropForeignIfExists($table, 'albums_cover_id_foreign'); + $this->optimize->dropForeignIfExists($table, 'albums_parent_id_foreign'); + $this->optimize->dropIndexIfExists($table, 'albums__lft__rgt_index'); + }); + Schema::rename('albums', 'albums_tmp'); + Schema::table('photos', function (Blueprint $table) { + $this->optimize->dropForeignIfExists($table, 'photos_album_id_foreign'); + $this->optimize->dropForeignIfExists($table, 'photos_owner_id_foreign'); + $this->optimize->dropIndexIfExists($table, 'photos_created_at_index'); + $this->optimize->dropIndexIfExists($table, 'photos_updated_at_index'); + $this->optimize->dropIndexIfExists($table, 'photos_taken_at_index'); + $this->optimize->dropIndexIfExists($table, 'photos_original_checksum_index'); + $this->optimize->dropIndexIfExists($table, 'photos_checksum_index'); + $this->optimize->dropIndexIfExists($table, 'photos_live_photo_content_id_index'); + $this->optimize->dropIndexIfExists($table, 'photos_livephotocontentid_index'); + $this->optimize->dropIndexIfExists($table, 'photos_live_photo_checksum_index'); + $this->optimize->dropIndexIfExists($table, 'photos_livephotochecksum_index'); + $this->optimize->dropIndexIfExists($table, 'photos_is_public_index'); + $this->optimize->dropIndexIfExists($table, 'photos_is_starred_index'); + $this->optimize->dropIndexIfExists($table, 'photos_album_id_taken_at_index'); + $this->optimize->dropIndexIfExists($table, 'photos_album_id_created_at_index'); + $this->optimize->dropIndexIfExists($table, 'photos_album_id_is_starred_index'); + $this->optimize->dropIndexIfExists($table, 'photos_album_id_is_public_index'); + $this->optimize->dropIndexIfExists($table, 'photos_album_id_type_index'); + $this->optimize->dropIndexIfExists($table, 'photos_album_id_is_starred_created_at_index'); + $this->optimize->dropIndexIfExists($table, 'photos_album_id_is_starred_taken_at_index'); + $this->optimize->dropIndexIfExists($table, 'photos_album_id_is_starred_is_public_index'); + $this->optimize->dropIndexIfExists($table, 'photos_album_id_is_starred_type_index'); + }); + Schema::rename('photos', 'photos_tmp'); + Schema::table('web_authn_credentials', function (Blueprint $table) { + $this->optimize->dropForeignIfExists($table, 'web_authn_credentials_user_id_foreign'); + }); + Schema::rename('web_authn_credentials', 'web_authn_credentials_tmp'); + Schema::table('users', function (Blueprint $table) { + $this->optimize->dropUniqueIfExists($table, 'users_username_unique'); + $this->optimize->dropUniqueIfExists($table, 'users_email_unique'); + }); + Schema::rename('users', 'users_tmp'); + $this->renamePageContentTable(); + Schema::rename('pages', 'pages_tmp'); + Schema::rename('logs', 'logs_tmp'); + } + + /** + * Drops temporary tables which have been created by + * {@link RefactorAlbumModel::renameTables()} or have become unnecessary. + * + * The order is important to avoid error due to unsatisfied foreign + * constraints. + */ + private function dropTemporaryTablesUp(): void + { + Schema::drop('user_album'); + // We must remove any foreign link from `albums` to `photos` to + // break up circular dependencies. + DB::table('albums_tmp')->update(['cover_id' => null]); + Schema::drop('photos_tmp'); + Schema::drop('albums_tmp'); + Schema::drop('web_authn_credentials_tmp'); + Schema::drop('users_tmp'); + Schema::drop('page_contents_tmp'); + Schema::drop('pages_tmp'); + Schema::drop('logs_tmp'); + } + + /** + * Drops temporary tables which have been created by + * {@link RefactorAlbumModel::renameTables()} or have become unnecessary. + * + * The order is important to avoid error due to unsatisfied foreign + * constraints. + */ + private function dropTemporaryTablesDown(): void + { + Schema::drop('user_base_album'); + Schema::drop('size_variants'); + // We must remove any foreign link from `albums` to `photos` to + // break up circular dependencies. + DB::table('albums_tmp')->update(['cover_id' => null]); + Schema::drop('photos_tmp'); + Schema::drop('albums_tmp'); + Schema::drop('tag_albums'); + Schema::drop('base_albums'); + Schema::drop('web_authn_credentials_tmp'); + Schema::drop('users_tmp'); + Schema::drop('page_contents_tmp'); + Schema::drop('pages_tmp'); + Schema::drop('logs_tmp'); + } + + /** + * Creates the new table `users` with improved attribute names. + * + * Note: Actually, renaming of the attributes `lock` to `is_locked` and + * `upload` to `may_upload` should not be part of this migration, because + * it is unrelated to the refactored, new architecture. + * However, there will be a subsequent PR which aims at making the JSON + * API more consistent and in this context this migration make sense. + * Unfortunately, SQLite does not support renaming of columns in place. + * Under the hood, SQLite drops the entire table and re-creates it. + * But this fails, if there are foreign key constraints from other tables + * to `users`. + * Eventually, we would end up with re-creating the whole DB again. :-( + * Hence, we bring forward this migration when we re-create the whole DB + * anyway. + */ + private function createUsersTableUp(): void + { + Schema::create('users', function (Blueprint $table) { + $table->increments('id'); + $table->dateTime('created_at', 6)->nullable(false); + $table->dateTime('updated_at', 6)->nullable(false); + $table->string('username', 100)->nullable(false)->unique(); + $table->string('password', 100)->nullable(true); + $table->string('email', 100)->nullable()->unique(); + $table->boolean('may_upload')->nullable(false)->default(false); + $table->boolean('is_locked')->nullable(false)->default(false); + $table->rememberToken(); + }); + } + + /** + * Creates the old table `users`. + */ + private function createUsersTableDown(): void + { + Schema::create('users', function (Blueprint $table) { + $table->increments('id'); + $table->dateTime('created_at')->nullable(false); + $table->dateTime('updated_at')->nullable(false); + $table->string('username', 100)->nullable(false)->unique(); + $table->string('password', 100)->nullable(true); + $table->string('email', 100)->nullable()->unique(); + $table->boolean('upload')->nullable(false)->default(false); + $table->boolean('lock')->nullable(false)->default(false); + $table->rememberToken(); + }); + } + + /** + * Creates the table `base_albums`. + * + * The table `base_albums` contains all columns of the old table + * `albums` which are common to normal albums and tag albums. + */ + private function createBaseAlbumTable(): void + { + Schema::create('base_albums', function (Blueprint $table) { + // Column definitions + $table->char('id', self::RANDOM_ID_LENGTH)->nullable(false); + $table->unsignedBigInteger('legacy_id')->nullable(false); + $table->dateTime('created_at', 6)->nullable(false); + $table->dateTime('updated_at', 6)->nullable(false); + $table->string('title', 100)->nullable(false); + $table->text('description')->nullable(); + $table->unsignedInteger('owner_id')->nullable(false)->default(0); + $table->boolean('is_public')->nullable(false)->default(false); + $table->boolean('grants_full_photo')->nullable(false)->default(true); + $table->boolean('requires_link')->nullable(false)->default(false); + $table->boolean('is_downloadable')->nullable(false)->default(false); + $table->boolean('is_share_button_visible')->nullable(false)->default(false); + $table->boolean('is_nsfw')->nullable(false)->default(false); + $table->string('password', 100)->nullable()->default(null); + $table->string('sorting_col', 30)->nullable()->default(null); + $table->string('sorting_order', 4)->nullable()->default(null); + // Indices and constraint definitions + $table->primary('id'); + $table->unique('legacy_id'); + $table->foreign('owner_id')->references('id')->on('users'); + // These indices are required for efficient filtering for accessible and/or visible albums + $table->index(['requires_link', 'is_public']); // for albums which don't require a direct link and are public + $table->index(['owner_id']); // for albums which are own by the currently authenticated user + $table->index(['is_public', 'password']); // for albums which are public and how no password + }); + } + + /** + * Creates the table `albums` acc. to the new architecture. + * + * The new table `albums` only contains the columns which are specific + * to real albums and are irrelevant for tag albums. + */ + private function createAlbumTableUp(): void + { + Schema::create('albums', function (Blueprint $table) { + // Column definitions + $table->char('id', self::RANDOM_ID_LENGTH)->nullable(false); + $table->char('parent_id', self::RANDOM_ID_LENGTH)->nullable()->default(null); + $table->string('license', 20)->nullable(false)->default('none'); + $table->char('cover_id', self::RANDOM_ID_LENGTH)->nullable()->default(null); + $table->unsignedBigInteger('_lft')->nullable(false)->default(0); + $table->unsignedBigInteger('_rgt')->nullable(false)->default(0); + // Indices and constraint definitions + $table->primary('id'); + $table->index([DB::raw('_lft asc'), DB::raw('_rgt desc')], 'albums__lft__rgt__index'); + $table->foreign('id')->references('id')->on('base_albums'); + $table->foreign('parent_id')->references('id')->on('albums'); + // Sic! + // Columns `created_at` and `updated_at` left out by intention. + // The albums belong to their "parent" base album and are tied to the same timestamps + }); + } + + /** + * Creates the table `tag_albums`. + * + * The table `tag_albums` only contains the columns which are specific + * to tag albums and are irrelevant for real albums. + */ + private function createTagAlbumTable(): void + { + Schema::create('tag_albums', function (Blueprint $table) { + // Column definitions + $table->char('id', self::RANDOM_ID_LENGTH)->nullable(false); + $table->text('show_tags')->nullable(); + // Indices and constraint definitions + $table->primary('id'); + $table->foreign('id')->references('id')->on('base_albums'); + // Sic! + // Columns `created_at` and `updated_at` left out by intention. + // The tag albums belong to their "parent" base album and are tied to the same timestamps + }); + } + + /** + * Creates the table `albums` acc. to the old architecture. + * + * The old table `albums` only contains the union of all columns of + * `base_albums`, (the new table) `albums` and `tag_albums`. + * Also see + * {@link RefactorAlbumModel::createBaseAlbumTable()}, + * {@link RefactorAlbumModel::createAlbumTableUp()} and + * {@link RefactorAlbumModel::createTagAlbumTable()}. + */ + private function createAlbumTableDown(): void + { + Schema::create('albums', function (Blueprint $table) { + // Column definitions + $table->bigIncrements('id')->nullable(false); + $table->unsignedBigInteger('parent_id')->nullable()->default(null); + $table->dateTime('created_at')->nullable(false); + $table->dateTime('updated_at')->nullable(false); + $table->string('title', 100)->nullable(false); + $table->text('description')->nullable(); + $table->string('license', 20)->nullable(false)->default('none'); + $table->unsignedInteger('owner_id')->nullable(false)->default(0); + $table->boolean('smart')->nullable(false)->default(false); + $table->text('showtags')->nullable(); + $table->boolean('public')->nullable(false)->default(false); + $table->boolean('full_photo')->nullable(false)->default(true); + $table->boolean('viewable')->nullable(false)->default(false); + $table->boolean('downloadable')->nullable(false)->default(false); + $table->boolean('share_button_visible')->nullable(false)->default(false); + $table->boolean('nsfw')->nullable(false)->default(false); + $table->string('password', 100)->nullable()->default(null); + $table->unsignedBigInteger('cover_id')->nullable()->default(null); + $table->string('sorting_col', 30)->nullable()->default(null); + $table->string('sorting_order', 4)->nullable()->default(null); + $table->unsignedBigInteger('_lft')->nullable()->default(null); + $table->unsignedBigInteger('_rgt')->nullable()->default(null); + // Indices and constraint definitions + $table->foreign('parent_id')->references('id')->on('albums'); + }); + } + + /** + * Creates the table `user_base_album`. + * + * The created table is the pivot table for the (m:n)-relationship between + * an owner (user) and a base album. + */ + private function createUserBaseAlbumTableUp(): void + { + Schema::create('user_base_album', function (Blueprint $table) { + // Column definitions + $table->bigIncrements('id')->nullable(false); + $table->unsignedInteger('user_id')->nullable(false); + $table->char('base_album_id', self::RANDOM_ID_LENGTH)->nullable(false); + // Indices and constraint definitions + $table->foreign('user_id')->references('id')->on('users')->cascadeOnUpdate()->cascadeOnDelete(); + $table->foreign('base_album_id')->references('id')->on('base_albums')->cascadeOnUpdate()->cascadeOnDelete(); + // This index is required to efficiently filter those albums + // which are shared with a particular user + $table->unique(['base_album_id', 'user_id']); + }); + } + + /** + * Creates the table `user_album`. + * + * The created table is the pivot table for the (m:n)-relationship between + * an owner (user) and an album. + */ + private function createUserAlbumTableDown(): void + { + Schema::create('user_album', function (Blueprint $table) { + // Column definitions + $table->bigIncrements('id')->nullable(false); + $table->unsignedInteger('user_id')->nullable(false); + $table->unsignedBigInteger('album_id')->nullable(false); + // Indices and constraint definitions + $table->foreign('user_id')->references('id')->on('users')->cascadeOnUpdate()->cascadeOnDelete(); + $table->foreign('album_id')->references('id')->on('albums')->cascadeOnUpdate()->cascadeOnDelete(); + }); + } + + /** + * Creates the table `photos` acc. to the new architecture. + */ + private function createPhotoTableUp(): void + { + Schema::create('photos', function (Blueprint $table) { + // Column definitions + $table->char('id', self::RANDOM_ID_LENGTH)->nullable(false); + $table->unsignedBigInteger('legacy_id')->nullable(false); + $table->dateTime('created_at', 6)->nullable(false); + $table->dateTime('updated_at', 6)->nullable(false); + $table->unsignedInteger('owner_id')->unsigned()->nullable(false)->default(0); + $table->char('album_id', self::RANDOM_ID_LENGTH)->nullable()->default(null); + $table->string('title', 100)->nullable(false); + $table->text('description')->nullable(); + $table->text('tags')->nullable(); + $table->string('license', 20)->nullable(false)->default('none'); + $table->boolean('is_public')->nullable(false)->default(false); + $table->boolean('is_starred')->nullable(false)->default(false); + $table->string('iso')->nullable()->default(null); + $table->string('make')->nullable()->default(null); + $table->string('model')->nullable()->default(null); + $table->string('lens')->nullable()->default(null); + $table->string('aperture')->nullable()->default(null); + $table->string('shutter')->nullable()->default(null); + $table->string('focal')->nullable()->default(null); + $table->decimal('latitude', 10, 8)->nullable()->default(null); + $table->decimal('longitude', 11, 8)->nullable()->default(null); + $table->decimal('altitude', 10, 4)->nullable()->default(null); + $table->decimal('img_direction', 10, 4)->nullable()->default(null); + $table->string('location')->nullable()->default(null); + $table->dateTime('taken_at', 6)->nullable(true)->default(null)->comment('relative to UTC'); + $table->string('taken_at_orig_tz', 31)->nullable(true)->default(null)->comment('the timezone at which the photo has originally been taken'); + $table->string('type', 30)->nullable(false); + $table->unsignedBigInteger('filesize')->nullable(false)->default(0); + $table->string('checksum', 40)->nullable(false); + $table->string('original_checksum', 40)->nullable(false); + $table->string('live_photo_short_path')->nullable()->default(null); + $table->string('live_photo_content_id')->nullable()->default(null); + $table->string('live_photo_checksum', 40)->nullable()->default(null); + // Indices and constraint definitions + $table->primary('id'); + $table->unique('legacy_id'); + $table->foreign('owner_id')->references('id')->on('users'); + $table->foreign('album_id')->references('id')->on('albums'); + $table->index('created_at'); + $table->index('updated_at'); + $table->index('taken_at'); + $table->index('checksum'); + $table->index('original_checksum'); + $table->index('live_photo_content_id'); + $table->index('live_photo_checksum'); + $table->index('is_public'); + $table->index('is_starred'); + // This index is needed to efficiently add the range of take dates + // to each album. + $table->index(['album_id', 'taken_at']); + // These indices are needed to efficiently list all photos of an + // album acc. to different sorting criteria + // Upload time, take date, is starred or is public + $table->index(['album_id', 'created_at']); + $table->index(['album_id', 'is_starred']); + $table->index(['album_id', 'is_public']); + $table->index(['album_id', 'type']); + // These indices are needed to efficiently retrieve the covers of + // albums acc. to different sorting criteria + // Note, that covers are always sorted acc. to `is_starred` first. + $table->index(['album_id', 'is_starred', 'created_at']); + $table->index(['album_id', 'is_starred', 'taken_at']); + $table->index(['album_id', 'is_starred', 'is_public']); + $table->index(['album_id', 'is_starred', 'type']); + }); + } + + /** + * Creates the table `photos` acc. to the old architecture. + */ + private function createPhotoTableDown(): void + { + Schema::create('photos', function (Blueprint $table) { + // Column definitions + $table->bigIncrements('id')->nullable(false); + $table->dateTime('created_at')->nullable(false); + $table->dateTime('updated_at')->nullable(false); + $table->unsignedInteger('owner_id')->nullable(false)->default(0); + $table->unsignedBigInteger('album_id')->nullable()->default(null); + $table->string('title', 100)->nullable(false); + $table->text('description')->nullable(true); + $table->string('tags')->nullable(false)->default(''); + $table->string('license', 20)->nullable(false)->default('none'); + $table->boolean('public')->nullable(false)->default(false); + $table->boolean('star')->nullable(false)->default(false); + $table->string('iso')->nullable(false)->default(''); + $table->string('make')->nullable(false)->default(''); + $table->string('model')->nullable(false)->default(''); + $table->string('lens')->nullable(false)->default(''); + $table->string('aperture')->nullable(false)->default(''); + $table->string('shutter')->nullable(false)->default(''); + $table->string('focal')->nullable(false)->default(''); + $table->decimal('latitude', 10, 8)->nullable()->default(null); + $table->decimal('longitude', 11, 8)->nullable()->default(null); + $table->decimal('altitude', 10, 4)->nullable()->default(null); + $table->decimal('imgDirection', 10, 4)->nullable()->default(null); + $table->string('location')->nullable()->default(null); + $table->dateTime('taken_at')->nullable(true)->default(null)->comment('relative to UTC'); + $table->string('taken_at_orig_tz', 31)->nullable(true)->default(null)->comment('the timezone at which the photo has originally been taken'); + $table->string('type', 30)->nullable(false); + $table->string('url', 100)->default(''); + $table->unsignedBigInteger('filesize')->nullable(false)->default(0); + $table->string('checksum', 40)->nullable(false); + for ($i = self::VARIANT_ORIGINAL; $i <= self::VARIANT_SMALL; $i++) { + $table->integer(self::VARIANT_2_WIDTH_ATTRIBUTE[$i])->unsigned()->nullable()->default(null); + $table->integer(self::VARIANT_2_HEIGHT_ATTRIBUTE[$i])->unsigned()->nullable()->default(null); + } + $table->boolean('thumb2x')->default(false); + $table->string('thumbUrl', 37)->default(''); + $table->string('livePhotoUrl')->nullable()->default(null); + $table->string('livePhotoContentID')->nullable()->default(null); + $table->string('livePhotoChecksum', 40)->nullable()->default(null); + // Indices and constraint definitions + $table->foreign('album_id')->references('id')->on('albums'); + }); + } + + private function createSizeVariantTableUp(): void + { + Schema::create('size_variants', function (Blueprint $table) { + // Column definitions + $table->bigIncrements('id')->nullable(false); + $table->char('photo_id', self::RANDOM_ID_LENGTH)->nullable(false); + $table->unsignedInteger('type')->nullable(false)->default(0)->comment('0: original, ..., 6: thumb'); + $table->string('short_path')->nullable(false); + $table->integer('width')->nullable(false); + $table->integer('height')->nullable(false); + // Indices and constraint definitions + $table->unique(['photo_id', 'type']); + $table->foreign('photo_id')->references('id')->on('photos'); + // Sic! + // Columns `created_at` and `updated_at` left out by intention. + // The size variants belong to their "parent" photo model and are tied to the same timestamps + }); + } + + private function createSymLinkTableUp(): void + { + Schema::create('sym_links', function (Blueprint $table) { + // Column definitions + $table->bigIncrements('id')->nullable(false); + $table->dateTime('created_at', 6)->nullable(false); + $table->dateTime('updated_at', 6)->nullable(false); + $table->unsignedBigInteger('size_variant_id')->nullable(false); + $table->string('short_path')->nullable(false); + // Indices and constraint definitions + $table->index('created_at'); + $table->index('updated_at'); + $table->foreign('size_variant_id')->references('id')->on('size_variants'); + // This index is needed to efficiently find the latest symbolic link + // for each size variant + $table->index(['size_variant_id', 'created_at']); + }); + } + + private function createSymLinkTableDown(): void + { + Schema::create('sym_links', function (Blueprint $table) { + // Column definitions + $table->bigIncrements('id')->nullable(false); + $table->dateTime('created_at')->nullable(false); + $table->dateTime('updated_at')->nullable(false); + $table->unsignedBigInteger('photo_id')->nullable(false); + $table->string('url')->default(''); + $table->string('medium')->default(''); + $table->string('medium2x')->default(''); + $table->string('small')->default(''); + $table->string('small2x')->default(''); + $table->string('thumbUrl')->default(''); + $table->string('thumb2x')->default(''); + // Indices and constraint definitions + $table->foreign('photo_id')->references('id')->on('photos'); + }); + } + + private function createLogTable(int $precision): void + { + Schema::create('logs', function (Blueprint $table) use ($precision) { + $table->bigIncrements('id'); + $table->dateTime('created_at', $precision)->nullable(false); + $table->dateTime('updated_at', $precision)->nullable(false); + $table->string('type', 11); + $table->string('function', 100); + $table->integer('line'); + $table->text('text'); + }); + } + + private function createLogTableUp(): void + { + $this->createLogTable(6); + } + + private function createLogTableDown(): void + { + $this->createLogTable(0); + } + + private function createPageTable(int $precision): void + { + Schema::create('pages', function (Blueprint $table) use ($precision) { + $table->increments('id'); + $table->dateTime('created_at', $precision)->nullable(false); + $table->dateTime('updated_at', $precision)->nullable(false); + $table->string('title', 150)->default(''); + $table->string('menu_title', 100)->default(''); + $table->boolean('in_menu')->default(false); + $table->boolean('enabled')->default(false); + $table->string('link', 150)->default(''); + $table->integer('order')->default(0); + }); + } + + private function createPageTableUp(): void + { + $this->createPageTable(6); + } + + private function createPageTableDown(): void + { + $this->createPageTable(0); + } + + private function createPageContentTable(string $tableName, int $precision): void + { + Schema::create($tableName, function (Blueprint $table) use ($precision) { + $table->increments('id'); + $table->dateTime('created_at', $precision)->nullable(false); + $table->dateTime('updated_at', $precision)->nullable(false); + $table->unsignedInteger('page_id'); + $table->text('content'); + $table->string('class', 150); + $table->enum('type', ['div', 'img']); + $table->integer('order')->default(0); + // Indices + $table->foreign('page_id') + ->references('id')->on('pages') + ->onDelete('cascade'); + }); + } + + private function createPageContentTableUp(): void + { + $this->createPageContentTable('page_contents', 6); + } + + private function createPageContentTableDown(): void + { + $this->createPageContentTable('page_contents', 0); + } + + /** + * Renames table `page_content` to `page_content_tmp` using a work-around. + * + * Ideally, we would simply use + * `Schema::rename('page_content', 'page_content_tmp')` + * in {@link RefactorModels::renameTables()} as for any other table. + * Unfortunately, a bug in Laravel/Eloquent does not allow this, so we + * need to create a table `page_contents_tmp` copy everything into that + * table, and drop `page_contents`. + * (And yes, we do it the other way around just some minutes later.) + * Yikes! + * + * The cause of the problem is that the table uses the non-SQL type + * `enum` (see `CreatePageContentsTable::up` in + * `2019_02_21_114408_create_page_contents_table.php`). + * Under the hood, Laravel/Eloquent registers this proprietary extension + * with the DBAL (database abstraction layer) and a callback ensures + * that this type gets properly translated into an actual SQL type + * whenever the DBAL encounters this type depending on the SQL backend: + * + * - MySQL: `ENUM` + * - PostgreSQL: `VARCHAR` with a `CHECK`-constraint + * - SQLite: `VARCHAR` + * + * However, Laravel/Eloquent only registers this type extension for + * table creation. + * (That is actually a known bug which Laravel/Eloquent refuses to fix.) + * As a result, the DBAL will bail out with an exception whenever it tries + * to modify the table schema in the slightest way (rename the table, + * drop/add/rename a column, change a column) even if the modification + * does not alter the enum-column itself, because it will topple over an + * unknown type. + * Essentially, the table schema becomes immutable. + * The only possible action left which does not trigger an exception is to + * drop the table. + */ + private function renamePageContentTable(): void + { + $nowString = Carbon::now(self::SQL_TIMEZONE_NAME)->format(self::SQL_DATETIME_FORMAT); + + $this->createPageContentTable('page_contents_tmp', 0); + $pageContents = DB::table('page_contents')->get(); + foreach ($pageContents as $pageContent) { + DB::table('page_contents_tmp')->insert([ + 'id' => $pageContent->id, + 'created_at' => $pageContent->created_at ?? $nowString, + 'updated_at' => $pageContent->updated_at ?? $nowString, + 'page_id' => $pageContent->page_id, + 'content' => $pageContent->content, + 'class' => $pageContent->class, + 'type' => $pageContent->type, + 'order' => $pageContent->order, + ]); + } + Schema::drop('page_contents'); + } + + private function createWebAuthnTable(int $precision): void + { + Schema::create('web_authn_credentials', function (Blueprint $table) use ($precision) { + $table->string('id', 255); + $table->dateTime('created_at', $precision)->nullable(false); + $table->dateTime('updated_at', $precision)->nullable(false); + $table->dateTime('disabled_at', $precision)->nullable(true); + $table->unsignedInteger('user_id')->nullable(false); + $table->string('name')->nullable(); + $table->string('type', 16); + $table->json('transports'); + $table->json('attestation_type'); + $table->json('trust_path'); + $table->uuid('aaguid'); + $table->binary('public_key'); + $table->unsignedInteger('counter')->default(0); + $table->uuid('user_handle')->nullable(); + // Indices + $table->primary(['id', 'user_id']); + $table->foreign('user_id') + ->references('id')->on('users') + ->cascadeOnDelete(); + }); + } + + private function createWebAuthnTableUp(): void + { + $this->createWebAuthnTable(6); + } + + private function createWebAuthnTableDown(): void + { + $this->createWebAuthnTable(0); + } + + /** + * Creates remaining foreign constraints which could not immediately be + * created while the owning table was created due to circular dependencies. + * + * Note, this method has no effect for a SQLite installation. + */ + private function createRemainingForeignConstraints(): void + { + Schema::table('albums', function (Blueprint $table) { + $table->foreign('cover_id') + ->references('id')->on('photos') + ->onUpdate('CASCADE') + ->onDelete('SET NULL'); + }); + } + + /** + * @throws InvalidArgumentException + */ + private function upgradeCopy(): void + { + $pgBar = $this->getProgressBar('users'); + $users = DB::table('users_tmp')->get(); + $pgBar->setMaxSteps($users->count()); + foreach ($users as $user) { + $pgBar->advance(); + DB::table('users')->insert([ + 'id' => $user->id, + 'created_at' => $user->created_at, + 'updated_at' => $user->updated_at, + 'username' => $user->username, + 'password' => $user->password, + 'email' => $user->email, + 'may_upload' => $user->upload, + 'is_locked' => $user->lock, + 'remember_token' => $user->remember_token, + ]); + } + + // Ordering by `_lft` is important, because we must copy parent + // albums first. + // Otherwise, foreign key constraint to `parent_id` may fail. + $pgBar = $this->getProgressBar('albums'); + $albums = DB::table('albums_tmp')->orderBy('_lft')->lazyById(); + $pgBar->setMaxSteps($albums->count()); + $mapSorting = function (?string $sortingCol): ?string { + if (empty($sortingCol)) { + return null; + } elseif ($sortingCol === 'id') { + return 'created_at'; + } elseif ($sortingCol === 'public') { + return 'is_public'; + } elseif ($sortingCol === 'star') { + return 'is_starred'; + } else { + return $sortingCol; + } + }; + foreach ($albums as $album) { + $pgBar->advance(); + $newAlbumID = $this->generateKey(); + $this->albumIDCache[strval($album->id)] = $newAlbumID; + + DB::table('base_albums')->insert([ + 'id' => $newAlbumID, + 'legacy_id' => $album->id, + 'created_at' => $this->calculateBestCreatedAt($album->id, $album->created_at), + 'updated_at' => $album->updated_at, + 'title' => $album->title, + 'description' => empty($album->description) ? null : $album->description, + 'owner_id' => $album->owner_id, + 'is_public' => $album->public, + 'grants_full_photo' => $album->full_photo, + 'requires_link' => !($album->viewable), + 'is_downloadable' => $album->downloadable, + 'is_share_button_visible' => $album->share_button_visible, + 'is_nsfw' => $album->nsfw, + 'password' => empty($album->password) ? null : $album->password, + 'sorting_col' => $mapSorting($album->sorting_col), + 'sorting_order' => empty($album->sorting_col) ? null : $album->sorting_order, + ]); + + if ($album->smart) { + DB::table('tag_albums')->insert([ + 'id' => $newAlbumID, + 'show_tags' => $album->showtags, + ]); + } else { + // Don't copy `cover_id` yet, because the photos have not been + // copied yet. + // Explicit `cover_id` needs to be set belated. + // Otherwise, the foreign key constraint between `cover_id` + // and `photos.id` fails. + DB::table('albums')->insert([ + 'id' => $newAlbumID, + 'parent_id' => $album->parent_id ? $this->albumIDCache[strval($album->parent_id)] : null, + 'license' => $album->license, + 'cover_id' => null, + '_lft' => $album->_lft ?? 0, + '_rgt' => $album->_rgt ?? 0, + ]); + } + } + + RefactorAlbumModel_AlbumModel::query()->fixTree(); + + $pgBar = $this->getProgressBar('user_base_album'); + $userAlbumRelations = DB::table('user_album')->lazyById(); + $pgBar->setMaxSteps($userAlbumRelations->count()); + foreach ($userAlbumRelations as $userAlbumRelation) { + $pgBar->advance(); + DB::table('user_base_album')->insert([ + 'id' => $userAlbumRelation->id, + 'user_id' => $userAlbumRelation->user_id, + 'base_album_id' => $this->albumIDCache[strval($userAlbumRelation->album_id)], + ]); + } + + $pgBar = $this->getProgressBar('photos'); + $photos = DB::table('photos_tmp')->lazyById(); + $pgBar->setMaxSteps($photos->count()); + foreach ($photos as $photo) { + $pgBar->advance(); + $newPhotoID = $this->generateKey(); + $this->photoIDCache[strval($photo->id)] = $newPhotoID; + + DB::table('photos')->insert([ + 'id' => $newPhotoID, + 'legacy_id' => $photo->id, + 'created_at' => $this->calculateBestCreatedAt($photo->id, $photo->created_at), + 'updated_at' => $photo->updated_at, + 'owner_id' => $photo->owner_id, + 'album_id' => $photo->album_id ? $this->albumIDCache[strval($photo->album_id)] : null, + 'title' => $photo->title, + 'description' => empty($photo->description) ? null : $photo->description, + 'tags' => empty($photo->tags) ? null : $photo->tags, + 'license' => $photo->license, + 'is_public' => $photo->public, + 'is_starred' => $photo->star, + 'iso' => empty($photo->iso) ? null : $photo->iso, + 'make' => empty($photo->make) ? null : $photo->make, + 'model' => empty($photo->model) ? null : $photo->model, + 'lens' => empty($photo->lens) ? null : $photo->lens, + 'aperture' => empty($photo->aperture) ? null : $photo->aperture, + 'shutter' => empty($photo->shutter) ? null : $photo->shutter, + 'focal' => empty($photo->focal) ? null : $photo->focal, + 'latitude' => $photo->latitude, + 'longitude' => $photo->longitude, + 'altitude' => $photo->altitude, + 'img_direction' => empty($photo->imgDirection) ? null : $photo->imgDirection, + 'location' => empty($photo->location) ? null : $photo->location, + 'taken_at' => $photo->taken_at, + 'taken_at_orig_tz' => $photo->taken_at_orig_tz, + 'type' => $photo->type, + 'filesize' => $photo->filesize, + 'checksum' => $photo->checksum, + 'original_checksum' => $photo->checksum, + 'live_photo_short_path' => $photo->livePhotoUrl, + 'live_photo_content_id' => $photo->livePhotoContentID, + 'live_photo_checksum' => $photo->livePhotoChecksum, + ]); + + for ($variantType = self::VARIANT_ORIGINAL; $variantType <= self::VARIANT_THUMB; $variantType++) { + if ($this->hasSizeVariant($photo, $variantType)) { + DB::table('size_variants')->insert([ + 'photo_id' => $newPhotoID, + 'type' => $variantType, + 'short_path' => $this->getShortPathOfPhoto($photo, $variantType), + 'width' => $this->getWidth($photo, $variantType), + 'height' => $this->getHeight($photo, $variantType), + ]); + } + } + } + + // Restore explicit covers of albums + $pgBar = $this->getProgressBar('albums (covered)'); + $coveredAlbums = DB::table('albums_tmp') + ->whereNotNull('cover_id') + ->where('smart', '=', false) + ->lazyById(); + $pgBar->setMaxSteps($coveredAlbums->count()); + foreach ($coveredAlbums as $coveredAlbum) { + $pgBar->advance(); + DB::table('albums') + ->where('id', '=', $this->albumIDCache[strval($coveredAlbum->id)]) + ->update(['cover_id' => $this->photoIDCache[strval($coveredAlbum->cover_id)]]); + } + } + + /** + * @throws InvalidArgumentException + */ + private function downgradeCopy(): void + { + $pgBar = $this->getProgressBar('users'); + $users = DB::table('users_tmp')->get(); + $pgBar->setMaxSteps($users->count()); + foreach ($users as $user) { + $pgBar->advance(); + DB::table('users')->insert([ + 'id' => $user->id, + 'created_at' => $user->created_at, + 'updated_at' => $user->updated_at, + 'username' => $user->username, + 'password' => $user->password, + 'email' => $user->email, + 'upload' => $user->may_upload, + 'lock' => $user->is_locked, + 'remember_token' => $user->remember_token, + ]); + } + + $pgBar = $this->getProgressBar('base_albums'); + $baseAlbums = DB::table('base_albums')->lazyById(); + $pgBar->setMaxSteps($baseAlbums->count()); + $mapSorting = function (?string $sortingCol): ?string { + if (empty($sortingCol)) { + return null; + } elseif ($sortingCol === 'created_at') { + return 'id'; + } elseif ($sortingCol === 'is_public') { + return 'public'; + } elseif ($sortingCol === 'is_starred') { + return 'star'; + } else { + return $sortingCol; + } + }; + foreach ($baseAlbums as $oldBaseAlbum) { + $pgBar->advance(); + $legacyAlbumID = intval($oldBaseAlbum->legacy_id); + $this->albumIDCache[$oldBaseAlbum->id] = $legacyAlbumID; + + DB::table('albums')->insert([ + 'id' => $legacyAlbumID, + 'created_at' => $oldBaseAlbum->created_at, + 'updated_at' => $oldBaseAlbum->updated_at, + 'title' => $oldBaseAlbum->title, + 'description' => empty($oldBaseAlbum->description) ? '' : $oldBaseAlbum->description, + 'owner_id' => $oldBaseAlbum->owner_id, + 'public' => $oldBaseAlbum->is_public, + 'full_photo' => $oldBaseAlbum->grants_full_photo, + 'viewable' => !($oldBaseAlbum->requires_link), + 'downloadable' => $oldBaseAlbum->is_downloadable, + 'share_button_visible' => $oldBaseAlbum->is_share_button_visible, + 'nsfw' => $oldBaseAlbum->is_nsfw, + 'password' => empty($oldBaseAlbum->password) ? null : $oldBaseAlbum->password, + 'sorting_col' => $mapSorting($oldBaseAlbum->sorting_col), + 'sorting_order' => empty($oldBaseAlbum->sorting_col) ? null : $oldBaseAlbum->sorting_order, + ]); + } + + // Ordering by `_lft` is important, because we must copy parent + // albums first. + // Otherwise, foreign key constraint to `parent_id` may fail. + // Also, don't copy `cover_id` yet, because the photos have not been + // copied yet. + // Explicit `cover_id` needs to be set belated. + $pgBar = $this->getProgressBar('albums'); + $albums = DB::table('albums_tmp')->orderBy('_lft')->lazyById(); + $pgBar->setMaxSteps($albums->count()); + foreach ($albums as $album) { + $pgBar->advance(); + DB::table('albums') + ->where('id', '=', $this->albumIDCache[$album->id]) + ->update([ + 'smart' => false, + 'parent_id' => $album->parent_id ? $this->albumIDCache[$album->parent_id] : null, + 'license' => $album->license, + 'cover_id' => null, + '_lft' => $album->_lft, + '_rgt' => $album->_rgt, + ]); + } + + $pgBar = $this->getProgressBar('tag_albums'); + $tagAlbums = DB::table('tag_albums')->lazyById(); + $pgBar->setMaxSteps($tagAlbums->count()); + foreach ($tagAlbums as $tagAlbum) { + $pgBar->advance(); + DB::table('albums') + ->where('id', '=', $this->albumIDCache[$tagAlbum->id]) + ->update([ + 'smart' => true, + 'showtags' => $tagAlbum->show_tags, + ]); + } + + RefactorAlbumModel_AlbumModel::query()->fixTree(); + + $pgBar = $this->getProgressBar('user_album'); + $userBaseAlbumRelations = DB::table('user_base_album')->lazyById(); + $pgBar->setMaxSteps($userBaseAlbumRelations->count()); + foreach ($userBaseAlbumRelations as $userBaseAlbumRelation) { + $pgBar->advance(); + DB::table('user_album')->insert([ + 'id' => $userBaseAlbumRelation->id, + 'user_id' => $userBaseAlbumRelation->user_id, + 'album_id' => $this->albumIDCache[$userBaseAlbumRelation->base_album_id], + ]); + } + + $pgBar = $this->getProgressBar('photos'); + $photos = DB::table('photos_tmp')->lazyById(); + $pgBar->setMaxSteps($photos->count()); + foreach ($photos as $photo) { + $pgBar->advance(); + $legacyPhotoID = intval($photo->legacy_id); + $this->photoIDCache[$photo->id] = $legacyPhotoID; + $photoAttributes = [ + 'id' => $legacyPhotoID, + 'created_at' => $photo->created_at, + 'updated_at' => $photo->updated_at, + 'owner_id' => $photo->owner_id, + 'album_id' => $photo->album_id ? $this->albumIDCache[$photo->album_id] : null, + 'title' => $photo->title, + 'description' => empty($photo->description) ? '' : $photo->description, + 'tags' => empty($photo->tags) ? '' : $photo->tags, + 'license' => $photo->license, + 'public' => $photo->is_public, + 'star' => $photo->is_starred, + 'iso' => empty($photo->iso) ? '' : $photo->iso, + 'make' => empty($photo->make) ? '' : $photo->make, + 'model' => empty($photo->model) ? '' : $photo->model, + 'lens' => empty($photo->lens) ? '' : $photo->lens, + 'aperture' => empty($photo->aperture) ? '' : $photo->aperture, + 'shutter' => empty($photo->shutter) ? '' : $photo->shutter, + 'focal' => empty($photo->focal) ? '' : $photo->focal, + 'latitude' => $photo->latitude, + 'longitude' => $photo->longitude, + 'altitude' => $photo->altitude, + 'imgDirection' => $photo->img_direction, + 'location' => empty($photo->location) ? null : $photo->location, + 'taken_at' => $photo->taken_at, + 'taken_at_orig_tz' => $photo->taken_at_orig_tz, + 'type' => $photo->type, + 'filesize' => $photo->filesize, + 'checksum' => $photo->original_checksum, + 'livePhotoUrl' => $photo->live_photo_short_path, + 'livePhotoContentID' => $photo->live_photo_content_id, + 'livePhotoChecksum' => $photo->live_photo_checksum, + ]; + + // Get all size variants for the photo and explicitly extract + // the size variant "original". + // If there are no size variants at all or a size variant + // "original" does not exist, continue. + // Note, this is actually an error, because there must not + // be any photo without at least a size variant "original". + $sizeVariants = DB::table('size_variants') + ->where('photo_id', '=', $photo->id) + ->orderBy('type') + ->get(); + if ($sizeVariants->isEmpty()) { + continue; + } + $originalSizeVariant = $sizeVariants->first(); + if ($originalSizeVariant->type !== self::VARIANT_ORIGINAL) { + continue; + } + + // We use the original size variant as a baseline to extract the + // common core of the basename of all size variants. + // Note: The newly introduced `SizeVariantNamingStrategy` + // effectively allows that each size variant uses its own file + // name which may be completely independent of the file names of + // the other size variants. + // However, the old code assumes that the file names follow a + // certain naming pattern which is built around a shared and + // equal part within the file's basename. + // Moreover, this common portion must not be longer than 32 + // characters. + $expectedBasename = substr( + pathinfo($originalSizeVariant->short_path, PATHINFO_FILENAME), + 0, + 32 + ); + + /** + * Iterate over all size variants and ensure that they are named + * as expected by the old naming scheme. + * + * @var object $sizeVariant + */ + foreach ($sizeVariants as $sizeVariant) { + $fileExtension = '.' . pathinfo($sizeVariant->short_path, PATHINFO_EXTENSION); + if ( + $sizeVariant->type === self::VARIANT_THUMB2X || + $sizeVariant->type === self::VARIANT_SMALL2X || + $sizeVariant->type === self::VARIANT_MEDIUM2X + ) { + $expectedFilename = $expectedBasename . '@2x' . $fileExtension; + } else { + $expectedFilename = $expectedBasename . $fileExtension; + } + $expectedPathPrefix = self::VARIANT_2_PATH_PREFIX[$sizeVariant->type] . '/'; + if ($sizeVariant->type === self::VARIANT_ORIGINAL && $this->isRaw($photo)) { + $expectedPathPrefix = 'raw/'; + } + $expectedShortPath = $expectedPathPrefix . $expectedFilename; + + // Ensure that the size variant is stored at the location which + // is expected acc. to the old naming scheme + if ($sizeVariant->short_path !== $expectedShortPath) { + try { + Storage::move($sizeVariant->short_path, $expectedShortPath); + } catch (\Throwable $e) { + // sic! just ignore + // This exception is thrown if there are duplicate + // photos which point to the same physical file. + // Then the file is renamed when the first occurrence + // of those duplicates is processed and subsequent, + // failing attempts to rename the file must be ignored. + } + } + + if ($sizeVariant->type === self::VARIANT_THUMB2X) { + $photoAttributes['thumb2x'] = true; + } elseif ($sizeVariant->type === self::VARIANT_THUMB) { + $photoAttributes['thumbUrl'] = $expectedFilename; + } else { + if ($sizeVariant->type === self::VARIANT_ORIGINAL) { + $photoAttributes['url'] = $expectedFilename; + } + $photoAttributes[self::VARIANT_2_WIDTH_ATTRIBUTE[$sizeVariant->type]] = $sizeVariant->width; + $photoAttributes[self::VARIANT_2_HEIGHT_ATTRIBUTE[$sizeVariant->type]] = $sizeVariant->height; + } + } + + DB::table('photos')->insert($photoAttributes); + } + + // Restore explicit covers of albums + $pgBar = $this->getProgressBar('albums (covered)'); + $coveredAlbums = DB::table('albums_tmp') + ->whereNotNull('cover_id') + ->lazyById(); + $pgBar->setMaxSteps($coveredAlbums->count()); + foreach ($coveredAlbums as $coveredAlbum) { + $pgBar->advance(); + DB::table('albums') + ->where('id', '=', $this->albumIDCache[$coveredAlbum->id]) + ->update(['cover_id' => $this->photoIDCache[$coveredAlbum->cover_id]]); + } + } + + /** + * Copies those table which have not changed structurally, but whose + * date/time precision has changed. + */ + private function copyStructurallyUnchangedTables(): void + { + $pgBar = $this->getProgressBar('web_authn_credentials'); + $credentials = DB::table('web_authn_credentials_tmp')->get(); + $pgBar->setMaxSteps($credentials->count()); + foreach ($credentials as $credential) { + $pgBar->advance(); + DB::table('web_authn_credentials')->insert([ + 'id' => $credential->id, + 'created_at' => $credential->created_at, + 'updated_at' => $credential->updated_at, + 'disabled_at' => $credential->disabled_at, + 'user_id' => $credential->user_id, + 'name' => $credential->name, + 'type' => $credential->type, + 'transports' => $credential->transports, + 'attestation_type' => $credential->attestation_type, + 'trust_path' => $credential->trust_path, + 'aaguid' => $credential->aaguid, + 'public_key' => $credential->public_key, + 'counter' => $credential->counter, + 'user_handle' => $credential->user_handle, + ]); + } + + $pgBar = $this->getProgressBar('pages'); + $pages = DB::table('pages_tmp')->get(); + $pgBar->setMaxSteps($pages->count()); + foreach ($pages as $page) { + $pgBar->advance(); + DB::table('pages')->insert([ + 'id' => $page->id, + 'created_at' => $page->created_at, + 'updated_at' => $page->updated_at, + 'title' => $page->title, + 'menu_title' => $page->menu_title, + 'in_menu' => $page->in_menu, + 'enabled' => $page->enabled, + 'link' => $page->link, + 'order' => $page->order, + ]); + } + + $pgBar = $this->getProgressBar('page_contents'); + $pageContents = DB::table('page_contents_tmp')->get(); + $pgBar->setMaxSteps($pageContents->count()); + foreach ($pageContents as $pageContent) { + $pgBar->advance(); + DB::table('page_contents')->insert([ + 'id' => $pageContent->id, + 'created_at' => $pageContent->created_at, + 'updated_at' => $pageContent->updated_at, + 'page_id' => $pageContent->page_id, + 'content' => $pageContent->content, + 'class' => $pageContent->class, + 'type' => $pageContent->type, + 'order' => $pageContent->order, + ]); + } + + $pgBar = $this->getProgressBar('logs'); + $logs = DB::table('logs_tmp')->get(); + $pgBar->setMaxSteps($logs->count()); + foreach ($logs as $log) { + $pgBar->advance(); + DB::table('logs')->insert([ + 'id' => $log->id, + 'created_at' => $log->created_at, + 'updated_at' => $log->updated_at, + 'type' => $log->type, + 'function' => $log->function, + 'line' => $log->line, + 'text' => $log->text, + ]); + } + } + + /** + * Upgrades the configuration of default ordering to the new column names. + * + * @throws InvalidArgumentException + */ + private function upgradeConfig(): void + { + DB::table('configs') + ->where('key', '=', 'sorting_Photos_col') + ->update(['type_range' => 'created_at|taken_at|title|description|is_public|is_starred|type']); + DB::table('configs') + ->where('key', '=', 'sorting_Photos_col') + ->where('value', '=', 'id') + ->update(['value' => 'created_at']); + DB::table('configs') + ->where('key', '=', 'sorting_Photos_col') + ->where('value', '=', 'public') + ->update(['value' => 'is_public']); + DB::table('configs') + ->where('key', '=', 'sorting_Photos_col') + ->where('value', '=', 'star') + ->update(['value' => 'is_starred']); + DB::table('configs') + ->where('key', '=', 'sorting_Albums_col') + ->update(['type_range' => 'created_at|title|description|is_public|max_taken_at|min_taken_at']); + DB::table('configs') + ->where('key', '=', 'sorting_Albums_col') + ->where('value', '=', 'id') + ->update(['value' => 'created_at']); + DB::table('configs') + ->where('key', '=', 'sorting_Albums_col') + ->where('value', '=', 'public') + ->update(['value' => 'is_public']); + DB::table('configs') + ->insert([ + 'key' => 'legacy_id_redirection', + 'value' => '1', + 'cat' => 'config', + 'confidentiality' => 0, + 'type_range' => '0|1', + 'description' => 'Enables/disables the redirection support for legacy IDs', + ]); + } + + /** + * Downgrades the configuration of default ordering to the new column names. + * + * @throws InvalidArgumentException + */ + private function downgradeConfig(): void + { + DB::table('configs') + ->where('key', '=', 'sorting_Photos_col') + ->update(['type_range' => 'id|taken_at|title|description|public|star|type']); + DB::table('configs') + ->where('key', '=', 'sorting_Photos_col') + ->where('value', '=', 'created_at') + ->update(['value' => 'id']); + DB::table('configs') + ->where('key', '=', 'sorting_Photos_col') + ->where('value', '=', 'is_public') + ->update(['value' => 'public']); + DB::table('configs') + ->where('key', '=', 'sorting_Photos_col') + ->where('value', '=', 'is_starred') + ->update(['value' => 'star']); + DB::table('configs') + ->where('key', '=', 'sorting_Albums_col') + ->update(['type_range' => 'id|title|description|public|max_taken_at|min_taken_at']); + DB::table('configs') + ->where('key', '=', 'sorting_Albums_col') + ->where('value', '=', 'created_at') + ->update(['value' => 'id']); + DB::table('configs') + ->where('key', '=', 'sorting_Albums_col') + ->where('value', '=', 'is_public') + ->update(['value' => 'public']); + DB::table('configs') + ->where('key', '=', 'legacy_id_redirection') + ->delete(); + } + + /** + * Returns the short path of a picture file for the designated size + * variant from an old-style photo wrt. to the old naming scheme. + * + * @param object $photo an object with attributes of the old photo table + * + * @return string the short path + * + * @throws InvalidArgumentException + */ + public function getShortPathOfPhoto(object $photo, int $variant): string + { + $origFilename = $photo->url; + $thumbFilename = $photo->thumbUrl; + $thumbFilename2x = $this->add2xToFilename($thumbFilename); + $otherFilename = ($this->isVideo($photo) || $this->isRaw($photo)) ? $thumbFilename : $origFilename; + $otherFilename2x = $this->add2xToFilename($otherFilename); + switch ($variant) { + case self::VARIANT_THUMB: + $filename = $thumbFilename; + break; + case self::VARIANT_THUMB2X: + $filename = $thumbFilename2x; + break; + case self::VARIANT_SMALL: + case self::VARIANT_MEDIUM: + $filename = $otherFilename; + break; + case self::VARIANT_SMALL2X: + case self::VARIANT_MEDIUM2X: + $filename = $otherFilename2x; + break; + case self::VARIANT_ORIGINAL: + $filename = $origFilename; + break; + default: + throw new InvalidArgumentException('Invalid size variant: ' . $variant); + } + $directory = self::VARIANT_2_PATH_PREFIX[$variant] . '/'; + if ($variant === self::VARIANT_ORIGINAL && $this->isRaw($photo)) { + $directory = 'raw/'; + } + + return $directory . $filename; + } + + protected function isVideo(object $photo): bool + { + return in_array($photo->type, self::VALID_VIDEO_TYPES, true); + } + + protected function isRaw(object $photo): bool + { + return $photo->type === 'raw'; + } + + /** + * Given a filename generates the @2x corresponding filename. + * This is used for thumbs, small and medium. + */ + protected function add2xToFilename(string $filename): string + { + $filename2x = explode('.', $filename); + + return (count($filename2x) === 2) ? + $filename2x[0] . '@2x.' . $filename2x[1] : + $filename2x[0] . '@2x'; + } + + /** + * @throws InvalidArgumentException + */ + protected function getWidth(object $photo, int $variant): int + { + switch ($variant) { + case self::VARIANT_THUMB: + return self::THUMBNAIL_DIM; + case self::VARIANT_THUMB2X: + return self::THUMBNAIL2X_DIM; + case self::VARIANT_SMALL: + return $photo->small_width ?: 0; + case self::VARIANT_SMALL2X: + return $photo->small2x_width ?: 0; + case self::VARIANT_MEDIUM: + return $photo->medium_width ?: 0; + case self::VARIANT_MEDIUM2X: + return $photo->medium2x_width ?: 0; + case self::VARIANT_ORIGINAL: + return $photo->width; + default: + throw new InvalidArgumentException('Invalid size variant: ' . $variant); + } + } + + /** + * @throws InvalidArgumentException + */ + protected function getHeight(object $photo, int $variant): int + { + switch ($variant) { + case self::VARIANT_THUMB: + return self::THUMBNAIL_DIM; + case self::VARIANT_THUMB2X: + return self::THUMBNAIL2X_DIM; + case self::VARIANT_SMALL: + return $photo->small_height ?: 0; + case self::VARIANT_SMALL2X: + return $photo->small2x_height ?: 0; + case self::VARIANT_MEDIUM: + return $photo->medium_height ?: 0; + case self::VARIANT_MEDIUM2X: + return $photo->medium2x_height ?: 0; + case self::VARIANT_ORIGINAL: + return $photo->height; + default: + throw new InvalidArgumentException('Invalid size variant: ' . $variant); + } + } + + /** + * @throws InvalidArgumentException + */ + protected function hasSizeVariant(object $photo, int $variantType): bool + { + if ($variantType === self::VARIANT_ORIGINAL || $variantType === self::VARIANT_THUMB) { + return true; + } elseif ($variantType === self::VARIANT_THUMB2X) { + return (bool) ($photo->thumb2x); + } else { + return $this->getWidth($photo, $variantType) !== 0; + } + } + + private function generateKey(): string + { + // URl-compatible variant of base64 encoding + // `+` and `/` are replaced by `-` and `_`, resp. + // The other characters (a-z, A-Z, 0-9) are legal within an URL. + // As the number of bytes is divisible by 3, no trailing `=` occurs. + return strtr(base64_encode(random_bytes(3 * self::RANDOM_ID_LENGTH / 4)), '+/', '-_'); + } + + /** + * Converts a legacy ID to a Carbon instance. + * + * The method handles 32bit and 64bit integers with second and + * 1/10 millisecond resolution. + * + * @param int $id + * + * @return Carbon + * + * @throws OutOfBoundsException thrown, if `$id` is out of reasonable bounds + */ + private function convertLegacyIdToTime(int $id): Carbon + { + // Typically, the legacy ID should have either + // + // - 10 digits for 32bit platforms, or + // - 14 digits (for 64bit platforms). + // + // On 32bit platforms, the ID indicates the creation date in + // seconds since epoch. + // On 64bit platforms, the ID indicates the creation date in + // 1/10 of microseconds since epoch. + // This means we have four decimal digits of additional precision. + // + // Unfortunately, due to a bug in Lychee at some time, trailing zeros + // were stripped off. + // This means, the 2-digit number 16 might actually indicate + // the timestamp 1600000000 (Sep 13th, 2020) on a 32bit platform. + // Likewise, the 12-digit number 162033368845 might actually indicate + // the timestamp 16203336884500 (May 6h, 2021) on a 64bit platform. + // + // However, in any case we know that the integer part (measured in + // seconds since epoch) must have 10 digits. + // Any other value would not be reasonable, as 999,999,999 is a date + // in 2001 long before the birth of Lychee. + // Also, `self::BIRTH_OF_LYCHEE` is approx. one half of + // `self::MAX_SIGNED_32BIT_INT` (Jan 19th, 2038) which is far in the + // future. + // So, we can multiply/divide the id by ten for numbers which are too + // small/large and be safe that there is at most only a single + // value in the reasonable interval. + // For 32bit platforms we must take care of overflows for the + // multiplication, i.e. we must check for self::MAX_SIGNED_32BIT_INT) { + $id = (float) $id; + while ($id >= self::MAX_SIGNED_32BIT_INT) { + $id /= 10.0; + } + } + + if ($id <= self::BIRTH_OF_LYCHEE) { + throw new \OutOfBoundsException('ID-based creation time is out of reasonable bounds'); + } + + return Carbon::createFromTimestampUTC($id); + } + + /** + * Calculates the best creation time of a DB record. + * + * The method takes the (legacy) ID and the alleged creation date + * (as an SQL string) and returns an SQL string of the "best" creation + * time. + * The best creation time is either the converted, legacy ID as it + * provides a higher accuracy, or the original creation time, if the + * time based on the ID and the original creation time differ by more + * than 30 seconds. + * The latter is a safety measure in case someone has internally tweaked + * the IDs or the creation date, or if something is completely wrong + * with the timezone settings. + * + * @param int $legacyID the legacy ID of the record + * @param string $sqlCreatedAt the original creation time of the record (as an SQL string) + * + * @return string the improved creation time of the record (as an SQL string) + */ + private function calculateBestCreatedAt(int $legacyID, string $sqlCreatedAt): string + { + $result = $sqlCreatedAt; + + try { + try { + $originalCreatedAt = Carbon::createFromFormat( + 'Y-m-d H:i:s.u', + $sqlCreatedAt, + 'UTC' + ); + } catch (InvalidFormatException $e) { + $originalCreatedAt = Carbon::createFromFormat( + 'Y-m-d H:i:s', + $sqlCreatedAt, + 'UTC' + ); + } + + $idBasesCreatedAt = $this->convertLegacyIdToTime($legacyID); + $diff = $originalCreatedAt->diff($idBasesCreatedAt, true); + + if ($diff->y === 0 || $diff->m === 0 || $diff->d === 0 || $diff->h === 0 || $diff->i === 0 || $diff->s < 30) { + $result = $idBasesCreatedAt->format('Y-m-d H:i:s.u'); + } else { + throw new \RangeException('ID-based creation time and original creation time differ more than 30s'); + } + } catch (\RangeException $e) { + $this->printWarning( + 'Model ID ' . $legacyID . ' - ' . + class_basename($e) . ' - ' . $e->getMessage() + ); + } catch (\Throwable $e) { + $this->printError( + 'Model ID ' . $legacyID . ' - ' . + class_basename($e) . ' - ' . $e->getMessage() + ); + } + + return $result; + } + + /** + * Ensures the consistency of the DB on the upgrade path. + * + * The method checks the DB for consistency. + * In case of errors, the method either + * + * 1. automatically corrects the problem if the fix is easy and prints + * a warning, or + * 2. bails out with an exception and prints an error message, if the + * problem needs manual attention. + * + * The method either returns or bails out with an exception. + * + * @throws RuntimeException thrown, if DB is inconsistent + * @throws InvalidArgumentException + */ + private function ensureDBConsistency(): void + { + $checkRelation = function ( + string $modelName, + string $table, + string $column, + string $foreignModelName, + string $foreignTable, + string $fixMethod = '', + ): bool { + $missing = DB::table($table) + ->whereNotIn($column, function (Builder $q) use ($foreignTable) { + $q->from($foreignTable)->select('id'); + }) + ->select('id', $column) + ->get(); + + foreach ($missing as $m) { + $msg = 'Found ' . $modelName . + ' with ID ' . $m->id . + ' which refers to non-existing ' . $foreignModelName . + ' with ID ' . $m->{$column}; + if (empty($fixMethod)) { + $this->printError($msg); + } else { + $this->printWarning($msg); + } + } + + if ($missing->isEmpty()) { + return true; + } + + $fixQuery = DB::table($table)->whereIn('id', $missing->pluck('id')); + + switch ($fixMethod) { + case 'nullify': + $this->printInfo('Nullifying the affected relations from ' . $modelName . 's to ' . $foreignModelName . 's'); + $fixQuery->update([$column => null]); + + return true; + case 'zeroize': + $this->printInfo('Zeroizing the affected relations from ' . $modelName . 's to ' . $foreignModelName . 's'); + $fixQuery->update([$column => 0]); + + return true; + case 'delete': + $this->printInfo('Deleting the affected ' . $modelName . 's'); + $fixQuery->delete(); + + return true; + default: + $this->printInfo('Error is not automatically fixable'); + + return false; + } + }; + + // If the owner of an album is missing, assign it to the admin user + $isConsistent = $checkRelation('album', 'albums', 'owner_id', 'user', 'users', 'zeroize'); + // Move orphaned albums to the top-level + $isConsistent &= $checkRelation('album', 'albums', 'parent_id', 'parent album', 'albums', 'nullify'); + // If the cover of an album is missing, unset the cover + $isConsistent &= $checkRelation('album', 'albums', 'cover_id', 'cover photo', 'photos', 'nullify'); + // Delete orphaned shares + $isConsistent &= $checkRelation('share', 'user_album', 'user_id', 'user', 'users', 'delete'); + $isConsistent &= $checkRelation('share', 'user_album', 'album_id', 'album', 'albums', 'delete'); + // If the owner of a photo is missing, assign it to the admin user + $isConsistent &= $checkRelation('photo', 'photos', 'owner_id', 'user', 'users', 'zeroize'); + // If the album of a photo is missing, assign it to root (unsorted) album + $isConsistent &= $checkRelation('photo', 'photos', 'album_id', 'album', 'albums', 'nullify'); + // Delete orphaned WebAuthn credentials + $isConsistent &= $checkRelation('web authentication credential', 'web_authn_credentials', 'user_id', 'user', 'users', 'delete'); + // There is no obvious fix for orphaned page content + $isConsistent &= $checkRelation('page content', 'page_contents', 'page_id', 'page', 'pages'); + + // As we might have moved orphaned albums to the top, + // we need to fix the tree. + // Even if we did not move any album, fixing the tree before + // the migration does not harm as users might have fiddled with their + // DB without taking care of + // `_lft` and `_rgt` + RefactorAlbumModel_AlbumModel::query()->fixTree(); + + if (!$isConsistent) { + $this->printError('Your database is inconsistent and not fit for migration. Please fix your DB manually first.'); + throw new \RuntimeException('Inconsistent DB'); + } + } +}; diff --git a/database/migrations/2022_01_13_183131_bump_version040500.php b/database/migrations/2022_01_13_183131_bump_version040500.php new file mode 100644 index 00000000000..90422785b51 --- /dev/null +++ b/database/migrations/2022_01_13_183131_bump_version040500.php @@ -0,0 +1,28 @@ +where('key', 'version')->update(['value' => '040500']); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '040400']); + } +}; diff --git a/database/migrations/2022_01_16_181337_optimize_tables.php b/database/migrations/2022_01_16_181337_optimize_tables.php new file mode 100644 index 00000000000..ec60a125c6a --- /dev/null +++ b/database/migrations/2022_01_16_181337_optimize_tables.php @@ -0,0 +1,36 @@ +optimize = new OptimizeTables(); + } + + /** + * Run the migrations. + */ + public function up(): void + { + $this->optimize->exec(); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // Nothing do to here. + } +}; diff --git a/database/migrations/2022_02_02_203008_filesize_size_variants.php b/database/migrations/2022_02_02_203008_filesize_size_variants.php new file mode 100644 index 00000000000..758937f7083 --- /dev/null +++ b/database/migrations/2022_02_02_203008_filesize_size_variants.php @@ -0,0 +1,115 @@ +unsignedBigInteger(self::SIZE_COL)->nullable(false)->default(0); + }); + } + + DB::beginTransaction(); + + // Copy the filesize from photo to the original size variant + DB::table(self::VAR_TAB) + ->where(self::VAR_TAB . '.' . self::TYPE_COL, '=', self::TYPE_ORIGINAL) + ->update([self::SIZE_COL => DB::raw('(' . + DB::table(self::PHOTOS_TAB) + ->select([self::SIZE_COL]) + ->whereColumn(self::PHOTOS_TAB . '.' . self::ID_COL, '=', self::VAR_TAB . '.' . self::PHOTO_FK) + ->toSql() . + ')' + )]); + + /* + * Ideally, we would be using dropColumn. However it seems that the Eloquent implementation + * of dropColumn for SQLite was before SQLite supported the ALTER TABLE DROP COLUMN statement. + * Thus, the technique was to drop all constraints, copy to a temporary table without + * the deleted column, and remove the old column. This can be seen by inspecting + * the SQL commands when trying to run the migration (env DB_LOG_SQL=true). + * However, in this scenario, the command fails because of : + * `FOREIGN KEY constraint failed (SQL: DROP TABLE photos)`. + * This is a really strange bug, because redoing by hand all migration commands just work. + * To avoid corrupting user databases, and because SQLite now support column deletion, + * just run the command manually. + */ + // Schema::table(self::PHOTOS_TAB, function (Blueprint $table) { + // $table->dropColumn(self::SIZE_COL); + // }); + /* + * However, we cannot use a raw statement, because DROP COLUMN was added in SQLite 3.35, + * which is now widely available at the time. Thus, we change all values to 0 as a marker + * and will drop the column later. See PR 1239 for the entire discussion. + */ + // DB::statement('ALTER TABLE ' . self::PHOTOS_TAB . ' DROP COLUMN ' . self::SIZE_COL); + DB::table(self::PHOTOS_TAB)->update([self::SIZE_COL => 0]); + + DB::commit(); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::beginTransaction(); + + // Copy the filesize from the original size variant (if it exists) to photos + DB::table(self::PHOTOS_TAB) + ->addBinding(self::TYPE_ORIGINAL) // we must add the binding of the sub-query below as it is wrapped in a raw statement + ->update([self::SIZE_COL => DB::raw('COALESCE((' . + DB::table(self::VAR_TAB) + ->select([self::SIZE_COL]) + ->where(self::VAR_TAB . '.' . self::TYPE_COL, ' = ', self::TYPE_ORIGINAL) + ->whereColumn(self::VAR_TAB . '.' . self::PHOTO_FK, '=', self::PHOTOS_TAB . '.' . self::ID_COL) + ->toSql() . + '), 0)' + )]); + + // See comment if the upward migration. + // Schema::table(self::VAR_TAB, function (Blueprint $table) { + // $table->dropColumn(self::SIZE_COL); + // }); + // DB::statement('ALTER TABLE ' . self::VAR_TAB . ' DROP COLUMN ' . self::SIZE_COL); + DB::table(self::VAR_TAB)->update([self::SIZE_COL => 0]); + + DB::commit(); + } +}; diff --git a/database/migrations/2022_02_22_194700_fix_sorting_config.php b/database/migrations/2022_02_22_194700_fix_sorting_config.php new file mode 100644 index 00000000000..e9870944a7a --- /dev/null +++ b/database/migrations/2022_02_22_194700_fix_sorting_config.php @@ -0,0 +1,34 @@ +where('key', 'sorting_Albums_col')->update(['key' => 'sorting_albums_col']); + DB::table('configs')->where('key', 'sorting_Albums_order')->update(['key' => 'sorting_albums_order']); + DB::table('configs')->where('key', 'sorting_Photos_col')->update(['key' => 'sorting_photos_col']); + DB::table('configs')->where('key', 'sorting_Photos_order')->update(['key' => 'sorting_photos_order']); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::table('configs')->where('key', 'sorting_albums_col')->update(['key' => 'sorting_Albums_col']); + DB::table('configs')->where('key', 'sorting_albums_order')->update(['key' => 'sorting_Albums_order']); + DB::table('configs')->where('key', 'sorting_photos_col')->update(['key' => 'sorting_Photos_col']); + DB::table('configs')->where('key', 'sorting_photos_order')->update(['key' => 'sorting_Photos_order']); + } +}; diff --git a/database/migrations/2022_04_06_091900_drop_objectionable_indices.php b/database/migrations/2022_04_06_091900_drop_objectionable_indices.php new file mode 100644 index 00000000000..bad851fcd7c --- /dev/null +++ b/database/migrations/2022_04_06_091900_drop_objectionable_indices.php @@ -0,0 +1,56 @@ +optimize = new OptimizeTables(); + } + + public function up(): void + { + Schema::table('photos', function (Blueprint $table) { + $this->optimize->dropIndexIfExists($table, 'photos_created_at_index'); + $this->optimize->dropIndexIfExists($table, 'photos_updated_at_index'); + $this->optimize->dropIndexIfExists($table, 'photos_taken_at_index'); + $this->optimize->dropIndexIfExists($table, 'photos_is_public_index'); + $this->optimize->dropIndexIfExists($table, 'photos_is_starred_index'); + }); + + Schema::table('sym_links', function (Blueprint $table) { + $this->optimize->dropIndexIfExists($table, 'sym_links_updated_at_index'); + }); + } + + public function down(): void + { + } +}; \ No newline at end of file diff --git a/database/migrations/2022_04_13_094611_add_track_short_path_to_album_table.php b/database/migrations/2022_04_13_094611_add_track_short_path_to_album_table.php new file mode 100644 index 00000000000..33b0f1c37b3 --- /dev/null +++ b/database/migrations/2022_04_13_094611_add_track_short_path_to_album_table.php @@ -0,0 +1,33 @@ +string('track_short_path')->after('cover_id')->nullable()->default(null); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('albums', function (Blueprint $table) { + $table->dropColumn('track_short_path'); + }); + } +}; diff --git a/database/migrations/2022_04_16_170724_add_missing_indices.php b/database/migrations/2022_04_16_170724_add_missing_indices.php new file mode 100644 index 00000000000..b523511718f --- /dev/null +++ b/database/migrations/2022_04_16_170724_add_missing_indices.php @@ -0,0 +1,65 @@ +getConnection(); + $this->driverName = $connection->getDriverName(); + $this->optimize = new OptimizeTables(); + } + + public function up(): void + { + Schema::table('photos', function (Blueprint $table) { + // These indices are needed to efficiently retrieve the covers of + // albums acc. to different sorting criteria + // Note, that covers are always sorted acc. to `is_starred` first. + $table->index(['album_id', 'is_starred', 'title']); + + // In the case of mysql we apply the RAW query below. + if ($this->driverName !== 'mysql') { + $table->index(['album_id', 'is_starred', 'description']); + } + }); + + // MySQL cannot create indices over unlimited string values + // So we must explicitly define an upper bound on how many characters + // are analyzed for sorting + if ($this->driverName === 'mysql') { + DB::statement('alter table `photos` add index `photos_album_id_is_starred_description(128)_index`(album_id, is_starred, description(128))'); + } + } + + public function down(): void + { + $descriptionSQL = match ($this->driverName) { + 'mysql' => 'description(128)', + default => 'description', + }; + + Schema::table('photos', function (Blueprint $table) use ($descriptionSQL) { + $this->optimize->dropIndexIfExists($table, 'photos_album_id_is_starred_title_index'); + $this->optimize->dropIndexIfExists($table, 'photos_album_id_is_starred_' . $descriptionSQL . '_index'); + }); + } +}; diff --git a/database/migrations/2022_04_16_174503_bump_version040501.php b/database/migrations/2022_04_16_174503_bump_version040501.php new file mode 100644 index 00000000000..b78284af3ac --- /dev/null +++ b/database/migrations/2022_04_16_174503_bump_version040501.php @@ -0,0 +1,28 @@ +where('key', 'version')->update(['value' => '040501']); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '040500']); + } +}; diff --git a/database/migrations/2022_04_18_150400_add_index_for_delete.php b/database/migrations/2022_04_18_150400_add_index_for_delete.php new file mode 100644 index 00000000000..18a70e43949 --- /dev/null +++ b/database/migrations/2022_04_18_150400_add_index_for_delete.php @@ -0,0 +1,45 @@ +optimize = new OptimizeTables(); + } + + public function up(): void + { + Schema::table('size_variants', function (Blueprint $table) { + // This index is required by \App\Actions\SizeVariant\Delete::do() + // for `SizeVariant::query()` + $table->index(['short_path']); + }); + } + + public function down(): void + { + Schema::table('size_variants', function (Blueprint $table) { + $this->optimize->dropIndexIfExists($table, 'size_variants_short_path_index'); + }); + } +}; diff --git a/database/migrations/2022_04_18_174417_fix_live_photo_short_path.php b/database/migrations/2022_04_18_174417_fix_live_photo_short_path.php new file mode 100644 index 00000000000..79b61f6bfc1 --- /dev/null +++ b/database/migrations/2022_04_18_174417_fix_live_photo_short_path.php @@ -0,0 +1,69 @@ +getConnection(); + $this->driverName = $connection->getDriverName(); + } + + /** + * Run the migrations. + * + * @throws RuntimeException + */ + public function up(): void + { + // MySQL misuses the ANSI SQL concatenation operator `||` for + // a logical OR and provides the proprietary `CONCAT` statement + // instead. + $sqlConcatLivePhotoPath = match ($this->driverName) { + 'mysql' => DB::raw('CONCAT(\'big/\', live_photo_short_path)'), + 'pgsql', 'sqlite' => DB::raw('\'big/\' || live_photo_short_path'), + default => throw new \RuntimeException('Unknown DBMS'), + }; + + DB::table('photos') + ->whereNotNull('live_photo_short_path') + ->where('live_photo_short_path', 'not like', '%/%') + ->update(['live_photo_short_path' => $sqlConcatLivePhotoPath]); + } + + /** + * Reverse the migrations. + * + * @throws RuntimeException + */ + public function down(): void + { + // In contrast to all other programming languages, the first character + // of a string has index 1 (not 0) in SQL. + // We want to remove `'big/'` or `'raw/'` from the string. + $sqlSubstringLivePhotoPath = match ($this->driverName) { + 'mysql', 'pgsql' => DB::raw('SUBSTRING(live_photo_short_path FROM 5)'), + 'sqlite' => DB::raw('SUBSTR(live_photo_short_path, 5)'), + default => throw new \RuntimeException('Unknown DBMS'), + }; + + DB::table('photos') + ->whereNotNull('live_photo_short_path') + ->where('live_photo_short_path', 'like', '%/%') + ->update(['live_photo_short_path' => $sqlSubstringLivePhotoPath]); + } +}; diff --git a/database/migrations/2022_06_12_075709_add_token_to_user_table.php b/database/migrations/2022_06_12_075709_add_token_to_user_table.php new file mode 100644 index 00000000000..a38f7cc4631 --- /dev/null +++ b/database/migrations/2022_06_12_075709_add_token_to_user_table.php @@ -0,0 +1,45 @@ +where('key', '=', 'api_key')->delete(); + + if (!Schema::hasColumn('users', 'token')) { + Schema::table('users', function (Blueprint $table) { + $table->char('token', 128)->after('email')->unique()->nullable()->default(null); + }); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::table('configs')->insert([ + [ + 'key' => 'api_key', + 'value' => '', + 'confidentiality' => 3, + 'cat' => 'Admin', + ], + ]); + } +}; diff --git a/database/migrations/2022_07_09_130303_create_webauthn_credentials.php b/database/migrations/2022_07_09_130303_create_webauthn_credentials.php new file mode 100644 index 00000000000..160705c6758 --- /dev/null +++ b/database/migrations/2022_07_09_130303_create_webauthn_credentials.php @@ -0,0 +1,108 @@ +string('id')->primary(); + + $table->morphs('authenticatable', 'webauthn_user_index'); + + // This is the user UUID that is generated automatically when a credential for the + // given user is created. If a second credential is created, this UUID is queried + // and then copied on top of the new one, this way the real User ID doesn't change. + $table->uuid('user_id'); + + // The app may allow the user to name or rename a credential to a friendly name, + // like "John's iPhone" or "Office Computer". + $table->string('alias')->nullable(); + + // Allows to detect cloned credentials when the assertion does not have this same counter. + $table->unsignedBigInteger('counter')->nullable(); + // Who created the credential. Should be the same reported by the Authenticator. + $table->string('rp_id'); + // Where the credential was created. Should be the same reported by the Authenticator. + $table->string('origin'); + $table->json('transports')->nullable(); + $table->uuid('aaguid')->nullable(); // GUID are essentially UUID + + // This is the public key the credential uses to verify the challenges. + $table->text('public_key'); + // The attestation of the public key. + $table->string('attestation_format')->default('none'); + // This would hold the certificate chain for other different attestation formats. + $table->json('certificates')->nullable(); + + // A way to disable the credential without deleting it. + $table->dateTime('disabled_at')->nullable(); + $table->dateTime('created_at')->nullable(false); + $table->dateTime('updated_at')->nullable(false); + } + + /** + * Generate the default blueprint for the WebAuthn credentials table. + * + * @param \Illuminate\Database\Schema\Blueprint $table + */ + protected static function oldTable(Blueprint $table): void + { + $table->string('id', 255); + $table->dateTime('created_at', 6)->nullable(false); + $table->dateTime('updated_at', 6)->nullable(false); + $table->dateTime('disabled_at', 6)->nullable(true); + $table->unsignedInteger('user_id')->nullable(false); + $table->string('name')->nullable(); + $table->string('type', 16); + $table->json('transports'); + $table->json('attestation_type'); + $table->json('trust_path'); + $table->uuid('aaguid'); + $table->binary('public_key'); + $table->unsignedInteger('counter')->default(0); + $table->uuid('user_handle')->nullable(); + // Indices + $table->primary(['id', 'user_id']); + $table->foreign('user_id') + ->references('id')->on('users') + ->cascadeOnDelete(); + } +}; \ No newline at end of file diff --git a/database/migrations/2022_07_13_174800_permission_test.php b/database/migrations/2022_07_13_174800_permission_test.php new file mode 100644 index 00000000000..4acb9437824 --- /dev/null +++ b/database/migrations/2022_07_13_174800_permission_test.php @@ -0,0 +1,27 @@ + 1]); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + } +}; diff --git a/database/migrations/2022_07_24_102214_bump_version040502.php b/database/migrations/2022_07_24_102214_bump_version040502.php new file mode 100644 index 00000000000..47712df397a --- /dev/null +++ b/database/migrations/2022_07_24_102214_bump_version040502.php @@ -0,0 +1,28 @@ +where('key', 'version')->update(['value' => '040502']); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '040501']); + } +}; diff --git a/database/migrations/2022_08_03_184746_add_zip_options.php b/database/migrations/2022_08_03_184746_add_zip_options.php new file mode 100644 index 00000000000..8971188a968 --- /dev/null +++ b/database/migrations/2022_08_03_184746_add_zip_options.php @@ -0,0 +1,25 @@ + 'zip_deflate_level', + 'value' => '6', + 'confidentiality' => '0', + 'cat' => 'config', + 'type_range' => '-1|0|1|2|3|4|5|6|7|8|9', + 'description' => 'DEFLATE compression level: -1 = disable compression (use STORE method), 0 = no compression (use DEFLATE method), 1 = minimal compression (fast), ... 9 = maximum compression (slow)', + ], + ]; + } +}; diff --git a/database/migrations/2022_08_06_205701_bump_version040503.php b/database/migrations/2022_08_06_205701_bump_version040503.php new file mode 100644 index 00000000000..bedd5fbf9bd --- /dev/null +++ b/database/migrations/2022_08_06_205701_bump_version040503.php @@ -0,0 +1,28 @@ +where('key', 'version')->update(['value' => '040503']); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '040502']); + } +}; diff --git a/database/migrations/2022_08_06_210757_bump_version040600.php b/database/migrations/2022_08_06_210757_bump_version040600.php new file mode 100644 index 00000000000..7a2625e1d17 --- /dev/null +++ b/database/migrations/2022_08_06_210757_bump_version040600.php @@ -0,0 +1,28 @@ +where('key', 'version')->update(['value' => '040600']); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '040502']); + } +}; diff --git a/database/migrations/2022_08_27_103010_drop_page_support.php b/database/migrations/2022_08_27_103010_drop_page_support.php new file mode 100644 index 00000000000..05e7aea9954 --- /dev/null +++ b/database/migrations/2022_08_27_103010_drop_page_support.php @@ -0,0 +1,79 @@ +format(self::SQL_DATETIME_FORMAT); + + Schema::dropIfExists('pages'); + Schema::create('pages', function (Blueprint $table) { + $table->increments('id'); + $table->string('title', 150)->default(''); + $table->string('menu_title', 100)->default(''); + $table->boolean('in_menu')->default(false); + $table->boolean('enabled')->default(false); + $table->string('link', 150)->default(''); + $table->integer('order')->default(0); + $table->dateTime('created_at', 0)->nullable(false); + $table->dateTime('updated_at', 0)->nullable(false); + }); + + DB::table('pages')->insert([ + [ + 'title' => 'gallery', + 'menu_title' => 'gallery', + 'in_menu' => true, + 'link' => '/gallery', + 'enabled' => true, + 'order' => 2, + 'created_at' => $strNow, + 'updated_at' => $strNow, // also set `updated_at` to ensure that `updated_at` is not before `created_at` + ], + ]); + + Schema::dropIfExists('page_contents'); + Schema::create('page_contents', function (Blueprint $table) { + $table->increments('id'); + $table->integer('page_id')->unsigned(); + $table->foreign('page_id')->references('id')->on('pages')->onDelete('cascade'); + $table->text('content'); + $table->string('class', 150); + $table->enum('type', ['div', 'img']); + $table->integer('order')->default(0); + $table->dateTime('created_at', 0)->nullable(false); + $table->dateTime('updated_at', 0)->nullable(false); + }); + } +}; diff --git a/database/migrations/2022_08_27_110209_drop_admin_user_config.php b/database/migrations/2022_08_27_110209_drop_admin_user_config.php new file mode 100644 index 00000000000..eca0916ce8b --- /dev/null +++ b/database/migrations/2022_08_27_110209_drop_admin_user_config.php @@ -0,0 +1,48 @@ +whereIn('key', ['username', 'password']) + ->delete(); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + defined('STRING_REQ') or define('STRING_REQ', 'string_required'); + + DB::table('configs')->insert([ + [ + 'key' => 'username', + 'value' => '', + 'confidentiality' => '4', + 'cat' => 'Admin', + 'type_range' => STRING_REQ, + ], + [ + 'key' => 'password', + 'value' => '', + 'confidentiality' => '4', + 'cat' => 'Admin', + 'type_range' => STRING_REQ, + ], + ] + ); + } +}; diff --git a/database/migrations/2022_09_27_103710_bump_version040601.php b/database/migrations/2022_09_27_103710_bump_version040601.php new file mode 100644 index 00000000000..8fe51429d72 --- /dev/null +++ b/database/migrations/2022_09_27_103710_bump_version040601.php @@ -0,0 +1,28 @@ +where('key', 'version')->update(['value' => '040601']); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '040600']); + } +}; diff --git a/database/migrations/2022_10_23_143201_make_wui_settings_public.php b/database/migrations/2022_10_23_143201_make_wui_settings_public.php new file mode 100644 index 00000000000..7016011fcef --- /dev/null +++ b/database/migrations/2022_10_23_143201_make_wui_settings_public.php @@ -0,0 +1,237 @@ +where('key', '=', 'landing_owner') + ->update([ + 'key' => 'site_owner', + 'cat' => self::CONF_CATEGORY_CONF, + 'confidentiality' => 0, + ]); + + // The social media links are not only used on the landing page, + // but also in the general footer of the gallery as well, if enabled. + // Rename them to reflect this and put them into their own category + // to keep them together. + + DB::table('configs') + ->where('key', '=', 'landing_facebook') + ->update([ + 'key' => 'sm_facebook_url', + 'cat' => self::CONF_CATEGORY_SOCIAL_MEDIA, + 'confidentiality' => 0, + ]); + DB::table('configs') + ->where('key', '=', 'landing_flickr') + ->update([ + 'key' => 'sm_flickr_url', + 'cat' => self::CONF_CATEGORY_SOCIAL_MEDIA, + 'confidentiality' => 0, + ]); + DB::table('configs') + ->where('key', '=', 'landing_twitter') + ->update([ + 'key' => 'sm_twitter_url', + 'cat' => self::CONF_CATEGORY_SOCIAL_MEDIA, + 'confidentiality' => 0, + ]); + DB::table('configs') + ->where('key', '=', 'landing_instagram') + ->update([ + 'key' => 'sm_instagram_url', + 'cat' => self::CONF_CATEGORY_SOCIAL_MEDIA, + 'confidentiality' => 0, + ]); + DB::table('configs') + ->where('key', '=', 'landing_youtube') + ->update([ + 'key' => 'sm_youtube_url', + 'cat' => self::CONF_CATEGORY_SOCIAL_MEDIA, + 'confidentiality' => 0, + ]); + + // Make footer settings public and group them together + + DB::table('configs') + ->where('key', '=', 'display_social_in_gallery') + ->update([ + 'key' => 'footer_show_social_media', + 'cat' => self::CONF_CATEGORY_FOOTER, + 'confidentiality' => 0, + ]); + DB::table('configs') + ->where('key', '=', 'site_copyright_enable') + ->update([ + 'key' => 'footer_show_copyright', + 'cat' => self::CONF_CATEGORY_FOOTER, + 'confidentiality' => 0, + ]); + DB::table('configs') + ->where('key', '=', 'additional_footer_text') + ->update([ + 'key' => 'footer_additional_text', + 'cat' => self::CONF_CATEGORY_FOOTER, + 'confidentiality' => 0, + ]); + + // Make copyright settings available to WUI + + DB::table('configs') + ->where('key', '=', 'site_copyright_begin') + ->update(['confidentiality' => 0]); + DB::table('configs') + ->where('key', '=', 'site_copyright_end') + ->update(['confidentiality' => 0]); + + // Rename NSFW text config, make it available to WUI and clear its + // value, if it still equals the default value in order to use the + // localized variant in the default case + DB::table('configs') + ->where('key', '=', 'nsfw_warning_text') + ->update([ + 'key' => 'nsfw_banner_override', + 'type_range' => 'string', + 'confidentiality' => 0, + ]); + DB::table('configs') + ->where('key', '=', 'nsfw_banner_override') + ->where('value', '=', '

Sensitive content

This album contains sensitive content which some people may find offensive or disturbing.

Tap to consent.

') + ->update(['value' => '']); + + // Make setting key use small letters like everywhere else + DB::table('configs') + ->where('key', '=', 'Mod_Frame') + ->update(['key' => 'mod_frame_enabled']); + DB::table('configs') + ->where('key', '=', 'Mod_Frame_refresh') + ->update(['key' => 'mod_frame_refresh']); + } + + /** + * Reverse the migrations. + * + * @throws InvalidArgumentException + */ + public function down(): void + { + DB::table('configs') + ->where('key', '=', 'site_owner') + ->update([ + 'key' => 'landing_owner', + 'cat' => self::CONF_CATEGORY_LANDING, + 'confidentiality' => 2, + ]); + DB::table('configs') + ->where('key', '=', 'sm_facebook_url') + ->update([ + 'key' => 'landing_facebook', + 'cat' => self::CONF_CATEGORY_LANDING, + 'confidentiality' => 2, + ]); + DB::table('configs') + ->where('key', '=', 'sm_flickr_url') + ->update([ + 'key' => 'landing_flickr', + 'cat' => self::CONF_CATEGORY_LANDING, + 'confidentiality' => 2, + ]); + DB::table('configs') + ->where('key', '=', 'sm_twitter_url') + ->update([ + 'key' => 'landing_twitter', + 'cat' => self::CONF_CATEGORY_LANDING, + 'confidentiality' => 2, + ]); + DB::table('configs') + ->where('key', '=', 'sm_instagram_url') + ->update([ + 'key' => 'landing_instagram', + 'cat' => self::CONF_CATEGORY_LANDING, + 'confidentiality' => 2, + ]); + DB::table('configs') + ->where('key', '=', 'sm_youtube_url') + ->update([ + 'key' => 'landing_youtube', + 'cat' => self::CONF_CATEGORY_LANDING, + 'confidentiality' => 2, + ]); + DB::table('configs') + ->where('key', '=', 'footer_show_social_media') + ->update([ + 'key' => 'display_social_in_gallery', + 'cat' => self::CONF_CATEGORY_CONF, + 'confidentiality' => 2, + ]); + DB::table('configs') + ->where('key', '=', 'footer_show_copyright') + ->update([ + 'key' => 'site_copyright_enable', + 'cat' => self::CONF_CATEGORY_CONF, + 'confidentiality' => 2, + ]); + DB::table('configs') + ->where('key', '=', 'footer_additional_text') + ->update([ + 'key' => 'additional_footer_text', + 'cat' => self::CONF_CATEGORY_CONF, + 'confidentiality' => 2, + ]); + DB::table('configs') + ->where('key', '=', 'nsfw_banner_override') + ->update([ + 'key' => 'nsfw_warning_text', + 'confidentiality' => 3, + 'type_range' => 'string_required', + ]); + DB::table('configs') + ->where('key', '=', 'nsfw_warning_text') + ->where('value', '=', '') + ->update(['value' => '

Sensitive content

This album contains sensitive content which some people may find offensive or disturbing.

Tap to consent.

']); + DB::table('configs') + ->where('key', '=', 'mod_frame_enabled') + ->update(['key' => 'Mod_Frame']); + DB::table('configs') + ->where('key', '=', 'mod_frame_refresh') + ->update(['key' => 'Mod_Frame_refresh']); + } +}; diff --git a/database/migrations/2022_10_28_232159_bump_version040602.php b/database/migrations/2022_10_28_232159_bump_version040602.php new file mode 100644 index 00000000000..474a37c307a --- /dev/null +++ b/database/migrations/2022_10_28_232159_bump_version040602.php @@ -0,0 +1,28 @@ +where('key', 'version')->update(['value' => '040602']); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '040601']); + } +}; diff --git a/database/migrations/2022_11_07_171403_add_config_descriptions.php b/database/migrations/2022_11_07_171403_add_config_descriptions.php new file mode 100644 index 00000000000..abdf364d02e --- /dev/null +++ b/database/migrations/2022_11_07_171403_add_config_descriptions.php @@ -0,0 +1,118 @@ +where('key', 'version')->update(['description' => 'Current version of Lychee']); + DB::table('configs')->where('key', 'check_for_updates')->update(['description' => 'Automatically check for new updates']); + DB::table('configs')->where('key', 'sorting_photos_col')->update(['description' => 'Default column used for sorting photos']); + DB::table('configs')->where('key', 'sorting_photos_order')->update(['description' => 'Default order used for sorting photos']); + DB::table('configs')->where('key', 'sorting_albums_col')->update(['description' => 'Default column used for sorting albums']); + DB::table('configs')->where('key', 'sorting_albums_order')->update(['description' => 'Default order used for sorting albums']); + DB::table('configs')->where('key', 'imagick')->update(['description' => 'Enable imagick processing']); + DB::table('configs')->where('key', 'dropbox_key')->update(['description' => 'Dropbox API key']); + DB::table('configs')->where('key', 'skip_duplicates')->update(['description' => 'Skip duplicate if found on import']); + DB::table('configs')->where('key', 'lang')->update(['description' => 'Language used by Lychee']); + DB::table('configs')->where('key', 'layout')->update(['description' => 'Layout for pictures']); + DB::table('configs')->where('key', 'default_license')->update(['description' => 'Default license used for albums']); + DB::table('configs')->where('key', 'small_max_width')->update(['description' => 'Maximum width for small thumbs ((un)justified album view)']); + DB::table('configs')->where('key', 'small_max_height')->update(['description' => 'Maximum height for small thumbs ((un)justified album view)']); + DB::table('configs')->where('key', 'medium_max_width')->update(['description' => 'Maximum width for medium image (photo view)']); + DB::table('configs')->where('key', 'medium_max_height')->update(['description' => 'Maximum height for medium image (photo view)']); + DB::table('configs')->where('key', 'full_photo')->update(['description' => 'Allows access to full resolution by default']); + DB::table('configs')->where('key', 'delete_imported')->update(['description' => 'When importing from server, delete originals']); + DB::table('configs')->where('key', 'Mod_Frame')->update(['description' => 'Enable Frame mode']); + DB::table('configs')->where('key', 'Mod_Frame_refresh')->update(['description' => 'Refresh rate of the Frame']); + DB::table('configs')->where('key', 'image_overlay_type')->update(['description' => 'Default image overlay information']); + DB::table('configs')->where('key', 'compression_quality')->update(['description' => 'Compression percent when generating thumbs']); + DB::table('configs')->where('key', 'landing_page_enable')->update(['description' => 'Display the landing page']); + DB::table('configs')->where('key', 'landing_owner')->update(['description' => 'Owner of the Website']); + DB::table('configs')->where('key', 'landing_title')->update(['description' => 'Title on the landing page']); + DB::table('configs')->where('key', 'landing_subtitle')->update(['description' => 'Subtitle on the landing page']); + DB::table('configs')->where('key', 'landing_facebook')->update(['description' => 'Link to facebook user account']); + DB::table('configs')->where('key', 'landing_flickr')->update(['description' => 'Link to flickr user account']); + DB::table('configs')->where('key', 'landing_twitter')->update(['description' => 'Link to twitter user account']); + DB::table('configs')->where('key', 'landing_instagram')->update(['description' => 'Link to instagram user account']); + DB::table('configs')->where('key', 'landing_youtube')->update(['description' => 'Link to youtube user account']); + DB::table('configs')->where('key', 'landing_background')->update(['description' => 'URL of background image']); + DB::table('configs')->where('key', 'thumb_2x')->update(['description' => 'Enable 2x size of square thumbs']); + DB::table('configs')->where('key', 'small_2x')->update(['description' => 'Enable 2x size of small thumbs']); + DB::table('configs')->where('key', 'medium_2x')->update(['description' => 'Enable 2x size of medium pictures']); + DB::table('configs')->where('key', 'site_title')->update(['description' => 'Website title']); + DB::table('configs')->where('key', 'site_copyright_enable')->update(['description' => 'Enable copyright notice at the bottom']); + DB::table('configs')->where('key', 'site_copyright_begin')->update(['description' => 'Initial year of copyright']); + DB::table('configs')->where('key', 'site_copyright_end')->update(['description' => 'Last year of copyright']); + DB::table('configs')->where('key', 'api_key')->update(['description' => 'Deprecated']); + DB::table('configs')->where('key', 'allow_online_git_pull')->update(['description' => 'Allow git pull via web interface']); + DB::table('configs')->where('key', 'force_migration_in_production')->update(['description' => 'Force migration even if app is in production mode']); + DB::table('configs')->where('key', 'additional_footer_text')->update(['description' => 'Extra text at the bottom of the page']); + DB::table('configs')->where('key', 'display_social_in_gallery')->update(['description' => 'Display social links at the bottom of the gallery']); + DB::table('configs')->where('key', 'public_search')->update(['description' => 'Allows anonymous user to use the Search bar']); + DB::table('configs')->where('key', 'gen_demo_js')->update(['description' => 'Enable generation of JS responses for demo purposes']); + DB::table('configs')->where('key', 'hide_version_number')->update(['description' => 'Hide current version number']); + DB::table('configs')->where('key', 'public_recent')->update(['description' => 'Make Recent smart album accessible to anonymous users']); + DB::table('configs')->where('key', 'recent_age')->update(['description' => 'Maximum age of pictures in Recent in days']); + DB::table('configs')->where('key', 'public_starred')->update(['description' => 'Make Starred smart album accessible to anonymous users']); + DB::table('configs')->where('key', 'SL_enable')->update(['description' => 'Enable symbolic link protection']); + DB::table('configs')->where('key', 'SL_for_admin')->update(['description' => 'Enable symbolic links on logged in admin user']); + DB::table('configs')->where('key', 'SL_life_time_days')->update(['description' => 'Maximum life time for symbolic link']); + DB::table('configs')->where('key', 'photos_wraparound')->update(['description' => 'Once reaching last picture of an album, loop back to the start']); + DB::table('configs')->where('key', 'raw_formats')->update(['description' => 'Allowed extra formats, will not be processed']); + DB::table('configs')->where('key', 'map_display')->update(['description' => 'Display the map given GPS coordinates']); + DB::table('configs')->where('key', 'zip64')->update(['description' => 'Use Zip 64bits instead of 32 bits']); + DB::table('configs')->where('key', 'force_32bit_ids')->update(['description' => 'Force 32 bit legacy identifiers in the database']); + DB::table('configs')->where('key', 'map_display_public')->update(['description' => 'Allow anonymous users to access the map']); + DB::table('configs')->where('key', 'map_provider')->update(['description' => 'Defines the map provider']); + DB::table('configs')->where('key', 'map_include_subalbums')->update(['description' => 'Includes pictures of the sub albums on the map']); + DB::table('configs')->where('key', 'update_check_every_days')->update(['description' => 'Frequency of Lychee update checks']); + DB::table('configs')->where('key', 'has_exiftool')->update(['description' => 'Defines whether exiftool processing is available']); + DB::table('configs')->where('key', 'share_button_visible')->update(['description' => 'Share button visibility in the header']); + DB::table('configs')->where('key', 'has_ffmpeg')->update(['description' => 'Defines whether ffmpeg processing is available']); + DB::table('configs')->where('key', 'import_via_symlink')->update(['description' => 'Use symbolic links instead of copying the original on import from server']); + DB::table('configs')->where('key', 'apply_composer_update')->update(['description' => 'Apply composer update on lychee update via web interface']); + DB::table('configs')->where('key', 'location_decoding')->update(['description' => 'Use GPS location decoding']); + DB::table('configs')->where('key', 'location_decoding_timeout')->update(['description' => 'Timeout for the GPS decoding queries']); + DB::table('configs')->where('key', 'location_show')->update(['description' => 'Show location extracted from GPS coordinates']); + DB::table('configs')->where('key', 'location_show_public')->update(['description' => 'Anonymous users can acess the extracted location from GPS coordinates']); + DB::table('configs')->where('key', 'rss_enable')->update(['description' => 'Enable RSS feed']); + DB::table('configs')->where('key', 'rss_recent_days')->update(['description' => 'Display the last X days in the RSS feed']); + DB::table('configs')->where('key', 'rss_max_items')->update(['description' => 'Max number of items in the RSS feed']); + DB::table('configs')->where('key', 'prefer_available_xmp_metadata')->update(['description' => 'Use sidecar if provided instead of exif metadata']); + DB::table('configs')->where('key', 'editor_enabled')->update(['description' => 'Enable manual rotation of images']); + DB::table('configs')->where('key', 'lossless_optimization')->update(['description' => 'Apply additional compression on images']); + DB::table('configs')->where('key', 'swipe_tolerance_x')->update(['description' => 'Defines default horizontal swipe tolerance for mobile interactions']); + DB::table('configs')->where('key', 'swipe_tolerance_y')->update(['description' => 'Defines default vertical swipe tolerance for mobile interactions']); + DB::table('configs')->where('key', 'log_max_num_line')->update(['description' => 'Display the last X most recent lines in Logs']); + DB::table('configs')->where('key', 'unlock_password_photos_with_url_param')->update(['description' => 'Allow password to be passed as a URL parameter to unlock albums']); + DB::table('configs')->where('key', 'nsfw_visible')->update(['description' => 'Make sensitive albums visible by default']); + DB::table('configs')->where('key', 'nsfw_blur')->update(['description' => 'Blur sensitive albums']); + DB::table('configs')->where('key', 'nsfw_warning')->update(['description' => 'Enable sensitive albums warning']); + DB::table('configs')->where('key', 'nsfw_warning_admin')->update(['description' => 'Enable sensitive albums warning when logged in']); + DB::table('configs')->where('key', 'nsfw_warning_text')->update(['description' => 'Text of the sensitive albums warning']); + DB::table('configs')->where('key', 'map_display_direction')->update(['description' => 'Display the direction of the picture on the map if available']); + DB::table('configs')->where('key', 'album_subtitle_type')->update(['description' => 'Defines the subtitle of album in albums view']); + DB::table('configs')->where('key', 'upload_processing_limit')->update(['description' => 'Maximum number of images processed in parallel']); + DB::table('configs')->where('key', 'public_photos_hidden')->update(['description' => 'Keep singular public pictures hidden from search results, smart albums & tag albums']); + DB::table('configs')->where('key', 'new_photos_notification')->update(['description' => 'Enable notifications when new photos are added']); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::table('configs')->whereIn('key', ['version', 'check_for_updates', 'sorting_photos_col', 'sorting_photos_order', 'sorting_albums_col', 'sorting_albums_order', 'imagick', 'dropbox_key', 'skip_duplicates', 'lang', 'layout', 'default_license', 'small_max_width', 'small_max_height', 'medium_max_width', 'medium_max_height', 'full_photo', 'delete_imported', 'Mod_Frame', 'Mod_Frame_refresh', 'image_overlay_type', 'compression_quality', 'landing_page_enable', 'landing_owner', 'landing_title', 'landing_subtitle', 'landing_facebook', 'landing_flickr', 'landing_twitter', 'landing_instagram', 'landing_youtube', 'landing_background', 'thumb_2x', 'small_2x', 'medium_2x', 'site_title', 'site_copyright_enable', 'site_copyright_begin', 'site_copyright_end', 'api_key', 'allow_online_git_pull', 'force_migration_in_production', 'additional_footer_text', 'display_social_in_gallery', 'public_search', 'gen_demo_js', 'hide_version_number', 'public_recent', 'recent_age', 'public_starred', 'SL_enable', 'SL_for_admin', 'SL_life_time_days', 'photos_wraparound', 'raw_formats', 'map_display', 'zip64', 'force_32bit_ids', 'map_display_public', 'map_provider', 'map_include_subalbums', 'update_check_every_days', 'has_exiftool', 'share_button_visible', 'has_ffmpeg', 'import_via_symlink', 'apply_composer_update', 'location_decoding', 'location_decoding_timeout', 'location_show', 'location_show_public', 'rss_enable', 'rss_recent_days', 'rss_max_items', 'prefer_available_xmp_metadata', 'editor_enabled', 'lossless_optimization', 'swipe_tolerance_x', 'swipe_tolerance_y', 'log_max_num_line', 'unlock_password_photos_with_url_param', 'nsfw_visible', 'nsfw_blur', 'nsfw_warning', 'nsfw_warning_admin', 'nsfw_warning_text', 'map_display_direction', 'album_subtitle_type', 'upload_processing_limit', 'public_photos_hidden', 'new_photos_notification'])->update(['description' => '']); + } +}; diff --git a/database/migrations/2022_11_27_143608_bump_version040603.php b/database/migrations/2022_11_27_143608_bump_version040603.php new file mode 100644 index 00000000000..6a76780e161 --- /dev/null +++ b/database/migrations/2022_11_27_143608_bump_version040603.php @@ -0,0 +1,28 @@ +where('key', 'version')->update(['value' => '040603']); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '040602']); + } +}; diff --git a/database/migrations/2022_12_05_195600_enable_disable_smart_albums.php b/database/migrations/2022_12_05_195600_enable_disable_smart_albums.php new file mode 100644 index 00000000000..5d9c83dabcc --- /dev/null +++ b/database/migrations/2022_12_05_195600_enable_disable_smart_albums.php @@ -0,0 +1,25 @@ + 'SA_enabled', + 'value' => '1', + 'confidentiality' => '2', + 'cat' => 'Smart Albums', + 'type_range' => self::BOOL, + 'description' => 'Enable Smart Albums', + ], + ]; + } +}; diff --git a/database/migrations/2022_12_07_141854_rename_capabilities.php b/database/migrations/2022_12_07_141854_rename_capabilities.php new file mode 100644 index 00000000000..f49f40cdd24 --- /dev/null +++ b/database/migrations/2022_12_07_141854_rename_capabilities.php @@ -0,0 +1,63 @@ +update(['is_locked' => DB::raw('NOT is_locked')]); + + // rename the column + Schema::table('users', function (Blueprint $table) { + $table->renameColumn('is_locked', 'may_edit_own_settings'); + }); + + // create administration variable + Schema::table('users', function (Blueprint $table) { + $table->boolean('may_administrate')->after('email')->default(false); + }); + DB::table('users')->where('id', '=', '0')->update(['may_administrate' => true]); + if (DB::getDriverName() === 'sqlite') { + Schema::enableForeignKeyConstraints(); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + if (DB::getDriverName() === 'sqlite') { + Schema::disableForeignKeyConstraints(); + } + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('may_administrate'); + }); + + // rename and flip. + Schema::table('users', function (Blueprint $table) { + $table->renameColumn('may_edit_own_settings', 'is_locked'); + }); + // flip the locked value + DB::table('users')->update(['is_locked' => DB::raw('NOT is_locked')]); + if (DB::getDriverName() === 'sqlite') { + Schema::enableForeignKeyConstraints(); + } + } +}; diff --git a/database/migrations/2022_12_07_143755_add_default_protection_option.php b/database/migrations/2022_12_07_143755_add_default_protection_option.php new file mode 100644 index 00000000000..2241abdd29c --- /dev/null +++ b/database/migrations/2022_12_07_143755_add_default_protection_option.php @@ -0,0 +1,25 @@ + 'default_album_protection', + 'value' => '1', + 'confidentiality' => '0', + 'cat' => 'config', + 'type_range' => '1|2|3', + 'description' => 'Default protection for newly created albums. 1 = private, 2 = public, 3 = inherit from parent', + ], + ]; + } +}; diff --git a/database/migrations/2022_12_07_164417_config_uniformize_rights.php b/database/migrations/2022_12_07_164417_config_uniformize_rights.php new file mode 100644 index 00000000000..52d7beaee6f --- /dev/null +++ b/database/migrations/2022_12_07_164417_config_uniformize_rights.php @@ -0,0 +1,30 @@ +where('key', 'full_photo')->update(['key' => 'grants_full_photo_access']); + DB::table('configs')->where('key', 'downloadable')->update(['key' => 'grants_download']); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::table('configs')->where('key', 'grants_full_photo_access')->update(['key' => 'full_photo']); + DB::table('configs')->where('key', 'grants_download')->update(['key' => 'downloadable']); + } +}; diff --git a/database/migrations/2022_12_07_175257_rename_attributes_grants.php b/database/migrations/2022_12_07_175257_rename_attributes_grants.php new file mode 100644 index 00000000000..7d05f21a8aa --- /dev/null +++ b/database/migrations/2022_12_07_175257_rename_attributes_grants.php @@ -0,0 +1,58 @@ +renameColumn('requires_link', 'is_link_required'); + }); + Schema::table('base_albums', function (Blueprint $table) { + $table->renameColumn('is_downloadable', 'grants_download'); + }); + Schema::table('base_albums', function (Blueprint $table) { + $table->renameColumn('grants_full_photo', 'grants_full_photo_access'); + }); + if (DB::getDriverName() === 'sqlite') { + Schema::enableForeignKeyConstraints(); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + if (DB::getDriverName() === 'sqlite') { + Schema::disableForeignKeyConstraints(); + } + Schema::table('base_albums', function (Blueprint $table) { + $table->renameColumn('is_link_required', 'requires_link'); + }); + Schema::table('base_albums', function (Blueprint $table) { + $table->renameColumn('grants_download', 'is_downloadable'); + }); + Schema::table('base_albums', function (Blueprint $table) { + $table->renameColumn('grants_full_photo_access', 'grants_full_photo'); + }); + if (DB::getDriverName() === 'sqlite') { + Schema::enableForeignKeyConstraints(); + } + } +}; diff --git a/database/migrations/2022_12_10_183251_increment_user_i_ds.php b/database/migrations/2022_12_10_183251_increment_user_i_ds.php new file mode 100644 index 00000000000..6065bef3dea --- /dev/null +++ b/database/migrations/2022_12_10_183251_increment_user_i_ds.php @@ -0,0 +1,105 @@ +defer('base_albums', 'base_albums_owner_id_foreign'); + $this->defer('user_base_album', 'user_base_album_user_id_foreign'); + $this->defer('photos', 'photos_owner_id_foreign'); + } + + /** @var App\Models\User|null $admin */ + $admin = DB::table('users')->find(0); + if ($admin !== null && ($admin->username === '' || $admin->password === '')) { + // The admin user (id 0) has never set a username and password, so we remove it. + // This should only happen on a completely new installation where the admin user is created by the + // MigrateAdminUser migration and the user has never logged in. + DB::table('users')->where('id', '=', 0)->delete(); + } + + /** @var App\Models\User $user */ + foreach (DB::table('users')->orderByDesc('id')->get() as $user) { + $oldID = $user->id; + $newID = $oldID + 1; + DB::table('users')->where('id', '=', $oldID)->update(['id' => $newID]); + // update other columns referencing user ID + DB::table('base_albums')->where('owner_id', '=', $oldID)->update(['owner_id' => $newID]); + DB::table('photos')->where('owner_id', '=', $oldID)->update(['owner_id' => $newID]); + DB::table('user_base_album')->where('user_id', '=', $oldID)->update(['user_id' => $newID]); + DB::table('webauthn_credentials')->where('authenticatable_id', '=', $oldID)->update(['authenticatable_id' => $newID]); + DB::table('users')->delete($oldID); + } + + if (DB::getDriverName() === 'pgsql' && DB::table('users')->count() > 0) { + // when using PostgreSQL, the new IDs are not updated after incrementing. Thus, we need to reset the index to the greatest ID + 1 + // the sequence is called `users_id_seq1` + /** @var App\Models\User $lastUser */ + $lastUser = DB::table('users')->orderByDesc('id')->first(); + DB::statement('ALTER SEQUENCE users_id_seq1 RESTART WITH ' . strval($lastUser->id + 1)); + } + DB::commit(); + Schema::enableForeignKeyConstraints(); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::disableForeignKeyConstraints(); + DB::beginTransaction(); + + // In the case of pgsql we mark the following foreign keys to be defered after the commit transaction + // rather than after every requests + if (DB::getDriverName() === 'pgsql') { + $this->defer('base_albums', 'base_albums_owner_id_foreign'); + $this->defer('user_base_album', 'user_base_album_user_id_foreign'); + $this->defer('photos', 'photos_owner_id_foreign'); + } + + /** @var App\Models\User $user */ + foreach (User::query()->orderBy('id')->get() as $user) { + $oldID = $user->id; + $newID = $oldID - 1; + $user->id = $newID; + $user->incrementing = false; + $user->save(); + // update other columns referencing user ID + DB::table('base_albums')->where('owner_id', '=', $oldID)->update(['owner_id' => $newID]); + DB::table('photos')->where('owner_id', '=', $oldID)->update(['owner_id' => $newID]); + DB::table('user_base_album')->where('user_id', '=', $oldID)->update(['user_id' => $newID]); + DB::table('webauthn_credentials')->where('authenticatable_id', '=', $oldID)->update(['authenticatable_id' => $newID]); + DB::table('users')->delete($oldID); + } + DB::commit(); + Schema::enableForeignKeyConstraints(); + } + + /** + * Defer a foreign key evalation to the end of a transaction in pgsql. + */ + private function defer(string $tableName, string $fkName): void + { + DB::select('ALTER TABLE ' . $tableName . ' ALTER CONSTRAINT ' . $fkName . ' DEFERRABLE INITIALLY DEFERRED;'); + } +}; diff --git a/database/migrations/2022_12_12_100000_enable_disable_album_photo_counters.php b/database/migrations/2022_12_12_100000_enable_disable_album_photo_counters.php new file mode 100644 index 00000000000..a8bfae75660 --- /dev/null +++ b/database/migrations/2022_12_12_100000_enable_disable_album_photo_counters.php @@ -0,0 +1,33 @@ + 'album_decoration', + 'value' => 'layers', + 'confidentiality' => '0', + 'cat' => 'Gallery', + 'type_range' => 'none|layers|album|photo|all', + 'description' => 'Show decorations on album cover (sub-album and/or photo count)', + ], + [ + 'key' => 'album_decoration_orientation', + 'value' => 'row', + 'confidentiality' => '0', + 'cat' => 'Gallery', + 'type_range' => 'column|column-reverse|row|row-reverse', + 'description' => 'Align album decorations horizontally or vertically', + ], + ]; + } +}; diff --git a/database/migrations/2022_12_21_212143_use_webp_for_example.php b/database/migrations/2022_12_21_212143_use_webp_for_example.php new file mode 100644 index 00000000000..a0d043ca4c1 --- /dev/null +++ b/database/migrations/2022_12_21_212143_use_webp_for_example.php @@ -0,0 +1,28 @@ +where('key', 'landing_background')->update(['value' => 'dist/cat.webp']); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::table('configs')->where('key', 'landing_background')->update(['value' => 'dist/cat.jpg']); + } +}; diff --git a/database/migrations/2022_12_25_103052_bump_version040604.php b/database/migrations/2022_12_25_103052_bump_version040604.php new file mode 100644 index 00000000000..10d34223edf --- /dev/null +++ b/database/migrations/2022_12_25_103052_bump_version040604.php @@ -0,0 +1,28 @@ +where('key', 'version')->update(['value' => '040604']); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '040603']); + } +}; diff --git a/database/migrations/2022_12_26_101639_allow_username_change.php b/database/migrations/2022_12_26_101639_allow_username_change.php new file mode 100644 index 00000000000..53fcae8f8b2 --- /dev/null +++ b/database/migrations/2022_12_26_101639_allow_username_change.php @@ -0,0 +1,25 @@ + 'allow_username_change', + 'value' => '1', + 'cat' => 'config', + 'type_range' => '0|1', + 'confidentiality' => '0', + 'description' => 'Allow users to change their username.', + ], + ]; + } +}; diff --git a/database/migrations/2022_12_26_111139_bump_version040605.php b/database/migrations/2022_12_26_111139_bump_version040605.php new file mode 100644 index 00000000000..25cc0a3b1e5 --- /dev/null +++ b/database/migrations/2022_12_26_111139_bump_version040605.php @@ -0,0 +1,28 @@ +where('key', 'version')->update(['value' => '040605']); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '040604']); + } +}; diff --git a/database/migrations/2022_12_28_164844_remove-demo.php b/database/migrations/2022_12_28_164844_remove-demo.php new file mode 100644 index 00000000000..7a3b47951fd --- /dev/null +++ b/database/migrations/2022_12_28_164844_remove-demo.php @@ -0,0 +1,35 @@ +where('key', '=', 'gen_demo_js')->delete(); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::table('configs')->insert([ + 'key' => 'gen_demo_js', + 'value' => '0', + 'cat' => 'Admin', + 'type_range' => '0|1', + 'confidentiality' => '3', + 'description' => 'Enable generation of JS responses for demo purposes', + ]); + } +}; diff --git a/database/migrations/2022_12_31_103416_bump_version040700.php b/database/migrations/2022_12_31_103416_bump_version040700.php new file mode 100644 index 00000000000..4e6c461505b --- /dev/null +++ b/database/migrations/2022_12_31_103416_bump_version040700.php @@ -0,0 +1,28 @@ +where('key', 'version')->update(['value' => '040700']); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '040605']); + } +}; diff --git a/database/migrations/2023_01_09_133603_public_on_this_day.php b/database/migrations/2023_01_09_133603_public_on_this_day.php new file mode 100644 index 00000000000..bed4dd03ac5 --- /dev/null +++ b/database/migrations/2023_01_09_133603_public_on_this_day.php @@ -0,0 +1,25 @@ + 'public_on_this_day', + 'value' => '0', + 'cat' => 'Smart Albums', + 'type_range' => self::BOOL, + 'confidentiality' => '0', + 'description' => 'Make "On This Day" smart album accessible to anonymous users', + ], + ]; + } +}; diff --git a/database/migrations/2023_01_25_140614_change-locale.php b/database/migrations/2023_01_25_140614_change-locale.php new file mode 100644 index 00000000000..11df25f2e82 --- /dev/null +++ b/database/migrations/2023_01_25_140614_change-locale.php @@ -0,0 +1,56 @@ +where('value', '=', self::CHINESE_TRADITIONAL) + ->update(['value' => self::CHINESE_TRADITIONAL_CODE]); + + DB::table('configs') + ->where('value', '=', self::CHINESE_SIMPLIFIED) + ->update(['value' => self::CHINESE_SIMPLIFIED_CODE]); + + DB::table('configs') + ->where('value', '=', self::NORWEGIAN) + ->update(['value' => self::NORWEGIAN_CODE]); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::table('configs') + ->where('value', '=', self::CHINESE_TRADITIONAL_CODE) + ->update(['value' => self::CHINESE_TRADITIONAL]); + + DB::table('configs') + ->where('value', '=', self::CHINESE_SIMPLIFIED_CODE) + ->update(['value' => self::CHINESE_SIMPLIFIED]); + + DB::table('configs') + ->where('value', '=', self::NORWEGIAN_CODE) + ->update(['value' => self::NORWEGIAN]); + } +}; diff --git a/database/migrations/2023_02_05_155552_bump_version040701.php b/database/migrations/2023_02_05_155552_bump_version040701.php new file mode 100644 index 00000000000..117a7b8554a --- /dev/null +++ b/database/migrations/2023_02_05_155552_bump_version040701.php @@ -0,0 +1,28 @@ +where('key', 'version')->update(['value' => '040701']); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '040700']); + } +}; diff --git a/database/migrations/2023_02_23_192505_add_auto_fix_orientation_setting.php b/database/migrations/2023_02_23_192505_add_auto_fix_orientation_setting.php new file mode 100644 index 00000000000..4ee5dfa5d9e --- /dev/null +++ b/database/migrations/2023_02_23_192505_add_auto_fix_orientation_setting.php @@ -0,0 +1,25 @@ + 'auto_fix_orientation', + 'value' => '1', + 'cat' => 'Image Processing', + 'type_range' => self::BOOL, + 'confidentiality' => '0', + 'description' => 'Automatically rotate imported images', + ], + ]; + } +}; diff --git a/database/migrations/2023_03_08_103109_bump_version040702.php b/database/migrations/2023_03_08_103109_bump_version040702.php new file mode 100644 index 00000000000..b6e8fe8c21a --- /dev/null +++ b/database/migrations/2023_03_08_103109_bump_version040702.php @@ -0,0 +1,28 @@ +where('key', 'version')->update(['value' => '040702']); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '040701']); + } +}; diff --git a/database/migrations/2023_04_05_150337_create_jobs_table.php b/database/migrations/2023_04_05_150337_create_jobs_table.php new file mode 100644 index 00000000000..8d971a16ddf --- /dev/null +++ b/database/migrations/2023_04_05_150337_create_jobs_table.php @@ -0,0 +1,37 @@ +bigIncrements('id'); + $table->string('queue')->index(); + $table->longText('payload'); + $table->unsignedTinyInteger('attempts'); + $table->unsignedInteger('reserved_at')->nullable(); + $table->unsignedInteger('available_at'); + $table->unsignedInteger('created_at'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('jobs'); + } +}; diff --git a/database/migrations/2023_04_05_150625_queue-processing.php b/database/migrations/2023_04_05_150625_queue-processing.php new file mode 100644 index 00000000000..74768cfceae --- /dev/null +++ b/database/migrations/2023_04_05_150625_queue-processing.php @@ -0,0 +1,25 @@ + 'use_job_queues', + 'value' => '0', + 'cat' => 'Image Processing', + 'type_range' => self::BOOL, + 'confidentiality' => '0', + 'description' => 'Use job queues instead of directly live connection.', + ], + ]; + } +}; diff --git a/database/migrations/2023_04_05_220214_create_failed_jobs_table.php b/database/migrations/2023_04_05_220214_create_failed_jobs_table.php new file mode 100644 index 00000000000..55b9e2d9f4a --- /dev/null +++ b/database/migrations/2023_04_05_220214_create_failed_jobs_table.php @@ -0,0 +1,37 @@ +id(); + $table->string('uuid')->unique(); + $table->text('connection'); + $table->text('queue'); + $table->longText('payload'); + $table->longText('exception'); + $table->timestamp('failed_at')->useCurrent(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('failed_jobs'); + } +}; diff --git a/database/migrations/2023_04_09_142907_create_job_history_table.php b/database/migrations/2023_04_09_142907_create_job_history_table.php new file mode 100644 index 00000000000..5e4b03a37b3 --- /dev/null +++ b/database/migrations/2023_04_09_142907_create_job_history_table.php @@ -0,0 +1,53 @@ +bigIncrements('id'); + $table->unsignedInteger('owner_id'); + $table->string('job', 200); // brief description of the job + $table->char('parent_id', self::RANDOM_ID_LENGTH)->nullable(true); // parentId = album ID + $table->integer('status')->default(0); // 0 - not run, 1 success, 2 failure + + $table->dateTime( + self::CREATED_AT_COL_NAME, + self::DATETIME_PRECISION + )->nullable(); + $table->dateTime( + self::UPDATED_AT_COL_NAME, + self::DATETIME_PRECISION + )->nullable(); + + $table->foreign('owner_id')->references('id')->on('users')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::disableForeignKeyConstraints(); + Schema::dropIfExists('jobs_history'); + Schema::enableForeignKeyConstraints(); + } +}; diff --git a/database/migrations/2023_04_18_065457_bump_version040703.php b/database/migrations/2023_04_18_065457_bump_version040703.php new file mode 100644 index 00000000000..eab1f8f8fbb --- /dev/null +++ b/database/migrations/2023_04_18_065457_bump_version040703.php @@ -0,0 +1,32 @@ +where('key', 'version')->update(['value' => '040703']); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '040702']); + } +}; diff --git a/database/migrations/2023_05_01_165730_add_random_photo_settings.php b/database/migrations/2023_05_01_165730_add_random_photo_settings.php new file mode 100644 index 00000000000..72d592ec073 --- /dev/null +++ b/database/migrations/2023_05_01_165730_add_random_photo_settings.php @@ -0,0 +1,25 @@ + 'random_album_id', + 'value' => 'starred', + 'cat' => 'Mod Frame', + 'type_range' => 'string', + 'confidentiality' => '0', + 'description' => 'Album id to be used by for random function.', + ], + ]; + } +}; diff --git a/database/migrations/2023_05_04_070132_bump_version040704.php b/database/migrations/2023_05_04_070132_bump_version040704.php new file mode 100644 index 00000000000..2d143adf7c7 --- /dev/null +++ b/database/migrations/2023_05_04_070132_bump_version040704.php @@ -0,0 +1,32 @@ +where('key', 'version')->update(['value' => '040704']); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '040703']); + } +}; diff --git a/database/migrations/2023_05_04_193000_add_use_last_modified_date_when_no_exit_date_setting.php b/database/migrations/2023_05_04_193000_add_use_last_modified_date_when_no_exit_date_setting.php new file mode 100644 index 00000000000..362a774f84c --- /dev/null +++ b/database/migrations/2023_05_04_193000_add_use_last_modified_date_when_no_exit_date_setting.php @@ -0,0 +1,25 @@ + 'use_last_modified_date_when_no_exif_date', + 'value' => '0', + 'cat' => 'Image Processing', + 'type_range' => '0|1', + 'confidentiality' => '0', + 'description' => 'Use the file\'s last modified time when Exif data has no creation date', + ], + ]; + } +}; diff --git a/database/migrations/2023_05_05_052254_create_access_permissions.php b/database/migrations/2023_05_05_052254_create_access_permissions.php new file mode 100644 index 00000000000..3b3f6805d71 --- /dev/null +++ b/database/migrations/2023_05_05_052254_create_access_permissions.php @@ -0,0 +1,152 @@ +optimize = new OptimizeTables(); + } + + /** + * Run the migrations. + */ + public function up(): void + { + $this->createAccessPermissionTable(); + DB::transaction(fn () => $this->populateAccessPermissionTable()); + + $this->optimize->exec(); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists(self::TABLE_NAME); + } + + /** + * Generate the table for access_permissions. + */ + private function createAccessPermissionTable(): void + { + // Any old data is not relevant. + Schema::dropIfExists(self::TABLE_NAME); + + Schema::create(self::TABLE_NAME, function (Blueprint $table) { + $table->bigIncrements('id'); + + // User associated with the access capabilities + // If null we consider the album public + $table->unsignedInteger(self::USER_ID)->nullable()->default(null); + + // parentId = album ID + $table->char(self::BASE_ALBUM_ID, self::RANDOM_ID_LENGTH)->nullable(true); + + // basic access rights for anonymous users. + $table->boolean(self::IS_LINK_REQUIRED)->nullable(false)->default(false); + $table->string(self::PASSWORD, 100)->nullable()->default(null); + + // Grants capabilities + $table->boolean(self::GRANTS_FULL_PHOTO_ACCESS)->nullable(false)->default(false); + $table->boolean(self::GRANTS_DOWNLOAD)->nullable(false)->default(false); + $table->boolean(self::GRANTS_UPLOAD)->nullable(false)->default(false); + $table->boolean(self::GRANTS_EDIT)->nullable(false)->default(false); + $table->boolean(self::GRANTS_DELETE)->nullable(false)->default(false); + + $table->dateTime(self::CREATED_AT_COL_NAME, self::DATETIME_PRECISION)->nullable(); + $table->dateTime(self::UPDATED_AT_COL_NAME, self::DATETIME_PRECISION)->nullable(); + + $table->index([self::USER_ID]); // for albums which are own by the currently authenticated user + $table->index([self::BASE_ALBUM_ID]); // for albums which are own by the currently authenticated user + + // It is not possible to make a foreign key between base album and access permission. + // The reason being that `recent` `starred` `on_this_day` are 3 smart albums which do not have an associated album + // For this reason the only foreign key we consider is the one related to User. + $table->foreign(self::USER_ID)->references('id')->on('users')->cascadeOnUpdate()->cascadeOnDelete(); + // This index is required to efficiently filter those albums + // which are shared with a particular user + $table->unique([self::BASE_ALBUM_ID, self::USER_ID]); + }); + } + + private function populateAccessPermissionTable(): void + { + $baseAlbums = DB::table('base_albums')->where('is_public', '=', true)->get(); + foreach ($baseAlbums as $baseAlbum) { + DB::table(self::TABLE_NAME)->insert([ + [ + self::USER_ID => null, + self::BASE_ALBUM_ID => $baseAlbum->id, + self::IS_LINK_REQUIRED => $baseAlbum->is_link_required, + self::PASSWORD => $baseAlbum->password, + self::GRANTS_FULL_PHOTO_ACCESS => $baseAlbum->grants_full_photo_access, + self::GRANTS_DOWNLOAD => $baseAlbum->grants_download, + self::GRANTS_UPLOAD => false, + self::GRANTS_EDIT => false, + self::GRANTS_DELETE => false, + ], + ]); + } + + // Loop over every base album + $currentShares = DB::table('user_base_album') + ->join('base_albums', 'base_albums.id', '=', 'user_base_album.base_album_id') + ->select([ + self::BASE_ALBUM_ID, + self::USER_ID, + self::GRANTS_DOWNLOAD, + self::GRANTS_FULL_PHOTO_ACCESS, + ]) + ->get(); + + foreach ($currentShares as $share) { + DB::table(self::TABLE_NAME)-> + insert([ + [ + self::USER_ID => $share->user_id, + self::BASE_ALBUM_ID => $share->base_album_id, + self::IS_LINK_REQUIRED => false, + self::PASSWORD => null, + self::GRANTS_FULL_PHOTO_ACCESS => $share->grants_full_photo_access, + self::GRANTS_DOWNLOAD => $share->grants_download, + self::GRANTS_UPLOAD => false, + self::GRANTS_EDIT => false, + self::GRANTS_DELETE => false, + ], + ]); + } + } +}; diff --git a/database/migrations/2023_05_05_052255_simplify_user_base_album.php b/database/migrations/2023_05_05_052255_simplify_user_base_album.php new file mode 100644 index 00000000000..b2e815df176 --- /dev/null +++ b/database/migrations/2023_05_05_052255_simplify_user_base_album.php @@ -0,0 +1,77 @@ +optimize = new OptimizeTables(); + } + + /** + * Run the migrations. + */ + public function up(): void + { + Schema::dropIfExists('user_base_album'); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + $this->createUserBaseAlbumTable(); + DB::transaction(fn () => $this->populateUserBaseAlbumTable()); + + $this->optimize->exec(); + } + + private function createUserBaseAlbumTable(): void + { + Schema::create('user_base_album', function (Blueprint $table) { + // Column definitions + $table->bigIncrements('id')->nullable(false); + $table->unsignedInteger(self::USER_ID)->nullable(false); + $table->char(self::BASE_ALBUM_ID, self::RANDOM_ID_LENGTH)->nullable(false); + // Indices and constraint definitions + $table->foreign(self::USER_ID)->references('id')->on('users')->cascadeOnUpdate()->cascadeOnDelete(); + $table->foreign(self::BASE_ALBUM_ID)->references('id')->on('base_albums')->cascadeOnUpdate()->cascadeOnDelete(); + // This index is required to efficiently filter those albums + // which are shared with a particular user + $table->unique([self::BASE_ALBUM_ID, self::USER_ID]); + }); + } + + private function populateUserBaseAlbumTable(): void + { + $shared = DB::table(self::TABLE_NAME)->whereNotNull(self::USER_ID)->get(); + foreach ($shared as $share) { + DB::table('user_base_album')-> + insert([[ + self::USER_ID => $share->user_id, + self::BASE_ALBUM_ID => $share->base_album_id, + ]]); + } + } +}; diff --git a/database/migrations/2023_05_05_052256_simplify_base_album.php b/database/migrations/2023_05_05_052256_simplify_base_album.php new file mode 100644 index 00000000000..08f1d31d6f8 --- /dev/null +++ b/database/migrations/2023_05_05_052256_simplify_base_album.php @@ -0,0 +1,116 @@ +optimize = new OptimizeTables(); + } + + /** + * Run the migrations. + */ + public function up(): void + { + $this->dropColumnsBaseAlbumTable(); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + $this->fixBaseAlbumTable(); + DB::transaction(fn () => $this->populateBaseAlbumTable()); + + $this->optimize->exec(); + } + + private function dropColumnsBaseAlbumTable(): void + { + Schema::disableForeignKeyConstraints(); + Schema::table(self::TABLE_BASE_ALBUMS, function ($table) { + $this->optimize->dropIndexIfExists($table, ['requires_link', self::IS_PUBLIC]); + $this->optimize->dropIndexIfExists($table, [self::IS_PUBLIC, self::PASSWORD]); + $table->dropColumn(self::IS_PUBLIC); + }); + Schema::table(self::TABLE_BASE_ALBUMS, function ($table) { + $table->dropColumn(self::GRANTS_FULL_PHOTO_ACCESS); + }); + Schema::table(self::TABLE_BASE_ALBUMS, function ($table) { + $table->dropColumn(self::GRANTS_DOWNLOAD); + }); + Schema::table(self::TABLE_BASE_ALBUMS, function ($table) { + $table->dropColumn(self::IS_LINK_REQUIRED); + }); + Schema::table(self::TABLE_BASE_ALBUMS, function ($table) { + $table->dropColumn(self::PASSWORD); + }); + Schema::table(self::TABLE_BASE_ALBUMS, function ($table) { + $table->dropColumn(self::IS_SHARE_BUTTON_VISIBLE); + }); + Schema::enableForeignKeyConstraints(); + } + + /** + * Creates the table `base_albums`. + * + * The table `base_albums` contains all columns of the old table + * `albums` which are common to normal albums and tag albums. + */ + private function fixBaseAlbumTable(): void + { + Schema::table(self::TABLE_BASE_ALBUMS, function (Blueprint $table) { + // Column definitions + $table->boolean(self::IS_PUBLIC)->nullable(false)->default(false); + $table->boolean(self::GRANTS_FULL_PHOTO_ACCESS)->nullable(false)->default(true); + $table->boolean(self::IS_LINK_REQUIRED)->nullable(false)->default(false); + $table->boolean(self::GRANTS_DOWNLOAD)->nullable(false)->default(false); + $table->boolean(self::IS_SHARE_BUTTON_VISIBLE)->nullable(false)->default(false); + $table->string(self::PASSWORD, 100)->nullable()->default(null); + // These indices are required for efficient filtering for accessible and/or visible albums + $table->index([self::IS_LINK_REQUIRED, self::IS_PUBLIC]); // for albums which don't require a direct link and are public + $table->index([self::IS_PUBLIC, self::PASSWORD]); // for albums which are public and how no password + }); + } + + private function populateBaseAlbumTable(): void + { + $publics = DB::table(self::TABLE_NAME)->whereNull('user_id')->get(); + foreach ($publics as $public) { + DB::table(self::TABLE_BASE_ALBUMS) + ->where('id', '=', $public->base_album_id) + ->update([ + self::IS_PUBLIC => true, // Duh ! + self::IS_LINK_REQUIRED => $public->is_link_required, + self::PASSWORD => $public->password, + self::GRANTS_FULL_PHOTO_ACCESS => $public->grants_full_photo_access, + self::GRANTS_DOWNLOAD => $public->grants_download, + ]); + } + } +}; diff --git a/database/migrations/2023_05_05_052257_create_access_permissions_for_smart_albums.php b/database/migrations/2023_05_05_052257_create_access_permissions_for_smart_albums.php new file mode 100644 index 00000000000..aca57f22e2b --- /dev/null +++ b/database/migrations/2023_05_05_052257_create_access_permissions_for_smart_albums.php @@ -0,0 +1,98 @@ +where('key', '=', 'grants_full_photo_access')->first('value')?->value === '1'; + $default_download = DB::table('configs')->where('key', '=', 'grants_download')->first('value')?->value === '1'; + + $val = DB::table(self::TABLE_CONFIGS)->where('key', '=', 'public_recent')->first('value')?->value; + if ($val === '1') { + DB::table(self::TABLE_NAME)->insert([[ + self::BASE_ALBUM_ID => 'recent', + self::GRANTS_FULL_PHOTO_ACCESS => $default_full_photo_access, + self::GRANTS_DOWNLOAD => $default_download], + ]); + } + + $val = DB::table(self::TABLE_CONFIGS)->where('key', '=', 'public_starred')->first('value')?->value; + if ($val === '1') { + DB::table(self::TABLE_NAME)->insert([[ + self::BASE_ALBUM_ID => 'starred', + self::GRANTS_FULL_PHOTO_ACCESS => $default_full_photo_access, + self::GRANTS_DOWNLOAD => $default_download], + ]); + } + $val = DB::table(self::TABLE_CONFIGS)->where('key', '=', 'public_on_this_day')->first('value')?->value; + + if ($val === '1') { + DB::table(self::TABLE_NAME)->insert([[ + self::BASE_ALBUM_ID => 'on_this_day', + self::GRANTS_FULL_PHOTO_ACCESS => $default_full_photo_access, + self::GRANTS_DOWNLOAD => $default_download], + ]); + } + + DB::table(self::TABLE_CONFIGS)->whereIn('key', ['public_recent', 'public_starred', 'public_on_this_day'])->delete(); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + $is_public_recent = DB::table(self::TABLE_NAME)->where(self::BASE_ALBUM_ID, '=', 'recent')->first() !== null; + $is_public_starred = DB::table(self::TABLE_NAME)->where(self::BASE_ALBUM_ID, '=', 'starred')->first() !== null; + $is_public_on_this_day = DB::table(self::TABLE_NAME)->where(self::BASE_ALBUM_ID, '=', 'on_this_day')->first() !== null; + + DB::table(self::TABLE_CONFIGS)->insert([ + [ + 'key' => 'public_recent', + 'value' => $is_public_recent ? '1' : '0', + 'cat' => self::SMART_ALBUMS, + 'type_range' => '0|1', + 'confidentiality' => '0', + 'description' => 'Make Recent smart album accessible to anonymous users', + ], + [ + 'key' => 'public_starred', + 'value' => $is_public_starred ? '1' : '0', + 'cat' => self::SMART_ALBUMS, + 'type_range' => '0|1', + 'confidentiality' => '0', + 'description' => 'Make Starred smart album accessible to anonymous users', + ], + [ + 'key' => 'public_on_this_day', + 'value' => $is_public_on_this_day ? '1' : '0', + 'cat' => self::SMART_ALBUMS, + 'type_range' => '0|1', + 'confidentiality' => '0', + 'description' => 'Make "On This Day" smart album accessible to anonymous users', + ], + ]); + + DB::table(self::TABLE_NAME)->whereIn(self::BASE_ALBUM_ID, ['recent', 'starred', 'on_this_day'])->delete(); + } +}; diff --git a/database/migrations/2023_05_05_123230_bump_version040800.php b/database/migrations/2023_05_05_123230_bump_version040800.php new file mode 100644 index 00000000000..8d5a52fe7ce --- /dev/null +++ b/database/migrations/2023_05_05_123230_bump_version040800.php @@ -0,0 +1,32 @@ +where('key', 'version')->update(['value' => '040800']); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '040703']); + } +}; diff --git a/database/migrations/2023_05_15_081406_bump_version040801.php b/database/migrations/2023_05_15_081406_bump_version040801.php new file mode 100644 index 00000000000..e2c1cd88943 --- /dev/null +++ b/database/migrations/2023_05_15_081406_bump_version040801.php @@ -0,0 +1,32 @@ +where('key', 'version')->update(['value' => '040801']); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '040800']); + } +}; diff --git a/database/migrations/2023_05_15_211448_remove_is_public_album_sorting.php b/database/migrations/2023_05_15_211448_remove_is_public_album_sorting.php new file mode 100644 index 00000000000..27d3b5dc2c8 --- /dev/null +++ b/database/migrations/2023_05_15_211448_remove_is_public_album_sorting.php @@ -0,0 +1,29 @@ +where('key', '=', 'sorting_albums_col')->where('value', '=', 'is_public')->update(['value' => 'max_taken_at']); + DB::table('configs')->where('key', '=', 'sorting_albums_col')->update(['type_range' => 'created_at|title|description|max_taken_at|min_taken_at']); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::table('configs')->where('key', '=', 'sorting_albums_col')->update(['type_range' => 'created_at|title|description|is_public|max_taken_at|min_taken_at']); + } +}; diff --git a/database/migrations/2023_05_18_103903_bump_version040900.php b/database/migrations/2023_05_18_103903_bump_version040900.php new file mode 100644 index 00000000000..e78c41f57f9 --- /dev/null +++ b/database/migrations/2023_05_18_103903_bump_version040900.php @@ -0,0 +1,32 @@ +where('key', 'version')->update(['value' => '040900']); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '040801']); + } +}; diff --git a/database/migrations/2023_05_19_131139_bump_version040901.php b/database/migrations/2023_05_19_131139_bump_version040901.php new file mode 100644 index 00000000000..79cd42b00b7 --- /dev/null +++ b/database/migrations/2023_05_19_131139_bump_version040901.php @@ -0,0 +1,32 @@ +where('key', 'version')->update(['value' => '040901']); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '040900']); + } +}; diff --git a/database/migrations/2023_05_21_225616_bump_version040902.php b/database/migrations/2023_05_21_225616_bump_version040902.php new file mode 100644 index 00000000000..6861f38361d --- /dev/null +++ b/database/migrations/2023_05_21_225616_bump_version040902.php @@ -0,0 +1,32 @@ +where('key', 'version')->update(['value' => '040902']); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '040901']); + } +}; diff --git a/database/migrations/2023_06_20_174639_bump_version040903.php b/database/migrations/2023_06_20_174639_bump_version040903.php new file mode 100644 index 00000000000..cbe1c85747b --- /dev/null +++ b/database/migrations/2023_06_20_174639_bump_version040903.php @@ -0,0 +1,32 @@ +where('key', 'version')->update(['value' => '040903']); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '040902']); + } +}; diff --git a/database/migrations/2023_06_24_161541_add_indexes.php b/database/migrations/2023_06_24_161541_add_indexes.php new file mode 100644 index 00000000000..4f41fb33e97 --- /dev/null +++ b/database/migrations/2023_06_24_161541_add_indexes.php @@ -0,0 +1,56 @@ +optimize = new OptimizeTables(); + } + + /** + * Run the migrations. + */ + public function up(): void + { + Schema::table(self::ACCESS_PERMISSIONS, function (Blueprint $table) { + $table->index([self::IS_LINK_REQUIRED]); // for albums which don't require a direct link and are public + $table->index([self::IS_LINK_REQUIRED, self::PASSWORD]); // for albums which are public and how no password + }); + + $this->optimize->exec(); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table(self::ACCESS_PERMISSIONS, function (Blueprint $table) { + $this->optimize->dropIndexIfExists($table, self::ACCESS_PERMISSIONS . '_' . self::IS_LINK_REQUIRED . '_index'); + $this->optimize->dropIndexIfExists($table, self::ACCESS_PERMISSIONS . '_' . self::IS_LINK_REQUIRED . '_' . self::PASSWORD . '_index'); + }); + } +}; diff --git a/database/migrations/2023_06_28_144440_bump_version040904.php b/database/migrations/2023_06_28_144440_bump_version040904.php new file mode 100644 index 00000000000..7ccc900179a --- /dev/null +++ b/database/migrations/2023_06_28_144440_bump_version040904.php @@ -0,0 +1,32 @@ +where('key', 'version')->update(['value' => '040904']); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '040903']); + } +}; diff --git a/database/migrations/2023_07_07_143908_add_ratio_size_variants.php b/database/migrations/2023_07_07_143908_add_ratio_size_variants.php new file mode 100644 index 00000000000..2fd8c925d6d --- /dev/null +++ b/database/migrations/2023_07_07_143908_add_ratio_size_variants.php @@ -0,0 +1,54 @@ +optimize = new OptimizeTables(); + } + + /** + * Run the migrations. + */ + public function up(): void + { + Schema::table('size_variants', function (Blueprint $table) { + // This index is required by \App\Actions\SizeVariant\Delete::do() + // for `SizeVariant::query()` + $table->float('ratio')->after('height')->default(1); + $table->index(['photo_id', 'type', 'ratio']); + }); + + DB::table('size_variants') + ->where('height', '>', 0) + ->update(['ratio' => DB::raw('width / height')]); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('size_variants', function (Blueprint $table) { + $this->optimize->dropIndexIfExists($table, 'size_variants_photo_id_type_ratio_index'); + }); + + Schema::table('size_variants', function (Blueprint $table) { + $table->dropColumn('ratio'); + }); + } +}; diff --git a/database/migrations/2023_07_16_110146_bump_version041000.php b/database/migrations/2023_07_16_110146_bump_version041000.php new file mode 100644 index 00000000000..538644bf4ae --- /dev/null +++ b/database/migrations/2023_07_16_110146_bump_version041000.php @@ -0,0 +1,32 @@ +where('key', 'version')->update(['value' => '041000']); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '040904']); + } +}; diff --git a/database/migrations/2023_08_07_182802_add_config_ffmpeg_path.php b/database/migrations/2023_08_07_182802_add_config_ffmpeg_path.php new file mode 100644 index 00000000000..b37a2730171 --- /dev/null +++ b/database/migrations/2023_08_07_182802_add_config_ffmpeg_path.php @@ -0,0 +1,70 @@ +insert([ + [ + 'key' => 'ffmpeg_path', + 'value' => $path_ffmpeg, + 'confidentiality' => 1, + 'cat' => 'Image Processing', + 'type_range' => 'string', + 'description' => 'Path to the binary of ffmpeg', + ], + [ + 'key' => 'ffprobe_path', + 'value' => $path_ffprobe, + 'confidentiality' => 1, + 'cat' => 'Image Processing', + 'type_range' => 'string', + 'description' => 'Path to the binary of ffprobe', + ], + ]); + } + + /** + * Reverse the migrations. + * + * @throws InvalidArgumentException + */ + public function down(): void + { + DB::table('configs') + ->where('key', '=', 'ffmpeg_path') + ->orWhere('key', '=', 'ffprobe_path') + ->delete(); + } +}; diff --git a/database/migrations/2023_08_11_134652_bump_version041100.php b/database/migrations/2023_08_11_134652_bump_version041100.php new file mode 100644 index 00000000000..57a693a31bd --- /dev/null +++ b/database/migrations/2023_08_11_134652_bump_version041100.php @@ -0,0 +1,32 @@ +where('key', 'version')->update(['value' => '041100']); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '041000']); + } +}; diff --git a/database/migrations/2023_09_03_124836_bump_version041101.php b/database/migrations/2023_09_03_124836_bump_version041101.php new file mode 100644 index 00000000000..e16cbe6c623 --- /dev/null +++ b/database/migrations/2023_09_03_124836_bump_version041101.php @@ -0,0 +1,32 @@ +where('key', 'version')->update(['value' => '041101']); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '041100']); + } +}; diff --git a/database/migrations/2023_09_16_070405_refactor_type_layout.php b/database/migrations/2023_09_16_070405_refactor_type_layout.php new file mode 100644 index 00000000000..842e4f08239 --- /dev/null +++ b/database/migrations/2023_09_16_070405_refactor_type_layout.php @@ -0,0 +1,76 @@ +select('value')->where('key', '=', 'layout')->first()->value; + DB::table('configs')->where('key', '=', 'layout')->delete(); + DB::table('configs')->insert([ + [ + 'key' => 'layout', + 'value' => $this->toEnum($layout), + 'confidentiality' => 0, + 'cat' => 'Gallery', + 'type_range' => self::SQUARE . '|' . self::JUSTIFIED . '|' . self::UNJUSTIFIED, + 'description' => 'Layout for pictures', + ], + ]); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + /** @var string $layout */ + $layout = DB::table('configs')->select('value')->where('key', '=', 'layout')->first()->value; + DB::table('configs')->where('key', '=', 'layout')->delete(); + DB::table('configs')->insert([ + [ + 'key' => 'layout', + 'value' => $this->fromEnum($layout), + 'confidentiality' => 0, + 'cat' => 'Gallery', + 'type_range' => '0|1|2', + 'description' => 'Layout for pictures', + ], + ]); + } + + private function toEnum(int $layout): string + { + return match ($layout) { + 0 => self::SQUARE, + 1 => self::JUSTIFIED, + 2 => self::UNJUSTIFIED, + default => self::JUSTIFIED, + }; + } + + private function fromEnum(string $layout): int + { + return match ($layout) { + self::SQUARE => 0, + self::JUSTIFIED => 1, + self::UNJUSTIFIED => 2, + default => 1, + }; + } +}; diff --git a/database/migrations/2023_09_16_074731_bump_version041200.php b/database/migrations/2023_09_16_074731_bump_version041200.php new file mode 100644 index 00000000000..db31b2dd38a --- /dev/null +++ b/database/migrations/2023_09_16_074731_bump_version041200.php @@ -0,0 +1,32 @@ +where('key', 'version')->update(['value' => '041200']); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '041101']); + } +}; diff --git a/database/migrations/2023_09_16_234050_require_single_key_in_config.php b/database/migrations/2023_09_16_234050_require_single_key_in_config.php new file mode 100644 index 00000000000..655963905e6 --- /dev/null +++ b/database/migrations/2023_09_16_234050_require_single_key_in_config.php @@ -0,0 +1,33 @@ +unique('key'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('configs', function (Blueprint $table) { + $table->dropUnique(['key']); + }); + } +}; diff --git a/database/migrations/2023_09_23_204910_bump_version041300.php b/database/migrations/2023_09_23_204910_bump_version041300.php new file mode 100644 index 00000000000..0809eae3af2 --- /dev/null +++ b/database/migrations/2023_09_23_204910_bump_version041300.php @@ -0,0 +1,32 @@ +where('key', 'version')->update(['value' => '041300']); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '041200']); + } +}; diff --git a/database/migrations/2023_09_24_110932_add_date_display_configurations.php b/database/migrations/2023_09_24_110932_add_date_display_configurations.php new file mode 100644 index 00000000000..af782536a74 --- /dev/null +++ b/database/migrations/2023_09_24_110932_add_date_display_configurations.php @@ -0,0 +1,76 @@ + 'date_format_photo_thumb', + 'value' => 'M j, Y, g:i:s A e', + 'confidentiality' => '0', + 'cat' => self::GALLERY, + 'type_range' => self::STRING_REQ, + 'description' => 'Format the date for the photo thumbs. See https://www.php.net/manual/en/datetime.format.php', + ], + [ + 'key' => 'date_format_photo_overlay', + 'value' => 'M j, Y, g:i:s A e', + 'confidentiality' => '0', + 'cat' => self::GALLERY, + 'type_range' => self::STRING_REQ, + 'description' => 'Format the date for the photo overlay. See https://www.php.net/manual/en/datetime.format.php', + ], + [ + 'key' => 'date_format_sidebar_uploaded', + 'value' => 'M j, Y, g:i:s A e', + 'confidentiality' => '0', + 'cat' => self::GALLERY, + 'type_range' => self::STRING_REQ, + 'description' => 'Format the upload date for the photo sidebar. See https://www.php.net/manual/en/datetime.format.php', + ], + [ + 'key' => 'date_format_sidebar_taken_at', + 'value' => 'M j, Y, g:i:s A e', + 'confidentiality' => '0', + 'cat' => self::GALLERY, + 'type_range' => self::STRING_REQ, + 'description' => 'Format the capture date for the photo sidebar. See https://www.php.net/manual/en/datetime.format.php', + ], + [ + 'key' => 'date_format_hero_min_max', + 'value' => 'F Y', + 'confidentiality' => '0', + 'cat' => self::GALLERY, + 'type_range' => self::STRING_REQ, + 'description' => 'Format the date for the album hero. See https://www.php.net/manual/en/datetime.format.php', + ], + [ + 'key' => 'date_format_hero_created_at', + 'value' => 'M j, Y, g:i:s A T', + 'confidentiality' => '0', + 'cat' => self::GALLERY, + 'type_range' => self::STRING_REQ, + 'description' => 'Format the created date for the album details. See https://www.php.net/manual/en/datetime.format.php', + ], + [ + 'key' => 'date_format_album_thumb', + 'value' => 'M Y', + 'confidentiality' => '0', + 'cat' => self::GALLERY, + 'type_range' => self::STRING_REQ, + 'description' => 'Format the date for the album thumbs. See https://www.php.net/manual/en/datetime.format.php', + ], + ]; + } +}; diff --git a/database/migrations/2023_09_24_223901_add_config_livewire_chunk_size.php b/database/migrations/2023_09_24_223901_add_config_livewire_chunk_size.php new file mode 100644 index 00000000000..2c8d5e33299 --- /dev/null +++ b/database/migrations/2023_09_24_223901_add_config_livewire_chunk_size.php @@ -0,0 +1,28 @@ + 'upload_chunk_size', + 'value' => '0', + 'confidentiality' => '0', + 'cat' => self::IMAGE_PROCESSING, + 'type_range' => self::INT, + 'description' => 'Size of chunks when uploading in bytes: 0 is auto', + ], + ]; + } +}; diff --git a/database/migrations/2023_09_24_233717_refactor_type_layout_livewire.php b/database/migrations/2023_09_24_233717_refactor_type_layout_livewire.php new file mode 100644 index 00000000000..d63ea1e3b18 --- /dev/null +++ b/database/migrations/2023_09_24_233717_refactor_type_layout_livewire.php @@ -0,0 +1,42 @@ +where('key', '=', 'layout')->update([ + 'type_range' => self::SQUARE . '|' . self::JUSTIFIED . '|' . self::MASONRY . '|' . self::GRID, + 'description' => 'Layout for pictures', + ], + ); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::table('configs')->where('key', '=', 'layout')->update([ + 'type_range' => self::SQUARE . '|' . self::JUSTIFIED . '|' . self::UNJUSTIFIED, + 'description' => 'Layout for pictures', + ], + ); + } +}; diff --git a/database/migrations/2023_09_25_123925_config_blur_nsfw.php b/database/migrations/2023_09_25_123925_config_blur_nsfw.php new file mode 100644 index 00000000000..cc78aa7fbfd --- /dev/null +++ b/database/migrations/2023_09_25_123925_config_blur_nsfw.php @@ -0,0 +1,27 @@ + 'nsfw_banner_blur_backdrop', + 'value' => '0', + 'confidentiality' => '0', + 'cat' => self::MOD_NSFW, + 'type_range' => self::BOOL, + 'description' => 'Blur background instead of dark red opaque.', + ], + ]; + } +}; diff --git a/database/migrations/2023_10_01_143159_config_map_provider.php b/database/migrations/2023_10_01_143159_config_map_provider.php new file mode 100644 index 00000000000..0efc669dc9f --- /dev/null +++ b/database/migrations/2023_10_01_143159_config_map_provider.php @@ -0,0 +1,28 @@ +where('key', '=', 'map_provider')->update(['type_range' => 'map_provider']); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::table('configs')->where('key', '=', 'map_provider')->update(['type_range' => 'Wikimedia|OpenStreetMap.org|OpenStreetMap.de|OpenStreetMap.fr|RRZE']); + } +}; diff --git a/database/migrations/2023_12_18_191723_config_public_search.php b/database/migrations/2023_12_18_191723_config_public_search.php new file mode 100644 index 00000000000..5fd72af967b --- /dev/null +++ b/database/migrations/2023_12_18_191723_config_public_search.php @@ -0,0 +1,44 @@ +where('key', 'public_search')->update( + ['key' => 'search_public', 'cat' => self::MOD_SEARCH]); + DB::table('configs')->where('key', 'public_photos_hidden')->delete(); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::table('configs')->where('key', 'search_public')->update( + ['key' => 'public_search', 'cat' => 'config']); + DB::table('configs')->insert([ + [ + 'key' => 'public_photos_hidden', + 'value' => '1', + 'confidentiality' => 0, + 'cat' => 'config', + 'type_range' => self::BOOL, + 'description' => 'Keep singular public pictures hidden from search results, smart albums & tag albums', + ], + ]); + } +}; diff --git a/database/migrations/2023_12_18_232500_config_pagination_search_limit.php b/database/migrations/2023_12_18_232500_config_pagination_search_limit.php new file mode 100644 index 00000000000..131f482514b --- /dev/null +++ b/database/migrations/2023_12_18_232500_config_pagination_search_limit.php @@ -0,0 +1,27 @@ + 'search_pagination_limit', + 'value' => '1000', + 'confidentiality' => '0', + 'cat' => self::MOD_SEARCH, + 'type_range' => self::POSITIVE, + 'description' => 'Number of results to display per page.', + ], + ]; + } +}; diff --git a/database/migrations/2023_12_19_115547_search_characters_limit.php b/database/migrations/2023_12_19_115547_search_characters_limit.php new file mode 100644 index 00000000000..7f164197098 --- /dev/null +++ b/database/migrations/2023_12_19_115547_search_characters_limit.php @@ -0,0 +1,27 @@ + 'search_minimum_length_required', + 'value' => '4', + 'confidentiality' => '0', + 'cat' => self::MOD_SEARCH, + 'type_range' => self::POSITIVE, + 'description' => 'Number of characters required to trigger search (default: 4).', + ], + ]; + } +}; diff --git a/database/migrations/2023_12_19_122408_add_positive_requirements.php b/database/migrations/2023_12_19_122408_add_positive_requirements.php new file mode 100644 index 00000000000..bd031978edf --- /dev/null +++ b/database/migrations/2023_12_19_122408_add_positive_requirements.php @@ -0,0 +1,44 @@ +whereIn('key', self::KEYS)->update(['type_range' => 'positive']); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::table('configs')->whereIn('key', self::KEYS)->update(['type_range' => 'int']); + } +}; diff --git a/database/migrations/2023_12_20_180854_add_setting_height_width_gallery.php b/database/migrations/2023_12_20_180854_add_setting_height_width_gallery.php new file mode 100644 index 00000000000..4735b64c457 --- /dev/null +++ b/database/migrations/2023_12_20_180854_add_setting_height_width_gallery.php @@ -0,0 +1,62 @@ + 'photo_layout_justified_row_height', + 'value' => '320', + 'confidentiality' => '0', + 'cat' => self::GALLERY, + 'type_range' => self::POSITIVE, + 'description' => 'Heights of rows in Justified photo layout', + ], + [ + // md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-7 + 'key' => 'photo_layout_masonry_column_width', + 'value' => '300', + 'confidentiality' => '0', + 'cat' => self::GALLERY, + 'type_range' => self::POSITIVE, + 'description' => 'Minimum column width in Masonry photo layout.', + ], + [ + // md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-7 + 'key' => 'photo_layout_grid_column_width', + 'value' => '250', + 'confidentiality' => '0', + 'cat' => self::GALLERY, + 'type_range' => self::POSITIVE, + 'description' => 'Minimum column width in Grid photo layout.', + ], + [ + // md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-7 + 'key' => 'photo_layout_square_column_width', + 'value' => '200', + 'confidentiality' => '0', + 'cat' => self::GALLERY, + 'type_range' => self::POSITIVE, + 'description' => 'Minimum column width in Square photo layout.', + ], + [ + 'key' => 'photo_layout_gap', + 'value' => '12', + 'confidentiality' => '0', + 'cat' => self::GALLERY, + 'type_range' => self::POSITIVE, + 'description' => 'Gap between columns in Square/Masonry/Grid photo layout.', + ], + ]; + } +}; diff --git a/database/migrations/2023_12_23_160356_bump_version050000.php b/database/migrations/2023_12_23_160356_bump_version050000.php new file mode 100644 index 00000000000..174c58432f6 --- /dev/null +++ b/database/migrations/2023_12_23_160356_bump_version050000.php @@ -0,0 +1,30 @@ +where('key', 'version')->update(['value' => '050000']); + DB::table('configs')->where('value', '=', 'Lychee v4')->update(['value' => 'Lychee v5']); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '041300']); + DB::table('configs')->where('value', '=', 'Lychee v5')->update(['value' => 'Lychee v4']); + } +}; diff --git a/database/migrations/2023_12_25_115454_add_setting_display_thumb_overlay.php b/database/migrations/2023_12_25_115454_add_setting_display_thumb_overlay.php new file mode 100644 index 00000000000..3743ac5a876 --- /dev/null +++ b/database/migrations/2023_12_25_115454_add_setting_display_thumb_overlay.php @@ -0,0 +1,35 @@ + 'display_thumb_album_overlay', + 'value' => 'always', + 'confidentiality' => '0', + 'cat' => self::GALLERY, + 'type_range' => 'always|hover|never', + 'description' => 'Display the title and metadata on album thumbs (always|hover|never)', + ], + [ + 'key' => 'display_thumb_photo_overlay', + 'value' => 'hover', + 'confidentiality' => '0', + 'cat' => self::GALLERY, + 'type_range' => 'always|hover|never', + 'description' => 'Display the title and metadata on album thumbs (always|hover|never)', + ], + ]; + } +}; diff --git a/database/migrations/2023_12_27_163004_bump_version050001.php b/database/migrations/2023_12_27_163004_bump_version050001.php new file mode 100644 index 00000000000..c1681ea823e --- /dev/null +++ b/database/migrations/2023_12_27_163004_bump_version050001.php @@ -0,0 +1,32 @@ +where('key', 'version')->update(['value' => '050001']); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '050000']); + } +}; diff --git a/database/migrations/2023_12_28_144906_bump_version050002.php b/database/migrations/2023_12_28_144906_bump_version050002.php new file mode 100644 index 00000000000..d59f422a7c7 --- /dev/null +++ b/database/migrations/2023_12_28_144906_bump_version050002.php @@ -0,0 +1,32 @@ +where('key', 'version')->update(['value' => '050002']); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '050001']); + } +}; diff --git a/database/migrations/2023_12_28_165358_add_subalbum_sorting_per_album.php b/database/migrations/2023_12_28_165358_add_subalbum_sorting_per_album.php new file mode 100644 index 00000000000..ab40c1004fa --- /dev/null +++ b/database/migrations/2023_12_28_165358_add_subalbum_sorting_per_album.php @@ -0,0 +1,43 @@ +string(self::SORT_COLUMN_NAME, 30)->nullable()->default(null)->after('license'); + }); + Schema::table(self::ALBUM, function ($table) { + $table->string(self::SORT_COLUMN_ORDER, 10)->nullable()->default(null)->after(self::SORT_COLUMN_NAME); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table(self::ALBUM, function (Blueprint $table) { + $table->dropColumn(self::SORT_COLUMN_NAME); + }); + Schema::table(self::ALBUM, function (Blueprint $table) { + $table->dropColumn(self::SORT_COLUMN_ORDER); + }); + } +}; diff --git a/database/migrations/2023_12_30_220515_add_thumbs_albums_aspect_ratio.php b/database/migrations/2023_12_30_220515_add_thumbs_albums_aspect_ratio.php new file mode 100644 index 00000000000..cfbd6779c56 --- /dev/null +++ b/database/migrations/2023_12_30_220515_add_thumbs_albums_aspect_ratio.php @@ -0,0 +1,27 @@ + 'default_album_thumb_aspect_ratio', + 'value' => '1/1', + 'confidentiality' => '0', + 'cat' => self::GALLERY, + 'type_range' => '1/1|2/3|3/2|4/5|5/4|16/9', + 'description' => 'Default aspect ratio for album thumbs, one of: 1/1, 2/3, 3/2, 4/5, 5/4, 16/9', + ], + ]; + } +}; diff --git a/database/migrations/2023_12_30_221215_add_thumbs_albums_aspect_ratio_per_album.php b/database/migrations/2023_12_30_221215_add_thumbs_albums_aspect_ratio_per_album.php new file mode 100644 index 00000000000..261358d5f41 --- /dev/null +++ b/database/migrations/2023_12_30_221215_add_thumbs_albums_aspect_ratio_per_album.php @@ -0,0 +1,36 @@ +string(self::ASPECT_RATIO_COLUMN_NAME, 6)->nullable()->default(null)->after('license'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table(self::ALBUM, function (Blueprint $table) { + $table->dropColumn(self::ASPECT_RATIO_COLUMN_NAME); + }); + } +}; diff --git a/database/migrations/2024_01_03_154055_add_album_no_header_setting.php b/database/migrations/2024_01_03_154055_add_album_no_header_setting.php new file mode 100644 index 00000000000..460636f8d93 --- /dev/null +++ b/database/migrations/2024_01_03_154055_add_album_no_header_setting.php @@ -0,0 +1,27 @@ + 'use_album_compact_header', + 'value' => '0', + 'confidentiality' => '0', + 'cat' => self::GALLERY, + 'type_range' => self::BOOL, + 'description' => 'Disable the header image in albums (0|1)', + ], + ]; + } +}; diff --git a/database/migrations/2024_01_08_155917_bump_version050003.php b/database/migrations/2024_01_08_155917_bump_version050003.php new file mode 100644 index 00000000000..a61293c7cb7 --- /dev/null +++ b/database/migrations/2024_01_08_155917_bump_version050003.php @@ -0,0 +1,32 @@ +where('key', 'version')->update(['value' => '050003']); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '050002']); + } +}; diff --git a/database/migrations/2024_01_08_163328_left_right_login_and_back.php b/database/migrations/2024_01_08_163328_left_right_login_and_back.php new file mode 100644 index 00000000000..4484ec10e27 --- /dev/null +++ b/database/migrations/2024_01_08_163328_left_right_login_and_back.php @@ -0,0 +1,54 @@ + 'login_button_position', + 'value' => 'left', + 'confidentiality' => '0', + 'cat' => self::CONFIG, + 'type_range' => self::ENUM, + 'description' => 'Position of the login button (left | right)', + ], + [ + 'key' => 'back_button_enabled', + 'value' => '0', + 'confidentiality' => '0', + 'cat' => self::BACK_BUTTON, + 'type_range' => self::BOOL, + 'description' => 'Enable/disable back button on gallery (0 | 1)', + ], + [ + 'key' => 'back_button_text', + 'value' => 'Return to Home', + 'confidentiality' => '0', + 'cat' => self::BACK_BUTTON, + 'type_range' => self::STRING, + 'description' => 'Text of the back button (will be positioned opposite to Login)', + ], + [ + 'key' => 'back_button_url', + 'value' => '/', + 'confidentiality' => '0', + 'cat' => self::BACK_BUTTON, + 'type_range' => self::STRING, + 'description' => 'Link of the back button', + ], + ]; + } +}; diff --git a/database/migrations/2024_01_13_124937_create_oauth_credentials_table.php b/database/migrations/2024_01_13_124937_create_oauth_credentials_table.php new file mode 100644 index 00000000000..7f542844782 --- /dev/null +++ b/database/migrations/2024_01_13_124937_create_oauth_credentials_table.php @@ -0,0 +1,61 @@ +bigIncrements('id'); + + // User associated with the access capabilities + // If null we consider the album public + $table->unsignedInteger(self::USER_ID)->nullable()->default(null); + + $table->string(self::PROVIDER, 20); + $table->string(self::TOKEN_ID); + + $table->dateTime(self::CREATED_AT_COL_NAME, self::DATETIME_PRECISION)->nullable(); + $table->dateTime(self::UPDATED_AT_COL_NAME, self::DATETIME_PRECISION)->nullable(); + + $table->index([self::USER_ID]); // for credentials which are own by the currently authenticated user + $table->index([self::TOKEN_ID]); + $table->unique([self::TOKEN_ID]); + $table->index([self::TOKEN_ID, self::PROVIDER]); + $table->unique([self::PROVIDER, self::USER_ID]); + $table->foreign(self::USER_ID)->references('id')->on('users')->cascadeOnUpdate()->cascadeOnDelete(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists(self::TABLE_NAME); + } +}; diff --git a/database/migrations/2024_01_17_101240_bump_version050100.php b/database/migrations/2024_01_17_101240_bump_version050100.php new file mode 100644 index 00000000000..08599342733 --- /dev/null +++ b/database/migrations/2024_01_17_101240_bump_version050100.php @@ -0,0 +1,32 @@ +where('key', 'version')->update(['value' => '050100']); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '050004']); + } +}; diff --git a/database/migrations/2024_01_22_121406_bump_version050101.php b/database/migrations/2024_01_22_121406_bump_version050101.php new file mode 100644 index 00000000000..e001f5d9164 --- /dev/null +++ b/database/migrations/2024_01_22_121406_bump_version050101.php @@ -0,0 +1,32 @@ +where('key', 'version')->update(['value' => '050101']); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '050100']); + } +}; diff --git a/database/migrations/2024_01_23_190637_bump_version050102.php b/database/migrations/2024_01_23_190637_bump_version050102.php new file mode 100644 index 00000000000..e0c1f63c62e --- /dev/null +++ b/database/migrations/2024_01_23_190637_bump_version050102.php @@ -0,0 +1,32 @@ +where('key', 'version')->update(['value' => '050102']); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '050101']); + } +}; diff --git a/database/migrations/2024_01_23_192800_remove_is_public_photo_sorting.php b/database/migrations/2024_01_23_192800_remove_is_public_photo_sorting.php new file mode 100644 index 00000000000..71b8b0acfe2 --- /dev/null +++ b/database/migrations/2024_01_23_192800_remove_is_public_photo_sorting.php @@ -0,0 +1,29 @@ +where('key', '=', 'sorting_photos_col')->where('value', '=', 'is_public')->update(['value' => 'taken_at']); + DB::table('configs')->where('key', '=', 'sorting_photos_col')->update(['type_range' => 'created_at|taken_at|title|description|is_starred|type']); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::table('configs')->where('key', '=', 'sorting_photos_col')->update(['type_range' => 'created_at|taken_at|title|description|is_public|is_starred|type']); + } +}; diff --git a/database/migrations/2024_01_23_192814_remove_keys_and_column.php b/database/migrations/2024_01_23_192814_remove_keys_and_column.php new file mode 100644 index 00000000000..790ac978757 --- /dev/null +++ b/database/migrations/2024_01_23_192814_remove_keys_and_column.php @@ -0,0 +1,55 @@ +optimize = new OptimizeTables(); + } + + /** + * Run the migrations. + */ + public function up(): void + { + Schema::table(self::TABLE, function (Blueprint $table) { + $this->optimize->dropIndexIfExists($table, 'photos_album_id_is_public_index'); + $this->optimize->dropIndexIfExists($table, 'photos_album_id_is_starred_is_public_index'); + }); + Schema::disableForeignKeyConstraints(); + Schema::table(self::TABLE, function (Blueprint $table) { + $table->dropColumn(self::COLUMN); + }); + Schema::enableForeignKeyConstraints(); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table(self::TABLE, function ($table) { + $table->boolean(self::COLUMN)->nullable(false)->default(false); + }); + Schema::table(self::TABLE, function (Blueprint $table) { + $table->index(['album_id', self::COLUMN]); + $table->index(['album_id', 'is_starred', self::COLUMN]); + }); + } +}; diff --git a/database/migrations/2024_01_23_232103_remove_album_id_from_jobhistory.php b/database/migrations/2024_01_23_232103_remove_album_id_from_jobhistory.php new file mode 100644 index 00000000000..529af0710ee --- /dev/null +++ b/database/migrations/2024_01_23_232103_remove_album_id_from_jobhistory.php @@ -0,0 +1,39 @@ +dropColumn(self::COLUMN); + }); + Schema::enableForeignKeyConstraints(); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table(self::TABLE, function (Blueprint $table) { + $table->char(self::COLUMN, self::RANDOM_ID_LENGTH)->nullable(true); // parentId = album ID + }); + } +}; diff --git a/database/migrations/2024_01_24_063519_job_feedback_options.php b/database/migrations/2024_01_24_063519_job_feedback_options.php new file mode 100644 index 00000000000..3997539f294 --- /dev/null +++ b/database/migrations/2024_01_24_063519_job_feedback_options.php @@ -0,0 +1,27 @@ + 'current_job_processing_visible', + 'value' => '1', + 'confidentiality' => '0', + 'cat' => self::PROCESSING, + 'type_range' => self::BOOL, + 'description' => 'Make the processing job queue visible by default (0|1).', + ], + ]; + } +}; diff --git a/database/migrations/2024_02_28_004535_add_header_id_col.php b/database/migrations/2024_02_28_004535_add_header_id_col.php new file mode 100644 index 00000000000..ffed759ea66 --- /dev/null +++ b/database/migrations/2024_02_28_004535_add_header_id_col.php @@ -0,0 +1,35 @@ +char('header_id', self::RANDOM_ID_LENGTH)->after('cover_id')->nullable()->default(null); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('albums', function (Blueprint $table) { + $table->dropColumn('header_id'); + }); + } +}; diff --git a/database/migrations/2024_04_06_165355_bump_version050200.php b/database/migrations/2024_04_06_165355_bump_version050200.php new file mode 100644 index 00000000000..153403b6dbe --- /dev/null +++ b/database/migrations/2024_04_06_165355_bump_version050200.php @@ -0,0 +1,32 @@ +where('key', 'version')->update(['value' => '050200']); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '050102']); + } +}; diff --git a/database/migrations/2024_04_09_121410_decrease_noise_diagnostics.php b/database/migrations/2024_04_09_121410_decrease_noise_diagnostics.php new file mode 100644 index 00000000000..66529d36f18 --- /dev/null +++ b/database/migrations/2024_04_09_121410_decrease_noise_diagnostics.php @@ -0,0 +1,211 @@ +boolean(self::NEW_COL_NAME)->after(self::OLD_COL_NAME)->default(false); + }); + + Schema::table(self::TABLE_NAME, function (Blueprint $table) { + $table->dropColumn(self::OLD_COL_NAME); + }); + + DB::table(self::TABLE_NAME)->whereIn('key', self::IS_SECRET)->update([self::NEW_COL_NAME => true]); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table(self::TABLE_NAME, function (Blueprint $table) { + $table->tinyInteger(self::OLD_COL_NAME)->after(self::NEW_COL_NAME)->default(0); + }); + + Schema::table(self::TABLE_NAME, function (Blueprint $table) { + $table->dropColumn(self::NEW_COL_NAME); + }); + + DB::table(self::TABLE_NAME)->whereIn('key', self::CONF_0)->update([self::OLD_COL_NAME => 0]); + DB::table(self::TABLE_NAME)->whereIn('key', self::CONF_1)->update([self::OLD_COL_NAME => 1]); + DB::table(self::TABLE_NAME)->whereIn('key', self::CONF_2)->update([self::OLD_COL_NAME => 2]); + DB::table(self::TABLE_NAME)->whereIn('key', self::CONF_3)->update([self::OLD_COL_NAME => 3]); + } +}; diff --git a/database/migrations/2024_04_14_103639_bump_version050201.php b/database/migrations/2024_04_14_103639_bump_version050201.php new file mode 100644 index 00000000000..b4e46a1db07 --- /dev/null +++ b/database/migrations/2024_04_14_103639_bump_version050201.php @@ -0,0 +1,32 @@ +where('key', 'version')->update(['value' => '050201']); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '050200']); + } +}; diff --git a/database/migrations/2024_04_19_141432_fix_license.php b/database/migrations/2024_04_19_141432_fix_license.php new file mode 100644 index 00000000000..890f3da6736 --- /dev/null +++ b/database/migrations/2024_04_19_141432_fix_license.php @@ -0,0 +1,29 @@ +where('license', '=', 'CC-BY')->update(['license' => 'CC-BY-4.0']); + DB::table('albums')->where('license', '=', 'CC-BY-ND')->update(['license' => 'CC-BY-ND-4.0']); + DB::table('albums')->where('license', '=', 'CC-BY-NC-ND')->update(['license' => 'CC-BY-NC-ND-4.0']); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + } +}; diff --git a/database/migrations/2024_04_20_132955_bump_version050202.php b/database/migrations/2024_04_20_132955_bump_version050202.php new file mode 100644 index 00000000000..ffd7752193c --- /dev/null +++ b/database/migrations/2024_04_20_132955_bump_version050202.php @@ -0,0 +1,32 @@ +where('key', 'version')->update(['value' => '050202']); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '050201']); + } +}; diff --git a/database/migrations/2024_04_26_201931_add_storate_disk_to_size_variants.php b/database/migrations/2024_04_26_201931_add_storate_disk_to_size_variants.php new file mode 100644 index 00000000000..b72a6bd1aae --- /dev/null +++ b/database/migrations/2024_04_26_201931_add_storate_disk_to_size_variants.php @@ -0,0 +1,33 @@ +string(self::COLUMN)->default(self::DEFAULT); + }); + } + } + + public function down(): void + { + Schema::table(self::TABLE_SV, function (Blueprint $table) { + $table->dropColumn(self::COLUMN); + }); + } +}; diff --git a/database/migrations/2024_04_28_135546_fix_license_again.php b/database/migrations/2024_04_28_135546_fix_license_again.php new file mode 100644 index 00000000000..b53d3d04bce --- /dev/null +++ b/database/migrations/2024_04_28_135546_fix_license_again.php @@ -0,0 +1,28 @@ +where('license', '=', 'CC-BY-SA')->update(['license' => 'CC-BY-SA-4.0']); + DB::table('albums')->where('license', '=', 'CC-BY-NC-SA')->update(['license' => 'CC-BY-NC-SA-4.0']); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + } +}; diff --git a/database/migrations/2024_04_28_172241_add_album_copyright.php b/database/migrations/2024_04_28_172241_add_album_copyright.php new file mode 100644 index 00000000000..e4e08d592c3 --- /dev/null +++ b/database/migrations/2024_04_28_172241_add_album_copyright.php @@ -0,0 +1,36 @@ +string(self::COLUMN, 300)->nullable()->default(null); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table(self::TABLE, function (Blueprint $table) { + $table->dropColumn(self::COLUMN); + }); + } +}; diff --git a/database/migrations/2024_04_28_191004_bump_version050300.php b/database/migrations/2024_04_28_191004_bump_version050300.php new file mode 100644 index 00000000000..62d7fe8ce08 --- /dev/null +++ b/database/migrations/2024_04_28_191004_bump_version050300.php @@ -0,0 +1,32 @@ +where('key', 'version')->update(['value' => '050300']); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '050202']); + } +}; diff --git a/database/migrations/2024_05_13_175529_config_random_thumb_smart_album.php b/database/migrations/2024_05_13_175529_config_random_thumb_smart_album.php new file mode 100644 index 00000000000..4b4cd24de25 --- /dev/null +++ b/database/migrations/2024_05_13_175529_config_random_thumb_smart_album.php @@ -0,0 +1,27 @@ + 'SA_random_thumbs', + 'value' => '0', + 'is_secret' => false, + 'cat' => self::PROCESSING, + 'type_range' => self::BOOL, + 'description' => 'Use random thumbs instead of stared/sorting order.', + ], + ]; + } +}; diff --git a/database/migrations/2024_06_08_093403_primary_key_job_history.php b/database/migrations/2024_06_08_093403_primary_key_job_history.php new file mode 100644 index 00000000000..d1dfd7f0633 --- /dev/null +++ b/database/migrations/2024_06_08_093403_primary_key_job_history.php @@ -0,0 +1,31 @@ +index(['owner_id', 'status']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // We cannot remove index key because owner_id is used for FK constraint. + } +}; diff --git a/database/migrations/2024_06_08_103842_add_config_display_processing_queue.php b/database/migrations/2024_06_08_103842_add_config_display_processing_queue.php new file mode 100644 index 00000000000..1f7c7378786 --- /dev/null +++ b/database/migrations/2024_06_08_103842_add_config_display_processing_queue.php @@ -0,0 +1,27 @@ + 'job_processing_queue_visible', + 'value' => '1', + 'is_secret' => false, + 'cat' => self::PROCESSING, + 'type_range' => self::BOOL, + 'description' => 'Enable the processing queue in the bottom left corner.', + ], + ]; + } +}; diff --git a/database/migrations/2024_06_08_183427_bump_version050301.php b/database/migrations/2024_06_08_183427_bump_version050301.php new file mode 100644 index 00000000000..e5ba80e07fb --- /dev/null +++ b/database/migrations/2024_06_08_183427_bump_version050301.php @@ -0,0 +1,32 @@ +where('key', 'version')->update(['value' => '050301']); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '050300']); + } +}; diff --git a/database/migrations/2024_06_10_103843_add_login_required_option.php b/database/migrations/2024_06_10_103843_add_login_required_option.php new file mode 100644 index 00000000000..cd1209b3842 --- /dev/null +++ b/database/migrations/2024_06_10_103843_add_login_required_option.php @@ -0,0 +1,27 @@ + 'login_required', + 'value' => '0', + 'is_secret' => false, + 'cat' => self::GALLERY, + 'type_range' => self::BOOL, + 'description' => 'Require user to login to access gallery.', + ], + ]; + } +}; diff --git a/database/migrations/2024_06_11_204410_bump_version050400.php b/database/migrations/2024_06_11_204410_bump_version050400.php new file mode 100644 index 00000000000..54ea4240a93 --- /dev/null +++ b/database/migrations/2024_06_11_204410_bump_version050400.php @@ -0,0 +1,32 @@ +where('key', 'version')->update(['value' => '050400']); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '050301']); + } +}; diff --git a/database/migrations/2024_06_17_171051_disable_per_smart_album_config.php b/database/migrations/2024_06_17_171051_disable_per_smart_album_config.php new file mode 100644 index 00000000000..bc2d09ffea2 --- /dev/null +++ b/database/migrations/2024_06_17_171051_disable_per_smart_album_config.php @@ -0,0 +1,51 @@ + 'enable_unsorted', + 'value' => '1', + 'is_secret' => false, + 'cat' => self::SMART_ALBUMS, + 'type_range' => self::BOOL, + 'description' => 'Enable Unsorted smart album. Warning! Disabling this will make pictures without an album invisible.', + ], + [ + 'key' => 'enable_starred', + 'value' => '1', + 'is_secret' => false, + 'cat' => self::SMART_ALBUMS, + 'type_range' => self::BOOL, + 'description' => 'Enable Starred smart album.', + ], + [ + 'key' => 'enable_recent', + 'value' => '1', + 'is_secret' => false, + 'cat' => self::SMART_ALBUMS, + 'type_range' => self::BOOL, + 'description' => 'Enable Recent uploads smart album.', + ], + [ + 'key' => 'enable_on_this_day', + 'value' => '1', + 'is_secret' => false, + 'cat' => self::SMART_ALBUMS, + 'type_range' => self::BOOL, + 'description' => 'Enable On this day smart album.', + ], + ]; + } +}; diff --git a/database/migrations/2024_06_17_172448_remove_global_disable_smart_albums.php b/database/migrations/2024_06_17_172448_remove_global_disable_smart_albums.php new file mode 100644 index 00000000000..dd3c1d00f2e --- /dev/null +++ b/database/migrations/2024_06_17_172448_remove_global_disable_smart_albums.php @@ -0,0 +1,52 @@ +select('value')->where('key', '=', self::SA)->first()->value; + DB::table(self::TABLE)->whereIn('key', [ + self::UNSORTED, + self::STARRED, + self::RECENT, + self::ON_THIS_DAY, ])->update(['value' => $val]); + DB::table(self::TABLE)->where('key', '=', self::SA)->delete(); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::table(self::TABLE)->insert([ + [ + 'key' => 'SA_enabled', + 'value' => '1', + 'is_secret' => false, + 'cat' => self::SMART_ALBUMS, + 'type_range' => self::BOOL, + 'description' => 'Enable Smart Albums.', + ], + ]); + } +}; diff --git a/database/migrations/2024_06_21_154247_create_user_if_not_exists_on_oauth.php b/database/migrations/2024_06_21_154247_create_user_if_not_exists_on_oauth.php new file mode 100644 index 00000000000..6ffef788edd --- /dev/null +++ b/database/migrations/2024_06_21_154247_create_user_if_not_exists_on_oauth.php @@ -0,0 +1,43 @@ + 'oauth_create_user_on_first_attempt', + 'value' => '0', + 'is_secret' => true, + 'cat' => self::OAUTH, + 'type_range' => '0|1', + 'description' => 'Allow user creation when oauth id does not exist.', + ], + [ + 'key' => 'oauth_grant_new_user_upload_rights', + 'value' => '0', + 'is_secret' => true, + 'cat' => self::OAUTH, + 'type_range' => '0|1', + 'description' => 'Newly created user are allowed to upload content.', + ], + [ + 'key' => 'oauth_grant_new_user_modification_rights', + 'value' => '0', + 'is_secret' => true, + 'cat' => self::OAUTH, + 'type_range' => '0|1', + 'description' => 'Newly created user are allowed to edit their profile.', + ], + ]; + } +}; \ No newline at end of file diff --git a/database/migrations/2024_06_23_201042_enforce_login_on_gallery_only.php b/database/migrations/2024_06_23_201042_enforce_login_on_gallery_only.php new file mode 100644 index 00000000000..2e498c3a893 --- /dev/null +++ b/database/migrations/2024_06_23_201042_enforce_login_on_gallery_only.php @@ -0,0 +1,27 @@ + 'login_required_root_only', + 'value' => '1', + 'is_secret' => false, + 'cat' => self::GALLERY, + 'type_range' => self::BOOL, + 'description' => 'Require user to login only on root. A user with a direct link to an album can still access it.', + ], + ]; + } +}; diff --git a/database/migrations/2024_06_25_153527_bump_version050500.php b/database/migrations/2024_06_25_153527_bump_version050500.php new file mode 100644 index 00000000000..a8cc9681cf9 --- /dev/null +++ b/database/migrations/2024_06_25_153527_bump_version050500.php @@ -0,0 +1,32 @@ +where('key', 'version')->update(['value' => '050500']); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '050400']); + } +}; diff --git a/database/migrations/2024_07_01_231053_path_for_exiftool.php b/database/migrations/2024_07_01_231053_path_for_exiftool.php new file mode 100644 index 00000000000..6abc8acc4f5 --- /dev/null +++ b/database/migrations/2024_07_01_231053_path_for_exiftool.php @@ -0,0 +1,27 @@ + 'exiftool_path', + 'value' => '', + 'is_secret' => true, + 'cat' => self::PROCESSING, + 'type_range' => 'string', + 'description' => 'Path to the binary of exiftool.', + ], + ]; + } +}; diff --git a/database/migrations/2024_07_03_170506_bump_version050501.php b/database/migrations/2024_07_03_170506_bump_version050501.php new file mode 100644 index 00000000000..62093a5635a --- /dev/null +++ b/database/migrations/2024_07_03_170506_bump_version050501.php @@ -0,0 +1,32 @@ +where('key', 'version')->update(['value' => '050501']); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '050500']); + } +}; diff --git a/database/migrations/2024_07_06_214241_thumb_min_max_take_date_order.php b/database/migrations/2024_07_06_214241_thumb_min_max_take_date_order.php new file mode 100644 index 00000000000..c1c8d1dbe5f --- /dev/null +++ b/database/migrations/2024_07_06_214241_thumb_min_max_take_date_order.php @@ -0,0 +1,35 @@ + 'thumb_min_max_order', + 'value' => 'younger_older', + 'is_secret' => true, + 'cat' => self::GALLERY, + 'type_range' => 'older_younger|younger_older', + 'description' => 'Set which date to display first in thumb. Allowed values: older_younger, younger_older', + ], + [ + 'key' => 'header_min_max_order', + 'value' => 'older_younger', + 'is_secret' => true, + 'cat' => self::GALLERY, + 'type_range' => 'older_younger|younger_older', + 'description' => 'Set which date to display first in header. Allowed values: older_younger, younger_older', + ], + ]; + } +}; diff --git a/database/migrations/2024_07_12_183751_add_auto_play_config.php b/database/migrations/2024_07_12_183751_add_auto_play_config.php new file mode 100644 index 00000000000..f15cc65fde7 --- /dev/null +++ b/database/migrations/2024_07_12_183751_add_auto_play_config.php @@ -0,0 +1,27 @@ + 'autoplay_enabled', + 'value' => '1', + 'is_secret' => true, + 'cat' => self::GALLERY, + 'type_range' => self::BOOL, + 'description' => 'Set autoplay attribute on videos.', + ], + ]; + } +}; diff --git a/database/migrations/2024_07_26_120007_simplify_config.php b/database/migrations/2024_07_26_120007_simplify_config.php new file mode 100644 index 00000000000..80689e3d318 --- /dev/null +++ b/database/migrations/2024_07_26_120007_simplify_config.php @@ -0,0 +1,32 @@ +where('cat', '=', self::BOOL_STRING)->update(['cat' => self::BOOL]); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // Nothing to do + } +}; diff --git a/database/migrations/2024_07_29_172018_fix_settings.php b/database/migrations/2024_07_29_172018_fix_settings.php new file mode 100644 index 00000000000..979ff95e388 --- /dev/null +++ b/database/migrations/2024_07_29_172018_fix_settings.php @@ -0,0 +1,39 @@ +where('key', '=', 'site_owner')->update(['description' => 'Website Owner']); + DB::table('configs')->where('key', '=', 'mod_frame_enabled')->update(['description' => 'Enable Mod Frame']); + DB::table('configs')->where('key', '=', 'sm_facebook_url')->update(['description' => 'Url of facebook profile']); + DB::table('configs')->where('key', '=', 'sm_flickr_url')->update(['description' => 'Url of flickr profile']); + DB::table('configs')->where('key', '=', 'sm_twitter_url')->update(['description' => 'Url of twitter profile']); + DB::table('configs')->where('key', '=', 'sm_instagram_url')->update(['description' => 'Url of instagram profile']); + DB::table('configs')->where('key', '=', 'sm_youtube_url')->update(['description' => 'Url of youtube profile']); + DB::table('configs')->where('key', '=', 'footer_show_copyright')->update(['description' => 'Display copyright in footer.']); + DB::table('configs')->where('key', '=', 'footer_additional_text')->update(['description' => 'Additional text of the footer.']); + DB::table('configs')->where('key', '=', 'footer_show_social_media')->update(['description' => 'Show social media links in footer.']); + DB::table('configs')->where('key', '=', 'grants_download')->update(['description' => 'Grants download by default.']); + DB::table('configs')->where('key', '=', 'nsfw_banner_override')->update(['description' => 'Custom warning text instead of default.']); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // nothing to do. + } +}; diff --git a/database/migrations/2024_08_09_205532_white_theme.php b/database/migrations/2024_08_09_205532_white_theme.php new file mode 100644 index 00000000000..2bc8f24597f --- /dev/null +++ b/database/migrations/2024_08_09_205532_white_theme.php @@ -0,0 +1,27 @@ + 'dark_mode_enabled', + 'value' => '1', + 'is_secret' => false, + 'cat' => self::CONFIG, + 'type_range' => self::BOOL, + 'description' => 'Use dark mode for Lychee.', + ], + ]; + } +}; diff --git a/database/migrations/2024_08_15_163203_config_low_quality_image_placeholder.php b/database/migrations/2024_08_15_163203_config_low_quality_image_placeholder.php new file mode 100644 index 00000000000..89e6a4b21fb --- /dev/null +++ b/database/migrations/2024_08_15_163203_config_low_quality_image_placeholder.php @@ -0,0 +1,27 @@ + 'low_quality_image_placeholder', + 'value' => '1', + 'cat' => self::PROCESSING, + 'type_range' => self::BOOL, + 'description' => 'Enable low quality image placeholders', + 'is_secret' => false, + ], + ]; + } +}; \ No newline at end of file diff --git a/database/migrations/2024_08_31_090626_config_help_popup.php b/database/migrations/2024_08_31_090626_config_help_popup.php new file mode 100644 index 00000000000..56eca4a28ee --- /dev/null +++ b/database/migrations/2024_08_31_090626_config_help_popup.php @@ -0,0 +1,34 @@ + 'show_keybinding_help_popup', + 'value' => '1', + 'is_secret' => false, + 'cat' => 'config', + 'type_range' => self::BOOL, + 'description' => 'Display keybinding help pop-up on login.', + ], + [ + 'key' => 'show_keybinding_help_button', + 'value' => '1', + 'is_secret' => false, + 'cat' => 'config', + 'type_range' => self::BOOL, + 'description' => 'Show keybinding help button in header.', + ], + ]; + } +}; + diff --git a/database/migrations/2024_08_31_090740_fix_description_always_hover_hidden_photo.php b/database/migrations/2024_08_31_090740_fix_description_always_hover_hidden_photo.php new file mode 100644 index 00000000000..93988ea3464 --- /dev/null +++ b/database/migrations/2024_08_31_090740_fix_description_always_hover_hidden_photo.php @@ -0,0 +1,29 @@ +where('key', '=', 'display_thumb_photo_overlay')->update(['description' => 'Display the title and metadata on photo thumbs (always|hover|never)']); + DB::table('configs')->where('key', '=', 'small_max_width')->update(['description' => 'Maximum width for small thumbs (album view)']); + DB::table('configs')->where('key', '=', 'small_max_height')->update(['description' => 'Maximum height for small thumbs (album view)']); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // Nothing. + } +}; diff --git a/database/migrations/2024_09_14_143949_add_settings_level.php b/database/migrations/2024_09_14_143949_add_settings_level.php new file mode 100644 index 00000000000..9fd61d4a3ce --- /dev/null +++ b/database/migrations/2024_09_14_143949_add_settings_level.php @@ -0,0 +1,37 @@ +string('details', 200)->default(''); + $table->integer('level')->default(0); // 0 for all modifiable, 1 for supported, 2 for plus (if we ever decide to use that) + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('configs', function (Blueprint $table) { + $table->dropColumn('details'); + }); + Schema::table('configs', function (Blueprint $table) { + $table->dropColumn('level'); + }); + } +}; diff --git a/database/migrations/2024_09_14_153948_fix_more_descriptions.php b/database/migrations/2024_09_14_153948_fix_more_descriptions.php new file mode 100644 index 00000000000..4d51a14cbc6 --- /dev/null +++ b/database/migrations/2024_09_14_153948_fix_more_descriptions.php @@ -0,0 +1,104 @@ +where('key', '=', 'zip_deflate_level')->update([ + 'details' => '-1 = disable compression (use STORE method), 0 = no compression (use DEFLATE method), 1 = minimal compression (fast), ... 9 = maximum compression (slow)', + 'description' => 'Zip compression level.', + ]); + DB::table('configs')->where('key', '=', 'default_album_protection')->update(['description' => 'Default protection for newly created albums']); + DB::table('configs')->where('key', '=', 'login_button_position')->update(['description' => 'Position of the login button']); + + DB::table('configs')->where('key', '=', 'date_format_photo_thumb')->update(['description' => 'Format the date for the photo thumbs.']); + DB::table('configs')->where('key', '=', 'date_format_photo_overlay')->update(['description' => 'Format the date for the photo overlay.']); + DB::table('configs')->where('key', '=', 'date_format_sidebar_uploaded')->update(['description' => 'Format the upload date for the photo sidebar.']); + DB::table('configs')->where('key', '=', 'date_format_sidebar_taken_at')->update(['description' => 'Format the capture date for the photo sidebar.']); + DB::table('configs')->where('key', '=', 'date_format_hero_min_max')->update(['description' => 'Format the date for the album hero.']); + DB::table('configs')->where('key', '=', 'date_format_hero_created_at')->update(['description' => 'Format the created date for the album details.']); + DB::table('configs')->where('key', '=', 'date_format_album_thumb')->update(['description' => 'Format the date for the album thumbs.']); + DB::table('configs')->whereIn('key', [ + 'date_format_photo_thumb', + 'date_format_photo_overlay', + 'date_format_sidebar_uploaded', + 'date_format_sidebar_taken_at', + 'date_format_hero_min_max', + 'date_format_hero_created_at', + 'date_format_album_thumb', + ])->update(['details' => 'See https://www.php.net/manual/en/datetime.format.php']); + DB::table('configs')->where('key', '=', 'display_thumb_album_overlay')->update(['description' => 'Display the title and metadata on album thumbs']); + DB::table('configs')->where('key', '=', 'display_thumb_photo_overlay')->update(['description' => 'Display the title and metadata on photo thumbs']); + DB::table('configs')->where('key', '=', 'default_album_thumb_aspect_ratio')->update(['description' => 'Default aspect ratio for album thumbs']); + DB::table('configs')->where('key', '=', 'use_album_compact_header')->update(['description' => 'Disable the header image in albums']); + DB::table('configs')->where('key', '=', 'thumb_min_max_order')->update(['description' => 'Set which date to display first in thumb.']); + DB::table('configs')->where('key', '=', 'header_min_max_order')->update(['description' => 'Set which date to display first in header.']); + DB::table('configs')->where('key', '=', 'current_job_processing_visible')->update(['description' => 'Make the processing job queue visible by default']); + DB::table('configs')->where('key', '=', 'local_takestamp_video_formats')->update(['description' => 'Use local takestamp for the following video formats']); + DB::table('configs')->where('key', '=', 'back_button_enabled')->update(['description' => 'Enable/disable back button on gallery']); + + DB::table('configs')->where('key', '=', 'enable_unsorted')->update(['description' => 'Enable Unsorted smart album.', + 'details' => 'Warning! Disabling this will make pictures without an album invisible.']); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::table('configs')->where('key', '=', 'zip_deflate_level')->update([ + 'details' => '', + 'description' => 'DEFLATE compression level: -1 = disable compression (use STORE method), 0 = no compression (use DEFLATE method), 1 = minimal compression (fast), ... 9 = maximum compression (slow)', + ]); + DB::table('configs')->where('key', '=', 'default_album_protection')->update(['description' => 'Default protection for newly created albums. 1 = private, 2 = public, 3 = inherit from parent']); + DB::table('configs')->where('key', '=', 'login_button_position')->update(['description' => 'Position of the login button (left | right)']); + + DB::table('configs')->where('key', '=', 'date_format_photo_thumb')->update([ + 'details' => '', + 'description' => 'Format the date for the photo thumbs. See https://www.php.net/manual/en/datetime.format.php', + ]); + DB::table('configs')->where('key', '=', 'date_format_photo_overlay')->update([ + 'details' => '', + 'description' => 'Format the date for the photo overlay. See https://www.php.net/manual/en/datetime.format.php', + ]); + DB::table('configs')->where('key', '=', 'date_format_sidebar_uploaded')->update([ + 'details' => '', + 'description' => 'Format the upload date for the photo sidebar. See https://www.php.net/manual/en/datetime.format.php', + ]); + DB::table('configs')->where('key', '=', 'date_format_sidebar_taken_at')->update([ + 'details' => '', + 'description' => 'Format the capture date for the photo sidebar. See https://www.php.net/manual/en/datetime.format.php', + ]); + DB::table('configs')->where('key', '=', 'date_format_hero_min_max')->update([ + 'details' => '', + 'description' => 'Format the date for the album hero. See https://www.php.net/manual/en/datetime.format.php', + ]); + DB::table('configs')->where('key', '=', 'date_format_hero_created_at')->update([ + 'details' => '', + 'description' => 'Format the created date for the album details. See https://www.php.net/manual/en/datetime.format.php', + ]); + DB::table('configs')->where('key', '=', 'date_format_album_thumb')->update([ + 'details' => '', + 'description' => 'Format the date for the album thumbs. See https://www.php.net/manual/en/datetime.format.php', + ]); + DB::table('configs')->where('key', '=', 'display_thumb_album_overlay')->update(['description' => 'Display the title and metadata on album thumbs (always|hover|never)']); + DB::table('configs')->where('key', '=', 'display_thumb_photo_overlay')->update(['description' => 'Display the title and metadata on photo thumbs (always|hover|never)']); + DB::table('configs')->where('key', '=', 'default_album_thumb_aspect_ratio')->update(['description' => 'Default aspect ratio for album thumbs, one of: 1/1, 2/3, 3/2, 4/5, 5/4, 16/9']); + DB::table('configs')->where('key', '=', 'use_album_compact_header')->update(['description' => 'Disable the header image in albums (0|1)']); + DB::table('configs')->where('key', '=', 'thumb_min_max_order')->update(['description' => 'Set which date to display first in thumb. Allowed values: older_younger, younger_older']); + DB::table('configs')->where('key', '=', 'header_min_max_order')->update(['description' => 'Set which date to display first in header. Allowed values: older_younger, younger_older']); + DB::table('configs')->where('key', '=', 'current_job_processing_visible')->update(['description' => 'Make the processing job queue visible by default (0|1).']); + DB::table('configs')->where('key', '=', 'back_button_enabled')->update(['description' => 'Enable/disable back button on gallery (0 | 1)']); + DB::table('configs')->where('key', '=', 'enable_unsorted')->update(['description' => 'Enable Unsorted smart album. Warning! Disabling this will make pictures without an album invisible.']); + } +}; diff --git a/database/migrations/2024_09_27_144741_add_supporter_fields.php b/database/migrations/2024_09_27_144741_add_supporter_fields.php new file mode 100644 index 00000000000..2eed23b14e5 --- /dev/null +++ b/database/migrations/2024_09_27_144741_add_supporter_fields.php @@ -0,0 +1,55 @@ + 'email', + 'value' => '', + 'is_secret' => true, + 'cat' => self::SE, + 'type_range' => self::STRING, + 'description' => 'Email used when requesting the license.', + 'details' => '', + ], + [ + 'key' => 'license_key', + 'value' => '', + 'is_secret' => true, + 'cat' => self::SE, + 'type_range' => self::STRING, + 'description' => 'Lychee License key', + 'details' => 'Get Supporter Edition here: https://lycheeorg.dev/get-supporter-edition', + ], + [ + 'key' => 'disable_se_call_for_actions', + 'value' => '0', + 'is_secret' => false, + 'cat' => self::SE, + 'type_range' => self::BOOL, + 'description' => 'Disable Lychee SE hint', + 'details' => 'Hides Lychee SE call for actions.', + ], + [ + 'key' => 'enable_se_preview', + 'value' => '0', + 'is_secret' => false, + 'cat' => self::SE, + 'type_range' => self::BOOL, + 'description' => 'Enable preview of Lychee SE features', + 'details' => '', + ], + ]; + } +}; diff --git a/database/migrations/2024_10_05_125328_add_slideshow_timeout.php b/database/migrations/2024_10_05_125328_add_slideshow_timeout.php new file mode 100644 index 00000000000..d412f623dc0 --- /dev/null +++ b/database/migrations/2024_10_05_125328_add_slideshow_timeout.php @@ -0,0 +1,28 @@ + 'slideshow_timeout', + 'value' => '5', + 'is_secret' => false, + 'cat' => self::GALLERY, + 'type_range' => self::POSITIVE, + 'description' => 'Refresh rate of the slideshow in seconds.', + 'details' => 'Show next picture after x seconds.', + ], + ]; + } +}; diff --git a/database/migrations/2024_10_05_125833_lang_is_admin_setting.php b/database/migrations/2024_10_05_125833_lang_is_admin_setting.php new file mode 100644 index 00000000000..b1207587cf1 --- /dev/null +++ b/database/migrations/2024_10_05_125833_lang_is_admin_setting.php @@ -0,0 +1,27 @@ +where('key', '=', 'lang')->update(['cat' => 'Admin']); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::table('configs')->where('key', '=', 'lang')->update(['cat' => 'Gallery']); + } +}; diff --git a/database/migrations/2024_10_05_184315_bump_version060000.php b/database/migrations/2024_10_05_184315_bump_version060000.php new file mode 100644 index 00000000000..e3ec2d13588 --- /dev/null +++ b/database/migrations/2024_10_05_184315_bump_version060000.php @@ -0,0 +1,32 @@ +where('key', 'version')->update(['value' => '060000']); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '050501']); + } +}; diff --git a/database/migrations/2024_10_09_191528_image_processing_backup.php b/database/migrations/2024_10_09_191528_image_processing_backup.php new file mode 100644 index 00000000000..c5da2a32b30 --- /dev/null +++ b/database/migrations/2024_10_09_191528_image_processing_backup.php @@ -0,0 +1,28 @@ + 'keep_original_untouched', + 'value' => '1', + 'cat' => self::PROCESSING, + 'type_range' => self::BOOL, + 'description' => 'Keep Original image untouched.', + 'details' => 'In case of auto rotation, the original image will be kept untouched.', + 'is_secret' => false, + ], + ]; + } +}; \ No newline at end of file diff --git a/database/migrations/2024_10_09_194010_image_processing_details_and_others.php b/database/migrations/2024_10_09_194010_image_processing_details_and_others.php new file mode 100644 index 00000000000..7fd9f1e1537 --- /dev/null +++ b/database/migrations/2024_10_09_194010_image_processing_details_and_others.php @@ -0,0 +1,47 @@ +where('key', 'auto_fix_orientation')->update(['details' => ' Original images will be overwritten and compressed.']); + DB::table('configs')->whereIn('key', [ + 'date_format_photo_thumb', + 'date_format_photo_overlay', + 'date_format_sidebar_uploaded', + 'date_format_sidebar_taken_at', + 'date_format_hero_min_max', + 'date_format_hero_created_at', + 'date_format_album_thumb', + ])->update(['details' => 'See datetime.format.php']); + DB::table('configs')->where('key', 'license_key')->update(['details' => 'Get Supporter Edition here: https://lycheeorg.dev/get-supporter-edition']); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::table('configs')->where('key', 'auto_fix_orientation')->update(['details' => '']); + DB::table('configs')->whereIn('key', [ + 'date_format_photo_thumb', + 'date_format_photo_overlay', + 'date_format_sidebar_uploaded', + 'date_format_sidebar_taken_at', + 'date_format_hero_min_max', + 'date_format_hero_created_at', + 'date_format_album_thumb', + ])->update(['details' => 'See https://www.php.net/manual/en/datetime.format.php']); + } +}; diff --git a/database/migrations/2024_10_10_101333_set_dropbox_disabled.php b/database/migrations/2024_10_10_101333_set_dropbox_disabled.php new file mode 100644 index 00000000000..77fec4751ee --- /dev/null +++ b/database/migrations/2024_10_10_101333_set_dropbox_disabled.php @@ -0,0 +1,38 @@ +where('key', '=', 'dropbox_key') + ->where('value', '=', '') + ->update([ + 'value' => 'disabled', + 'details' => 'Use value "disabled" to mark this setting as such.']); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::table('configs') + ->where('key', '=', 'dropbox_key') + ->where('value', '=', 'disabled') + ->update([ + 'value' => '', + 'details' => '']); + } +}; diff --git a/database/migrations/2024_10_11_173054_add_space_quota_per_user.php b/database/migrations/2024_10_11_173054_add_space_quota_per_user.php new file mode 100644 index 00000000000..9be9021ea1e --- /dev/null +++ b/database/migrations/2024_10_11_173054_add_space_quota_per_user.php @@ -0,0 +1,42 @@ +bigInteger('quota_kb')->nullable(true)->default(null); + $table->text('description')->nullable(true)->default(null); + $table->text('note')->nullable(true)->default(null); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('quota_kb'); + }); + + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('description'); + }); + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('note'); + }); + } +}; diff --git a/database/migrations/2024_10_11_173106_add_space_quota_configuration.php b/database/migrations/2024_10_11_173106_add_space_quota_configuration.php new file mode 100644 index 00000000000..627dd58595b --- /dev/null +++ b/database/migrations/2024_10_11_173106_add_space_quota_configuration.php @@ -0,0 +1,29 @@ + 'default_user_quota', + 'value' => '0', + 'cat' => self::OAUTH, + 'type_range' => self::INT, + 'description' => 'Default space quota for new users.', + 'details' => 'Value in KB, keep at 0 to disable quota.', + 'is_secret' => true, + 'level' => 1, + ], + ]; + } +}; diff --git a/database/migrations/2024_10_13_134419_oath_group_to_users.php b/database/migrations/2024_10_13_134419_oath_group_to_users.php new file mode 100644 index 00000000000..2374dae41e2 --- /dev/null +++ b/database/migrations/2024_10_13_134419_oath_group_to_users.php @@ -0,0 +1,31 @@ +where('cat', self::OAUTH)->update(['cat' => self::USERS]); + DB::table('configs')->where('key', '=', 'allow_username_change')->update(['cat' => self::USERS]); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::table('configs')->where('cat', self::USERS)->update(['cat' => self::OAUTH]); + } +}; diff --git a/database/migrations/2024_10_14_104644_show_nsfw_in_smart_albums.php b/database/migrations/2024_10_14_104644_show_nsfw_in_smart_albums.php new file mode 100644 index 00000000000..2b425829796 --- /dev/null +++ b/database/migrations/2024_10_14_104644_show_nsfw_in_smart_albums.php @@ -0,0 +1,27 @@ + 'hide_nsfw_in_smart_albums_and_search', + 'value' => '1', // safe default + 'cat' => 'Mod NSFW', + 'type_range' => self::BOOL, + 'description' => 'Do not show sensitive photos in Smart Albums and Search.', + 'details' => 'Pictures placed in sensive albums will not be shown in Smart Albums and Search.', + 'is_secret' => false, + 'level' => 0, + ], + ]; + } +}; diff --git a/database/migrations/2024_10_14_104823_refine_text_enable_unsorted_smart_album.php b/database/migrations/2024_10_14_104823_refine_text_enable_unsorted_smart_album.php new file mode 100644 index 00000000000..0c66b754015 --- /dev/null +++ b/database/migrations/2024_10_14_104823_refine_text_enable_unsorted_smart_album.php @@ -0,0 +1,27 @@ +where('key', '=', 'enable_unsorted')->update(['details' => ' Disabling this smart album will make pictures without an album invisible.']); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::table('configs')->where('key', '=', 'enable_unsorted')->update(['details' => 'Warning! Disabling this will make pictures without an album invisible.']); + } +}; \ No newline at end of file diff --git a/database/migrations/2024_10_20_125449_move-socials-to-footer.php b/database/migrations/2024_10_20_125449_move-socials-to-footer.php new file mode 100644 index 00000000000..ecd4e90c666 --- /dev/null +++ b/database/migrations/2024_10_20_125449_move-socials-to-footer.php @@ -0,0 +1,28 @@ +where('cat', 'Social Media')->update(['cat' => 'Footer']); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::table('configs')->whereLike('key', 'sm_%_url')->update(['cat' => 'Social Media']); + } +}; diff --git a/database/migrations/2024_10_20_173904_add_photo_layout_per_album.php b/database/migrations/2024_10_20_173904_add_photo_layout_per_album.php new file mode 100644 index 00000000000..bf358b02a29 --- /dev/null +++ b/database/migrations/2024_10_20_173904_add_photo_layout_per_album.php @@ -0,0 +1,36 @@ +string(self::PHOTO_LAYOUT_COLUMN_NAME, 20)->nullable()->default(null)->after('copyright'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table(self::BASE_ALBUM, function (Blueprint $table) { + $table->dropColumn(self::PHOTO_LAYOUT_COLUMN_NAME); + }); + } +}; diff --git a/database/migrations/2024_10_23_222857_change_header.php b/database/migrations/2024_10_23_222857_change_header.php new file mode 100644 index 00000000000..c0f3607ebf7 --- /dev/null +++ b/database/migrations/2024_10_23_222857_change_header.php @@ -0,0 +1,28 @@ +where('value', '=', 'Lychee v5')->update(['value' => 'Lychee v6']); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::table('configs')->where('value', '=', 'Lychee v6')->update(['value' => 'Lychee v5']); + } +}; diff --git a/database/migrations/2024_10_23_225332_warning_html_content.php b/database/migrations/2024_10_23_225332_warning_html_content.php new file mode 100644 index 00000000000..7f8ec43fe91 --- /dev/null +++ b/database/migrations/2024_10_23_225332_warning_html_content.php @@ -0,0 +1,27 @@ +whereIn('key', ['nsfw_banner_override', 'footer_additional_text'])->update(['details' => ' Unsanitized html field.']); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::table('configs')->whereIn('key', ['nsfw_banner_override', 'footer_additional_text'])->update(['details' => '']); + } +}; diff --git a/database/migrations/2024_10_29_184020_bump_version060001.php b/database/migrations/2024_10_29_184020_bump_version060001.php new file mode 100644 index 00000000000..ee0f760bd50 --- /dev/null +++ b/database/migrations/2024_10_29_184020_bump_version060001.php @@ -0,0 +1,32 @@ +where('key', 'version')->update(['value' => '060001']); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '060000']); + } +}; diff --git a/database/migrations/2024_10_30_064336_timeline_options.php b/database/migrations/2024_10_30_064336_timeline_options.php new file mode 100644 index 00000000000..0c99d5a736e --- /dev/null +++ b/database/migrations/2024_10_30_064336_timeline_options.php @@ -0,0 +1,195 @@ + 'timeline_photos_enabled', + 'value' => '1', + 'cat' => self::TIMELINE, + 'type_range' => self::BOOL, + 'description' => 'Enable timeline for photos', + 'details' => '', + 'is_secret' => false, + 'level' => 0, + ], + [ + 'key' => 'timeline_photos_public', + 'value' => '0', + 'cat' => self::TIMELINE, + 'type_range' => self::BOOL, + 'description' => 'Allow anonymous users to access the photo timeline', + 'details' => '', + 'is_secret' => false, + 'level' => 0, + ], + [ + 'key' => 'timeline_photos_granularity', + 'value' => 'day', + 'cat' => self::TIMELINE, + 'type_range' => 'year|month|day|hour', + 'description' => 'Timeline granularity for photos', + 'details' => '', + 'is_secret' => false, + 'level' => 1, + ], + [ + 'key' => 'timeline_photos_order', + 'value' => 'taken_at', + 'cat' => self::TIMELINE, + 'type_range' => 'taken_at|created_at', + 'description' => 'Order photos on', + 'details' => 'This determines whether the captured date or the upload date will be used to order the photos.', + 'is_secret' => false, + 'level' => 0, + ], + [ + 'key' => 'timeline_photos_layout', + 'value' => self::SQUARE, + 'cat' => self::TIMELINE, + 'type_range' => self::SQUARE . '|' . self::JUSTIFIED . '|' . self::MASONRY . '|' . self::GRID, + 'description' => 'Photo layout for timeline page', + 'details' => '', + 'is_secret' => false, + 'level' => 0, + ], + [ + 'key' => 'timeline_photos_pagination_limit', + 'value' => '200', + 'cat' => self::TIMELINE, + 'type_range' => self::POSITIVE, + 'description' => 'Number of photos to display per page in timeline', + 'details' => '', + 'is_secret' => false, + 'level' => 0, + ], + [ + 'key' => 'timeline_albums_enabled', + 'value' => '1', + 'cat' => self::TIMELINE, + 'type_range' => self::BOOL, + 'description' => 'Enable timeline for albums', + 'details' => '', + 'is_secret' => false, + 'level' => 0, + ], + [ + 'key' => 'timeline_albums_public', + 'value' => '0', + 'cat' => self::TIMELINE, + 'type_range' => self::BOOL, + 'description' => 'Display the albums timeline for anonymous users', + 'details' => '', + 'is_secret' => false, + 'level' => 0, + ], + [ + 'key' => 'timeline_albums_granularity', + 'value' => 'year', + 'cat' => self::TIMELINE, + 'type_range' => 'year|month|day', + 'description' => 'Timeline granularity for albums', + 'details' => '', + 'is_secret' => false, + 'level' => 1, + ], + [ + 'key' => 'timeline_left_border_enabled', + 'value' => '1', + 'cat' => self::TIMELINE, + 'type_range' => self::BOOL, + 'description' => 'Enable the left border line on timelines', + 'details' => '', + 'is_secret' => false, + 'level' => 1, + ], + [ + 'key' => 'timeline_photo_date_format_year', + 'value' => 'Y', + 'cat' => self::TIMELINE, + 'type_range' => self::STRING_REQ, + 'description' => 'Format the date at year granularity for photos', + 'details' => 'See datetime.format.php', + 'is_secret' => false, + 'level' => 1, + ], + [ + 'key' => 'timeline_photo_date_format_month', + 'value' => 'M Y', + 'cat' => self::TIMELINE, + 'type_range' => self::STRING_REQ, + 'description' => 'Format the date at month granularity for photos', + 'details' => 'See datetime.format.php', + 'is_secret' => false, + 'level' => 1, + ], + [ + 'key' => 'timeline_photo_date_format_day', + 'value' => 'j M Y', + 'cat' => self::TIMELINE, + 'type_range' => self::STRING_REQ, + 'description' => 'Format the date at day granularity for photos', + 'details' => 'See datetime.format.php', + 'is_secret' => false, + 'level' => 1, + ], + [ + 'key' => 'timeline_photo_date_format_hour', + 'value' => 'g:i', + 'cat' => self::TIMELINE, + 'type_range' => self::STRING_REQ, + 'description' => 'Format the date at hour granularity for photos', + 'details' => 'See datetime.format.php', + 'is_secret' => false, + 'level' => 1, + ], + [ + 'key' => 'timeline_album_date_format_year', + 'value' => 'Y', + 'cat' => self::TIMELINE, + 'type_range' => self::STRING_REQ, + 'description' => 'Format the date at year granularity for albums', + 'details' => 'See datetime.format.php', + 'is_secret' => false, + 'level' => 1, + ], + [ + 'key' => 'timeline_album_date_format_month', + 'value' => 'M Y', + 'cat' => self::TIMELINE, + 'type_range' => self::STRING_REQ, + 'description' => 'Format the date at month granularity for albums', + 'details' => 'See datetime.format.php', + 'is_secret' => false, + 'level' => 1, + ], + [ + 'key' => 'timeline_album_date_format_day', + 'value' => 'j M', + 'cat' => self::TIMELINE, + 'type_range' => self::STRING_REQ, + 'description' => 'Format the date at day granularity for albums', + 'details' => 'See datetime.format.php', + 'is_secret' => false, + 'level' => 1, + ], + ]; + } +}; diff --git a/database/migrations/2024_11_10_171126_timeline_in_albums.php b/database/migrations/2024_11_10_171126_timeline_in_albums.php new file mode 100644 index 00000000000..998abbb1c2e --- /dev/null +++ b/database/migrations/2024_11_10_171126_timeline_in_albums.php @@ -0,0 +1,44 @@ +string(self::ALBUM_TIMELINE_COLUMN_NAME, 20)->nullable()->default(null)->after('album_thumb_aspect_ratio'); + }); + Schema::table(self::BASE_ALBUMS, function ($table) { + $table->string(self::PHOTO_TIMELINE_COLUMN_NAME, 20)->nullable()->default(null)->after('photo_layout'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table(self::ALBUMS, function (Blueprint $table) { + $table->dropColumn(self::ALBUM_TIMELINE_COLUMN_NAME); + }); + Schema::table(self::BASE_ALBUMS, function (Blueprint $table) { + $table->dropColumn(self::PHOTO_TIMELINE_COLUMN_NAME); + }); + } +}; diff --git a/database/migrations/2024_11_11_141241_remove_config_hide_nsfw_in_smart_albums_and_search.php b/database/migrations/2024_11_11_141241_remove_config_hide_nsfw_in_smart_albums_and_search.php new file mode 100644 index 00000000000..006b0f35c77 --- /dev/null +++ b/database/migrations/2024_11_11_141241_remove_config_hide_nsfw_in_smart_albums_and_search.php @@ -0,0 +1,27 @@ + 'hide_nsfw_in_smart_albums_and_search', + 'value' => '1', // safe default + 'cat' => 'Mod NSFW', + 'type_range' => self::BOOL, + 'description' => 'Do not show sensitive photos in Smart Albums and Search.', + 'details' => 'Pictures placed in sensive albums will not be shown in Smart Albums and Search.', + 'is_secret' => false, + 'level' => 0, + ], + ]; + } +}; diff --git a/database/migrations/2024_11_11_141334_add_hide_nsfw_smart_search_config.php b/database/migrations/2024_11_11_141334_add_hide_nsfw_smart_search_config.php new file mode 100644 index 00000000000..8161f734f97 --- /dev/null +++ b/database/migrations/2024_11_11_141334_add_hide_nsfw_smart_search_config.php @@ -0,0 +1,93 @@ + 'hide_nsfw_in_smart_albums', + 'value' => '1', // safe default + 'cat' => 'Smart Albums', + 'type_range' => self::BOOL, + 'description' => 'Do not show sensitive photos in Smart Albums', + 'details' => 'Pictures placed in sensive albums will not be shown in Smart Albums.', + 'is_secret' => false, + 'level' => 0, + ], + [ + 'key' => 'hide_nsfw_in_frame', + 'value' => '1', // safe default + 'cat' => 'Mod Frame', + 'type_range' => self::BOOL, + 'description' => 'Do not show sensitive photos in Frame', + 'details' => 'Pictures placed in sensive albums will not be shown on the Frame.', + 'is_secret' => false, + 'level' => 0, + ], + [ + 'key' => 'hide_nsfw_in_map', + 'value' => '1', // safe default + 'cat' => 'Mod Map', + 'type_range' => self::BOOL, + 'description' => 'Do not show sensitive photos in Map', + 'details' => 'Pictures placed in sensive albums will not be shown on the Map.', + 'is_secret' => false, + 'level' => 0, + ], + [ + 'key' => 'hide_nsfw_in_search', + 'value' => '1', // safe default + 'cat' => 'Mod Search', + 'type_range' => self::BOOL, + 'description' => 'Do not show sensitive photos in Search', + 'details' => 'Pictures placed in sensive albums will not be shown in Search.', + 'is_secret' => false, + 'level' => 0, + ], + [ + 'key' => 'search_photos_layout', + 'value' => self::SQUARE, + 'cat' => 'Mod Search', + 'type_range' => self::SQUARE . '|' . self::JUSTIFIED . '|' . self::MASONRY . '|' . self::GRID, + 'description' => 'Photo layout for search page', + 'details' => '', + 'is_secret' => false, + 'level' => 0, + ], + [ + 'key' => 'hide_nsfw_in_rss', + 'value' => '1', // safe default + 'cat' => 'Mod RSS', + 'type_range' => self::BOOL, + 'description' => 'Do not show sensitive photos in RSS', + 'details' => 'Pictures placed in sensive albums will not be shown in the RSS feed.', + 'is_secret' => false, + 'level' => 0, + ], + [ + 'key' => 'hide_nsfw_in_timeline', + 'value' => '1', // safe default + 'cat' => 'Mod Timeline', + 'type_range' => self::BOOL, + 'description' => 'Do not show sensitive photos in Timeline', + 'details' => 'Pictures placed in sensive albums will not be shown in the timeline page.', + 'is_secret' => false, + 'level' => 0, + ], + ]; + } +}; + diff --git a/database/migrations/2024_11_25_211912_bump_version060100.php b/database/migrations/2024_11_25_211912_bump_version060100.php new file mode 100644 index 00000000000..ca63748d226 --- /dev/null +++ b/database/migrations/2024_11_25_211912_bump_version060100.php @@ -0,0 +1,32 @@ +where('key', 'version')->update(['value' => '060100']); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '060001']); + } +}; diff --git a/database/migrations/2024_11_26_125145_improve_info_timeline.php b/database/migrations/2024_11_26_125145_improve_info_timeline.php new file mode 100644 index 00000000000..ff68f2f676a --- /dev/null +++ b/database/migrations/2024_11_26_125145_improve_info_timeline.php @@ -0,0 +1,31 @@ +where('key', '=', 'timeline_photos_layout')->update(['details' => "Not available yet."]); + DB::table('configs')->where('key', '=', 'timeline_photos_pagination_limit')->update(['details' => "Not available yet."]); + DB::table('configs')->where('key', '=', 'timeline_photos_enabled')->update(['details' => 'Globally enable photo timelines in each albums. This can also be disabled/enabled per album.']); + DB::table('configs')->where('key', '=', 'timeline_albums_enabled')->update(['details' => 'Globally enable albums timelines in each albums (and root). This can also be disabled/enabled per album.']); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::table('configs')->whereIn('key', ['timeline_photos_layout', 'timeline_photos_pagination_limit', 'timeline_photos_enabled', 'timeline_albums_enabled'])->update(['details' => '']); + } +}; diff --git a/database/migrations/2024_11_26_131136_bump_version060101.php b/database/migrations/2024_11_26_131136_bump_version060101.php new file mode 100644 index 00000000000..77eaeeb18c0 --- /dev/null +++ b/database/migrations/2024_11_26_131136_bump_version060101.php @@ -0,0 +1,32 @@ +where('key', 'version')->update(['value' => '060101']); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '060100']); + } +}; diff --git a/database/migrations/2024_11_27_085119_bump_version060102.php b/database/migrations/2024_11_27_085119_bump_version060102.php new file mode 100644 index 00000000000..127ee9edf0f --- /dev/null +++ b/database/migrations/2024_11_27_085119_bump_version060102.php @@ -0,0 +1,32 @@ +where('key', 'version')->update(['value' => '060102']); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '060101']); + } +}; diff --git a/database/migrations/2024_12_03_221445_set_number_icons_mobile.php b/database/migrations/2024_12_03_221445_set_number_icons_mobile.php new file mode 100644 index 00000000000..543c3a552f1 --- /dev/null +++ b/database/migrations/2024_12_03_221445_set_number_icons_mobile.php @@ -0,0 +1,27 @@ + 'number_albums_per_row_mobile', + 'value' => '3', // safe default + 'cat' => 'Gallery', + 'type_range' => '1|2|3', + 'description' => 'Number of albums per row on mobile view', + 'details' => '', + 'is_secret' => false, + 'level' => 1, + ], + ]; + } +}; diff --git a/database/migrations/2024_12_16_132422_bump_version060200.php b/database/migrations/2024_12_16_132422_bump_version060200.php new file mode 100644 index 00000000000..775f5ccec08 --- /dev/null +++ b/database/migrations/2024_12_16_132422_bump_version060200.php @@ -0,0 +1,32 @@ +where('key', 'version')->update(['value' => '060200']); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down(): void + { + DB::table('configs')->where('key', 'version')->update(['value' => '060102']); + } +}; diff --git a/database/migrations/2064_12_25_0000_generate_installed_log.php b/database/migrations/2064_12_25_0000_generate_installed_log.php deleted file mode 100644 index 0ed46e26d03..00000000000 --- a/database/migrations/2064_12_25_0000_generate_installed_log.php +++ /dev/null @@ -1,32 +0,0 @@ - + */ +class NestedSetForAlbums_AlbumModel extends Model implements Node +{ + /** @phpstan-use NodeTrait */ + use NodeTrait; + + protected $table = 'albums'; +} \ No newline at end of file diff --git a/database/migrations/TemporaryModels/OptimizeTables.php b/database/migrations/TemporaryModels/OptimizeTables.php new file mode 100644 index 00000000000..30950011cc1 --- /dev/null +++ b/database/migrations/TemporaryModels/OptimizeTables.php @@ -0,0 +1,109 @@ +output = new ConsoleOutput(); + $this->msgSection = $this->output->section(); + $connection = Schema::connection(null)->getConnection(); + $this->driverName = $connection->getDriverName(); + } + + /** + * Run the optimization. + */ + public function exec(): void + { + /** @var array{name:string,schema:?string,size:int,comment:?string,collation:?string,engine:?string}[] */ + $tables = Schema::getTables(); + + match ($this->driverName) { + 'mysql','pgsql','sqlite' => 'print nothing.', + default => $this->msgSection->writeln('Warning: Unknown DBMS; doing nothing.'), + }; + + $sql = match ($this->driverName) { + 'mysql' => 'ANALYZE TABLE ', + 'pgsql' => 'ANALYZE ', + 'sqlite' => 'ANALYZE ', + default => 'NOTHING', + }; + + if ($sql === 'NOTHING') { + return; + } + + foreach ($tables as $table) { + try { + DB::statement($sql . $table['name']); + } catch (\Throwable $th) { + $this->msgSection->writeln('Error: could not optimize ' . $table['name'] . '.'); + $this->msgSection->writeln('Error: ' . $th->getMessage()); + } + } + } + + /** + * A helper function that allows to drop an index if exists. + * + * @param Blueprint $table + * @param string|string[] $indexName + */ + public function dropIndexIfExists(Blueprint $table, string|array $indexName): void + { + $indexTableName = !is_array($indexName) ? $indexName : ($table->getTable() . '_' . implode('_', $indexName) . '_index'); + $indexes = collect(Schema::getIndexes($table->getTable()))->map(fn ($a) => $a['name'])->all(); + if (in_array($indexTableName, $indexes, true)) { + $table->dropIndex($indexTableName); + } + } + + /** + * A helper function that allows to drop an unique constraint if exists. + * + * @param Blueprint $table + * @param string $indexName + */ + public function dropUniqueIfExists(Blueprint $table, string $indexName): void + { + $indexes = collect(Schema::getIndexes($table->getTable()))->map(fn ($a) => $a['name'])->all(); + if (in_array($indexName, $indexes, true)) { + $table->dropUnique($indexName); + } + } + + /** + * A helper function that allows to drop an foreign key if exists. + * + * @param Blueprint $table + * @param string $indexName + */ + public function dropForeignIfExists(Blueprint $table, string $indexName): void + { + if ($this->driverName === 'sqlite') { + return; + } + + $fk = collect(Schema::getForeignKeys($table->getTable()))->map(fn ($a) => $a['name'])->all(); + if (in_array($indexName, $fk, true)) { + $table->dropForeign($indexName); + } + } +} \ No newline at end of file diff --git a/database/migrations/TemporaryModels/RefactorAlbumModel_AlbumModel.php b/database/migrations/TemporaryModels/RefactorAlbumModel_AlbumModel.php new file mode 100644 index 00000000000..216c91e3ef0 --- /dev/null +++ b/database/migrations/TemporaryModels/RefactorAlbumModel_AlbumModel.php @@ -0,0 +1,39 @@ + + */ +class RefactorAlbumModel_AlbumModel extends Model implements Node +{ + /** @phpstan-use NodeTrait */ + use NodeTrait; + + protected $table = 'albums'; + + protected $keyType = 'string'; + + public $timestamps = false; +} diff --git a/database/seeds/DatabaseSeeder.php b/database/seeds/DatabaseSeeder.php index 313d9ab2d09..33b2dc28943 100644 --- a/database/seeds/DatabaseSeeder.php +++ b/database/seeds/DatabaseSeeder.php @@ -1,5 +1,11 @@ create(); } diff --git a/developer-notes.md b/developer-notes.md index 27304a1825f..fc39d7a4691 100644 --- a/developer-notes.md +++ b/developer-notes.md @@ -3,7 +3,7 @@ This guide contains some tricks and "do"s and "don't"s for new developer. In particular, it highlights some pitfalls one can easily trap into. -# TLTR for the Impatient +# TL;DR for the Impatient 1. If you create a new Eloquent model, use the trait `\App\Models\Extensions\UTCBasedTimes`. @@ -133,3 +133,19 @@ With respect to functional behaviour, we have two "broken" mappings that should This means only the methods `timestamp_tz` and `datetime` are usable in a DB-independent manner. Also, the convenient method `timestamps` must not be used. Taking into account the conclusion from above, the Lychee Application only uses `Blueprint::datetime`, because it shows identical behaviour for each DBMS and has no year-2038-problem on MySQL. + + +### Responses types + +To generate proper responses types, we use Spatie Data + Spatie Typescript. + +Create a new resource and add the attribute `#[TypeScript()]` from `use Spatie\TypeScriptTransformer\Attributes\TypeScript;` + +Generate the types with: +```sh +php artisan typescript:transform +``` + +### Language translations + +We use https://github.com/xiCO2k/laravel-vue-i18n diff --git a/lang/cz/aspect_ratio.php b/lang/cz/aspect_ratio.php new file mode 100644 index 00000000000..2c7e8fb56ac --- /dev/null +++ b/lang/cz/aspect_ratio.php @@ -0,0 +1,21 @@ + '5/4 (instagram landscape)', + '4by5' => '4/5 (instagram portrait)', + '2by3' => '2/3 (portrait)', + '3by2' => '3/2 (landscape)', + '1by1' => 'square', + '1byx9' => '16/9 (landscape)', +]; \ No newline at end of file diff --git a/lang/cz/diagnostics.php b/lang/cz/diagnostics.php new file mode 100644 index 00000000000..0fadd640428 --- /dev/null +++ b/lang/cz/diagnostics.php @@ -0,0 +1,30 @@ + 'Diagnostics', + + 'copy_to_clipboard' => 'Copy diagnostics to clipboard', + 'self-diagnosis' => 'Self-diagnosis', + 'info' => 'Info', + 'space' => 'Space', + 'load_space' => 'Load space usage.', + 'configuration' => 'Configuration', + 'loading' => 'Loading...', + 'identical_content' => 'Identical content', + + 'toast' => [ + 'info' => 'Info', + 'copy' => 'Diagnostics copied to clipboard!', + ], +]; \ No newline at end of file diff --git a/lang/cz/dialogs.php b/lang/cz/dialogs.php new file mode 100644 index 00000000000..4afd65fae3f --- /dev/null +++ b/lang/cz/dialogs.php @@ -0,0 +1,221 @@ + [ + 'close' => 'Close', + 'cancel' => 'Cancel', + 'save' => 'Save', + 'delete' => 'Delete', + 'move' => 'Move', + ], + 'about' => [ + 'subtitle' => 'Self-hosted photo-management done right', + 'description' => 'Lychee is a free photo-management tool, which runs on your server or web-space. Installing is a matter of seconds. Upload, manage and share photos like from a native application. Lychee comes with everything you need and all your photos are stored securely.', + 'update_available' => 'Update available!', + 'thank_you' => 'Thank you for your support!', + 'get_supporter_or_register' => 'Get exclusive features and support the development of Lychee.
Unlock the Supporter Edition or register your License key', + 'here' => 'here', + ], + 'dropbox' => [ + 'not_configured' => 'Dropbox is not configured.', + ], + 'import_from_link' => [ + 'instructions' => 'Please enter the direct link to a photo to import it:', + 'import' => 'Import', + ], + 'keybindings' => [ + 'don_t_show_again' => 'Don\'t show this again', + 'side_wide' => 'Site-wide Shortcuts', + 'back_cancel' => 'Back/Cancel', + 'confirm' => 'Confirm', + 'login' => 'Login', + 'toggle_full_screen' => 'Toggle Full Screen', + 'toggle_sensitive_albums' => 'Toggle Sensitive Albums', + + 'albums' => 'Albums Shortcuts', + 'new_album' => 'New Album', + 'upload_photos' => 'Upload Photos', + 'search' => 'Search', + 'show_this_modal' => 'Show this modal', + 'select_all' => 'Select All', + 'move_selection' => 'Move Selection', + 'delete_selection' => 'Delete Selection', + + 'album' => 'Album Shortcuts', + 'slideshow' => 'Start/Stop Slideshow', + 'toggle' => 'Toggle panel', + + 'photo' => 'Photo Shortcuts', + 'previous' => 'Previous photo', + 'next' => 'Next photo', + 'cycle' => 'Cycle overlay mode', + 'star' => 'Star the photo', + 'move' => 'Move the photo', + 'delete' => 'Delete the photo', + 'edit' => 'Edit information', + 'show_hide_meta' => 'Show information', + + 'keep_hidden' => 'We will keep it hidden.', + ], + 'login' => [ + 'username' => 'Username', + 'password' => 'Password', + 'unknown_invalid' => 'Unknown user or invalid password.', + 'signin' => 'Sign-In', + ], + 'register' => [ + 'enter_license' => 'Enter your license key below:', + 'license_key' => 'License key', + 'invalid_license' => 'Invalid license key.', + 'register' => 'Register', + ], + 'share_album' => [ + 'url_copied' => 'Copied URL to clipboard!', + ], + 'upload' => [ + 'completed' => 'Completed', + 'uploaded' => 'Uploaded:', + 'release' => 'Release file to upload!', + 'select' => 'Click here to select files to upload', + 'drag' => '(Or drag files to the page)', + 'loading' => 'Loading', + 'resume' => 'Resume', + 'uploading' => 'Uploading', + 'finished' => 'Finished', + 'failed_error' => 'Upload failed. The server returned an error!', + ], + 'visibility' => [ + 'public' => 'Public', + 'public_expl' => 'Anonymous users can access this album, subject to the restrictions below.', + 'full' => 'Original', + 'full_expl' => 'Anonymous users can view full-resolution photos.', + 'hidden' => 'Hidden', + 'hidden_expl' => 'Anonymous users need a direct link to access this album.', + 'downloadable' => 'Downloadable', + 'downloadable_expl' => 'Anonymous users can download this album.', + 'password' => 'Password', + 'password_prot' => 'Password protected', + 'password_prot_expl' => 'Anonymous users need a shared password to access this album.', + 'nsfw' => 'Sensitive', + 'nsfw_expl' => 'Album contains sensitive content.', + 'visibility_updated' => 'Visibility updated.', + ], + 'move_album' => [ + 'confirm_single' => 'Are you sure you want to move the album “%1$s” into the album “%2$s”?', + 'confirm_multiple' => 'Are you sure you want to move all selected albums into the album “%s”?', + 'move_single' => 'Move Album', + 'move_to' => 'Move to', + 'move_to_single' => 'Move %s to:', + 'move_to_multiple' => 'Move %d albums to:', + 'no_album_target' => 'No album to move to', + 'moved_single' => 'Album moved!', + 'moved_single_details' => '%1$s moved to %2$s', + 'moved_details' => 'Album(s) moved to %s', + ], + 'new_album' => [ + 'menu' => 'Create Album', + 'info' => 'Enter a title for the new album:', + 'title' => 'title', + 'create' => 'Create Album', + ], + 'new_tag_album' => [ + 'menu' => 'Create Tag Album', + 'info' => 'Enter a title for the new tag album:', + 'title' => 'title', + 'set_tags' => 'Set tags to show', + 'warn' => 'Make sure to press enter after each tag', + 'create' => 'Create Tag Album', + ], + 'delete_album' => [ + 'confirmation' => 'Are you sure you want to delete the album “%s” and all of the photos it contains?', + 'confirmation_multiple' => 'Are you sure you want to delete all %d selected albums and all of the photos they contain?', + 'warning' => 'This action can not be undone!', + 'delete' => 'Delete Album and Photos', + ], + 'transfer' => [ + 'query' => 'Transfer ownership of album to', + 'confirmation' => 'Are you sure you want to transfer the ownership of album “%s” and all the photos it contains to "%s"?', + 'lost_access_warning' => 'Your access to this album will be lost.', + 'warning' => 'This action can not be undone!', + 'transfer' => 'Transfer ownership of album and photos', + ], + 'rename' => [ + 'photo' => 'Enter a new title for this photo:', + 'album' => 'Enter a new title for this album:', + 'rename' => 'Rename', + ], + 'merge' => [ + 'merge_to' => 'Merge %s to:', + 'merge_to_multiple' => 'Merge %d albums to:', + 'no_albums' => 'No albums to merge to.', + 'confirm' => 'Are you sure you want to merge the album “%1$s” into the album “%2$s”?', + 'confirm_multiple' => 'Are you sure you want to merge all selected albums into the album “%s”?', + 'merge' => 'Merge Albums', + 'merged' => 'Album(s) merged to %s!', + ], + 'unlock' => [ + 'password_required' => 'This album is protected by a password. Enter the password below to view the photos of this album:', + 'password' => 'Password', + 'unlock' => 'Unlock', + ], + 'photo_tags' => [ + 'question' => 'Enter your tags for this photo.', + 'question_multiple' => 'Enter your tags for all %d selected photos. Existing tags will be overwritten.', + 'no_tags' => 'No Tags', + 'set_tags' => 'Set Tags', + 'updated' => 'Tags updated!', + 'tags_override_info' => 'If this is unchecked, the tags will be added to the existing tags of the photo.', + ], + 'photo_copy' => [ + 'no_albums' => 'No albums to copy to', + 'copy_to' => 'Copy %s to:', + 'copy_to_multiple' => 'Copy %d photos to:', + 'confirm' => 'Copy %s to %s.', + 'confirm_multiple' => 'Copy %d photos to %s.', + 'copy' => 'Copy', + 'copied' => 'Photo(s) copied!', + ], + 'photo_delete' => [ + 'confirm' => 'Are you sure you want to delete the photo “%s”?', + 'confirm_multiple' => 'Are you sure you want to delete all %d selected photos?', + 'deleted' => 'Photo(s) deleted!', + ], + 'move_photo' => [ + 'move_single' => 'Move %s to:', + 'move_multiple' => 'Move %d photos to:', + 'confirm' => 'Move %s to %s.', + 'confirm_multiple' => 'Move %d photos to %s.', + 'moved' => 'Photo(s) moved to %s!', + ], + 'target_user' => [ + 'placeholder' => 'Select user', + ], + 'target_album' => [ + 'placeholder' => 'Select album', + ], + 'webauthn' => [ + 'u2f' => 'U2F', + 'success' => 'Authentication successful!', + 'error' => 'Whoops, it looks like something went wrong. Please reload the site and try again!', + ], + 'se' => [ + 'available' => 'Available in the Supporter Edition', + ], + 'session_expired' => [ + 'title' => 'Session expired', + 'message' => 'Your session has expired.
Please reload the page.', + 'reload' => 'Reload', + 'go_to_gallery' => 'Go to the Gallery', + ], +]; \ No newline at end of file diff --git a/lang/cz/fix-tree.php b/lang/cz/fix-tree.php new file mode 100644 index 00000000000..64803e310e6 --- /dev/null +++ b/lang/cz/fix-tree.php @@ -0,0 +1,55 @@ + 'Maintenance', + 'intro' => 'This page allows you to re-order and fix your albums manually.
Before any modifications, we strongly recommend you to read about Nested Set tree structures.', + 'warning' => 'You can really break your Lychee installation here, modify values at your own risks.', + + 'help' => [ + 'header' => 'Help', + 'hover' => 'Hover ids or titles to highlight related albums.', + 'left' => 'Left', + 'right' => 'Right', + 'convenience' => 'For your convenience, the and buttons allow you to change the values of %s and %s by respectively +1 and -1 with propagation.', + 'left-right-warn' => 'The and indicates that the value of %s (and respectively %s) is duplicated somewhere.', + 'parent-marked' => 'Marked Parent Id indicates that the %s and %s do not satisfy the Nest Set tree structures. Edit either the Parent Id or the %s/%s values.', + 'slowness' => 'This page will be slow with a large number of albums.', + ], + + 'buttons' => [ + 'reset' => 'Reset', + 'check' => 'Check', + 'apply' => 'Apply', + ], + + 'table' => [ + 'title' => 'Title', + 'left' => 'Left', + 'right' => 'Right', + 'id' => 'Id', + 'parent' => 'Parent Id', + ], + + 'errors' => [ + 'invalid' => 'Invalid tree!', + 'invalid_details' => 'We are not applying this as it is guaranteed to be a broken state.', + 'invalid_left' => 'Album %s has an invalid left value.', + 'invalid_right' => 'Album %s has an invalid right value.', + 'invalid_left_right' => 'Album %s has an invalid left/right values. Left should be strictly smaller than right: %s < %s.', + 'duplicate_left' => 'Album %s has a duplicate left value %s.', + 'duplicate_right' => 'Album %s has a duplicate right value %s.', + 'parent' => 'Album %s has an unexpected parent id %s.', + 'unknown' => 'Album %s has an unknown error.', + ], +]; \ No newline at end of file diff --git a/lang/cz/gallery.php b/lang/cz/gallery.php new file mode 100644 index 00000000000..eb8008827e0 --- /dev/null +++ b/lang/cz/gallery.php @@ -0,0 +1,241 @@ + 'Gallery', + + 'smart_albums' => 'Smart albums', + 'albums' => 'Albums', + 'root' => 'Albums', + + 'original' => 'Original', + 'medium' => 'Medium', + 'medium_hidpi' => 'Medium HiDPI', + 'small' => 'Thumb', + 'small_hidpi' => 'Thumb HiDPI', + 'thumb' => 'Square thumb', + 'thumb_hidpi' => 'Square thumb HiDPI', + 'placeholder' => 'Low Quality Image Placeholder', + 'thumbnail' => 'Photo thumbnail', + 'live_video' => 'Video part of live-photo', + + 'camera_data' => 'Camera date', + 'album_reserved' => 'All Rights Reserved', + + 'map' => [ + 'error_gpx' => 'Error loading GPX file', + 'osm_contributors' => 'OpenStreetMap contributors', + ], + + 'search' => [ + 'title' => 'Search', + 'searching' => 'Searching…', + 'no_results' => 'Nothing matches your search query.', + 'searchbox' => 'Search…', + 'minimum_chars' => 'Minimum %s characters required.', + 'photos' => 'Photos (%s)', + 'albums' => 'Albums (%s)', + ], + + 'smart_album' => [ + 'unsorted' => 'Unsorted', + 'starred' => 'Starred', + 'recent' => 'Recent', + 'public' => 'Public', + 'on_this_day' => 'On This Day', + ], + + 'layout' => [ + 'squares' => 'Square thumbnails', + 'justified' => 'With aspect, justified', + 'masonry' => 'With aspect, masonry', + 'grid' => 'With aspect, grid', + ], + + 'overlay' => [ + 'none' => 'None', + 'exif' => 'EXIF data', + 'description' => 'Description', + 'date' => 'Date taken', + ], + + 'timeline' => [ + 'default' => 'default', + 'disabled' => 'disabled', + 'year' => 'Year', + 'month' => 'Month', + 'day' => 'Day', + 'hour' => 'Hour', + ], + + 'album' => [ + 'header_albums' => 'Albums', + 'header_photos' => 'Photos', + 'no_results' => 'Nothing to see here', + 'upload' => 'Upload photos', + + 'tabs' => [ + 'about' => 'About Album', + 'share' => 'Share Album', + 'move' => 'Move Album', + 'danger' => 'DANGER ZONE', + ], + + 'hero' => [ + 'created' => 'Created', + 'copyright' => 'Copyright', + 'subalbums' => 'Subalbums', + 'images' => 'Photos', + 'download' => 'Download Album', + 'share' => 'Share Album', + 'stats_only_se' => 'Statistics available in the Supporter Edition', + ], + + 'stats' => [ + 'lens' => 'Lens', + 'shutter' => 'Shutter speed', + 'iso' => 'ISO', + 'model' => 'Model', + 'aperture' => 'Aperture', + 'no_data' => 'No data', + ], + + 'properties' => [ + 'title' => 'Title', + 'description' => 'Description', + 'photo_ordering' => 'Order photos by', + 'children_ordering' => 'Order albums by', + 'asc/desc' => 'asc/desc', + 'header' => 'Set album header', + 'compact_header' => 'Use compact header', + 'license' => 'Set license', + 'copyright' => 'Set copyright', + 'aspect_ratio' => 'Set album thumbs aspect ratio', + 'album_timeline' => 'Set album timeline mode', + 'photo_timeline' => 'Set photo timeline mode', + 'layout' => 'Set photo layout', + 'show_tags' => 'Set tags to show', + 'tags_required' => 'Tags are required.', + ], + ], + + 'photo' => [ + 'actions' => [ + 'star' => 'Star', + 'unstar' => 'Unstar', + 'set_album_header' => 'Set as album header', + 'move' => 'Move', + 'delete' => 'Delete', + 'header_set' => 'Header set', + ], + + 'details' => [ + 'about' => 'About', + 'basics' => 'Basics', + 'title' => 'Title', + 'uploaded' => 'Uploaded', + 'description' => 'Description', + 'license' => 'License', + 'reuse' => 'Reuse', + 'latitude' => 'Latitude', + 'longitude' => 'Longitude', + 'altitude' => 'Altitude', + 'location' => 'Location', + 'image' => 'Image', + 'video' => 'Video', + 'size' => 'Size', + 'format' => 'Format', + 'resolution' => 'Resolution', + 'duration' => 'Duration', + 'fps' => 'Frame rate', + 'tags' => 'Tags', + 'camera' => 'Camera', + 'captured' => 'Captured', + 'make' => 'Make', + 'type' => 'Type/Model', + 'lens' => 'Lens', + 'shutter' => 'Shutter Speed', + 'aperture' => 'Aperture', + 'focal' => 'Focal Length', + 'iso' => 'ISO %s', + ], + + 'edit' => [ + 'set_title' => 'Set Title', + 'set_description' => 'Set Description', + 'set_license' => 'Set License', + 'no_tags' => 'No Tags', + 'set_tags' => 'Set Tags', + 'set_created_at' => 'Set Upload Date', + ], + ], + + 'nsfw' => [ + 'header' => 'Sensitive content', + 'description' => 'This album contains sensitive content which some people may find offensive or disturbing.', + 'consent' => 'Tap to consent.', + ], + + 'menus' => [ + 'star' => 'Star', + 'unstar' => 'Unstar', + 'star_all' => 'Star Selected', + 'unstar_all' => 'Unstar Selected', + 'tag' => 'Tag', + 'tag_all' => 'Tag Selected', + 'set_cover' => 'Set Album Cover', + 'remove_header' => 'Remove Album Header', + 'set_header' => 'Set Album Header', + 'copy_to' => 'Copy to …', + 'copy_all_to' => 'Copy Selected to …', + 'rename' => 'Rename', + 'move' => 'Move', + 'move_all' => 'Move Selected', + 'delete' => 'Delete', + 'delete_all' => 'Delete Selected', + 'download' => 'Download', + 'download_all' => 'Download Selected', + 'merge' => 'Merge', + 'merge_all' => 'Merge Selected', + + 'upload_photo' => 'Upload Photo', + 'import_link' => 'Import from Link', + 'import_dropbox' => 'Import from Dropbox', + 'new_album' => 'New Album', + 'new_tag_album' => 'New Tag Album', + 'upload_track' => 'Upload track', + 'delete_track' => 'Delete track', + ], + + 'sort' => [ + 'photo_select_1' => 'Upload Time', + 'photo_select_2' => 'Take Date', + 'photo_select_3' => 'Title', + 'photo_select_4' => 'Description', + 'photo_select_6' => 'Star', + 'photo_select_7' => 'Photo Format', + 'ascending' => 'Ascending', + 'descending' => 'Descending', + 'album_select_1' => 'Creation Time', + 'album_select_2' => 'Title', + 'album_select_3' => 'Description', + 'album_select_5' => 'Latest Take Date', + 'album_select_6' => 'Oldest Take Date', + ], + + 'albums_protection' => [ + 'private' => 'private', + 'public' => 'public', + 'inherit_from_parent' => 'inherit from parent', + ], +]; \ No newline at end of file diff --git a/lang/cz/jobs.php b/lang/cz/jobs.php new file mode 100644 index 00000000000..5d952b76012 --- /dev/null +++ b/lang/cz/jobs.php @@ -0,0 +1,18 @@ + 'Jobs', + + 'no_data' => 'No Jobs have been executed yet.', +]; \ No newline at end of file diff --git a/lang/cz/landing.php b/lang/cz/landing.php new file mode 100644 index 00000000000..fe6fe55b8ea --- /dev/null +++ b/lang/cz/landing.php @@ -0,0 +1,19 @@ + 'Gallery', + 'access_gallery' => 'Access the gallery', + 'hosted_with_lychee' => 'Hosted with Lychee', + 'copyright' => 'All images on this website are subject to copyright by %1$s © %2$s', +]; \ No newline at end of file diff --git a/lang/cz/left-menu.php b/lang/cz/left-menu.php new file mode 100644 index 00000000000..9a3e91f4037 --- /dev/null +++ b/lang/cz/left-menu.php @@ -0,0 +1,29 @@ + 'Back to Gallery', + + 'admin' => 'Admin', + 'clockwork' => 'Clockwork App', + 'logs' => 'Show Logs', + 'jobs' => 'Show Job History', + 'user' => 'User', + + 'sign_out' => 'Sign Out', + + 'about' => 'About', + 'api' => 'API Documentation', + 'source_code' => 'Source Code', + 'support' => 'Support', +]; \ No newline at end of file diff --git a/lang/cz/lychee.php b/lang/cz/lychee.php new file mode 100644 index 00000000000..0af9be02b52 --- /dev/null +++ b/lang/cz/lychee.php @@ -0,0 +1,535 @@ + 'Uživatelské jméno', + 'PASSWORD' => 'Heslo', + 'ENTER' => 'Vložit', + 'CANCEL' => 'Storno', + 'CONFIRM' => 'Confirm', + 'SIGN_IN' => 'Příhlásit se', + 'CLOSE' => 'Zavřít', + 'SETTINGS' => 'Nastavení', + 'SEARCH' => 'Hledat …', + 'MORE' => 'Rozšířená nastavení', + 'DEFAULT' => 'Default', + 'GALLERY' => 'Gallery', + + 'USERS' => 'Uživatelé', + 'PROFILE' => 'Profile', + 'CREATE' => 'Create', + 'REMOVE' => 'Remove', + 'SHARE' => 'Share', + 'U2F' => 'U2F', + 'NOTIFICATIONS' => 'Notifications', + 'SHARING' => 'Sdílení', + 'CHANGE_LOGIN' => 'Změnit přihlášení', + 'CHANGE_SORTING' => 'Změnt řazení', + 'SET_DROPBOX' => 'Nastavit Dropbox', + 'ABOUT_LYCHEE' => 'O Lychee', + 'DIAGNOSTICS' => 'Diagnostika', + 'DIAGNOSTICS_GET_SIZE' => 'Request space usage', + 'JOBS' => 'Show job history', + 'LOGS' => 'Protokoly', + 'SIGN_OUT' => 'Odhlásit se', + 'UPDATE_AVAILABLE' => 'Update je k dispozici!', + 'MIGRATION_AVAILABLE' => 'Migration available!', + 'CHECK_FOR_UPDATE' => 'Check for updates', + 'DEFAULT_LICENSE' => 'Výchozí licence pro nové uploady:', + 'SET_LICENSE' => 'Nastavit licenci', + 'SET_OVERLAY_TYPE' => 'Nastavit překrytí', + 'SET_ALBUM_DECORATION' => 'Set album decorations', + 'SET_MAP_PROVIDER' => 'Nastavit providera OpenStreetMap', + 'FULL_SETTINGS' => 'Full Settings', + 'UPDATE' => 'Update', + 'RESET' => 'Reset', + 'DISABLE_TOKEN_TOOLTIP' => 'Disable', + 'ENABLE_TOKEN' => 'Enable API token', + 'DISABLED_TOKEN_STATUS_MSG' => 'Disabled', + 'TOKEN_BUTTON' => 'API Token ...', + 'TOKEN_NOT_AVAILABLE' => 'You have already viewed this token.', + 'TOKEN_WAIT' => 'Wait ...', + + 'SMART_ALBUMS' => 'Chytrá alba', + 'SHARED_ALBUMS' => 'Sdílená alba', + 'ALBUMS' => 'Alba', + 'PHOTOS' => 'Obrázky', + 'SEARCH_RESULTS' => 'Výsledky hledání', + + 'RENAME' => 'Přejmenovat', + 'RENAME_ALL' => 'Přejmenovat vybrané', + 'MERGE' => 'Sloučit', + 'MERGE_ALL' => 'Sloučit vybrané', + 'MAKE_PUBLIC' => 'Zveřejnit', + 'SHARE_ALBUM' => 'Sdílet album', + 'SHARE_PHOTO' => 'Sdílet fotografii', + 'VISIBILITY_ALBUM' => 'Viditelnost alba', + 'VISIBILITY_PHOTO' => 'Viditelnost fotografie', + 'DOWNLOAD_ALBUM' => 'Stáhnout album', + 'ABOUT_ALBUM' => 'O albu', + 'DELETE_ALBUM' => 'Smazat album', + 'MOVE_ALBUM' => 'Přesunout album', + 'FULLSCREEN_ENTER' => 'Spustit režim celé obrazovky', + 'FULLSCREEN_EXIT' => 'Ukončit režim celé obrazovky', + + 'SHARING_ALBUM_USERS' => 'Share this album with users', + 'WAIT_FETCH_DATA' => 'Please wait while we get the data …', + 'SHARING_ALBUM_USERS_NO_USERS' => 'There are no users to share the album with', + 'SHARING_ALBUM_USERS_LONG_MESSAGE' => 'Select the users to share this album with', + + 'DELETE_ALBUM_QUESTION' => 'Mazání alba a fotografie', + 'KEEP_ALBUM' => 'Ponechat album', + 'DELETE_ALBUM_CONFIRMATION' => 'Opravdu smazat album „%s“ a všechny fotografie, které obsahuje? Tento krok je nevratný!', + + 'DELETE_TAG_ALBUM_QUESTION' => 'Delete Album', + 'DELETE_TAG_ALBUM_CONFIRMATION' => 'Are you sure you want to delete the album „%s“ (any photos inside will not be deleted)? This action can’t be undone!', + + 'DELETE_ALBUMS_QUESTION' => 'Mazání alb a fotografií', + 'KEEP_ALBUMS' => 'Ponechat alba', + 'DELETE_ALBUMS_CONFIRMATION' => 'Opravdu smazat všechna vybraná %d alba a fotografie, které obsahují? Tento krok je nevratný!', + + 'DELETE_UNSORTED_CONFIRM' => 'Opravdu odstranit všechny „Nesetříděné“ fotografie? Tento krok je nevratný!', + 'CLEAR_UNSORTED' => 'Odstranit Nesetříděné', + 'KEEP_UNSORTED' => 'Ponechat Nesetříděné', + + 'EDIT_SHARING' => 'Upravit sdílení', + 'MAKE_PRIVATE' => 'Nastavit jako privátní', + + 'CLOSE_ALBUM' => 'Zavřít album', + 'CLOSE_PHOTO' => 'Zavřít fotografii', + 'CLOSE_MAP' => 'Zavřít mapu', + + 'ADD' => 'Přidat', + 'MOVE' => 'Přesunout', + 'MOVE_ALL' => 'Přesunout vybrané', + 'DUPLICATE' => 'Duplikovat', + 'DUPLICATE_ALL' => 'Duplikovat vybrané', + 'COPY_TO' => 'Kopírovat do …', + 'COPY_ALL_TO' => 'Kopírovat vybrané do …', + 'DELETE' => 'Odstranit', + 'SAVE' => 'Save', + 'DELETE_ALL' => 'Odstranit vybrané', + 'DOWNLOAD' => 'Stáhnout', + 'DOWNLOAD_ALL' => 'Stánout vybrané', + 'UPLOAD_PHOTO' => 'Odeslat fotografii', + 'IMPORT_LINK' => 'Importovat z odkazu', + 'IMPORT_DROPBOX' => 'Importovat z Dropboxu', + 'IMPORT_SERVER' => 'Importovat ze serveru', + 'NEW_ALBUM' => 'Nové album', + 'NEW_TAG_ALBUM' => 'Nové tag album', + 'UPLOAD_TRACK' => 'Upload track', + 'DELETE_TRACK' => 'Delete track', + + 'TITLE_NEW_ALBUM' => 'Zadejte název nového alba:', + 'UNTITLED' => 'Bezejmanné', + 'UNSORTED' => 'Nesetříděné', + 'STARRED' => 'Oblíbené', + 'RECENT' => 'Poslední', + 'PUBLIC' => 'Veřejné', + 'ON_THIS_DAY' => 'On This Day', + 'NUM_PHOTOS' => 'fotografií', + + 'CREATE_ALBUM' => 'Vytvořit album', + 'CREATE_TAG_ALBUM' => 'Vytvořit Tag album', + + 'STAR_PHOTO' => 'Označit jako oblíbené', + 'STAR' => 'Označit jako oblíbené', + 'UNSTAR' => 'Unstar', + 'STAR_ALL' => 'Vše označit jako oblíbené', + 'UNSTAR_ALL' => 'Unstar Selected', + 'TAG' => 'Štítek', + 'TAG_ALL' => 'Oštítkovat vše', + 'UNSTAR_PHOTO' => 'Odebrat z oblíbených', + 'SET_COVER' => 'Set Album Cover', + 'REMOVE_COVER' => 'Remove Album Cover', + 'SET_HEADER' => 'Set Album Header', + 'REMOVE_HEADER' => 'Remove Album Header', + 'SET_COMPACT_HEADER' => 'Use Compact Header', + + 'FULL_PHOTO' => 'Otevřít originál', + 'ABOUT_PHOTO' => 'O fotografii', + 'DISPLAY_FULL_MAP' => 'Mapa', + 'DIRECT_LINK' => 'Přímý odkaz', + 'DIRECT_LINKS' => 'Přímé odkazy', + 'QR_CODE' => 'QR Code', + + 'ALBUM_ABOUT' => 'O albu', + 'ALBUM_BASICS' => 'Základní informace', + 'ALBUM_TITLE' => 'Název', + 'ALBUM_COPYRIGHT' => 'Copyright', + 'ALBUM_SET_COPYRIGHT' => 'Set copyright', + 'ALBUM_NEW_TITLE' => 'Zadat nový název alba:', + 'ALBUMS_NEW_TITLE' => 'Zadat nový název pro %d vybraná alba:', + 'ALBUM_SET_TITLE' => 'Uložit název', + 'ALBUM_DESCRIPTION' => 'Popis', + 'ALBUM_SHOW_TAGS' => 'Zobrazené tagy', + 'ALBUM_NEW_DESCRIPTION' => 'Zadat nový popis pro album:', + 'ALBUM_SET_DESCRIPTION' => 'Uložit popis', + 'ALBUM_NEW_SHOWTAGS' => 'Zadejte tagy fotografií, které budou viditelné v albu:', + 'ALBUM_SET_SHOWTAGS' => 'Tagy k zobrazení', + 'ALBUM_ALBUM' => 'Album', + 'ALBUM_CREATED' => 'Vytvořeno', + 'ALBUM_IMAGES' => 'Obrázky', + 'ALBUM_VIDEOS' => 'Videa', + 'ALBUM_SUBALBUMS' => 'Subalba', + 'ALBUM_SHARING' => 'Sdílení', + 'ALBUM_SHR_YES' => 'Ano', + 'ALBUM_SHR_NO' => 'Ne', + 'ALBUM_PUBLIC' => 'Veřejné', + 'ALBUM_PUBLIC_EXPL' => 'Anonymous users can access this album, subject to the restrictions below.', + 'ALBUM_FULL' => 'Originál', + 'ALBUM_FULL_EXPL' => 'Anonymous users can behold full-resolution photos.', + 'ALBUM_HIDDEN' => 'Skryté', + 'ALBUM_HIDDEN_EXPL' => 'Anonymous users need a direct link to access this album.', + 'ALBUM_MARK_NSFW' => 'Mark album as sensitive', + 'ALBUM_UNMARK_NSFW' => 'Unmark album as sensitive', + 'ALBUM_NSFW' => 'Sensitive', + 'ALBUM_NSFW_EXPL' => 'Album is marked to contain sensitive content.', + 'ALBUM_DOWNLOADABLE' => 'Stažitelné', + 'ALBUM_DOWNLOADABLE_EXPL' => 'Anonymous users can download this album.', + 'ALBUM_SHARE_BUTTON_VISIBLE' => 'Tlačítko sdílet je viditelné', + 'ALBUM_SHARE_BUTTON_VISIBLE_EXPL' => 'Anonymous users see social media sharing links.', + 'ALBUM_PASSWORD' => 'Heslo', + 'ALBUM_PASSWORD_PROT' => 'Chráněné heslem', + 'ALBUM_PASSWORD_PROT_EXPL' => 'Anonymous users need a shared password to access this album.', + 'ALBUM_PASSWORD_REQUIRED' => 'Toto album je chráněno heslem. K jeho zobrazení zadejte prosím platné heslo:', + 'ALBUM_MERGE' => 'Sloučení vybraných alb „%1$s“ do jednoho alba „%2$s“?', + 'ALBUMS_MERGE' => 'Sloučit „%s“?', + 'MERGE_ALBUM' => 'Sloučit alba', + 'DONT_MERGE' => 'Neslučovat', + 'ALBUM_MOVE' => 'Opravdu přesunout album „%1$s“ do alba „%2$s“?', + 'ALBUMS_MOVE' => 'Opravdu přesunout vybraná alba do alba „%s“?', + 'MOVE_ALBUMS' => 'Přesunout album', + 'NOT_MOVE_ALBUMS' => 'Nepřesouvat', + 'ROOT' => 'Alba', + 'ALBUM_REUSE' => 'Použití', + 'ALBUM_LICENSE' => 'Licence', + 'ALBUM_SET_LICENSE' => 'Nastavit licenci', + 'ALBUM_LICENSE_HELP' => 'Potřebujete pomoci s výběrem?', + 'ALBUM_LICENSE_NONE' => 'Žádná', + 'ALBUM_RESERVED' => 'Všechna práva vyhrazena', + 'ALBUM_SET_ORDER' => 'Set Order', + 'ALBUM_ORDERING' => 'Order by', + 'ALBUM_PHOTO_ORDERING' => 'Order photos by', + 'ALBUM_CHILDREN_ORDERING' => 'Order albums by', + 'ALBUM_OWNER' => 'Owner', + + 'PHOTO_ABOUT' => 'O fotografii', + 'PHOTO_BASICS' => 'Základní informace', + 'PHOTO_TITLE' => 'Název', + 'PHOTO_NEW_TITLE' => 'Zadat nový název fotografie:', + 'PHOTO_SET_TITLE' => 'Uložit název', + 'PHOTO_UPLOADED' => 'Odesláno', + 'PHOTO_DESCRIPTION' => 'Popis', + 'PHOTO_NEW_DESCRIPTION' => 'Zadejte nový název pro tuto fotografii:', + 'PHOTO_SET_DESCRIPTION' => 'Uložit popis', + 'PHOTO_NEW_LICENSE' => 'Přidat licenci', + 'PHOTO_SET_LICENSE' => 'Uložit licenci', + 'PHOTO_LICENSE' => 'Licence', + 'PHOTO_LICENSE_HELP' => 'Need help choosing?', + 'PHOTO_REUSE' => 'Opakované použití', + 'PHOTO_LICENSE_NONE' => 'Žádná', + 'PHOTO_RESERVED' => 'Všechna práva vyhrazena', + 'PHOTO_LATITUDE' => 'Zeměpisná šířka', + 'PHOTO_LONGITUDE' => 'Zeměpisná délka', + 'PHOTO_ALTITUDE' => 'Nadmořská výška', + 'PHOTO_IMGDIRECTION' => 'Směr', + 'PHOTO_LOCATION' => 'Location', + 'PHOTO_IMAGE' => 'Fotografie', + 'PHOTO_VIDEO' => 'Video', + 'PHOTO_SIZE' => 'Velikost', + 'PHOTO_FORMAT' => 'Formát', + 'PHOTO_RESOLUTION' => 'Rozlišení', + 'PHOTO_DURATION' => 'Trvání', + 'PHOTO_FPS' => 'Frekvence snímků', + 'PHOTO_TAGS' => 'Štítky', + 'PHOTO_NOTAGS' => 'Bez štítků', + 'PHOTO_NEW_TAGS' => 'Zadejte štítky pro tento obrázek. Jednotlivé štítky oddělte čárkou:', + 'PHOTOS_NEW_TAGS' => 'Zadejte štítky pro všechny %d vybrané fotografie. Stávající štítky budou přepsány. Jednotlivé štítky oddělte čárkou:', + 'PHOTO_SET_TAGS' => 'Uložit štítky', + 'PHOTO_CAMERA' => 'Fotoaparát', + 'PHOTO_CAPTURED' => 'Pořízeno', + 'PHOTO_MAKE' => 'Značka', + 'PHOTO_TYPE' => 'Typ/model', + 'PHOTO_LENS' => 'Objektiv', + 'PHOTO_SHUTTER' => 'Uzávěrka', + 'PHOTO_APERTURE' => 'Clona', + 'PHOTO_FOCAL' => 'Fokus', + 'PHOTO_ISO' => 'ISO %s', + 'PHOTO_SHARING' => 'Sdílet', + 'PHOTO_DELETE' => 'Odstranit fotografii', + 'PHOTO_KEEP' => 'Ponechat fotografii', + 'PHOTO_DELETE_CONFIRMATION' => 'Opravdu odstranit fotografii „%s“? Tento krok je nevratný!', + 'PHOTO_DELETE_ALL' => 'Opravdu odstranit všechny %d vybrané fotografie? Tento krok je nevratný!', + 'PHOTOS_NEW_TITLE' => 'Zadejte nový název pro všechny %d vybrané fotografie:', + 'PHOTO_MAKE_PRIVATE_ALBUM' => 'Tato fotografie je umístěna ve veřejném albu. Fotografii jako veřejnou nebo soukromou musíte nastavit v albu, v nemž je umístěna.', + 'PHOTO_SHOW_ALBUM' => 'Zobrazit album', + 'PHOTO_PUBLIC' => 'Veřejné', + 'PHOTO_PUBLIC_EXPL' => 'Anonymous users can view this photo, subject to the restrictions below.', + 'PHOTO_FULL' => 'Originál', + 'PHOTO_FULL_EXPL' => 'Anonymous users can behold full-resolution photo.', + 'PHOTO_HIDDEN' => 'Skrytá', + 'PHOTO_HIDDEN_EXPL' => 'Anonymous users need a direct link to view this photo.', + 'PHOTO_DOWNLOADABLE' => 'Stažitelná', + 'PHOTO_DOWNLOADABLE_EXPL' => 'Anonymous users may download this photo.', + 'PHOTO_SHARE_BUTTON_VISIBLE' => 'Share button is visible', + 'PHOTO_SHARE_BUTTON_VISIBLE_EXPL' => 'Anonymous users can see social media sharing links.', + 'PHOTO_PASSWORD_PROT' => 'Zabezpečená heslem', + 'PHOTO_PASSWORD_PROT_EXPL' => 'Anonymous users need a shared password to view this photo.', + 'PHOTO_EDIT_SHARING_TEXT' => 'Vlastnosti sdílení fotografie budou změněny takto:', + 'PHOTO_NO_EDIT_SHARING_TEXT' => 'Protože je tato fotografie umístěna ve veřejném albu, zdědí i nastavení tohoto veřejného alba. Aktuální stav viditelnosti je uveden pouze pro informaci.', + 'PHOTO_EDIT_GLOBAL_SHARING_TEXT' => 'Viditelnost této fotografie lze doladit pomocí globálních nastavení. Aktuální stav viditelnosti je uveden pouze pro informaci.', + 'PHOTO_NEW_CREATED_AT' => 'Enter the upload date for this photo. mm/dd/yyyy, hh:mm [am/pm]', + 'PHOTO_SET_CREATED_AT' => 'Set upload date', + + 'LOADING' => 'Probíhá příprava', + 'ERROR' => 'Chyba', + 'ERROR_TEXT' => 'Něco není v pořádku. Obnovte stránku a postup zopakujte!', + 'ERROR_UNKNOWN' => 'Neočekávaná chyba. Postup prosím opakujte a ujistěte se o správnosti instalace na serveru. Další informace jsou k dispozici v souboru README.', + 'ERROR_MAP_DEACTIVATED' => 'Funkce Mapy byla v nastavení deaktivována.', + 'ERROR_SEARCH_DEACTIVATED' => 'Funkce hledání byla v nastavení deaktivována.', + 'SUCCESS' => 'OK', + 'CHANGE_SUCCESS' => 'Change successful.', + 'RETRY' => 'Opakovat', + 'OVERRIDE' => 'Override', + 'TAGS_OVERRIDE_INFO' => 'If this is unchecked, the tags will be added to the existing tags of the photo.', + + 'SETTINGS_SUCCESS_LOGIN' => 'Přihlašovací údaje byly aktualizovány.', + 'SETTINGS_SUCCESS_SORT' => 'Stav řazení byl aktulizován.', + 'SETTINGS_SUCCESS_DROPBOX' => 'Dropbox Key byl aktualizován.', + 'SETTINGS_SUCCESS_LANG' => 'Jazyk byl aktualizován', + 'SETTINGS_SUCCESS_LAYOUT' => 'Vzhled byl aktualizován', + 'SETTINGS_SUCCESS_IMAGE_OVERLAY' => 'EXIF překryv byl aktulizován', + 'SETTINGS_SUCCESS_PUBLIC_SEARCH' => 'Veřejné vyhledávání bylo aktulizováno', + 'SETTINGS_SUCCESS_LICENSE' => 'Výchozí licence byla aktualizována', + 'SETTINGS_SUCCESS_MAP_DISPLAY' => 'Nastavení zobrazeni Map bylo aktualizováno', + 'SETTINGS_SUCCESS_MAP_DISPLAY_PUBLIC' => 'Nastavení zobrazeni Map pro veřejná alba bylo aktualizováno', + 'SETTINGS_SUCCESS_MAP_PROVIDER' => 'Poskytovatel Map byl aktualizován', + 'SETTINGS_SUCCESS_CSS' => 'CSS aktualizováno', + 'SETTINGS_SUCCESS_JS' => 'JS aktualizováno', + 'SETTINGS_SUCCESS_UPDATE' => 'Nastavení úspešně aktualizováno', + 'SETTINGS_DROPBOX_KEY' => 'Dropbox API Key', + 'SETTINGS_ADVANCED_WARNING_EXPL' => 'Změna rozšířených nastavení může mít negativní vliv na stabilitu, bezpečnost a rychlost Lychee. Měňte pouze to, co opravdu dobře chápete.', + 'SETTINGS_ADVANCED_SAVE' => 'Uložit změny, rizika jsou mi známa!', + + 'U2F_NOT_SUPPORTED' => 'U2F not supported. Sorry.', + 'U2F_NOT_SECURE' => 'Environment not secured. U2F not available.', + 'U2F_REGISTER_KEY' => 'Register new device.', + 'U2F_REGISTRATION_SUCCESS' => 'Registration successful!', + 'U2F_AUTHENTIFICATION_SUCCESS' => 'Authentication successful!', + 'U2F_CREDENTIALS' => 'Credentials', + 'U2F_CREDENTIALS_DELETED' => 'Credentials deleted!', + 'U2F_LOGIN' => 'Log in with WebAuthn', + + 'NEW_PHOTOS_NOTIFICATION' => 'Send new photos notification emails.', + 'SETTINGS_SUCCESS_NEW_PHOTOS_NOTIFICATION' => 'New photos notification updated', + 'USER_EMAIL_INSTRUCTION' => 'Add your email below to enable receiving email notifications. To stop receiving emails, simply remove your email below.', + + 'LOGIN_USERNAME' => 'Jméno uživatele', + 'LOGIN_PASSWORD' => 'Heslo', + 'LOGIN_PASSWORD_CONFIRM' => 'Zopakujte heslo', + 'PASSWORD_TITLE' => 'Zadejte aktuální heslo:', + 'PASSWORD_CURRENT' => 'Aktuální heslo', + 'PASSWORD_TEXT' => 'Vaše uživatelské jméno a heslo budou změněny následovně:', + 'PASSWORD_CHANGE' => 'Změnit přihlášení', + + 'EDIT_SHARING_TITLE' => 'Editace sdílení', + 'EDIT_SHARING_TEXT' => 'Vlastnosti sdílení tohoto alba budou změněny následovně:', + 'SHARE_ALBUM_TEXT' => 'Album bude sdíleno s následujícími parametry:', + + 'SORT_DIALOG_ATTRIBUTE_LABEL' => 'Attribute', + 'SORT_DIALOG_ORDER_LABEL' => 'Order', + + 'SORT_ALBUM_BY' => 'Řadit alba podle %1$s ve %2$s pořadí.', + + 'SORT_ALBUM_SELECT_1' => 'Data vytvoření', + 'SORT_ALBUM_SELECT_2' => 'Názvu', + 'SORT_ALBUM_SELECT_3' => 'Popisu', + 'SORT_ALBUM_SELECT_5' => 'Nejmladšího data snímku', + 'SORT_ALBUM_SELECT_6' => 'Nejstaršího data snímku', + + 'SORT_PHOTO_BY' => 'Řadit alba podle %1$s ve %2$s pořadí.', + + 'SORT_PHOTO_SELECT_1' => 'Data uložení', + 'SORT_PHOTO_SELECT_2' => 'Data záznamu', + 'SORT_PHOTO_SELECT_3' => 'Názvu', + 'SORT_PHOTO_SELECT_4' => 'Popisu', + 'SORT_PHOTO_SELECT_6' => 'Oblíbenosti', + 'SORT_PHOTO_SELECT_7' => 'Formátu', + + 'SORT_ASCENDING' => 'Vzestupném', + 'SORT_DESCENDING' => 'Sestupném', + 'SORT_CHANGE' => 'Změnit řazení', + + 'DROPBOX_TITLE' => 'Dropbox - nastavení', + 'DROPBOX_TEXT' => "Pro uspěšný import fotografií z Dropboxu je řeba platný API klíč, který lze získat na stránkách Dropboxu. Vygenerovaný osobní klíč zadejte níže:", + + 'LANG_TEXT' => 'Změnit jazyk Lychee na:', + 'LANG_TITLE' => 'Změnit jazyk', + + 'SETTING_RECENT_PUBLIC_TEXT' => 'Make "Recent" smart album accessible to anonymous users', + 'SETTING_STARRED_PUBLIC_TEXT' => 'Make "Starred" smart album accessible to anonymous users', + 'SETTING_ONTHISDAY_PUBLIC_TEXT' => 'Make "On This Day" smart album accessible to anonymous users', + + 'CSS_TEXT' => 'Vlastní CSS:', + 'CSS_TITLE' => 'Změnit CSS', + 'JS_TEXT' => 'Vlastní JS:', + 'JS_TITLE' => 'Změnit JS', + 'PUBLIC_SEARCH_TEXT' => 'Veřejné vyhledávání povoleno:', + 'OVERLAY_TYPE' => 'Data, která budou použita na překryvu:', + 'OVERLAY_NONE' => 'None', + 'OVERLAY_EXIF' => 'EXIF data', + 'OVERLAY_DESCRIPTION' => 'Popis', + 'OVERLAY_DATE' => 'Datum pořízení', + 'ALBUM_DECORATION' => 'Album decorations:', + 'ALBUM_DECORATION_NONE' => 'None', + 'ALBUM_DECORATION_ORIGINAL' => 'Sub-album marker', + 'ALBUM_DECORATION_ALBUM' => 'Number of sub-albums', + 'ALBUM_DECORATION_PHOTO' => 'Number of photos', + 'ALBUM_DECORATION_ALL' => 'Number of sub-albums and photos', + 'ALBUM_DECORATION_ORIENTATION' => 'Orientation of album decorations:', + 'ALBUM_DECORATION_ORIENTATION_ROW' => 'Horizontal (photos, albums)', + 'ALBUM_DECORATION_ORIENTATION_ROW_REVERSE' => 'Horizontal (albums, photos)', + 'ALBUM_DECORATION_ORIENTATION_COLUMN' => 'Vertical (top photos, albums)', + 'ALBUM_DECORATION_ORIENTATION_COLUMN_REVERSE' => 'Vertical (top albums, photos)', + 'MAP_DISPLAY_TEXT' => 'Povolit Mapy (poskytovatel OpenStreetMap):', + 'MAP_DISPLAY_PUBLIC_TEXT' => 'Povolit mapy pro veřejná alba (poskytovatel OpenStreetMap):', + 'MAP_PROVIDER' => 'Poskytovatel OpenStreetMap názvů:', + 'MAP_PROVIDER_WIKIMEDIA' => 'Wikimedia', + 'MAP_PROVIDER_OSM_ORG' => 'OpenStreetMap.org (bez HiDPI)', + 'MAP_PROVIDER_OSM_DE' => 'OpenStreetMap.de (bez HiDPI)', + 'MAP_PROVIDER_OSM_FR' => 'OpenStreetMap.fr (bez HiDPI)', + 'MAP_PROVIDER_RRZE' => 'Universita v Erlangenu, Německo (pouze HiDPI)', + 'MAP_INCLUDE_SUBALBUMS_TEXT' => 'Včetně fotografií v subalbech:', + 'LOCATION_DECODING' => 'Přeložít GPS data na název místa', + 'LOCATION_SHOW' => 'Zobrazit název místa', + 'LOCATION_SHOW_PUBLIC' => 'Zobrazit název místa v public módu', + + 'LAYOUT_TYPE' => 'Vzhled fotografií:', + 'LAYOUT_SQUARES' => 'Čtvercové náhledy', + 'LAYOUT_JUSTIFIED' => 'V poměru stran, zarovnáno', + 'LAYOUT_MASONRY' => 'V poměru stran, masonry', + 'LAYOUT_GRID' => 'V poměru stran, grid', + 'LAYOUT_UNJUSTIFIED' => 'V poměru stran, nezarovnáno', + 'SET_LAYOUT' => 'Změnit vzhled', + + 'NSFW_VISIBLE_TEXT_1' => 'Make Sensitive albums visible by default.', + 'NSFW_VISIBLE_TEXT_2' => 'If the album is public, it is still accessible, just hidden from the view and can be revealed by pressing H.', + 'SETTINGS_SUCCESS_NSFW_VISIBLE' => 'Default sensitive album visibility updated with success.', + + 'NSFW_BANNER' => '

Sensitive content

This album contains sensitive content which some people may find offensive or disturbing.

Tap to consent.

', + 'NSFW_HEADER' => 'Sensitive content', + 'NSFW_EXPLANATION' => 'This album contains sensitive content which some people may find offensive or disturbing.', + 'TAP_CONSENT' => 'Tap to consent.', + + 'VIEW_NO_RESULT' => 'Bez výsledku', + 'VIEW_NO_PUBLIC_ALBUMS' => 'Veřejná alba nejsou k dispozici', + 'VIEW_NO_CONFIGURATION' => 'Žádná konfigurace', + 'VIEW_PHOTO_NOT_FOUND' => 'Fotografie nenalezena', + + 'NO_TAGS' => 'Žádné štítky', + + 'UPLOAD_MANAGE_NEW_PHOTOS' => 'Nyní můžete spravovat nové nové obrázky.', + 'UPLOAD_COMPLETE' => 'Upload dokončen', + 'UPLOAD_COMPLETE_FAILED' => 'Chyba při uploadu jedné nebo více fotografií.', + 'UPLOAD_IMPORTING' => 'Import', + 'UPLOAD_IMPORTING_URL' => 'URL pro import', + 'UPLOAD_UPLOADING' => 'Probíhá upload', + 'UPLOAD_FINISHED' => 'Dokončeno', + 'UPLOAD_PROCESSING' => 'Zpracovává se', + 'UPLOAD_FAILED' => 'Selhání', + 'UPLOAD_FAILED_ERROR' => 'Upload selhal. Server vrátil chybu!', + 'UPLOAD_FAILED_WARNING' => 'Upload selhal. Server vrátil upozornění!', + 'UPLOAD_CANCELLED' => 'Cancelled', + 'UPLOAD_SKIPPED' => 'Vynecháno', + 'UPLOAD_UPDATED' => 'Updated', + 'UPLOAD_GENERAL' => 'General', + 'UPLOAD_IMPORT_SKIPPED_DUPLICATE' => 'This photo has been skipped because it’s already in your library.', + 'UPLOAD_IMPORT_RESYNCED_DUPLICATE' => 'This photo has been skipped because it’s already in your library, but its metadata has been updated.', + 'UPLOAD_ERROR_CONSOLE' => 'Podrobnosti získáte v konzoli svého prohlížeče.', + 'UPLOAD_UNKNOWN' => 'Server vrátil neočkávanou dopověď. Podrobnosti získáte v konzoli svého prohlížeče.', + 'UPLOAD_ERROR_UNKNOWN' => 'Upload selhal. Server vrátil neznámou chybu!', + 'UPLOAD_ERROR_POSTSIZE' => 'Upload failed. The PHP post_max_size may be too small! Otherwise check the FAQ.', + 'UPLOAD_ERROR_FILESIZE' => 'Upload failed. The PHP upload_max_filesize may be too small! Otherwise check the FAQ.', + 'UPLOAD_IN_PROGRESS' => 'Právě probíhá upload na Lychee!', + 'UPLOAD_IMPORT_WARN_ERR' => 'Import byl dokončen s upozorněními nebo chybami. Podrobnosti si prosím prohlédněte v protokolu (Nastavení -> Protokoly).', + 'UPLOAD_IMPORT_COMPLETE' => 'Import dokončen', + 'UPLOAD_IMPORT_INSTR' => 'Zadejte prosím přímý odkaz k fotografii, která má být naimportována:', + 'UPLOAD_IMPORT' => 'Importovat', + 'UPLOAD_IMPORT_SERVER' => 'Import ze serveru', + 'UPLOAD_IMPORT_SERVER_FOLD' => 'Složka je prázdná nebo neobsahuje soubory, které lze zpracovat. Podrobnosti si prosím prohlédněte v protokolu (Nastavení -> Protokoly).', + 'UPLOAD_IMPORT_SERVER_INSTR' => 'Import all photos, folders and sub-folders located in the folders with the following absolute paths (on server). Paths are space separated, use \\ to escape a space in a path.', + 'UPLOAD_ABSOLUTE_PATH' => 'Absolute path to directories, space separated', + 'UPLOAD_IMPORT_SERVER_EMPT' => 'Import neproběhl, protože složka je prázdná!', + 'UPLOAD_IMPORT_DELETE_ORIGINALS' => 'Odstranit původní soubory', + 'UPLOAD_IMPORT_DELETE_ORIGINALS_EXPL' => 'Pokud to bude možné, původní soubory budou po importu odstraněny.', + 'UPLOAD_IMPORT_VIA_SYMLINK' => 'Symbolic links', + 'UPLOAD_IMPORT_VIA_SYMLINK_EXPL' => 'Import files using symbolic links to originals.', + 'UPLOAD_IMPORT_SKIP_DUPLICATES' => 'Skip duplicates', + 'UPLOAD_IMPORT_SKIP_DUPLICATES_EXPL' => 'Existing media files are skipped.', + 'UPLOAD_IMPORT_RESYNC_METADATA' => 'Re-sync metadata', + 'UPLOAD_IMPORT_RESYNC_METADATA_EXPL' => 'Update metadata of existing media files.', + 'UPLOAD_IMPORT_LOW_MEMORY_EXPL' => 'Proces importu alokuje příliš mnoho paměti serveru a může být tedy neočekávaně přerušen.', + 'UPLOAD_WARNING' => 'Upozornění', + 'UPLOAD_IMPORT_NOT_A_DIRECTORY' => 'Uvedená cesta není čitelnou složkou!', + 'UPLOAD_IMPORT_PATH_RESERVED' => 'Uvedená cesta je rezervována pro Lychee!', + 'UPLOAD_IMPORT_FAILED' => 'Soubor nelze importovat!', + 'UPLOAD_IMPORT_UNSUPPORTED' => 'Nepodporovaný typ souboru!', + 'UPLOAD_IMPORT_CANCELLED' => 'Import cancelled', + + 'ABOUT_SUBTITLE' => 'Ideální řešení vlastního hostování a správy fotografií', + 'ABOUT_DESCRIPTION' => 'Lychee je open-source nástroj na správu fotogragfií na Vašem serveru nebo webu. Instalace je hotova dílem okmažiku. Upload, správa a sdílení fotografií se provádí běžnými aplikacemi. Lychee přináší vše, co je třeba pro bezpečné online uložení Vašich fotografií.', + 'FOOTER_COPYRIGHT' => 'Všechny fotografie na tomto webu jsou ve vlastnictví copyright %1$s © %2$s', + 'HOSTED_WITH_LYCHEE' => 'Hostováno na Lychee', + + 'URL_COPY_TO_CLIPBOARD' => 'Kopírovat do schránky', + 'URL_COPIED_TO_CLIPBOARD' => 'URL zkopírována do schránky!', + 'PHOTO_DIRECT_LINKS_TO_IMAGES' => 'Přímý odkaz k souborům:', + 'PHOTO_ORIGINAL' => 'Original', + 'PHOTO_MEDIUM' => 'Střední', + 'PHOTO_MEDIUM_HIDPI' => 'Střední HiDPI', + 'PHOTO_SMALL' => 'Náhled', + 'PHOTO_SMALL_HIDPI' => 'Náhled HiDPI', + 'PHOTO_THUMB' => 'Čtvercový náhled', + 'PHOTO_THUMB_HIDPI' => 'Čtvercový náhled HiDPI', + 'PHOTO_PLACEHOLDER' => 'Low Quality Image Placeholder', + 'PHOTO_THUMBNAIL' => 'Photo thumbnail', + 'PHOTO_LIVE_VIDEO' => 'Video part of live-photo', + 'PHOTO_VIEW' => 'Zobrazení foto Lychee:', + + 'PHOTO_EDIT_ROTATECWISE' => 'Otočit doprava', + 'PHOTO_EDIT_ROTATECCWISE' => 'Otočit doleva', + + 'ERROR_GPX' => 'Error loading GPX file: ', + 'ERROR_EITHER_ALBUMS_OR_PHOTOS' => 'Please select either albums or photos!', + 'ERROR_COULD_NOT_FIND' => 'Could not find what you want.', + 'ERROR_INVALID_EMAIL' => 'Not a valid email address.', + 'EMAIL_SUCCESS' => 'Email updated!', + 'ERROR_PHOTO_NOT_FOUND' => 'Error: photo %s not found !', + 'ERROR_EMPTY_USERNAME' => 'new username cannot be empty.', + 'ERROR_PASSWORD_DOES_NOT_MATCH' => 'new password does not match.', + 'ERROR_EMPTY_PASSWORD' => 'new password cannot be empty.', + 'ERROR_SELECT_ALBUM' => 'Select an album to share!', + 'ERROR_SELECT_USER' => 'Select a user to share with!', + 'ERROR_SELECT_SHARING' => 'Select a sharing to remove!', + 'SHARING_SUCCESS' => 'Sharing updated!', + 'SHARING_REMOVED' => 'Sharing removed!', + 'USER_CREATED' => 'User created!', + 'USER_DELETED' => 'User deleted!', + 'USER_UPDATED' => 'User updated!', + 'ENTER_EMAIL' => 'Enter your email address:', + 'ERROR_ALBUM_JSON_NOT_FOUND' => 'Error: Album json not found!', + 'ERROR_ALBUM_NOT_FOUND' => 'Error: album %s not found', + 'ERROR_DROPBOX_KEY' => 'Error: Dropbox key not set', + 'ERROR_SESSION' => 'Session expired.', + 'CAMERA_DATE' => 'Camera date', + 'NEW_PASSWORD' => 'new password', + 'ALLOW_UPLOADS' => 'Allow uploads', + 'ALLOW_USER_SELF_EDIT' => 'Allow self-management of user account', + 'OSM_CONTRIBUTORS' => 'OpenStreetMap contributors', +]; diff --git a/lang/cz/maintenance.php b/lang/cz/maintenance.php new file mode 100644 index 00000000000..f86de3d6f46 --- /dev/null +++ b/lang/cz/maintenance.php @@ -0,0 +1,60 @@ + 'Maintenance', + 'description' => 'You will find on this page, all the required actions to keep your Lychee installation running smooth and nicely.', + 'cleaning' => [ + 'title' => 'Cleaning %s', + 'result' => '%s deleted.', + 'description' => 'Remove all contents from %s', + 'button' => 'Clean', + ], + 'fix-jobs' => [ + 'title' => 'Fixing Jobs History', + 'description' => 'Mark jobs with status %s or %s as %s.', + 'button' => 'Fix job history', + ], + 'gen-sizevariants' => [ + 'title' => 'Missing %s', + 'description' => 'Found %d %s that could be generated.', + 'button' => 'Generate!', + 'success' => 'Successfully generated %d %s.', + ], + 'fill-filesize-sizevariants' => [ + 'title' => 'File sizes missing', + 'description' => 'Found %d small variants without file size.', + 'button' => 'Fetch data!', + 'success' => 'Successfully computed sizes of %d small variants.', + ], + 'fix-tree' => [ + 'title' => 'Tree statistics', + 'Oddness' => 'Oddness', + 'Duplicates' => 'Duplicates', + 'Wrong parents' => 'Wrong parents', + 'Missing parents' => 'Missing parents', + 'button' => 'Fix tree', + ], + 'optimize' => [ + 'title' => 'Optimize Database', + 'description' => 'If you notice slowdown in your installation, it may be because your database does not + have all its needed index.', + 'button' => 'Optimize Database', + ], + 'update' => [ + 'title' => 'Updates', + 'check-button' => 'Check for updates', + 'update-button' => 'Update', + 'no-pending-updates' => 'No pending update.', + ], +]; \ No newline at end of file diff --git a/lang/cz/profile.php b/lang/cz/profile.php new file mode 100644 index 00000000000..cc24b97452c --- /dev/null +++ b/lang/cz/profile.php @@ -0,0 +1,64 @@ + 'Profile', + + 'login' => [ + 'header' => 'Profile', + 'enter_current_password' => 'Enter your current password:', + 'current_password' => 'Current password', + 'credentials_update' => 'Your credentials will be changed to the following:', + 'username' => 'Username', + 'new_password' => 'New password', + 'confirm_new_password' => 'Confirm new password', + 'email_instruction' => 'Add your email below to enable receiving email notifications. To stop receiving emails, simply remove your email below.', + 'email' => 'Email', + 'change' => 'Change Login', + 'api_token' => 'API Token ...', + + 'missing_fields' => 'Missing fields', + ], + + 'token' => [ + 'unavailable' => 'You have already viewed this token.', + 'no_data' => 'No token API have been generated.', + 'disable' => 'Disable', + 'disabled' => 'Token disabled', + 'warning' => 'This token will not be displayed again. Copy it and keep it in a safe place.', + 'reset' => 'Reset the token', + 'create' => 'Create a new token', + ], + + 'oauth' => [ + 'header' => 'OAuth', + 'header_not_available' => 'OAuth is not available', + 'setup_env' => 'Set up the credentials in your .env', + 'token_registered' => '%s token registered.', + 'setup' => 'Set up %s', + 'reset' => 'reset', + 'credential_deleted' => 'Credential deleted!', + ], + + 'u2f' => [ + 'header' => 'Passkey/MFA/2FA', + 'info' => 'This only provides the ability to use WebAuthn to authenticate instead of username & password.', + 'empty' => 'Credentials list is empty!', + 'not_secure' => 'Environment not secured. U2F not available.', + 'new' => 'Register new device.', + 'credential_deleted' => 'Credential deleted!', + 'credential_updated' => 'Credential updated!', + 'credential_registred' => 'Registration successful!', + '5_chars' => 'At least 5 chars.', + ], +]; \ No newline at end of file diff --git a/lang/cz/settings.php b/lang/cz/settings.php new file mode 100644 index 00000000000..fd197f11135 --- /dev/null +++ b/lang/cz/settings.php @@ -0,0 +1,92 @@ + 'Settings', + 'small_screen' => 'For better a experience on the Settings page,
we recommend you use a larger screen.', + 'tabs' => [ + 'basic' => 'Basic', + 'all_settings' => 'All settings', + ], + 'toasts' => [ + 'change_saved' => 'Change saved!', + 'details' => 'Settings have been modified as per request', + 'error' => 'Error!', + 'error_load_css' => 'Could not load dist/user.css', + 'error_load_js' => 'Could not load dist/custom.js', + 'error_save_css' => 'Could not save CSS', + 'error_save_js' => 'Could not save JS', + 'thank_you' => 'Thank you for your support.', + 'reload' => 'Reload your page for full functionalities.', + ], + 'system' => [ + 'header' => 'System', + 'use_dark_mode' => 'Use dark mode for Lychee', + 'language' => 'Language used by Lychee', + 'nsfw_album_visibility' => 'Make Sensitive albums visible by default.', + 'nsfw_album_explanation' => 'If the album is public, it is still accessible, just hidden from the view and can be revealed by pressing H.', + ], + 'lychee_se' => [ + 'header' => 'Lychee SE', + 'call4action' => 'Get exclusive features and support the development of Lychee. Unlock the SE edition.', + 'preview' => 'Enable preview of Lychee SE features', + 'hide_call4action' => 'Hide this Lychee SE registration form. I am happy with Lychee as-is. :)', + 'hide_warning' => 'If enabled, the only way to register your license key will be via the More tab above. Changes are applied on page reload.', + ], + 'dropbox' => [ + 'header' => 'Dropbox', + 'instruction' => 'In order to import photos from your Dropbox, you need a valid drop-ins app key from their website.', + 'api_key' => 'Dropbox API Key', + 'set_key' => 'Set Dropbox Key', + ], + 'gallery' => [ + 'header' => 'Gallery', + 'photo_order_column' => 'Default column used for sorting photos', + 'photo_order_direction' => 'Default order used for sorting photos', + 'album_order_column' => 'Default column used for sorting albums', + 'album_order_direction' => 'Default order used for sorting albums', + 'aspect_ratio' => 'Default aspect ratio for album thumbs', + 'photo_layout' => 'Layout for pictures', + 'album_decoration' => 'Show decorations on album cover (sub-album and/or photo count)', + 'album_decoration_direction' => 'Align album decorations horizontally or vertically', + 'photo_overlay' => 'Default image overlay information', + 'license_default' => 'Default license used for albums', + 'license_help' => 'Need help choosing?', + ], + 'geolocation' => [ + 'header' => 'Geo-location', + 'map_display' => 'Display the map given GPS coordinates', + 'map_display_public' => 'Allow anonymous users to access the map', + 'map_provider' => 'Defines the map provider', + 'map_include_subalbums' => 'Includes pictures of the sub albums on the map', + 'location_decoding' => 'Use GPS location decoding', + 'location_show' => 'Show location extracted from GPS coordinates', + 'location_show_public' => 'Anonymous users can access the extracted location from GPS coordinates', + ], + 'advanced' => [ + 'header' => 'Advanced Customization', + 'change_css' => 'Change CSS', + 'change_js' => 'Change JS', + ], + 'all' => [ + 'old_setting_style' => 'Old setting style', + 'change_detected' => 'Some settings changed.', + 'save' => 'Save', + ], + + 'tool_option' => [ + 'disabled' => 'disabled', + 'enabled' => 'enabled', + 'discover' => 'discover', + ], +]; \ No newline at end of file diff --git a/lang/cz/sharing.php b/lang/cz/sharing.php new file mode 100644 index 00000000000..69de18cc6d0 --- /dev/null +++ b/lang/cz/sharing.php @@ -0,0 +1,33 @@ + 'Sharing', + + 'info' => 'This page gives an overview of and the ability to edit the sharing rights associated with albums.', + 'album_title' => 'Album title', + 'username' => 'Username', + 'no_data' => 'Sharing list is empty.', + 'share' => 'Share', + 'permission_deleted' => 'Permission deleted!', + 'permission_created' => 'Permission created!', + + 'grants' => [ + 'read' => 'Grants read access', + 'original' => 'Grants access to original photo', + 'download' => 'Grants download', + 'upload' => 'Grants upload', + 'edit' => 'Grants edit', + 'delete' => 'Grants delete', + ], +]; \ No newline at end of file diff --git a/lang/cz/statistics.php b/lang/cz/statistics.php new file mode 100644 index 00000000000..2baf855bbd5 --- /dev/null +++ b/lang/cz/statistics.php @@ -0,0 +1,34 @@ + 'Statistics', + + 'preview_text' => 'This is a preview of the statistics page available in Lychee SE.
The data shown here are randomly generated and do not reflect your server.', + 'no_data' => 'User does not have data on server.', + 'collapse' => 'Collapse albums sizes', + + 'total' => [ + 'total' => 'Total', + 'albums' => 'Albums', + 'photos' => 'Photos', + 'size' => 'Size', + ], + 'table' => [ + 'username' => 'Owner', + 'title' => 'Title', + 'photos' => 'Photos', + 'descendants' => 'Children', + 'size' => 'Size', + ], +]; \ No newline at end of file diff --git a/lang/cz/toasts.php b/lang/cz/toasts.php new file mode 100644 index 00000000000..293d4b72594 --- /dev/null +++ b/lang/cz/toasts.php @@ -0,0 +1,17 @@ + 'Error', + 'success' => 'Success', +]; \ No newline at end of file diff --git a/lang/cz/users.php b/lang/cz/users.php new file mode 100644 index 00000000000..599bb833454 --- /dev/null +++ b/lang/cz/users.php @@ -0,0 +1,44 @@ + 'Users', + 'description' => 'Here you can manage the users of your Lychee installation. You can create, edit and delete users.', + 'create' => 'Create a new user', + 'username' => 'Username', + 'password' => 'Password', + 'legend' => 'Legend', + 'upload_rights' => 'When selected, the user can upload content.', + 'edit_rights' => 'When selected, the user can modify their profile (username, password).', + 'quota' => 'When set, the user has a space quota for pictures (in kB).', + + 'user_deleted' => 'User deleted', + 'user_created' => 'User created', + 'user_updated' => 'User updated', + 'change_saved' => 'Change saved!', + + 'create_edit' => [ + 'upload_rights' => 'User can upload content.', + 'edit_rights' => 'User can modify their profile (username, password).', + 'quota' => 'User has quota limit.', + 'quota_kb' => 'quota in kB (0 for default)', + 'note' => 'Admin note (not publically visible)', + 'create' => 'Create', + 'edit' => 'Edit', + ], + 'line' => [ + 'admin' => 'admin user', + 'edit' => 'Edit', + 'delete' => 'Delete', + ], +]; \ No newline at end of file diff --git a/lang/de/aspect_ratio.php b/lang/de/aspect_ratio.php new file mode 100644 index 00000000000..384a5147f4c --- /dev/null +++ b/lang/de/aspect_ratio.php @@ -0,0 +1,21 @@ + '5/4 (instagram Querformat)', + '4by5' => '4/5 (instagram Portrait)', + '2by3' => '2/3 (Portrait)', + '3by2' => '3/2 (Querformat)', + '1by1' => 'square', + '1byx9' => '16/9 (Querformat)', +]; diff --git a/lang/de/diagnostics.php b/lang/de/diagnostics.php new file mode 100644 index 00000000000..0fadd640428 --- /dev/null +++ b/lang/de/diagnostics.php @@ -0,0 +1,30 @@ + 'Diagnostics', + + 'copy_to_clipboard' => 'Copy diagnostics to clipboard', + 'self-diagnosis' => 'Self-diagnosis', + 'info' => 'Info', + 'space' => 'Space', + 'load_space' => 'Load space usage.', + 'configuration' => 'Configuration', + 'loading' => 'Loading...', + 'identical_content' => 'Identical content', + + 'toast' => [ + 'info' => 'Info', + 'copy' => 'Diagnostics copied to clipboard!', + ], +]; \ No newline at end of file diff --git a/lang/de/dialogs.php b/lang/de/dialogs.php new file mode 100644 index 00000000000..4afd65fae3f --- /dev/null +++ b/lang/de/dialogs.php @@ -0,0 +1,221 @@ + [ + 'close' => 'Close', + 'cancel' => 'Cancel', + 'save' => 'Save', + 'delete' => 'Delete', + 'move' => 'Move', + ], + 'about' => [ + 'subtitle' => 'Self-hosted photo-management done right', + 'description' => 'Lychee is a free photo-management tool, which runs on your server or web-space. Installing is a matter of seconds. Upload, manage and share photos like from a native application. Lychee comes with everything you need and all your photos are stored securely.', + 'update_available' => 'Update available!', + 'thank_you' => 'Thank you for your support!', + 'get_supporter_or_register' => 'Get exclusive features and support the development of Lychee.
Unlock the Supporter Edition or register your License key', + 'here' => 'here', + ], + 'dropbox' => [ + 'not_configured' => 'Dropbox is not configured.', + ], + 'import_from_link' => [ + 'instructions' => 'Please enter the direct link to a photo to import it:', + 'import' => 'Import', + ], + 'keybindings' => [ + 'don_t_show_again' => 'Don\'t show this again', + 'side_wide' => 'Site-wide Shortcuts', + 'back_cancel' => 'Back/Cancel', + 'confirm' => 'Confirm', + 'login' => 'Login', + 'toggle_full_screen' => 'Toggle Full Screen', + 'toggle_sensitive_albums' => 'Toggle Sensitive Albums', + + 'albums' => 'Albums Shortcuts', + 'new_album' => 'New Album', + 'upload_photos' => 'Upload Photos', + 'search' => 'Search', + 'show_this_modal' => 'Show this modal', + 'select_all' => 'Select All', + 'move_selection' => 'Move Selection', + 'delete_selection' => 'Delete Selection', + + 'album' => 'Album Shortcuts', + 'slideshow' => 'Start/Stop Slideshow', + 'toggle' => 'Toggle panel', + + 'photo' => 'Photo Shortcuts', + 'previous' => 'Previous photo', + 'next' => 'Next photo', + 'cycle' => 'Cycle overlay mode', + 'star' => 'Star the photo', + 'move' => 'Move the photo', + 'delete' => 'Delete the photo', + 'edit' => 'Edit information', + 'show_hide_meta' => 'Show information', + + 'keep_hidden' => 'We will keep it hidden.', + ], + 'login' => [ + 'username' => 'Username', + 'password' => 'Password', + 'unknown_invalid' => 'Unknown user or invalid password.', + 'signin' => 'Sign-In', + ], + 'register' => [ + 'enter_license' => 'Enter your license key below:', + 'license_key' => 'License key', + 'invalid_license' => 'Invalid license key.', + 'register' => 'Register', + ], + 'share_album' => [ + 'url_copied' => 'Copied URL to clipboard!', + ], + 'upload' => [ + 'completed' => 'Completed', + 'uploaded' => 'Uploaded:', + 'release' => 'Release file to upload!', + 'select' => 'Click here to select files to upload', + 'drag' => '(Or drag files to the page)', + 'loading' => 'Loading', + 'resume' => 'Resume', + 'uploading' => 'Uploading', + 'finished' => 'Finished', + 'failed_error' => 'Upload failed. The server returned an error!', + ], + 'visibility' => [ + 'public' => 'Public', + 'public_expl' => 'Anonymous users can access this album, subject to the restrictions below.', + 'full' => 'Original', + 'full_expl' => 'Anonymous users can view full-resolution photos.', + 'hidden' => 'Hidden', + 'hidden_expl' => 'Anonymous users need a direct link to access this album.', + 'downloadable' => 'Downloadable', + 'downloadable_expl' => 'Anonymous users can download this album.', + 'password' => 'Password', + 'password_prot' => 'Password protected', + 'password_prot_expl' => 'Anonymous users need a shared password to access this album.', + 'nsfw' => 'Sensitive', + 'nsfw_expl' => 'Album contains sensitive content.', + 'visibility_updated' => 'Visibility updated.', + ], + 'move_album' => [ + 'confirm_single' => 'Are you sure you want to move the album “%1$s” into the album “%2$s”?', + 'confirm_multiple' => 'Are you sure you want to move all selected albums into the album “%s”?', + 'move_single' => 'Move Album', + 'move_to' => 'Move to', + 'move_to_single' => 'Move %s to:', + 'move_to_multiple' => 'Move %d albums to:', + 'no_album_target' => 'No album to move to', + 'moved_single' => 'Album moved!', + 'moved_single_details' => '%1$s moved to %2$s', + 'moved_details' => 'Album(s) moved to %s', + ], + 'new_album' => [ + 'menu' => 'Create Album', + 'info' => 'Enter a title for the new album:', + 'title' => 'title', + 'create' => 'Create Album', + ], + 'new_tag_album' => [ + 'menu' => 'Create Tag Album', + 'info' => 'Enter a title for the new tag album:', + 'title' => 'title', + 'set_tags' => 'Set tags to show', + 'warn' => 'Make sure to press enter after each tag', + 'create' => 'Create Tag Album', + ], + 'delete_album' => [ + 'confirmation' => 'Are you sure you want to delete the album “%s” and all of the photos it contains?', + 'confirmation_multiple' => 'Are you sure you want to delete all %d selected albums and all of the photos they contain?', + 'warning' => 'This action can not be undone!', + 'delete' => 'Delete Album and Photos', + ], + 'transfer' => [ + 'query' => 'Transfer ownership of album to', + 'confirmation' => 'Are you sure you want to transfer the ownership of album “%s” and all the photos it contains to "%s"?', + 'lost_access_warning' => 'Your access to this album will be lost.', + 'warning' => 'This action can not be undone!', + 'transfer' => 'Transfer ownership of album and photos', + ], + 'rename' => [ + 'photo' => 'Enter a new title for this photo:', + 'album' => 'Enter a new title for this album:', + 'rename' => 'Rename', + ], + 'merge' => [ + 'merge_to' => 'Merge %s to:', + 'merge_to_multiple' => 'Merge %d albums to:', + 'no_albums' => 'No albums to merge to.', + 'confirm' => 'Are you sure you want to merge the album “%1$s” into the album “%2$s”?', + 'confirm_multiple' => 'Are you sure you want to merge all selected albums into the album “%s”?', + 'merge' => 'Merge Albums', + 'merged' => 'Album(s) merged to %s!', + ], + 'unlock' => [ + 'password_required' => 'This album is protected by a password. Enter the password below to view the photos of this album:', + 'password' => 'Password', + 'unlock' => 'Unlock', + ], + 'photo_tags' => [ + 'question' => 'Enter your tags for this photo.', + 'question_multiple' => 'Enter your tags for all %d selected photos. Existing tags will be overwritten.', + 'no_tags' => 'No Tags', + 'set_tags' => 'Set Tags', + 'updated' => 'Tags updated!', + 'tags_override_info' => 'If this is unchecked, the tags will be added to the existing tags of the photo.', + ], + 'photo_copy' => [ + 'no_albums' => 'No albums to copy to', + 'copy_to' => 'Copy %s to:', + 'copy_to_multiple' => 'Copy %d photos to:', + 'confirm' => 'Copy %s to %s.', + 'confirm_multiple' => 'Copy %d photos to %s.', + 'copy' => 'Copy', + 'copied' => 'Photo(s) copied!', + ], + 'photo_delete' => [ + 'confirm' => 'Are you sure you want to delete the photo “%s”?', + 'confirm_multiple' => 'Are you sure you want to delete all %d selected photos?', + 'deleted' => 'Photo(s) deleted!', + ], + 'move_photo' => [ + 'move_single' => 'Move %s to:', + 'move_multiple' => 'Move %d photos to:', + 'confirm' => 'Move %s to %s.', + 'confirm_multiple' => 'Move %d photos to %s.', + 'moved' => 'Photo(s) moved to %s!', + ], + 'target_user' => [ + 'placeholder' => 'Select user', + ], + 'target_album' => [ + 'placeholder' => 'Select album', + ], + 'webauthn' => [ + 'u2f' => 'U2F', + 'success' => 'Authentication successful!', + 'error' => 'Whoops, it looks like something went wrong. Please reload the site and try again!', + ], + 'se' => [ + 'available' => 'Available in the Supporter Edition', + ], + 'session_expired' => [ + 'title' => 'Session expired', + 'message' => 'Your session has expired.
Please reload the page.', + 'reload' => 'Reload', + 'go_to_gallery' => 'Go to the Gallery', + ], +]; \ No newline at end of file diff --git a/lang/de/fix-tree.php b/lang/de/fix-tree.php new file mode 100644 index 00000000000..64803e310e6 --- /dev/null +++ b/lang/de/fix-tree.php @@ -0,0 +1,55 @@ + 'Maintenance', + 'intro' => 'This page allows you to re-order and fix your albums manually.
Before any modifications, we strongly recommend you to read about Nested Set tree structures.', + 'warning' => 'You can really break your Lychee installation here, modify values at your own risks.', + + 'help' => [ + 'header' => 'Help', + 'hover' => 'Hover ids or titles to highlight related albums.', + 'left' => 'Left', + 'right' => 'Right', + 'convenience' => 'For your convenience, the and buttons allow you to change the values of %s and %s by respectively +1 and -1 with propagation.', + 'left-right-warn' => 'The and indicates that the value of %s (and respectively %s) is duplicated somewhere.', + 'parent-marked' => 'Marked Parent Id indicates that the %s and %s do not satisfy the Nest Set tree structures. Edit either the Parent Id or the %s/%s values.', + 'slowness' => 'This page will be slow with a large number of albums.', + ], + + 'buttons' => [ + 'reset' => 'Reset', + 'check' => 'Check', + 'apply' => 'Apply', + ], + + 'table' => [ + 'title' => 'Title', + 'left' => 'Left', + 'right' => 'Right', + 'id' => 'Id', + 'parent' => 'Parent Id', + ], + + 'errors' => [ + 'invalid' => 'Invalid tree!', + 'invalid_details' => 'We are not applying this as it is guaranteed to be a broken state.', + 'invalid_left' => 'Album %s has an invalid left value.', + 'invalid_right' => 'Album %s has an invalid right value.', + 'invalid_left_right' => 'Album %s has an invalid left/right values. Left should be strictly smaller than right: %s < %s.', + 'duplicate_left' => 'Album %s has a duplicate left value %s.', + 'duplicate_right' => 'Album %s has a duplicate right value %s.', + 'parent' => 'Album %s has an unexpected parent id %s.', + 'unknown' => 'Album %s has an unknown error.', + ], +]; \ No newline at end of file diff --git a/lang/de/gallery.php b/lang/de/gallery.php new file mode 100644 index 00000000000..eb8008827e0 --- /dev/null +++ b/lang/de/gallery.php @@ -0,0 +1,241 @@ + 'Gallery', + + 'smart_albums' => 'Smart albums', + 'albums' => 'Albums', + 'root' => 'Albums', + + 'original' => 'Original', + 'medium' => 'Medium', + 'medium_hidpi' => 'Medium HiDPI', + 'small' => 'Thumb', + 'small_hidpi' => 'Thumb HiDPI', + 'thumb' => 'Square thumb', + 'thumb_hidpi' => 'Square thumb HiDPI', + 'placeholder' => 'Low Quality Image Placeholder', + 'thumbnail' => 'Photo thumbnail', + 'live_video' => 'Video part of live-photo', + + 'camera_data' => 'Camera date', + 'album_reserved' => 'All Rights Reserved', + + 'map' => [ + 'error_gpx' => 'Error loading GPX file', + 'osm_contributors' => 'OpenStreetMap contributors', + ], + + 'search' => [ + 'title' => 'Search', + 'searching' => 'Searching…', + 'no_results' => 'Nothing matches your search query.', + 'searchbox' => 'Search…', + 'minimum_chars' => 'Minimum %s characters required.', + 'photos' => 'Photos (%s)', + 'albums' => 'Albums (%s)', + ], + + 'smart_album' => [ + 'unsorted' => 'Unsorted', + 'starred' => 'Starred', + 'recent' => 'Recent', + 'public' => 'Public', + 'on_this_day' => 'On This Day', + ], + + 'layout' => [ + 'squares' => 'Square thumbnails', + 'justified' => 'With aspect, justified', + 'masonry' => 'With aspect, masonry', + 'grid' => 'With aspect, grid', + ], + + 'overlay' => [ + 'none' => 'None', + 'exif' => 'EXIF data', + 'description' => 'Description', + 'date' => 'Date taken', + ], + + 'timeline' => [ + 'default' => 'default', + 'disabled' => 'disabled', + 'year' => 'Year', + 'month' => 'Month', + 'day' => 'Day', + 'hour' => 'Hour', + ], + + 'album' => [ + 'header_albums' => 'Albums', + 'header_photos' => 'Photos', + 'no_results' => 'Nothing to see here', + 'upload' => 'Upload photos', + + 'tabs' => [ + 'about' => 'About Album', + 'share' => 'Share Album', + 'move' => 'Move Album', + 'danger' => 'DANGER ZONE', + ], + + 'hero' => [ + 'created' => 'Created', + 'copyright' => 'Copyright', + 'subalbums' => 'Subalbums', + 'images' => 'Photos', + 'download' => 'Download Album', + 'share' => 'Share Album', + 'stats_only_se' => 'Statistics available in the Supporter Edition', + ], + + 'stats' => [ + 'lens' => 'Lens', + 'shutter' => 'Shutter speed', + 'iso' => 'ISO', + 'model' => 'Model', + 'aperture' => 'Aperture', + 'no_data' => 'No data', + ], + + 'properties' => [ + 'title' => 'Title', + 'description' => 'Description', + 'photo_ordering' => 'Order photos by', + 'children_ordering' => 'Order albums by', + 'asc/desc' => 'asc/desc', + 'header' => 'Set album header', + 'compact_header' => 'Use compact header', + 'license' => 'Set license', + 'copyright' => 'Set copyright', + 'aspect_ratio' => 'Set album thumbs aspect ratio', + 'album_timeline' => 'Set album timeline mode', + 'photo_timeline' => 'Set photo timeline mode', + 'layout' => 'Set photo layout', + 'show_tags' => 'Set tags to show', + 'tags_required' => 'Tags are required.', + ], + ], + + 'photo' => [ + 'actions' => [ + 'star' => 'Star', + 'unstar' => 'Unstar', + 'set_album_header' => 'Set as album header', + 'move' => 'Move', + 'delete' => 'Delete', + 'header_set' => 'Header set', + ], + + 'details' => [ + 'about' => 'About', + 'basics' => 'Basics', + 'title' => 'Title', + 'uploaded' => 'Uploaded', + 'description' => 'Description', + 'license' => 'License', + 'reuse' => 'Reuse', + 'latitude' => 'Latitude', + 'longitude' => 'Longitude', + 'altitude' => 'Altitude', + 'location' => 'Location', + 'image' => 'Image', + 'video' => 'Video', + 'size' => 'Size', + 'format' => 'Format', + 'resolution' => 'Resolution', + 'duration' => 'Duration', + 'fps' => 'Frame rate', + 'tags' => 'Tags', + 'camera' => 'Camera', + 'captured' => 'Captured', + 'make' => 'Make', + 'type' => 'Type/Model', + 'lens' => 'Lens', + 'shutter' => 'Shutter Speed', + 'aperture' => 'Aperture', + 'focal' => 'Focal Length', + 'iso' => 'ISO %s', + ], + + 'edit' => [ + 'set_title' => 'Set Title', + 'set_description' => 'Set Description', + 'set_license' => 'Set License', + 'no_tags' => 'No Tags', + 'set_tags' => 'Set Tags', + 'set_created_at' => 'Set Upload Date', + ], + ], + + 'nsfw' => [ + 'header' => 'Sensitive content', + 'description' => 'This album contains sensitive content which some people may find offensive or disturbing.', + 'consent' => 'Tap to consent.', + ], + + 'menus' => [ + 'star' => 'Star', + 'unstar' => 'Unstar', + 'star_all' => 'Star Selected', + 'unstar_all' => 'Unstar Selected', + 'tag' => 'Tag', + 'tag_all' => 'Tag Selected', + 'set_cover' => 'Set Album Cover', + 'remove_header' => 'Remove Album Header', + 'set_header' => 'Set Album Header', + 'copy_to' => 'Copy to …', + 'copy_all_to' => 'Copy Selected to …', + 'rename' => 'Rename', + 'move' => 'Move', + 'move_all' => 'Move Selected', + 'delete' => 'Delete', + 'delete_all' => 'Delete Selected', + 'download' => 'Download', + 'download_all' => 'Download Selected', + 'merge' => 'Merge', + 'merge_all' => 'Merge Selected', + + 'upload_photo' => 'Upload Photo', + 'import_link' => 'Import from Link', + 'import_dropbox' => 'Import from Dropbox', + 'new_album' => 'New Album', + 'new_tag_album' => 'New Tag Album', + 'upload_track' => 'Upload track', + 'delete_track' => 'Delete track', + ], + + 'sort' => [ + 'photo_select_1' => 'Upload Time', + 'photo_select_2' => 'Take Date', + 'photo_select_3' => 'Title', + 'photo_select_4' => 'Description', + 'photo_select_6' => 'Star', + 'photo_select_7' => 'Photo Format', + 'ascending' => 'Ascending', + 'descending' => 'Descending', + 'album_select_1' => 'Creation Time', + 'album_select_2' => 'Title', + 'album_select_3' => 'Description', + 'album_select_5' => 'Latest Take Date', + 'album_select_6' => 'Oldest Take Date', + ], + + 'albums_protection' => [ + 'private' => 'private', + 'public' => 'public', + 'inherit_from_parent' => 'inherit from parent', + ], +]; \ No newline at end of file diff --git a/lang/de/jobs.php b/lang/de/jobs.php new file mode 100644 index 00000000000..5d952b76012 --- /dev/null +++ b/lang/de/jobs.php @@ -0,0 +1,18 @@ + 'Jobs', + + 'no_data' => 'No Jobs have been executed yet.', +]; \ No newline at end of file diff --git a/lang/de/landing.php b/lang/de/landing.php new file mode 100644 index 00000000000..fe6fe55b8ea --- /dev/null +++ b/lang/de/landing.php @@ -0,0 +1,19 @@ + 'Gallery', + 'access_gallery' => 'Access the gallery', + 'hosted_with_lychee' => 'Hosted with Lychee', + 'copyright' => 'All images on this website are subject to copyright by %1$s © %2$s', +]; \ No newline at end of file diff --git a/lang/de/left-menu.php b/lang/de/left-menu.php new file mode 100644 index 00000000000..9a3e91f4037 --- /dev/null +++ b/lang/de/left-menu.php @@ -0,0 +1,29 @@ + 'Back to Gallery', + + 'admin' => 'Admin', + 'clockwork' => 'Clockwork App', + 'logs' => 'Show Logs', + 'jobs' => 'Show Job History', + 'user' => 'User', + + 'sign_out' => 'Sign Out', + + 'about' => 'About', + 'api' => 'API Documentation', + 'source_code' => 'Source Code', + 'support' => 'Support', +]; \ No newline at end of file diff --git a/lang/de/lychee.php b/lang/de/lychee.php new file mode 100644 index 00000000000..f4f614ae613 --- /dev/null +++ b/lang/de/lychee.php @@ -0,0 +1,535 @@ + 'Benutzername', + 'PASSWORD' => 'Kennwort', + 'ENTER' => 'Eingabe', + 'CANCEL' => 'Abbrechen', + 'CONFIRM' => 'Bestätigen', + 'SIGN_IN' => 'Anmelden', + 'CLOSE' => 'Schließen', + 'SETTINGS' => 'Einstellungen', + 'SEARCH' => 'Suchen …', + 'MORE' => 'Mehr', + 'DEFAULT' => 'Standard', + 'GALLERY' => 'Galerie', + + 'USERS' => 'Benutzer', + 'PROFILE' => 'Profil', + 'CREATE' => 'Erstellen', + 'REMOVE' => 'Entfernen', + 'SHARE' => 'Freigeben', + 'U2F' => 'U2F', + 'NOTIFICATIONS' => 'Benachrichtigungen', + 'SHARING' => 'Freigabe', + 'CHANGE_LOGIN' => 'Anmeldung ändern', + 'CHANGE_SORTING' => 'Sortierung ändern', + 'SET_DROPBOX' => 'Dropbox einrichten', + 'ABOUT_LYCHEE' => 'Über Lychee', + 'DIAGNOSTICS' => 'Diagnose', + 'DIAGNOSTICS_GET_SIZE' => 'Speicherplatz-Nutzung abrufen', + 'JOBS' => 'Job-Verlauf anzeigen', + 'LOGS' => 'Logs anzeigen', + 'SIGN_OUT' => 'Abmelden', + 'UPDATE_AVAILABLE' => 'Update verfügbar!', + 'MIGRATION_AVAILABLE' => 'Migration verfügbar!', + 'CHECK_FOR_UPDATE' => 'auf Updates prüfen', + 'DEFAULT_LICENSE' => 'Standard-Lizenz für neue Uploads:', + 'SET_LICENSE' => 'Lizenz anwenden', + 'SET_OVERLAY_TYPE' => 'Setze Overlay', + 'SET_ALBUM_DECORATION' => 'Albumdekoration speichern', + 'SET_MAP_PROVIDER' => 'Speichere Provider für OpenStreetMap Karten', + 'FULL_SETTINGS' => 'Alle Einstellungen', + 'UPDATE' => 'Update', + 'RESET' => 'Zurücksetzen', + 'DISABLE_TOKEN_TOOLTIP' => 'Deaktivieren', + 'ENABLE_TOKEN' => 'API-Schlüssel aktivieren', + 'DISABLED_TOKEN_STATUS_MSG' => 'Deaktiviert', + 'TOKEN_BUTTON' => 'API-Schlüssel ...', + 'TOKEN_NOT_AVAILABLE' => 'Sie haben diesen Schlüssel bereits angesehen.', + 'TOKEN_WAIT' => 'Warten ...', + + 'SMART_ALBUMS' => 'Intelligente Alben', + 'SHARED_ALBUMS' => 'Freigegebene Alben', + 'ALBUMS' => 'Alben', + 'PHOTOS' => 'Bilder', + 'SEARCH_RESULTS' => 'Suchergebnisse', + + 'RENAME' => 'Umbenennen', + 'RENAME_ALL' => 'Ausgewählte umbenennen', + 'MERGE' => 'Zusammenführen', + 'MERGE_ALL' => 'Ausgewählte zusammenführen', + 'MAKE_PUBLIC' => 'Veröffentlichen', + 'SHARE_ALBUM' => 'Album freigeben', + 'SHARE_PHOTO' => 'Foto freigeben', + 'VISIBILITY_ALBUM' => 'Sichtbarkeit des Albums', + 'VISIBILITY_PHOTO' => 'Sichtbarkeit des Fotos', + 'DOWNLOAD_ALBUM' => 'Album herunterladen', + 'ABOUT_ALBUM' => 'Über dieses Album', + 'DELETE_ALBUM' => 'Album löschen', + 'MOVE_ALBUM' => 'Album verschieben', + 'FULLSCREEN_ENTER' => 'Vollbild', + 'FULLSCREEN_EXIT' => 'Vollbild beenden', + + 'SHARING_ALBUM_USERS' => 'Teile dieses Album mit Benutzern', + 'WAIT_FETCH_DATA' => 'Bitte warten Sie, während die Daten abgerufen werden…', + 'SHARING_ALBUM_USERS_NO_USERS' => 'Es sind keine Benutzer vorhanden, mit denen das Album geteilt werden kann', + 'SHARING_ALBUM_USERS_LONG_MESSAGE' => 'Wählen Sie die Benutzer aus, mit denen das Album geteilt werden soll', + + 'DELETE_ALBUM_QUESTION' => 'Album und Fotos löschen', + 'KEEP_ALBUM' => 'Album behalten', + 'DELETE_ALBUM_CONFIRMATION' => 'Sind Sie sicher, dass Sie das Album „%s“ und alle enthaltenen Fotos löschen wollen? Diese Aktion kann nicht rückgängig gemacht werden!', + + 'DELETE_TAG_ALBUM_QUESTION' => 'Album löschen', + 'DELETE_TAG_ALBUM_CONFIRMATION' => 'Sind Sie sicher, dass Sie das Album „%s“ löschen wollen (Fotos darin werden nicht gelöscht)? Diese Aktion kann nicht rückgängig gemacht werden!', + + 'DELETE_ALBUMS_QUESTION' => 'Alben und Fotos löschen', + 'KEEP_ALBUMS' => 'Alben behalten', + 'DELETE_ALBUMS_CONFIRMATION' => 'Sind Sie sicher, dass Sie alle %d ausgewählten Alben und die enthaltenen Fotos löschen wollen? Diese Aktion kann nicht rückgängig gemacht werden!', + + 'DELETE_UNSORTED_CONFIRM' => 'Sind Sie sicher, dass Sie alle Fotos aus „Unsortiert“ löschen wollen? Diese Aktion kann nicht rückgängig gemacht werden!', + 'CLEAR_UNSORTED' => 'Unsortierte löschen', + 'KEEP_UNSORTED' => 'Unsortierte behalten', + + 'EDIT_SHARING' => 'Freigabe bearbeiten', + 'MAKE_PRIVATE' => 'Privat', + + 'CLOSE_ALBUM' => 'Album schließen', + 'CLOSE_PHOTO' => 'Foto schließen', + 'CLOSE_MAP' => 'Karte schließen', + + 'ADD' => 'Hinzufügen', + 'MOVE' => 'Verschieben', + 'MOVE_ALL' => 'Ausgewählte verschieben', + 'DUPLICATE' => 'Duplizieren', + 'DUPLICATE_ALL' => 'Ausgewählte duplizieren', + 'COPY_TO' => 'Kopieren nach …', + 'COPY_ALL_TO' => 'Ausgewählte kopieren nach …', + 'DELETE' => 'Löschen', + 'SAVE' => 'Speichern', + 'DELETE_ALL' => 'Ausgewählte löschen', + 'DOWNLOAD' => 'Herunterladen', + 'DOWNLOAD_ALL' => 'Ausgewählte herunterladen', + 'UPLOAD_PHOTO' => 'Foto hochladen', + 'IMPORT_LINK' => 'Aus Link importieren', + 'IMPORT_DROPBOX' => 'Aus Dropbox importieren', + 'IMPORT_SERVER' => 'Von Server importieren', + 'NEW_ALBUM' => 'Neues Album', + 'NEW_TAG_ALBUM' => 'Neues Tag-Album', + 'UPLOAD_TRACK' => 'Track hochladen', + 'DELETE_TRACK' => 'Track löschen', + + 'TITLE_NEW_ALBUM' => 'Geben Sie einen Titel für das neue Album ein:', + 'UNTITLED' => 'Unbenannt', + 'UNSORTED' => 'Unsortiert', + 'STARRED' => 'Favoriten', + 'RECENT' => 'Zuletzt benutzt', + 'PUBLIC' => 'Öffentlich', + 'ON_THIS_DAY' => 'An diesem Tag', + 'NUM_PHOTOS' => 'Fotos', + + 'CREATE_ALBUM' => 'Album erstellen', + 'CREATE_TAG_ALBUM' => 'Neues Tag-Album erstellen', + + 'STAR_PHOTO' => 'Foto als Favorit markieren', + 'STAR' => 'Als Favorit markieren', + 'UNSTAR' => 'Als Favorit demarkieren', + 'STAR_ALL' => 'Ausgewählte als Favoriten markieren', + 'UNSTAR_ALL' => 'Ausgewählte als Favoriten demarkieren', + 'TAG' => 'Taggen', + 'TAG_ALL' => 'Ausgewählte taggen', + 'UNSTAR_PHOTO' => 'Foto von Favoriten entfernen', + 'SET_COVER' => 'Als Album-Cover setzen', + 'REMOVE_COVER' => 'Als Album-Cover entfernen', + 'SET_HEADER' => 'Set Album Header', + 'REMOVE_HEADER' => 'Remove Album Header', + 'SET_COMPACT_HEADER' => 'Use Compact Header', + + 'FULL_PHOTO' => 'Original öffnen', + 'ABOUT_PHOTO' => 'Über dieses Foto', + 'DISPLAY_FULL_MAP' => 'Karte', + 'DIRECT_LINK' => 'Direkter Link', + 'DIRECT_LINKS' => 'Direkte Links', + 'QR_CODE' => 'QR-Code', + + 'ALBUM_ABOUT' => 'Über', + 'ALBUM_BASICS' => 'Grundlegende Informationen', + 'ALBUM_TITLE' => 'Titel', + 'ALBUM_COPYRIGHT' => 'Copyright', + 'ALBUM_SET_COPYRIGHT' => 'Copyright setzen', + 'ALBUM_NEW_TITLE' => 'Geben Sie einen neuen Titel für dieses Album ein:', + 'ALBUMS_NEW_TITLE' => 'Geben Sie einen Titel für alle %d ausgewählten Alben ein:', + 'ALBUM_SET_TITLE' => 'Titel speichern', + 'ALBUM_DESCRIPTION' => 'Beschreibung', + 'ALBUM_SHOW_TAGS' => 'Angezeigte Tags', + 'ALBUM_NEW_DESCRIPTION' => 'Geben Sie eine neue Beschreibung für dieses Album ein:', + 'ALBUM_SET_DESCRIPTION' => 'Beschreibung speichern', + 'ALBUM_NEW_SHOWTAGS' => 'Gebe Tags der Bilder ein, die in diesem Album sichtbar sein sollen:', + 'ALBUM_SET_SHOWTAGS' => 'Setze Tags zum Anschauen', + 'ALBUM_ALBUM' => 'Album', + 'ALBUM_CREATED' => 'Erstellt', + 'ALBUM_IMAGES' => 'Bilder', + 'ALBUM_VIDEOS' => 'Videos', + 'ALBUM_SUBALBUMS' => 'Unteralben', + 'ALBUM_SHARING' => 'Teilen', + 'ALBUM_SHR_YES' => 'Ja', + 'ALBUM_SHR_NO' => 'Nein', + 'ALBUM_PUBLIC' => 'Öffentlich', + 'ALBUM_PUBLIC_EXPL' => 'Anonyme Nutzer können, abhängig von den Einstellungen unten, auf dieses Album zugreifen.', + 'ALBUM_FULL' => 'Original', + 'ALBUM_FULL_EXPL' => 'Anonyme Nutzer können Fotos in Originalauflösung betrachten.', + 'ALBUM_HIDDEN' => 'Versteckt', + 'ALBUM_HIDDEN_EXPL' => 'Anonyme Nutzer benötigen einen direkten Link, um auf dieses Album zuzugreifen.', + 'ALBUM_MARK_NSFW' => 'Markiere Album als sensibel', + 'ALBUM_UNMARK_NSFW' => 'Entferne Markierung des Albums als sensibel', + 'ALBUM_NSFW' => 'Sensibel', + 'ALBUM_NSFW_EXPL' => 'Album enthält sensible Inhalte.', + 'ALBUM_DOWNLOADABLE' => 'Zum Herunterladen', + 'ALBUM_DOWNLOADABLE_EXPL' => 'Anonyme Nutzer können dieses Album herunterladen.', + 'ALBUM_SHARE_BUTTON_VISIBLE' => 'Teilen-Button ist sichtbar', + 'ALBUM_SHARE_BUTTON_VISIBLE_EXPL' => 'Anonyme Nutzer können Links zum Teilen in sozialen Medien sehen.', + 'ALBUM_PASSWORD' => 'Kennwort', + 'ALBUM_PASSWORD_PROT' => 'Kennwortgeschützt', + 'ALBUM_PASSWORD_PROT_EXPL' => 'Anonyme Nutzer benötigen ein Passwort, um auf dieses Album zuzugreifen.', + 'ALBUM_PASSWORD_REQUIRED' => 'Dieses Album ist mit einem Kennwort geschützt. Geben Sie unten das Kennwort ein, um das Album anzusehen:', + 'ALBUM_MERGE' => 'Sind Sie sicher, dass Sie das Album „%1$s“ mit dem Album „%2$s“ zusammenführen wollen?', + 'ALBUMS_MERGE' => 'Sind Sie sicher, dass Sie alle ausgewählten Alben mit dem Album „%s“ zusammenführen möchten?', + 'MERGE_ALBUM' => 'Alben zusammenführen', + 'DONT_MERGE' => 'Nicht zusammenführen', + 'ALBUM_MOVE' => 'Sind Sie sicher, dass Sie das Album „%1$s“ nach „%2$s“ verschieben möchten?', + 'ALBUMS_MOVE' => 'Sind Sie sicher, dass Sie die ausgewählten Alben in das Album „%s“ verschieben wollen?', + 'MOVE_ALBUMS' => 'Alben verschieben', + 'NOT_MOVE_ALBUMS' => 'Nicht verschieben', + 'ROOT' => 'Alben', + 'ALBUM_REUSE' => 'Weiterverwendung', + 'ALBUM_LICENSE' => 'Lizenz', + 'ALBUM_SET_LICENSE' => 'Lizenz festlegen', + 'ALBUM_LICENSE_HELP' => 'Benötigen Sie Hilfe bei der Auswahl?', + 'ALBUM_LICENSE_NONE' => 'Keine', + 'ALBUM_RESERVED' => 'Alle Rechte vorbehalten', + 'ALBUM_SET_ORDER' => 'Reihenfolge festlegen', + 'ALBUM_ORDERING' => 'Sortieren nach', + 'ALBUM_PHOTO_ORDERING' => 'Fotos sortieren nach', + 'ALBUM_CHILDREN_ORDERING' => 'Alben sortieren nach', + 'ALBUM_OWNER' => 'Besitzer', + + 'PHOTO_ABOUT' => 'Über', + 'PHOTO_BASICS' => 'Grundlegende Informationen', + 'PHOTO_TITLE' => 'Titel', + 'PHOTO_NEW_TITLE' => 'Geben Sie einen neuen Titel für dieses Foto ein:', + 'PHOTO_SET_TITLE' => 'Titel speichern', + 'PHOTO_UPLOADED' => 'Hochgeladen', + 'PHOTO_DESCRIPTION' => 'Beschreibung', + 'PHOTO_NEW_DESCRIPTION' => 'Geben Sie eine neue Beschreibung für dieses Foto ein:', + 'PHOTO_SET_DESCRIPTION' => 'Beschreibung speichern', + 'PHOTO_NEW_LICENSE' => 'Neue Lizenz hinzufügen', + 'PHOTO_SET_LICENSE' => 'Lizenz festlegen', + 'PHOTO_LICENSE' => 'Lizenz', + 'PHOTO_LICENSE_HELP' => 'Benötigen Sie Hilfe beim Auswählen?', + 'PHOTO_REUSE' => 'Weiterverwendung', + 'PHOTO_LICENSE_NONE' => 'Keine', + 'PHOTO_RESERVED' => 'Alle Rechte vorbehalten', + 'PHOTO_LATITUDE' => 'Breite', + 'PHOTO_LONGITUDE' => 'Länge', + 'PHOTO_ALTITUDE' => 'Höhe', + 'PHOTO_IMGDIRECTION' => 'Richtung', + 'PHOTO_LOCATION' => 'Ort', + 'PHOTO_IMAGE' => 'Bild', + 'PHOTO_VIDEO' => 'Video', + 'PHOTO_SIZE' => 'Größe', + 'PHOTO_FORMAT' => 'Format', + 'PHOTO_RESOLUTION' => 'Auflösung', + 'PHOTO_DURATION' => 'Dauer', + 'PHOTO_FPS' => 'Bilder pro Sekunde', + 'PHOTO_TAGS' => 'Tags', + 'PHOTO_NOTAGS' => 'Keine Tags', + 'PHOTO_NEW_TAGS' => 'Geben Sie die Tags für dieses Foto ein. Sie können mehrere Tags hinzufügen, indem Sie sie mit einem Komma trennen:', + 'PHOTOS_NEW_TAGS' => 'Geben Sie die Tags für alle %d ausgewählten Fotos ein. Bestehende Tags werden überschrieben. Sie können mehrere Tags hinzufügen, indem Sie sie mit einem Komma trennen:', + 'PHOTO_SET_TAGS' => 'Tags speichern', + 'PHOTO_CAMERA' => 'Kamera', + 'PHOTO_CAPTURED' => 'Aufgenommen', + 'PHOTO_MAKE' => 'Marke', + 'PHOTO_TYPE' => 'Typ/Modell', + 'PHOTO_LENS' => 'Objektiv', + 'PHOTO_SHUTTER' => 'Verschlusszeit', + 'PHOTO_APERTURE' => 'Blende', + 'PHOTO_FOCAL' => 'Brennweite', + 'PHOTO_ISO' => 'ISO %s', + 'PHOTO_SHARING' => 'Teilen', + 'PHOTO_DELETE' => 'Foto löschen', + 'PHOTO_KEEP' => 'Foto behalten', + 'PHOTO_DELETE_CONFIRMATION' => 'Sind Sie sicher, dass Sie das Foto „%s“ löschen wollen? Diese Aktion kann nicht rückgängig gemacht werden!', + 'PHOTO_DELETE_ALL' => 'Sind Sie sicher, dass Sie alle %d ausgewählten Fotos löschen wollen? Diese Aktion kann nicht rückgängig gemacht werden!', + 'PHOTOS_NEW_TITLE' => 'Geben Sie einen Titel für die %d ausgewählten Fotos ein:', + 'PHOTO_MAKE_PRIVATE_ALBUM' => 'Dieses Foto befindet sich in einem öffentlichen Album. Um dieses Foto als privat oder öffentlich zu markieren, bearbeiten Sie die Sichtbarkeit des übergeordneten Albums.', + 'PHOTO_SHOW_ALBUM' => 'Album anzeigen', + 'PHOTO_PUBLIC' => 'Öffentlich', + 'PHOTO_PUBLIC_EXPL' => 'Anonyme Nutzer können, abhängig von den Einstellungen unten, dieses Foto betrachten.', + 'PHOTO_FULL' => 'Original', + 'PHOTO_FULL_EXPL' => 'Anonyme Nutzer können das Foto in Originalauflösung betrachten.', + 'PHOTO_HIDDEN' => 'Versteckt', + 'PHOTO_HIDDEN_EXPL' => 'Anonyme Nutzer benötigen einen direkten Link, um auf das Foto zu betrachten.', + 'PHOTO_DOWNLOADABLE' => 'Herunterladbar', + 'PHOTO_DOWNLOADABLE_EXPL' => 'Anonyme Nutzer können dieses Foto herunterladen.', + 'PHOTO_SHARE_BUTTON_VISIBLE' => 'Teilen-Button ist sichtbar', + 'PHOTO_SHARE_BUTTON_VISIBLE_EXPL' => 'Anonyme Nutzer können Links zum Teilen in sozialen Medien sehen.', + 'PHOTO_PASSWORD_PROT' => 'Passwortgeschützt', + 'PHOTO_PASSWORD_PROT_EXPL' => 'Anonyme Nutzer benötigen ein Passwort, um dieses Foto zu sehen.', + 'PHOTO_EDIT_SHARING_TEXT' => 'Die Einstellungen zum Teilen des Fotos werden wie folgt angepasst:', + 'PHOTO_NO_EDIT_SHARING_TEXT' => 'Dieses Foto ist in einem öffentlichen Album und erbt deshalb die Sichtbarkeitseinstellungen des Albums. Die aktuellen Sichtbarkeitseinstellungen werden unten nur zur Info dargestellt.', + 'PHOTO_EDIT_GLOBAL_SHARING_TEXT' => 'Die Sichtbarkeit dieses Fotos kann über die globalen Lychee Einstellungen modifiziert werden. Die aktuellen Sichtbarkeitseinstellungen werden unten nur zur Info dargestellt.', + 'PHOTO_NEW_CREATED_AT' => 'Geben Sie das Upload-Datum für dieses Foto ein. MM/TT/JJJJ, hh:mm', + 'PHOTO_SET_CREATED_AT' => 'Upload-Datum festlegen', + + 'LOADING' => 'Laden', + 'ERROR' => 'Fehler', + 'ERROR_TEXT' => 'Hoppla, da ist etwas schiefgegangen. Bitte laden Sie die Seite erneut und probieren Sie es noch einmal!', + 'ERROR_UNKNOWN' => 'Etwas Unerwartetes ist passiert. Bitte probieren Sie es erneut und überprüfen Sie die Installation und Ihren Server. Lesen Sie die README-Datei für mehr Informationen.', + 'ERROR_MAP_DEACTIVATED' => 'Karten sind unter Einstellungen deaktiviert worden.', + 'ERROR_SEARCH_DEACTIVATED' => 'Suchfunktion wurde unter Einstellungen deaktiviert.', + 'SUCCESS' => 'OK', + 'CHANGE_SUCCESS' => 'Änderung erfolgreich.', + 'RETRY' => 'Noch einmal versuchen', + 'OVERRIDE' => 'Überschreiben', + 'TAGS_OVERRIDE_INFO' => 'Wenn das nicht aktiviert ist, werden die Tags zu den vorhandenen Tags des Fotos hinzugefügt.', + + 'SETTINGS_SUCCESS_LOGIN' => 'Benutzerdaten aktualisiert', + 'SETTINGS_SUCCESS_SORT' => 'Sortierreihenfolge aktualisiert', + 'SETTINGS_SUCCESS_DROPBOX' => 'Dropbox-Schlüssel aktualisiert', + 'SETTINGS_SUCCESS_LANG' => 'Sprache aktualisiert', + 'SETTINGS_SUCCESS_LAYOUT' => 'Layout aktualisiert', + 'SETTINGS_SUCCESS_IMAGE_OVERLAY' => 'EXIF-Overlay-Einstellungen aktualisiert', + 'SETTINGS_SUCCESS_PUBLIC_SEARCH' => 'Öffentliche Suche geändert', + 'SETTINGS_SUCCESS_LICENSE' => 'Standard-Lizenz aktualisiert', + 'SETTINGS_SUCCESS_MAP_DISPLAY' => 'Karteneinstellungen erfolgreich aktualisiert', + 'SETTINGS_SUCCESS_MAP_DISPLAY_PUBLIC' => 'Karteneinstellungen für öffentlichen Alben erfolgreich aktualisiert', + 'SETTINGS_SUCCESS_MAP_PROVIDER' => 'Provider für Karten erfolgreich aktualisiert', + 'SETTINGS_SUCCESS_CSS' => 'CSS aktualisiert', + 'SETTINGS_SUCCESS_JS' => 'JS aktualisiert', + 'SETTINGS_SUCCESS_UPDATE' => 'Einstellungen erfolgreich aktualisiert', + 'SETTINGS_DROPBOX_KEY' => 'Dropbox-API-Schlüssel', + 'SETTINGS_ADVANCED_WARNING_EXPL' => 'Ändern dieser erweiterten Einstellungen kann sich negativ auf die Stabilität, Sicherheit und Geschwindigkeit dieser Anwendung auswirken. Sie sollten sie nur ändern, wenn Sie genau wissen, was Sie tun.', + 'SETTINGS_ADVANCED_SAVE' => 'Änderungen speichern, ich kenne das Risiko!', + + 'U2F_NOT_SUPPORTED' => 'U2F wird nicht unterstützt. Sorry.', + 'U2F_NOT_SECURE' => 'Umgebung ist nicht sicher. U2F ist nicht verfügbar.', + 'U2F_REGISTER_KEY' => 'Neues Gerät registrieren', + 'U2F_REGISTRATION_SUCCESS' => 'Registrierung erfolgreich!', + 'U2F_AUTHENTIFICATION_SUCCESS' => 'Authentifizierung erfolgreich!', + 'U2F_CREDENTIALS' => 'Anmeldedaten', + 'U2F_CREDENTIALS_DELETED' => 'Anmeldedaten gelöscht!', + 'U2F_LOGIN' => 'Mit WebAuthn anmelden', + + 'NEW_PHOTOS_NOTIFICATION' => 'E-Mails für neue Fotos senden', + 'SETTINGS_SUCCESS_NEW_PHOTOS_NOTIFICATION' => 'Benachrichtigung für neue Fotos aktualisiert', + 'USER_EMAIL_INSTRUCTION' => 'Geben Sie Ihre E-Mail-Adresse unten ein, um Benachrichtigungen zu aktivieren. Um Benachrichtigungen zu deaktivieren, entfernen Sie die E-Mail-Adresse unten einfach.', + + 'LOGIN_USERNAME' => 'Neuer Benutzername', + 'LOGIN_PASSWORD' => 'Neues Kennwort', + 'LOGIN_PASSWORD_CONFIRM' => 'Passwort bestätigen', + 'PASSWORD_TITLE' => 'Geben Sie Ihr bestehendes Kennwort ein:', + 'PASSWORD_CURRENT' => 'Bestehendes Kennwort', + 'PASSWORD_TEXT' => 'Ihr Benutzername und Passwort werden wie folgt geändert:', + 'PASSWORD_CHANGE' => 'Benutzer ändern', + + 'EDIT_SHARING_TITLE' => 'Freigabe bearbeiten', + 'EDIT_SHARING_TEXT' => 'Die Freigabeeinstellungen für dieses Album werden wie folgt geändert:', + 'SHARE_ALBUM_TEXT' => 'Dieses Album wird mit folgenden Einstellungen freigegeben:', + + 'SORT_DIALOG_ATTRIBUTE_LABEL' => 'Attribut', + 'SORT_DIALOG_ORDER_LABEL' => 'Reihenfolge', + + 'SORT_ALBUM_BY' => 'Alben nach %1$s in einer %2$s Reihenfolge sortieren.', + + 'SORT_ALBUM_SELECT_1' => 'Erstellungszeitpunkt', + 'SORT_ALBUM_SELECT_2' => 'Titel', + 'SORT_ALBUM_SELECT_3' => 'Beschreibung', + 'SORT_ALBUM_SELECT_5' => 'Neuestes Aufnahmedatum', + 'SORT_ALBUM_SELECT_6' => 'Ältestes Aufnahmedatum', + + 'SORT_PHOTO_BY' => 'Fotos nach %1$s in einer %2$s Reihenfolge sortieren.', + + 'SORT_PHOTO_SELECT_1' => 'Zeitpunkt des Hochladens', + 'SORT_PHOTO_SELECT_2' => 'Aufnahmedatum', + 'SORT_PHOTO_SELECT_3' => 'Titel', + 'SORT_PHOTO_SELECT_4' => 'Beschreibung', + 'SORT_PHOTO_SELECT_6' => 'Favorit', + 'SORT_PHOTO_SELECT_7' => 'Fotoformat', + + 'SORT_ASCENDING' => 'aufsteigenden', + 'SORT_DESCENDING' => 'absteigenden', + 'SORT_CHANGE' => 'Sortierung ändern', + + 'DROPBOX_TITLE' => 'Dropbox-Schlüssel festlegen', + 'DROPBOX_TEXT' => "Um Ihre Fotos von Dropbox zu importieren, brauchen Sie einen gültigen API-Key von der Dropbox-Webseite. Erstellen Sie einen persönlichen Schlüssel und geben Sie ihn hier ein:", + + 'LANG_TEXT' => 'Sprache für Lychee ändern:', + 'LANG_TITLE' => 'Sprache festlegen', + + 'SETTING_RECENT_PUBLIC_TEXT' => 'Intelligentes Album „Zuletzt benutzt“ für anonyme Nutzer aktivieren', + 'SETTING_STARRED_PUBLIC_TEXT' => 'Intelligentes Album „Favoriten“ für anonyme Nutzer aktivieren', + 'SETTING_ONTHISDAY_PUBLIC_TEXT' => 'Intelligentes Album „An diesem Tag“ für anonyme Nutzer aktivieren', + + 'CSS_TEXT' => 'CSS personalisieren:', + 'CSS_TITLE' => 'CSS ändern', + 'JS_TEXT' => 'JS personalisieren:', + 'JS_TITLE' => 'JS ändern', + 'PUBLIC_SEARCH_TEXT' => 'Öffentliche Suche erlauben', + 'OVERLAY_TYPE' => 'Daten für Foto-Overlay:', + 'OVERLAY_NONE' => 'Kein Overlay', + 'OVERLAY_EXIF' => 'EXIF-Daten des Fotos', + 'OVERLAY_DESCRIPTION' => 'Beschreibung des Fotos', + 'OVERLAY_DATE' => 'Erstellungsdatum des Fotos', + 'ALBUM_DECORATION' => 'Albumdekoration:', + 'ALBUM_DECORATION_NONE' => 'Keine', + 'ALBUM_DECORATION_ORIGINAL' => 'Unteralben ohne Anzahl', + 'ALBUM_DECORATION_ALBUM' => 'Anzahl Unteralben', + 'ALBUM_DECORATION_PHOTO' => 'Anzahl Fotos', + 'ALBUM_DECORATION_ALL' => 'Anzahl Unteralben und Fotos', + 'ALBUM_DECORATION_ORIENTATION' => 'Ausrichtung der Albumdekoration:', + 'ALBUM_DECORATION_ORIENTATION_ROW' => 'Horizontal (Fotos, Alben)', + 'ALBUM_DECORATION_ORIENTATION_ROW_REVERSE' => 'Horizontal (Alben, Fotos)', + 'ALBUM_DECORATION_ORIENTATION_COLUMN' => 'Vertikal (oben Fotos, Alben)', + 'ALBUM_DECORATION_ORIENTATION_COLUMN_REVERSE' => 'Vertikal (oben Alben, Fotos)', + 'MAP_DISPLAY_TEXT' => 'Kartenfunktionalitäten aktivieren (OpenStreetMap)', + 'MAP_DISPLAY_PUBLIC_TEXT' => 'Kartenfunktionalität für öffentliche Alben aktivieren (OpenStreetMap)', + 'MAP_PROVIDER' => 'Provider für OpenStreetMap-Karten:', + 'MAP_PROVIDER_WIKIMEDIA' => 'Wikimedia', + 'MAP_PROVIDER_OSM_ORG' => 'OpenStreetMap.org (kein HiDPI)', + 'MAP_PROVIDER_OSM_DE' => 'OpenStreetMap.de (kein HiDPI)', + 'MAP_PROVIDER_OSM_FR' => 'OpenStreetMap.fr (kein HiDPI)', + 'MAP_PROVIDER_RRZE' => 'Universität Erlangen, Deutschland (nur HiDPI)', + 'MAP_INCLUDE_SUBALBUMS_TEXT' => 'Fotos von Unterordnern für Karten berücksichtigen', + 'LOCATION_DECODING' => 'Ortsnamen mittels GPS-Daten bestimmen', + 'LOCATION_SHOW' => 'Zeige Ortsnamen', + 'LOCATION_SHOW_PUBLIC' => 'Zeige Ortsnamen für öffentliche Alben', + + 'LAYOUT_TYPE' => 'Layout des Fotos:', + 'LAYOUT_SQUARES' => 'Quadratische Miniaturansichten', + 'LAYOUT_JUSTIFIED' => 'Seitenverhältnis beibehalten, Blocksatz', + 'LAYOUT_MASONRY' => 'Seitenverhältnis beibehalten, masonry', + 'LAYOUT_GRID' => 'Seitenverhältnis beibehalten, Gitter', + 'LAYOUT_UNJUSTIFIED' => 'Seitenverhältnis beibehalten, Flattersatz', + 'SET_LAYOUT' => 'Ausgerichtetes Layout benutzen:', + + 'NSFW_VISIBLE_TEXT_1' => 'Sensible Alben sind standardmäßig auf sichtbar', + 'NSFW_VISIBLE_TEXT_2' => 'Wenn das Album öffentlich ist, kann weiterhin zugegriffen werden. Es wird nur ausgeblendet und kann durch Drücken der Taste H sichtbar gemacht werden..', + 'SETTINGS_SUCCESS_NSFW_VISIBLE' => 'Standardmäßige Sichtbarkeit wurde erfolgreich geändert.', + + 'NSFW_BANNER' => '

Kritischer Inhalt

Diese Album enthält kritische Inhalte, die manche Personen anstößig oder verstörend finden könnten.

Zur Einwilligung klicken.

', + 'NSFW_HEADER' => 'Kritischer Inhalt', + 'NSFW_EXPLANATION' => 'Diese Album enthält kritische Inhalte, die manche Personen anstößig oder verstörend finden könnten.', + 'TAP_CONSENT' => 'Zur Einwilligung klicken.', + + 'VIEW_NO_RESULT' => 'Keine Ergebnisse', + 'VIEW_NO_PUBLIC_ALBUMS' => 'Keine öffentlichen Alben', + 'VIEW_NO_CONFIGURATION' => 'Keine Konfiguration', + 'VIEW_PHOTO_NOT_FOUND' => 'Foto nicht gefunden', + + 'NO_TAGS' => 'Keine Tags', + + 'UPLOAD_MANAGE_NEW_PHOTOS' => 'Sie können jetzt Ihre neuen Fotos verwalten.', + 'UPLOAD_COMPLETE' => 'Hochladen abgeschlossen', + 'UPLOAD_COMPLETE_FAILED' => 'Fehler beim Hochladen eines oder mehrerer Fotos.', + 'UPLOAD_IMPORTING' => 'Importieren', + 'UPLOAD_IMPORTING_URL' => 'URL importieren', + 'UPLOAD_UPLOADING' => 'Hochladen', + 'UPLOAD_FINISHED' => 'Beendet', + 'UPLOAD_PROCESSING' => 'Verarbeiten', + 'UPLOAD_FAILED' => 'Fehlgeschlagen', + 'UPLOAD_FAILED_ERROR' => 'Hochladen fehlgeschlagen. Der Server hat einen Fehler gemeldet!', + 'UPLOAD_FAILED_WARNING' => 'Hochladen fehlgeschlagen. Der Server hat eine Warnung ausgegeben!', + 'UPLOAD_CANCELLED' => 'Abgebrochen', + 'UPLOAD_SKIPPED' => 'Übersprungen', + 'UPLOAD_UPDATED' => 'Upgedatet', + 'UPLOAD_GENERAL' => 'Allgemein', + 'UPLOAD_IMPORT_SKIPPED_DUPLICATE' => 'Dieses Foto wurde übersprungen, da es bereits in deiner Bibliothek vorhanden ist.', + 'UPLOAD_IMPORT_RESYNCED_DUPLICATE' => 'Dieses Foto wurde übersprungen, da es bereits in deiner Bibliothek vorhanden ist, jedoch wurden die Metadaten upgedatet.', + 'UPLOAD_ERROR_CONSOLE' => 'Bitte schauen Sie in die Konsole Ihres Browsers, um weiter Details zu erfahren.', + 'UPLOAD_UNKNOWN' => 'Der Server hat eine unbekannte Antwort gegeben. Bitte schauen Sie in die Konsole Ihres Browsers, um weiter Details zu erfahren.', + 'UPLOAD_ERROR_UNKNOWN' => 'Hochladen fehlgeschlagen. Der Server hat einen unbekannten Fehler gemeldet!', + 'UPLOAD_ERROR_POSTSIZE' => 'Upload fehlgeschlagen. Das post_max_size_Limit ist zu klein!', + 'UPLOAD_ERROR_FILESIZE' => 'Upload fehlgeschlagen. Das upload_max_filesize-Limit ist zu klein!', + 'UPLOAD_IN_PROGRESS' => 'Lychee ist gerade beim Hochladen!', + 'UPLOAD_IMPORT_WARN_ERR' => 'Der Import ist fertig, hat aber Warnungen oder Fehler zurückgegeben. Schauen Sie bitte ins Protokoll (Einstellungen/Protokoll ansehen).', + 'UPLOAD_IMPORT_COMPLETE' => 'Import abgeschlossen', + 'UPLOAD_IMPORT_INSTR' => 'Geben Sie bitte den direkten Link ein, um ihn zu importieren:', + 'UPLOAD_IMPORT' => 'Importieren', + 'UPLOAD_IMPORT_SERVER' => 'Importieren von Server', + 'UPLOAD_IMPORT_SERVER_FOLD' => 'Der Ordner ist leer oder enthält keine lesbaren Dateien zum Verarbeiten. Schauen Sie bitte ins Protokoll (Einstellungen/Protokoll ansehen).', + 'UPLOAD_IMPORT_SERVER_INSTR' => 'Importiert alle Fotos, Ordner und Unterordner in den folgenden absoluten Pfaden (auf dem Server). Mehrere Pfade können mit Leerzeichen getrennt werden; mit \\ können Sie ein Leerzeichen im Pfad verwenden.', + 'UPLOAD_ABSOLUTE_PATH' => 'Absolute Pfade zu Verzeichnissen, mit Leerzeichen getrennt', + 'UPLOAD_IMPORT_SERVER_EMPT' => 'Konnte Import nicht starten, weil der Ordner leer ist.', + 'UPLOAD_IMPORT_DELETE_ORIGINALS' => 'Originale löschen', + 'UPLOAD_IMPORT_DELETE_ORIGINALS_EXPL' => 'Die Originaldateien werden nach dem Import gelöscht, falls möglich.', + 'UPLOAD_IMPORT_VIA_SYMLINK' => 'Symbolischer Link', + 'UPLOAD_IMPORT_VIA_SYMLINK_EXPL' => 'Importiere Dateien durch symbolische Links zu den Originalen.', + 'UPLOAD_IMPORT_SKIP_DUPLICATES' => 'Überspringe Duplikate', + 'UPLOAD_IMPORT_SKIP_DUPLICATES_EXPL' => 'Bestehende Medien-Dateien wurden übersprungen.', + 'UPLOAD_IMPORT_RESYNC_METADATA' => 'Synchronisiere Metadaten erneut', + 'UPLOAD_IMPORT_RESYNC_METADATA_EXPL' => 'Update Metadaten der bestehenden Medien-Dateien.', + 'UPLOAD_IMPORT_LOW_MEMORY_EXPL' => 'Der Importprozess auf dem Server nähert sich dem Speicherlimit und wird eventuell vorzeitig beendet.', + 'UPLOAD_WARNING' => 'Warnung', + 'UPLOAD_IMPORT_NOT_A_DIRECTORY' => 'Der angegebene Pfad ist kein lesbares Verzeichnis!', + 'UPLOAD_IMPORT_PATH_RESERVED' => 'Der angegebene Pfad ist ein von Lychee reservierter Pfad!', + 'UPLOAD_IMPORT_FAILED' => 'The Datei konnte nicht importiert werden!', + 'UPLOAD_IMPORT_UNSUPPORTED' => 'Dateityp wird nicht unterstützt!', + 'UPLOAD_IMPORT_CANCELLED' => 'Import abgebrochen', + + 'ABOUT_SUBTITLE' => 'Selbst gehostetes Foto-Management, aber richtig!', + 'ABOUT_DESCRIPTION' => 'Lychee ist ein freies Foto-Management-Werkzeug, dass auf Ihrem Server oder Webspace läuft. Die Installation ist eine Sache von Sekunden. Hochladen, Organisieren und Teilen von Fotos funktioniert wie in einer nativen Anwendung. Lychee hält alles bereit, was Sie benötigen, und alle Bilder werden sicher abgespeichert.', + 'FOOTER_COPYRIGHT' => 'Alle Bilder auf dieser Website unterliegen dem Urheberrecht von %1$s © %2$s', + 'HOSTED_WITH_LYCHEE' => 'Läuft mit Lychee', + + 'URL_COPY_TO_CLIPBOARD' => 'In die Zwischenablage kopiert', + 'URL_COPIED_TO_CLIPBOARD' => 'URL in die Zwischenablage kopiert!', + 'PHOTO_DIRECT_LINKS_TO_IMAGES' => 'Direkte Links zu den Bilddateien:', + 'PHOTO_ORIGINAL' => 'Original', + 'PHOTO_MEDIUM' => 'Mittlere Größe', + 'PHOTO_MEDIUM_HIDPI' => 'Mittlere Größe HiDPI', + 'PHOTO_SMALL' => 'Miniaturansicht', + 'PHOTO_SMALL_HIDPI' => 'Miniaturansicht HiDPI', + 'PHOTO_THUMB' => 'Quadratische Miniaturansicht', + 'PHOTO_THUMB_HIDPI' => 'Quadratische Miniaturansicht HiDPI', + 'PHOTO_PLACEHOLDER' => 'Low Quality Image Placeholder', + 'PHOTO_THUMBNAIL' => 'Foto-Vorschau', + 'PHOTO_LIVE_VIDEO' => 'Video des Live-Fotos', + 'PHOTO_VIEW' => 'Lychees Foto-Ansicht:', + + 'PHOTO_EDIT_ROTATECWISE' => 'Im Uhrzeigersinn drehen', + 'PHOTO_EDIT_ROTATECCWISE' => 'Gegen den Uhrzeigersinn drehen', + + 'ERROR_GPX' => 'Fehler beim Laden der GPX-Datei: ', + 'ERROR_EITHER_ALBUMS_OR_PHOTOS' => 'Entweder Alben oder Fotos auswählen!', + 'ERROR_COULD_NOT_FIND' => 'Konnte nicht finden, was Sie suchen.', + 'ERROR_INVALID_EMAIL' => 'Ungültige E-Mail-Adresse', + 'EMAIL_SUCCESS' => 'E-Mail-Adresse aktualisiert!', + 'ERROR_PHOTO_NOT_FOUND' => 'Fehler: Foto „%s“ wurde nicht gefunden!', + 'ERROR_EMPTY_USERNAME' => 'Neuer Benutzername kann nicht leer sein.', + 'ERROR_PASSWORD_DOES_NOT_MATCH' => 'Passwörter stimmt nicht überein.', + 'ERROR_EMPTY_PASSWORD' => 'Neues Passwort kann nicht leer sein.', + 'ERROR_SELECT_ALBUM' => 'Wählen Sie ein Album zum Freigeben aus!', + 'ERROR_SELECT_USER' => 'Wählen Sie einen Benutzer aus, mit dem das Album geteilt wird!', + 'ERROR_SELECT_SHARING' => 'Zu entfernende Freigabe auswählen!', + 'SHARING_SUCCESS' => 'Freigabe aktualisiert!', + 'SHARING_REMOVED' => 'Freigabe entfernt!', + 'USER_CREATED' => 'Benutzer erstellt!', + 'USER_DELETED' => 'Benutzer gelöscht!', + 'USER_UPDATED' => 'Benutzer aktualisiert!', + 'ENTER_EMAIL' => 'Geben Sie Ihre E-Mail-Adresse ein:', + 'ERROR_ALBUM_JSON_NOT_FOUND' => 'Fehler: Album-JSON wurde nicht gefunden!', + 'ERROR_ALBUM_NOT_FOUND' => 'Fehler: Album „%s“ wurde nicht gefunden', + 'ERROR_DROPBOX_KEY' => 'Fehler: Dropbox-Schlüssel nicht gesetzt', + 'ERROR_SESSION' => 'Sitzung abgelaufen', + 'CAMERA_DATE' => 'Kameradatum', + 'NEW_PASSWORD' => 'Neues Password', + 'ALLOW_UPLOADS' => 'Hochladen erlauben', + 'ALLOW_USER_SELF_EDIT' => 'Erlaube Selbstverwaltung des Nutzerkontos', + 'OSM_CONTRIBUTORS' => 'OpenStreetMap-Beitragende', +]; diff --git a/lang/de/maintenance.php b/lang/de/maintenance.php new file mode 100644 index 00000000000..d632d542d1e --- /dev/null +++ b/lang/de/maintenance.php @@ -0,0 +1,59 @@ + 'Wartung', + 'description' => 'Auf dieser Seite finden Sie alle notwendigen Funktionen für den reibungslosen Betrieb Ihrer Lychee Installation.', + 'cleaning' => [ + 'title' => 'Säubern %s', + 'result' => '%s gelöscht.', + 'description' => 'Lösche den gesamten Inhalt aus %s', + 'button' => 'Säubern', + ], + 'fix-jobs' => [ + 'title' => 'Job Historie reparieren', + 'description' => 'Markiere Jobs mit dem Status %s oder %s als %s.', + 'button' => 'Repariere Job Historie', + ], + 'gen-sizevariants' => [ + 'title' => 'Fehlende %s', + 'description' => 'Es wurden %d %s gefunden, welche noch angelegt werden können.', + 'button' => 'Anlegen', + 'success' => 'Erfolgreich angelegt. %d %s.', + ], + 'fill-filesize-sizevariants' => [ + 'title' => 'Fehlende größenvariante', + 'description' => 'Es wurden %d kleine Varianten ohne Dateigröße gefunden.', + 'button' => 'Daten sammeln', + 'success' => 'Die Daten für %d kleine Varianten wurden erfolgreich verarbeitet.', + ], + 'fix-tree' => [ + 'title' => 'Baumstruktur Statistik', + 'Oddness' => 'Eigenartig', + 'Duplicates' => 'Duplikate', + 'Wrong parents' => 'Falsche Oberkategorie', + 'Missing parents' => 'Fehlende Oberkategorie', + 'button' => 'Baumstruktur reparieren', + ], + 'optimize' => [ + 'title' => 'Datenbank optimieren', + 'description' => 'Wenn Sie eine Verlangsamung Ihrer Installation festgestellt haben, könnte dies an fehlenden Datenbankindizes liegen.', + 'button' => 'Datenbank optimieren', + ], + 'update' => [ + 'title' => 'Updates', + 'check-button' => 'Auf Updates prüfen', + 'update-button' => 'Update', + 'no-pending-updates' => 'Keine Updates verfügbar.', + ], +]; diff --git a/lang/de/profile.php b/lang/de/profile.php new file mode 100644 index 00000000000..cc24b97452c --- /dev/null +++ b/lang/de/profile.php @@ -0,0 +1,64 @@ + 'Profile', + + 'login' => [ + 'header' => 'Profile', + 'enter_current_password' => 'Enter your current password:', + 'current_password' => 'Current password', + 'credentials_update' => 'Your credentials will be changed to the following:', + 'username' => 'Username', + 'new_password' => 'New password', + 'confirm_new_password' => 'Confirm new password', + 'email_instruction' => 'Add your email below to enable receiving email notifications. To stop receiving emails, simply remove your email below.', + 'email' => 'Email', + 'change' => 'Change Login', + 'api_token' => 'API Token ...', + + 'missing_fields' => 'Missing fields', + ], + + 'token' => [ + 'unavailable' => 'You have already viewed this token.', + 'no_data' => 'No token API have been generated.', + 'disable' => 'Disable', + 'disabled' => 'Token disabled', + 'warning' => 'This token will not be displayed again. Copy it and keep it in a safe place.', + 'reset' => 'Reset the token', + 'create' => 'Create a new token', + ], + + 'oauth' => [ + 'header' => 'OAuth', + 'header_not_available' => 'OAuth is not available', + 'setup_env' => 'Set up the credentials in your .env', + 'token_registered' => '%s token registered.', + 'setup' => 'Set up %s', + 'reset' => 'reset', + 'credential_deleted' => 'Credential deleted!', + ], + + 'u2f' => [ + 'header' => 'Passkey/MFA/2FA', + 'info' => 'This only provides the ability to use WebAuthn to authenticate instead of username & password.', + 'empty' => 'Credentials list is empty!', + 'not_secure' => 'Environment not secured. U2F not available.', + 'new' => 'Register new device.', + 'credential_deleted' => 'Credential deleted!', + 'credential_updated' => 'Credential updated!', + 'credential_registred' => 'Registration successful!', + '5_chars' => 'At least 5 chars.', + ], +]; \ No newline at end of file diff --git a/lang/de/settings.php b/lang/de/settings.php new file mode 100644 index 00000000000..fd197f11135 --- /dev/null +++ b/lang/de/settings.php @@ -0,0 +1,92 @@ + 'Settings', + 'small_screen' => 'For better a experience on the Settings page,
we recommend you use a larger screen.', + 'tabs' => [ + 'basic' => 'Basic', + 'all_settings' => 'All settings', + ], + 'toasts' => [ + 'change_saved' => 'Change saved!', + 'details' => 'Settings have been modified as per request', + 'error' => 'Error!', + 'error_load_css' => 'Could not load dist/user.css', + 'error_load_js' => 'Could not load dist/custom.js', + 'error_save_css' => 'Could not save CSS', + 'error_save_js' => 'Could not save JS', + 'thank_you' => 'Thank you for your support.', + 'reload' => 'Reload your page for full functionalities.', + ], + 'system' => [ + 'header' => 'System', + 'use_dark_mode' => 'Use dark mode for Lychee', + 'language' => 'Language used by Lychee', + 'nsfw_album_visibility' => 'Make Sensitive albums visible by default.', + 'nsfw_album_explanation' => 'If the album is public, it is still accessible, just hidden from the view and can be revealed by pressing H.', + ], + 'lychee_se' => [ + 'header' => 'Lychee SE', + 'call4action' => 'Get exclusive features and support the development of Lychee. Unlock the SE edition.', + 'preview' => 'Enable preview of Lychee SE features', + 'hide_call4action' => 'Hide this Lychee SE registration form. I am happy with Lychee as-is. :)', + 'hide_warning' => 'If enabled, the only way to register your license key will be via the More tab above. Changes are applied on page reload.', + ], + 'dropbox' => [ + 'header' => 'Dropbox', + 'instruction' => 'In order to import photos from your Dropbox, you need a valid drop-ins app key from their website.', + 'api_key' => 'Dropbox API Key', + 'set_key' => 'Set Dropbox Key', + ], + 'gallery' => [ + 'header' => 'Gallery', + 'photo_order_column' => 'Default column used for sorting photos', + 'photo_order_direction' => 'Default order used for sorting photos', + 'album_order_column' => 'Default column used for sorting albums', + 'album_order_direction' => 'Default order used for sorting albums', + 'aspect_ratio' => 'Default aspect ratio for album thumbs', + 'photo_layout' => 'Layout for pictures', + 'album_decoration' => 'Show decorations on album cover (sub-album and/or photo count)', + 'album_decoration_direction' => 'Align album decorations horizontally or vertically', + 'photo_overlay' => 'Default image overlay information', + 'license_default' => 'Default license used for albums', + 'license_help' => 'Need help choosing?', + ], + 'geolocation' => [ + 'header' => 'Geo-location', + 'map_display' => 'Display the map given GPS coordinates', + 'map_display_public' => 'Allow anonymous users to access the map', + 'map_provider' => 'Defines the map provider', + 'map_include_subalbums' => 'Includes pictures of the sub albums on the map', + 'location_decoding' => 'Use GPS location decoding', + 'location_show' => 'Show location extracted from GPS coordinates', + 'location_show_public' => 'Anonymous users can access the extracted location from GPS coordinates', + ], + 'advanced' => [ + 'header' => 'Advanced Customization', + 'change_css' => 'Change CSS', + 'change_js' => 'Change JS', + ], + 'all' => [ + 'old_setting_style' => 'Old setting style', + 'change_detected' => 'Some settings changed.', + 'save' => 'Save', + ], + + 'tool_option' => [ + 'disabled' => 'disabled', + 'enabled' => 'enabled', + 'discover' => 'discover', + ], +]; \ No newline at end of file diff --git a/lang/de/sharing.php b/lang/de/sharing.php new file mode 100644 index 00000000000..69de18cc6d0 --- /dev/null +++ b/lang/de/sharing.php @@ -0,0 +1,33 @@ + 'Sharing', + + 'info' => 'This page gives an overview of and the ability to edit the sharing rights associated with albums.', + 'album_title' => 'Album title', + 'username' => 'Username', + 'no_data' => 'Sharing list is empty.', + 'share' => 'Share', + 'permission_deleted' => 'Permission deleted!', + 'permission_created' => 'Permission created!', + + 'grants' => [ + 'read' => 'Grants read access', + 'original' => 'Grants access to original photo', + 'download' => 'Grants download', + 'upload' => 'Grants upload', + 'edit' => 'Grants edit', + 'delete' => 'Grants delete', + ], +]; \ No newline at end of file diff --git a/lang/de/statistics.php b/lang/de/statistics.php new file mode 100644 index 00000000000..2baf855bbd5 --- /dev/null +++ b/lang/de/statistics.php @@ -0,0 +1,34 @@ + 'Statistics', + + 'preview_text' => 'This is a preview of the statistics page available in Lychee SE.
The data shown here are randomly generated and do not reflect your server.', + 'no_data' => 'User does not have data on server.', + 'collapse' => 'Collapse albums sizes', + + 'total' => [ + 'total' => 'Total', + 'albums' => 'Albums', + 'photos' => 'Photos', + 'size' => 'Size', + ], + 'table' => [ + 'username' => 'Owner', + 'title' => 'Title', + 'photos' => 'Photos', + 'descendants' => 'Children', + 'size' => 'Size', + ], +]; \ No newline at end of file diff --git a/lang/de/toasts.php b/lang/de/toasts.php new file mode 100644 index 00000000000..293d4b72594 --- /dev/null +++ b/lang/de/toasts.php @@ -0,0 +1,17 @@ + 'Error', + 'success' => 'Success', +]; \ No newline at end of file diff --git a/lang/de/users.php b/lang/de/users.php new file mode 100644 index 00000000000..599bb833454 --- /dev/null +++ b/lang/de/users.php @@ -0,0 +1,44 @@ + 'Users', + 'description' => 'Here you can manage the users of your Lychee installation. You can create, edit and delete users.', + 'create' => 'Create a new user', + 'username' => 'Username', + 'password' => 'Password', + 'legend' => 'Legend', + 'upload_rights' => 'When selected, the user can upload content.', + 'edit_rights' => 'When selected, the user can modify their profile (username, password).', + 'quota' => 'When set, the user has a space quota for pictures (in kB).', + + 'user_deleted' => 'User deleted', + 'user_created' => 'User created', + 'user_updated' => 'User updated', + 'change_saved' => 'Change saved!', + + 'create_edit' => [ + 'upload_rights' => 'User can upload content.', + 'edit_rights' => 'User can modify their profile (username, password).', + 'quota' => 'User has quota limit.', + 'quota_kb' => 'quota in kB (0 for default)', + 'note' => 'Admin note (not publically visible)', + 'create' => 'Create', + 'edit' => 'Edit', + ], + 'line' => [ + 'admin' => 'admin user', + 'edit' => 'Edit', + 'delete' => 'Delete', + ], +]; \ No newline at end of file diff --git a/lang/el/aspect_ratio.php b/lang/el/aspect_ratio.php new file mode 100644 index 00000000000..2c7e8fb56ac --- /dev/null +++ b/lang/el/aspect_ratio.php @@ -0,0 +1,21 @@ + '5/4 (instagram landscape)', + '4by5' => '4/5 (instagram portrait)', + '2by3' => '2/3 (portrait)', + '3by2' => '3/2 (landscape)', + '1by1' => 'square', + '1byx9' => '16/9 (landscape)', +]; \ No newline at end of file diff --git a/lang/el/diagnostics.php b/lang/el/diagnostics.php new file mode 100644 index 00000000000..0fadd640428 --- /dev/null +++ b/lang/el/diagnostics.php @@ -0,0 +1,30 @@ + 'Diagnostics', + + 'copy_to_clipboard' => 'Copy diagnostics to clipboard', + 'self-diagnosis' => 'Self-diagnosis', + 'info' => 'Info', + 'space' => 'Space', + 'load_space' => 'Load space usage.', + 'configuration' => 'Configuration', + 'loading' => 'Loading...', + 'identical_content' => 'Identical content', + + 'toast' => [ + 'info' => 'Info', + 'copy' => 'Diagnostics copied to clipboard!', + ], +]; \ No newline at end of file diff --git a/lang/el/dialogs.php b/lang/el/dialogs.php new file mode 100644 index 00000000000..4afd65fae3f --- /dev/null +++ b/lang/el/dialogs.php @@ -0,0 +1,221 @@ + [ + 'close' => 'Close', + 'cancel' => 'Cancel', + 'save' => 'Save', + 'delete' => 'Delete', + 'move' => 'Move', + ], + 'about' => [ + 'subtitle' => 'Self-hosted photo-management done right', + 'description' => 'Lychee is a free photo-management tool, which runs on your server or web-space. Installing is a matter of seconds. Upload, manage and share photos like from a native application. Lychee comes with everything you need and all your photos are stored securely.', + 'update_available' => 'Update available!', + 'thank_you' => 'Thank you for your support!', + 'get_supporter_or_register' => 'Get exclusive features and support the development of Lychee.
Unlock the Supporter Edition or register your License key', + 'here' => 'here', + ], + 'dropbox' => [ + 'not_configured' => 'Dropbox is not configured.', + ], + 'import_from_link' => [ + 'instructions' => 'Please enter the direct link to a photo to import it:', + 'import' => 'Import', + ], + 'keybindings' => [ + 'don_t_show_again' => 'Don\'t show this again', + 'side_wide' => 'Site-wide Shortcuts', + 'back_cancel' => 'Back/Cancel', + 'confirm' => 'Confirm', + 'login' => 'Login', + 'toggle_full_screen' => 'Toggle Full Screen', + 'toggle_sensitive_albums' => 'Toggle Sensitive Albums', + + 'albums' => 'Albums Shortcuts', + 'new_album' => 'New Album', + 'upload_photos' => 'Upload Photos', + 'search' => 'Search', + 'show_this_modal' => 'Show this modal', + 'select_all' => 'Select All', + 'move_selection' => 'Move Selection', + 'delete_selection' => 'Delete Selection', + + 'album' => 'Album Shortcuts', + 'slideshow' => 'Start/Stop Slideshow', + 'toggle' => 'Toggle panel', + + 'photo' => 'Photo Shortcuts', + 'previous' => 'Previous photo', + 'next' => 'Next photo', + 'cycle' => 'Cycle overlay mode', + 'star' => 'Star the photo', + 'move' => 'Move the photo', + 'delete' => 'Delete the photo', + 'edit' => 'Edit information', + 'show_hide_meta' => 'Show information', + + 'keep_hidden' => 'We will keep it hidden.', + ], + 'login' => [ + 'username' => 'Username', + 'password' => 'Password', + 'unknown_invalid' => 'Unknown user or invalid password.', + 'signin' => 'Sign-In', + ], + 'register' => [ + 'enter_license' => 'Enter your license key below:', + 'license_key' => 'License key', + 'invalid_license' => 'Invalid license key.', + 'register' => 'Register', + ], + 'share_album' => [ + 'url_copied' => 'Copied URL to clipboard!', + ], + 'upload' => [ + 'completed' => 'Completed', + 'uploaded' => 'Uploaded:', + 'release' => 'Release file to upload!', + 'select' => 'Click here to select files to upload', + 'drag' => '(Or drag files to the page)', + 'loading' => 'Loading', + 'resume' => 'Resume', + 'uploading' => 'Uploading', + 'finished' => 'Finished', + 'failed_error' => 'Upload failed. The server returned an error!', + ], + 'visibility' => [ + 'public' => 'Public', + 'public_expl' => 'Anonymous users can access this album, subject to the restrictions below.', + 'full' => 'Original', + 'full_expl' => 'Anonymous users can view full-resolution photos.', + 'hidden' => 'Hidden', + 'hidden_expl' => 'Anonymous users need a direct link to access this album.', + 'downloadable' => 'Downloadable', + 'downloadable_expl' => 'Anonymous users can download this album.', + 'password' => 'Password', + 'password_prot' => 'Password protected', + 'password_prot_expl' => 'Anonymous users need a shared password to access this album.', + 'nsfw' => 'Sensitive', + 'nsfw_expl' => 'Album contains sensitive content.', + 'visibility_updated' => 'Visibility updated.', + ], + 'move_album' => [ + 'confirm_single' => 'Are you sure you want to move the album “%1$s” into the album “%2$s”?', + 'confirm_multiple' => 'Are you sure you want to move all selected albums into the album “%s”?', + 'move_single' => 'Move Album', + 'move_to' => 'Move to', + 'move_to_single' => 'Move %s to:', + 'move_to_multiple' => 'Move %d albums to:', + 'no_album_target' => 'No album to move to', + 'moved_single' => 'Album moved!', + 'moved_single_details' => '%1$s moved to %2$s', + 'moved_details' => 'Album(s) moved to %s', + ], + 'new_album' => [ + 'menu' => 'Create Album', + 'info' => 'Enter a title for the new album:', + 'title' => 'title', + 'create' => 'Create Album', + ], + 'new_tag_album' => [ + 'menu' => 'Create Tag Album', + 'info' => 'Enter a title for the new tag album:', + 'title' => 'title', + 'set_tags' => 'Set tags to show', + 'warn' => 'Make sure to press enter after each tag', + 'create' => 'Create Tag Album', + ], + 'delete_album' => [ + 'confirmation' => 'Are you sure you want to delete the album “%s” and all of the photos it contains?', + 'confirmation_multiple' => 'Are you sure you want to delete all %d selected albums and all of the photos they contain?', + 'warning' => 'This action can not be undone!', + 'delete' => 'Delete Album and Photos', + ], + 'transfer' => [ + 'query' => 'Transfer ownership of album to', + 'confirmation' => 'Are you sure you want to transfer the ownership of album “%s” and all the photos it contains to "%s"?', + 'lost_access_warning' => 'Your access to this album will be lost.', + 'warning' => 'This action can not be undone!', + 'transfer' => 'Transfer ownership of album and photos', + ], + 'rename' => [ + 'photo' => 'Enter a new title for this photo:', + 'album' => 'Enter a new title for this album:', + 'rename' => 'Rename', + ], + 'merge' => [ + 'merge_to' => 'Merge %s to:', + 'merge_to_multiple' => 'Merge %d albums to:', + 'no_albums' => 'No albums to merge to.', + 'confirm' => 'Are you sure you want to merge the album “%1$s” into the album “%2$s”?', + 'confirm_multiple' => 'Are you sure you want to merge all selected albums into the album “%s”?', + 'merge' => 'Merge Albums', + 'merged' => 'Album(s) merged to %s!', + ], + 'unlock' => [ + 'password_required' => 'This album is protected by a password. Enter the password below to view the photos of this album:', + 'password' => 'Password', + 'unlock' => 'Unlock', + ], + 'photo_tags' => [ + 'question' => 'Enter your tags for this photo.', + 'question_multiple' => 'Enter your tags for all %d selected photos. Existing tags will be overwritten.', + 'no_tags' => 'No Tags', + 'set_tags' => 'Set Tags', + 'updated' => 'Tags updated!', + 'tags_override_info' => 'If this is unchecked, the tags will be added to the existing tags of the photo.', + ], + 'photo_copy' => [ + 'no_albums' => 'No albums to copy to', + 'copy_to' => 'Copy %s to:', + 'copy_to_multiple' => 'Copy %d photos to:', + 'confirm' => 'Copy %s to %s.', + 'confirm_multiple' => 'Copy %d photos to %s.', + 'copy' => 'Copy', + 'copied' => 'Photo(s) copied!', + ], + 'photo_delete' => [ + 'confirm' => 'Are you sure you want to delete the photo “%s”?', + 'confirm_multiple' => 'Are you sure you want to delete all %d selected photos?', + 'deleted' => 'Photo(s) deleted!', + ], + 'move_photo' => [ + 'move_single' => 'Move %s to:', + 'move_multiple' => 'Move %d photos to:', + 'confirm' => 'Move %s to %s.', + 'confirm_multiple' => 'Move %d photos to %s.', + 'moved' => 'Photo(s) moved to %s!', + ], + 'target_user' => [ + 'placeholder' => 'Select user', + ], + 'target_album' => [ + 'placeholder' => 'Select album', + ], + 'webauthn' => [ + 'u2f' => 'U2F', + 'success' => 'Authentication successful!', + 'error' => 'Whoops, it looks like something went wrong. Please reload the site and try again!', + ], + 'se' => [ + 'available' => 'Available in the Supporter Edition', + ], + 'session_expired' => [ + 'title' => 'Session expired', + 'message' => 'Your session has expired.
Please reload the page.', + 'reload' => 'Reload', + 'go_to_gallery' => 'Go to the Gallery', + ], +]; \ No newline at end of file diff --git a/lang/el/fix-tree.php b/lang/el/fix-tree.php new file mode 100644 index 00000000000..64803e310e6 --- /dev/null +++ b/lang/el/fix-tree.php @@ -0,0 +1,55 @@ + 'Maintenance', + 'intro' => 'This page allows you to re-order and fix your albums manually.
Before any modifications, we strongly recommend you to read about Nested Set tree structures.', + 'warning' => 'You can really break your Lychee installation here, modify values at your own risks.', + + 'help' => [ + 'header' => 'Help', + 'hover' => 'Hover ids or titles to highlight related albums.', + 'left' => 'Left', + 'right' => 'Right', + 'convenience' => 'For your convenience, the and buttons allow you to change the values of %s and %s by respectively +1 and -1 with propagation.', + 'left-right-warn' => 'The and indicates that the value of %s (and respectively %s) is duplicated somewhere.', + 'parent-marked' => 'Marked Parent Id indicates that the %s and %s do not satisfy the Nest Set tree structures. Edit either the Parent Id or the %s/%s values.', + 'slowness' => 'This page will be slow with a large number of albums.', + ], + + 'buttons' => [ + 'reset' => 'Reset', + 'check' => 'Check', + 'apply' => 'Apply', + ], + + 'table' => [ + 'title' => 'Title', + 'left' => 'Left', + 'right' => 'Right', + 'id' => 'Id', + 'parent' => 'Parent Id', + ], + + 'errors' => [ + 'invalid' => 'Invalid tree!', + 'invalid_details' => 'We are not applying this as it is guaranteed to be a broken state.', + 'invalid_left' => 'Album %s has an invalid left value.', + 'invalid_right' => 'Album %s has an invalid right value.', + 'invalid_left_right' => 'Album %s has an invalid left/right values. Left should be strictly smaller than right: %s < %s.', + 'duplicate_left' => 'Album %s has a duplicate left value %s.', + 'duplicate_right' => 'Album %s has a duplicate right value %s.', + 'parent' => 'Album %s has an unexpected parent id %s.', + 'unknown' => 'Album %s has an unknown error.', + ], +]; \ No newline at end of file diff --git a/lang/el/gallery.php b/lang/el/gallery.php new file mode 100644 index 00000000000..eb8008827e0 --- /dev/null +++ b/lang/el/gallery.php @@ -0,0 +1,241 @@ + 'Gallery', + + 'smart_albums' => 'Smart albums', + 'albums' => 'Albums', + 'root' => 'Albums', + + 'original' => 'Original', + 'medium' => 'Medium', + 'medium_hidpi' => 'Medium HiDPI', + 'small' => 'Thumb', + 'small_hidpi' => 'Thumb HiDPI', + 'thumb' => 'Square thumb', + 'thumb_hidpi' => 'Square thumb HiDPI', + 'placeholder' => 'Low Quality Image Placeholder', + 'thumbnail' => 'Photo thumbnail', + 'live_video' => 'Video part of live-photo', + + 'camera_data' => 'Camera date', + 'album_reserved' => 'All Rights Reserved', + + 'map' => [ + 'error_gpx' => 'Error loading GPX file', + 'osm_contributors' => 'OpenStreetMap contributors', + ], + + 'search' => [ + 'title' => 'Search', + 'searching' => 'Searching…', + 'no_results' => 'Nothing matches your search query.', + 'searchbox' => 'Search…', + 'minimum_chars' => 'Minimum %s characters required.', + 'photos' => 'Photos (%s)', + 'albums' => 'Albums (%s)', + ], + + 'smart_album' => [ + 'unsorted' => 'Unsorted', + 'starred' => 'Starred', + 'recent' => 'Recent', + 'public' => 'Public', + 'on_this_day' => 'On This Day', + ], + + 'layout' => [ + 'squares' => 'Square thumbnails', + 'justified' => 'With aspect, justified', + 'masonry' => 'With aspect, masonry', + 'grid' => 'With aspect, grid', + ], + + 'overlay' => [ + 'none' => 'None', + 'exif' => 'EXIF data', + 'description' => 'Description', + 'date' => 'Date taken', + ], + + 'timeline' => [ + 'default' => 'default', + 'disabled' => 'disabled', + 'year' => 'Year', + 'month' => 'Month', + 'day' => 'Day', + 'hour' => 'Hour', + ], + + 'album' => [ + 'header_albums' => 'Albums', + 'header_photos' => 'Photos', + 'no_results' => 'Nothing to see here', + 'upload' => 'Upload photos', + + 'tabs' => [ + 'about' => 'About Album', + 'share' => 'Share Album', + 'move' => 'Move Album', + 'danger' => 'DANGER ZONE', + ], + + 'hero' => [ + 'created' => 'Created', + 'copyright' => 'Copyright', + 'subalbums' => 'Subalbums', + 'images' => 'Photos', + 'download' => 'Download Album', + 'share' => 'Share Album', + 'stats_only_se' => 'Statistics available in the Supporter Edition', + ], + + 'stats' => [ + 'lens' => 'Lens', + 'shutter' => 'Shutter speed', + 'iso' => 'ISO', + 'model' => 'Model', + 'aperture' => 'Aperture', + 'no_data' => 'No data', + ], + + 'properties' => [ + 'title' => 'Title', + 'description' => 'Description', + 'photo_ordering' => 'Order photos by', + 'children_ordering' => 'Order albums by', + 'asc/desc' => 'asc/desc', + 'header' => 'Set album header', + 'compact_header' => 'Use compact header', + 'license' => 'Set license', + 'copyright' => 'Set copyright', + 'aspect_ratio' => 'Set album thumbs aspect ratio', + 'album_timeline' => 'Set album timeline mode', + 'photo_timeline' => 'Set photo timeline mode', + 'layout' => 'Set photo layout', + 'show_tags' => 'Set tags to show', + 'tags_required' => 'Tags are required.', + ], + ], + + 'photo' => [ + 'actions' => [ + 'star' => 'Star', + 'unstar' => 'Unstar', + 'set_album_header' => 'Set as album header', + 'move' => 'Move', + 'delete' => 'Delete', + 'header_set' => 'Header set', + ], + + 'details' => [ + 'about' => 'About', + 'basics' => 'Basics', + 'title' => 'Title', + 'uploaded' => 'Uploaded', + 'description' => 'Description', + 'license' => 'License', + 'reuse' => 'Reuse', + 'latitude' => 'Latitude', + 'longitude' => 'Longitude', + 'altitude' => 'Altitude', + 'location' => 'Location', + 'image' => 'Image', + 'video' => 'Video', + 'size' => 'Size', + 'format' => 'Format', + 'resolution' => 'Resolution', + 'duration' => 'Duration', + 'fps' => 'Frame rate', + 'tags' => 'Tags', + 'camera' => 'Camera', + 'captured' => 'Captured', + 'make' => 'Make', + 'type' => 'Type/Model', + 'lens' => 'Lens', + 'shutter' => 'Shutter Speed', + 'aperture' => 'Aperture', + 'focal' => 'Focal Length', + 'iso' => 'ISO %s', + ], + + 'edit' => [ + 'set_title' => 'Set Title', + 'set_description' => 'Set Description', + 'set_license' => 'Set License', + 'no_tags' => 'No Tags', + 'set_tags' => 'Set Tags', + 'set_created_at' => 'Set Upload Date', + ], + ], + + 'nsfw' => [ + 'header' => 'Sensitive content', + 'description' => 'This album contains sensitive content which some people may find offensive or disturbing.', + 'consent' => 'Tap to consent.', + ], + + 'menus' => [ + 'star' => 'Star', + 'unstar' => 'Unstar', + 'star_all' => 'Star Selected', + 'unstar_all' => 'Unstar Selected', + 'tag' => 'Tag', + 'tag_all' => 'Tag Selected', + 'set_cover' => 'Set Album Cover', + 'remove_header' => 'Remove Album Header', + 'set_header' => 'Set Album Header', + 'copy_to' => 'Copy to …', + 'copy_all_to' => 'Copy Selected to …', + 'rename' => 'Rename', + 'move' => 'Move', + 'move_all' => 'Move Selected', + 'delete' => 'Delete', + 'delete_all' => 'Delete Selected', + 'download' => 'Download', + 'download_all' => 'Download Selected', + 'merge' => 'Merge', + 'merge_all' => 'Merge Selected', + + 'upload_photo' => 'Upload Photo', + 'import_link' => 'Import from Link', + 'import_dropbox' => 'Import from Dropbox', + 'new_album' => 'New Album', + 'new_tag_album' => 'New Tag Album', + 'upload_track' => 'Upload track', + 'delete_track' => 'Delete track', + ], + + 'sort' => [ + 'photo_select_1' => 'Upload Time', + 'photo_select_2' => 'Take Date', + 'photo_select_3' => 'Title', + 'photo_select_4' => 'Description', + 'photo_select_6' => 'Star', + 'photo_select_7' => 'Photo Format', + 'ascending' => 'Ascending', + 'descending' => 'Descending', + 'album_select_1' => 'Creation Time', + 'album_select_2' => 'Title', + 'album_select_3' => 'Description', + 'album_select_5' => 'Latest Take Date', + 'album_select_6' => 'Oldest Take Date', + ], + + 'albums_protection' => [ + 'private' => 'private', + 'public' => 'public', + 'inherit_from_parent' => 'inherit from parent', + ], +]; \ No newline at end of file diff --git a/lang/el/jobs.php b/lang/el/jobs.php new file mode 100644 index 00000000000..5d952b76012 --- /dev/null +++ b/lang/el/jobs.php @@ -0,0 +1,18 @@ + 'Jobs', + + 'no_data' => 'No Jobs have been executed yet.', +]; \ No newline at end of file diff --git a/lang/el/landing.php b/lang/el/landing.php new file mode 100644 index 00000000000..fe6fe55b8ea --- /dev/null +++ b/lang/el/landing.php @@ -0,0 +1,19 @@ + 'Gallery', + 'access_gallery' => 'Access the gallery', + 'hosted_with_lychee' => 'Hosted with Lychee', + 'copyright' => 'All images on this website are subject to copyright by %1$s © %2$s', +]; \ No newline at end of file diff --git a/lang/el/left-menu.php b/lang/el/left-menu.php new file mode 100644 index 00000000000..9a3e91f4037 --- /dev/null +++ b/lang/el/left-menu.php @@ -0,0 +1,29 @@ + 'Back to Gallery', + + 'admin' => 'Admin', + 'clockwork' => 'Clockwork App', + 'logs' => 'Show Logs', + 'jobs' => 'Show Job History', + 'user' => 'User', + + 'sign_out' => 'Sign Out', + + 'about' => 'About', + 'api' => 'API Documentation', + 'source_code' => 'Source Code', + 'support' => 'Support', +]; \ No newline at end of file diff --git a/lang/el/lychee.php b/lang/el/lychee.php new file mode 100644 index 00000000000..1290e4d0d8a --- /dev/null +++ b/lang/el/lychee.php @@ -0,0 +1,535 @@ + 'Óνομα χρήστη', + 'PASSWORD' => 'Κωδικός πρόσβασης', + 'ENTER' => 'Είσοδος', + 'CANCEL' => 'Άκυρο', + 'CONFIRM' => 'Confirm', + 'SIGN_IN' => 'Συνδεθείτε', + 'CLOSE' => 'Κλείσιμο', + 'SETTINGS' => 'Ρυθμίσεις', + 'SEARCH' => 'Αναζήτηση …', + 'MORE' => 'Περισσότερα', + 'DEFAULT' => 'Default', + 'GALLERY' => 'Gallery', + + 'USERS' => 'Χρήστες', + 'PROFILE' => 'Profile', + 'CREATE' => 'Create', + 'REMOVE' => 'Remove', + 'SHARE' => 'Share', + 'U2F' => 'U2F', + 'NOTIFICATIONS' => 'Notifications', + 'SHARING' => 'Κοινή χρήση', + 'CHANGE_LOGIN' => 'Αλλαγή σύνδεσης', + 'CHANGE_SORTING' => 'Αλλαγή Ταξινόμησης', + 'SET_DROPBOX' => 'Ορίστε λογαριασμό Dropbox', + 'ABOUT_LYCHEE' => 'Περί Lychee', + 'DIAGNOSTICS' => 'Διαγνωστικά', + 'DIAGNOSTICS_GET_SIZE' => 'Request space usage', + 'JOBS' => 'Show job history', + 'LOGS' => 'Εμφάνιση Καταγραφών', + 'SIGN_OUT' => 'Αποσύνδεση', + 'UPDATE_AVAILABLE' => 'Διαθέσιμη Ενημέρωση!', + 'MIGRATION_AVAILABLE' => 'Migration available!', + 'CHECK_FOR_UPDATE' => 'Check for updates', + 'DEFAULT_LICENSE' => 'Προεπιλεγμένη άδεια για τις νέες μεταφορτώσεις:', + 'SET_LICENSE' => 'Ορισμός Άδειας', + 'SET_OVERLAY_TYPE' => 'Ορισμός Τύπου Overlay', + 'SET_ALBUM_DECORATION' => 'Set album decorations', + 'SET_MAP_PROVIDER' => 'Set OpenStreetMap tiles provider', + 'FULL_SETTINGS' => 'Full Settings', + 'UPDATE' => 'Update', + 'RESET' => 'Reset', + 'DISABLE_TOKEN_TOOLTIP' => 'Disable', + 'ENABLE_TOKEN' => 'Enable API token', + 'DISABLED_TOKEN_STATUS_MSG' => 'Disabled', + 'TOKEN_BUTTON' => 'API Token ...', + 'TOKEN_NOT_AVAILABLE' => 'You have already viewed this token.', + 'TOKEN_WAIT' => 'Wait ...', + + 'SMART_ALBUMS' => 'Έξυπνα λευκώματα', + 'SHARED_ALBUMS' => 'Κοινόχρηστα λευκώματα', + 'ALBUMS' => 'Λευκώματα', + 'PHOTOS' => 'Εικόνες', + 'SEARCH_RESULTS' => 'Search results', + + 'RENAME' => 'Μετονομασία', + 'RENAME_ALL' => 'Μετονομασία Επιλεγμένων', + 'MERGE' => 'Συγχώνευση', + 'MERGE_ALL' => 'Συγχώνευση Επιλεγμένων', + 'MAKE_PUBLIC' => 'Κάντε το Δημόσιο', + 'SHARE_ALBUM' => 'Κοινή χρήση Λευκώματος', + 'SHARE_PHOTO' => 'Κοινή χρήση Φωτογραφίας', + 'VISIBILITY_ALBUM' => 'Ορατότητα Λευκώματος', + 'VISIBILITY_PHOTO' => 'Ορατότητα Φωτογραφίας', + 'DOWNLOAD_ALBUM' => 'Λήψη Λευκώματος', + 'ABOUT_ALBUM' => 'Πληροφορίες Λευκώματος', + 'DELETE_ALBUM' => 'Διαγραφή Λευκώματος', + 'MOVE_ALBUM' => 'Μετακίνηση Λευκώματος', + 'FULLSCREEN_ENTER' => 'Εισέλθετε σε λειτουργία Πλήρης Οθόνης', + 'FULLSCREEN_EXIT' => 'Εξέλθετε από λειτουργία Πλήρης Οθόνης', + + 'SHARING_ALBUM_USERS' => 'Share this album with users', + 'WAIT_FETCH_DATA' => 'Please wait while we get the data …', + 'SHARING_ALBUM_USERS_NO_USERS' => 'There are no users to share the album with', + 'SHARING_ALBUM_USERS_LONG_MESSAGE' => 'Select the users to share this album with', + + 'DELETE_ALBUM_QUESTION' => 'Διαγραφή Λευκώματος και Φωτογραφιών', + 'KEEP_ALBUM' => 'Διατήρηση Λευκώματος', + 'DELETE_ALBUM_CONFIRMATION' => 'Είστε σίγουρη/ος πως θέλετε να διαγράψετε αυτό το λεύκωμα «%s» και όλες τις φωτογραφίες που περιέχει; Αυτή η ενέργεια δεν μπορεί να αναιρεθεί!', + + 'DELETE_TAG_ALBUM_QUESTION' => 'Delete Album', + 'DELETE_TAG_ALBUM_CONFIRMATION' => 'Are you sure you want to delete the album «%s» (any photos inside will not be deleted)? This action can’t be undone!', + + 'DELETE_ALBUMS_QUESTION' => 'Διαγραφή Λευκωμάτων και Φωτογραφιών', + 'KEEP_ALBUMS' => 'Διατήρηση Λευκωμάτων', + 'DELETE_ALBUMS_CONFIRMATION' => 'Είστε σίγουρη/ος πως θέλετε να διαγράψετε όλα %d τα επιλεγμένα λευκώματα και όλες τις φωτογραφίες που περιέχουν; Αυτή η ενέργεια δεν μπορεί να αναιρεθεί!', + + 'DELETE_UNSORTED_CONFIRM' => 'Είστε σίγουρη/ος πως θέλετε να διαγράψετε όλες τις «Μη Ταξινομημένες» φωτογραφίες? Αυτή η ενέργεια δεν μπορεί να αναιρεθεί!', + 'CLEAR_UNSORTED' => 'Καθαρισμός των μη ταξινομημένων φωτογραφιών', + 'KEEP_UNSORTED' => 'Διατήρηση των Μη Ταξινομημένων', + + 'EDIT_SHARING' => 'Επεξεργασία Κοινής Χρήσης', + 'MAKE_PRIVATE' => 'Κάντε το Ιδιωτικό', + + 'CLOSE_ALBUM' => 'Κλείσιμο Λευκώματος', + 'CLOSE_PHOTO' => 'Κλείσιμο Φωτογραφίας', + 'CLOSE_MAP' => 'Close Map', + + 'ADD' => 'Προσθήκη', + 'MOVE' => 'Μετακίνηση', + 'MOVE_ALL' => 'Μετακίνηση Επιλεγμένων', + 'DUPLICATE' => 'Κλώνοποίηση', + 'DUPLICATE_ALL' => 'Κλώνοποίηση Επιλεγμένων', + 'COPY_TO' => 'Αντιγραφή σε …', + 'COPY_ALL_TO' => 'Αντιγραφή Επιλεγμένων σε …', + 'DELETE' => 'Διαγραφή', + 'SAVE' => 'Save', + 'DELETE_ALL' => 'Διαγραφή Επιλεγμένων', + 'DOWNLOAD' => 'Λήψη', + 'DOWNLOAD_ALL' => 'Λήψη Επιλεγμένων', + 'UPLOAD_PHOTO' => 'Μεταφόρτωση Φωτογραφίας', + 'IMPORT_LINK' => 'Εισαγωγή από Σύνδεσμο', + 'IMPORT_DROPBOX' => 'Εισαγωγή από Dropbox', + 'IMPORT_SERVER' => 'Εισαγωγή από Εξυπηρετητή', + 'NEW_ALBUM' => 'Νέο Λεύκωμα', + 'NEW_TAG_ALBUM' => 'New Tag Album', + 'UPLOAD_TRACK' => 'Upload track', + 'DELETE_TRACK' => 'Delete track', + + 'TITLE_NEW_ALBUM' => 'Εισάγετε έναν τίτλο για το νέο λεύκωμα:', + 'UNTITLED' => 'Χωρίς Τίτλο', + 'UNSORTED' => 'Μη Ταξινομημένα', + 'STARRED' => 'Με Αστέρι', + 'RECENT' => 'Πρόσφατα', + 'PUBLIC' => 'Δημόσια', + 'ON_THIS_DAY' => 'On This Day', + 'NUM_PHOTOS' => 'Φωτογραφίες', + + 'CREATE_ALBUM' => 'Δημιουργία Λευκώματος', + 'CREATE_TAG_ALBUM' => 'Create Tag Album', + + 'STAR_PHOTO' => 'Βάλτε Αστέρι στη Φωτογραφία', + 'STAR' => 'Βάλτε Αστέρι', + 'UNSTAR' => 'Unstar', + 'STAR_ALL' => 'Βάλτε Αστέρι στα επιλεγμένα', + 'UNSTAR_ALL' => 'Unstar Selected', + 'TAG' => 'Ετικέτες', + 'TAG_ALL' => 'Ετικέτες στα επιλεγμένα', + 'UNSTAR_PHOTO' => 'Αφαιρέστε Αστέρια από τη Φωτογραφία', + 'SET_COVER' => 'Set Album Cover', + 'REMOVE_COVER' => 'Remove Album Cover', + 'SET_HEADER' => 'Set Album Header', + 'REMOVE_HEADER' => 'Remove Album Header', + 'SET_COMPACT_HEADER' => 'Use Compact Header', + + 'FULL_PHOTO' => 'Πρωτότυπη Φωτογραφία', + 'ABOUT_PHOTO' => 'Πληροφορίες Φωτογραφίας', + 'DISPLAY_FULL_MAP' => 'Map', + 'DIRECT_LINK' => 'Απευθείας Σύνδεσμος', + 'DIRECT_LINKS' => 'Απευθείας Σύνδεσμοι', + 'QR_CODE' => 'QR Code', + + 'ALBUM_ABOUT' => 'Περί', + 'ALBUM_BASICS' => 'Βασικές Πληροφορίες', + 'ALBUM_TITLE' => 'Τίτλος', + 'ALBUM_COPYRIGHT' => 'Πνευματική ιδιοκτησία', + 'ALBUM_SET_COPYRIGHT' => 'Δήλωσε πνευματική ιδιοκτησία', + 'ALBUM_NEW_TITLE' => 'Εισάγετε έναν νέο τίτλο για αυτό το Λεύκωμα:', + 'ALBUMS_NEW_TITLE' => 'Εισάγετε νέο τίτλο για όλα %d τα επιλεγμένα λευκώματα:', + 'ALBUM_SET_TITLE' => 'Ορίστε Τίτλο', + 'ALBUM_DESCRIPTION' => 'Περιγραφή', + 'ALBUM_SHOW_TAGS' => 'Tags to show', + 'ALBUM_NEW_DESCRIPTION' => 'Εισάγετε μία νέα περιγραφή για αυτό το λεύκωμα:', + 'ALBUM_SET_DESCRIPTION' => 'Ορίστε Περιγραφή', + 'ALBUM_NEW_SHOWTAGS' => 'Enter tags of photos that will be visible in this album:', + 'ALBUM_SET_SHOWTAGS' => 'Set tags to show', + 'ALBUM_ALBUM' => 'Λεύκωμα', + 'ALBUM_CREATED' => 'Δημιουργήθηκε', + 'ALBUM_IMAGES' => 'Εικόνες', + 'ALBUM_VIDEOS' => 'Βίντεο', + 'ALBUM_SUBALBUMS' => 'Υπο-λευκώματα', + 'ALBUM_SHARING' => 'Κοινή Χρήση', + 'ALBUM_SHR_YES' => 'ΝΑΙ', + 'ALBUM_SHR_NO' => 'Όχι', + 'ALBUM_PUBLIC' => 'Δημόσιο', + 'ALBUM_PUBLIC_EXPL' => 'Anonymous users can access this album, subject to the restrictions below.', + 'ALBUM_FULL' => 'Πρωτότυπο', + 'ALBUM_FULL_EXPL' => 'Anonymous users can behold full-resolution photos.', + 'ALBUM_HIDDEN' => 'Κρυφό', + 'ALBUM_HIDDEN_EXPL' => 'Anonymous users need a direct link to access this album.', + 'ALBUM_MARK_NSFW' => 'Mark album as sensitive', + 'ALBUM_UNMARK_NSFW' => 'Unmark album as sensitive', + 'ALBUM_NSFW' => 'Sensitive', + 'ALBUM_NSFW_EXPL' => 'Album is marked to contain sensitive content.', + 'ALBUM_DOWNLOADABLE' => 'Δυνατότητα Λήψης', + 'ALBUM_DOWNLOADABLE_EXPL' => 'Anonymous users can download this album.', + 'ALBUM_SHARE_BUTTON_VISIBLE' => 'Share button is visible', + 'ALBUM_SHARE_BUTTON_VISIBLE_EXPL' => 'Anonymous users can see social media sharing links.', + 'ALBUM_PASSWORD' => 'Κωδικός Πρόσβασης', + 'ALBUM_PASSWORD_PROT' => 'Προστατεύεται με κωδικό πρόσβασης', + 'ALBUM_PASSWORD_PROT_EXPL' => 'Anonymous users need a shared password to access this album.', + 'ALBUM_PASSWORD_REQUIRED' => 'Αυτό το λεύκωμα προστατεύεται με κωδικό πρόσβασης. Εισάγετε τον κωδικό πρόσβασης παρακάτω για να δείτε τις φωτογραφίες αυτού του λευκώματος:', + 'ALBUM_MERGE' => 'Είστε σίγουρη/ος πως θέλετε να συγχωνεύσετε αυτό το λεύκωμα «%1$s» σε αυτό το λεύκωμα «%2$s»?', + 'ALBUMS_MERGE' => 'Είστε σίγουρη/ος πως θέλετε να συγχωνεύσετε όλα τα επιλεγμένα λευκώματα «%s»?', + 'MERGE_ALBUM' => 'Συγχώνευση Λευκωμάτων', + 'DONT_MERGE' => 'Να μη γίνει συγχώνευση', + 'ALBUM_MOVE' => 'Είστε σίγουρη/ος πως θέλετε να μετακινήσετε το λεύκωμα «%1$s» σε αυτό το λεύκωμα «%2$s»?', + 'ALBUMS_MOVE' => 'Είστε σίγουρη/ος πως θέλετε να μετακινήσετε όλα τα επιλεγμένα λευκώματα σε αυτό το λεύκωμα «%s»?', + 'MOVE_ALBUMS' => 'Μετακίνηση Λευκωμάτων', + 'NOT_MOVE_ALBUMS' => 'Να μη γίνει μετακίνηση', + 'ROOT' => 'Λευκώματα', + 'ALBUM_REUSE' => 'Επαναχρησιμοποίηση', + 'ALBUM_LICENSE' => 'Άδεια', + 'ALBUM_SET_LICENSE' => 'Ορισμός Άδειας', + 'ALBUM_LICENSE_HELP' => 'Χρειάζεστε βοήθεια για την επιλογή άδειας;', + 'ALBUM_LICENSE_NONE' => 'Καμία', + 'ALBUM_RESERVED' => 'Με επιφύλαξη παντός δικαιώματος', + 'ALBUM_SET_ORDER' => 'Set Order', + 'ALBUM_ORDERING' => 'Order by', + 'ALBUM_PHOTO_ORDERING' => 'Order photos by', + 'ALBUM_CHILDREN_ORDERING' => 'Order albums by', + 'ALBUM_OWNER' => 'Owner', + + 'PHOTO_ABOUT' => 'Περί', + 'PHOTO_BASICS' => 'Βασικές Πληροφορίες', + 'PHOTO_TITLE' => 'Τίτλος', + 'PHOTO_NEW_TITLE' => 'Εισάγετε έναν νέο τίτλο για αυτή τη φωτογραφία:', + 'PHOTO_SET_TITLE' => 'Ορισμός Τίτλου', + 'PHOTO_UPLOADED' => 'Μεταφορτώθηκε', + 'PHOTO_DESCRIPTION' => 'Περιγραφή', + 'PHOTO_NEW_DESCRIPTION' => 'Εισάγετε μία νέα περιγραφή για αυτή τη φωτογραφία:', + 'PHOTO_SET_DESCRIPTION' => 'Ορισμός Περιγραφής', + 'PHOTO_NEW_LICENSE' => 'Προσθήκη Άδειας', + 'PHOTO_SET_LICENSE' => 'Ορισμός Άδειας', + 'PHOTO_LICENSE' => 'Άδεια', + 'PHOTO_LICENSE_HELP' => 'Need help choosing?', + 'PHOTO_REUSE' => 'Επαναχρησιμοποίηση', + 'PHOTO_LICENSE_NONE' => 'Καμία', + 'PHOTO_RESERVED' => 'Με επιφύλαξη παντός δικαιώματος', + 'PHOTO_LATITUDE' => 'Γεωγραφικό πλάτος', + 'PHOTO_LONGITUDE' => 'Γεωγραφικό μήκος', + 'PHOTO_ALTITUDE' => 'Υψόμετρο', + 'PHOTO_IMGDIRECTION' => 'Κατεύθυνση', + 'PHOTO_LOCATION' => 'Location', + 'PHOTO_IMAGE' => 'Εικόνα', + 'PHOTO_VIDEO' => 'Video', + 'PHOTO_SIZE' => 'Μέγεθος', + 'PHOTO_FORMAT' => 'Μορφή', + 'PHOTO_RESOLUTION' => 'Ανάλυση', + 'PHOTO_DURATION' => 'Duration', + 'PHOTO_FPS' => 'Ρυθμός καρέ', + 'PHOTO_TAGS' => 'Ετικέτες', + 'PHOTO_NOTAGS' => 'Χωρίς Ετικέτες', + 'PHOTO_NEW_TAGS' => 'Εισάγετε τις ετικέτες σας για αυτή τη φωτογραφία. Μπορείτε να προσθέσετε πολλαπλές ετικέτες χωρίζοντάς ’τες με ένα κόμμα:', + 'PHOTOS_NEW_TAGS' => 'Εισάγετε τις ετικέτες σας για όλες %d τις επιλεγμένες φωγογραφίες. Υφιστάμενες ετικέτες θα αντικατασταθούν. Μπορείτε να προσθέσετε πολλαπλές ετικέτες χωρίζοντάς ’τες με ένα κόμμα:', + 'PHOTO_SET_TAGS' => 'Ορισμός Ετικετών', + 'PHOTO_CAMERA' => 'Κάμερα', + 'PHOTO_CAPTURED' => 'Φωτογραφήθηκε', + 'PHOTO_MAKE' => 'Έτος Κατασκευής', + 'PHOTO_TYPE' => 'Τύπος/Μοντέλο', + 'PHOTO_LENS' => 'Lens', + 'PHOTO_SHUTTER' => 'Ταχύτητα Κλείστρου', + 'PHOTO_APERTURE' => 'Διάφραγμα', + 'PHOTO_FOCAL' => 'Εστιακό μήκος', + 'PHOTO_ISO' => 'ISO %s', + 'PHOTO_SHARING' => 'Κοινή Χρήση', + 'PHOTO_DELETE' => 'Διαγραφή Φωτογραφίας', + 'PHOTO_KEEP' => 'Να μη γίνει διαγραφή', + 'PHOTO_DELETE_CONFIRMATION' => 'Είστε σίγουρη/ος πως θέλετε να διαγράψετε αυτή τη φωτογραφία «%s»? Αυτή η ενέργεια δεν μπορεί να αναιρεθεί!', + 'PHOTO_DELETE_ALL' => 'Είστε σίγουρη/ος πως θέλετε να διαγράψετε όλες %d τις επιλεγμένες φωτογραφίες; Αυτή η ενέργεια δεν μπορεί να αναιρεθεί!', + 'PHOTOS_NEW_TITLE' => 'Εισάγετε νέο τίτλο για όλες %d τις επιλεγμένες φωτογραφίες:', + 'PHOTO_MAKE_PRIVATE_ALBUM' => 'Αυτή η φωτογραφία βρίσκεται σε ένα δημόσιο λεύκωμα. Για να κάνετε αυτή τη φωτογραφία ιδιωτική ή δημόσια, επεξεργαστείτε τις ρυθμίσεις ορατότητας του συσχετιζόμενου Λευκώματος.', + 'PHOTO_SHOW_ALBUM' => 'Εμφάνιση Λευκώματος', + 'PHOTO_PUBLIC' => 'Δημόσια', + 'PHOTO_PUBLIC_EXPL' => 'Anonymous users can view this photo, subject to the restrictions below.', + 'PHOTO_FULL' => 'Πρωτότυπη', + 'PHOTO_FULL_EXPL' => 'Anonymous users can behold full-resolution photo.', + 'PHOTO_HIDDEN' => 'Hidden', + 'PHOTO_HIDDEN_EXPL' => 'Anonymous users need a direct link to view this photo.', + 'PHOTO_DOWNLOADABLE' => 'Δυνατότητα Λήψης', + 'PHOTO_DOWNLOADABLE_EXPL' => 'Anonymous users may download this photo.', + 'PHOTO_SHARE_BUTTON_VISIBLE' => 'Share button is visible', + 'PHOTO_SHARE_BUTTON_VISIBLE_EXPL' => 'Anonymous users can see social media sharing links.', + 'PHOTO_PASSWORD_PROT' => 'Προστατεύεται με κωδικό πρόσβασης', + 'PHOTO_PASSWORD_PROT_EXPL' => 'Anonymous users need a shared password to view this photo.', + 'PHOTO_EDIT_SHARING_TEXT' => 'Οι ιδιότητες κοινής χρήσης αυτής της φωτογραφίας θα αλλάξουν στις ακόλουθες:', + 'PHOTO_NO_EDIT_SHARING_TEXT' => 'Επειδή αυτή η φωτογραφία βρίσκεται σε ένα δημόσιο λεύκωμα, κληρονομεί τις ρυθμίσεις ορατότητας του λευκώματος στο οποίο ανήκει. Η τρέχουσα ορατότητά της φαίνεται παρακάτω για ενημερωτικούς λόγους μόνο.', + 'PHOTO_EDIT_GLOBAL_SHARING_TEXT' => 'Η ορατότητα αυτής της φωτογραφίας μπορεί να ρυθμιστεί με μεγαλύτερη λεπτομέρεια χρησιμοποιώντας τις γενικές ρυθμίσεις του Lychee. Η τρέχουσα ορατότητά της φαίνεται παρακάτω για ενημερωτικούς λόγους μόνο.', + 'PHOTO_NEW_CREATED_AT' => 'Enter the upload date for this photo. mm/dd/yyyy, hh:mm [am/pm]', + 'PHOTO_SET_CREATED_AT' => 'Set upload date', + + 'LOADING' => 'Φορτώνει', + 'ERROR' => 'Σφάλμα', + 'ERROR_TEXT' => 'Ουπς, φαίνεται πως κάτι πήγε στραβά. Παρακαλούμε κάντε ανανέωση της σελίδας και προσπαθήστε ξανά!', + 'ERROR_UNKNOWN' => 'Κάτι απρόσμενο συνέβη. Παρακαλούμε προσπαθείστε ξανά και ελέγξτε την εγκατάστασή σας και τον εξυπηρετητή. Ρίξτε μια ματιά στο αρχείο readme για περισσότερες πληροφορίες.', + 'ERROR_MAP_DEACTIVATED' => 'Map functionality has been deactivated under settings.', + 'ERROR_SEARCH_DEACTIVATED' => 'Search functionality has been deactivated under settings.', + 'SUCCESS' => 'OK', + 'CHANGE_SUCCESS' => 'Change successful.', + 'RETRY' => 'Προσπάθεια ξανά', + 'OVERRIDE' => 'Override', + 'TAGS_OVERRIDE_INFO' => 'If this is unchecked, the tags will be added to the existing tags of the photo.', + + 'SETTINGS_SUCCESS_LOGIN' => 'Τα στοιχεία εισόδου ενημερώθηκαν.', + 'SETTINGS_SUCCESS_SORT' => 'Η Ταξινόμηση ενημερώθηκε.', + 'SETTINGS_SUCCESS_DROPBOX' => 'Το κλειδί για το Dropbox ενημερώθηκε.', + 'SETTINGS_SUCCESS_LANG' => 'Η γλώσσα ενημερώθηκε', + 'SETTINGS_SUCCESS_LAYOUT' => 'Η διάταξη ενημερώθηκε', + 'SETTINGS_SUCCESS_IMAGE_OVERLAY' => 'Οι ρυθμίσεις επιφάνειας EXIF ενημερώθηκαν', + 'SETTINGS_SUCCESS_PUBLIC_SEARCH' => 'Η δημόσια αναζήτηση ενημερώθηκε', + 'SETTINGS_SUCCESS_LICENSE' => 'Η προεπιλεγμένη άδεια ενημερώθηκε', + 'SETTINGS_SUCCESS_MAP_DISPLAY' => 'Οι ρυθμίσεις εμφάνισης χάρτη ενημερώθηκαν', + 'SETTINGS_SUCCESS_MAP_DISPLAY_PUBLIC' => 'Map display settings for public albums updated', + 'SETTINGS_SUCCESS_MAP_PROVIDER' => 'Map provider settings updated', + 'SETTINGS_SUCCESS_CSS' => 'Stylesheets updated', + 'SETTINGS_SUCCESS_JS' => 'JS updated', + 'SETTINGS_SUCCESS_UPDATE' => 'Settings updated successfully', + 'SETTINGS_DROPBOX_KEY' => 'Dropbox API Key', + 'SETTINGS_ADVANCED_WARNING_EXPL' => 'Changing these advanced settings can be harmful to the stability, security and performance of this application. You should only modify them if you are sure of what you are doing.', + 'SETTINGS_ADVANCED_SAVE' => 'Save my modifications, I accept the risk!', + + 'U2F_NOT_SUPPORTED' => 'U2F not supported. Sorry.', + 'U2F_NOT_SECURE' => 'Environment not secured. U2F not available.', + 'U2F_REGISTER_KEY' => 'Register new device.', + 'U2F_REGISTRATION_SUCCESS' => 'Registration successful!', + 'U2F_AUTHENTIFICATION_SUCCESS' => 'Authentication successful!', + 'U2F_CREDENTIALS' => 'Credentials', + 'U2F_CREDENTIALS_DELETED' => 'Credentials deleted!', + 'U2F_LOGIN' => 'Log in with WebAuthn', + + 'NEW_PHOTOS_NOTIFICATION' => 'Send new photos notification emails.', + 'SETTINGS_SUCCESS_NEW_PHOTOS_NOTIFICATION' => 'New photos notification updated', + 'USER_EMAIL_INSTRUCTION' => 'Add your email below to enable receiving email notifications. To stop receiving emails, simply remove your email below.', + + 'LOGIN_USERNAME' => 'Νέο όνομα χρήστη', + 'LOGIN_PASSWORD' => 'Νέος κωδικός πρόσβασης', + 'LOGIN_PASSWORD_CONFIRM' => 'Επιβεβαίωση κωδικού πρόσβασης', + 'PASSWORD_TITLE' => 'Εισάγετε τον τρέχον κωδικό πρόσβασης:', + 'PASSWORD_CURRENT' => 'Τρέχον κωδικός πρόσβασης', + 'PASSWORD_TEXT' => 'Το όνομα χρήστη και ο κωδικός πρόσβασής σας θα αλλάξουν στα παρακάτω:', + 'PASSWORD_CHANGE' => 'Αλλαγή στοιχείων εισόδου', + + 'EDIT_SHARING_TITLE' => 'Επεξεργασία κοινής χρήσης', + 'EDIT_SHARING_TEXT' => 'Οι ιδιότητες κοινής χρήσης αυτού του λευκώματος θα αλλάξουν στις παρακάτω:', + 'SHARE_ALBUM_TEXT' => 'Αυτό το λεύκωμα θα κοινοποιείται με τις παρακάτω ιδιότητες:', + + 'SORT_DIALOG_ATTRIBUTE_LABEL' => 'Attribute', + 'SORT_DIALOG_ORDER_LABEL' => 'Order', + + 'SORT_ALBUM_BY' => 'Ταξινόμηση λευκωμάτων κατά %1$s με %2$s σειρά.', + + 'SORT_ALBUM_SELECT_1' => 'Ημερομηνία Δημιουργίας', + 'SORT_ALBUM_SELECT_2' => 'Τίτλος', + 'SORT_ALBUM_SELECT_3' => 'Περιγραφή', + 'SORT_ALBUM_SELECT_5' => 'Νεότερη Ημερομηνία Λήψης', + 'SORT_ALBUM_SELECT_6' => 'Παλαιότερη Ημερομηνία Λήψης', + + 'SORT_PHOTO_BY' => 'Ταξινόμηση Φωτογραφιών κατά %1$s με %2$s σειρά.', + + 'SORT_PHOTO_SELECT_1' => 'Ημερομηνία Μεταφόρτωσης', + 'SORT_PHOTO_SELECT_2' => 'Ημερομηνία Λήψης', + 'SORT_PHOTO_SELECT_3' => 'Τίτλος', + 'SORT_PHOTO_SELECT_4' => 'Περιγραφή', + 'SORT_PHOTO_SELECT_6' => 'Αστέρια', + 'SORT_PHOTO_SELECT_7' => 'Μορφή Φωτογραφίας', + + 'SORT_ASCENDING' => 'Αύξουσα', + 'SORT_DESCENDING' => 'Φθίνουσα', + 'SORT_CHANGE' => 'Αλλαγή Ταξινόμησης', + + 'DROPBOX_TITLE' => 'Ορισμός Κλειδιού Dropbox', + 'DROPBOX_TEXT' => "Για να μπορέσουμε να εισάγουμε φωτογραφίες από το δικό σας Dropbox, θα χρειαστείτε ένα έγκυρο κλειδί drop-ins app από την ιστοσελίδα του Dropbox. Παράγετε ένα προσωπικό κλειδί και εισάγετέ το παρακάτω:", + + 'LANG_TEXT' => 'Αλλαγή γλώσσας του Lychee για:', + 'LANG_TITLE' => 'Αλλαγή Γλώσσας', + + 'SETTING_RECENT_PUBLIC_TEXT' => 'Make "Recent" smart album accessible to anonymous users', + 'SETTING_STARRED_PUBLIC_TEXT' => 'Make "Starred" smart album accessible to anonymous users', + 'SETTING_ONTHISDAY_PUBLIC_TEXT' => 'Make "On This Day" smart album accessible to anonymous users', + + 'CSS_TEXT' => 'Personalize CSS:', + 'CSS_TITLE' => 'Change CSS', + 'JS_TEXT' => 'Custom JS:', + 'JS_TITLE' => 'Change JS', + 'PUBLIC_SEARCH_TEXT' => 'Να επιτρέπεται η δημόσια αναζήτηση:', + 'OVERLAY_TYPE' => 'Δεδομένα που θα χρησιμοποιηθούν στο overlay εικόνας:', + 'OVERLAY_NONE' => 'None', + 'OVERLAY_EXIF' => 'EXIF δεδομένα φωτογραφίας', + 'OVERLAY_DESCRIPTION' => 'Περιγραφή φωτογραφίας', + 'OVERLAY_DATE' => 'Ημερομηνία λήψης της φωτογραφίας', + 'ALBUM_DECORATION' => 'Album decorations:', + 'ALBUM_DECORATION_NONE' => 'None', + 'ALBUM_DECORATION_ORIGINAL' => 'Sub-album marker', + 'ALBUM_DECORATION_ALBUM' => 'Number of sub-albums', + 'ALBUM_DECORATION_PHOTO' => 'Number of photos', + 'ALBUM_DECORATION_ALL' => 'Number of sub-albums and photos', + 'ALBUM_DECORATION_ORIENTATION' => 'Orientation of album decorations:', + 'ALBUM_DECORATION_ORIENTATION_ROW' => 'Horizontal (photos, albums)', + 'ALBUM_DECORATION_ORIENTATION_ROW_REVERSE' => 'Horizontal (albums, photos)', + 'ALBUM_DECORATION_ORIENTATION_COLUMN' => 'Vertical (top photos, albums)', + 'ALBUM_DECORATION_ORIENTATION_COLUMN_REVERSE' => 'Vertical (top albums, photos)', + 'MAP_DISPLAY_TEXT' => 'Εμφάνιση συντεταγμένων στον χάρτη (OpenStreetMap):', + 'MAP_DISPLAY_PUBLIC_TEXT' => 'Enable maps for public albums (provided by OpenStreetMap):', + 'MAP_PROVIDER' => 'Provider of OpenStreetMap tiles:', + 'MAP_PROVIDER_WIKIMEDIA' => 'Wikimedia', + 'MAP_PROVIDER_OSM_ORG' => 'OpenStreetMap.org (no HiDPI)', + 'MAP_PROVIDER_OSM_DE' => 'OpenStreetMap.de (no HiDPI)', + 'MAP_PROVIDER_OSM_FR' => 'OpenStreetMap.fr (no HiDPI)', + 'MAP_PROVIDER_RRZE' => 'University of Erlangen, Germany (only HiDPI)', + 'MAP_INCLUDE_SUBALBUMS_TEXT' => 'Include photos of subalbums on map:', + 'LOCATION_DECODING' => 'Decode GPS data into location name', + 'LOCATION_SHOW' => 'Show location name', + 'LOCATION_SHOW_PUBLIC' => 'Show location name for public mode', + + 'LAYOUT_TYPE' => 'Διάταξη φωτογραφιών:', + 'LAYOUT_SQUARES' => 'Τετράγωνες μικρογραφίες', + 'LAYOUT_JUSTIFIED' => 'Με ίσες αναλογίες', + 'LAYOUT_MASONRY' => 'Με ίσες masonry', + 'LAYOUT_GRID' => 'Με ίσες grid', + 'LAYOUT_UNJUSTIFIED' => 'Με άνισες αναλογίες', + 'SET_LAYOUT' => 'Αλλαγή διάταξης', + + 'NSFW_VISIBLE_TEXT_1' => 'Make Sensitive albums visible by default.', + 'NSFW_VISIBLE_TEXT_2' => 'If the album is public, it is still accessible, just hidden from the view and can be revealed by pressing H.', + 'SETTINGS_SUCCESS_NSFW_VISIBLE' => 'Default sensitive album visibility updated with success.', + + 'NSFW_BANNER' => '

Sensitive content

This album contains sensitive content which some people may find offensive or disturbing.

Tap to consent.

', + 'NSFW_HEADER' => 'Sensitive content', + 'NSFW_EXPLANATION' => 'This album contains sensitive content which some people may find offensive or disturbing.', + 'TAP_CONSENT' => 'Tap to consent.', + + 'VIEW_NO_RESULT' => 'Κανένα αποτέλεσμα', + 'VIEW_NO_PUBLIC_ALBUMS' => 'Κανένα δημόσιο λεύκωμα', + 'VIEW_NO_CONFIGURATION' => 'Καμία ρύθμιση', + 'VIEW_PHOTO_NOT_FOUND' => 'Η φωτογραφία δεν βρέθηκε', + + 'NO_TAGS' => 'Καμία ετικέτα', + + 'UPLOAD_MANAGE_NEW_PHOTOS' => 'Μπορείτε τώρα να διαχειριστείτε τις νέες φωτογραφίες σας.', + 'UPLOAD_COMPLETE' => 'Η μεταφόρτωση ολοκληρώθηκε', + 'UPLOAD_COMPLETE_FAILED' => 'Αποτυχία μεταφόρτωσης μιας ή περισσότερων φωτογραφιών.', + 'UPLOAD_IMPORTING' => 'Γίνεται εισαγωγή', + 'UPLOAD_IMPORTING_URL' => 'Εισαγωγή URL', + 'UPLOAD_UPLOADING' => 'Γίνεται μεταφόρτωση', + 'UPLOAD_FINISHED' => 'Ολοκληρώθηκε', + 'UPLOAD_PROCESSING' => 'Γίνεται επεξεργασία', + 'UPLOAD_FAILED' => 'Απέτυχε', + 'UPLOAD_FAILED_ERROR' => 'Η μεταφόρτωση απέτυχε. Ο εξυπηρετητής επέστρεψε ένα σφάλμα!', + 'UPLOAD_FAILED_WARNING' => 'Η μεταφόρτωση απέτυχε. Ο εξυπηρετητής επέστρεψε μία προειδοποίηση!', + 'UPLOAD_CANCELLED' => 'Cancelled', + 'UPLOAD_SKIPPED' => 'Παραλείφθηκε', + 'UPLOAD_UPDATED' => 'Updated', + 'UPLOAD_GENERAL' => 'General', + 'UPLOAD_IMPORT_SKIPPED_DUPLICATE' => 'This photo has been skipped because it’s already in your library.', + 'UPLOAD_IMPORT_RESYNCED_DUPLICATE' => 'This photo has been skipped because it’s already in your library, but its metadata has been updated.', + 'UPLOAD_ERROR_CONSOLE' => 'Παρακαλούμε ρίξτε μια ματιά στην κονσόλα του περιηγητή σας για περισσότερες λεπτομέρειες.', + 'UPLOAD_UNKNOWN' => 'Ο εξυπηρετητής επέστρεψε μία άγνωστη απόκριση. Παρακαλούμε ρίξτε μια ματιά στην κονσόλα του περιηγητή σας για περισσότερες λεπτομέρειες.', + 'UPLOAD_ERROR_UNKNOWN' => 'Η μεταφόρτωση απέτυχε. Ο εξυπηρετητής επέστρεψε ένα άγνωστο σφάλμα!', + 'UPLOAD_ERROR_POSTSIZE' => 'Upload failed. The PHP post_max_size may be too small! Otherwise check the FAQ.', + 'UPLOAD_ERROR_FILESIZE' => 'Upload failed. The PHP upload_max_filesize may be too small! Otherwise check the FAQ.', + 'UPLOAD_IN_PROGRESS' => 'Το Lychee αυτή τη στιγμή μεταφορτώνει!', + 'UPLOAD_IMPORT_WARN_ERR' => 'Η εισαγωγή ολοκληρώθηκε, αλλά επέστρεψε προειδοποιήσεις ή σφάλματα. Παρακαλούμε ρίξτε μια ματία στις καταγραφές (Ρυθμίσεις -> Εμφάνιση Καταγραφών) για περισσότερες λεπτομέρειες.', + 'UPLOAD_IMPORT_COMPLETE' => 'Η εισαγωγή ολοκληρώθηκε', + 'UPLOAD_IMPORT_INSTR' => 'Παρακαλούμε εισάγετε τον απευθείας σύνδεσμο μιας φωτογραφίας για να την εισάγετε:', + 'UPLOAD_IMPORT' => 'Εισαγωγή', + 'UPLOAD_IMPORT_SERVER' => 'Γίνεται εισαγωγή από εξυπηρετητή', + 'UPLOAD_IMPORT_SERVER_FOLD' => 'Ο φάκελος είναι άδειος ή μη αναγνώσιμα αρχεία προς επεξεργασία. Παρακαλούμε ρίξτε μια ματία στις καταγραφές (Ρυθμίσεις -> Εμφάνιση Καταγραφών) για περισσότερες λεπτομέρειες.', + 'UPLOAD_IMPORT_SERVER_INSTR' => 'Import all photos, folders and sub-folders located in the folders with the following absolute paths (on server). Paths are space separated, use \\ to escape a space in a path.', + 'UPLOAD_ABSOLUTE_PATH' => 'Absolute path to directories, space separated', + 'UPLOAD_IMPORT_SERVER_EMPT' => 'Δεν ήταν δυνατό να ξεκινήσει η διαδικασία εισαγωγής, διότι ο κατάλογος ήταν άδειος!', + 'UPLOAD_IMPORT_DELETE_ORIGINALS' => 'Διαγραφή πρωτότυπων', + 'UPLOAD_IMPORT_DELETE_ORIGINALS_EXPL' => 'Αν είναι εφικτό τα πρωτότυπα αρχεία θα διαγραφούν αφού ολοκληρωθεί η εισαγωγή τους.', + 'UPLOAD_IMPORT_VIA_SYMLINK' => 'Symbolic links', + 'UPLOAD_IMPORT_VIA_SYMLINK_EXPL' => 'Import files using symbolic links to originals.', + 'UPLOAD_IMPORT_SKIP_DUPLICATES' => 'Skip duplicates', + 'UPLOAD_IMPORT_SKIP_DUPLICATES_EXPL' => 'Existing media files are skipped.', + 'UPLOAD_IMPORT_RESYNC_METADATA' => 'Re-sync metadata', + 'UPLOAD_IMPORT_RESYNC_METADATA_EXPL' => 'Update metadata of existing media files.', + 'UPLOAD_IMPORT_LOW_MEMORY_EXPL' => 'Η διεργασία εισαγωγής στον εξυπηρετητή πλησιάζει τα όρια μνήμης και μπορεί να τερματιστεί πρόωρα.', + 'UPLOAD_WARNING' => 'Προειδοποίηση', + 'UPLOAD_IMPORT_NOT_A_DIRECTORY' => 'Η δοθείσα διαδρομή δεν είναι ένας αναγνώσιμος κατάλογος!', + 'UPLOAD_IMPORT_PATH_RESERVED' => 'Η δοθείσα διαδρομή χρησιμοποιείται από το Lychee!', + 'UPLOAD_IMPORT_FAILED' => 'Δεν ήταν δυνατή η εισαγωγή του αρχείου!', + 'UPLOAD_IMPORT_UNSUPPORTED' => 'Μη υποστηριζόμενος τύπος αρχείου!', + 'UPLOAD_IMPORT_CANCELLED' => 'Import cancelled', + + 'ABOUT_SUBTITLE' => 'Αυτό-φιλοξενούμενη διαχείριση φωτογραφιών καμωμένη σωστά', + 'ABOUT_DESCRIPTION' => 'Lychee είναι ένα δωρεάν εργαλείο διαχείρισης φωτογραφιών, το οποίο "τρέχει" στον δικό σας εξυπηρετητή ή δικτυακό-χώρο. Εγκαθίσταται σε μερικά δευτερόλεπτα. Μεταφορτώστε, διαχειριστείτε και κοινοποιήστε φωτογραφίες σαν από εγκατεστημένη εφαρμογή. Το Lychee παρέχεται με όλες τις λειτουργίες που χρειάζεστε και όλες οι φωτογραφίες σας είναι αποθηκευμένες με ασφάλεια.', + 'FOOTER_COPYRIGHT' => 'Όλες οι εικόνες σε αυτή την ιστοσελίδα υπόκεινται σε πνευματικά δικαιώματα από %1$s © %2$s', + 'HOSTED_WITH_LYCHEE' => 'Φιλοξενείται από το Lychee', + + 'URL_COPY_TO_CLIPBOARD' => 'Αντιγραφή στο πρόχειρο', + 'URL_COPIED_TO_CLIPBOARD' => 'Η διεύθυνση URL αντιγράφηκε στο πρόχειρο!', + 'PHOTO_DIRECT_LINKS_TO_IMAGES' => 'Απευθείας σύνδεσμοι στα αρχεία εικόνων:', + 'PHOTO_ORIGINAL' => 'Original', + 'PHOTO_MEDIUM' => 'Μέτρια', + 'PHOTO_MEDIUM_HIDPI' => 'Μέτρια HiDPI', + 'PHOTO_SMALL' => 'Μικρογραφία', + 'PHOTO_SMALL_HIDPI' => 'Μικρογραφία HiDPI', + 'PHOTO_THUMB' => 'Τετράγωνη Μικρογραφία', + 'PHOTO_THUMB_HIDPI' => 'Τετράγωνη Μικρογραφία HiDPI', + 'PHOTO_PLACEHOLDER' => 'Low Quality Image Placeholder', + 'PHOTO_THUMBNAIL' => 'Photo thumbnail', + 'PHOTO_LIVE_VIDEO' => 'Video part of live-photo', + 'PHOTO_VIEW' => 'Lychee Προβολή Φωτογραφιών:', + + 'PHOTO_EDIT_ROTATECWISE' => 'Rotate clockwise', + 'PHOTO_EDIT_ROTATECCWISE' => 'Rotate counter-clockwise', + + 'ERROR_GPX' => 'Error loading GPX file: ', + 'ERROR_EITHER_ALBUMS_OR_PHOTOS' => 'Please select either albums or photos!', + 'ERROR_COULD_NOT_FIND' => 'Could not find what you want.', + 'ERROR_INVALID_EMAIL' => 'Not a valid email address.', + 'EMAIL_SUCCESS' => 'Email updated!', + 'ERROR_PHOTO_NOT_FOUND' => 'Error: photo %s not found !', + 'ERROR_EMPTY_USERNAME' => 'new username cannot be empty.', + 'ERROR_PASSWORD_DOES_NOT_MATCH' => 'new password does not match.', + 'ERROR_EMPTY_PASSWORD' => 'new password cannot be empty.', + 'ERROR_SELECT_ALBUM' => 'Select an album to share!', + 'ERROR_SELECT_USER' => 'Select a user to share with!', + 'ERROR_SELECT_SHARING' => 'Select a sharing to remove!', + 'SHARING_SUCCESS' => 'Sharing updated!', + 'SHARING_REMOVED' => 'Sharing removed!', + 'USER_CREATED' => 'User created!', + 'USER_DELETED' => 'User deleted!', + 'USER_UPDATED' => 'User updated!', + 'ENTER_EMAIL' => 'Enter your email address:', + 'ERROR_ALBUM_JSON_NOT_FOUND' => 'Error: Album json not found!', + 'ERROR_ALBUM_NOT_FOUND' => 'Error: album %s not found', + 'ERROR_DROPBOX_KEY' => 'Error: Dropbox key not set', + 'ERROR_SESSION' => 'Session expired.', + 'CAMERA_DATE' => 'Camera date', + 'NEW_PASSWORD' => 'new password', + 'ALLOW_UPLOADS' => 'Allow uploads', + 'ALLOW_USER_SELF_EDIT' => 'Allow self-management of user account', + 'OSM_CONTRIBUTORS' => 'OpenStreetMap contributors', +]; diff --git a/lang/el/maintenance.php b/lang/el/maintenance.php new file mode 100644 index 00000000000..f86de3d6f46 --- /dev/null +++ b/lang/el/maintenance.php @@ -0,0 +1,60 @@ + 'Maintenance', + 'description' => 'You will find on this page, all the required actions to keep your Lychee installation running smooth and nicely.', + 'cleaning' => [ + 'title' => 'Cleaning %s', + 'result' => '%s deleted.', + 'description' => 'Remove all contents from %s', + 'button' => 'Clean', + ], + 'fix-jobs' => [ + 'title' => 'Fixing Jobs History', + 'description' => 'Mark jobs with status %s or %s as %s.', + 'button' => 'Fix job history', + ], + 'gen-sizevariants' => [ + 'title' => 'Missing %s', + 'description' => 'Found %d %s that could be generated.', + 'button' => 'Generate!', + 'success' => 'Successfully generated %d %s.', + ], + 'fill-filesize-sizevariants' => [ + 'title' => 'File sizes missing', + 'description' => 'Found %d small variants without file size.', + 'button' => 'Fetch data!', + 'success' => 'Successfully computed sizes of %d small variants.', + ], + 'fix-tree' => [ + 'title' => 'Tree statistics', + 'Oddness' => 'Oddness', + 'Duplicates' => 'Duplicates', + 'Wrong parents' => 'Wrong parents', + 'Missing parents' => 'Missing parents', + 'button' => 'Fix tree', + ], + 'optimize' => [ + 'title' => 'Optimize Database', + 'description' => 'If you notice slowdown in your installation, it may be because your database does not + have all its needed index.', + 'button' => 'Optimize Database', + ], + 'update' => [ + 'title' => 'Updates', + 'check-button' => 'Check for updates', + 'update-button' => 'Update', + 'no-pending-updates' => 'No pending update.', + ], +]; \ No newline at end of file diff --git a/lang/el/profile.php b/lang/el/profile.php new file mode 100644 index 00000000000..cc24b97452c --- /dev/null +++ b/lang/el/profile.php @@ -0,0 +1,64 @@ + 'Profile', + + 'login' => [ + 'header' => 'Profile', + 'enter_current_password' => 'Enter your current password:', + 'current_password' => 'Current password', + 'credentials_update' => 'Your credentials will be changed to the following:', + 'username' => 'Username', + 'new_password' => 'New password', + 'confirm_new_password' => 'Confirm new password', + 'email_instruction' => 'Add your email below to enable receiving email notifications. To stop receiving emails, simply remove your email below.', + 'email' => 'Email', + 'change' => 'Change Login', + 'api_token' => 'API Token ...', + + 'missing_fields' => 'Missing fields', + ], + + 'token' => [ + 'unavailable' => 'You have already viewed this token.', + 'no_data' => 'No token API have been generated.', + 'disable' => 'Disable', + 'disabled' => 'Token disabled', + 'warning' => 'This token will not be displayed again. Copy it and keep it in a safe place.', + 'reset' => 'Reset the token', + 'create' => 'Create a new token', + ], + + 'oauth' => [ + 'header' => 'OAuth', + 'header_not_available' => 'OAuth is not available', + 'setup_env' => 'Set up the credentials in your .env', + 'token_registered' => '%s token registered.', + 'setup' => 'Set up %s', + 'reset' => 'reset', + 'credential_deleted' => 'Credential deleted!', + ], + + 'u2f' => [ + 'header' => 'Passkey/MFA/2FA', + 'info' => 'This only provides the ability to use WebAuthn to authenticate instead of username & password.', + 'empty' => 'Credentials list is empty!', + 'not_secure' => 'Environment not secured. U2F not available.', + 'new' => 'Register new device.', + 'credential_deleted' => 'Credential deleted!', + 'credential_updated' => 'Credential updated!', + 'credential_registred' => 'Registration successful!', + '5_chars' => 'At least 5 chars.', + ], +]; \ No newline at end of file diff --git a/lang/el/settings.php b/lang/el/settings.php new file mode 100644 index 00000000000..fd197f11135 --- /dev/null +++ b/lang/el/settings.php @@ -0,0 +1,92 @@ + 'Settings', + 'small_screen' => 'For better a experience on the Settings page,
we recommend you use a larger screen.', + 'tabs' => [ + 'basic' => 'Basic', + 'all_settings' => 'All settings', + ], + 'toasts' => [ + 'change_saved' => 'Change saved!', + 'details' => 'Settings have been modified as per request', + 'error' => 'Error!', + 'error_load_css' => 'Could not load dist/user.css', + 'error_load_js' => 'Could not load dist/custom.js', + 'error_save_css' => 'Could not save CSS', + 'error_save_js' => 'Could not save JS', + 'thank_you' => 'Thank you for your support.', + 'reload' => 'Reload your page for full functionalities.', + ], + 'system' => [ + 'header' => 'System', + 'use_dark_mode' => 'Use dark mode for Lychee', + 'language' => 'Language used by Lychee', + 'nsfw_album_visibility' => 'Make Sensitive albums visible by default.', + 'nsfw_album_explanation' => 'If the album is public, it is still accessible, just hidden from the view and can be revealed by pressing H.', + ], + 'lychee_se' => [ + 'header' => 'Lychee SE', + 'call4action' => 'Get exclusive features and support the development of Lychee. Unlock the SE edition.', + 'preview' => 'Enable preview of Lychee SE features', + 'hide_call4action' => 'Hide this Lychee SE registration form. I am happy with Lychee as-is. :)', + 'hide_warning' => 'If enabled, the only way to register your license key will be via the More tab above. Changes are applied on page reload.', + ], + 'dropbox' => [ + 'header' => 'Dropbox', + 'instruction' => 'In order to import photos from your Dropbox, you need a valid drop-ins app key from their website.', + 'api_key' => 'Dropbox API Key', + 'set_key' => 'Set Dropbox Key', + ], + 'gallery' => [ + 'header' => 'Gallery', + 'photo_order_column' => 'Default column used for sorting photos', + 'photo_order_direction' => 'Default order used for sorting photos', + 'album_order_column' => 'Default column used for sorting albums', + 'album_order_direction' => 'Default order used for sorting albums', + 'aspect_ratio' => 'Default aspect ratio for album thumbs', + 'photo_layout' => 'Layout for pictures', + 'album_decoration' => 'Show decorations on album cover (sub-album and/or photo count)', + 'album_decoration_direction' => 'Align album decorations horizontally or vertically', + 'photo_overlay' => 'Default image overlay information', + 'license_default' => 'Default license used for albums', + 'license_help' => 'Need help choosing?', + ], + 'geolocation' => [ + 'header' => 'Geo-location', + 'map_display' => 'Display the map given GPS coordinates', + 'map_display_public' => 'Allow anonymous users to access the map', + 'map_provider' => 'Defines the map provider', + 'map_include_subalbums' => 'Includes pictures of the sub albums on the map', + 'location_decoding' => 'Use GPS location decoding', + 'location_show' => 'Show location extracted from GPS coordinates', + 'location_show_public' => 'Anonymous users can access the extracted location from GPS coordinates', + ], + 'advanced' => [ + 'header' => 'Advanced Customization', + 'change_css' => 'Change CSS', + 'change_js' => 'Change JS', + ], + 'all' => [ + 'old_setting_style' => 'Old setting style', + 'change_detected' => 'Some settings changed.', + 'save' => 'Save', + ], + + 'tool_option' => [ + 'disabled' => 'disabled', + 'enabled' => 'enabled', + 'discover' => 'discover', + ], +]; \ No newline at end of file diff --git a/lang/el/sharing.php b/lang/el/sharing.php new file mode 100644 index 00000000000..69de18cc6d0 --- /dev/null +++ b/lang/el/sharing.php @@ -0,0 +1,33 @@ + 'Sharing', + + 'info' => 'This page gives an overview of and the ability to edit the sharing rights associated with albums.', + 'album_title' => 'Album title', + 'username' => 'Username', + 'no_data' => 'Sharing list is empty.', + 'share' => 'Share', + 'permission_deleted' => 'Permission deleted!', + 'permission_created' => 'Permission created!', + + 'grants' => [ + 'read' => 'Grants read access', + 'original' => 'Grants access to original photo', + 'download' => 'Grants download', + 'upload' => 'Grants upload', + 'edit' => 'Grants edit', + 'delete' => 'Grants delete', + ], +]; \ No newline at end of file diff --git a/lang/el/statistics.php b/lang/el/statistics.php new file mode 100644 index 00000000000..2baf855bbd5 --- /dev/null +++ b/lang/el/statistics.php @@ -0,0 +1,34 @@ + 'Statistics', + + 'preview_text' => 'This is a preview of the statistics page available in Lychee SE.
The data shown here are randomly generated and do not reflect your server.', + 'no_data' => 'User does not have data on server.', + 'collapse' => 'Collapse albums sizes', + + 'total' => [ + 'total' => 'Total', + 'albums' => 'Albums', + 'photos' => 'Photos', + 'size' => 'Size', + ], + 'table' => [ + 'username' => 'Owner', + 'title' => 'Title', + 'photos' => 'Photos', + 'descendants' => 'Children', + 'size' => 'Size', + ], +]; \ No newline at end of file diff --git a/lang/el/toasts.php b/lang/el/toasts.php new file mode 100644 index 00000000000..293d4b72594 --- /dev/null +++ b/lang/el/toasts.php @@ -0,0 +1,17 @@ + 'Error', + 'success' => 'Success', +]; \ No newline at end of file diff --git a/lang/el/users.php b/lang/el/users.php new file mode 100644 index 00000000000..599bb833454 --- /dev/null +++ b/lang/el/users.php @@ -0,0 +1,44 @@ + 'Users', + 'description' => 'Here you can manage the users of your Lychee installation. You can create, edit and delete users.', + 'create' => 'Create a new user', + 'username' => 'Username', + 'password' => 'Password', + 'legend' => 'Legend', + 'upload_rights' => 'When selected, the user can upload content.', + 'edit_rights' => 'When selected, the user can modify their profile (username, password).', + 'quota' => 'When set, the user has a space quota for pictures (in kB).', + + 'user_deleted' => 'User deleted', + 'user_created' => 'User created', + 'user_updated' => 'User updated', + 'change_saved' => 'Change saved!', + + 'create_edit' => [ + 'upload_rights' => 'User can upload content.', + 'edit_rights' => 'User can modify their profile (username, password).', + 'quota' => 'User has quota limit.', + 'quota_kb' => 'quota in kB (0 for default)', + 'note' => 'Admin note (not publically visible)', + 'create' => 'Create', + 'edit' => 'Edit', + ], + 'line' => [ + 'admin' => 'admin user', + 'edit' => 'Edit', + 'delete' => 'Delete', + ], +]; \ No newline at end of file diff --git a/lang/en/aspect_ratio.php b/lang/en/aspect_ratio.php new file mode 100644 index 00000000000..2c7e8fb56ac --- /dev/null +++ b/lang/en/aspect_ratio.php @@ -0,0 +1,21 @@ + '5/4 (instagram landscape)', + '4by5' => '4/5 (instagram portrait)', + '2by3' => '2/3 (portrait)', + '3by2' => '3/2 (landscape)', + '1by1' => 'square', + '1byx9' => '16/9 (landscape)', +]; \ No newline at end of file diff --git a/lang/en/diagnostics.php b/lang/en/diagnostics.php new file mode 100644 index 00000000000..0fadd640428 --- /dev/null +++ b/lang/en/diagnostics.php @@ -0,0 +1,30 @@ + 'Diagnostics', + + 'copy_to_clipboard' => 'Copy diagnostics to clipboard', + 'self-diagnosis' => 'Self-diagnosis', + 'info' => 'Info', + 'space' => 'Space', + 'load_space' => 'Load space usage.', + 'configuration' => 'Configuration', + 'loading' => 'Loading...', + 'identical_content' => 'Identical content', + + 'toast' => [ + 'info' => 'Info', + 'copy' => 'Diagnostics copied to clipboard!', + ], +]; \ No newline at end of file diff --git a/lang/en/dialogs.php b/lang/en/dialogs.php new file mode 100644 index 00000000000..4afd65fae3f --- /dev/null +++ b/lang/en/dialogs.php @@ -0,0 +1,221 @@ + [ + 'close' => 'Close', + 'cancel' => 'Cancel', + 'save' => 'Save', + 'delete' => 'Delete', + 'move' => 'Move', + ], + 'about' => [ + 'subtitle' => 'Self-hosted photo-management done right', + 'description' => 'Lychee is a free photo-management tool, which runs on your server or web-space. Installing is a matter of seconds. Upload, manage and share photos like from a native application. Lychee comes with everything you need and all your photos are stored securely.', + 'update_available' => 'Update available!', + 'thank_you' => 'Thank you for your support!', + 'get_supporter_or_register' => 'Get exclusive features and support the development of Lychee.
Unlock the Supporter Edition or register your License key', + 'here' => 'here', + ], + 'dropbox' => [ + 'not_configured' => 'Dropbox is not configured.', + ], + 'import_from_link' => [ + 'instructions' => 'Please enter the direct link to a photo to import it:', + 'import' => 'Import', + ], + 'keybindings' => [ + 'don_t_show_again' => 'Don\'t show this again', + 'side_wide' => 'Site-wide Shortcuts', + 'back_cancel' => 'Back/Cancel', + 'confirm' => 'Confirm', + 'login' => 'Login', + 'toggle_full_screen' => 'Toggle Full Screen', + 'toggle_sensitive_albums' => 'Toggle Sensitive Albums', + + 'albums' => 'Albums Shortcuts', + 'new_album' => 'New Album', + 'upload_photos' => 'Upload Photos', + 'search' => 'Search', + 'show_this_modal' => 'Show this modal', + 'select_all' => 'Select All', + 'move_selection' => 'Move Selection', + 'delete_selection' => 'Delete Selection', + + 'album' => 'Album Shortcuts', + 'slideshow' => 'Start/Stop Slideshow', + 'toggle' => 'Toggle panel', + + 'photo' => 'Photo Shortcuts', + 'previous' => 'Previous photo', + 'next' => 'Next photo', + 'cycle' => 'Cycle overlay mode', + 'star' => 'Star the photo', + 'move' => 'Move the photo', + 'delete' => 'Delete the photo', + 'edit' => 'Edit information', + 'show_hide_meta' => 'Show information', + + 'keep_hidden' => 'We will keep it hidden.', + ], + 'login' => [ + 'username' => 'Username', + 'password' => 'Password', + 'unknown_invalid' => 'Unknown user or invalid password.', + 'signin' => 'Sign-In', + ], + 'register' => [ + 'enter_license' => 'Enter your license key below:', + 'license_key' => 'License key', + 'invalid_license' => 'Invalid license key.', + 'register' => 'Register', + ], + 'share_album' => [ + 'url_copied' => 'Copied URL to clipboard!', + ], + 'upload' => [ + 'completed' => 'Completed', + 'uploaded' => 'Uploaded:', + 'release' => 'Release file to upload!', + 'select' => 'Click here to select files to upload', + 'drag' => '(Or drag files to the page)', + 'loading' => 'Loading', + 'resume' => 'Resume', + 'uploading' => 'Uploading', + 'finished' => 'Finished', + 'failed_error' => 'Upload failed. The server returned an error!', + ], + 'visibility' => [ + 'public' => 'Public', + 'public_expl' => 'Anonymous users can access this album, subject to the restrictions below.', + 'full' => 'Original', + 'full_expl' => 'Anonymous users can view full-resolution photos.', + 'hidden' => 'Hidden', + 'hidden_expl' => 'Anonymous users need a direct link to access this album.', + 'downloadable' => 'Downloadable', + 'downloadable_expl' => 'Anonymous users can download this album.', + 'password' => 'Password', + 'password_prot' => 'Password protected', + 'password_prot_expl' => 'Anonymous users need a shared password to access this album.', + 'nsfw' => 'Sensitive', + 'nsfw_expl' => 'Album contains sensitive content.', + 'visibility_updated' => 'Visibility updated.', + ], + 'move_album' => [ + 'confirm_single' => 'Are you sure you want to move the album “%1$s” into the album “%2$s”?', + 'confirm_multiple' => 'Are you sure you want to move all selected albums into the album “%s”?', + 'move_single' => 'Move Album', + 'move_to' => 'Move to', + 'move_to_single' => 'Move %s to:', + 'move_to_multiple' => 'Move %d albums to:', + 'no_album_target' => 'No album to move to', + 'moved_single' => 'Album moved!', + 'moved_single_details' => '%1$s moved to %2$s', + 'moved_details' => 'Album(s) moved to %s', + ], + 'new_album' => [ + 'menu' => 'Create Album', + 'info' => 'Enter a title for the new album:', + 'title' => 'title', + 'create' => 'Create Album', + ], + 'new_tag_album' => [ + 'menu' => 'Create Tag Album', + 'info' => 'Enter a title for the new tag album:', + 'title' => 'title', + 'set_tags' => 'Set tags to show', + 'warn' => 'Make sure to press enter after each tag', + 'create' => 'Create Tag Album', + ], + 'delete_album' => [ + 'confirmation' => 'Are you sure you want to delete the album “%s” and all of the photos it contains?', + 'confirmation_multiple' => 'Are you sure you want to delete all %d selected albums and all of the photos they contain?', + 'warning' => 'This action can not be undone!', + 'delete' => 'Delete Album and Photos', + ], + 'transfer' => [ + 'query' => 'Transfer ownership of album to', + 'confirmation' => 'Are you sure you want to transfer the ownership of album “%s” and all the photos it contains to "%s"?', + 'lost_access_warning' => 'Your access to this album will be lost.', + 'warning' => 'This action can not be undone!', + 'transfer' => 'Transfer ownership of album and photos', + ], + 'rename' => [ + 'photo' => 'Enter a new title for this photo:', + 'album' => 'Enter a new title for this album:', + 'rename' => 'Rename', + ], + 'merge' => [ + 'merge_to' => 'Merge %s to:', + 'merge_to_multiple' => 'Merge %d albums to:', + 'no_albums' => 'No albums to merge to.', + 'confirm' => 'Are you sure you want to merge the album “%1$s” into the album “%2$s”?', + 'confirm_multiple' => 'Are you sure you want to merge all selected albums into the album “%s”?', + 'merge' => 'Merge Albums', + 'merged' => 'Album(s) merged to %s!', + ], + 'unlock' => [ + 'password_required' => 'This album is protected by a password. Enter the password below to view the photos of this album:', + 'password' => 'Password', + 'unlock' => 'Unlock', + ], + 'photo_tags' => [ + 'question' => 'Enter your tags for this photo.', + 'question_multiple' => 'Enter your tags for all %d selected photos. Existing tags will be overwritten.', + 'no_tags' => 'No Tags', + 'set_tags' => 'Set Tags', + 'updated' => 'Tags updated!', + 'tags_override_info' => 'If this is unchecked, the tags will be added to the existing tags of the photo.', + ], + 'photo_copy' => [ + 'no_albums' => 'No albums to copy to', + 'copy_to' => 'Copy %s to:', + 'copy_to_multiple' => 'Copy %d photos to:', + 'confirm' => 'Copy %s to %s.', + 'confirm_multiple' => 'Copy %d photos to %s.', + 'copy' => 'Copy', + 'copied' => 'Photo(s) copied!', + ], + 'photo_delete' => [ + 'confirm' => 'Are you sure you want to delete the photo “%s”?', + 'confirm_multiple' => 'Are you sure you want to delete all %d selected photos?', + 'deleted' => 'Photo(s) deleted!', + ], + 'move_photo' => [ + 'move_single' => 'Move %s to:', + 'move_multiple' => 'Move %d photos to:', + 'confirm' => 'Move %s to %s.', + 'confirm_multiple' => 'Move %d photos to %s.', + 'moved' => 'Photo(s) moved to %s!', + ], + 'target_user' => [ + 'placeholder' => 'Select user', + ], + 'target_album' => [ + 'placeholder' => 'Select album', + ], + 'webauthn' => [ + 'u2f' => 'U2F', + 'success' => 'Authentication successful!', + 'error' => 'Whoops, it looks like something went wrong. Please reload the site and try again!', + ], + 'se' => [ + 'available' => 'Available in the Supporter Edition', + ], + 'session_expired' => [ + 'title' => 'Session expired', + 'message' => 'Your session has expired.
Please reload the page.', + 'reload' => 'Reload', + 'go_to_gallery' => 'Go to the Gallery', + ], +]; \ No newline at end of file diff --git a/lang/en/fix-tree.php b/lang/en/fix-tree.php new file mode 100644 index 00000000000..64803e310e6 --- /dev/null +++ b/lang/en/fix-tree.php @@ -0,0 +1,55 @@ + 'Maintenance', + 'intro' => 'This page allows you to re-order and fix your albums manually.
Before any modifications, we strongly recommend you to read about Nested Set tree structures.', + 'warning' => 'You can really break your Lychee installation here, modify values at your own risks.', + + 'help' => [ + 'header' => 'Help', + 'hover' => 'Hover ids or titles to highlight related albums.', + 'left' => 'Left', + 'right' => 'Right', + 'convenience' => 'For your convenience, the and buttons allow you to change the values of %s and %s by respectively +1 and -1 with propagation.', + 'left-right-warn' => 'The and indicates that the value of %s (and respectively %s) is duplicated somewhere.', + 'parent-marked' => 'Marked Parent Id indicates that the %s and %s do not satisfy the Nest Set tree structures. Edit either the Parent Id or the %s/%s values.', + 'slowness' => 'This page will be slow with a large number of albums.', + ], + + 'buttons' => [ + 'reset' => 'Reset', + 'check' => 'Check', + 'apply' => 'Apply', + ], + + 'table' => [ + 'title' => 'Title', + 'left' => 'Left', + 'right' => 'Right', + 'id' => 'Id', + 'parent' => 'Parent Id', + ], + + 'errors' => [ + 'invalid' => 'Invalid tree!', + 'invalid_details' => 'We are not applying this as it is guaranteed to be a broken state.', + 'invalid_left' => 'Album %s has an invalid left value.', + 'invalid_right' => 'Album %s has an invalid right value.', + 'invalid_left_right' => 'Album %s has an invalid left/right values. Left should be strictly smaller than right: %s < %s.', + 'duplicate_left' => 'Album %s has a duplicate left value %s.', + 'duplicate_right' => 'Album %s has a duplicate right value %s.', + 'parent' => 'Album %s has an unexpected parent id %s.', + 'unknown' => 'Album %s has an unknown error.', + ], +]; \ No newline at end of file diff --git a/lang/en/gallery.php b/lang/en/gallery.php new file mode 100644 index 00000000000..eb8008827e0 --- /dev/null +++ b/lang/en/gallery.php @@ -0,0 +1,241 @@ + 'Gallery', + + 'smart_albums' => 'Smart albums', + 'albums' => 'Albums', + 'root' => 'Albums', + + 'original' => 'Original', + 'medium' => 'Medium', + 'medium_hidpi' => 'Medium HiDPI', + 'small' => 'Thumb', + 'small_hidpi' => 'Thumb HiDPI', + 'thumb' => 'Square thumb', + 'thumb_hidpi' => 'Square thumb HiDPI', + 'placeholder' => 'Low Quality Image Placeholder', + 'thumbnail' => 'Photo thumbnail', + 'live_video' => 'Video part of live-photo', + + 'camera_data' => 'Camera date', + 'album_reserved' => 'All Rights Reserved', + + 'map' => [ + 'error_gpx' => 'Error loading GPX file', + 'osm_contributors' => 'OpenStreetMap contributors', + ], + + 'search' => [ + 'title' => 'Search', + 'searching' => 'Searching…', + 'no_results' => 'Nothing matches your search query.', + 'searchbox' => 'Search…', + 'minimum_chars' => 'Minimum %s characters required.', + 'photos' => 'Photos (%s)', + 'albums' => 'Albums (%s)', + ], + + 'smart_album' => [ + 'unsorted' => 'Unsorted', + 'starred' => 'Starred', + 'recent' => 'Recent', + 'public' => 'Public', + 'on_this_day' => 'On This Day', + ], + + 'layout' => [ + 'squares' => 'Square thumbnails', + 'justified' => 'With aspect, justified', + 'masonry' => 'With aspect, masonry', + 'grid' => 'With aspect, grid', + ], + + 'overlay' => [ + 'none' => 'None', + 'exif' => 'EXIF data', + 'description' => 'Description', + 'date' => 'Date taken', + ], + + 'timeline' => [ + 'default' => 'default', + 'disabled' => 'disabled', + 'year' => 'Year', + 'month' => 'Month', + 'day' => 'Day', + 'hour' => 'Hour', + ], + + 'album' => [ + 'header_albums' => 'Albums', + 'header_photos' => 'Photos', + 'no_results' => 'Nothing to see here', + 'upload' => 'Upload photos', + + 'tabs' => [ + 'about' => 'About Album', + 'share' => 'Share Album', + 'move' => 'Move Album', + 'danger' => 'DANGER ZONE', + ], + + 'hero' => [ + 'created' => 'Created', + 'copyright' => 'Copyright', + 'subalbums' => 'Subalbums', + 'images' => 'Photos', + 'download' => 'Download Album', + 'share' => 'Share Album', + 'stats_only_se' => 'Statistics available in the Supporter Edition', + ], + + 'stats' => [ + 'lens' => 'Lens', + 'shutter' => 'Shutter speed', + 'iso' => 'ISO', + 'model' => 'Model', + 'aperture' => 'Aperture', + 'no_data' => 'No data', + ], + + 'properties' => [ + 'title' => 'Title', + 'description' => 'Description', + 'photo_ordering' => 'Order photos by', + 'children_ordering' => 'Order albums by', + 'asc/desc' => 'asc/desc', + 'header' => 'Set album header', + 'compact_header' => 'Use compact header', + 'license' => 'Set license', + 'copyright' => 'Set copyright', + 'aspect_ratio' => 'Set album thumbs aspect ratio', + 'album_timeline' => 'Set album timeline mode', + 'photo_timeline' => 'Set photo timeline mode', + 'layout' => 'Set photo layout', + 'show_tags' => 'Set tags to show', + 'tags_required' => 'Tags are required.', + ], + ], + + 'photo' => [ + 'actions' => [ + 'star' => 'Star', + 'unstar' => 'Unstar', + 'set_album_header' => 'Set as album header', + 'move' => 'Move', + 'delete' => 'Delete', + 'header_set' => 'Header set', + ], + + 'details' => [ + 'about' => 'About', + 'basics' => 'Basics', + 'title' => 'Title', + 'uploaded' => 'Uploaded', + 'description' => 'Description', + 'license' => 'License', + 'reuse' => 'Reuse', + 'latitude' => 'Latitude', + 'longitude' => 'Longitude', + 'altitude' => 'Altitude', + 'location' => 'Location', + 'image' => 'Image', + 'video' => 'Video', + 'size' => 'Size', + 'format' => 'Format', + 'resolution' => 'Resolution', + 'duration' => 'Duration', + 'fps' => 'Frame rate', + 'tags' => 'Tags', + 'camera' => 'Camera', + 'captured' => 'Captured', + 'make' => 'Make', + 'type' => 'Type/Model', + 'lens' => 'Lens', + 'shutter' => 'Shutter Speed', + 'aperture' => 'Aperture', + 'focal' => 'Focal Length', + 'iso' => 'ISO %s', + ], + + 'edit' => [ + 'set_title' => 'Set Title', + 'set_description' => 'Set Description', + 'set_license' => 'Set License', + 'no_tags' => 'No Tags', + 'set_tags' => 'Set Tags', + 'set_created_at' => 'Set Upload Date', + ], + ], + + 'nsfw' => [ + 'header' => 'Sensitive content', + 'description' => 'This album contains sensitive content which some people may find offensive or disturbing.', + 'consent' => 'Tap to consent.', + ], + + 'menus' => [ + 'star' => 'Star', + 'unstar' => 'Unstar', + 'star_all' => 'Star Selected', + 'unstar_all' => 'Unstar Selected', + 'tag' => 'Tag', + 'tag_all' => 'Tag Selected', + 'set_cover' => 'Set Album Cover', + 'remove_header' => 'Remove Album Header', + 'set_header' => 'Set Album Header', + 'copy_to' => 'Copy to …', + 'copy_all_to' => 'Copy Selected to …', + 'rename' => 'Rename', + 'move' => 'Move', + 'move_all' => 'Move Selected', + 'delete' => 'Delete', + 'delete_all' => 'Delete Selected', + 'download' => 'Download', + 'download_all' => 'Download Selected', + 'merge' => 'Merge', + 'merge_all' => 'Merge Selected', + + 'upload_photo' => 'Upload Photo', + 'import_link' => 'Import from Link', + 'import_dropbox' => 'Import from Dropbox', + 'new_album' => 'New Album', + 'new_tag_album' => 'New Tag Album', + 'upload_track' => 'Upload track', + 'delete_track' => 'Delete track', + ], + + 'sort' => [ + 'photo_select_1' => 'Upload Time', + 'photo_select_2' => 'Take Date', + 'photo_select_3' => 'Title', + 'photo_select_4' => 'Description', + 'photo_select_6' => 'Star', + 'photo_select_7' => 'Photo Format', + 'ascending' => 'Ascending', + 'descending' => 'Descending', + 'album_select_1' => 'Creation Time', + 'album_select_2' => 'Title', + 'album_select_3' => 'Description', + 'album_select_5' => 'Latest Take Date', + 'album_select_6' => 'Oldest Take Date', + ], + + 'albums_protection' => [ + 'private' => 'private', + 'public' => 'public', + 'inherit_from_parent' => 'inherit from parent', + ], +]; \ No newline at end of file diff --git a/lang/en/jobs.php b/lang/en/jobs.php new file mode 100644 index 00000000000..5d952b76012 --- /dev/null +++ b/lang/en/jobs.php @@ -0,0 +1,18 @@ + 'Jobs', + + 'no_data' => 'No Jobs have been executed yet.', +]; \ No newline at end of file diff --git a/lang/en/landing.php b/lang/en/landing.php new file mode 100644 index 00000000000..fe6fe55b8ea --- /dev/null +++ b/lang/en/landing.php @@ -0,0 +1,19 @@ + 'Gallery', + 'access_gallery' => 'Access the gallery', + 'hosted_with_lychee' => 'Hosted with Lychee', + 'copyright' => 'All images on this website are subject to copyright by %1$s © %2$s', +]; \ No newline at end of file diff --git a/lang/en/left-menu.php b/lang/en/left-menu.php new file mode 100644 index 00000000000..9a3e91f4037 --- /dev/null +++ b/lang/en/left-menu.php @@ -0,0 +1,29 @@ + 'Back to Gallery', + + 'admin' => 'Admin', + 'clockwork' => 'Clockwork App', + 'logs' => 'Show Logs', + 'jobs' => 'Show Job History', + 'user' => 'User', + + 'sign_out' => 'Sign Out', + + 'about' => 'About', + 'api' => 'API Documentation', + 'source_code' => 'Source Code', + 'support' => 'Support', +]; \ No newline at end of file diff --git a/lang/en/lychee.php b/lang/en/lychee.php new file mode 100644 index 00000000000..804dfcf8f29 --- /dev/null +++ b/lang/en/lychee.php @@ -0,0 +1,542 @@ + 'Username', + 'PASSWORD' => 'Password', + 'ENTER' => 'Enter', + 'CANCEL' => 'Cancel', + 'CONFIRM' => 'Confirm', + 'SIGN_IN' => 'Sign-In', + 'CLOSE' => 'Close', + 'SETTINGS' => 'Settings', + 'SEARCH' => 'Search …', + 'MORE' => 'More', + 'DEFAULT' => 'Default', + 'GALLERY' => 'Gallery', + + 'USERS' => 'Users', + 'PROFILE' => 'Profile', + 'CREATE' => 'Create', + 'REMOVE' => 'Remove', + 'SHARE' => 'Share', + 'U2F' => 'U2F', + 'NOTIFICATIONS' => 'Notifications', + 'SHARING' => 'Sharing', + 'CHANGE_LOGIN' => 'Change Login', + 'CHANGE_SORTING' => 'Change Sorting', + 'SET_DROPBOX' => 'Set Dropbox', + 'ABOUT_LYCHEE' => 'About Lychee', + 'DIAGNOSTICS' => 'Diagnostics', + 'DIAGNOSTICS_GET_SIZE' => 'Request space usage', + 'JOBS' => 'Show job history', + 'LOGS' => 'Show Logs', + 'SIGN_OUT' => 'Sign Out', + 'UPDATE_AVAILABLE' => 'Update available!', + 'MIGRATION_AVAILABLE' => 'Migration available!', + 'CHECK_FOR_UPDATE' => 'Check for updates', + 'DEFAULT_LICENSE' => 'Default license for new uploads:', + 'SET_LICENSE' => 'Set License', + 'SET_OVERLAY_TYPE' => 'Set Overlay', + 'SET_ALBUM_DECORATION' => 'Set album decorations', + 'SET_MAP_PROVIDER' => 'Set OpenStreetMap tiles provider', + 'FULL_SETTINGS' => 'Full Settings', + 'UPDATE' => 'Update', + 'RESET' => 'Reset', + 'DISABLE_TOKEN_TOOLTIP' => 'Disable', + 'ENABLE_TOKEN' => 'Enable API token', + 'DISABLED_TOKEN_STATUS_MSG' => 'Disabled', + 'TOKEN_BUTTON' => 'API Token ...', + 'TOKEN_NOT_AVAILABLE' => 'You have already viewed this token.', + 'TOKEN_WAIT' => 'Wait ...', + + 'SMART_ALBUMS' => 'Smart albums', + 'SHARED_ALBUMS' => 'Shared albums', + 'ALBUMS' => 'Albums', + 'PHOTOS' => 'Pictures', + 'SEARCH_RESULTS' => 'Search results', + + 'RENAME' => 'Rename', + 'RENAME_ALL' => 'Rename Selected', + 'MERGE' => 'Merge', + 'MERGE_ALL' => 'Merge Selected', + 'MAKE_PUBLIC' => 'Make Public', + 'SHARE_ALBUM' => 'Share Album', + 'SHARE_PHOTO' => 'Share Photo', + 'VISIBILITY_ALBUM' => 'Album Visibility', + 'VISIBILITY_PHOTO' => 'Photo Visibility', + 'DOWNLOAD_ALBUM' => 'Download Album', + 'ABOUT_ALBUM' => 'About Album', + 'DELETE_ALBUM' => 'Delete Album', + 'MOVE_ALBUM' => 'Move Album', + 'FULLSCREEN_ENTER' => 'Enter Fullscreen', + 'FULLSCREEN_EXIT' => 'Exit Fullscreen', + + 'SHARING_ALBUM_USERS' => 'Share this album with users', + 'WAIT_FETCH_DATA' => 'Please wait while we get the data …', + 'SHARING_ALBUM_USERS_NO_USERS' => 'There are no users to share the album with', + 'SHARING_ALBUM_USERS_LONG_MESSAGE' => 'Select the users to share this album with', + + 'DELETE_ALBUM_QUESTION' => 'Delete Album and Photos', + 'KEEP_ALBUM' => 'Keep Album', + 'DELETE_ALBUM_CONFIRMATION' => 'Are you sure you want to delete the album “%s” and all of the photos it contains? This action can’t be undone!', + + 'DELETE_TAG_ALBUM_QUESTION' => 'Delete Album', + 'DELETE_TAG_ALBUM_CONFIRMATION' => 'Are you sure you want to delete the album “%s” (any photos inside will not be deleted)? This action can’t be undone!', + + 'DELETE_ALBUMS_QUESTION' => 'Delete Albums and Photos', + 'KEEP_ALBUMS' => 'Keep Albums', + 'DELETE_ALBUMS_CONFIRMATION' => 'Are you sure you want to delete all %d selected albums and all of the photos they contain? This action can’t be undone!', + + 'DELETE_UNSORTED_CONFIRM' => 'Are you sure you want to delete all photos from “Unsorted”? This action can’t be undone!', + 'CLEAR_UNSORTED' => 'Clear Unsorted', + 'KEEP_UNSORTED' => 'Keep Unsorted', + + 'EDIT_SHARING' => 'Edit Sharing', + 'MAKE_PRIVATE' => 'Make Private', + + 'CLOSE_ALBUM' => 'Close Album', + 'CLOSE_PHOTO' => 'Close Photo', + 'CLOSE_MAP' => 'Close Map', + + 'ADD' => 'Add', + 'MOVE' => 'Move', + 'MOVE_ALL' => 'Move Selected', + 'DUPLICATE' => 'Duplicate', + 'DUPLICATE_ALL' => 'Duplicate Selected', + 'COPY_TO' => 'Copy to …', + 'COPY_ALL_TO' => 'Copy Selected to …', + 'DELETE' => 'Delete', + 'SAVE' => 'Save', + 'DELETE_ALL' => 'Delete Selected', + 'DOWNLOAD' => 'Download', + 'DOWNLOAD_ALL' => 'Download Selected', + 'UPLOAD_PHOTO' => 'Upload Photo', + 'IMPORT_LINK' => 'Import from Link', + 'IMPORT_DROPBOX' => 'Import from Dropbox', + 'IMPORT_SERVER' => 'Import from Server', + 'NEW_ALBUM' => 'New Album', + 'NEW_TAG_ALBUM' => 'New Tag Album', + 'UPLOAD_TRACK' => 'Upload track', + 'DELETE_TRACK' => 'Delete track', + + 'TITLE_NEW_ALBUM' => 'Enter a title for the new album:', + 'UNTITLED' => 'Untitled', + 'UNSORTED' => 'Unsorted', + 'STARRED' => 'Starred', + 'RECENT' => 'Recent', + 'PUBLIC' => 'Public', + 'ON_THIS_DAY' => 'On This Day', + 'NUM_PHOTOS' => 'Photos', + + 'CREATE_ALBUM' => 'Create Album', + 'CREATE_TAG_ALBUM' => 'Create Tag Album', + + 'STAR_PHOTO' => 'Star Photo', + 'STAR' => 'Star', + 'UNSTAR' => 'Unstar', + 'STAR_ALL' => 'Star Selected', + 'UNSTAR_ALL' => 'Unstar Selected', + 'TAG' => 'Tag', + 'TAG_ALL' => 'Tag Selected', + 'UNSTAR_PHOTO' => 'Unstar Photo', + 'SET_COVER' => 'Set Album Cover', + 'REMOVE_COVER' => 'Remove Album Cover', + 'SET_HEADER' => 'Set Album Header', + 'REMOVE_HEADER' => 'Remove Album Header', + 'SET_COMPACT_HEADER' => 'Use Compact Header', + + 'FULL_PHOTO' => 'Open Original', + 'ABOUT_PHOTO' => 'About Photo', + 'DISPLAY_FULL_MAP' => 'Map', + 'DIRECT_LINK' => 'Direct Link', + 'DIRECT_LINKS' => 'Direct Links', + 'QR_CODE' => 'QR Code', + + 'ALBUM_ABOUT' => 'About', + 'ALBUM_BASICS' => 'Basics', + 'ALBUM_TITLE' => 'Title', + 'ALBUM_COPYRIGHT' => 'Copyright', + 'ALBUM_SET_COPYRIGHT' => 'Set copyright', + 'ALBUM_NEW_TITLE' => 'Enter a new title for this album:', + 'ALBUMS_NEW_TITLE' => 'Enter a title for all %d selected albums:', + 'ALBUM_SET_TITLE' => 'Set Title', + 'ALBUM_DESCRIPTION' => 'Description', + 'ALBUM_SHOW_TAGS' => 'Tags to show', + 'ALBUM_NEW_DESCRIPTION' => 'Enter a new description for this album:', + 'ALBUM_SET_DESCRIPTION' => 'Set Description', + 'ALBUM_NEW_SHOWTAGS' => 'Enter tags of photos that will be visible in this album:', + 'ALBUM_SET_SHOWTAGS' => 'Set tags to show', + 'ALBUM_ALBUM' => 'Album', + 'ALBUM_CREATED' => 'Created', + 'ALBUM_IMAGES' => 'Images', + 'ALBUM_VIDEOS' => 'Videos', + 'ALBUM_SUBALBUMS' => 'Subalbums', + 'ALBUM_SHARING' => 'Share', + 'ALBUM_SHR_YES' => 'YES', + 'ALBUM_SHR_NO' => 'No', + 'ALBUM_PUBLIC' => 'Public', + 'ALBUM_PUBLIC_EXPL' => 'Anonymous users can access this album, subject to the restrictions below.', + 'ALBUM_FULL' => 'Original', + 'ALBUM_FULL_EXPL' => 'Anonymous users can behold full-resolution photos.', + 'ALBUM_HIDDEN' => 'Hidden', + 'ALBUM_HIDDEN_EXPL' => 'Anonymous users need a direct link to access this album.', + 'ALBUM_MARK_NSFW' => 'Mark album as sensitive', + 'ALBUM_UNMARK_NSFW' => 'Unmark album as sensitive', + 'ALBUM_NSFW' => 'Sensitive', + 'ALBUM_NSFW_EXPL' => 'Album contains sensitive content.', + 'ALBUM_DOWNLOADABLE' => 'Downloadable', + 'ALBUM_DOWNLOADABLE_EXPL' => 'Anonymous users can download this album.', + 'ALBUM_SHARE_BUTTON_VISIBLE' => 'Share button is visible', + 'ALBUM_SHARE_BUTTON_VISIBLE_EXPL' => 'Anonymous users can see social media sharing links.', + 'ALBUM_PASSWORD' => 'Password', + 'ALBUM_PASSWORD_PROT' => 'Password protected', + 'ALBUM_PASSWORD_PROT_EXPL' => 'Anonymous users need a shared password to access this album.', + 'ALBUM_PASSWORD_REQUIRED' => 'This album is protected by a password. Enter the password below to view the photos of this album:', + 'ALBUM_MERGE' => 'Are you sure you want to merge the album “%1$s” into the album “%2$s”?', + 'ALBUMS_MERGE' => 'Are you sure you want to merge all selected albums into the album “%s”?', + 'MERGE_ALBUM' => 'Merge Albums', + 'DONT_MERGE' => 'Don’t Merge', + 'ALBUM_MOVE' => 'Are you sure you want to move the album “%1$s” into the album “%2$s”?', + 'ALBUMS_MOVE' => 'Are you sure you want to move all selected albums into the album “%s”?', + 'MOVE_ALBUMS' => 'Move Albums', + 'NOT_MOVE_ALBUMS' => 'Don’t Move', + 'ROOT' => 'Albums', + 'ALBUM_REUSE' => 'Reuse', + 'ALBUM_LICENSE' => 'License', + 'ALBUM_SET_LICENSE' => 'Set License', + 'ALBUM_LICENSE_HELP' => 'Need help choosing?', + 'ALBUM_LICENSE_NONE' => 'None', + 'ALBUM_RESERVED' => 'All Rights Reserved', + 'ALBUM_SET_ORDER' => 'Set Order', + 'ALBUM_ORDERING' => 'Order by', + 'ALBUM_PHOTO_ORDERING' => 'Order photos by', + 'ALBUM_CHILDREN_ORDERING' => 'Order albums by', + 'ALBUM_OWNER' => 'Owner', + + 'PHOTO_ABOUT' => 'About', + 'PHOTO_BASICS' => 'Basics', + 'PHOTO_TITLE' => 'Title', + 'PHOTO_NEW_TITLE' => 'Enter a new title for this photo:', + 'PHOTO_SET_TITLE' => 'Set Title', + 'PHOTO_UPLOADED' => 'Uploaded', + 'PHOTO_DESCRIPTION' => 'Description', + 'PHOTO_NEW_DESCRIPTION' => 'Enter a new description for this photo:', + 'PHOTO_SET_DESCRIPTION' => 'Set Description', + 'PHOTO_NEW_LICENSE' => 'Add a License', + 'PHOTO_SET_LICENSE' => 'Set License', + 'PHOTO_LICENSE' => 'License', + 'PHOTO_LICENSE_HELP' => 'Need help choosing?', + 'PHOTO_REUSE' => 'Reuse', + 'PHOTO_LICENSE_NONE' => 'None', + 'PHOTO_RESERVED' => 'All Rights Reserved', + 'PHOTO_LATITUDE' => 'Latitude', + 'PHOTO_LONGITUDE' => 'Longitude', + 'PHOTO_ALTITUDE' => 'Altitude', + 'PHOTO_IMGDIRECTION' => 'Direction', + 'PHOTO_LOCATION' => 'Location', + 'PHOTO_IMAGE' => 'Image', + 'PHOTO_VIDEO' => 'Video', + 'PHOTO_SIZE' => 'Size', + 'PHOTO_FORMAT' => 'Format', + 'PHOTO_RESOLUTION' => 'Resolution', + 'PHOTO_DURATION' => 'Duration', + 'PHOTO_FPS' => 'Frame rate', + 'PHOTO_TAGS' => 'Tags', + 'PHOTO_NOTAGS' => 'No Tags', + 'PHOTO_NEW_TAGS' => 'Enter your tags for this photo. You can add multiple tags by separating them with a comma:', + 'PHOTOS_NEW_TAGS' => 'Enter your tags for all %d selected photos. Existing tags will be overwritten. You can add multiple tags by separating them with a comma:', + 'PHOTO_SET_TAGS' => 'Set Tags', + 'PHOTO_CAMERA' => 'Camera', + 'PHOTO_CAPTURED' => 'Captured', + 'PHOTO_MAKE' => 'Make', + 'PHOTO_TYPE' => 'Type/Model', + 'PHOTO_LENS' => 'Lens', + 'PHOTO_SHUTTER' => 'Shutter Speed', + 'PHOTO_APERTURE' => 'Aperture', + 'PHOTO_FOCAL' => 'Focal Length', + 'PHOTO_ISO' => 'ISO %s', + 'PHOTO_SHARING' => 'Sharing', + 'PHOTO_DELETE' => 'Delete Photo', + 'PHOTO_KEEP' => 'Keep Photo', + 'PHOTO_DELETE_CONFIRMATION' => 'Are you sure you want to delete the photo “%s”? This action can’t be undone!', + 'PHOTO_DELETE_ALL' => 'Are you sure you want to delete all %d selected photo? This action can’t be undone!', + 'PHOTOS_NEW_TITLE' => 'Enter a title for all %d selected photos:', + 'PHOTO_MAKE_PRIVATE_ALBUM' => 'This photo is located in a public album. To make this photo private or public, edit the visibility of the associated album.', + 'PHOTO_SHOW_ALBUM' => 'Show Album', + 'PHOTO_PUBLIC' => 'Public', + 'PHOTO_PUBLIC_EXPL' => 'Anonymous users can view this photo, subject to the restrictions below.', + 'PHOTO_FULL' => 'Original', + 'PHOTO_FULL_EXPL' => 'Anonymous users can behold full-resolution photo.', + 'PHOTO_HIDDEN' => 'Hidden', + 'PHOTO_HIDDEN_EXPL' => 'Anonymous users need a direct link to view this photo.', + 'PHOTO_DOWNLOADABLE' => 'Downloadable', + 'PHOTO_DOWNLOADABLE_EXPL' => 'Anonymous users may download this photo.', + 'PHOTO_SHARE_BUTTON_VISIBLE' => 'Share button is visible', + 'PHOTO_SHARE_BUTTON_VISIBLE_EXPL' => 'Anonymous users can see social media sharing links.', + 'PHOTO_PASSWORD_PROT' => 'Password protected', + 'PHOTO_PASSWORD_PROT_EXPL' => 'Anonymous users need a shared password to view this photo.', + 'PHOTO_EDIT_SHARING_TEXT' => 'The sharing properties of this photo will be changed to the following:', + 'PHOTO_NO_EDIT_SHARING_TEXT' => 'Because this photo is located in a public album, it inherits that album’s visibility settings. Its current visibility is shown below for informational purposes only.', + 'PHOTO_EDIT_GLOBAL_SHARING_TEXT' => 'The visibility of this photo can be fine-tuned using global Lychee settings. Its current visibility is shown below for informational purposes only.', + 'PHOTO_NEW_CREATED_AT' => 'Enter the upload date for this photo. mm/dd/yyyy, hh:mm [am/pm]', + 'PHOTO_SET_CREATED_AT' => 'Set upload date', + + 'LOADING' => 'Loading', + 'ERROR' => 'Error', + 'ERROR_TEXT' => 'Whoops, it looks like something went wrong. Please reload the site and try again!', + 'ERROR_UNKNOWN' => 'Something unexpected happened. Please try again and check your installation and server. Take a look at the readme for more information.', + 'ERROR_MAP_DEACTIVATED' => 'Map functionality has been deactivated under settings.', + 'ERROR_SEARCH_DEACTIVATED' => 'Search functionality has been deactivated under settings.', + 'SUCCESS' => 'OK', + 'CHANGE_SUCCESS' => 'Change successful.', + 'RETRY' => 'Retry', + 'OVERRIDE' => 'Override', + 'TAGS_OVERRIDE_INFO' => 'If this is unchecked, the tags will be added to the existing tags of the photo.', + + 'SETTINGS_SUCCESS_LOGIN' => 'Login Info updated.', + 'SETTINGS_SUCCESS_SORT' => 'Sorting order updated.', + 'SETTINGS_SUCCESS_DROPBOX' => 'Dropbox Key updated.', + 'SETTINGS_SUCCESS_LANG' => 'Language updated', + 'SETTINGS_SUCCESS_LAYOUT' => 'Layout updated', + 'SETTINGS_SUCCESS_IMAGE_OVERLAY' => 'EXIF Overlay setting updated', + 'SETTINGS_SUCCESS_PUBLIC_SEARCH' => 'Public search updated', + 'SETTINGS_SUCCESS_LICENSE' => 'Default license updated', + 'SETTINGS_SUCCESS_MAP_DISPLAY' => 'Map display settings updated', + 'SETTINGS_SUCCESS_MAP_DISPLAY_PUBLIC' => 'Map display settings for public albums updated', + 'SETTINGS_SUCCESS_MAP_PROVIDER' => 'Map provider settings updated', + 'SETTINGS_SUCCESS_CSS' => 'CSS updated', + 'SETTINGS_SUCCESS_JS' => 'JS updated', + 'SETTINGS_SUCCESS_UPDATE' => 'Settings updated successfully', + 'SETTINGS_DROPBOX_KEY' => 'Dropbox API Key', + 'SETTINGS_ADVANCED_WARNING_EXPL' => 'Changing these advanced settings can be harmful to the stability, security and performance of this application. You should only modify them if you are sure of what you are doing.', + 'SETTINGS_ADVANCED_SAVE' => 'Save my modifications, I accept the risk!', + + 'U2F_NOT_SUPPORTED' => 'U2F not supported. Sorry.', + 'U2F_NOT_SECURE' => 'Environment not secured. U2F not available.', + 'U2F_REGISTER_KEY' => 'Register new device.', + 'U2F_REGISTRATION_SUCCESS' => 'Registration successful!', + 'U2F_AUTHENTIFICATION_SUCCESS' => 'Authentication successful!', + 'U2F_CREDENTIALS' => 'Credentials', + 'U2F_CREDENTIALS_DELETED' => 'Credentials deleted!', + 'U2F_LOGIN' => 'Log in with WebAuthn', + + 'NEW_PHOTOS_NOTIFICATION' => 'Send new photos notification emails.', + 'SETTINGS_SUCCESS_NEW_PHOTOS_NOTIFICATION' => 'New photos notification updated', + 'USER_EMAIL_INSTRUCTION' => 'Add your email below to enable receiving email notifications. To stop receiving emails, simply remove your email below.', + + 'LOGIN_USERNAME' => 'New Username', + 'LOGIN_PASSWORD' => 'New Password', + 'LOGIN_PASSWORD_CONFIRM' => 'Confirm Password', + 'PASSWORD_TITLE' => 'Enter your current password:', + 'PASSWORD_CURRENT' => 'Current Password', + 'PASSWORD_TEXT' => 'Your credentials will be changed to the following:', + 'PASSWORD_CHANGE' => 'Change Login', + + 'EDIT_SHARING_TITLE' => 'Edit Sharing', + 'EDIT_SHARING_TEXT' => 'The sharing properties of this album will be changed to the following:', + 'SHARE_ALBUM_TEXT' => 'This album will be shared with the following properties:', + + 'SORT_DIALOG_ATTRIBUTE_LABEL' => 'Attribute', + 'SORT_DIALOG_ORDER_LABEL' => 'Order', + + 'SORT_ALBUM_BY' => 'Sort albums by %1$s in an %2$s order.', + + 'SORT_ALBUM_SELECT_1' => 'Creation Time', + 'SORT_ALBUM_SELECT_2' => 'Title', + 'SORT_ALBUM_SELECT_3' => 'Description', + 'SORT_ALBUM_SELECT_5' => 'Latest Take Date', + 'SORT_ALBUM_SELECT_6' => 'Oldest Take Date', + + 'SORT_PHOTO_BY' => 'Sort photos by %1$s in an %2$s order.', + + 'SORT_PHOTO_SELECT_1' => 'Upload Time', + 'SORT_PHOTO_SELECT_2' => 'Take Date', + 'SORT_PHOTO_SELECT_3' => 'Title', + 'SORT_PHOTO_SELECT_4' => 'Description', + 'SORT_PHOTO_SELECT_6' => 'Star', + 'SORT_PHOTO_SELECT_7' => 'Photo Format', + + 'SORT_ASCENDING' => 'Ascending', + 'SORT_DESCENDING' => 'Descending', + 'SORT_CHANGE' => 'Change Sorting', + + 'DROPBOX_TITLE' => 'Set Dropbox Key', + 'DROPBOX_TEXT' => "In order to import photos from your Dropbox, you need a valid drop-ins app key from their website. Generate yourself a personal key and enter it below:", + + 'LANG_TEXT' => 'Change Lychee language for:', + 'LANG_TITLE' => 'Change Language', + + 'SETTING_RECENT_PUBLIC_TEXT' => 'Make "Recent" smart album accessible to anonymous users', + 'SETTING_STARRED_PUBLIC_TEXT' => 'Make "Starred" smart album accessible to anonymous users', + 'SETTING_ONTHISDAY_PUBLIC_TEXT' => 'Make "On This Day" smart album accessible to anonymous users', + + 'CSS_TEXT' => 'Personalize CSS:', + 'CSS_TITLE' => 'Change CSS', + 'JS_TEXT' => 'Custom JS:', + 'JS_TITLE' => 'Change JS', + 'PUBLIC_SEARCH_TEXT' => 'Public search allowed:', + 'OVERLAY_TYPE' => 'Photo overlay:', + 'OVERLAY_NONE' => 'None', + 'OVERLAY_EXIF' => 'EXIF data', + 'OVERLAY_DESCRIPTION' => 'Description', + 'OVERLAY_DATE' => 'Date taken', + 'ALBUM_DECORATION' => 'Album decorations:', + 'ALBUM_DECORATION_NONE' => 'None', + 'ALBUM_DECORATION_ORIGINAL' => 'Sub-album marker', + 'ALBUM_DECORATION_ALBUM' => 'Number of sub-albums', + 'ALBUM_DECORATION_PHOTO' => 'Number of photos', + 'ALBUM_DECORATION_ALL' => 'Number of sub-albums and photos', + 'ALBUM_DECORATION_ORIENTATION' => 'Orientation of album decorations:', + 'ALBUM_DECORATION_ORIENTATION_ROW' => 'Horizontal (photos, albums)', + 'ALBUM_DECORATION_ORIENTATION_ROW_REVERSE' => 'Horizontal (albums, photos)', + 'ALBUM_DECORATION_ORIENTATION_COLUMN' => 'Vertical (top photos, albums)', + 'ALBUM_DECORATION_ORIENTATION_COLUMN_REVERSE' => 'Vertical (top albums, photos)', + 'MAP_DISPLAY_TEXT' => 'Enable maps (provided by OpenStreetMap):', + 'MAP_DISPLAY_PUBLIC_TEXT' => 'Enable maps for public albums (provided by OpenStreetMap):', + 'MAP_PROVIDER' => 'Provider of OpenStreetMap tiles:', + 'MAP_PROVIDER_WIKIMEDIA' => 'Wikimedia', + 'MAP_PROVIDER_OSM_ORG' => 'OpenStreetMap.org (no HiDPI)', + 'MAP_PROVIDER_OSM_DE' => 'OpenStreetMap.de (no HiDPI)', + 'MAP_PROVIDER_OSM_FR' => 'OpenStreetMap.fr (no HiDPI)', + 'MAP_PROVIDER_RRZE' => 'University of Erlangen, Germany (only HiDPI)', + 'MAP_INCLUDE_SUBALBUMS_TEXT' => 'Include photos of subalbums on map:', + 'LOCATION_DECODING' => 'Decode GPS data into location name', + 'LOCATION_SHOW' => 'Show location name', + 'LOCATION_SHOW_PUBLIC' => 'Show location name for public mode', + + 'LAYOUT_TYPE' => 'Layout of photos:', + 'LAYOUT_SQUARES' => 'Square thumbnails', + 'LAYOUT_JUSTIFIED' => 'With aspect, justified', + 'LAYOUT_MASONRY' => 'With aspect, masonry', + 'LAYOUT_GRID' => 'With aspect, grid', + 'LAYOUT_UNJUSTIFIED' => 'With aspect, unjustified', + 'SET_LAYOUT' => 'Change layout', + + 'NSFW_VISIBLE_TEXT_1' => 'Make Sensitive albums visible by default.', + 'NSFW_VISIBLE_TEXT_2' => 'If the album is public, it is still accessible, just hidden from the view and can be revealed by pressing H.', + 'SETTINGS_SUCCESS_NSFW_VISIBLE' => 'Default sensitive album visibility updated with success.', + + 'NSFW_BANNER' => '

Sensitive content

This album contains sensitive content which some people may find offensive or disturbing.

Tap to consent.

', + 'NSFW_HEADER' => 'Sensitive content', + 'NSFW_EXPLANATION' => 'This album contains sensitive content which some people may find offensive or disturbing.', + 'TAP_CONSENT' => 'Tap to consent.', + + 'VIEW_NO_RESULT' => 'No results', + 'VIEW_NO_PUBLIC_ALBUMS' => 'No public albums', + 'VIEW_NO_CONFIGURATION' => 'No configuration', + 'VIEW_PHOTO_NOT_FOUND' => 'Photo not found', + + 'NO_TAGS' => 'No Tags', + + 'UPLOAD_MANAGE_NEW_PHOTOS' => 'You can now manage your new photo(s).', + 'UPLOAD_COMPLETE' => 'Upload complete', + 'UPLOAD_COMPLETE_FAILED' => 'Failed to upload one or more photos.', + 'UPLOAD_IMPORTING' => 'Importing', + 'UPLOAD_IMPORTING_URL' => 'Importing URL', + 'UPLOAD_UPLOADING' => 'Uploading', + 'UPLOAD_FINISHED' => 'Finished', + 'UPLOAD_PROCESSING' => 'Processing', + 'UPLOAD_FAILED' => 'Failed', + 'UPLOAD_FAILED_ERROR' => 'Upload failed. The server returned an error!', + 'UPLOAD_FAILED_WARNING' => 'Upload failed. The server returned a warning!', + 'UPLOAD_CANCELLED' => 'Cancelled', + 'UPLOAD_SKIPPED' => 'Skipped', + 'UPLOAD_UPDATED' => 'Updated', + 'UPLOAD_GENERAL' => 'General', + 'UPLOAD_IMPORT_SKIPPED_DUPLICATE' => 'This photo has been skipped because it’s already in your library.', + 'UPLOAD_IMPORT_RESYNCED_DUPLICATE' => 'This photo has been skipped because it’s already in your library, but its metadata has been updated.', + 'UPLOAD_ERROR_CONSOLE' => 'Please take a look at the console of your browser for further details.', + 'UPLOAD_UNKNOWN' => 'Server returned an unknown response. Please take a look at the console of your browser for further details.', + 'UPLOAD_ERROR_UNKNOWN' => 'Upload failed. The server returned an unknown error!', + 'UPLOAD_ERROR_POSTSIZE' => 'Upload failed. The PHP post_max_size may be too small! Otherwise check the FAQ.', + 'UPLOAD_ERROR_FILESIZE' => 'Upload failed. The PHP upload_max_filesize may be too small! Otherwise check the FAQ.', + 'UPLOAD_IN_PROGRESS' => 'Lychee is currently uploading!', + 'UPLOAD_IMPORT_WARN_ERR' => 'The import has been finished, but returned warnings or errors. Please take a look at the log (Settings -> Show Log) for further details.', + 'UPLOAD_IMPORT_COMPLETE' => 'Import complete', + 'UPLOAD_IMPORT_INSTR' => 'Please enter the direct link to a photo to import it:', + 'UPLOAD_IMPORT' => 'Import', + 'UPLOAD_IMPORT_SERVER' => 'Importing from server', + 'UPLOAD_IMPORT_SERVER_FOLD' => 'Folder empty or no readable files to process. Please take a look at the log (Settings -> Show Log) for further details.', + 'UPLOAD_IMPORT_SERVER_INSTR' => 'Import all photos, folders, and sub-folders located in the folders with the following absolute paths (on the server). Paths are space-separated, use \\ to escape a space in a path.', + 'UPLOAD_ABSOLUTE_PATH' => 'Absolute path to directories, space separated', + 'UPLOAD_IMPORT_SERVER_EMPT' => 'Could not start import because the folder was empty!', + 'UPLOAD_IMPORT_DELETE_ORIGINALS' => 'Delete originals', + 'UPLOAD_IMPORT_DELETE_ORIGINALS_EXPL' => 'Original files will be deleted after the import when possible.', + 'UPLOAD_IMPORT_VIA_SYMLINK' => 'Symbolic links', + 'UPLOAD_IMPORT_VIA_SYMLINK_EXPL' => 'Import files using symbolic links to originals.', + 'UPLOAD_IMPORT_SKIP_DUPLICATES' => 'Skip duplicates', + 'UPLOAD_IMPORT_SKIP_DUPLICATES_EXPL' => 'Existing media files are skipped.', + 'UPLOAD_IMPORT_RESYNC_METADATA' => 'Re-sync metadata', + 'UPLOAD_IMPORT_RESYNC_METADATA_EXPL' => 'Update metadata of existing media files.', + 'UPLOAD_IMPORT_LOW_MEMORY_EXPL' => 'The import process on the server is approaching the memory limit and may end up being terminated prematurely.', + 'UPLOAD_WARNING' => 'Warning', + 'UPLOAD_IMPORT_NOT_A_DIRECTORY' => 'The given path is not a readable directory!', + 'UPLOAD_IMPORT_PATH_RESERVED' => 'The given path is a reserved path of Lychee!', + 'UPLOAD_IMPORT_FAILED' => 'Could not import the file!', + 'UPLOAD_IMPORT_UNSUPPORTED' => 'Unsupported file type!', + 'UPLOAD_IMPORT_CANCELLED' => 'Import cancelled', + + 'ABOUT_SUBTITLE' => 'Self-hosted photo-management done right', + 'ABOUT_DESCRIPTION' => 'Lychee is a free photo-management tool, which runs on your server or web-space. Installing is a matter of seconds. Upload, manage and share photos like from a native application. Lychee comes with everything you need and all your photos are stored securely.', + 'FOOTER_COPYRIGHT' => 'All images on this website are subject to copyright by %1$s © %2$s', + 'HOSTED_WITH_LYCHEE' => 'Hosted with Lychee', + + 'URL_COPY_TO_CLIPBOARD' => 'Copy to clipboard', + 'URL_COPIED_TO_CLIPBOARD' => 'Copied URL to clipboard!', + 'PHOTO_DIRECT_LINKS_TO_IMAGES' => 'Direct links to image files:', + 'PHOTO_ORIGINAL' => 'Original', + 'PHOTO_MEDIUM' => 'Medium', + 'PHOTO_MEDIUM_HIDPI' => 'Medium HiDPI', + 'PHOTO_SMALL' => 'Thumb', + 'PHOTO_SMALL_HIDPI' => 'Thumb HiDPI', + 'PHOTO_THUMB' => 'Square thumb', + 'PHOTO_THUMB_HIDPI' => 'Square thumb HiDPI', + 'PHOTO_PLACEHOLDER' => 'Low Quality Image Placeholder', + 'PHOTO_THUMBNAIL' => 'Photo thumbnail', + 'PHOTO_LIVE_VIDEO' => 'Video part of live-photo', + 'PHOTO_VIEW' => 'Lychee Photo View:', + + 'PHOTO_EDIT_ROTATECWISE' => 'Rotate clockwise', + 'PHOTO_EDIT_ROTATECCWISE' => 'Rotate counter-clockwise', + + 'ERROR_GPX' => 'Error loading GPX file: ', + 'ERROR_EITHER_ALBUMS_OR_PHOTOS' => 'Please select either albums or photos!', + 'ERROR_COULD_NOT_FIND' => 'Could not find what you want.', + 'ERROR_INVALID_EMAIL' => 'Not a valid email address.', + 'EMAIL_SUCCESS' => 'Email updated!', + 'ERROR_PHOTO_NOT_FOUND' => 'Error: photo %s not found!', + 'ERROR_EMPTY_USERNAME' => 'new username cannot be empty.', + 'ERROR_PASSWORD_DOES_NOT_MATCH' => 'new password does not match.', + 'ERROR_EMPTY_PASSWORD' => 'new password cannot be empty.', + 'ERROR_SELECT_ALBUM' => 'Select an album to share!', + 'ERROR_SELECT_USER' => 'Select a user to share with!', + 'ERROR_SELECT_SHARING' => 'Select a sharing to remove!', + 'SHARING_SUCCESS' => 'Sharing updated!', + 'SHARING_REMOVED' => 'Sharing removed!', + 'USER_CREATED' => 'User created!', + 'USER_DELETED' => 'User deleted!', + 'USER_UPDATED' => 'User updated!', + 'ENTER_EMAIL' => 'Enter your email address:', + 'ERROR_ALBUM_JSON_NOT_FOUND' => 'Error: Album JSON not found!', + 'ERROR_ALBUM_NOT_FOUND' => 'Error: album %s not found', + 'ERROR_DROPBOX_KEY' => 'Error: Dropbox key not set', + 'ERROR_SESSION' => 'Session expired.', + 'CAMERA_DATE' => 'Camera date', + 'NEW_PASSWORD' => 'new password', + 'ALLOW_UPLOADS' => 'Allow uploads', + 'ALLOW_USER_SELF_EDIT' => 'Allow self-management of user account', + 'OSM_CONTRIBUTORS' => 'OpenStreetMap contributors', +]; diff --git a/lang/en/maintenance.php b/lang/en/maintenance.php new file mode 100644 index 00000000000..f54a9f54ca3 --- /dev/null +++ b/lang/en/maintenance.php @@ -0,0 +1,60 @@ + 'Maintenance', + 'description' => 'On this page you will find, all the required actions to keep your Lychee installation running smooth and nicely.', + 'cleaning' => [ + 'title' => 'Cleaning %s', + 'result' => '%s deleted.', + 'description' => 'Remove all contents from %s', + 'button' => 'Clean', + ], + 'fix-jobs' => [ + 'title' => 'Fixing Jobs History', + 'description' => 'Mark jobs with status %s or %s as %s.', + 'button' => 'Fix job history', + ], + 'gen-sizevariants' => [ + 'title' => 'Missing %s', + 'description' => 'Found %d %s that could be generated.', + 'button' => 'Generate!', + 'success' => 'Successfully generated %d %s.', + ], + 'fill-filesize-sizevariants' => [ + 'title' => 'File sizes missing', + 'description' => 'Found %d small variants without file size.', + 'button' => 'Fetch data!', + 'success' => 'Successfully computed sizes of %d small variants.', + ], + 'fix-tree' => [ + 'title' => 'Tree statistics', + 'Oddness' => 'Oddness', + 'Duplicates' => 'Duplicates', + 'Wrong parents' => 'Wrong parents', + 'Missing parents' => 'Missing parents', + 'button' => 'Fix tree', + ], + 'optimize' => [ + 'title' => 'Optimize Database', + 'description' => 'If you notice slowdown in your installation, it may be because your database does not + have all its needed index.', + 'button' => 'Optimize Database', + ], + 'update' => [ + 'title' => 'Updates', + 'check-button' => 'Check for updates', + 'update-button' => 'Update', + 'no-pending-updates' => 'No pending update.', + ], +]; diff --git a/lang/en/profile.php b/lang/en/profile.php new file mode 100644 index 00000000000..cc24b97452c --- /dev/null +++ b/lang/en/profile.php @@ -0,0 +1,64 @@ + 'Profile', + + 'login' => [ + 'header' => 'Profile', + 'enter_current_password' => 'Enter your current password:', + 'current_password' => 'Current password', + 'credentials_update' => 'Your credentials will be changed to the following:', + 'username' => 'Username', + 'new_password' => 'New password', + 'confirm_new_password' => 'Confirm new password', + 'email_instruction' => 'Add your email below to enable receiving email notifications. To stop receiving emails, simply remove your email below.', + 'email' => 'Email', + 'change' => 'Change Login', + 'api_token' => 'API Token ...', + + 'missing_fields' => 'Missing fields', + ], + + 'token' => [ + 'unavailable' => 'You have already viewed this token.', + 'no_data' => 'No token API have been generated.', + 'disable' => 'Disable', + 'disabled' => 'Token disabled', + 'warning' => 'This token will not be displayed again. Copy it and keep it in a safe place.', + 'reset' => 'Reset the token', + 'create' => 'Create a new token', + ], + + 'oauth' => [ + 'header' => 'OAuth', + 'header_not_available' => 'OAuth is not available', + 'setup_env' => 'Set up the credentials in your .env', + 'token_registered' => '%s token registered.', + 'setup' => 'Set up %s', + 'reset' => 'reset', + 'credential_deleted' => 'Credential deleted!', + ], + + 'u2f' => [ + 'header' => 'Passkey/MFA/2FA', + 'info' => 'This only provides the ability to use WebAuthn to authenticate instead of username & password.', + 'empty' => 'Credentials list is empty!', + 'not_secure' => 'Environment not secured. U2F not available.', + 'new' => 'Register new device.', + 'credential_deleted' => 'Credential deleted!', + 'credential_updated' => 'Credential updated!', + 'credential_registred' => 'Registration successful!', + '5_chars' => 'At least 5 chars.', + ], +]; \ No newline at end of file diff --git a/lang/en/settings.php b/lang/en/settings.php new file mode 100644 index 00000000000..fd197f11135 --- /dev/null +++ b/lang/en/settings.php @@ -0,0 +1,92 @@ + 'Settings', + 'small_screen' => 'For better a experience on the Settings page,
we recommend you use a larger screen.', + 'tabs' => [ + 'basic' => 'Basic', + 'all_settings' => 'All settings', + ], + 'toasts' => [ + 'change_saved' => 'Change saved!', + 'details' => 'Settings have been modified as per request', + 'error' => 'Error!', + 'error_load_css' => 'Could not load dist/user.css', + 'error_load_js' => 'Could not load dist/custom.js', + 'error_save_css' => 'Could not save CSS', + 'error_save_js' => 'Could not save JS', + 'thank_you' => 'Thank you for your support.', + 'reload' => 'Reload your page for full functionalities.', + ], + 'system' => [ + 'header' => 'System', + 'use_dark_mode' => 'Use dark mode for Lychee', + 'language' => 'Language used by Lychee', + 'nsfw_album_visibility' => 'Make Sensitive albums visible by default.', + 'nsfw_album_explanation' => 'If the album is public, it is still accessible, just hidden from the view and can be revealed by pressing H.', + ], + 'lychee_se' => [ + 'header' => 'Lychee SE', + 'call4action' => 'Get exclusive features and support the development of Lychee. Unlock the SE edition.', + 'preview' => 'Enable preview of Lychee SE features', + 'hide_call4action' => 'Hide this Lychee SE registration form. I am happy with Lychee as-is. :)', + 'hide_warning' => 'If enabled, the only way to register your license key will be via the More tab above. Changes are applied on page reload.', + ], + 'dropbox' => [ + 'header' => 'Dropbox', + 'instruction' => 'In order to import photos from your Dropbox, you need a valid drop-ins app key from their website.', + 'api_key' => 'Dropbox API Key', + 'set_key' => 'Set Dropbox Key', + ], + 'gallery' => [ + 'header' => 'Gallery', + 'photo_order_column' => 'Default column used for sorting photos', + 'photo_order_direction' => 'Default order used for sorting photos', + 'album_order_column' => 'Default column used for sorting albums', + 'album_order_direction' => 'Default order used for sorting albums', + 'aspect_ratio' => 'Default aspect ratio for album thumbs', + 'photo_layout' => 'Layout for pictures', + 'album_decoration' => 'Show decorations on album cover (sub-album and/or photo count)', + 'album_decoration_direction' => 'Align album decorations horizontally or vertically', + 'photo_overlay' => 'Default image overlay information', + 'license_default' => 'Default license used for albums', + 'license_help' => 'Need help choosing?', + ], + 'geolocation' => [ + 'header' => 'Geo-location', + 'map_display' => 'Display the map given GPS coordinates', + 'map_display_public' => 'Allow anonymous users to access the map', + 'map_provider' => 'Defines the map provider', + 'map_include_subalbums' => 'Includes pictures of the sub albums on the map', + 'location_decoding' => 'Use GPS location decoding', + 'location_show' => 'Show location extracted from GPS coordinates', + 'location_show_public' => 'Anonymous users can access the extracted location from GPS coordinates', + ], + 'advanced' => [ + 'header' => 'Advanced Customization', + 'change_css' => 'Change CSS', + 'change_js' => 'Change JS', + ], + 'all' => [ + 'old_setting_style' => 'Old setting style', + 'change_detected' => 'Some settings changed.', + 'save' => 'Save', + ], + + 'tool_option' => [ + 'disabled' => 'disabled', + 'enabled' => 'enabled', + 'discover' => 'discover', + ], +]; \ No newline at end of file diff --git a/lang/en/sharing.php b/lang/en/sharing.php new file mode 100644 index 00000000000..69de18cc6d0 --- /dev/null +++ b/lang/en/sharing.php @@ -0,0 +1,33 @@ + 'Sharing', + + 'info' => 'This page gives an overview of and the ability to edit the sharing rights associated with albums.', + 'album_title' => 'Album title', + 'username' => 'Username', + 'no_data' => 'Sharing list is empty.', + 'share' => 'Share', + 'permission_deleted' => 'Permission deleted!', + 'permission_created' => 'Permission created!', + + 'grants' => [ + 'read' => 'Grants read access', + 'original' => 'Grants access to original photo', + 'download' => 'Grants download', + 'upload' => 'Grants upload', + 'edit' => 'Grants edit', + 'delete' => 'Grants delete', + ], +]; \ No newline at end of file diff --git a/lang/en/statistics.php b/lang/en/statistics.php new file mode 100644 index 00000000000..2baf855bbd5 --- /dev/null +++ b/lang/en/statistics.php @@ -0,0 +1,34 @@ + 'Statistics', + + 'preview_text' => 'This is a preview of the statistics page available in Lychee SE.
The data shown here are randomly generated and do not reflect your server.', + 'no_data' => 'User does not have data on server.', + 'collapse' => 'Collapse albums sizes', + + 'total' => [ + 'total' => 'Total', + 'albums' => 'Albums', + 'photos' => 'Photos', + 'size' => 'Size', + ], + 'table' => [ + 'username' => 'Owner', + 'title' => 'Title', + 'photos' => 'Photos', + 'descendants' => 'Children', + 'size' => 'Size', + ], +]; \ No newline at end of file diff --git a/lang/en/toasts.php b/lang/en/toasts.php new file mode 100644 index 00000000000..293d4b72594 --- /dev/null +++ b/lang/en/toasts.php @@ -0,0 +1,17 @@ + 'Error', + 'success' => 'Success', +]; \ No newline at end of file diff --git a/lang/en/users.php b/lang/en/users.php new file mode 100644 index 00000000000..599bb833454 --- /dev/null +++ b/lang/en/users.php @@ -0,0 +1,44 @@ + 'Users', + 'description' => 'Here you can manage the users of your Lychee installation. You can create, edit and delete users.', + 'create' => 'Create a new user', + 'username' => 'Username', + 'password' => 'Password', + 'legend' => 'Legend', + 'upload_rights' => 'When selected, the user can upload content.', + 'edit_rights' => 'When selected, the user can modify their profile (username, password).', + 'quota' => 'When set, the user has a space quota for pictures (in kB).', + + 'user_deleted' => 'User deleted', + 'user_created' => 'User created', + 'user_updated' => 'User updated', + 'change_saved' => 'Change saved!', + + 'create_edit' => [ + 'upload_rights' => 'User can upload content.', + 'edit_rights' => 'User can modify their profile (username, password).', + 'quota' => 'User has quota limit.', + 'quota_kb' => 'quota in kB (0 for default)', + 'note' => 'Admin note (not publically visible)', + 'create' => 'Create', + 'edit' => 'Edit', + ], + 'line' => [ + 'admin' => 'admin user', + 'edit' => 'Edit', + 'delete' => 'Delete', + ], +]; \ No newline at end of file diff --git a/lang/es/aspect_ratio.php b/lang/es/aspect_ratio.php new file mode 100644 index 00000000000..2c7e8fb56ac --- /dev/null +++ b/lang/es/aspect_ratio.php @@ -0,0 +1,21 @@ + '5/4 (instagram landscape)', + '4by5' => '4/5 (instagram portrait)', + '2by3' => '2/3 (portrait)', + '3by2' => '3/2 (landscape)', + '1by1' => 'square', + '1byx9' => '16/9 (landscape)', +]; \ No newline at end of file diff --git a/lang/es/diagnostics.php b/lang/es/diagnostics.php new file mode 100644 index 00000000000..0fadd640428 --- /dev/null +++ b/lang/es/diagnostics.php @@ -0,0 +1,30 @@ + 'Diagnostics', + + 'copy_to_clipboard' => 'Copy diagnostics to clipboard', + 'self-diagnosis' => 'Self-diagnosis', + 'info' => 'Info', + 'space' => 'Space', + 'load_space' => 'Load space usage.', + 'configuration' => 'Configuration', + 'loading' => 'Loading...', + 'identical_content' => 'Identical content', + + 'toast' => [ + 'info' => 'Info', + 'copy' => 'Diagnostics copied to clipboard!', + ], +]; \ No newline at end of file diff --git a/lang/es/dialogs.php b/lang/es/dialogs.php new file mode 100644 index 00000000000..4afd65fae3f --- /dev/null +++ b/lang/es/dialogs.php @@ -0,0 +1,221 @@ + [ + 'close' => 'Close', + 'cancel' => 'Cancel', + 'save' => 'Save', + 'delete' => 'Delete', + 'move' => 'Move', + ], + 'about' => [ + 'subtitle' => 'Self-hosted photo-management done right', + 'description' => 'Lychee is a free photo-management tool, which runs on your server or web-space. Installing is a matter of seconds. Upload, manage and share photos like from a native application. Lychee comes with everything you need and all your photos are stored securely.', + 'update_available' => 'Update available!', + 'thank_you' => 'Thank you for your support!', + 'get_supporter_or_register' => 'Get exclusive features and support the development of Lychee.
Unlock the Supporter Edition or register your License key', + 'here' => 'here', + ], + 'dropbox' => [ + 'not_configured' => 'Dropbox is not configured.', + ], + 'import_from_link' => [ + 'instructions' => 'Please enter the direct link to a photo to import it:', + 'import' => 'Import', + ], + 'keybindings' => [ + 'don_t_show_again' => 'Don\'t show this again', + 'side_wide' => 'Site-wide Shortcuts', + 'back_cancel' => 'Back/Cancel', + 'confirm' => 'Confirm', + 'login' => 'Login', + 'toggle_full_screen' => 'Toggle Full Screen', + 'toggle_sensitive_albums' => 'Toggle Sensitive Albums', + + 'albums' => 'Albums Shortcuts', + 'new_album' => 'New Album', + 'upload_photos' => 'Upload Photos', + 'search' => 'Search', + 'show_this_modal' => 'Show this modal', + 'select_all' => 'Select All', + 'move_selection' => 'Move Selection', + 'delete_selection' => 'Delete Selection', + + 'album' => 'Album Shortcuts', + 'slideshow' => 'Start/Stop Slideshow', + 'toggle' => 'Toggle panel', + + 'photo' => 'Photo Shortcuts', + 'previous' => 'Previous photo', + 'next' => 'Next photo', + 'cycle' => 'Cycle overlay mode', + 'star' => 'Star the photo', + 'move' => 'Move the photo', + 'delete' => 'Delete the photo', + 'edit' => 'Edit information', + 'show_hide_meta' => 'Show information', + + 'keep_hidden' => 'We will keep it hidden.', + ], + 'login' => [ + 'username' => 'Username', + 'password' => 'Password', + 'unknown_invalid' => 'Unknown user or invalid password.', + 'signin' => 'Sign-In', + ], + 'register' => [ + 'enter_license' => 'Enter your license key below:', + 'license_key' => 'License key', + 'invalid_license' => 'Invalid license key.', + 'register' => 'Register', + ], + 'share_album' => [ + 'url_copied' => 'Copied URL to clipboard!', + ], + 'upload' => [ + 'completed' => 'Completed', + 'uploaded' => 'Uploaded:', + 'release' => 'Release file to upload!', + 'select' => 'Click here to select files to upload', + 'drag' => '(Or drag files to the page)', + 'loading' => 'Loading', + 'resume' => 'Resume', + 'uploading' => 'Uploading', + 'finished' => 'Finished', + 'failed_error' => 'Upload failed. The server returned an error!', + ], + 'visibility' => [ + 'public' => 'Public', + 'public_expl' => 'Anonymous users can access this album, subject to the restrictions below.', + 'full' => 'Original', + 'full_expl' => 'Anonymous users can view full-resolution photos.', + 'hidden' => 'Hidden', + 'hidden_expl' => 'Anonymous users need a direct link to access this album.', + 'downloadable' => 'Downloadable', + 'downloadable_expl' => 'Anonymous users can download this album.', + 'password' => 'Password', + 'password_prot' => 'Password protected', + 'password_prot_expl' => 'Anonymous users need a shared password to access this album.', + 'nsfw' => 'Sensitive', + 'nsfw_expl' => 'Album contains sensitive content.', + 'visibility_updated' => 'Visibility updated.', + ], + 'move_album' => [ + 'confirm_single' => 'Are you sure you want to move the album “%1$s” into the album “%2$s”?', + 'confirm_multiple' => 'Are you sure you want to move all selected albums into the album “%s”?', + 'move_single' => 'Move Album', + 'move_to' => 'Move to', + 'move_to_single' => 'Move %s to:', + 'move_to_multiple' => 'Move %d albums to:', + 'no_album_target' => 'No album to move to', + 'moved_single' => 'Album moved!', + 'moved_single_details' => '%1$s moved to %2$s', + 'moved_details' => 'Album(s) moved to %s', + ], + 'new_album' => [ + 'menu' => 'Create Album', + 'info' => 'Enter a title for the new album:', + 'title' => 'title', + 'create' => 'Create Album', + ], + 'new_tag_album' => [ + 'menu' => 'Create Tag Album', + 'info' => 'Enter a title for the new tag album:', + 'title' => 'title', + 'set_tags' => 'Set tags to show', + 'warn' => 'Make sure to press enter after each tag', + 'create' => 'Create Tag Album', + ], + 'delete_album' => [ + 'confirmation' => 'Are you sure you want to delete the album “%s” and all of the photos it contains?', + 'confirmation_multiple' => 'Are you sure you want to delete all %d selected albums and all of the photos they contain?', + 'warning' => 'This action can not be undone!', + 'delete' => 'Delete Album and Photos', + ], + 'transfer' => [ + 'query' => 'Transfer ownership of album to', + 'confirmation' => 'Are you sure you want to transfer the ownership of album “%s” and all the photos it contains to "%s"?', + 'lost_access_warning' => 'Your access to this album will be lost.', + 'warning' => 'This action can not be undone!', + 'transfer' => 'Transfer ownership of album and photos', + ], + 'rename' => [ + 'photo' => 'Enter a new title for this photo:', + 'album' => 'Enter a new title for this album:', + 'rename' => 'Rename', + ], + 'merge' => [ + 'merge_to' => 'Merge %s to:', + 'merge_to_multiple' => 'Merge %d albums to:', + 'no_albums' => 'No albums to merge to.', + 'confirm' => 'Are you sure you want to merge the album “%1$s” into the album “%2$s”?', + 'confirm_multiple' => 'Are you sure you want to merge all selected albums into the album “%s”?', + 'merge' => 'Merge Albums', + 'merged' => 'Album(s) merged to %s!', + ], + 'unlock' => [ + 'password_required' => 'This album is protected by a password. Enter the password below to view the photos of this album:', + 'password' => 'Password', + 'unlock' => 'Unlock', + ], + 'photo_tags' => [ + 'question' => 'Enter your tags for this photo.', + 'question_multiple' => 'Enter your tags for all %d selected photos. Existing tags will be overwritten.', + 'no_tags' => 'No Tags', + 'set_tags' => 'Set Tags', + 'updated' => 'Tags updated!', + 'tags_override_info' => 'If this is unchecked, the tags will be added to the existing tags of the photo.', + ], + 'photo_copy' => [ + 'no_albums' => 'No albums to copy to', + 'copy_to' => 'Copy %s to:', + 'copy_to_multiple' => 'Copy %d photos to:', + 'confirm' => 'Copy %s to %s.', + 'confirm_multiple' => 'Copy %d photos to %s.', + 'copy' => 'Copy', + 'copied' => 'Photo(s) copied!', + ], + 'photo_delete' => [ + 'confirm' => 'Are you sure you want to delete the photo “%s”?', + 'confirm_multiple' => 'Are you sure you want to delete all %d selected photos?', + 'deleted' => 'Photo(s) deleted!', + ], + 'move_photo' => [ + 'move_single' => 'Move %s to:', + 'move_multiple' => 'Move %d photos to:', + 'confirm' => 'Move %s to %s.', + 'confirm_multiple' => 'Move %d photos to %s.', + 'moved' => 'Photo(s) moved to %s!', + ], + 'target_user' => [ + 'placeholder' => 'Select user', + ], + 'target_album' => [ + 'placeholder' => 'Select album', + ], + 'webauthn' => [ + 'u2f' => 'U2F', + 'success' => 'Authentication successful!', + 'error' => 'Whoops, it looks like something went wrong. Please reload the site and try again!', + ], + 'se' => [ + 'available' => 'Available in the Supporter Edition', + ], + 'session_expired' => [ + 'title' => 'Session expired', + 'message' => 'Your session has expired.
Please reload the page.', + 'reload' => 'Reload', + 'go_to_gallery' => 'Go to the Gallery', + ], +]; \ No newline at end of file diff --git a/lang/es/fix-tree.php b/lang/es/fix-tree.php new file mode 100644 index 00000000000..64803e310e6 --- /dev/null +++ b/lang/es/fix-tree.php @@ -0,0 +1,55 @@ + 'Maintenance', + 'intro' => 'This page allows you to re-order and fix your albums manually.
Before any modifications, we strongly recommend you to read about Nested Set tree structures.', + 'warning' => 'You can really break your Lychee installation here, modify values at your own risks.', + + 'help' => [ + 'header' => 'Help', + 'hover' => 'Hover ids or titles to highlight related albums.', + 'left' => 'Left', + 'right' => 'Right', + 'convenience' => 'For your convenience, the and buttons allow you to change the values of %s and %s by respectively +1 and -1 with propagation.', + 'left-right-warn' => 'The and indicates that the value of %s (and respectively %s) is duplicated somewhere.', + 'parent-marked' => 'Marked Parent Id indicates that the %s and %s do not satisfy the Nest Set tree structures. Edit either the Parent Id or the %s/%s values.', + 'slowness' => 'This page will be slow with a large number of albums.', + ], + + 'buttons' => [ + 'reset' => 'Reset', + 'check' => 'Check', + 'apply' => 'Apply', + ], + + 'table' => [ + 'title' => 'Title', + 'left' => 'Left', + 'right' => 'Right', + 'id' => 'Id', + 'parent' => 'Parent Id', + ], + + 'errors' => [ + 'invalid' => 'Invalid tree!', + 'invalid_details' => 'We are not applying this as it is guaranteed to be a broken state.', + 'invalid_left' => 'Album %s has an invalid left value.', + 'invalid_right' => 'Album %s has an invalid right value.', + 'invalid_left_right' => 'Album %s has an invalid left/right values. Left should be strictly smaller than right: %s < %s.', + 'duplicate_left' => 'Album %s has a duplicate left value %s.', + 'duplicate_right' => 'Album %s has a duplicate right value %s.', + 'parent' => 'Album %s has an unexpected parent id %s.', + 'unknown' => 'Album %s has an unknown error.', + ], +]; \ No newline at end of file diff --git a/lang/es/gallery.php b/lang/es/gallery.php new file mode 100644 index 00000000000..eb8008827e0 --- /dev/null +++ b/lang/es/gallery.php @@ -0,0 +1,241 @@ + 'Gallery', + + 'smart_albums' => 'Smart albums', + 'albums' => 'Albums', + 'root' => 'Albums', + + 'original' => 'Original', + 'medium' => 'Medium', + 'medium_hidpi' => 'Medium HiDPI', + 'small' => 'Thumb', + 'small_hidpi' => 'Thumb HiDPI', + 'thumb' => 'Square thumb', + 'thumb_hidpi' => 'Square thumb HiDPI', + 'placeholder' => 'Low Quality Image Placeholder', + 'thumbnail' => 'Photo thumbnail', + 'live_video' => 'Video part of live-photo', + + 'camera_data' => 'Camera date', + 'album_reserved' => 'All Rights Reserved', + + 'map' => [ + 'error_gpx' => 'Error loading GPX file', + 'osm_contributors' => 'OpenStreetMap contributors', + ], + + 'search' => [ + 'title' => 'Search', + 'searching' => 'Searching…', + 'no_results' => 'Nothing matches your search query.', + 'searchbox' => 'Search…', + 'minimum_chars' => 'Minimum %s characters required.', + 'photos' => 'Photos (%s)', + 'albums' => 'Albums (%s)', + ], + + 'smart_album' => [ + 'unsorted' => 'Unsorted', + 'starred' => 'Starred', + 'recent' => 'Recent', + 'public' => 'Public', + 'on_this_day' => 'On This Day', + ], + + 'layout' => [ + 'squares' => 'Square thumbnails', + 'justified' => 'With aspect, justified', + 'masonry' => 'With aspect, masonry', + 'grid' => 'With aspect, grid', + ], + + 'overlay' => [ + 'none' => 'None', + 'exif' => 'EXIF data', + 'description' => 'Description', + 'date' => 'Date taken', + ], + + 'timeline' => [ + 'default' => 'default', + 'disabled' => 'disabled', + 'year' => 'Year', + 'month' => 'Month', + 'day' => 'Day', + 'hour' => 'Hour', + ], + + 'album' => [ + 'header_albums' => 'Albums', + 'header_photos' => 'Photos', + 'no_results' => 'Nothing to see here', + 'upload' => 'Upload photos', + + 'tabs' => [ + 'about' => 'About Album', + 'share' => 'Share Album', + 'move' => 'Move Album', + 'danger' => 'DANGER ZONE', + ], + + 'hero' => [ + 'created' => 'Created', + 'copyright' => 'Copyright', + 'subalbums' => 'Subalbums', + 'images' => 'Photos', + 'download' => 'Download Album', + 'share' => 'Share Album', + 'stats_only_se' => 'Statistics available in the Supporter Edition', + ], + + 'stats' => [ + 'lens' => 'Lens', + 'shutter' => 'Shutter speed', + 'iso' => 'ISO', + 'model' => 'Model', + 'aperture' => 'Aperture', + 'no_data' => 'No data', + ], + + 'properties' => [ + 'title' => 'Title', + 'description' => 'Description', + 'photo_ordering' => 'Order photos by', + 'children_ordering' => 'Order albums by', + 'asc/desc' => 'asc/desc', + 'header' => 'Set album header', + 'compact_header' => 'Use compact header', + 'license' => 'Set license', + 'copyright' => 'Set copyright', + 'aspect_ratio' => 'Set album thumbs aspect ratio', + 'album_timeline' => 'Set album timeline mode', + 'photo_timeline' => 'Set photo timeline mode', + 'layout' => 'Set photo layout', + 'show_tags' => 'Set tags to show', + 'tags_required' => 'Tags are required.', + ], + ], + + 'photo' => [ + 'actions' => [ + 'star' => 'Star', + 'unstar' => 'Unstar', + 'set_album_header' => 'Set as album header', + 'move' => 'Move', + 'delete' => 'Delete', + 'header_set' => 'Header set', + ], + + 'details' => [ + 'about' => 'About', + 'basics' => 'Basics', + 'title' => 'Title', + 'uploaded' => 'Uploaded', + 'description' => 'Description', + 'license' => 'License', + 'reuse' => 'Reuse', + 'latitude' => 'Latitude', + 'longitude' => 'Longitude', + 'altitude' => 'Altitude', + 'location' => 'Location', + 'image' => 'Image', + 'video' => 'Video', + 'size' => 'Size', + 'format' => 'Format', + 'resolution' => 'Resolution', + 'duration' => 'Duration', + 'fps' => 'Frame rate', + 'tags' => 'Tags', + 'camera' => 'Camera', + 'captured' => 'Captured', + 'make' => 'Make', + 'type' => 'Type/Model', + 'lens' => 'Lens', + 'shutter' => 'Shutter Speed', + 'aperture' => 'Aperture', + 'focal' => 'Focal Length', + 'iso' => 'ISO %s', + ], + + 'edit' => [ + 'set_title' => 'Set Title', + 'set_description' => 'Set Description', + 'set_license' => 'Set License', + 'no_tags' => 'No Tags', + 'set_tags' => 'Set Tags', + 'set_created_at' => 'Set Upload Date', + ], + ], + + 'nsfw' => [ + 'header' => 'Sensitive content', + 'description' => 'This album contains sensitive content which some people may find offensive or disturbing.', + 'consent' => 'Tap to consent.', + ], + + 'menus' => [ + 'star' => 'Star', + 'unstar' => 'Unstar', + 'star_all' => 'Star Selected', + 'unstar_all' => 'Unstar Selected', + 'tag' => 'Tag', + 'tag_all' => 'Tag Selected', + 'set_cover' => 'Set Album Cover', + 'remove_header' => 'Remove Album Header', + 'set_header' => 'Set Album Header', + 'copy_to' => 'Copy to …', + 'copy_all_to' => 'Copy Selected to …', + 'rename' => 'Rename', + 'move' => 'Move', + 'move_all' => 'Move Selected', + 'delete' => 'Delete', + 'delete_all' => 'Delete Selected', + 'download' => 'Download', + 'download_all' => 'Download Selected', + 'merge' => 'Merge', + 'merge_all' => 'Merge Selected', + + 'upload_photo' => 'Upload Photo', + 'import_link' => 'Import from Link', + 'import_dropbox' => 'Import from Dropbox', + 'new_album' => 'New Album', + 'new_tag_album' => 'New Tag Album', + 'upload_track' => 'Upload track', + 'delete_track' => 'Delete track', + ], + + 'sort' => [ + 'photo_select_1' => 'Upload Time', + 'photo_select_2' => 'Take Date', + 'photo_select_3' => 'Title', + 'photo_select_4' => 'Description', + 'photo_select_6' => 'Star', + 'photo_select_7' => 'Photo Format', + 'ascending' => 'Ascending', + 'descending' => 'Descending', + 'album_select_1' => 'Creation Time', + 'album_select_2' => 'Title', + 'album_select_3' => 'Description', + 'album_select_5' => 'Latest Take Date', + 'album_select_6' => 'Oldest Take Date', + ], + + 'albums_protection' => [ + 'private' => 'private', + 'public' => 'public', + 'inherit_from_parent' => 'inherit from parent', + ], +]; \ No newline at end of file diff --git a/lang/es/jobs.php b/lang/es/jobs.php new file mode 100644 index 00000000000..5d952b76012 --- /dev/null +++ b/lang/es/jobs.php @@ -0,0 +1,18 @@ + 'Jobs', + + 'no_data' => 'No Jobs have been executed yet.', +]; \ No newline at end of file diff --git a/lang/es/landing.php b/lang/es/landing.php new file mode 100644 index 00000000000..fe6fe55b8ea --- /dev/null +++ b/lang/es/landing.php @@ -0,0 +1,19 @@ + 'Gallery', + 'access_gallery' => 'Access the gallery', + 'hosted_with_lychee' => 'Hosted with Lychee', + 'copyright' => 'All images on this website are subject to copyright by %1$s © %2$s', +]; \ No newline at end of file diff --git a/lang/es/left-menu.php b/lang/es/left-menu.php new file mode 100644 index 00000000000..9a3e91f4037 --- /dev/null +++ b/lang/es/left-menu.php @@ -0,0 +1,29 @@ + 'Back to Gallery', + + 'admin' => 'Admin', + 'clockwork' => 'Clockwork App', + 'logs' => 'Show Logs', + 'jobs' => 'Show Job History', + 'user' => 'User', + + 'sign_out' => 'Sign Out', + + 'about' => 'About', + 'api' => 'API Documentation', + 'source_code' => 'Source Code', + 'support' => 'Support', +]; \ No newline at end of file diff --git a/lang/es/lychee.php b/lang/es/lychee.php new file mode 100644 index 00000000000..7d271086727 --- /dev/null +++ b/lang/es/lychee.php @@ -0,0 +1,535 @@ + 'Nombre de usuario', + 'PASSWORD' => 'Contraseña', + 'ENTER' => 'Entrar', + 'CANCEL' => 'Cancelar', + 'CONFIRM' => 'Confirm', + 'SIGN_IN' => 'Iniciar sesión', + 'CLOSE' => 'Cerrar', + 'SETTINGS' => 'Configuraciones', + 'SEARCH' => 'Buscar …', + 'MORE' => 'Más', + 'DEFAULT' => 'Por Defecto', + 'GALLERY' => 'Galería', + + 'USERS' => 'Usuarios', + 'PROFILE' => 'Profile', + 'CREATE' => 'Crear', + 'REMOVE' => 'Remove', + 'SHARE' => 'Share', + 'U2F' => 'U2F', + 'NOTIFICATIONS' => 'Notificaciones', + 'SHARING' => 'Compartir', + 'CHANGE_LOGIN' => 'Cambiar inicio de sesión', + 'CHANGE_SORTING' => 'Cambiar clasificación', + 'SET_DROPBOX' => 'Establecer Dropbox', + 'ABOUT_LYCHEE' => 'Acerca de Lychee', + 'DIAGNOSTICS' => 'Diagnóstico', + 'DIAGNOSTICS_GET_SIZE' => 'Pedir uso de espacio', + 'JOBS' => 'Show job history', + 'LOGS' => 'Mostrar Registros', + 'SIGN_OUT' => 'Cerrar Sesión', + 'UPDATE_AVAILABLE' => '¡Actualización disponible!', + 'MIGRATION_AVAILABLE' => 'Migración disponible!', + 'CHECK_FOR_UPDATE' => 'Check for updates', + 'DEFAULT_LICENSE' => 'Licencia predeterminada para nuevas cargas:', + 'SET_LICENSE' => 'Establecer Licencia', + 'SET_OVERLAY_TYPE' => 'Establecer Superposición', + 'SET_ALBUM_DECORATION' => 'Set album decorations', + 'SET_MAP_PROVIDER' => 'Establecer proveedor de capas de OpenStreetMap', + 'FULL_SETTINGS' => 'Configuración completa', + 'UPDATE' => 'Actualizar', + 'RESET' => 'Resetear', + 'DISABLE_TOKEN_TOOLTIP' => 'Deshabilitar', + 'ENABLE_TOKEN' => 'Habilitar token de API', + 'DISABLED_TOKEN_STATUS_MSG' => 'Deshabilitado', + 'TOKEN_BUTTON' => 'Token de API ...', + 'TOKEN_NOT_AVAILABLE' => 'Ya has visto este token.', + 'TOKEN_WAIT' => 'Espera ...', + + 'SMART_ALBUMS' => 'Álbumes inteligentes', + 'SHARED_ALBUMS' => 'Álbumes compartidos', + 'ALBUMS' => 'Álbumes', + 'PHOTOS' => 'Imágenes', + 'SEARCH_RESULTS' => 'Resultados de la búsqueda', + + 'RENAME' => 'Renombrar', + 'RENAME_ALL' => 'Renombrar Todo', + 'MERGE' => 'Unir', + 'MERGE_ALL' => 'Unir Todo', + 'MAKE_PUBLIC' => 'Hacer Público', + 'SHARE_ALBUM' => 'Compartir Álbum', + 'SHARE_PHOTO' => 'Compartir Foto', + 'VISIBILITY_ALBUM' => 'Visibilidad del Álbum', + 'VISIBILITY_PHOTO' => 'Visibilidad de la Foto', + 'DOWNLOAD_ALBUM' => 'Descargar Álbum', + 'ABOUT_ALBUM' => 'Acerca del Álbum', + 'DELETE_ALBUM' => 'Eliminar Álbum', + 'MOVE_ALBUM' => 'Mover Álbum', + 'FULLSCREEN_ENTER' => 'Ingreser a pantalla completa', + 'FULLSCREEN_EXIT' => 'Salir de pantalla completa', + + 'SHARING_ALBUM_USERS' => 'Compartir este álbum con usuarios', + 'WAIT_FETCH_DATA' => 'Espere mientras obtenemos los datos …', + 'SHARING_ALBUM_USERS_NO_USERS' => 'No hay usuarios con los que compartir el álbum', + 'SHARING_ALBUM_USERS_LONG_MESSAGE' => 'Seleccione los usuarios con los que compartir este álbum', + + 'DELETE_ALBUM_QUESTION' => 'Eliminar Álbum y Fotos', + 'KEEP_ALBUM' => 'Mantener Álbum', + 'DELETE_ALBUM_CONFIRMATION' => '¿Estás seguro de que deseas eliminar el álbum «%s»? ¿Ha seleccionado un álbum y todas las fotos que contiene? ¡Esta acción no se puede deshacer!', + + 'DELETE_TAG_ALBUM_QUESTION' => 'Eliminar Álbum', + 'DELETE_TAG_ALBUM_CONFIRMATION' => '¿Estás seguro de que deseas eliminar el álbum «%s» (cualquier foto dentro no será eliminada)? ¡Esta acción no se puede deshacer!', + + 'DELETE_ALBUMS_QUESTION' => 'Eliminar Álbumes y Fotos', + 'KEEP_ALBUMS' => 'Mantener Álbumes', + 'DELETE_ALBUMS_CONFIRMATION' => '¿Está seguro de que desea eliminar todo %d? Ha seleccionado álbumes y todas las fotos que contienen ¡Esta acción no se puede deshacer!', + + 'DELETE_UNSORTED_CONFIRM' => '¿Estás seguro de que deseas eliminar todas las fotos de «Sin clasificar»? ¡Esta acción no se puede deshacer!', + 'CLEAR_UNSORTED' => 'Borrar «Sin Clasificar»', + 'KEEP_UNSORTED' => 'Mantener «Sin Clasificar»', + + 'EDIT_SHARING' => 'Editar Compartido', + 'MAKE_PRIVATE' => 'Hazlo Privado', + + 'CLOSE_ALBUM' => 'Cerrar Álbum', + 'CLOSE_PHOTO' => 'Cerrar Foto', + 'CLOSE_MAP' => 'Cerrar Mapa', + + 'ADD' => 'Añadir', + 'MOVE' => 'Mover', + 'MOVE_ALL' => 'Mover Todo', + 'DUPLICATE' => 'Duplicar', + 'DUPLICATE_ALL' => 'Duplicar Todo', + 'COPY_TO' => 'Copiar a …', + 'COPY_ALL_TO' => 'Copiar Todo a …', + 'DELETE' => 'Eliminar', + 'SAVE' => 'Guardar', + 'DELETE_ALL' => 'Eliminar Todos', + 'DOWNLOAD' => 'Descargar', + 'DOWNLOAD_ALL' => 'Descargar Seleccionados', + 'UPLOAD_PHOTO' => 'Subir Foto', + 'IMPORT_LINK' => 'Importar desde Enlace', + 'IMPORT_DROPBOX' => 'Importar desde Dropbox', + 'IMPORT_SERVER' => 'Importar desde Servidor', + 'NEW_ALBUM' => 'Nuevo Álbum', + 'NEW_TAG_ALBUM' => 'Nuevo Album de Etiquetas', + 'UPLOAD_TRACK' => 'Subir pista', + 'DELETE_TRACK' => 'Borrar pista', + + 'TITLE_NEW_ALBUM' => 'Ingrese un título para el nuevo álbum:', + 'UNTITLED' => 'Sin Título', + 'UNSORTED' => 'Sin Clasificar', + 'STARRED' => 'Destacado', + 'RECENT' => 'Reciente', + 'PUBLIC' => 'Público', + 'ON_THIS_DAY' => 'On This Day', + 'NUM_PHOTOS' => 'Fotos', + + 'CREATE_ALBUM' => 'Crear Álbum', + 'CREATE_TAG_ALBUM' => 'Crear Album de Etiquetas', + + 'STAR_PHOTO' => 'Destacar Photo', + 'STAR' => 'Destacar', + 'UNSTAR' => 'No Destacar', + 'STAR_ALL' => 'Destacar Todo', + 'UNSTAR_ALL' => 'No Destacar Seleccionados', + 'TAG' => 'Etiquetar', + 'TAG_ALL' => 'Etiquetar Todo', + 'UNSTAR_PHOTO' => 'Desetiquetar Foto', + 'SET_COVER' => 'Establecer portada del álbum', + 'REMOVE_COVER' => 'Eliminar portada del álbum', + 'SET_HEADER' => 'Set Album Header', + 'REMOVE_HEADER' => 'Remove Album Header', + 'SET_COMPACT_HEADER' => 'Use Compact Header', + + 'FULL_PHOTO' => 'Foto Completa', + 'ABOUT_PHOTO' => 'Acerca de la Foto', + 'DISPLAY_FULL_MAP' => 'Mapa', + 'DIRECT_LINK' => 'Enlace Directo', + 'DIRECT_LINKS' => 'Enlaces Directos', + 'QR_CODE' => 'Código QR', + + 'ALBUM_ABOUT' => 'Acerca de', + 'ALBUM_BASICS' => 'Básico', + 'ALBUM_TITLE' => 'Título', + 'ALBUM_COPYRIGHT' => 'Copyright', + 'ALBUM_SET_COPYRIGHT' => 'Set copyright', + 'ALBUM_NEW_TITLE' => 'Ingrese un nuevo título para este álbum:', + 'ALBUMS_NEW_TITLE' => 'Ingrese un título para todos %d álbumes seleccionados:', + 'ALBUM_SET_TITLE' => 'Establecer Título', + 'ALBUM_DESCRIPTION' => 'Descripción', + 'ALBUM_SHOW_TAGS' => 'Etiquetas para mostrar', + 'ALBUM_NEW_DESCRIPTION' => 'Ingrese una nueva descripción para este álbum:', + 'ALBUM_SET_DESCRIPTION' => 'Establecer Descripción', + 'ALBUM_NEW_SHOWTAGS' => 'Ingrese las etiquetas de las fotos que serán visibles en este álbum:', + 'ALBUM_SET_SHOWTAGS' => 'Establecer Etiquetas para mostrar', + 'ALBUM_ALBUM' => 'Álbum', + 'ALBUM_CREATED' => 'Creado', + 'ALBUM_IMAGES' => 'Imágenes', + 'ALBUM_VIDEOS' => 'Videos', + 'ALBUM_SUBALBUMS' => 'Subalbums', + 'ALBUM_SHARING' => 'Compartir', + 'ALBUM_SHR_YES' => 'SI', + 'ALBUM_SHR_NO' => 'No', + 'ALBUM_PUBLIC' => 'Público', + 'ALBUM_PUBLIC_EXPL' => 'Anonymous users can access this album, subject to the restrictions below.', + 'ALBUM_FULL' => 'Original', + 'ALBUM_FULL_EXPL' => 'Anonymous users can behold full-resolution photos.', + 'ALBUM_HIDDEN' => 'Oculto', + 'ALBUM_HIDDEN_EXPL' => 'Anonymous users need a direct link to access this album.', + 'ALBUM_MARK_NSFW' => 'Marcar álbum como sensible', + 'ALBUM_UNMARK_NSFW' => 'Desmarcar álbum como sensible', + 'ALBUM_NSFW' => 'Sensible', + 'ALBUM_NSFW_EXPL' => 'El álbum está marcado para contener contenido confidencial.', + 'ALBUM_DOWNLOADABLE' => 'Descargable', + 'ALBUM_DOWNLOADABLE_EXPL' => 'Anonymous users can download this album.', + 'ALBUM_SHARE_BUTTON_VISIBLE' => 'El botón Compartir está visible', + 'ALBUM_SHARE_BUTTON_VISIBLE_EXPL' => 'Anonymous users can see social media sharing links.', + 'ALBUM_PASSWORD' => 'Contraseña', + 'ALBUM_PASSWORD_PROT' => 'Contraseña protegida', + 'ALBUM_PASSWORD_PROT_EXPL' => 'Anonymous users need a shared password to access this album.', + 'ALBUM_PASSWORD_REQUIRED' => 'Este álbum está protegido por una contraseña. Ingrese la contraseña a continuación para ver las fotos de este álbum:', + 'ALBUM_MERGE' => '¿Estás seguro de que quieres fusionar el álbum «%1$s» en el álbum «%2$s»?', + 'ALBUMS_MERGE' => '¿Está seguro de que desea fusionar todos los álbumes seleccionados en el álbum «%s»?', + 'MERGE_ALBUM' => 'Fusionar álbumes', + 'DONT_MERGE' => 'No combinar', + 'ALBUM_MOVE' => '¿Estás seguro de que quieres mover el álbum «%1$s» detro del álbum «%2$s»?', + 'ALBUMS_MOVE' => '¿Está seguro de que desea mover todos los álbumes seleccionados al álbum «%s»?', + 'MOVE_ALBUMS' => 'Mover álbumes', + 'NOT_MOVE_ALBUMS' => 'No mover', + 'ROOT' => 'Inicio', + 'ALBUM_REUSE' => 'Reutilizar', + 'ALBUM_LICENSE' => 'Licencia', + 'ALBUM_SET_LICENSE' => 'Establecer licencia', + 'ALBUM_LICENSE_HELP' => '¿Necesitas ayuda para elegir?', + 'ALBUM_LICENSE_NONE' => 'Ninguna', + 'ALBUM_RESERVED' => 'Todos los derechos reservados', + 'ALBUM_SET_ORDER' => 'Establecer orden', + 'ALBUM_ORDERING' => 'Ordenar por', + 'ALBUM_PHOTO_ORDERING' => 'Order photos by', + 'ALBUM_CHILDREN_ORDERING' => 'Order albums by', + 'ALBUM_OWNER' => 'Dueño', + + 'PHOTO_ABOUT' => 'Acerca de', + 'PHOTO_BASICS' => 'Básico', + 'PHOTO_TITLE' => 'Título', + 'PHOTO_NEW_TITLE' => 'Ingrese un nuevo título para esta foto:', + 'PHOTO_SET_TITLE' => 'Establecer título', + 'PHOTO_UPLOADED' => 'Subido', + 'PHOTO_DESCRIPTION' => 'Descripción', + 'PHOTO_NEW_DESCRIPTION' => 'Ingrese una nueva descripción para esta foto:', + 'PHOTO_SET_DESCRIPTION' => 'Establecer descripción', + 'PHOTO_NEW_LICENSE' => 'Agregar una licencia', + 'PHOTO_SET_LICENSE' => 'Establecer licencia', + 'PHOTO_LICENSE' => 'Licencia', + 'PHOTO_LICENSE_HELP' => '¿Necesitas ayuda para elegir?', + 'PHOTO_REUSE' => 'Reutilizar', + 'PHOTO_LICENSE_NONE' => 'Ninguna', + 'PHOTO_RESERVED' => 'Todos los derechos reservados', + 'PHOTO_LATITUDE' => 'Latitud', + 'PHOTO_LONGITUDE' => 'Longitud', + 'PHOTO_ALTITUDE' => 'Altitud', + 'PHOTO_IMGDIRECTION' => 'Dirección', + 'PHOTO_LOCATION' => 'Ubicación', + 'PHOTO_IMAGE' => 'Imagen', + 'PHOTO_VIDEO' => 'Vídeo', + 'PHOTO_SIZE' => 'Tamaño', + 'PHOTO_FORMAT' => 'Formato', + 'PHOTO_RESOLUTION' => 'Resolución', + 'PHOTO_DURATION' => 'Duración', + 'PHOTO_FPS' => 'Cuadros por segundo', + 'PHOTO_TAGS' => 'Etiquetas', + 'PHOTO_NOTAGS' => 'Sin etiquetas', + 'PHOTO_NEW_TAGS' => 'Ingrese sus etiquetas para esta foto. Puede agregar varias etiquetas separándolas con una coma:', + 'PHOTOS_NEW_TAGS' => 'Ingrese sus etiquetas para todos %d fotos seleccionadas. Las etiquetas existentes se sobrescribirán. Puede agregar varias etiquetas separándolas con una coma:', + 'PHOTO_SET_TAGS' => 'Establecer etiquetas', + 'PHOTO_CAMERA' => 'Cámara', + 'PHOTO_CAPTURED' => 'Capturado', + 'PHOTO_MAKE' => 'Hacer', + 'PHOTO_TYPE' => 'Tipo / Modelo', + 'PHOTO_LENS' => 'Lens', + 'PHOTO_SHUTTER' => 'Velocidad de obturación', + 'PHOTO_APERTURE' => 'Abertura', + 'PHOTO_FOCAL' => 'Longitud focal', + 'PHOTO_ISO' => 'ISO %s', + 'PHOTO_SHARING' => 'Compartir', + 'PHOTO_DELETE' => 'Borrar Foto', + 'PHOTO_KEEP' => 'Mantener Foto', + 'PHOTO_DELETE_CONFIRMATION' => '¿Estás seguro de que deseas eliminar la foto «%s»? ¡Esta acción no se puede deshacer!', + 'PHOTO_DELETE_ALL' => '¿Está seguro de que desea eliminar todo %d foto seleccionada? ¡Esta acción no se puede deshacer!', + 'PHOTOS_NEW_TITLE' => 'Ingrese un título para todos %d fotos seleccionadas:', + 'PHOTO_MAKE_PRIVATE_ALBUM' => 'Esta foto se encuentra en un álbum público. Para que esta foto sea privada o pública, edite la visibilidad del álbum asociado.', + 'PHOTO_SHOW_ALBUM' => 'Mostrar álbum', + 'PHOTO_PUBLIC' => 'Público', + 'PHOTO_PUBLIC_EXPL' => 'Anonymous users can view this photo, subject to the restrictions below.', + 'PHOTO_FULL' => 'Original', + 'PHOTO_FULL_EXPL' => 'Anonymous users can behold full-resolution photo.', + 'PHOTO_HIDDEN' => 'Oculto', + 'PHOTO_HIDDEN_EXPL' => 'Anonymous users need a direct link to view this photo.', + 'PHOTO_DOWNLOADABLE' => 'Descargable', + 'PHOTO_DOWNLOADABLE_EXPL' => 'Anonymous users may download this photo.', + 'PHOTO_SHARE_BUTTON_VISIBLE' => 'El botón Compartir está visible', + 'PHOTO_SHARE_BUTTON_VISIBLE_EXPL' => 'Anonymous users can see social media sharing links.', + 'PHOTO_PASSWORD_PROT' => 'Contraseña protegida', + 'PHOTO_PASSWORD_PROT_EXPL' => 'Anonymous users need a shared password to view this photo.', + 'PHOTO_EDIT_SHARING_TEXT' => 'Las propiedades para compartir de esta foto se cambiarán a lo siguiente:', + 'PHOTO_NO_EDIT_SHARING_TEXT' => 'Debido a que esta foto se encuentra en un álbum público, hereda la configuración de visibilidad de ese álbum. Su visibilidad actual se muestra a continuación solo con fines informativos.', + 'PHOTO_EDIT_GLOBAL_SHARING_TEXT' => 'La visibilidad de esta foto se puede ajustar utilizando la configuración global de Lychee. Su visibilidad actual se muestra a continuación solo con fines informativos.', + 'PHOTO_NEW_CREATED_AT' => 'Enter the upload date for this photo. mm/dd/yyyy, hh:mm [am/pm]', + 'PHOTO_SET_CREATED_AT' => 'Set upload date', + + 'LOADING' => 'Cargando', + 'ERROR' => 'Error', + 'ERROR_TEXT' => 'Vaya, parece que algo salió mal. ¡Vuelva a cargar el sitio e intente nuevamente!', + 'ERROR_UNKNOWN' => 'Algo inesperado sucedió. Intente nuevamente y verifique su instalación y servidor. Eche un vistazo al archivo Léame para obtener más información.', + 'ERROR_MAP_DEACTIVATED' => 'La funcionalidad del mapa se ha desactivado en la configuración.', + 'ERROR_SEARCH_DEACTIVATED' => 'La función de búsqueda se ha desactivado en la configuración.', + 'SUCCESS' => 'Vale', + 'CHANGE_SUCCESS' => 'Change successful.', + 'RETRY' => 'Procesar de nuevo', + 'OVERRIDE' => 'Override', + 'TAGS_OVERRIDE_INFO' => 'If this is unchecked, the tags will be added to the existing tags of the photo.', + + 'SETTINGS_SUCCESS_LOGIN' => 'Información de inicio de sesión actualizada', + 'SETTINGS_SUCCESS_SORT' => 'Orden de clasificación actualizado', + 'SETTINGS_SUCCESS_DROPBOX' => 'Clave Dropbox actualizada', + 'SETTINGS_SUCCESS_LANG' => 'Idioma actualizado', + 'SETTINGS_SUCCESS_LAYOUT' => 'Diseño actualizado', + 'SETTINGS_SUCCESS_IMAGE_OVERLAY' => 'Configuración de superposición EXIF actualizada', + 'SETTINGS_SUCCESS_PUBLIC_SEARCH' => 'Búsqueda pública actualizada', + 'SETTINGS_SUCCESS_LICENSE' => 'Licencia predeterminada actualizada', + 'SETTINGS_SUCCESS_MAP_DISPLAY' => 'Configuración de visualización del mapa actualizada', + 'SETTINGS_SUCCESS_MAP_DISPLAY_PUBLIC' => 'Configuración de visualización de mapa para álbumes públicos actualizada', + 'SETTINGS_SUCCESS_MAP_PROVIDER' => 'Configuración del proveedor de mapas actualizada', + 'SETTINGS_SUCCESS_CSS' => 'Stylesheets updated', + 'SETTINGS_SUCCESS_JS' => 'JS updated', + 'SETTINGS_SUCCESS_UPDATE' => 'Settings updated successfully', + 'SETTINGS_DROPBOX_KEY' => 'Clave API Dropbox', + 'SETTINGS_ADVANCED_WARNING_EXPL' => 'Changing these advanced settings can be harmful to the stability, security and performance of this application. You should only modify them if you are sure of what you are doing.', + 'SETTINGS_ADVANCED_SAVE' => 'Save my modifications, I accept the risk!', + + 'U2F_NOT_SUPPORTED' => 'U2F no compatible. Lo siento.', + 'U2F_NOT_SECURE' => 'Entorno no protegido. U2F no disponible.', + 'U2F_REGISTER_KEY' => 'Registrar nuevo dispositivo.', + 'U2F_REGISTRATION_SUCCESS' => '¡Registro correcto!', + 'U2F_AUTHENTIFICATION_SUCCESS' => '¡Autenticación correcta!', + 'U2F_CREDENTIALS' => 'Credenciales', + 'U2F_CREDENTIALS_DELETED' => '¡Credenciales eliminadas!', + 'U2F_LOGIN' => 'Log in with WebAuthn', + + 'NEW_PHOTOS_NOTIFICATION' => 'Enviar correos electrónicos de notificación de nuevas fotos.', + 'SETTINGS_SUCCESS_NEW_PHOTOS_NOTIFICATION' => 'Notificación de nuevas fotos actualizada', + 'USER_EMAIL_INSTRUCTION' => 'Agregue su correo electrónico a continuación para habilitar la recepción de notificaciones. Para dejar de recibir correos electrónicos, simplemente elimine su correo electrónico a continuación.', + + 'LOGIN_USERNAME' => 'Nuevo nombre de usuario', + 'LOGIN_PASSWORD' => 'Nueva contraseña', + 'LOGIN_PASSWORD_CONFIRM' => 'Confirmar contraseña', + 'PASSWORD_TITLE' => 'Introduce tu contraseña actual:', + 'PASSWORD_CURRENT' => 'Contraseña actual', + 'PASSWORD_TEXT' => 'Su nombre de usuario y contraseña se cambiarán a lo siguiente:', + 'PASSWORD_CHANGE' => 'Cambiar inicio de sesión', + + 'EDIT_SHARING_TITLE' => 'Editar compartir', + 'EDIT_SHARING_TEXT' => 'Las propiedades para compartir de este álbum se cambiarán a lo siguiente:', + 'SHARE_ALBUM_TEXT' => 'Este álbum se compartirá con las siguientes propiedades:', + + 'SORT_DIALOG_ATTRIBUTE_LABEL' => 'Attribute', + 'SORT_DIALOG_ORDER_LABEL' => 'Order', + + 'SORT_ALBUM_BY' => 'Ordenar álbumes por %1$s en un %2$s orden.', + + 'SORT_ALBUM_SELECT_1' => 'Tiempo de creación', + 'SORT_ALBUM_SELECT_2' => 'Título', + 'SORT_ALBUM_SELECT_3' => 'Descripción', + 'SORT_ALBUM_SELECT_5' => 'Última fecha de toma', + 'SORT_ALBUM_SELECT_6' => 'La fecha de toma más antigua', + + 'SORT_PHOTO_BY' => 'Ordenar fotos por %1$s en un %2$s orden.', + + 'SORT_PHOTO_SELECT_1' => 'Tiempo de carga', + 'SORT_PHOTO_SELECT_2' => 'Fecha Realización', + 'SORT_PHOTO_SELECT_3' => 'Título', + 'SORT_PHOTO_SELECT_4' => 'Descripción', + 'SORT_PHOTO_SELECT_6' => 'Estrella', + 'SORT_PHOTO_SELECT_7' => 'Formato de foto', + + 'SORT_ASCENDING' => 'Ascendente', + 'SORT_DESCENDING' => 'Descendente', + 'SORT_CHANGE' => 'Cambiar clasificación', + + 'DROPBOX_TITLE' => 'Establecer clave de Dropbox', + 'DROPBOX_TEXT' => 'Para importar fotos desde su Dropbox, necesita una clave de aplicación válida desde su sitio web . Generar usted mismo una clave personal e ingrésela a continuación:', + + 'LANG_TEXT' => 'Cambiar el idioma Lychee para:', + 'LANG_TITLE' => 'Cambiar idioma', + + 'SETTING_RECENT_PUBLIC_TEXT' => 'Rendre l\'album "Recent" accessible a tous publics', + 'SETTING_STARRED_PUBLIC_TEXT' => 'Make "Starred" smart album accessible to anonymous users', + 'SETTING_ONTHISDAY_PUBLIC_TEXT' => 'Make "On This Day" smart album accessible to anonymous users', + + 'CSS_TEXT' => 'Personalize CSS:', + 'CSS_TITLE' => 'Change CSS', + 'JS_TEXT' => 'Custom JS:', + 'JS_TITLE' => 'Change JS', + 'PUBLIC_SEARCH_TEXT' => 'Búsqueda pública permitida:', + 'OVERLAY_TYPE' => 'Datos para usar en la superposición de imágenes:', + 'OVERLAY_NONE' => 'Ninguna', + 'OVERLAY_EXIF' => 'Datos EXIF de fotos', + 'OVERLAY_DESCRIPTION' => 'Descripción de la foto', + 'OVERLAY_DATE' => 'Fecha de foto tomada', + 'ALBUM_DECORATION' => 'Album decorations:', + 'ALBUM_DECORATION_NONE' => 'None', + 'ALBUM_DECORATION_ORIGINAL' => 'Sub-album marker', + 'ALBUM_DECORATION_ALBUM' => 'Number of sub-albums', + 'ALBUM_DECORATION_PHOTO' => 'Number of photos', + 'ALBUM_DECORATION_ALL' => 'Number of sub-albums and photos', + 'ALBUM_DECORATION_ORIENTATION' => 'Orientation of album decorations:', + 'ALBUM_DECORATION_ORIENTATION_ROW' => 'Horizontal (photos, albums)', + 'ALBUM_DECORATION_ORIENTATION_ROW_REVERSE' => 'Horizontal (albums, photos)', + 'ALBUM_DECORATION_ORIENTATION_COLUMN' => 'Vertical (top photos, albums)', + 'ALBUM_DECORATION_ORIENTATION_COLUMN_REVERSE' => 'Vertical (top albums, photos)', + 'MAP_DISPLAY_TEXT' => 'Habilitar mapas (OpenStreetMap):', + 'MAP_DISPLAY_PUBLIC_TEXT' => 'Habilitar mapas para álbumes públicos (proporcionados por OpenStreetMap):', + 'MAP_PROVIDER' => 'Proveedor de capas de OpenStreetMap:', + 'MAP_PROVIDER_WIKIMEDIA' => 'Wikimedia', + 'MAP_PROVIDER_OSM_ORG' => 'OpenStreetMap.org (no HiDPI)', + 'MAP_PROVIDER_OSM_DE' => 'OpenStreetMap.de (no HiDPI)', + 'MAP_PROVIDER_OSM_FR' => 'OpenStreetMap.fr (no HiDPI)', + 'MAP_PROVIDER_RRZE' => 'University of Erlangen, Germany (only HiDPI)', + 'MAP_INCLUDE_SUBALBUMS_TEXT' => 'Incluir fotos de subálbumes en el mapa:', + 'LOCATION_DECODING' => 'Decodificar datos GPS en nombre de ubicación', + 'LOCATION_SHOW' => 'Mostrar nombre de ubicación', + 'LOCATION_SHOW_PUBLIC' => 'Mostrar el nombre de la ubicación para el modo público', + + 'LAYOUT_TYPE' => 'Diseño de fotos:', + 'LAYOUT_SQUARES' => 'Miniaturas cuadradas', + 'LAYOUT_JUSTIFIED' => 'Con aspecto justificado', + 'LAYOUT_MASONRY' => 'Con aspecto, masonry', + 'LAYOUT_GRID' => 'Con aspecto, grid', + 'LAYOUT_UNJUSTIFIED' => 'Con aspecto, injustificado', + 'SET_LAYOUT' => 'Cambia el diseño', + + 'NSFW_VISIBLE_TEXT_1' => 'Haz que los álbumes confidenciales sean visibles de forma predeterminada.', + 'NSFW_VISIBLE_TEXT_2' => 'Si el álbum es público, todavía está accesible, simplemente oculto a la vista y puede revelarse presionando H.', + 'SETTINGS_SUCCESS_NSFW_VISIBLE' => 'Visibilidad predeterminada del álbum sensible actualizada con éxito.', + + 'NSFW_BANNER' => '

Sensitive content

This album contains sensitive content which some people may find offensive or disturbing.

Tap to consent.

', + 'NSFW_HEADER' => 'Sensitive content', + 'NSFW_EXPLANATION' => 'This album contains sensitive content which some people may find offensive or disturbing.', + 'TAP_CONSENT' => 'Tap to consent.', + + 'VIEW_NO_RESULT' => 'No hay resultados', + 'VIEW_NO_PUBLIC_ALBUMS' => 'Sin álbumes públicos', + 'VIEW_NO_CONFIGURATION' => 'Sin configuración', + 'VIEW_PHOTO_NOT_FOUND' => 'Foto no encontrada', + + 'NO_TAGS' => 'Sin etiquetas', + + 'UPLOAD_MANAGE_NEW_PHOTOS' => 'Ahora puede administrar sus nuevas fotos.', + 'UPLOAD_COMPLETE' => 'Carga completa', + 'UPLOAD_COMPLETE_FAILED' => 'No se pudo cargar una o más fotos.', + 'UPLOAD_IMPORTING' => 'Importador', + 'UPLOAD_IMPORTING_URL' => 'Importando URL', + 'UPLOAD_UPLOADING' => 'Subiendo', + 'UPLOAD_FINISHED' => 'Terminado', + 'UPLOAD_PROCESSING' => 'Tratamiento', + 'UPLOAD_FAILED' => 'Ha fallado', + 'UPLOAD_FAILED_ERROR' => 'Subida fallida. ¡El servidor devolvió un error!', + 'UPLOAD_FAILED_WARNING' => 'Subida fallida. ¡El servidor devolvió una advertencia!', + 'UPLOAD_CANCELLED' => 'Cancelado', + 'UPLOAD_SKIPPED' => 'Saltado', + 'UPLOAD_UPDATED' => 'Actualizado', + 'UPLOAD_GENERAL' => 'General', + 'UPLOAD_IMPORT_SKIPPED_DUPLICATE' => 'Esta foto ha sido omitida porque ya está en tu biblioteca.', + 'UPLOAD_IMPORT_RESYNCED_DUPLICATE' => 'Esta foto se omitió porque ya está en tu biblioteca, pero sus metadatos se actualizaron.', + 'UPLOAD_ERROR_CONSOLE' => 'Por favor, eche un vistazo a la consola de su navegador para más detalles.', + 'UPLOAD_UNKNOWN' => 'El servidor devolvió una respuesta desconocida. Por favor, eche un vistazo a la consola de su navegador para más detalles.', + 'UPLOAD_ERROR_UNKNOWN' => 'Subida fallida. ¡El servidor devolvió un error desconocido!', + 'UPLOAD_ERROR_POSTSIZE' => 'Subida fallida. ¡El PHP post_max_size puede ser demasiado pequeño! De lo contrario, consulta las preguntas frecuentes.', + 'UPLOAD_ERROR_FILESIZE' => 'Subida fallida. ¡El PHP upload_max_filesize puede ser demasiado pequeño! De lo contrario, consulta las preguntas frecuentes.', + 'UPLOAD_IN_PROGRESS' => '¡Lychee está subiendo actualmente!', + 'UPLOAD_IMPORT_WARN_ERR' => 'La importación ha finalizado, pero devolvió advertencias o errores. Por favor, eche un vistazo al registro (Configuración -> Mostrar registro) para obtener más detalles.', + 'UPLOAD_IMPORT_COMPLETE' => 'Importación completa', + 'UPLOAD_IMPORT_INSTR' => 'Ingrese el enlace directo a una foto para importarla:', + 'UPLOAD_IMPORT' => 'Importar', + 'UPLOAD_IMPORT_SERVER' => 'Importando desde el servidor', + 'UPLOAD_IMPORT_SERVER_FOLD' => 'Carpeta vacía o no hay archivos legibles para procesar. Por favor, eche un vistazo al registro (Configuración -> Mostrar registro) para obtener más detalles.', + 'UPLOAD_IMPORT_SERVER_INSTR' => 'Importe todas las fotos, carpetas y subcarpetas ubicadas en las carpetas con las siguientes rutas absolutas (en el servidor). Las rutas están separadas por espacios, use \\ para escapar de un espacio en una ruta.', + 'UPLOAD_ABSOLUTE_PATH' => 'Ruta absoluta a los directorios, separados por espacios', + 'UPLOAD_IMPORT_SERVER_EMPT' => 'No se pudo iniciar la importación porque la carpeta estaba vacía', + 'UPLOAD_IMPORT_DELETE_ORIGINALS' => 'Eliminar originales', + 'UPLOAD_IMPORT_DELETE_ORIGINALS_EXPL' => 'Los archivos originales se eliminarán después de la importación cuando sea posible', + 'UPLOAD_IMPORT_VIA_SYMLINK' => 'Enlaces simbólicos', + 'UPLOAD_IMPORT_VIA_SYMLINK_EXPL' => 'Importar archivos utilizando enlaces simbólicos a originales.', + 'UPLOAD_IMPORT_SKIP_DUPLICATES' => 'Omitir duplicados', + 'UPLOAD_IMPORT_SKIP_DUPLICATES_EXPL' => 'Se omiten los archivos multimedia existentes.', + 'UPLOAD_IMPORT_RESYNC_METADATA' => 'Volver a sincronizar metadatos', + 'UPLOAD_IMPORT_RESYNC_METADATA_EXPL' => 'Actualizar metadatos de archivos multimedia existentes.', + 'UPLOAD_IMPORT_LOW_MEMORY_EXPL' => 'El proceso de importación en el servidor se acerca al límite de memoria y puede terminar antes de tiempo.', + 'UPLOAD_WARNING' => 'Advertencia', + 'UPLOAD_IMPORT_NOT_A_DIRECTORY' => '¡La ruta dada no es un directorio legible!', + 'UPLOAD_IMPORT_PATH_RESERVED' => '¡El camino dado es un camino reservado de Lychee!', + 'UPLOAD_IMPORT_FAILED' => '¡No se pudo importar el archivo!', + 'UPLOAD_IMPORT_UNSUPPORTED' => '¡Tipo de archivo no soportado!', + 'UPLOAD_IMPORT_CANCELLED' => 'Importación cancelada', + + 'ABOUT_SUBTITLE' => 'Un auto-hosteado gestor de imagenes, bien hecho', + 'ABOUT_DESCRIPTION' => 'Lychee es una herramienta gratuita de gestión de fotos, que se ejecuta en su servidor o espacio web. La instalación es cuestión de segundos. Cargue, administre y comparta fotos como desde una aplicación nativa. Lychee viene con todo lo que necesitas y todas tus fotos se almacenan de forma segura.', + 'FOOTER_COPYRIGHT' => 'Todas las imágenes de este sitio web están sujetas a copyright por %1$s © %2$s', + 'HOSTED_WITH_LYCHEE' => 'Alojado con Lychee', + + 'URL_COPY_TO_CLIPBOARD' => 'Copiar al portapapeles', + 'URL_COPIED_TO_CLIPBOARD' => '¡URL copiada al portapapeles!', + 'PHOTO_DIRECT_LINKS_TO_IMAGES' => 'Enlaces directos a archivos de imagen:', + 'PHOTO_ORIGINAL' => 'Original', + 'PHOTO_MEDIUM' => 'Mediana', + 'PHOTO_MEDIUM_HIDPI' => 'Mediana HiDPI', + 'PHOTO_SMALL' => 'Miniatura', + 'PHOTO_SMALL_HIDPI' => 'Miniatura HiDPI', + 'PHOTO_THUMB' => 'Cuadrado de Miniatura', + 'PHOTO_THUMB_HIDPI' => 'Cuadrado de Miniatura HiDPI', + 'PHOTO_PLACEHOLDER' => 'Low Quality Image Placeholder', + 'PHOTO_THUMBNAIL' => 'Miniatura de la foto', + 'PHOTO_LIVE_VIDEO' => 'Video de live-photo', + 'PHOTO_VIEW' => 'Vista de Foto de Lychee', + + 'PHOTO_EDIT_ROTATECWISE' => 'Rotate clockwise', + 'PHOTO_EDIT_ROTATECCWISE' => 'Rotate counter-clockwise', + + 'ERROR_GPX' => 'Error al cargar archivo GPX: ', + 'ERROR_EITHER_ALBUMS_OR_PHOTOS' => '¡Seleccione álbumes o fotos!', + 'ERROR_COULD_NOT_FIND' => 'No se pudo encontrar lo que buscas.', + 'ERROR_INVALID_EMAIL' => 'No es una dirección de correo electrónico válida.', + 'EMAIL_SUCCESS' => '¡Correo electrónico actualizado!', + 'ERROR_PHOTO_NOT_FOUND' => 'Error: ¡foto %s no encontrada!', + 'ERROR_EMPTY_USERNAME' => 'el nuevo nombre de usuario no puede estar vacío.', + 'ERROR_PASSWORD_DOES_NOT_MATCH' => 'la nueva contraseña no coincide.', + 'ERROR_EMPTY_PASSWORD' => 'la nueva contraseña no puede estar vacía.', + 'ERROR_SELECT_ALBUM' => '¡Selecciona un álbum para compartir!', + 'ERROR_SELECT_USER' => '¡Seleccione un usuario con el que compartir!', + 'ERROR_SELECT_SHARING' => '¡Seleccione un recurso compartido para eliminar!', + 'SHARING_SUCCESS' => '¡Compartir actualizado!', + 'SHARING_REMOVED' => '¡Compartir eliminado!', + 'USER_CREATED' => '¡Usuario creado!', + 'USER_DELETED' => '¡Usuario eliminado!', + 'USER_UPDATED' => '¡Usuario actualizado!', + 'ENTER_EMAIL' => 'Ingrese su dirección de correo electrónico:', + 'ERROR_ALBUM_JSON_NOT_FOUND' => 'Error: ¡Álbum JSON no encontrado!', + 'ERROR_ALBUM_NOT_FOUND' => 'Error: álbum %s no encontrado', + 'ERROR_DROPBOX_KEY' => 'Error: clave de Dropbox no configurada', + 'ERROR_SESSION' => 'Sesión caducada.', + 'CAMERA_DATE' => 'Fecha de la cámara', + 'NEW_PASSWORD' => 'nueva contraseña', + 'ALLOW_UPLOADS' => 'Permitir subidas', + 'ALLOW_USER_SELF_EDIT' => 'Allow self-management of user account', + 'OSM_CONTRIBUTORS' => 'Contribuidores de OpenStreetMap', +]; diff --git a/lang/es/maintenance.php b/lang/es/maintenance.php new file mode 100644 index 00000000000..f86de3d6f46 --- /dev/null +++ b/lang/es/maintenance.php @@ -0,0 +1,60 @@ + 'Maintenance', + 'description' => 'You will find on this page, all the required actions to keep your Lychee installation running smooth and nicely.', + 'cleaning' => [ + 'title' => 'Cleaning %s', + 'result' => '%s deleted.', + 'description' => 'Remove all contents from %s', + 'button' => 'Clean', + ], + 'fix-jobs' => [ + 'title' => 'Fixing Jobs History', + 'description' => 'Mark jobs with status %s or %s as %s.', + 'button' => 'Fix job history', + ], + 'gen-sizevariants' => [ + 'title' => 'Missing %s', + 'description' => 'Found %d %s that could be generated.', + 'button' => 'Generate!', + 'success' => 'Successfully generated %d %s.', + ], + 'fill-filesize-sizevariants' => [ + 'title' => 'File sizes missing', + 'description' => 'Found %d small variants without file size.', + 'button' => 'Fetch data!', + 'success' => 'Successfully computed sizes of %d small variants.', + ], + 'fix-tree' => [ + 'title' => 'Tree statistics', + 'Oddness' => 'Oddness', + 'Duplicates' => 'Duplicates', + 'Wrong parents' => 'Wrong parents', + 'Missing parents' => 'Missing parents', + 'button' => 'Fix tree', + ], + 'optimize' => [ + 'title' => 'Optimize Database', + 'description' => 'If you notice slowdown in your installation, it may be because your database does not + have all its needed index.', + 'button' => 'Optimize Database', + ], + 'update' => [ + 'title' => 'Updates', + 'check-button' => 'Check for updates', + 'update-button' => 'Update', + 'no-pending-updates' => 'No pending update.', + ], +]; \ No newline at end of file diff --git a/lang/es/profile.php b/lang/es/profile.php new file mode 100644 index 00000000000..cc24b97452c --- /dev/null +++ b/lang/es/profile.php @@ -0,0 +1,64 @@ + 'Profile', + + 'login' => [ + 'header' => 'Profile', + 'enter_current_password' => 'Enter your current password:', + 'current_password' => 'Current password', + 'credentials_update' => 'Your credentials will be changed to the following:', + 'username' => 'Username', + 'new_password' => 'New password', + 'confirm_new_password' => 'Confirm new password', + 'email_instruction' => 'Add your email below to enable receiving email notifications. To stop receiving emails, simply remove your email below.', + 'email' => 'Email', + 'change' => 'Change Login', + 'api_token' => 'API Token ...', + + 'missing_fields' => 'Missing fields', + ], + + 'token' => [ + 'unavailable' => 'You have already viewed this token.', + 'no_data' => 'No token API have been generated.', + 'disable' => 'Disable', + 'disabled' => 'Token disabled', + 'warning' => 'This token will not be displayed again. Copy it and keep it in a safe place.', + 'reset' => 'Reset the token', + 'create' => 'Create a new token', + ], + + 'oauth' => [ + 'header' => 'OAuth', + 'header_not_available' => 'OAuth is not available', + 'setup_env' => 'Set up the credentials in your .env', + 'token_registered' => '%s token registered.', + 'setup' => 'Set up %s', + 'reset' => 'reset', + 'credential_deleted' => 'Credential deleted!', + ], + + 'u2f' => [ + 'header' => 'Passkey/MFA/2FA', + 'info' => 'This only provides the ability to use WebAuthn to authenticate instead of username & password.', + 'empty' => 'Credentials list is empty!', + 'not_secure' => 'Environment not secured. U2F not available.', + 'new' => 'Register new device.', + 'credential_deleted' => 'Credential deleted!', + 'credential_updated' => 'Credential updated!', + 'credential_registred' => 'Registration successful!', + '5_chars' => 'At least 5 chars.', + ], +]; \ No newline at end of file diff --git a/lang/es/settings.php b/lang/es/settings.php new file mode 100644 index 00000000000..fd197f11135 --- /dev/null +++ b/lang/es/settings.php @@ -0,0 +1,92 @@ + 'Settings', + 'small_screen' => 'For better a experience on the Settings page,
we recommend you use a larger screen.', + 'tabs' => [ + 'basic' => 'Basic', + 'all_settings' => 'All settings', + ], + 'toasts' => [ + 'change_saved' => 'Change saved!', + 'details' => 'Settings have been modified as per request', + 'error' => 'Error!', + 'error_load_css' => 'Could not load dist/user.css', + 'error_load_js' => 'Could not load dist/custom.js', + 'error_save_css' => 'Could not save CSS', + 'error_save_js' => 'Could not save JS', + 'thank_you' => 'Thank you for your support.', + 'reload' => 'Reload your page for full functionalities.', + ], + 'system' => [ + 'header' => 'System', + 'use_dark_mode' => 'Use dark mode for Lychee', + 'language' => 'Language used by Lychee', + 'nsfw_album_visibility' => 'Make Sensitive albums visible by default.', + 'nsfw_album_explanation' => 'If the album is public, it is still accessible, just hidden from the view and can be revealed by pressing H.', + ], + 'lychee_se' => [ + 'header' => 'Lychee SE', + 'call4action' => 'Get exclusive features and support the development of Lychee. Unlock the SE edition.', + 'preview' => 'Enable preview of Lychee SE features', + 'hide_call4action' => 'Hide this Lychee SE registration form. I am happy with Lychee as-is. :)', + 'hide_warning' => 'If enabled, the only way to register your license key will be via the More tab above. Changes are applied on page reload.', + ], + 'dropbox' => [ + 'header' => 'Dropbox', + 'instruction' => 'In order to import photos from your Dropbox, you need a valid drop-ins app key from their website.', + 'api_key' => 'Dropbox API Key', + 'set_key' => 'Set Dropbox Key', + ], + 'gallery' => [ + 'header' => 'Gallery', + 'photo_order_column' => 'Default column used for sorting photos', + 'photo_order_direction' => 'Default order used for sorting photos', + 'album_order_column' => 'Default column used for sorting albums', + 'album_order_direction' => 'Default order used for sorting albums', + 'aspect_ratio' => 'Default aspect ratio for album thumbs', + 'photo_layout' => 'Layout for pictures', + 'album_decoration' => 'Show decorations on album cover (sub-album and/or photo count)', + 'album_decoration_direction' => 'Align album decorations horizontally or vertically', + 'photo_overlay' => 'Default image overlay information', + 'license_default' => 'Default license used for albums', + 'license_help' => 'Need help choosing?', + ], + 'geolocation' => [ + 'header' => 'Geo-location', + 'map_display' => 'Display the map given GPS coordinates', + 'map_display_public' => 'Allow anonymous users to access the map', + 'map_provider' => 'Defines the map provider', + 'map_include_subalbums' => 'Includes pictures of the sub albums on the map', + 'location_decoding' => 'Use GPS location decoding', + 'location_show' => 'Show location extracted from GPS coordinates', + 'location_show_public' => 'Anonymous users can access the extracted location from GPS coordinates', + ], + 'advanced' => [ + 'header' => 'Advanced Customization', + 'change_css' => 'Change CSS', + 'change_js' => 'Change JS', + ], + 'all' => [ + 'old_setting_style' => 'Old setting style', + 'change_detected' => 'Some settings changed.', + 'save' => 'Save', + ], + + 'tool_option' => [ + 'disabled' => 'disabled', + 'enabled' => 'enabled', + 'discover' => 'discover', + ], +]; \ No newline at end of file diff --git a/lang/es/sharing.php b/lang/es/sharing.php new file mode 100644 index 00000000000..69de18cc6d0 --- /dev/null +++ b/lang/es/sharing.php @@ -0,0 +1,33 @@ + 'Sharing', + + 'info' => 'This page gives an overview of and the ability to edit the sharing rights associated with albums.', + 'album_title' => 'Album title', + 'username' => 'Username', + 'no_data' => 'Sharing list is empty.', + 'share' => 'Share', + 'permission_deleted' => 'Permission deleted!', + 'permission_created' => 'Permission created!', + + 'grants' => [ + 'read' => 'Grants read access', + 'original' => 'Grants access to original photo', + 'download' => 'Grants download', + 'upload' => 'Grants upload', + 'edit' => 'Grants edit', + 'delete' => 'Grants delete', + ], +]; \ No newline at end of file diff --git a/lang/es/statistics.php b/lang/es/statistics.php new file mode 100644 index 00000000000..2baf855bbd5 --- /dev/null +++ b/lang/es/statistics.php @@ -0,0 +1,34 @@ + 'Statistics', + + 'preview_text' => 'This is a preview of the statistics page available in Lychee SE.
The data shown here are randomly generated and do not reflect your server.', + 'no_data' => 'User does not have data on server.', + 'collapse' => 'Collapse albums sizes', + + 'total' => [ + 'total' => 'Total', + 'albums' => 'Albums', + 'photos' => 'Photos', + 'size' => 'Size', + ], + 'table' => [ + 'username' => 'Owner', + 'title' => 'Title', + 'photos' => 'Photos', + 'descendants' => 'Children', + 'size' => 'Size', + ], +]; \ No newline at end of file diff --git a/lang/es/toasts.php b/lang/es/toasts.php new file mode 100644 index 00000000000..293d4b72594 --- /dev/null +++ b/lang/es/toasts.php @@ -0,0 +1,17 @@ + 'Error', + 'success' => 'Success', +]; \ No newline at end of file diff --git a/lang/es/users.php b/lang/es/users.php new file mode 100644 index 00000000000..599bb833454 --- /dev/null +++ b/lang/es/users.php @@ -0,0 +1,44 @@ + 'Users', + 'description' => 'Here you can manage the users of your Lychee installation. You can create, edit and delete users.', + 'create' => 'Create a new user', + 'username' => 'Username', + 'password' => 'Password', + 'legend' => 'Legend', + 'upload_rights' => 'When selected, the user can upload content.', + 'edit_rights' => 'When selected, the user can modify their profile (username, password).', + 'quota' => 'When set, the user has a space quota for pictures (in kB).', + + 'user_deleted' => 'User deleted', + 'user_created' => 'User created', + 'user_updated' => 'User updated', + 'change_saved' => 'Change saved!', + + 'create_edit' => [ + 'upload_rights' => 'User can upload content.', + 'edit_rights' => 'User can modify their profile (username, password).', + 'quota' => 'User has quota limit.', + 'quota_kb' => 'quota in kB (0 for default)', + 'note' => 'Admin note (not publically visible)', + 'create' => 'Create', + 'edit' => 'Edit', + ], + 'line' => [ + 'admin' => 'admin user', + 'edit' => 'Edit', + 'delete' => 'Delete', + ], +]; \ No newline at end of file diff --git a/lang/fr/aspect_ratio.php b/lang/fr/aspect_ratio.php new file mode 100644 index 00000000000..242f0d07659 --- /dev/null +++ b/lang/fr/aspect_ratio.php @@ -0,0 +1,21 @@ + '5/4 (instagram paysage)', + '4by5' => '4/5 (instagram portrait)', + '2by3' => '2/3 (portrait)', + '3by2' => '3/2 (paysage)', + '1by1' => 'square', + '1byx9' => '16/9 (paysage)', +]; \ No newline at end of file diff --git a/lang/fr/diagnostics.php b/lang/fr/diagnostics.php new file mode 100644 index 00000000000..0fadd640428 --- /dev/null +++ b/lang/fr/diagnostics.php @@ -0,0 +1,30 @@ + 'Diagnostics', + + 'copy_to_clipboard' => 'Copy diagnostics to clipboard', + 'self-diagnosis' => 'Self-diagnosis', + 'info' => 'Info', + 'space' => 'Space', + 'load_space' => 'Load space usage.', + 'configuration' => 'Configuration', + 'loading' => 'Loading...', + 'identical_content' => 'Identical content', + + 'toast' => [ + 'info' => 'Info', + 'copy' => 'Diagnostics copied to clipboard!', + ], +]; \ No newline at end of file diff --git a/lang/fr/dialogs.php b/lang/fr/dialogs.php new file mode 100644 index 00000000000..4afd65fae3f --- /dev/null +++ b/lang/fr/dialogs.php @@ -0,0 +1,221 @@ + [ + 'close' => 'Close', + 'cancel' => 'Cancel', + 'save' => 'Save', + 'delete' => 'Delete', + 'move' => 'Move', + ], + 'about' => [ + 'subtitle' => 'Self-hosted photo-management done right', + 'description' => 'Lychee is a free photo-management tool, which runs on your server or web-space. Installing is a matter of seconds. Upload, manage and share photos like from a native application. Lychee comes with everything you need and all your photos are stored securely.', + 'update_available' => 'Update available!', + 'thank_you' => 'Thank you for your support!', + 'get_supporter_or_register' => 'Get exclusive features and support the development of Lychee.
Unlock the Supporter Edition or register your License key', + 'here' => 'here', + ], + 'dropbox' => [ + 'not_configured' => 'Dropbox is not configured.', + ], + 'import_from_link' => [ + 'instructions' => 'Please enter the direct link to a photo to import it:', + 'import' => 'Import', + ], + 'keybindings' => [ + 'don_t_show_again' => 'Don\'t show this again', + 'side_wide' => 'Site-wide Shortcuts', + 'back_cancel' => 'Back/Cancel', + 'confirm' => 'Confirm', + 'login' => 'Login', + 'toggle_full_screen' => 'Toggle Full Screen', + 'toggle_sensitive_albums' => 'Toggle Sensitive Albums', + + 'albums' => 'Albums Shortcuts', + 'new_album' => 'New Album', + 'upload_photos' => 'Upload Photos', + 'search' => 'Search', + 'show_this_modal' => 'Show this modal', + 'select_all' => 'Select All', + 'move_selection' => 'Move Selection', + 'delete_selection' => 'Delete Selection', + + 'album' => 'Album Shortcuts', + 'slideshow' => 'Start/Stop Slideshow', + 'toggle' => 'Toggle panel', + + 'photo' => 'Photo Shortcuts', + 'previous' => 'Previous photo', + 'next' => 'Next photo', + 'cycle' => 'Cycle overlay mode', + 'star' => 'Star the photo', + 'move' => 'Move the photo', + 'delete' => 'Delete the photo', + 'edit' => 'Edit information', + 'show_hide_meta' => 'Show information', + + 'keep_hidden' => 'We will keep it hidden.', + ], + 'login' => [ + 'username' => 'Username', + 'password' => 'Password', + 'unknown_invalid' => 'Unknown user or invalid password.', + 'signin' => 'Sign-In', + ], + 'register' => [ + 'enter_license' => 'Enter your license key below:', + 'license_key' => 'License key', + 'invalid_license' => 'Invalid license key.', + 'register' => 'Register', + ], + 'share_album' => [ + 'url_copied' => 'Copied URL to clipboard!', + ], + 'upload' => [ + 'completed' => 'Completed', + 'uploaded' => 'Uploaded:', + 'release' => 'Release file to upload!', + 'select' => 'Click here to select files to upload', + 'drag' => '(Or drag files to the page)', + 'loading' => 'Loading', + 'resume' => 'Resume', + 'uploading' => 'Uploading', + 'finished' => 'Finished', + 'failed_error' => 'Upload failed. The server returned an error!', + ], + 'visibility' => [ + 'public' => 'Public', + 'public_expl' => 'Anonymous users can access this album, subject to the restrictions below.', + 'full' => 'Original', + 'full_expl' => 'Anonymous users can view full-resolution photos.', + 'hidden' => 'Hidden', + 'hidden_expl' => 'Anonymous users need a direct link to access this album.', + 'downloadable' => 'Downloadable', + 'downloadable_expl' => 'Anonymous users can download this album.', + 'password' => 'Password', + 'password_prot' => 'Password protected', + 'password_prot_expl' => 'Anonymous users need a shared password to access this album.', + 'nsfw' => 'Sensitive', + 'nsfw_expl' => 'Album contains sensitive content.', + 'visibility_updated' => 'Visibility updated.', + ], + 'move_album' => [ + 'confirm_single' => 'Are you sure you want to move the album “%1$s” into the album “%2$s”?', + 'confirm_multiple' => 'Are you sure you want to move all selected albums into the album “%s”?', + 'move_single' => 'Move Album', + 'move_to' => 'Move to', + 'move_to_single' => 'Move %s to:', + 'move_to_multiple' => 'Move %d albums to:', + 'no_album_target' => 'No album to move to', + 'moved_single' => 'Album moved!', + 'moved_single_details' => '%1$s moved to %2$s', + 'moved_details' => 'Album(s) moved to %s', + ], + 'new_album' => [ + 'menu' => 'Create Album', + 'info' => 'Enter a title for the new album:', + 'title' => 'title', + 'create' => 'Create Album', + ], + 'new_tag_album' => [ + 'menu' => 'Create Tag Album', + 'info' => 'Enter a title for the new tag album:', + 'title' => 'title', + 'set_tags' => 'Set tags to show', + 'warn' => 'Make sure to press enter after each tag', + 'create' => 'Create Tag Album', + ], + 'delete_album' => [ + 'confirmation' => 'Are you sure you want to delete the album “%s” and all of the photos it contains?', + 'confirmation_multiple' => 'Are you sure you want to delete all %d selected albums and all of the photos they contain?', + 'warning' => 'This action can not be undone!', + 'delete' => 'Delete Album and Photos', + ], + 'transfer' => [ + 'query' => 'Transfer ownership of album to', + 'confirmation' => 'Are you sure you want to transfer the ownership of album “%s” and all the photos it contains to "%s"?', + 'lost_access_warning' => 'Your access to this album will be lost.', + 'warning' => 'This action can not be undone!', + 'transfer' => 'Transfer ownership of album and photos', + ], + 'rename' => [ + 'photo' => 'Enter a new title for this photo:', + 'album' => 'Enter a new title for this album:', + 'rename' => 'Rename', + ], + 'merge' => [ + 'merge_to' => 'Merge %s to:', + 'merge_to_multiple' => 'Merge %d albums to:', + 'no_albums' => 'No albums to merge to.', + 'confirm' => 'Are you sure you want to merge the album “%1$s” into the album “%2$s”?', + 'confirm_multiple' => 'Are you sure you want to merge all selected albums into the album “%s”?', + 'merge' => 'Merge Albums', + 'merged' => 'Album(s) merged to %s!', + ], + 'unlock' => [ + 'password_required' => 'This album is protected by a password. Enter the password below to view the photos of this album:', + 'password' => 'Password', + 'unlock' => 'Unlock', + ], + 'photo_tags' => [ + 'question' => 'Enter your tags for this photo.', + 'question_multiple' => 'Enter your tags for all %d selected photos. Existing tags will be overwritten.', + 'no_tags' => 'No Tags', + 'set_tags' => 'Set Tags', + 'updated' => 'Tags updated!', + 'tags_override_info' => 'If this is unchecked, the tags will be added to the existing tags of the photo.', + ], + 'photo_copy' => [ + 'no_albums' => 'No albums to copy to', + 'copy_to' => 'Copy %s to:', + 'copy_to_multiple' => 'Copy %d photos to:', + 'confirm' => 'Copy %s to %s.', + 'confirm_multiple' => 'Copy %d photos to %s.', + 'copy' => 'Copy', + 'copied' => 'Photo(s) copied!', + ], + 'photo_delete' => [ + 'confirm' => 'Are you sure you want to delete the photo “%s”?', + 'confirm_multiple' => 'Are you sure you want to delete all %d selected photos?', + 'deleted' => 'Photo(s) deleted!', + ], + 'move_photo' => [ + 'move_single' => 'Move %s to:', + 'move_multiple' => 'Move %d photos to:', + 'confirm' => 'Move %s to %s.', + 'confirm_multiple' => 'Move %d photos to %s.', + 'moved' => 'Photo(s) moved to %s!', + ], + 'target_user' => [ + 'placeholder' => 'Select user', + ], + 'target_album' => [ + 'placeholder' => 'Select album', + ], + 'webauthn' => [ + 'u2f' => 'U2F', + 'success' => 'Authentication successful!', + 'error' => 'Whoops, it looks like something went wrong. Please reload the site and try again!', + ], + 'se' => [ + 'available' => 'Available in the Supporter Edition', + ], + 'session_expired' => [ + 'title' => 'Session expired', + 'message' => 'Your session has expired.
Please reload the page.', + 'reload' => 'Reload', + 'go_to_gallery' => 'Go to the Gallery', + ], +]; \ No newline at end of file diff --git a/lang/fr/fix-tree.php b/lang/fr/fix-tree.php new file mode 100644 index 00000000000..64803e310e6 --- /dev/null +++ b/lang/fr/fix-tree.php @@ -0,0 +1,55 @@ + 'Maintenance', + 'intro' => 'This page allows you to re-order and fix your albums manually.
Before any modifications, we strongly recommend you to read about Nested Set tree structures.', + 'warning' => 'You can really break your Lychee installation here, modify values at your own risks.', + + 'help' => [ + 'header' => 'Help', + 'hover' => 'Hover ids or titles to highlight related albums.', + 'left' => 'Left', + 'right' => 'Right', + 'convenience' => 'For your convenience, the and buttons allow you to change the values of %s and %s by respectively +1 and -1 with propagation.', + 'left-right-warn' => 'The and indicates that the value of %s (and respectively %s) is duplicated somewhere.', + 'parent-marked' => 'Marked Parent Id indicates that the %s and %s do not satisfy the Nest Set tree structures. Edit either the Parent Id or the %s/%s values.', + 'slowness' => 'This page will be slow with a large number of albums.', + ], + + 'buttons' => [ + 'reset' => 'Reset', + 'check' => 'Check', + 'apply' => 'Apply', + ], + + 'table' => [ + 'title' => 'Title', + 'left' => 'Left', + 'right' => 'Right', + 'id' => 'Id', + 'parent' => 'Parent Id', + ], + + 'errors' => [ + 'invalid' => 'Invalid tree!', + 'invalid_details' => 'We are not applying this as it is guaranteed to be a broken state.', + 'invalid_left' => 'Album %s has an invalid left value.', + 'invalid_right' => 'Album %s has an invalid right value.', + 'invalid_left_right' => 'Album %s has an invalid left/right values. Left should be strictly smaller than right: %s < %s.', + 'duplicate_left' => 'Album %s has a duplicate left value %s.', + 'duplicate_right' => 'Album %s has a duplicate right value %s.', + 'parent' => 'Album %s has an unexpected parent id %s.', + 'unknown' => 'Album %s has an unknown error.', + ], +]; \ No newline at end of file diff --git a/lang/fr/gallery.php b/lang/fr/gallery.php new file mode 100644 index 00000000000..eb8008827e0 --- /dev/null +++ b/lang/fr/gallery.php @@ -0,0 +1,241 @@ + 'Gallery', + + 'smart_albums' => 'Smart albums', + 'albums' => 'Albums', + 'root' => 'Albums', + + 'original' => 'Original', + 'medium' => 'Medium', + 'medium_hidpi' => 'Medium HiDPI', + 'small' => 'Thumb', + 'small_hidpi' => 'Thumb HiDPI', + 'thumb' => 'Square thumb', + 'thumb_hidpi' => 'Square thumb HiDPI', + 'placeholder' => 'Low Quality Image Placeholder', + 'thumbnail' => 'Photo thumbnail', + 'live_video' => 'Video part of live-photo', + + 'camera_data' => 'Camera date', + 'album_reserved' => 'All Rights Reserved', + + 'map' => [ + 'error_gpx' => 'Error loading GPX file', + 'osm_contributors' => 'OpenStreetMap contributors', + ], + + 'search' => [ + 'title' => 'Search', + 'searching' => 'Searching…', + 'no_results' => 'Nothing matches your search query.', + 'searchbox' => 'Search…', + 'minimum_chars' => 'Minimum %s characters required.', + 'photos' => 'Photos (%s)', + 'albums' => 'Albums (%s)', + ], + + 'smart_album' => [ + 'unsorted' => 'Unsorted', + 'starred' => 'Starred', + 'recent' => 'Recent', + 'public' => 'Public', + 'on_this_day' => 'On This Day', + ], + + 'layout' => [ + 'squares' => 'Square thumbnails', + 'justified' => 'With aspect, justified', + 'masonry' => 'With aspect, masonry', + 'grid' => 'With aspect, grid', + ], + + 'overlay' => [ + 'none' => 'None', + 'exif' => 'EXIF data', + 'description' => 'Description', + 'date' => 'Date taken', + ], + + 'timeline' => [ + 'default' => 'default', + 'disabled' => 'disabled', + 'year' => 'Year', + 'month' => 'Month', + 'day' => 'Day', + 'hour' => 'Hour', + ], + + 'album' => [ + 'header_albums' => 'Albums', + 'header_photos' => 'Photos', + 'no_results' => 'Nothing to see here', + 'upload' => 'Upload photos', + + 'tabs' => [ + 'about' => 'About Album', + 'share' => 'Share Album', + 'move' => 'Move Album', + 'danger' => 'DANGER ZONE', + ], + + 'hero' => [ + 'created' => 'Created', + 'copyright' => 'Copyright', + 'subalbums' => 'Subalbums', + 'images' => 'Photos', + 'download' => 'Download Album', + 'share' => 'Share Album', + 'stats_only_se' => 'Statistics available in the Supporter Edition', + ], + + 'stats' => [ + 'lens' => 'Lens', + 'shutter' => 'Shutter speed', + 'iso' => 'ISO', + 'model' => 'Model', + 'aperture' => 'Aperture', + 'no_data' => 'No data', + ], + + 'properties' => [ + 'title' => 'Title', + 'description' => 'Description', + 'photo_ordering' => 'Order photos by', + 'children_ordering' => 'Order albums by', + 'asc/desc' => 'asc/desc', + 'header' => 'Set album header', + 'compact_header' => 'Use compact header', + 'license' => 'Set license', + 'copyright' => 'Set copyright', + 'aspect_ratio' => 'Set album thumbs aspect ratio', + 'album_timeline' => 'Set album timeline mode', + 'photo_timeline' => 'Set photo timeline mode', + 'layout' => 'Set photo layout', + 'show_tags' => 'Set tags to show', + 'tags_required' => 'Tags are required.', + ], + ], + + 'photo' => [ + 'actions' => [ + 'star' => 'Star', + 'unstar' => 'Unstar', + 'set_album_header' => 'Set as album header', + 'move' => 'Move', + 'delete' => 'Delete', + 'header_set' => 'Header set', + ], + + 'details' => [ + 'about' => 'About', + 'basics' => 'Basics', + 'title' => 'Title', + 'uploaded' => 'Uploaded', + 'description' => 'Description', + 'license' => 'License', + 'reuse' => 'Reuse', + 'latitude' => 'Latitude', + 'longitude' => 'Longitude', + 'altitude' => 'Altitude', + 'location' => 'Location', + 'image' => 'Image', + 'video' => 'Video', + 'size' => 'Size', + 'format' => 'Format', + 'resolution' => 'Resolution', + 'duration' => 'Duration', + 'fps' => 'Frame rate', + 'tags' => 'Tags', + 'camera' => 'Camera', + 'captured' => 'Captured', + 'make' => 'Make', + 'type' => 'Type/Model', + 'lens' => 'Lens', + 'shutter' => 'Shutter Speed', + 'aperture' => 'Aperture', + 'focal' => 'Focal Length', + 'iso' => 'ISO %s', + ], + + 'edit' => [ + 'set_title' => 'Set Title', + 'set_description' => 'Set Description', + 'set_license' => 'Set License', + 'no_tags' => 'No Tags', + 'set_tags' => 'Set Tags', + 'set_created_at' => 'Set Upload Date', + ], + ], + + 'nsfw' => [ + 'header' => 'Sensitive content', + 'description' => 'This album contains sensitive content which some people may find offensive or disturbing.', + 'consent' => 'Tap to consent.', + ], + + 'menus' => [ + 'star' => 'Star', + 'unstar' => 'Unstar', + 'star_all' => 'Star Selected', + 'unstar_all' => 'Unstar Selected', + 'tag' => 'Tag', + 'tag_all' => 'Tag Selected', + 'set_cover' => 'Set Album Cover', + 'remove_header' => 'Remove Album Header', + 'set_header' => 'Set Album Header', + 'copy_to' => 'Copy to …', + 'copy_all_to' => 'Copy Selected to …', + 'rename' => 'Rename', + 'move' => 'Move', + 'move_all' => 'Move Selected', + 'delete' => 'Delete', + 'delete_all' => 'Delete Selected', + 'download' => 'Download', + 'download_all' => 'Download Selected', + 'merge' => 'Merge', + 'merge_all' => 'Merge Selected', + + 'upload_photo' => 'Upload Photo', + 'import_link' => 'Import from Link', + 'import_dropbox' => 'Import from Dropbox', + 'new_album' => 'New Album', + 'new_tag_album' => 'New Tag Album', + 'upload_track' => 'Upload track', + 'delete_track' => 'Delete track', + ], + + 'sort' => [ + 'photo_select_1' => 'Upload Time', + 'photo_select_2' => 'Take Date', + 'photo_select_3' => 'Title', + 'photo_select_4' => 'Description', + 'photo_select_6' => 'Star', + 'photo_select_7' => 'Photo Format', + 'ascending' => 'Ascending', + 'descending' => 'Descending', + 'album_select_1' => 'Creation Time', + 'album_select_2' => 'Title', + 'album_select_3' => 'Description', + 'album_select_5' => 'Latest Take Date', + 'album_select_6' => 'Oldest Take Date', + ], + + 'albums_protection' => [ + 'private' => 'private', + 'public' => 'public', + 'inherit_from_parent' => 'inherit from parent', + ], +]; \ No newline at end of file diff --git a/lang/fr/jobs.php b/lang/fr/jobs.php new file mode 100644 index 00000000000..5d952b76012 --- /dev/null +++ b/lang/fr/jobs.php @@ -0,0 +1,18 @@ + 'Jobs', + + 'no_data' => 'No Jobs have been executed yet.', +]; \ No newline at end of file diff --git a/lang/fr/landing.php b/lang/fr/landing.php new file mode 100644 index 00000000000..fe6fe55b8ea --- /dev/null +++ b/lang/fr/landing.php @@ -0,0 +1,19 @@ + 'Gallery', + 'access_gallery' => 'Access the gallery', + 'hosted_with_lychee' => 'Hosted with Lychee', + 'copyright' => 'All images on this website are subject to copyright by %1$s © %2$s', +]; \ No newline at end of file diff --git a/lang/fr/left-menu.php b/lang/fr/left-menu.php new file mode 100644 index 00000000000..9a3e91f4037 --- /dev/null +++ b/lang/fr/left-menu.php @@ -0,0 +1,29 @@ + 'Back to Gallery', + + 'admin' => 'Admin', + 'clockwork' => 'Clockwork App', + 'logs' => 'Show Logs', + 'jobs' => 'Show Job History', + 'user' => 'User', + + 'sign_out' => 'Sign Out', + + 'about' => 'About', + 'api' => 'API Documentation', + 'source_code' => 'Source Code', + 'support' => 'Support', +]; \ No newline at end of file diff --git a/lang/fr/lychee.php b/lang/fr/lychee.php new file mode 100644 index 00000000000..ee4ead71601 --- /dev/null +++ b/lang/fr/lychee.php @@ -0,0 +1,535 @@ + 'Nom d’utilisateur', + 'PASSWORD' => 'Mot de passe', + 'ENTER' => 'OK', + 'CANCEL' => 'Annuler', + 'CONFIRM' => 'Confirmer', + 'SIGN_IN' => 'Connexion', + 'CLOSE' => 'Fermer', + 'SETTINGS' => 'Paramètres', + 'SEARCH' => 'Rechercher…', + 'MORE' => 'Plus', + 'DEFAULT' => 'Valeur par défaut', + 'GALLERY' => 'Galerie', + + 'USERS' => 'Utilisateurs', + 'PROFILE' => 'Profil', + 'CREATE' => 'Créer', + 'REMOVE' => 'Retirer', + 'SHARE' => 'Partager', + 'U2F' => 'U2F', + 'NOTIFICATIONS' => 'Informations', + 'SHARING' => 'Partage', + 'CHANGE_LOGIN' => 'Changer le nom d’utilisateur', + 'CHANGE_SORTING' => 'Changer le tri', + 'SET_DROPBOX' => 'Configurer Dropbox', + 'ABOUT_LYCHEE' => 'À propos de Lychee', + 'DIAGNOSTICS' => 'Diagnostics', + 'DIAGNOSTICS_GET_SIZE' => 'Calculer l’espace utilisé', + 'JOBS' => 'Historique des tâches', + 'LOGS' => 'Logs', + 'SIGN_OUT' => 'Déconnexion', + 'UPDATE_AVAILABLE' => 'Une mise à jour est disponible !', + 'MIGRATION_AVAILABLE' => 'Une migration est disponible !', + 'CHECK_FOR_UPDATE' => 'Vérifier les mises à jour', + 'DEFAULT_LICENSE' => 'License par defaut pour les nouveaux ajouts :', + 'SET_LICENSE' => 'Sélectionner une license', + 'SET_OVERLAY_TYPE' => 'Sélectionner le type d’Overlay', + 'SET_ALBUM_DECORATION' => 'Sélectionner les décorations des album', + 'SET_MAP_PROVIDER' => 'Sélectionner le fournisseur de données cartographiques', + 'FULL_SETTINGS' => 'Tous les paramètres', + 'UPDATE' => 'Mettre à jour', + 'RESET' => 'Réinitialiser', + 'DISABLE_TOKEN_TOOLTIP' => 'Désactiver', + 'ENABLE_TOKEN' => 'Activer le token d’API', + 'DISABLED_TOKEN_STATUS_MSG' => 'Désactiver', + 'TOKEN_BUTTON' => 'API Token...', + 'TOKEN_NOT_AVAILABLE' => 'Vous avez déjà vu ce token.', + 'TOKEN_WAIT' => 'Patientez...', + + 'SMART_ALBUMS' => 'Albums intelligents', + 'SHARED_ALBUMS' => 'Albums partagés', + 'ALBUMS' => 'Albums', + 'PHOTOS' => 'Photos', + 'SEARCH_RESULTS' => 'Résultats', + + 'RENAME' => 'Renommer', + 'RENAME_ALL' => 'Renommer la sélection', + 'MERGE' => 'Fusionner', + 'MERGE_ALL' => 'Fusionner la sélection', + 'MAKE_PUBLIC' => 'Rendre public', + 'SHARE_ALBUM' => 'Partager l’album', + 'SHARE_PHOTO' => 'Partager la photo', + 'VISIBILITY_ALBUM' => 'Visibilité de l’album', + 'VISIBILITY_PHOTO' => 'Visibilité de la Photo', + 'DOWNLOAD_ALBUM' => 'Télécharger l’album', + 'ABOUT_ALBUM' => 'À propos de l’album', + 'DELETE_ALBUM' => 'Supprimer l’album', + 'MOVE_ALBUM' => 'Déplacer l’album', + 'FULLSCREEN_ENTER' => 'Entrer en mode plein écran', + 'FULLSCREEN_EXIT' => 'Sortir du mode plein écran', + + 'SHARING_ALBUM_USERS' => 'Partager l’album avec des utilisateurs', + 'WAIT_FETCH_DATA' => 'Merci de patienter le temps que les données soient récupérées…', + 'SHARING_ALBUM_USERS_NO_USERS' => 'Il n’y pas d’utilisateurs avec qui partager cet album', + 'SHARING_ALBUM_USERS_LONG_MESSAGE' => 'Sélectionner les utilisateurs avec qui partager cet album', + + 'DELETE_ALBUM_QUESTION' => 'Supprimer l’album et ses photos', + 'KEEP_ALBUM' => 'Garder l’album', + 'DELETE_ALBUM_CONFIRMATION' => 'Voulez-vous vraiment supprimer l’album «%s» et toutes les photos qu’il contient ? Cette action est irréversible !', + + 'DELETE_TAG_ALBUM_QUESTION' => 'Supprimer l’album', + 'DELETE_TAG_ALBUM_CONFIRMATION' => 'Voulez-vous vraiment supprimer l’album «%s» (les photos qu’il contient ne seront pas supprimées)? Cette action est irréversible !', + + 'DELETE_ALBUMS_QUESTION' => 'Supprimer les albums et leurs photos', + 'KEEP_ALBUMS' => 'Conserver les albums', + 'DELETE_ALBUMS_CONFIRMATION' => 'Voulez-vous vraiment supprimer les %d albums selectionnés et toutes leurs photos ? Cette action est irréversible !', + + 'DELETE_UNSORTED_CONFIRM' => 'Voulez-vous vraiment supprimer toutes les photos de «Non-triés» ? Cette action est irréversible !', + 'CLEAR_UNSORTED' => 'Vider Non-triés', + 'KEEP_UNSORTED' => 'Conserver Non-triés', + + 'EDIT_SHARING' => 'Éditer le partage', + 'MAKE_PRIVATE' => 'Rendre privé', + + 'CLOSE_ALBUM' => 'Fermer l’album', + 'CLOSE_PHOTO' => 'Fermer la photo', + 'CLOSE_MAP' => 'Fermer la carte', + + 'ADD' => 'Ajouter', + 'MOVE' => 'Déplacer', + 'MOVE_ALL' => 'Déplacer la sélection', + 'DUPLICATE' => 'Dupliquer', + 'DUPLICATE_ALL' => 'Dupliquer la sélection', + 'COPY_TO' => 'Copier vers…', + 'COPY_ALL_TO' => 'Copier la sélection vers…', + 'DELETE' => 'Supprimer', + 'SAVE' => 'Sauvegarder', + 'DELETE_ALL' => 'Supprimer la sélection', + 'DOWNLOAD' => 'Télécharger', + 'DOWNLOAD_ALL' => 'Télécharger la sélection', + 'UPLOAD_PHOTO' => 'Ajouter une photo ou une vidéo', + 'IMPORT_LINK' => 'Importer depuis un lien', + 'IMPORT_DROPBOX' => 'Importer depuis Dropbox', + 'IMPORT_SERVER' => 'Importer depuis le serveur', + 'NEW_ALBUM' => 'Nouvel album', + 'NEW_TAG_ALBUM' => 'Nouvel album d’étiquette', + 'UPLOAD_TRACK' => 'Ajouter une trace', + 'DELETE_TRACK' => 'Supprimer la trace', + + 'TITLE_NEW_ALBUM' => 'Saisissez le titre du nouvel album :', + 'UNTITLED' => 'Sans titre', + 'UNSORTED' => 'Non-triés', + 'STARRED' => 'Favoris', + 'RECENT' => 'Récent', + 'PUBLIC' => 'Public', + 'ON_THIS_DAY' => 'Ce jour-là', + 'NUM_PHOTOS' => 'Photos', + + 'CREATE_ALBUM' => 'Créer un album', + 'CREATE_TAG_ALBUM' => 'Créer un album d’étiquette', + + 'STAR_PHOTO' => 'Mettre en Favoris', + 'STAR' => 'Favori', + 'UNSTAR' => 'Retirer des favoris', + 'STAR_ALL' => 'Marquer la sélection comme favoris', + 'UNSTAR_ALL' => 'Retirer la sélection des favoris', + 'TAG' => 'Tagger', + 'TAG_ALL' => 'Tagger la sélection', + 'UNSTAR_PHOTO' => 'Retirer des Favoris', + 'SET_COVER' => 'Changer la couverture de l’album', + 'REMOVE_COVER' => 'Supprimer la couverture de l’album', + 'SET_HEADER' => 'Définir comme image d’en-tête', + 'REMOVE_HEADER' => 'Supprimer l\image d’en-tête', + 'SET_COMPACT_HEADER' => 'Utiliser l’entête compacte', + + 'FULL_PHOTO' => 'Ouvrir l’original', + 'ABOUT_PHOTO' => 'À propos de la photo', + 'DISPLAY_FULL_MAP' => 'Carte', + 'DIRECT_LINK' => 'Lien direct', + 'DIRECT_LINKS' => 'Liens directs', + 'QR_CODE' => 'QR Code', + + 'ALBUM_ABOUT' => 'À propos', + 'ALBUM_BASICS' => 'Informations de base', + 'ALBUM_TITLE' => 'Titre', + 'ALBUM_COPYRIGHT' => 'Droits d’auteur', + 'ALBUM_SET_COPYRIGHT' => 'Définir les droits d’auteur', + 'ALBUM_NEW_TITLE' => 'Saisissez un nouveau titre pour cet album :', + 'ALBUMS_NEW_TITLE' => 'Saisissez un titre pour les %d albums sélectionnés :', + 'ALBUM_SET_TITLE' => 'Enregistrer le titre', + 'ALBUM_DESCRIPTION' => 'Description', + 'ALBUM_SHOW_TAGS' => 'Étiquettes à afficher', + 'ALBUM_NEW_DESCRIPTION' => 'Saisissez une nouvelle description pour cet album :', + 'ALBUM_SET_DESCRIPTION' => 'Choisir une description', + 'ALBUM_NEW_SHOWTAGS' => 'Saisissez les étiquettes des photos qui seront affichées dans cet album :', + 'ALBUM_SET_SHOWTAGS' => 'Afficher ces étiquettes', + 'ALBUM_ALBUM' => 'Album', + 'ALBUM_CREATED' => '', + 'ALBUM_IMAGES' => 'Images', + 'ALBUM_VIDEOS' => 'Videos', + 'ALBUM_SUBALBUMS' => 'Sous-albums', + 'ALBUM_SHARING' => 'Partager', + 'ALBUM_SHR_YES' => 'Oui', + 'ALBUM_SHR_NO' => 'Non', + 'ALBUM_PUBLIC' => 'Public', + 'ALBUM_PUBLIC_EXPL' => 'Les utilisateurs anonymes peuvent accéder à cet album, sous réserve des restrictions suivantes.', + 'ALBUM_FULL' => 'Originaux', + 'ALBUM_FULL_EXPL' => 'Les utilisateurs anonymes peuvent contempler des photos en pleine résolution.', + 'ALBUM_HIDDEN' => 'Masqué', + 'ALBUM_HIDDEN_EXPL' => 'Les utilisateurs anonymes ont besoin d’un lien direct pour accéder à cet album.', + 'ALBUM_MARK_NSFW' => 'Marquer cet album comme sensible', + 'ALBUM_UNMARK_NSFW' => 'Retirer la marque «album sensible»', + 'ALBUM_NSFW' => 'Sensible', + 'ALBUM_NSFW_EXPL' => 'Cet album contient des photos pouvant choquer les utilisateurs.', + 'ALBUM_DOWNLOADABLE' => 'Téléchargeable', + 'ALBUM_DOWNLOADABLE_EXPL' => 'Les utilisateurs anonymes peuvent télécharger cet album.', + 'ALBUM_SHARE_BUTTON_VISIBLE' => 'Visibilité du bouton de partage.', + 'ALBUM_SHARE_BUTTON_VISIBLE_EXPL' => 'Les utilisateurs anonymes peuvent voir les liens de partage sur les média sociaux.', + 'ALBUM_PASSWORD' => 'Mot de passe', + 'ALBUM_PASSWORD_PROT' => 'Protéger par un mot de passe.', + 'ALBUM_PASSWORD_PROT_EXPL' => 'Les utilisateurs anonymes ont besoin d’un mot de passe partagé pour accéder à cet album.', + 'ALBUM_PASSWORD_REQUIRED' => 'Cet album est protégé par un mot de passe. Entrez le mot de passe pour afficher les photos de cet album :', + 'ALBUM_MERGE' => 'Voulez-vous vraiment fusionner l’album «%1$s» dans l’album «%2$s» ?', // `dans` est important car il indique la direction du merge + 'ALBUMS_MERGE' => 'Voulez-vous vraiment fusionner les albums selectionnés avec l’album «%s»?', + 'MERGE_ALBUM' => 'Fusionner les albums', + 'DONT_MERGE' => 'Ne pas fusionner.', + 'ALBUM_MOVE' => 'Voulez-vous vraiment déplacer l’album «%1$s» dans l’album «%2$s» ?', + 'ALBUMS_MOVE' => 'Voulez-vous vraiment déplacer les albums selectionnés dans l’album «%s» ?', + 'MOVE_ALBUMS' => 'Déplacer les albums', + 'NOT_MOVE_ALBUMS' => 'Ne pas déplacer', + 'ROOT' => 'Albums', + 'ALBUM_REUSE' => 'Réutilisation', + 'ALBUM_LICENSE' => 'License', + 'ALBUM_SET_LICENSE' => 'Sélectionner une license', + 'ALBUM_LICENSE_HELP' => 'Un doute sur le choix ?', + 'ALBUM_LICENSE_NONE' => 'Aucune', + 'ALBUM_RESERVED' => 'Tous droits réservés', + 'ALBUM_SET_ORDER' => 'Changer l’ordre', + 'ALBUM_ORDERING' => 'Trier par', + 'ALBUM_PHOTO_ORDERING' => 'Trier les photos par', + 'ALBUM_CHILDREN_ORDERING' => 'Tier les sous-albums par', + 'ALBUM_OWNER' => 'Propriétaire', + + 'PHOTO_ABOUT' => 'À propos', + 'PHOTO_BASICS' => 'Informations de base', + 'PHOTO_TITLE' => 'Titre', + 'PHOTO_NEW_TITLE' => 'Entrer un nouveau titre pour cette photo :', + 'PHOTO_SET_TITLE' => 'Choisir un titre', + 'PHOTO_UPLOADED' => 'Téléversé', + 'PHOTO_DESCRIPTION' => 'Description', + 'PHOTO_NEW_DESCRIPTION' => 'Saisissez une nouvelle description pour cette photo :', + 'PHOTO_SET_DESCRIPTION' => 'Choisir une description', + 'PHOTO_NEW_LICENSE' => 'Ajouter une License', + 'PHOTO_SET_LICENSE' => 'Sélectionner License', + 'PHOTO_LICENSE' => 'License', + 'PHOTO_LICENSE_HELP' => 'Un doute sur le choix ?', + 'PHOTO_REUSE' => 'Réutilisation', + 'PHOTO_LICENSE_NONE' => 'Aucune', + 'PHOTO_RESERVED' => 'Tous droits réservés', + 'PHOTO_LATITUDE' => 'Latitude', + 'PHOTO_LONGITUDE' => 'Longitude', + 'PHOTO_ALTITUDE' => 'Altitude', + 'PHOTO_IMGDIRECTION' => 'Direction', + 'PHOTO_LOCATION' => 'Localisation', + 'PHOTO_IMAGE' => 'Image', + 'PHOTO_VIDEO' => 'Vidéo', + 'PHOTO_SIZE' => 'Dimension', + 'PHOTO_FORMAT' => 'Format', + 'PHOTO_RESOLUTION' => 'Résolution', + 'PHOTO_DURATION' => 'Durée', + 'PHOTO_FPS' => 'Fréquence', + 'PHOTO_TAGS' => 'Étiquettes', + 'PHOTO_NOTAGS' => 'Pas d’étiquette', + 'PHOTO_NEW_TAGS' => 'Saisissez vos étiquettes pour cette photo. Vous pouvez ajouter plusieurs étiquettes en les séparant avec une virgule :', + 'PHOTOS_NEW_TAGS' => 'Saisissez vos étiquettes pour toutes les %d photos selectionnées. Les tags existants seront remplacés. Vous pouvez ajouter plusieurs tags en les séparant avec une virgule :', + 'PHOTO_SET_TAGS' => 'Établir les étiquettes', + 'PHOTO_CAMERA' => 'Appareil', + 'PHOTO_CAPTURED' => 'Date de prise de vue', + 'PHOTO_MAKE' => 'Marque', + 'PHOTO_TYPE' => 'Modèle', + 'PHOTO_LENS' => 'Objectif', + 'PHOTO_SHUTTER' => 'Durée d’exposition', + 'PHOTO_APERTURE' => 'Ouverture', + 'PHOTO_FOCAL' => 'Distance focale', + 'PHOTO_ISO' => 'ISO %s', + 'PHOTO_SHARING' => 'Partager', + 'PHOTO_DELETE' => 'Supprimer la photo', + 'PHOTO_KEEP' => 'Garder la photo', + 'PHOTO_DELETE_CONFIRMATION' => 'Voulez-vous vraiment supprimer la photo «%s» ? Cette action est irréversible !', + 'PHOTO_DELETE_ALL' => 'Voulez-vous vraiment supprimer toutes les %d photos sélectionnées ? Cette action est irréversible !', + 'PHOTOS_NEW_TITLE' => 'Entrer un titre pour toutes les %d photos sélectionnées :', + 'PHOTO_MAKE_PRIVATE_ALBUM' => 'Cette photo est située dans un album public. Pour rendre cette photo privée ou publique, modifiez la visibilité de l’album associé.', + 'PHOTO_SHOW_ALBUM' => 'Afficher l’album', + 'PHOTO_PUBLIC' => 'Public', + 'PHOTO_PUBLIC_EXPL' => 'Les utilisateurs anonymes peuvent visualiser cette photo sous réserve des restrictions suivantes.', + 'PHOTO_FULL' => 'Originale', + 'PHOTO_FULL_EXPL' => 'Les utilisateurs anonymes peuvent voir la photo en pleine résolution.', + 'PHOTO_HIDDEN' => 'Cachée.', + 'PHOTO_HIDDEN_EXPL' => 'Les utilisateurs anonymes ont besoin d’un lien direct pour voir cette photo.', + 'PHOTO_DOWNLOADABLE' => 'Téléchargeable.', + 'PHOTO_DOWNLOADABLE_EXPL' => 'Les utilisateurs anonymes peuvent télécharger cette photo.', + 'PHOTO_SHARE_BUTTON_VISIBLE' => 'Visibilité du bouton de partage', + 'PHOTO_SHARE_BUTTON_VISIBLE_EXPL' => 'Les utilisateurs anonymes peuvent voir les liens de partage sur les média sociaux.', + 'PHOTO_PASSWORD_PROT' => 'Protéger par un mot de passe.', + 'PHOTO_PASSWORD_PROT_EXPL' => 'Les utilisateurs anonymes ont besoin d’un mot de passe partagé pour voir cette photo.', + 'PHOTO_EDIT_SHARING_TEXT' => 'Les propriétés de partages de cette photo seront changées pour les photos suivantes :', + 'PHOTO_NO_EDIT_SHARING_TEXT' => 'Parce que cette photo est dans un album public, elle hérite des propriétés de partage de l’album. Sa visibilité est indiquée ci-dessous pour votre information.', + 'PHOTO_EDIT_GLOBAL_SHARING_TEXT' => 'La visibilité de cette photo est ajustable avec les paramètres généraux de Lychee. Sa visibilité est indiquée ci-dessous pour information.', + 'PHOTO_NEW_CREATED_AT' => 'Saisissez la date de téléversement de cette photo. mm/dd/yyyy, hh:mm [am/pm]', + 'PHOTO_SET_CREATED_AT' => 'Changer la date', + + 'LOADING' => 'Chargement en cours', + 'ERROR' => 'Erreur', + 'ERROR_TEXT' => 'Une erreur est survenue. Veuillez rafraichir la page et réessayer !', + 'ERROR_UNKNOWN' => 'Une erreur inattendue est survenue. Veuillez réessayer et vérifier votre installation et votre serveur. Consultez le fichier Readme pour obtenir plus d’informations.', + 'ERROR_MAP_DEACTIVATED' => 'La carte a été désactivée dans les paramètres.', + 'ERROR_SEARCH_DEACTIVATED' => 'La recherche a été désactivée dans les paramètres.', + 'SUCCESS' => 'OK', + 'CHANGE_SUCCESS' => 'Changement réussi.', + 'RETRY' => 'Réessayer', + 'OVERRIDE' => 'Override', + 'TAGS_OVERRIDE_INFO' => 'Si décoché, les tags seront ajoutés aux tags de la photo.', + + 'SETTINGS_SUCCESS_LOGIN' => 'Informations de connexion mise à jour.', + 'SETTINGS_SUCCESS_SORT' => 'Ordre d’affichage mis à jour.', + 'SETTINGS_SUCCESS_DROPBOX' => 'Clé Dropbox mise à jour.', + 'SETTINGS_SUCCESS_LANG' => 'Langue mis à jour.', + 'SETTINGS_SUCCESS_LAYOUT' => 'Affichage mis à jour.', + 'SETTINGS_SUCCESS_IMAGE_OVERLAY' => 'Overlay EXIF mis à jour.', + 'SETTINGS_SUCCESS_PUBLIC_SEARCH' => 'Recherche publique mise à jour.', + 'SETTINGS_SUCCESS_LICENSE' => 'License par défaut mise à jour.', + 'SETTINGS_SUCCESS_MAP_DISPLAY' => 'Paramètres de la carte mis à jour.', + 'SETTINGS_SUCCESS_MAP_DISPLAY_PUBLIC' => 'Paramètres de la carte pour les albums publics mis à jour.', + 'SETTINGS_SUCCESS_MAP_PROVIDER' => 'Fournisseur de la carte mis à jour.', + 'SETTINGS_SUCCESS_CSS' => 'CSS mise à jour.', + 'SETTINGS_SUCCESS_JS' => 'JS mise à jour.', + 'SETTINGS_SUCCESS_UPDATE' => 'Paramètres mis à jour avec succes.', + 'SETTINGS_DROPBOX_KEY' => 'API Key Dropbox', + 'SETTINGS_ADVANCED_WARNING_EXPL' => 'Changer les paramètres avancés peut influencer la stabilité, la securité et les performances de Lychee. Modifiez à vos risques et périls.', + 'SETTINGS_ADVANCED_SAVE' => 'Sauvegarder mes modifications, j’accepte le risque !', + + 'U2F_NOT_SUPPORTED' => 'U2F non suporté. Desolé.', + 'U2F_NOT_SECURE' => 'Environment non sécurisé. U2F indisponible.', + 'U2F_REGISTER_KEY' => 'Enregistrer une nouvelle clé.', + 'U2F_REGISTRATION_SUCCESS' => 'Enregistrement réussi !', + 'U2F_AUTHENTIFICATION_SUCCESS' => 'Authentification réussie !', + 'U2F_CREDENTIALS' => 'Clés', + 'U2F_CREDENTIALS_DELETED' => 'Clé supprimée !', + 'U2F_LOGIN' => 'Connexion avec WebAuthn', + + 'NEW_PHOTOS_NOTIFICATION' => 'Envoyer les notifications de nouvelles photos par emails.', + 'SETTINGS_SUCCESS_NEW_PHOTOS_NOTIFICATION' => 'Notification de nouvelles photos mise à jour', + 'USER_EMAIL_INSTRUCTION' => 'Ajouter votre adresse email ci-dessous pour recevoir des notifications. Pour arrêter de recevoir des emails, enlever simplement votre adresse ci-dessous.', + + 'LOGIN_USERNAME' => 'Nouvel utilisateur', + 'LOGIN_PASSWORD' => 'Nouveau mot de passe', + 'LOGIN_PASSWORD_CONFIRM' => 'Confirmez le mot de passe', + 'PASSWORD_TITLE' => 'Entrez votre mot de passe actuel:', + 'PASSWORD_CURRENT' => 'Mot de passe actuel :', + 'PASSWORD_TEXT' => 'Vos identifiants seront modifiés comme suit :', + 'PASSWORD_CHANGE' => 'Modifier les informations de connexion', + + 'EDIT_SHARING_TITLE' => 'Modifier le partage', + 'EDIT_SHARING_TEXT' => 'Les propriétés de partage de cet album vont être modifiées comme suit :', + 'SHARE_ALBUM_TEXT' => 'Cet album sera partagé avec les propriétés suivantes :', + + 'SORT_DIALOG_ATTRIBUTE_LABEL' => 'Attribut', + 'SORT_DIALOG_ORDER_LABEL' => 'Ordre', + + 'SORT_ALBUM_BY' => 'Trier les albums %1$s dans l’ordre %2$s.', + + 'SORT_ALBUM_SELECT_1' => 'Heure de création', + 'SORT_ALBUM_SELECT_2' => 'Titre', + 'SORT_ALBUM_SELECT_3' => 'Description', + 'SORT_ALBUM_SELECT_5' => 'Prise de vue la plus récente', + 'SORT_ALBUM_SELECT_6' => 'Prise de vue la plus ancienne', + + 'SORT_PHOTO_BY' => 'Trier les photos %1$s dans l’ordre %2$s.', + + 'SORT_PHOTO_SELECT_1' => 'Date de téléversement', + 'SORT_PHOTO_SELECT_2' => 'Date de prise de vue', + 'SORT_PHOTO_SELECT_3' => 'Titre', + 'SORT_PHOTO_SELECT_4' => 'Description', + 'SORT_PHOTO_SELECT_6' => 'Favoris', + 'SORT_PHOTO_SELECT_7' => 'Format de la photo', + + 'SORT_ASCENDING' => 'Croissant', + 'SORT_DESCENDING' => 'Décroissant', + 'SORT_CHANGE' => 'Modifier le tri', + + 'DROPBOX_TITLE' => 'Définir une clé Dropbox', + 'DROPBOX_TEXT' => 'Pour pouvoir importer des photos à partir de votre Dropbox, vous aurez besoin d’une clé d’application «drop-ins» valide à créer sur leur site. Générez votre clé personnelle puis saisissez-la ci-dessous :', + + 'LANG_TEXT' => 'Changer la langue de Lychee par :', + 'LANG_TITLE' => 'Changer la langue', + + 'SETTING_RECENT_PUBLIC_TEXT' => 'Rendre l’album "Recent" accessible à tous publics', + 'SETTING_STARRED_PUBLIC_TEXT' => 'Rendre l’album "Favoris" accessible à tous publics', + 'SETTING_ONTHISDAY_PUBLIC_TEXT' => 'Rendre l’album "Ce jour-là" accessible à tous publics', + + 'CSS_TEXT' => 'Personnaliser le CSS :', + 'CSS_TITLE' => 'Personnaliser le CSS', + 'JS_TEXT' => 'Personnaliser le JS:', + 'JS_TITLE' => 'Personnaliser le code JS', + 'PUBLIC_SEARCH_TEXT' => 'Recherche publique autorisée :', + 'OVERLAY_TYPE' => 'Informations à utiliser pour l’overlay :', + 'OVERLAY_NONE' => 'Pas d’overlay', + 'OVERLAY_EXIF' => 'Informations EXIF', + 'OVERLAY_DESCRIPTION' => 'Description de la photo', + 'OVERLAY_DATE' => 'Date de la photo', + 'ALBUM_DECORATION' => 'Décorations des album:', + 'ALBUM_DECORATION_NONE' => 'Pas de décorations', + 'ALBUM_DECORATION_ORIGINAL' => 'Montrer la présence de sous-album', + 'ALBUM_DECORATION_ALBUM' => 'Montrer le nombre de sous-album', + 'ALBUM_DECORATION_PHOTO' => 'Montrer le nombre de photos', + 'ALBUM_DECORATION_ALL' => 'Montrer le nombre de sous-album et de photos', + 'ALBUM_DECORATION_ORIENTATION' => 'Orientation des décorations:', + 'ALBUM_DECORATION_ORIENTATION_ROW' => 'Horizontale (photos, albums)', + 'ALBUM_DECORATION_ORIENTATION_ROW_REVERSE' => 'Horizontale (albums, photos)', + 'ALBUM_DECORATION_ORIENTATION_COLUMN' => 'Verticale (photos au-dessus, albums)', + 'ALBUM_DECORATION_ORIENTATION_COLUMN_REVERSE' => 'Verticale (albums au-dessus, photos)', + 'MAP_DISPLAY_TEXT' => 'Activer les cartes (fournies par OpenStreetMap):', + 'MAP_DISPLAY_PUBLIC_TEXT' => 'Activer les cartes pour les albums publics (fournies par OpenStreetMap):', + 'MAP_PROVIDER' => 'Fournisseur de cartes OpenStreetMap :', + 'MAP_PROVIDER_WIKIMEDIA' => 'Wikimedia', + 'MAP_PROVIDER_OSM_ORG' => 'OpenStreetMap.org (no HiDPI)', + 'MAP_PROVIDER_OSM_DE' => 'OpenStreetMap.de (no HiDPI)', + 'MAP_PROVIDER_OSM_FR' => 'OpenStreetMap.fr (no HiDPI)', + 'MAP_PROVIDER_RRZE' => 'University de Erlangen, Allemagne (only HiDPI)', + 'MAP_INCLUDE_SUBALBUMS_TEXT' => 'Inclure les photos des sous-albums sur la carte :', + 'LOCATION_DECODING' => 'Convertir les informations GPS en nom de localisation', + 'LOCATION_SHOW' => 'Montrer le nom de la localisation', + 'LOCATION_SHOW_PUBLIC' => 'Montrer le nom de la localisation en mode public', + + 'LAYOUT_TYPE' => 'Affichage des photos :', + 'LAYOUT_SQUARES' => 'Miniatures carrées', + 'LAYOUT_JUSTIFIED' => 'Mode proportionnel, justifié', + 'LAYOUT_MASONRY' => 'Mode proportionnel, en maçonnerie', + 'LAYOUT_GRID' => 'Mode proportionnel, en grille', + 'LAYOUT_UNJUSTIFIED' => 'mode proportionnel, non-justifié', + 'SET_LAYOUT' => 'Modifier l’affichage', + + 'NSFW_VISIBLE_TEXT_1' => 'Rendre les albums sensibles visibles par défaut.', + 'NSFW_VISIBLE_TEXT_2' => 'Si l’album est public, il est toujours accessible, juste masqué et peut-être révélé avec la touche H.', + 'SETTINGS_SUCCESS_NSFW_VISIBLE' => 'Visibilité par default des albums sensible mis à jour.', + + 'NSFW_BANNER' => '

Contenu sensible

Cet album contient du contenu sensible qui peut déranger ou offenser certaines personnes.

Cliquer pour accepter.

', + 'NSFW_HEADER' => 'Contenu Sensible', + 'NSFW_EXPLANATION' => 'Cet album contient des contenus sensible que certaines personnes peuvent trouver choquant ou offensant.', + 'TAP_CONSENT' => 'Cliquer pour accepter.', + + 'VIEW_NO_RESULT' => 'Aucun résultat', + 'VIEW_NO_PUBLIC_ALBUMS' => 'Aucun album public', + 'VIEW_NO_CONFIGURATION' => 'Aucune configuration', + 'VIEW_PHOTO_NOT_FOUND' => 'Photo introuvable', + + 'NO_TAGS' => 'Aucun tag', + + 'UPLOAD_MANAGE_NEW_PHOTOS' => 'Vous pouvez désormais gérer vos nouvelles photos.', + 'UPLOAD_COMPLETE' => 'Téléversement terminé', + 'UPLOAD_COMPLETE_FAILED' => 'Le téléversement d’une ou plusieurs photos a échoué.', + 'UPLOAD_IMPORTING' => 'Importation', + 'UPLOAD_IMPORTING_URL' => 'Importation depuis l’URL', + 'UPLOAD_UPLOADING' => 'Téléversement en cours', + 'UPLOAD_FINISHED' => 'Terminé', + 'UPLOAD_PROCESSING' => 'Traitement', + 'UPLOAD_FAILED' => 'Échec', + 'UPLOAD_FAILED_ERROR' => 'Le téléversement a échoué. Le serveur a retourné une erreur !', + 'UPLOAD_FAILED_WARNING' => 'Le téléversement a échoué. Le serveur a retourné un avertissement !', + 'UPLOAD_CANCELLED' => 'Annulé', + 'UPLOAD_SKIPPED' => 'Ignoré', + 'UPLOAD_UPDATED' => 'Mis à jour', + 'UPLOAD_GENERAL' => 'General', + 'UPLOAD_IMPORT_SKIPPED_DUPLICATE' => 'Cette photo a été sautée parce qu’elle est deja dans votre galerie.', + 'UPLOAD_IMPORT_RESYNCED_DUPLICATE' => 'Cette photo a été sautée parce qu’elle est deja dans votre galerie mais ses metadatas ont été mises à jour.', + 'UPLOAD_ERROR_CONSOLE' => 'Veuillez consulter la console de votre navigateur pour obtenir plus de détails.', + 'UPLOAD_UNKNOWN' => 'Le serveur a retourné une reponse inconnue. Veuillez consulter la console de votre navigateur pour obtenir plus de détails.', + 'UPLOAD_ERROR_UNKNOWN' => 'Le téléversement a échoué. Le serveur a retourné une erreur inconnue !', + 'UPLOAD_ERROR_POSTSIZE' => 'Le téléversement a échoué. La valeur PHP post_max_size est peut-être trop petit, sinon consultez la FAQ.', + 'UPLOAD_ERROR_FILESIZE' => 'Le téléversement a échoué. La valeur PHP upload_max_filesize est peut-être trop petit, sinon consultez la FAQ.', + 'UPLOAD_IN_PROGRESS' => 'Lychee est en cours de téléchargement !', + 'UPLOAD_IMPORT_WARN_ERR' => 'L’importation est terminée mais des erreurs ou des avertissements ont été retournés. Veuillez consulter le fichier de Log (Paramètres -> Afficher les logs) pour obtenir plus de détails.', + 'UPLOAD_IMPORT_COMPLETE' => 'Importation terminée', + 'UPLOAD_IMPORT_INSTR' => 'Veuillez entrer un lien direct vers une photo pour l’importer :', + 'UPLOAD_IMPORT' => 'Importer', + 'UPLOAD_IMPORT_SERVER' => 'Importation à partir du serveur', + 'UPLOAD_IMPORT_SERVER_FOLD' => 'Dossier vide ou aucun fichier lisible à traiter. Veuillez consulter le journal (Paramètres -> Afficher le journal) pour obtenir plus de détails.', + 'UPLOAD_IMPORT_SERVER_INSTR' => 'Cette action importera toutes les photos ainsi que tous les dossiers et sous-dossiers situés dans les répertoires suivants séparés par des espaces. Utilisez \\ pour les espaces dans les chemins.', + 'UPLOAD_ABSOLUTE_PATH' => 'Chemins absolus des répertoires, séparés par des espaces', + 'UPLOAD_IMPORT_SERVER_EMPT' => 'Impossible de démarrer l’importation car le dossier était vide !', + 'UPLOAD_IMPORT_DELETE_ORIGINALS' => 'Supprimer les originaux', + 'UPLOAD_IMPORT_DELETE_ORIGINALS_EXPL' => 'Les fichiers originaux seront supprimés après l’importation lorsque cela est possible.', + 'UPLOAD_IMPORT_VIA_SYMLINK' => 'Liens symboliques', + 'UPLOAD_IMPORT_VIA_SYMLINK_EXPL' => 'Importer les fichier en utilisant des liens symboliques vers les originaux.', + 'UPLOAD_IMPORT_SKIP_DUPLICATES' => 'Sauter les doublons', + 'UPLOAD_IMPORT_SKIP_DUPLICATES_EXPL' => 'Les médias déjà existants ne seront pas importés.', + 'UPLOAD_IMPORT_RESYNC_METADATA' => 'Mettre à jour les metadatas', + 'UPLOAD_IMPORT_RESYNC_METADATA_EXPL' => 'Mettre à jour les metadatas des fichiers existants.', + 'UPLOAD_IMPORT_LOW_MEMORY_EXPL' => 'Le processus d’importation du serveur approche la limite de la mémoire disponible et peut être terminé prématurément.', + 'UPLOAD_WARNING' => 'Attention', + 'UPLOAD_IMPORT_NOT_A_DIRECTORY' => 'Le chemin fourni n’est pas un reportoire lisible !', + 'UPLOAD_IMPORT_PATH_RESERVED' => 'Le chemin fourni est reservé à Lychee !', + 'UPLOAD_IMPORT_FAILED' => 'Impossible d’importer le fichier !', + 'UPLOAD_IMPORT_UNSUPPORTED' => 'Type de fichier non supporté !', + 'UPLOAD_IMPORT_CANCELLED' => 'Import annulé', + + 'ABOUT_SUBTITLE' => 'Hébergement personnalisé de photo à votre façon !', + 'ABOUT_DESCRIPTION' => 'Lychee est une outil de gestion de galerie gratuit qui fonctionne sur votre propre serveur. L’installation est rapide. Uploadez, gérez et partagez vos photos comme avec une application propre. Lychee vous fourni tout ce dont vous avez besoin et vos photos sont stockées en sécurité chez vous.', + 'FOOTER_COPYRIGHT' => 'Toutes les images de ce site Web sont protégées par le droit d’auteur par %1$s © %2$s', + 'HOSTED_WITH_LYCHEE' => 'Hébergé par Lychee', + + 'URL_COPY_TO_CLIPBOARD' => 'Copier dans le presse-papier', + 'URL_COPIED_TO_CLIPBOARD' => 'l’URL a été copiée dans le presse-papier !', + 'PHOTO_DIRECT_LINKS_TO_IMAGES' => 'Liens directs pour les fichiers de l’image :', + 'PHOTO_ORIGINAL' => 'Taille originale', + 'PHOTO_MEDIUM' => 'Taille moyenne', + 'PHOTO_MEDIUM_HIDPI' => 'Taille moyenne HiDPI', + 'PHOTO_SMALL' => 'Petite taille', + 'PHOTO_SMALL_HIDPI' => 'Petite taille HiDPI', + 'PHOTO_THUMB' => 'Mignature carrée', + 'PHOTO_THUMB_HIDPI' => 'Miniature carrée HiDPI', + 'PHOTO_PLACEHOLDER' => 'Miniature de 32 bit"', + 'PHOTO_THUMBNAIL' => 'Photo miniature', + 'PHOTO_LIVE_VIDEO' => 'Partie vidéo d’une live-photo', + 'PHOTO_VIEW' => 'Vue photo de Lychee :', + + 'PHOTO_EDIT_ROTATECWISE' => 'Pivoter dans le sens des aiguilles d’une montre.', + 'PHOTO_EDIT_ROTATECCWISE' => 'Pivoter dans le sens inverse des aiguilles d’une montre.', + + 'ERROR_GPX' => 'Erreur lors du chargement du fichier GPX : ', + 'ERROR_EITHER_ALBUMS_OR_PHOTOS' => 'Merci de sélectionner soit des albums, soit des photos !', + 'ERROR_COULD_NOT_FIND' => 'Nous ne trouvons pas ce que vous cherchez.', + 'ERROR_INVALID_EMAIL' => 'Email non valide.', + 'EMAIL_SUCCESS' => 'Email mis à jour !', + 'ERROR_PHOTO_NOT_FOUND' => 'Erreur : photo %s non trouvée !', + 'ERROR_EMPTY_USERNAME' => 'Le nom d utilisateur ne peut être vide.', + 'ERROR_PASSWORD_DOES_NOT_MATCH' => 'Le nouveau mot de passe ne correspond pas.', + 'ERROR_EMPTY_PASSWORD' => 'Le nouveau mot de passe ne peut pas être vide.', + 'ERROR_SELECT_ALBUM' => 'Sélectionnez un album à partager !', + 'ERROR_SELECT_USER' => 'Sélectionnez un utilisateur avec qui partager !', + 'ERROR_SELECT_SHARING' => 'Sélectionnez un partage à retirer !', + 'SHARING_SUCCESS' => 'Partage mis à jour !', + 'SHARING_REMOVED' => 'Partage supprimé !', + 'USER_CREATED' => 'Utilisateur créé !', + 'USER_DELETED' => 'Utilisateur supprimé !', + 'USER_UPDATED' => 'Utilisateur mis à jour !', + 'ENTER_EMAIL' => 'Entrer une address email :', + 'ERROR_ALBUM_JSON_NOT_FOUND' => 'Erreur : Album JSON non trouvé !', + 'ERROR_ALBUM_NOT_FOUND' => 'Erreur : album %s non trouvé', + 'ERROR_DROPBOX_KEY' => 'Erreur : la clé Dropbox est vide', + 'ERROR_SESSION' => 'La session a expiré.', + 'CAMERA_DATE' => 'Camera date', + 'NEW_PASSWORD' => 'Nouveau mot de passe', + 'ALLOW_UPLOADS' => 'Autoriser les téléversements', + 'ALLOW_USER_SELF_EDIT' => 'Autoriser l’auto-gestion de compte utilisateur', + 'OSM_CONTRIBUTORS' => 'Contributeur OpenStreetMap', +]; diff --git a/lang/fr/maintenance.php b/lang/fr/maintenance.php new file mode 100644 index 00000000000..3df6d002f01 --- /dev/null +++ b/lang/fr/maintenance.php @@ -0,0 +1,59 @@ + 'Maintenance', + 'description' => 'Vous trouverez sur cette page toutes les actions nécessaires au bon fonctionment de Lychee.', + 'cleaning' => [ + 'title' => 'Nettoyer %s', + 'result' => '%s supprimé.', + 'description' => 'Supprimer le contenu de %s', + 'button' => 'Nettoyer', + ], + 'fix-jobs' => [ + 'title' => 'Réparer l’historique des Jobs', + 'description' => 'Marquer les jobs avec status %s ou %s comme %s.', + 'button' => 'Réparer l’historique', + ], + 'gen-sizevariants' => [ + 'title' => '%s manquants', + 'description' => 'Nous avons trouvé %d %s qui peuvent être générés.', + 'button' => 'Générer !', + 'success' => 'Nous avons créé %d %s avec succès.', + ], + 'fill-filesize-sizevariants' => [ + 'title' => 'Tailles de fichiers manquantes', + 'description' => '%d petites variantes sans taille de fichier trouvées.', + 'button' => 'Récuperer les données !', + 'success' => 'Succès du calcul des tailles de %d petites variantes.', + ], + 'fix-tree' => [ + 'title' => 'Statistique d’arbres', + 'Oddness' => 'Imparité', + 'Duplicates' => 'Duplicata', + 'Wrong parents' => 'Mauvais parents', + 'Missing parents' => 'Parents manquants', + 'button' => 'Fix tree', + ], + 'optimize' => [ + 'title' => 'Optimisation de la base de donnée', + 'description' => 'Si vous remarquez que votre installation est devenue lente, il est possible que votre base de donnée n’ait pas les index requis.', + 'button' => 'Optimiser la base de donnée', + ], + 'update' => [ + 'title' => 'Mises à jour', + 'check-button' => 'Vérifier les mise-à-jour', + 'update-button' => 'Mettre à jour', + 'no-pending-updates' => 'Aucune mise-à-jour disponible', + ], +]; diff --git a/lang/fr/profile.php b/lang/fr/profile.php new file mode 100644 index 00000000000..cc24b97452c --- /dev/null +++ b/lang/fr/profile.php @@ -0,0 +1,64 @@ + 'Profile', + + 'login' => [ + 'header' => 'Profile', + 'enter_current_password' => 'Enter your current password:', + 'current_password' => 'Current password', + 'credentials_update' => 'Your credentials will be changed to the following:', + 'username' => 'Username', + 'new_password' => 'New password', + 'confirm_new_password' => 'Confirm new password', + 'email_instruction' => 'Add your email below to enable receiving email notifications. To stop receiving emails, simply remove your email below.', + 'email' => 'Email', + 'change' => 'Change Login', + 'api_token' => 'API Token ...', + + 'missing_fields' => 'Missing fields', + ], + + 'token' => [ + 'unavailable' => 'You have already viewed this token.', + 'no_data' => 'No token API have been generated.', + 'disable' => 'Disable', + 'disabled' => 'Token disabled', + 'warning' => 'This token will not be displayed again. Copy it and keep it in a safe place.', + 'reset' => 'Reset the token', + 'create' => 'Create a new token', + ], + + 'oauth' => [ + 'header' => 'OAuth', + 'header_not_available' => 'OAuth is not available', + 'setup_env' => 'Set up the credentials in your .env', + 'token_registered' => '%s token registered.', + 'setup' => 'Set up %s', + 'reset' => 'reset', + 'credential_deleted' => 'Credential deleted!', + ], + + 'u2f' => [ + 'header' => 'Passkey/MFA/2FA', + 'info' => 'This only provides the ability to use WebAuthn to authenticate instead of username & password.', + 'empty' => 'Credentials list is empty!', + 'not_secure' => 'Environment not secured. U2F not available.', + 'new' => 'Register new device.', + 'credential_deleted' => 'Credential deleted!', + 'credential_updated' => 'Credential updated!', + 'credential_registred' => 'Registration successful!', + '5_chars' => 'At least 5 chars.', + ], +]; \ No newline at end of file diff --git a/lang/fr/settings.php b/lang/fr/settings.php new file mode 100644 index 00000000000..fd197f11135 --- /dev/null +++ b/lang/fr/settings.php @@ -0,0 +1,92 @@ + 'Settings', + 'small_screen' => 'For better a experience on the Settings page,
we recommend you use a larger screen.', + 'tabs' => [ + 'basic' => 'Basic', + 'all_settings' => 'All settings', + ], + 'toasts' => [ + 'change_saved' => 'Change saved!', + 'details' => 'Settings have been modified as per request', + 'error' => 'Error!', + 'error_load_css' => 'Could not load dist/user.css', + 'error_load_js' => 'Could not load dist/custom.js', + 'error_save_css' => 'Could not save CSS', + 'error_save_js' => 'Could not save JS', + 'thank_you' => 'Thank you for your support.', + 'reload' => 'Reload your page for full functionalities.', + ], + 'system' => [ + 'header' => 'System', + 'use_dark_mode' => 'Use dark mode for Lychee', + 'language' => 'Language used by Lychee', + 'nsfw_album_visibility' => 'Make Sensitive albums visible by default.', + 'nsfw_album_explanation' => 'If the album is public, it is still accessible, just hidden from the view and can be revealed by pressing H.', + ], + 'lychee_se' => [ + 'header' => 'Lychee SE', + 'call4action' => 'Get exclusive features and support the development of Lychee. Unlock the SE edition.', + 'preview' => 'Enable preview of Lychee SE features', + 'hide_call4action' => 'Hide this Lychee SE registration form. I am happy with Lychee as-is. :)', + 'hide_warning' => 'If enabled, the only way to register your license key will be via the More tab above. Changes are applied on page reload.', + ], + 'dropbox' => [ + 'header' => 'Dropbox', + 'instruction' => 'In order to import photos from your Dropbox, you need a valid drop-ins app key from their website.', + 'api_key' => 'Dropbox API Key', + 'set_key' => 'Set Dropbox Key', + ], + 'gallery' => [ + 'header' => 'Gallery', + 'photo_order_column' => 'Default column used for sorting photos', + 'photo_order_direction' => 'Default order used for sorting photos', + 'album_order_column' => 'Default column used for sorting albums', + 'album_order_direction' => 'Default order used for sorting albums', + 'aspect_ratio' => 'Default aspect ratio for album thumbs', + 'photo_layout' => 'Layout for pictures', + 'album_decoration' => 'Show decorations on album cover (sub-album and/or photo count)', + 'album_decoration_direction' => 'Align album decorations horizontally or vertically', + 'photo_overlay' => 'Default image overlay information', + 'license_default' => 'Default license used for albums', + 'license_help' => 'Need help choosing?', + ], + 'geolocation' => [ + 'header' => 'Geo-location', + 'map_display' => 'Display the map given GPS coordinates', + 'map_display_public' => 'Allow anonymous users to access the map', + 'map_provider' => 'Defines the map provider', + 'map_include_subalbums' => 'Includes pictures of the sub albums on the map', + 'location_decoding' => 'Use GPS location decoding', + 'location_show' => 'Show location extracted from GPS coordinates', + 'location_show_public' => 'Anonymous users can access the extracted location from GPS coordinates', + ], + 'advanced' => [ + 'header' => 'Advanced Customization', + 'change_css' => 'Change CSS', + 'change_js' => 'Change JS', + ], + 'all' => [ + 'old_setting_style' => 'Old setting style', + 'change_detected' => 'Some settings changed.', + 'save' => 'Save', + ], + + 'tool_option' => [ + 'disabled' => 'disabled', + 'enabled' => 'enabled', + 'discover' => 'discover', + ], +]; \ No newline at end of file diff --git a/lang/fr/sharing.php b/lang/fr/sharing.php new file mode 100644 index 00000000000..69de18cc6d0 --- /dev/null +++ b/lang/fr/sharing.php @@ -0,0 +1,33 @@ + 'Sharing', + + 'info' => 'This page gives an overview of and the ability to edit the sharing rights associated with albums.', + 'album_title' => 'Album title', + 'username' => 'Username', + 'no_data' => 'Sharing list is empty.', + 'share' => 'Share', + 'permission_deleted' => 'Permission deleted!', + 'permission_created' => 'Permission created!', + + 'grants' => [ + 'read' => 'Grants read access', + 'original' => 'Grants access to original photo', + 'download' => 'Grants download', + 'upload' => 'Grants upload', + 'edit' => 'Grants edit', + 'delete' => 'Grants delete', + ], +]; \ No newline at end of file diff --git a/lang/fr/statistics.php b/lang/fr/statistics.php new file mode 100644 index 00000000000..2baf855bbd5 --- /dev/null +++ b/lang/fr/statistics.php @@ -0,0 +1,34 @@ + 'Statistics', + + 'preview_text' => 'This is a preview of the statistics page available in Lychee SE.
The data shown here are randomly generated and do not reflect your server.', + 'no_data' => 'User does not have data on server.', + 'collapse' => 'Collapse albums sizes', + + 'total' => [ + 'total' => 'Total', + 'albums' => 'Albums', + 'photos' => 'Photos', + 'size' => 'Size', + ], + 'table' => [ + 'username' => 'Owner', + 'title' => 'Title', + 'photos' => 'Photos', + 'descendants' => 'Children', + 'size' => 'Size', + ], +]; \ No newline at end of file diff --git a/lang/fr/toasts.php b/lang/fr/toasts.php new file mode 100644 index 00000000000..293d4b72594 --- /dev/null +++ b/lang/fr/toasts.php @@ -0,0 +1,17 @@ + 'Error', + 'success' => 'Success', +]; \ No newline at end of file diff --git a/lang/fr/users.php b/lang/fr/users.php new file mode 100644 index 00000000000..599bb833454 --- /dev/null +++ b/lang/fr/users.php @@ -0,0 +1,44 @@ + 'Users', + 'description' => 'Here you can manage the users of your Lychee installation. You can create, edit and delete users.', + 'create' => 'Create a new user', + 'username' => 'Username', + 'password' => 'Password', + 'legend' => 'Legend', + 'upload_rights' => 'When selected, the user can upload content.', + 'edit_rights' => 'When selected, the user can modify their profile (username, password).', + 'quota' => 'When set, the user has a space quota for pictures (in kB).', + + 'user_deleted' => 'User deleted', + 'user_created' => 'User created', + 'user_updated' => 'User updated', + 'change_saved' => 'Change saved!', + + 'create_edit' => [ + 'upload_rights' => 'User can upload content.', + 'edit_rights' => 'User can modify their profile (username, password).', + 'quota' => 'User has quota limit.', + 'quota_kb' => 'quota in kB (0 for default)', + 'note' => 'Admin note (not publically visible)', + 'create' => 'Create', + 'edit' => 'Edit', + ], + 'line' => [ + 'admin' => 'admin user', + 'edit' => 'Edit', + 'delete' => 'Delete', + ], +]; \ No newline at end of file diff --git a/lang/hu/aspect_ratio.php b/lang/hu/aspect_ratio.php new file mode 100644 index 00000000000..2c7e8fb56ac --- /dev/null +++ b/lang/hu/aspect_ratio.php @@ -0,0 +1,21 @@ + '5/4 (instagram landscape)', + '4by5' => '4/5 (instagram portrait)', + '2by3' => '2/3 (portrait)', + '3by2' => '3/2 (landscape)', + '1by1' => 'square', + '1byx9' => '16/9 (landscape)', +]; \ No newline at end of file diff --git a/lang/hu/diagnostics.php b/lang/hu/diagnostics.php new file mode 100644 index 00000000000..0fadd640428 --- /dev/null +++ b/lang/hu/diagnostics.php @@ -0,0 +1,30 @@ + 'Diagnostics', + + 'copy_to_clipboard' => 'Copy diagnostics to clipboard', + 'self-diagnosis' => 'Self-diagnosis', + 'info' => 'Info', + 'space' => 'Space', + 'load_space' => 'Load space usage.', + 'configuration' => 'Configuration', + 'loading' => 'Loading...', + 'identical_content' => 'Identical content', + + 'toast' => [ + 'info' => 'Info', + 'copy' => 'Diagnostics copied to clipboard!', + ], +]; \ No newline at end of file diff --git a/lang/hu/dialogs.php b/lang/hu/dialogs.php new file mode 100644 index 00000000000..4afd65fae3f --- /dev/null +++ b/lang/hu/dialogs.php @@ -0,0 +1,221 @@ + [ + 'close' => 'Close', + 'cancel' => 'Cancel', + 'save' => 'Save', + 'delete' => 'Delete', + 'move' => 'Move', + ], + 'about' => [ + 'subtitle' => 'Self-hosted photo-management done right', + 'description' => 'Lychee is a free photo-management tool, which runs on your server or web-space. Installing is a matter of seconds. Upload, manage and share photos like from a native application. Lychee comes with everything you need and all your photos are stored securely.', + 'update_available' => 'Update available!', + 'thank_you' => 'Thank you for your support!', + 'get_supporter_or_register' => 'Get exclusive features and support the development of Lychee.
Unlock the Supporter Edition or register your License key', + 'here' => 'here', + ], + 'dropbox' => [ + 'not_configured' => 'Dropbox is not configured.', + ], + 'import_from_link' => [ + 'instructions' => 'Please enter the direct link to a photo to import it:', + 'import' => 'Import', + ], + 'keybindings' => [ + 'don_t_show_again' => 'Don\'t show this again', + 'side_wide' => 'Site-wide Shortcuts', + 'back_cancel' => 'Back/Cancel', + 'confirm' => 'Confirm', + 'login' => 'Login', + 'toggle_full_screen' => 'Toggle Full Screen', + 'toggle_sensitive_albums' => 'Toggle Sensitive Albums', + + 'albums' => 'Albums Shortcuts', + 'new_album' => 'New Album', + 'upload_photos' => 'Upload Photos', + 'search' => 'Search', + 'show_this_modal' => 'Show this modal', + 'select_all' => 'Select All', + 'move_selection' => 'Move Selection', + 'delete_selection' => 'Delete Selection', + + 'album' => 'Album Shortcuts', + 'slideshow' => 'Start/Stop Slideshow', + 'toggle' => 'Toggle panel', + + 'photo' => 'Photo Shortcuts', + 'previous' => 'Previous photo', + 'next' => 'Next photo', + 'cycle' => 'Cycle overlay mode', + 'star' => 'Star the photo', + 'move' => 'Move the photo', + 'delete' => 'Delete the photo', + 'edit' => 'Edit information', + 'show_hide_meta' => 'Show information', + + 'keep_hidden' => 'We will keep it hidden.', + ], + 'login' => [ + 'username' => 'Username', + 'password' => 'Password', + 'unknown_invalid' => 'Unknown user or invalid password.', + 'signin' => 'Sign-In', + ], + 'register' => [ + 'enter_license' => 'Enter your license key below:', + 'license_key' => 'License key', + 'invalid_license' => 'Invalid license key.', + 'register' => 'Register', + ], + 'share_album' => [ + 'url_copied' => 'Copied URL to clipboard!', + ], + 'upload' => [ + 'completed' => 'Completed', + 'uploaded' => 'Uploaded:', + 'release' => 'Release file to upload!', + 'select' => 'Click here to select files to upload', + 'drag' => '(Or drag files to the page)', + 'loading' => 'Loading', + 'resume' => 'Resume', + 'uploading' => 'Uploading', + 'finished' => 'Finished', + 'failed_error' => 'Upload failed. The server returned an error!', + ], + 'visibility' => [ + 'public' => 'Public', + 'public_expl' => 'Anonymous users can access this album, subject to the restrictions below.', + 'full' => 'Original', + 'full_expl' => 'Anonymous users can view full-resolution photos.', + 'hidden' => 'Hidden', + 'hidden_expl' => 'Anonymous users need a direct link to access this album.', + 'downloadable' => 'Downloadable', + 'downloadable_expl' => 'Anonymous users can download this album.', + 'password' => 'Password', + 'password_prot' => 'Password protected', + 'password_prot_expl' => 'Anonymous users need a shared password to access this album.', + 'nsfw' => 'Sensitive', + 'nsfw_expl' => 'Album contains sensitive content.', + 'visibility_updated' => 'Visibility updated.', + ], + 'move_album' => [ + 'confirm_single' => 'Are you sure you want to move the album “%1$s” into the album “%2$s”?', + 'confirm_multiple' => 'Are you sure you want to move all selected albums into the album “%s”?', + 'move_single' => 'Move Album', + 'move_to' => 'Move to', + 'move_to_single' => 'Move %s to:', + 'move_to_multiple' => 'Move %d albums to:', + 'no_album_target' => 'No album to move to', + 'moved_single' => 'Album moved!', + 'moved_single_details' => '%1$s moved to %2$s', + 'moved_details' => 'Album(s) moved to %s', + ], + 'new_album' => [ + 'menu' => 'Create Album', + 'info' => 'Enter a title for the new album:', + 'title' => 'title', + 'create' => 'Create Album', + ], + 'new_tag_album' => [ + 'menu' => 'Create Tag Album', + 'info' => 'Enter a title for the new tag album:', + 'title' => 'title', + 'set_tags' => 'Set tags to show', + 'warn' => 'Make sure to press enter after each tag', + 'create' => 'Create Tag Album', + ], + 'delete_album' => [ + 'confirmation' => 'Are you sure you want to delete the album “%s” and all of the photos it contains?', + 'confirmation_multiple' => 'Are you sure you want to delete all %d selected albums and all of the photos they contain?', + 'warning' => 'This action can not be undone!', + 'delete' => 'Delete Album and Photos', + ], + 'transfer' => [ + 'query' => 'Transfer ownership of album to', + 'confirmation' => 'Are you sure you want to transfer the ownership of album “%s” and all the photos it contains to "%s"?', + 'lost_access_warning' => 'Your access to this album will be lost.', + 'warning' => 'This action can not be undone!', + 'transfer' => 'Transfer ownership of album and photos', + ], + 'rename' => [ + 'photo' => 'Enter a new title for this photo:', + 'album' => 'Enter a new title for this album:', + 'rename' => 'Rename', + ], + 'merge' => [ + 'merge_to' => 'Merge %s to:', + 'merge_to_multiple' => 'Merge %d albums to:', + 'no_albums' => 'No albums to merge to.', + 'confirm' => 'Are you sure you want to merge the album “%1$s” into the album “%2$s”?', + 'confirm_multiple' => 'Are you sure you want to merge all selected albums into the album “%s”?', + 'merge' => 'Merge Albums', + 'merged' => 'Album(s) merged to %s!', + ], + 'unlock' => [ + 'password_required' => 'This album is protected by a password. Enter the password below to view the photos of this album:', + 'password' => 'Password', + 'unlock' => 'Unlock', + ], + 'photo_tags' => [ + 'question' => 'Enter your tags for this photo.', + 'question_multiple' => 'Enter your tags for all %d selected photos. Existing tags will be overwritten.', + 'no_tags' => 'No Tags', + 'set_tags' => 'Set Tags', + 'updated' => 'Tags updated!', + 'tags_override_info' => 'If this is unchecked, the tags will be added to the existing tags of the photo.', + ], + 'photo_copy' => [ + 'no_albums' => 'No albums to copy to', + 'copy_to' => 'Copy %s to:', + 'copy_to_multiple' => 'Copy %d photos to:', + 'confirm' => 'Copy %s to %s.', + 'confirm_multiple' => 'Copy %d photos to %s.', + 'copy' => 'Copy', + 'copied' => 'Photo(s) copied!', + ], + 'photo_delete' => [ + 'confirm' => 'Are you sure you want to delete the photo “%s”?', + 'confirm_multiple' => 'Are you sure you want to delete all %d selected photos?', + 'deleted' => 'Photo(s) deleted!', + ], + 'move_photo' => [ + 'move_single' => 'Move %s to:', + 'move_multiple' => 'Move %d photos to:', + 'confirm' => 'Move %s to %s.', + 'confirm_multiple' => 'Move %d photos to %s.', + 'moved' => 'Photo(s) moved to %s!', + ], + 'target_user' => [ + 'placeholder' => 'Select user', + ], + 'target_album' => [ + 'placeholder' => 'Select album', + ], + 'webauthn' => [ + 'u2f' => 'U2F', + 'success' => 'Authentication successful!', + 'error' => 'Whoops, it looks like something went wrong. Please reload the site and try again!', + ], + 'se' => [ + 'available' => 'Available in the Supporter Edition', + ], + 'session_expired' => [ + 'title' => 'Session expired', + 'message' => 'Your session has expired.
Please reload the page.', + 'reload' => 'Reload', + 'go_to_gallery' => 'Go to the Gallery', + ], +]; \ No newline at end of file diff --git a/lang/hu/fix-tree.php b/lang/hu/fix-tree.php new file mode 100644 index 00000000000..64803e310e6 --- /dev/null +++ b/lang/hu/fix-tree.php @@ -0,0 +1,55 @@ + 'Maintenance', + 'intro' => 'This page allows you to re-order and fix your albums manually.
Before any modifications, we strongly recommend you to read about Nested Set tree structures.', + 'warning' => 'You can really break your Lychee installation here, modify values at your own risks.', + + 'help' => [ + 'header' => 'Help', + 'hover' => 'Hover ids or titles to highlight related albums.', + 'left' => 'Left', + 'right' => 'Right', + 'convenience' => 'For your convenience, the and buttons allow you to change the values of %s and %s by respectively +1 and -1 with propagation.', + 'left-right-warn' => 'The and indicates that the value of %s (and respectively %s) is duplicated somewhere.', + 'parent-marked' => 'Marked Parent Id indicates that the %s and %s do not satisfy the Nest Set tree structures. Edit either the Parent Id or the %s/%s values.', + 'slowness' => 'This page will be slow with a large number of albums.', + ], + + 'buttons' => [ + 'reset' => 'Reset', + 'check' => 'Check', + 'apply' => 'Apply', + ], + + 'table' => [ + 'title' => 'Title', + 'left' => 'Left', + 'right' => 'Right', + 'id' => 'Id', + 'parent' => 'Parent Id', + ], + + 'errors' => [ + 'invalid' => 'Invalid tree!', + 'invalid_details' => 'We are not applying this as it is guaranteed to be a broken state.', + 'invalid_left' => 'Album %s has an invalid left value.', + 'invalid_right' => 'Album %s has an invalid right value.', + 'invalid_left_right' => 'Album %s has an invalid left/right values. Left should be strictly smaller than right: %s < %s.', + 'duplicate_left' => 'Album %s has a duplicate left value %s.', + 'duplicate_right' => 'Album %s has a duplicate right value %s.', + 'parent' => 'Album %s has an unexpected parent id %s.', + 'unknown' => 'Album %s has an unknown error.', + ], +]; \ No newline at end of file diff --git a/lang/hu/gallery.php b/lang/hu/gallery.php new file mode 100644 index 00000000000..eb8008827e0 --- /dev/null +++ b/lang/hu/gallery.php @@ -0,0 +1,241 @@ + 'Gallery', + + 'smart_albums' => 'Smart albums', + 'albums' => 'Albums', + 'root' => 'Albums', + + 'original' => 'Original', + 'medium' => 'Medium', + 'medium_hidpi' => 'Medium HiDPI', + 'small' => 'Thumb', + 'small_hidpi' => 'Thumb HiDPI', + 'thumb' => 'Square thumb', + 'thumb_hidpi' => 'Square thumb HiDPI', + 'placeholder' => 'Low Quality Image Placeholder', + 'thumbnail' => 'Photo thumbnail', + 'live_video' => 'Video part of live-photo', + + 'camera_data' => 'Camera date', + 'album_reserved' => 'All Rights Reserved', + + 'map' => [ + 'error_gpx' => 'Error loading GPX file', + 'osm_contributors' => 'OpenStreetMap contributors', + ], + + 'search' => [ + 'title' => 'Search', + 'searching' => 'Searching…', + 'no_results' => 'Nothing matches your search query.', + 'searchbox' => 'Search…', + 'minimum_chars' => 'Minimum %s characters required.', + 'photos' => 'Photos (%s)', + 'albums' => 'Albums (%s)', + ], + + 'smart_album' => [ + 'unsorted' => 'Unsorted', + 'starred' => 'Starred', + 'recent' => 'Recent', + 'public' => 'Public', + 'on_this_day' => 'On This Day', + ], + + 'layout' => [ + 'squares' => 'Square thumbnails', + 'justified' => 'With aspect, justified', + 'masonry' => 'With aspect, masonry', + 'grid' => 'With aspect, grid', + ], + + 'overlay' => [ + 'none' => 'None', + 'exif' => 'EXIF data', + 'description' => 'Description', + 'date' => 'Date taken', + ], + + 'timeline' => [ + 'default' => 'default', + 'disabled' => 'disabled', + 'year' => 'Year', + 'month' => 'Month', + 'day' => 'Day', + 'hour' => 'Hour', + ], + + 'album' => [ + 'header_albums' => 'Albums', + 'header_photos' => 'Photos', + 'no_results' => 'Nothing to see here', + 'upload' => 'Upload photos', + + 'tabs' => [ + 'about' => 'About Album', + 'share' => 'Share Album', + 'move' => 'Move Album', + 'danger' => 'DANGER ZONE', + ], + + 'hero' => [ + 'created' => 'Created', + 'copyright' => 'Copyright', + 'subalbums' => 'Subalbums', + 'images' => 'Photos', + 'download' => 'Download Album', + 'share' => 'Share Album', + 'stats_only_se' => 'Statistics available in the Supporter Edition', + ], + + 'stats' => [ + 'lens' => 'Lens', + 'shutter' => 'Shutter speed', + 'iso' => 'ISO', + 'model' => 'Model', + 'aperture' => 'Aperture', + 'no_data' => 'No data', + ], + + 'properties' => [ + 'title' => 'Title', + 'description' => 'Description', + 'photo_ordering' => 'Order photos by', + 'children_ordering' => 'Order albums by', + 'asc/desc' => 'asc/desc', + 'header' => 'Set album header', + 'compact_header' => 'Use compact header', + 'license' => 'Set license', + 'copyright' => 'Set copyright', + 'aspect_ratio' => 'Set album thumbs aspect ratio', + 'album_timeline' => 'Set album timeline mode', + 'photo_timeline' => 'Set photo timeline mode', + 'layout' => 'Set photo layout', + 'show_tags' => 'Set tags to show', + 'tags_required' => 'Tags are required.', + ], + ], + + 'photo' => [ + 'actions' => [ + 'star' => 'Star', + 'unstar' => 'Unstar', + 'set_album_header' => 'Set as album header', + 'move' => 'Move', + 'delete' => 'Delete', + 'header_set' => 'Header set', + ], + + 'details' => [ + 'about' => 'About', + 'basics' => 'Basics', + 'title' => 'Title', + 'uploaded' => 'Uploaded', + 'description' => 'Description', + 'license' => 'License', + 'reuse' => 'Reuse', + 'latitude' => 'Latitude', + 'longitude' => 'Longitude', + 'altitude' => 'Altitude', + 'location' => 'Location', + 'image' => 'Image', + 'video' => 'Video', + 'size' => 'Size', + 'format' => 'Format', + 'resolution' => 'Resolution', + 'duration' => 'Duration', + 'fps' => 'Frame rate', + 'tags' => 'Tags', + 'camera' => 'Camera', + 'captured' => 'Captured', + 'make' => 'Make', + 'type' => 'Type/Model', + 'lens' => 'Lens', + 'shutter' => 'Shutter Speed', + 'aperture' => 'Aperture', + 'focal' => 'Focal Length', + 'iso' => 'ISO %s', + ], + + 'edit' => [ + 'set_title' => 'Set Title', + 'set_description' => 'Set Description', + 'set_license' => 'Set License', + 'no_tags' => 'No Tags', + 'set_tags' => 'Set Tags', + 'set_created_at' => 'Set Upload Date', + ], + ], + + 'nsfw' => [ + 'header' => 'Sensitive content', + 'description' => 'This album contains sensitive content which some people may find offensive or disturbing.', + 'consent' => 'Tap to consent.', + ], + + 'menus' => [ + 'star' => 'Star', + 'unstar' => 'Unstar', + 'star_all' => 'Star Selected', + 'unstar_all' => 'Unstar Selected', + 'tag' => 'Tag', + 'tag_all' => 'Tag Selected', + 'set_cover' => 'Set Album Cover', + 'remove_header' => 'Remove Album Header', + 'set_header' => 'Set Album Header', + 'copy_to' => 'Copy to …', + 'copy_all_to' => 'Copy Selected to …', + 'rename' => 'Rename', + 'move' => 'Move', + 'move_all' => 'Move Selected', + 'delete' => 'Delete', + 'delete_all' => 'Delete Selected', + 'download' => 'Download', + 'download_all' => 'Download Selected', + 'merge' => 'Merge', + 'merge_all' => 'Merge Selected', + + 'upload_photo' => 'Upload Photo', + 'import_link' => 'Import from Link', + 'import_dropbox' => 'Import from Dropbox', + 'new_album' => 'New Album', + 'new_tag_album' => 'New Tag Album', + 'upload_track' => 'Upload track', + 'delete_track' => 'Delete track', + ], + + 'sort' => [ + 'photo_select_1' => 'Upload Time', + 'photo_select_2' => 'Take Date', + 'photo_select_3' => 'Title', + 'photo_select_4' => 'Description', + 'photo_select_6' => 'Star', + 'photo_select_7' => 'Photo Format', + 'ascending' => 'Ascending', + 'descending' => 'Descending', + 'album_select_1' => 'Creation Time', + 'album_select_2' => 'Title', + 'album_select_3' => 'Description', + 'album_select_5' => 'Latest Take Date', + 'album_select_6' => 'Oldest Take Date', + ], + + 'albums_protection' => [ + 'private' => 'private', + 'public' => 'public', + 'inherit_from_parent' => 'inherit from parent', + ], +]; \ No newline at end of file diff --git a/lang/hu/jobs.php b/lang/hu/jobs.php new file mode 100644 index 00000000000..5d952b76012 --- /dev/null +++ b/lang/hu/jobs.php @@ -0,0 +1,18 @@ + 'Jobs', + + 'no_data' => 'No Jobs have been executed yet.', +]; \ No newline at end of file diff --git a/lang/hu/landing.php b/lang/hu/landing.php new file mode 100644 index 00000000000..fe6fe55b8ea --- /dev/null +++ b/lang/hu/landing.php @@ -0,0 +1,19 @@ + 'Gallery', + 'access_gallery' => 'Access the gallery', + 'hosted_with_lychee' => 'Hosted with Lychee', + 'copyright' => 'All images on this website are subject to copyright by %1$s © %2$s', +]; \ No newline at end of file diff --git a/lang/hu/left-menu.php b/lang/hu/left-menu.php new file mode 100644 index 00000000000..9a3e91f4037 --- /dev/null +++ b/lang/hu/left-menu.php @@ -0,0 +1,29 @@ + 'Back to Gallery', + + 'admin' => 'Admin', + 'clockwork' => 'Clockwork App', + 'logs' => 'Show Logs', + 'jobs' => 'Show Job History', + 'user' => 'User', + + 'sign_out' => 'Sign Out', + + 'about' => 'About', + 'api' => 'API Documentation', + 'source_code' => 'Source Code', + 'support' => 'Support', +]; \ No newline at end of file diff --git a/lang/hu/lychee.php b/lang/hu/lychee.php new file mode 100644 index 00000000000..216c58ff22a --- /dev/null +++ b/lang/hu/lychee.php @@ -0,0 +1,535 @@ + 'Felhasználónév', + 'PASSWORD' => 'Jelszó', + 'ENTER' => 'Belépés', + 'CANCEL' => 'Mégse', + 'CONFIRM' => 'Confirm', + 'SIGN_IN' => 'Bejelentkezés', + 'CLOSE' => 'Bezárás', + 'SETTINGS' => 'Beállítások', + 'SEARCH' => 'Keresés …', + 'MORE' => 'Több', + 'DEFAULT' => 'alapértelmezett', + 'GALLERY' => 'Galéria', + + 'USERS' => 'Felhasználók', + 'PROFILE' => 'Profile', + 'CREATE' => 'Létrehozás', + 'REMOVE' => 'Törlés', + 'SHARE' => 'Megosztás', + 'U2F' => '2 faktoros azonosítás', + 'NOTIFICATIONS' => 'Értesítések', + 'SHARING' => 'Megosztás', + 'CHANGE_LOGIN' => 'Bejelentkezés módosítása', + 'CHANGE_SORTING' => 'Rendezés módosítása', + 'SET_DROPBOX' => 'Dropbox beállítása', + 'ABOUT_LYCHEE' => 'A Lychee-ről', + 'DIAGNOSTICS' => 'Diagnosztika', + 'DIAGNOSTICS_GET_SIZE' => 'Térhasználat lekérése', + 'JOBS' => 'Show job history', + 'LOGS' => 'Naplók megtekintése', + 'SIGN_OUT' => 'Kijelentkezés', + 'UPDATE_AVAILABLE' => 'Frissítés elérhető!', + 'MIGRATION_AVAILABLE' => 'Migráció elérhető!', + 'CHECK_FOR_UPDATE' => 'Frissítések keresése', + 'DEFAULT_LICENSE' => 'Alapértelmezett licenc új feltöltésekhez:', + 'SET_LICENSE' => 'Licenc beállítása', + 'SET_OVERLAY_TYPE' => 'Átlátszó réteg beállítása', + 'SET_ALBUM_DECORATION' => 'Album díszítések beállítása', + 'SET_MAP_PROVIDER' => 'OpenStreetMap csempék szolgáltatójának beállítása', + 'FULL_SETTINGS' => 'Teljes beállítások', + 'UPDATE' => 'Frissítés', + 'RESET' => 'Alaphelyzetbe állítás', + 'DISABLE_TOKEN_TOOLTIP' => 'Letiltás', + 'ENABLE_TOKEN' => 'API token engedélyezése', + 'DISABLED_TOKEN_STATUS_MSG' => 'Letiltott', + 'TOKEN_BUTTON' => 'API Token ...', + 'TOKEN_NOT_AVAILABLE' => 'Már megtekintetted ezt a tokent.', + 'TOKEN_WAIT' => 'Várakozás ...', + + 'SMART_ALBUMS' => 'Okos albumok', + 'SHARED_ALBUMS' => 'Megosztott albumok', + 'ALBUMS' => 'Albumok', + 'PHOTOS' => 'Fényképek', + 'SEARCH_RESULTS' => 'Keresési eredmények', + + 'RENAME' => 'Átnevezés', + 'RENAME_ALL' => 'Kijelöltek átnevezése', + 'MERGE' => 'Összevonás', + 'MERGE_ALL' => 'Kijelöltek összevonása', + 'MAKE_PUBLIC' => 'Nyilvánosságra hozás', + 'SHARE_ALBUM' => 'Album megosztása', + 'SHARE_PHOTO' => 'Fotó megosztása', + 'VISIBILITY_ALBUM' => 'Album láthatósága', + 'VISIBILITY_PHOTO' => 'Fotó láthatósága', + 'DOWNLOAD_ALBUM' => 'Album letöltése', + 'ABOUT_ALBUM' => 'Az albumról', + 'DELETE_ALBUM' => 'Album törlése', + 'MOVE_ALBUM' => 'Album áthelyezése', + 'FULLSCREEN_ENTER' => 'Teljes képernyőre váltás', + 'FULLSCREEN_EXIT' => 'Kilépés a teljes képernyőből', + + 'SHARING_ALBUM_USERS' => 'Az album megosztása felhasználókkal', + 'WAIT_FETCH_DATA' => 'Kérjük, várjon, amíg lekérdezzük az adatokat …', + 'SHARING_ALBUM_USERS_NO_USERS' => 'Nincsenek felhasználók, akikkel meg lehetne osztani az albumot', + 'SHARING_ALBUM_USERS_LONG_MESSAGE' => 'Válassza ki a felhasználókat, akikkel megosztja ezt az albumot', + + 'DELETE_ALBUM_QUESTION' => 'Album és fényképek törlése', + 'KEEP_ALBUM' => 'Album megtartása', + 'DELETE_ALBUM_CONFIRMATION' => 'Biztosan törölni szeretné a(z) „%s” nevű albumot és az összes benne lévő fényképet? Ezt a műveletet nem lehet visszavonni!', + + 'DELETE_TAG_ALBUM_QUESTION' => 'Album törlése', + 'DELETE_TAG_ALBUM_CONFIRMATION' => 'Biztosan törölni szeretné a(z) „%s” nevű albumot (a benne lévő fényképek nem kerülnek törlésre)? Ezt a műveletet nem lehet visszavonni!', + + 'DELETE_ALBUMS_QUESTION' => 'Albumok és fényképek törlése', + 'KEEP_ALBUMS' => 'Albumok megtartása', + 'DELETE_ALBUMS_CONFIRMATION' => 'Biztosan törölni szeretné az összes %d kijelölt albumot és az azokban lévő összes fényképet? Ezt a műveletet nem lehet visszavonni!', + + 'DELETE_UNSORTED_CONFIRM' => 'Biztosan törölni szeretné az összes fotót a "Nem rendezett" albumból? Ezt a műveletet nem lehet visszavonni!', + 'CLEAR_UNSORTED' => 'Nem rendezett törlése', + 'KEEP_UNSORTED' => 'Nem rendezett megtartása', + + 'EDIT_SHARING' => 'Megosztás szerkesztése', + 'MAKE_PRIVATE' => 'Priváttá tétel', + + 'CLOSE_ALBUM' => 'Album bezárása', + 'CLOSE_PHOTO' => 'Fotó bezárása', + 'CLOSE_MAP' => 'Térkép bezárása', + + 'ADD' => 'Hozzáadás', + 'MOVE' => 'Áthelyezés', + 'MOVE_ALL' => 'Kijelöltek áthelyezése', + 'DUPLICATE' => 'Másolás', + 'DUPLICATE_ALL' => 'Kijelöltek másolása', + 'COPY_TO' => 'Másolás ide …', + 'COPY_ALL_TO' => 'Kijelöltek másolása ide …', + 'DELETE' => 'Törlés', + 'SAVE' => 'Mentés', + 'DELETE_ALL' => 'Kijelöltek törlése', + 'DOWNLOAD' => 'Letöltés', + 'DOWNLOAD_ALL' => 'Kijelöltek letöltése', + 'UPLOAD_PHOTO' => 'Fénykép feltöltése', + 'IMPORT_LINK' => 'Importálás hivatkozásból', + 'IMPORT_DROPBOX' => 'Importálás Dropboxból', + 'IMPORT_SERVER' => 'Importálás szerverről', + 'NEW_ALBUM' => 'Új album', + 'NEW_TAG_ALBUM' => 'Új címkézett album', + 'UPLOAD_TRACK' => 'Zeneszám feltöltése', + 'DELETE_TRACK' => 'Zeneszám törlése', + + 'TITLE_NEW_ALBUM' => 'Adjon meg egy címet az új albumnak:', + 'UNTITLED' => 'Cím nélküli', + 'UNSORTED' => 'Nem rendezett', + 'STARRED' => 'Kedvencek', + 'RECENT' => 'Legutóbbi', + 'PUBLIC' => 'Nyilvános', + 'ON_THIS_DAY' => 'Ezen a napon', + 'NUM_PHOTOS' => 'Fényképek', + + 'CREATE_ALBUM' => 'Album létrehozása', + 'CREATE_TAG_ALBUM' => 'Címke album létrehozása', + + 'STAR_PHOTO' => 'Kedvenc fotó', + 'STAR' => 'Kedvenc', + 'UNSTAR' => 'Nem kedvenc', + 'STAR_ALL' => 'Összes kedvenc', + 'UNSTAR_ALL' => 'Összes nem kedvenc', + 'TAG' => 'Címke', + 'TAG_ALL' => 'Összes címke', + 'UNSTAR_PHOTO' => 'Fotó kedvencjelölésének eltávolítása', + 'SET_COVER' => 'Album borítójának beállítása', + 'REMOVE_COVER' => 'Album borítójának eltávolítása', + 'SET_HEADER' => 'Set Album Header', + 'REMOVE_HEADER' => 'Remove Album Header', + 'SET_COMPACT_HEADER' => 'Use Compact Header', + + 'FULL_PHOTO' => 'Eredeti megnyitása', + 'ABOUT_PHOTO' => 'A fotóról', + 'DISPLAY_FULL_MAP' => 'Térkép', + 'DIRECT_LINK' => 'Közvetlen hivatkozás', + 'DIRECT_LINKS' => 'Közvetlen hivatkozások', + 'QR_CODE' => 'QR kód', + + 'ALBUM_ABOUT' => 'Névjegy', + 'ALBUM_BASICS' => 'Alapok', + 'ALBUM_TITLE' => 'Cím', + 'ALBUM_COPYRIGHT' => 'Copyright', + 'ALBUM_SET_COPYRIGHT' => 'Set copyright', + 'ALBUM_NEW_TITLE' => 'Adjon meg egy új címet ennek az albumnak:', + 'ALBUMS_NEW_TITLE' => 'Adjon meg címet az összes %d kiválasztott albumnak:', + 'ALBUM_SET_TITLE' => 'Cím beállítása', + 'ALBUM_DESCRIPTION' => 'Leírás', + 'ALBUM_SHOW_TAGS' => 'Megjelenítendő címkék', + 'ALBUM_NEW_DESCRIPTION' => 'Adjon meg egy új leírást ennek az albumnak:', + 'ALBUM_SET_DESCRIPTION' => 'Leírás beállítása', + 'ALBUM_NEW_SHOWTAGS' => 'Adjon meg olyan fényképek címkéit, amelyek láthatóak lesznek ebben az albumban:', + 'ALBUM_SET_SHOWTAGS' => 'Megjelenítendő címkék beállítása', + 'ALBUM_ALBUM' => 'Album', + 'ALBUM_CREATED' => 'Létrehozva', + 'ALBUM_IMAGES' => 'Képek', + 'ALBUM_VIDEOS' => 'Videók', + 'ALBUM_SUBALBUMS' => 'Al-albumok', + 'ALBUM_SHARING' => 'Megosztás', + 'ALBUM_SHR_YES' => 'IGEN', + 'ALBUM_SHR_NO' => 'Nem', + 'ALBUM_PUBLIC' => 'Nyilvános', + 'ALBUM_PUBLIC_EXPL' => 'Az anonim felhasználók hozzáférhetnek ehhez az albumhoz a következő korlátozások betartásával.', + 'ALBUM_FULL' => 'Eredeti', + 'ALBUM_FULL_EXPL' => 'Az anonim felhasználók megnézhetik a teljes felbontású fényképeket.', + 'ALBUM_HIDDEN' => 'Rejtett', + 'ALBUM_HIDDEN_EXPL' => 'Az anonim felhasználóknak közvetlen hivatkozásra van szükségük ennek az albumnak az eléréséhez.', + 'ALBUM_MARK_NSFW' => 'Album megjelölése érzékeny tartalomként', + 'ALBUM_UNMARK_NSFW' => 'Album érzékeny tartalomként jelölésének eltávolítása', + 'ALBUM_NSFW' => 'Érzékeny tartalom', + 'ALBUM_NSFW_EXPL' => 'Az album érzékeny tartalmat tartalmaz.', + 'ALBUM_DOWNLOADABLE' => 'Letölthető', + 'ALBUM_DOWNLOADABLE_EXPL' => 'Az anonim felhasználók letölthetik ezt az albumot.', + 'ALBUM_SHARE_BUTTON_VISIBLE' => 'Megosztás gomb látható', + 'ALBUM_SHARE_BUTTON_VISIBLE_EXPL' => 'Az anonim felhasználók láthatják a közösségi média megosztási linkeket.', + 'ALBUM_PASSWORD' => 'Jelszó', + 'ALBUM_PASSWORD_PROT' => 'Jelszóval védett', + 'ALBUM_PASSWORD_PROT_EXPL' => 'Az anonim felhasználóknak megosztott jelszóra van szükségük ennek az albumnak az eléréséhez.', + 'ALBUM_PASSWORD_REQUIRED' => 'Ezt az albumot jelszó véd. Az album fényképeinek megtekintéséhez adja meg a jelszót:', + 'ALBUM_MERGE' => 'Biztosan össze akarja olvasztani a(z) „%1$s” albumot a(z) „%2$s” albumba?', + 'ALBUMS_MERGE' => 'Biztosan össze akarja olvasztani az összes kiválasztott albumot a(z) „%s” albumba?', + 'MERGE_ALBUM' => 'Albumok összeolvasztása', + 'DONT_MERGE' => 'Ne olvassza össze', + 'ALBUM_MOVE' => 'Biztosan át akarja mozgatni a(z) „%1$s” albumot a(z) „%2$s” albumba?', + 'ALBUMS_MOVE' => 'Biztosan át akarja mozgatni az összes kiválasztott albumot a(z) „%s” albumba?', + 'MOVE_ALBUMS' => 'Albumok áthelyezése', + 'NOT_MOVE_ALBUMS' => 'Ne helyezze át', + 'ROOT' => 'Albumok', + 'ALBUM_REUSE' => 'Újrafelhasználás', + 'ALBUM_LICENSE' => 'Licenc', + 'ALBUM_SET_LICENSE' => 'Licenc beállítása', + 'ALBUM_LICENSE_HELP' => 'Segítségre van szüksége a választáshoz?', + 'ALBUM_LICENSE_NONE' => 'Nincs', + 'ALBUM_RESERVED' => 'Minden jog fenntartva', + 'ALBUM_SET_ORDER' => 'Sorrend beállítása', + 'ALBUM_ORDERING' => 'Sorrend:', + 'ALBUM_PHOTO_ORDERING' => 'Order photos by', + 'ALBUM_CHILDREN_ORDERING' => 'Order albums by', + 'ALBUM_OWNER' => 'Tulajdonos', + + 'PHOTO_ABOUT' => 'Névjegy', + 'PHOTO_BASICS' => 'Alapok', + 'PHOTO_TITLE' => 'Cím', + 'PHOTO_NEW_TITLE' => 'Adjon meg egy új címet ennek a fényképnek:', + 'PHOTO_SET_TITLE' => 'Cím beállítása', + 'PHOTO_UPLOADED' => 'Feltöltve', + 'PHOTO_DESCRIPTION' => 'Leírás', + 'PHOTO_NEW_DESCRIPTION' => 'Adjon meg egy új leírást ennek a fényképnek:', + 'PHOTO_SET_DESCRIPTION' => 'Leírás beállítása', + 'PHOTO_NEW_LICENSE' => 'Licenc hozzáadása', + 'PHOTO_SET_LICENSE' => 'Licenc beállítása', + 'PHOTO_LICENSE' => 'Licenc', + 'PHOTO_LICENSE_HELP' => 'Segítségre van szüksége a választáshoz?', + 'PHOTO_REUSE' => 'Újrafelhasználás', + 'PHOTO_LICENSE_NONE' => 'Nincs', + 'PHOTO_RESERVED' => 'Minden jog fenntartva', + 'PHOTO_LATITUDE' => 'Szélesség', + 'PHOTO_LONGITUDE' => 'Hosszúság', + 'PHOTO_ALTITUDE' => 'Magasság', + 'PHOTO_IMGDIRECTION' => 'Irány', + 'PHOTO_LOCATION' => 'Hely', + 'PHOTO_IMAGE' => 'Kép', + 'PHOTO_VIDEO' => 'Videó', + 'PHOTO_SIZE' => 'Méret', + 'PHOTO_FORMAT' => 'Formátum', + 'PHOTO_RESOLUTION' => 'Felbontás', + 'PHOTO_DURATION' => 'Időtartam', + 'PHOTO_FPS' => 'Képfrissítési ráta', + 'PHOTO_TAGS' => 'Címkék', + 'PHOTO_NOTAGS' => 'Nincsenek címkék', + 'PHOTO_NEW_TAGS' => 'Adja meg a címkéket ehhez a fényképhez. Több címkét is hozzáadhat, vesszővel elválasztva:', + 'PHOTOS_NEW_TAGS' => 'Adja meg a címkéket az összes %d kiválasztott fényképhez. A meglévő címkéket felülírják. Több címkét is hozzáadhat, vesszővel elválasztva:', + 'PHOTO_SET_TAGS' => 'Címkék beállítása', + 'PHOTO_CAMERA' => 'Kamera', + 'PHOTO_CAPTURED' => 'Rögzítve', + 'PHOTO_MAKE' => 'Gyártó', + 'PHOTO_TYPE' => 'Típus/Modell', + 'PHOTO_LENS' => 'Objektív', + 'PHOTO_SHUTTER' => 'Záridő', + 'PHOTO_APERTURE' => 'Rekeszérték', + 'PHOTO_FOCAL' => 'Fókusztávolság', + 'PHOTO_ISO' => 'ISO %s', + 'PHOTO_SHARING' => 'Megosztás', + 'PHOTO_DELETE' => 'Fénykép törlése', + 'PHOTO_KEEP' => 'Fénykép megtartása', + 'PHOTO_DELETE_CONFIRMATION' => 'Biztosan törölni szeretné a(z) „%s” fényképet? Ez a művelet nem vonható vissza!', + 'PHOTO_DELETE_ALL' => 'Biztosan törölni szeretné az összes %d kiválasztott fényképet? Ez a művelet nem vonható vissza!', + 'PHOTOS_NEW_TITLE' => 'Adjon meg egy címet az összes %d kiválasztott fényképnek:', + 'PHOTO_MAKE_PRIVATE_ALBUM' => 'Ez a fénykép egy nyilvános albumon található. A fénykép privát vagy nyilvános beállításainak megváltoztatásához szerkessze az ehhez a fényképhez tartozó album láthatóságát.', + 'PHOTO_SHOW_ALBUM' => 'Album megjelenítése', + 'PHOTO_PUBLIC' => 'Nyilvános', + 'PHOTO_PUBLIC_EXPL' => 'Az anonim felhasználók megtekinthetik ezt a fényképet a következő korlátozások betartásával.', + 'PHOTO_FULL' => 'Eredeti', + 'PHOTO_FULL_EXPL' => 'Az anonim felhasználók megtekinthetik a teljes felbontású fényképet.', + 'PHOTO_HIDDEN' => 'Rejtett', + 'PHOTO_HIDDEN_EXPL' => 'Az anonim felhasználóknak szükségük van egy közvetlen hivatkozásra a fénykép megtekintéséhez.', + 'PHOTO_DOWNLOADABLE' => 'Letölthető', + 'PHOTO_DOWNLOADABLE_EXPL' => 'Az anonim felhasználók letölthetik ezt a fényképet.', + 'PHOTO_SHARE_BUTTON_VISIBLE' => 'Megosztás gomb látható', + 'PHOTO_SHARE_BUTTON_VISIBLE_EXPL' => 'Az anonim felhasználók láthatják a közösségi média megosztási linkeket.', + 'PHOTO_PASSWORD_PROT' => 'Jelszóval védett', + 'PHOTO_PASSWORD_PROT_EXPL' => 'Az anonim felhasználóknak megosztott jelszóra van szükségük a fénykép megtekintéséhez.', + 'PHOTO_EDIT_SHARING_TEXT' => 'E fénykép megosztási tulajdonságai a következőként fognak megváltozni:', + 'PHOTO_NO_EDIT_SHARING_TEXT' => 'Mivel ez a fénykép egy nyilvános albumon található, örökli azt az album láthatósági beállításait. Jelenlegi láthatósága csak tájékoztató jelleggel jelenik meg.', + 'PHOTO_EDIT_GLOBAL_SHARING_TEXT' => 'E fénykép láthatóságát globális Lychee beállításokkal finomhangolhatja. Jelenlegi láthatósága csak tájékoztató jelleggel jelenik meg.', + 'PHOTO_NEW_CREATED_AT' => 'Adja meg ennek a fényképnek a feltöltési dátumát. hh/nn/éééé, óó:pp [de/du]', + 'PHOTO_SET_CREATED_AT' => 'Feltöltési dátum beállítása', + + 'LOADING' => 'Betöltés', + 'ERROR' => 'Hiba', + 'ERROR_TEXT' => 'Hoppá, úgy tűnik, valami hiba történt. Kérjük, frissítse az oldalt és próbálja újra!', + 'ERROR_UNKNOWN' => 'Valami váratlan történt. Kérjük, próbálja újra, és ellenőrizze a telepítést és a szervert. További információért tekintse meg az útmutatót.', + 'ERROR_MAP_DEACTIVATED' => 'Térkép funkció letiltva a beállításokban.', + 'ERROR_SEARCH_DEACTIVATED' => 'Keresési funkció letiltva a beállításokban.', + 'SUCCESS' => 'Rendben', + 'CHANGE_SUCCESS' => 'Change successful.', + 'RETRY' => 'Újra', + 'OVERRIDE' => 'Felülbírálás', + 'TAGS_OVERRIDE_INFO' => 'Ha ez nincs bejelölve, a címkéket hozzáadják a fénykép meglévő címkéihez.', + + 'SETTINGS_SUCCESS_LOGIN' => 'Bejelentkezési információk frissítve.', + 'SETTINGS_SUCCESS_SORT' => 'Rendezési sorrend frissítve.', + 'SETTINGS_SUCCESS_DROPBOX' => 'Dropbox kulcs frissítve.', + 'SETTINGS_SUCCESS_LANG' => 'Nyelv frissítve', + 'SETTINGS_SUCCESS_LAYOUT' => 'Elrendezés frissítve', + 'SETTINGS_SUCCESS_IMAGE_OVERLAY' => 'EXIF fedőréteg beállítás frissítve', + 'SETTINGS_SUCCESS_PUBLIC_SEARCH' => 'Nyilvános keresés frissítve', + 'SETTINGS_SUCCESS_LICENSE' => 'Alapértelmezett licenc frissítve', + 'SETTINGS_SUCCESS_MAP_DISPLAY' => 'Térkép megjelenítési beállítások frissítve', + 'SETTINGS_SUCCESS_MAP_DISPLAY_PUBLIC' => 'Térkép megjelenítési beállítások nyilvános albumokhoz frissítve', + 'SETTINGS_SUCCESS_MAP_PROVIDER' => 'Térkép szolgáltató beállítások frissítve', + 'SETTINGS_SUCCESS_CSS' => 'CSS frissítve', + 'SETTINGS_SUCCESS_JS' => 'JS frissítve', + 'SETTINGS_SUCCESS_UPDATE' => 'Beállítások sikeresen frissítve', + 'SETTINGS_DROPBOX_KEY' => 'Dropbox API kulcs', + 'SETTINGS_ADVANCED_WARNING_EXPL' => 'Ezen speciális beállítások módosítása káros lehet az alkalmazás stabilitására, biztonságára és teljesítményére nézve. Csak akkor módosítsa őket, ha biztos benne, hogy tudja, mit csinál.', + 'SETTINGS_ADVANCED_SAVE' => 'Módosítások mentése, elfogadom a kockázatot!', + + 'U2F_NOT_SUPPORTED' => 'Az U2F nem támogatott. Sajnáljuk.', + 'U2F_NOT_SECURE' => 'Nem biztonságos környezet. Az U2F nem érhető el.', + 'U2F_REGISTER_KEY' => 'Új eszköz regisztrálása.', + 'U2F_REGISTRATION_SUCCESS' => 'Sikeres regisztráció!', + 'U2F_AUTHENTIFICATION_SUCCESS' => 'Sikeres hitelesítés!', + 'U2F_CREDENTIALS' => 'Hitelesítő adatok', + 'U2F_CREDENTIALS_DELETED' => 'Hitelesítő adatok törölve!', + 'U2F_LOGIN' => 'Log in with WebAuthn', + + 'NEW_PHOTOS_NOTIFICATION' => 'Küldjön értesítést az új fényképekről e-mailben.', + 'SETTINGS_SUCCESS_NEW_PHOTOS_NOTIFICATION' => 'Az új fényképek értesítése frissítve', + 'USER_EMAIL_INSTRUCTION' => 'Adja hozzá az alábbi e-mail címet a fogadásra jogosító e-mail értesítések engedélyezéséhez. Az e-mail értesítések visszavonásához egyszerűen távolítsa el az e-mail címet alulról.', + + 'LOGIN_USERNAME' => 'Új felhasználónév', + 'LOGIN_PASSWORD' => 'Új jelszó', + 'LOGIN_PASSWORD_CONFIRM' => 'Jelszó megerősítése', + 'PASSWORD_TITLE' => 'Adja meg jelenlegi jelszavát:', + 'PASSWORD_CURRENT' => 'Jelenlegi jelszó', + 'PASSWORD_TEXT' => 'A hitelesítő adatai a következőre lesznek megváltoztatva:', + 'PASSWORD_CHANGE' => 'Bejelentkezés változtatása', + + 'EDIT_SHARING_TITLE' => 'Megosztás szerkesztése', + 'EDIT_SHARING_TEXT' => 'Az album megosztási tulajdonságai a következőre lesznek megváltoztatva:', + 'SHARE_ALBUM_TEXT' => 'Ez az album a következő tulajdonságokkal lesz megosztva:', + + 'SORT_DIALOG_ATTRIBUTE_LABEL' => 'Attribútum', + 'SORT_DIALOG_ORDER_LABEL' => 'Rendelés', + + 'SORT_ALBUM_BY' => 'Albumok rendezése %1$s alapján %2$s rendben.', + + 'SORT_ALBUM_SELECT_1' => 'Létrehozás ideje', + 'SORT_ALBUM_SELECT_2' => 'Cím', + 'SORT_ALBUM_SELECT_3' => 'Leírás', + 'SORT_ALBUM_SELECT_5' => 'Legutóbbi felvétel dátuma', + 'SORT_ALBUM_SELECT_6' => 'Legrégebbi felvétel dátuma', + + 'SORT_PHOTO_BY' => 'Fényképek rendezése %1$s alapján %2$s rendben.', + + 'SORT_PHOTO_SELECT_1' => 'Feltöltés ideje', + 'SORT_PHOTO_SELECT_2' => 'Felvétel dátuma', + 'SORT_PHOTO_SELECT_3' => 'Cím', + 'SORT_PHOTO_SELECT_4' => 'Leírás', + 'SORT_PHOTO_SELECT_6' => 'Csillag', + 'SORT_PHOTO_SELECT_7' => 'Fénykép formátuma', + + 'SORT_ASCENDING' => 'Növekvő', + 'SORT_DESCENDING' => 'Csökkenő', + 'SORT_CHANGE' => 'Rendezés megváltoztatása', + + 'DROPBOX_TITLE' => 'Dropbox Kulcs Beállítása', + 'DROPBOX_TEXT' => "Annak érdekében, hogy képeket importáljon a Dropbox-ról, érvényes 'drop-ins app' kulcsra van szüksége a weboldalukon. Generáljon magának egy személyes kulcsot és írja be az alábbiakban:", + + 'LANG_TEXT' => 'Változtassa meg a Lychee nyelvét:', + 'LANG_TITLE' => 'Nyelv megváltoztatása', + + 'SETTING_RECENT_PUBLIC_TEXT' => '"Legutóbbi" okos album hozzáférhetővé tétele névtelen felhasználók számára', + 'SETTING_STARRED_PUBLIC_TEXT' => '"Csillagozott" okos album hozzáférhetővé tétele névtelen felhasználók számára', + 'SETTING_ONTHISDAY_PUBLIC_TEXT' => '"Ezen a napon" okos album hozzáférhetővé tétele névtelen felhasználók számára', + + 'CSS_TEXT' => 'Testreszabott CSS:', + 'CSS_TITLE' => 'CSS megváltoztatása', + 'JS_TEXT' => 'Testreszabott JS:', + 'JS_TITLE' => 'JS megváltoztatása', + 'PUBLIC_SEARCH_TEXT' => 'Nyilvános keresés engedélyezve:', + 'OVERLAY_TYPE' => 'Fénykép fedőréteg:', + 'OVERLAY_NONE' => 'Nincs', + 'OVERLAY_EXIF' => 'EXIF adatok', + 'OVERLAY_DESCRIPTION' => 'Leírás', + 'OVERLAY_DATE' => 'Felvétel dátuma', + 'ALBUM_DECORATION' => 'Album dekorációk:', + 'ALBUM_DECORATION_NONE' => 'Nincs', + 'ALBUM_DECORATION_ORIGINAL' => 'Al-album jelző', + 'ALBUM_DECORATION_ALBUM' => 'Al-albumok száma', + 'ALBUM_DECORATION_PHOTO' => 'Fényképek száma', + 'ALBUM_DECORATION_ALL' => 'Al-albumok és fényképek száma', + 'ALBUM_DECORATION_ORIENTATION' => 'Album dekorációk elrendezése:', + 'ALBUM_DECORATION_ORIENTATION_ROW' => 'Vízszintes (fényképek, albumok)', + 'ALBUM_DECORATION_ORIENTATION_ROW_REVERSE' => 'Vízszintes (albumok, fényképek)', + 'ALBUM_DECORATION_ORIENTATION_COLUMN' => 'Függőleges (felső fényképek, albumok)', + 'ALBUM_DECORATION_ORIENTATION_COLUMN_REVERSE' => 'Függőleges (felső albumok, fényképek)', + 'MAP_DISPLAY_TEXT' => 'Térképek engedélyezése (OpenStreetMap által):', + 'MAP_DISPLAY_PUBLIC_TEXT' => 'Térképek engedélyezése nyilvános albumokhoz (OpenStreetMap által):', + 'MAP_PROVIDER' => 'OpenStreetMap csempék szolgáltatója:', + 'MAP_PROVIDER_WIKIMEDIA' => 'Wikimedia', + 'MAP_PROVIDER_OSM_ORG' => 'OpenStreetMap.org (nem HiDPI)', + 'MAP_PROVIDER_OSM_DE' => 'OpenStreetMap.de (nem HiDPI)', + 'MAP_PROVIDER_OSM_FR' => 'OpenStreetMap.fr (nem HiDPI)', + 'MAP_PROVIDER_RRZE' => 'Erlangen Egyetem, Németország (csak HiDPI)', + 'MAP_INCLUDE_SUBALBUMS_TEXT' => 'Al-albumok fényképeinek belefoglalása a térképbe:', + 'LOCATION_DECODING' => 'GPS adatok dekódolása helyszín névbe', + 'LOCATION_SHOW' => 'Helyszín név megjelenítése', + 'LOCATION_SHOW_PUBLIC' => 'Helyszín név megjelenítése nyilvános módban', + + 'LAYOUT_TYPE' => 'Fényképek elrendezése:', + 'LAYOUT_SQUARES' => 'Négyzet alakú bélyegképek', + 'LAYOUT_JUSTIFIED' => 'Képaránnyal, igazított', + 'LAYOUT_MASONRY' => 'Képaránnyal, masonry', + 'LAYOUT_GRID' => 'Képaránnyal, grid', + 'LAYOUT_UNJUSTIFIED' => 'Képaránnyal, igazítatlan', + 'SET_LAYOUT' => 'Elrendezés megváltoztatása', + + 'NSFW_VISIBLE_TEXT_1' => 'Érzékeny albumok alapértelmezett láthatóságának beállítása.', + 'NSFW_VISIBLE_TEXT_2' => 'Ha az album nyilvános, még mindig elérhető, csak el van rejtve a nézetből és megjeleníthető a H billentyű megnyomásával.', + 'SETTINGS_SUCCESS_NSFW_VISIBLE' => 'Az alapértelmezett érzékeny album láthatósága sikeresen frissítve.', + + 'NSFW_BANNER' => '

Érzékeny tartalom

Ez az album érzékeny tartalmat tartalmaz, amit néhány ember zavaró vagy zavarba ejtő lehet. Tapintson az engedélyezéshez.

', + 'NSFW_HEADER' => 'Érzékeny tartalom', + 'NSFW_EXPLANATION' => 'Ez az album érzékeny tartalmat tartalmaz, amit néhány ember zavaró vagy zavarba ejtő lehet.', + 'TAP_CONSENT' => 'Tapintson az engedélyezéshez.', + + 'VIEW_NO_RESULT' => 'Nincs találat', + 'VIEW_NO_PUBLIC_ALBUMS' => 'Nincsenek nyilvános albumok', + 'VIEW_NO_CONFIGURATION' => 'Nincs konfiguráció', + 'VIEW_PHOTO_NOT_FOUND' => 'A fénykép nem található', + + 'NO_TAGS' => 'Nincsenek címkék', + + 'UPLOAD_MANAGE_NEW_PHOTOS' => 'Most már kezelheti az új fénykép(ek)et.', + 'UPLOAD_COMPLETE' => 'Feltöltés befejeződött', + 'UPLOAD_COMPLETE_FAILED' => 'Egy vagy több fénykép feltöltése sikertelen.', + 'UPLOAD_IMPORTING' => 'Importálás', + 'UPLOAD_IMPORTING_URL' => 'URL importálás', + 'UPLOAD_UPLOADING' => 'Feltöltés', + 'UPLOAD_FINISHED' => 'Befejezve', + 'UPLOAD_PROCESSING' => 'Feldolgozás', + 'UPLOAD_FAILED' => 'Sikertelen', + 'UPLOAD_FAILED_ERROR' => 'A feltöltés sikertelen. A kiszolgáló hibát adott vissza!', + 'UPLOAD_FAILED_WARNING' => 'A feltöltés sikertelen. A kiszolgáló figyelmeztetést adott vissza!', + 'UPLOAD_CANCELLED' => 'Megszakítva', + 'UPLOAD_SKIPPED' => 'Kihagyva', + 'UPLOAD_UPDATED' => 'Frissítve', + 'UPLOAD_GENERAL' => 'Általános', + 'UPLOAD_IMPORT_SKIPPED_DUPLICATE' => 'Ez a fénykép kimaradt, mert már a könyvtárában van.', + 'UPLOAD_IMPORT_RESYNCED_DUPLICATE' => 'Ez a fénykép kimaradt, mert már a könyvtárában van, de a metaadata frissítve lett.', + 'UPLOAD_ERROR_CONSOLE' => 'Tekintse meg a böngésző konzolját további részletekért.', + 'UPLOAD_UNKNOWN' => 'A kiszolgáló ismeretlen választ adott vissza. Tekintse meg a böngésző konzolját további részletekért.', + 'UPLOAD_ERROR_UNKNOWN' => 'A feltöltés sikertelen. A kiszolgáló ismeretlen hibát adott vissza!', + 'UPLOAD_ERROR_POSTSIZE' => 'A feltöltés sikertelen. A PHP post_max_size mérete lehet túl kicsi! Kérjük, ellenőrizze a GYIK-et.', + 'UPLOAD_ERROR_FILESIZE' => 'A feltöltés sikertelen. A PHP upload_max_filesize mérete lehet túl kicsi! Kérjük, ellenőrizze a GYIK-et.', + 'UPLOAD_IN_PROGRESS' => 'A Lychee jelenleg feltölti!', + 'UPLOAD_IMPORT_WARN_ERR' => 'Az importálás befejeződött, de figyelmeztetések vagy hibák jelentkeztek. Tekintse meg a naplót (Beállítások -> Napló mutatása) további részletekért.', + 'UPLOAD_IMPORT_COMPLETE' => 'Az importálás befejeződött', + 'UPLOAD_IMPORT_INSTR' => 'Kérjük, adja meg a fénykép közvetlen hivatkozását az importáláshoz:', + 'UPLOAD_IMPORT' => 'Importálás', + 'UPLOAD_IMPORT_SERVER' => 'Importálás a kiszolgálóról', + 'UPLOAD_IMPORT_SERVER_FOLD' => 'A mappa üres vagy nincsenek olvasható fájlok feldolgozásra. Tekintse meg a naplót (Beállítások -> Napló mutatása) további részletekért.', + 'UPLOAD_IMPORT_SERVER_INSTR' => 'Az alábbi abszolút elérési útvonalakkal (a kiszolgálón) rendelkező mappákban található összes fénykép, mappa és almappa importálása. Az elérési útvonalak szóközzel vannak elválasztva, a szóköz escape-elve van \'\\\'-vel az útvonalon.', + 'UPLOAD_ABSOLUTE_PATH' => 'Abszolút elérési útvonalak a könyvtárakhoz, szóközzel elválasztva', + 'UPLOAD_IMPORT_SERVER_EMPT' => 'Az importálás nem indítható el, mert a mappa üres!', + 'UPLOAD_IMPORT_DELETE_ORIGINALS' => 'Eredeti törlése', + 'UPLOAD_IMPORT_DELETE_ORIGINALS_EXPL' => 'Az eredeti fájlok az importálás után törölve lesznek, ha lehetséges.', + 'UPLOAD_IMPORT_VIA_SYMLINK' => 'Szimbolikus hivatkozások', + 'UPLOAD_IMPORT_VIA_SYMLINK_EXPL' => 'Fájlok importálása szimbolikus hivatkozásokkal az eredeti fájlokhoz.', + 'UPLOAD_IMPORT_SKIP_DUPLICATES' => 'Ismétlődők kihagyása', + 'UPLOAD_IMPORT_SKIP_DUPLICATES_EXPL' => 'Létező médiafájlok kihagyása.', + 'UPLOAD_IMPORT_RESYNC_METADATA' => 'Metaadat újrakapcsolása', + 'UPLOAD_IMPORT_RESYNC_METADATA_EXPL' => 'Meglévő médiafájlok metaadatainak frissítése.', + 'UPLOAD_IMPORT_LOW_MEMORY_EXPL' => 'Az import folyamata a kiszolgálón a memórialimithez közeledik, és előfordulhat, hogy előbb-utóbb megszakad.', + 'UPLOAD_WARNING' => 'Figyelmeztetés', + 'UPLOAD_IMPORT_NOT_A_DIRECTORY' => 'A megadott útvonal nem olvasható mappa!', + 'UPLOAD_IMPORT_PATH_RESERVED' => 'A megadott útvonal a Lychee fenntartott útvonala!', + 'UPLOAD_IMPORT_FAILED' => 'Nem sikerült importálni a fájlt!', + 'UPLOAD_IMPORT_UNSUPPORTED' => 'Nem támogatott fájltípus!', + 'UPLOAD_IMPORT_CANCELLED' => 'Az importálás megszakítva', + + 'ABOUT_SUBTITLE' => 'Önállóan futtatható, jól megtervezett fényképkezelés', + 'ABOUT_DESCRIPTION' => 'Lychee egy ingyenes fényképkezelő eszköz, amely saját szerverén vagy webtárhelyén fut. Az installáció néhány másodpercet vesz igénybe. Töltsön fel, kezeljen és osszon meg fényképeket, mintha egy natív alkalmazásban lenne. A Lychee mindennel rendelkezik, amire szüksége van, és az összes fényképe biztonságosan van tárolva.', + 'FOOTER_COPYRIGHT' => 'Az összes kép ezen a weboldalon szerzői jogi védelem alatt áll: %1$s © %2$s', + 'HOSTED_WITH_LYCHEE' => 'Futtatja: Lychee', + + 'URL_COPY_TO_CLIPBOARD' => 'Vágólapra másolás', + 'URL_COPIED_TO_CLIPBOARD' => 'URL másolva a vágólapra!', + 'PHOTO_DIRECT_LINKS_TO_IMAGES' => 'Közvetlen linkek a képfájlokhoz:', + 'PHOTO_ORIGINAL' => 'Eredeti', + 'PHOTO_MEDIUM' => 'Közepes', + 'PHOTO_MEDIUM_HIDPI' => 'Közepes HiDPI', + 'PHOTO_SMALL' => 'Bélyegkép', + 'PHOTO_SMALL_HIDPI' => 'Bélyegkép HiDPI', + 'PHOTO_THUMB' => 'Négyzetes bélyegkép', + 'PHOTO_THUMB_HIDPI' => 'Négyzetes bélyegkép HiDPI', + 'PHOTO_PLACEHOLDER' => 'Low Quality Image Placeholder', + 'PHOTO_THUMBNAIL' => 'Kép bélyegképe', + 'PHOTO_LIVE_VIDEO' => 'Videó része a live-fotónak', + 'PHOTO_VIEW' => 'Lychee Kép Megtekintés:', + + 'PHOTO_EDIT_ROTATECWISE' => 'Forgatás óramutató járásával megegyezően', + 'PHOTO_EDIT_ROTATECCWISE' => 'Forgatás óramutató járásával ellenkezőleg', + + 'ERROR_GPX' => 'Hiba a GPX fájl betöltésekor: ', + 'ERROR_EITHER_ALBUMS_OR_PHOTOS' => 'Kérlek válassz albumban vagy fényképeknél közül!', + 'ERROR_COULD_NOT_FIND' => 'Nem található, amit keresel.', + 'ERROR_INVALID_EMAIL' => 'Érvénytelen email cím.', + 'EMAIL_SUCCESS' => 'Email frissítve!', + 'ERROR_PHOTO_NOT_FOUND' => 'Hiba: fénykép %s nem található!', + 'ERROR_EMPTY_USERNAME' => 'az új felhasználónév nem lehet üres.', + 'ERROR_PASSWORD_DOES_NOT_MATCH' => 'az új jelszó nem egyezik meg.', + 'ERROR_EMPTY_PASSWORD' => 'az új jelszó nem lehet üres.', + 'ERROR_SELECT_ALBUM' => 'Válassz egy albumot a megosztáshoz!', + 'ERROR_SELECT_USER' => 'Válassz egy felhasználót a megosztáshoz!', + 'ERROR_SELECT_SHARING' => 'Válassz egy megosztást a törléshez!', + 'SHARING_SUCCESS' => 'Megosztás frissítve!', + 'SHARING_REMOVED' => 'Megosztás eltávolítva!', + 'USER_CREATED' => 'Felhasználó létrehozva!', + 'USER_DELETED' => 'Felhasználó törölve!', + 'USER_UPDATED' => 'Felhasználó frissítve!', + 'ENTER_EMAIL' => 'Add meg az email címed:', + 'ERROR_ALBUM_JSON_NOT_FOUND' => 'Hiba: Album JSON nem található!', + 'ERROR_ALBUM_NOT_FOUND' => 'Hiba: album %s nem található', + 'ERROR_DROPBOX_KEY' => 'Hiba: A Dropbox kulcs nincs beállítva', + 'ERROR_SESSION' => 'A munkamenet lejárt.', + 'CAMERA_DATE' => 'Kamera dátuma', + 'NEW_PASSWORD' => 'új jelszó', + 'ALLOW_UPLOADS' => 'Feltöltések engedélyezése', + 'ALLOW_USER_SELF_EDIT' => 'Felhasználói fiókok önkezelésének engedélyezése', + 'OSM_CONTRIBUTORS' => 'OpenStreetMap hozzájárulók', +]; diff --git a/lang/hu/maintenance.php b/lang/hu/maintenance.php new file mode 100644 index 00000000000..f86de3d6f46 --- /dev/null +++ b/lang/hu/maintenance.php @@ -0,0 +1,60 @@ + 'Maintenance', + 'description' => 'You will find on this page, all the required actions to keep your Lychee installation running smooth and nicely.', + 'cleaning' => [ + 'title' => 'Cleaning %s', + 'result' => '%s deleted.', + 'description' => 'Remove all contents from %s', + 'button' => 'Clean', + ], + 'fix-jobs' => [ + 'title' => 'Fixing Jobs History', + 'description' => 'Mark jobs with status %s or %s as %s.', + 'button' => 'Fix job history', + ], + 'gen-sizevariants' => [ + 'title' => 'Missing %s', + 'description' => 'Found %d %s that could be generated.', + 'button' => 'Generate!', + 'success' => 'Successfully generated %d %s.', + ], + 'fill-filesize-sizevariants' => [ + 'title' => 'File sizes missing', + 'description' => 'Found %d small variants without file size.', + 'button' => 'Fetch data!', + 'success' => 'Successfully computed sizes of %d small variants.', + ], + 'fix-tree' => [ + 'title' => 'Tree statistics', + 'Oddness' => 'Oddness', + 'Duplicates' => 'Duplicates', + 'Wrong parents' => 'Wrong parents', + 'Missing parents' => 'Missing parents', + 'button' => 'Fix tree', + ], + 'optimize' => [ + 'title' => 'Optimize Database', + 'description' => 'If you notice slowdown in your installation, it may be because your database does not + have all its needed index.', + 'button' => 'Optimize Database', + ], + 'update' => [ + 'title' => 'Updates', + 'check-button' => 'Check for updates', + 'update-button' => 'Update', + 'no-pending-updates' => 'No pending update.', + ], +]; \ No newline at end of file diff --git a/lang/hu/profile.php b/lang/hu/profile.php new file mode 100644 index 00000000000..cc24b97452c --- /dev/null +++ b/lang/hu/profile.php @@ -0,0 +1,64 @@ + 'Profile', + + 'login' => [ + 'header' => 'Profile', + 'enter_current_password' => 'Enter your current password:', + 'current_password' => 'Current password', + 'credentials_update' => 'Your credentials will be changed to the following:', + 'username' => 'Username', + 'new_password' => 'New password', + 'confirm_new_password' => 'Confirm new password', + 'email_instruction' => 'Add your email below to enable receiving email notifications. To stop receiving emails, simply remove your email below.', + 'email' => 'Email', + 'change' => 'Change Login', + 'api_token' => 'API Token ...', + + 'missing_fields' => 'Missing fields', + ], + + 'token' => [ + 'unavailable' => 'You have already viewed this token.', + 'no_data' => 'No token API have been generated.', + 'disable' => 'Disable', + 'disabled' => 'Token disabled', + 'warning' => 'This token will not be displayed again. Copy it and keep it in a safe place.', + 'reset' => 'Reset the token', + 'create' => 'Create a new token', + ], + + 'oauth' => [ + 'header' => 'OAuth', + 'header_not_available' => 'OAuth is not available', + 'setup_env' => 'Set up the credentials in your .env', + 'token_registered' => '%s token registered.', + 'setup' => 'Set up %s', + 'reset' => 'reset', + 'credential_deleted' => 'Credential deleted!', + ], + + 'u2f' => [ + 'header' => 'Passkey/MFA/2FA', + 'info' => 'This only provides the ability to use WebAuthn to authenticate instead of username & password.', + 'empty' => 'Credentials list is empty!', + 'not_secure' => 'Environment not secured. U2F not available.', + 'new' => 'Register new device.', + 'credential_deleted' => 'Credential deleted!', + 'credential_updated' => 'Credential updated!', + 'credential_registred' => 'Registration successful!', + '5_chars' => 'At least 5 chars.', + ], +]; \ No newline at end of file diff --git a/lang/hu/settings.php b/lang/hu/settings.php new file mode 100644 index 00000000000..fd197f11135 --- /dev/null +++ b/lang/hu/settings.php @@ -0,0 +1,92 @@ + 'Settings', + 'small_screen' => 'For better a experience on the Settings page,
we recommend you use a larger screen.', + 'tabs' => [ + 'basic' => 'Basic', + 'all_settings' => 'All settings', + ], + 'toasts' => [ + 'change_saved' => 'Change saved!', + 'details' => 'Settings have been modified as per request', + 'error' => 'Error!', + 'error_load_css' => 'Could not load dist/user.css', + 'error_load_js' => 'Could not load dist/custom.js', + 'error_save_css' => 'Could not save CSS', + 'error_save_js' => 'Could not save JS', + 'thank_you' => 'Thank you for your support.', + 'reload' => 'Reload your page for full functionalities.', + ], + 'system' => [ + 'header' => 'System', + 'use_dark_mode' => 'Use dark mode for Lychee', + 'language' => 'Language used by Lychee', + 'nsfw_album_visibility' => 'Make Sensitive albums visible by default.', + 'nsfw_album_explanation' => 'If the album is public, it is still accessible, just hidden from the view and can be revealed by pressing H.', + ], + 'lychee_se' => [ + 'header' => 'Lychee SE', + 'call4action' => 'Get exclusive features and support the development of Lychee. Unlock the SE edition.', + 'preview' => 'Enable preview of Lychee SE features', + 'hide_call4action' => 'Hide this Lychee SE registration form. I am happy with Lychee as-is. :)', + 'hide_warning' => 'If enabled, the only way to register your license key will be via the More tab above. Changes are applied on page reload.', + ], + 'dropbox' => [ + 'header' => 'Dropbox', + 'instruction' => 'In order to import photos from your Dropbox, you need a valid drop-ins app key from their website.', + 'api_key' => 'Dropbox API Key', + 'set_key' => 'Set Dropbox Key', + ], + 'gallery' => [ + 'header' => 'Gallery', + 'photo_order_column' => 'Default column used for sorting photos', + 'photo_order_direction' => 'Default order used for sorting photos', + 'album_order_column' => 'Default column used for sorting albums', + 'album_order_direction' => 'Default order used for sorting albums', + 'aspect_ratio' => 'Default aspect ratio for album thumbs', + 'photo_layout' => 'Layout for pictures', + 'album_decoration' => 'Show decorations on album cover (sub-album and/or photo count)', + 'album_decoration_direction' => 'Align album decorations horizontally or vertically', + 'photo_overlay' => 'Default image overlay information', + 'license_default' => 'Default license used for albums', + 'license_help' => 'Need help choosing?', + ], + 'geolocation' => [ + 'header' => 'Geo-location', + 'map_display' => 'Display the map given GPS coordinates', + 'map_display_public' => 'Allow anonymous users to access the map', + 'map_provider' => 'Defines the map provider', + 'map_include_subalbums' => 'Includes pictures of the sub albums on the map', + 'location_decoding' => 'Use GPS location decoding', + 'location_show' => 'Show location extracted from GPS coordinates', + 'location_show_public' => 'Anonymous users can access the extracted location from GPS coordinates', + ], + 'advanced' => [ + 'header' => 'Advanced Customization', + 'change_css' => 'Change CSS', + 'change_js' => 'Change JS', + ], + 'all' => [ + 'old_setting_style' => 'Old setting style', + 'change_detected' => 'Some settings changed.', + 'save' => 'Save', + ], + + 'tool_option' => [ + 'disabled' => 'disabled', + 'enabled' => 'enabled', + 'discover' => 'discover', + ], +]; \ No newline at end of file diff --git a/lang/hu/sharing.php b/lang/hu/sharing.php new file mode 100644 index 00000000000..69de18cc6d0 --- /dev/null +++ b/lang/hu/sharing.php @@ -0,0 +1,33 @@ + 'Sharing', + + 'info' => 'This page gives an overview of and the ability to edit the sharing rights associated with albums.', + 'album_title' => 'Album title', + 'username' => 'Username', + 'no_data' => 'Sharing list is empty.', + 'share' => 'Share', + 'permission_deleted' => 'Permission deleted!', + 'permission_created' => 'Permission created!', + + 'grants' => [ + 'read' => 'Grants read access', + 'original' => 'Grants access to original photo', + 'download' => 'Grants download', + 'upload' => 'Grants upload', + 'edit' => 'Grants edit', + 'delete' => 'Grants delete', + ], +]; \ No newline at end of file diff --git a/lang/hu/statistics.php b/lang/hu/statistics.php new file mode 100644 index 00000000000..2baf855bbd5 --- /dev/null +++ b/lang/hu/statistics.php @@ -0,0 +1,34 @@ + 'Statistics', + + 'preview_text' => 'This is a preview of the statistics page available in Lychee SE.
The data shown here are randomly generated and do not reflect your server.', + 'no_data' => 'User does not have data on server.', + 'collapse' => 'Collapse albums sizes', + + 'total' => [ + 'total' => 'Total', + 'albums' => 'Albums', + 'photos' => 'Photos', + 'size' => 'Size', + ], + 'table' => [ + 'username' => 'Owner', + 'title' => 'Title', + 'photos' => 'Photos', + 'descendants' => 'Children', + 'size' => 'Size', + ], +]; \ No newline at end of file diff --git a/lang/hu/toasts.php b/lang/hu/toasts.php new file mode 100644 index 00000000000..293d4b72594 --- /dev/null +++ b/lang/hu/toasts.php @@ -0,0 +1,17 @@ + 'Error', + 'success' => 'Success', +]; \ No newline at end of file diff --git a/lang/hu/users.php b/lang/hu/users.php new file mode 100644 index 00000000000..599bb833454 --- /dev/null +++ b/lang/hu/users.php @@ -0,0 +1,44 @@ + 'Users', + 'description' => 'Here you can manage the users of your Lychee installation. You can create, edit and delete users.', + 'create' => 'Create a new user', + 'username' => 'Username', + 'password' => 'Password', + 'legend' => 'Legend', + 'upload_rights' => 'When selected, the user can upload content.', + 'edit_rights' => 'When selected, the user can modify their profile (username, password).', + 'quota' => 'When set, the user has a space quota for pictures (in kB).', + + 'user_deleted' => 'User deleted', + 'user_created' => 'User created', + 'user_updated' => 'User updated', + 'change_saved' => 'Change saved!', + + 'create_edit' => [ + 'upload_rights' => 'User can upload content.', + 'edit_rights' => 'User can modify their profile (username, password).', + 'quota' => 'User has quota limit.', + 'quota_kb' => 'quota in kB (0 for default)', + 'note' => 'Admin note (not publically visible)', + 'create' => 'Create', + 'edit' => 'Edit', + ], + 'line' => [ + 'admin' => 'admin user', + 'edit' => 'Edit', + 'delete' => 'Delete', + ], +]; \ No newline at end of file diff --git a/lang/it/aspect_ratio.php b/lang/it/aspect_ratio.php new file mode 100644 index 00000000000..2c7e8fb56ac --- /dev/null +++ b/lang/it/aspect_ratio.php @@ -0,0 +1,21 @@ + '5/4 (instagram landscape)', + '4by5' => '4/5 (instagram portrait)', + '2by3' => '2/3 (portrait)', + '3by2' => '3/2 (landscape)', + '1by1' => 'square', + '1byx9' => '16/9 (landscape)', +]; \ No newline at end of file diff --git a/lang/it/diagnostics.php b/lang/it/diagnostics.php new file mode 100644 index 00000000000..0fadd640428 --- /dev/null +++ b/lang/it/diagnostics.php @@ -0,0 +1,30 @@ + 'Diagnostics', + + 'copy_to_clipboard' => 'Copy diagnostics to clipboard', + 'self-diagnosis' => 'Self-diagnosis', + 'info' => 'Info', + 'space' => 'Space', + 'load_space' => 'Load space usage.', + 'configuration' => 'Configuration', + 'loading' => 'Loading...', + 'identical_content' => 'Identical content', + + 'toast' => [ + 'info' => 'Info', + 'copy' => 'Diagnostics copied to clipboard!', + ], +]; \ No newline at end of file diff --git a/lang/it/dialogs.php b/lang/it/dialogs.php new file mode 100644 index 00000000000..4afd65fae3f --- /dev/null +++ b/lang/it/dialogs.php @@ -0,0 +1,221 @@ + [ + 'close' => 'Close', + 'cancel' => 'Cancel', + 'save' => 'Save', + 'delete' => 'Delete', + 'move' => 'Move', + ], + 'about' => [ + 'subtitle' => 'Self-hosted photo-management done right', + 'description' => 'Lychee is a free photo-management tool, which runs on your server or web-space. Installing is a matter of seconds. Upload, manage and share photos like from a native application. Lychee comes with everything you need and all your photos are stored securely.', + 'update_available' => 'Update available!', + 'thank_you' => 'Thank you for your support!', + 'get_supporter_or_register' => 'Get exclusive features and support the development of Lychee.
Unlock the Supporter Edition or register your License key', + 'here' => 'here', + ], + 'dropbox' => [ + 'not_configured' => 'Dropbox is not configured.', + ], + 'import_from_link' => [ + 'instructions' => 'Please enter the direct link to a photo to import it:', + 'import' => 'Import', + ], + 'keybindings' => [ + 'don_t_show_again' => 'Don\'t show this again', + 'side_wide' => 'Site-wide Shortcuts', + 'back_cancel' => 'Back/Cancel', + 'confirm' => 'Confirm', + 'login' => 'Login', + 'toggle_full_screen' => 'Toggle Full Screen', + 'toggle_sensitive_albums' => 'Toggle Sensitive Albums', + + 'albums' => 'Albums Shortcuts', + 'new_album' => 'New Album', + 'upload_photos' => 'Upload Photos', + 'search' => 'Search', + 'show_this_modal' => 'Show this modal', + 'select_all' => 'Select All', + 'move_selection' => 'Move Selection', + 'delete_selection' => 'Delete Selection', + + 'album' => 'Album Shortcuts', + 'slideshow' => 'Start/Stop Slideshow', + 'toggle' => 'Toggle panel', + + 'photo' => 'Photo Shortcuts', + 'previous' => 'Previous photo', + 'next' => 'Next photo', + 'cycle' => 'Cycle overlay mode', + 'star' => 'Star the photo', + 'move' => 'Move the photo', + 'delete' => 'Delete the photo', + 'edit' => 'Edit information', + 'show_hide_meta' => 'Show information', + + 'keep_hidden' => 'We will keep it hidden.', + ], + 'login' => [ + 'username' => 'Username', + 'password' => 'Password', + 'unknown_invalid' => 'Unknown user or invalid password.', + 'signin' => 'Sign-In', + ], + 'register' => [ + 'enter_license' => 'Enter your license key below:', + 'license_key' => 'License key', + 'invalid_license' => 'Invalid license key.', + 'register' => 'Register', + ], + 'share_album' => [ + 'url_copied' => 'Copied URL to clipboard!', + ], + 'upload' => [ + 'completed' => 'Completed', + 'uploaded' => 'Uploaded:', + 'release' => 'Release file to upload!', + 'select' => 'Click here to select files to upload', + 'drag' => '(Or drag files to the page)', + 'loading' => 'Loading', + 'resume' => 'Resume', + 'uploading' => 'Uploading', + 'finished' => 'Finished', + 'failed_error' => 'Upload failed. The server returned an error!', + ], + 'visibility' => [ + 'public' => 'Public', + 'public_expl' => 'Anonymous users can access this album, subject to the restrictions below.', + 'full' => 'Original', + 'full_expl' => 'Anonymous users can view full-resolution photos.', + 'hidden' => 'Hidden', + 'hidden_expl' => 'Anonymous users need a direct link to access this album.', + 'downloadable' => 'Downloadable', + 'downloadable_expl' => 'Anonymous users can download this album.', + 'password' => 'Password', + 'password_prot' => 'Password protected', + 'password_prot_expl' => 'Anonymous users need a shared password to access this album.', + 'nsfw' => 'Sensitive', + 'nsfw_expl' => 'Album contains sensitive content.', + 'visibility_updated' => 'Visibility updated.', + ], + 'move_album' => [ + 'confirm_single' => 'Are you sure you want to move the album “%1$s” into the album “%2$s”?', + 'confirm_multiple' => 'Are you sure you want to move all selected albums into the album “%s”?', + 'move_single' => 'Move Album', + 'move_to' => 'Move to', + 'move_to_single' => 'Move %s to:', + 'move_to_multiple' => 'Move %d albums to:', + 'no_album_target' => 'No album to move to', + 'moved_single' => 'Album moved!', + 'moved_single_details' => '%1$s moved to %2$s', + 'moved_details' => 'Album(s) moved to %s', + ], + 'new_album' => [ + 'menu' => 'Create Album', + 'info' => 'Enter a title for the new album:', + 'title' => 'title', + 'create' => 'Create Album', + ], + 'new_tag_album' => [ + 'menu' => 'Create Tag Album', + 'info' => 'Enter a title for the new tag album:', + 'title' => 'title', + 'set_tags' => 'Set tags to show', + 'warn' => 'Make sure to press enter after each tag', + 'create' => 'Create Tag Album', + ], + 'delete_album' => [ + 'confirmation' => 'Are you sure you want to delete the album “%s” and all of the photos it contains?', + 'confirmation_multiple' => 'Are you sure you want to delete all %d selected albums and all of the photos they contain?', + 'warning' => 'This action can not be undone!', + 'delete' => 'Delete Album and Photos', + ], + 'transfer' => [ + 'query' => 'Transfer ownership of album to', + 'confirmation' => 'Are you sure you want to transfer the ownership of album “%s” and all the photos it contains to "%s"?', + 'lost_access_warning' => 'Your access to this album will be lost.', + 'warning' => 'This action can not be undone!', + 'transfer' => 'Transfer ownership of album and photos', + ], + 'rename' => [ + 'photo' => 'Enter a new title for this photo:', + 'album' => 'Enter a new title for this album:', + 'rename' => 'Rename', + ], + 'merge' => [ + 'merge_to' => 'Merge %s to:', + 'merge_to_multiple' => 'Merge %d albums to:', + 'no_albums' => 'No albums to merge to.', + 'confirm' => 'Are you sure you want to merge the album “%1$s” into the album “%2$s”?', + 'confirm_multiple' => 'Are you sure you want to merge all selected albums into the album “%s”?', + 'merge' => 'Merge Albums', + 'merged' => 'Album(s) merged to %s!', + ], + 'unlock' => [ + 'password_required' => 'This album is protected by a password. Enter the password below to view the photos of this album:', + 'password' => 'Password', + 'unlock' => 'Unlock', + ], + 'photo_tags' => [ + 'question' => 'Enter your tags for this photo.', + 'question_multiple' => 'Enter your tags for all %d selected photos. Existing tags will be overwritten.', + 'no_tags' => 'No Tags', + 'set_tags' => 'Set Tags', + 'updated' => 'Tags updated!', + 'tags_override_info' => 'If this is unchecked, the tags will be added to the existing tags of the photo.', + ], + 'photo_copy' => [ + 'no_albums' => 'No albums to copy to', + 'copy_to' => 'Copy %s to:', + 'copy_to_multiple' => 'Copy %d photos to:', + 'confirm' => 'Copy %s to %s.', + 'confirm_multiple' => 'Copy %d photos to %s.', + 'copy' => 'Copy', + 'copied' => 'Photo(s) copied!', + ], + 'photo_delete' => [ + 'confirm' => 'Are you sure you want to delete the photo “%s”?', + 'confirm_multiple' => 'Are you sure you want to delete all %d selected photos?', + 'deleted' => 'Photo(s) deleted!', + ], + 'move_photo' => [ + 'move_single' => 'Move %s to:', + 'move_multiple' => 'Move %d photos to:', + 'confirm' => 'Move %s to %s.', + 'confirm_multiple' => 'Move %d photos to %s.', + 'moved' => 'Photo(s) moved to %s!', + ], + 'target_user' => [ + 'placeholder' => 'Select user', + ], + 'target_album' => [ + 'placeholder' => 'Select album', + ], + 'webauthn' => [ + 'u2f' => 'U2F', + 'success' => 'Authentication successful!', + 'error' => 'Whoops, it looks like something went wrong. Please reload the site and try again!', + ], + 'se' => [ + 'available' => 'Available in the Supporter Edition', + ], + 'session_expired' => [ + 'title' => 'Session expired', + 'message' => 'Your session has expired.
Please reload the page.', + 'reload' => 'Reload', + 'go_to_gallery' => 'Go to the Gallery', + ], +]; \ No newline at end of file diff --git a/lang/it/fix-tree.php b/lang/it/fix-tree.php new file mode 100644 index 00000000000..64803e310e6 --- /dev/null +++ b/lang/it/fix-tree.php @@ -0,0 +1,55 @@ + 'Maintenance', + 'intro' => 'This page allows you to re-order and fix your albums manually.
Before any modifications, we strongly recommend you to read about Nested Set tree structures.', + 'warning' => 'You can really break your Lychee installation here, modify values at your own risks.', + + 'help' => [ + 'header' => 'Help', + 'hover' => 'Hover ids or titles to highlight related albums.', + 'left' => 'Left', + 'right' => 'Right', + 'convenience' => 'For your convenience, the and buttons allow you to change the values of %s and %s by respectively +1 and -1 with propagation.', + 'left-right-warn' => 'The and indicates that the value of %s (and respectively %s) is duplicated somewhere.', + 'parent-marked' => 'Marked Parent Id indicates that the %s and %s do not satisfy the Nest Set tree structures. Edit either the Parent Id or the %s/%s values.', + 'slowness' => 'This page will be slow with a large number of albums.', + ], + + 'buttons' => [ + 'reset' => 'Reset', + 'check' => 'Check', + 'apply' => 'Apply', + ], + + 'table' => [ + 'title' => 'Title', + 'left' => 'Left', + 'right' => 'Right', + 'id' => 'Id', + 'parent' => 'Parent Id', + ], + + 'errors' => [ + 'invalid' => 'Invalid tree!', + 'invalid_details' => 'We are not applying this as it is guaranteed to be a broken state.', + 'invalid_left' => 'Album %s has an invalid left value.', + 'invalid_right' => 'Album %s has an invalid right value.', + 'invalid_left_right' => 'Album %s has an invalid left/right values. Left should be strictly smaller than right: %s < %s.', + 'duplicate_left' => 'Album %s has a duplicate left value %s.', + 'duplicate_right' => 'Album %s has a duplicate right value %s.', + 'parent' => 'Album %s has an unexpected parent id %s.', + 'unknown' => 'Album %s has an unknown error.', + ], +]; \ No newline at end of file diff --git a/lang/it/gallery.php b/lang/it/gallery.php new file mode 100644 index 00000000000..eb8008827e0 --- /dev/null +++ b/lang/it/gallery.php @@ -0,0 +1,241 @@ + 'Gallery', + + 'smart_albums' => 'Smart albums', + 'albums' => 'Albums', + 'root' => 'Albums', + + 'original' => 'Original', + 'medium' => 'Medium', + 'medium_hidpi' => 'Medium HiDPI', + 'small' => 'Thumb', + 'small_hidpi' => 'Thumb HiDPI', + 'thumb' => 'Square thumb', + 'thumb_hidpi' => 'Square thumb HiDPI', + 'placeholder' => 'Low Quality Image Placeholder', + 'thumbnail' => 'Photo thumbnail', + 'live_video' => 'Video part of live-photo', + + 'camera_data' => 'Camera date', + 'album_reserved' => 'All Rights Reserved', + + 'map' => [ + 'error_gpx' => 'Error loading GPX file', + 'osm_contributors' => 'OpenStreetMap contributors', + ], + + 'search' => [ + 'title' => 'Search', + 'searching' => 'Searching…', + 'no_results' => 'Nothing matches your search query.', + 'searchbox' => 'Search…', + 'minimum_chars' => 'Minimum %s characters required.', + 'photos' => 'Photos (%s)', + 'albums' => 'Albums (%s)', + ], + + 'smart_album' => [ + 'unsorted' => 'Unsorted', + 'starred' => 'Starred', + 'recent' => 'Recent', + 'public' => 'Public', + 'on_this_day' => 'On This Day', + ], + + 'layout' => [ + 'squares' => 'Square thumbnails', + 'justified' => 'With aspect, justified', + 'masonry' => 'With aspect, masonry', + 'grid' => 'With aspect, grid', + ], + + 'overlay' => [ + 'none' => 'None', + 'exif' => 'EXIF data', + 'description' => 'Description', + 'date' => 'Date taken', + ], + + 'timeline' => [ + 'default' => 'default', + 'disabled' => 'disabled', + 'year' => 'Year', + 'month' => 'Month', + 'day' => 'Day', + 'hour' => 'Hour', + ], + + 'album' => [ + 'header_albums' => 'Albums', + 'header_photos' => 'Photos', + 'no_results' => 'Nothing to see here', + 'upload' => 'Upload photos', + + 'tabs' => [ + 'about' => 'About Album', + 'share' => 'Share Album', + 'move' => 'Move Album', + 'danger' => 'DANGER ZONE', + ], + + 'hero' => [ + 'created' => 'Created', + 'copyright' => 'Copyright', + 'subalbums' => 'Subalbums', + 'images' => 'Photos', + 'download' => 'Download Album', + 'share' => 'Share Album', + 'stats_only_se' => 'Statistics available in the Supporter Edition', + ], + + 'stats' => [ + 'lens' => 'Lens', + 'shutter' => 'Shutter speed', + 'iso' => 'ISO', + 'model' => 'Model', + 'aperture' => 'Aperture', + 'no_data' => 'No data', + ], + + 'properties' => [ + 'title' => 'Title', + 'description' => 'Description', + 'photo_ordering' => 'Order photos by', + 'children_ordering' => 'Order albums by', + 'asc/desc' => 'asc/desc', + 'header' => 'Set album header', + 'compact_header' => 'Use compact header', + 'license' => 'Set license', + 'copyright' => 'Set copyright', + 'aspect_ratio' => 'Set album thumbs aspect ratio', + 'album_timeline' => 'Set album timeline mode', + 'photo_timeline' => 'Set photo timeline mode', + 'layout' => 'Set photo layout', + 'show_tags' => 'Set tags to show', + 'tags_required' => 'Tags are required.', + ], + ], + + 'photo' => [ + 'actions' => [ + 'star' => 'Star', + 'unstar' => 'Unstar', + 'set_album_header' => 'Set as album header', + 'move' => 'Move', + 'delete' => 'Delete', + 'header_set' => 'Header set', + ], + + 'details' => [ + 'about' => 'About', + 'basics' => 'Basics', + 'title' => 'Title', + 'uploaded' => 'Uploaded', + 'description' => 'Description', + 'license' => 'License', + 'reuse' => 'Reuse', + 'latitude' => 'Latitude', + 'longitude' => 'Longitude', + 'altitude' => 'Altitude', + 'location' => 'Location', + 'image' => 'Image', + 'video' => 'Video', + 'size' => 'Size', + 'format' => 'Format', + 'resolution' => 'Resolution', + 'duration' => 'Duration', + 'fps' => 'Frame rate', + 'tags' => 'Tags', + 'camera' => 'Camera', + 'captured' => 'Captured', + 'make' => 'Make', + 'type' => 'Type/Model', + 'lens' => 'Lens', + 'shutter' => 'Shutter Speed', + 'aperture' => 'Aperture', + 'focal' => 'Focal Length', + 'iso' => 'ISO %s', + ], + + 'edit' => [ + 'set_title' => 'Set Title', + 'set_description' => 'Set Description', + 'set_license' => 'Set License', + 'no_tags' => 'No Tags', + 'set_tags' => 'Set Tags', + 'set_created_at' => 'Set Upload Date', + ], + ], + + 'nsfw' => [ + 'header' => 'Sensitive content', + 'description' => 'This album contains sensitive content which some people may find offensive or disturbing.', + 'consent' => 'Tap to consent.', + ], + + 'menus' => [ + 'star' => 'Star', + 'unstar' => 'Unstar', + 'star_all' => 'Star Selected', + 'unstar_all' => 'Unstar Selected', + 'tag' => 'Tag', + 'tag_all' => 'Tag Selected', + 'set_cover' => 'Set Album Cover', + 'remove_header' => 'Remove Album Header', + 'set_header' => 'Set Album Header', + 'copy_to' => 'Copy to …', + 'copy_all_to' => 'Copy Selected to …', + 'rename' => 'Rename', + 'move' => 'Move', + 'move_all' => 'Move Selected', + 'delete' => 'Delete', + 'delete_all' => 'Delete Selected', + 'download' => 'Download', + 'download_all' => 'Download Selected', + 'merge' => 'Merge', + 'merge_all' => 'Merge Selected', + + 'upload_photo' => 'Upload Photo', + 'import_link' => 'Import from Link', + 'import_dropbox' => 'Import from Dropbox', + 'new_album' => 'New Album', + 'new_tag_album' => 'New Tag Album', + 'upload_track' => 'Upload track', + 'delete_track' => 'Delete track', + ], + + 'sort' => [ + 'photo_select_1' => 'Upload Time', + 'photo_select_2' => 'Take Date', + 'photo_select_3' => 'Title', + 'photo_select_4' => 'Description', + 'photo_select_6' => 'Star', + 'photo_select_7' => 'Photo Format', + 'ascending' => 'Ascending', + 'descending' => 'Descending', + 'album_select_1' => 'Creation Time', + 'album_select_2' => 'Title', + 'album_select_3' => 'Description', + 'album_select_5' => 'Latest Take Date', + 'album_select_6' => 'Oldest Take Date', + ], + + 'albums_protection' => [ + 'private' => 'private', + 'public' => 'public', + 'inherit_from_parent' => 'inherit from parent', + ], +]; \ No newline at end of file diff --git a/lang/it/jobs.php b/lang/it/jobs.php new file mode 100644 index 00000000000..5d952b76012 --- /dev/null +++ b/lang/it/jobs.php @@ -0,0 +1,18 @@ + 'Jobs', + + 'no_data' => 'No Jobs have been executed yet.', +]; \ No newline at end of file diff --git a/lang/it/landing.php b/lang/it/landing.php new file mode 100644 index 00000000000..fe6fe55b8ea --- /dev/null +++ b/lang/it/landing.php @@ -0,0 +1,19 @@ + 'Gallery', + 'access_gallery' => 'Access the gallery', + 'hosted_with_lychee' => 'Hosted with Lychee', + 'copyright' => 'All images on this website are subject to copyright by %1$s © %2$s', +]; \ No newline at end of file diff --git a/lang/it/left-menu.php b/lang/it/left-menu.php new file mode 100644 index 00000000000..9a3e91f4037 --- /dev/null +++ b/lang/it/left-menu.php @@ -0,0 +1,29 @@ + 'Back to Gallery', + + 'admin' => 'Admin', + 'clockwork' => 'Clockwork App', + 'logs' => 'Show Logs', + 'jobs' => 'Show Job History', + 'user' => 'User', + + 'sign_out' => 'Sign Out', + + 'about' => 'About', + 'api' => 'API Documentation', + 'source_code' => 'Source Code', + 'support' => 'Support', +]; \ No newline at end of file diff --git a/lang/it/lychee.php b/lang/it/lychee.php new file mode 100644 index 00000000000..53ee6a5aeaf --- /dev/null +++ b/lang/it/lychee.php @@ -0,0 +1,535 @@ + 'Nome utente', + 'PASSWORD' => 'Password', + 'ENTER' => 'Invia', + 'CANCEL' => 'Annulla', + 'CONFIRM' => 'Confirm', + 'SIGN_IN' => 'Entra', + 'CLOSE' => 'Chiudi', + 'SETTINGS' => 'Impostazioni', + 'SEARCH' => 'Cerca …', + 'MORE' => 'Altro', + 'DEFAULT' => 'Default', + 'GALLERY' => 'Gallery', + + 'USERS' => 'Utenti', + 'PROFILE' => 'Profile', + 'CREATE' => 'Create', + 'REMOVE' => 'Remove', + 'SHARE' => 'Share', + 'U2F' => 'U2F', + 'NOTIFICATIONS' => 'Notifications', + 'SHARING' => 'Condivisione', + 'CHANGE_LOGIN' => 'Cambia Login', + 'CHANGE_SORTING' => 'Cambia Ordinamento', + 'SET_DROPBOX' => 'Imposta Dropbox', + 'ABOUT_LYCHEE' => 'Informazioni su Lychee', + 'DIAGNOSTICS' => 'Diagnostica', + 'DIAGNOSTICS_GET_SIZE' => 'Request space usage', + 'JOBS' => 'Show job history', + 'LOGS' => 'Visualizza Log', + 'SIGN_OUT' => 'Esci', + 'UPDATE_AVAILABLE' => 'Aggiornamento disponibile!', + 'MIGRATION_AVAILABLE' => 'Migration available!', + 'CHECK_FOR_UPDATE' => 'Check for updates', + 'DEFAULT_LICENSE' => 'Licenza predefinita per i nuovi caricamenti:', + 'SET_LICENSE' => 'Imposta Licenza', + 'SET_OVERLAY_TYPE' => 'Imposta Filigrana', + 'SET_ALBUM_DECORATION' => 'Set album decorations', + 'SET_MAP_PROVIDER' => 'Set OpenStreetMap tiles provider', + 'FULL_SETTINGS' => 'Full Settings', + 'UPDATE' => 'Update', + 'RESET' => 'Reset', + 'DISABLE_TOKEN_TOOLTIP' => 'Disable', + 'ENABLE_TOKEN' => 'Enable API token', + 'DISABLED_TOKEN_STATUS_MSG' => 'Disabled', + 'TOKEN_BUTTON' => 'API Token ...', + 'TOKEN_NOT_AVAILABLE' => 'You have already viewed this token.', + 'TOKEN_WAIT' => 'Wait ...', + + 'SMART_ALBUMS' => 'Album smart', + 'SHARED_ALBUMS' => 'Album condivisi', + 'ALBUMS' => 'Album', + 'PHOTOS' => 'Immagini', + 'SEARCH_RESULTS' => 'Search results', + + 'RENAME' => 'Rinomina', + 'RENAME_ALL' => 'Rinomina Tutto', + 'MERGE' => 'Unisci', + 'MERGE_ALL' => 'Unisci Tutto', + 'MAKE_PUBLIC' => 'Rendi Pubblico', + 'SHARE_ALBUM' => 'Condividi Album', + 'SHARE_PHOTO' => 'Condividi Photo', + 'VISIBILITY_ALBUM' => 'Album Visibility', + 'VISIBILITY_PHOTO' => 'Photo Visibility', + 'DOWNLOAD_ALBUM' => 'Scarica Album', + 'ABOUT_ALBUM' => 'Informazioni Album', + 'DELETE_ALBUM' => 'Elimina Album', + 'MOVE_ALBUM' => 'Move Album', + 'FULLSCREEN_ENTER' => 'Entra In Modalità Schermo Intero', + 'FULLSCREEN_EXIT' => 'Esci Dalla Modalità Schermo Intero', + + 'SHARING_ALBUM_USERS' => 'Share this album with users', + 'WAIT_FETCH_DATA' => 'Please wait while we get the data …', + 'SHARING_ALBUM_USERS_NO_USERS' => 'There are no users to share the album with', + 'SHARING_ALBUM_USERS_LONG_MESSAGE' => 'Select the users to share this album with', + + 'DELETE_ALBUM_QUESTION' => 'Elimina Album e Immagini', + 'KEEP_ALBUM' => 'Mantieni Album', + 'DELETE_ALBUM_CONFIRMATION' => 'Sei sicuro di voler eliminare l’album «%s» e tutte le immagini che contiene? Questa azione non può essere annullata successivamente!', + + 'DELETE_TAG_ALBUM_QUESTION' => 'Delete Album', + 'DELETE_TAG_ALBUM_CONFIRMATION' => 'Are you sure you want to delete the album «%s» (any photos inside will not be deleted)? This action can’t be undone!', + + 'DELETE_ALBUMS_QUESTION' => 'Elimina gli Album e le Immagini', + 'KEEP_ALBUMS' => 'Mantieni gli Album', + 'DELETE_ALBUMS_CONFIRMATION' => 'Sei sicuro di voler eliminare tutti %d gli album selezionati e le immagini contenute in essi? Questa azione non può essere annullata successivamente!', + + 'DELETE_UNSORTED_CONFIRM' => 'Sei sicuro di voler eliminare tutte le immagini da «Non Catalogate»? Questa azione non può essere annullata successivamente!', + 'CLEAR_UNSORTED' => 'Rimuovi Immagini Non Catalogate', + 'KEEP_UNSORTED' => 'Mantieni Immagini Non Catalogate', + + 'EDIT_SHARING' => 'Modifica Condivisibilità', + 'MAKE_PRIVATE' => 'Rendi Privato', + + 'CLOSE_ALBUM' => 'Chiudi Album', + 'CLOSE_PHOTO' => 'Chiudi Foto', + 'CLOSE_MAP' => 'Close Map', + + 'ADD' => 'Aggiungi', + 'MOVE' => 'Sposta', + 'MOVE_ALL' => 'Sposta Tutto', + 'DUPLICATE' => 'Duplica', + 'DUPLICATE_ALL' => 'Duplica Tutto', + 'COPY_TO' => 'Copia in …', + 'COPY_ALL_TO' => 'Copia Tutto in …', + 'DELETE' => 'Elimina', + 'SAVE' => 'Save', + 'DELETE_ALL' => 'Elimina Tutto', + 'DOWNLOAD' => 'Scarica', + 'DOWNLOAD_ALL' => 'Scarica Tutto', + 'UPLOAD_PHOTO' => 'Carica Foto', + 'IMPORT_LINK' => 'Importa da Link', + 'IMPORT_DROPBOX' => 'Importa da Dropbox', + 'IMPORT_SERVER' => 'Importa da Server', + 'NEW_ALBUM' => 'Nuovo Album', + 'NEW_TAG_ALBUM' => 'New Tag Album', + 'UPLOAD_TRACK' => 'Upload track', + 'DELETE_TRACK' => 'Delete track', + + 'TITLE_NEW_ALBUM' => 'Inserire un titolo per il nuovo album:', + 'UNTITLED' => 'Senza Titolo', + 'UNSORTED' => 'Non Catalogate', + 'STARRED' => 'Speciali', + 'RECENT' => 'Recenti', + 'PUBLIC' => 'Pubbliche', + 'ON_THIS_DAY' => 'On This Day', + 'NUM_PHOTOS' => 'Foto', + + 'CREATE_ALBUM' => 'Crea Album', + 'CREATE_TAG_ALBUM' => 'Create Tag Album', + + 'STAR_PHOTO' => 'Contrassegna la Foto come Speciale', + 'STAR' => 'Contrassegna come Speciale', + 'UNSTAR' => 'Unstar', + 'STAR_ALL' => 'Contrassegna Tutto come Speciale', + 'UNSTAR_ALL' => 'Unstar Selected', + 'TAG' => 'Tag', + 'TAG_ALL' => 'Tagga Tutto', + 'UNSTAR_PHOTO' => 'Rimuovi dalle Foto Speciali', + 'SET_COVER' => 'Set Album Cover', + 'REMOVE_COVER' => 'Remove Album Cover', + 'SET_HEADER' => 'Set Album Header', + 'REMOVE_HEADER' => 'Remove Album Header', + 'SET_COMPACT_HEADER' => 'Use Compact Header', + + 'FULL_PHOTO' => 'Open Original', + 'ABOUT_PHOTO' => 'Informazioni sulla Foto', + 'DISPLAY_FULL_MAP' => 'Map', + 'DIRECT_LINK' => 'Link Diretto', + 'DIRECT_LINKS' => 'Direct Links', + 'QR_CODE' => 'QR Code', + + 'ALBUM_ABOUT' => 'Informazioni', + 'ALBUM_BASICS' => 'Base', + 'ALBUM_TITLE' => 'Titolo', + 'ALBUM_COPYRIGHT' => 'Copyright', + 'ALBUM_SET_COPYRIGHT' => 'Set copyright', + 'ALBUM_NEW_TITLE' => 'Inserire un nuovo titolo per questo album:', + 'ALBUMS_NEW_TITLE' => 'Inserire un nuovo titolo per %d gli album selezionati:', + 'ALBUM_SET_TITLE' => 'Imposta Titolo', + 'ALBUM_DESCRIPTION' => 'Descrizione', + 'ALBUM_SHOW_TAGS' => 'Tags to show', + 'ALBUM_NEW_DESCRIPTION' => 'Inserire una nuova descrizione per questo album:', + 'ALBUM_SET_DESCRIPTION' => 'Imposta Descrizione', + 'ALBUM_NEW_SHOWTAGS' => 'Enter tags of photos that will be visible in this album:', + 'ALBUM_SET_SHOWTAGS' => 'Set tags to show', + 'ALBUM_ALBUM' => 'Album', + 'ALBUM_CREATED' => 'Creato', + 'ALBUM_IMAGES' => 'Immagini', + 'ALBUM_VIDEOS' => 'Video', + 'ALBUM_SUBALBUMS' => 'Subalbums', + 'ALBUM_SHARING' => 'Condividi', + 'ALBUM_SHR_YES' => 'SI', + 'ALBUM_SHR_NO' => 'No', + 'ALBUM_PUBLIC' => 'Pubblico', + 'ALBUM_PUBLIC_EXPL' => 'Anonymous users can access this album, subject to the restrictions below.', + 'ALBUM_FULL' => 'Original', + 'ALBUM_FULL_EXPL' => 'Anonymous users can behold full-resolution photos.', + 'ALBUM_HIDDEN' => 'Nascosto', + 'ALBUM_HIDDEN_EXPL' => 'Anonymous users need a direct link to access this album.', + 'ALBUM_MARK_NSFW' => 'Mark album as sensitive', + 'ALBUM_UNMARK_NSFW' => 'Unmark album as sensitive', + 'ALBUM_NSFW' => 'Sensitive', + 'ALBUM_NSFW_EXPL' => 'Album is marked to contain sensitive content.', + 'ALBUM_DOWNLOADABLE' => 'Scaricabile', + 'ALBUM_DOWNLOADABLE_EXPL' => 'Anonymous users can download this album.', + 'ALBUM_SHARE_BUTTON_VISIBLE' => 'Share button is visible', + 'ALBUM_SHARE_BUTTON_VISIBLE_EXPL' => 'Anonymous users can see social media sharing links.', + 'ALBUM_PASSWORD' => 'Password', + 'ALBUM_PASSWORD_PROT' => 'Password protetta', + 'ALBUM_PASSWORD_PROT_EXPL' => 'Anonymous users need a shared password to access this album.', + 'ALBUM_PASSWORD_REQUIRED' => 'Questo album è protetto da password. Inserire la password sotto per vedere le foto di questo album:', + 'ALBUM_MERGE' => 'Sei sicuro di voler unire l’album «%1$s» nell’album «%2$s»?', + 'ALBUMS_MERGE' => 'Sei sicuro di voler unire tutti gli album selezionati nell’album «%s»?', + 'MERGE_ALBUM' => 'Unisci Album', + 'DONT_MERGE' => 'Non Unire', + 'ALBUM_MOVE' => 'Sei sicuro di voler spostare l’album «%1$s» nell’album «%2$s»?', + 'ALBUMS_MOVE' => 'Sei sicure di voler spostare tutti gli album selezionati nell’album «%s»?', + 'MOVE_ALBUMS' => 'Sposta Album', + 'NOT_MOVE_ALBUMS' => 'Non Spostare', + 'ROOT' => 'Radice', + 'ALBUM_REUSE' => 'Riutilizza', + 'ALBUM_LICENSE' => 'Licenza', + 'ALBUM_SET_LICENSE' => 'Imposta licenza', + 'ALBUM_LICENSE_HELP' => 'Hai bisogno di aiuto per scegliere?', + 'ALBUM_LICENSE_NONE' => 'Nessuna', + 'ALBUM_RESERVED' => 'Tutti i Diritti Riservati', + 'ALBUM_SET_ORDER' => 'Set Order', + 'ALBUM_ORDERING' => 'Order by', + 'ALBUM_PHOTO_ORDERING' => 'Order photos by', + 'ALBUM_CHILDREN_ORDERING' => 'Order albums by', + 'ALBUM_OWNER' => 'Owner', + + 'PHOTO_ABOUT' => 'Informazioni', + 'PHOTO_BASICS' => 'Base', + 'PHOTO_TITLE' => 'Titolo', + 'PHOTO_NEW_TITLE' => 'Inserisci un nuovo titolo per questa foto:', + 'PHOTO_SET_TITLE' => 'Imposta Titolo', + 'PHOTO_UPLOADED' => 'Caricata', + 'PHOTO_DESCRIPTION' => 'Descrizione', + 'PHOTO_NEW_DESCRIPTION' => 'Inserire una nuova descrizione per questa foto:', + 'PHOTO_SET_DESCRIPTION' => 'Imposta Descrizione', + 'PHOTO_NEW_LICENSE' => 'Aggiungi una Licenze', + 'PHOTO_SET_LICENSE' => 'Imposta Licenza', + 'PHOTO_LICENSE' => 'Licenza', + 'PHOTO_LICENSE_HELP' => 'Need help choosing?', + 'PHOTO_REUSE' => 'Riutilizzo', + 'PHOTO_LICENSE_NONE' => 'Nessuna', + 'PHOTO_RESERVED' => 'Tutti i Diritti Riservati', + 'PHOTO_LATITUDE' => 'Latitude', + 'PHOTO_LONGITUDE' => 'Longitude', + 'PHOTO_ALTITUDE' => 'Altitude', + 'PHOTO_LOCATION' => 'Location', + 'PHOTO_IMGDIRECTION' => 'Direction', + 'PHOTO_IMAGE' => 'Immagine', + 'PHOTO_VIDEO' => 'Video', + 'PHOTO_SIZE' => 'Dimensioni', + 'PHOTO_FORMAT' => 'Formato', + 'PHOTO_RESOLUTION' => 'Risoluzione', + 'PHOTO_DURATION' => 'Durata', + 'PHOTO_FPS' => 'Frame rate', + 'PHOTO_TAGS' => 'Tag', + 'PHOTO_NOTAGS' => 'Nessun Tag', + 'PHOTO_NEW_TAGS' => 'Inserisci i tuoi tag per questa foto. Puoi aggiungere più tag separandoli con una virgola:', + 'PHOTOS_NEW_TAGS' => 'Inserisci i tuoi tag per tutte %d le foto selezionate. I tag esistenti verrano sovrascritti. Puoi aggiungere più tag separandoli con una virgola:', + 'PHOTO_SET_TAGS' => 'Imposta Tag', + 'PHOTO_CAMERA' => 'Fotocamera', + 'PHOTO_CAPTURED' => 'Scattata', + 'PHOTO_MAKE' => 'Produttore', + 'PHOTO_TYPE' => 'Tipo/Modello', + 'PHOTO_LENS' => 'Lens', + 'PHOTO_SHUTTER' => 'Tempo di Esposizione', + 'PHOTO_APERTURE' => 'Apertura', + 'PHOTO_FOCAL' => 'Lunghezza Focale', + 'PHOTO_ISO' => 'ISO %s', + 'PHOTO_SHARING' => 'Condivisione', + 'PHOTO_DELETE' => 'Elimina Photo', + 'PHOTO_KEEP' => 'Mantieni Photo', + 'PHOTO_DELETE_CONFIRMATION' => 'Sei sicuro di voler eliminare la foto «%s»? Questa operazione non può essere annullata successivamente!', + 'PHOTO_DELETE_ALL' => 'Sei sicuro di voler eliminare tutte le %d foto selezionate? Questa operazione non può essere annullata successivamente!', + 'PHOTOS_NEW_TITLE' => 'Inserisci un titolo per tutte le %d foto selezionate:', + 'PHOTO_MAKE_PRIVATE_ALBUM' => 'Questa foto è all’interno di un album pubblico. Per rendere questa foto privata o pubblica, modifica la visibilità dell’album associato.', + 'PHOTO_SHOW_ALBUM' => 'Visualizza Album', + 'PHOTO_PUBLIC' => 'Public', + 'PHOTO_PUBLIC_EXPL' => 'Anonymous users can view this photo, subject to the restrictions below.', + 'PHOTO_FULL' => 'Original', + 'PHOTO_FULL_EXPL' => 'Anonymous users can behold full-resolution photo.', + 'PHOTO_HIDDEN' => 'Hidden', + 'PHOTO_HIDDEN_EXPL' => 'Anonymous users need a direct link to view this photo.', + 'PHOTO_DOWNLOADABLE' => 'Downloadable', + 'PHOTO_DOWNLOADABLE_EXPL' => 'Anonymous users may download this photo.', + 'PHOTO_SHARE_BUTTON_VISIBLE' => 'Share button is visible', + 'PHOTO_SHARE_BUTTON_VISIBLE_EXPL' => 'Anonymous users can see social media sharing links.', + 'PHOTO_PASSWORD_PROT' => 'Password protected', + 'PHOTO_PASSWORD_PROT_EXPL' => 'Anonymous users need a shared password to view this photo.', + 'PHOTO_EDIT_SHARING_TEXT' => 'The sharing properties of this photo will be changed to the following:', + 'PHOTO_NO_EDIT_SHARING_TEXT' => 'Because this photo is located in a public album, it inherits that album’s visibility settings. Its current visibility is shown below for informational purposes only.', + 'PHOTO_EDIT_GLOBAL_SHARING_TEXT' => 'The visibility of this photo can be fine-tuned using global Lychee settings. Its current visibility is shown below for informational purposes only.', + 'PHOTO_NEW_CREATED_AT' => 'Enter the upload date for this photo. mm/dd/yyyy, hh:mm [am/pm]', + 'PHOTO_SET_CREATED_AT' => 'Set upload date', + + 'LOADING' => 'Caricamento', + 'ERROR' => 'Errore', + 'ERROR_TEXT' => 'Oops, sembra che qualcosa sia andato storto. Per favore ricarica il sito e prova di nuovo!', + 'ERROR_UNKNOWN' => 'È successo qualcosa di inaspettato. Per favore prova di nuovo e controlla la tua installazione e il tuo server. Controlla il readme per più informazioni.', + 'ERROR_MAP_DEACTIVATED' => 'Map functionality has been deactivated under settings.', + 'ERROR_SEARCH_DEACTIVATED' => 'Search functionality has been deactivated under settings.', + 'SUCCESS' => 'OK', + 'CHANGE_SUCCESS' => 'Change successful.', + 'RETRY' => 'Riprova', + 'OVERRIDE' => 'Override', + 'TAGS_OVERRIDE_INFO' => 'If this is unchecked, the tags will be added to the existing tags of the photo.', + + 'SETTINGS_SUCCESS_LOGIN' => 'Informazioni di Login Aggiornate.', + 'SETTINGS_SUCCESS_SORT' => 'Modalità di ordinamento aggiornate.', + 'SETTINGS_SUCCESS_DROPBOX' => 'Chaive Dropbox aggiornata.', + 'SETTINGS_SUCCESS_LANG' => 'Lingua aggiornata', + 'SETTINGS_SUCCESS_LAYOUT' => 'Layout aggiornato', + 'SETTINGS_SUCCESS_IMAGE_OVERLAY' => 'Impostazioni filigrana EXIF aggiornate', + 'SETTINGS_SUCCESS_PUBLIC_SEARCH' => 'Ricerca pubblica aggiornata', + 'SETTINGS_SUCCESS_LICENSE' => 'Licenza predefinita aggiornata', + 'SETTINGS_SUCCESS_MAP_DISPLAY' => 'Map display settings updated', + 'SETTINGS_SUCCESS_MAP_DISPLAY_PUBLIC' => 'Map display settings for public albums updated', + 'SETTINGS_SUCCESS_MAP_PROVIDER' => 'Map provider settings updated', + 'SETTINGS_SUCCESS_CSS' => 'Stylesheets updated', + 'SETTINGS_SUCCESS_JS' => 'JS updated', + 'SETTINGS_SUCCESS_UPDATE' => 'Settings updated successfully', + 'SETTINGS_DROPBOX_KEY' => 'Dropbox API Key', + 'SETTINGS_ADVANCED_WARNING_EXPL' => 'Changing these advanced settings can be harmful to the stability, security and performance of this application. You should only modify them if you are sure of what you are doing.', + 'SETTINGS_ADVANCED_SAVE' => 'Save my modifications, I accept the risk!', + + 'U2F_NOT_SUPPORTED' => 'U2F not supported. Sorry.', + 'U2F_NOT_SECURE' => 'Environment not secured. U2F not available.', + 'U2F_REGISTER_KEY' => 'Register new device.', + 'U2F_REGISTRATION_SUCCESS' => 'Registration successful!', + 'U2F_AUTHENTIFICATION_SUCCESS' => 'Authentication successful!', + 'U2F_CREDENTIALS' => 'Credentials', + 'U2F_CREDENTIALS_DELETED' => 'Credentials deleted!', + 'U2F_LOGIN' => 'Log in with WebAuthn', + + 'NEW_PHOTOS_NOTIFICATION' => 'Send new photos notification emails.', + 'SETTINGS_SUCCESS_NEW_PHOTOS_NOTIFICATION' => 'New photos notification updated', + 'USER_EMAIL_INSTRUCTION' => 'Add your email below to enable receiving email notifications. To stop receiving emails, simply remove your email below.', + + 'LOGIN_USERNAME' => 'Nuovo Nome Utente', + 'LOGIN_PASSWORD' => 'Nuova Password', + 'LOGIN_PASSWORD_CONFIRM' => 'Conferma Password', + 'PASSWORD_TITLE' => 'Inserisci la tua password attuale:', + 'PASSWORD_CURRENT' => 'Password Attuale', + 'PASSWORD_TEXT' => 'Il tuo nome utente e password verrano cambiati nei seguenti:', + 'PASSWORD_CHANGE' => 'Cambia Login', + + 'EDIT_SHARING_TITLE' => 'Modifica Condivisibilità', + 'EDIT_SHARING_TEXT' => 'Le proprietà di condivisione di questo album verrano cambiate nelle seguenti:', + 'SHARE_ALBUM_TEXT' => 'Questo album verrà condiviso con le seguenti proprietà:', + + 'SORT_DIALOG_ATTRIBUTE_LABEL' => 'Attribute', + 'SORT_DIALOG_ORDER_LABEL' => 'Order', + + 'SORT_ALBUM_BY' => 'Ordina album per %1$s in un ordine %2$s.', + + 'SORT_ALBUM_SELECT_1' => 'Data di Creazione', + 'SORT_ALBUM_SELECT_2' => 'Titolo', + 'SORT_ALBUM_SELECT_3' => 'Descrizione', + 'SORT_ALBUM_SELECT_5' => 'Ultima Aggiornamento', + 'SORT_ALBUM_SELECT_6' => 'Aggiornamento più vecchio', + + 'SORT_PHOTO_BY' => 'Ordina foto per %1$s in un ordine %2$s.', + + 'SORT_PHOTO_SELECT_1' => 'Data di Upload', + 'SORT_PHOTO_SELECT_2' => 'Data di Creazione', + 'SORT_PHOTO_SELECT_3' => 'Titolo', + 'SORT_PHOTO_SELECT_4' => 'Descrizione', + 'SORT_PHOTO_SELECT_6' => 'Speciale', + 'SORT_PHOTO_SELECT_7' => 'Formato Photo', + + 'SORT_ASCENDING' => 'Crescente', + 'SORT_DESCENDING' => 'Decrescente', + 'SORT_CHANGE' => 'Cambia Ordinamento', + + 'DROPBOX_TITLE' => 'Imposta Chiave Dropbox', + 'DROPBOX_TEXT' => "Per importare foto dal tuo Dropbox, ha bisogno di una chiave valida ottenibile da their website. Genera la tua chiave personale e inseriscila qui di seguito:", + + 'LANG_TEXT' => 'Cambia Lingua Lychee per:', + 'LANG_TITLE' => 'Cambia Lingua', + + 'SETTING_RECENT_PUBLIC_TEXT' => 'Make "Recent" smart album accessible to anonymous users', + 'SETTING_STARRED_PUBLIC_TEXT' => 'Make "Starred" smart album accessible to anonymous users', + 'SETTING_ONTHISDAY_PUBLIC_TEXT' => 'Make "On This Day" smart album accessible to anonymous users', + + 'CSS_TEXT' => 'Personalize CSS:', + 'CSS_TITLE' => 'Change CSS', + 'JS_TEXT' => 'Custom JS:', + 'JS_TITLE' => 'Change JS', + 'PUBLIC_SEARCH_TEXT' => 'Ricerca pubblica consentita:', + 'OVERLAY_TYPE' => 'Contenuto da utilizzare nella filigrana:', + 'OVERLAY_NONE' => 'None', + 'OVERLAY_EXIF' => 'Dati Foto EXIF', + 'OVERLAY_DESCRIPTION' => 'Descrizione della Foto', + 'OVERLAY_DATE' => 'Data di Creazione della Foto', + 'ALBUM_DECORATION' => 'Album decorations:', + 'ALBUM_DECORATION_NONE' => 'None', + 'ALBUM_DECORATION_ORIGINAL' => 'Sub-album marker', + 'ALBUM_DECORATION_ALBUM' => 'Number of sub-albums', + 'ALBUM_DECORATION_PHOTO' => 'Number of photos', + 'ALBUM_DECORATION_ALL' => 'Number of sub-albums and photos', + 'ALBUM_DECORATION_ORIENTATION' => 'Orientation of album decorations:', + 'ALBUM_DECORATION_ORIENTATION_ROW' => 'Horizontal (photos, albums)', + 'ALBUM_DECORATION_ORIENTATION_ROW_REVERSE' => 'Horizontal (albums, photos)', + 'ALBUM_DECORATION_ORIENTATION_COLUMN' => 'Vertical (top photos, albums)', + 'ALBUM_DECORATION_ORIENTATION_COLUMN_REVERSE' => 'Vertical (top albums, photos)', + 'MAP_DISPLAY_TEXT' => 'Enable maps (provided by OpenStreetMap):', + 'MAP_DISPLAY_PUBLIC_TEXT' => 'Enable maps for public albums (provided by OpenStreetMap):', + 'MAP_PROVIDER' => 'Provider of OpenStreetMap tiles:', + 'MAP_PROVIDER_WIKIMEDIA' => 'Wikimedia', + 'MAP_PROVIDER_OSM_ORG' => 'OpenStreetMap.org (no HiDPI)', + 'MAP_PROVIDER_OSM_DE' => 'OpenStreetMap.de (no HiDPI)', + 'MAP_PROVIDER_OSM_FR' => 'OpenStreetMap.fr (no HiDPI)', + 'MAP_PROVIDER_RRZE' => 'University of Erlangen, Germany (only HiDPI)', + 'MAP_INCLUDE_SUBALBUMS_TEXT' => 'Include photos of subalbums on map:', + 'LOCATION_DECODING' => 'Decode GPS data into location name', + 'LOCATION_SHOW' => 'Show location name', + 'LOCATION_SHOW_PUBLIC' => 'Show location name for public mode', + + 'LAYOUT_TYPE' => 'Layout delle foto:', + 'LAYOUT_SQUARES' => 'Miniature Quadrate', + 'LAYOUT_JUSTIFIED' => 'Relativo all’aspetto, giustificate', + 'LAYOUT_MASONRY' => 'Relativo all’aspetto, masonry', + 'LAYOUT_GRID' => 'Relativo all’aspetto, grid', + 'LAYOUT_UNJUSTIFIED' => 'Relativo all’aspetto, non giustificate', + 'SET_LAYOUT' => 'Cambia layout', + + 'NSFW_VISIBLE_TEXT_1' => 'Make Sensitive albums visible by default.', + 'NSFW_VISIBLE_TEXT_2' => 'If the album is public, it is still accessible, just hidden from the view and can be revealed by pressing H.', + 'SETTINGS_SUCCESS_NSFW_VISIBLE' => 'Default sensitive album visibility updated with success.', + + 'NSFW_BANNER' => '

Sensitive content

This album contains sensitive content which some people may find offensive or disturbing.

Tap to consent.

', + 'NSFW_HEADER' => 'Sensitive content', + 'NSFW_EXPLANATION' => 'This album contains sensitive content which some people may find offensive or disturbing.', + 'TAP_CONSENT' => 'Tap to consent.', + + 'VIEW_NO_RESULT' => 'Nessun risultato', + 'VIEW_NO_PUBLIC_ALBUMS' => 'Nessun album pubblico', + 'VIEW_NO_CONFIGURATION' => 'Nessuna configurazione', + 'VIEW_PHOTO_NOT_FOUND' => 'Foto non trovata', + + 'NO_TAGS' => 'Nessun Tag', + + 'UPLOAD_MANAGE_NEW_PHOTOS' => 'Adesso puoi gestire le tue nuove foto.', + 'UPLOAD_COMPLETE' => 'Caricamento completato', + 'UPLOAD_COMPLETE_FAILED' => 'Caricamento fallito per una o più foto.', + 'UPLOAD_IMPORTING' => 'Importazione', + 'UPLOAD_IMPORTING_URL' => 'Importazione URL', + 'UPLOAD_UPLOADING' => 'Caricamento', + 'UPLOAD_FINISHED' => 'Finito', + 'UPLOAD_PROCESSING' => 'In Elaborazione', + 'UPLOAD_FAILED' => 'Fallito', + 'UPLOAD_FAILED_ERROR' => 'Caricamento fallito. Il server ha restituito un errore!', + 'UPLOAD_FAILED_WARNING' => 'Caricamento fallito. Il server ha restituito un avviso!', + 'UPLOAD_CANCELLED' => 'Cancelled', + 'UPLOAD_SKIPPED' => 'Saltato', + 'UPLOAD_UPDATED' => 'Updated', + 'UPLOAD_GENERAL' => 'General', + 'UPLOAD_IMPORT_SKIPPED_DUPLICATE' => 'This photo has been skipped because it’s already in your library.', + 'UPLOAD_IMPORT_RESYNCED_DUPLICATE' => 'This photo has been skipped because it’s already in your library, but its metadata has been updated.', + 'UPLOAD_ERROR_CONSOLE' => 'Per favore controlla la console del tuo browser per ulteriori dettagli.', + 'UPLOAD_UNKNOWN' => 'Il server ha restituito una risposta sconosciuta. Per favore controlla la console del tuo browser per ulteriori dettagli.', + 'UPLOAD_ERROR_UNKNOWN' => 'Caricamneto fallito. Il server ha restituito un errore sconosciuto!', + 'UPLOAD_ERROR_POSTSIZE' => 'Upload failed. The PHP post_max_size may be too small! Otherwise check the FAQ.', + 'UPLOAD_ERROR_FILESIZE' => 'Upload failed. The PHP upload_max_filesize may be too small! Otherwise check the FAQ.', + 'UPLOAD_IN_PROGRESS' => 'Lychee sta momentaneamente caricando!', + 'UPLOAD_IMPORT_WARN_ERR' => 'L’importazione è finita, ma ha restituito errori o avvisi. Per favore controlla il log (Impostazioni -> Visualizza Log) per ulteriori dettagli.', + 'UPLOAD_IMPORT_COMPLETE' => 'Importazione completata', + 'UPLOAD_IMPORT_INSTR' => 'Per favore inserisci il link diretto alla foto per importarla:', + 'UPLOAD_IMPORT' => 'Importa', + 'UPLOAD_IMPORT_SERVER' => 'Importa da server', + 'UPLOAD_IMPORT_SERVER_FOLD' => 'Cartella vuota o nessun file leggibile da elaborare. Per favore controlla il log (Impostazioni -> Visualizza Log) per ulteriori dettagli.', + 'UPLOAD_IMPORT_SERVER_INSTR' => 'Import all photos, folders and sub-folders located in the folders with the following absolute paths (on server). Paths are space separated, use \\ to escape a space in a path.', + 'UPLOAD_ABSOLUTE_PATH' => 'Absolute path to directories, space separated', + 'UPLOAD_IMPORT_SERVER_EMPT' => 'È stato impossibile avviare l’importazione dato che la cartella era vuota!', + 'UPLOAD_IMPORT_DELETE_ORIGINALS' => 'Delete originals', + 'UPLOAD_IMPORT_DELETE_ORIGINALS_EXPL' => 'I file originali saranno eliminati dopo l’importazione se possibile.', + 'UPLOAD_IMPORT_VIA_SYMLINK' => 'Symbolic links', + 'UPLOAD_IMPORT_VIA_SYMLINK_EXPL' => 'Import files using symbolic links to originals.', + 'UPLOAD_IMPORT_SKIP_DUPLICATES' => 'Skip duplicates', + 'UPLOAD_IMPORT_SKIP_DUPLICATES_EXPL' => 'Existing media files are skipped.', + 'UPLOAD_IMPORT_RESYNC_METADATA' => 'Re-sync metadata', + 'UPLOAD_IMPORT_RESYNC_METADATA_EXPL' => 'Update metadata of existing media files.', + 'UPLOAD_IMPORT_LOW_MEMORY_EXPL' => 'The import process on the server is approaching the memory limit and may end up being terminated prematurely.', + 'UPLOAD_WARNING' => 'Warning', + 'UPLOAD_IMPORT_NOT_A_DIRECTORY' => 'The given path is not a readable directory!', + 'UPLOAD_IMPORT_PATH_RESERVED' => 'The given path is a reserved path of Lychee!', + 'UPLOAD_IMPORT_FAILED' => 'Could not import the file!', + 'UPLOAD_IMPORT_UNSUPPORTED' => 'Unsupported file type!', + 'UPLOAD_IMPORT_CANCELLED' => 'Import cancelled', + + 'ABOUT_SUBTITLE' => 'Gestione propria delle foto fatta nel modo giusto', + 'ABOUT_DESCRIPTION' => 'Lychee è uno strumento gratuito di gestione delle foto, eseguito nel server o sul tuo spazio web. L’installazione è questione di secondi. Carica, gestisci e condividi foto come in un’applicazione nativa. Lychee offre tutto ciò di cui hai bisogno e tutte le tue foto vengono salvate in modo sicuro.', + 'FOOTER_COPYRIGHT' => 'Tutte le immagini su questo sito web sono soggette a copyright di %1$s © %2$s', + 'HOSTED_WITH_LYCHEE' => 'Hosted with Lychee', + + 'URL_COPY_TO_CLIPBOARD' => 'Copy to clipboard', + 'URL_COPIED_TO_CLIPBOARD' => 'Copied URL to clipboard!', + 'PHOTO_DIRECT_LINKS_TO_IMAGES' => 'Direct links to image files:', + 'PHOTO_ORIGINAL' => 'Original', + 'PHOTO_MEDIUM' => 'Medium', + 'PHOTO_MEDIUM_HIDPI' => 'Medium HiDPI', + 'PHOTO_SMALL' => 'Thumb', + 'PHOTO_SMALL_HIDPI' => 'Thumb HiDPI', + 'PHOTO_THUMB' => 'Square thumb', + 'PHOTO_THUMB_HIDPI' => 'Square thumb HiDPI', + 'PHOTO_PLACEHOLDER' => 'Low Quality Image Placeholder', + 'PHOTO_THUMBNAIL' => 'Photo thumbnail', + 'PHOTO_LIVE_VIDEO' => 'Video part of live-photo', + 'PHOTO_VIEW' => 'Lychee Photo View:', + + 'PHOTO_EDIT_ROTATECWISE' => 'Ruota in senso orario', + 'PHOTO_EDIT_ROTATECCWISE' => 'Ruota in senso anti-orario', + + 'ERROR_GPX' => 'Error loading GPX file: ', + 'ERROR_EITHER_ALBUMS_OR_PHOTOS' => 'Please select either albums or photos!', + 'ERROR_COULD_NOT_FIND' => 'Could not find what you want.', + 'ERROR_INVALID_EMAIL' => 'Not a valid email address.', + 'EMAIL_SUCCESS' => 'Email updated!', + 'ERROR_PHOTO_NOT_FOUND' => 'Error: photo %s not found !', + 'ERROR_EMPTY_USERNAME' => 'new username cannot be empty.', + 'ERROR_PASSWORD_DOES_NOT_MATCH' => 'new password does not match.', + 'ERROR_EMPTY_PASSWORD' => 'new password cannot be empty.', + 'ERROR_SELECT_ALBUM' => 'Select an album to share!', + 'ERROR_SELECT_USER' => 'Select a user to share with!', + 'ERROR_SELECT_SHARING' => 'Select a sharing to remove!', + 'SHARING_SUCCESS' => 'Sharing updated!', + 'SHARING_REMOVED' => 'Sharing removed!', + 'USER_CREATED' => 'User created!', + 'USER_DELETED' => 'User deleted!', + 'USER_UPDATED' => 'User updated!', + 'ENTER_EMAIL' => 'Enter your email address:', + 'ERROR_ALBUM_JSON_NOT_FOUND' => 'Error: Album json not found!', + 'ERROR_ALBUM_NOT_FOUND' => 'Error: album %s not found', + 'ERROR_DROPBOX_KEY' => 'Error: Dropbox key not set', + 'ERROR_SESSION' => 'Session expired.', + 'CAMERA_DATE' => 'Camera date', + 'NEW_PASSWORD' => 'new password', + 'ALLOW_UPLOADS' => 'Allow uploads', + 'ALLOW_USER_SELF_EDIT' => 'Allow self-management of user account', + 'OSM_CONTRIBUTORS' => 'OpenStreetMap contributors', +]; diff --git a/lang/it/maintenance.php b/lang/it/maintenance.php new file mode 100644 index 00000000000..f86de3d6f46 --- /dev/null +++ b/lang/it/maintenance.php @@ -0,0 +1,60 @@ + 'Maintenance', + 'description' => 'You will find on this page, all the required actions to keep your Lychee installation running smooth and nicely.', + 'cleaning' => [ + 'title' => 'Cleaning %s', + 'result' => '%s deleted.', + 'description' => 'Remove all contents from %s', + 'button' => 'Clean', + ], + 'fix-jobs' => [ + 'title' => 'Fixing Jobs History', + 'description' => 'Mark jobs with status %s or %s as %s.', + 'button' => 'Fix job history', + ], + 'gen-sizevariants' => [ + 'title' => 'Missing %s', + 'description' => 'Found %d %s that could be generated.', + 'button' => 'Generate!', + 'success' => 'Successfully generated %d %s.', + ], + 'fill-filesize-sizevariants' => [ + 'title' => 'File sizes missing', + 'description' => 'Found %d small variants without file size.', + 'button' => 'Fetch data!', + 'success' => 'Successfully computed sizes of %d small variants.', + ], + 'fix-tree' => [ + 'title' => 'Tree statistics', + 'Oddness' => 'Oddness', + 'Duplicates' => 'Duplicates', + 'Wrong parents' => 'Wrong parents', + 'Missing parents' => 'Missing parents', + 'button' => 'Fix tree', + ], + 'optimize' => [ + 'title' => 'Optimize Database', + 'description' => 'If you notice slowdown in your installation, it may be because your database does not + have all its needed index.', + 'button' => 'Optimize Database', + ], + 'update' => [ + 'title' => 'Updates', + 'check-button' => 'Check for updates', + 'update-button' => 'Update', + 'no-pending-updates' => 'No pending update.', + ], +]; \ No newline at end of file diff --git a/lang/it/profile.php b/lang/it/profile.php new file mode 100644 index 00000000000..cc24b97452c --- /dev/null +++ b/lang/it/profile.php @@ -0,0 +1,64 @@ + 'Profile', + + 'login' => [ + 'header' => 'Profile', + 'enter_current_password' => 'Enter your current password:', + 'current_password' => 'Current password', + 'credentials_update' => 'Your credentials will be changed to the following:', + 'username' => 'Username', + 'new_password' => 'New password', + 'confirm_new_password' => 'Confirm new password', + 'email_instruction' => 'Add your email below to enable receiving email notifications. To stop receiving emails, simply remove your email below.', + 'email' => 'Email', + 'change' => 'Change Login', + 'api_token' => 'API Token ...', + + 'missing_fields' => 'Missing fields', + ], + + 'token' => [ + 'unavailable' => 'You have already viewed this token.', + 'no_data' => 'No token API have been generated.', + 'disable' => 'Disable', + 'disabled' => 'Token disabled', + 'warning' => 'This token will not be displayed again. Copy it and keep it in a safe place.', + 'reset' => 'Reset the token', + 'create' => 'Create a new token', + ], + + 'oauth' => [ + 'header' => 'OAuth', + 'header_not_available' => 'OAuth is not available', + 'setup_env' => 'Set up the credentials in your .env', + 'token_registered' => '%s token registered.', + 'setup' => 'Set up %s', + 'reset' => 'reset', + 'credential_deleted' => 'Credential deleted!', + ], + + 'u2f' => [ + 'header' => 'Passkey/MFA/2FA', + 'info' => 'This only provides the ability to use WebAuthn to authenticate instead of username & password.', + 'empty' => 'Credentials list is empty!', + 'not_secure' => 'Environment not secured. U2F not available.', + 'new' => 'Register new device.', + 'credential_deleted' => 'Credential deleted!', + 'credential_updated' => 'Credential updated!', + 'credential_registred' => 'Registration successful!', + '5_chars' => 'At least 5 chars.', + ], +]; \ No newline at end of file diff --git a/lang/it/settings.php b/lang/it/settings.php new file mode 100644 index 00000000000..fd197f11135 --- /dev/null +++ b/lang/it/settings.php @@ -0,0 +1,92 @@ + 'Settings', + 'small_screen' => 'For better a experience on the Settings page,
we recommend you use a larger screen.', + 'tabs' => [ + 'basic' => 'Basic', + 'all_settings' => 'All settings', + ], + 'toasts' => [ + 'change_saved' => 'Change saved!', + 'details' => 'Settings have been modified as per request', + 'error' => 'Error!', + 'error_load_css' => 'Could not load dist/user.css', + 'error_load_js' => 'Could not load dist/custom.js', + 'error_save_css' => 'Could not save CSS', + 'error_save_js' => 'Could not save JS', + 'thank_you' => 'Thank you for your support.', + 'reload' => 'Reload your page for full functionalities.', + ], + 'system' => [ + 'header' => 'System', + 'use_dark_mode' => 'Use dark mode for Lychee', + 'language' => 'Language used by Lychee', + 'nsfw_album_visibility' => 'Make Sensitive albums visible by default.', + 'nsfw_album_explanation' => 'If the album is public, it is still accessible, just hidden from the view and can be revealed by pressing H.', + ], + 'lychee_se' => [ + 'header' => 'Lychee SE', + 'call4action' => 'Get exclusive features and support the development of Lychee. Unlock the SE edition.', + 'preview' => 'Enable preview of Lychee SE features', + 'hide_call4action' => 'Hide this Lychee SE registration form. I am happy with Lychee as-is. :)', + 'hide_warning' => 'If enabled, the only way to register your license key will be via the More tab above. Changes are applied on page reload.', + ], + 'dropbox' => [ + 'header' => 'Dropbox', + 'instruction' => 'In order to import photos from your Dropbox, you need a valid drop-ins app key from their website.', + 'api_key' => 'Dropbox API Key', + 'set_key' => 'Set Dropbox Key', + ], + 'gallery' => [ + 'header' => 'Gallery', + 'photo_order_column' => 'Default column used for sorting photos', + 'photo_order_direction' => 'Default order used for sorting photos', + 'album_order_column' => 'Default column used for sorting albums', + 'album_order_direction' => 'Default order used for sorting albums', + 'aspect_ratio' => 'Default aspect ratio for album thumbs', + 'photo_layout' => 'Layout for pictures', + 'album_decoration' => 'Show decorations on album cover (sub-album and/or photo count)', + 'album_decoration_direction' => 'Align album decorations horizontally or vertically', + 'photo_overlay' => 'Default image overlay information', + 'license_default' => 'Default license used for albums', + 'license_help' => 'Need help choosing?', + ], + 'geolocation' => [ + 'header' => 'Geo-location', + 'map_display' => 'Display the map given GPS coordinates', + 'map_display_public' => 'Allow anonymous users to access the map', + 'map_provider' => 'Defines the map provider', + 'map_include_subalbums' => 'Includes pictures of the sub albums on the map', + 'location_decoding' => 'Use GPS location decoding', + 'location_show' => 'Show location extracted from GPS coordinates', + 'location_show_public' => 'Anonymous users can access the extracted location from GPS coordinates', + ], + 'advanced' => [ + 'header' => 'Advanced Customization', + 'change_css' => 'Change CSS', + 'change_js' => 'Change JS', + ], + 'all' => [ + 'old_setting_style' => 'Old setting style', + 'change_detected' => 'Some settings changed.', + 'save' => 'Save', + ], + + 'tool_option' => [ + 'disabled' => 'disabled', + 'enabled' => 'enabled', + 'discover' => 'discover', + ], +]; \ No newline at end of file diff --git a/lang/it/sharing.php b/lang/it/sharing.php new file mode 100644 index 00000000000..69de18cc6d0 --- /dev/null +++ b/lang/it/sharing.php @@ -0,0 +1,33 @@ + 'Sharing', + + 'info' => 'This page gives an overview of and the ability to edit the sharing rights associated with albums.', + 'album_title' => 'Album title', + 'username' => 'Username', + 'no_data' => 'Sharing list is empty.', + 'share' => 'Share', + 'permission_deleted' => 'Permission deleted!', + 'permission_created' => 'Permission created!', + + 'grants' => [ + 'read' => 'Grants read access', + 'original' => 'Grants access to original photo', + 'download' => 'Grants download', + 'upload' => 'Grants upload', + 'edit' => 'Grants edit', + 'delete' => 'Grants delete', + ], +]; \ No newline at end of file diff --git a/lang/it/statistics.php b/lang/it/statistics.php new file mode 100644 index 00000000000..2baf855bbd5 --- /dev/null +++ b/lang/it/statistics.php @@ -0,0 +1,34 @@ + 'Statistics', + + 'preview_text' => 'This is a preview of the statistics page available in Lychee SE.
The data shown here are randomly generated and do not reflect your server.', + 'no_data' => 'User does not have data on server.', + 'collapse' => 'Collapse albums sizes', + + 'total' => [ + 'total' => 'Total', + 'albums' => 'Albums', + 'photos' => 'Photos', + 'size' => 'Size', + ], + 'table' => [ + 'username' => 'Owner', + 'title' => 'Title', + 'photos' => 'Photos', + 'descendants' => 'Children', + 'size' => 'Size', + ], +]; \ No newline at end of file diff --git a/lang/it/toasts.php b/lang/it/toasts.php new file mode 100644 index 00000000000..293d4b72594 --- /dev/null +++ b/lang/it/toasts.php @@ -0,0 +1,17 @@ + 'Error', + 'success' => 'Success', +]; \ No newline at end of file diff --git a/lang/it/users.php b/lang/it/users.php new file mode 100644 index 00000000000..599bb833454 --- /dev/null +++ b/lang/it/users.php @@ -0,0 +1,44 @@ + 'Users', + 'description' => 'Here you can manage the users of your Lychee installation. You can create, edit and delete users.', + 'create' => 'Create a new user', + 'username' => 'Username', + 'password' => 'Password', + 'legend' => 'Legend', + 'upload_rights' => 'When selected, the user can upload content.', + 'edit_rights' => 'When selected, the user can modify their profile (username, password).', + 'quota' => 'When set, the user has a space quota for pictures (in kB).', + + 'user_deleted' => 'User deleted', + 'user_created' => 'User created', + 'user_updated' => 'User updated', + 'change_saved' => 'Change saved!', + + 'create_edit' => [ + 'upload_rights' => 'User can upload content.', + 'edit_rights' => 'User can modify their profile (username, password).', + 'quota' => 'User has quota limit.', + 'quota_kb' => 'quota in kB (0 for default)', + 'note' => 'Admin note (not publically visible)', + 'create' => 'Create', + 'edit' => 'Edit', + ], + 'line' => [ + 'admin' => 'admin user', + 'edit' => 'Edit', + 'delete' => 'Delete', + ], +]; \ No newline at end of file diff --git a/lang/ja/aspect_ratio.php b/lang/ja/aspect_ratio.php new file mode 100644 index 00000000000..32671d2f494 --- /dev/null +++ b/lang/ja/aspect_ratio.php @@ -0,0 +1,21 @@ + '5/4 (Instagram 横向き)', + '4by5' => '4/5 (Instagram 縦向き)', + '2by3' => '2/3 (縦向き)', + '3by2' => '3/2 (横向き)', + '1by1' => 'スクエア', + '1byx9' => '16/9 (横向き)', +]; \ No newline at end of file diff --git a/lang/ja/diagnostics.php b/lang/ja/diagnostics.php new file mode 100644 index 00000000000..0fadd640428 --- /dev/null +++ b/lang/ja/diagnostics.php @@ -0,0 +1,30 @@ + 'Diagnostics', + + 'copy_to_clipboard' => 'Copy diagnostics to clipboard', + 'self-diagnosis' => 'Self-diagnosis', + 'info' => 'Info', + 'space' => 'Space', + 'load_space' => 'Load space usage.', + 'configuration' => 'Configuration', + 'loading' => 'Loading...', + 'identical_content' => 'Identical content', + + 'toast' => [ + 'info' => 'Info', + 'copy' => 'Diagnostics copied to clipboard!', + ], +]; \ No newline at end of file diff --git a/lang/ja/dialogs.php b/lang/ja/dialogs.php new file mode 100644 index 00000000000..4afd65fae3f --- /dev/null +++ b/lang/ja/dialogs.php @@ -0,0 +1,221 @@ + [ + 'close' => 'Close', + 'cancel' => 'Cancel', + 'save' => 'Save', + 'delete' => 'Delete', + 'move' => 'Move', + ], + 'about' => [ + 'subtitle' => 'Self-hosted photo-management done right', + 'description' => 'Lychee is a free photo-management tool, which runs on your server or web-space. Installing is a matter of seconds. Upload, manage and share photos like from a native application. Lychee comes with everything you need and all your photos are stored securely.', + 'update_available' => 'Update available!', + 'thank_you' => 'Thank you for your support!', + 'get_supporter_or_register' => 'Get exclusive features and support the development of Lychee.
Unlock the Supporter Edition or register your License key', + 'here' => 'here', + ], + 'dropbox' => [ + 'not_configured' => 'Dropbox is not configured.', + ], + 'import_from_link' => [ + 'instructions' => 'Please enter the direct link to a photo to import it:', + 'import' => 'Import', + ], + 'keybindings' => [ + 'don_t_show_again' => 'Don\'t show this again', + 'side_wide' => 'Site-wide Shortcuts', + 'back_cancel' => 'Back/Cancel', + 'confirm' => 'Confirm', + 'login' => 'Login', + 'toggle_full_screen' => 'Toggle Full Screen', + 'toggle_sensitive_albums' => 'Toggle Sensitive Albums', + + 'albums' => 'Albums Shortcuts', + 'new_album' => 'New Album', + 'upload_photos' => 'Upload Photos', + 'search' => 'Search', + 'show_this_modal' => 'Show this modal', + 'select_all' => 'Select All', + 'move_selection' => 'Move Selection', + 'delete_selection' => 'Delete Selection', + + 'album' => 'Album Shortcuts', + 'slideshow' => 'Start/Stop Slideshow', + 'toggle' => 'Toggle panel', + + 'photo' => 'Photo Shortcuts', + 'previous' => 'Previous photo', + 'next' => 'Next photo', + 'cycle' => 'Cycle overlay mode', + 'star' => 'Star the photo', + 'move' => 'Move the photo', + 'delete' => 'Delete the photo', + 'edit' => 'Edit information', + 'show_hide_meta' => 'Show information', + + 'keep_hidden' => 'We will keep it hidden.', + ], + 'login' => [ + 'username' => 'Username', + 'password' => 'Password', + 'unknown_invalid' => 'Unknown user or invalid password.', + 'signin' => 'Sign-In', + ], + 'register' => [ + 'enter_license' => 'Enter your license key below:', + 'license_key' => 'License key', + 'invalid_license' => 'Invalid license key.', + 'register' => 'Register', + ], + 'share_album' => [ + 'url_copied' => 'Copied URL to clipboard!', + ], + 'upload' => [ + 'completed' => 'Completed', + 'uploaded' => 'Uploaded:', + 'release' => 'Release file to upload!', + 'select' => 'Click here to select files to upload', + 'drag' => '(Or drag files to the page)', + 'loading' => 'Loading', + 'resume' => 'Resume', + 'uploading' => 'Uploading', + 'finished' => 'Finished', + 'failed_error' => 'Upload failed. The server returned an error!', + ], + 'visibility' => [ + 'public' => 'Public', + 'public_expl' => 'Anonymous users can access this album, subject to the restrictions below.', + 'full' => 'Original', + 'full_expl' => 'Anonymous users can view full-resolution photos.', + 'hidden' => 'Hidden', + 'hidden_expl' => 'Anonymous users need a direct link to access this album.', + 'downloadable' => 'Downloadable', + 'downloadable_expl' => 'Anonymous users can download this album.', + 'password' => 'Password', + 'password_prot' => 'Password protected', + 'password_prot_expl' => 'Anonymous users need a shared password to access this album.', + 'nsfw' => 'Sensitive', + 'nsfw_expl' => 'Album contains sensitive content.', + 'visibility_updated' => 'Visibility updated.', + ], + 'move_album' => [ + 'confirm_single' => 'Are you sure you want to move the album “%1$s” into the album “%2$s”?', + 'confirm_multiple' => 'Are you sure you want to move all selected albums into the album “%s”?', + 'move_single' => 'Move Album', + 'move_to' => 'Move to', + 'move_to_single' => 'Move %s to:', + 'move_to_multiple' => 'Move %d albums to:', + 'no_album_target' => 'No album to move to', + 'moved_single' => 'Album moved!', + 'moved_single_details' => '%1$s moved to %2$s', + 'moved_details' => 'Album(s) moved to %s', + ], + 'new_album' => [ + 'menu' => 'Create Album', + 'info' => 'Enter a title for the new album:', + 'title' => 'title', + 'create' => 'Create Album', + ], + 'new_tag_album' => [ + 'menu' => 'Create Tag Album', + 'info' => 'Enter a title for the new tag album:', + 'title' => 'title', + 'set_tags' => 'Set tags to show', + 'warn' => 'Make sure to press enter after each tag', + 'create' => 'Create Tag Album', + ], + 'delete_album' => [ + 'confirmation' => 'Are you sure you want to delete the album “%s” and all of the photos it contains?', + 'confirmation_multiple' => 'Are you sure you want to delete all %d selected albums and all of the photos they contain?', + 'warning' => 'This action can not be undone!', + 'delete' => 'Delete Album and Photos', + ], + 'transfer' => [ + 'query' => 'Transfer ownership of album to', + 'confirmation' => 'Are you sure you want to transfer the ownership of album “%s” and all the photos it contains to "%s"?', + 'lost_access_warning' => 'Your access to this album will be lost.', + 'warning' => 'This action can not be undone!', + 'transfer' => 'Transfer ownership of album and photos', + ], + 'rename' => [ + 'photo' => 'Enter a new title for this photo:', + 'album' => 'Enter a new title for this album:', + 'rename' => 'Rename', + ], + 'merge' => [ + 'merge_to' => 'Merge %s to:', + 'merge_to_multiple' => 'Merge %d albums to:', + 'no_albums' => 'No albums to merge to.', + 'confirm' => 'Are you sure you want to merge the album “%1$s” into the album “%2$s”?', + 'confirm_multiple' => 'Are you sure you want to merge all selected albums into the album “%s”?', + 'merge' => 'Merge Albums', + 'merged' => 'Album(s) merged to %s!', + ], + 'unlock' => [ + 'password_required' => 'This album is protected by a password. Enter the password below to view the photos of this album:', + 'password' => 'Password', + 'unlock' => 'Unlock', + ], + 'photo_tags' => [ + 'question' => 'Enter your tags for this photo.', + 'question_multiple' => 'Enter your tags for all %d selected photos. Existing tags will be overwritten.', + 'no_tags' => 'No Tags', + 'set_tags' => 'Set Tags', + 'updated' => 'Tags updated!', + 'tags_override_info' => 'If this is unchecked, the tags will be added to the existing tags of the photo.', + ], + 'photo_copy' => [ + 'no_albums' => 'No albums to copy to', + 'copy_to' => 'Copy %s to:', + 'copy_to_multiple' => 'Copy %d photos to:', + 'confirm' => 'Copy %s to %s.', + 'confirm_multiple' => 'Copy %d photos to %s.', + 'copy' => 'Copy', + 'copied' => 'Photo(s) copied!', + ], + 'photo_delete' => [ + 'confirm' => 'Are you sure you want to delete the photo “%s”?', + 'confirm_multiple' => 'Are you sure you want to delete all %d selected photos?', + 'deleted' => 'Photo(s) deleted!', + ], + 'move_photo' => [ + 'move_single' => 'Move %s to:', + 'move_multiple' => 'Move %d photos to:', + 'confirm' => 'Move %s to %s.', + 'confirm_multiple' => 'Move %d photos to %s.', + 'moved' => 'Photo(s) moved to %s!', + ], + 'target_user' => [ + 'placeholder' => 'Select user', + ], + 'target_album' => [ + 'placeholder' => 'Select album', + ], + 'webauthn' => [ + 'u2f' => 'U2F', + 'success' => 'Authentication successful!', + 'error' => 'Whoops, it looks like something went wrong. Please reload the site and try again!', + ], + 'se' => [ + 'available' => 'Available in the Supporter Edition', + ], + 'session_expired' => [ + 'title' => 'Session expired', + 'message' => 'Your session has expired.
Please reload the page.', + 'reload' => 'Reload', + 'go_to_gallery' => 'Go to the Gallery', + ], +]; \ No newline at end of file diff --git a/lang/ja/fix-tree.php b/lang/ja/fix-tree.php new file mode 100644 index 00000000000..64803e310e6 --- /dev/null +++ b/lang/ja/fix-tree.php @@ -0,0 +1,55 @@ + 'Maintenance', + 'intro' => 'This page allows you to re-order and fix your albums manually.
Before any modifications, we strongly recommend you to read about Nested Set tree structures.', + 'warning' => 'You can really break your Lychee installation here, modify values at your own risks.', + + 'help' => [ + 'header' => 'Help', + 'hover' => 'Hover ids or titles to highlight related albums.', + 'left' => 'Left', + 'right' => 'Right', + 'convenience' => 'For your convenience, the and buttons allow you to change the values of %s and %s by respectively +1 and -1 with propagation.', + 'left-right-warn' => 'The and indicates that the value of %s (and respectively %s) is duplicated somewhere.', + 'parent-marked' => 'Marked Parent Id indicates that the %s and %s do not satisfy the Nest Set tree structures. Edit either the Parent Id or the %s/%s values.', + 'slowness' => 'This page will be slow with a large number of albums.', + ], + + 'buttons' => [ + 'reset' => 'Reset', + 'check' => 'Check', + 'apply' => 'Apply', + ], + + 'table' => [ + 'title' => 'Title', + 'left' => 'Left', + 'right' => 'Right', + 'id' => 'Id', + 'parent' => 'Parent Id', + ], + + 'errors' => [ + 'invalid' => 'Invalid tree!', + 'invalid_details' => 'We are not applying this as it is guaranteed to be a broken state.', + 'invalid_left' => 'Album %s has an invalid left value.', + 'invalid_right' => 'Album %s has an invalid right value.', + 'invalid_left_right' => 'Album %s has an invalid left/right values. Left should be strictly smaller than right: %s < %s.', + 'duplicate_left' => 'Album %s has a duplicate left value %s.', + 'duplicate_right' => 'Album %s has a duplicate right value %s.', + 'parent' => 'Album %s has an unexpected parent id %s.', + 'unknown' => 'Album %s has an unknown error.', + ], +]; \ No newline at end of file diff --git a/lang/ja/gallery.php b/lang/ja/gallery.php new file mode 100644 index 00000000000..eb8008827e0 --- /dev/null +++ b/lang/ja/gallery.php @@ -0,0 +1,241 @@ + 'Gallery', + + 'smart_albums' => 'Smart albums', + 'albums' => 'Albums', + 'root' => 'Albums', + + 'original' => 'Original', + 'medium' => 'Medium', + 'medium_hidpi' => 'Medium HiDPI', + 'small' => 'Thumb', + 'small_hidpi' => 'Thumb HiDPI', + 'thumb' => 'Square thumb', + 'thumb_hidpi' => 'Square thumb HiDPI', + 'placeholder' => 'Low Quality Image Placeholder', + 'thumbnail' => 'Photo thumbnail', + 'live_video' => 'Video part of live-photo', + + 'camera_data' => 'Camera date', + 'album_reserved' => 'All Rights Reserved', + + 'map' => [ + 'error_gpx' => 'Error loading GPX file', + 'osm_contributors' => 'OpenStreetMap contributors', + ], + + 'search' => [ + 'title' => 'Search', + 'searching' => 'Searching…', + 'no_results' => 'Nothing matches your search query.', + 'searchbox' => 'Search…', + 'minimum_chars' => 'Minimum %s characters required.', + 'photos' => 'Photos (%s)', + 'albums' => 'Albums (%s)', + ], + + 'smart_album' => [ + 'unsorted' => 'Unsorted', + 'starred' => 'Starred', + 'recent' => 'Recent', + 'public' => 'Public', + 'on_this_day' => 'On This Day', + ], + + 'layout' => [ + 'squares' => 'Square thumbnails', + 'justified' => 'With aspect, justified', + 'masonry' => 'With aspect, masonry', + 'grid' => 'With aspect, grid', + ], + + 'overlay' => [ + 'none' => 'None', + 'exif' => 'EXIF data', + 'description' => 'Description', + 'date' => 'Date taken', + ], + + 'timeline' => [ + 'default' => 'default', + 'disabled' => 'disabled', + 'year' => 'Year', + 'month' => 'Month', + 'day' => 'Day', + 'hour' => 'Hour', + ], + + 'album' => [ + 'header_albums' => 'Albums', + 'header_photos' => 'Photos', + 'no_results' => 'Nothing to see here', + 'upload' => 'Upload photos', + + 'tabs' => [ + 'about' => 'About Album', + 'share' => 'Share Album', + 'move' => 'Move Album', + 'danger' => 'DANGER ZONE', + ], + + 'hero' => [ + 'created' => 'Created', + 'copyright' => 'Copyright', + 'subalbums' => 'Subalbums', + 'images' => 'Photos', + 'download' => 'Download Album', + 'share' => 'Share Album', + 'stats_only_se' => 'Statistics available in the Supporter Edition', + ], + + 'stats' => [ + 'lens' => 'Lens', + 'shutter' => 'Shutter speed', + 'iso' => 'ISO', + 'model' => 'Model', + 'aperture' => 'Aperture', + 'no_data' => 'No data', + ], + + 'properties' => [ + 'title' => 'Title', + 'description' => 'Description', + 'photo_ordering' => 'Order photos by', + 'children_ordering' => 'Order albums by', + 'asc/desc' => 'asc/desc', + 'header' => 'Set album header', + 'compact_header' => 'Use compact header', + 'license' => 'Set license', + 'copyright' => 'Set copyright', + 'aspect_ratio' => 'Set album thumbs aspect ratio', + 'album_timeline' => 'Set album timeline mode', + 'photo_timeline' => 'Set photo timeline mode', + 'layout' => 'Set photo layout', + 'show_tags' => 'Set tags to show', + 'tags_required' => 'Tags are required.', + ], + ], + + 'photo' => [ + 'actions' => [ + 'star' => 'Star', + 'unstar' => 'Unstar', + 'set_album_header' => 'Set as album header', + 'move' => 'Move', + 'delete' => 'Delete', + 'header_set' => 'Header set', + ], + + 'details' => [ + 'about' => 'About', + 'basics' => 'Basics', + 'title' => 'Title', + 'uploaded' => 'Uploaded', + 'description' => 'Description', + 'license' => 'License', + 'reuse' => 'Reuse', + 'latitude' => 'Latitude', + 'longitude' => 'Longitude', + 'altitude' => 'Altitude', + 'location' => 'Location', + 'image' => 'Image', + 'video' => 'Video', + 'size' => 'Size', + 'format' => 'Format', + 'resolution' => 'Resolution', + 'duration' => 'Duration', + 'fps' => 'Frame rate', + 'tags' => 'Tags', + 'camera' => 'Camera', + 'captured' => 'Captured', + 'make' => 'Make', + 'type' => 'Type/Model', + 'lens' => 'Lens', + 'shutter' => 'Shutter Speed', + 'aperture' => 'Aperture', + 'focal' => 'Focal Length', + 'iso' => 'ISO %s', + ], + + 'edit' => [ + 'set_title' => 'Set Title', + 'set_description' => 'Set Description', + 'set_license' => 'Set License', + 'no_tags' => 'No Tags', + 'set_tags' => 'Set Tags', + 'set_created_at' => 'Set Upload Date', + ], + ], + + 'nsfw' => [ + 'header' => 'Sensitive content', + 'description' => 'This album contains sensitive content which some people may find offensive or disturbing.', + 'consent' => 'Tap to consent.', + ], + + 'menus' => [ + 'star' => 'Star', + 'unstar' => 'Unstar', + 'star_all' => 'Star Selected', + 'unstar_all' => 'Unstar Selected', + 'tag' => 'Tag', + 'tag_all' => 'Tag Selected', + 'set_cover' => 'Set Album Cover', + 'remove_header' => 'Remove Album Header', + 'set_header' => 'Set Album Header', + 'copy_to' => 'Copy to …', + 'copy_all_to' => 'Copy Selected to …', + 'rename' => 'Rename', + 'move' => 'Move', + 'move_all' => 'Move Selected', + 'delete' => 'Delete', + 'delete_all' => 'Delete Selected', + 'download' => 'Download', + 'download_all' => 'Download Selected', + 'merge' => 'Merge', + 'merge_all' => 'Merge Selected', + + 'upload_photo' => 'Upload Photo', + 'import_link' => 'Import from Link', + 'import_dropbox' => 'Import from Dropbox', + 'new_album' => 'New Album', + 'new_tag_album' => 'New Tag Album', + 'upload_track' => 'Upload track', + 'delete_track' => 'Delete track', + ], + + 'sort' => [ + 'photo_select_1' => 'Upload Time', + 'photo_select_2' => 'Take Date', + 'photo_select_3' => 'Title', + 'photo_select_4' => 'Description', + 'photo_select_6' => 'Star', + 'photo_select_7' => 'Photo Format', + 'ascending' => 'Ascending', + 'descending' => 'Descending', + 'album_select_1' => 'Creation Time', + 'album_select_2' => 'Title', + 'album_select_3' => 'Description', + 'album_select_5' => 'Latest Take Date', + 'album_select_6' => 'Oldest Take Date', + ], + + 'albums_protection' => [ + 'private' => 'private', + 'public' => 'public', + 'inherit_from_parent' => 'inherit from parent', + ], +]; \ No newline at end of file diff --git a/lang/ja/jobs.php b/lang/ja/jobs.php new file mode 100644 index 00000000000..5d952b76012 --- /dev/null +++ b/lang/ja/jobs.php @@ -0,0 +1,18 @@ + 'Jobs', + + 'no_data' => 'No Jobs have been executed yet.', +]; \ No newline at end of file diff --git a/lang/ja/landing.php b/lang/ja/landing.php new file mode 100644 index 00000000000..fe6fe55b8ea --- /dev/null +++ b/lang/ja/landing.php @@ -0,0 +1,19 @@ + 'Gallery', + 'access_gallery' => 'Access the gallery', + 'hosted_with_lychee' => 'Hosted with Lychee', + 'copyright' => 'All images on this website are subject to copyright by %1$s © %2$s', +]; \ No newline at end of file diff --git a/lang/ja/left-menu.php b/lang/ja/left-menu.php new file mode 100644 index 00000000000..9a3e91f4037 --- /dev/null +++ b/lang/ja/left-menu.php @@ -0,0 +1,29 @@ + 'Back to Gallery', + + 'admin' => 'Admin', + 'clockwork' => 'Clockwork App', + 'logs' => 'Show Logs', + 'jobs' => 'Show Job History', + 'user' => 'User', + + 'sign_out' => 'Sign Out', + + 'about' => 'About', + 'api' => 'API Documentation', + 'source_code' => 'Source Code', + 'support' => 'Support', +]; \ No newline at end of file diff --git a/lang/ja/lychee.php b/lang/ja/lychee.php new file mode 100644 index 00000000000..7194e35f977 --- /dev/null +++ b/lang/ja/lychee.php @@ -0,0 +1,535 @@ + 'ユーザー名', + 'PASSWORD' => 'パスワード', + 'ENTER' => '決定', + 'CANCEL' => 'キャンセル', + 'CONFIRM' => '確認', + 'SIGN_IN' => 'サインイン', + 'CLOSE' => '閉じる', + 'SETTINGS' => '設定', + 'SEARCH' => '検索 …', + 'MORE' => 'さらに表示', + 'DEFAULT' => 'デフォルト', + 'GALLERY' => 'ギャラリー', + + 'USERS' => 'ユーザー', + 'PROFILE' => 'プロフィール', + 'CREATE' => '作成', + 'REMOVE' => '削除', + 'SHARE' => '共有', + 'U2F' => 'U2F', + 'NOTIFICATIONS' => '通知', + 'SHARING' => '共有', + 'CHANGE_LOGIN' => 'ログイン情報を変更', + 'CHANGE_SORTING' => '順序を変更', + 'SET_DROPBOX' => 'Dropboxを設定', + 'ABOUT_LYCHEE' => 'Lycheeについて', + 'DIAGNOSTICS' => '診断', + 'DIAGNOSTICS_GET_SIZE' => '使用ストレージを取得', + 'JOBS' => 'ジョブ履歴を表示', + 'LOGS' => 'ログを表示', + 'SIGN_OUT' => 'サインアウト', + 'UPDATE_AVAILABLE' => '更新が利用可能です!', + 'MIGRATION_AVAILABLE' => 'データ移行が利用可能です!', + 'CHECK_FOR_UPDATE' => '更新を確認', + 'DEFAULT_LICENSE' => '既定のライセンス:', + 'SET_LICENSE' => 'ライセンスを設定', + 'SET_OVERLAY_TYPE' => 'オーバーレイを設定', + 'SET_ALBUM_DECORATION' => 'アルバムの装飾を設定', + 'SET_MAP_PROVIDER' => 'OpenStreetMapタイルのプロバイダを設定', + 'FULL_SETTINGS' => 'すべての設定', + 'UPDATE' => '更新', + 'RESET' => 'リセット', + 'DISABLE_TOKEN_TOOLTIP' => '無効化', + 'ENABLE_TOKEN' => 'APIトークンを有効化', + 'DISABLED_TOKEN_STATUS_MSG' => '無効です', + 'TOKEN_BUTTON' => 'APIトークン ...', + 'TOKEN_NOT_AVAILABLE' => 'このトークンは既に閲覧されています', + 'TOKEN_WAIT' => 'しばらくお待ち下さい ...', + + 'SMART_ALBUMS' => 'スマートアルバム', + 'SHARED_ALBUMS' => '共有アルバム', + 'ALBUMS' => 'アルバム', + 'PHOTOS' => '写真', + 'SEARCH_RESULTS' => '検索結果', + + 'RENAME' => '名前を変更', + 'RENAME_ALL' => '名前を変更', + 'MERGE' => '結合', + 'MERGE_ALL' => '結合', + 'MAKE_PUBLIC' => '公開', + 'SHARE_ALBUM' => 'アルバムを共有', + 'SHARE_PHOTO' => '写真を共有', + 'VISIBILITY_ALBUM' => 'Album Visibility', + 'VISIBILITY_PHOTO' => 'Photo Visibility', + 'DOWNLOAD_ALBUM' => 'アルバムをダウンロード', + 'ABOUT_ALBUM' => 'アルバムについて', + 'DELETE_ALBUM' => 'アルバムを削除', + 'MOVE_ALBUM' => 'アルバムを移動', + 'FULLSCREEN_ENTER' => 'フルスクリーンを開始', + 'FULLSCREEN_EXIT' => 'フルスクリーンを終了', + + 'SHARING_ALBUM_USERS' => 'アルバムを他のユーザーと共有', + 'WAIT_FETCH_DATA' => 'データを取得中 …', + 'SHARING_ALBUM_USERS_NO_USERS' => '共有するユーザーがいません', + 'SHARING_ALBUM_USERS_LONG_MESSAGE' => '共有するユーザーを選択', + + 'DELETE_ALBUM_QUESTION' => 'アルバムと写真を削除', + 'KEEP_ALBUM' => 'アルバムを保持', + 'DELETE_ALBUM_CONFIRMATION' => 'アルバム “%s” とそこに含まれる写真を削除しますがよろしいですか? この操作は取り消しできません!', + + 'DELETE_TAG_ALBUM_QUESTION' => 'アルバムを削除', + 'DELETE_TAG_ALBUM_CONFIRMATION' => 'アルバム “%s” を削除しますがよろしいですか? (アルバム内の写真は削除されません) この操作は取り消しできません!', + + 'DELETE_ALBUMS_QUESTION' => 'アルバムと写真を削除', + 'KEEP_ALBUMS' => 'アルバムを保持', + 'DELETE_ALBUMS_CONFIRMATION' => '%d個のアルバムとそこに含まれる写真を削除しますがよろしいですか? この操作は取り消しできません!', + + 'DELETE_UNSORTED_CONFIRM' => '“未整理”アルバムとそこに含まれる写真を削除しますがよろしいですか? この操作は取り消しできません!', + 'CLEAR_UNSORTED' => '“未整理”アルバムを消去', + 'KEEP_UNSORTED' => '“未整理”アルバムを保持', + + 'EDIT_SHARING' => '共有を編集', + 'MAKE_PRIVATE' => '非公開', + + 'CLOSE_ALBUM' => 'アルバムを閉じる', + 'CLOSE_PHOTO' => '写真を閉じる', + 'CLOSE_MAP' => 'マップを閉じる', + + 'ADD' => '追加', + 'MOVE' => '移動', + 'MOVE_ALL' => '移動', + 'DUPLICATE' => '複製', + 'DUPLICATE_ALL' => '複製', + 'COPY_TO' => 'コピー …', + 'COPY_ALL_TO' => 'コピー …', + 'DELETE' => '削除', + 'SAVE' => '保存', + 'DELETE_ALL' => '削除', + 'DOWNLOAD' => 'ダウンロード', + 'DOWNLOAD_ALL' => 'ダウンロード', + 'UPLOAD_PHOTO' => '写真をアップロード', + 'IMPORT_LINK' => 'リンク先からインポート', + 'IMPORT_DROPBOX' => 'Dropboxからインポート', + 'IMPORT_SERVER' => 'サーバーからインポート', + 'NEW_ALBUM' => '新しいアルバム', + 'NEW_TAG_ALBUM' => '新しいタグアルバム', + 'UPLOAD_TRACK' => 'トラックをアップロード', + 'DELETE_TRACK' => 'トラックを削除', + + 'TITLE_NEW_ALBUM' => '新しいアルバムの名称を入力', + 'UNTITLED' => '名称未設定', + 'UNSORTED' => '未整理', + 'STARRED' => 'スター付き', + 'RECENT' => '最近追加された項目', + 'PUBLIC' => '公開', + 'ON_THIS_DAY' => 'On This Day', + 'NUM_PHOTOS' => '写真', + + 'CREATE_ALBUM' => 'アルバムを作成', + 'CREATE_TAG_ALBUM' => 'タグアルバムの作成', + + 'STAR_PHOTO' => 'スターを付ける', + 'STAR' => 'スターを付ける', + 'UNSTAR' => 'スターを外す', + 'STAR_ALL' => 'スターを付ける', + 'UNSTAR_ALL' => 'スターを外す', + 'TAG' => 'タグ', + 'TAG_ALL' => 'タグ', + 'UNSTAR_PHOTO' => 'スターを外す', + 'SET_COVER' => 'アルバムカバーに設定', + 'REMOVE_COVER' => 'アルバムカバーを外す', + 'SET_HEADER' => 'アルバムのヘッダーに設定', + 'REMOVE_HEADER' => 'アルバムのヘッダーを外す', + 'SET_COMPACT_HEADER' => 'コンパクトヘッダーを使用する', + + 'FULL_PHOTO' => 'オリジナルを開く', + 'ABOUT_PHOTO' => '写真の情報', + 'DISPLAY_FULL_MAP' => 'マップ', + 'DIRECT_LINK' => 'ダイレクトリンク', + 'DIRECT_LINKS' => 'ダイレクトリンク', + 'QR_CODE' => 'QRコード', + + 'ALBUM_ABOUT' => '情報', + 'ALBUM_BASICS' => '基本', + 'ALBUM_TITLE' => 'タイトル', + 'ALBUM_COPYRIGHT' => '著作権', + 'ALBUM_SET_COPYRIGHT' => '著作権を設定', + 'ALBUM_NEW_TITLE' => 'このアルバムのの新しいタイトルを入力:', + 'ALBUMS_NEW_TITLE' => '%d個のアルバムの新しいタイトルを入力:', + 'ALBUM_SET_TITLE' => 'タイトルを設定', + 'ALBUM_DESCRIPTION' => '説明', + 'ALBUM_SHOW_TAGS' => '表示されるタグ', + 'ALBUM_NEW_DESCRIPTION' => 'このアルバムの新しい説明を入力:', + 'ALBUM_SET_DESCRIPTION' => '説明を設定', + 'ALBUM_NEW_SHOWTAGS' => 'アルバムに表示されるタグを入力:', + 'ALBUM_SET_SHOWTAGS' => '表示されるタグを設定', + 'ALBUM_ALBUM' => 'アルバム', + 'ALBUM_CREATED' => '作成時間', + 'ALBUM_IMAGES' => '画像数', + 'ALBUM_VIDEOS' => '動画数', + 'ALBUM_SUBALBUMS' => 'サブアルバム数', + 'ALBUM_SHARING' => '共有', + 'ALBUM_SHR_YES' => 'オン', + 'ALBUM_SHR_NO' => 'オフ', + 'ALBUM_PUBLIC' => '公開', + 'ALBUM_PUBLIC_EXPL' => '匿名ユーザーは、以下の制限に従ってこのアルバムにアクセスできます。', + 'ALBUM_FULL' => 'オリジナル', + 'ALBUM_FULL_EXPL' => '匿名ユーザーはフル解像度の写真を見ることができます。', + 'ALBUM_HIDDEN' => '非表示', + 'ALBUM_HIDDEN_EXPL' => '匿名ユーザーがこのアルバムにアクセスするには、直接リンクが必要です。', + 'ALBUM_MARK_NSFW' => 'センシティブな内容として設定', + 'ALBUM_UNMARK_NSFW' => 'センシティブな内容ではないと設定', + 'ALBUM_NSFW' => 'センシティブ', + 'ALBUM_NSFW_EXPL' => 'センシティブなコンテンツを含んでいます', + 'ALBUM_DOWNLOADABLE' => 'ダウンロード可能', + 'ALBUM_DOWNLOADABLE_EXPL' => '匿名ユーザーはこのアルバムをダウンロードできます。', + 'ALBUM_SHARE_BUTTON_VISIBLE' => '共有ボタンを表示', + 'ALBUM_SHARE_BUTTON_VISIBLE_EXPL' => '匿名ユーザーはソーシャル メディアの共有リンクを見ることができます。', + 'ALBUM_PASSWORD' => 'パスワード', + 'ALBUM_PASSWORD_PROT' => 'パスワードで保護', + 'ALBUM_PASSWORD_PROT_EXPL' => '匿名ユーザーがこのアルバムにアクセスするには、共有パスワードが必要です。', + 'ALBUM_PASSWORD_REQUIRED' => 'このアルバムはパスワードで保護されています。このアルバムの写真を表示するには、パスワードを入力してください:', + 'ALBUM_MERGE' => 'アルバム “%1$s” を アルバム “%2$s”に結合してもよろしいですか?', + 'ALBUMS_MERGE' => '選択した全てのアルバムをアルバム “%s” に結合してもよろしいですか?', + 'MERGE_ALBUM' => 'アルバムを結合', + 'DONT_MERGE' => '結合しない', + 'ALBUM_MOVE' => 'アルバム “%1$s” をアルバム “%2$s” に移動してもよろしいですか?', + 'ALBUMS_MOVE' => '選択した全てのアルバムをアルバム “%s” に移動してもよろしいですか?', + 'MOVE_ALBUMS' => 'アルバムを移動', + 'NOT_MOVE_ALBUMS' => '移動しない', + 'ROOT' => 'アルバム', + 'ALBUM_REUSE' => '再使用', + 'ALBUM_LICENSE' => 'ライセンス', + 'ALBUM_SET_LICENSE' => 'ライセンスを使用', + 'ALBUM_LICENSE_HELP' => '選択する際にサポートが必要ですか?', + 'ALBUM_LICENSE_NONE' => 'なし', + 'ALBUM_RESERVED' => '無断転載を禁じる', + 'ALBUM_SET_ORDER' => '順序を設定する', + 'ALBUM_ORDERING' => '並び替え', + 'ALBUM_PHOTO_ORDERING' => '写真を並び替え', + 'ALBUM_CHILDREN_ORDERING' => 'アルバムを並び替え', + 'ALBUM_OWNER' => '所有者', + + 'PHOTO_ABOUT' => '情報', + 'PHOTO_BASICS' => '基本', + 'PHOTO_TITLE' => 'タイトル', + 'PHOTO_NEW_TITLE' => 'この写真の新しいタイトルを入力してください:', + 'PHOTO_SET_TITLE' => 'タイトルを設定', + 'PHOTO_UPLOADED' => 'アップロード日時', + 'PHOTO_DESCRIPTION' => '設定', + 'PHOTO_NEW_DESCRIPTION' => 'この写真のの新しい説明を入力:', + 'PHOTO_SET_DESCRIPTION' => '説明を設定', + 'PHOTO_NEW_LICENSE' => 'ライセンスを追加', + 'PHOTO_SET_LICENSE' => 'ライセンスを設定', + 'PHOTO_LICENSE' => 'ライセンス', + 'PHOTO_LICENSE_HELP' => '選択する際にサポートが必要ですか?', + 'PHOTO_REUSE' => '再使用', + 'PHOTO_LICENSE_NONE' => 'なし', + 'PHOTO_RESERVED' => '無断転載を禁じる', + 'PHOTO_LATITUDE' => '緯度', + 'PHOTO_LONGITUDE' => '経度', + 'PHOTO_ALTITUDE' => '高度', + 'PHOTO_IMGDIRECTION' => '方角', + 'PHOTO_LOCATION' => '位置', + 'PHOTO_IMAGE' => '画像', + 'PHOTO_VIDEO' => '動画', + 'PHOTO_SIZE' => 'サイズ', + 'PHOTO_FORMAT' => 'フォーマット', + 'PHOTO_RESOLUTION' => '解像度', + 'PHOTO_DURATION' => '再生時間', + 'PHOTO_FPS' => 'フレームレート', + 'PHOTO_TAGS' => 'タグ', + 'PHOTO_NOTAGS' => 'タグがありません', + 'PHOTO_NEW_TAGS' => 'この写真のタグを入力してください。カンマで区切って複数のタグを追加できます:', + 'PHOTOS_NEW_TAGS' => '選択した %d 枚の写真すべてに設定するタグを入力してください。既存のタグは上書きされます。カンマで区切って複数のタグを追加できます:', + 'PHOTO_SET_TAGS' => 'タグを設定', + 'PHOTO_CAMERA' => 'カメラ情報', + 'PHOTO_CAPTURED' => '撮影日時', + 'PHOTO_MAKE' => '製造元', + 'PHOTO_TYPE' => 'モデル名', + 'PHOTO_LENS' => 'レンズ', + 'PHOTO_SHUTTER' => 'シャッタースピード', + 'PHOTO_APERTURE' => '絞り', + 'PHOTO_FOCAL' => '焦点距離', + 'PHOTO_ISO' => 'ISO感度 %s', + 'PHOTO_SHARING' => '共有', + 'PHOTO_DELETE' => '写真を削除', + 'PHOTO_KEEP' => '写真を保持', + 'PHOTO_DELETE_CONFIRMATION' => '写真 “%s” を削除してもよろしいですか?この操作は取り消しできません!', + 'PHOTO_DELETE_ALL' => '選択した写真 %d 枚すべてを削除してもよろしいですか?この操作は取り消しできません!', + 'PHOTOS_NEW_TITLE' => '選択した %d 枚の写真のタイトルを入力してください:', + 'PHOTO_MAKE_PRIVATE_ALBUM' => 'この写真は公開アルバムにあります。この写真を非公開または公開にするには、関連するアルバムの公開設定を編集してください。', + 'PHOTO_SHOW_ALBUM' => 'アルバムを表示', + 'PHOTO_PUBLIC' => '公開', + 'PHOTO_PUBLIC_EXPL' => '匿名ユーザーは、以下の制限に従ってこの写真を閲覧できます。', + 'PHOTO_FULL' => 'オリジナル', + 'PHOTO_FULL_EXPL' => '匿名ユーザーはフル解像度の写真を見ることができます。', + 'PHOTO_HIDDEN' => '非表示', + 'PHOTO_HIDDEN_EXPL' => '匿名ユーザーがこの写真を閲覧するには直接リンクが必要です。', + 'PHOTO_DOWNLOADABLE' => 'ダウンロード可能', + 'PHOTO_DOWNLOADABLE_EXPL' => '匿名ユーザーはこの写真をダウンロードできます。', + 'PHOTO_SHARE_BUTTON_VISIBLE' => '共有ボタンを表示', + 'PHOTO_SHARE_BUTTON_VISIBLE_EXPL' => '匿名ユーザーはソーシャル メディアの共有リンクを見ることができます。', + 'PHOTO_PASSWORD_PROT' => 'パスワードで保護', + 'PHOTO_PASSWORD_PROT_EXPL' => '匿名ユーザーがこの写真を閲覧するには共有パスワードが必要です。', + 'PHOTO_EDIT_SHARING_TEXT' => 'この写真の共有プロパティは次のように変更されます:', + 'PHOTO_NO_EDIT_SHARING_TEXT' => 'この写真は公開アルバムにあるため、そのアルバムの公開設定を継承します。現在の公開設定は、情報提供のみを目的として以下に表示されます。', + 'PHOTO_EDIT_GLOBAL_SHARING_TEXT' => 'この写真の表示設定は、Lychee のグローバル設定を使用して微調整できます。現在の表示設定は、情報提供のみを目的として以下に表示されます。', + 'PHOTO_NEW_CREATED_AT' => 'この写真のアップロード日を入力してください。 mm/dd/yyyy, hh:mm [am/pm]', + 'PHOTO_SET_CREATED_AT' => 'アップロード日を設定', + + 'LOADING' => '読み込み中', + 'ERROR' => 'エラー', + 'ERROR_TEXT' => '何か問題が発生したようです。サイトを再読み込みしてもう一度お試しください。', + 'ERROR_UNKNOWN' => '予期しない問題が発生しました。もう一度試して、インストールとサーバーを確認してください。詳細については、readme を参照してください。', + 'ERROR_MAP_DEACTIVATED' => '設定でマップ機能が無効になっています。', + 'ERROR_SEARCH_DEACTIVATED' => '設定で検索機能が無効になっています。', + 'SUCCESS' => '成功しました', + 'CHANGE_SUCCESS' => '変更に成功しました。', + 'RETRY' => '再試行', + 'OVERRIDE' => '上書き', + 'TAGS_OVERRIDE_INFO' => 'チェックを外すと、写真の既存のタグにタグが追加されます。', + + 'SETTINGS_SUCCESS_LOGIN' => 'ログイン情報が更新されました', + 'SETTINGS_SUCCESS_SORT' => '並び替え順序が更新されました', + 'SETTINGS_SUCCESS_DROPBOX' => 'Dropboxのキーが更新されました', + 'SETTINGS_SUCCESS_LANG' => '言語が更新されました', + 'SETTINGS_SUCCESS_LAYOUT' => 'レイアウトが更新されました', + 'SETTINGS_SUCCESS_IMAGE_OVERLAY' => 'EXIFオーバーレイ設定が更新されました', + 'SETTINGS_SUCCESS_PUBLIC_SEARCH' => '公開検索が更新されました', + 'SETTINGS_SUCCESS_LICENSE' => '既定のライセンスが更新されました', + 'SETTINGS_SUCCESS_MAP_DISPLAY' => 'マップの表示設定が更新されました', + 'SETTINGS_SUCCESS_MAP_DISPLAY_PUBLIC' => '公開アルバムのマップ表示設定が更新されました', + 'SETTINGS_SUCCESS_MAP_PROVIDER' => 'マップタイルのプロバイダ設定が更新されました', + 'SETTINGS_SUCCESS_CSS' => 'CSSが更新されました', + 'SETTINGS_SUCCESS_JS' => 'JSが更新されました', + 'SETTINGS_SUCCESS_UPDATE' => '設定が正常に更新されました', + 'SETTINGS_DROPBOX_KEY' => 'Dropbox APIキー', + 'SETTINGS_ADVANCED_WARNING_EXPL' => 'これらの詳細設定を変更すると、このアプリケーションの安定性、セキュリティ、パフォーマンスに悪影響を与える可能性があります。変更内容に確信がある場合のみ、設定を変更してください。', + 'SETTINGS_ADVANCED_SAVE' => 'リスクについて理解し、変更を保存します', + + 'U2F_NOT_SUPPORTED' => 'U2F はサポートされていません', + 'U2F_NOT_SECURE' => '保護されていないため、U2F は利用できません。', + 'U2F_REGISTER_KEY' => '新しいデバイスを登録', + 'U2F_REGISTRATION_SUCCESS' => '登録に成功しました!', + 'U2F_AUTHENTIFICATION_SUCCESS' => '認証に成功しました!', + 'U2F_CREDENTIALS' => '資格情報', + 'U2F_CREDENTIALS_DELETED' => '資格情報は削除されました', + 'U2F_LOGIN' => 'WebAuthnでログイン', + + 'NEW_PHOTOS_NOTIFICATION' => '新しい写真の通知メールを送信します。', + 'SETTINGS_SUCCESS_NEW_PHOTOS_NOTIFICATION' => '新しい写真の通知が更新されました', + 'USER_EMAIL_INSTRUCTION' => 'メール通知の受信を有効にするには、以下にメールアドレスを入力してください。メールの受信を停止するには、以下のメールアドレスを削除してください。', + + 'LOGIN_USERNAME' => '新しいユーザー名', + 'LOGIN_PASSWORD' => '新しいパスワード', + 'LOGIN_PASSWORD_CONFIRM' => 'パスワードの確認', + 'PASSWORD_TITLE' => '現在のパスワードを入力:', + 'PASSWORD_CURRENT' => '現在のパスワード', + 'PASSWORD_TEXT' => '資格情報は次のように変更されます:', + 'PASSWORD_CHANGE' => 'パスワードを変更', + + 'EDIT_SHARING_TITLE' => '共有を編集', + 'EDIT_SHARING_TEXT' => 'このアルバムの共有プロパティは次のように変更されます:', + 'SHARE_ALBUM_TEXT' => 'このアルバムは次のプロパティで共有されます:', + + 'SORT_DIALOG_ATTRIBUTE_LABEL' => '属性', + 'SORT_DIALOG_ORDER_LABEL' => '順序', + + 'SORT_ALBUM_BY' => 'アルバムを %1$s で %2$s 順に並べ替えます。', + + 'SORT_ALBUM_SELECT_1' => '作成日時', + 'SORT_ALBUM_SELECT_2' => 'タイトル', + 'SORT_ALBUM_SELECT_3' => '説明', + 'SORT_ALBUM_SELECT_5' => '最新の撮影日', + 'SORT_ALBUM_SELECT_6' => '最も古い撮影日', + + 'SORT_PHOTO_BY' => '写真を %1$s で %2$s 順に並べ替えます。', + + 'SORT_PHOTO_SELECT_1' => 'アップロード日時', + 'SORT_PHOTO_SELECT_2' => '撮影日時', + 'SORT_PHOTO_SELECT_3' => 'タイトル', + 'SORT_PHOTO_SELECT_4' => '説明', + 'SORT_PHOTO_SELECT_6' => 'スター', + 'SORT_PHOTO_SELECT_7' => 'フォーマット', + + 'SORT_ASCENDING' => '昇順', + 'SORT_DESCENDING' => '降順', + 'SORT_CHANGE' => '並べ替えを変更', + + 'DROPBOX_TITLE' => 'Dropboxキーを設定', + 'DROPBOX_TEXT' => "Dropbox から写真をインポートするには、Dropbox の Web サイトから有効なドロップイン アプリ キーを取得する必要があります。パーソナルキーを生成し、以下に入力します。", + + 'LANG_TEXT' => 'Lycheeの言語を変更する:', + 'LANG_TITLE' => '言語を変更する', + + 'SETTING_RECENT_PUBLIC_TEXT' => 'スマートアルバム "最近追加された項目" を匿名ユーザーに公開する', + 'SETTING_STARRED_PUBLIC_TEXT' => 'スマートアルバム "スター付き" を匿名ユーザーに公開する', + 'SETTING_ONTHISDAY_PUBLIC_TEXT' => 'スマートアルバム "On This Day" を匿名ユーザーに公開する', + + 'CSS_TEXT' => 'カスタム CSS:', + 'CSS_TITLE' => 'CSSを変更', + 'JS_TEXT' => 'カスタム JS:', + 'JS_TITLE' => 'JSを変更', + 'PUBLIC_SEARCH_TEXT' => '公開検索を許可:', + 'OVERLAY_TYPE' => '写真のオーバーレイ:', + 'OVERLAY_NONE' => 'なし', + 'OVERLAY_EXIF' => 'EXIF データ', + 'OVERLAY_DESCRIPTION' => '説明', + 'OVERLAY_DATE' => '撮影日時', + 'ALBUM_DECORATION' => 'アルバムの装飾:', + 'ALBUM_DECORATION_NONE' => 'なし', + 'ALBUM_DECORATION_ORIGINAL' => 'サブアルバムのマーカー', + 'ALBUM_DECORATION_ALBUM' => 'サブアルバムの数', + 'ALBUM_DECORATION_PHOTO' => '写真の数', + 'ALBUM_DECORATION_ALL' => 'サブアルバムと写真の数', + 'ALBUM_DECORATION_ORIENTATION' => 'アルバムの装飾の方向:', + 'ALBUM_DECORATION_ORIENTATION_ROW' => '水平 (写真, アルバム)', + 'ALBUM_DECORATION_ORIENTATION_ROW_REVERSE' => '水平 (アルバム, 写真)', + 'ALBUM_DECORATION_ORIENTATION_COLUMN' => '垂直 (上部に写真, 下部にアルバム)', + 'ALBUM_DECORATION_ORIENTATION_COLUMN_REVERSE' => '垂直 (上部にアルバム, 下部に写真)', + 'MAP_DISPLAY_TEXT' => 'マップを有効化 (OpenStreetMapから提供):', + 'MAP_DISPLAY_PUBLIC_TEXT' => '公開アルバムのマップを有効化 (OpenStreetMapから提供):', + 'MAP_PROVIDER' => 'OpenStreetMapタイルのプロバイダ:', + 'MAP_PROVIDER_WIKIMEDIA' => 'Wikimedia', + 'MAP_PROVIDER_OSM_ORG' => 'OpenStreetMap.org (no HiDPI)', + 'MAP_PROVIDER_OSM_DE' => 'OpenStreetMap.de (no HiDPI)', + 'MAP_PROVIDER_OSM_FR' => 'OpenStreetMap.fr (no HiDPI)', + 'MAP_PROVIDER_RRZE' => 'University of Erlangen, Germany (only HiDPI)', + 'MAP_INCLUDE_SUBALBUMS_TEXT' => 'サブアルバムの写真をマップに表示:', + 'LOCATION_DECODING' => 'GPSデータを位置情報に変換', + 'LOCATION_SHOW' => '位置情報を表示', + 'LOCATION_SHOW_PUBLIC' => '公開モードで位置情報を表示', + + 'LAYOUT_TYPE' => '写真のレイアウト', + 'LAYOUT_SQUARES' => '正方形のサムネイル', + 'LAYOUT_JUSTIFIED' => '比率に合わせ、縦幅を合わせる', + 'LAYOUT_MASONRY' => '比率に合わせ、横幅を合わせる', + 'LAYOUT_GRID' => '比率に合わせ、均等に並べる', + 'LAYOUT_UNJUSTIFIED' => '比率に合わせ、幅を合わせない', + 'SET_LAYOUT' => 'レイアウトを変更', + + 'NSFW_VISIBLE_TEXT_1' => 'センシティブなアルバムをデフォルトで表示する', + 'NSFW_VISIBLE_TEXT_2' => 'アルバムが公開されている場合は、ビューから非表示になっているだけで、アクセスは可能です。H を押すと表示されます。', + 'SETTINGS_SUCCESS_NSFW_VISIBLE' => 'デフォルトのセンシティブなアルバムの表示設定が正常に更新されました。', + + 'NSFW_BANNER' => '

センシティブなコンテンツ

このアルバムには、一部の人が不快または不快に感じる可能性のあるセンシティブなコンテンツが含まれています。

承認するにはタップしてください。

', + 'NSFW_HEADER' => 'センシティブなコンテンツ', + 'NSFW_EXPLANATION' => 'このアルバムには、一部の人が不快または不快に感じる可能性のあるセンシティブなコンテンツが含まれています。', + 'TAP_CONSENT' => '承認するにはタップしてください。', + + 'VIEW_NO_RESULT' => '結果が見つかりませんでした', + 'VIEW_NO_PUBLIC_ALBUMS' => '共有アルバムがありません', + 'VIEW_NO_CONFIGURATION' => '設定がありません', + 'VIEW_PHOTO_NOT_FOUND' => '写真が見つかりません', + + 'NO_TAGS' => 'タグがありません', + + 'UPLOAD_MANAGE_NEW_PHOTOS' => '新しい写真を管理できるようになりました。', + 'UPLOAD_COMPLETE' => 'アップロードが完了しました', + 'UPLOAD_COMPLETE_FAILED' => 'アップロードに失敗しました', + 'UPLOAD_IMPORTING' => 'インポートしています', + 'UPLOAD_IMPORTING_URL' => 'URLをインポートしています', + 'UPLOAD_UPLOADING' => 'アップロードしています', + 'UPLOAD_FINISHED' => '終了しました', + 'UPLOAD_PROCESSING' => '処理中です', + 'UPLOAD_FAILED' => '失敗しました', + 'UPLOAD_FAILED_ERROR' => 'アップロードに失敗しました。サーバーからエラーが返されました', + 'UPLOAD_FAILED_WARNING' => 'アップロードに失敗しました。サーバーから警告が返されました。', + 'UPLOAD_CANCELLED' => 'キャンセル済み', + 'UPLOAD_SKIPPED' => '省略しました', + 'UPLOAD_UPDATED' => '更新しました', + 'UPLOAD_GENERAL' => '一般', + 'UPLOAD_IMPORT_SKIPPED_DUPLICATE' => 'この写真は既にライブラリにあるため省略されました。', + 'UPLOAD_IMPORT_RESYNCED_DUPLICATE' => 'この写真は既にライブラリにあるため省略されましたが、メタデータは更新されました', + 'UPLOAD_ERROR_CONSOLE' => '詳細についてはブラウザのコンソールを参照してください。', + 'UPLOAD_UNKNOWN' => 'サーバーから不明な応答が返されました。詳細についてはブラウザのコンソールを参照してください。', + 'UPLOAD_ERROR_UNKNOWN' => 'アップロードに失敗しました。サーバーが不明なエラーを返しました。', + 'UPLOAD_ERROR_POSTSIZE' => 'アップロードに失敗しました。PHP post_max_size が小さすぎる可能性があります。それ以外の場合は FAQ を確認してください。', + 'UPLOAD_ERROR_FILESIZE' => 'アップロードに失敗しました。PHP upload_max_filesize が小さすぎる可能性があります。それ以外の場合は FAQ を確認してください。', + 'UPLOAD_IN_PROGRESS' => 'Lycheeはアップロード中です!', + 'UPLOAD_IMPORT_WARN_ERR' => 'インポートは完了しましたが、警告またはエラーが返されました。詳細については、ログ (設定 -> ログを表示) を確認してください。', + 'UPLOAD_IMPORT_COMPLETE' => 'インポートが完了しました', + 'UPLOAD_IMPORT_INSTR' => '写真をインポートするには、写真への直接リンクを入力してください:', + 'UPLOAD_IMPORT' => 'インポート', + 'UPLOAD_IMPORT_SERVER' => 'サーバーからインポート', + 'UPLOAD_IMPORT_SERVER_FOLD' => 'フォルダーが空であるか、処理する読み取り可能なファイルがありません。詳細については、ログ (設定 -> ログを表示) を確認してください。', + 'UPLOAD_IMPORT_SERVER_INSTR' => 'サーバー上で次の絶対パスを持つフォルダーにあるすべての写真、フォルダー、サブフォルダーをインポートします。パスはスペースで区切られており、パス内のスペースをエスケープするには \\ を使用します。', + 'UPLOAD_ABSOLUTE_PATH' => 'ディレクトリへの絶対パス(スペース区切り)', + 'UPLOAD_IMPORT_SERVER_EMPT' => 'フォルダーが空のためインポートを開始できませんでした。', + 'UPLOAD_IMPORT_DELETE_ORIGINALS' => 'オリジナルを削除', + 'UPLOAD_IMPORT_DELETE_ORIGINALS_EXPL' => '可能な場合は、インポート後にオリジナルのファイルが削除されます。', + 'UPLOAD_IMPORT_VIA_SYMLINK' => 'シンボリックリンク', + 'UPLOAD_IMPORT_VIA_SYMLINK_EXPL' => 'オリジナルへのシンボリック リンクを使用してファイルをインポートします。', + 'UPLOAD_IMPORT_SKIP_DUPLICATES' => '重複を無視します', + 'UPLOAD_IMPORT_SKIP_DUPLICATES_EXPL' => '既存のメディアファイルはスキップされます。', + 'UPLOAD_IMPORT_RESYNC_METADATA' => 'メタデータの再同期', + 'UPLOAD_IMPORT_RESYNC_METADATA_EXPL' => '既存のメディアファイルのメタデータを更新します。', + 'UPLOAD_IMPORT_LOW_MEMORY_EXPL' => 'サーバー上のインポートプロセスがメモリ制限に近づいており、途中で終了する可能性があります。', + 'UPLOAD_WARNING' => '警告', + 'UPLOAD_IMPORT_NOT_A_DIRECTORY' => '指定されたパスは読み取り可能なディレクトリではありません。', + 'UPLOAD_IMPORT_PATH_RESERVED' => '指定されたパスはLycheeの予約パスです。', + 'UPLOAD_IMPORT_FAILED' => 'ファイルをインポートできませんでした', + 'UPLOAD_IMPORT_UNSUPPORTED' => 'サポートされていないファイルタイプです', + 'UPLOAD_IMPORT_CANCELLED' => 'インポートがキャンセルされました', + + 'ABOUT_SUBTITLE' => '正しく実行されるSelf-Hosted型の写真管理', + 'ABOUT_DESCRIPTION' => 'Lychee は、サーバーまたは Web スペースで実行される無料の写真管理ツールです。インストールはほんの数秒で完了します。ネイティブ アプリケーションのように写真をアップロード、管理、共有できます。Lychee には必要なものがすべて揃っており、すべての写真が安全に保存されます。', + 'FOOTER_COPYRIGHT' => 'All images on this website are subject to copyright by %1$s © %2$s', + 'HOSTED_WITH_LYCHEE' => 'Lychee でホストされています', + + 'URL_COPY_TO_CLIPBOARD' => 'クリップボードにコピー', + 'URL_COPIED_TO_CLIPBOARD' => 'コピーされました', + 'PHOTO_DIRECT_LINKS_TO_IMAGES' => '画像ファイルへのダイレクトリンク:', + 'PHOTO_ORIGINAL' => 'オリジナル', + 'PHOTO_MEDIUM' => '標準', + 'PHOTO_MEDIUM_HIDPI' => '標準 HiDPI', + 'PHOTO_SMALL' => '小', + 'PHOTO_SMALL_HIDPI' => '小 HiDPI', + 'PHOTO_THUMB' => '正方形サムネイル', + 'PHOTO_THUMB_HIDPI' => '正方形サムネイル HiDPI', + 'PHOTO_PLACEHOLDER' => 'Low Quality Image Placeholder', + 'PHOTO_THUMBNAIL' => 'サムネイル', + 'PHOTO_LIVE_VIDEO' => 'ライブフォトのビデオ部分', + 'PHOTO_VIEW' => 'Lychee の写真表示:', + + 'PHOTO_EDIT_ROTATECWISE' => '右に回転', + 'PHOTO_EDIT_ROTATECCWISE' => '左に回転', + + 'ERROR_GPX' => 'GPX ファイルの読み込み中にエラーが発生しました:', + 'ERROR_EITHER_ALBUMS_OR_PHOTOS' => 'アルバムか写真のどちらかを選択してください。', + 'ERROR_COULD_NOT_FIND' => 'ご希望のものが見つかりませんでした。', + 'ERROR_INVALID_EMAIL' => '有効なメールアドレスではありません。', + 'EMAIL_SUCCESS' => 'メールアドレスが更新されました', + 'ERROR_PHOTO_NOT_FOUND' => '写真 %s が見つかりません。', + 'ERROR_EMPTY_USERNAME' => '新しいユーザー名は空欄にできません。', + 'ERROR_PASSWORD_DOES_NOT_MATCH' => '新しいパスワードが一致しません。', + 'ERROR_EMPTY_PASSWORD' => '新しいパスワードは空欄にできません。', + 'ERROR_SELECT_ALBUM' => '共有するアルバムを選択してください。', + 'ERROR_SELECT_USER' => '共有するユーザーを選択してください。', + 'ERROR_SELECT_SHARING' => '削除する共有を選択してください。', + 'SHARING_SUCCESS' => '共有が更新されました', + 'SHARING_REMOVED' => '共有が削除されました', + 'USER_CREATED' => 'ユーザーが作成されました', + 'USER_DELETED' => 'ユーザーが削除されました', + 'USER_UPDATED' => 'ユーザーが更新されました', + 'ENTER_EMAIL' => 'メールアドレスを入力してください:', + 'ERROR_ALBUM_JSON_NOT_FOUND' => 'アルバム JSON が見つかりません。', + 'ERROR_ALBUM_NOT_FOUND' => 'アルバム %s が見つかりません。', + 'ERROR_DROPBOX_KEY' => 'Dropbox キーが設定されていません。', + 'ERROR_SESSION' => 'セッションが有効期限切れです。', + 'CAMERA_DATE' => 'カメラの日付', + 'NEW_PASSWORD' => '新しいパスワード', + 'ALLOW_UPLOADS' => 'アップロードを許可', + 'ALLOW_USER_SELF_EDIT' => '自身のユーザーアカウントの管理を許可する', + 'OSM_CONTRIBUTORS' => 'OpenStreetMapの貢献者', +]; diff --git a/lang/ja/maintenance.php b/lang/ja/maintenance.php new file mode 100644 index 00000000000..da622e41e20 --- /dev/null +++ b/lang/ja/maintenance.php @@ -0,0 +1,59 @@ + 'メンテナンス', + 'description' => 'このページには、Lychee のインストールをスムーズかつ適切に実行するために必要なすべてのアクションが記載されています。', + 'cleaning' => [ + 'title' => '%s を削除', + 'result' => '%s が削除されました。', + 'description' => '%s からすべてのコンテンツを削除します', + 'button' => '削除', + ], + 'fix-jobs' => [ + 'title' => 'ジョブ履歴の修正', + 'description' => 'ステータスが %s または %s のジョブを %s としてマークします。', + 'button' => 'ジョブ履歴を修正', + ], + 'gen-sizevariants' => [ + 'title' => '存在しない %s', + 'description' => '生成可能な %d 個の %s が見つかりました。', + 'button' => '生成', + 'success' => '%d 個の %s が正常に生成されました。', + ], + 'fill-filesize-sizevariants' => [ + 'title' => 'ファイルサイズが見つかりません', + 'description' => 'ファイルサイズのない小さなバリアントが %d 個見つかりました。', + 'button' => 'データを取得', + 'success' => '%d 個の小さなバリアントのサイズを正常に計算しました。', + ], + 'fix-tree' => [ + 'title' => 'ツリー統計', + 'Oddness' => 'Oddness', + 'Duplicates' => '重複', + 'Wrong parents' => '間違った親要素', + 'Missing parents' => '存在しない親要素', + 'button' => 'ツリーを修正', + ], + 'optimize' => [ + 'title' => 'データベースを最適化', + 'description' => 'インストールの速度低下に気付いた場合、データベースに必要なインデックスがすべて揃っていないことが原因の可能性があります。', + 'button' => 'データベースを最適化', + ], + 'update' => [ + 'title' => '更新', + 'check-button' => '更新を確認', + 'update-button' => '更新', + 'no-pending-updates' => '保留中の更新はありません', + ], +]; diff --git a/lang/ja/profile.php b/lang/ja/profile.php new file mode 100644 index 00000000000..cc24b97452c --- /dev/null +++ b/lang/ja/profile.php @@ -0,0 +1,64 @@ + 'Profile', + + 'login' => [ + 'header' => 'Profile', + 'enter_current_password' => 'Enter your current password:', + 'current_password' => 'Current password', + 'credentials_update' => 'Your credentials will be changed to the following:', + 'username' => 'Username', + 'new_password' => 'New password', + 'confirm_new_password' => 'Confirm new password', + 'email_instruction' => 'Add your email below to enable receiving email notifications. To stop receiving emails, simply remove your email below.', + 'email' => 'Email', + 'change' => 'Change Login', + 'api_token' => 'API Token ...', + + 'missing_fields' => 'Missing fields', + ], + + 'token' => [ + 'unavailable' => 'You have already viewed this token.', + 'no_data' => 'No token API have been generated.', + 'disable' => 'Disable', + 'disabled' => 'Token disabled', + 'warning' => 'This token will not be displayed again. Copy it and keep it in a safe place.', + 'reset' => 'Reset the token', + 'create' => 'Create a new token', + ], + + 'oauth' => [ + 'header' => 'OAuth', + 'header_not_available' => 'OAuth is not available', + 'setup_env' => 'Set up the credentials in your .env', + 'token_registered' => '%s token registered.', + 'setup' => 'Set up %s', + 'reset' => 'reset', + 'credential_deleted' => 'Credential deleted!', + ], + + 'u2f' => [ + 'header' => 'Passkey/MFA/2FA', + 'info' => 'This only provides the ability to use WebAuthn to authenticate instead of username & password.', + 'empty' => 'Credentials list is empty!', + 'not_secure' => 'Environment not secured. U2F not available.', + 'new' => 'Register new device.', + 'credential_deleted' => 'Credential deleted!', + 'credential_updated' => 'Credential updated!', + 'credential_registred' => 'Registration successful!', + '5_chars' => 'At least 5 chars.', + ], +]; \ No newline at end of file diff --git a/lang/ja/settings.php b/lang/ja/settings.php new file mode 100644 index 00000000000..fd197f11135 --- /dev/null +++ b/lang/ja/settings.php @@ -0,0 +1,92 @@ + 'Settings', + 'small_screen' => 'For better a experience on the Settings page,
we recommend you use a larger screen.', + 'tabs' => [ + 'basic' => 'Basic', + 'all_settings' => 'All settings', + ], + 'toasts' => [ + 'change_saved' => 'Change saved!', + 'details' => 'Settings have been modified as per request', + 'error' => 'Error!', + 'error_load_css' => 'Could not load dist/user.css', + 'error_load_js' => 'Could not load dist/custom.js', + 'error_save_css' => 'Could not save CSS', + 'error_save_js' => 'Could not save JS', + 'thank_you' => 'Thank you for your support.', + 'reload' => 'Reload your page for full functionalities.', + ], + 'system' => [ + 'header' => 'System', + 'use_dark_mode' => 'Use dark mode for Lychee', + 'language' => 'Language used by Lychee', + 'nsfw_album_visibility' => 'Make Sensitive albums visible by default.', + 'nsfw_album_explanation' => 'If the album is public, it is still accessible, just hidden from the view and can be revealed by pressing H.', + ], + 'lychee_se' => [ + 'header' => 'Lychee SE', + 'call4action' => 'Get exclusive features and support the development of Lychee. Unlock the SE edition.', + 'preview' => 'Enable preview of Lychee SE features', + 'hide_call4action' => 'Hide this Lychee SE registration form. I am happy with Lychee as-is. :)', + 'hide_warning' => 'If enabled, the only way to register your license key will be via the More tab above. Changes are applied on page reload.', + ], + 'dropbox' => [ + 'header' => 'Dropbox', + 'instruction' => 'In order to import photos from your Dropbox, you need a valid drop-ins app key from their website.', + 'api_key' => 'Dropbox API Key', + 'set_key' => 'Set Dropbox Key', + ], + 'gallery' => [ + 'header' => 'Gallery', + 'photo_order_column' => 'Default column used for sorting photos', + 'photo_order_direction' => 'Default order used for sorting photos', + 'album_order_column' => 'Default column used for sorting albums', + 'album_order_direction' => 'Default order used for sorting albums', + 'aspect_ratio' => 'Default aspect ratio for album thumbs', + 'photo_layout' => 'Layout for pictures', + 'album_decoration' => 'Show decorations on album cover (sub-album and/or photo count)', + 'album_decoration_direction' => 'Align album decorations horizontally or vertically', + 'photo_overlay' => 'Default image overlay information', + 'license_default' => 'Default license used for albums', + 'license_help' => 'Need help choosing?', + ], + 'geolocation' => [ + 'header' => 'Geo-location', + 'map_display' => 'Display the map given GPS coordinates', + 'map_display_public' => 'Allow anonymous users to access the map', + 'map_provider' => 'Defines the map provider', + 'map_include_subalbums' => 'Includes pictures of the sub albums on the map', + 'location_decoding' => 'Use GPS location decoding', + 'location_show' => 'Show location extracted from GPS coordinates', + 'location_show_public' => 'Anonymous users can access the extracted location from GPS coordinates', + ], + 'advanced' => [ + 'header' => 'Advanced Customization', + 'change_css' => 'Change CSS', + 'change_js' => 'Change JS', + ], + 'all' => [ + 'old_setting_style' => 'Old setting style', + 'change_detected' => 'Some settings changed.', + 'save' => 'Save', + ], + + 'tool_option' => [ + 'disabled' => 'disabled', + 'enabled' => 'enabled', + 'discover' => 'discover', + ], +]; \ No newline at end of file diff --git a/lang/ja/sharing.php b/lang/ja/sharing.php new file mode 100644 index 00000000000..69de18cc6d0 --- /dev/null +++ b/lang/ja/sharing.php @@ -0,0 +1,33 @@ + 'Sharing', + + 'info' => 'This page gives an overview of and the ability to edit the sharing rights associated with albums.', + 'album_title' => 'Album title', + 'username' => 'Username', + 'no_data' => 'Sharing list is empty.', + 'share' => 'Share', + 'permission_deleted' => 'Permission deleted!', + 'permission_created' => 'Permission created!', + + 'grants' => [ + 'read' => 'Grants read access', + 'original' => 'Grants access to original photo', + 'download' => 'Grants download', + 'upload' => 'Grants upload', + 'edit' => 'Grants edit', + 'delete' => 'Grants delete', + ], +]; \ No newline at end of file diff --git a/lang/ja/statistics.php b/lang/ja/statistics.php new file mode 100644 index 00000000000..2baf855bbd5 --- /dev/null +++ b/lang/ja/statistics.php @@ -0,0 +1,34 @@ + 'Statistics', + + 'preview_text' => 'This is a preview of the statistics page available in Lychee SE.
The data shown here are randomly generated and do not reflect your server.', + 'no_data' => 'User does not have data on server.', + 'collapse' => 'Collapse albums sizes', + + 'total' => [ + 'total' => 'Total', + 'albums' => 'Albums', + 'photos' => 'Photos', + 'size' => 'Size', + ], + 'table' => [ + 'username' => 'Owner', + 'title' => 'Title', + 'photos' => 'Photos', + 'descendants' => 'Children', + 'size' => 'Size', + ], +]; \ No newline at end of file diff --git a/lang/ja/toasts.php b/lang/ja/toasts.php new file mode 100644 index 00000000000..293d4b72594 --- /dev/null +++ b/lang/ja/toasts.php @@ -0,0 +1,17 @@ + 'Error', + 'success' => 'Success', +]; \ No newline at end of file diff --git a/lang/ja/users.php b/lang/ja/users.php new file mode 100644 index 00000000000..599bb833454 --- /dev/null +++ b/lang/ja/users.php @@ -0,0 +1,44 @@ + 'Users', + 'description' => 'Here you can manage the users of your Lychee installation. You can create, edit and delete users.', + 'create' => 'Create a new user', + 'username' => 'Username', + 'password' => 'Password', + 'legend' => 'Legend', + 'upload_rights' => 'When selected, the user can upload content.', + 'edit_rights' => 'When selected, the user can modify their profile (username, password).', + 'quota' => 'When set, the user has a space quota for pictures (in kB).', + + 'user_deleted' => 'User deleted', + 'user_created' => 'User created', + 'user_updated' => 'User updated', + 'change_saved' => 'Change saved!', + + 'create_edit' => [ + 'upload_rights' => 'User can upload content.', + 'edit_rights' => 'User can modify their profile (username, password).', + 'quota' => 'User has quota limit.', + 'quota_kb' => 'quota in kB (0 for default)', + 'note' => 'Admin note (not publically visible)', + 'create' => 'Create', + 'edit' => 'Edit', + ], + 'line' => [ + 'admin' => 'admin user', + 'edit' => 'Edit', + 'delete' => 'Delete', + ], +]; \ No newline at end of file diff --git a/lang/nl/aspect_ratio.php b/lang/nl/aspect_ratio.php new file mode 100644 index 00000000000..2c7e8fb56ac --- /dev/null +++ b/lang/nl/aspect_ratio.php @@ -0,0 +1,21 @@ + '5/4 (instagram landscape)', + '4by5' => '4/5 (instagram portrait)', + '2by3' => '2/3 (portrait)', + '3by2' => '3/2 (landscape)', + '1by1' => 'square', + '1byx9' => '16/9 (landscape)', +]; \ No newline at end of file diff --git a/lang/nl/diagnostics.php b/lang/nl/diagnostics.php new file mode 100644 index 00000000000..0fadd640428 --- /dev/null +++ b/lang/nl/diagnostics.php @@ -0,0 +1,30 @@ + 'Diagnostics', + + 'copy_to_clipboard' => 'Copy diagnostics to clipboard', + 'self-diagnosis' => 'Self-diagnosis', + 'info' => 'Info', + 'space' => 'Space', + 'load_space' => 'Load space usage.', + 'configuration' => 'Configuration', + 'loading' => 'Loading...', + 'identical_content' => 'Identical content', + + 'toast' => [ + 'info' => 'Info', + 'copy' => 'Diagnostics copied to clipboard!', + ], +]; \ No newline at end of file diff --git a/lang/nl/dialogs.php b/lang/nl/dialogs.php new file mode 100644 index 00000000000..4afd65fae3f --- /dev/null +++ b/lang/nl/dialogs.php @@ -0,0 +1,221 @@ + [ + 'close' => 'Close', + 'cancel' => 'Cancel', + 'save' => 'Save', + 'delete' => 'Delete', + 'move' => 'Move', + ], + 'about' => [ + 'subtitle' => 'Self-hosted photo-management done right', + 'description' => 'Lychee is a free photo-management tool, which runs on your server or web-space. Installing is a matter of seconds. Upload, manage and share photos like from a native application. Lychee comes with everything you need and all your photos are stored securely.', + 'update_available' => 'Update available!', + 'thank_you' => 'Thank you for your support!', + 'get_supporter_or_register' => 'Get exclusive features and support the development of Lychee.
Unlock the Supporter Edition or register your License key', + 'here' => 'here', + ], + 'dropbox' => [ + 'not_configured' => 'Dropbox is not configured.', + ], + 'import_from_link' => [ + 'instructions' => 'Please enter the direct link to a photo to import it:', + 'import' => 'Import', + ], + 'keybindings' => [ + 'don_t_show_again' => 'Don\'t show this again', + 'side_wide' => 'Site-wide Shortcuts', + 'back_cancel' => 'Back/Cancel', + 'confirm' => 'Confirm', + 'login' => 'Login', + 'toggle_full_screen' => 'Toggle Full Screen', + 'toggle_sensitive_albums' => 'Toggle Sensitive Albums', + + 'albums' => 'Albums Shortcuts', + 'new_album' => 'New Album', + 'upload_photos' => 'Upload Photos', + 'search' => 'Search', + 'show_this_modal' => 'Show this modal', + 'select_all' => 'Select All', + 'move_selection' => 'Move Selection', + 'delete_selection' => 'Delete Selection', + + 'album' => 'Album Shortcuts', + 'slideshow' => 'Start/Stop Slideshow', + 'toggle' => 'Toggle panel', + + 'photo' => 'Photo Shortcuts', + 'previous' => 'Previous photo', + 'next' => 'Next photo', + 'cycle' => 'Cycle overlay mode', + 'star' => 'Star the photo', + 'move' => 'Move the photo', + 'delete' => 'Delete the photo', + 'edit' => 'Edit information', + 'show_hide_meta' => 'Show information', + + 'keep_hidden' => 'We will keep it hidden.', + ], + 'login' => [ + 'username' => 'Username', + 'password' => 'Password', + 'unknown_invalid' => 'Unknown user or invalid password.', + 'signin' => 'Sign-In', + ], + 'register' => [ + 'enter_license' => 'Enter your license key below:', + 'license_key' => 'License key', + 'invalid_license' => 'Invalid license key.', + 'register' => 'Register', + ], + 'share_album' => [ + 'url_copied' => 'Copied URL to clipboard!', + ], + 'upload' => [ + 'completed' => 'Completed', + 'uploaded' => 'Uploaded:', + 'release' => 'Release file to upload!', + 'select' => 'Click here to select files to upload', + 'drag' => '(Or drag files to the page)', + 'loading' => 'Loading', + 'resume' => 'Resume', + 'uploading' => 'Uploading', + 'finished' => 'Finished', + 'failed_error' => 'Upload failed. The server returned an error!', + ], + 'visibility' => [ + 'public' => 'Public', + 'public_expl' => 'Anonymous users can access this album, subject to the restrictions below.', + 'full' => 'Original', + 'full_expl' => 'Anonymous users can view full-resolution photos.', + 'hidden' => 'Hidden', + 'hidden_expl' => 'Anonymous users need a direct link to access this album.', + 'downloadable' => 'Downloadable', + 'downloadable_expl' => 'Anonymous users can download this album.', + 'password' => 'Password', + 'password_prot' => 'Password protected', + 'password_prot_expl' => 'Anonymous users need a shared password to access this album.', + 'nsfw' => 'Sensitive', + 'nsfw_expl' => 'Album contains sensitive content.', + 'visibility_updated' => 'Visibility updated.', + ], + 'move_album' => [ + 'confirm_single' => 'Are you sure you want to move the album “%1$s” into the album “%2$s”?', + 'confirm_multiple' => 'Are you sure you want to move all selected albums into the album “%s”?', + 'move_single' => 'Move Album', + 'move_to' => 'Move to', + 'move_to_single' => 'Move %s to:', + 'move_to_multiple' => 'Move %d albums to:', + 'no_album_target' => 'No album to move to', + 'moved_single' => 'Album moved!', + 'moved_single_details' => '%1$s moved to %2$s', + 'moved_details' => 'Album(s) moved to %s', + ], + 'new_album' => [ + 'menu' => 'Create Album', + 'info' => 'Enter a title for the new album:', + 'title' => 'title', + 'create' => 'Create Album', + ], + 'new_tag_album' => [ + 'menu' => 'Create Tag Album', + 'info' => 'Enter a title for the new tag album:', + 'title' => 'title', + 'set_tags' => 'Set tags to show', + 'warn' => 'Make sure to press enter after each tag', + 'create' => 'Create Tag Album', + ], + 'delete_album' => [ + 'confirmation' => 'Are you sure you want to delete the album “%s” and all of the photos it contains?', + 'confirmation_multiple' => 'Are you sure you want to delete all %d selected albums and all of the photos they contain?', + 'warning' => 'This action can not be undone!', + 'delete' => 'Delete Album and Photos', + ], + 'transfer' => [ + 'query' => 'Transfer ownership of album to', + 'confirmation' => 'Are you sure you want to transfer the ownership of album “%s” and all the photos it contains to "%s"?', + 'lost_access_warning' => 'Your access to this album will be lost.', + 'warning' => 'This action can not be undone!', + 'transfer' => 'Transfer ownership of album and photos', + ], + 'rename' => [ + 'photo' => 'Enter a new title for this photo:', + 'album' => 'Enter a new title for this album:', + 'rename' => 'Rename', + ], + 'merge' => [ + 'merge_to' => 'Merge %s to:', + 'merge_to_multiple' => 'Merge %d albums to:', + 'no_albums' => 'No albums to merge to.', + 'confirm' => 'Are you sure you want to merge the album “%1$s” into the album “%2$s”?', + 'confirm_multiple' => 'Are you sure you want to merge all selected albums into the album “%s”?', + 'merge' => 'Merge Albums', + 'merged' => 'Album(s) merged to %s!', + ], + 'unlock' => [ + 'password_required' => 'This album is protected by a password. Enter the password below to view the photos of this album:', + 'password' => 'Password', + 'unlock' => 'Unlock', + ], + 'photo_tags' => [ + 'question' => 'Enter your tags for this photo.', + 'question_multiple' => 'Enter your tags for all %d selected photos. Existing tags will be overwritten.', + 'no_tags' => 'No Tags', + 'set_tags' => 'Set Tags', + 'updated' => 'Tags updated!', + 'tags_override_info' => 'If this is unchecked, the tags will be added to the existing tags of the photo.', + ], + 'photo_copy' => [ + 'no_albums' => 'No albums to copy to', + 'copy_to' => 'Copy %s to:', + 'copy_to_multiple' => 'Copy %d photos to:', + 'confirm' => 'Copy %s to %s.', + 'confirm_multiple' => 'Copy %d photos to %s.', + 'copy' => 'Copy', + 'copied' => 'Photo(s) copied!', + ], + 'photo_delete' => [ + 'confirm' => 'Are you sure you want to delete the photo “%s”?', + 'confirm_multiple' => 'Are you sure you want to delete all %d selected photos?', + 'deleted' => 'Photo(s) deleted!', + ], + 'move_photo' => [ + 'move_single' => 'Move %s to:', + 'move_multiple' => 'Move %d photos to:', + 'confirm' => 'Move %s to %s.', + 'confirm_multiple' => 'Move %d photos to %s.', + 'moved' => 'Photo(s) moved to %s!', + ], + 'target_user' => [ + 'placeholder' => 'Select user', + ], + 'target_album' => [ + 'placeholder' => 'Select album', + ], + 'webauthn' => [ + 'u2f' => 'U2F', + 'success' => 'Authentication successful!', + 'error' => 'Whoops, it looks like something went wrong. Please reload the site and try again!', + ], + 'se' => [ + 'available' => 'Available in the Supporter Edition', + ], + 'session_expired' => [ + 'title' => 'Session expired', + 'message' => 'Your session has expired.
Please reload the page.', + 'reload' => 'Reload', + 'go_to_gallery' => 'Go to the Gallery', + ], +]; \ No newline at end of file diff --git a/lang/nl/fix-tree.php b/lang/nl/fix-tree.php new file mode 100644 index 00000000000..64803e310e6 --- /dev/null +++ b/lang/nl/fix-tree.php @@ -0,0 +1,55 @@ + 'Maintenance', + 'intro' => 'This page allows you to re-order and fix your albums manually.
Before any modifications, we strongly recommend you to read about Nested Set tree structures.', + 'warning' => 'You can really break your Lychee installation here, modify values at your own risks.', + + 'help' => [ + 'header' => 'Help', + 'hover' => 'Hover ids or titles to highlight related albums.', + 'left' => 'Left', + 'right' => 'Right', + 'convenience' => 'For your convenience, the and buttons allow you to change the values of %s and %s by respectively +1 and -1 with propagation.', + 'left-right-warn' => 'The and indicates that the value of %s (and respectively %s) is duplicated somewhere.', + 'parent-marked' => 'Marked Parent Id indicates that the %s and %s do not satisfy the Nest Set tree structures. Edit either the Parent Id or the %s/%s values.', + 'slowness' => 'This page will be slow with a large number of albums.', + ], + + 'buttons' => [ + 'reset' => 'Reset', + 'check' => 'Check', + 'apply' => 'Apply', + ], + + 'table' => [ + 'title' => 'Title', + 'left' => 'Left', + 'right' => 'Right', + 'id' => 'Id', + 'parent' => 'Parent Id', + ], + + 'errors' => [ + 'invalid' => 'Invalid tree!', + 'invalid_details' => 'We are not applying this as it is guaranteed to be a broken state.', + 'invalid_left' => 'Album %s has an invalid left value.', + 'invalid_right' => 'Album %s has an invalid right value.', + 'invalid_left_right' => 'Album %s has an invalid left/right values. Left should be strictly smaller than right: %s < %s.', + 'duplicate_left' => 'Album %s has a duplicate left value %s.', + 'duplicate_right' => 'Album %s has a duplicate right value %s.', + 'parent' => 'Album %s has an unexpected parent id %s.', + 'unknown' => 'Album %s has an unknown error.', + ], +]; \ No newline at end of file diff --git a/lang/nl/gallery.php b/lang/nl/gallery.php new file mode 100644 index 00000000000..eb8008827e0 --- /dev/null +++ b/lang/nl/gallery.php @@ -0,0 +1,241 @@ + 'Gallery', + + 'smart_albums' => 'Smart albums', + 'albums' => 'Albums', + 'root' => 'Albums', + + 'original' => 'Original', + 'medium' => 'Medium', + 'medium_hidpi' => 'Medium HiDPI', + 'small' => 'Thumb', + 'small_hidpi' => 'Thumb HiDPI', + 'thumb' => 'Square thumb', + 'thumb_hidpi' => 'Square thumb HiDPI', + 'placeholder' => 'Low Quality Image Placeholder', + 'thumbnail' => 'Photo thumbnail', + 'live_video' => 'Video part of live-photo', + + 'camera_data' => 'Camera date', + 'album_reserved' => 'All Rights Reserved', + + 'map' => [ + 'error_gpx' => 'Error loading GPX file', + 'osm_contributors' => 'OpenStreetMap contributors', + ], + + 'search' => [ + 'title' => 'Search', + 'searching' => 'Searching…', + 'no_results' => 'Nothing matches your search query.', + 'searchbox' => 'Search…', + 'minimum_chars' => 'Minimum %s characters required.', + 'photos' => 'Photos (%s)', + 'albums' => 'Albums (%s)', + ], + + 'smart_album' => [ + 'unsorted' => 'Unsorted', + 'starred' => 'Starred', + 'recent' => 'Recent', + 'public' => 'Public', + 'on_this_day' => 'On This Day', + ], + + 'layout' => [ + 'squares' => 'Square thumbnails', + 'justified' => 'With aspect, justified', + 'masonry' => 'With aspect, masonry', + 'grid' => 'With aspect, grid', + ], + + 'overlay' => [ + 'none' => 'None', + 'exif' => 'EXIF data', + 'description' => 'Description', + 'date' => 'Date taken', + ], + + 'timeline' => [ + 'default' => 'default', + 'disabled' => 'disabled', + 'year' => 'Year', + 'month' => 'Month', + 'day' => 'Day', + 'hour' => 'Hour', + ], + + 'album' => [ + 'header_albums' => 'Albums', + 'header_photos' => 'Photos', + 'no_results' => 'Nothing to see here', + 'upload' => 'Upload photos', + + 'tabs' => [ + 'about' => 'About Album', + 'share' => 'Share Album', + 'move' => 'Move Album', + 'danger' => 'DANGER ZONE', + ], + + 'hero' => [ + 'created' => 'Created', + 'copyright' => 'Copyright', + 'subalbums' => 'Subalbums', + 'images' => 'Photos', + 'download' => 'Download Album', + 'share' => 'Share Album', + 'stats_only_se' => 'Statistics available in the Supporter Edition', + ], + + 'stats' => [ + 'lens' => 'Lens', + 'shutter' => 'Shutter speed', + 'iso' => 'ISO', + 'model' => 'Model', + 'aperture' => 'Aperture', + 'no_data' => 'No data', + ], + + 'properties' => [ + 'title' => 'Title', + 'description' => 'Description', + 'photo_ordering' => 'Order photos by', + 'children_ordering' => 'Order albums by', + 'asc/desc' => 'asc/desc', + 'header' => 'Set album header', + 'compact_header' => 'Use compact header', + 'license' => 'Set license', + 'copyright' => 'Set copyright', + 'aspect_ratio' => 'Set album thumbs aspect ratio', + 'album_timeline' => 'Set album timeline mode', + 'photo_timeline' => 'Set photo timeline mode', + 'layout' => 'Set photo layout', + 'show_tags' => 'Set tags to show', + 'tags_required' => 'Tags are required.', + ], + ], + + 'photo' => [ + 'actions' => [ + 'star' => 'Star', + 'unstar' => 'Unstar', + 'set_album_header' => 'Set as album header', + 'move' => 'Move', + 'delete' => 'Delete', + 'header_set' => 'Header set', + ], + + 'details' => [ + 'about' => 'About', + 'basics' => 'Basics', + 'title' => 'Title', + 'uploaded' => 'Uploaded', + 'description' => 'Description', + 'license' => 'License', + 'reuse' => 'Reuse', + 'latitude' => 'Latitude', + 'longitude' => 'Longitude', + 'altitude' => 'Altitude', + 'location' => 'Location', + 'image' => 'Image', + 'video' => 'Video', + 'size' => 'Size', + 'format' => 'Format', + 'resolution' => 'Resolution', + 'duration' => 'Duration', + 'fps' => 'Frame rate', + 'tags' => 'Tags', + 'camera' => 'Camera', + 'captured' => 'Captured', + 'make' => 'Make', + 'type' => 'Type/Model', + 'lens' => 'Lens', + 'shutter' => 'Shutter Speed', + 'aperture' => 'Aperture', + 'focal' => 'Focal Length', + 'iso' => 'ISO %s', + ], + + 'edit' => [ + 'set_title' => 'Set Title', + 'set_description' => 'Set Description', + 'set_license' => 'Set License', + 'no_tags' => 'No Tags', + 'set_tags' => 'Set Tags', + 'set_created_at' => 'Set Upload Date', + ], + ], + + 'nsfw' => [ + 'header' => 'Sensitive content', + 'description' => 'This album contains sensitive content which some people may find offensive or disturbing.', + 'consent' => 'Tap to consent.', + ], + + 'menus' => [ + 'star' => 'Star', + 'unstar' => 'Unstar', + 'star_all' => 'Star Selected', + 'unstar_all' => 'Unstar Selected', + 'tag' => 'Tag', + 'tag_all' => 'Tag Selected', + 'set_cover' => 'Set Album Cover', + 'remove_header' => 'Remove Album Header', + 'set_header' => 'Set Album Header', + 'copy_to' => 'Copy to …', + 'copy_all_to' => 'Copy Selected to …', + 'rename' => 'Rename', + 'move' => 'Move', + 'move_all' => 'Move Selected', + 'delete' => 'Delete', + 'delete_all' => 'Delete Selected', + 'download' => 'Download', + 'download_all' => 'Download Selected', + 'merge' => 'Merge', + 'merge_all' => 'Merge Selected', + + 'upload_photo' => 'Upload Photo', + 'import_link' => 'Import from Link', + 'import_dropbox' => 'Import from Dropbox', + 'new_album' => 'New Album', + 'new_tag_album' => 'New Tag Album', + 'upload_track' => 'Upload track', + 'delete_track' => 'Delete track', + ], + + 'sort' => [ + 'photo_select_1' => 'Upload Time', + 'photo_select_2' => 'Take Date', + 'photo_select_3' => 'Title', + 'photo_select_4' => 'Description', + 'photo_select_6' => 'Star', + 'photo_select_7' => 'Photo Format', + 'ascending' => 'Ascending', + 'descending' => 'Descending', + 'album_select_1' => 'Creation Time', + 'album_select_2' => 'Title', + 'album_select_3' => 'Description', + 'album_select_5' => 'Latest Take Date', + 'album_select_6' => 'Oldest Take Date', + ], + + 'albums_protection' => [ + 'private' => 'private', + 'public' => 'public', + 'inherit_from_parent' => 'inherit from parent', + ], +]; \ No newline at end of file diff --git a/lang/nl/jobs.php b/lang/nl/jobs.php new file mode 100644 index 00000000000..5d952b76012 --- /dev/null +++ b/lang/nl/jobs.php @@ -0,0 +1,18 @@ + 'Jobs', + + 'no_data' => 'No Jobs have been executed yet.', +]; \ No newline at end of file diff --git a/lang/nl/landing.php b/lang/nl/landing.php new file mode 100644 index 00000000000..fe6fe55b8ea --- /dev/null +++ b/lang/nl/landing.php @@ -0,0 +1,19 @@ + 'Gallery', + 'access_gallery' => 'Access the gallery', + 'hosted_with_lychee' => 'Hosted with Lychee', + 'copyright' => 'All images on this website are subject to copyright by %1$s © %2$s', +]; \ No newline at end of file diff --git a/lang/nl/left-menu.php b/lang/nl/left-menu.php new file mode 100644 index 00000000000..9a3e91f4037 --- /dev/null +++ b/lang/nl/left-menu.php @@ -0,0 +1,29 @@ + 'Back to Gallery', + + 'admin' => 'Admin', + 'clockwork' => 'Clockwork App', + 'logs' => 'Show Logs', + 'jobs' => 'Show Job History', + 'user' => 'User', + + 'sign_out' => 'Sign Out', + + 'about' => 'About', + 'api' => 'API Documentation', + 'source_code' => 'Source Code', + 'support' => 'Support', +]; \ No newline at end of file diff --git a/lang/nl/lychee.php b/lang/nl/lychee.php new file mode 100644 index 00000000000..ef26ad34fa8 --- /dev/null +++ b/lang/nl/lychee.php @@ -0,0 +1,535 @@ + 'Gebruikersnaam', + 'PASSWORD' => 'Wachtwoord', + 'ENTER' => 'Enter', + 'CANCEL' => 'Annuleer', + 'CONFIRM' => 'Doe maar', + 'SIGN_IN' => 'Log in', + 'CLOSE' => 'Sluit', + 'SETTINGS' => 'Settings', + 'SEARCH' => 'Search …', + 'MORE' => 'More', + 'DEFAULT' => 'Default', + 'GALLERY' => 'Gallery', + + 'USERS' => 'Users', + 'PROFILE' => 'Profile', + 'CREATE' => 'Create', + 'REMOVE' => 'Remove', + 'SHARE' => 'Share', + 'U2F' => 'U2F', + 'NOTIFICATIONS' => 'Notifications', + 'SHARING' => 'Sharing', + 'CHANGE_LOGIN' => 'Verander Login', + 'CHANGE_SORTING' => 'Verander Sortering', + 'SET_DROPBOX' => 'Set Dropbox', + 'ABOUT_LYCHEE' => 'Over Lychee', + 'DIAGNOSTICS' => 'Diagnostics', + 'DIAGNOSTICS_GET_SIZE' => 'Request space usage', + 'JOBS' => 'Show job history', + 'LOGS' => 'Laat logs zien', + 'SIGN_OUT' => 'Log uit', + 'UPDATE_AVAILABLE' => 'Update beschikbaar!', + 'MIGRATION_AVAILABLE' => 'Migration available!', + 'CHECK_FOR_UPDATE' => 'Check for updates', + 'DEFAULT_LICENSE' => 'Default license for new uploads:', + 'SET_LICENSE' => 'Set License', + 'SET_OVERLAY_TYPE' => 'Set Overlay', + 'SET_ALBUM_DECORATION' => 'Set album decorations', + 'SET_MAP_PROVIDER' => 'Set OpenStreetMap tiles provider', + 'FULL_SETTINGS' => 'Full Settings', + 'UPDATE' => 'Update', + 'RESET' => 'Reset', + 'DISABLE_TOKEN_TOOLTIP' => 'Disable', + 'ENABLE_TOKEN' => 'Enable API token', + 'DISABLED_TOKEN_STATUS_MSG' => 'Disabled', + 'TOKEN_BUTTON' => 'API Token ...', + 'TOKEN_NOT_AVAILABLE' => 'You have already viewed this token.', + 'TOKEN_WAIT' => 'Wait ...', + + 'SMART_ALBUMS' => 'Slimme albums', + 'SHARED_ALBUMS' => 'Shared albums', + 'ALBUMS' => 'Albums', + 'PHOTOS' => 'Pictures', + 'SEARCH_RESULTS' => 'Search results', + + 'RENAME' => 'Hernoem', + 'RENAME_ALL' => 'Geselecteerde Hernoem', + 'MERGE' => 'Voeg samen', + 'MERGE_ALL' => 'Geselecteerd samenvoegen', + 'MAKE_PUBLIC' => 'Maak Publiek', + 'SHARE_ALBUM' => 'Deel Album', + 'SHARE_PHOTO' => 'Deel Photo', + 'VISIBILITY_ALBUM' => 'Album Visibility', + 'VISIBILITY_PHOTO' => 'Photo Visibility', + 'DOWNLOAD_ALBUM' => 'Download Album', + 'ABOUT_ALBUM' => 'Over Album', + 'DELETE_ALBUM' => 'Verwijder Album', + 'MOVE_ALBUM' => 'Move Album', + 'FULLSCREEN_ENTER' => 'Enter Fullscreen', + 'FULLSCREEN_EXIT' => 'Exit Fullscreen', + + 'SHARING_ALBUM_USERS' => 'Share this album with users', + 'WAIT_FETCH_DATA' => 'Please wait while we get the data …', + 'SHARING_ALBUM_USERS_NO_USERS' => 'There are no users to share the album with', + 'SHARING_ALBUM_USERS_LONG_MESSAGE' => 'Select the users to share this album with', + + 'DELETE_ALBUM_QUESTION' => 'Verwijder Album en Foto’s', + 'KEEP_ALBUM' => 'Behoud Album', + 'DELETE_ALBUM_CONFIRMATION' => 'Weet je zeker dat je dit album en alle foto’s die het “%s” bevat wilt verwijderen? Deze actie kan niet ongedaan gemaakt worden!', + + 'DELETE_TAG_ALBUM_QUESTION' => 'Delete Album', + 'DELETE_TAG_ALBUM_CONFIRMATION' => 'Are you sure you want to delete the album “%s” (any photos inside will not be deleted)? This action can’t be undone!', + + 'DELETE_ALBUMS_QUESTION' => 'Verwijder Albums en Foto’s', + 'KEEP_ALBUMS' => 'Behoud Albums', + 'DELETE_ALBUMS_CONFIRMATION' => 'Weet je zeker dat je deze albums en alle foto’s die ze %d bevatten wilt verwijderen? Deze actie kan niet ongedaan gemaakt worden!', + + 'DELETE_UNSORTED_CONFIRM' => 'Weet je zeker dat je alle foto’s van “Ongesoorteerd” wilt verwijdren? Deze actie kan niet ongedaan gemaakt worden!', + 'CLEAR_UNSORTED' => 'Wis Ongesoorteerd', + 'KEEP_UNSORTED' => 'Behoud Ongesoorteerd', + + 'EDIT_SHARING' => 'Bewerk delen', + 'MAKE_PRIVATE' => 'Maak privé', + + 'CLOSE_ALBUM' => 'Sluit Album', + 'CLOSE_PHOTO' => 'Sluit Foto', + 'CLOSE_MAP' => 'Close Map', + + 'ADD' => 'Voeg toe', + 'MOVE' => 'Verplaats', + 'MOVE_ALL' => 'Verplaatsen Geselecteerd', + 'DUPLICATE' => 'Dupliceer', + 'DUPLICATE_ALL' => 'Duplicaat Geselecteerd', + 'COPY_TO' => 'Copy to …', + 'COPY_ALL_TO' => 'Kopiëren geselecteerd om …', + 'DELETE' => 'Verwijder', + 'SAVE' => 'Save', + 'DELETE_ALL' => 'Geselecteerde verwijderen', + 'DOWNLOAD' => 'Download', + 'DOWNLOAD_ALL' => 'Download Geselecteerd', + 'UPLOAD_PHOTO' => 'Upload Foto', + 'IMPORT_LINK' => 'Importeer van Link', + 'IMPORT_DROPBOX' => 'Importeer van Dropbox', + 'IMPORT_SERVER' => 'Importeer van Server', + 'NEW_ALBUM' => 'Nieuw Album', + 'NEW_TAG_ALBUM' => 'New Tag Album', + 'UPLOAD_TRACK' => 'Upload track', + 'DELETE_TRACK' => 'Delete track', + + 'TITLE_NEW_ALBUM' => 'Voer een titel voor het album in:', + 'UNTITLED' => 'Ongetiteld', + 'UNSORTED' => 'Ongesoorteerd', + 'STARRED' => 'Met ster', + 'RECENT' => 'Recentelijk', + 'PUBLIC' => 'Publiekelijk', + 'ON_THIS_DAY' => 'Deze Dag', + 'NUM_PHOTOS' => 'Foto’s', + + 'CREATE_ALBUM' => 'Maak Album', + 'CREATE_TAG_ALBUM' => 'Create Tag Album', + + 'STAR_PHOTO' => 'Markeer met ster', + 'STAR' => 'Ster', + 'UNSTAR' => 'Unstar', + 'STAR_ALL' => 'Markeer geselecteerd als favorieten', + 'UNSTAR_ALL' => 'Verwijder geselecteerd als favorieten', + 'TAG' => 'Tags', + 'TAG_ALL' => 'Geselecteerde tags', + 'UNSTAR_PHOTO' => 'Verwijder ster markeering', + 'SET_COVER' => 'Set Album Cover', + 'REMOVE_COVER' => 'Remove Album Cover', + 'SET_HEADER' => 'Set Album Header', + 'REMOVE_HEADER' => 'Remove Album Header', + 'SET_COMPACT_HEADER' => 'Use Compact Header', + + 'FULL_PHOTO' => 'Open Original', + 'ABOUT_PHOTO' => 'Over Foto', + 'DISPLAY_FULL_MAP' => 'Map', + 'DIRECT_LINK' => 'Directe Link', + 'DIRECT_LINKS' => 'Directe Links', + 'QR_CODE' => 'QR Code', + + 'ALBUM_ABOUT' => 'Over', + 'ALBUM_BASICS' => 'Basics', + 'ALBUM_TITLE' => 'Titel', + 'ALBUM_COPYRIGHT' => 'Copyright', + 'ALBUM_SET_COPYRIGHT' => 'Set copyright', + 'ALBUM_NEW_TITLE' => 'Geef dit album een nieuwe titel:', + 'ALBUMS_NEW_TITLE' => 'Geef alle geselecteerde %d albums een nieuwe titel:', + 'ALBUM_SET_TITLE' => 'Sla Titel op', + 'ALBUM_DESCRIPTION' => 'Onderwerk', + 'ALBUM_SHOW_TAGS' => 'Tags to show', + 'ALBUM_NEW_DESCRIPTION' => 'Geef een nieuwe omschrijving in:', + 'ALBUM_SET_DESCRIPTION' => 'Sla Omschrijving op', + 'ALBUM_NEW_SHOWTAGS' => 'Enter tags of photos that will be visible in this album:', + 'ALBUM_SET_SHOWTAGS' => 'Set tags to show', + 'ALBUM_ALBUM' => 'Album', + 'ALBUM_CREATED' => 'Aangemaakt', + 'ALBUM_IMAGES' => 'Afbeeldingen', + 'ALBUM_VIDEOS' => 'Videos', + 'ALBUM_SUBALBUMS' => 'Subalbums', + 'ALBUM_SHARING' => 'Deel', + 'ALBUM_SHR_YES' => 'Ja', + 'ALBUM_SHR_NO' => 'Nee', + 'ALBUM_PUBLIC' => 'Publiekelijk', + 'ALBUM_PUBLIC_EXPL' => 'Anonymous users can access this album, subject to the restrictions below.', + 'ALBUM_FULL' => 'Original', + 'ALBUM_FULL_EXPL' => 'Anonymous users can behold full-resolution photos.', + 'ALBUM_HIDDEN' => 'Verborgen', + 'ALBUM_HIDDEN_EXPL' => 'Anonymous users need a direct link to access this album.', + 'ALBUM_MARK_NSFW' => 'Mark album as sensitive', + 'ALBUM_UNMARK_NSFW' => 'Unmark album as sensitive', + 'ALBUM_NSFW' => 'Sensitive', + 'ALBUM_NSFW_EXPL' => 'Album is marked to contain sensitive content.', + 'ALBUM_DOWNLOADABLE' => 'Downloadbaar', + 'ALBUM_DOWNLOADABLE_EXPL' => 'Anonymous users can download this album.', + 'ALBUM_SHARE_BUTTON_VISIBLE' => 'Share button is visible', + 'ALBUM_SHARE_BUTTON_VISIBLE_EXPL' => 'Anonymous users see social media sharing links.', + 'ALBUM_PASSWORD' => 'Wachtwoord', + 'ALBUM_PASSWORD_PROT' => 'Met wachtwoord beschermt', + 'ALBUM_PASSWORD_PROT_EXPL' => 'Anonymous users need a shared password to access this album.', + 'ALBUM_PASSWORD_REQUIRED' => 'Dit album is met een wachtwoord beschermt, voer het wachtwoord in:', + 'ALBUM_MERGE' => 'Weet je zeker dat je dit album wilt samenvoegen “%1$s” met het album “%2$s”?', + 'ALBUMS_MERGE' => 'Weet je zeker dat je alle albums wilt samenvoegen naar “%s”?', + 'MERGE_ALBUM' => 'Voeg albums samen', + 'DONT_MERGE' => 'Voeg niet samen', + 'ALBUM_MOVE' => 'Are you sure you want to move the album “%1$s” into the album “%2$s”?', + 'ALBUMS_MOVE' => 'Are you sure you want to move all selected albums into the album “%s”?', + 'MOVE_ALBUMS' => 'Move Albums', + 'NOT_MOVE_ALBUMS' => "Don't Move", + 'ROOT' => 'Albums', + 'ALBUM_REUSE' => 'Reuse', + 'ALBUM_LICENSE' => 'License', + 'ALBUM_SET_LICENSE' => 'Set License', + 'ALBUM_LICENSE_HELP' => 'Need help choosing?', + 'ALBUM_LICENSE_NONE' => 'None', + 'ALBUM_RESERVED' => 'All Rights Reserved', + 'ALBUM_SET_ORDER' => 'Set Order', + 'ALBUM_ORDERING' => 'Order by', + 'ALBUM_PHOTO_ORDERING' => 'Order photos by', + 'ALBUM_CHILDREN_ORDERING' => 'Order albums by', + 'ALBUM_OWNER' => 'Owner', + + 'PHOTO_ABOUT' => 'Over', + 'PHOTO_BASICS' => 'Basics', + 'PHOTO_TITLE' => 'Titel', + 'PHOTO_NEW_TITLE' => 'Geef deze foto een nieuwe titel:', + 'PHOTO_SET_TITLE' => 'Sla Titel op', + 'PHOTO_UPLOADED' => 'Geupload', + 'PHOTO_DESCRIPTION' => 'Omschrijving', + 'PHOTO_NEW_DESCRIPTION' => 'Geef deze foto een nieuwe omschrijving:', + 'PHOTO_SET_DESCRIPTION' => 'Sla omschrijving op', + 'PHOTO_NEW_LICENSE' => 'Add a License', + 'PHOTO_SET_LICENSE' => 'Set License', + 'PHOTO_LICENSE' => 'License', + 'PHOTO_LICENSE_HELP' => 'Need help choosing?', + 'PHOTO_REUSE' => 'Reuse', + 'PHOTO_LICENSE_NONE' => 'None', + 'PHOTO_RESERVED' => 'All Rights Reserved', + 'PHOTO_LATITUDE' => 'Latitude', + 'PHOTO_LONGITUDE' => 'Longitude', + 'PHOTO_ALTITUDE' => 'Altitude', + 'PHOTO_IMGDIRECTION' => 'Direction', + 'PHOTO_LOCATION' => 'Location', + 'PHOTO_IMAGE' => 'Afbeelding', + 'PHOTO_VIDEO' => 'Video', + 'PHOTO_SIZE' => 'Grootte', + 'PHOTO_FORMAT' => 'Formaat', + 'PHOTO_RESOLUTION' => 'Resolutie', + 'PHOTO_DURATION' => 'Duration', + 'PHOTO_FPS' => 'Frame rate', + 'PHOTO_TAGS' => 'Tags', + 'PHOTO_NOTAGS' => 'Geen Tags', + 'PHOTO_NEW_TAGS' => 'Voer je tags voor deze foto in, meerdere tags kunnen worden gescheiden door komma’s:', + 'PHOTOS_NEW_TAGS' => 'Voer je tags in voor alle %d geselecteerde foto’s, meerdere tags kunnen worden gescheiden door komma’s:', + 'PHOTO_SET_TAGS' => 'Sla Tags op', + 'PHOTO_CAMERA' => 'Camera', + 'PHOTO_CAPTURED' => 'Gefotografeerd', + 'PHOTO_MAKE' => 'Fabricant', + 'PHOTO_TYPE' => 'Type/Model', + 'PHOTO_LENS' => 'Lens', + 'PHOTO_SHUTTER' => 'Sluitertijd', + 'PHOTO_APERTURE' => 'Diafragma', + 'PHOTO_FOCAL' => 'Brandpuntafstand', + 'PHOTO_ISO' => 'ISO %s', + 'PHOTO_SHARING' => 'Deling', + 'PHOTO_DELETE' => 'Verwijder Foto', + 'PHOTO_KEEP' => 'Behoud Foto', + 'PHOTO_DELETE_CONFIRMATION' => 'Weet je zeker dat je deze foto “%s” wilt verwijderen? Deze actie kan niet ongedaan gemaakt worden!', + 'PHOTO_DELETE_ALL' => 'Weet je zeker dat je alle geslecteerd foto’s wilt verwijderen? %d Deze actie kan niet ongedaan gemaakt worden!', + 'PHOTOS_NEW_TITLE' => 'Voer een titel in voor alle %d geselecteerde foto’s:', + 'PHOTO_MAKE_PRIVATE_ALBUM' => 'Deze foto bevind zich in een gedeeld album. Om de zichtbaarheid van deze foto te wijzigen pas je de zichtbaarheid van het album aan.', + 'PHOTO_SHOW_ALBUM' => 'Geef album weer', + 'PHOTO_PUBLIC' => 'Public', + 'PHOTO_PUBLIC_EXPL' => 'Anonymous users can view this photo, subject to the restrictions below.', + 'PHOTO_FULL' => 'Original', + 'PHOTO_FULL_EXPL' => 'Anonymous users can behold full-resolution photo.', + 'PHOTO_HIDDEN' => 'Hidden', + 'PHOTO_HIDDEN_EXPL' => 'Anonymous users need a direct link to view this photo.', + 'PHOTO_DOWNLOADABLE' => 'Downloadable', + 'PHOTO_DOWNLOADABLE_EXPL' => 'Anonymous users may download this photo.', + 'PHOTO_SHARE_BUTTON_VISIBLE' => 'Share button is visible', + 'PHOTO_SHARE_BUTTON_VISIBLE_EXPL' => 'Anonymous users can see social media sharing links.', + 'PHOTO_PASSWORD_PROT' => 'Password protected', + 'PHOTO_PASSWORD_PROT_EXPL' => 'Anonymous users need a shared password to view this photo.', + 'PHOTO_EDIT_SHARING_TEXT' => 'The sharing properties of this photo will be changed to the following:', + 'PHOTO_NO_EDIT_SHARING_TEXT' => 'Because this photo is located in a public album, it inherits that album’s visibility settings. Its current visibility is shown below for informational purposes only.', + 'PHOTO_EDIT_GLOBAL_SHARING_TEXT' => 'The visibility of this photo can be fine-tuned using global Lychee settings. Its current visibility is shown below for informational purposes only.', + 'PHOTO_NEW_CREATED_AT' => 'Enter the upload date for this photo. mm/dd/yyyy, hh:mm [am/pm]', + 'PHOTO_SET_CREATED_AT' => 'Set upload date', + + 'LOADING' => 'Laden', + 'ERROR' => 'Error', + 'ERROR_TEXT' => 'Whoops, er is iets misgegaan. Herlaad de pagina en probeer het opnieuw!', + 'ERROR_UNKNOWN' => 'Er is iets onverwachts gebeurd. Probeer het opnieuw of controleer je installatie en server. Kijk naar de readme voor meer informatie.', + 'ERROR_MAP_DEACTIVATED' => 'Map functionality has been deactivated under settings.', + 'ERROR_SEARCH_DEACTIVATED' => 'Search functionality has been deactivated under settings.', + 'SUCCESS' => 'OK', + 'CHANGE_SUCCESS' => 'Change successful.', + 'RETRY' => 'Probeer opnieuw', + 'OVERRIDE' => 'Override', + 'TAGS_OVERRIDE_INFO' => 'If this is unchecked, the tags will be added to the existing tags of the photo.', + + 'SETTINGS_SUCCESS_LOGIN' => 'Login Info updated.', + 'SETTINGS_SUCCESS_SORT' => 'Sorting order updated.', + 'SETTINGS_SUCCESS_DROPBOX' => 'Dropbox Key updated.', + 'SETTINGS_SUCCESS_LANG' => 'Language updated', + 'SETTINGS_SUCCESS_LAYOUT' => 'Layout updated', + 'SETTINGS_SUCCESS_IMAGE_OVERLAY' => 'EXIF Overlay setting updated', + 'SETTINGS_SUCCESS_PUBLIC_SEARCH' => 'Publieke zoekactie bijgewerkt', + 'SETTINGS_SUCCESS_LICENSE' => 'Default license updated', + 'SETTINGS_SUCCESS_MAP_DISPLAY' => 'Map display settings updated', + 'SETTINGS_SUCCESS_MAP_DISPLAY_PUBLIC' => 'Map display settings for public albums updated', + 'SETTINGS_SUCCESS_MAP_PROVIDER' => 'Map provider settings updated', + 'SETTINGS_SUCCESS_CSS' => 'Stylesheets updated', + 'SETTINGS_SUCCESS_JS' => 'JS updated', + 'SETTINGS_SUCCESS_UPDATE' => 'Settings updated successfully', + 'SETTINGS_DROPBOX_KEY' => 'Dropbox API Key', + 'SETTINGS_ADVANCED_WARNING_EXPL' => 'Changing these advanced settings can be harmful to the stability, security and performance of this application. You should only modify them if you are sure of what you are doing.', + 'SETTINGS_ADVANCED_SAVE' => 'Save my modifications, I accept the risk!', + + 'U2F_NOT_SUPPORTED' => 'U2F not supported. Sorry.', + 'U2F_NOT_SECURE' => 'Environment not secured. U2F not available.', + 'U2F_REGISTER_KEY' => 'Register new device.', + 'U2F_REGISTRATION_SUCCESS' => 'Registration successful!', + 'U2F_AUTHENTIFICATION_SUCCESS' => 'Authentication successful!', + 'U2F_CREDENTIALS' => 'Credentials', + 'U2F_CREDENTIALS_DELETED' => 'Credentials deleted!', + 'U2F_LOGIN' => 'Log in with WebAuthn', + + 'NEW_PHOTOS_NOTIFICATION' => 'Send new photos notification emails.', + 'SETTINGS_SUCCESS_NEW_PHOTOS_NOTIFICATION' => 'New photos notification updated', + 'USER_EMAIL_INSTRUCTION' => 'Add your email below to enable receiving email notifications. To stop receiving emails, simply remove your email below.', + + 'LOGIN_USERNAME' => 'Nieuw Gebruikersnaam', + 'LOGIN_PASSWORD' => 'Nieuw Wachtwoord', + 'LOGIN_PASSWORD_CONFIRM' => 'Confirm Password', + 'PASSWORD_TITLE' => 'Voer je huidige wachtwoord in:', + 'PASSWORD_CURRENT' => 'Huidig Wachtwoord', + 'PASSWORD_TEXT' => 'Je gebruikersnaam en wachtwoord worden verandert naar:', + 'PASSWORD_CHANGE' => 'Verander Login', + + 'EDIT_SHARING_TITLE' => 'Bewerk delen', + 'EDIT_SHARING_TEXT' => 'De deelinstellingen van dit album worden alsvolgt ingesteld:', + 'SHARE_ALBUM_TEXT' => 'Dit album wordt gedeeld met de volgende instellingen:', + + 'SORT_DIALOG_ATTRIBUTE_LABEL' => 'Attribute', + 'SORT_DIALOG_ORDER_LABEL' => 'Order', + + 'SORT_ALBUM_BY' => 'Sorteer albums op %1$s in een %2$s volgorde.', + + 'SORT_ALBUM_SELECT_1' => 'Aangemaakt op', + 'SORT_ALBUM_SELECT_2' => 'Titel', + 'SORT_ALBUM_SELECT_3' => 'Omschrijving', + 'SORT_ALBUM_SELECT_5' => 'Nieuwste foto datum', + 'SORT_ALBUM_SELECT_6' => 'Oudste foto datum', + + 'SORT_PHOTO_BY' => 'Sorteer albums op %1$s in een %2$s volgorde.', + + 'SORT_PHOTO_SELECT_1' => 'Upload Tijd', + 'SORT_PHOTO_SELECT_2' => 'Aangemaakt op', + 'SORT_PHOTO_SELECT_3' => 'Titel', + 'SORT_PHOTO_SELECT_4' => 'Omschrijving', + 'SORT_PHOTO_SELECT_6' => 'Ster', + 'SORT_PHOTO_SELECT_7' => 'Foto formaat', + + 'SORT_ASCENDING' => 'Oplopende', + 'SORT_DESCENDING' => 'Aflopende', + 'SORT_CHANGE' => 'Change Sorting', + + 'DROPBOX_TITLE' => 'Stel Dropbox sleutel in', + 'DROPBOX_TEXT' => "Om foto’s vanuit Dropbox te kunnen importeren moet je een geldige drop-ins app sleutel hebben van hun website. Genereer een sleutel en voer die in:", + + 'LANG_TEXT' => 'Change Lychee language for:', + 'LANG_TITLE' => 'Change Language', + + 'SETTING_RECENT_PUBLIC_TEXT' => 'Make "Recent" smart album accessible to anonymous users', + 'SETTING_STARRED_PUBLIC_TEXT' => 'Make "Starred" smart album accessible to anonymous users', + 'SETTING_ONTHISDAY_PUBLIC_TEXT' => 'Make "On This Day" smart album accessible to anonymous users', + + 'CSS_TEXT' => 'Personalize CSS:', + 'CSS_TITLE' => 'Change CSS', + 'JS_TEXT' => 'Custom JS:', + 'JS_TITLE' => 'Change JS', + 'PUBLIC_SEARCH_TEXT' => 'Openbare zoekactie toegestaan:', + 'OVERLAY_TYPE' => 'Photo overlay:', + 'OVERLAY_NONE' => 'None', + 'OVERLAY_EXIF' => 'EXIF data', + 'OVERLAY_DESCRIPTION' => 'Description', + 'OVERLAY_DATE' => 'Date taken', + 'ALBUM_DECORATION' => 'Album decorations:', + 'ALBUM_DECORATION_NONE' => 'None', + 'ALBUM_DECORATION_ORIGINAL' => 'Sub-album marker', + 'ALBUM_DECORATION_ALBUM' => 'Number of sub-albums', + 'ALBUM_DECORATION_PHOTO' => 'Number of photos', + 'ALBUM_DECORATION_ALL' => 'Number of sub-albums and photos', + 'ALBUM_DECORATION_ORIENTATION' => 'Orientation of album decorations:', + 'ALBUM_DECORATION_ORIENTATION_ROW' => 'Horizontal (photos, albums)', + 'ALBUM_DECORATION_ORIENTATION_ROW_REVERSE' => 'Horizontal (albums, photos)', + 'ALBUM_DECORATION_ORIENTATION_COLUMN' => 'Vertical (top photos, albums)', + 'ALBUM_DECORATION_ORIENTATION_COLUMN_REVERSE' => 'Vertical (top albums, photos)', + 'MAP_DISPLAY_TEXT' => 'Enable maps (provided by OpenStreetMap):', + 'MAP_DISPLAY_PUBLIC_TEXT' => 'Enable maps for public albums (provided by OpenStreetMap):', + 'MAP_PROVIDER' => 'Provider of OpenStreetMap tiles:', + 'MAP_PROVIDER_WIKIMEDIA' => 'Wikimedia', + 'MAP_PROVIDER_OSM_ORG' => 'OpenStreetMap.org (no HiDPI)', + 'MAP_PROVIDER_OSM_DE' => 'OpenStreetMap.de (no HiDPI)', + 'MAP_PROVIDER_OSM_FR' => 'OpenStreetMap.fr (no HiDPI)', + 'MAP_PROVIDER_RRZE' => 'University of Erlangen, Germany (only HiDPI)', + 'MAP_INCLUDE_SUBALBUMS_TEXT' => 'Include photos of subalbums on map:', + 'LOCATION_DECODING' => 'Decode GPS data into location name', + 'LOCATION_SHOW' => 'Show location name', + 'LOCATION_SHOW_PUBLIC' => 'Show location name for public mode', + + 'LAYOUT_TYPE' => 'Layout of photos:', + 'LAYOUT_SQUARES' => 'Square thumbnails', + 'LAYOUT_JUSTIFIED' => 'With aspect, justified', + 'LAYOUT_MASONRY' => 'With aspect, masonry', + 'LAYOUT_GRID' => 'With aspect, grid', + 'LAYOUT_UNJUSTIFIED' => 'With aspect, unjustified', + 'SET_LAYOUT' => 'Change layout', + + 'NSFW_VISIBLE_TEXT_1' => 'Make Sensitive albums visible by default.', + 'NSFW_VISIBLE_TEXT_2' => 'If the album is public, it is still accessible, just hidden from the view and can be revealed by pressing H.', + 'SETTINGS_SUCCESS_NSFW_VISIBLE' => 'Default sensitive album visibility updated with success.', + + 'NSFW_BANNER' => '

Sensitive content

This album contains sensitive content which some people may find offensive or disturbing.

Tap to consent.

', + 'NSFW_HEADER' => 'Sensitive content', + 'NSFW_EXPLANATION' => 'This album contains sensitive content which some people may find offensive or disturbing.', + 'TAP_CONSENT' => 'Tap to consent.', + + 'VIEW_NO_RESULT' => 'Geen resultaten', + 'VIEW_NO_PUBLIC_ALBUMS' => 'Geen publieke albums', + 'VIEW_NO_CONFIGURATION' => 'Geen configutatie', + 'VIEW_PHOTO_NOT_FOUND' => 'Foto niet gevonden', + + 'NO_TAGS' => 'Geen tags', + + 'UPLOAD_MANAGE_NEW_PHOTOS' => 'Je kan je nieuwe foto(’s) nu beheren.', + 'UPLOAD_COMPLETE' => 'Upload voltooid', + 'UPLOAD_COMPLETE_FAILED' => 'Fout bij het uploaden van een of meerdere foto’s.', + 'UPLOAD_IMPORTING' => 'Importeren', + 'UPLOAD_IMPORTING_URL' => 'Importeren van URL', + 'UPLOAD_UPLOADING' => 'Uploaden', + 'UPLOAD_FINISHED' => 'Afgerond', + 'UPLOAD_PROCESSING' => 'Verwerken', + 'UPLOAD_FAILED' => 'Mislukt', + 'UPLOAD_FAILED_ERROR' => 'Upload mislukt. Server gaf een error!', + 'UPLOAD_FAILED_WARNING' => 'Upload mislukt. Server gaf een waarschuwing!', + 'UPLOAD_CANCELLED' => 'Cancelled', + 'UPLOAD_SKIPPED' => 'Overgeslagen', + 'UPLOAD_UPDATED' => 'Updated', + 'UPLOAD_GENERAL' => 'General', + 'UPLOAD_IMPORT_SKIPPED_DUPLICATE' => 'This photo has been skipped because it’s already in your library.', + 'UPLOAD_IMPORT_RESYNCED_DUPLICATE' => 'This photo has been skipped because it’s already in your library, but its metadata has been updated.', + 'UPLOAD_ERROR_CONSOLE' => 'Kijk naar je browsers console voor meer informatie.', + 'UPLOAD_UNKNOWN' => 'Server gaf een onbekende terugkoppeling, kijk naar je browsers console voor meer informatie.', + 'UPLOAD_ERROR_UNKNOWN' => 'Upload mislukt. Server gaf een onbekende error!', + 'UPLOAD_ERROR_POSTSIZE' => 'Upload failed. The PHP post_max_size may be too small! Otherwise check the FAQ.', + 'UPLOAD_ERROR_FILESIZE' => 'Upload failed. The PHP upload_max_filesize may be too small! Otherwise check the FAQ.', + 'UPLOAD_IN_PROGRESS' => 'Lychee is aan het uploaden!', + 'UPLOAD_IMPORT_WARN_ERR' => 'De import is voltooid maar gaf waarschuwingen of errors terug. Kijk naar de logs (instellingen -> Show Log) for further details.', + 'UPLOAD_IMPORT_COMPLETE' => 'Import complete', + 'UPLOAD_IMPORT_INSTR' => 'Please enter the direct link to a photo to import it:', + 'UPLOAD_IMPORT' => 'Import', + 'UPLOAD_IMPORT_SERVER' => 'Importing from server', + 'UPLOAD_IMPORT_SERVER_FOLD' => 'Folder empty or no readable files to process. Please take a look at the log (Settings -> Laat logs zien) voor meer informatie.', + 'UPLOAD_IMPORT_SERVER_INSTR' => 'Import all photos, folders and sub-folders located in the folders with the following absolute paths (on server). Paths are space separated, use \\ to escape a space in a path.', + 'UPLOAD_ABSOLUTE_PATH' => 'Absolute path to directories, space separated', + 'UPLOAD_IMPORT_SERVER_EMPT' => 'Kan de import niet starten, folder is leeg!', + 'UPLOAD_IMPORT_DELETE_ORIGINALS' => 'Delete originals', + 'UPLOAD_IMPORT_DELETE_ORIGINALS_EXPL' => 'De orginele bestanden worden verwijderd na de import indien mogelijk.', + 'UPLOAD_IMPORT_VIA_SYMLINK' => 'Symbolic links', + 'UPLOAD_IMPORT_VIA_SYMLINK_EXPL' => 'Import files using symbolic links to originals.', + 'UPLOAD_IMPORT_SKIP_DUPLICATES' => 'Skip duplicates', + 'UPLOAD_IMPORT_SKIP_DUPLICATES_EXPL' => 'Existing media files are skipped.', + 'UPLOAD_IMPORT_RESYNC_METADATA' => 'Re-sync metadata', + 'UPLOAD_IMPORT_RESYNC_METADATA_EXPL' => 'Update metadata of existing media files.', + 'UPLOAD_IMPORT_LOW_MEMORY_EXPL' => 'The import process on the server is approaching the memory limit and may end up being terminated prematurely.', + 'UPLOAD_WARNING' => 'Warning', + 'UPLOAD_IMPORT_NOT_A_DIRECTORY' => 'The given path is not a readable directory!', + 'UPLOAD_IMPORT_PATH_RESERVED' => 'The given path is a reserved path of Lychee!', + 'UPLOAD_IMPORT_FAILED' => 'Could not import the file!', + 'UPLOAD_IMPORT_UNSUPPORTED' => 'Unsupported file type!', + 'UPLOAD_IMPORT_CANCELLED' => 'Import cancelled', + + 'ABOUT_SUBTITLE' => 'Self-hosted photo-management done right', + 'ABOUT_DESCRIPTION' => 'Lychee is a free photo-management tool, which runs on your server or web-space. Installing is a matter of seconds. Upload, manage and share photos like from a native application. Lychee comes with everything you need and all your photos are stored securely.', + 'FOOTER_COPYRIGHT' => 'Alle afbeeldingen op deze website zijn onderworpen aan het auteursrecht van %1$s © %2$s', + 'HOSTED_WITH_LYCHEE' => 'Hosted with Lychee', + + 'URL_COPY_TO_CLIPBOARD' => 'Copy to clipboard', + 'URL_COPIED_TO_CLIPBOARD' => 'Copied URL to clipboard!', + 'PHOTO_DIRECT_LINKS_TO_IMAGES' => 'Direct links to image files:', + 'PHOTO_ORIGINAL' => 'Original', + 'PHOTO_MEDIUM' => 'Medium', + 'PHOTO_MEDIUM_HIDPI' => 'Medium HiDPI', + 'PHOTO_SMALL' => 'Thumb', + 'PHOTO_SMALL_HIDPI' => 'Thumb HiDPI', + 'PHOTO_THUMB' => 'Square thumb', + 'PHOTO_THUMB_HIDPI' => 'Square thumb HiDPI', + 'PHOTO_PLACEHOLDER' => 'Low Quality Image Placeholder', + 'PHOTO_THUMBNAIL' => 'Photo thumbnail', + 'PHOTO_LIVE_VIDEO' => 'Video part of live-photo', + 'PHOTO_VIEW' => 'Lychee Photo View:', + + 'PHOTO_EDIT_ROTATECWISE' => 'Rotate clockwise', + 'PHOTO_EDIT_ROTATECCWISE' => 'Rotate counter-clockwise', + + 'ERROR_GPX' => 'Error loading GPX file: ', + 'ERROR_EITHER_ALBUMS_OR_PHOTOS' => 'Please select either albums or photos!', + 'ERROR_COULD_NOT_FIND' => 'Could not find what you want.', + 'ERROR_INVALID_EMAIL' => 'Not a valid email address.', + 'EMAIL_SUCCESS' => 'Email updated!', + 'ERROR_PHOTO_NOT_FOUND' => 'Error: photo %s not found !', + 'ERROR_EMPTY_USERNAME' => 'new username cannot be empty.', + 'ERROR_PASSWORD_DOES_NOT_MATCH' => 'new password does not match.', + 'ERROR_EMPTY_PASSWORD' => 'new password cannot be empty.', + 'ERROR_SELECT_ALBUM' => 'Select an album to share!', + 'ERROR_SELECT_USER' => 'Select a user to share with!', + 'ERROR_SELECT_SHARING' => 'Select a sharing to remove!', + 'SHARING_SUCCESS' => 'Sharing updated!', + 'SHARING_REMOVED' => 'Sharing removed!', + 'USER_CREATED' => 'User created!', + 'USER_DELETED' => 'User deleted!', + 'USER_UPDATED' => 'User updated!', + 'ENTER_EMAIL' => 'Enter your email address:', + 'ERROR_ALBUM_JSON_NOT_FOUND' => 'Error: Album json not found!', + 'ERROR_ALBUM_NOT_FOUND' => 'Error: album %s not found', + 'ERROR_DROPBOX_KEY' => 'Error: Dropbox key not set', + 'ERROR_SESSION' => 'Session expired.', + 'CAMERA_DATE' => 'Camera date', + 'NEW_PASSWORD' => 'new password', + 'ALLOW_UPLOADS' => 'Allow uploads', + 'ALLOW_USER_SELF_EDIT' => 'Allow self-management of user account', + 'OSM_CONTRIBUTORS' => 'OpenStreetMap contributors', +]; diff --git a/lang/nl/maintenance.php b/lang/nl/maintenance.php new file mode 100644 index 00000000000..f86de3d6f46 --- /dev/null +++ b/lang/nl/maintenance.php @@ -0,0 +1,60 @@ + 'Maintenance', + 'description' => 'You will find on this page, all the required actions to keep your Lychee installation running smooth and nicely.', + 'cleaning' => [ + 'title' => 'Cleaning %s', + 'result' => '%s deleted.', + 'description' => 'Remove all contents from %s', + 'button' => 'Clean', + ], + 'fix-jobs' => [ + 'title' => 'Fixing Jobs History', + 'description' => 'Mark jobs with status %s or %s as %s.', + 'button' => 'Fix job history', + ], + 'gen-sizevariants' => [ + 'title' => 'Missing %s', + 'description' => 'Found %d %s that could be generated.', + 'button' => 'Generate!', + 'success' => 'Successfully generated %d %s.', + ], + 'fill-filesize-sizevariants' => [ + 'title' => 'File sizes missing', + 'description' => 'Found %d small variants without file size.', + 'button' => 'Fetch data!', + 'success' => 'Successfully computed sizes of %d small variants.', + ], + 'fix-tree' => [ + 'title' => 'Tree statistics', + 'Oddness' => 'Oddness', + 'Duplicates' => 'Duplicates', + 'Wrong parents' => 'Wrong parents', + 'Missing parents' => 'Missing parents', + 'button' => 'Fix tree', + ], + 'optimize' => [ + 'title' => 'Optimize Database', + 'description' => 'If you notice slowdown in your installation, it may be because your database does not + have all its needed index.', + 'button' => 'Optimize Database', + ], + 'update' => [ + 'title' => 'Updates', + 'check-button' => 'Check for updates', + 'update-button' => 'Update', + 'no-pending-updates' => 'No pending update.', + ], +]; \ No newline at end of file diff --git a/lang/nl/profile.php b/lang/nl/profile.php new file mode 100644 index 00000000000..cc24b97452c --- /dev/null +++ b/lang/nl/profile.php @@ -0,0 +1,64 @@ + 'Profile', + + 'login' => [ + 'header' => 'Profile', + 'enter_current_password' => 'Enter your current password:', + 'current_password' => 'Current password', + 'credentials_update' => 'Your credentials will be changed to the following:', + 'username' => 'Username', + 'new_password' => 'New password', + 'confirm_new_password' => 'Confirm new password', + 'email_instruction' => 'Add your email below to enable receiving email notifications. To stop receiving emails, simply remove your email below.', + 'email' => 'Email', + 'change' => 'Change Login', + 'api_token' => 'API Token ...', + + 'missing_fields' => 'Missing fields', + ], + + 'token' => [ + 'unavailable' => 'You have already viewed this token.', + 'no_data' => 'No token API have been generated.', + 'disable' => 'Disable', + 'disabled' => 'Token disabled', + 'warning' => 'This token will not be displayed again. Copy it and keep it in a safe place.', + 'reset' => 'Reset the token', + 'create' => 'Create a new token', + ], + + 'oauth' => [ + 'header' => 'OAuth', + 'header_not_available' => 'OAuth is not available', + 'setup_env' => 'Set up the credentials in your .env', + 'token_registered' => '%s token registered.', + 'setup' => 'Set up %s', + 'reset' => 'reset', + 'credential_deleted' => 'Credential deleted!', + ], + + 'u2f' => [ + 'header' => 'Passkey/MFA/2FA', + 'info' => 'This only provides the ability to use WebAuthn to authenticate instead of username & password.', + 'empty' => 'Credentials list is empty!', + 'not_secure' => 'Environment not secured. U2F not available.', + 'new' => 'Register new device.', + 'credential_deleted' => 'Credential deleted!', + 'credential_updated' => 'Credential updated!', + 'credential_registred' => 'Registration successful!', + '5_chars' => 'At least 5 chars.', + ], +]; \ No newline at end of file diff --git a/lang/nl/settings.php b/lang/nl/settings.php new file mode 100644 index 00000000000..fd197f11135 --- /dev/null +++ b/lang/nl/settings.php @@ -0,0 +1,92 @@ + 'Settings', + 'small_screen' => 'For better a experience on the Settings page,
we recommend you use a larger screen.', + 'tabs' => [ + 'basic' => 'Basic', + 'all_settings' => 'All settings', + ], + 'toasts' => [ + 'change_saved' => 'Change saved!', + 'details' => 'Settings have been modified as per request', + 'error' => 'Error!', + 'error_load_css' => 'Could not load dist/user.css', + 'error_load_js' => 'Could not load dist/custom.js', + 'error_save_css' => 'Could not save CSS', + 'error_save_js' => 'Could not save JS', + 'thank_you' => 'Thank you for your support.', + 'reload' => 'Reload your page for full functionalities.', + ], + 'system' => [ + 'header' => 'System', + 'use_dark_mode' => 'Use dark mode for Lychee', + 'language' => 'Language used by Lychee', + 'nsfw_album_visibility' => 'Make Sensitive albums visible by default.', + 'nsfw_album_explanation' => 'If the album is public, it is still accessible, just hidden from the view and can be revealed by pressing H.', + ], + 'lychee_se' => [ + 'header' => 'Lychee SE', + 'call4action' => 'Get exclusive features and support the development of Lychee. Unlock the SE edition.', + 'preview' => 'Enable preview of Lychee SE features', + 'hide_call4action' => 'Hide this Lychee SE registration form. I am happy with Lychee as-is. :)', + 'hide_warning' => 'If enabled, the only way to register your license key will be via the More tab above. Changes are applied on page reload.', + ], + 'dropbox' => [ + 'header' => 'Dropbox', + 'instruction' => 'In order to import photos from your Dropbox, you need a valid drop-ins app key from their website.', + 'api_key' => 'Dropbox API Key', + 'set_key' => 'Set Dropbox Key', + ], + 'gallery' => [ + 'header' => 'Gallery', + 'photo_order_column' => 'Default column used for sorting photos', + 'photo_order_direction' => 'Default order used for sorting photos', + 'album_order_column' => 'Default column used for sorting albums', + 'album_order_direction' => 'Default order used for sorting albums', + 'aspect_ratio' => 'Default aspect ratio for album thumbs', + 'photo_layout' => 'Layout for pictures', + 'album_decoration' => 'Show decorations on album cover (sub-album and/or photo count)', + 'album_decoration_direction' => 'Align album decorations horizontally or vertically', + 'photo_overlay' => 'Default image overlay information', + 'license_default' => 'Default license used for albums', + 'license_help' => 'Need help choosing?', + ], + 'geolocation' => [ + 'header' => 'Geo-location', + 'map_display' => 'Display the map given GPS coordinates', + 'map_display_public' => 'Allow anonymous users to access the map', + 'map_provider' => 'Defines the map provider', + 'map_include_subalbums' => 'Includes pictures of the sub albums on the map', + 'location_decoding' => 'Use GPS location decoding', + 'location_show' => 'Show location extracted from GPS coordinates', + 'location_show_public' => 'Anonymous users can access the extracted location from GPS coordinates', + ], + 'advanced' => [ + 'header' => 'Advanced Customization', + 'change_css' => 'Change CSS', + 'change_js' => 'Change JS', + ], + 'all' => [ + 'old_setting_style' => 'Old setting style', + 'change_detected' => 'Some settings changed.', + 'save' => 'Save', + ], + + 'tool_option' => [ + 'disabled' => 'disabled', + 'enabled' => 'enabled', + 'discover' => 'discover', + ], +]; \ No newline at end of file diff --git a/lang/nl/sharing.php b/lang/nl/sharing.php new file mode 100644 index 00000000000..69de18cc6d0 --- /dev/null +++ b/lang/nl/sharing.php @@ -0,0 +1,33 @@ + 'Sharing', + + 'info' => 'This page gives an overview of and the ability to edit the sharing rights associated with albums.', + 'album_title' => 'Album title', + 'username' => 'Username', + 'no_data' => 'Sharing list is empty.', + 'share' => 'Share', + 'permission_deleted' => 'Permission deleted!', + 'permission_created' => 'Permission created!', + + 'grants' => [ + 'read' => 'Grants read access', + 'original' => 'Grants access to original photo', + 'download' => 'Grants download', + 'upload' => 'Grants upload', + 'edit' => 'Grants edit', + 'delete' => 'Grants delete', + ], +]; \ No newline at end of file diff --git a/lang/nl/statistics.php b/lang/nl/statistics.php new file mode 100644 index 00000000000..2baf855bbd5 --- /dev/null +++ b/lang/nl/statistics.php @@ -0,0 +1,34 @@ + 'Statistics', + + 'preview_text' => 'This is a preview of the statistics page available in Lychee SE.
The data shown here are randomly generated and do not reflect your server.', + 'no_data' => 'User does not have data on server.', + 'collapse' => 'Collapse albums sizes', + + 'total' => [ + 'total' => 'Total', + 'albums' => 'Albums', + 'photos' => 'Photos', + 'size' => 'Size', + ], + 'table' => [ + 'username' => 'Owner', + 'title' => 'Title', + 'photos' => 'Photos', + 'descendants' => 'Children', + 'size' => 'Size', + ], +]; \ No newline at end of file diff --git a/lang/nl/toasts.php b/lang/nl/toasts.php new file mode 100644 index 00000000000..293d4b72594 --- /dev/null +++ b/lang/nl/toasts.php @@ -0,0 +1,17 @@ + 'Error', + 'success' => 'Success', +]; \ No newline at end of file diff --git a/lang/nl/users.php b/lang/nl/users.php new file mode 100644 index 00000000000..599bb833454 --- /dev/null +++ b/lang/nl/users.php @@ -0,0 +1,44 @@ + 'Users', + 'description' => 'Here you can manage the users of your Lychee installation. You can create, edit and delete users.', + 'create' => 'Create a new user', + 'username' => 'Username', + 'password' => 'Password', + 'legend' => 'Legend', + 'upload_rights' => 'When selected, the user can upload content.', + 'edit_rights' => 'When selected, the user can modify their profile (username, password).', + 'quota' => 'When set, the user has a space quota for pictures (in kB).', + + 'user_deleted' => 'User deleted', + 'user_created' => 'User created', + 'user_updated' => 'User updated', + 'change_saved' => 'Change saved!', + + 'create_edit' => [ + 'upload_rights' => 'User can upload content.', + 'edit_rights' => 'User can modify their profile (username, password).', + 'quota' => 'User has quota limit.', + 'quota_kb' => 'quota in kB (0 for default)', + 'note' => 'Admin note (not publically visible)', + 'create' => 'Create', + 'edit' => 'Edit', + ], + 'line' => [ + 'admin' => 'admin user', + 'edit' => 'Edit', + 'delete' => 'Delete', + ], +]; \ No newline at end of file diff --git a/lang/no/aspect_ratio.php b/lang/no/aspect_ratio.php new file mode 100644 index 00000000000..2c7e8fb56ac --- /dev/null +++ b/lang/no/aspect_ratio.php @@ -0,0 +1,21 @@ + '5/4 (instagram landscape)', + '4by5' => '4/5 (instagram portrait)', + '2by3' => '2/3 (portrait)', + '3by2' => '3/2 (landscape)', + '1by1' => 'square', + '1byx9' => '16/9 (landscape)', +]; \ No newline at end of file diff --git a/lang/no/diagnostics.php b/lang/no/diagnostics.php new file mode 100644 index 00000000000..0fadd640428 --- /dev/null +++ b/lang/no/diagnostics.php @@ -0,0 +1,30 @@ + 'Diagnostics', + + 'copy_to_clipboard' => 'Copy diagnostics to clipboard', + 'self-diagnosis' => 'Self-diagnosis', + 'info' => 'Info', + 'space' => 'Space', + 'load_space' => 'Load space usage.', + 'configuration' => 'Configuration', + 'loading' => 'Loading...', + 'identical_content' => 'Identical content', + + 'toast' => [ + 'info' => 'Info', + 'copy' => 'Diagnostics copied to clipboard!', + ], +]; \ No newline at end of file diff --git a/lang/no/dialogs.php b/lang/no/dialogs.php new file mode 100644 index 00000000000..4afd65fae3f --- /dev/null +++ b/lang/no/dialogs.php @@ -0,0 +1,221 @@ + [ + 'close' => 'Close', + 'cancel' => 'Cancel', + 'save' => 'Save', + 'delete' => 'Delete', + 'move' => 'Move', + ], + 'about' => [ + 'subtitle' => 'Self-hosted photo-management done right', + 'description' => 'Lychee is a free photo-management tool, which runs on your server or web-space. Installing is a matter of seconds. Upload, manage and share photos like from a native application. Lychee comes with everything you need and all your photos are stored securely.', + 'update_available' => 'Update available!', + 'thank_you' => 'Thank you for your support!', + 'get_supporter_or_register' => 'Get exclusive features and support the development of Lychee.
Unlock the Supporter Edition or register your License key', + 'here' => 'here', + ], + 'dropbox' => [ + 'not_configured' => 'Dropbox is not configured.', + ], + 'import_from_link' => [ + 'instructions' => 'Please enter the direct link to a photo to import it:', + 'import' => 'Import', + ], + 'keybindings' => [ + 'don_t_show_again' => 'Don\'t show this again', + 'side_wide' => 'Site-wide Shortcuts', + 'back_cancel' => 'Back/Cancel', + 'confirm' => 'Confirm', + 'login' => 'Login', + 'toggle_full_screen' => 'Toggle Full Screen', + 'toggle_sensitive_albums' => 'Toggle Sensitive Albums', + + 'albums' => 'Albums Shortcuts', + 'new_album' => 'New Album', + 'upload_photos' => 'Upload Photos', + 'search' => 'Search', + 'show_this_modal' => 'Show this modal', + 'select_all' => 'Select All', + 'move_selection' => 'Move Selection', + 'delete_selection' => 'Delete Selection', + + 'album' => 'Album Shortcuts', + 'slideshow' => 'Start/Stop Slideshow', + 'toggle' => 'Toggle panel', + + 'photo' => 'Photo Shortcuts', + 'previous' => 'Previous photo', + 'next' => 'Next photo', + 'cycle' => 'Cycle overlay mode', + 'star' => 'Star the photo', + 'move' => 'Move the photo', + 'delete' => 'Delete the photo', + 'edit' => 'Edit information', + 'show_hide_meta' => 'Show information', + + 'keep_hidden' => 'We will keep it hidden.', + ], + 'login' => [ + 'username' => 'Username', + 'password' => 'Password', + 'unknown_invalid' => 'Unknown user or invalid password.', + 'signin' => 'Sign-In', + ], + 'register' => [ + 'enter_license' => 'Enter your license key below:', + 'license_key' => 'License key', + 'invalid_license' => 'Invalid license key.', + 'register' => 'Register', + ], + 'share_album' => [ + 'url_copied' => 'Copied URL to clipboard!', + ], + 'upload' => [ + 'completed' => 'Completed', + 'uploaded' => 'Uploaded:', + 'release' => 'Release file to upload!', + 'select' => 'Click here to select files to upload', + 'drag' => '(Or drag files to the page)', + 'loading' => 'Loading', + 'resume' => 'Resume', + 'uploading' => 'Uploading', + 'finished' => 'Finished', + 'failed_error' => 'Upload failed. The server returned an error!', + ], + 'visibility' => [ + 'public' => 'Public', + 'public_expl' => 'Anonymous users can access this album, subject to the restrictions below.', + 'full' => 'Original', + 'full_expl' => 'Anonymous users can view full-resolution photos.', + 'hidden' => 'Hidden', + 'hidden_expl' => 'Anonymous users need a direct link to access this album.', + 'downloadable' => 'Downloadable', + 'downloadable_expl' => 'Anonymous users can download this album.', + 'password' => 'Password', + 'password_prot' => 'Password protected', + 'password_prot_expl' => 'Anonymous users need a shared password to access this album.', + 'nsfw' => 'Sensitive', + 'nsfw_expl' => 'Album contains sensitive content.', + 'visibility_updated' => 'Visibility updated.', + ], + 'move_album' => [ + 'confirm_single' => 'Are you sure you want to move the album “%1$s” into the album “%2$s”?', + 'confirm_multiple' => 'Are you sure you want to move all selected albums into the album “%s”?', + 'move_single' => 'Move Album', + 'move_to' => 'Move to', + 'move_to_single' => 'Move %s to:', + 'move_to_multiple' => 'Move %d albums to:', + 'no_album_target' => 'No album to move to', + 'moved_single' => 'Album moved!', + 'moved_single_details' => '%1$s moved to %2$s', + 'moved_details' => 'Album(s) moved to %s', + ], + 'new_album' => [ + 'menu' => 'Create Album', + 'info' => 'Enter a title for the new album:', + 'title' => 'title', + 'create' => 'Create Album', + ], + 'new_tag_album' => [ + 'menu' => 'Create Tag Album', + 'info' => 'Enter a title for the new tag album:', + 'title' => 'title', + 'set_tags' => 'Set tags to show', + 'warn' => 'Make sure to press enter after each tag', + 'create' => 'Create Tag Album', + ], + 'delete_album' => [ + 'confirmation' => 'Are you sure you want to delete the album “%s” and all of the photos it contains?', + 'confirmation_multiple' => 'Are you sure you want to delete all %d selected albums and all of the photos they contain?', + 'warning' => 'This action can not be undone!', + 'delete' => 'Delete Album and Photos', + ], + 'transfer' => [ + 'query' => 'Transfer ownership of album to', + 'confirmation' => 'Are you sure you want to transfer the ownership of album “%s” and all the photos it contains to "%s"?', + 'lost_access_warning' => 'Your access to this album will be lost.', + 'warning' => 'This action can not be undone!', + 'transfer' => 'Transfer ownership of album and photos', + ], + 'rename' => [ + 'photo' => 'Enter a new title for this photo:', + 'album' => 'Enter a new title for this album:', + 'rename' => 'Rename', + ], + 'merge' => [ + 'merge_to' => 'Merge %s to:', + 'merge_to_multiple' => 'Merge %d albums to:', + 'no_albums' => 'No albums to merge to.', + 'confirm' => 'Are you sure you want to merge the album “%1$s” into the album “%2$s”?', + 'confirm_multiple' => 'Are you sure you want to merge all selected albums into the album “%s”?', + 'merge' => 'Merge Albums', + 'merged' => 'Album(s) merged to %s!', + ], + 'unlock' => [ + 'password_required' => 'This album is protected by a password. Enter the password below to view the photos of this album:', + 'password' => 'Password', + 'unlock' => 'Unlock', + ], + 'photo_tags' => [ + 'question' => 'Enter your tags for this photo.', + 'question_multiple' => 'Enter your tags for all %d selected photos. Existing tags will be overwritten.', + 'no_tags' => 'No Tags', + 'set_tags' => 'Set Tags', + 'updated' => 'Tags updated!', + 'tags_override_info' => 'If this is unchecked, the tags will be added to the existing tags of the photo.', + ], + 'photo_copy' => [ + 'no_albums' => 'No albums to copy to', + 'copy_to' => 'Copy %s to:', + 'copy_to_multiple' => 'Copy %d photos to:', + 'confirm' => 'Copy %s to %s.', + 'confirm_multiple' => 'Copy %d photos to %s.', + 'copy' => 'Copy', + 'copied' => 'Photo(s) copied!', + ], + 'photo_delete' => [ + 'confirm' => 'Are you sure you want to delete the photo “%s”?', + 'confirm_multiple' => 'Are you sure you want to delete all %d selected photos?', + 'deleted' => 'Photo(s) deleted!', + ], + 'move_photo' => [ + 'move_single' => 'Move %s to:', + 'move_multiple' => 'Move %d photos to:', + 'confirm' => 'Move %s to %s.', + 'confirm_multiple' => 'Move %d photos to %s.', + 'moved' => 'Photo(s) moved to %s!', + ], + 'target_user' => [ + 'placeholder' => 'Select user', + ], + 'target_album' => [ + 'placeholder' => 'Select album', + ], + 'webauthn' => [ + 'u2f' => 'U2F', + 'success' => 'Authentication successful!', + 'error' => 'Whoops, it looks like something went wrong. Please reload the site and try again!', + ], + 'se' => [ + 'available' => 'Available in the Supporter Edition', + ], + 'session_expired' => [ + 'title' => 'Session expired', + 'message' => 'Your session has expired.
Please reload the page.', + 'reload' => 'Reload', + 'go_to_gallery' => 'Go to the Gallery', + ], +]; \ No newline at end of file diff --git a/lang/no/fix-tree.php b/lang/no/fix-tree.php new file mode 100644 index 00000000000..64803e310e6 --- /dev/null +++ b/lang/no/fix-tree.php @@ -0,0 +1,55 @@ + 'Maintenance', + 'intro' => 'This page allows you to re-order and fix your albums manually.
Before any modifications, we strongly recommend you to read about Nested Set tree structures.', + 'warning' => 'You can really break your Lychee installation here, modify values at your own risks.', + + 'help' => [ + 'header' => 'Help', + 'hover' => 'Hover ids or titles to highlight related albums.', + 'left' => 'Left', + 'right' => 'Right', + 'convenience' => 'For your convenience, the and buttons allow you to change the values of %s and %s by respectively +1 and -1 with propagation.', + 'left-right-warn' => 'The and indicates that the value of %s (and respectively %s) is duplicated somewhere.', + 'parent-marked' => 'Marked Parent Id indicates that the %s and %s do not satisfy the Nest Set tree structures. Edit either the Parent Id or the %s/%s values.', + 'slowness' => 'This page will be slow with a large number of albums.', + ], + + 'buttons' => [ + 'reset' => 'Reset', + 'check' => 'Check', + 'apply' => 'Apply', + ], + + 'table' => [ + 'title' => 'Title', + 'left' => 'Left', + 'right' => 'Right', + 'id' => 'Id', + 'parent' => 'Parent Id', + ], + + 'errors' => [ + 'invalid' => 'Invalid tree!', + 'invalid_details' => 'We are not applying this as it is guaranteed to be a broken state.', + 'invalid_left' => 'Album %s has an invalid left value.', + 'invalid_right' => 'Album %s has an invalid right value.', + 'invalid_left_right' => 'Album %s has an invalid left/right values. Left should be strictly smaller than right: %s < %s.', + 'duplicate_left' => 'Album %s has a duplicate left value %s.', + 'duplicate_right' => 'Album %s has a duplicate right value %s.', + 'parent' => 'Album %s has an unexpected parent id %s.', + 'unknown' => 'Album %s has an unknown error.', + ], +]; \ No newline at end of file diff --git a/lang/no/gallery.php b/lang/no/gallery.php new file mode 100644 index 00000000000..eb8008827e0 --- /dev/null +++ b/lang/no/gallery.php @@ -0,0 +1,241 @@ + 'Gallery', + + 'smart_albums' => 'Smart albums', + 'albums' => 'Albums', + 'root' => 'Albums', + + 'original' => 'Original', + 'medium' => 'Medium', + 'medium_hidpi' => 'Medium HiDPI', + 'small' => 'Thumb', + 'small_hidpi' => 'Thumb HiDPI', + 'thumb' => 'Square thumb', + 'thumb_hidpi' => 'Square thumb HiDPI', + 'placeholder' => 'Low Quality Image Placeholder', + 'thumbnail' => 'Photo thumbnail', + 'live_video' => 'Video part of live-photo', + + 'camera_data' => 'Camera date', + 'album_reserved' => 'All Rights Reserved', + + 'map' => [ + 'error_gpx' => 'Error loading GPX file', + 'osm_contributors' => 'OpenStreetMap contributors', + ], + + 'search' => [ + 'title' => 'Search', + 'searching' => 'Searching…', + 'no_results' => 'Nothing matches your search query.', + 'searchbox' => 'Search…', + 'minimum_chars' => 'Minimum %s characters required.', + 'photos' => 'Photos (%s)', + 'albums' => 'Albums (%s)', + ], + + 'smart_album' => [ + 'unsorted' => 'Unsorted', + 'starred' => 'Starred', + 'recent' => 'Recent', + 'public' => 'Public', + 'on_this_day' => 'On This Day', + ], + + 'layout' => [ + 'squares' => 'Square thumbnails', + 'justified' => 'With aspect, justified', + 'masonry' => 'With aspect, masonry', + 'grid' => 'With aspect, grid', + ], + + 'overlay' => [ + 'none' => 'None', + 'exif' => 'EXIF data', + 'description' => 'Description', + 'date' => 'Date taken', + ], + + 'timeline' => [ + 'default' => 'default', + 'disabled' => 'disabled', + 'year' => 'Year', + 'month' => 'Month', + 'day' => 'Day', + 'hour' => 'Hour', + ], + + 'album' => [ + 'header_albums' => 'Albums', + 'header_photos' => 'Photos', + 'no_results' => 'Nothing to see here', + 'upload' => 'Upload photos', + + 'tabs' => [ + 'about' => 'About Album', + 'share' => 'Share Album', + 'move' => 'Move Album', + 'danger' => 'DANGER ZONE', + ], + + 'hero' => [ + 'created' => 'Created', + 'copyright' => 'Copyright', + 'subalbums' => 'Subalbums', + 'images' => 'Photos', + 'download' => 'Download Album', + 'share' => 'Share Album', + 'stats_only_se' => 'Statistics available in the Supporter Edition', + ], + + 'stats' => [ + 'lens' => 'Lens', + 'shutter' => 'Shutter speed', + 'iso' => 'ISO', + 'model' => 'Model', + 'aperture' => 'Aperture', + 'no_data' => 'No data', + ], + + 'properties' => [ + 'title' => 'Title', + 'description' => 'Description', + 'photo_ordering' => 'Order photos by', + 'children_ordering' => 'Order albums by', + 'asc/desc' => 'asc/desc', + 'header' => 'Set album header', + 'compact_header' => 'Use compact header', + 'license' => 'Set license', + 'copyright' => 'Set copyright', + 'aspect_ratio' => 'Set album thumbs aspect ratio', + 'album_timeline' => 'Set album timeline mode', + 'photo_timeline' => 'Set photo timeline mode', + 'layout' => 'Set photo layout', + 'show_tags' => 'Set tags to show', + 'tags_required' => 'Tags are required.', + ], + ], + + 'photo' => [ + 'actions' => [ + 'star' => 'Star', + 'unstar' => 'Unstar', + 'set_album_header' => 'Set as album header', + 'move' => 'Move', + 'delete' => 'Delete', + 'header_set' => 'Header set', + ], + + 'details' => [ + 'about' => 'About', + 'basics' => 'Basics', + 'title' => 'Title', + 'uploaded' => 'Uploaded', + 'description' => 'Description', + 'license' => 'License', + 'reuse' => 'Reuse', + 'latitude' => 'Latitude', + 'longitude' => 'Longitude', + 'altitude' => 'Altitude', + 'location' => 'Location', + 'image' => 'Image', + 'video' => 'Video', + 'size' => 'Size', + 'format' => 'Format', + 'resolution' => 'Resolution', + 'duration' => 'Duration', + 'fps' => 'Frame rate', + 'tags' => 'Tags', + 'camera' => 'Camera', + 'captured' => 'Captured', + 'make' => 'Make', + 'type' => 'Type/Model', + 'lens' => 'Lens', + 'shutter' => 'Shutter Speed', + 'aperture' => 'Aperture', + 'focal' => 'Focal Length', + 'iso' => 'ISO %s', + ], + + 'edit' => [ + 'set_title' => 'Set Title', + 'set_description' => 'Set Description', + 'set_license' => 'Set License', + 'no_tags' => 'No Tags', + 'set_tags' => 'Set Tags', + 'set_created_at' => 'Set Upload Date', + ], + ], + + 'nsfw' => [ + 'header' => 'Sensitive content', + 'description' => 'This album contains sensitive content which some people may find offensive or disturbing.', + 'consent' => 'Tap to consent.', + ], + + 'menus' => [ + 'star' => 'Star', + 'unstar' => 'Unstar', + 'star_all' => 'Star Selected', + 'unstar_all' => 'Unstar Selected', + 'tag' => 'Tag', + 'tag_all' => 'Tag Selected', + 'set_cover' => 'Set Album Cover', + 'remove_header' => 'Remove Album Header', + 'set_header' => 'Set Album Header', + 'copy_to' => 'Copy to …', + 'copy_all_to' => 'Copy Selected to …', + 'rename' => 'Rename', + 'move' => 'Move', + 'move_all' => 'Move Selected', + 'delete' => 'Delete', + 'delete_all' => 'Delete Selected', + 'download' => 'Download', + 'download_all' => 'Download Selected', + 'merge' => 'Merge', + 'merge_all' => 'Merge Selected', + + 'upload_photo' => 'Upload Photo', + 'import_link' => 'Import from Link', + 'import_dropbox' => 'Import from Dropbox', + 'new_album' => 'New Album', + 'new_tag_album' => 'New Tag Album', + 'upload_track' => 'Upload track', + 'delete_track' => 'Delete track', + ], + + 'sort' => [ + 'photo_select_1' => 'Upload Time', + 'photo_select_2' => 'Take Date', + 'photo_select_3' => 'Title', + 'photo_select_4' => 'Description', + 'photo_select_6' => 'Star', + 'photo_select_7' => 'Photo Format', + 'ascending' => 'Ascending', + 'descending' => 'Descending', + 'album_select_1' => 'Creation Time', + 'album_select_2' => 'Title', + 'album_select_3' => 'Description', + 'album_select_5' => 'Latest Take Date', + 'album_select_6' => 'Oldest Take Date', + ], + + 'albums_protection' => [ + 'private' => 'private', + 'public' => 'public', + 'inherit_from_parent' => 'inherit from parent', + ], +]; \ No newline at end of file diff --git a/lang/no/jobs.php b/lang/no/jobs.php new file mode 100644 index 00000000000..5d952b76012 --- /dev/null +++ b/lang/no/jobs.php @@ -0,0 +1,18 @@ + 'Jobs', + + 'no_data' => 'No Jobs have been executed yet.', +]; \ No newline at end of file diff --git a/lang/no/landing.php b/lang/no/landing.php new file mode 100644 index 00000000000..fe6fe55b8ea --- /dev/null +++ b/lang/no/landing.php @@ -0,0 +1,19 @@ + 'Gallery', + 'access_gallery' => 'Access the gallery', + 'hosted_with_lychee' => 'Hosted with Lychee', + 'copyright' => 'All images on this website are subject to copyright by %1$s © %2$s', +]; \ No newline at end of file diff --git a/lang/no/left-menu.php b/lang/no/left-menu.php new file mode 100644 index 00000000000..9a3e91f4037 --- /dev/null +++ b/lang/no/left-menu.php @@ -0,0 +1,29 @@ + 'Back to Gallery', + + 'admin' => 'Admin', + 'clockwork' => 'Clockwork App', + 'logs' => 'Show Logs', + 'jobs' => 'Show Job History', + 'user' => 'User', + + 'sign_out' => 'Sign Out', + + 'about' => 'About', + 'api' => 'API Documentation', + 'source_code' => 'Source Code', + 'support' => 'Support', +]; \ No newline at end of file diff --git a/lang/no/lychee.php b/lang/no/lychee.php new file mode 100644 index 00000000000..085c45f31bb --- /dev/null +++ b/lang/no/lychee.php @@ -0,0 +1,535 @@ + 'Brukernavn', + 'PASSWORD' => 'Passord', + 'ENTER' => 'Stig inn', + 'CANCEL' => 'Avbryt', + 'CONFIRM' => 'Confirm', + 'SIGN_IN' => 'Logg inn', + 'CLOSE' => 'Lukk', + 'SETTINGS' => 'Innstillinger', + 'SEARCH' => 'Søk …', + 'MORE' => 'Mer', + 'DEFAULT' => 'Default', + 'GALLERY' => 'Gallery', + + 'USERS' => 'Brukere', + 'PROFILE' => 'Profile', + 'CREATE' => 'Create', + 'REMOVE' => 'Remove', + 'SHARE' => 'Share', + 'U2F' => 'U2F', + 'NOTIFICATIONS' => 'Notifications', + 'SHARING' => 'Deling', + 'CHANGE_LOGIN' => 'Endre Bruker', + 'CHANGE_SORTING' => 'Endre sortering', + 'SET_DROPBOX' => 'Lagre Dropbox', + 'ABOUT_LYCHEE' => 'Om Lychee', + 'DIAGNOSTICS' => 'Diagnostikk', + 'DIAGNOSTICS_GET_SIZE' => 'Hent diskbruk', + 'JOBS' => 'Show job history', + 'LOGS' => 'Vis Logg', + 'SIGN_OUT' => 'Logg Ut', + 'UPDATE_AVAILABLE' => 'Oppdatering er tilgjengelig!', + 'MIGRATION_AVAILABLE' => 'Migrering er tilgjengelig!', + 'CHECK_FOR_UPDATE' => 'Check for updates', + 'DEFAULT_LICENSE' => 'Standard lisens for nye opplastinger:', + 'SET_LICENSE' => 'Lagre Lisens', + 'SET_OVERLAY_TYPE' => 'Lagre overvisning', + 'SET_ALBUM_DECORATION' => 'Set album decorations', + 'SET_MAP_PROVIDER' => 'Lagre leverandør for OpenStreetMap fliser', + 'FULL_SETTINGS' => 'Full Settings', + 'UPDATE' => 'Update', + 'RESET' => 'Reset', + 'DISABLE_TOKEN_TOOLTIP' => 'Disable', + 'ENABLE_TOKEN' => 'Enable API token', + 'DISABLED_TOKEN_STATUS_MSG' => 'Disabled', + 'TOKEN_BUTTON' => 'API Token ...', + 'TOKEN_NOT_AVAILABLE' => 'You have already viewed this token.', + 'TOKEN_WAIT' => 'Wait ...', + + 'SMART_ALBUMS' => 'Automatiske album', + 'SHARED_ALBUMS' => 'Delte album', + 'ALBUMS' => 'Album', + 'PHOTOS' => 'Bilder', + 'SEARCH_RESULTS' => 'Søkeresultater', + + 'RENAME' => 'Gi nytt navn', + 'RENAME_ALL' => 'Gi nytt navn til Valgte', + 'MERGE' => 'Slå sammen', + 'MERGE_ALL' => 'Slå sammen Valgte', + 'MAKE_PUBLIC' => 'Gjør Offentlig', + 'SHARE_ALBUM' => 'Del Albumet', + 'SHARE_PHOTO' => 'Del Bilde', + 'VISIBILITY_ALBUM' => 'Albumsynlighet', + 'VISIBILITY_PHOTO' => 'Bildesynlighet', + 'DOWNLOAD_ALBUM' => 'Last ned Albumet', + 'ABOUT_ALBUM' => 'Om Albumet', + 'DELETE_ALBUM' => 'Fjern Albumet', + 'MOVE_ALBUM' => 'Flytt Albumet', + 'FULLSCREEN_ENTER' => 'Gå i Fullskjermvisning', + 'FULLSCREEN_EXIT' => 'Slutt Fullskjermvisning', + + 'SHARING_ALBUM_USERS' => 'Share this album with users', + 'WAIT_FETCH_DATA' => 'Please wait while we get the data …', + 'SHARING_ALBUM_USERS_NO_USERS' => 'There are no users to share the album with', + 'SHARING_ALBUM_USERS_LONG_MESSAGE' => 'Select the users to share this album with', + + 'DELETE_ALBUM_QUESTION' => 'Fjern Album og Bilder', + 'KEEP_ALBUM' => 'Behold Album', + 'DELETE_ALBUM_CONFIRMATION' => 'Ønsker du virkelig å fjerne album «%s» og alle bildene i det? Denne handlingen kan ikke angres!', + + 'DELETE_TAG_ALBUM_QUESTION' => 'Delete Album', + 'DELETE_TAG_ALBUM_CONFIRMATION' => 'Are you sure you want to delete the album «%s» (any photos inside will not be deleted)? This action can’t be undone!', + + 'DELETE_ALBUMS_QUESTION' => 'Fjern Album og Bilder', + 'KEEP_ALBUMS' => 'Behold Album', + 'DELETE_ALBUMS_CONFIRMATION' => 'Ønsker du virkelig å fjerne %d valgte album og alle bildene i disse? Denne handlingen kan ikke angres!', + + 'DELETE_UNSORTED_CONFIRM' => 'Ønsker du virkelig å fjerne alle bilder fra «Usorterte»? Denne handlingen kan ikke angres!', + 'CLEAR_UNSORTED' => 'Fjern Usorterte', + 'KEEP_UNSORTED' => 'Behold Usorterte', + + 'EDIT_SHARING' => 'Endre Deling', + 'MAKE_PRIVATE' => 'Gjør Privat', + + 'CLOSE_ALBUM' => 'Lukk Album', + 'CLOSE_PHOTO' => 'Lukk Bilde', + 'CLOSE_MAP' => 'Lukk Kart', + + 'ADD' => 'Legg til', + 'MOVE' => 'Flytt', + 'MOVE_ALL' => 'Flytt Valgte', + 'DUPLICATE' => 'Dupliser', + 'DUPLICATE_ALL' => 'Dupliser Valgte', + 'COPY_TO' => 'Kopier til …', + 'COPY_ALL_TO' => 'Kopier Valgte til …', + 'DELETE' => 'Fjern', + 'SAVE' => 'Save', + 'DELETE_ALL' => 'Fjern Valgte', + 'DOWNLOAD' => 'Last ned', + 'DOWNLOAD_ALL' => 'Last ned Valgte', + 'UPLOAD_PHOTO' => 'Last opp Bilde', + 'IMPORT_LINK' => 'Importer fra Lenke', + 'IMPORT_DROPBOX' => 'Importer fra Dropbox', + 'IMPORT_SERVER' => 'Importer fra Serveren', + 'NEW_ALBUM' => 'Nytt Album', + 'NEW_TAG_ALBUM' => 'New Tag Album', + 'UPLOAD_TRACK' => 'Upload track', + 'DELETE_TRACK' => 'Delete track', + + 'TITLE_NEW_ALBUM' => 'Legg inn en tittel for det nye albumet:', + 'UNTITLED' => 'Uten Tittel', + 'UNSORTED' => 'Usorterte', + 'STARRED' => 'Favoritter', + 'RECENT' => 'Nylige', + 'PUBLIC' => 'Offentlige', + 'ON_THIS_DAY' => 'On This Day', + 'NUM_PHOTOS' => 'Bilder', + + 'CREATE_ALBUM' => 'Lag Album', + 'CREATE_TAG_ALBUM' => 'Create Tag Album', + + 'STAR_PHOTO' => 'Stjernemerk Bilde', + 'STAR' => 'Stjernemerk', + 'UNSTAR' => 'Unstar', + 'STAR_ALL' => 'Stjernemerk Valgte', + 'UNSTAR_ALL' => 'Unstar Selected', + 'TAG' => 'Tagg', + 'TAG_ALL' => 'Tagg Valgte', + 'UNSTAR_PHOTO' => 'Fjern Stjernemerke', + 'SET_COVER' => 'Set Album Cover', + 'REMOVE_COVER' => 'Remove Album Cover', + 'SET_HEADER' => 'Set Album Header', + 'REMOVE_HEADER' => 'Remove Album Header', + 'SET_COMPACT_HEADER' => 'Use Compact Header', + + 'FULL_PHOTO' => 'Originalbildet', + 'ABOUT_PHOTO' => 'Om Bildet', + 'DISPLAY_FULL_MAP' => 'Kart', + 'DIRECT_LINK' => 'Direktelenke', + 'DIRECT_LINKS' => 'Direktelenker', + 'QR_CODE' => 'QR Code', + + 'ALBUM_ABOUT' => 'Om', + 'ALBUM_BASICS' => 'Grunnleggende', + 'ALBUM_TITLE' => 'Tittel', + 'ALBUM_COPYRIGHT' => 'Copyright', + 'ALBUM_SET_COPYRIGHT' => 'Set copyright', + 'ALBUM_NEW_TITLE' => 'Legg inn en ny tittel for Albumet:', + 'ALBUMS_NEW_TITLE' => 'Legg inn en ny tittel for %d valgte album:', + 'ALBUM_SET_TITLE' => 'Lagre Tittel', + 'ALBUM_DESCRIPTION' => 'Beskrivelse', + 'ALBUM_SHOW_TAGS' => 'Tags to show', + 'ALBUM_NEW_DESCRIPTION' => 'Legg inn en ny beskrivelse for Albumet:', + 'ALBUM_SET_DESCRIPTION' => 'Lagre Beskrivelsen', + 'ALBUM_NEW_SHOWTAGS' => 'Enter tags of photos that will be visible in this album:', + 'ALBUM_SET_SHOWTAGS' => 'Set tags to show', + 'ALBUM_ALBUM' => 'Album', + 'ALBUM_CREATED' => 'Laget', + 'ALBUM_IMAGES' => 'Bilder', + 'ALBUM_VIDEOS' => 'Filmer', + 'ALBUM_SUBALBUMS' => 'Underalbum', + 'ALBUM_SHARING' => 'Deling', + 'ALBUM_SHR_YES' => 'JA', + 'ALBUM_SHR_NO' => 'Nei', + 'ALBUM_PUBLIC' => 'Offentlig', + 'ALBUM_PUBLIC_EXPL' => 'Anonymous users can access this album, subject to the restrictions below.', + 'ALBUM_FULL' => 'Original', + 'ALBUM_FULL_EXPL' => 'Anonymous users can behold full-resolution photos.', + 'ALBUM_HIDDEN' => 'Skjult', + 'ALBUM_HIDDEN_EXPL' => 'Anonymous users need a direct link to access this album.', + 'ALBUM_MARK_NSFW' => 'Mark album as sensitive', + 'ALBUM_UNMARK_NSFW' => 'Unmark album as sensitive', + 'ALBUM_NSFW' => 'Sensitive', + 'ALBUM_NSFW_EXPL' => 'Album is marked to contain sensitive content.', + 'ALBUM_DOWNLOADABLE' => 'Nedlastbar', + 'ALBUM_DOWNLOADABLE_EXPL' => 'Anonymous users can download this album.', + 'ALBUM_SHARE_BUTTON_VISIBLE' => 'Delingsknappen er synlig', + 'ALBUM_SHARE_BUTTON_VISIBLE_EXPL' => 'Anonymous users can see social media sharing links.', + 'ALBUM_PASSWORD' => 'Passord', + 'ALBUM_PASSWORD_PROT' => 'Passordbeskyttet', + 'ALBUM_PASSWORD_PROT_EXPL' => 'Anonymous users need a shared password to access this album.', + 'ALBUM_PASSWORD_REQUIRED' => 'Albumet er beskyttet med et passord. Fyll inn passordet under for å se bildene i Albumet:', + 'ALBUM_MERGE' => 'Ønsker du virkelig slå sammen album «%1$s» med album «%2$s»?', + 'ALBUMS_MERGE' => 'Ønsker du virkelig å slå sammen alle valge album til albumet «%s»?', + 'MERGE_ALBUM' => 'Slå sammen Album', + 'DONT_MERGE' => 'Ikke slå sammen', + 'ALBUM_MOVE' => 'Ønsker du virkelig å flytte album «%1$s» inn i album «%2$s»?', + 'ALBUMS_MOVE' => 'Ønsker du virkelig å flytte alle valge album inn i album «%2$s»?', + 'MOVE_ALBUMS' => 'Flytt Album', + 'NOT_MOVE_ALBUMS' => 'Ikke flytt', + 'ROOT' => 'Album', + 'ALBUM_REUSE' => 'Bruk om igjen', + 'ALBUM_LICENSE' => 'Lisens', + 'ALBUM_SET_LICENSE' => 'Lagre Lisens', + 'ALBUM_LICENSE_HELP' => 'Trenger du hjelp for å velge?', + 'ALBUM_LICENSE_NONE' => 'Ingen', + 'ALBUM_RESERVED' => 'Alle Rettigheter Forbeholdt', + 'ALBUM_SET_ORDER' => 'Set Order', + 'ALBUM_ORDERING' => 'Order by', + 'ALBUM_PHOTO_ORDERING' => 'Order photos by', + 'ALBUM_CHILDREN_ORDERING' => 'Order albums by', + 'ALBUM_OWNER' => 'Owner', + + 'PHOTO_ABOUT' => 'Om', + 'PHOTO_BASICS' => 'Grunnleggende', + 'PHOTO_TITLE' => 'Tittel', + 'PHOTO_NEW_TITLE' => 'Fyll inn en ny tittel for bildet:', + 'PHOTO_SET_TITLE' => 'Lagre Tittelen', + 'PHOTO_UPLOADED' => 'Opplastet', + 'PHOTO_DESCRIPTION' => 'Beskrivelse', + 'PHOTO_NEW_DESCRIPTION' => 'Fyll inn en ny beskrivelse for dette bildet:', + 'PHOTO_SET_DESCRIPTION' => 'Lagre Beskrivelsen', + 'PHOTO_NEW_LICENSE' => 'Legg til en Lisens', + 'PHOTO_SET_LICENSE' => 'Lagre Lisens', + 'PHOTO_LICENSE' => 'Lisens', + 'PHOTO_LICENSE_HELP' => 'Need help choosing?', + 'PHOTO_REUSE' => 'Bruk om igjen', + 'PHOTO_LICENSE_NONE' => 'Ingen', + 'PHOTO_RESERVED' => 'Alle Rettigheter Forbeholdt', + 'PHOTO_LATITUDE' => 'Breddegrad', + 'PHOTO_LONGITUDE' => 'Lengdegrad', + 'PHOTO_ALTITUDE' => 'Høyde', + 'PHOTO_IMGDIRECTION' => 'Retning', + 'PHOTO_LOCATION' => 'Sted', + 'PHOTO_IMAGE' => 'Bilde', + 'PHOTO_VIDEO' => 'Film', + 'PHOTO_SIZE' => 'Størrelse', + 'PHOTO_FORMAT' => 'Format', + 'PHOTO_RESOLUTION' => 'Oppløsning', + 'PHOTO_DURATION' => 'Lengde', + 'PHOTO_FPS' => 'Bilderate', + 'PHOTO_TAGS' => 'Tagger', + 'PHOTO_NOTAGS' => 'Ingen Tagger', + 'PHOTO_NEW_TAGS' => 'Fyll inn tagger for dette bildet. Du kan legge inn flere tagger ved å dele de med komma', + 'PHOTOS_NEW_TAGS' => 'Legg inn tagger for %d valgte bilder. Tagger vil bli overskrevet. Du kan legge inn flere tagger ved å dele de med komma', + 'PHOTO_SET_TAGS' => 'Lagre Tagger', + 'PHOTO_CAMERA' => 'Kamera', + 'PHOTO_CAPTURED' => 'Tatt', + 'PHOTO_MAKE' => 'Produsent', + 'PHOTO_TYPE' => 'Type/Modell', + 'PHOTO_LENS' => 'Linse', + 'PHOTO_SHUTTER' => 'Lukkertid', + 'PHOTO_APERTURE' => 'Blendertall', + 'PHOTO_FOCAL' => 'Brennvidde', + 'PHOTO_ISO' => 'ISO %s', + 'PHOTO_SHARING' => 'Deling', + 'PHOTO_DELETE' => 'Fjern Bilde', + 'PHOTO_KEEP' => 'Behold Bilde', + 'PHOTO_DELETE_CONFIRMATION' => 'Ønsker du virkelig å fjerne bilde «%s»? Denne handlingen kan ikke angres!', + 'PHOTO_DELETE_ALL' => 'Ønsker du virkelig å fjerne %d valgte bilder? Denne handlingen kan ikke angres!', + 'PHOTOS_NEW_TITLE' => 'Fyll inn en tittel for %d valgte bilder:', + 'PHOTO_MAKE_PRIVATE_ALBUM' => 'Bildet er i et offentlig album. Synligheten til bildet kan endres gjennom egenskapene for albumet.', + 'PHOTO_SHOW_ALBUM' => 'Vis Album', + 'PHOTO_PUBLIC' => 'Offentlig', + 'PHOTO_PUBLIC_EXPL' => 'Anonymous users can view this photo, subject to the restrictions below.', + 'PHOTO_FULL' => 'Original', + 'PHOTO_FULL_EXPL' => 'Anonymous users can behold full-resolution photo.', + 'PHOTO_HIDDEN' => 'Gjemt', + 'PHOTO_HIDDEN_EXPL' => 'Anonymous users need a direct link to view this photo.', + 'PHOTO_DOWNLOADABLE' => 'Kan lastes ned', + 'PHOTO_DOWNLOADABLE_EXPL' => 'Anonymous users may download this photo.', + 'PHOTO_SHARE_BUTTON_VISIBLE' => 'Delingsknappen er synlig', + 'PHOTO_SHARE_BUTTON_VISIBLE_EXPL' => 'Anonymous users can see social media sharing links.', + 'PHOTO_PASSWORD_PROT' => 'Passordbeskyttet', + 'PHOTO_PASSWORD_PROT_EXPL' => 'Anonymous users need a shared password to view this photo.', + 'PHOTO_EDIT_SHARING_TEXT' => 'Innstillingene for deling av bildet vil bli endret til:', + 'PHOTO_NO_EDIT_SHARING_TEXT' => 'Dette bildet er i et offentlig album som arver synligheten til albumet. Nåværende synlighet er vist bare for informasjon.', + 'PHOTO_EDIT_GLOBAL_SHARING_TEXT' => 'Synligeten til bildet kan bli finjustert gjennom innstillingene til Lychee. Nåværende synlighet er vist bare for informasjon.', + 'PHOTO_NEW_CREATED_AT' => 'Enter the upload date for this photo. mm/dd/yyyy, hh:mm [am/pm]', + 'PHOTO_SET_CREATED_AT' => 'Set upload date', + + 'LOADING' => 'Laster', + 'ERROR' => 'Feil', + 'ERROR_TEXT' => 'Oisann, her ser det ut som noe gikk galt. Vennligst last inn siden på nytt og prøv igjen!', + 'ERROR_UNKNOWN' => 'Noe uventet skjedde. Prøv på nytt og kontroller installasjonen av Lychee og serveren. Se readme for mer informasjon', + 'ERROR_MAP_DEACTIVATED' => 'Kartfunksjoner har blitt deaktivert under innstillinger', + 'ERROR_SEARCH_DEACTIVATED' => 'Søkefunksjoner har blitt deaktivert under innstillinger', + 'SUCCESS' => 'OK', + 'CHANGE_SUCCESS' => 'Change successful.', + 'RETRY' => 'Prøv igjen', + 'OVERRIDE' => 'Override', + 'TAGS_OVERRIDE_INFO' => 'If this is unchecked, the tags will be added to the existing tags of the photo.', + + 'SETTINGS_SUCCESS_LOGIN' => 'Innlogging oppdatert.', + 'SETTINGS_SUCCESS_SORT' => 'Sorteringsrekkefølge oppdatert.', + 'SETTINGS_SUCCESS_DROPBOX' => 'Dropboxnøkkel oppdatert.', + 'SETTINGS_SUCCESS_LANG' => 'Språk oppdatert', + 'SETTINGS_SUCCESS_LAYOUT' => 'Oppsett oppdatert', + 'SETTINGS_SUCCESS_IMAGE_OVERLAY' => 'Instilling for EXIF overvisning oppdatert', + 'SETTINGS_SUCCESS_PUBLIC_SEARCH' => 'Offentlig søk oppdatert', + 'SETTINGS_SUCCESS_LICENSE' => 'Standard lisens oppdatert', + 'SETTINGS_SUCCESS_MAP_DISPLAY' => 'Innstillinger for Kartvisning oppdatert', + 'SETTINGS_SUCCESS_MAP_DISPLAY_PUBLIC' => 'Innstillinger for Kartvisning for offentlige album oppdatert', + 'SETTINGS_SUCCESS_MAP_PROVIDER' => 'Innstillinger for kartleverandør oppdatert', + 'SETTINGS_SUCCESS_CSS' => 'Stylesheets updated', + 'SETTINGS_SUCCESS_JS' => 'JS updated', + 'SETTINGS_SUCCESS_UPDATE' => 'Settings updated successfully', + 'SETTINGS_DROPBOX_KEY' => 'Dropbox API Key', + 'SETTINGS_ADVANCED_WARNING_EXPL' => 'Changing these advanced settings can be harmful to the stability, security and performance of this application. You should only modify them if you are sure of what you are doing.', + 'SETTINGS_ADVANCED_SAVE' => 'Save my modifications, I accept the risk!', + + 'U2F_NOT_SUPPORTED' => 'U2F not supported. Sorry.', + 'U2F_NOT_SECURE' => 'Environment not secured. U2F not available.', + 'U2F_REGISTER_KEY' => 'Register new device.', + 'U2F_REGISTRATION_SUCCESS' => 'Registration successful!', + 'U2F_AUTHENTIFICATION_SUCCESS' => 'Authentication successful!', + 'U2F_CREDENTIALS' => 'Credentials', + 'U2F_CREDENTIALS_DELETED' => 'Credentials deleted!', + 'U2F_LOGIN' => 'Log in with WebAuthn', + + 'NEW_PHOTOS_NOTIFICATION' => 'Send new photos notification emails.', + 'SETTINGS_SUCCESS_NEW_PHOTOS_NOTIFICATION' => 'New photos notification updated', + 'USER_EMAIL_INSTRUCTION' => 'Add your email below to enable receiving email notifications. To stop receiving emails, simply remove your email below.', + + 'LOGIN_USERNAME' => 'Nytt Brukernavn', + 'LOGIN_PASSWORD' => 'Nytt Passord', + 'LOGIN_PASSWORD_CONFIRM' => 'Bekreft Passord', + 'PASSWORD_TITLE' => 'Fyll inn ditt nåværende passord:', + 'PASSWORD_CURRENT' => 'Nåværende Passord', + 'PASSWORD_TEXT' => 'Brukernavnet og passordet ditt vil bli endret til det følgende:', + 'PASSWORD_CHANGE' => 'Lagre brukernavn og passord', + + 'EDIT_SHARING_TITLE' => 'Endre Deling', + 'EDIT_SHARING_TEXT' => 'Egenskapene for deling for dette albumet vil bli endret til følgende:', + 'SHARE_ALBUM_TEXT' => 'Albumet vil bli delt med følgende egenskaper:', + + 'SORT_DIALOG_ATTRIBUTE_LABEL' => 'Attribute', + 'SORT_DIALOG_ORDER_LABEL' => 'Order', + + 'SORT_ALBUM_BY' => 'Sorter album etter %1$s i en %2$s rekkefølge.', + + 'SORT_ALBUM_SELECT_1' => 'Opprettelsestid', + 'SORT_ALBUM_SELECT_2' => 'Tittel', + 'SORT_ALBUM_SELECT_3' => 'Beskrivelse', + 'SORT_ALBUM_SELECT_5' => 'Seneste fangstdato', + 'SORT_ALBUM_SELECT_6' => 'Eldste fangstdato', + + 'SORT_PHOTO_BY' => 'Sorter bilder etter %1$s i en %2$s rekkefølge.', + + 'SORT_PHOTO_SELECT_1' => 'Opplastingstid', + 'SORT_PHOTO_SELECT_2' => 'Fangsdato', + 'SORT_PHOTO_SELECT_3' => 'Tittel', + 'SORT_PHOTO_SELECT_4' => 'Beskrivelse', + 'SORT_PHOTO_SELECT_6' => 'Stjernemerk', + 'SORT_PHOTO_SELECT_7' => 'Bildeformat', + + 'SORT_ASCENDING' => 'Stigende', + 'SORT_DESCENDING' => 'Fallende', + 'SORT_CHANGE' => 'Lagre Rekkefølge', + + 'DROPBOX_TITLE' => 'Lagre nøkkel for Dropbox', + 'DROPBOX_TEXT' => "For å importere bilder fra Dropbox trengs en gyldig applikasjonsnøkkel fra deres nettside. Lag en personlig nøkkel og fyll inn denne under:", + + 'LANG_TEXT' => 'Endre språk for Lychee til:', + 'LANG_TITLE' => 'Lagre innstilling for språk', + + 'SETTING_RECENT_PUBLIC_TEXT' => 'Make "Recent" smart album accessible to anonymous users', + 'SETTING_STARRED_PUBLIC_TEXT' => 'Make "Starred" smart album accessible to anonymous users', + 'SETTING_ONTHISDAY_PUBLIC_TEXT' => 'Make "On This Day" smart album accessible to anonymous users', + + 'CSS_TEXT' => 'Personalize CSS:', + 'CSS_TITLE' => 'Change CSS', + 'JS_TEXT' => 'Custom JS:', + 'JS_TITLE' => 'Change JS', + 'PUBLIC_SEARCH_TEXT' => 'Offentlig søk tillatt:', + 'OVERLAY_TYPE' => 'Data som skal brukes til overvisning:', + 'OVERLAY_NONE' => 'None', + 'OVERLAY_EXIF' => 'EXIF bildedata', + 'OVERLAY_DESCRIPTION' => 'Bildebeskrivelser', + 'OVERLAY_DATE' => 'Dato for når bildet ble tatt', + 'ALBUM_DECORATION' => 'Album decorations:', + 'ALBUM_DECORATION_NONE' => 'None', + 'ALBUM_DECORATION_ORIGINAL' => 'Sub-album marker', + 'ALBUM_DECORATION_ALBUM' => 'Number of sub-albums', + 'ALBUM_DECORATION_PHOTO' => 'Number of photos', + 'ALBUM_DECORATION_ALL' => 'Number of sub-albums and photos', + 'ALBUM_DECORATION_ORIENTATION' => 'Orientation of album decorations:', + 'ALBUM_DECORATION_ORIENTATION_ROW' => 'Horizontal (photos, albums)', + 'ALBUM_DECORATION_ORIENTATION_ROW_REVERSE' => 'Horizontal (albums, photos)', + 'ALBUM_DECORATION_ORIENTATION_COLUMN' => 'Vertical (top photos, albums)', + 'ALBUM_DECORATION_ORIENTATION_COLUMN_REVERSE' => 'Vertical (top albums, photos)', + 'MAP_DISPLAY_TEXT' => 'Skru på kart (levert av OpenStreetMap):', + 'MAP_DISPLAY_PUBLIC_TEXT' => 'Skru på kart for offentlige album (levert av OpenStreetMap):', + 'LOCATION_DECODING' => 'Benytt GPS data for å fylle ut stedsnavn', + 'LOCATION_SHOW' => 'Vis stedsnavn', + 'LOCATION_SHOW_PUBLIC' => 'Vis stedsnavn i offentlig modus', + 'MAP_PROVIDER' => 'Leverandør av OpenStreetMap fliser:', + 'MAP_PROVIDER_WIKIMEDIA' => 'Wikimedia', + 'MAP_PROVIDER_OSM_ORG' => 'OpenStreetMap.org (ikke HiDPI)', + 'MAP_PROVIDER_OSM_DE' => 'OpenStreetMap.de (ikke HiDPI)', + 'MAP_PROVIDER_OSM_FR' => 'OpenStreetMap.fr (ikke HiDPI)', + 'MAP_PROVIDER_RRZE' => 'University of Erlangen, Germany (bare HiDPI)', + 'MAP_INCLUDE_SUBALBUMS_TEXT' => 'Inkluder bilder av underalbum på kart:', + + 'LAYOUT_TYPE' => 'Oppsett for bilder:', + 'LAYOUT_SQUARES' => 'Kvadratiske miniatyrbilder', + 'LAYOUT_JUSTIFIED' => 'Med aspektratio, justert', + 'LAYOUT_MASONRY' => 'Med aspektratio, masonry', + 'LAYOUT_GRID' => 'Med aspektratio, grid', + 'LAYOUT_UNJUSTIFIED' => 'Med aspektratio, ikke justert', + 'SET_LAYOUT' => 'Lagre oppsett', + + 'NSFW_VISIBLE_TEXT_1' => 'Make Sensitive albums visible by default.', + 'NSFW_VISIBLE_TEXT_2' => 'If the album is public, it is still accessible, just hidden from the view and can be revealed by pressing H.', + 'SETTINGS_SUCCESS_NSFW_VISIBLE' => 'Default sensitive album visibility updated with success.', + + 'NSFW_BANNER' => '

Sensitive content

This album contains sensitive content which some people may find offensive or disturbing.

Tap to consent.

', + 'NSFW_HEADER' => 'Sensitive content', + 'NSFW_EXPLANATION' => 'This album contains sensitive content which some people may find offensive or disturbing.', + 'TAP_CONSENT' => 'Tap to consent.', + + 'VIEW_NO_RESULT' => 'Ingen resultater', + 'VIEW_NO_PUBLIC_ALBUMS' => 'Ingen offentlige album', + 'VIEW_NO_CONFIGURATION' => 'Ingen innstillinger', + 'VIEW_PHOTO_NOT_FOUND' => 'Bildet ble ikke funnet', + + 'NO_TAGS' => 'Ingen Tagger', + + 'UPLOAD_MANAGE_NEW_PHOTOS' => 'Du kan nå håndtere de nye bildene.', + 'UPLOAD_COMPLETE' => 'Opplasting fullført', + 'UPLOAD_COMPLETE_FAILED' => 'Kunne ikke laste opp en eller flere av bildene.', + 'UPLOAD_IMPORTING' => 'Importerer', + 'UPLOAD_IMPORTING_URL' => 'Importerer lenke', + 'UPLOAD_UPLOADING' => 'Laster opp', + 'UPLOAD_FINISHED' => 'Fullført', + 'UPLOAD_PROCESSING' => 'Arbeider', + 'UPLOAD_FAILED' => 'Feilet', + 'UPLOAD_FAILED_ERROR' => 'Opplasting feilet. Serveren svarte med en feil!', + 'UPLOAD_FAILED_WARNING' => 'Opplasting feilet. Serveren svarte med en advarsel!', + 'UPLOAD_CANCELLED' => 'Cancelled', + 'UPLOAD_SKIPPED' => 'Hoppet over', + 'UPLOAD_UPDATED' => 'Updated', + 'UPLOAD_GENERAL' => 'General', + 'UPLOAD_IMPORT_SKIPPED_DUPLICATE' => 'This photo has been skipped because it’s already in your library.', + 'UPLOAD_IMPORT_RESYNCED_DUPLICATE' => 'This photo has been skipped because it’s already in your library, but its metadata has been updated.', + 'UPLOAD_ERROR_CONSOLE' => 'Vennligst se konsollen i nettleseren for mer informasjon.', + 'UPLOAD_UNKNOWN' => 'Serveren svarte med en ukjent feilmelding. Vennlist se konsollen i nettleseren for mer informasjon.', + 'UPLOAD_ERROR_UNKNOWN' => 'Opplasting feilet. Serveren svarte med en ukjent feil!', + 'UPLOAD_ERROR_POSTSIZE' => 'Upload failed. The PHP post_max_size may be too small! Otherwise check the FAQ.', + 'UPLOAD_ERROR_FILESIZE' => 'Upload failed. The PHP upload_max_filesize may be too small! Otherwise check the FAQ.', + 'UPLOAD_IN_PROGRESS' => 'Lychee laster for tiden opp!', + 'UPLOAD_IMPORT_WARN_ERR' => 'Importeringen er ferdig, men advarsler eller feil ble returnert. Vennligst see loggen (Innstilinger -> Vis Logg) for mer informasjon.', + 'UPLOAD_IMPORT_COMPLETE' => 'Importering fullført', + 'UPLOAD_IMPORT_INSTR' => 'Vennlist fyll inn en direkte lenke til et bilde for å importere det:', + 'UPLOAD_IMPORT' => 'Importer', + 'UPLOAD_IMPORT_SERVER' => 'Importer fra server', + 'UPLOAD_IMPORT_SERVER_FOLD' => 'Mappen er tom eller inneholder ingen lesbare filer som kan behandles. Vennligst se loggen (Innstillinger -> Vis Logg) for mer informasjon.', + 'UPLOAD_IMPORT_SERVER_INSTR' => 'Import all photos, folders and sub-folders located in the folders with the following absolute paths (on server). Paths are space separated, use \\ to escape a space in a path.', + 'UPLOAD_ABSOLUTE_PATH' => 'Absolute path to directories, space separated', + 'UPLOAD_IMPORT_SERVER_EMPT' => 'Kunne ikke starte importeringen siden mappen var tom!', + 'UPLOAD_IMPORT_DELETE_ORIGINALS' => 'Fjern originalene', + 'UPLOAD_IMPORT_DELETE_ORIGINALS_EXPL' => 'De opprinnelige filene vil bli fjernet etter importeringen når mulig.', + 'UPLOAD_IMPORT_VIA_SYMLINK' => 'Symbolic links', + 'UPLOAD_IMPORT_VIA_SYMLINK_EXPL' => 'Import files using symbolic links to originals.', + 'UPLOAD_IMPORT_SKIP_DUPLICATES' => 'Skip duplicates', + 'UPLOAD_IMPORT_SKIP_DUPLICATES_EXPL' => 'Existing media files are skipped.', + 'UPLOAD_IMPORT_RESYNC_METADATA' => 'Re-sync metadata', + 'UPLOAD_IMPORT_RESYNC_METADATA_EXPL' => 'Update metadata of existing media files.', + 'UPLOAD_IMPORT_LOW_MEMORY_EXPL' => 'Importeringsprossesen på serverren nærmer seg grensen for hvor mye minne som kan brukes, og kan bli avbrutt før den er ferdig.', + 'UPLOAD_WARNING' => 'Advarsel', + 'UPLOAD_IMPORT_NOT_A_DIRECTORY' => 'Stien er ikke en lesbar mappe!', + 'UPLOAD_IMPORT_PATH_RESERVED' => 'Stien er en sti som er reservert for Lychee!', + 'UPLOAD_IMPORT_FAILED' => 'Kan ikke importere filen!', + 'UPLOAD_IMPORT_UNSUPPORTED' => 'Filtypen er ikke støttet!', + 'UPLOAD_IMPORT_CANCELLED' => 'Import cancelled', + + 'ABOUT_SUBTITLE' => 'Selvlevert bildehåndtering den riktige måten!', + 'ABOUT_DESCRIPTION' => 'Lychee er et gratis bildehåndteringsverktøy, som kjører på serveren eller en webhost som du eier og kontrollerer. Installasjon tar sekunder. Last opp, håndter, og del bilder som om det er din egen maskin. Lychee leverer alt du trenger, og alle bildene er trygt lagret.', + 'FOOTER_COPYRIGHT' => 'Alle bildene på denne nettsiden er bundet av opphavsrett fra %1$s © %2$s', + 'HOSTED_WITH_LYCHEE' => 'Levert av Lychee', + + 'URL_COPY_TO_CLIPBOARD' => 'Kopier til utklippstavlen', + 'URL_COPIED_TO_CLIPBOARD' => 'Kopierte lenke til utklippstavlen!', + 'PHOTO_DIRECT_LINKS_TO_IMAGES' => 'Direkte lenke til bildefiler:', + 'PHOTO_ORIGINAL' => 'Original', + 'PHOTO_MEDIUM' => 'Medium', + 'PHOTO_MEDIUM_HIDPI' => 'Medium HiDPI', + 'PHOTO_SMALL' => 'Miniatyr', + 'PHOTO_SMALL_HIDPI' => 'Miniatyr HiDPI', + 'PHOTO_THUMB' => 'Kvadratisk miniatyr', + 'PHOTO_THUMB_HIDPI' => 'Kvadratisk miniatyr HiDPI', + 'PHOTO_PLACEHOLDER' => 'Low Quality Image Placeholder', + 'PHOTO_THUMBNAIL' => 'Photo thumbnail', + 'PHOTO_LIVE_VIDEO' => 'Filmdel av livebilde', + 'PHOTO_VIEW' => 'Lychee Bildevisning:', + + 'PHOTO_EDIT_ROTATECWISE' => 'Roter med klokken', + 'PHOTO_EDIT_ROTATECCWISE' => 'Roter mot klokken', + + 'ERROR_GPX' => 'Error loading GPX file: ', + 'ERROR_EITHER_ALBUMS_OR_PHOTOS' => 'Please select either albums or photos!', + 'ERROR_COULD_NOT_FIND' => 'Could not find what you want.', + 'ERROR_INVALID_EMAIL' => 'Not a valid email address.', + 'EMAIL_SUCCESS' => 'Email updated!', + 'ERROR_PHOTO_NOT_FOUND' => 'Error: photo %s not found !', + 'ERROR_EMPTY_USERNAME' => 'new username cannot be empty.', + 'ERROR_PASSWORD_DOES_NOT_MATCH' => 'new password does not match.', + 'ERROR_EMPTY_PASSWORD' => 'new password cannot be empty.', + 'ERROR_SELECT_ALBUM' => 'Select an album to share!', + 'ERROR_SELECT_USER' => 'Select a user to share with!', + 'ERROR_SELECT_SHARING' => 'Select a sharing to remove!', + 'SHARING_SUCCESS' => 'Sharing updated!', + 'SHARING_REMOVED' => 'Sharing removed!', + 'USER_CREATED' => 'User created!', + 'USER_DELETED' => 'User deleted!', + 'USER_UPDATED' => 'User updated!', + 'ENTER_EMAIL' => 'Enter your email address:', + 'ERROR_ALBUM_JSON_NOT_FOUND' => 'Error: Album json not found!', + 'ERROR_ALBUM_NOT_FOUND' => 'Error: album %s not found', + 'ERROR_DROPBOX_KEY' => 'Error: Dropbox key not set', + 'ERROR_SESSION' => 'Session expired.', + 'CAMERA_DATE' => 'Camera date', + 'NEW_PASSWORD' => 'new password', + 'ALLOW_UPLOADS' => 'Allow uploads', + 'ALLOW_USER_SELF_EDIT' => 'Allow self-management of user account', + 'OSM_CONTRIBUTORS' => 'OpenStreetMap contributors', +]; diff --git a/lang/no/maintenance.php b/lang/no/maintenance.php new file mode 100644 index 00000000000..f86de3d6f46 --- /dev/null +++ b/lang/no/maintenance.php @@ -0,0 +1,60 @@ + 'Maintenance', + 'description' => 'You will find on this page, all the required actions to keep your Lychee installation running smooth and nicely.', + 'cleaning' => [ + 'title' => 'Cleaning %s', + 'result' => '%s deleted.', + 'description' => 'Remove all contents from %s', + 'button' => 'Clean', + ], + 'fix-jobs' => [ + 'title' => 'Fixing Jobs History', + 'description' => 'Mark jobs with status %s or %s as %s.', + 'button' => 'Fix job history', + ], + 'gen-sizevariants' => [ + 'title' => 'Missing %s', + 'description' => 'Found %d %s that could be generated.', + 'button' => 'Generate!', + 'success' => 'Successfully generated %d %s.', + ], + 'fill-filesize-sizevariants' => [ + 'title' => 'File sizes missing', + 'description' => 'Found %d small variants without file size.', + 'button' => 'Fetch data!', + 'success' => 'Successfully computed sizes of %d small variants.', + ], + 'fix-tree' => [ + 'title' => 'Tree statistics', + 'Oddness' => 'Oddness', + 'Duplicates' => 'Duplicates', + 'Wrong parents' => 'Wrong parents', + 'Missing parents' => 'Missing parents', + 'button' => 'Fix tree', + ], + 'optimize' => [ + 'title' => 'Optimize Database', + 'description' => 'If you notice slowdown in your installation, it may be because your database does not + have all its needed index.', + 'button' => 'Optimize Database', + ], + 'update' => [ + 'title' => 'Updates', + 'check-button' => 'Check for updates', + 'update-button' => 'Update', + 'no-pending-updates' => 'No pending update.', + ], +]; \ No newline at end of file diff --git a/lang/no/profile.php b/lang/no/profile.php new file mode 100644 index 00000000000..cc24b97452c --- /dev/null +++ b/lang/no/profile.php @@ -0,0 +1,64 @@ + 'Profile', + + 'login' => [ + 'header' => 'Profile', + 'enter_current_password' => 'Enter your current password:', + 'current_password' => 'Current password', + 'credentials_update' => 'Your credentials will be changed to the following:', + 'username' => 'Username', + 'new_password' => 'New password', + 'confirm_new_password' => 'Confirm new password', + 'email_instruction' => 'Add your email below to enable receiving email notifications. To stop receiving emails, simply remove your email below.', + 'email' => 'Email', + 'change' => 'Change Login', + 'api_token' => 'API Token ...', + + 'missing_fields' => 'Missing fields', + ], + + 'token' => [ + 'unavailable' => 'You have already viewed this token.', + 'no_data' => 'No token API have been generated.', + 'disable' => 'Disable', + 'disabled' => 'Token disabled', + 'warning' => 'This token will not be displayed again. Copy it and keep it in a safe place.', + 'reset' => 'Reset the token', + 'create' => 'Create a new token', + ], + + 'oauth' => [ + 'header' => 'OAuth', + 'header_not_available' => 'OAuth is not available', + 'setup_env' => 'Set up the credentials in your .env', + 'token_registered' => '%s token registered.', + 'setup' => 'Set up %s', + 'reset' => 'reset', + 'credential_deleted' => 'Credential deleted!', + ], + + 'u2f' => [ + 'header' => 'Passkey/MFA/2FA', + 'info' => 'This only provides the ability to use WebAuthn to authenticate instead of username & password.', + 'empty' => 'Credentials list is empty!', + 'not_secure' => 'Environment not secured. U2F not available.', + 'new' => 'Register new device.', + 'credential_deleted' => 'Credential deleted!', + 'credential_updated' => 'Credential updated!', + 'credential_registred' => 'Registration successful!', + '5_chars' => 'At least 5 chars.', + ], +]; \ No newline at end of file diff --git a/lang/no/settings.php b/lang/no/settings.php new file mode 100644 index 00000000000..fd197f11135 --- /dev/null +++ b/lang/no/settings.php @@ -0,0 +1,92 @@ + 'Settings', + 'small_screen' => 'For better a experience on the Settings page,
we recommend you use a larger screen.', + 'tabs' => [ + 'basic' => 'Basic', + 'all_settings' => 'All settings', + ], + 'toasts' => [ + 'change_saved' => 'Change saved!', + 'details' => 'Settings have been modified as per request', + 'error' => 'Error!', + 'error_load_css' => 'Could not load dist/user.css', + 'error_load_js' => 'Could not load dist/custom.js', + 'error_save_css' => 'Could not save CSS', + 'error_save_js' => 'Could not save JS', + 'thank_you' => 'Thank you for your support.', + 'reload' => 'Reload your page for full functionalities.', + ], + 'system' => [ + 'header' => 'System', + 'use_dark_mode' => 'Use dark mode for Lychee', + 'language' => 'Language used by Lychee', + 'nsfw_album_visibility' => 'Make Sensitive albums visible by default.', + 'nsfw_album_explanation' => 'If the album is public, it is still accessible, just hidden from the view and can be revealed by pressing H.', + ], + 'lychee_se' => [ + 'header' => 'Lychee SE', + 'call4action' => 'Get exclusive features and support the development of Lychee. Unlock the SE edition.', + 'preview' => 'Enable preview of Lychee SE features', + 'hide_call4action' => 'Hide this Lychee SE registration form. I am happy with Lychee as-is. :)', + 'hide_warning' => 'If enabled, the only way to register your license key will be via the More tab above. Changes are applied on page reload.', + ], + 'dropbox' => [ + 'header' => 'Dropbox', + 'instruction' => 'In order to import photos from your Dropbox, you need a valid drop-ins app key from their website.', + 'api_key' => 'Dropbox API Key', + 'set_key' => 'Set Dropbox Key', + ], + 'gallery' => [ + 'header' => 'Gallery', + 'photo_order_column' => 'Default column used for sorting photos', + 'photo_order_direction' => 'Default order used for sorting photos', + 'album_order_column' => 'Default column used for sorting albums', + 'album_order_direction' => 'Default order used for sorting albums', + 'aspect_ratio' => 'Default aspect ratio for album thumbs', + 'photo_layout' => 'Layout for pictures', + 'album_decoration' => 'Show decorations on album cover (sub-album and/or photo count)', + 'album_decoration_direction' => 'Align album decorations horizontally or vertically', + 'photo_overlay' => 'Default image overlay information', + 'license_default' => 'Default license used for albums', + 'license_help' => 'Need help choosing?', + ], + 'geolocation' => [ + 'header' => 'Geo-location', + 'map_display' => 'Display the map given GPS coordinates', + 'map_display_public' => 'Allow anonymous users to access the map', + 'map_provider' => 'Defines the map provider', + 'map_include_subalbums' => 'Includes pictures of the sub albums on the map', + 'location_decoding' => 'Use GPS location decoding', + 'location_show' => 'Show location extracted from GPS coordinates', + 'location_show_public' => 'Anonymous users can access the extracted location from GPS coordinates', + ], + 'advanced' => [ + 'header' => 'Advanced Customization', + 'change_css' => 'Change CSS', + 'change_js' => 'Change JS', + ], + 'all' => [ + 'old_setting_style' => 'Old setting style', + 'change_detected' => 'Some settings changed.', + 'save' => 'Save', + ], + + 'tool_option' => [ + 'disabled' => 'disabled', + 'enabled' => 'enabled', + 'discover' => 'discover', + ], +]; \ No newline at end of file diff --git a/lang/no/sharing.php b/lang/no/sharing.php new file mode 100644 index 00000000000..69de18cc6d0 --- /dev/null +++ b/lang/no/sharing.php @@ -0,0 +1,33 @@ + 'Sharing', + + 'info' => 'This page gives an overview of and the ability to edit the sharing rights associated with albums.', + 'album_title' => 'Album title', + 'username' => 'Username', + 'no_data' => 'Sharing list is empty.', + 'share' => 'Share', + 'permission_deleted' => 'Permission deleted!', + 'permission_created' => 'Permission created!', + + 'grants' => [ + 'read' => 'Grants read access', + 'original' => 'Grants access to original photo', + 'download' => 'Grants download', + 'upload' => 'Grants upload', + 'edit' => 'Grants edit', + 'delete' => 'Grants delete', + ], +]; \ No newline at end of file diff --git a/lang/no/statistics.php b/lang/no/statistics.php new file mode 100644 index 00000000000..2baf855bbd5 --- /dev/null +++ b/lang/no/statistics.php @@ -0,0 +1,34 @@ + 'Statistics', + + 'preview_text' => 'This is a preview of the statistics page available in Lychee SE.
The data shown here are randomly generated and do not reflect your server.', + 'no_data' => 'User does not have data on server.', + 'collapse' => 'Collapse albums sizes', + + 'total' => [ + 'total' => 'Total', + 'albums' => 'Albums', + 'photos' => 'Photos', + 'size' => 'Size', + ], + 'table' => [ + 'username' => 'Owner', + 'title' => 'Title', + 'photos' => 'Photos', + 'descendants' => 'Children', + 'size' => 'Size', + ], +]; \ No newline at end of file diff --git a/lang/no/toasts.php b/lang/no/toasts.php new file mode 100644 index 00000000000..293d4b72594 --- /dev/null +++ b/lang/no/toasts.php @@ -0,0 +1,17 @@ + 'Error', + 'success' => 'Success', +]; \ No newline at end of file diff --git a/lang/no/users.php b/lang/no/users.php new file mode 100644 index 00000000000..599bb833454 --- /dev/null +++ b/lang/no/users.php @@ -0,0 +1,44 @@ + 'Users', + 'description' => 'Here you can manage the users of your Lychee installation. You can create, edit and delete users.', + 'create' => 'Create a new user', + 'username' => 'Username', + 'password' => 'Password', + 'legend' => 'Legend', + 'upload_rights' => 'When selected, the user can upload content.', + 'edit_rights' => 'When selected, the user can modify their profile (username, password).', + 'quota' => 'When set, the user has a space quota for pictures (in kB).', + + 'user_deleted' => 'User deleted', + 'user_created' => 'User created', + 'user_updated' => 'User updated', + 'change_saved' => 'Change saved!', + + 'create_edit' => [ + 'upload_rights' => 'User can upload content.', + 'edit_rights' => 'User can modify their profile (username, password).', + 'quota' => 'User has quota limit.', + 'quota_kb' => 'quota in kB (0 for default)', + 'note' => 'Admin note (not publically visible)', + 'create' => 'Create', + 'edit' => 'Edit', + ], + 'line' => [ + 'admin' => 'admin user', + 'edit' => 'Edit', + 'delete' => 'Delete', + ], +]; \ No newline at end of file diff --git a/lang/pl/aspect_ratio.php b/lang/pl/aspect_ratio.php new file mode 100644 index 00000000000..2c7e8fb56ac --- /dev/null +++ b/lang/pl/aspect_ratio.php @@ -0,0 +1,21 @@ + '5/4 (instagram landscape)', + '4by5' => '4/5 (instagram portrait)', + '2by3' => '2/3 (portrait)', + '3by2' => '3/2 (landscape)', + '1by1' => 'square', + '1byx9' => '16/9 (landscape)', +]; \ No newline at end of file diff --git a/lang/pl/diagnostics.php b/lang/pl/diagnostics.php new file mode 100644 index 00000000000..0fadd640428 --- /dev/null +++ b/lang/pl/diagnostics.php @@ -0,0 +1,30 @@ + 'Diagnostics', + + 'copy_to_clipboard' => 'Copy diagnostics to clipboard', + 'self-diagnosis' => 'Self-diagnosis', + 'info' => 'Info', + 'space' => 'Space', + 'load_space' => 'Load space usage.', + 'configuration' => 'Configuration', + 'loading' => 'Loading...', + 'identical_content' => 'Identical content', + + 'toast' => [ + 'info' => 'Info', + 'copy' => 'Diagnostics copied to clipboard!', + ], +]; \ No newline at end of file diff --git a/lang/pl/dialogs.php b/lang/pl/dialogs.php new file mode 100644 index 00000000000..4afd65fae3f --- /dev/null +++ b/lang/pl/dialogs.php @@ -0,0 +1,221 @@ + [ + 'close' => 'Close', + 'cancel' => 'Cancel', + 'save' => 'Save', + 'delete' => 'Delete', + 'move' => 'Move', + ], + 'about' => [ + 'subtitle' => 'Self-hosted photo-management done right', + 'description' => 'Lychee is a free photo-management tool, which runs on your server or web-space. Installing is a matter of seconds. Upload, manage and share photos like from a native application. Lychee comes with everything you need and all your photos are stored securely.', + 'update_available' => 'Update available!', + 'thank_you' => 'Thank you for your support!', + 'get_supporter_or_register' => 'Get exclusive features and support the development of Lychee.
Unlock the Supporter Edition or register your License key', + 'here' => 'here', + ], + 'dropbox' => [ + 'not_configured' => 'Dropbox is not configured.', + ], + 'import_from_link' => [ + 'instructions' => 'Please enter the direct link to a photo to import it:', + 'import' => 'Import', + ], + 'keybindings' => [ + 'don_t_show_again' => 'Don\'t show this again', + 'side_wide' => 'Site-wide Shortcuts', + 'back_cancel' => 'Back/Cancel', + 'confirm' => 'Confirm', + 'login' => 'Login', + 'toggle_full_screen' => 'Toggle Full Screen', + 'toggle_sensitive_albums' => 'Toggle Sensitive Albums', + + 'albums' => 'Albums Shortcuts', + 'new_album' => 'New Album', + 'upload_photos' => 'Upload Photos', + 'search' => 'Search', + 'show_this_modal' => 'Show this modal', + 'select_all' => 'Select All', + 'move_selection' => 'Move Selection', + 'delete_selection' => 'Delete Selection', + + 'album' => 'Album Shortcuts', + 'slideshow' => 'Start/Stop Slideshow', + 'toggle' => 'Toggle panel', + + 'photo' => 'Photo Shortcuts', + 'previous' => 'Previous photo', + 'next' => 'Next photo', + 'cycle' => 'Cycle overlay mode', + 'star' => 'Star the photo', + 'move' => 'Move the photo', + 'delete' => 'Delete the photo', + 'edit' => 'Edit information', + 'show_hide_meta' => 'Show information', + + 'keep_hidden' => 'We will keep it hidden.', + ], + 'login' => [ + 'username' => 'Username', + 'password' => 'Password', + 'unknown_invalid' => 'Unknown user or invalid password.', + 'signin' => 'Sign-In', + ], + 'register' => [ + 'enter_license' => 'Enter your license key below:', + 'license_key' => 'License key', + 'invalid_license' => 'Invalid license key.', + 'register' => 'Register', + ], + 'share_album' => [ + 'url_copied' => 'Copied URL to clipboard!', + ], + 'upload' => [ + 'completed' => 'Completed', + 'uploaded' => 'Uploaded:', + 'release' => 'Release file to upload!', + 'select' => 'Click here to select files to upload', + 'drag' => '(Or drag files to the page)', + 'loading' => 'Loading', + 'resume' => 'Resume', + 'uploading' => 'Uploading', + 'finished' => 'Finished', + 'failed_error' => 'Upload failed. The server returned an error!', + ], + 'visibility' => [ + 'public' => 'Public', + 'public_expl' => 'Anonymous users can access this album, subject to the restrictions below.', + 'full' => 'Original', + 'full_expl' => 'Anonymous users can view full-resolution photos.', + 'hidden' => 'Hidden', + 'hidden_expl' => 'Anonymous users need a direct link to access this album.', + 'downloadable' => 'Downloadable', + 'downloadable_expl' => 'Anonymous users can download this album.', + 'password' => 'Password', + 'password_prot' => 'Password protected', + 'password_prot_expl' => 'Anonymous users need a shared password to access this album.', + 'nsfw' => 'Sensitive', + 'nsfw_expl' => 'Album contains sensitive content.', + 'visibility_updated' => 'Visibility updated.', + ], + 'move_album' => [ + 'confirm_single' => 'Are you sure you want to move the album “%1$s” into the album “%2$s”?', + 'confirm_multiple' => 'Are you sure you want to move all selected albums into the album “%s”?', + 'move_single' => 'Move Album', + 'move_to' => 'Move to', + 'move_to_single' => 'Move %s to:', + 'move_to_multiple' => 'Move %d albums to:', + 'no_album_target' => 'No album to move to', + 'moved_single' => 'Album moved!', + 'moved_single_details' => '%1$s moved to %2$s', + 'moved_details' => 'Album(s) moved to %s', + ], + 'new_album' => [ + 'menu' => 'Create Album', + 'info' => 'Enter a title for the new album:', + 'title' => 'title', + 'create' => 'Create Album', + ], + 'new_tag_album' => [ + 'menu' => 'Create Tag Album', + 'info' => 'Enter a title for the new tag album:', + 'title' => 'title', + 'set_tags' => 'Set tags to show', + 'warn' => 'Make sure to press enter after each tag', + 'create' => 'Create Tag Album', + ], + 'delete_album' => [ + 'confirmation' => 'Are you sure you want to delete the album “%s” and all of the photos it contains?', + 'confirmation_multiple' => 'Are you sure you want to delete all %d selected albums and all of the photos they contain?', + 'warning' => 'This action can not be undone!', + 'delete' => 'Delete Album and Photos', + ], + 'transfer' => [ + 'query' => 'Transfer ownership of album to', + 'confirmation' => 'Are you sure you want to transfer the ownership of album “%s” and all the photos it contains to "%s"?', + 'lost_access_warning' => 'Your access to this album will be lost.', + 'warning' => 'This action can not be undone!', + 'transfer' => 'Transfer ownership of album and photos', + ], + 'rename' => [ + 'photo' => 'Enter a new title for this photo:', + 'album' => 'Enter a new title for this album:', + 'rename' => 'Rename', + ], + 'merge' => [ + 'merge_to' => 'Merge %s to:', + 'merge_to_multiple' => 'Merge %d albums to:', + 'no_albums' => 'No albums to merge to.', + 'confirm' => 'Are you sure you want to merge the album “%1$s” into the album “%2$s”?', + 'confirm_multiple' => 'Are you sure you want to merge all selected albums into the album “%s”?', + 'merge' => 'Merge Albums', + 'merged' => 'Album(s) merged to %s!', + ], + 'unlock' => [ + 'password_required' => 'This album is protected by a password. Enter the password below to view the photos of this album:', + 'password' => 'Password', + 'unlock' => 'Unlock', + ], + 'photo_tags' => [ + 'question' => 'Enter your tags for this photo.', + 'question_multiple' => 'Enter your tags for all %d selected photos. Existing tags will be overwritten.', + 'no_tags' => 'No Tags', + 'set_tags' => 'Set Tags', + 'updated' => 'Tags updated!', + 'tags_override_info' => 'If this is unchecked, the tags will be added to the existing tags of the photo.', + ], + 'photo_copy' => [ + 'no_albums' => 'No albums to copy to', + 'copy_to' => 'Copy %s to:', + 'copy_to_multiple' => 'Copy %d photos to:', + 'confirm' => 'Copy %s to %s.', + 'confirm_multiple' => 'Copy %d photos to %s.', + 'copy' => 'Copy', + 'copied' => 'Photo(s) copied!', + ], + 'photo_delete' => [ + 'confirm' => 'Are you sure you want to delete the photo “%s”?', + 'confirm_multiple' => 'Are you sure you want to delete all %d selected photos?', + 'deleted' => 'Photo(s) deleted!', + ], + 'move_photo' => [ + 'move_single' => 'Move %s to:', + 'move_multiple' => 'Move %d photos to:', + 'confirm' => 'Move %s to %s.', + 'confirm_multiple' => 'Move %d photos to %s.', + 'moved' => 'Photo(s) moved to %s!', + ], + 'target_user' => [ + 'placeholder' => 'Select user', + ], + 'target_album' => [ + 'placeholder' => 'Select album', + ], + 'webauthn' => [ + 'u2f' => 'U2F', + 'success' => 'Authentication successful!', + 'error' => 'Whoops, it looks like something went wrong. Please reload the site and try again!', + ], + 'se' => [ + 'available' => 'Available in the Supporter Edition', + ], + 'session_expired' => [ + 'title' => 'Session expired', + 'message' => 'Your session has expired.
Please reload the page.', + 'reload' => 'Reload', + 'go_to_gallery' => 'Go to the Gallery', + ], +]; \ No newline at end of file diff --git a/lang/pl/fix-tree.php b/lang/pl/fix-tree.php new file mode 100644 index 00000000000..64803e310e6 --- /dev/null +++ b/lang/pl/fix-tree.php @@ -0,0 +1,55 @@ + 'Maintenance', + 'intro' => 'This page allows you to re-order and fix your albums manually.
Before any modifications, we strongly recommend you to read about Nested Set tree structures.', + 'warning' => 'You can really break your Lychee installation here, modify values at your own risks.', + + 'help' => [ + 'header' => 'Help', + 'hover' => 'Hover ids or titles to highlight related albums.', + 'left' => 'Left', + 'right' => 'Right', + 'convenience' => 'For your convenience, the and buttons allow you to change the values of %s and %s by respectively +1 and -1 with propagation.', + 'left-right-warn' => 'The and indicates that the value of %s (and respectively %s) is duplicated somewhere.', + 'parent-marked' => 'Marked Parent Id indicates that the %s and %s do not satisfy the Nest Set tree structures. Edit either the Parent Id or the %s/%s values.', + 'slowness' => 'This page will be slow with a large number of albums.', + ], + + 'buttons' => [ + 'reset' => 'Reset', + 'check' => 'Check', + 'apply' => 'Apply', + ], + + 'table' => [ + 'title' => 'Title', + 'left' => 'Left', + 'right' => 'Right', + 'id' => 'Id', + 'parent' => 'Parent Id', + ], + + 'errors' => [ + 'invalid' => 'Invalid tree!', + 'invalid_details' => 'We are not applying this as it is guaranteed to be a broken state.', + 'invalid_left' => 'Album %s has an invalid left value.', + 'invalid_right' => 'Album %s has an invalid right value.', + 'invalid_left_right' => 'Album %s has an invalid left/right values. Left should be strictly smaller than right: %s < %s.', + 'duplicate_left' => 'Album %s has a duplicate left value %s.', + 'duplicate_right' => 'Album %s has a duplicate right value %s.', + 'parent' => 'Album %s has an unexpected parent id %s.', + 'unknown' => 'Album %s has an unknown error.', + ], +]; \ No newline at end of file diff --git a/lang/pl/gallery.php b/lang/pl/gallery.php new file mode 100644 index 00000000000..eb8008827e0 --- /dev/null +++ b/lang/pl/gallery.php @@ -0,0 +1,241 @@ + 'Gallery', + + 'smart_albums' => 'Smart albums', + 'albums' => 'Albums', + 'root' => 'Albums', + + 'original' => 'Original', + 'medium' => 'Medium', + 'medium_hidpi' => 'Medium HiDPI', + 'small' => 'Thumb', + 'small_hidpi' => 'Thumb HiDPI', + 'thumb' => 'Square thumb', + 'thumb_hidpi' => 'Square thumb HiDPI', + 'placeholder' => 'Low Quality Image Placeholder', + 'thumbnail' => 'Photo thumbnail', + 'live_video' => 'Video part of live-photo', + + 'camera_data' => 'Camera date', + 'album_reserved' => 'All Rights Reserved', + + 'map' => [ + 'error_gpx' => 'Error loading GPX file', + 'osm_contributors' => 'OpenStreetMap contributors', + ], + + 'search' => [ + 'title' => 'Search', + 'searching' => 'Searching…', + 'no_results' => 'Nothing matches your search query.', + 'searchbox' => 'Search…', + 'minimum_chars' => 'Minimum %s characters required.', + 'photos' => 'Photos (%s)', + 'albums' => 'Albums (%s)', + ], + + 'smart_album' => [ + 'unsorted' => 'Unsorted', + 'starred' => 'Starred', + 'recent' => 'Recent', + 'public' => 'Public', + 'on_this_day' => 'On This Day', + ], + + 'layout' => [ + 'squares' => 'Square thumbnails', + 'justified' => 'With aspect, justified', + 'masonry' => 'With aspect, masonry', + 'grid' => 'With aspect, grid', + ], + + 'overlay' => [ + 'none' => 'None', + 'exif' => 'EXIF data', + 'description' => 'Description', + 'date' => 'Date taken', + ], + + 'timeline' => [ + 'default' => 'default', + 'disabled' => 'disabled', + 'year' => 'Year', + 'month' => 'Month', + 'day' => 'Day', + 'hour' => 'Hour', + ], + + 'album' => [ + 'header_albums' => 'Albums', + 'header_photos' => 'Photos', + 'no_results' => 'Nothing to see here', + 'upload' => 'Upload photos', + + 'tabs' => [ + 'about' => 'About Album', + 'share' => 'Share Album', + 'move' => 'Move Album', + 'danger' => 'DANGER ZONE', + ], + + 'hero' => [ + 'created' => 'Created', + 'copyright' => 'Copyright', + 'subalbums' => 'Subalbums', + 'images' => 'Photos', + 'download' => 'Download Album', + 'share' => 'Share Album', + 'stats_only_se' => 'Statistics available in the Supporter Edition', + ], + + 'stats' => [ + 'lens' => 'Lens', + 'shutter' => 'Shutter speed', + 'iso' => 'ISO', + 'model' => 'Model', + 'aperture' => 'Aperture', + 'no_data' => 'No data', + ], + + 'properties' => [ + 'title' => 'Title', + 'description' => 'Description', + 'photo_ordering' => 'Order photos by', + 'children_ordering' => 'Order albums by', + 'asc/desc' => 'asc/desc', + 'header' => 'Set album header', + 'compact_header' => 'Use compact header', + 'license' => 'Set license', + 'copyright' => 'Set copyright', + 'aspect_ratio' => 'Set album thumbs aspect ratio', + 'album_timeline' => 'Set album timeline mode', + 'photo_timeline' => 'Set photo timeline mode', + 'layout' => 'Set photo layout', + 'show_tags' => 'Set tags to show', + 'tags_required' => 'Tags are required.', + ], + ], + + 'photo' => [ + 'actions' => [ + 'star' => 'Star', + 'unstar' => 'Unstar', + 'set_album_header' => 'Set as album header', + 'move' => 'Move', + 'delete' => 'Delete', + 'header_set' => 'Header set', + ], + + 'details' => [ + 'about' => 'About', + 'basics' => 'Basics', + 'title' => 'Title', + 'uploaded' => 'Uploaded', + 'description' => 'Description', + 'license' => 'License', + 'reuse' => 'Reuse', + 'latitude' => 'Latitude', + 'longitude' => 'Longitude', + 'altitude' => 'Altitude', + 'location' => 'Location', + 'image' => 'Image', + 'video' => 'Video', + 'size' => 'Size', + 'format' => 'Format', + 'resolution' => 'Resolution', + 'duration' => 'Duration', + 'fps' => 'Frame rate', + 'tags' => 'Tags', + 'camera' => 'Camera', + 'captured' => 'Captured', + 'make' => 'Make', + 'type' => 'Type/Model', + 'lens' => 'Lens', + 'shutter' => 'Shutter Speed', + 'aperture' => 'Aperture', + 'focal' => 'Focal Length', + 'iso' => 'ISO %s', + ], + + 'edit' => [ + 'set_title' => 'Set Title', + 'set_description' => 'Set Description', + 'set_license' => 'Set License', + 'no_tags' => 'No Tags', + 'set_tags' => 'Set Tags', + 'set_created_at' => 'Set Upload Date', + ], + ], + + 'nsfw' => [ + 'header' => 'Sensitive content', + 'description' => 'This album contains sensitive content which some people may find offensive or disturbing.', + 'consent' => 'Tap to consent.', + ], + + 'menus' => [ + 'star' => 'Star', + 'unstar' => 'Unstar', + 'star_all' => 'Star Selected', + 'unstar_all' => 'Unstar Selected', + 'tag' => 'Tag', + 'tag_all' => 'Tag Selected', + 'set_cover' => 'Set Album Cover', + 'remove_header' => 'Remove Album Header', + 'set_header' => 'Set Album Header', + 'copy_to' => 'Copy to …', + 'copy_all_to' => 'Copy Selected to …', + 'rename' => 'Rename', + 'move' => 'Move', + 'move_all' => 'Move Selected', + 'delete' => 'Delete', + 'delete_all' => 'Delete Selected', + 'download' => 'Download', + 'download_all' => 'Download Selected', + 'merge' => 'Merge', + 'merge_all' => 'Merge Selected', + + 'upload_photo' => 'Upload Photo', + 'import_link' => 'Import from Link', + 'import_dropbox' => 'Import from Dropbox', + 'new_album' => 'New Album', + 'new_tag_album' => 'New Tag Album', + 'upload_track' => 'Upload track', + 'delete_track' => 'Delete track', + ], + + 'sort' => [ + 'photo_select_1' => 'Upload Time', + 'photo_select_2' => 'Take Date', + 'photo_select_3' => 'Title', + 'photo_select_4' => 'Description', + 'photo_select_6' => 'Star', + 'photo_select_7' => 'Photo Format', + 'ascending' => 'Ascending', + 'descending' => 'Descending', + 'album_select_1' => 'Creation Time', + 'album_select_2' => 'Title', + 'album_select_3' => 'Description', + 'album_select_5' => 'Latest Take Date', + 'album_select_6' => 'Oldest Take Date', + ], + + 'albums_protection' => [ + 'private' => 'private', + 'public' => 'public', + 'inherit_from_parent' => 'inherit from parent', + ], +]; \ No newline at end of file diff --git a/lang/pl/jobs.php b/lang/pl/jobs.php new file mode 100644 index 00000000000..5d952b76012 --- /dev/null +++ b/lang/pl/jobs.php @@ -0,0 +1,18 @@ + 'Jobs', + + 'no_data' => 'No Jobs have been executed yet.', +]; \ No newline at end of file diff --git a/lang/pl/landing.php b/lang/pl/landing.php new file mode 100644 index 00000000000..fe6fe55b8ea --- /dev/null +++ b/lang/pl/landing.php @@ -0,0 +1,19 @@ + 'Gallery', + 'access_gallery' => 'Access the gallery', + 'hosted_with_lychee' => 'Hosted with Lychee', + 'copyright' => 'All images on this website are subject to copyright by %1$s © %2$s', +]; \ No newline at end of file diff --git a/lang/pl/left-menu.php b/lang/pl/left-menu.php new file mode 100644 index 00000000000..9a3e91f4037 --- /dev/null +++ b/lang/pl/left-menu.php @@ -0,0 +1,29 @@ + 'Back to Gallery', + + 'admin' => 'Admin', + 'clockwork' => 'Clockwork App', + 'logs' => 'Show Logs', + 'jobs' => 'Show Job History', + 'user' => 'User', + + 'sign_out' => 'Sign Out', + + 'about' => 'About', + 'api' => 'API Documentation', + 'source_code' => 'Source Code', + 'support' => 'Support', +]; \ No newline at end of file diff --git a/lang/pl/lychee.php b/lang/pl/lychee.php new file mode 100644 index 00000000000..608dad0f64b --- /dev/null +++ b/lang/pl/lychee.php @@ -0,0 +1,535 @@ + 'Nazwa użytkownika', + 'PASSWORD' => 'Hasło', + 'ENTER' => 'Potwierdź', + 'CANCEL' => 'Anuluj', + 'CONFIRM' => 'Confirm', + 'SIGN_IN' => 'Zaloguj', + 'CLOSE' => 'Zamknij', + 'SETTINGS' => 'Ustawienia', + 'SEARCH' => 'Szukaj …', + 'MORE' => 'Więcej', + 'DEFAULT' => 'Domyślne', + 'GALLERY' => 'Gallery', + + 'USERS' => 'Użytkownicy', + 'PROFILE' => 'Profile', + 'CREATE' => 'Create', + 'REMOVE' => 'Remove', + 'SHARE' => 'Share', + 'U2F' => 'U2F', + 'NOTIFICATIONS' => 'Notifications', + 'SHARING' => 'Udostępnianie', + 'CHANGE_LOGIN' => 'Zmień login', + 'CHANGE_SORTING' => 'Zmień sortowanie', + 'SET_DROPBOX' => 'Zapisz', + 'ABOUT_LYCHEE' => 'O Lychee', + 'DIAGNOSTICS' => 'Informacje techniczne', + 'DIAGNOSTICS_GET_SIZE' => 'Analiza miejsca na dysku', + 'JOBS' => 'Show job history', + 'LOGS' => 'Logi', + 'SIGN_OUT' => 'Wyloguj', + 'UPDATE_AVAILABLE' => 'Dostępna aktualizacja!', + 'MIGRATION_AVAILABLE' => 'Dostępna migracja!', + 'CHECK_FOR_UPDATE' => 'Check for updates', + 'DEFAULT_LICENSE' => 'Domyślna licencja dla nowych wrzutek:', + 'SET_LICENSE' => 'Zapisz', + 'SET_OVERLAY_TYPE' => 'Set Overlay', + 'SET_ALBUM_DECORATION' => 'Set album decorations', + 'SET_MAP_PROVIDER' => 'Set OpenStreetMap tiles provider', + 'FULL_SETTINGS' => 'Full Settings', + 'UPDATE' => 'Update', + 'RESET' => 'Reset', + 'DISABLE_TOKEN_TOOLTIP' => 'Disable', + 'ENABLE_TOKEN' => 'Enable API token', + 'DISABLED_TOKEN_STATUS_MSG' => 'Disabled', + 'TOKEN_BUTTON' => 'API Token ...', + 'TOKEN_NOT_AVAILABLE' => 'You have already viewed this token.', + 'TOKEN_WAIT' => 'Wait ...', + + 'SMART_ALBUMS' => 'Inteligentne albumy', + 'SHARED_ALBUMS' => 'Udostępnione albumy', + 'ALBUMS' => 'Albumy', + 'PHOTOS' => 'Zdjęcia', + 'SEARCH_RESULTS' => 'Wyniki wyszukiwania', + + 'RENAME' => 'Zmień nazwę', + 'RENAME_ALL' => 'Zamień zaznaczone', + 'MERGE' => 'Połącz z …', + 'MERGE_ALL' => 'Połącz zaznaczone', + 'MAKE_PUBLIC' => 'Ustaw jako publiczne', + 'SHARE_ALBUM' => 'Udostępnij album', + 'SHARE_PHOTO' => 'Udostępnij zdjęcie', + 'VISIBILITY_ALBUM' => 'Widoczność', + 'VISIBILITY_PHOTO' => 'Widoczność', + 'DOWNLOAD_ALBUM' => 'Pobierz album', + 'ABOUT_ALBUM' => 'O albumie', + 'DELETE_ALBUM' => 'Usuń album', + 'MOVE_ALBUM' => 'Przenieś album', + 'FULLSCREEN_ENTER' => 'Włącz pełny ekran', + 'FULLSCREEN_EXIT' => 'Wyłącz pełny rkran', + + 'SHARING_ALBUM_USERS' => 'Udostępnij album', + 'WAIT_FETCH_DATA' => 'Proszę czekać, trwa pobieranie danych …', + 'SHARING_ALBUM_USERS_NO_USERS' => 'Brak użytkowników do udostępnienia albumu', + 'SHARING_ALBUM_USERS_LONG_MESSAGE' => 'Wybierz użytkowników aby udostępnić ten album', + + 'DELETE_ALBUM_QUESTION' => 'Usuń album i zdjęcia', + 'KEEP_ALBUM' => 'Zatrzymaj album', + 'DELETE_ALBUM_CONFIRMATION' => 'Czy na pewno chcesz usunąć album „%s” razem z zawartością ? Ta akcja jest nieodwracalna!', + + 'DELETE_TAG_ALBUM_QUESTION' => 'Delete Album', + 'DELETE_TAG_ALBUM_CONFIRMATION' => 'Are you sure you want to delete the album „%s” (any photos inside will not be deleted)? This action can’t be undone!', + + 'DELETE_ALBUMS_QUESTION' => 'Usuń album wraz z zawartością', + 'KEEP_ALBUMS' => 'Zatrzymaj Albumy', + 'DELETE_ALBUMS_CONFIRMATION' => 'Czy na pewno usunąć %d zaznaczone albumy wraz z zawartością? Ta akcja jest nieodwracalna!', + + 'DELETE_UNSORTED_CONFIRM' => 'Czy na pewno usunąć wszystkie zdjęcia z „Nieposortowane”? Ta operacja nie może zostać cofnięta!', + 'CLEAR_UNSORTED' => 'Wyczyść Nieposortowane', + 'KEEP_UNSORTED' => 'Zatrzymaj Nieposortowane', + + 'EDIT_SHARING' => 'Edytuj udostępnianie', + 'MAKE_PRIVATE' => 'Oznacz jako prywatne', + + 'CLOSE_ALBUM' => 'Zmaknij album', + 'CLOSE_PHOTO' => 'Zamknij zdjęcie', + 'CLOSE_MAP' => 'Zamknij mapę', + + 'ADD' => 'Dodaj', + 'MOVE' => 'Przenieś do …', + 'MOVE_ALL' => 'Przenieś zaznaczone', + 'DUPLICATE' => 'Kopiuj', + 'DUPLICATE_ALL' => 'Kopiuj zaznaczone', + 'COPY_TO' => 'Kopiuj do …', + 'COPY_ALL_TO' => 'Kopiuj zaznaczone do …', + 'DELETE' => 'Usuń', + 'SAVE' => 'Save', + 'DELETE_ALL' => 'Usuń zaznaczone', + 'DOWNLOAD' => 'Pobierz', + 'DOWNLOAD_ALL' => 'Pobierz zaznaczone', + 'UPLOAD_PHOTO' => 'Wgraj zdjęcie', + 'IMPORT_LINK' => 'Importuj z adresu', + 'IMPORT_DROPBOX' => 'Importuj z Dropbox', + 'IMPORT_SERVER' => 'Importuj z serwera', + 'NEW_ALBUM' => 'Dodaj album', + 'NEW_TAG_ALBUM' => 'Dodaj album z tagami', + 'UPLOAD_TRACK' => 'Upload track', + 'DELETE_TRACK' => 'Delete track', + + 'TITLE_NEW_ALBUM' => 'Wpisz tytuł dla nowego albumu:', + 'UNTITLED' => 'Bez nazwy', + 'UNSORTED' => 'Nieposortowane', + 'STARRED' => 'Oznaczone', + 'RECENT' => 'Ostatnie', + 'PUBLIC' => 'Publiczne', + 'ON_THIS_DAY' => 'On This Day', + 'NUM_PHOTOS' => 'Zdjęć', + + 'CREATE_ALBUM' => 'Utwórz album', + 'CREATE_TAG_ALBUM' => 'Utwórz album z tagami', + + 'STAR_PHOTO' => 'Oznacz', + 'STAR' => 'Oznacz', + 'UNSTAR' => 'Unstar', + 'STAR_ALL' => 'Oznacz zaznaczone', + 'UNSTAR_ALL' => 'Unstar Selected', + 'TAG' => 'Otaguj', + 'TAG_ALL' => 'Otaguj zaznaczone', + 'UNSTAR_PHOTO' => 'Cofnij oznaczenie', + 'SET_COVER' => 'Ustaw jako okładkę albumu', + 'REMOVE_COVER' => 'Usuń okładkę albumu', + 'SET_HEADER' => 'Set Album Header', + 'REMOVE_HEADER' => 'Remove Album Header', + 'SET_COMPACT_HEADER' => 'Use Compact Header', + + 'FULL_PHOTO' => 'Otwórz oryginalne', + 'ABOUT_PHOTO' => 'Informacje o zdjęciu', + 'DISPLAY_FULL_MAP' => 'Mapa', + 'DIRECT_LINK' => 'Link bezpośredni', + 'DIRECT_LINKS' => 'Linki bezpośrednie', + 'QR_CODE' => 'QR Code', + + 'ALBUM_ABOUT' => 'Informacje o albumie', + 'ALBUM_BASICS' => 'Informacje podstawowe', + 'ALBUM_TITLE' => 'Tytuł', + 'ALBUM_COPYRIGHT' => 'Copyright', + 'ALBUM_SET_COPYRIGHT' => 'Set copyright', + 'ALBUM_NEW_TITLE' => 'Edytuj tytuł albumu:', + 'ALBUMS_NEW_TITLE' => 'Wpisz tytuł dla %d wybranych albumów:', + 'ALBUM_SET_TITLE' => 'Zapisz', + 'ALBUM_DESCRIPTION' => 'Opis', + 'ALBUM_SHOW_TAGS' => 'Tagi do pokazania', + 'ALBUM_NEW_DESCRIPTION' => 'Edytuj opis albumu:', + 'ALBUM_SET_DESCRIPTION' => 'Zapisz', + 'ALBUM_NEW_SHOWTAGS' => 'Enter tags of photos that will be visible in this album:', + 'ALBUM_SET_SHOWTAGS' => 'Ustaw tagi do pokazania', + 'ALBUM_ALBUM' => 'Album', + 'ALBUM_CREATED' => 'Utworzone', + 'ALBUM_IMAGES' => 'Zdjęcia', + 'ALBUM_VIDEOS' => 'Filmy', + 'ALBUM_SUBALBUMS' => 'Albumy podrzędne', + 'ALBUM_SHARING' => 'Udostępnianie', + 'ALBUM_SHR_YES' => 'TAK', + 'ALBUM_SHR_NO' => 'Nie', + 'ALBUM_PUBLIC' => 'Publiczny', + 'ALBUM_PUBLIC_EXPL' => 'Anonymous users can access this album, subject to the restrictions below.', + 'ALBUM_FULL' => 'Oryginalne zdjęcia', + 'ALBUM_FULL_EXPL' => 'Anonymous users can behold full-resolution photos.', + 'ALBUM_HIDDEN' => 'Ukryty', + 'ALBUM_HIDDEN_EXPL' => 'Anonymous users need a direct link to access this album.', + 'ALBUM_MARK_NSFW' => 'Oznacz album jako poufny', + 'ALBUM_UNMARK_NSFW' => 'Odznacz album jako poufny', + 'ALBUM_NSFW' => 'Poufny', + 'ALBUM_NSFW_EXPL' => 'Album zawiera poufne informacje.', + 'ALBUM_DOWNLOADABLE' => 'Pobieranie', + 'ALBUM_DOWNLOADABLE_EXPL' => 'Anonymous users can download this album.', + 'ALBUM_SHARE_BUTTON_VISIBLE' => 'Udostępnianie', + 'ALBUM_SHARE_BUTTON_VISIBLE_EXPL' => 'Anonymous users can see social media sharing links.', + 'ALBUM_PASSWORD' => 'Hasło', + 'ALBUM_PASSWORD_PROT' => 'Zabezpieczony', + 'ALBUM_PASSWORD_PROT_EXPL' => 'Anonymous users need a shared password to access this album.', + 'ALBUM_PASSWORD_REQUIRED' => 'Album chroniony jest hasłem. Wpisz hasło aby zobaczyć zawartość:', + 'ALBUM_MERGE' => 'Czy na pewno połączyć ten album „%1$s” z albumem „%2$s”?', + 'ALBUMS_MERGE' => 'Czy na pewno połączyć zaznaczone albumy z „%s”?', + 'MERGE_ALBUM' => 'Połącz albumy', + 'DONT_MERGE' => 'Anuluj', + 'ALBUM_MOVE' => 'Czy na pewno przenieść album „%1$s” do albumu „%2$s”?', + 'ALBUMS_MOVE' => 'Czy na pewno przenieść zaznaczone albumy do albumu „%s”?', + 'MOVE_ALBUMS' => 'Przenieś albumy', + 'NOT_MOVE_ALBUMS' => 'Anuluj', + 'ROOT' => 'Albumy', + 'ALBUM_REUSE' => 'Prawo do wykorzystania', + 'ALBUM_LICENSE' => 'Licencja', + 'ALBUM_SET_LICENSE' => 'Zapisz', + 'ALBUM_LICENSE_HELP' => 'Potrzebujesz pomocy?', + 'ALBUM_LICENSE_NONE' => 'Brak', + 'ALBUM_RESERVED' => 'Wszelkie prawa zastrzeżone', + 'ALBUM_SET_ORDER' => 'Zapisz', + 'ALBUM_ORDERING' => 'Sortowanie', + 'ALBUM_PHOTO_ORDERING' => 'Order photos by', + 'ALBUM_CHILDREN_ORDERING' => 'Order albums by', + 'ALBUM_OWNER' => 'Owner', + + 'PHOTO_ABOUT' => 'Informacje o zdjęciu', + 'PHOTO_BASICS' => 'Informacje podstawowe', + 'PHOTO_TITLE' => 'Tytuł', + 'PHOTO_NEW_TITLE' => 'Wpisz nowy tytuł:', + 'PHOTO_SET_TITLE' => 'Ustaw tytuł', + 'PHOTO_UPLOADED' => 'Wgrany', + 'PHOTO_DESCRIPTION' => 'Opis', + 'PHOTO_NEW_DESCRIPTION' => 'Wpisz nowy opis:', + 'PHOTO_SET_DESCRIPTION' => 'Ustaw opis', + 'PHOTO_NEW_LICENSE' => 'Dodaj licencję', + 'PHOTO_SET_LICENSE' => 'Ustaw licencję', + 'PHOTO_LICENSE' => 'Licencja', + 'PHOTO_LICENSE_HELP' => 'Need help choosing?', + 'PHOTO_REUSE' => 'Ponowne wykorzystanie', + 'PHOTO_LICENSE_NONE' => 'Brak', + 'PHOTO_RESERVED' => 'Wszelkie prawa zastrzeżone', + 'PHOTO_LATITUDE' => 'Długość geograficzna', + 'PHOTO_LONGITUDE' => 'Szerokość geograficzna', + 'PHOTO_ALTITUDE' => 'Wysokość', + 'PHOTO_IMGDIRECTION' => 'Kierunek', + 'PHOTO_LOCATION' => 'Lokalizacja', + 'PHOTO_IMAGE' => 'Zdjęcie', + 'PHOTO_VIDEO' => 'Film', + 'PHOTO_SIZE' => 'Rozmiar', + 'PHOTO_FORMAT' => 'Format', + 'PHOTO_RESOLUTION' => 'Rozdzielczość', + 'PHOTO_DURATION' => 'Czas trwania', + 'PHOTO_FPS' => 'Przepustowość klatek/sek', + 'PHOTO_TAGS' => 'Tagi', + 'PHOTO_NOTAGS' => 'Brak Tagów', + 'PHOTO_NEW_TAGS' => 'Wpisz tagi rozdzielając je przecinkiem:', + 'PHOTOS_NEW_TAGS' => 'Wpisz tagi dla %d zaznaczonych zdjęć. Istniejące tagi zostaną nadpisane. Możesz wpisać więcej tagów rozdzielając je przecinkiem:', + 'PHOTO_SET_TAGS' => 'Zapisz', + 'PHOTO_CAMERA' => 'Aparat', + 'PHOTO_CAPTURED' => 'Zrzut', + 'PHOTO_MAKE' => 'Marka', + 'PHOTO_TYPE' => 'Typ/Model', + 'PHOTO_LENS' => 'Obiektyw', + 'PHOTO_SHUTTER' => 'Szybkość migawki', + 'PHOTO_APERTURE' => 'Przysłona', + 'PHOTO_FOCAL' => 'Ogniskowa', + 'PHOTO_ISO' => 'ISO %s', + 'PHOTO_SHARING' => 'Udostępnianie', + 'PHOTO_DELETE' => 'Usuń Zdjęcie', + 'PHOTO_KEEP' => 'Anuluj', + 'PHOTO_DELETE_CONFIRMATION' => 'Czy na pewno usunąć zdjęcie „%s”? Akcja jest nieodwracalna!', + 'PHOTO_DELETE_ALL' => 'Czy na pewno usunąć %d zaznaczone zdjęcia? Akcja jest nieodwracalna!', + 'PHOTOS_NEW_TITLE' => 'Wpisz tytuł dla %d zaznaczonych zdjęć:', + 'PHOTO_MAKE_PRIVATE_ALBUM' => 'Zdjęcie znajduje się w albumie publicznym. Aby ustawić je jako prywatne/publiczne, edytuj ustawienie widoczności w albumie, w którym zdjęcie się znajduje.', + 'PHOTO_SHOW_ALBUM' => 'Pokaż album', + 'PHOTO_PUBLIC' => 'Publiczne', + 'PHOTO_PUBLIC_EXPL' => 'Anonymous users can view this photo, subject to the restrictions below.', + 'PHOTO_FULL' => 'Originalne zdjęcia', + 'PHOTO_FULL_EXPL' => 'Anonymous users can behold full-resolution photo.', + 'PHOTO_HIDDEN' => 'Ukryty', + 'PHOTO_HIDDEN_EXPL' => 'Anonymous users need a direct link to view this photo.', + 'PHOTO_DOWNLOADABLE' => 'Pobieranie', + 'PHOTO_DOWNLOADABLE_EXPL' => 'Anonymous users may download this photo.', + 'PHOTO_SHARE_BUTTON_VISIBLE' => 'Udostępnianie', + 'PHOTO_SHARE_BUTTON_VISIBLE_EXPL' => 'Anonymous users can see social media sharing links.', + 'PHOTO_PASSWORD_PROT' => 'Zabezpieczony', + 'PHOTO_PASSWORD_PROT_EXPL' => 'Anonymous users need a shared password to view this photo.', + 'PHOTO_EDIT_SHARING_TEXT' => 'Ustawienia udostępniania tego zdjęcia zostaną zmienione na następujące:', + 'PHOTO_NO_EDIT_SHARING_TEXT' => 'Ponieważ zdjęcie znajduje się w albumie publicznym, dzieli jego ustawienia widoczności. Aktualna wartość widoczna jest poniżej (tylko informacyjnie).', + 'PHOTO_EDIT_GLOBAL_SHARING_TEXT' => 'Widoczność zdjęcia można dostosować używając globalnych ustawień Lychee. Aktualna wartość widoczna jest poniżej (tylko informacyjnie).', + 'PHOTO_NEW_CREATED_AT' => 'Enter the upload date for this photo. mm/dd/yyyy, hh:mm [am/pm]', + 'PHOTO_SET_CREATED_AT' => 'Set upload date', + + 'LOADING' => 'Wczytywanie', + 'ERROR' => 'Błąd', + 'ERROR_TEXT' => 'Ups, wygląda na to że coś poszło nie tak. Odśwież stronę i spróbuj ponownie!', + 'ERROR_UNKNOWN' => 'Wystąpił nieoczekiwany błąd. Spróbuj ponownie i sprawdź swoją instalację oraz serwer. Przejrzyj plik README dla bardziej szczegółowych informacji.', + 'ERROR_MAP_DEACTIVATED' => 'Funkcja mapy została wyłączona w ustawieniach.', + 'ERROR_SEARCH_DEACTIVATED' => 'Funkcja wyszkukiwania została wyłączona w ustawieniach.', + 'SUCCESS' => 'OK', + 'CHANGE_SUCCESS' => 'Change successful.', + 'RETRY' => 'Ponów', + 'OVERRIDE' => 'Override', + 'TAGS_OVERRIDE_INFO' => 'If this is unchecked, the tags will be added to the existing tags of the photo.', + + 'SETTINGS_SUCCESS_LOGIN' => 'Zaktualizowano informacje o loginie.', + 'SETTINGS_SUCCESS_SORT' => 'Zaktualizowano kolejność sortowania.', + 'SETTINGS_SUCCESS_DROPBOX' => 'Zaktualizowano klucz Dropbox.', + 'SETTINGS_SUCCESS_LANG' => 'Zaktualizowano język', + 'SETTINGS_SUCCESS_LAYOUT' => 'Zaktualizowano układ', + 'SETTINGS_SUCCESS_IMAGE_OVERLAY' => 'EXIF Overlay setting updated', + 'SETTINGS_SUCCESS_PUBLIC_SEARCH' => 'Zaktualizowano publiczne wyszukiwanie', + 'SETTINGS_SUCCESS_LICENSE' => 'Zaktualizowano domyślną licencję', + 'SETTINGS_SUCCESS_MAP_DISPLAY' => 'Zaktualizowano ustawienia wyświetlania mapy', + 'SETTINGS_SUCCESS_MAP_DISPLAY_PUBLIC' => 'Zaktualizowano ustawienia wyświetlania mapy dla albumów publicznych', + 'SETTINGS_SUCCESS_MAP_PROVIDER' => 'Zaktualizowano ustawienia dostawcy map', + 'SETTINGS_SUCCESS_CSS' => 'Stylesheets updated', + 'SETTINGS_SUCCESS_JS' => 'JS updated', + 'SETTINGS_SUCCESS_UPDATE' => 'Settings updated successfully', + 'SETTINGS_DROPBOX_KEY' => 'Dropbox API Key', + 'SETTINGS_ADVANCED_WARNING_EXPL' => 'Changing these advanced settings can be harmful to the stability, security and performance of this application. You should only modify them if you are sure of what you are doing.', + 'SETTINGS_ADVANCED_SAVE' => 'Save my modifications, I accept the risk!', + + 'U2F_NOT_SUPPORTED' => 'Brak obsługi U2F. Przepraszamy.', + 'U2F_NOT_SECURE' => 'Środowisko nie zostało zabezpieczone. U2F nie jest dostępne.', + 'U2F_REGISTER_KEY' => 'Zarejestruj nowe urządzenie.', + 'U2F_REGISTRATION_SUCCESS' => 'Rejestracja pomyślna!', + 'U2F_AUTHENTIFICATION_SUCCESS' => 'Autoryzacja pomyślna!', + 'U2F_CREDENTIALS' => 'Dane uwierzytelniające', + 'U2F_CREDENTIALS_DELETED' => 'Usunięto dane uwierzytelniające!', + 'U2F_LOGIN' => 'Log in with WebAuthn', + + 'NEW_PHOTOS_NOTIFICATION' => 'Send new photos notification emails.', + 'SETTINGS_SUCCESS_NEW_PHOTOS_NOTIFICATION' => 'New photos notification updated', + 'USER_EMAIL_INSTRUCTION' => 'Add your email below to enable receiving email notifications. To stop receiving emails, simply remove your email below.', + + 'LOGIN_USERNAME' => 'Nowa nazwa użytkownika', + 'LOGIN_PASSWORD' => 'Nowe hasło', + 'LOGIN_PASSWORD_CONFIRM' => 'Potwierdź nowe hasło', + 'PASSWORD_TITLE' => 'Wpisz aktualne dane dostępowe:', + 'PASSWORD_CURRENT' => 'Aktualne hasło', + 'PASSWORD_TEXT' => 'Twoja nazwa użytkownika oraz hasło zostaną zmienione na następujące:', + 'PASSWORD_CHANGE' => 'Zmień login', + + 'EDIT_SHARING_TITLE' => 'Edytuj udostępnianie', + 'EDIT_SHARING_TEXT' => 'Ustawienia udostępniania zostaną zmienione na następujące:', + 'SHARE_ALBUM_TEXT' => 'Album zostanie udostępniony z następującymi ustawieniami:', + + 'SORT_DIALOG_ATTRIBUTE_LABEL' => 'Attribute', + 'SORT_DIALOG_ORDER_LABEL' => 'Order', + + 'SORT_ALBUM_BY' => 'Sortuj albumy według pola %1$s w kolejności %2$s', + + 'SORT_ALBUM_SELECT_1' => 'data utworzenia', + 'SORT_ALBUM_SELECT_2' => 'tytuł', + 'SORT_ALBUM_SELECT_3' => 'opis', + 'SORT_ALBUM_SELECT_5' => 'Latest Take Date', + 'SORT_ALBUM_SELECT_6' => 'Oldest Take Date', + + 'SORT_PHOTO_BY' => 'Sortuj zdjęcia według pola %1$s w kolejności %2$s', + + 'SORT_PHOTO_SELECT_1' => 'data dodania', + 'SORT_PHOTO_SELECT_2' => 'Take Date', + 'SORT_PHOTO_SELECT_3' => 'tytuł', + 'SORT_PHOTO_SELECT_4' => 'opis', + 'SORT_PHOTO_SELECT_6' => 'oznaczony', + 'SORT_PHOTO_SELECT_7' => 'format', + + 'SORT_ASCENDING' => 'rosnącej', + 'SORT_DESCENDING' => 'malejącej', + 'SORT_CHANGE' => 'Zmień sortowanie', + + 'DROPBOX_TITLE' => 'Ustaw klucz Dropbox', + 'DROPBOX_TEXT' => "In order to import photos from your Dropbox, you need a valid drop-ins app key from their website. Generate yourself a personal key and enter it below:", + + 'LANG_TEXT' => 'Zmień język na:', + 'LANG_TITLE' => 'Zmień język', + + 'SETTING_RECENT_PUBLIC_TEXT' => 'Make "Recent" smart album accessible to anonymous users', + 'SETTING_STARRED_PUBLIC_TEXT' => 'Make "Starred" smart album accessible to anonymous users', + 'SETTING_ONTHISDAY_PUBLIC_TEXT' => 'Make "On This Day" smart album accessible to anonymous users', + + 'CSS_TEXT' => 'Personalize CSS:', + 'CSS_TITLE' => 'Change CSS', + 'JS_TEXT' => 'Custom JS:', + 'JS_TITLE' => 'Change JS', + 'PUBLIC_SEARCH_TEXT' => 'Public search allowed:', + 'OVERLAY_TYPE' => 'Photo overlay:', + 'OVERLAY_NONE' => 'None', + 'OVERLAY_EXIF' => 'EXIF data', + 'OVERLAY_DESCRIPTION' => 'Description', + 'OVERLAY_DATE' => 'Date taken', + 'ALBUM_DECORATION' => 'Album decorations:', + 'ALBUM_DECORATION_NONE' => 'None', + 'ALBUM_DECORATION_ORIGINAL' => 'Sub-album marker', + 'ALBUM_DECORATION_ALBUM' => 'Number of sub-albums', + 'ALBUM_DECORATION_PHOTO' => 'Number of photos', + 'ALBUM_DECORATION_ALL' => 'Number of sub-albums and photos', + 'ALBUM_DECORATION_ORIENTATION' => 'Orientation of album decorations:', + 'ALBUM_DECORATION_ORIENTATION_ROW' => 'Horizontal (photos, albums)', + 'ALBUM_DECORATION_ORIENTATION_ROW_REVERSE' => 'Horizontal (albums, photos)', + 'ALBUM_DECORATION_ORIENTATION_COLUMN' => 'Vertical (top photos, albums)', + 'ALBUM_DECORATION_ORIENTATION_COLUMN_REVERSE' => 'Vertical (top albums, photos)', + 'MAP_DISPLAY_TEXT' => 'Enable maps (provided by OpenStreetMap):', + 'MAP_DISPLAY_PUBLIC_TEXT' => 'Enable maps for public albums (provided by OpenStreetMap):', + 'MAP_PROVIDER' => 'Provider of OpenStreetMap tiles:', + 'MAP_PROVIDER_WIKIMEDIA' => 'Wikimedia', + 'MAP_PROVIDER_OSM_ORG' => 'OpenStreetMap.org (no HiDPI)', + 'MAP_PROVIDER_OSM_DE' => 'OpenStreetMap.de (no HiDPI)', + 'MAP_PROVIDER_OSM_FR' => 'OpenStreetMap.fr (no HiDPI)', + 'MAP_PROVIDER_RRZE' => 'University of Erlangen, Germany (only HiDPI)', + 'MAP_INCLUDE_SUBALBUMS_TEXT' => 'Include photos of subalbums on map:', + 'LOCATION_DECODING' => 'Decode GPS data into location name', + 'LOCATION_SHOW' => 'Pokaż lokalizację', + 'LOCATION_SHOW_PUBLIC' => 'Show location name for public mode', + + 'LAYOUT_TYPE' => 'Układ zdjęć:', + 'LAYOUT_SQUARES' => 'Kwadratowe miniaturki', + 'LAYOUT_JUSTIFIED' => 'Aspekt, wyrównane', + 'LAYOUT_MASONRY' => 'Aspekt, masonry', + 'LAYOUT_GRID' => 'Aspekt, grid', + 'LAYOUT_UNJUSTIFIED' => 'Aspekt, bez wyrównania', + 'SET_LAYOUT' => 'Zmień układ', + + 'NSFW_VISIBLE_TEXT_1' => 'Ustaw poufne albumy domyślnie widoczne.', + 'NSFW_VISIBLE_TEXT_2' => 'Jeśli album jest publiczny, wciąż jest dostępny, jedynie został ukryty do przeglądania i może zostać pokazany poprzez naciśnięcie H.', + 'SETTINGS_SUCCESS_NSFW_VISIBLE' => 'Domyślne ustawienie dotyczące widoczności albumów poufnych ostało zaktualizowane.', + + 'NSFW_BANNER' => '

Sensitive content

This album contains sensitive content which some people may find offensive or disturbing.

Tap to consent.

', + 'NSFW_HEADER' => 'Sensitive content', + 'NSFW_EXPLANATION' => 'This album contains sensitive content which some people may find offensive or disturbing.', + 'TAP_CONSENT' => 'Tap to consent.', + + 'VIEW_NO_RESULT' => 'Brak wyników', + 'VIEW_NO_PUBLIC_ALBUMS' => 'Brak albumów publicznych', + 'VIEW_NO_CONFIGURATION' => 'Brak konfiguracji', + 'VIEW_PHOTO_NOT_FOUND' => 'Zdjęcie nie zostało znalezione', + + 'NO_TAGS' => 'Brak tagów', + + 'UPLOAD_MANAGE_NEW_PHOTOS' => 'Możesz już zarządzać nowymi zdjęciami.', + 'UPLOAD_COMPLETE' => 'Zakońcozno wgrywanie', + 'UPLOAD_COMPLETE_FAILED' => 'Nie udało się wgrać jednego lub więcej plików.', + 'UPLOAD_IMPORTING' => 'Importowanie', + 'UPLOAD_IMPORTING_URL' => 'Importowanie URL', + 'UPLOAD_UPLOADING' => 'Wgrywanie', + 'UPLOAD_FINISHED' => 'Ukończono', + 'UPLOAD_PROCESSING' => 'Przetwarzanie', + 'UPLOAD_FAILED' => 'Nie udało się', + 'UPLOAD_FAILED_ERROR' => 'Wgrywanie nie powiodło się. Serwer zwrócił błąd!', + 'UPLOAD_FAILED_WARNING' => 'Wgrywanie nie powiodło się. Serwer zwrócił ostrzeżenie!', + 'UPLOAD_CANCELLED' => 'Cancelled', + 'UPLOAD_SKIPPED' => 'Pominięto', + 'UPLOAD_UPDATED' => 'Updated', + 'UPLOAD_GENERAL' => 'General', + 'UPLOAD_IMPORT_SKIPPED_DUPLICATE' => 'This photo has been skipped because it’s already in your library.', + 'UPLOAD_IMPORT_RESYNCED_DUPLICATE' => 'This photo has been skipped because it’s already in your library, but its metadata has been updated.', + 'UPLOAD_ERROR_CONSOLE' => 'Proszę przejrzeć konsolę błędów przeglądarki aby spradzić szczegóły.', + 'UPLOAD_UNKNOWN' => 'Server returned an unknown response. Please take a look at the console of your browser for further details.', + 'UPLOAD_ERROR_UNKNOWN' => 'Wgrywanie nie powiodło się. Server returned an unkown error!', + 'UPLOAD_ERROR_POSTSIZE' => 'Wgrywanie nie powiodło się. Wartość post_max_size w PHP jest zbyt niska!', + 'UPLOAD_ERROR_FILESIZE' => 'Wgrywanie nie powiodło się. Wartość upload_max_filesize w PHP jest zbyt niska!', + 'UPLOAD_IN_PROGRESS' => 'Lychee jest w trakcie wgrywania!', + 'UPLOAD_IMPORT_WARN_ERR' => 'The import has been finished, but returned warnings or errors. Please take a look at the log (Settings -> Show Log) for further details.', + 'UPLOAD_IMPORT_COMPLETE' => 'Import complete', + 'UPLOAD_IMPORT_INSTR' => 'Please enter the direct link to a photo to import it:', + 'UPLOAD_IMPORT' => 'Import', + 'UPLOAD_IMPORT_SERVER' => 'Importing from server', + 'UPLOAD_IMPORT_SERVER_FOLD' => 'Folder empty or no readable files to process. Please take a look at the log (Settings -> Show Log) for further details.', + 'UPLOAD_IMPORT_SERVER_INSTR' => 'Import all photos, folders and sub-folders located in the folders with the following absolute paths (on server). Paths are space separated, use \\ to escape a space in a path.', + 'UPLOAD_ABSOLUTE_PATH' => 'Absolute path to directories, space separated', + 'UPLOAD_IMPORT_SERVER_EMPT' => 'Could not start import because the folder was empty!', + 'UPLOAD_IMPORT_DELETE_ORIGINALS' => 'Delete originals', + 'UPLOAD_IMPORT_DELETE_ORIGINALS_EXPL' => 'Original files will be deleted after the import when possible.', + 'UPLOAD_IMPORT_VIA_SYMLINK' => 'Symbolic links', + 'UPLOAD_IMPORT_VIA_SYMLINK_EXPL' => 'Import files using symbolic links to originals.', + 'UPLOAD_IMPORT_SKIP_DUPLICATES' => 'Skip duplicates', + 'UPLOAD_IMPORT_SKIP_DUPLICATES_EXPL' => 'Existing media files are skipped.', + 'UPLOAD_IMPORT_RESYNC_METADATA' => 'Re-sync metadata', + 'UPLOAD_IMPORT_RESYNC_METADATA_EXPL' => 'Update metadata of existing media files.', + 'UPLOAD_IMPORT_LOW_MEMORY_EXPL' => 'The import process on the server is approaching the memory limit and may end up being terminated prematurely.', + 'UPLOAD_WARNING' => 'Warning', + 'UPLOAD_IMPORT_NOT_A_DIRECTORY' => 'The given path is not a readable directory!', + 'UPLOAD_IMPORT_PATH_RESERVED' => 'The given path is a reserved path of Lychee!', + 'UPLOAD_IMPORT_FAILED' => 'Could not import the file!', + 'UPLOAD_IMPORT_UNSUPPORTED' => 'Unsupported file type!', + 'UPLOAD_IMPORT_CANCELLED' => 'Import cancelled', + + 'ABOUT_SUBTITLE' => 'Self-hosted photo-management done right', + 'ABOUT_DESCRIPTION' => 'Lychee is a free photo-management tool, which runs on your server or web-space. Installing is a matter of seconds. Upload, manage and share photos like from a native application. Lychee comes with everything you need and all your photos are stored securely.', + 'FOOTER_COPYRIGHT' => 'All images on this website are subject to copyright by %1$s © %2$s', + 'HOSTED_WITH_LYCHEE' => 'Hosted with Lychee', + + 'URL_COPY_TO_CLIPBOARD' => 'Kopiuj do schowka', + 'URL_COPIED_TO_CLIPBOARD' => 'Skopiowano URL do schowka!', + 'PHOTO_DIRECT_LINKS_TO_IMAGES' => 'Direct links to image files:', + 'PHOTO_ORIGINAL' => 'Original', + 'PHOTO_MEDIUM' => ' Średnie', + 'PHOTO_MEDIUM_HIDPI' => 'Medium HiDPI', + 'PHOTO_SMALL' => 'Miniaturka', + 'PHOTO_SMALL_HIDPI' => 'Thumb HiDPI', + 'PHOTO_THUMB' => 'Kwadratowa miniaturka', + 'PHOTO_THUMB_HIDPI' => 'Square thumb HiDPI', + 'PHOTO_PLACEHOLDER' => 'Low Quality Image Placeholder', + 'PHOTO_THUMBNAIL' => 'Photo thumbnail', + 'PHOTO_LIVE_VIDEO' => 'Video part of live-photo', + 'PHOTO_VIEW' => 'Lychee Photo View:', + + 'PHOTO_EDIT_ROTATECWISE' => 'Obróć w prawo', + 'PHOTO_EDIT_ROTATECCWISE' => 'Obróć w lewo', + + 'ERROR_GPX' => 'Error loading GPX file: ', + 'ERROR_EITHER_ALBUMS_OR_PHOTOS' => 'Please select either albums or photos!', + 'ERROR_COULD_NOT_FIND' => 'Could not find what you want.', + 'ERROR_INVALID_EMAIL' => 'Not a valid email address.', + 'EMAIL_SUCCESS' => 'Email updated!', + 'ERROR_PHOTO_NOT_FOUND' => 'Error: photo %s not found !', + 'ERROR_EMPTY_USERNAME' => 'new username cannot be empty.', + 'ERROR_PASSWORD_DOES_NOT_MATCH' => 'new password does not match.', + 'ERROR_EMPTY_PASSWORD' => 'new password cannot be empty.', + 'ERROR_SELECT_ALBUM' => 'Select an album to share!', + 'ERROR_SELECT_USER' => 'Select a user to share with!', + 'ERROR_SELECT_SHARING' => 'Select a sharing to remove!', + 'SHARING_SUCCESS' => 'Sharing updated!', + 'SHARING_REMOVED' => 'Sharing removed!', + 'USER_CREATED' => 'User created!', + 'USER_DELETED' => 'User deleted!', + 'USER_UPDATED' => 'User updated!', + 'ENTER_EMAIL' => 'Enter your email address:', + 'ERROR_ALBUM_JSON_NOT_FOUND' => 'Error: Album json not found!', + 'ERROR_ALBUM_NOT_FOUND' => 'Error: album %s not found', + 'ERROR_DROPBOX_KEY' => 'Error: Dropbox key not set', + 'ERROR_SESSION' => 'Session expired.', + 'CAMERA_DATE' => 'Camera date', + 'NEW_PASSWORD' => 'new password', + 'ALLOW_UPLOADS' => 'Allow uploads', + 'ALLOW_USER_SELF_EDIT' => 'Allow self-management of user account', + 'OSM_CONTRIBUTORS' => 'OpenStreetMap contributors', +]; diff --git a/lang/pl/maintenance.php b/lang/pl/maintenance.php new file mode 100644 index 00000000000..f86de3d6f46 --- /dev/null +++ b/lang/pl/maintenance.php @@ -0,0 +1,60 @@ + 'Maintenance', + 'description' => 'You will find on this page, all the required actions to keep your Lychee installation running smooth and nicely.', + 'cleaning' => [ + 'title' => 'Cleaning %s', + 'result' => '%s deleted.', + 'description' => 'Remove all contents from %s', + 'button' => 'Clean', + ], + 'fix-jobs' => [ + 'title' => 'Fixing Jobs History', + 'description' => 'Mark jobs with status %s or %s as %s.', + 'button' => 'Fix job history', + ], + 'gen-sizevariants' => [ + 'title' => 'Missing %s', + 'description' => 'Found %d %s that could be generated.', + 'button' => 'Generate!', + 'success' => 'Successfully generated %d %s.', + ], + 'fill-filesize-sizevariants' => [ + 'title' => 'File sizes missing', + 'description' => 'Found %d small variants without file size.', + 'button' => 'Fetch data!', + 'success' => 'Successfully computed sizes of %d small variants.', + ], + 'fix-tree' => [ + 'title' => 'Tree statistics', + 'Oddness' => 'Oddness', + 'Duplicates' => 'Duplicates', + 'Wrong parents' => 'Wrong parents', + 'Missing parents' => 'Missing parents', + 'button' => 'Fix tree', + ], + 'optimize' => [ + 'title' => 'Optimize Database', + 'description' => 'If you notice slowdown in your installation, it may be because your database does not + have all its needed index.', + 'button' => 'Optimize Database', + ], + 'update' => [ + 'title' => 'Updates', + 'check-button' => 'Check for updates', + 'update-button' => 'Update', + 'no-pending-updates' => 'No pending update.', + ], +]; \ No newline at end of file diff --git a/lang/pl/profile.php b/lang/pl/profile.php new file mode 100644 index 00000000000..cc24b97452c --- /dev/null +++ b/lang/pl/profile.php @@ -0,0 +1,64 @@ + 'Profile', + + 'login' => [ + 'header' => 'Profile', + 'enter_current_password' => 'Enter your current password:', + 'current_password' => 'Current password', + 'credentials_update' => 'Your credentials will be changed to the following:', + 'username' => 'Username', + 'new_password' => 'New password', + 'confirm_new_password' => 'Confirm new password', + 'email_instruction' => 'Add your email below to enable receiving email notifications. To stop receiving emails, simply remove your email below.', + 'email' => 'Email', + 'change' => 'Change Login', + 'api_token' => 'API Token ...', + + 'missing_fields' => 'Missing fields', + ], + + 'token' => [ + 'unavailable' => 'You have already viewed this token.', + 'no_data' => 'No token API have been generated.', + 'disable' => 'Disable', + 'disabled' => 'Token disabled', + 'warning' => 'This token will not be displayed again. Copy it and keep it in a safe place.', + 'reset' => 'Reset the token', + 'create' => 'Create a new token', + ], + + 'oauth' => [ + 'header' => 'OAuth', + 'header_not_available' => 'OAuth is not available', + 'setup_env' => 'Set up the credentials in your .env', + 'token_registered' => '%s token registered.', + 'setup' => 'Set up %s', + 'reset' => 'reset', + 'credential_deleted' => 'Credential deleted!', + ], + + 'u2f' => [ + 'header' => 'Passkey/MFA/2FA', + 'info' => 'This only provides the ability to use WebAuthn to authenticate instead of username & password.', + 'empty' => 'Credentials list is empty!', + 'not_secure' => 'Environment not secured. U2F not available.', + 'new' => 'Register new device.', + 'credential_deleted' => 'Credential deleted!', + 'credential_updated' => 'Credential updated!', + 'credential_registred' => 'Registration successful!', + '5_chars' => 'At least 5 chars.', + ], +]; \ No newline at end of file diff --git a/lang/pl/settings.php b/lang/pl/settings.php new file mode 100644 index 00000000000..fd197f11135 --- /dev/null +++ b/lang/pl/settings.php @@ -0,0 +1,92 @@ + 'Settings', + 'small_screen' => 'For better a experience on the Settings page,
we recommend you use a larger screen.', + 'tabs' => [ + 'basic' => 'Basic', + 'all_settings' => 'All settings', + ], + 'toasts' => [ + 'change_saved' => 'Change saved!', + 'details' => 'Settings have been modified as per request', + 'error' => 'Error!', + 'error_load_css' => 'Could not load dist/user.css', + 'error_load_js' => 'Could not load dist/custom.js', + 'error_save_css' => 'Could not save CSS', + 'error_save_js' => 'Could not save JS', + 'thank_you' => 'Thank you for your support.', + 'reload' => 'Reload your page for full functionalities.', + ], + 'system' => [ + 'header' => 'System', + 'use_dark_mode' => 'Use dark mode for Lychee', + 'language' => 'Language used by Lychee', + 'nsfw_album_visibility' => 'Make Sensitive albums visible by default.', + 'nsfw_album_explanation' => 'If the album is public, it is still accessible, just hidden from the view and can be revealed by pressing H.', + ], + 'lychee_se' => [ + 'header' => 'Lychee SE', + 'call4action' => 'Get exclusive features and support the development of Lychee. Unlock the SE edition.', + 'preview' => 'Enable preview of Lychee SE features', + 'hide_call4action' => 'Hide this Lychee SE registration form. I am happy with Lychee as-is. :)', + 'hide_warning' => 'If enabled, the only way to register your license key will be via the More tab above. Changes are applied on page reload.', + ], + 'dropbox' => [ + 'header' => 'Dropbox', + 'instruction' => 'In order to import photos from your Dropbox, you need a valid drop-ins app key from their website.', + 'api_key' => 'Dropbox API Key', + 'set_key' => 'Set Dropbox Key', + ], + 'gallery' => [ + 'header' => 'Gallery', + 'photo_order_column' => 'Default column used for sorting photos', + 'photo_order_direction' => 'Default order used for sorting photos', + 'album_order_column' => 'Default column used for sorting albums', + 'album_order_direction' => 'Default order used for sorting albums', + 'aspect_ratio' => 'Default aspect ratio for album thumbs', + 'photo_layout' => 'Layout for pictures', + 'album_decoration' => 'Show decorations on album cover (sub-album and/or photo count)', + 'album_decoration_direction' => 'Align album decorations horizontally or vertically', + 'photo_overlay' => 'Default image overlay information', + 'license_default' => 'Default license used for albums', + 'license_help' => 'Need help choosing?', + ], + 'geolocation' => [ + 'header' => 'Geo-location', + 'map_display' => 'Display the map given GPS coordinates', + 'map_display_public' => 'Allow anonymous users to access the map', + 'map_provider' => 'Defines the map provider', + 'map_include_subalbums' => 'Includes pictures of the sub albums on the map', + 'location_decoding' => 'Use GPS location decoding', + 'location_show' => 'Show location extracted from GPS coordinates', + 'location_show_public' => 'Anonymous users can access the extracted location from GPS coordinates', + ], + 'advanced' => [ + 'header' => 'Advanced Customization', + 'change_css' => 'Change CSS', + 'change_js' => 'Change JS', + ], + 'all' => [ + 'old_setting_style' => 'Old setting style', + 'change_detected' => 'Some settings changed.', + 'save' => 'Save', + ], + + 'tool_option' => [ + 'disabled' => 'disabled', + 'enabled' => 'enabled', + 'discover' => 'discover', + ], +]; \ No newline at end of file diff --git a/lang/pl/sharing.php b/lang/pl/sharing.php new file mode 100644 index 00000000000..69de18cc6d0 --- /dev/null +++ b/lang/pl/sharing.php @@ -0,0 +1,33 @@ + 'Sharing', + + 'info' => 'This page gives an overview of and the ability to edit the sharing rights associated with albums.', + 'album_title' => 'Album title', + 'username' => 'Username', + 'no_data' => 'Sharing list is empty.', + 'share' => 'Share', + 'permission_deleted' => 'Permission deleted!', + 'permission_created' => 'Permission created!', + + 'grants' => [ + 'read' => 'Grants read access', + 'original' => 'Grants access to original photo', + 'download' => 'Grants download', + 'upload' => 'Grants upload', + 'edit' => 'Grants edit', + 'delete' => 'Grants delete', + ], +]; \ No newline at end of file diff --git a/lang/pl/statistics.php b/lang/pl/statistics.php new file mode 100644 index 00000000000..2baf855bbd5 --- /dev/null +++ b/lang/pl/statistics.php @@ -0,0 +1,34 @@ + 'Statistics', + + 'preview_text' => 'This is a preview of the statistics page available in Lychee SE.
The data shown here are randomly generated and do not reflect your server.', + 'no_data' => 'User does not have data on server.', + 'collapse' => 'Collapse albums sizes', + + 'total' => [ + 'total' => 'Total', + 'albums' => 'Albums', + 'photos' => 'Photos', + 'size' => 'Size', + ], + 'table' => [ + 'username' => 'Owner', + 'title' => 'Title', + 'photos' => 'Photos', + 'descendants' => 'Children', + 'size' => 'Size', + ], +]; \ No newline at end of file diff --git a/lang/pl/toasts.php b/lang/pl/toasts.php new file mode 100644 index 00000000000..293d4b72594 --- /dev/null +++ b/lang/pl/toasts.php @@ -0,0 +1,17 @@ + 'Error', + 'success' => 'Success', +]; \ No newline at end of file diff --git a/lang/pl/users.php b/lang/pl/users.php new file mode 100644 index 00000000000..599bb833454 --- /dev/null +++ b/lang/pl/users.php @@ -0,0 +1,44 @@ + 'Users', + 'description' => 'Here you can manage the users of your Lychee installation. You can create, edit and delete users.', + 'create' => 'Create a new user', + 'username' => 'Username', + 'password' => 'Password', + 'legend' => 'Legend', + 'upload_rights' => 'When selected, the user can upload content.', + 'edit_rights' => 'When selected, the user can modify their profile (username, password).', + 'quota' => 'When set, the user has a space quota for pictures (in kB).', + + 'user_deleted' => 'User deleted', + 'user_created' => 'User created', + 'user_updated' => 'User updated', + 'change_saved' => 'Change saved!', + + 'create_edit' => [ + 'upload_rights' => 'User can upload content.', + 'edit_rights' => 'User can modify their profile (username, password).', + 'quota' => 'User has quota limit.', + 'quota_kb' => 'quota in kB (0 for default)', + 'note' => 'Admin note (not publically visible)', + 'create' => 'Create', + 'edit' => 'Edit', + ], + 'line' => [ + 'admin' => 'admin user', + 'edit' => 'Edit', + 'delete' => 'Delete', + ], +]; \ No newline at end of file diff --git a/lang/pt/aspect_ratio.php b/lang/pt/aspect_ratio.php new file mode 100644 index 00000000000..2c7e8fb56ac --- /dev/null +++ b/lang/pt/aspect_ratio.php @@ -0,0 +1,21 @@ + '5/4 (instagram landscape)', + '4by5' => '4/5 (instagram portrait)', + '2by3' => '2/3 (portrait)', + '3by2' => '3/2 (landscape)', + '1by1' => 'square', + '1byx9' => '16/9 (landscape)', +]; \ No newline at end of file diff --git a/lang/pt/diagnostics.php b/lang/pt/diagnostics.php new file mode 100644 index 00000000000..0fadd640428 --- /dev/null +++ b/lang/pt/diagnostics.php @@ -0,0 +1,30 @@ + 'Diagnostics', + + 'copy_to_clipboard' => 'Copy diagnostics to clipboard', + 'self-diagnosis' => 'Self-diagnosis', + 'info' => 'Info', + 'space' => 'Space', + 'load_space' => 'Load space usage.', + 'configuration' => 'Configuration', + 'loading' => 'Loading...', + 'identical_content' => 'Identical content', + + 'toast' => [ + 'info' => 'Info', + 'copy' => 'Diagnostics copied to clipboard!', + ], +]; \ No newline at end of file diff --git a/lang/pt/dialogs.php b/lang/pt/dialogs.php new file mode 100644 index 00000000000..4afd65fae3f --- /dev/null +++ b/lang/pt/dialogs.php @@ -0,0 +1,221 @@ + [ + 'close' => 'Close', + 'cancel' => 'Cancel', + 'save' => 'Save', + 'delete' => 'Delete', + 'move' => 'Move', + ], + 'about' => [ + 'subtitle' => 'Self-hosted photo-management done right', + 'description' => 'Lychee is a free photo-management tool, which runs on your server or web-space. Installing is a matter of seconds. Upload, manage and share photos like from a native application. Lychee comes with everything you need and all your photos are stored securely.', + 'update_available' => 'Update available!', + 'thank_you' => 'Thank you for your support!', + 'get_supporter_or_register' => 'Get exclusive features and support the development of Lychee.
Unlock the Supporter Edition or register your License key', + 'here' => 'here', + ], + 'dropbox' => [ + 'not_configured' => 'Dropbox is not configured.', + ], + 'import_from_link' => [ + 'instructions' => 'Please enter the direct link to a photo to import it:', + 'import' => 'Import', + ], + 'keybindings' => [ + 'don_t_show_again' => 'Don\'t show this again', + 'side_wide' => 'Site-wide Shortcuts', + 'back_cancel' => 'Back/Cancel', + 'confirm' => 'Confirm', + 'login' => 'Login', + 'toggle_full_screen' => 'Toggle Full Screen', + 'toggle_sensitive_albums' => 'Toggle Sensitive Albums', + + 'albums' => 'Albums Shortcuts', + 'new_album' => 'New Album', + 'upload_photos' => 'Upload Photos', + 'search' => 'Search', + 'show_this_modal' => 'Show this modal', + 'select_all' => 'Select All', + 'move_selection' => 'Move Selection', + 'delete_selection' => 'Delete Selection', + + 'album' => 'Album Shortcuts', + 'slideshow' => 'Start/Stop Slideshow', + 'toggle' => 'Toggle panel', + + 'photo' => 'Photo Shortcuts', + 'previous' => 'Previous photo', + 'next' => 'Next photo', + 'cycle' => 'Cycle overlay mode', + 'star' => 'Star the photo', + 'move' => 'Move the photo', + 'delete' => 'Delete the photo', + 'edit' => 'Edit information', + 'show_hide_meta' => 'Show information', + + 'keep_hidden' => 'We will keep it hidden.', + ], + 'login' => [ + 'username' => 'Username', + 'password' => 'Password', + 'unknown_invalid' => 'Unknown user or invalid password.', + 'signin' => 'Sign-In', + ], + 'register' => [ + 'enter_license' => 'Enter your license key below:', + 'license_key' => 'License key', + 'invalid_license' => 'Invalid license key.', + 'register' => 'Register', + ], + 'share_album' => [ + 'url_copied' => 'Copied URL to clipboard!', + ], + 'upload' => [ + 'completed' => 'Completed', + 'uploaded' => 'Uploaded:', + 'release' => 'Release file to upload!', + 'select' => 'Click here to select files to upload', + 'drag' => '(Or drag files to the page)', + 'loading' => 'Loading', + 'resume' => 'Resume', + 'uploading' => 'Uploading', + 'finished' => 'Finished', + 'failed_error' => 'Upload failed. The server returned an error!', + ], + 'visibility' => [ + 'public' => 'Public', + 'public_expl' => 'Anonymous users can access this album, subject to the restrictions below.', + 'full' => 'Original', + 'full_expl' => 'Anonymous users can view full-resolution photos.', + 'hidden' => 'Hidden', + 'hidden_expl' => 'Anonymous users need a direct link to access this album.', + 'downloadable' => 'Downloadable', + 'downloadable_expl' => 'Anonymous users can download this album.', + 'password' => 'Password', + 'password_prot' => 'Password protected', + 'password_prot_expl' => 'Anonymous users need a shared password to access this album.', + 'nsfw' => 'Sensitive', + 'nsfw_expl' => 'Album contains sensitive content.', + 'visibility_updated' => 'Visibility updated.', + ], + 'move_album' => [ + 'confirm_single' => 'Are you sure you want to move the album “%1$s” into the album “%2$s”?', + 'confirm_multiple' => 'Are you sure you want to move all selected albums into the album “%s”?', + 'move_single' => 'Move Album', + 'move_to' => 'Move to', + 'move_to_single' => 'Move %s to:', + 'move_to_multiple' => 'Move %d albums to:', + 'no_album_target' => 'No album to move to', + 'moved_single' => 'Album moved!', + 'moved_single_details' => '%1$s moved to %2$s', + 'moved_details' => 'Album(s) moved to %s', + ], + 'new_album' => [ + 'menu' => 'Create Album', + 'info' => 'Enter a title for the new album:', + 'title' => 'title', + 'create' => 'Create Album', + ], + 'new_tag_album' => [ + 'menu' => 'Create Tag Album', + 'info' => 'Enter a title for the new tag album:', + 'title' => 'title', + 'set_tags' => 'Set tags to show', + 'warn' => 'Make sure to press enter after each tag', + 'create' => 'Create Tag Album', + ], + 'delete_album' => [ + 'confirmation' => 'Are you sure you want to delete the album “%s” and all of the photos it contains?', + 'confirmation_multiple' => 'Are you sure you want to delete all %d selected albums and all of the photos they contain?', + 'warning' => 'This action can not be undone!', + 'delete' => 'Delete Album and Photos', + ], + 'transfer' => [ + 'query' => 'Transfer ownership of album to', + 'confirmation' => 'Are you sure you want to transfer the ownership of album “%s” and all the photos it contains to "%s"?', + 'lost_access_warning' => 'Your access to this album will be lost.', + 'warning' => 'This action can not be undone!', + 'transfer' => 'Transfer ownership of album and photos', + ], + 'rename' => [ + 'photo' => 'Enter a new title for this photo:', + 'album' => 'Enter a new title for this album:', + 'rename' => 'Rename', + ], + 'merge' => [ + 'merge_to' => 'Merge %s to:', + 'merge_to_multiple' => 'Merge %d albums to:', + 'no_albums' => 'No albums to merge to.', + 'confirm' => 'Are you sure you want to merge the album “%1$s” into the album “%2$s”?', + 'confirm_multiple' => 'Are you sure you want to merge all selected albums into the album “%s”?', + 'merge' => 'Merge Albums', + 'merged' => 'Album(s) merged to %s!', + ], + 'unlock' => [ + 'password_required' => 'This album is protected by a password. Enter the password below to view the photos of this album:', + 'password' => 'Password', + 'unlock' => 'Unlock', + ], + 'photo_tags' => [ + 'question' => 'Enter your tags for this photo.', + 'question_multiple' => 'Enter your tags for all %d selected photos. Existing tags will be overwritten.', + 'no_tags' => 'No Tags', + 'set_tags' => 'Set Tags', + 'updated' => 'Tags updated!', + 'tags_override_info' => 'If this is unchecked, the tags will be added to the existing tags of the photo.', + ], + 'photo_copy' => [ + 'no_albums' => 'No albums to copy to', + 'copy_to' => 'Copy %s to:', + 'copy_to_multiple' => 'Copy %d photos to:', + 'confirm' => 'Copy %s to %s.', + 'confirm_multiple' => 'Copy %d photos to %s.', + 'copy' => 'Copy', + 'copied' => 'Photo(s) copied!', + ], + 'photo_delete' => [ + 'confirm' => 'Are you sure you want to delete the photo “%s”?', + 'confirm_multiple' => 'Are you sure you want to delete all %d selected photos?', + 'deleted' => 'Photo(s) deleted!', + ], + 'move_photo' => [ + 'move_single' => 'Move %s to:', + 'move_multiple' => 'Move %d photos to:', + 'confirm' => 'Move %s to %s.', + 'confirm_multiple' => 'Move %d photos to %s.', + 'moved' => 'Photo(s) moved to %s!', + ], + 'target_user' => [ + 'placeholder' => 'Select user', + ], + 'target_album' => [ + 'placeholder' => 'Select album', + ], + 'webauthn' => [ + 'u2f' => 'U2F', + 'success' => 'Authentication successful!', + 'error' => 'Whoops, it looks like something went wrong. Please reload the site and try again!', + ], + 'se' => [ + 'available' => 'Available in the Supporter Edition', + ], + 'session_expired' => [ + 'title' => 'Session expired', + 'message' => 'Your session has expired.
Please reload the page.', + 'reload' => 'Reload', + 'go_to_gallery' => 'Go to the Gallery', + ], +]; \ No newline at end of file diff --git a/lang/pt/fix-tree.php b/lang/pt/fix-tree.php new file mode 100644 index 00000000000..64803e310e6 --- /dev/null +++ b/lang/pt/fix-tree.php @@ -0,0 +1,55 @@ + 'Maintenance', + 'intro' => 'This page allows you to re-order and fix your albums manually.
Before any modifications, we strongly recommend you to read about Nested Set tree structures.', + 'warning' => 'You can really break your Lychee installation here, modify values at your own risks.', + + 'help' => [ + 'header' => 'Help', + 'hover' => 'Hover ids or titles to highlight related albums.', + 'left' => 'Left', + 'right' => 'Right', + 'convenience' => 'For your convenience, the and buttons allow you to change the values of %s and %s by respectively +1 and -1 with propagation.', + 'left-right-warn' => 'The and indicates that the value of %s (and respectively %s) is duplicated somewhere.', + 'parent-marked' => 'Marked Parent Id indicates that the %s and %s do not satisfy the Nest Set tree structures. Edit either the Parent Id or the %s/%s values.', + 'slowness' => 'This page will be slow with a large number of albums.', + ], + + 'buttons' => [ + 'reset' => 'Reset', + 'check' => 'Check', + 'apply' => 'Apply', + ], + + 'table' => [ + 'title' => 'Title', + 'left' => 'Left', + 'right' => 'Right', + 'id' => 'Id', + 'parent' => 'Parent Id', + ], + + 'errors' => [ + 'invalid' => 'Invalid tree!', + 'invalid_details' => 'We are not applying this as it is guaranteed to be a broken state.', + 'invalid_left' => 'Album %s has an invalid left value.', + 'invalid_right' => 'Album %s has an invalid right value.', + 'invalid_left_right' => 'Album %s has an invalid left/right values. Left should be strictly smaller than right: %s < %s.', + 'duplicate_left' => 'Album %s has a duplicate left value %s.', + 'duplicate_right' => 'Album %s has a duplicate right value %s.', + 'parent' => 'Album %s has an unexpected parent id %s.', + 'unknown' => 'Album %s has an unknown error.', + ], +]; \ No newline at end of file diff --git a/lang/pt/gallery.php b/lang/pt/gallery.php new file mode 100644 index 00000000000..eb8008827e0 --- /dev/null +++ b/lang/pt/gallery.php @@ -0,0 +1,241 @@ + 'Gallery', + + 'smart_albums' => 'Smart albums', + 'albums' => 'Albums', + 'root' => 'Albums', + + 'original' => 'Original', + 'medium' => 'Medium', + 'medium_hidpi' => 'Medium HiDPI', + 'small' => 'Thumb', + 'small_hidpi' => 'Thumb HiDPI', + 'thumb' => 'Square thumb', + 'thumb_hidpi' => 'Square thumb HiDPI', + 'placeholder' => 'Low Quality Image Placeholder', + 'thumbnail' => 'Photo thumbnail', + 'live_video' => 'Video part of live-photo', + + 'camera_data' => 'Camera date', + 'album_reserved' => 'All Rights Reserved', + + 'map' => [ + 'error_gpx' => 'Error loading GPX file', + 'osm_contributors' => 'OpenStreetMap contributors', + ], + + 'search' => [ + 'title' => 'Search', + 'searching' => 'Searching…', + 'no_results' => 'Nothing matches your search query.', + 'searchbox' => 'Search…', + 'minimum_chars' => 'Minimum %s characters required.', + 'photos' => 'Photos (%s)', + 'albums' => 'Albums (%s)', + ], + + 'smart_album' => [ + 'unsorted' => 'Unsorted', + 'starred' => 'Starred', + 'recent' => 'Recent', + 'public' => 'Public', + 'on_this_day' => 'On This Day', + ], + + 'layout' => [ + 'squares' => 'Square thumbnails', + 'justified' => 'With aspect, justified', + 'masonry' => 'With aspect, masonry', + 'grid' => 'With aspect, grid', + ], + + 'overlay' => [ + 'none' => 'None', + 'exif' => 'EXIF data', + 'description' => 'Description', + 'date' => 'Date taken', + ], + + 'timeline' => [ + 'default' => 'default', + 'disabled' => 'disabled', + 'year' => 'Year', + 'month' => 'Month', + 'day' => 'Day', + 'hour' => 'Hour', + ], + + 'album' => [ + 'header_albums' => 'Albums', + 'header_photos' => 'Photos', + 'no_results' => 'Nothing to see here', + 'upload' => 'Upload photos', + + 'tabs' => [ + 'about' => 'About Album', + 'share' => 'Share Album', + 'move' => 'Move Album', + 'danger' => 'DANGER ZONE', + ], + + 'hero' => [ + 'created' => 'Created', + 'copyright' => 'Copyright', + 'subalbums' => 'Subalbums', + 'images' => 'Photos', + 'download' => 'Download Album', + 'share' => 'Share Album', + 'stats_only_se' => 'Statistics available in the Supporter Edition', + ], + + 'stats' => [ + 'lens' => 'Lens', + 'shutter' => 'Shutter speed', + 'iso' => 'ISO', + 'model' => 'Model', + 'aperture' => 'Aperture', + 'no_data' => 'No data', + ], + + 'properties' => [ + 'title' => 'Title', + 'description' => 'Description', + 'photo_ordering' => 'Order photos by', + 'children_ordering' => 'Order albums by', + 'asc/desc' => 'asc/desc', + 'header' => 'Set album header', + 'compact_header' => 'Use compact header', + 'license' => 'Set license', + 'copyright' => 'Set copyright', + 'aspect_ratio' => 'Set album thumbs aspect ratio', + 'album_timeline' => 'Set album timeline mode', + 'photo_timeline' => 'Set photo timeline mode', + 'layout' => 'Set photo layout', + 'show_tags' => 'Set tags to show', + 'tags_required' => 'Tags are required.', + ], + ], + + 'photo' => [ + 'actions' => [ + 'star' => 'Star', + 'unstar' => 'Unstar', + 'set_album_header' => 'Set as album header', + 'move' => 'Move', + 'delete' => 'Delete', + 'header_set' => 'Header set', + ], + + 'details' => [ + 'about' => 'About', + 'basics' => 'Basics', + 'title' => 'Title', + 'uploaded' => 'Uploaded', + 'description' => 'Description', + 'license' => 'License', + 'reuse' => 'Reuse', + 'latitude' => 'Latitude', + 'longitude' => 'Longitude', + 'altitude' => 'Altitude', + 'location' => 'Location', + 'image' => 'Image', + 'video' => 'Video', + 'size' => 'Size', + 'format' => 'Format', + 'resolution' => 'Resolution', + 'duration' => 'Duration', + 'fps' => 'Frame rate', + 'tags' => 'Tags', + 'camera' => 'Camera', + 'captured' => 'Captured', + 'make' => 'Make', + 'type' => 'Type/Model', + 'lens' => 'Lens', + 'shutter' => 'Shutter Speed', + 'aperture' => 'Aperture', + 'focal' => 'Focal Length', + 'iso' => 'ISO %s', + ], + + 'edit' => [ + 'set_title' => 'Set Title', + 'set_description' => 'Set Description', + 'set_license' => 'Set License', + 'no_tags' => 'No Tags', + 'set_tags' => 'Set Tags', + 'set_created_at' => 'Set Upload Date', + ], + ], + + 'nsfw' => [ + 'header' => 'Sensitive content', + 'description' => 'This album contains sensitive content which some people may find offensive or disturbing.', + 'consent' => 'Tap to consent.', + ], + + 'menus' => [ + 'star' => 'Star', + 'unstar' => 'Unstar', + 'star_all' => 'Star Selected', + 'unstar_all' => 'Unstar Selected', + 'tag' => 'Tag', + 'tag_all' => 'Tag Selected', + 'set_cover' => 'Set Album Cover', + 'remove_header' => 'Remove Album Header', + 'set_header' => 'Set Album Header', + 'copy_to' => 'Copy to …', + 'copy_all_to' => 'Copy Selected to …', + 'rename' => 'Rename', + 'move' => 'Move', + 'move_all' => 'Move Selected', + 'delete' => 'Delete', + 'delete_all' => 'Delete Selected', + 'download' => 'Download', + 'download_all' => 'Download Selected', + 'merge' => 'Merge', + 'merge_all' => 'Merge Selected', + + 'upload_photo' => 'Upload Photo', + 'import_link' => 'Import from Link', + 'import_dropbox' => 'Import from Dropbox', + 'new_album' => 'New Album', + 'new_tag_album' => 'New Tag Album', + 'upload_track' => 'Upload track', + 'delete_track' => 'Delete track', + ], + + 'sort' => [ + 'photo_select_1' => 'Upload Time', + 'photo_select_2' => 'Take Date', + 'photo_select_3' => 'Title', + 'photo_select_4' => 'Description', + 'photo_select_6' => 'Star', + 'photo_select_7' => 'Photo Format', + 'ascending' => 'Ascending', + 'descending' => 'Descending', + 'album_select_1' => 'Creation Time', + 'album_select_2' => 'Title', + 'album_select_3' => 'Description', + 'album_select_5' => 'Latest Take Date', + 'album_select_6' => 'Oldest Take Date', + ], + + 'albums_protection' => [ + 'private' => 'private', + 'public' => 'public', + 'inherit_from_parent' => 'inherit from parent', + ], +]; \ No newline at end of file diff --git a/lang/pt/jobs.php b/lang/pt/jobs.php new file mode 100644 index 00000000000..5d952b76012 --- /dev/null +++ b/lang/pt/jobs.php @@ -0,0 +1,18 @@ + 'Jobs', + + 'no_data' => 'No Jobs have been executed yet.', +]; \ No newline at end of file diff --git a/lang/pt/landing.php b/lang/pt/landing.php new file mode 100644 index 00000000000..fe6fe55b8ea --- /dev/null +++ b/lang/pt/landing.php @@ -0,0 +1,19 @@ + 'Gallery', + 'access_gallery' => 'Access the gallery', + 'hosted_with_lychee' => 'Hosted with Lychee', + 'copyright' => 'All images on this website are subject to copyright by %1$s © %2$s', +]; \ No newline at end of file diff --git a/lang/pt/left-menu.php b/lang/pt/left-menu.php new file mode 100644 index 00000000000..9a3e91f4037 --- /dev/null +++ b/lang/pt/left-menu.php @@ -0,0 +1,29 @@ + 'Back to Gallery', + + 'admin' => 'Admin', + 'clockwork' => 'Clockwork App', + 'logs' => 'Show Logs', + 'jobs' => 'Show Job History', + 'user' => 'User', + + 'sign_out' => 'Sign Out', + + 'about' => 'About', + 'api' => 'API Documentation', + 'source_code' => 'Source Code', + 'support' => 'Support', +]; \ No newline at end of file diff --git a/lang/pt/lychee.php b/lang/pt/lychee.php new file mode 100644 index 00000000000..0aede470acf --- /dev/null +++ b/lang/pt/lychee.php @@ -0,0 +1,535 @@ + 'Nome de utilizador', + 'PASSWORD' => 'Password', + 'ENTER' => 'Inserir', + 'CANCEL' => 'Cancelar', + 'CONFIRM' => 'Confirm', + 'SIGN_IN' => 'Iniciar Sessão', + 'CLOSE' => 'Fechar', + 'SETTINGS' => 'Configurações', + 'SEARCH' => 'Pesquisar …', + 'MORE' => 'Mais', + 'DEFAULT' => 'Predefinição', + 'GALLERY' => 'Gallery', + + 'USERS' => 'Utilizadores', + 'PROFILE' => 'Profile', + 'CREATE' => 'Create', + 'REMOVE' => 'Remove', + 'SHARE' => 'Share', + 'U2F' => 'U2F', + 'NOTIFICATIONS' => 'Notifications', + 'SHARING' => 'Partilha', + 'CHANGE_LOGIN' => 'Alterar Login', + 'CHANGE_SORTING' => 'Alterar Ordenação', + 'SET_DROPBOX' => 'Escolher Dropbox', + 'ABOUT_LYCHEE' => 'Acerca do Lychee', + 'DIAGNOSTICS' => 'Diagnosticos', + 'DIAGNOSTICS_GET_SIZE' => 'Pedir utilização de espaço', + 'JOBS' => 'Show job history', + 'LOGS' => 'Mostrar Logs', + 'SIGN_OUT' => 'Terminar Sessão', + 'UPDATE_AVAILABLE' => 'Atualização disponível!', + 'MIGRATION_AVAILABLE' => 'Migração disponível!', + 'CHECK_FOR_UPDATE' => 'Check for updates', + 'DEFAULT_LICENSE' => 'Default licença for new uploads:', + 'SET_LICENSE' => 'Escolher Licença', + 'SET_OVERLAY_TYPE' => 'Escolher Overlay', + 'SET_ALBUM_DECORATION' => 'Set album decorations', + 'SET_MAP_PROVIDER' => 'Escolher OpenStreetMap tiles provider', + 'FULL_SETTINGS' => 'Full Settings', + 'UPDATE' => 'Update', + 'RESET' => 'Reset', + 'DISABLE_TOKEN_TOOLTIP' => 'Disable', + 'ENABLE_TOKEN' => 'Enable API token', + 'DISABLED_TOKEN_STATUS_MSG' => 'Disabled', + 'TOKEN_BUTTON' => 'API Token ...', + 'TOKEN_NOT_AVAILABLE' => 'You have already viewed this token.', + 'TOKEN_WAIT' => 'Wait ...', + + 'SMART_ALBUMS' => 'Smart álbums', + 'SHARED_ALBUMS' => 'Álbums partilhados', + 'ALBUMS' => 'Álbums', + 'PHOTOS' => 'Fotos', + 'SEARCH_RESULTS' => 'Resultados da pesquisa', + + 'RENAME' => 'Renomear', + 'RENAME_ALL' => 'Renomear Seleção', + 'MERGE' => 'Unir', + 'MERGE_ALL' => 'Unir Seleção', + 'MAKE_PUBLIC' => 'Tornar Público', + 'SHARE_ALBUM' => 'Partilhar Álbum', + 'SHARE_PHOTO' => 'Partilhar Fotografia', + 'VISIBILITY_ALBUM' => 'Visibilidade do Álbum', + 'VISIBILITY_PHOTO' => 'Visibilidade da Fotografia', + 'DOWNLOAD_ALBUM' => 'Transferir Álbum', + 'ABOUT_ALBUM' => 'Acerca do Álbum', + 'DELETE_ALBUM' => 'Eliminar Álbum', + 'MOVE_ALBUM' => 'Moverr Álbum', + 'FULLSCREEN_ENTER' => 'Entrar em Tela Cheia', + 'FULLSCREEN_EXIT' => 'Sair da Tela Cheia', + + 'SHARING_ALBUM_USERS' => 'Partilhar este álbum com utilizadores', + 'WAIT_FETCH_DATA' => 'Por favor aguarde enquanto transferimos os dados …', + 'SHARING_ALBUM_USERS_NO_USERS' => 'Não há utilizadores com quem partilhar o álbum', + 'SHARING_ALBUM_USERS_LONG_MESSAGE' => 'Selecione os utilizadores com quem quer partilhar este álbum', + + 'DELETE_ALBUM_QUESTION' => 'Eliminar Álbum e Fotos', + 'KEEP_ALBUM' => 'Manter Álbum', + 'DELETE_ALBUM_CONFIRMATION' => 'De certeza que quer eliminar o álbum “%s” e todas as fotografias contidas? Esta ação não pode ser desfeita!', + + 'DELETE_TAG_ALBUM_QUESTION' => 'Delete Album', + 'DELETE_TAG_ALBUM_CONFIRMATION' => 'Are you sure you want to delete the album “%s” (any photos inside will not be deleted)? This action can’t be undone!', + + 'DELETE_ALBUMS_QUESTION' => 'Eliminar Álbums e Fotos', + 'KEEP_ALBUMS' => 'Manter Álbums', + 'DELETE_ALBUMS_CONFIRMATION' => 'De certeza que quer eliminar todos %d os álbums e todas as fotografias neles contidas? Esta ação não pode ser desfeita!', + + 'DELETE_UNSORTED_CONFIRM' => 'De certeza que quer eliminar todas as fotografias de “Desordenadas”? Esta ação não pode ser desfeita!', + 'CLEAR_UNSORTED' => 'Limpar Desordenadas', + 'KEEP_UNSORTED' => 'Manter Desordenadas', + + 'EDIT_SHARING' => 'Editar Sharing', + 'MAKE_PRIVATE' => 'Tornar Privado', + + 'CLOSE_ALBUM' => 'Fechar Álbum', + 'CLOSE_PHOTO' => 'Fechar Fotografia', + 'CLOSE_MAP' => 'Fechar Mapa', + + 'ADD' => 'Adicionar', + 'MOVE' => 'Mover', + 'MOVE_ALL' => 'Mover Selecionadas', + 'DUPLICATE' => 'Duplicar', + 'DUPLICATE_ALL' => 'Duplicar Selecionadas', + 'COPY_TO' => 'Copiar para …', + 'COPY_ALL_TO' => 'Copiar Selecionadas para …', + 'DELETE' => 'Eliminar', + 'SAVE' => 'Save', + 'DELETE_ALL' => 'Eliminar Selecionadas', + 'DOWNLOAD' => 'Transferir', + 'DOWNLOAD_ALL' => 'Transferir Selecionadas', + 'UPLOAD_PHOTO' => 'Enviar Fotografia', + 'IMPORT_LINK' => 'Importar a partir de um Link', + 'IMPORT_DROPBOX' => 'Importar do Dropbox', + 'IMPORT_SERVER' => 'Importar de um Servidor', + 'NEW_ALBUM' => 'Novo Álbum', + 'NEW_TAG_ALBUM' => 'Nova Etiqueta de Álbum', + 'UPLOAD_TRACK' => 'Upload track', + 'DELETE_TRACK' => 'Delete track', + + 'TITLE_NEW_ALBUM' => 'Insira um título para o novo álbum:', + 'UNTITLED' => 'Sem Título', + 'UNSORTED' => 'Desordenadas', + 'STARRED' => 'Favoritas', + 'RECENT' => 'Recentes', + 'PUBLIC' => 'Públicas', + 'ON_THIS_DAY' => 'On This Day', + 'NUM_PHOTOS' => 'Fotografias', + + 'CREATE_ALBUM' => 'Criar Álbum', + 'CREATE_TAG_ALBUM' => 'Criar Etiqueta Álbum', + + 'STAR_PHOTO' => 'Marcar como Favorita', + 'STAR' => 'Favorita', + 'UNSTAR' => 'Unstar', + 'STAR_ALL' => 'Marcar Selecionadas como Favoritas', + 'UNSTAR_ALL' => 'Unstar Selected', + 'TAG' => 'Etiqueta', + 'TAG_ALL' => 'Etiquetar Selecionadas', + 'UNSTAR_PHOTO' => 'Desmarcar como Favorita', + 'SET_COVER' => 'Escolher para Capa de Álbum', + 'REMOVE_COVER' => 'Remover Capa de Álbum', + 'SET_HEADER' => 'Set Album Header', + 'REMOVE_HEADER' => 'Remove Album Header', + 'SET_COMPACT_HEADER' => 'Use Compact Header', + + 'FULL_PHOTO' => 'Abrir Original', + 'ABOUT_PHOTO' => 'Acerca da Fotografia', + 'DISPLAY_FULL_MAP' => 'Mapa', + 'DIRECT_LINK' => 'Link Direto', + 'DIRECT_LINKS' => 'Links Diretos', + 'QR_CODE' => 'QR Code', + + 'ALBUM_ABOUT' => 'Acerca de', + 'ALBUM_BASICS' => 'Básicos', + 'ALBUM_TITLE' => 'Título', + 'ALBUM_COPYRIGHT' => 'Copyright', + 'ALBUM_SET_COPYRIGHT' => 'Set copyright', + 'ALBUM_NEW_TITLE' => 'Inserir novo título para este álbum:', + 'ALBUMS_NEW_TITLE' => 'Inserir um título para todos %d álbums selecionados:', + 'ALBUM_SET_TITLE' => 'Escolher Título', + 'ALBUM_DESCRIPTION' => 'Descrição', + 'ALBUM_SHOW_TAGS' => 'Etiquetas para mostrar', + 'ALBUM_NEW_DESCRIPTION' => 'Inserir uma nova descrição para este álbum:', + 'ALBUM_SET_DESCRIPTION' => 'Escolher Descrição', + 'ALBUM_NEW_SHOWTAGS' => 'Inserir etiquetas de fotografias que irão ficar visíveis neste álbum:', + 'ALBUM_SET_SHOWTAGS' => 'Escolher etiquetas a mostrar', + 'ALBUM_ALBUM' => 'Álbum', + 'ALBUM_CREATED' => 'Criado', + 'ALBUM_IMAGES' => 'Imagens', + 'ALBUM_VIDEOS' => 'Videos', + 'ALBUM_SUBALBUMS' => 'Subálbums', + 'ALBUM_SHARING' => 'Partilhar', + 'ALBUM_SHR_YES' => 'SIM', + 'ALBUM_SHR_NO' => 'Não', + 'ALBUM_PUBLIC' => 'Público', + 'ALBUM_PUBLIC_EXPL' => 'Anonymous users can access this album, subject to the restrictions below.', + 'ALBUM_FULL' => 'Original', + 'ALBUM_FULL_EXPL' => 'Anonymous users can behold full-resolution photos.', + 'ALBUM_HIDDEN' => 'Oculto', + 'ALBUM_HIDDEN_EXPL' => 'Anonymous users need a direct link to access this album.', + 'ALBUM_MARK_NSFW' => 'Marcar álbum como sensível', + 'ALBUM_UNMARK_NSFW' => 'Desmarcar álbum como sensível', + 'ALBUM_NSFW' => 'Sensível', + 'ALBUM_NSFW_EXPL' => 'Álbum está marcado como contendo conteúdo sensível.', + 'ALBUM_DOWNLOADABLE' => 'Transferível', + 'ALBUM_DOWNLOADABLE_EXPL' => 'Anonymous users can download this album.', + 'ALBUM_SHARE_BUTTON_VISIBLE' => 'Botão Partilhar está visível', + 'ALBUM_SHARE_BUTTON_VISIBLE_EXPL' => 'Anonymous users can see social media sharing links.', + 'ALBUM_PASSWORD' => 'Password', + 'ALBUM_PASSWORD_PROT' => 'Protegido por password', + 'ALBUM_PASSWORD_PROT_EXPL' => 'Anonymous users need a shared password to access this album.', + 'ALBUM_PASSWORD_REQUIRED' => 'Este álbum está protegido por password. Inserir a password para ver as fotografias deste álbum:', + 'ALBUM_MERGE' => 'De certeza que quer fundir o álbum “%1$s” com o álbum “%2$s”?', + 'ALBUMS_MERGE' => 'De certeza que quer fundir todos os álbums selecionados com o álbum “%s”?', + 'MERGE_ALBUM' => 'Fundir Álbums', + 'DONT_MERGE' => 'Não Fundir', + 'ALBUM_MOVE' => 'De certeza que quer mover o álbum “%1$s” para o álbum “%2$s”?', + 'ALBUMS_MOVE' => 'De certeza que quer mover todos os álbums selecionados para o álbum “%s”?', + 'MOVE_ALBUMS' => 'Mover Álbums', + 'NOT_MOVE_ALBUMS' => 'Não Mover', + 'ROOT' => 'Álbums', + 'ALBUM_REUSE' => 'Reutilizar', + 'ALBUM_LICENSE' => 'Licença', + 'ALBUM_SET_LICENSE' => 'Escolher Licença', + 'ALBUM_LICENSE_HELP' => 'Precisa de ajuda para escolher?', + 'ALBUM_LICENSE_NONE' => 'Nenhuma', + 'ALBUM_RESERVED' => 'Todos os Direitos Reservados', + 'ALBUM_SET_ORDER' => 'Escolher Ordem', + 'ALBUM_ORDERING' => 'Ordenar por', + 'ALBUM_PHOTO_ORDERING' => 'Order photos by', + 'ALBUM_CHILDREN_ORDERING' => 'Order albums by', + 'ALBUM_OWNER' => 'Owner', + + 'PHOTO_ABOUT' => 'Acerca de', + 'PHOTO_BASICS' => 'Básicos', + 'PHOTO_TITLE' => 'Título', + 'PHOTO_NEW_TITLE' => 'Inserir um novo título para esta fotografia:', + 'PHOTO_SET_TITLE' => 'Escolher Título', + 'PHOTO_UPLOADED' => 'Enviada', + 'PHOTO_DESCRIPTION' => 'Descrição', + 'PHOTO_NEW_DESCRIPTION' => 'Inserir uma nova descrição para esta fotografia:', + 'PHOTO_SET_DESCRIPTION' => 'Escolher Descrição', + 'PHOTO_NEW_LICENSE' => 'Adicionar uma Licença', + 'PHOTO_SET_LICENSE' => 'Escolher Licença', + 'PHOTO_LICENSE' => 'Licença', + 'PHOTO_LICENSE_HELP' => 'Need help choosing?', + 'PHOTO_REUSE' => 'Reutilizar', + 'PHOTO_LICENSE_NONE' => 'Nenhuma', + 'PHOTO_RESERVED' => 'Todos os Direitos Reservados', + 'PHOTO_LATITUDE' => 'Latitude', + 'PHOTO_LONGITUDE' => 'Longitude', + 'PHOTO_ALTITUDE' => 'Altitude', + 'PHOTO_IMGDIRECTION' => 'Direção', + 'PHOTO_LOCATION' => 'Localização', + 'PHOTO_IMAGE' => 'Imagem', + 'PHOTO_VIDEO' => 'Vídeo', + 'PHOTO_SIZE' => 'Tamanho', + 'PHOTO_FORMAT' => 'Formato', + 'PHOTO_RESOLUTION' => 'Resolução', + 'PHOTO_DURATION' => 'Duração', + 'PHOTO_FPS' => 'Frame rate', + 'PHOTO_TAGS' => 'Etiquetas', + 'PHOTO_NOTAGS' => 'Sem Etiquetas', + 'PHOTO_NEW_TAGS' => 'Inserir as suas etiquetas para esta fotografia. Pode adicionar várias etiquetas separando-as com uma vírgula:', + 'PHOTOS_NEW_TAGS' => 'Inserir as suas etiquetas para todas %d as fotografias selecionadas. Etiquetas existentes vão ser substituídas. Pode adicionar várias etiquetas separando-as com uma vírgula:', + 'PHOTO_SET_TAGS' => 'Escolher Etiquetas', + 'PHOTO_CAMERA' => 'Camera', + 'PHOTO_CAPTURED' => 'Capturada', + 'PHOTO_MAKE' => 'Criada', + 'PHOTO_TYPE' => 'Tipo/Modelo', + 'PHOTO_LENS' => 'Lente', + 'PHOTO_SHUTTER' => 'Velocidade do Obturador', + 'PHOTO_APERTURE' => 'Abertura', + 'PHOTO_FOCAL' => 'Distância Focal', + 'PHOTO_ISO' => 'ISO %s', + 'PHOTO_SHARING' => 'Partilhada', + 'PHOTO_DELETE' => 'Eliminar Fotografia', + 'PHOTO_KEEP' => 'Manter Fotografia', + 'PHOTO_DELETE_CONFIRMATION' => 'De certeza que quer eliminar a fotografia “%s”? Esta ação não pode ser desfeita!', + 'PHOTO_DELETE_ALL' => 'De certeza que quer eliminar todas %d as fotografias selecionadas? Esta ação não pode ser desfeita!', + 'PHOTOS_NEW_TITLE' => 'Inserir um título para todas %d as fotografias selecionadas:', + 'PHOTO_MAKE_PRIVATE_ALBUM' => 'Esta fotografia está localizada num álbum público. Para tornar esta fotografia privada ou pública, edite a visibilidade do álbum associado.', + 'PHOTO_SHOW_ALBUM' => 'Mostrar Álbum', + 'PHOTO_PUBLIC' => 'Público', + 'PHOTO_PUBLIC_EXPL' => 'Anonymous users can view this photo, subject to the restrictions below.', + 'PHOTO_FULL' => 'Original', + 'PHOTO_FULL_EXPL' => 'Anonymous users can behold full-resolution photo.', + 'PHOTO_HIDDEN' => 'Oculta', + 'PHOTO_HIDDEN_EXPL' => 'Anonymous users need a direct link to view this photo.', + 'PHOTO_DOWNLOADABLE' => 'Transferível', + 'PHOTO_DOWNLOADABLE_EXPL' => 'Anonymous users may download this photo.', + 'PHOTO_SHARE_BUTTON_VISIBLE' => 'Botão Partilhar está visível', + 'PHOTO_SHARE_BUTTON_VISIBLE_EXPL' => 'Anonymous users can see social media sharing links.', + 'PHOTO_PASSWORD_PROT' => 'Protegido por password', + 'PHOTO_PASSWORD_PROT_EXPL' => 'Anonymous users need a shared password to view this photo.', + 'PHOTO_EDIT_SHARING_TEXT' => 'As propriedades de partilha desta fotografia vão ser alteradas para o seguinte:', + 'PHOTO_NO_EDIT_SHARING_TEXT' => 'Porque esta fotografia está localizada num álbum público, herda as configurações de visibilidade desse álbum. A sua visibilidade atual é mostrada abaixo apenas como informação.', + 'PHOTO_EDIT_GLOBAL_SHARING_TEXT' => 'A visibilidade desta fotografia pode ser afinada através das configurações globais do Lychee. A sua visibilidade atual é mostrada abaixo apenas como informação.', + 'PHOTO_NEW_CREATED_AT' => 'Enter the upload date for this photo. mm/dd/yyyy, hh:mm [am/pm]', + 'PHOTO_SET_CREATED_AT' => 'Set upload date', + + 'LOADING' => 'A carregar', + 'ERROR' => 'Erro', + 'ERROR_TEXT' => 'Whoops, parece que algo de errado aconteceu. Por favor, atualize a página e tente de novo!', + 'ERROR_UNKNOWN' => 'Algo de inesperado aconteceu. Por favor tente de novo e verifique a sua instalação e servidor. Dê uma vista de olhos ao readme para mais informação.', + 'ERROR_MAP_DEACTIVATED' => 'A funcionalidade do mapa foi desativada nas configurações.', + 'ERROR_SEARCH_DEACTIVATED' => 'A funcionalidade de procura foi desativada nas configurações.', + 'SUCCESS' => 'OK', + 'CHANGE_SUCCESS' => 'Change successful.', + 'RETRY' => 'Tentar de novo', + 'OVERRIDE' => 'Override', + 'TAGS_OVERRIDE_INFO' => 'If this is unchecked, the tags will be added to the existing tags of the photo.', + + 'SETTINGS_SUCCESS_LOGIN' => 'Informação de Login atualizada.', + 'SETTINGS_SUCCESS_SORT' => 'Ordenação atualizada.', + 'SETTINGS_SUCCESS_DROPBOX' => 'Dropbox Key atualizada.', + 'SETTINGS_SUCCESS_LANG' => 'Linguagem atualizada', + 'SETTINGS_SUCCESS_LAYOUT' => 'Layout atualizado', + 'SETTINGS_SUCCESS_IMAGE_OVERLAY' => 'Configuração de Overlay EXIF atualizada', + 'SETTINGS_SUCCESS_PUBLIC_SEARCH' => 'Pesquisa pública atualizada', + 'SETTINGS_SUCCESS_LICENSE' => 'licença predefinida atualizada', + 'SETTINGS_SUCCESS_MAP_DISPLAY' => 'Configuração da janela do mapa atualizada', + 'SETTINGS_SUCCESS_MAP_DISPLAY_PUBLIC' => 'Configuração da janela do mapa para álbums públicos atualizada', + 'SETTINGS_SUCCESS_MAP_PROVIDER' => 'Provider do mapa atualizado', + 'SETTINGS_SUCCESS_CSS' => 'Stylesheets updated', + 'SETTINGS_SUCCESS_JS' => 'JS updated', + 'SETTINGS_SUCCESS_UPDATE' => 'Settings updated successfully', + 'SETTINGS_DROPBOX_KEY' => 'Dropbox API Key', + 'SETTINGS_ADVANCED_WARNING_EXPL' => 'Changing these advanced settings can be harmful to the stability, security and performance of this application. You should only modify them if you are sure of what you are doing.', + 'SETTINGS_ADVANCED_SAVE' => 'Save my modifications, I accept the risk!', + + 'U2F_NOT_SUPPORTED' => 'U2F não suportado. Desculpe.', + 'U2F_NOT_SECURE' => 'Ambiente não seguro. U2F não disponível.', + 'U2F_REGISTER_KEY' => 'Registar novo dispositivo.', + 'U2F_REGISTRATION_SUCCESS' => 'Registo bem-sucedido!', + 'U2F_AUTHENTIFICATION_SUCCESS' => 'Authenticação bem-sucedida!', + 'U2F_CREDENTIALS' => 'Credenciais', + 'U2F_CREDENTIALS_DELETED' => 'Credenciais eliminadas!', + 'U2F_LOGIN' => 'Log in with WebAuthn', + + 'NEW_PHOTOS_NOTIFICATION' => 'Send new photos notification emails.', + 'SETTINGS_SUCCESS_NEW_PHOTOS_NOTIFICATION' => 'New photos notification updated', + 'USER_EMAIL_INSTRUCTION' => 'Add your email below to enable receiving email notifications. To stop receiving emails, simply remove your email below.', + + 'LOGIN_USERNAME' => 'Novo Nome de Utilizador', + 'LOGIN_PASSWORD' => 'Nova Password', + 'LOGIN_PASSWORD_CONFIRM' => 'Confirmar Password', + 'PASSWORD_TITLE' => 'Inserir a sua password atual:', + 'PASSWORD_CURRENT' => 'Password Atual', + 'PASSWORD_TEXT' => 'O seu nome de utilizador e password vão ser alterados para o seguinte:', + 'PASSWORD_CHANGE' => 'Alterar Login', + + 'EDIT_SHARING_TITLE' => 'Editar Partilha', + 'EDIT_SHARING_TEXT' => 'As propriedades de partilha deste álbum vão ser alteradas para o seguinte:', + 'SHARE_ALBUM_TEXT' => 'Este álbum vai ser partilhado com as seguintes propriedades:', + + 'SORT_DIALOG_ATTRIBUTE_LABEL' => 'Attribute', + 'SORT_DIALOG_ORDER_LABEL' => 'Order', + + 'SORT_ALBUM_BY' => 'Ordenar álbums por %1$s numa %2$s ordem.', + + 'SORT_ALBUM_SELECT_1' => 'Hora de Criação', + 'SORT_ALBUM_SELECT_2' => 'Título', + 'SORT_ALBUM_SELECT_3' => 'Descrição', + 'SORT_ALBUM_SELECT_5' => 'Data da Modificação Mais Recente', + 'SORT_ALBUM_SELECT_6' => 'Data da Modificação Mais Antiga', + + 'SORT_PHOTO_BY' => 'Ordenar fotografias por %1$s numa %2$s ordem.', + + 'SORT_PHOTO_SELECT_1' => 'Hora de Envio', + 'SORT_PHOTO_SELECT_2' => 'Data de Modificação', + 'SORT_PHOTO_SELECT_3' => 'Título', + 'SORT_PHOTO_SELECT_4' => 'Descrição', + 'SORT_PHOTO_SELECT_6' => 'Favorito', + 'SORT_PHOTO_SELECT_7' => 'Formato da Fotografia', + + 'SORT_ASCENDING' => 'Ascendente', + 'SORT_DESCENDING' => 'Descendente', + 'SORT_CHANGE' => 'Alterar Ordenação', + + 'DROPBOX_TITLE' => 'Escolher a Dropbox Key', + 'DROPBOX_TEXT' => "Para importar fotografias do seu Dropbox, precisa de uma key válida de drop-in do website deles. Crie uma key pessoal e insira-a abaixo:", + + 'LANG_TEXT' => 'Alterar língua do Lychee para:', + 'LANG_TITLE' => 'Alterar Linguagem', + + 'SETTING_RECENT_PUBLIC_TEXT' => 'Make "Recent" smart album accessible to anonymous users', + 'SETTING_STARRED_PUBLIC_TEXT' => 'Make "Starred" smart album accessible to anonymous users', + 'SETTING_ONTHISDAY_PUBLIC_TEXT' => 'Make "On This Day" smart album accessible to anonymous users', + + 'CSS_TEXT' => 'Personalize CSS:', + 'CSS_TITLE' => 'Change CSS', + 'JS_TEXT' => 'Custom JS:', + 'JS_TITLE' => 'Change JS', + 'PUBLIC_SEARCH_TEXT' => 'Pesquisa pública permitida:', + 'OVERLAY_TYPE' => 'Data a usar no overlay da imagem:', + 'OVERLAY_NONE' => 'None', + 'OVERLAY_EXIF' => 'Informação EXIF da fotografia', + 'OVERLAY_DESCRIPTION' => 'Descrição da fotografia', + 'OVERLAY_DATE' => 'Data da Fotografia', + 'ALBUM_DECORATION' => 'Album decorations:', + 'ALBUM_DECORATION_NONE' => 'None', + 'ALBUM_DECORATION_ORIGINAL' => 'Sub-album marker', + 'ALBUM_DECORATION_ALBUM' => 'Number of sub-albums', + 'ALBUM_DECORATION_PHOTO' => 'Number of photos', + 'ALBUM_DECORATION_ALL' => 'Number of sub-albums and photos', + 'ALBUM_DECORATION_ORIENTATION' => 'Orientation of album decorations:', + 'ALBUM_DECORATION_ORIENTATION_ROW' => 'Horizontal (photos, albums)', + 'ALBUM_DECORATION_ORIENTATION_ROW_REVERSE' => 'Horizontal (albums, photos)', + 'ALBUM_DECORATION_ORIENTATION_COLUMN' => 'Vertical (top photos, albums)', + 'ALBUM_DECORATION_ORIENTATION_COLUMN_REVERSE' => 'Vertical (top albums, photos)', + 'MAP_DISPLAY_TEXT' => 'Ligar mapas (fornecidos por OpenStreetMap):', + 'MAP_DISPLAY_PUBLIC_TEXT' => 'Permitir mapas para álbums públicos (fornecidos por OpenStreetMap):', + 'MAP_PROVIDER' => 'Fornecedor das janelas OpenStreetMap:', + 'MAP_PROVIDER_WIKIMEDIA' => 'Wikimedia', + 'MAP_PROVIDER_OSM_ORG' => 'OpenStreetMap.org (sem HiDPI)', + 'MAP_PROVIDER_OSM_DE' => 'OpenStreetMap.de (sem HiDPI)', + 'MAP_PROVIDER_OSM_FR' => 'OpenStreetMap.fr (sem HiDPI)', + 'MAP_PROVIDER_RRZE' => 'Universidade de Erlangen, Alemanha (apenas HiDPI)', + 'MAP_INCLUDE_SUBALBUMS_TEXT' => 'Incluír fotografias de subálbums no mapa:', + 'LOCATION_DECODING' => 'Transformar a informação GPS no nome da localização', + 'LOCATION_SHOW' => 'Mostrar nome da localização', + 'LOCATION_SHOW_PUBLIC' => 'Mostrar nome da localização no modo público', + + 'LAYOUT_TYPE' => 'Disposição das fotografias:', + 'LAYOUT_SQUARES' => 'Miniaturas quadradas', + 'LAYOUT_JUSTIFIED' => 'Com formatação, justificada', + 'LAYOUT_MASONRY' => 'Com formatação, masonry', + 'LAYOUT_GRID' => 'Com formatação, grid', + 'LAYOUT_UNJUSTIFIED' => 'Com formatação, não justificada', + 'SET_LAYOUT' => 'Alterar disposição', + + 'NSFW_VISIBLE_TEXT_1' => 'Tornar Sensível os álbums visíveis por defeito.', + 'NSFW_VISIBLE_TEXT_2' => 'Se o álbum é público, continua acessível, apenas ocultado da visualização e pode ser mostrado pressionando H.', + 'SETTINGS_SUCCESS_NSFW_VISIBLE' => 'Sensibilidade predefinida do álbum visível atualizada com sucesso.', + + 'NSFW_BANNER' => '

Sensitive content

This album contains sensitive content which some people may find offensive or disturbing.

Tap to consent.

', + 'NSFW_HEADER' => 'Sensitive content', + 'NSFW_EXPLANATION' => 'This album contains sensitive content which some people may find offensive or disturbing.', + 'TAP_CONSENT' => 'Tap to consent.', + + 'VIEW_NO_RESULT' => 'Sem resultados', + 'VIEW_NO_PUBLIC_ALBUMS' => 'Sem álbums públicos', + 'VIEW_NO_CONFIGURATION' => 'Sem configuração', + 'VIEW_PHOTO_NOT_FOUND' => 'Fotografia não encontrada', + + 'NO_TAGS' => 'Sem Etiquetas', + + 'UPLOAD_MANAGE_NEW_PHOTOS' => 'Pode agora gerir a(s) sua(s) nova(s) fotografia(s).', + 'UPLOAD_COMPLETE' => 'Envio completo', + 'UPLOAD_COMPLETE_FAILED' => 'Falha ao enviar uma ou mais fotografias.', + 'UPLOAD_IMPORTING' => 'A importar', + 'UPLOAD_IMPORTING_URL' => 'A importar URL', + 'UPLOAD_UPLOADING' => 'A enviar', + 'UPLOAD_FINISHED' => 'Acabado', + 'UPLOAD_PROCESSING' => 'A processar', + 'UPLOAD_FAILED' => 'Falhado', + 'UPLOAD_FAILED_ERROR' => 'Envio falhado. O servidor respondeu com um erro!', + 'UPLOAD_FAILED_WARNING' => 'Envio falhado. O servidor respondeu com um aviso!', + 'UPLOAD_CANCELLED' => 'Cancelado', + 'UPLOAD_SKIPPED' => 'Saltado', + 'UPLOAD_UPDATED' => 'Ignorado', + 'UPLOAD_GENERAL' => 'General', + 'UPLOAD_IMPORT_SKIPPED_DUPLICATE' => 'Esta fotografia foi ignorada porque já está na sua livraria.', + 'UPLOAD_IMPORT_RESYNCED_DUPLICATE' => 'Esta fotografia foi ignorada porque já está na sua livraria, mas os seus metadados foram atualizados.', + 'UPLOAD_ERROR_CONSOLE' => 'Por favor dá uma vista de olhos na consola do teu navegador para mais detalhes.', + 'UPLOAD_UNKNOWN' => 'O servidor respondeu com uma mensagem desconhecida. Por favor dá uma vista de olhos na consola do teu navegador para mais detalhes.', + 'UPLOAD_ERROR_UNKNOWN' => 'Envio falhado. O servidor respondeu com um erro desconhecido!', + 'UPLOAD_ERROR_POSTSIZE' => 'Envio falhado. O limite post_max_size do PHP é demasiado pequeno!', + 'UPLOAD_ERROR_FILESIZE' => 'Envio falhado. O limite upload_max_filesize do PHP é demasiado pequeno!', + 'UPLOAD_IN_PROGRESS' => 'O Lychee está a enviar ficheiros de momento!', + 'UPLOAD_IMPORT_WARN_ERR' => 'A importação acabou, mas foi respondida com avisos ou erros. Por favor, dá uma vista de olhos no log (Configurações -> Mostrar Log) para mais detalhes.', + 'UPLOAD_IMPORT_COMPLETE' => 'Importação completa', + 'UPLOAD_IMPORT_INSTR' => 'Por favor insere o link direto da fotografia para a importar:', + 'UPLOAD_IMPORT' => 'Importar', + 'UPLOAD_IMPORT_SERVER' => 'A importar do servidor', + 'UPLOAD_IMPORT_SERVER_FOLD' => 'Pasta vazia ou sem ficheiros legíveis para processar. Por favor, dá uma vista de olhos no log (Configurações -> Mostrar Log) para mais detalhes.', + 'UPLOAD_IMPORT_SERVER_INSTR' => 'Import all photos, folders and sub-folders located in the folders with the following absolute paths (on server). Paths are space separated, use \\ to escape a space in a path.', + 'UPLOAD_ABSOLUTE_PATH' => 'Absolute path to directories, space separated', + 'UPLOAD_IMPORT_SERVER_EMPT' => 'Não foi conseguido iniciar a importação porque a pasta estava vazia!', + 'UPLOAD_IMPORT_DELETE_ORIGINALS' => 'Eliminar originais', + 'UPLOAD_IMPORT_DELETE_ORIGINALS_EXPL' => 'Ficheiros originais vão ser eliminados depois da importação assim que possível.', + 'UPLOAD_IMPORT_VIA_SYMLINK' => 'Links Simbólicos', + 'UPLOAD_IMPORT_VIA_SYMLINK_EXPL' => 'Importar ficheiros utilizando links simbólicos dos links originais.', + 'UPLOAD_IMPORT_SKIP_DUPLICATES' => 'Saltar duplicados', + 'UPLOAD_IMPORT_SKIP_DUPLICATES_EXPL' => 'Ficheiros de multimédia existentes serão ignorados.', + 'UPLOAD_IMPORT_RESYNC_METADATA' => 'Re-sincronizar metadados', + 'UPLOAD_IMPORT_RESYNC_METADATA_EXPL' => 'Atualizar metadados dos ficheiros multimédia existentes.', + 'UPLOAD_IMPORT_LOW_MEMORY_EXPL' => 'O processo de importação no servidor está a chegar ao limite de memória e pode acabar por ser terminado de forma permatura.', + 'UPLOAD_WARNING' => 'Aviso', + 'UPLOAD_IMPORT_NOT_A_DIRECTORY' => 'O caminho indicado não é um diretório legível!', + 'UPLOAD_IMPORT_PATH_RESERVED' => 'O caminho indicado é um caminho reservado pelo Lychee!', + 'UPLOAD_IMPORT_FAILED' => 'Não foi conseguido importar o ficheiro!', + 'UPLOAD_IMPORT_UNSUPPORTED' => 'Tipo de ficheiro não suportado!', + 'UPLOAD_IMPORT_CANCELLED' => 'Importação cancelada', + + 'ABOUT_SUBTITLE' => 'Gestão de fotografias auto-hospedada e bem feita', + 'ABOUT_DESCRIPTION' => 'Lychee é uma ferramenta gratuita de gestão de fotografias, que corre no teu servidor ou espaço web. A instalação demora segundos. Enviar, gerir e partilhar fotografias como uma aplicação nativa. O Lychee vem com tudo o que precisas e todas as tuas fotografias são guardadas de forma segura.', + 'FOOTER_COPYRIGHT' => 'Todas as imagens neste website estão sujeitas a direitos autorais por %1$s © %2$s', + 'HOSTED_WITH_LYCHEE' => 'Hospedado com Lychee', + + 'URL_COPY_TO_CLIPBOARD' => 'Copiar para o clipboard', + 'URL_COPIED_TO_CLIPBOARD' => 'URL copiado para o clipboard!', + 'PHOTO_DIRECT_LINKS_TO_IMAGES' => 'Links diretos para os ficheiros de imagem:', + 'PHOTO_ORIGINAL' => 'Original', + 'PHOTO_MEDIUM' => 'Média', + 'PHOTO_MEDIUM_HIDPI' => 'Média HiDPI', + 'PHOTO_SMALL' => 'Pequena', + 'PHOTO_SMALL_HIDPI' => 'Pequena HiDPI', + 'PHOTO_THUMB' => 'Quadrada pequena', + 'PHOTO_THUMB_HIDPI' => 'Quadrada pequena HiDPI', + 'PHOTO_PLACEHOLDER' => 'Low Quality Image Placeholder', + 'PHOTO_THUMBNAIL' => 'Photo thumbnail', + 'PHOTO_LIVE_VIDEO' => 'Video parte de fotografia ao vivo', + 'PHOTO_VIEW' => 'Vista de Fotografia Lychee:', + + 'PHOTO_EDIT_ROTATECWISE' => 'Rodar no sentido dos ponteiros do relógio', + 'PHOTO_EDIT_ROTATECCWISE' => 'Rodar contra o sentido dos ponteiros do relógio', + + 'ERROR_GPX' => 'Error loading GPX file: ', + 'ERROR_EITHER_ALBUMS_OR_PHOTOS' => 'Please select either albums or photos!', + 'ERROR_COULD_NOT_FIND' => 'Could not find what you want.', + 'ERROR_INVALID_EMAIL' => 'Not a valid email address.', + 'EMAIL_SUCCESS' => 'Email updated!', + 'ERROR_PHOTO_NOT_FOUND' => 'Error: photo %s not found !', + 'ERROR_EMPTY_USERNAME' => 'new username cannot be empty.', + 'ERROR_PASSWORD_DOES_NOT_MATCH' => 'new password does not match.', + 'ERROR_EMPTY_PASSWORD' => 'new password cannot be empty.', + 'ERROR_SELECT_ALBUM' => 'Select an album to share!', + 'ERROR_SELECT_USER' => 'Select a user to share with!', + 'ERROR_SELECT_SHARING' => 'Select a sharing to remove!', + 'SHARING_SUCCESS' => 'Sharing updated!', + 'SHARING_REMOVED' => 'Sharing removed!', + 'USER_CREATED' => 'User created!', + 'USER_DELETED' => 'User deleted!', + 'USER_UPDATED' => 'User updated!', + 'ENTER_EMAIL' => 'Enter your email address:', + 'ERROR_ALBUM_JSON_NOT_FOUND' => 'Error: Album json not found!', + 'ERROR_ALBUM_NOT_FOUND' => 'Error: album %s not found', + 'ERROR_DROPBOX_KEY' => 'Error: Dropbox key not set', + 'ERROR_SESSION' => 'Session expired.', + 'CAMERA_DATE' => 'Camera date', + 'NEW_PASSWORD' => 'new password', + 'ALLOW_UPLOADS' => 'Allow uploads', + 'ALLOW_USER_SELF_EDIT' => 'Allow self-management of user account', + 'OSM_CONTRIBUTORS' => 'OpenStreetMap contributors', +]; diff --git a/lang/pt/maintenance.php b/lang/pt/maintenance.php new file mode 100644 index 00000000000..f86de3d6f46 --- /dev/null +++ b/lang/pt/maintenance.php @@ -0,0 +1,60 @@ + 'Maintenance', + 'description' => 'You will find on this page, all the required actions to keep your Lychee installation running smooth and nicely.', + 'cleaning' => [ + 'title' => 'Cleaning %s', + 'result' => '%s deleted.', + 'description' => 'Remove all contents from %s', + 'button' => 'Clean', + ], + 'fix-jobs' => [ + 'title' => 'Fixing Jobs History', + 'description' => 'Mark jobs with status %s or %s as %s.', + 'button' => 'Fix job history', + ], + 'gen-sizevariants' => [ + 'title' => 'Missing %s', + 'description' => 'Found %d %s that could be generated.', + 'button' => 'Generate!', + 'success' => 'Successfully generated %d %s.', + ], + 'fill-filesize-sizevariants' => [ + 'title' => 'File sizes missing', + 'description' => 'Found %d small variants without file size.', + 'button' => 'Fetch data!', + 'success' => 'Successfully computed sizes of %d small variants.', + ], + 'fix-tree' => [ + 'title' => 'Tree statistics', + 'Oddness' => 'Oddness', + 'Duplicates' => 'Duplicates', + 'Wrong parents' => 'Wrong parents', + 'Missing parents' => 'Missing parents', + 'button' => 'Fix tree', + ], + 'optimize' => [ + 'title' => 'Optimize Database', + 'description' => 'If you notice slowdown in your installation, it may be because your database does not + have all its needed index.', + 'button' => 'Optimize Database', + ], + 'update' => [ + 'title' => 'Updates', + 'check-button' => 'Check for updates', + 'update-button' => 'Update', + 'no-pending-updates' => 'No pending update.', + ], +]; \ No newline at end of file diff --git a/lang/pt/profile.php b/lang/pt/profile.php new file mode 100644 index 00000000000..cc24b97452c --- /dev/null +++ b/lang/pt/profile.php @@ -0,0 +1,64 @@ + 'Profile', + + 'login' => [ + 'header' => 'Profile', + 'enter_current_password' => 'Enter your current password:', + 'current_password' => 'Current password', + 'credentials_update' => 'Your credentials will be changed to the following:', + 'username' => 'Username', + 'new_password' => 'New password', + 'confirm_new_password' => 'Confirm new password', + 'email_instruction' => 'Add your email below to enable receiving email notifications. To stop receiving emails, simply remove your email below.', + 'email' => 'Email', + 'change' => 'Change Login', + 'api_token' => 'API Token ...', + + 'missing_fields' => 'Missing fields', + ], + + 'token' => [ + 'unavailable' => 'You have already viewed this token.', + 'no_data' => 'No token API have been generated.', + 'disable' => 'Disable', + 'disabled' => 'Token disabled', + 'warning' => 'This token will not be displayed again. Copy it and keep it in a safe place.', + 'reset' => 'Reset the token', + 'create' => 'Create a new token', + ], + + 'oauth' => [ + 'header' => 'OAuth', + 'header_not_available' => 'OAuth is not available', + 'setup_env' => 'Set up the credentials in your .env', + 'token_registered' => '%s token registered.', + 'setup' => 'Set up %s', + 'reset' => 'reset', + 'credential_deleted' => 'Credential deleted!', + ], + + 'u2f' => [ + 'header' => 'Passkey/MFA/2FA', + 'info' => 'This only provides the ability to use WebAuthn to authenticate instead of username & password.', + 'empty' => 'Credentials list is empty!', + 'not_secure' => 'Environment not secured. U2F not available.', + 'new' => 'Register new device.', + 'credential_deleted' => 'Credential deleted!', + 'credential_updated' => 'Credential updated!', + 'credential_registred' => 'Registration successful!', + '5_chars' => 'At least 5 chars.', + ], +]; \ No newline at end of file diff --git a/lang/pt/settings.php b/lang/pt/settings.php new file mode 100644 index 00000000000..fd197f11135 --- /dev/null +++ b/lang/pt/settings.php @@ -0,0 +1,92 @@ + 'Settings', + 'small_screen' => 'For better a experience on the Settings page,
we recommend you use a larger screen.', + 'tabs' => [ + 'basic' => 'Basic', + 'all_settings' => 'All settings', + ], + 'toasts' => [ + 'change_saved' => 'Change saved!', + 'details' => 'Settings have been modified as per request', + 'error' => 'Error!', + 'error_load_css' => 'Could not load dist/user.css', + 'error_load_js' => 'Could not load dist/custom.js', + 'error_save_css' => 'Could not save CSS', + 'error_save_js' => 'Could not save JS', + 'thank_you' => 'Thank you for your support.', + 'reload' => 'Reload your page for full functionalities.', + ], + 'system' => [ + 'header' => 'System', + 'use_dark_mode' => 'Use dark mode for Lychee', + 'language' => 'Language used by Lychee', + 'nsfw_album_visibility' => 'Make Sensitive albums visible by default.', + 'nsfw_album_explanation' => 'If the album is public, it is still accessible, just hidden from the view and can be revealed by pressing H.', + ], + 'lychee_se' => [ + 'header' => 'Lychee SE', + 'call4action' => 'Get exclusive features and support the development of Lychee. Unlock the SE edition.', + 'preview' => 'Enable preview of Lychee SE features', + 'hide_call4action' => 'Hide this Lychee SE registration form. I am happy with Lychee as-is. :)', + 'hide_warning' => 'If enabled, the only way to register your license key will be via the More tab above. Changes are applied on page reload.', + ], + 'dropbox' => [ + 'header' => 'Dropbox', + 'instruction' => 'In order to import photos from your Dropbox, you need a valid drop-ins app key from their website.', + 'api_key' => 'Dropbox API Key', + 'set_key' => 'Set Dropbox Key', + ], + 'gallery' => [ + 'header' => 'Gallery', + 'photo_order_column' => 'Default column used for sorting photos', + 'photo_order_direction' => 'Default order used for sorting photos', + 'album_order_column' => 'Default column used for sorting albums', + 'album_order_direction' => 'Default order used for sorting albums', + 'aspect_ratio' => 'Default aspect ratio for album thumbs', + 'photo_layout' => 'Layout for pictures', + 'album_decoration' => 'Show decorations on album cover (sub-album and/or photo count)', + 'album_decoration_direction' => 'Align album decorations horizontally or vertically', + 'photo_overlay' => 'Default image overlay information', + 'license_default' => 'Default license used for albums', + 'license_help' => 'Need help choosing?', + ], + 'geolocation' => [ + 'header' => 'Geo-location', + 'map_display' => 'Display the map given GPS coordinates', + 'map_display_public' => 'Allow anonymous users to access the map', + 'map_provider' => 'Defines the map provider', + 'map_include_subalbums' => 'Includes pictures of the sub albums on the map', + 'location_decoding' => 'Use GPS location decoding', + 'location_show' => 'Show location extracted from GPS coordinates', + 'location_show_public' => 'Anonymous users can access the extracted location from GPS coordinates', + ], + 'advanced' => [ + 'header' => 'Advanced Customization', + 'change_css' => 'Change CSS', + 'change_js' => 'Change JS', + ], + 'all' => [ + 'old_setting_style' => 'Old setting style', + 'change_detected' => 'Some settings changed.', + 'save' => 'Save', + ], + + 'tool_option' => [ + 'disabled' => 'disabled', + 'enabled' => 'enabled', + 'discover' => 'discover', + ], +]; \ No newline at end of file diff --git a/lang/pt/sharing.php b/lang/pt/sharing.php new file mode 100644 index 00000000000..69de18cc6d0 --- /dev/null +++ b/lang/pt/sharing.php @@ -0,0 +1,33 @@ + 'Sharing', + + 'info' => 'This page gives an overview of and the ability to edit the sharing rights associated with albums.', + 'album_title' => 'Album title', + 'username' => 'Username', + 'no_data' => 'Sharing list is empty.', + 'share' => 'Share', + 'permission_deleted' => 'Permission deleted!', + 'permission_created' => 'Permission created!', + + 'grants' => [ + 'read' => 'Grants read access', + 'original' => 'Grants access to original photo', + 'download' => 'Grants download', + 'upload' => 'Grants upload', + 'edit' => 'Grants edit', + 'delete' => 'Grants delete', + ], +]; \ No newline at end of file diff --git a/lang/pt/statistics.php b/lang/pt/statistics.php new file mode 100644 index 00000000000..2baf855bbd5 --- /dev/null +++ b/lang/pt/statistics.php @@ -0,0 +1,34 @@ + 'Statistics', + + 'preview_text' => 'This is a preview of the statistics page available in Lychee SE.
The data shown here are randomly generated and do not reflect your server.', + 'no_data' => 'User does not have data on server.', + 'collapse' => 'Collapse albums sizes', + + 'total' => [ + 'total' => 'Total', + 'albums' => 'Albums', + 'photos' => 'Photos', + 'size' => 'Size', + ], + 'table' => [ + 'username' => 'Owner', + 'title' => 'Title', + 'photos' => 'Photos', + 'descendants' => 'Children', + 'size' => 'Size', + ], +]; \ No newline at end of file diff --git a/lang/pt/toasts.php b/lang/pt/toasts.php new file mode 100644 index 00000000000..293d4b72594 --- /dev/null +++ b/lang/pt/toasts.php @@ -0,0 +1,17 @@ + 'Error', + 'success' => 'Success', +]; \ No newline at end of file diff --git a/lang/pt/users.php b/lang/pt/users.php new file mode 100644 index 00000000000..599bb833454 --- /dev/null +++ b/lang/pt/users.php @@ -0,0 +1,44 @@ + 'Users', + 'description' => 'Here you can manage the users of your Lychee installation. You can create, edit and delete users.', + 'create' => 'Create a new user', + 'username' => 'Username', + 'password' => 'Password', + 'legend' => 'Legend', + 'upload_rights' => 'When selected, the user can upload content.', + 'edit_rights' => 'When selected, the user can modify their profile (username, password).', + 'quota' => 'When set, the user has a space quota for pictures (in kB).', + + 'user_deleted' => 'User deleted', + 'user_created' => 'User created', + 'user_updated' => 'User updated', + 'change_saved' => 'Change saved!', + + 'create_edit' => [ + 'upload_rights' => 'User can upload content.', + 'edit_rights' => 'User can modify their profile (username, password).', + 'quota' => 'User has quota limit.', + 'quota_kb' => 'quota in kB (0 for default)', + 'note' => 'Admin note (not publically visible)', + 'create' => 'Create', + 'edit' => 'Edit', + ], + 'line' => [ + 'admin' => 'admin user', + 'edit' => 'Edit', + 'delete' => 'Delete', + ], +]; \ No newline at end of file diff --git a/lang/ru/aspect_ratio.php b/lang/ru/aspect_ratio.php new file mode 100644 index 00000000000..2c7e8fb56ac --- /dev/null +++ b/lang/ru/aspect_ratio.php @@ -0,0 +1,21 @@ + '5/4 (instagram landscape)', + '4by5' => '4/5 (instagram portrait)', + '2by3' => '2/3 (portrait)', + '3by2' => '3/2 (landscape)', + '1by1' => 'square', + '1byx9' => '16/9 (landscape)', +]; \ No newline at end of file diff --git a/lang/ru/diagnostics.php b/lang/ru/diagnostics.php new file mode 100644 index 00000000000..0fadd640428 --- /dev/null +++ b/lang/ru/diagnostics.php @@ -0,0 +1,30 @@ + 'Diagnostics', + + 'copy_to_clipboard' => 'Copy diagnostics to clipboard', + 'self-diagnosis' => 'Self-diagnosis', + 'info' => 'Info', + 'space' => 'Space', + 'load_space' => 'Load space usage.', + 'configuration' => 'Configuration', + 'loading' => 'Loading...', + 'identical_content' => 'Identical content', + + 'toast' => [ + 'info' => 'Info', + 'copy' => 'Diagnostics copied to clipboard!', + ], +]; \ No newline at end of file diff --git a/lang/ru/dialogs.php b/lang/ru/dialogs.php new file mode 100644 index 00000000000..4afd65fae3f --- /dev/null +++ b/lang/ru/dialogs.php @@ -0,0 +1,221 @@ + [ + 'close' => 'Close', + 'cancel' => 'Cancel', + 'save' => 'Save', + 'delete' => 'Delete', + 'move' => 'Move', + ], + 'about' => [ + 'subtitle' => 'Self-hosted photo-management done right', + 'description' => 'Lychee is a free photo-management tool, which runs on your server or web-space. Installing is a matter of seconds. Upload, manage and share photos like from a native application. Lychee comes with everything you need and all your photos are stored securely.', + 'update_available' => 'Update available!', + 'thank_you' => 'Thank you for your support!', + 'get_supporter_or_register' => 'Get exclusive features and support the development of Lychee.
Unlock the Supporter Edition or register your License key', + 'here' => 'here', + ], + 'dropbox' => [ + 'not_configured' => 'Dropbox is not configured.', + ], + 'import_from_link' => [ + 'instructions' => 'Please enter the direct link to a photo to import it:', + 'import' => 'Import', + ], + 'keybindings' => [ + 'don_t_show_again' => 'Don\'t show this again', + 'side_wide' => 'Site-wide Shortcuts', + 'back_cancel' => 'Back/Cancel', + 'confirm' => 'Confirm', + 'login' => 'Login', + 'toggle_full_screen' => 'Toggle Full Screen', + 'toggle_sensitive_albums' => 'Toggle Sensitive Albums', + + 'albums' => 'Albums Shortcuts', + 'new_album' => 'New Album', + 'upload_photos' => 'Upload Photos', + 'search' => 'Search', + 'show_this_modal' => 'Show this modal', + 'select_all' => 'Select All', + 'move_selection' => 'Move Selection', + 'delete_selection' => 'Delete Selection', + + 'album' => 'Album Shortcuts', + 'slideshow' => 'Start/Stop Slideshow', + 'toggle' => 'Toggle panel', + + 'photo' => 'Photo Shortcuts', + 'previous' => 'Previous photo', + 'next' => 'Next photo', + 'cycle' => 'Cycle overlay mode', + 'star' => 'Star the photo', + 'move' => 'Move the photo', + 'delete' => 'Delete the photo', + 'edit' => 'Edit information', + 'show_hide_meta' => 'Show information', + + 'keep_hidden' => 'We will keep it hidden.', + ], + 'login' => [ + 'username' => 'Username', + 'password' => 'Password', + 'unknown_invalid' => 'Unknown user or invalid password.', + 'signin' => 'Sign-In', + ], + 'register' => [ + 'enter_license' => 'Enter your license key below:', + 'license_key' => 'License key', + 'invalid_license' => 'Invalid license key.', + 'register' => 'Register', + ], + 'share_album' => [ + 'url_copied' => 'Copied URL to clipboard!', + ], + 'upload' => [ + 'completed' => 'Completed', + 'uploaded' => 'Uploaded:', + 'release' => 'Release file to upload!', + 'select' => 'Click here to select files to upload', + 'drag' => '(Or drag files to the page)', + 'loading' => 'Loading', + 'resume' => 'Resume', + 'uploading' => 'Uploading', + 'finished' => 'Finished', + 'failed_error' => 'Upload failed. The server returned an error!', + ], + 'visibility' => [ + 'public' => 'Public', + 'public_expl' => 'Anonymous users can access this album, subject to the restrictions below.', + 'full' => 'Original', + 'full_expl' => 'Anonymous users can view full-resolution photos.', + 'hidden' => 'Hidden', + 'hidden_expl' => 'Anonymous users need a direct link to access this album.', + 'downloadable' => 'Downloadable', + 'downloadable_expl' => 'Anonymous users can download this album.', + 'password' => 'Password', + 'password_prot' => 'Password protected', + 'password_prot_expl' => 'Anonymous users need a shared password to access this album.', + 'nsfw' => 'Sensitive', + 'nsfw_expl' => 'Album contains sensitive content.', + 'visibility_updated' => 'Visibility updated.', + ], + 'move_album' => [ + 'confirm_single' => 'Are you sure you want to move the album “%1$s” into the album “%2$s”?', + 'confirm_multiple' => 'Are you sure you want to move all selected albums into the album “%s”?', + 'move_single' => 'Move Album', + 'move_to' => 'Move to', + 'move_to_single' => 'Move %s to:', + 'move_to_multiple' => 'Move %d albums to:', + 'no_album_target' => 'No album to move to', + 'moved_single' => 'Album moved!', + 'moved_single_details' => '%1$s moved to %2$s', + 'moved_details' => 'Album(s) moved to %s', + ], + 'new_album' => [ + 'menu' => 'Create Album', + 'info' => 'Enter a title for the new album:', + 'title' => 'title', + 'create' => 'Create Album', + ], + 'new_tag_album' => [ + 'menu' => 'Create Tag Album', + 'info' => 'Enter a title for the new tag album:', + 'title' => 'title', + 'set_tags' => 'Set tags to show', + 'warn' => 'Make sure to press enter after each tag', + 'create' => 'Create Tag Album', + ], + 'delete_album' => [ + 'confirmation' => 'Are you sure you want to delete the album “%s” and all of the photos it contains?', + 'confirmation_multiple' => 'Are you sure you want to delete all %d selected albums and all of the photos they contain?', + 'warning' => 'This action can not be undone!', + 'delete' => 'Delete Album and Photos', + ], + 'transfer' => [ + 'query' => 'Transfer ownership of album to', + 'confirmation' => 'Are you sure you want to transfer the ownership of album “%s” and all the photos it contains to "%s"?', + 'lost_access_warning' => 'Your access to this album will be lost.', + 'warning' => 'This action can not be undone!', + 'transfer' => 'Transfer ownership of album and photos', + ], + 'rename' => [ + 'photo' => 'Enter a new title for this photo:', + 'album' => 'Enter a new title for this album:', + 'rename' => 'Rename', + ], + 'merge' => [ + 'merge_to' => 'Merge %s to:', + 'merge_to_multiple' => 'Merge %d albums to:', + 'no_albums' => 'No albums to merge to.', + 'confirm' => 'Are you sure you want to merge the album “%1$s” into the album “%2$s”?', + 'confirm_multiple' => 'Are you sure you want to merge all selected albums into the album “%s”?', + 'merge' => 'Merge Albums', + 'merged' => 'Album(s) merged to %s!', + ], + 'unlock' => [ + 'password_required' => 'This album is protected by a password. Enter the password below to view the photos of this album:', + 'password' => 'Password', + 'unlock' => 'Unlock', + ], + 'photo_tags' => [ + 'question' => 'Enter your tags for this photo.', + 'question_multiple' => 'Enter your tags for all %d selected photos. Existing tags will be overwritten.', + 'no_tags' => 'No Tags', + 'set_tags' => 'Set Tags', + 'updated' => 'Tags updated!', + 'tags_override_info' => 'If this is unchecked, the tags will be added to the existing tags of the photo.', + ], + 'photo_copy' => [ + 'no_albums' => 'No albums to copy to', + 'copy_to' => 'Copy %s to:', + 'copy_to_multiple' => 'Copy %d photos to:', + 'confirm' => 'Copy %s to %s.', + 'confirm_multiple' => 'Copy %d photos to %s.', + 'copy' => 'Copy', + 'copied' => 'Photo(s) copied!', + ], + 'photo_delete' => [ + 'confirm' => 'Are you sure you want to delete the photo “%s”?', + 'confirm_multiple' => 'Are you sure you want to delete all %d selected photos?', + 'deleted' => 'Photo(s) deleted!', + ], + 'move_photo' => [ + 'move_single' => 'Move %s to:', + 'move_multiple' => 'Move %d photos to:', + 'confirm' => 'Move %s to %s.', + 'confirm_multiple' => 'Move %d photos to %s.', + 'moved' => 'Photo(s) moved to %s!', + ], + 'target_user' => [ + 'placeholder' => 'Select user', + ], + 'target_album' => [ + 'placeholder' => 'Select album', + ], + 'webauthn' => [ + 'u2f' => 'U2F', + 'success' => 'Authentication successful!', + 'error' => 'Whoops, it looks like something went wrong. Please reload the site and try again!', + ], + 'se' => [ + 'available' => 'Available in the Supporter Edition', + ], + 'session_expired' => [ + 'title' => 'Session expired', + 'message' => 'Your session has expired.
Please reload the page.', + 'reload' => 'Reload', + 'go_to_gallery' => 'Go to the Gallery', + ], +]; \ No newline at end of file diff --git a/lang/ru/fix-tree.php b/lang/ru/fix-tree.php new file mode 100644 index 00000000000..64803e310e6 --- /dev/null +++ b/lang/ru/fix-tree.php @@ -0,0 +1,55 @@ + 'Maintenance', + 'intro' => 'This page allows you to re-order and fix your albums manually.
Before any modifications, we strongly recommend you to read about Nested Set tree structures.', + 'warning' => 'You can really break your Lychee installation here, modify values at your own risks.', + + 'help' => [ + 'header' => 'Help', + 'hover' => 'Hover ids or titles to highlight related albums.', + 'left' => 'Left', + 'right' => 'Right', + 'convenience' => 'For your convenience, the and buttons allow you to change the values of %s and %s by respectively +1 and -1 with propagation.', + 'left-right-warn' => 'The and indicates that the value of %s (and respectively %s) is duplicated somewhere.', + 'parent-marked' => 'Marked Parent Id indicates that the %s and %s do not satisfy the Nest Set tree structures. Edit either the Parent Id or the %s/%s values.', + 'slowness' => 'This page will be slow with a large number of albums.', + ], + + 'buttons' => [ + 'reset' => 'Reset', + 'check' => 'Check', + 'apply' => 'Apply', + ], + + 'table' => [ + 'title' => 'Title', + 'left' => 'Left', + 'right' => 'Right', + 'id' => 'Id', + 'parent' => 'Parent Id', + ], + + 'errors' => [ + 'invalid' => 'Invalid tree!', + 'invalid_details' => 'We are not applying this as it is guaranteed to be a broken state.', + 'invalid_left' => 'Album %s has an invalid left value.', + 'invalid_right' => 'Album %s has an invalid right value.', + 'invalid_left_right' => 'Album %s has an invalid left/right values. Left should be strictly smaller than right: %s < %s.', + 'duplicate_left' => 'Album %s has a duplicate left value %s.', + 'duplicate_right' => 'Album %s has a duplicate right value %s.', + 'parent' => 'Album %s has an unexpected parent id %s.', + 'unknown' => 'Album %s has an unknown error.', + ], +]; \ No newline at end of file diff --git a/lang/ru/gallery.php b/lang/ru/gallery.php new file mode 100644 index 00000000000..eb8008827e0 --- /dev/null +++ b/lang/ru/gallery.php @@ -0,0 +1,241 @@ + 'Gallery', + + 'smart_albums' => 'Smart albums', + 'albums' => 'Albums', + 'root' => 'Albums', + + 'original' => 'Original', + 'medium' => 'Medium', + 'medium_hidpi' => 'Medium HiDPI', + 'small' => 'Thumb', + 'small_hidpi' => 'Thumb HiDPI', + 'thumb' => 'Square thumb', + 'thumb_hidpi' => 'Square thumb HiDPI', + 'placeholder' => 'Low Quality Image Placeholder', + 'thumbnail' => 'Photo thumbnail', + 'live_video' => 'Video part of live-photo', + + 'camera_data' => 'Camera date', + 'album_reserved' => 'All Rights Reserved', + + 'map' => [ + 'error_gpx' => 'Error loading GPX file', + 'osm_contributors' => 'OpenStreetMap contributors', + ], + + 'search' => [ + 'title' => 'Search', + 'searching' => 'Searching…', + 'no_results' => 'Nothing matches your search query.', + 'searchbox' => 'Search…', + 'minimum_chars' => 'Minimum %s characters required.', + 'photos' => 'Photos (%s)', + 'albums' => 'Albums (%s)', + ], + + 'smart_album' => [ + 'unsorted' => 'Unsorted', + 'starred' => 'Starred', + 'recent' => 'Recent', + 'public' => 'Public', + 'on_this_day' => 'On This Day', + ], + + 'layout' => [ + 'squares' => 'Square thumbnails', + 'justified' => 'With aspect, justified', + 'masonry' => 'With aspect, masonry', + 'grid' => 'With aspect, grid', + ], + + 'overlay' => [ + 'none' => 'None', + 'exif' => 'EXIF data', + 'description' => 'Description', + 'date' => 'Date taken', + ], + + 'timeline' => [ + 'default' => 'default', + 'disabled' => 'disabled', + 'year' => 'Year', + 'month' => 'Month', + 'day' => 'Day', + 'hour' => 'Hour', + ], + + 'album' => [ + 'header_albums' => 'Albums', + 'header_photos' => 'Photos', + 'no_results' => 'Nothing to see here', + 'upload' => 'Upload photos', + + 'tabs' => [ + 'about' => 'About Album', + 'share' => 'Share Album', + 'move' => 'Move Album', + 'danger' => 'DANGER ZONE', + ], + + 'hero' => [ + 'created' => 'Created', + 'copyright' => 'Copyright', + 'subalbums' => 'Subalbums', + 'images' => 'Photos', + 'download' => 'Download Album', + 'share' => 'Share Album', + 'stats_only_se' => 'Statistics available in the Supporter Edition', + ], + + 'stats' => [ + 'lens' => 'Lens', + 'shutter' => 'Shutter speed', + 'iso' => 'ISO', + 'model' => 'Model', + 'aperture' => 'Aperture', + 'no_data' => 'No data', + ], + + 'properties' => [ + 'title' => 'Title', + 'description' => 'Description', + 'photo_ordering' => 'Order photos by', + 'children_ordering' => 'Order albums by', + 'asc/desc' => 'asc/desc', + 'header' => 'Set album header', + 'compact_header' => 'Use compact header', + 'license' => 'Set license', + 'copyright' => 'Set copyright', + 'aspect_ratio' => 'Set album thumbs aspect ratio', + 'album_timeline' => 'Set album timeline mode', + 'photo_timeline' => 'Set photo timeline mode', + 'layout' => 'Set photo layout', + 'show_tags' => 'Set tags to show', + 'tags_required' => 'Tags are required.', + ], + ], + + 'photo' => [ + 'actions' => [ + 'star' => 'Star', + 'unstar' => 'Unstar', + 'set_album_header' => 'Set as album header', + 'move' => 'Move', + 'delete' => 'Delete', + 'header_set' => 'Header set', + ], + + 'details' => [ + 'about' => 'About', + 'basics' => 'Basics', + 'title' => 'Title', + 'uploaded' => 'Uploaded', + 'description' => 'Description', + 'license' => 'License', + 'reuse' => 'Reuse', + 'latitude' => 'Latitude', + 'longitude' => 'Longitude', + 'altitude' => 'Altitude', + 'location' => 'Location', + 'image' => 'Image', + 'video' => 'Video', + 'size' => 'Size', + 'format' => 'Format', + 'resolution' => 'Resolution', + 'duration' => 'Duration', + 'fps' => 'Frame rate', + 'tags' => 'Tags', + 'camera' => 'Camera', + 'captured' => 'Captured', + 'make' => 'Make', + 'type' => 'Type/Model', + 'lens' => 'Lens', + 'shutter' => 'Shutter Speed', + 'aperture' => 'Aperture', + 'focal' => 'Focal Length', + 'iso' => 'ISO %s', + ], + + 'edit' => [ + 'set_title' => 'Set Title', + 'set_description' => 'Set Description', + 'set_license' => 'Set License', + 'no_tags' => 'No Tags', + 'set_tags' => 'Set Tags', + 'set_created_at' => 'Set Upload Date', + ], + ], + + 'nsfw' => [ + 'header' => 'Sensitive content', + 'description' => 'This album contains sensitive content which some people may find offensive or disturbing.', + 'consent' => 'Tap to consent.', + ], + + 'menus' => [ + 'star' => 'Star', + 'unstar' => 'Unstar', + 'star_all' => 'Star Selected', + 'unstar_all' => 'Unstar Selected', + 'tag' => 'Tag', + 'tag_all' => 'Tag Selected', + 'set_cover' => 'Set Album Cover', + 'remove_header' => 'Remove Album Header', + 'set_header' => 'Set Album Header', + 'copy_to' => 'Copy to …', + 'copy_all_to' => 'Copy Selected to …', + 'rename' => 'Rename', + 'move' => 'Move', + 'move_all' => 'Move Selected', + 'delete' => 'Delete', + 'delete_all' => 'Delete Selected', + 'download' => 'Download', + 'download_all' => 'Download Selected', + 'merge' => 'Merge', + 'merge_all' => 'Merge Selected', + + 'upload_photo' => 'Upload Photo', + 'import_link' => 'Import from Link', + 'import_dropbox' => 'Import from Dropbox', + 'new_album' => 'New Album', + 'new_tag_album' => 'New Tag Album', + 'upload_track' => 'Upload track', + 'delete_track' => 'Delete track', + ], + + 'sort' => [ + 'photo_select_1' => 'Upload Time', + 'photo_select_2' => 'Take Date', + 'photo_select_3' => 'Title', + 'photo_select_4' => 'Description', + 'photo_select_6' => 'Star', + 'photo_select_7' => 'Photo Format', + 'ascending' => 'Ascending', + 'descending' => 'Descending', + 'album_select_1' => 'Creation Time', + 'album_select_2' => 'Title', + 'album_select_3' => 'Description', + 'album_select_5' => 'Latest Take Date', + 'album_select_6' => 'Oldest Take Date', + ], + + 'albums_protection' => [ + 'private' => 'private', + 'public' => 'public', + 'inherit_from_parent' => 'inherit from parent', + ], +]; \ No newline at end of file diff --git a/lang/ru/jobs.php b/lang/ru/jobs.php new file mode 100644 index 00000000000..5d952b76012 --- /dev/null +++ b/lang/ru/jobs.php @@ -0,0 +1,18 @@ + 'Jobs', + + 'no_data' => 'No Jobs have been executed yet.', +]; \ No newline at end of file diff --git a/lang/ru/landing.php b/lang/ru/landing.php new file mode 100644 index 00000000000..fe6fe55b8ea --- /dev/null +++ b/lang/ru/landing.php @@ -0,0 +1,19 @@ + 'Gallery', + 'access_gallery' => 'Access the gallery', + 'hosted_with_lychee' => 'Hosted with Lychee', + 'copyright' => 'All images on this website are subject to copyright by %1$s © %2$s', +]; \ No newline at end of file diff --git a/lang/ru/left-menu.php b/lang/ru/left-menu.php new file mode 100644 index 00000000000..9a3e91f4037 --- /dev/null +++ b/lang/ru/left-menu.php @@ -0,0 +1,29 @@ + 'Back to Gallery', + + 'admin' => 'Admin', + 'clockwork' => 'Clockwork App', + 'logs' => 'Show Logs', + 'jobs' => 'Show Job History', + 'user' => 'User', + + 'sign_out' => 'Sign Out', + + 'about' => 'About', + 'api' => 'API Documentation', + 'source_code' => 'Source Code', + 'support' => 'Support', +]; \ No newline at end of file diff --git a/lang/ru/lychee.php b/lang/ru/lychee.php new file mode 100644 index 00000000000..a29c5686b1f --- /dev/null +++ b/lang/ru/lychee.php @@ -0,0 +1,535 @@ + 'Логин', + 'PASSWORD' => 'Пароль', + 'ENTER' => 'Enter', + 'CANCEL' => 'Отмена', + 'CONFIRM' => 'Confirm', + 'SIGN_IN' => 'Вход', + 'CLOSE' => 'Закрыть', + 'SETTINGS' => 'Параметры', + 'SEARCH' => 'Search …', + 'MORE' => 'More', + 'DEFAULT' => 'Default', + 'GALLERY' => 'Gallery', + + 'USERS' => 'Пользователи', + 'PROFILE' => 'Profile', + 'CREATE' => 'Create', + 'REMOVE' => 'Remove', + 'SHARE' => 'Share', + 'U2F' => 'U2F', + 'NOTIFICATIONS' => 'Notifications', + 'SHARING' => 'Поделиться', + 'CHANGE_LOGIN' => 'Изменить логин', + 'CHANGE_SORTING' => 'Порядок сортировки', + 'SET_DROPBOX' => 'Подключить Dropbox', + 'ABOUT_LYCHEE' => 'О Lychee', + 'DIAGNOSTICS' => 'Диагностика', + 'DIAGNOSTICS_GET_SIZE' => 'Request space usage', + 'JOBS' => 'Show job history', + 'LOGS' => 'Логи', + 'SIGN_OUT' => 'Выход', + 'UPDATE_AVAILABLE' => 'Доступно обновление!', + 'MIGRATION_AVAILABLE' => 'Migration available!', + 'CHECK_FOR_UPDATE' => 'Check for updates', + 'DEFAULT_LICENSE' => 'Лицензия для новых загрузок:', + 'SET_LICENSE' => 'Установить лицензию', + 'SET_OVERLAY_TYPE' => 'Установить оверлей', + 'SET_ALBUM_DECORATION' => 'Set album decorations', + 'SET_MAP_PROVIDER' => 'Set OpenStreetMap tiles provider', + 'FULL_SETTINGS' => 'Full Settings', + 'UPDATE' => 'Update', + 'RESET' => 'Reset', + 'DISABLE_TOKEN_TOOLTIP' => 'Disable', + 'ENABLE_TOKEN' => 'Enable API token', + 'DISABLED_TOKEN_STATUS_MSG' => 'Disabled', + 'TOKEN_BUTTON' => 'API Token ...', + 'TOKEN_NOT_AVAILABLE' => 'You have already viewed this token.', + 'TOKEN_WAIT' => 'Wait ...', + + 'SMART_ALBUMS' => 'Метаальбомы', + 'SHARED_ALBUMS' => 'Общие альбомы', + 'ALBUMS' => 'Альбомы', + 'PHOTOS' => 'Фотографии', + 'SEARCH_RESULTS' => 'Search results', + + 'RENAME' => 'Переименовать', + 'RENAME_ALL' => 'Переименовать все', + 'MERGE' => 'Объединить', + 'MERGE_ALL' => 'Объединить все', + 'MAKE_PUBLIC' => 'Сделать публичным', + 'SHARE_ALBUM' => 'Поделиться альбомом', + 'SHARE_PHOTO' => 'Поделиться фото', + 'VISIBILITY_ALBUM' => 'Album Visibility', + 'VISIBILITY_PHOTO' => 'Photo Visibility', + 'DOWNLOAD_ALBUM' => 'Скачать альбом', + 'ABOUT_ALBUM' => 'Об альбоме', + 'DELETE_ALBUM' => 'Удалить альбом', + 'MOVE_ALBUM' => 'Move Album', + 'FULLSCREEN_ENTER' => 'Полный экран', + 'FULLSCREEN_EXIT' => 'Оконный режим', + + 'SHARING_ALBUM_USERS' => 'Share this album with users', + 'WAIT_FETCH_DATA' => 'Please wait while we get the data …', + 'SHARING_ALBUM_USERS_NO_USERS' => 'There are no users to share the album with', + 'SHARING_ALBUM_USERS_LONG_MESSAGE' => 'Select the users to share this album with', + + 'DELETE_ALBUM_QUESTION' => 'Удалить альбом и все фото', + 'KEEP_ALBUM' => 'Сохранить альбом', + 'DELETE_ALBUM_CONFIRMATION' => 'Вы точно хотите удалить альбом «%s» со всеми фотографиями? Это действие необратимо!', + + 'DELETE_TAG_ALBUM_QUESTION' => 'Delete Album', + 'DELETE_TAG_ALBUM_CONFIRMATION' => 'Are you sure you want to delete the album «%s» (any photos inside will not be deleted)? This action can’t be undone!', + + 'DELETE_ALBUMS_QUESTION' => 'Удалить альбом и фотографии', + 'KEEP_ALBUMS' => 'Сохранить альбомы', + 'DELETE_ALBUMS_CONFIRMATION' => 'Вы точно хотите удалить альбомы %d со всеми фотографиями? Это действие необратимо!', + + 'DELETE_UNSORTED_CONFIRM' => 'Вы точно хотите удалить все фото из «Не отсортированные»? Это действие необратимо!', + 'CLEAR_UNSORTED' => 'Очистить «Не отсортированные»', + 'KEEP_UNSORTED' => 'Сохранить «Не отсортированные»', + + 'EDIT_SHARING' => 'Доступ', + 'MAKE_PRIVATE' => 'Сделать личным', + + 'CLOSE_ALBUM' => 'Закрыть альбом', + 'CLOSE_PHOTO' => 'Закрыть фото', + 'CLOSE_MAP' => 'Close Map', + + 'ADD' => 'Добавить', + 'MOVE' => 'Переместить', + 'MOVE_ALL' => 'Переместить все', + 'DUPLICATE' => 'Скопировать', + 'DUPLICATE_ALL' => 'Скопировать все', + 'COPY_TO' => 'Скопировать в …', + 'COPY_ALL_TO' => 'Скопировать все в …', + 'DELETE' => 'Удалить', + 'SAVE' => 'Save', + 'DELETE_ALL' => 'Удалить все', + 'DOWNLOAD' => 'Скачать', + 'DOWNLOAD_ALL' => 'Download Selected', + 'UPLOAD_PHOTO' => 'Загрузить фото', + 'IMPORT_LINK' => 'Загрузить по ссылке', + 'IMPORT_DROPBOX' => 'Импортировать из Dropbox', + 'IMPORT_SERVER' => 'Импортировать с сервера', + 'NEW_ALBUM' => 'Создать альбом', + 'NEW_TAG_ALBUM' => 'New Tag Album', + 'UPLOAD_TRACK' => 'Upload track', + 'DELETE_TRACK' => 'Delete track', + + 'TITLE_NEW_ALBUM' => 'Название нового альбома:', + 'UNTITLED' => 'Безымянный', + 'UNSORTED' => 'Не отсортированные', + 'STARRED' => 'Отмеченные', + 'RECENT' => 'Последние', + 'PUBLIC' => 'Общие', + 'ON_THIS_DAY' => 'On This Day', + 'NUM_PHOTOS' => 'фотографий', + + 'CREATE_ALBUM' => 'Создать альбом', + 'CREATE_TAG_ALBUM' => 'Create Tag Album', + + 'STAR_PHOTO' => 'Отметить фото', + 'STAR' => 'Отметить', + 'UNSTAR' => 'Unstar', + 'STAR_ALL' => 'Отметить все', + 'UNSTAR_ALL' => 'Unstar Selected', + 'TAG' => 'Теги', + 'TAG_ALL' => 'теги для всех', + 'UNSTAR_PHOTO' => 'Снять отметку', + 'SET_COVER' => 'Set Album Cover', + 'REMOVE_COVER' => 'Remove Album Cover', + 'SET_HEADER' => 'Set Album Header', + 'REMOVE_HEADER' => 'Remove Album Header', + 'SET_COMPACT_HEADER' => 'Use Compact Header', + + 'FULL_PHOTO' => 'Полный размер', + 'ABOUT_PHOTO' => 'О фотографии', + 'DISPLAY_FULL_MAP' => 'Map', + 'DIRECT_LINK' => 'Прямая ссылка', + 'DIRECT_LINKS' => 'Direct Links', + 'QR_CODE' => 'QR Code', + + 'ALBUM_ABOUT' => 'Об альбоме', + 'ALBUM_BASICS' => 'Основное', + 'ALBUM_TITLE' => 'Заголовок', + 'ALBUM_COPYRIGHT' => 'Copyright', + 'ALBUM_SET_COPYRIGHT' => 'Set copyright', + 'ALBUM_NEW_TITLE' => 'Новый заголовок альбома:', + 'ALBUMS_NEW_TITLE' => 'Введите заголовок для всех %d выбранных альбомов:', + 'ALBUM_SET_TITLE' => 'Сохранить заголовок', + 'ALBUM_DESCRIPTION' => 'Описание', + 'ALBUM_SHOW_TAGS' => 'Tags to show', + 'ALBUM_NEW_DESCRIPTION' => 'Введите описание этого альбома:', + 'ALBUM_SET_DESCRIPTION' => 'Сохранить описание', + 'ALBUM_NEW_SHOWTAGS' => 'Enter tags of photos that will be visible in this album:', + 'ALBUM_SET_SHOWTAGS' => 'Set tags to show', + 'ALBUM_ALBUM' => 'Альбом', + 'ALBUM_CREATED' => 'Создано', + 'ALBUM_IMAGES' => 'Фотографий', + 'ALBUM_VIDEOS' => 'Videos', + 'ALBUM_SUBALBUMS' => 'Subalbums', + 'ALBUM_SHARING' => 'Общее', + 'ALBUM_SHR_YES' => 'Да', + 'ALBUM_SHR_NO' => 'Нет', + 'ALBUM_PUBLIC' => 'Доступ', + 'ALBUM_PUBLIC_EXPL' => 'Anonymous users can access this album, subject to the restrictions below.', + 'ALBUM_FULL' => 'Original', + 'ALBUM_FULL_EXPL' => 'Anonymous users can behold full-resolution photos.', + 'ALBUM_HIDDEN' => 'Скрытый', + 'ALBUM_HIDDEN_EXPL' => 'Anonymous users need a direct link to access this album.', + 'ALBUM_MARK_NSFW' => 'Mark album as sensitive', + 'ALBUM_UNMARK_NSFW' => 'Unmark album as sensitive', + 'ALBUM_NSFW' => 'Sensitive', + 'ALBUM_NSFW_EXPL' => 'Album is marked to contain sensitive content.', + 'ALBUM_DOWNLOADABLE' => 'Скачивание разрешено', + 'ALBUM_DOWNLOADABLE_EXPL' => 'Anonymous users can download this album.', + 'ALBUM_SHARE_BUTTON_VISIBLE' => 'Share button is visible', + 'ALBUM_SHARE_BUTTON_VISIBLE_EXPL' => 'Anonymous users can see social media sharing links.', + 'ALBUM_PASSWORD' => 'Пароль', + 'ALBUM_PASSWORD_PROT' => 'Защищено паролем', + 'ALBUM_PASSWORD_PROT_EXPL' => 'Anonymous users need a shared password to access this album.', + 'ALBUM_PASSWORD_REQUIRED' => 'Этот альбом защищён паролем. Введите пароль для его просмотра:', + 'ALBUM_MERGE' => 'Вы точно хотите объединить альбом «%1$s» с альбомом «%2$s»?', + 'ALBUMS_MERGE' => 'Вы точно хотите объединить все выбранные альбомы в альбом «%s»?', + 'MERGE_ALBUM' => 'Объединить альбомы', + 'DONT_MERGE' => 'Не объединять', + 'ALBUM_MOVE' => 'Вы точно хотите переместить альбом «%1$s» в альбом «%2$s»?', + 'ALBUMS_MOVE' => 'Вы точно хотите объединить все выбранные альбомы в альбом «%s»?', + 'MOVE_ALBUMS' => 'Переместить альбомы', + 'NOT_MOVE_ALBUMS' => 'Не перемещать', + 'ROOT' => 'Root', + 'ALBUM_REUSE' => 'Авторские права', + 'ALBUM_LICENSE' => 'Лицензия', + 'ALBUM_SET_LICENSE' => 'Установить лицензию', + 'ALBUM_LICENSE_HELP' => 'Помочь выбрать?', + 'ALBUM_LICENSE_NONE' => 'Нет', + 'ALBUM_RESERVED' => 'Все права защищены', + 'ALBUM_SET_ORDER' => 'Set Order', + 'ALBUM_ORDERING' => 'Order by', + 'ALBUM_PHOTO_ORDERING' => 'Order photos by', + 'ALBUM_CHILDREN_ORDERING' => 'Order albums by', + 'ALBUM_OWNER' => 'Owner', + + 'PHOTO_ABOUT' => 'Описание', + 'PHOTO_BASICS' => 'Основное', + 'PHOTO_TITLE' => 'Заголовок', + 'PHOTO_NEW_TITLE' => 'Введите новый заголовок для этого фото:', + 'PHOTO_SET_TITLE' => 'Сохранить заголовок', + 'PHOTO_UPLOADED' => 'Загружено', + 'PHOTO_DESCRIPTION' => 'Описание', + 'PHOTO_NEW_DESCRIPTION' => 'Введите описание для этого фото:', + 'PHOTO_SET_DESCRIPTION' => 'Сохранить описание', + 'PHOTO_NEW_LICENSE' => 'Добавить лицензию', + 'PHOTO_SET_LICENSE' => 'Сохранить лицензию', + 'PHOTO_LICENSE' => 'Лицензия', + 'PHOTO_LICENSE_HELP' => 'Need help choosing?', + 'PHOTO_REUSE' => 'Авторские права', + 'PHOTO_LICENSE_NONE' => 'Нет', + 'PHOTO_RESERVED' => 'Все права защищены', + 'PHOTO_LATITUDE' => 'Latitude', + 'PHOTO_LONGITUDE' => 'Longitude', + 'PHOTO_ALTITUDE' => 'Altitude', + 'PHOTO_IMGDIRECTION' => 'Direction', + 'PHOTO_LOCATION' => 'Location', + 'PHOTO_IMAGE' => 'Изображение', + 'PHOTO_VIDEO' => 'Video', + 'PHOTO_SIZE' => 'Размер', + 'PHOTO_FORMAT' => 'Формат', + 'PHOTO_RESOLUTION' => 'Разрешение', + 'PHOTO_DURATION' => 'Duration', + 'PHOTO_FPS' => 'Frame rate', + 'PHOTO_TAGS' => 'Теги', + 'PHOTO_NOTAGS' => 'Тегов нет', + 'PHOTO_NEW_TAGS' => 'Укажите теги для этого фото. Можно добавить несколько тегов, разделяя их запятыми:', + 'PHOTOS_NEW_TAGS' => 'Введите теги для всех %d выбранных фотографий. Имеющиеся теги будут перезаписаны. Вы можете добавить несколько тегов, разделяя их запятыми:', + 'PHOTO_SET_TAGS' => 'Сохранить теги', + 'PHOTO_CAMERA' => 'Камера', + 'PHOTO_CAPTURED' => 'Дата съёмки', + 'PHOTO_MAKE' => 'Производитель', + 'PHOTO_TYPE' => 'Тип/Модель', + 'PHOTO_LENS' => 'Оптика', + 'PHOTO_SHUTTER' => 'Скорость затвора', + 'PHOTO_APERTURE' => 'Диафрагма', + 'PHOTO_FOCAL' => 'Фокусное расстояние', + 'PHOTO_ISO' => 'ISO %s', + 'PHOTO_SHARING' => 'Доступность', + 'PHOTO_DELETE' => 'Удалить фото', + 'PHOTO_KEEP' => 'Сохранить фото', + 'PHOTO_DELETE_CONFIRMATION' => 'Вы точно хотите удалить фото «%s»? Это действие необратимо!', + 'PHOTO_DELETE_ALL' => 'Вы точно хотите удалить все %d выбранные фото? Это действие необратимо!', + 'PHOTOS_NEW_TITLE' => 'Введите заголовок для всех %d выбранных фото:', + 'PHOTO_MAKE_PRIVATE_ALBUM' => 'Это фото находится в публичном альбоме. Чтобы сделать его личным или общедоступным, измените параметры альбома.', + 'PHOTO_SHOW_ALBUM' => 'Перейти к альбому', + 'PHOTO_PUBLIC' => 'Public', + 'PHOTO_PUBLIC_EXPL' => 'Anonymous users can view this photo, subject to the restrictions below.', + 'PHOTO_FULL' => 'Original', + 'PHOTO_FULL_EXPL' => 'Anonymous users can behold full-resolution photo.', + 'PHOTO_HIDDEN' => 'Hidden', + 'PHOTO_HIDDEN_EXPL' => 'Anonymous users need a direct link to view this photo.', + 'PHOTO_DOWNLOADABLE' => 'Downloadable', + 'PHOTO_DOWNLOADABLE_EXPL' => 'Anonymous users may download this photo.', + 'PHOTO_SHARE_BUTTON_VISIBLE' => 'Share button is visible', + 'PHOTO_SHARE_BUTTON_VISIBLE_EXPL' => 'Anonymous users can see social media sharing links.', + 'PHOTO_PASSWORD_PROT' => 'Password protected', + 'PHOTO_PASSWORD_PROT_EXPL' => 'Anonymous users need a shared password to view this photo.', + 'PHOTO_EDIT_SHARING_TEXT' => 'The sharing properties of this photo will be changed to the following:', + 'PHOTO_NO_EDIT_SHARING_TEXT' => 'Because this photo is located in a public album, it inherits that album’s visibility settings. Its current visibility is shown below for informational purposes only.', + 'PHOTO_EDIT_GLOBAL_SHARING_TEXT' => 'The visibility of this photo can be fine-tuned using global Lychee settings. Its current visibility is shown below for informational purposes only.', + 'PHOTO_NEW_CREATED_AT' => 'Enter the upload date for this photo. mm/dd/yyyy, hh:mm [am/pm]', + 'PHOTO_SET_CREATED_AT' => 'Set upload date', + + 'LOADING' => 'Загрузка', + 'ERROR' => 'Ошибка', + 'ERROR_TEXT' => 'Ой, что-то пошло не так … Пожалуйста, обновите страницу и попробуйте повторить.', + 'ERROR_UNKNOWN' => 'Произошло нечто неожиданное … Пожалуйста, повторите и проверьте папку установки и параметры сервера. За подробной информацией обратитесь к readme.', + 'ERROR_MAP_DEACTIVATED' => 'Map functionality has been deactivated under settings.', + 'ERROR_SEARCH_DEACTIVATED' => 'Search functionality has been deactivated under settings.', + 'SUCCESS' => 'Ок', + 'CHANGE_SUCCESS' => 'Change successful.', + 'RETRY' => 'Повторить', + 'OVERRIDE' => 'Override', + 'TAGS_OVERRIDE_INFO' => 'If this is unchecked, the tags will be added to the existing tags of the photo.', + + 'SETTINGS_SUCCESS_LOGIN' => 'Учётные данные обновлены.', + 'SETTINGS_SUCCESS_SORT' => 'Порядок сортировки обновлён.', + 'SETTINGS_SUCCESS_DROPBOX' => 'Ключ Dropbox обновлён.', + 'SETTINGS_SUCCESS_LANG' => 'Язык изменён', + 'SETTINGS_SUCCESS_LAYOUT' => 'Компоновка обновлена', + 'SETTINGS_SUCCESS_IMAGE_OVERLAY' => 'Данные наложения обновлены', + 'SETTINGS_SUCCESS_PUBLIC_SEARCH' => 'Public search updated', + 'SETTINGS_SUCCESS_LICENSE' => 'Лицензия по умолчанию установлена', + 'SETTINGS_SUCCESS_MAP_DISPLAY' => 'Map display settings updated', + 'SETTINGS_SUCCESS_MAP_DISPLAY_PUBLIC' => 'Map display settings for public albums updated', + 'SETTINGS_SUCCESS_MAP_PROVIDER' => 'Map provider settings updated', + 'SETTINGS_SUCCESS_CSS' => 'Stylesheets updated', + 'SETTINGS_SUCCESS_JS' => 'JS updated', + 'SETTINGS_SUCCESS_UPDATE' => 'Settings updated successfully', + 'SETTINGS_DROPBOX_KEY' => 'Dropbox API Key', + 'SETTINGS_ADVANCED_WARNING_EXPL' => 'Changing these advanced settings can be harmful to the stability, security and performance of this application. You should only modify them if you are sure of what you are doing.', + 'SETTINGS_ADVANCED_SAVE' => 'Save my modifications, I accept the risk!', + + 'U2F_NOT_SUPPORTED' => 'U2F not supported. Sorry.', + 'U2F_NOT_SECURE' => 'Environment not secured. U2F not available.', + 'U2F_REGISTER_KEY' => 'Register new device.', + 'U2F_REGISTRATION_SUCCESS' => 'Registration successful!', + 'U2F_AUTHENTIFICATION_SUCCESS' => 'Authentication successful!', + 'U2F_CREDENTIALS' => 'Credentials', + 'U2F_CREDENTIALS_DELETED' => 'Credentials deleted!', + 'U2F_LOGIN' => 'Log in with WebAuthn', + + 'NEW_PHOTOS_NOTIFICATION' => 'Send new photos notification emails.', + 'SETTINGS_SUCCESS_NEW_PHOTOS_NOTIFICATION' => 'New photos notification updated', + 'USER_EMAIL_INSTRUCTION' => 'Add your email below to enable receiving email notifications. To stop receiving emails, simply remove your email below.', + + 'LOGIN_USERNAME' => 'Новый логин', + 'LOGIN_PASSWORD' => 'Новый пароль', + 'LOGIN_PASSWORD_CONFIRM' => 'Повторите пароль', + 'PASSWORD_TITLE' => 'Введите текущий пароль:', + 'PASSWORD_CURRENT' => 'Текущий пароль', + 'PASSWORD_TEXT' => 'Ваши логин и пароль будут изменены на следующие:', + 'PASSWORD_CHANGE' => 'Изменить данные', + + 'EDIT_SHARING_TITLE' => 'Параметры доступа', + 'EDIT_SHARING_TEXT' => 'Параметры доступа к выбранному альбому будут изменены на следующие:', + 'SHARE_ALBUM_TEXT' => 'Этот альбом будет доступен со следующими условиями:', + + 'SORT_DIALOG_ATTRIBUTE_LABEL' => 'Attribute', + 'SORT_DIALOG_ORDER_LABEL' => 'Order', + + 'SORT_ALBUM_BY' => 'Сортировать альбомы %1$s в порядке %2$s.', + + 'SORT_ALBUM_SELECT_1' => 'даты создания', + 'SORT_ALBUM_SELECT_2' => 'заголовка', + 'SORT_ALBUM_SELECT_3' => 'описания', + 'SORT_ALBUM_SELECT_5' => 'свежайшего фото', + 'SORT_ALBUM_SELECT_6' => 'старейшего фото', + + 'SORT_PHOTO_BY' => 'Сортировать фотографии %1$s в порядке %2$s.', + + 'SORT_PHOTO_SELECT_1' => 'загрузки', + 'SORT_PHOTO_SELECT_2' => 'съёмки', + 'SORT_PHOTO_SELECT_3' => 'заголовка', + 'SORT_PHOTO_SELECT_4' => 'описания', + 'SORT_PHOTO_SELECT_6' => 'отметки', + 'SORT_PHOTO_SELECT_7' => 'формата', + + 'SORT_ASCENDING' => 'По возрастанию', + 'SORT_DESCENDING' => 'По убыванию', + 'SORT_CHANGE' => 'Сменить сортировку', + + 'DROPBOX_TITLE' => 'Задать ключ Dropbox', + 'DROPBOX_TEXT' => "Для загрузки фото из Dropbox Вам нужен ключ, который можно получить на специальной странице. Создайте личный ключ и вставьте здесь:", + + 'LANG_TEXT' => 'Change Lychee language to:', + 'LANG_TITLE' => 'Change Language', + + 'SETTING_RECENT_PUBLIC_TEXT' => 'Make "Recent" smart album accessible to anonymous users', + 'SETTING_STARRED_PUBLIC_TEXT' => 'Make "Starred" smart album accessible to anonymous users', + 'SETTING_ONTHISDAY_PUBLIC_TEXT' => 'Make "On This Day" smart album accessible to anonymous users', + + 'CSS_TEXT' => 'Personalize CSS:', + 'CSS_TITLE' => 'Change CSS', + 'JS_TEXT' => 'Custom JS:', + 'JS_TITLE' => 'Change JS', + 'PUBLIC_SEARCH_TEXT' => 'Разрешить публичный поиск:', + 'OVERLAY_TYPE' => 'Данные для наложения:', + 'OVERLAY_NONE' => 'None', + 'OVERLAY_EXIF' => 'EXIF данные', + 'OVERLAY_DESCRIPTION' => 'Описание фото', + 'OVERLAY_DATE' => 'Дата съёмки', + 'ALBUM_DECORATION' => 'Album decorations:', + 'ALBUM_DECORATION_NONE' => 'None', + 'ALBUM_DECORATION_ORIGINAL' => 'Sub-album marker', + 'ALBUM_DECORATION_ALBUM' => 'Number of sub-albums', + 'ALBUM_DECORATION_PHOTO' => 'Number of photos', + 'ALBUM_DECORATION_ALL' => 'Number of sub-albums and photos', + 'ALBUM_DECORATION_ORIENTATION' => 'Orientation of album decorations:', + 'ALBUM_DECORATION_ORIENTATION_ROW' => 'Horizontal (photos, albums)', + 'ALBUM_DECORATION_ORIENTATION_ROW_REVERSE' => 'Horizontal (albums, photos)', + 'ALBUM_DECORATION_ORIENTATION_COLUMN' => 'Vertical (top photos, albums)', + 'ALBUM_DECORATION_ORIENTATION_COLUMN_REVERSE' => 'Vertical (top albums, photos)', + 'MAP_DISPLAY_TEXT' => 'Enable maps (provided by OpenStreetMap):', + 'MAP_DISPLAY_PUBLIC_TEXT' => 'Enable maps for public albums (provided by OpenStreetMap):', + 'MAP_PROVIDER' => 'Provider of OpenStreetMap tiles:', + 'MAP_PROVIDER_WIKIMEDIA' => 'Wikimedia', + 'MAP_PROVIDER_OSM_ORG' => 'OpenStreetMap.org (no HiDPI)', + 'MAP_PROVIDER_OSM_DE' => 'OpenStreetMap.de (no HiDPI)', + 'MAP_PROVIDER_OSM_FR' => 'OpenStreetMap.fr (no HiDPI)', + 'MAP_PROVIDER_RRZE' => 'University of Erlangen, Germany (only HiDPI)', + 'MAP_INCLUDE_SUBALBUMS_TEXT' => 'Include photos of subalbums on map:', + 'LOCATION_DECODING' => 'Decode GPS data into location name', + 'LOCATION_SHOW' => 'Show location name', + 'LOCATION_SHOW_PUBLIC' => 'Show location name for public mode', + + 'LAYOUT_TYPE' => 'Компоновка фото:', + 'LAYOUT_SQUARES' => 'Квадратные превью', + 'LAYOUT_JUSTIFIED' => 'По формату, выровнять', + 'LAYOUT_MASONRY' => 'По формату, masonry', + 'LAYOUT_GRID' => 'По формату, grid', + 'LAYOUT_UNJUSTIFIED' => 'По формату, не выравнивать', + 'SET_LAYOUT' => 'Изменить компоновку', + + 'NSFW_VISIBLE_TEXT_1' => 'Make Sensitive albums visible by default.', + 'NSFW_VISIBLE_TEXT_2' => 'If the album is public, it is still accessible, just hidden from the view and can be revealed by pressing H.', + 'SETTINGS_SUCCESS_NSFW_VISIBLE' => 'Default sensitive album visibility updated with success.', + + 'NSFW_BANNER' => '

Sensitive content

This album contains sensitive content which some people may find offensive or disturbing.

Tap to consent.

', + 'NSFW_HEADER' => 'Sensitive content', + 'NSFW_EXPLANATION' => 'This album contains sensitive content which some people may find offensive or disturbing.', + 'TAP_CONSENT' => 'Tap to consent.', + + 'VIEW_NO_RESULT' => 'Не найдено', + 'VIEW_NO_PUBLIC_ALBUMS' => 'Нет публичных альбомов', + 'VIEW_NO_CONFIGURATION' => 'Не настроено', + 'VIEW_PHOTO_NOT_FOUND' => 'Фото не найдено', + + 'NO_TAGS' => 'Тегов нет', + + 'UPLOAD_MANAGE_NEW_PHOTOS' => 'Теперь вы можете управлять новыми фото.', + 'UPLOAD_COMPLETE' => 'Загрузка завершена', + 'UPLOAD_COMPLETE_FAILED' => 'Ошибка загрузки одного или более фото.', + 'UPLOAD_IMPORTING' => 'Импорт', + 'UPLOAD_IMPORTING_URL' => 'импорт по URL', + 'UPLOAD_UPLOADING' => 'Выгрузка', + 'UPLOAD_FINISHED' => 'Завершено', + 'UPLOAD_PROCESSING' => 'Выполняется', + 'UPLOAD_FAILED' => 'Ошибка', + 'UPLOAD_FAILED_ERROR' => 'Загрузка не удалась: сервер вернул ошибку.', + 'UPLOAD_FAILED_WARNING' => 'Загрузка не удалась, сервер вернул предупреждение.', + 'UPLOAD_CANCELLED' => 'Cancelled', + 'UPLOAD_SKIPPED' => 'Пропущено', + 'UPLOAD_UPDATED' => 'Updated', + 'UPLOAD_GENERAL' => 'General', + 'UPLOAD_IMPORT_SKIPPED_DUPLICATE' => 'This photo has been skipped because it’s already in your library.', + 'UPLOAD_IMPORT_RESYNCED_DUPLICATE' => 'This photo has been skipped because it’s already in your library, but its metadata has been updated.', + 'UPLOAD_ERROR_CONSOLE' => 'Подробности смотрите в консоли браузера.', + 'UPLOAD_UNKNOWN' => 'Сервер вернул непонятный ответ. Проверьте консоль браузера.', + 'UPLOAD_ERROR_UNKNOWN' => 'Загрузка не удалась: сервер вернул что-то непонятное!', + 'UPLOAD_ERROR_POSTSIZE' => 'Upload failed. The PHP post_max_size may be too small! Otherwise check the FAQ.', + 'UPLOAD_ERROR_FILESIZE' => 'Upload failed. The PHP upload_max_filesize may be too small! Otherwise check the FAQ.', + 'UPLOAD_IN_PROGRESS' => 'Lychee выполняет выгрузку.', + 'UPLOAD_IMPORT_WARN_ERR' => 'Импорт был завершён, но обнаружены ошибки или предупреждения. Пожалуйста, проверьте лог (Settings -> Логи).', + 'UPLOAD_IMPORT_COMPLETE' => 'Импорт завершён', + 'UPLOAD_IMPORT_INSTR' => 'Укажите прямую ссылку на фото для импорта:', + 'UPLOAD_IMPORT' => 'Импорт', + 'UPLOAD_IMPORT_SERVER' => 'Загрузка с сервера', + 'UPLOAD_IMPORT_SERVER_FOLD' => 'Каталог пуст или не содержит файлов, которые можно обработать. Пожалуйста, проверьте лог (Settings -> Логи).', + 'UPLOAD_IMPORT_SERVER_INSTR' => 'Import all photos, folders and sub-folders located in the folders with the following absolute paths (on server). Paths are space separated, use \\ to escape a space in a path.', + 'UPLOAD_ABSOLUTE_PATH' => 'Absolute path to directories, space separated', + 'UPLOAD_IMPORT_SERVER_EMPT' => 'Не могу импортировать: указанный каталог пуст!', + 'UPLOAD_IMPORT_DELETE_ORIGINALS' => 'Delete originals', + 'UPLOAD_IMPORT_DELETE_ORIGINALS_EXPL' => 'Original files will be deleted after the import when possible.', + 'UPLOAD_IMPORT_VIA_SYMLINK' => 'Symbolic links', + 'UPLOAD_IMPORT_VIA_SYMLINK_EXPL' => 'Import files using symbolic links to originals.', + 'UPLOAD_IMPORT_SKIP_DUPLICATES' => 'Skip duplicates', + 'UPLOAD_IMPORT_SKIP_DUPLICATES_EXPL' => 'Existing media files are skipped.', + 'UPLOAD_IMPORT_RESYNC_METADATA' => 'Re-sync metadata', + 'UPLOAD_IMPORT_RESYNC_METADATA_EXPL' => 'Update metadata of existing media files.', + 'UPLOAD_IMPORT_LOW_MEMORY_EXPL' => 'The import process on the server is approaching the memory limit and may end up being terminated prematurely.', + 'UPLOAD_WARNING' => 'Warning', + 'UPLOAD_IMPORT_NOT_A_DIRECTORY' => 'The given path is not a readable directory!', + 'UPLOAD_IMPORT_PATH_RESERVED' => 'The given path is a reserved path of Lychee!', + 'UPLOAD_IMPORT_FAILED' => 'Could not import the file!', + 'UPLOAD_IMPORT_UNSUPPORTED' => 'Unsupported file type!', + 'UPLOAD_IMPORT_CANCELLED' => 'Import cancelled', + + 'ABOUT_SUBTITLE' => 'Self-hosted photo-management done right', + 'ABOUT_DESCRIPTION' => "Lychee - это бесплатный фотоменеджер для Вашего сервера или хостинга. Установка занимает считанные секунды. Загружайте, редактируйте и делитесь фотографиями как в любимом приложении! Lychee обеспечит Вас всем необходимым, включая безопасность хранения Ваших фотографий! На русский язык перевёл Евгений Лебедев.", + 'FOOTER_COPYRIGHT' => 'All images on this website are subject to copyright by %1$s © %2$s', + 'HOSTED_WITH_LYCHEE' => 'Hosted with Lychee', + + 'URL_COPY_TO_CLIPBOARD' => 'Copy to clipboard', + 'URL_COPIED_TO_CLIPBOARD' => 'Copied URL to clipboard!', + 'PHOTO_DIRECT_LINKS_TO_IMAGES' => 'Direct links to image files:', + 'PHOTO_ORIGINAL' => 'Original', + 'PHOTO_MEDIUM' => 'Medium', + 'PHOTO_MEDIUM_HIDPI' => 'Medium HiDPI', + 'PHOTO_SMALL' => 'Thumb', + 'PHOTO_SMALL_HIDPI' => 'Thumb HiDPI', + 'PHOTO_THUMB' => 'Square thumb', + 'PHOTO_THUMB_HIDPI' => 'Square thumb HiDPI', + 'PHOTO_PLACEHOLDER' => 'Low Quality Image Placeholder', + 'PHOTO_THUMBNAIL' => 'Photo thumbnail', + 'PHOTO_LIVE_VIDEO' => 'Video part of live-photo', + 'PHOTO_VIEW' => 'Lychee Photo View:', + + 'PHOTO_EDIT_ROTATECWISE' => 'Rotate clockwise', + 'PHOTO_EDIT_ROTATECCWISE' => 'Rotate counter-clockwise', + + 'ERROR_GPX' => 'Error loading GPX file: ', + 'ERROR_EITHER_ALBUMS_OR_PHOTOS' => 'Please select either albums or photos!', + 'ERROR_COULD_NOT_FIND' => 'Could not find what you want.', + 'ERROR_INVALID_EMAIL' => 'Not a valid email address.', + 'EMAIL_SUCCESS' => 'Email updated!', + 'ERROR_PHOTO_NOT_FOUND' => 'Error: photo %s not found !', + 'ERROR_EMPTY_USERNAME' => 'new username cannot be empty.', + 'ERROR_PASSWORD_DOES_NOT_MATCH' => 'new password does not match.', + 'ERROR_EMPTY_PASSWORD' => 'new password cannot be empty.', + 'ERROR_SELECT_ALBUM' => 'Select an album to share!', + 'ERROR_SELECT_USER' => 'Select a user to share with!', + 'ERROR_SELECT_SHARING' => 'Select a sharing to remove!', + 'SHARING_SUCCESS' => 'Sharing updated!', + 'SHARING_REMOVED' => 'Sharing removed!', + 'USER_CREATED' => 'User created!', + 'USER_DELETED' => 'User deleted!', + 'USER_UPDATED' => 'User updated!', + 'ENTER_EMAIL' => 'Enter your email address:', + 'ERROR_ALBUM_JSON_NOT_FOUND' => 'Error: Album json not found!', + 'ERROR_ALBUM_NOT_FOUND' => 'Error: album %s not found', + 'ERROR_DROPBOX_KEY' => 'Error: Dropbox key not set', + 'ERROR_SESSION' => 'Session expired.', + 'CAMERA_DATE' => 'Camera date', + 'NEW_PASSWORD' => 'new password', + 'ALLOW_UPLOADS' => 'Allow uploads', + 'ALLOW_USER_SELF_EDIT' => 'Allow self-management of user account', + 'OSM_CONTRIBUTORS' => 'OpenStreetMap contributors', +]; diff --git a/lang/ru/maintenance.php b/lang/ru/maintenance.php new file mode 100644 index 00000000000..f86de3d6f46 --- /dev/null +++ b/lang/ru/maintenance.php @@ -0,0 +1,60 @@ + 'Maintenance', + 'description' => 'You will find on this page, all the required actions to keep your Lychee installation running smooth and nicely.', + 'cleaning' => [ + 'title' => 'Cleaning %s', + 'result' => '%s deleted.', + 'description' => 'Remove all contents from %s', + 'button' => 'Clean', + ], + 'fix-jobs' => [ + 'title' => 'Fixing Jobs History', + 'description' => 'Mark jobs with status %s or %s as %s.', + 'button' => 'Fix job history', + ], + 'gen-sizevariants' => [ + 'title' => 'Missing %s', + 'description' => 'Found %d %s that could be generated.', + 'button' => 'Generate!', + 'success' => 'Successfully generated %d %s.', + ], + 'fill-filesize-sizevariants' => [ + 'title' => 'File sizes missing', + 'description' => 'Found %d small variants without file size.', + 'button' => 'Fetch data!', + 'success' => 'Successfully computed sizes of %d small variants.', + ], + 'fix-tree' => [ + 'title' => 'Tree statistics', + 'Oddness' => 'Oddness', + 'Duplicates' => 'Duplicates', + 'Wrong parents' => 'Wrong parents', + 'Missing parents' => 'Missing parents', + 'button' => 'Fix tree', + ], + 'optimize' => [ + 'title' => 'Optimize Database', + 'description' => 'If you notice slowdown in your installation, it may be because your database does not + have all its needed index.', + 'button' => 'Optimize Database', + ], + 'update' => [ + 'title' => 'Updates', + 'check-button' => 'Check for updates', + 'update-button' => 'Update', + 'no-pending-updates' => 'No pending update.', + ], +]; \ No newline at end of file diff --git a/lang/ru/profile.php b/lang/ru/profile.php new file mode 100644 index 00000000000..cc24b97452c --- /dev/null +++ b/lang/ru/profile.php @@ -0,0 +1,64 @@ + 'Profile', + + 'login' => [ + 'header' => 'Profile', + 'enter_current_password' => 'Enter your current password:', + 'current_password' => 'Current password', + 'credentials_update' => 'Your credentials will be changed to the following:', + 'username' => 'Username', + 'new_password' => 'New password', + 'confirm_new_password' => 'Confirm new password', + 'email_instruction' => 'Add your email below to enable receiving email notifications. To stop receiving emails, simply remove your email below.', + 'email' => 'Email', + 'change' => 'Change Login', + 'api_token' => 'API Token ...', + + 'missing_fields' => 'Missing fields', + ], + + 'token' => [ + 'unavailable' => 'You have already viewed this token.', + 'no_data' => 'No token API have been generated.', + 'disable' => 'Disable', + 'disabled' => 'Token disabled', + 'warning' => 'This token will not be displayed again. Copy it and keep it in a safe place.', + 'reset' => 'Reset the token', + 'create' => 'Create a new token', + ], + + 'oauth' => [ + 'header' => 'OAuth', + 'header_not_available' => 'OAuth is not available', + 'setup_env' => 'Set up the credentials in your .env', + 'token_registered' => '%s token registered.', + 'setup' => 'Set up %s', + 'reset' => 'reset', + 'credential_deleted' => 'Credential deleted!', + ], + + 'u2f' => [ + 'header' => 'Passkey/MFA/2FA', + 'info' => 'This only provides the ability to use WebAuthn to authenticate instead of username & password.', + 'empty' => 'Credentials list is empty!', + 'not_secure' => 'Environment not secured. U2F not available.', + 'new' => 'Register new device.', + 'credential_deleted' => 'Credential deleted!', + 'credential_updated' => 'Credential updated!', + 'credential_registred' => 'Registration successful!', + '5_chars' => 'At least 5 chars.', + ], +]; \ No newline at end of file diff --git a/lang/ru/settings.php b/lang/ru/settings.php new file mode 100644 index 00000000000..fd197f11135 --- /dev/null +++ b/lang/ru/settings.php @@ -0,0 +1,92 @@ + 'Settings', + 'small_screen' => 'For better a experience on the Settings page,
we recommend you use a larger screen.', + 'tabs' => [ + 'basic' => 'Basic', + 'all_settings' => 'All settings', + ], + 'toasts' => [ + 'change_saved' => 'Change saved!', + 'details' => 'Settings have been modified as per request', + 'error' => 'Error!', + 'error_load_css' => 'Could not load dist/user.css', + 'error_load_js' => 'Could not load dist/custom.js', + 'error_save_css' => 'Could not save CSS', + 'error_save_js' => 'Could not save JS', + 'thank_you' => 'Thank you for your support.', + 'reload' => 'Reload your page for full functionalities.', + ], + 'system' => [ + 'header' => 'System', + 'use_dark_mode' => 'Use dark mode for Lychee', + 'language' => 'Language used by Lychee', + 'nsfw_album_visibility' => 'Make Sensitive albums visible by default.', + 'nsfw_album_explanation' => 'If the album is public, it is still accessible, just hidden from the view and can be revealed by pressing H.', + ], + 'lychee_se' => [ + 'header' => 'Lychee SE', + 'call4action' => 'Get exclusive features and support the development of Lychee. Unlock the SE edition.', + 'preview' => 'Enable preview of Lychee SE features', + 'hide_call4action' => 'Hide this Lychee SE registration form. I am happy with Lychee as-is. :)', + 'hide_warning' => 'If enabled, the only way to register your license key will be via the More tab above. Changes are applied on page reload.', + ], + 'dropbox' => [ + 'header' => 'Dropbox', + 'instruction' => 'In order to import photos from your Dropbox, you need a valid drop-ins app key from their website.', + 'api_key' => 'Dropbox API Key', + 'set_key' => 'Set Dropbox Key', + ], + 'gallery' => [ + 'header' => 'Gallery', + 'photo_order_column' => 'Default column used for sorting photos', + 'photo_order_direction' => 'Default order used for sorting photos', + 'album_order_column' => 'Default column used for sorting albums', + 'album_order_direction' => 'Default order used for sorting albums', + 'aspect_ratio' => 'Default aspect ratio for album thumbs', + 'photo_layout' => 'Layout for pictures', + 'album_decoration' => 'Show decorations on album cover (sub-album and/or photo count)', + 'album_decoration_direction' => 'Align album decorations horizontally or vertically', + 'photo_overlay' => 'Default image overlay information', + 'license_default' => 'Default license used for albums', + 'license_help' => 'Need help choosing?', + ], + 'geolocation' => [ + 'header' => 'Geo-location', + 'map_display' => 'Display the map given GPS coordinates', + 'map_display_public' => 'Allow anonymous users to access the map', + 'map_provider' => 'Defines the map provider', + 'map_include_subalbums' => 'Includes pictures of the sub albums on the map', + 'location_decoding' => 'Use GPS location decoding', + 'location_show' => 'Show location extracted from GPS coordinates', + 'location_show_public' => 'Anonymous users can access the extracted location from GPS coordinates', + ], + 'advanced' => [ + 'header' => 'Advanced Customization', + 'change_css' => 'Change CSS', + 'change_js' => 'Change JS', + ], + 'all' => [ + 'old_setting_style' => 'Old setting style', + 'change_detected' => 'Some settings changed.', + 'save' => 'Save', + ], + + 'tool_option' => [ + 'disabled' => 'disabled', + 'enabled' => 'enabled', + 'discover' => 'discover', + ], +]; \ No newline at end of file diff --git a/lang/ru/sharing.php b/lang/ru/sharing.php new file mode 100644 index 00000000000..69de18cc6d0 --- /dev/null +++ b/lang/ru/sharing.php @@ -0,0 +1,33 @@ + 'Sharing', + + 'info' => 'This page gives an overview of and the ability to edit the sharing rights associated with albums.', + 'album_title' => 'Album title', + 'username' => 'Username', + 'no_data' => 'Sharing list is empty.', + 'share' => 'Share', + 'permission_deleted' => 'Permission deleted!', + 'permission_created' => 'Permission created!', + + 'grants' => [ + 'read' => 'Grants read access', + 'original' => 'Grants access to original photo', + 'download' => 'Grants download', + 'upload' => 'Grants upload', + 'edit' => 'Grants edit', + 'delete' => 'Grants delete', + ], +]; \ No newline at end of file diff --git a/lang/ru/statistics.php b/lang/ru/statistics.php new file mode 100644 index 00000000000..2baf855bbd5 --- /dev/null +++ b/lang/ru/statistics.php @@ -0,0 +1,34 @@ + 'Statistics', + + 'preview_text' => 'This is a preview of the statistics page available in Lychee SE.
The data shown here are randomly generated and do not reflect your server.', + 'no_data' => 'User does not have data on server.', + 'collapse' => 'Collapse albums sizes', + + 'total' => [ + 'total' => 'Total', + 'albums' => 'Albums', + 'photos' => 'Photos', + 'size' => 'Size', + ], + 'table' => [ + 'username' => 'Owner', + 'title' => 'Title', + 'photos' => 'Photos', + 'descendants' => 'Children', + 'size' => 'Size', + ], +]; \ No newline at end of file diff --git a/lang/ru/toasts.php b/lang/ru/toasts.php new file mode 100644 index 00000000000..293d4b72594 --- /dev/null +++ b/lang/ru/toasts.php @@ -0,0 +1,17 @@ + 'Error', + 'success' => 'Success', +]; \ No newline at end of file diff --git a/lang/ru/users.php b/lang/ru/users.php new file mode 100644 index 00000000000..599bb833454 --- /dev/null +++ b/lang/ru/users.php @@ -0,0 +1,44 @@ + 'Users', + 'description' => 'Here you can manage the users of your Lychee installation. You can create, edit and delete users.', + 'create' => 'Create a new user', + 'username' => 'Username', + 'password' => 'Password', + 'legend' => 'Legend', + 'upload_rights' => 'When selected, the user can upload content.', + 'edit_rights' => 'When selected, the user can modify their profile (username, password).', + 'quota' => 'When set, the user has a space quota for pictures (in kB).', + + 'user_deleted' => 'User deleted', + 'user_created' => 'User created', + 'user_updated' => 'User updated', + 'change_saved' => 'Change saved!', + + 'create_edit' => [ + 'upload_rights' => 'User can upload content.', + 'edit_rights' => 'User can modify their profile (username, password).', + 'quota' => 'User has quota limit.', + 'quota_kb' => 'quota in kB (0 for default)', + 'note' => 'Admin note (not publically visible)', + 'create' => 'Create', + 'edit' => 'Edit', + ], + 'line' => [ + 'admin' => 'admin user', + 'edit' => 'Edit', + 'delete' => 'Delete', + ], +]; \ No newline at end of file diff --git a/lang/sk/aspect_ratio.php b/lang/sk/aspect_ratio.php new file mode 100644 index 00000000000..2c7e8fb56ac --- /dev/null +++ b/lang/sk/aspect_ratio.php @@ -0,0 +1,21 @@ + '5/4 (instagram landscape)', + '4by5' => '4/5 (instagram portrait)', + '2by3' => '2/3 (portrait)', + '3by2' => '3/2 (landscape)', + '1by1' => 'square', + '1byx9' => '16/9 (landscape)', +]; \ No newline at end of file diff --git a/lang/sk/diagnostics.php b/lang/sk/diagnostics.php new file mode 100644 index 00000000000..0fadd640428 --- /dev/null +++ b/lang/sk/diagnostics.php @@ -0,0 +1,30 @@ + 'Diagnostics', + + 'copy_to_clipboard' => 'Copy diagnostics to clipboard', + 'self-diagnosis' => 'Self-diagnosis', + 'info' => 'Info', + 'space' => 'Space', + 'load_space' => 'Load space usage.', + 'configuration' => 'Configuration', + 'loading' => 'Loading...', + 'identical_content' => 'Identical content', + + 'toast' => [ + 'info' => 'Info', + 'copy' => 'Diagnostics copied to clipboard!', + ], +]; \ No newline at end of file diff --git a/lang/sk/dialogs.php b/lang/sk/dialogs.php new file mode 100644 index 00000000000..4afd65fae3f --- /dev/null +++ b/lang/sk/dialogs.php @@ -0,0 +1,221 @@ + [ + 'close' => 'Close', + 'cancel' => 'Cancel', + 'save' => 'Save', + 'delete' => 'Delete', + 'move' => 'Move', + ], + 'about' => [ + 'subtitle' => 'Self-hosted photo-management done right', + 'description' => 'Lychee is a free photo-management tool, which runs on your server or web-space. Installing is a matter of seconds. Upload, manage and share photos like from a native application. Lychee comes with everything you need and all your photos are stored securely.', + 'update_available' => 'Update available!', + 'thank_you' => 'Thank you for your support!', + 'get_supporter_or_register' => 'Get exclusive features and support the development of Lychee.
Unlock the Supporter Edition or register your License key', + 'here' => 'here', + ], + 'dropbox' => [ + 'not_configured' => 'Dropbox is not configured.', + ], + 'import_from_link' => [ + 'instructions' => 'Please enter the direct link to a photo to import it:', + 'import' => 'Import', + ], + 'keybindings' => [ + 'don_t_show_again' => 'Don\'t show this again', + 'side_wide' => 'Site-wide Shortcuts', + 'back_cancel' => 'Back/Cancel', + 'confirm' => 'Confirm', + 'login' => 'Login', + 'toggle_full_screen' => 'Toggle Full Screen', + 'toggle_sensitive_albums' => 'Toggle Sensitive Albums', + + 'albums' => 'Albums Shortcuts', + 'new_album' => 'New Album', + 'upload_photos' => 'Upload Photos', + 'search' => 'Search', + 'show_this_modal' => 'Show this modal', + 'select_all' => 'Select All', + 'move_selection' => 'Move Selection', + 'delete_selection' => 'Delete Selection', + + 'album' => 'Album Shortcuts', + 'slideshow' => 'Start/Stop Slideshow', + 'toggle' => 'Toggle panel', + + 'photo' => 'Photo Shortcuts', + 'previous' => 'Previous photo', + 'next' => 'Next photo', + 'cycle' => 'Cycle overlay mode', + 'star' => 'Star the photo', + 'move' => 'Move the photo', + 'delete' => 'Delete the photo', + 'edit' => 'Edit information', + 'show_hide_meta' => 'Show information', + + 'keep_hidden' => 'We will keep it hidden.', + ], + 'login' => [ + 'username' => 'Username', + 'password' => 'Password', + 'unknown_invalid' => 'Unknown user or invalid password.', + 'signin' => 'Sign-In', + ], + 'register' => [ + 'enter_license' => 'Enter your license key below:', + 'license_key' => 'License key', + 'invalid_license' => 'Invalid license key.', + 'register' => 'Register', + ], + 'share_album' => [ + 'url_copied' => 'Copied URL to clipboard!', + ], + 'upload' => [ + 'completed' => 'Completed', + 'uploaded' => 'Uploaded:', + 'release' => 'Release file to upload!', + 'select' => 'Click here to select files to upload', + 'drag' => '(Or drag files to the page)', + 'loading' => 'Loading', + 'resume' => 'Resume', + 'uploading' => 'Uploading', + 'finished' => 'Finished', + 'failed_error' => 'Upload failed. The server returned an error!', + ], + 'visibility' => [ + 'public' => 'Public', + 'public_expl' => 'Anonymous users can access this album, subject to the restrictions below.', + 'full' => 'Original', + 'full_expl' => 'Anonymous users can view full-resolution photos.', + 'hidden' => 'Hidden', + 'hidden_expl' => 'Anonymous users need a direct link to access this album.', + 'downloadable' => 'Downloadable', + 'downloadable_expl' => 'Anonymous users can download this album.', + 'password' => 'Password', + 'password_prot' => 'Password protected', + 'password_prot_expl' => 'Anonymous users need a shared password to access this album.', + 'nsfw' => 'Sensitive', + 'nsfw_expl' => 'Album contains sensitive content.', + 'visibility_updated' => 'Visibility updated.', + ], + 'move_album' => [ + 'confirm_single' => 'Are you sure you want to move the album “%1$s” into the album “%2$s”?', + 'confirm_multiple' => 'Are you sure you want to move all selected albums into the album “%s”?', + 'move_single' => 'Move Album', + 'move_to' => 'Move to', + 'move_to_single' => 'Move %s to:', + 'move_to_multiple' => 'Move %d albums to:', + 'no_album_target' => 'No album to move to', + 'moved_single' => 'Album moved!', + 'moved_single_details' => '%1$s moved to %2$s', + 'moved_details' => 'Album(s) moved to %s', + ], + 'new_album' => [ + 'menu' => 'Create Album', + 'info' => 'Enter a title for the new album:', + 'title' => 'title', + 'create' => 'Create Album', + ], + 'new_tag_album' => [ + 'menu' => 'Create Tag Album', + 'info' => 'Enter a title for the new tag album:', + 'title' => 'title', + 'set_tags' => 'Set tags to show', + 'warn' => 'Make sure to press enter after each tag', + 'create' => 'Create Tag Album', + ], + 'delete_album' => [ + 'confirmation' => 'Are you sure you want to delete the album “%s” and all of the photos it contains?', + 'confirmation_multiple' => 'Are you sure you want to delete all %d selected albums and all of the photos they contain?', + 'warning' => 'This action can not be undone!', + 'delete' => 'Delete Album and Photos', + ], + 'transfer' => [ + 'query' => 'Transfer ownership of album to', + 'confirmation' => 'Are you sure you want to transfer the ownership of album “%s” and all the photos it contains to "%s"?', + 'lost_access_warning' => 'Your access to this album will be lost.', + 'warning' => 'This action can not be undone!', + 'transfer' => 'Transfer ownership of album and photos', + ], + 'rename' => [ + 'photo' => 'Enter a new title for this photo:', + 'album' => 'Enter a new title for this album:', + 'rename' => 'Rename', + ], + 'merge' => [ + 'merge_to' => 'Merge %s to:', + 'merge_to_multiple' => 'Merge %d albums to:', + 'no_albums' => 'No albums to merge to.', + 'confirm' => 'Are you sure you want to merge the album “%1$s” into the album “%2$s”?', + 'confirm_multiple' => 'Are you sure you want to merge all selected albums into the album “%s”?', + 'merge' => 'Merge Albums', + 'merged' => 'Album(s) merged to %s!', + ], + 'unlock' => [ + 'password_required' => 'This album is protected by a password. Enter the password below to view the photos of this album:', + 'password' => 'Password', + 'unlock' => 'Unlock', + ], + 'photo_tags' => [ + 'question' => 'Enter your tags for this photo.', + 'question_multiple' => 'Enter your tags for all %d selected photos. Existing tags will be overwritten.', + 'no_tags' => 'No Tags', + 'set_tags' => 'Set Tags', + 'updated' => 'Tags updated!', + 'tags_override_info' => 'If this is unchecked, the tags will be added to the existing tags of the photo.', + ], + 'photo_copy' => [ + 'no_albums' => 'No albums to copy to', + 'copy_to' => 'Copy %s to:', + 'copy_to_multiple' => 'Copy %d photos to:', + 'confirm' => 'Copy %s to %s.', + 'confirm_multiple' => 'Copy %d photos to %s.', + 'copy' => 'Copy', + 'copied' => 'Photo(s) copied!', + ], + 'photo_delete' => [ + 'confirm' => 'Are you sure you want to delete the photo “%s”?', + 'confirm_multiple' => 'Are you sure you want to delete all %d selected photos?', + 'deleted' => 'Photo(s) deleted!', + ], + 'move_photo' => [ + 'move_single' => 'Move %s to:', + 'move_multiple' => 'Move %d photos to:', + 'confirm' => 'Move %s to %s.', + 'confirm_multiple' => 'Move %d photos to %s.', + 'moved' => 'Photo(s) moved to %s!', + ], + 'target_user' => [ + 'placeholder' => 'Select user', + ], + 'target_album' => [ + 'placeholder' => 'Select album', + ], + 'webauthn' => [ + 'u2f' => 'U2F', + 'success' => 'Authentication successful!', + 'error' => 'Whoops, it looks like something went wrong. Please reload the site and try again!', + ], + 'se' => [ + 'available' => 'Available in the Supporter Edition', + ], + 'session_expired' => [ + 'title' => 'Session expired', + 'message' => 'Your session has expired.
Please reload the page.', + 'reload' => 'Reload', + 'go_to_gallery' => 'Go to the Gallery', + ], +]; \ No newline at end of file diff --git a/lang/sk/fix-tree.php b/lang/sk/fix-tree.php new file mode 100644 index 00000000000..64803e310e6 --- /dev/null +++ b/lang/sk/fix-tree.php @@ -0,0 +1,55 @@ + 'Maintenance', + 'intro' => 'This page allows you to re-order and fix your albums manually.
Before any modifications, we strongly recommend you to read about Nested Set tree structures.', + 'warning' => 'You can really break your Lychee installation here, modify values at your own risks.', + + 'help' => [ + 'header' => 'Help', + 'hover' => 'Hover ids or titles to highlight related albums.', + 'left' => 'Left', + 'right' => 'Right', + 'convenience' => 'For your convenience, the and buttons allow you to change the values of %s and %s by respectively +1 and -1 with propagation.', + 'left-right-warn' => 'The and indicates that the value of %s (and respectively %s) is duplicated somewhere.', + 'parent-marked' => 'Marked Parent Id indicates that the %s and %s do not satisfy the Nest Set tree structures. Edit either the Parent Id or the %s/%s values.', + 'slowness' => 'This page will be slow with a large number of albums.', + ], + + 'buttons' => [ + 'reset' => 'Reset', + 'check' => 'Check', + 'apply' => 'Apply', + ], + + 'table' => [ + 'title' => 'Title', + 'left' => 'Left', + 'right' => 'Right', + 'id' => 'Id', + 'parent' => 'Parent Id', + ], + + 'errors' => [ + 'invalid' => 'Invalid tree!', + 'invalid_details' => 'We are not applying this as it is guaranteed to be a broken state.', + 'invalid_left' => 'Album %s has an invalid left value.', + 'invalid_right' => 'Album %s has an invalid right value.', + 'invalid_left_right' => 'Album %s has an invalid left/right values. Left should be strictly smaller than right: %s < %s.', + 'duplicate_left' => 'Album %s has a duplicate left value %s.', + 'duplicate_right' => 'Album %s has a duplicate right value %s.', + 'parent' => 'Album %s has an unexpected parent id %s.', + 'unknown' => 'Album %s has an unknown error.', + ], +]; \ No newline at end of file diff --git a/lang/sk/gallery.php b/lang/sk/gallery.php new file mode 100644 index 00000000000..eb8008827e0 --- /dev/null +++ b/lang/sk/gallery.php @@ -0,0 +1,241 @@ + 'Gallery', + + 'smart_albums' => 'Smart albums', + 'albums' => 'Albums', + 'root' => 'Albums', + + 'original' => 'Original', + 'medium' => 'Medium', + 'medium_hidpi' => 'Medium HiDPI', + 'small' => 'Thumb', + 'small_hidpi' => 'Thumb HiDPI', + 'thumb' => 'Square thumb', + 'thumb_hidpi' => 'Square thumb HiDPI', + 'placeholder' => 'Low Quality Image Placeholder', + 'thumbnail' => 'Photo thumbnail', + 'live_video' => 'Video part of live-photo', + + 'camera_data' => 'Camera date', + 'album_reserved' => 'All Rights Reserved', + + 'map' => [ + 'error_gpx' => 'Error loading GPX file', + 'osm_contributors' => 'OpenStreetMap contributors', + ], + + 'search' => [ + 'title' => 'Search', + 'searching' => 'Searching…', + 'no_results' => 'Nothing matches your search query.', + 'searchbox' => 'Search…', + 'minimum_chars' => 'Minimum %s characters required.', + 'photos' => 'Photos (%s)', + 'albums' => 'Albums (%s)', + ], + + 'smart_album' => [ + 'unsorted' => 'Unsorted', + 'starred' => 'Starred', + 'recent' => 'Recent', + 'public' => 'Public', + 'on_this_day' => 'On This Day', + ], + + 'layout' => [ + 'squares' => 'Square thumbnails', + 'justified' => 'With aspect, justified', + 'masonry' => 'With aspect, masonry', + 'grid' => 'With aspect, grid', + ], + + 'overlay' => [ + 'none' => 'None', + 'exif' => 'EXIF data', + 'description' => 'Description', + 'date' => 'Date taken', + ], + + 'timeline' => [ + 'default' => 'default', + 'disabled' => 'disabled', + 'year' => 'Year', + 'month' => 'Month', + 'day' => 'Day', + 'hour' => 'Hour', + ], + + 'album' => [ + 'header_albums' => 'Albums', + 'header_photos' => 'Photos', + 'no_results' => 'Nothing to see here', + 'upload' => 'Upload photos', + + 'tabs' => [ + 'about' => 'About Album', + 'share' => 'Share Album', + 'move' => 'Move Album', + 'danger' => 'DANGER ZONE', + ], + + 'hero' => [ + 'created' => 'Created', + 'copyright' => 'Copyright', + 'subalbums' => 'Subalbums', + 'images' => 'Photos', + 'download' => 'Download Album', + 'share' => 'Share Album', + 'stats_only_se' => 'Statistics available in the Supporter Edition', + ], + + 'stats' => [ + 'lens' => 'Lens', + 'shutter' => 'Shutter speed', + 'iso' => 'ISO', + 'model' => 'Model', + 'aperture' => 'Aperture', + 'no_data' => 'No data', + ], + + 'properties' => [ + 'title' => 'Title', + 'description' => 'Description', + 'photo_ordering' => 'Order photos by', + 'children_ordering' => 'Order albums by', + 'asc/desc' => 'asc/desc', + 'header' => 'Set album header', + 'compact_header' => 'Use compact header', + 'license' => 'Set license', + 'copyright' => 'Set copyright', + 'aspect_ratio' => 'Set album thumbs aspect ratio', + 'album_timeline' => 'Set album timeline mode', + 'photo_timeline' => 'Set photo timeline mode', + 'layout' => 'Set photo layout', + 'show_tags' => 'Set tags to show', + 'tags_required' => 'Tags are required.', + ], + ], + + 'photo' => [ + 'actions' => [ + 'star' => 'Star', + 'unstar' => 'Unstar', + 'set_album_header' => 'Set as album header', + 'move' => 'Move', + 'delete' => 'Delete', + 'header_set' => 'Header set', + ], + + 'details' => [ + 'about' => 'About', + 'basics' => 'Basics', + 'title' => 'Title', + 'uploaded' => 'Uploaded', + 'description' => 'Description', + 'license' => 'License', + 'reuse' => 'Reuse', + 'latitude' => 'Latitude', + 'longitude' => 'Longitude', + 'altitude' => 'Altitude', + 'location' => 'Location', + 'image' => 'Image', + 'video' => 'Video', + 'size' => 'Size', + 'format' => 'Format', + 'resolution' => 'Resolution', + 'duration' => 'Duration', + 'fps' => 'Frame rate', + 'tags' => 'Tags', + 'camera' => 'Camera', + 'captured' => 'Captured', + 'make' => 'Make', + 'type' => 'Type/Model', + 'lens' => 'Lens', + 'shutter' => 'Shutter Speed', + 'aperture' => 'Aperture', + 'focal' => 'Focal Length', + 'iso' => 'ISO %s', + ], + + 'edit' => [ + 'set_title' => 'Set Title', + 'set_description' => 'Set Description', + 'set_license' => 'Set License', + 'no_tags' => 'No Tags', + 'set_tags' => 'Set Tags', + 'set_created_at' => 'Set Upload Date', + ], + ], + + 'nsfw' => [ + 'header' => 'Sensitive content', + 'description' => 'This album contains sensitive content which some people may find offensive or disturbing.', + 'consent' => 'Tap to consent.', + ], + + 'menus' => [ + 'star' => 'Star', + 'unstar' => 'Unstar', + 'star_all' => 'Star Selected', + 'unstar_all' => 'Unstar Selected', + 'tag' => 'Tag', + 'tag_all' => 'Tag Selected', + 'set_cover' => 'Set Album Cover', + 'remove_header' => 'Remove Album Header', + 'set_header' => 'Set Album Header', + 'copy_to' => 'Copy to …', + 'copy_all_to' => 'Copy Selected to …', + 'rename' => 'Rename', + 'move' => 'Move', + 'move_all' => 'Move Selected', + 'delete' => 'Delete', + 'delete_all' => 'Delete Selected', + 'download' => 'Download', + 'download_all' => 'Download Selected', + 'merge' => 'Merge', + 'merge_all' => 'Merge Selected', + + 'upload_photo' => 'Upload Photo', + 'import_link' => 'Import from Link', + 'import_dropbox' => 'Import from Dropbox', + 'new_album' => 'New Album', + 'new_tag_album' => 'New Tag Album', + 'upload_track' => 'Upload track', + 'delete_track' => 'Delete track', + ], + + 'sort' => [ + 'photo_select_1' => 'Upload Time', + 'photo_select_2' => 'Take Date', + 'photo_select_3' => 'Title', + 'photo_select_4' => 'Description', + 'photo_select_6' => 'Star', + 'photo_select_7' => 'Photo Format', + 'ascending' => 'Ascending', + 'descending' => 'Descending', + 'album_select_1' => 'Creation Time', + 'album_select_2' => 'Title', + 'album_select_3' => 'Description', + 'album_select_5' => 'Latest Take Date', + 'album_select_6' => 'Oldest Take Date', + ], + + 'albums_protection' => [ + 'private' => 'private', + 'public' => 'public', + 'inherit_from_parent' => 'inherit from parent', + ], +]; \ No newline at end of file diff --git a/lang/sk/jobs.php b/lang/sk/jobs.php new file mode 100644 index 00000000000..5d952b76012 --- /dev/null +++ b/lang/sk/jobs.php @@ -0,0 +1,18 @@ + 'Jobs', + + 'no_data' => 'No Jobs have been executed yet.', +]; \ No newline at end of file diff --git a/lang/sk/landing.php b/lang/sk/landing.php new file mode 100644 index 00000000000..fe6fe55b8ea --- /dev/null +++ b/lang/sk/landing.php @@ -0,0 +1,19 @@ + 'Gallery', + 'access_gallery' => 'Access the gallery', + 'hosted_with_lychee' => 'Hosted with Lychee', + 'copyright' => 'All images on this website are subject to copyright by %1$s © %2$s', +]; \ No newline at end of file diff --git a/lang/sk/left-menu.php b/lang/sk/left-menu.php new file mode 100644 index 00000000000..9a3e91f4037 --- /dev/null +++ b/lang/sk/left-menu.php @@ -0,0 +1,29 @@ + 'Back to Gallery', + + 'admin' => 'Admin', + 'clockwork' => 'Clockwork App', + 'logs' => 'Show Logs', + 'jobs' => 'Show Job History', + 'user' => 'User', + + 'sign_out' => 'Sign Out', + + 'about' => 'About', + 'api' => 'API Documentation', + 'source_code' => 'Source Code', + 'support' => 'Support', +]; \ No newline at end of file diff --git a/lang/sk/lychee.php b/lang/sk/lychee.php new file mode 100644 index 00000000000..72ee24f32e7 --- /dev/null +++ b/lang/sk/lychee.php @@ -0,0 +1,535 @@ + 'Meno užívateľa', + 'PASSWORD' => 'Heslo', + 'ENTER' => 'Zadať', + 'CANCEL' => 'Prerušiť', + 'CONFIRM' => 'Confirm', + 'SIGN_IN' => 'Prihlásiť', + 'CLOSE' => 'Zatvoriť', + 'SETTINGS' => 'Nastavenia', + 'SEARCH' => 'Hľadaj …', + 'MORE' => 'Viac', + 'DEFAULT' => 'Default', + 'GALLERY' => 'Gallery', + + 'USERS' => 'Užívatelia', + 'PROFILE' => 'Profile', + 'CREATE' => 'Create', + 'REMOVE' => 'Remove', + 'SHARE' => 'Share', + 'U2F' => 'U2F', + 'NOTIFICATIONS' => 'Notifications', + 'SHARING' => 'Zdieľanie', + 'CHANGE_LOGIN' => 'Zmena prihlásenia', + 'CHANGE_SORTING' => 'Zmena zoraďovania', + 'SET_DROPBOX' => 'Dropbox nastaviť', + 'ABOUT_LYCHEE' => 'O Lychee', + 'DIAGNOSTICS' => 'Diagnostika', + 'DIAGNOSTICS_GET_SIZE' => 'Request space usage', + 'JOBS' => 'Show job history', + 'LOGS' => 'Protokoly', + 'SIGN_OUT' => 'Odhlásiť', + 'UPDATE_AVAILABLE' => 'Update je k dispozícii!', + 'MIGRATION_AVAILABLE' => 'Migration available!', + 'CHECK_FOR_UPDATE' => 'Check for updates', + 'DEFAULT_LICENSE' => 'Predvolená licencia pre nové', + 'SET_LICENSE' => 'Použiť licenciu', + 'SET_OVERLAY_TYPE' => 'Nastaviť typ overlay', + 'SET_ALBUM_DECORATION' => 'Set album decorations', + 'SET_MAP_PROVIDER' => 'Set OpenStreetMap tiles provider', + 'FULL_SETTINGS' => 'Full Settings', + 'UPDATE' => 'Update', + 'RESET' => 'Reset', + 'DISABLE_TOKEN_TOOLTIP' => 'Disable', + 'ENABLE_TOKEN' => 'Enable API token', + 'DISABLED_TOKEN_STATUS_MSG' => 'Disabled', + 'TOKEN_BUTTON' => 'API Token ...', + 'TOKEN_NOT_AVAILABLE' => 'You have already viewed this token.', + 'TOKEN_WAIT' => 'Wait ...', + + 'SMART_ALBUMS' => 'Inteligentné albumy', + 'SHARED_ALBUMS' => 'Zdieľané albumy', + 'ALBUMS' => 'Albumy', + 'PHOTOS' => 'Obrázky', + 'SEARCH_RESULTS' => 'Search results', + + 'RENAME' => 'Premenovať', + 'RENAME_ALL' => 'Premenovať vybrané', + 'MERGE' => 'Zlúčiť', + 'MERGE_ALL' => 'Zlúčiť vybrané', + 'MAKE_PUBLIC' => 'Publikovať', + 'SHARE_ALBUM' => 'Album zdieľať', + 'SHARE_PHOTO' => 'Foto zdieľať', + 'VISIBILITY_ALBUM' => 'Album Visibility', + 'VISIBILITY_PHOTO' => 'Photo Visibility', + 'DOWNLOAD_ALBUM' => 'Album stiahnuť', + 'ABOUT_ALBUM' => 'O Albume', + 'DELETE_ALBUM' => 'Album zmazať', + 'MOVE_ALBUM' => 'Album presunúť', + 'FULLSCREEN_ENTER' => 'Celá obrazovka', + 'FULLSCREEN_EXIT' => 'Opustiť celú obrazovku', + + 'SHARING_ALBUM_USERS' => 'Share this album with users', + 'WAIT_FETCH_DATA' => 'Please wait while we get the data …', + 'SHARING_ALBUM_USERS_NO_USERS' => 'There are no users to share the album with', + 'SHARING_ALBUM_USERS_LONG_MESSAGE' => 'Select the users to share this album with', + + 'DELETE_ALBUM_QUESTION' => 'Album a obrázky zmazať', + 'KEEP_ALBUM' => 'Album ponechať', + 'DELETE_ALBUM_CONFIRMATION' => 'Ste si istý, že chcete album „%s“ a všetky obrázky v ňom zmazať? Táto akcia je nevratná!', + + 'DELETE_TAG_ALBUM_QUESTION' => 'Delete Album', + 'DELETE_TAG_ALBUM_CONFIRMATION' => 'Are you sure you want to delete the album „%s“ (any photos inside will not be deleted)? This action can’t be undone!', + + 'DELETE_ALBUMS_QUESTION' => 'Všetky albumy a obrázky zmazať', + 'KEEP_ALBUMS' => 'Albumy ponechať', + 'DELETE_ALBUMS_CONFIRMATION' => 'Ste si istý, že chcete všetky %d vybrané albumy a všetky obrázky v nich zmazať? Táto akcia je nevratná!', + + 'DELETE_UNSORTED_CONFIRM' => 'Ste si istý, že chcete všetky obrázky z „Netriedené“ zmazať? Táto akcia je nevratná!', + 'CLEAR_UNSORTED' => 'Netriedené zmazať', + 'KEEP_UNSORTED' => 'Netriedené ponechať', + + 'EDIT_SHARING' => 'Upraviť zdieľanie', + 'MAKE_PRIVATE' => 'Súkromné', + + 'CLOSE_ALBUM' => 'Album zavrieť', + 'CLOSE_PHOTO' => 'Foto Zavrieť', + 'CLOSE_MAP' => 'Close Map', + + 'ADD' => 'Pridať', + 'MOVE' => 'Presunúť', + 'MOVE_ALL' => 'Presunúť vybrané', + 'DUPLICATE' => 'Duplikovať', + 'DUPLICATE_ALL' => 'Duplikovať vybrané', + 'COPY_TO' => 'Kopírovať do …', + 'COPY_ALL_TO' => 'Kopírovať vybrané do …', + 'DELETE' => 'Zmazať', + 'SAVE' => 'Save', + 'DELETE_ALL' => 'Zmazať vybrané', + 'DOWNLOAD' => 'Stiahnuť', + 'DOWNLOAD_ALL' => 'Stiahnuť vybrané', + 'UPLOAD_PHOTO' => 'Foto nahrať', + 'IMPORT_LINK' => 'Importovať z linku', + 'IMPORT_DROPBOX' => 'Importovať z Dropbox', + 'IMPORT_SERVER' => 'Importovať zo servera', + 'NEW_ALBUM' => 'Nový album', + 'NEW_TAG_ALBUM' => 'New Tag Album', + 'UPLOAD_TRACK' => 'Upload track', + 'DELETE_TRACK' => 'Delete track', + + 'TITLE_NEW_ALBUM' => 'Zadajte názov pre nový album:', + 'UNTITLED' => 'Bez názvu', + 'UNSORTED' => 'Netriedený', + 'STARRED' => 'Obľúbený', + 'RECENT' => 'Naposledy použitý', + 'PUBLIC' => 'Verejný', + 'ON_THIS_DAY' => 'On This Day', + 'NUM_PHOTOS' => 'obrázkov', + + 'CREATE_ALBUM' => 'Album vytvoriť', + 'CREATE_TAG_ALBUM' => 'Create Tag Album', + + 'STAR_PHOTO' => 'Obrázok označiť ako obľúbený', + 'STAR' => 'označiť ako obľúbené', + 'UNSTAR' => 'Unstar', + 'STAR_ALL' => 'všetky označiť ako obľúbené', + 'UNSTAR_ALL' => 'Unstar Selected', + 'TAG' => 'Štítky', + 'TAG_ALL' => 'Štítky pre všetky', + 'UNSTAR_PHOTO' => 'Obrázok odstrániť z obľúbených', + 'SET_COVER' => 'Set Album Cover', + 'REMOVE_COVER' => 'Remove Album Cover', + 'SET_HEADER' => 'Set Album Header', + 'REMOVE_HEADER' => 'Remove Album Header', + 'SET_COMPACT_HEADER' => 'Use Compact Header', + + 'FULL_PHOTO' => 'Otvoriť originál', + 'ABOUT_PHOTO' => 'O tomto obrázku', + 'DISPLAY_FULL_MAP' => 'Map', + 'DIRECT_LINK' => 'Priamy link', + 'DIRECT_LINKS' => 'Priame linky', + 'QR_CODE' => 'QR Code', + + 'ALBUM_ABOUT' => 'O albume', + 'ALBUM_BASICS' => 'Základné informácie', + 'ALBUM_TITLE' => 'Názov', + 'ALBUM_COPYRIGHT' => 'Copyright', + 'ALBUM_SET_COPYRIGHT' => 'Set copyright', + 'ALBUM_NEW_TITLE' => 'Zadajte nový názov pre tento album:', + 'ALBUMS_NEW_TITLE' => 'Zadajte názov pre všetky %d vybrané albumy:', + 'ALBUM_SET_TITLE' => 'Názov uložiť', + 'ALBUM_DESCRIPTION' => 'Popis', + 'ALBUM_SHOW_TAGS' => 'Tags to show', + 'ALBUM_NEW_DESCRIPTION' => 'Zadajte nový popis pre tento album:', + 'ALBUM_SET_DESCRIPTION' => 'Popis uložiť', + 'ALBUM_NEW_SHOWTAGS' => 'Enter tags of photos that will be visible in this album:', + 'ALBUM_SET_SHOWTAGS' => 'Set tags to show', + 'ALBUM_ALBUM' => 'Album', + 'ALBUM_CREATED' => 'Vytvorené', + 'ALBUM_IMAGES' => 'Obrázky', + 'ALBUM_VIDEOS' => 'Videá', + 'ALBUM_SUBALBUMS' => 'Subalbumy', + 'ALBUM_SHARING' => 'Zdieľanie', + 'ALBUM_SHR_YES' => 'Áno', + 'ALBUM_SHR_NO' => 'Nie', + 'ALBUM_PUBLIC' => 'Verejný', + 'ALBUM_PUBLIC_EXPL' => 'Anonymous users can access this album, subject to the restrictions below.', + 'ALBUM_FULL' => 'Originál', + 'ALBUM_FULL_EXPL' => 'Anonymous users can behold full-resolution photos.', + 'ALBUM_HIDDEN' => 'Skrytý', + 'ALBUM_HIDDEN_EXPL' => 'Anonymous users need a direct link to access this album.', + 'ALBUM_MARK_NSFW' => 'Mark album as sensitive', + 'ALBUM_UNMARK_NSFW' => 'Unmark album as sensitive', + 'ALBUM_NSFW' => 'Sensitive', + 'ALBUM_NSFW_EXPL' => 'Album is marked to contain sensitive content.', + 'ALBUM_DOWNLOADABLE' => 'Stiahnuteľný', + 'ALBUM_DOWNLOADABLE_EXPL' => 'Anonymous users can download this album.', + 'ALBUM_SHARE_BUTTON_VISIBLE' => 'Share button is visible', + 'ALBUM_SHARE_BUTTON_VISIBLE_EXPL' => 'Anonymous users can see social media sharing links.', + 'ALBUM_PASSWORD' => 'Heslo', + 'ALBUM_PASSWORD_PROT' => 'Chránené heslom', + 'ALBUM_PASSWORD_PROT_EXPL' => 'Anonymous users need a shared password to access this album.', + 'ALBUM_PASSWORD_REQUIRED' => 'Tento album je chránený heslom. Zadajte heslo:', + 'ALBUM_MERGE' => 'Ste si istý, že chcete album „%1$s“ s týmto albumom zlúčiť „%2$s“?', + 'ALBUMS_MERGE' => 'Ste si istý, že chcete všetky vybrané albumy s týmto albumom zlúčiť „%s“?', + 'MERGE_ALBUM' => 'Albumy zlúčiť', + 'DONT_MERGE' => 'Nezlučovať', + 'ALBUM_MOVE' => 'Ste si istý, že chcete album „%1$s“ presunúť do nasledujúceho albumu „%2$s“?', + 'ALBUMS_MOVE' => 'Ste si istý, že chcete všetky vybrané albumy presunúť do nasledovného albumu „%s“?', + 'MOVE_ALBUMS' => 'Albumy presunúť', + 'NOT_MOVE_ALBUMS' => 'Nepresúvať', + 'ROOT' => 'Albumy', + 'ALBUM_REUSE' => 'Použiť znova', + 'ALBUM_LICENSE' => 'Licencia', + 'ALBUM_SET_LICENSE' => 'Licenciu určiť', + 'ALBUM_LICENSE_HELP' => 'Potrebujete pomoc pri výbere?', + 'ALBUM_LICENSE_NONE' => 'Žiadna', + 'ALBUM_RESERVED' => 'Všetky práva vyhradené', + 'ALBUM_SET_ORDER' => 'Set Order', + 'ALBUM_ORDERING' => 'Order by', + 'ALBUM_PHOTO_ORDERING' => 'Order photos by', + 'ALBUM_CHILDREN_ORDERING' => 'Order albums by', + 'ALBUM_OWNER' => 'Owner', + + 'PHOTO_ABOUT' => 'O obrázku', + 'PHOTO_BASICS' => 'Základné informácie', + 'PHOTO_TITLE' => 'Názov', + 'PHOTO_NEW_TITLE' => 'Zadajte nový názov pre tento obrázok:', + 'PHOTO_SET_TITLE' => 'Názov uložiť', + 'PHOTO_UPLOADED' => 'Nahratie', + 'PHOTO_DESCRIPTION' => 'Popis', + 'PHOTO_NEW_DESCRIPTION' => 'Zadajte nový popis pre tento obrázok:', + 'PHOTO_SET_DESCRIPTION' => 'Popis uložiť', + 'PHOTO_NEW_LICENSE' => 'Pridať novú licenciu', + 'PHOTO_SET_LICENSE' => 'Určiť licenciu', + 'PHOTO_LICENSE' => 'Licencia', + 'PHOTO_LICENSE_HELP' => 'Need help choosing?', + 'PHOTO_REUSE' => 'Opakované použitie', + 'PHOTO_LICENSE_NONE' => 'žiadne', + 'PHOTO_RESERVED' => 'Všetky práva vyhradené', + 'PHOTO_LATITUDE' => 'Zemepisná šírka', + 'PHOTO_LONGITUDE' => 'Zemepisná dĺžka', + 'PHOTO_ALTITUDE' => 'Nadmorská výška', + 'PHOTO_IMGDIRECTION' => 'Smer', + 'PHOTO_LOCATION' => 'Location', + 'PHOTO_IMAGE' => 'Obrázok', + 'PHOTO_VIDEO' => 'Video', + 'PHOTO_SIZE' => 'Veľkosť', + 'PHOTO_FORMAT' => 'Formát', + 'PHOTO_RESOLUTION' => 'Rozlíšenie', + 'PHOTO_DURATION' => 'Trvanie', + 'PHOTO_FPS' => 'Počet snímkov/s', + 'PHOTO_TAGS' => 'Štítky', + 'PHOTO_NOTAGS' => 'Žiadne štítky', + 'PHOTO_NEW_TAGS' => 'Zadajte štítky pre tento obrázok. Jednotlivé štítky oddeľte čiarkou:', + 'PHOTOS_NEW_TAGS' => 'Zadajte štítky pre všetky %d vybrané obrázky. Doterajšie štítky budú prepísané.Jednotlivé štítky oddeľte čiarkou:', + 'PHOTO_SET_TAGS' => 'Štítky uložiť', + 'PHOTO_CAMERA' => 'Kamera', + 'PHOTO_CAPTURED' => 'Zosnímané', + 'PHOTO_MAKE' => 'Značka', + 'PHOTO_TYPE' => 'Typ/Model', + 'PHOTO_LENS' => 'Objektív', + 'PHOTO_SHUTTER' => 'Uzávierka', + 'PHOTO_APERTURE' => 'Clona', + 'PHOTO_FOCAL' => 'Fokus', + 'PHOTO_ISO' => 'ISO %s', + 'PHOTO_SHARING' => 'Zdieľať', + 'PHOTO_DELETE' => 'Zmazať obrázok', + 'PHOTO_KEEP' => 'Obrázok ponechať', + 'PHOTO_DELETE_CONFIRMATION' => 'Ste si istý, že chcete obrázok „%s“ zmazať? Táto akcia je nevratná!', + 'PHOTO_DELETE_ALL' => 'Ste si istý, že chcete všetky %d vybrané obrázky zmazať? Táto akcia je nevratná!', + 'PHOTOS_NEW_TITLE' => 'Zadaje nový názov pre všetky %d vybrané obrázky:', + 'PHOTO_MAKE_PRIVATE_ALBUM' => 'Tento obrázok sa nachádza vo verejnom albume. Označenie obrázku ako verejný alebo súkromný musíte nastaviť na albume, v ktorom sa nachádza.', + 'PHOTO_SHOW_ALBUM' => 'Album zobraziť', + 'PHOTO_PUBLIC' => 'Verejný', + 'PHOTO_PUBLIC_EXPL' => 'Anonymous users can view this photo, subject to the restrictions below.', + 'PHOTO_FULL' => 'Originál', + 'PHOTO_FULL_EXPL' => 'Anonymous users can behold full-resolution photo.', + 'PHOTO_HIDDEN' => 'Skrytý', + 'PHOTO_HIDDEN_EXPL' => 'Anonymous users need a direct link to view this photo.', + 'PHOTO_DOWNLOADABLE' => 'Stiahnuteľný', + 'PHOTO_DOWNLOADABLE_EXPL' => 'Anonymous users may download this photo.', + 'PHOTO_SHARE_BUTTON_VISIBLE' => 'Share button is visible', + 'PHOTO_SHARE_BUTTON_VISIBLE_EXPL' => 'Anonymous users can see social media sharing links.', + 'PHOTO_PASSWORD_PROT' => 'Chránené heslom', + 'PHOTO_PASSWORD_PROT_EXPL' => 'Anonymous users need a shared password to view this photo.', + 'PHOTO_EDIT_SHARING_TEXT' => 'Vlastnosti zdieľania tejto fotografie sa zmenia na nasledujúce:', + 'PHOTO_NO_EDIT_SHARING_TEXT' => 'Pretože je táto fotografia umiestnená vo verejnom albume, zdedí nastavenie viditeľnosti daného albumu. Jeho aktuálna viditeľnosť je uvedená len na informačné účely.', + 'PHOTO_EDIT_GLOBAL_SHARING_TEXT' => 'Viditeľnosť tejto fotografie je možné doladiť pomocou globálnych nastavení. Jeho aktuálna viditeľnosť je uvedená len na informačné účely.', + 'PHOTO_NEW_CREATED_AT' => 'Enter the upload date for this photo. mm/dd/yyyy, hh:mm [am/pm]', + 'PHOTO_SET_CREATED_AT' => 'Set upload date', + + 'LOADING' => 'Nahráva sa', + 'ERROR' => 'Chyba', + 'ERROR_TEXT' => 'Asi sa niečo pokazilo. Obnovte stránku a skúste znova!', + 'ERROR_UNKNOWN' => 'Vyskytla sa neočakávaná chyba. Skúste to znova a skontrolujte inštaláciu na vašom serveri. Ďalšie informácie nájdete v súbore README.', + 'ERROR_MAP_DEACTIVATED' => 'Map functionality has been deactivated under settings.', + 'ERROR_SEARCH_DEACTIVATED' => 'Search functionality has been deactivated under settings.', + 'SUCCESS' => 'OK', + 'CHANGE_SUCCESS' => 'Change successful.', + 'RETRY' => 'Opakovať', + 'OVERRIDE' => 'Override', + 'TAGS_OVERRIDE_INFO' => 'If this is unchecked, the tags will be added to the existing tags of the photo.', + + 'SETTINGS_SUCCESS_LOGIN' => 'Užívateľské dáta aktualizované', + 'SETTINGS_SUCCESS_SORT' => 'Triedenie aktualizované', + 'SETTINGS_SUCCESS_DROPBOX' => 'Kľúč Dropbox aktualizovaný', + 'SETTINGS_SUCCESS_LANG' => 'Jazyk aktualizovaný', + 'SETTINGS_SUCCESS_LAYOUT' => 'Layout aktualizovaný', + 'SETTINGS_SUCCESS_IMAGE_OVERLAY' => 'EXIF-Overlay nastavenia aktualizované', + 'SETTINGS_SUCCESS_PUBLIC_SEARCH' => 'Verejné vyhľadávanie bolo aktualizované', + 'SETTINGS_SUCCESS_LICENSE' => 'Prednastavená licencia aktualizovaná', + 'SETTINGS_SUCCESS_MAP_DISPLAY' => 'Nastavenie zobrazenia mapy aktualizované', + 'SETTINGS_SUCCESS_MAP_DISPLAY_PUBLIC' => 'Map display settings for public albums updated', + 'SETTINGS_SUCCESS_MAP_PROVIDER' => 'Map provider settings updated', + 'SETTINGS_SUCCESS_CSS' => 'CSS aktualizované', + 'SETTINGS_SUCCESS_JS' => 'JS aktualizované', + 'SETTINGS_SUCCESS_UPDATE' => 'Nastavenia úspešne aktualizované', + 'SETTINGS_DROPBOX_KEY' => 'Dropbox API Key', + 'SETTINGS_ADVANCED_WARNING_EXPL' => 'Zmena rozšírených nastavení môže mať negatívny vplyv na stabilitu, bezpečnosť a rýchlosť tejto aplikácie. Upravujte len to, o čom presne viete, čo robíte.', + 'SETTINGS_ADVANCED_SAVE' => 'Zmeny uložiť, riziko je známe!', + + 'U2F_NOT_SUPPORTED' => 'U2F not supported. Sorry.', + 'U2F_NOT_SECURE' => 'Environment not secured. U2F not available.', + 'U2F_REGISTER_KEY' => 'Register new device.', + 'U2F_REGISTRATION_SUCCESS' => 'Registration successful!', + 'U2F_AUTHENTIFICATION_SUCCESS' => 'Authentication successful!', + 'U2F_CREDENTIALS' => 'Credentials', + 'U2F_CREDENTIALS_DELETED' => 'Credentials deleted!', + 'U2F_LOGIN' => 'Log in with WebAuthn', + + 'NEW_PHOTOS_NOTIFICATION' => 'Send new photos notification emails.', + 'SETTINGS_SUCCESS_NEW_PHOTOS_NOTIFICATION' => 'New photos notification updated', + 'USER_EMAIL_INSTRUCTION' => 'Add your email below to enable receiving email notifications. To stop receiving emails, simply remove your email below.', + + 'LOGIN_USERNAME' => 'Nový užívateľ', + 'LOGIN_PASSWORD' => 'Nové heslo', + 'LOGIN_PASSWORD_CONFIRM' => 'Heslo potvrdiť', + 'PASSWORD_TITLE' => 'Zadajte vaše heslo:', + 'PASSWORD_CURRENT' => 'Vaše pôvodné heslo', + 'PASSWORD_TEXT' => 'Vaše meno a heslo bolo zmenené nasledovne:', + 'PASSWORD_CHANGE' => 'Prihlásenie zmeniť', + + 'EDIT_SHARING_TITLE' => 'Zdieľanie spracovať', + 'EDIT_SHARING_TEXT' => 'Nastavenie zdieľania pre tento album bolo zmenené nasledovne:', + 'SHARE_ALBUM_TEXT' => 'Tento album bude zdieľaný s nasledovnými vlastnosťami:', + + 'SORT_DIALOG_ATTRIBUTE_LABEL' => 'Attribute', + 'SORT_DIALOG_ORDER_LABEL' => 'Order', + + 'SORT_ALBUM_BY' => 'Triediť albumy podľa %1$s v %2$s rade.', + + 'SORT_ALBUM_SELECT_1' => 'Čas vytvorenia', + 'SORT_ALBUM_SELECT_2' => 'Titul', + 'SORT_ALBUM_SELECT_3' => 'Popis', + 'SORT_ALBUM_SELECT_5' => 'Najnovšia zmena', + 'SORT_ALBUM_SELECT_6' => 'Najstaršia zmena', + + 'SORT_PHOTO_BY' => 'Obrázky triediť podľa %1$s v %2$s rade.', + + 'SORT_PHOTO_SELECT_1' => 'Čas nahratia', + 'SORT_PHOTO_SELECT_2' => 'čas snímku', + 'SORT_PHOTO_SELECT_3' => 'Titul', + 'SORT_PHOTO_SELECT_4' => 'Popis', + 'SORT_PHOTO_SELECT_6' => 'Obľúbený', + 'SORT_PHOTO_SELECT_7' => 'Formát', + + 'SORT_ASCENDING' => 'vzostupnej', + 'SORT_DESCENDING' => 'zostupnej', + 'SORT_CHANGE' => 'Zmeniť triedenie', + + 'DROPBOX_TITLE' => 'Nastaviť kľúč Dropbox', + 'DROPBOX_TEXT' => "Aby ste mohli importovať obrázky z Dropbox, potrebujete platný API-Key zo stránky Dropbox. Vytvorte personal key a zadajte ho:", + + 'LANG_TEXT' => 'Zmeniť jazyk Lychee na:', + 'LANG_TITLE' => 'Zmena jazyka', + + 'SETTING_RECENT_PUBLIC_TEXT' => 'Make "Recent" smart album accessible to anonymous users', + 'SETTING_STARRED_PUBLIC_TEXT' => 'Make "Starred" smart album accessible to anonymous users', + 'SETTING_ONTHISDAY_PUBLIC_TEXT' => 'Make "On This Day" smart album accessible to anonymous users', + + 'CSS_TEXT' => 'Vlastné CSS:', + 'CSS_TITLE' => 'CSS zmeniť', + 'JS_TEXT' => 'Vlastné JS:', + 'JS_TITLE' => 'JS zmeniť', + 'PUBLIC_SEARCH_TEXT' => 'Verejné vyhľadávanie povolené:', + 'OVERLAY_TYPE' => 'Dáta použité pre overlay:', + 'OVERLAY_NONE' => 'None', + 'OVERLAY_EXIF' => 'EXIF dáta obrázku', + 'OVERLAY_DESCRIPTION' => 'Popis obrázku', + 'OVERLAY_DATE' => 'Obrázok snímaný dňa', + 'ALBUM_DECORATION' => 'Album decorations:', + 'ALBUM_DECORATION_NONE' => 'None', + 'ALBUM_DECORATION_ORIGINAL' => 'Sub-album marker', + 'ALBUM_DECORATION_ALBUM' => 'Number of sub-albums', + 'ALBUM_DECORATION_PHOTO' => 'Number of photos', + 'ALBUM_DECORATION_ALL' => 'Number of sub-albums and photos', + 'ALBUM_DECORATION_ORIENTATION' => 'Orientation of album decorations:', + 'ALBUM_DECORATION_ORIENTATION_ROW' => 'Horizontal (photos, albums)', + 'ALBUM_DECORATION_ORIENTATION_ROW_REVERSE' => 'Horizontal (albums, photos)', + 'ALBUM_DECORATION_ORIENTATION_COLUMN' => 'Vertical (top photos, albums)', + 'ALBUM_DECORATION_ORIENTATION_COLUMN_REVERSE' => 'Vertical (top albums, photos)', + 'MAP_DISPLAY_TEXT' => 'Enable maps (provided by OpenStreetMap):', + 'MAP_DISPLAY_PUBLIC_TEXT' => 'Enable maps for public albums (provided by OpenStreetMap):', + 'MAP_PROVIDER' => 'Provider of OpenStreetMap tiles:', + 'MAP_PROVIDER_WIKIMEDIA' => 'Wikimedia', + 'MAP_PROVIDER_OSM_ORG' => 'OpenStreetMap.org (no HiDPI)', + 'MAP_PROVIDER_OSM_DE' => 'OpenStreetMap.de (no HiDPI)', + 'MAP_PROVIDER_OSM_FR' => 'OpenStreetMap.fr (no HiDPI)', + 'MAP_PROVIDER_RRZE' => 'University of Erlangen, Germany (only HiDPI)', + 'MAP_INCLUDE_SUBALBUMS_TEXT' => 'Include photos of subalbums on map:', + 'LOCATION_DECODING' => 'Decode GPS data into location name', + 'LOCATION_SHOW' => 'Show location name', + 'LOCATION_SHOW_PUBLIC' => 'Show location name for public mode', + + 'LAYOUT_TYPE' => 'Rozmiestnenie obrázkov:', + 'LAYOUT_SQUARES' => 'Štvorcové náhľady', + 'LAYOUT_JUSTIFIED' => 'Zachovaný pomer strán, zarovnané', + 'LAYOUT_MASONRY' => 'Zachovaný pomer strán, masonry', + 'LAYOUT_GRID' => 'Zachovaný pomer strán, grid', + 'LAYOUT_UNJUSTIFIED' => 'Zachovaný pomer strán, nezarovnané', + 'SET_LAYOUT' => 'Zmeniť rozmiestnenie', + + 'NSFW_VISIBLE_TEXT_1' => 'Make Sensitive albums visible by default.', + 'NSFW_VISIBLE_TEXT_2' => 'If the album is public, it is still accessible, just hidden from the view and can be revealed by pressing H.', + 'SETTINGS_SUCCESS_NSFW_VISIBLE' => 'Default sensitive album visibility updated with success.', + + 'NSFW_BANNER' => '

Sensitive content

This album contains sensitive content which some people may find offensive or disturbing.

Tap to consent.

', + 'NSFW_HEADER' => 'Sensitive content', + 'NSFW_EXPLANATION' => 'This album contains sensitive content which some people may find offensive or disturbing.', + 'TAP_CONSENT' => 'Tap to consent.', + + 'VIEW_NO_RESULT' => 'Žiadny výsledok', + 'VIEW_NO_PUBLIC_ALBUMS' => 'Žiadne verejné albumy', + 'VIEW_NO_CONFIGURATION' => 'Žiadna konfigurácia', + 'VIEW_PHOTO_NOT_FOUND' => 'Žiadny obrázok', + + 'NO_TAGS' => 'Žiadne štítky', + + 'UPLOAD_MANAGE_NEW_PHOTOS' => 'Môžete spravovať vaše nové obrázky.', + 'UPLOAD_COMPLETE' => 'Nahrávanie ukončené', + 'UPLOAD_COMPLETE_FAILED' => 'Chyba pri nahrávaní jedného alebo viacerých obrázkov.', + 'UPLOAD_IMPORTING' => 'Importovať', + 'UPLOAD_IMPORTING_URL' => 'Importovať URL', + 'UPLOAD_UPLOADING' => 'Nahrať', + 'UPLOAD_FINISHED' => 'Ukončené', + 'UPLOAD_PROCESSING' => 'Spracováva sa', + 'UPLOAD_FAILED' => 'Zlyhanie', + 'UPLOAD_FAILED_ERROR' => 'Nahrávanie zlyhalo. Server ohlásil chybu!', + 'UPLOAD_FAILED_WARNING' => 'Nahrávanie zlyhalo. Server ohlásil varovanie!', + 'UPLOAD_CANCELLED' => 'Cancelled', + 'UPLOAD_SKIPPED' => 'Preskočiť', + 'UPLOAD_UPDATED' => 'Updated', + 'UPLOAD_GENERAL' => 'General', + 'UPLOAD_IMPORT_SKIPPED_DUPLICATE' => 'This photo has been skipped because it’s already in your library.', + 'UPLOAD_IMPORT_RESYNCED_DUPLICATE' => 'This photo has been skipped because it’s already in your library, but its metadata has been updated.', + 'UPLOAD_ERROR_CONSOLE' => 'Skontrolujte konzolu prehliadača, pre zistenie ďalších podrobností.', + 'UPLOAD_UNKNOWN' => 'Server vrátil neznámu odpoveď.Skontrolujte konzolu prehliadača, pre zistenie ďalších podrobností.', + 'UPLOAD_ERROR_UNKNOWN' => 'Nahrávanie zlyhalo. Server ohlásil neznámu chybu!', + 'UPLOAD_ERROR_POSTSIZE' => 'Upload failed. The PHP post_max_size may be too small! Otherwise check the FAQ.', + 'UPLOAD_ERROR_FILESIZE' => 'Upload failed. The PHP upload_max_filesize may be too small! Otherwise check the FAQ.', + 'UPLOAD_IN_PROGRESS' => 'Lychee práve nahráva!', + 'UPLOAD_IMPORT_WARN_ERR' => 'Import je hotový, vyskytli sa ale chyby alebo varovania. Skontrolujte protokoly (Nastavenia/ Protokoly).', + 'UPLOAD_IMPORT_COMPLETE' => 'Import hotový', + 'UPLOAD_IMPORT_INSTR' => 'Pre import zadajte priamy link:', + 'UPLOAD_IMPORT' => 'Import', + 'UPLOAD_IMPORT_SERVER' => 'Import zo servera', + 'UPLOAD_IMPORT_SERVER_FOLD' => 'Priečinok je prázdny alebo obsahuje nečitateľný obsah pre spracovanie. Skontrolujte protokoly (Nastavenia/ Protokoly) pre zistenie ďalších podrobností.', + 'UPLOAD_IMPORT_SERVER_INSTR' => 'Import all photos, folders and sub-folders located in the folders with the following absolute paths (on server). Paths are space separated, use \\ to escape a space in a path.', + 'UPLOAD_ABSOLUTE_PATH' => 'Absolute path to directories, space separated', + 'UPLOAD_IMPORT_SERVER_EMPT' => 'Import sa nedá spustiť, priečinok je prázdny.', + 'UPLOAD_IMPORT_DELETE_ORIGINALS' => 'Zmazať originály', + 'UPLOAD_IMPORT_DELETE_ORIGINALS_EXPL' => 'Ak je možné, budú pôvodné súbory po importe zmazané.', + 'UPLOAD_IMPORT_VIA_SYMLINK' => 'Symbolic links', + 'UPLOAD_IMPORT_VIA_SYMLINK_EXPL' => 'Import files using symbolic links to originals.', + 'UPLOAD_IMPORT_SKIP_DUPLICATES' => 'Skip duplicates', + 'UPLOAD_IMPORT_SKIP_DUPLICATES_EXPL' => 'Existing media files are skipped.', + 'UPLOAD_IMPORT_RESYNC_METADATA' => 'Re-sync metadata', + 'UPLOAD_IMPORT_RESYNC_METADATA_EXPL' => 'Update metadata of existing media files.', + 'UPLOAD_IMPORT_LOW_MEMORY_EXPL' => 'Proces importu na serveri sa blíži k limitu pamäte a môže skončiť predčasným ukončením.', + 'UPLOAD_WARNING' => 'Varovanie', + 'UPLOAD_IMPORT_NOT_A_DIRECTORY' => 'Adresaár nie je čitateľný!', + 'UPLOAD_IMPORT_PATH_RESERVED' => 'Zadaná cesta je rezervovaná pre Lychee!', + 'UPLOAD_IMPORT_FAILED' => 'Súbor sa nedá naimportovať!', + 'UPLOAD_IMPORT_UNSUPPORTED' => 'Nepodporovaný typ súboru!', + 'UPLOAD_IMPORT_CANCELLED' => 'Import cancelled', + + 'ABOUT_SUBTITLE' => 'Vlastný hostovaný manažment obrázkov!', + 'ABOUT_DESCRIPTION' => 'Lychee je open-source nástroj, bežiaci na vašom vlastnom serveri alebo v cloude. Inštalácia je otázkou sekúnd. Nahrať, spravovať a zdieľať obrázky ako v natívnej aplikácii. Lychee ponúka všetko čo potrebujete vy a vaše obrázky pre bezpečné uloženie.', + 'FOOTER_COPYRIGHT' => 'Všetky obrázky na tejto webovej stránke sú chránené autorským právom %1$s © %2$s', + 'HOSTED_WITH_LYCHEE' => 'Hostované s Lychee', + + 'URL_COPY_TO_CLIPBOARD' => 'Skopírovať do schránky', + 'URL_COPIED_TO_CLIPBOARD' => 'URL skopírované do schránky!', + 'PHOTO_DIRECT_LINKS_TO_IMAGES' => 'Priame linky k súborom obrázkov:', + 'PHOTO_ORIGINAL' => 'Original', + 'PHOTO_MEDIUM' => 'Medium', + 'PHOTO_MEDIUM_HIDPI' => 'Medium HiDPI', + 'PHOTO_SMALL' => 'Náhľad', + 'PHOTO_SMALL_HIDPI' => 'Náhľad HiDPI', + 'PHOTO_THUMB' => 'Štvorcový náhľad', + 'PHOTO_THUMB_HIDPI' => 'Štvorcový náhľad HiDPI', + 'PHOTO_PLACEHOLDER' => 'Low Quality Image Placeholder', + 'PHOTO_THUMBNAIL' => 'Photo thumbnail', + 'PHOTO_LIVE_VIDEO' => 'Video part of live-photo', + 'PHOTO_VIEW' => 'Zobrazenie foto Lychee:', + + 'PHOTO_EDIT_ROTATECWISE' => 'Rotate clockwise', + 'PHOTO_EDIT_ROTATECCWISE' => 'Rotate counter-clockwise', + + 'ERROR_GPX' => 'Error loading GPX file: ', + 'ERROR_EITHER_ALBUMS_OR_PHOTOS' => 'Please select either albums or photos!', + 'ERROR_COULD_NOT_FIND' => 'Could not find what you want.', + 'ERROR_INVALID_EMAIL' => 'Not a valid email address.', + 'EMAIL_SUCCESS' => 'Email updated!', + 'ERROR_PHOTO_NOT_FOUND' => 'Error: photo %s not found !', + 'ERROR_EMPTY_USERNAME' => 'new username cannot be empty.', + 'ERROR_PASSWORD_DOES_NOT_MATCH' => 'new password does not match.', + 'ERROR_EMPTY_PASSWORD' => 'new password cannot be empty.', + 'ERROR_SELECT_ALBUM' => 'Select an album to share!', + 'ERROR_SELECT_USER' => 'Select a user to share with!', + 'ERROR_SELECT_SHARING' => 'Select a sharing to remove!', + 'SHARING_SUCCESS' => 'Sharing updated!', + 'SHARING_REMOVED' => 'Sharing removed!', + 'USER_CREATED' => 'User created!', + 'USER_DELETED' => 'User deleted!', + 'USER_UPDATED' => 'User updated!', + 'ENTER_EMAIL' => 'Enter your email address:', + 'ERROR_ALBUM_JSON_NOT_FOUND' => 'Error: Album json not found!', + 'ERROR_ALBUM_NOT_FOUND' => 'Error: album %s not found', + 'ERROR_DROPBOX_KEY' => 'Error: Dropbox key not set', + 'ERROR_SESSION' => 'Session expired.', + 'CAMERA_DATE' => 'Camera date', + 'NEW_PASSWORD' => 'new password', + 'ALLOW_UPLOADS' => 'Allow uploads', + 'ALLOW_USER_SELF_EDIT' => 'Allow self-management of user account', + 'OSM_CONTRIBUTORS' => 'OpenStreetMap contributors', +]; diff --git a/lang/sk/maintenance.php b/lang/sk/maintenance.php new file mode 100644 index 00000000000..f86de3d6f46 --- /dev/null +++ b/lang/sk/maintenance.php @@ -0,0 +1,60 @@ + 'Maintenance', + 'description' => 'You will find on this page, all the required actions to keep your Lychee installation running smooth and nicely.', + 'cleaning' => [ + 'title' => 'Cleaning %s', + 'result' => '%s deleted.', + 'description' => 'Remove all contents from %s', + 'button' => 'Clean', + ], + 'fix-jobs' => [ + 'title' => 'Fixing Jobs History', + 'description' => 'Mark jobs with status %s or %s as %s.', + 'button' => 'Fix job history', + ], + 'gen-sizevariants' => [ + 'title' => 'Missing %s', + 'description' => 'Found %d %s that could be generated.', + 'button' => 'Generate!', + 'success' => 'Successfully generated %d %s.', + ], + 'fill-filesize-sizevariants' => [ + 'title' => 'File sizes missing', + 'description' => 'Found %d small variants without file size.', + 'button' => 'Fetch data!', + 'success' => 'Successfully computed sizes of %d small variants.', + ], + 'fix-tree' => [ + 'title' => 'Tree statistics', + 'Oddness' => 'Oddness', + 'Duplicates' => 'Duplicates', + 'Wrong parents' => 'Wrong parents', + 'Missing parents' => 'Missing parents', + 'button' => 'Fix tree', + ], + 'optimize' => [ + 'title' => 'Optimize Database', + 'description' => 'If you notice slowdown in your installation, it may be because your database does not + have all its needed index.', + 'button' => 'Optimize Database', + ], + 'update' => [ + 'title' => 'Updates', + 'check-button' => 'Check for updates', + 'update-button' => 'Update', + 'no-pending-updates' => 'No pending update.', + ], +]; \ No newline at end of file diff --git a/lang/sk/profile.php b/lang/sk/profile.php new file mode 100644 index 00000000000..cc24b97452c --- /dev/null +++ b/lang/sk/profile.php @@ -0,0 +1,64 @@ + 'Profile', + + 'login' => [ + 'header' => 'Profile', + 'enter_current_password' => 'Enter your current password:', + 'current_password' => 'Current password', + 'credentials_update' => 'Your credentials will be changed to the following:', + 'username' => 'Username', + 'new_password' => 'New password', + 'confirm_new_password' => 'Confirm new password', + 'email_instruction' => 'Add your email below to enable receiving email notifications. To stop receiving emails, simply remove your email below.', + 'email' => 'Email', + 'change' => 'Change Login', + 'api_token' => 'API Token ...', + + 'missing_fields' => 'Missing fields', + ], + + 'token' => [ + 'unavailable' => 'You have already viewed this token.', + 'no_data' => 'No token API have been generated.', + 'disable' => 'Disable', + 'disabled' => 'Token disabled', + 'warning' => 'This token will not be displayed again. Copy it and keep it in a safe place.', + 'reset' => 'Reset the token', + 'create' => 'Create a new token', + ], + + 'oauth' => [ + 'header' => 'OAuth', + 'header_not_available' => 'OAuth is not available', + 'setup_env' => 'Set up the credentials in your .env', + 'token_registered' => '%s token registered.', + 'setup' => 'Set up %s', + 'reset' => 'reset', + 'credential_deleted' => 'Credential deleted!', + ], + + 'u2f' => [ + 'header' => 'Passkey/MFA/2FA', + 'info' => 'This only provides the ability to use WebAuthn to authenticate instead of username & password.', + 'empty' => 'Credentials list is empty!', + 'not_secure' => 'Environment not secured. U2F not available.', + 'new' => 'Register new device.', + 'credential_deleted' => 'Credential deleted!', + 'credential_updated' => 'Credential updated!', + 'credential_registred' => 'Registration successful!', + '5_chars' => 'At least 5 chars.', + ], +]; \ No newline at end of file diff --git a/lang/sk/settings.php b/lang/sk/settings.php new file mode 100644 index 00000000000..fd197f11135 --- /dev/null +++ b/lang/sk/settings.php @@ -0,0 +1,92 @@ + 'Settings', + 'small_screen' => 'For better a experience on the Settings page,
we recommend you use a larger screen.', + 'tabs' => [ + 'basic' => 'Basic', + 'all_settings' => 'All settings', + ], + 'toasts' => [ + 'change_saved' => 'Change saved!', + 'details' => 'Settings have been modified as per request', + 'error' => 'Error!', + 'error_load_css' => 'Could not load dist/user.css', + 'error_load_js' => 'Could not load dist/custom.js', + 'error_save_css' => 'Could not save CSS', + 'error_save_js' => 'Could not save JS', + 'thank_you' => 'Thank you for your support.', + 'reload' => 'Reload your page for full functionalities.', + ], + 'system' => [ + 'header' => 'System', + 'use_dark_mode' => 'Use dark mode for Lychee', + 'language' => 'Language used by Lychee', + 'nsfw_album_visibility' => 'Make Sensitive albums visible by default.', + 'nsfw_album_explanation' => 'If the album is public, it is still accessible, just hidden from the view and can be revealed by pressing H.', + ], + 'lychee_se' => [ + 'header' => 'Lychee SE', + 'call4action' => 'Get exclusive features and support the development of Lychee. Unlock the SE edition.', + 'preview' => 'Enable preview of Lychee SE features', + 'hide_call4action' => 'Hide this Lychee SE registration form. I am happy with Lychee as-is. :)', + 'hide_warning' => 'If enabled, the only way to register your license key will be via the More tab above. Changes are applied on page reload.', + ], + 'dropbox' => [ + 'header' => 'Dropbox', + 'instruction' => 'In order to import photos from your Dropbox, you need a valid drop-ins app key from their website.', + 'api_key' => 'Dropbox API Key', + 'set_key' => 'Set Dropbox Key', + ], + 'gallery' => [ + 'header' => 'Gallery', + 'photo_order_column' => 'Default column used for sorting photos', + 'photo_order_direction' => 'Default order used for sorting photos', + 'album_order_column' => 'Default column used for sorting albums', + 'album_order_direction' => 'Default order used for sorting albums', + 'aspect_ratio' => 'Default aspect ratio for album thumbs', + 'photo_layout' => 'Layout for pictures', + 'album_decoration' => 'Show decorations on album cover (sub-album and/or photo count)', + 'album_decoration_direction' => 'Align album decorations horizontally or vertically', + 'photo_overlay' => 'Default image overlay information', + 'license_default' => 'Default license used for albums', + 'license_help' => 'Need help choosing?', + ], + 'geolocation' => [ + 'header' => 'Geo-location', + 'map_display' => 'Display the map given GPS coordinates', + 'map_display_public' => 'Allow anonymous users to access the map', + 'map_provider' => 'Defines the map provider', + 'map_include_subalbums' => 'Includes pictures of the sub albums on the map', + 'location_decoding' => 'Use GPS location decoding', + 'location_show' => 'Show location extracted from GPS coordinates', + 'location_show_public' => 'Anonymous users can access the extracted location from GPS coordinates', + ], + 'advanced' => [ + 'header' => 'Advanced Customization', + 'change_css' => 'Change CSS', + 'change_js' => 'Change JS', + ], + 'all' => [ + 'old_setting_style' => 'Old setting style', + 'change_detected' => 'Some settings changed.', + 'save' => 'Save', + ], + + 'tool_option' => [ + 'disabled' => 'disabled', + 'enabled' => 'enabled', + 'discover' => 'discover', + ], +]; \ No newline at end of file diff --git a/lang/sk/sharing.php b/lang/sk/sharing.php new file mode 100644 index 00000000000..69de18cc6d0 --- /dev/null +++ b/lang/sk/sharing.php @@ -0,0 +1,33 @@ + 'Sharing', + + 'info' => 'This page gives an overview of and the ability to edit the sharing rights associated with albums.', + 'album_title' => 'Album title', + 'username' => 'Username', + 'no_data' => 'Sharing list is empty.', + 'share' => 'Share', + 'permission_deleted' => 'Permission deleted!', + 'permission_created' => 'Permission created!', + + 'grants' => [ + 'read' => 'Grants read access', + 'original' => 'Grants access to original photo', + 'download' => 'Grants download', + 'upload' => 'Grants upload', + 'edit' => 'Grants edit', + 'delete' => 'Grants delete', + ], +]; \ No newline at end of file diff --git a/lang/sk/statistics.php b/lang/sk/statistics.php new file mode 100644 index 00000000000..2baf855bbd5 --- /dev/null +++ b/lang/sk/statistics.php @@ -0,0 +1,34 @@ + 'Statistics', + + 'preview_text' => 'This is a preview of the statistics page available in Lychee SE.
The data shown here are randomly generated and do not reflect your server.', + 'no_data' => 'User does not have data on server.', + 'collapse' => 'Collapse albums sizes', + + 'total' => [ + 'total' => 'Total', + 'albums' => 'Albums', + 'photos' => 'Photos', + 'size' => 'Size', + ], + 'table' => [ + 'username' => 'Owner', + 'title' => 'Title', + 'photos' => 'Photos', + 'descendants' => 'Children', + 'size' => 'Size', + ], +]; \ No newline at end of file diff --git a/lang/sk/toasts.php b/lang/sk/toasts.php new file mode 100644 index 00000000000..293d4b72594 --- /dev/null +++ b/lang/sk/toasts.php @@ -0,0 +1,17 @@ + 'Error', + 'success' => 'Success', +]; \ No newline at end of file diff --git a/lang/sk/users.php b/lang/sk/users.php new file mode 100644 index 00000000000..599bb833454 --- /dev/null +++ b/lang/sk/users.php @@ -0,0 +1,44 @@ + 'Users', + 'description' => 'Here you can manage the users of your Lychee installation. You can create, edit and delete users.', + 'create' => 'Create a new user', + 'username' => 'Username', + 'password' => 'Password', + 'legend' => 'Legend', + 'upload_rights' => 'When selected, the user can upload content.', + 'edit_rights' => 'When selected, the user can modify their profile (username, password).', + 'quota' => 'When set, the user has a space quota for pictures (in kB).', + + 'user_deleted' => 'User deleted', + 'user_created' => 'User created', + 'user_updated' => 'User updated', + 'change_saved' => 'Change saved!', + + 'create_edit' => [ + 'upload_rights' => 'User can upload content.', + 'edit_rights' => 'User can modify their profile (username, password).', + 'quota' => 'User has quota limit.', + 'quota_kb' => 'quota in kB (0 for default)', + 'note' => 'Admin note (not publically visible)', + 'create' => 'Create', + 'edit' => 'Edit', + ], + 'line' => [ + 'admin' => 'admin user', + 'edit' => 'Edit', + 'delete' => 'Delete', + ], +]; \ No newline at end of file diff --git a/lang/sv/aspect_ratio.php b/lang/sv/aspect_ratio.php new file mode 100644 index 00000000000..2c7e8fb56ac --- /dev/null +++ b/lang/sv/aspect_ratio.php @@ -0,0 +1,21 @@ + '5/4 (instagram landscape)', + '4by5' => '4/5 (instagram portrait)', + '2by3' => '2/3 (portrait)', + '3by2' => '3/2 (landscape)', + '1by1' => 'square', + '1byx9' => '16/9 (landscape)', +]; \ No newline at end of file diff --git a/lang/sv/diagnostics.php b/lang/sv/diagnostics.php new file mode 100644 index 00000000000..0fadd640428 --- /dev/null +++ b/lang/sv/diagnostics.php @@ -0,0 +1,30 @@ + 'Diagnostics', + + 'copy_to_clipboard' => 'Copy diagnostics to clipboard', + 'self-diagnosis' => 'Self-diagnosis', + 'info' => 'Info', + 'space' => 'Space', + 'load_space' => 'Load space usage.', + 'configuration' => 'Configuration', + 'loading' => 'Loading...', + 'identical_content' => 'Identical content', + + 'toast' => [ + 'info' => 'Info', + 'copy' => 'Diagnostics copied to clipboard!', + ], +]; \ No newline at end of file diff --git a/lang/sv/dialogs.php b/lang/sv/dialogs.php new file mode 100644 index 00000000000..4afd65fae3f --- /dev/null +++ b/lang/sv/dialogs.php @@ -0,0 +1,221 @@ + [ + 'close' => 'Close', + 'cancel' => 'Cancel', + 'save' => 'Save', + 'delete' => 'Delete', + 'move' => 'Move', + ], + 'about' => [ + 'subtitle' => 'Self-hosted photo-management done right', + 'description' => 'Lychee is a free photo-management tool, which runs on your server or web-space. Installing is a matter of seconds. Upload, manage and share photos like from a native application. Lychee comes with everything you need and all your photos are stored securely.', + 'update_available' => 'Update available!', + 'thank_you' => 'Thank you for your support!', + 'get_supporter_or_register' => 'Get exclusive features and support the development of Lychee.
Unlock the Supporter Edition or register your License key', + 'here' => 'here', + ], + 'dropbox' => [ + 'not_configured' => 'Dropbox is not configured.', + ], + 'import_from_link' => [ + 'instructions' => 'Please enter the direct link to a photo to import it:', + 'import' => 'Import', + ], + 'keybindings' => [ + 'don_t_show_again' => 'Don\'t show this again', + 'side_wide' => 'Site-wide Shortcuts', + 'back_cancel' => 'Back/Cancel', + 'confirm' => 'Confirm', + 'login' => 'Login', + 'toggle_full_screen' => 'Toggle Full Screen', + 'toggle_sensitive_albums' => 'Toggle Sensitive Albums', + + 'albums' => 'Albums Shortcuts', + 'new_album' => 'New Album', + 'upload_photos' => 'Upload Photos', + 'search' => 'Search', + 'show_this_modal' => 'Show this modal', + 'select_all' => 'Select All', + 'move_selection' => 'Move Selection', + 'delete_selection' => 'Delete Selection', + + 'album' => 'Album Shortcuts', + 'slideshow' => 'Start/Stop Slideshow', + 'toggle' => 'Toggle panel', + + 'photo' => 'Photo Shortcuts', + 'previous' => 'Previous photo', + 'next' => 'Next photo', + 'cycle' => 'Cycle overlay mode', + 'star' => 'Star the photo', + 'move' => 'Move the photo', + 'delete' => 'Delete the photo', + 'edit' => 'Edit information', + 'show_hide_meta' => 'Show information', + + 'keep_hidden' => 'We will keep it hidden.', + ], + 'login' => [ + 'username' => 'Username', + 'password' => 'Password', + 'unknown_invalid' => 'Unknown user or invalid password.', + 'signin' => 'Sign-In', + ], + 'register' => [ + 'enter_license' => 'Enter your license key below:', + 'license_key' => 'License key', + 'invalid_license' => 'Invalid license key.', + 'register' => 'Register', + ], + 'share_album' => [ + 'url_copied' => 'Copied URL to clipboard!', + ], + 'upload' => [ + 'completed' => 'Completed', + 'uploaded' => 'Uploaded:', + 'release' => 'Release file to upload!', + 'select' => 'Click here to select files to upload', + 'drag' => '(Or drag files to the page)', + 'loading' => 'Loading', + 'resume' => 'Resume', + 'uploading' => 'Uploading', + 'finished' => 'Finished', + 'failed_error' => 'Upload failed. The server returned an error!', + ], + 'visibility' => [ + 'public' => 'Public', + 'public_expl' => 'Anonymous users can access this album, subject to the restrictions below.', + 'full' => 'Original', + 'full_expl' => 'Anonymous users can view full-resolution photos.', + 'hidden' => 'Hidden', + 'hidden_expl' => 'Anonymous users need a direct link to access this album.', + 'downloadable' => 'Downloadable', + 'downloadable_expl' => 'Anonymous users can download this album.', + 'password' => 'Password', + 'password_prot' => 'Password protected', + 'password_prot_expl' => 'Anonymous users need a shared password to access this album.', + 'nsfw' => 'Sensitive', + 'nsfw_expl' => 'Album contains sensitive content.', + 'visibility_updated' => 'Visibility updated.', + ], + 'move_album' => [ + 'confirm_single' => 'Are you sure you want to move the album “%1$s” into the album “%2$s”?', + 'confirm_multiple' => 'Are you sure you want to move all selected albums into the album “%s”?', + 'move_single' => 'Move Album', + 'move_to' => 'Move to', + 'move_to_single' => 'Move %s to:', + 'move_to_multiple' => 'Move %d albums to:', + 'no_album_target' => 'No album to move to', + 'moved_single' => 'Album moved!', + 'moved_single_details' => '%1$s moved to %2$s', + 'moved_details' => 'Album(s) moved to %s', + ], + 'new_album' => [ + 'menu' => 'Create Album', + 'info' => 'Enter a title for the new album:', + 'title' => 'title', + 'create' => 'Create Album', + ], + 'new_tag_album' => [ + 'menu' => 'Create Tag Album', + 'info' => 'Enter a title for the new tag album:', + 'title' => 'title', + 'set_tags' => 'Set tags to show', + 'warn' => 'Make sure to press enter after each tag', + 'create' => 'Create Tag Album', + ], + 'delete_album' => [ + 'confirmation' => 'Are you sure you want to delete the album “%s” and all of the photos it contains?', + 'confirmation_multiple' => 'Are you sure you want to delete all %d selected albums and all of the photos they contain?', + 'warning' => 'This action can not be undone!', + 'delete' => 'Delete Album and Photos', + ], + 'transfer' => [ + 'query' => 'Transfer ownership of album to', + 'confirmation' => 'Are you sure you want to transfer the ownership of album “%s” and all the photos it contains to "%s"?', + 'lost_access_warning' => 'Your access to this album will be lost.', + 'warning' => 'This action can not be undone!', + 'transfer' => 'Transfer ownership of album and photos', + ], + 'rename' => [ + 'photo' => 'Enter a new title for this photo:', + 'album' => 'Enter a new title for this album:', + 'rename' => 'Rename', + ], + 'merge' => [ + 'merge_to' => 'Merge %s to:', + 'merge_to_multiple' => 'Merge %d albums to:', + 'no_albums' => 'No albums to merge to.', + 'confirm' => 'Are you sure you want to merge the album “%1$s” into the album “%2$s”?', + 'confirm_multiple' => 'Are you sure you want to merge all selected albums into the album “%s”?', + 'merge' => 'Merge Albums', + 'merged' => 'Album(s) merged to %s!', + ], + 'unlock' => [ + 'password_required' => 'This album is protected by a password. Enter the password below to view the photos of this album:', + 'password' => 'Password', + 'unlock' => 'Unlock', + ], + 'photo_tags' => [ + 'question' => 'Enter your tags for this photo.', + 'question_multiple' => 'Enter your tags for all %d selected photos. Existing tags will be overwritten.', + 'no_tags' => 'No Tags', + 'set_tags' => 'Set Tags', + 'updated' => 'Tags updated!', + 'tags_override_info' => 'If this is unchecked, the tags will be added to the existing tags of the photo.', + ], + 'photo_copy' => [ + 'no_albums' => 'No albums to copy to', + 'copy_to' => 'Copy %s to:', + 'copy_to_multiple' => 'Copy %d photos to:', + 'confirm' => 'Copy %s to %s.', + 'confirm_multiple' => 'Copy %d photos to %s.', + 'copy' => 'Copy', + 'copied' => 'Photo(s) copied!', + ], + 'photo_delete' => [ + 'confirm' => 'Are you sure you want to delete the photo “%s”?', + 'confirm_multiple' => 'Are you sure you want to delete all %d selected photos?', + 'deleted' => 'Photo(s) deleted!', + ], + 'move_photo' => [ + 'move_single' => 'Move %s to:', + 'move_multiple' => 'Move %d photos to:', + 'confirm' => 'Move %s to %s.', + 'confirm_multiple' => 'Move %d photos to %s.', + 'moved' => 'Photo(s) moved to %s!', + ], + 'target_user' => [ + 'placeholder' => 'Select user', + ], + 'target_album' => [ + 'placeholder' => 'Select album', + ], + 'webauthn' => [ + 'u2f' => 'U2F', + 'success' => 'Authentication successful!', + 'error' => 'Whoops, it looks like something went wrong. Please reload the site and try again!', + ], + 'se' => [ + 'available' => 'Available in the Supporter Edition', + ], + 'session_expired' => [ + 'title' => 'Session expired', + 'message' => 'Your session has expired.
Please reload the page.', + 'reload' => 'Reload', + 'go_to_gallery' => 'Go to the Gallery', + ], +]; \ No newline at end of file diff --git a/lang/sv/fix-tree.php b/lang/sv/fix-tree.php new file mode 100644 index 00000000000..64803e310e6 --- /dev/null +++ b/lang/sv/fix-tree.php @@ -0,0 +1,55 @@ + 'Maintenance', + 'intro' => 'This page allows you to re-order and fix your albums manually.
Before any modifications, we strongly recommend you to read about Nested Set tree structures.', + 'warning' => 'You can really break your Lychee installation here, modify values at your own risks.', + + 'help' => [ + 'header' => 'Help', + 'hover' => 'Hover ids or titles to highlight related albums.', + 'left' => 'Left', + 'right' => 'Right', + 'convenience' => 'For your convenience, the and buttons allow you to change the values of %s and %s by respectively +1 and -1 with propagation.', + 'left-right-warn' => 'The and indicates that the value of %s (and respectively %s) is duplicated somewhere.', + 'parent-marked' => 'Marked Parent Id indicates that the %s and %s do not satisfy the Nest Set tree structures. Edit either the Parent Id or the %s/%s values.', + 'slowness' => 'This page will be slow with a large number of albums.', + ], + + 'buttons' => [ + 'reset' => 'Reset', + 'check' => 'Check', + 'apply' => 'Apply', + ], + + 'table' => [ + 'title' => 'Title', + 'left' => 'Left', + 'right' => 'Right', + 'id' => 'Id', + 'parent' => 'Parent Id', + ], + + 'errors' => [ + 'invalid' => 'Invalid tree!', + 'invalid_details' => 'We are not applying this as it is guaranteed to be a broken state.', + 'invalid_left' => 'Album %s has an invalid left value.', + 'invalid_right' => 'Album %s has an invalid right value.', + 'invalid_left_right' => 'Album %s has an invalid left/right values. Left should be strictly smaller than right: %s < %s.', + 'duplicate_left' => 'Album %s has a duplicate left value %s.', + 'duplicate_right' => 'Album %s has a duplicate right value %s.', + 'parent' => 'Album %s has an unexpected parent id %s.', + 'unknown' => 'Album %s has an unknown error.', + ], +]; \ No newline at end of file diff --git a/lang/sv/gallery.php b/lang/sv/gallery.php new file mode 100644 index 00000000000..eb8008827e0 --- /dev/null +++ b/lang/sv/gallery.php @@ -0,0 +1,241 @@ + 'Gallery', + + 'smart_albums' => 'Smart albums', + 'albums' => 'Albums', + 'root' => 'Albums', + + 'original' => 'Original', + 'medium' => 'Medium', + 'medium_hidpi' => 'Medium HiDPI', + 'small' => 'Thumb', + 'small_hidpi' => 'Thumb HiDPI', + 'thumb' => 'Square thumb', + 'thumb_hidpi' => 'Square thumb HiDPI', + 'placeholder' => 'Low Quality Image Placeholder', + 'thumbnail' => 'Photo thumbnail', + 'live_video' => 'Video part of live-photo', + + 'camera_data' => 'Camera date', + 'album_reserved' => 'All Rights Reserved', + + 'map' => [ + 'error_gpx' => 'Error loading GPX file', + 'osm_contributors' => 'OpenStreetMap contributors', + ], + + 'search' => [ + 'title' => 'Search', + 'searching' => 'Searching…', + 'no_results' => 'Nothing matches your search query.', + 'searchbox' => 'Search…', + 'minimum_chars' => 'Minimum %s characters required.', + 'photos' => 'Photos (%s)', + 'albums' => 'Albums (%s)', + ], + + 'smart_album' => [ + 'unsorted' => 'Unsorted', + 'starred' => 'Starred', + 'recent' => 'Recent', + 'public' => 'Public', + 'on_this_day' => 'On This Day', + ], + + 'layout' => [ + 'squares' => 'Square thumbnails', + 'justified' => 'With aspect, justified', + 'masonry' => 'With aspect, masonry', + 'grid' => 'With aspect, grid', + ], + + 'overlay' => [ + 'none' => 'None', + 'exif' => 'EXIF data', + 'description' => 'Description', + 'date' => 'Date taken', + ], + + 'timeline' => [ + 'default' => 'default', + 'disabled' => 'disabled', + 'year' => 'Year', + 'month' => 'Month', + 'day' => 'Day', + 'hour' => 'Hour', + ], + + 'album' => [ + 'header_albums' => 'Albums', + 'header_photos' => 'Photos', + 'no_results' => 'Nothing to see here', + 'upload' => 'Upload photos', + + 'tabs' => [ + 'about' => 'About Album', + 'share' => 'Share Album', + 'move' => 'Move Album', + 'danger' => 'DANGER ZONE', + ], + + 'hero' => [ + 'created' => 'Created', + 'copyright' => 'Copyright', + 'subalbums' => 'Subalbums', + 'images' => 'Photos', + 'download' => 'Download Album', + 'share' => 'Share Album', + 'stats_only_se' => 'Statistics available in the Supporter Edition', + ], + + 'stats' => [ + 'lens' => 'Lens', + 'shutter' => 'Shutter speed', + 'iso' => 'ISO', + 'model' => 'Model', + 'aperture' => 'Aperture', + 'no_data' => 'No data', + ], + + 'properties' => [ + 'title' => 'Title', + 'description' => 'Description', + 'photo_ordering' => 'Order photos by', + 'children_ordering' => 'Order albums by', + 'asc/desc' => 'asc/desc', + 'header' => 'Set album header', + 'compact_header' => 'Use compact header', + 'license' => 'Set license', + 'copyright' => 'Set copyright', + 'aspect_ratio' => 'Set album thumbs aspect ratio', + 'album_timeline' => 'Set album timeline mode', + 'photo_timeline' => 'Set photo timeline mode', + 'layout' => 'Set photo layout', + 'show_tags' => 'Set tags to show', + 'tags_required' => 'Tags are required.', + ], + ], + + 'photo' => [ + 'actions' => [ + 'star' => 'Star', + 'unstar' => 'Unstar', + 'set_album_header' => 'Set as album header', + 'move' => 'Move', + 'delete' => 'Delete', + 'header_set' => 'Header set', + ], + + 'details' => [ + 'about' => 'About', + 'basics' => 'Basics', + 'title' => 'Title', + 'uploaded' => 'Uploaded', + 'description' => 'Description', + 'license' => 'License', + 'reuse' => 'Reuse', + 'latitude' => 'Latitude', + 'longitude' => 'Longitude', + 'altitude' => 'Altitude', + 'location' => 'Location', + 'image' => 'Image', + 'video' => 'Video', + 'size' => 'Size', + 'format' => 'Format', + 'resolution' => 'Resolution', + 'duration' => 'Duration', + 'fps' => 'Frame rate', + 'tags' => 'Tags', + 'camera' => 'Camera', + 'captured' => 'Captured', + 'make' => 'Make', + 'type' => 'Type/Model', + 'lens' => 'Lens', + 'shutter' => 'Shutter Speed', + 'aperture' => 'Aperture', + 'focal' => 'Focal Length', + 'iso' => 'ISO %s', + ], + + 'edit' => [ + 'set_title' => 'Set Title', + 'set_description' => 'Set Description', + 'set_license' => 'Set License', + 'no_tags' => 'No Tags', + 'set_tags' => 'Set Tags', + 'set_created_at' => 'Set Upload Date', + ], + ], + + 'nsfw' => [ + 'header' => 'Sensitive content', + 'description' => 'This album contains sensitive content which some people may find offensive or disturbing.', + 'consent' => 'Tap to consent.', + ], + + 'menus' => [ + 'star' => 'Star', + 'unstar' => 'Unstar', + 'star_all' => 'Star Selected', + 'unstar_all' => 'Unstar Selected', + 'tag' => 'Tag', + 'tag_all' => 'Tag Selected', + 'set_cover' => 'Set Album Cover', + 'remove_header' => 'Remove Album Header', + 'set_header' => 'Set Album Header', + 'copy_to' => 'Copy to …', + 'copy_all_to' => 'Copy Selected to …', + 'rename' => 'Rename', + 'move' => 'Move', + 'move_all' => 'Move Selected', + 'delete' => 'Delete', + 'delete_all' => 'Delete Selected', + 'download' => 'Download', + 'download_all' => 'Download Selected', + 'merge' => 'Merge', + 'merge_all' => 'Merge Selected', + + 'upload_photo' => 'Upload Photo', + 'import_link' => 'Import from Link', + 'import_dropbox' => 'Import from Dropbox', + 'new_album' => 'New Album', + 'new_tag_album' => 'New Tag Album', + 'upload_track' => 'Upload track', + 'delete_track' => 'Delete track', + ], + + 'sort' => [ + 'photo_select_1' => 'Upload Time', + 'photo_select_2' => 'Take Date', + 'photo_select_3' => 'Title', + 'photo_select_4' => 'Description', + 'photo_select_6' => 'Star', + 'photo_select_7' => 'Photo Format', + 'ascending' => 'Ascending', + 'descending' => 'Descending', + 'album_select_1' => 'Creation Time', + 'album_select_2' => 'Title', + 'album_select_3' => 'Description', + 'album_select_5' => 'Latest Take Date', + 'album_select_6' => 'Oldest Take Date', + ], + + 'albums_protection' => [ + 'private' => 'private', + 'public' => 'public', + 'inherit_from_parent' => 'inherit from parent', + ], +]; \ No newline at end of file diff --git a/lang/sv/jobs.php b/lang/sv/jobs.php new file mode 100644 index 00000000000..5d952b76012 --- /dev/null +++ b/lang/sv/jobs.php @@ -0,0 +1,18 @@ + 'Jobs', + + 'no_data' => 'No Jobs have been executed yet.', +]; \ No newline at end of file diff --git a/lang/sv/landing.php b/lang/sv/landing.php new file mode 100644 index 00000000000..fe6fe55b8ea --- /dev/null +++ b/lang/sv/landing.php @@ -0,0 +1,19 @@ + 'Gallery', + 'access_gallery' => 'Access the gallery', + 'hosted_with_lychee' => 'Hosted with Lychee', + 'copyright' => 'All images on this website are subject to copyright by %1$s © %2$s', +]; \ No newline at end of file diff --git a/lang/sv/left-menu.php b/lang/sv/left-menu.php new file mode 100644 index 00000000000..9a3e91f4037 --- /dev/null +++ b/lang/sv/left-menu.php @@ -0,0 +1,29 @@ + 'Back to Gallery', + + 'admin' => 'Admin', + 'clockwork' => 'Clockwork App', + 'logs' => 'Show Logs', + 'jobs' => 'Show Job History', + 'user' => 'User', + + 'sign_out' => 'Sign Out', + + 'about' => 'About', + 'api' => 'API Documentation', + 'source_code' => 'Source Code', + 'support' => 'Support', +]; \ No newline at end of file diff --git a/lang/sv/lychee.php b/lang/sv/lychee.php new file mode 100644 index 00000000000..000ae9e9d0e --- /dev/null +++ b/lang/sv/lychee.php @@ -0,0 +1,535 @@ + 'Användarnamn', + 'PASSWORD' => 'Lösenord', + 'ENTER' => 'Stig in', + 'CANCEL' => 'Avbryt', + 'CONFIRM' => 'Confirm', + 'SIGN_IN' => 'Logga in', + 'CLOSE' => 'Stäng', + 'SETTINGS' => 'Settings', + 'SEARCH' => 'Search …', + 'MORE' => 'More', + 'DEFAULT' => 'Default', + 'GALLERY' => 'Gallery', + + 'USERS' => 'Users', + 'PROFILE' => 'Profile', + 'CREATE' => 'Create', + 'REMOVE' => 'Remove', + 'SHARE' => 'Share', + 'U2F' => 'U2F', + 'NOTIFICATIONS' => 'Notifications', + 'SHARING' => 'Sharing', + 'CHANGE_LOGIN' => 'Ändra inloggning', + 'CHANGE_SORTING' => 'Ändra sortering', + 'SET_DROPBOX' => 'Ställ in Dropbox', + 'ABOUT_LYCHEE' => 'Om Lychee', + 'DIAGNOSTICS' => 'Diagnostik', + 'DIAGNOSTICS_GET_SIZE' => 'Request space usage', + 'JOBS' => 'Show job history', + 'LOGS' => 'Visa logfilen', + 'SIGN_OUT' => 'Logga ut', + 'UPDATE_AVAILABLE' => 'En uppdatering finns!', + 'MIGRATION_AVAILABLE' => 'Migration available!', + 'CHECK_FOR_UPDATE' => 'Check for updates', + 'DEFAULT_LICENSE' => 'Default license for new uploads:', + 'SET_LICENSE' => 'Set License', + 'SET_OVERLAY_TYPE' => 'Set Overlay', + 'SET_ALBUM_DECORATION' => 'Set album decorations', + 'SET_MAP_PROVIDER' => 'Set OpenStreetMap tiles provider', + 'FULL_SETTINGS' => 'Full Settings', + 'UPDATE' => 'Update', + 'RESET' => 'Reset', + 'DISABLE_TOKEN_TOOLTIP' => 'Disable', + 'ENABLE_TOKEN' => 'Enable API token', + 'DISABLED_TOKEN_STATUS_MSG' => 'Disabled', + 'TOKEN_BUTTON' => 'API Token ...', + 'TOKEN_NOT_AVAILABLE' => 'You have already viewed this token.', + 'TOKEN_WAIT' => 'Wait ...', + + 'SMART_ALBUMS' => 'Smarta album', + 'SHARED_ALBUMS' => 'Shared albums', + 'ALBUMS' => 'Album', + 'PHOTOS' => 'Pictures', + 'SEARCH_RESULTS' => 'Search results', + + 'RENAME' => 'Ändra namnet', + 'RENAME_ALL' => 'Byt namn på vald', + 'MERGE' => 'Slå ihop', + 'MERGE_ALL' => 'Sammanfoga vald', + 'MAKE_PUBLIC' => 'Gör publika', + 'SHARE_ALBUM' => 'Dela album', + 'SHARE_PHOTO' => 'Dela fotografi', + 'VISIBILITY_ALBUM' => 'Album Visibility', + 'VISIBILITY_PHOTO' => 'Photo Visibility', + 'DOWNLOAD_ALBUM' => 'Ladda ner album', + 'ABOUT_ALBUM' => 'Om albumet', + 'DELETE_ALBUM' => 'Radera albumet', + 'MOVE_ALBUM' => 'Move Album', + 'FULLSCREEN_ENTER' => 'Enter Fullscreen', + 'FULLSCREEN_EXIT' => 'Exit Fullscreen', + + 'SHARING_ALBUM_USERS' => 'Share this album with users', + 'WAIT_FETCH_DATA' => 'Please wait while we get the data …', + 'SHARING_ALBUM_USERS_NO_USERS' => 'There are no users to share the album with', + 'SHARING_ALBUM_USERS_LONG_MESSAGE' => 'Select the users to share this album with', + + 'DELETE_ALBUM_QUESTION' => 'Radera album och fotografier', + 'KEEP_ALBUM' => 'Behåll albumet', + 'DELETE_ALBUM_CONFIRMATION' => 'Är du säker på att du vill radera albumet “%s” och alla fotografier det innehåller? Raderingen går inte att ångra!', + + 'DELETE_TAG_ALBUM_QUESTION' => 'Delete Album', + 'DELETE_TAG_ALBUM_CONFIRMATION' => 'Are you sure you want to delete the album “%s” (any photos inside will not be deleted)? This action can’t be undone!', + + 'DELETE_ALBUMS_QUESTION' => 'Radera album och fotografier', + 'KEEP_ALBUMS' => 'Behåll album', + 'DELETE_ALBUMS_CONFIRMATION' => 'Är du säker på att du vill radera alla %d valda album och alla fotografier de innehåller? Raderingen går inte att ångra!', + + 'DELETE_UNSORTED_CONFIRM' => 'Är du säker på att du vill radera alla fotografier från “Osorterat”? Det här går inte att ångra!', + 'CLEAR_UNSORTED' => 'Rensa osorterade', + 'KEEP_UNSORTED' => 'Behåll osorterade', + + 'EDIT_SHARING' => 'Redrigera delning', + 'MAKE_PRIVATE' => 'Gör privat', + + 'CLOSE_ALBUM' => 'Stäng albumet', + 'CLOSE_PHOTO' => 'Stäng fotografiet', + 'CLOSE_MAP' => 'Close Map', + + 'ADD' => 'Lägg till', + 'MOVE' => 'Flytta', + 'MOVE_ALL' => 'Flytta valda', + 'DUPLICATE' => 'Kopiera', + 'DUPLICATE_ALL' => 'Kopiera valda', + 'COPY_TO' => 'Kopiera till …', + 'COPY_ALL_TO' => 'Valda kopia till …', + 'DELETE' => 'Radera', + 'SAVE' => 'Save', + 'DELETE_ALL' => 'Radera vald', + 'DOWNLOAD' => 'Ladda ner', + 'DOWNLOAD_ALL' => 'Download Selected', + 'UPLOAD_PHOTO' => 'Ladda upp fotografi', + 'IMPORT_LINK' => 'Importera från länk', + 'IMPORT_DROPBOX' => 'Importera från Dropbox', + 'IMPORT_SERVER' => 'Importera från server', + 'NEW_ALBUM' => 'Nytt album', + 'NEW_TAG_ALBUM' => 'New Tag Album', + 'UPLOAD_TRACK' => 'Upload track', + 'DELETE_TRACK' => 'Delete track', + + 'TITLE_NEW_ALBUM' => 'Skriv en titel för det nya albumet:', + 'UNTITLED' => 'Saknar titel', + 'UNSORTED' => 'Osorterat', + 'STARRED' => 'Stjärnmärkta', + 'RECENT' => 'Nyligen', + 'PUBLIC' => 'Publika', + 'ON_THIS_DAY' => 'On This Day', + 'NUM_PHOTOS' => 'Fotografier', + + 'CREATE_ALBUM' => 'Skapa album', + 'CREATE_TAG_ALBUM' => 'Create Tag Album', + + 'STAR_PHOTO' => 'Stjärnmärk fotografi', + 'STAR' => 'Stjärnmärk', + 'UNSTAR' => 'Unstar', + 'STAR_ALL' => 'Markera valda som favoriter', + 'UNSTAR_ALL' => 'Unstar Selected', + 'TAG' => 'Tag', + 'TAG_ALL' => 'Vald taggen', + 'UNSTAR_PHOTO' => 'Ta bort stjärnmärke', + 'SET_COVER' => 'Set Album Cover', + 'REMOVE_COVER' => 'Remove Album Cover', + 'SET_HEADER' => 'Set Album Header', + 'REMOVE_HEADER' => 'Remove Album Header', + 'SET_COMPACT_HEADER' => 'Use Compact Header', + + 'FULL_PHOTO' => 'Originalfotografi', + 'ABOUT_PHOTO' => 'Om fotografiet', + 'DISPLAY_FULL_MAP' => 'Map', + 'DIRECT_LINK' => 'Direktlänk', + 'DIRECT_LINKS' => 'Direct Links', + 'QR_CODE' => 'QR Code', + + 'ALBUM_ABOUT' => 'Om', + 'ALBUM_BASICS' => 'Grundläggande', + 'ALBUM_TITLE' => 'Titel', + 'ALBUM_COPYRIGHT' => 'Copyright', + 'ALBUM_SET_COPYRIGHT' => 'Set copyright', + 'ALBUM_NEW_TITLE' => 'Skriv en ny titel för det här albumet:', + 'ALBUMS_NEW_TITLE' => 'Skriv en ny titel för alla %d valda album:', + 'ALBUM_SET_TITLE' => 'Spara titel', + 'ALBUM_DESCRIPTION' => 'Beskrivning', + 'ALBUM_SHOW_TAGS' => 'Tags to show', + 'ALBUM_NEW_DESCRIPTION' => 'Ny beskrivning för detta album:', + 'ALBUM_SET_DESCRIPTION' => 'Spara beskrivningen', + 'ALBUM_NEW_SHOWTAGS' => 'Enter tags of photos that will be visible in this album:', + 'ALBUM_SET_SHOWTAGS' => 'Set tags to show', + 'ALBUM_ALBUM' => 'Album', + 'ALBUM_CREATED' => 'Skapad', + 'ALBUM_IMAGES' => 'Fotografier', + 'ALBUM_VIDEOS' => 'Videos', + 'ALBUM_SUBALBUMS' => 'Subalbums', + 'ALBUM_SHARING' => 'Dela', + 'ALBUM_SHR_YES' => 'Ja', + 'ALBUM_SHR_NO' => 'Nej', + 'ALBUM_PUBLIC' => 'Publikt', + 'ALBUM_PUBLIC_EXPL' => 'Anonymous users can access this album, subject to the restrictions below.', + 'ALBUM_FULL' => 'Original', + 'ALBUM_FULL_EXPL' => 'Anonymous users can behold full-resolution photos.', + 'ALBUM_HIDDEN' => 'Dold', + 'ALBUM_HIDDEN_EXPL' => 'Anonymous users need a direct link to access this album.', + 'ALBUM_MARK_NSFW' => 'Mark album as sensitive', + 'ALBUM_UNMARK_NSFW' => 'Unmark album as sensitive', + 'ALBUM_NSFW' => 'Sensitive', + 'ALBUM_NSFW_EXPL' => 'Album is marked to contain sensitive content.', + 'ALBUM_DOWNLOADABLE' => 'Nedladdningsbart', + 'ALBUM_DOWNLOADABLE_EXPL' => 'Anonymous users can download this album.', + 'ALBUM_SHARE_BUTTON_VISIBLE' => 'Share button is visible', + 'ALBUM_SHARE_BUTTON_VISIBLE_EXPL' => 'Anonymous users can see social media sharing links.', + 'ALBUM_PASSWORD' => 'Lösenord', + 'ALBUM_PASSWORD_PROT' => 'Lösenordsskyddad', + 'ALBUM_PASSWORD_PROT_EXPL' => 'Anonymous users need a shared password to access this album.', + 'ALBUM_PASSWORD_REQUIRED' => 'Albumet är skyddat med ett lösenord. Ange lösenordet nedan för att se fotografierna i albumet:', + 'ALBUM_MERGE' => 'Är du säker på att du vill sammanfoga albumet “%1$s” med albumet “%2$s”?', + 'ALBUMS_MERGE' => 'Är du säker på att du vill sammanfoga samtliga valda album till albumet “%s”?', + 'MERGE_ALBUM' => 'Sammanfoga album', + 'DONT_MERGE' => 'Sammanfoga inte', + 'ALBUM_MOVE' => 'Are you sure you want to move the album “%1$s” into the album “%2$s”?', + 'ALBUMS_MOVE' => 'Are you sure you want to move all selected albums into the album “%s”?', + 'MOVE_ALBUMS' => 'Move Albums', + 'NOT_MOVE_ALBUMS' => "Don't Move", + 'ROOT' => 'Album', + 'ALBUM_REUSE' => 'Reuse', + 'ALBUM_LICENSE' => 'License', + 'ALBUM_SET_LICENSE' => 'Set License', + 'ALBUM_LICENSE_HELP' => 'Need help choosing?', + 'ALBUM_LICENSE_NONE' => 'None', + 'ALBUM_RESERVED' => 'All Rights Reserved', + 'ALBUM_SET_ORDER' => 'Set Order', + 'ALBUM_ORDERING' => 'Order by', + 'ALBUM_PHOTO_ORDERING' => 'Order photos by', + 'ALBUM_CHILDREN_ORDERING' => 'Order albums by', + 'ALBUM_OWNER' => 'Owner', + + 'PHOTO_ABOUT' => 'Om', + 'PHOTO_BASICS' => 'Grundläggande', + 'PHOTO_TITLE' => 'Titel', + 'PHOTO_NEW_TITLE' => 'Skriv in en ny tital för det hör fotografiet:', + 'PHOTO_SET_TITLE' => 'Spara titeln', + 'PHOTO_UPLOADED' => 'Uppladdat', + 'PHOTO_DESCRIPTION' => 'Beskrivning', + 'PHOTO_NEW_DESCRIPTION' => 'Skriv en ny beskrivning för detta fotografi:', + 'PHOTO_SET_DESCRIPTION' => 'Spara beskrivningen', + 'PHOTO_NEW_LICENSE' => 'Add a License', + 'PHOTO_SET_LICENSE' => 'Set License', + 'PHOTO_LICENSE' => 'License', + 'PHOTO_LICENSE_HELP' => 'Need help choosing?', + 'PHOTO_REUSE' => 'Reuse', + 'PHOTO_LICENSE_NONE' => 'None', + 'PHOTO_RESERVED' => 'All Rights Reserved', + 'PHOTO_LATITUDE' => 'Latitude', + 'PHOTO_LONGITUDE' => 'Longitude', + 'PHOTO_ALTITUDE' => 'Altitude', + 'PHOTO_IMGDIRECTION' => 'Direction', + 'PHOTO_LOCATION' => 'Location', + 'PHOTO_IMAGE' => 'Fotografi', + 'PHOTO_VIDEO' => 'Video', + 'PHOTO_SIZE' => 'Storlek', + 'PHOTO_FORMAT' => 'Filformat', + 'PHOTO_DURATION' => 'Duration', + 'PHOTO_FPS' => 'Frame rate', + 'PHOTO_RESOLUTION' => 'Mått', + 'PHOTO_TAGS' => 'Kategori', + 'PHOTO_NOTAGS' => 'Inga kategorier', + 'PHOTO_NEW_TAGS' => 'Skriv in din kategori för det här fotografier. Du kan ange flera kategori genom att separera dem med ett kommatecken:', + 'PHOTOS_NEW_TAGS' => 'Ange kategori för samtliga valda bilder %d Befintliga kategorier kommer att raderas. Du kan lägga till nya kategorier genom att separera dem med kommatecken:', + 'PHOTO_SET_TAGS' => 'Spara kategori', + 'PHOTO_CAMERA' => 'Kamera', + 'PHOTO_CAPTURED' => 'Digitaliserad', + 'PHOTO_MAKE' => 'Tillverkare', + 'PHOTO_TYPE' => 'Typ/Modell', + 'PHOTO_LENS' => 'Lens', + 'PHOTO_SHUTTER' => 'Slutartid', + 'PHOTO_APERTURE' => 'Bländartal', + 'PHOTO_FOCAL' => 'Brännvidd', + 'PHOTO_ISO' => 'ISO %s', + 'PHOTO_SHARING' => 'Delning', + 'PHOTO_DELETE' => 'Radera fotografi', + 'PHOTO_KEEP' => 'Spara fotografi', + 'PHOTO_DELETE_CONFIRMATION' => 'Är du säker på att du vill radera det här fotografiet “%s”? Raderingen går inte att ångra!', + 'PHOTO_DELETE_ALL' => 'Är du säker på att du vill radera alla %d valda fotografier? Raderingen går inte att ångra!', + 'PHOTOS_NEW_TITLE' => 'Ange en tital för alla %d valda fotografier:', + 'PHOTO_MAKE_PRIVATE_ALBUM' => 'Det här fotografier finns i ett publikt album. Du kan ändra fotografiets synlighet genom att redigera egenskapen för albumet.', + 'PHOTO_SHOW_ALBUM' => 'Visa album', + 'PHOTO_PUBLIC' => 'Public', + 'PHOTO_PUBLIC_EXPL' => 'Anonymous users can view this photo, subject to the restrictions below.', + 'PHOTO_FULL' => 'Original', + 'PHOTO_FULL_EXPL' => 'Anonymous users can behold full-resolution photo.', + 'PHOTO_HIDDEN' => 'Hidden', + 'PHOTO_HIDDEN_EXPL' => 'Anonymous users need a direct link to view this photo.', + 'PHOTO_DOWNLOADABLE' => 'Downloadable', + 'PHOTO_DOWNLOADABLE_EXPL' => 'Anonymous users may download this photo.', + 'PHOTO_SHARE_BUTTON_VISIBLE' => 'Share button is visible', + 'PHOTO_SHARE_BUTTON_VISIBLE_EXPL' => 'Anonymous users can see social media sharing links.', + 'PHOTO_PASSWORD_PROT' => 'Password protected', + 'PHOTO_PASSWORD_PROT_EXPL' => 'Anonymous users need a shared password to view this photo.', + 'PHOTO_EDIT_SHARING_TEXT' => 'The sharing properties of this photo will be changed to the following:', + 'PHOTO_NO_EDIT_SHARING_TEXT' => 'Because this photo is located in a public album, it inherits that album’s visibility settings. Its current visibility is shown below for informational purposes only.', + 'PHOTO_EDIT_GLOBAL_SHARING_TEXT' => 'The visibility of this photo can be fine-tuned using global Lychee settings. Its current visibility is shown below for informational purposes only.', + 'PHOTO_NEW_CREATED_AT' => 'Enter the upload date for this photo. mm/dd/yyyy, hh:mm [am/pm]', + 'PHOTO_SET_CREATED_AT' => 'Set upload date', + + 'LOADING' => 'Laddar', + 'ERROR' => 'Fel', + 'ERROR_TEXT' => 'Ojsan, något verkar ha gått lite fel. Prova att ladda om sidan och försök igen!', + 'ERROR_UNKNOWN' => 'Något oväntat inträffade. Vänligen försök igen och kontrollera installationen av Lychee och din server. För mera information läs dokumentet readme.', + 'ERROR_MAP_DEACTIVATED' => 'Map functionality has been deactivated under settings.', + 'ERROR_SEARCH_DEACTIVATED' => 'Search functionality has been deactivated under settings.', + 'SUCCESS' => 'OK', + 'CHANGE_SUCCESS' => 'Change successful.', + 'RETRY' => 'Försök igen', + 'OVERRIDE' => 'Override', + 'TAGS_OVERRIDE_INFO' => 'If this is unchecked, the tags will be added to the existing tags of the photo.', + + 'SETTINGS_SUCCESS_LOGIN' => 'Login Info updated.', + 'SETTINGS_SUCCESS_SORT' => 'Sorting order updated.', + 'SETTINGS_SUCCESS_DROPBOX' => 'Dropbox Key updated.', + 'SETTINGS_SUCCESS_LANG' => 'Language updated', + 'SETTINGS_SUCCESS_LAYOUT' => 'Layout updated', + 'SETTINGS_SUCCESS_IMAGE_OVERLAY' => 'EXIF Overlay setting updated', + 'SETTINGS_SUCCESS_PUBLIC_SEARCH' => 'Offentlig sökning uppdaterad', + 'SETTINGS_SUCCESS_LICENSE' => 'Default license updated', + 'SETTINGS_SUCCESS_MAP_DISPLAY' => 'Map display settings updated', + 'SETTINGS_SUCCESS_MAP_DISPLAY_PUBLIC' => 'Map display settings for public albums updated', + 'SETTINGS_SUCCESS_MAP_PROVIDER' => 'Map provider settings updated', + 'SETTINGS_SUCCESS_CSS' => 'Stylesheets updated', + 'SETTINGS_SUCCESS_JS' => 'JS updated', + 'SETTINGS_SUCCESS_UPDATE' => 'Settings updated successfully', + 'SETTINGS_DROPBOX_KEY' => 'Dropbox API Key', + 'SETTINGS_ADVANCED_WARNING_EXPL' => 'Changing these advanced settings can be harmful to the stability, security and performance of this application. You should only modify them if you are sure of what you are doing.', + 'SETTINGS_ADVANCED_SAVE' => 'Save my modifications, I accept the risk!', + + 'U2F_NOT_SUPPORTED' => 'U2F not supported. Sorry.', + 'U2F_NOT_SECURE' => 'Environment not secured. U2F not available.', + 'U2F_REGISTER_KEY' => 'Register new device.', + 'U2F_REGISTRATION_SUCCESS' => 'Registration successful!', + 'U2F_AUTHENTIFICATION_SUCCESS' => 'Authentication successful!', + 'U2F_CREDENTIALS' => 'Credentials', + 'U2F_CREDENTIALS_DELETED' => 'Credentials deleted!', + 'U2F_LOGIN' => 'Log in with WebAuthn', + + 'NEW_PHOTOS_NOTIFICATION' => 'Send new photos notification emails.', + 'SETTINGS_SUCCESS_NEW_PHOTOS_NOTIFICATION' => 'New photos notification updated', + 'USER_EMAIL_INSTRUCTION' => 'Add your email below to enable receiving email notifications. To stop receiving emails, simply remove your email below.', + + 'LOGIN_USERNAME' => 'Nytt användarnamn', + 'LOGIN_PASSWORD' => 'Nytt lösenord', + 'LOGIN_PASSWORD_CONFIRM' => 'Confirm Password', + 'PASSWORD_TITLE' => 'Ange dina befintliga inloggningsuppgifter:', + 'PASSWORD_CURRENT' => 'Befintligt lösenord', + 'PASSWORD_TEXT' => 'Ditt inloggningsuppgifter kommer att ändras till:', + 'PASSWORD_CHANGE' => 'Spara ändringar av inloggningsuppgifter', + + 'EDIT_SHARING_TITLE' => 'Redigera delning', + 'EDIT_SHARING_TEXT' => 'Albumets egenskaper för delning kommer att ändras till:', + 'SHARE_ALBUM_TEXT' => 'Det här albumet kommer att delas ut med dessa egenskaper::', + + 'SORT_DIALOG_ATTRIBUTE_LABEL' => 'Attribute', + 'SORT_DIALOG_ORDER_LABEL' => 'Order', + + 'SORT_ALBUM_BY' => 'Sortera album efter %1$s i en %2$s ordning.', + + 'SORT_ALBUM_SELECT_1' => 'skapelsetid', + 'SORT_ALBUM_SELECT_2' => 'titel', + 'SORT_ALBUM_SELECT_3' => 'beskrivning', + 'SORT_ALBUM_SELECT_5' => 'senaste datum', + 'SORT_ALBUM_SELECT_6' => 'äldsta datum', + + 'SORT_PHOTO_BY' => 'Sortera fotografier efter %1$s i en %2$s ordning.', + + 'SORT_PHOTO_SELECT_1' => 'Uppladdningstid', + 'SORT_PHOTO_SELECT_2' => 'Fotograferingsdatum', + 'SORT_PHOTO_SELECT_3' => 'Titel', + 'SORT_PHOTO_SELECT_4' => 'Beskrivning', + 'SORT_PHOTO_SELECT_6' => 'Stjärnmärkning', + 'SORT_PHOTO_SELECT_7' => 'Bildformat', + + 'SORT_ASCENDING' => 'stigande', + 'SORT_DESCENDING' => 'fallande', + 'SORT_CHANGE' => 'Spara ändringar av sorteringsföljden', + + 'DROPBOX_TITLE' => 'Spara nyckeln för Dropbox', + 'DROPBOX_TEXT' => "För att kunna importera fotografier från ditt Dropboxkonto behöver du en godkänd applikationsnyckel från Dropbox.\n Skapa en personlig nyckel och ange den sedan här nedan:", + + 'LANG_TEXT' => 'Ändra språket i Lychee till:', + 'LANG_TITLE' => 'Spara ändringen av språket', + + 'SETTING_RECENT_PUBLIC_TEXT' => 'Make "Recent" smart album accessible to anonymous users', + 'SETTING_STARRED_PUBLIC_TEXT' => 'Make "Starred" smart album accessible to anonymous users', + 'SETTING_ONTHISDAY_PUBLIC_TEXT' => 'Make "On This Day" smart album accessible to anonymous users', + + 'CSS_TEXT' => 'Personalize CSS:', + 'CSS_TITLE' => 'Change CSS', + 'JS_TEXT' => 'Custom JS:', + 'JS_TITLE' => 'Change JS', + 'PUBLIC_SEARCH_TEXT' => 'Offentlig sökning tillåts:', + 'OVERLAY_TYPE' => 'Photo overlay:', + 'OVERLAY_NONE' => 'None', + 'OVERLAY_EXIF' => 'EXIF data', + 'OVERLAY_DESCRIPTION' => 'Description', + 'OVERLAY_DATE' => 'Date taken', + 'ALBUM_DECORATION' => 'Album decorations:', + 'ALBUM_DECORATION_NONE' => 'None', + 'ALBUM_DECORATION_ORIGINAL' => 'Sub-album marker', + 'ALBUM_DECORATION_ALBUM' => 'Number of sub-albums', + 'ALBUM_DECORATION_PHOTO' => 'Number of photos', + 'ALBUM_DECORATION_ALL' => 'Number of sub-albums and photos', + 'ALBUM_DECORATION_ORIENTATION' => 'Orientation of album decorations:', + 'ALBUM_DECORATION_ORIENTATION_ROW' => 'Horizontal (photos, albums)', + 'ALBUM_DECORATION_ORIENTATION_ROW_REVERSE' => 'Horizontal (albums, photos)', + 'ALBUM_DECORATION_ORIENTATION_COLUMN' => 'Vertical (top photos, albums)', + 'ALBUM_DECORATION_ORIENTATION_COLUMN_REVERSE' => 'Vertical (top albums, photos)', + 'MAP_DISPLAY_TEXT' => 'Enable maps (provided by OpenStreetMap):', + 'MAP_DISPLAY_PUBLIC_TEXT' => 'Enable maps for public albums (provided by OpenStreetMap):', + 'MAP_PROVIDER' => 'Provider of OpenStreetMap tiles:', + 'MAP_PROVIDER_WIKIMEDIA' => 'Wikimedia', + 'MAP_PROVIDER_OSM_ORG' => 'OpenStreetMap.org (no HiDPI)', + 'MAP_PROVIDER_OSM_DE' => 'OpenStreetMap.de (no HiDPI)', + 'MAP_PROVIDER_OSM_FR' => 'OpenStreetMap.fr (no HiDPI)', + 'MAP_PROVIDER_RRZE' => 'University of Erlangen, Germany (only HiDPI)', + 'MAP_INCLUDE_SUBALBUMS_TEXT' => 'Include photos of subalbums on map:', + 'LOCATION_DECODING' => 'Decode GPS data into location name', + 'LOCATION_SHOW' => 'Show location name', + 'LOCATION_SHOW_PUBLIC' => 'Show location name for public mode', + + 'LAYOUT_TYPE' => 'Layout of photos:', + 'LAYOUT_SQUARES' => 'Square thumbnails', + 'LAYOUT_JUSTIFIED' => 'With aspect, justified', + 'LAYOUT_MASONRY' => 'With aspect, masonry', + 'LAYOUT_GRID' => 'With aspect, grid', + 'LAYOUT_UNJUSTIFIED' => 'With aspect, unjustified', + 'SET_LAYOUT' => 'Change layout', + + 'NSFW_VISIBLE_TEXT_1' => 'Make Sensitive albums visible by default.', + 'NSFW_VISIBLE_TEXT_2' => 'If the album is public, it is still accessible, just hidden from the view and can be revealed by pressing H.', + 'SETTINGS_SUCCESS_NSFW_VISIBLE' => 'Default sensitive album visibility updated with success.', + + 'NSFW_BANNER' => '

Sensitive content

This album contains sensitive content which some people may find offensive or disturbing.

Tap to consent.

', + 'NSFW_HEADER' => 'Sensitive content', + 'NSFW_EXPLANATION' => 'This album contains sensitive content which some people may find offensive or disturbing.', + 'TAP_CONSENT' => 'Tap to consent.', + + 'VIEW_NO_RESULT' => 'Inget resultat', + 'VIEW_NO_PUBLIC_ALBUMS' => 'Inga publika album', + 'VIEW_NO_CONFIGURATION' => 'Ingen konfigurering', + 'VIEW_PHOTO_NOT_FOUND' => 'Fotografiet hittade inte', + + 'NO_TAGS' => 'Inga kategorier', + + 'UPLOAD_MANAGE_NEW_PHOTOS' => 'Du kan nu hantera de nya bilderna.', + 'UPLOAD_COMPLETE' => 'Uppladdning klar', + 'UPLOAD_COMPLETE_FAILED' => 'Kunde inte ladda upp en eller flera bilder.', + 'UPLOAD_IMPORTING' => 'Importering', + 'UPLOAD_IMPORTING_URL' => 'Adress för importering', + 'UPLOAD_UPLOADING' => 'Uppladdning', + 'UPLOAD_FINISHED' => 'Klar', + 'UPLOAD_PROCESSING' => 'Bearbetning', + 'UPLOAD_FAILED' => 'Misslyckades', + 'UPLOAD_FAILED_ERROR' => 'Uppladdning misslyckades. Servern svarade med ett felmeddelande!', + 'UPLOAD_FAILED_WARNING' => 'Uppladdning misslyckades. Servern svarade med en varning!', + 'UPLOAD_CANCELLED' => 'Cancelled', + 'UPLOAD_SKIPPED' => 'Ignorerade', + 'UPLOAD_UPDATED' => 'Updated', + 'UPLOAD_GENERAL' => 'General', + 'UPLOAD_IMPORT_SKIPPED_DUPLICATE' => 'This photo has been skipped because it’s already in your library.', + 'UPLOAD_IMPORT_RESYNCED_DUPLICATE' => 'This photo has been skipped because it’s already in your library, but its metadata has been updated.', + 'UPLOAD_ERROR_CONSOLE' => 'Kontrollera din webbläsares konsoll för ytterligare information.', + 'UPLOAD_UNKNOWN' => 'Servern returnerade ett oklart svar. Kontrollera din webbläsares konsoll för ytterligare information.', + 'UPLOAD_ERROR_UNKNOWN' => 'Uppladdning misslyckades. Servern returnerade ett oklart fel!', + 'UPLOAD_ERROR_POSTSIZE' => 'Upload failed. The PHP post_max_size may be too small! Otherwise check the FAQ.', + 'UPLOAD_ERROR_FILESIZE' => 'Upload failed. The PHP upload_max_filesize may be too small! Otherwise check the FAQ.', + 'UPLOAD_IN_PROGRESS' => 'Lychee laddar för tillfället upp material!', + 'UPLOAD_IMPORT_WARN_ERR' => 'Importeringen är avslutad, men processen gav felmeddelanden. Kontrollera logfilen (Inställningar -> Visa logfilen) för ytterligare detaljer.', + 'UPLOAD_IMPORT_COMPLETE' => 'Importeringen klar', + 'UPLOAD_IMPORT_INSTR' => 'Ange den exakta länken till fotografiet du vill importera.', + 'UPLOAD_IMPORT' => 'Importera', + 'UPLOAD_IMPORT_SERVER' => 'Importera från server', + 'UPLOAD_IMPORT_SERVER_FOLD' => 'Mappen du angav var tom eller saknade läsbara filer. Kontrollera logfilen (Inställningar -> Visa logfilen) för ytterligare detaljer', + 'UPLOAD_IMPORT_SERVER_INSTR' => 'Import all photos, folders and sub-folders located in the folders with the following absolute paths (on server). Paths are space separated, use \\ to escape a space in a path.', + 'UPLOAD_ABSOLUTE_PATH' => 'Absolute path to directories, space separated', + 'UPLOAD_IMPORT_SERVER_EMPT' => 'Kunde inte påbörja importeringen då mappen saknade innehåll!', + 'UPLOAD_IMPORT_DELETE_ORIGINALS' => 'Delete originals', + 'UPLOAD_IMPORT_DELETE_ORIGINALS_EXPL' => 'Originalfotografierna kommer att raderas efter att importering genomförts.', + 'UPLOAD_IMPORT_VIA_SYMLINK' => 'Symbolic links', + 'UPLOAD_IMPORT_VIA_SYMLINK_EXPL' => 'Import files using symbolic links to originals.', + 'UPLOAD_IMPORT_SKIP_DUPLICATES' => 'Skip duplicates', + 'UPLOAD_IMPORT_SKIP_DUPLICATES_EXPL' => 'Existing media files are skipped.', + 'UPLOAD_IMPORT_RESYNC_METADATA' => 'Re-sync metadata', + 'UPLOAD_IMPORT_RESYNC_METADATA_EXPL' => 'Update metadata of existing media files.', + 'UPLOAD_IMPORT_LOW_MEMORY_EXPL' => 'The import process on the server is approaching the memory limit and may end up being terminated prematurely.', + 'UPLOAD_WARNING' => 'Warning', + 'UPLOAD_IMPORT_NOT_A_DIRECTORY' => 'The given path is not a readable directory!', + 'UPLOAD_IMPORT_PATH_RESERVED' => 'The given path is a reserved path of Lychee!', + 'UPLOAD_IMPORT_FAILED' => 'Could not import the file!', + 'UPLOAD_IMPORT_UNSUPPORTED' => 'Unsupported file type!', + 'UPLOAD_IMPORT_CANCELLED' => 'Import cancelled', + + 'ABOUT_SUBTITLE' => 'Self-hosted photo-management done right', + 'ABOUT_DESCRIPTION' => 'Lychee is a free photo-management tool, which runs on your server or web-space. Installing is a matter of seconds. Upload, manage and share photos like from a native application. Lychee comes with everything you need and all your photos are stored securely.', + 'FOOTER_COPYRIGHT' => 'Alla bilder på denna webbplats är föremål för upphovsrätt från %1$s © %2$s', + 'HOSTED_WITH_LYCHEE' => 'Hosted with Lychee', + + 'URL_COPY_TO_CLIPBOARD' => 'Copy to clipboard', + 'URL_COPIED_TO_CLIPBOARD' => 'Copied URL to clipboard!', + 'PHOTO_DIRECT_LINKS_TO_IMAGES' => 'Direct links to image files:', + 'PHOTO_ORIGINAL' => 'Original', + 'PHOTO_MEDIUM' => 'Medium', + 'PHOTO_MEDIUM_HIDPI' => 'Medium HiDPI', + 'PHOTO_SMALL' => 'Thumb', + 'PHOTO_SMALL_HIDPI' => 'Thumb HiDPI', + 'PHOTO_THUMB' => 'Square thumb', + 'PHOTO_THUMB_HIDPI' => 'Square thumb HiDPI', + 'PHOTO_PLACEHOLDER' => 'Low Quality Image Placeholder', + 'PHOTO_THUMBNAIL' => 'Photo thumbnail', + 'PHOTO_LIVE_VIDEO' => 'Video part of live-photo', + 'PHOTO_VIEW' => 'Lychee Photo View:', + + 'PHOTO_EDIT_ROTATECWISE' => 'Rotate clockwise', + 'PHOTO_EDIT_ROTATECCWISE' => 'Rotate counter-clockwise', + + 'ERROR_GPX' => 'Error loading GPX file: ', + 'ERROR_EITHER_ALBUMS_OR_PHOTOS' => 'Please select either albums or photos!', + 'ERROR_COULD_NOT_FIND' => 'Could not find what you want.', + 'ERROR_INVALID_EMAIL' => 'Not a valid email address.', + 'EMAIL_SUCCESS' => 'Email updated!', + 'ERROR_PHOTO_NOT_FOUND' => 'Error: photo %s not found !', + 'ERROR_EMPTY_USERNAME' => 'new username cannot be empty.', + 'ERROR_PASSWORD_DOES_NOT_MATCH' => 'new password does not match.', + 'ERROR_EMPTY_PASSWORD' => 'new password cannot be empty.', + 'ERROR_SELECT_ALBUM' => 'Select an album to share!', + 'ERROR_SELECT_USER' => 'Select a user to share with!', + 'ERROR_SELECT_SHARING' => 'Select a sharing to remove!', + 'SHARING_SUCCESS' => 'Sharing updated!', + 'SHARING_REMOVED' => 'Sharing removed!', + 'USER_CREATED' => 'User created!', + 'USER_DELETED' => 'User deleted!', + 'USER_UPDATED' => 'User updated!', + 'ENTER_EMAIL' => 'Enter your email address:', + 'ERROR_ALBUM_JSON_NOT_FOUND' => 'Error: Album json not found!', + 'ERROR_ALBUM_NOT_FOUND' => 'Error: album %s not found', + 'ERROR_DROPBOX_KEY' => 'Error: Dropbox key not set', + 'ERROR_SESSION' => 'Session expired.', + 'CAMERA_DATE' => 'Camera date', + 'NEW_PASSWORD' => 'new password', + 'ALLOW_UPLOADS' => 'Allow uploads', + 'ALLOW_USER_SELF_EDIT' => 'Allow self-management of user account', + 'OSM_CONTRIBUTORS' => 'OpenStreetMap contributors', +]; diff --git a/lang/sv/maintenance.php b/lang/sv/maintenance.php new file mode 100644 index 00000000000..f86de3d6f46 --- /dev/null +++ b/lang/sv/maintenance.php @@ -0,0 +1,60 @@ + 'Maintenance', + 'description' => 'You will find on this page, all the required actions to keep your Lychee installation running smooth and nicely.', + 'cleaning' => [ + 'title' => 'Cleaning %s', + 'result' => '%s deleted.', + 'description' => 'Remove all contents from %s', + 'button' => 'Clean', + ], + 'fix-jobs' => [ + 'title' => 'Fixing Jobs History', + 'description' => 'Mark jobs with status %s or %s as %s.', + 'button' => 'Fix job history', + ], + 'gen-sizevariants' => [ + 'title' => 'Missing %s', + 'description' => 'Found %d %s that could be generated.', + 'button' => 'Generate!', + 'success' => 'Successfully generated %d %s.', + ], + 'fill-filesize-sizevariants' => [ + 'title' => 'File sizes missing', + 'description' => 'Found %d small variants without file size.', + 'button' => 'Fetch data!', + 'success' => 'Successfully computed sizes of %d small variants.', + ], + 'fix-tree' => [ + 'title' => 'Tree statistics', + 'Oddness' => 'Oddness', + 'Duplicates' => 'Duplicates', + 'Wrong parents' => 'Wrong parents', + 'Missing parents' => 'Missing parents', + 'button' => 'Fix tree', + ], + 'optimize' => [ + 'title' => 'Optimize Database', + 'description' => 'If you notice slowdown in your installation, it may be because your database does not + have all its needed index.', + 'button' => 'Optimize Database', + ], + 'update' => [ + 'title' => 'Updates', + 'check-button' => 'Check for updates', + 'update-button' => 'Update', + 'no-pending-updates' => 'No pending update.', + ], +]; \ No newline at end of file diff --git a/lang/sv/profile.php b/lang/sv/profile.php new file mode 100644 index 00000000000..cc24b97452c --- /dev/null +++ b/lang/sv/profile.php @@ -0,0 +1,64 @@ + 'Profile', + + 'login' => [ + 'header' => 'Profile', + 'enter_current_password' => 'Enter your current password:', + 'current_password' => 'Current password', + 'credentials_update' => 'Your credentials will be changed to the following:', + 'username' => 'Username', + 'new_password' => 'New password', + 'confirm_new_password' => 'Confirm new password', + 'email_instruction' => 'Add your email below to enable receiving email notifications. To stop receiving emails, simply remove your email below.', + 'email' => 'Email', + 'change' => 'Change Login', + 'api_token' => 'API Token ...', + + 'missing_fields' => 'Missing fields', + ], + + 'token' => [ + 'unavailable' => 'You have already viewed this token.', + 'no_data' => 'No token API have been generated.', + 'disable' => 'Disable', + 'disabled' => 'Token disabled', + 'warning' => 'This token will not be displayed again. Copy it and keep it in a safe place.', + 'reset' => 'Reset the token', + 'create' => 'Create a new token', + ], + + 'oauth' => [ + 'header' => 'OAuth', + 'header_not_available' => 'OAuth is not available', + 'setup_env' => 'Set up the credentials in your .env', + 'token_registered' => '%s token registered.', + 'setup' => 'Set up %s', + 'reset' => 'reset', + 'credential_deleted' => 'Credential deleted!', + ], + + 'u2f' => [ + 'header' => 'Passkey/MFA/2FA', + 'info' => 'This only provides the ability to use WebAuthn to authenticate instead of username & password.', + 'empty' => 'Credentials list is empty!', + 'not_secure' => 'Environment not secured. U2F not available.', + 'new' => 'Register new device.', + 'credential_deleted' => 'Credential deleted!', + 'credential_updated' => 'Credential updated!', + 'credential_registred' => 'Registration successful!', + '5_chars' => 'At least 5 chars.', + ], +]; \ No newline at end of file diff --git a/lang/sv/settings.php b/lang/sv/settings.php new file mode 100644 index 00000000000..fd197f11135 --- /dev/null +++ b/lang/sv/settings.php @@ -0,0 +1,92 @@ + 'Settings', + 'small_screen' => 'For better a experience on the Settings page,
we recommend you use a larger screen.', + 'tabs' => [ + 'basic' => 'Basic', + 'all_settings' => 'All settings', + ], + 'toasts' => [ + 'change_saved' => 'Change saved!', + 'details' => 'Settings have been modified as per request', + 'error' => 'Error!', + 'error_load_css' => 'Could not load dist/user.css', + 'error_load_js' => 'Could not load dist/custom.js', + 'error_save_css' => 'Could not save CSS', + 'error_save_js' => 'Could not save JS', + 'thank_you' => 'Thank you for your support.', + 'reload' => 'Reload your page for full functionalities.', + ], + 'system' => [ + 'header' => 'System', + 'use_dark_mode' => 'Use dark mode for Lychee', + 'language' => 'Language used by Lychee', + 'nsfw_album_visibility' => 'Make Sensitive albums visible by default.', + 'nsfw_album_explanation' => 'If the album is public, it is still accessible, just hidden from the view and can be revealed by pressing H.', + ], + 'lychee_se' => [ + 'header' => 'Lychee SE', + 'call4action' => 'Get exclusive features and support the development of Lychee. Unlock the SE edition.', + 'preview' => 'Enable preview of Lychee SE features', + 'hide_call4action' => 'Hide this Lychee SE registration form. I am happy with Lychee as-is. :)', + 'hide_warning' => 'If enabled, the only way to register your license key will be via the More tab above. Changes are applied on page reload.', + ], + 'dropbox' => [ + 'header' => 'Dropbox', + 'instruction' => 'In order to import photos from your Dropbox, you need a valid drop-ins app key from their website.', + 'api_key' => 'Dropbox API Key', + 'set_key' => 'Set Dropbox Key', + ], + 'gallery' => [ + 'header' => 'Gallery', + 'photo_order_column' => 'Default column used for sorting photos', + 'photo_order_direction' => 'Default order used for sorting photos', + 'album_order_column' => 'Default column used for sorting albums', + 'album_order_direction' => 'Default order used for sorting albums', + 'aspect_ratio' => 'Default aspect ratio for album thumbs', + 'photo_layout' => 'Layout for pictures', + 'album_decoration' => 'Show decorations on album cover (sub-album and/or photo count)', + 'album_decoration_direction' => 'Align album decorations horizontally or vertically', + 'photo_overlay' => 'Default image overlay information', + 'license_default' => 'Default license used for albums', + 'license_help' => 'Need help choosing?', + ], + 'geolocation' => [ + 'header' => 'Geo-location', + 'map_display' => 'Display the map given GPS coordinates', + 'map_display_public' => 'Allow anonymous users to access the map', + 'map_provider' => 'Defines the map provider', + 'map_include_subalbums' => 'Includes pictures of the sub albums on the map', + 'location_decoding' => 'Use GPS location decoding', + 'location_show' => 'Show location extracted from GPS coordinates', + 'location_show_public' => 'Anonymous users can access the extracted location from GPS coordinates', + ], + 'advanced' => [ + 'header' => 'Advanced Customization', + 'change_css' => 'Change CSS', + 'change_js' => 'Change JS', + ], + 'all' => [ + 'old_setting_style' => 'Old setting style', + 'change_detected' => 'Some settings changed.', + 'save' => 'Save', + ], + + 'tool_option' => [ + 'disabled' => 'disabled', + 'enabled' => 'enabled', + 'discover' => 'discover', + ], +]; \ No newline at end of file diff --git a/lang/sv/sharing.php b/lang/sv/sharing.php new file mode 100644 index 00000000000..69de18cc6d0 --- /dev/null +++ b/lang/sv/sharing.php @@ -0,0 +1,33 @@ + 'Sharing', + + 'info' => 'This page gives an overview of and the ability to edit the sharing rights associated with albums.', + 'album_title' => 'Album title', + 'username' => 'Username', + 'no_data' => 'Sharing list is empty.', + 'share' => 'Share', + 'permission_deleted' => 'Permission deleted!', + 'permission_created' => 'Permission created!', + + 'grants' => [ + 'read' => 'Grants read access', + 'original' => 'Grants access to original photo', + 'download' => 'Grants download', + 'upload' => 'Grants upload', + 'edit' => 'Grants edit', + 'delete' => 'Grants delete', + ], +]; \ No newline at end of file diff --git a/lang/sv/statistics.php b/lang/sv/statistics.php new file mode 100644 index 00000000000..2baf855bbd5 --- /dev/null +++ b/lang/sv/statistics.php @@ -0,0 +1,34 @@ + 'Statistics', + + 'preview_text' => 'This is a preview of the statistics page available in Lychee SE.
The data shown here are randomly generated and do not reflect your server.', + 'no_data' => 'User does not have data on server.', + 'collapse' => 'Collapse albums sizes', + + 'total' => [ + 'total' => 'Total', + 'albums' => 'Albums', + 'photos' => 'Photos', + 'size' => 'Size', + ], + 'table' => [ + 'username' => 'Owner', + 'title' => 'Title', + 'photos' => 'Photos', + 'descendants' => 'Children', + 'size' => 'Size', + ], +]; \ No newline at end of file diff --git a/lang/sv/toasts.php b/lang/sv/toasts.php new file mode 100644 index 00000000000..293d4b72594 --- /dev/null +++ b/lang/sv/toasts.php @@ -0,0 +1,17 @@ + 'Error', + 'success' => 'Success', +]; \ No newline at end of file diff --git a/lang/sv/users.php b/lang/sv/users.php new file mode 100644 index 00000000000..599bb833454 --- /dev/null +++ b/lang/sv/users.php @@ -0,0 +1,44 @@ + 'Users', + 'description' => 'Here you can manage the users of your Lychee installation. You can create, edit and delete users.', + 'create' => 'Create a new user', + 'username' => 'Username', + 'password' => 'Password', + 'legend' => 'Legend', + 'upload_rights' => 'When selected, the user can upload content.', + 'edit_rights' => 'When selected, the user can modify their profile (username, password).', + 'quota' => 'When set, the user has a space quota for pictures (in kB).', + + 'user_deleted' => 'User deleted', + 'user_created' => 'User created', + 'user_updated' => 'User updated', + 'change_saved' => 'Change saved!', + + 'create_edit' => [ + 'upload_rights' => 'User can upload content.', + 'edit_rights' => 'User can modify their profile (username, password).', + 'quota' => 'User has quota limit.', + 'quota_kb' => 'quota in kB (0 for default)', + 'note' => 'Admin note (not publically visible)', + 'create' => 'Create', + 'edit' => 'Edit', + ], + 'line' => [ + 'admin' => 'admin user', + 'edit' => 'Edit', + 'delete' => 'Delete', + ], +]; \ No newline at end of file diff --git a/lang/vi/aspect_ratio.php b/lang/vi/aspect_ratio.php new file mode 100644 index 00000000000..2c7e8fb56ac --- /dev/null +++ b/lang/vi/aspect_ratio.php @@ -0,0 +1,21 @@ + '5/4 (instagram landscape)', + '4by5' => '4/5 (instagram portrait)', + '2by3' => '2/3 (portrait)', + '3by2' => '3/2 (landscape)', + '1by1' => 'square', + '1byx9' => '16/9 (landscape)', +]; \ No newline at end of file diff --git a/lang/vi/diagnostics.php b/lang/vi/diagnostics.php new file mode 100644 index 00000000000..0fadd640428 --- /dev/null +++ b/lang/vi/diagnostics.php @@ -0,0 +1,30 @@ + 'Diagnostics', + + 'copy_to_clipboard' => 'Copy diagnostics to clipboard', + 'self-diagnosis' => 'Self-diagnosis', + 'info' => 'Info', + 'space' => 'Space', + 'load_space' => 'Load space usage.', + 'configuration' => 'Configuration', + 'loading' => 'Loading...', + 'identical_content' => 'Identical content', + + 'toast' => [ + 'info' => 'Info', + 'copy' => 'Diagnostics copied to clipboard!', + ], +]; \ No newline at end of file diff --git a/lang/vi/dialogs.php b/lang/vi/dialogs.php new file mode 100644 index 00000000000..4afd65fae3f --- /dev/null +++ b/lang/vi/dialogs.php @@ -0,0 +1,221 @@ + [ + 'close' => 'Close', + 'cancel' => 'Cancel', + 'save' => 'Save', + 'delete' => 'Delete', + 'move' => 'Move', + ], + 'about' => [ + 'subtitle' => 'Self-hosted photo-management done right', + 'description' => 'Lychee is a free photo-management tool, which runs on your server or web-space. Installing is a matter of seconds. Upload, manage and share photos like from a native application. Lychee comes with everything you need and all your photos are stored securely.', + 'update_available' => 'Update available!', + 'thank_you' => 'Thank you for your support!', + 'get_supporter_or_register' => 'Get exclusive features and support the development of Lychee.
Unlock the Supporter Edition or register your License key', + 'here' => 'here', + ], + 'dropbox' => [ + 'not_configured' => 'Dropbox is not configured.', + ], + 'import_from_link' => [ + 'instructions' => 'Please enter the direct link to a photo to import it:', + 'import' => 'Import', + ], + 'keybindings' => [ + 'don_t_show_again' => 'Don\'t show this again', + 'side_wide' => 'Site-wide Shortcuts', + 'back_cancel' => 'Back/Cancel', + 'confirm' => 'Confirm', + 'login' => 'Login', + 'toggle_full_screen' => 'Toggle Full Screen', + 'toggle_sensitive_albums' => 'Toggle Sensitive Albums', + + 'albums' => 'Albums Shortcuts', + 'new_album' => 'New Album', + 'upload_photos' => 'Upload Photos', + 'search' => 'Search', + 'show_this_modal' => 'Show this modal', + 'select_all' => 'Select All', + 'move_selection' => 'Move Selection', + 'delete_selection' => 'Delete Selection', + + 'album' => 'Album Shortcuts', + 'slideshow' => 'Start/Stop Slideshow', + 'toggle' => 'Toggle panel', + + 'photo' => 'Photo Shortcuts', + 'previous' => 'Previous photo', + 'next' => 'Next photo', + 'cycle' => 'Cycle overlay mode', + 'star' => 'Star the photo', + 'move' => 'Move the photo', + 'delete' => 'Delete the photo', + 'edit' => 'Edit information', + 'show_hide_meta' => 'Show information', + + 'keep_hidden' => 'We will keep it hidden.', + ], + 'login' => [ + 'username' => 'Username', + 'password' => 'Password', + 'unknown_invalid' => 'Unknown user or invalid password.', + 'signin' => 'Sign-In', + ], + 'register' => [ + 'enter_license' => 'Enter your license key below:', + 'license_key' => 'License key', + 'invalid_license' => 'Invalid license key.', + 'register' => 'Register', + ], + 'share_album' => [ + 'url_copied' => 'Copied URL to clipboard!', + ], + 'upload' => [ + 'completed' => 'Completed', + 'uploaded' => 'Uploaded:', + 'release' => 'Release file to upload!', + 'select' => 'Click here to select files to upload', + 'drag' => '(Or drag files to the page)', + 'loading' => 'Loading', + 'resume' => 'Resume', + 'uploading' => 'Uploading', + 'finished' => 'Finished', + 'failed_error' => 'Upload failed. The server returned an error!', + ], + 'visibility' => [ + 'public' => 'Public', + 'public_expl' => 'Anonymous users can access this album, subject to the restrictions below.', + 'full' => 'Original', + 'full_expl' => 'Anonymous users can view full-resolution photos.', + 'hidden' => 'Hidden', + 'hidden_expl' => 'Anonymous users need a direct link to access this album.', + 'downloadable' => 'Downloadable', + 'downloadable_expl' => 'Anonymous users can download this album.', + 'password' => 'Password', + 'password_prot' => 'Password protected', + 'password_prot_expl' => 'Anonymous users need a shared password to access this album.', + 'nsfw' => 'Sensitive', + 'nsfw_expl' => 'Album contains sensitive content.', + 'visibility_updated' => 'Visibility updated.', + ], + 'move_album' => [ + 'confirm_single' => 'Are you sure you want to move the album “%1$s” into the album “%2$s”?', + 'confirm_multiple' => 'Are you sure you want to move all selected albums into the album “%s”?', + 'move_single' => 'Move Album', + 'move_to' => 'Move to', + 'move_to_single' => 'Move %s to:', + 'move_to_multiple' => 'Move %d albums to:', + 'no_album_target' => 'No album to move to', + 'moved_single' => 'Album moved!', + 'moved_single_details' => '%1$s moved to %2$s', + 'moved_details' => 'Album(s) moved to %s', + ], + 'new_album' => [ + 'menu' => 'Create Album', + 'info' => 'Enter a title for the new album:', + 'title' => 'title', + 'create' => 'Create Album', + ], + 'new_tag_album' => [ + 'menu' => 'Create Tag Album', + 'info' => 'Enter a title for the new tag album:', + 'title' => 'title', + 'set_tags' => 'Set tags to show', + 'warn' => 'Make sure to press enter after each tag', + 'create' => 'Create Tag Album', + ], + 'delete_album' => [ + 'confirmation' => 'Are you sure you want to delete the album “%s” and all of the photos it contains?', + 'confirmation_multiple' => 'Are you sure you want to delete all %d selected albums and all of the photos they contain?', + 'warning' => 'This action can not be undone!', + 'delete' => 'Delete Album and Photos', + ], + 'transfer' => [ + 'query' => 'Transfer ownership of album to', + 'confirmation' => 'Are you sure you want to transfer the ownership of album “%s” and all the photos it contains to "%s"?', + 'lost_access_warning' => 'Your access to this album will be lost.', + 'warning' => 'This action can not be undone!', + 'transfer' => 'Transfer ownership of album and photos', + ], + 'rename' => [ + 'photo' => 'Enter a new title for this photo:', + 'album' => 'Enter a new title for this album:', + 'rename' => 'Rename', + ], + 'merge' => [ + 'merge_to' => 'Merge %s to:', + 'merge_to_multiple' => 'Merge %d albums to:', + 'no_albums' => 'No albums to merge to.', + 'confirm' => 'Are you sure you want to merge the album “%1$s” into the album “%2$s”?', + 'confirm_multiple' => 'Are you sure you want to merge all selected albums into the album “%s”?', + 'merge' => 'Merge Albums', + 'merged' => 'Album(s) merged to %s!', + ], + 'unlock' => [ + 'password_required' => 'This album is protected by a password. Enter the password below to view the photos of this album:', + 'password' => 'Password', + 'unlock' => 'Unlock', + ], + 'photo_tags' => [ + 'question' => 'Enter your tags for this photo.', + 'question_multiple' => 'Enter your tags for all %d selected photos. Existing tags will be overwritten.', + 'no_tags' => 'No Tags', + 'set_tags' => 'Set Tags', + 'updated' => 'Tags updated!', + 'tags_override_info' => 'If this is unchecked, the tags will be added to the existing tags of the photo.', + ], + 'photo_copy' => [ + 'no_albums' => 'No albums to copy to', + 'copy_to' => 'Copy %s to:', + 'copy_to_multiple' => 'Copy %d photos to:', + 'confirm' => 'Copy %s to %s.', + 'confirm_multiple' => 'Copy %d photos to %s.', + 'copy' => 'Copy', + 'copied' => 'Photo(s) copied!', + ], + 'photo_delete' => [ + 'confirm' => 'Are you sure you want to delete the photo “%s”?', + 'confirm_multiple' => 'Are you sure you want to delete all %d selected photos?', + 'deleted' => 'Photo(s) deleted!', + ], + 'move_photo' => [ + 'move_single' => 'Move %s to:', + 'move_multiple' => 'Move %d photos to:', + 'confirm' => 'Move %s to %s.', + 'confirm_multiple' => 'Move %d photos to %s.', + 'moved' => 'Photo(s) moved to %s!', + ], + 'target_user' => [ + 'placeholder' => 'Select user', + ], + 'target_album' => [ + 'placeholder' => 'Select album', + ], + 'webauthn' => [ + 'u2f' => 'U2F', + 'success' => 'Authentication successful!', + 'error' => 'Whoops, it looks like something went wrong. Please reload the site and try again!', + ], + 'se' => [ + 'available' => 'Available in the Supporter Edition', + ], + 'session_expired' => [ + 'title' => 'Session expired', + 'message' => 'Your session has expired.
Please reload the page.', + 'reload' => 'Reload', + 'go_to_gallery' => 'Go to the Gallery', + ], +]; \ No newline at end of file diff --git a/lang/vi/fix-tree.php b/lang/vi/fix-tree.php new file mode 100644 index 00000000000..64803e310e6 --- /dev/null +++ b/lang/vi/fix-tree.php @@ -0,0 +1,55 @@ + 'Maintenance', + 'intro' => 'This page allows you to re-order and fix your albums manually.
Before any modifications, we strongly recommend you to read about Nested Set tree structures.', + 'warning' => 'You can really break your Lychee installation here, modify values at your own risks.', + + 'help' => [ + 'header' => 'Help', + 'hover' => 'Hover ids or titles to highlight related albums.', + 'left' => 'Left', + 'right' => 'Right', + 'convenience' => 'For your convenience, the and buttons allow you to change the values of %s and %s by respectively +1 and -1 with propagation.', + 'left-right-warn' => 'The and indicates that the value of %s (and respectively %s) is duplicated somewhere.', + 'parent-marked' => 'Marked Parent Id indicates that the %s and %s do not satisfy the Nest Set tree structures. Edit either the Parent Id or the %s/%s values.', + 'slowness' => 'This page will be slow with a large number of albums.', + ], + + 'buttons' => [ + 'reset' => 'Reset', + 'check' => 'Check', + 'apply' => 'Apply', + ], + + 'table' => [ + 'title' => 'Title', + 'left' => 'Left', + 'right' => 'Right', + 'id' => 'Id', + 'parent' => 'Parent Id', + ], + + 'errors' => [ + 'invalid' => 'Invalid tree!', + 'invalid_details' => 'We are not applying this as it is guaranteed to be a broken state.', + 'invalid_left' => 'Album %s has an invalid left value.', + 'invalid_right' => 'Album %s has an invalid right value.', + 'invalid_left_right' => 'Album %s has an invalid left/right values. Left should be strictly smaller than right: %s < %s.', + 'duplicate_left' => 'Album %s has a duplicate left value %s.', + 'duplicate_right' => 'Album %s has a duplicate right value %s.', + 'parent' => 'Album %s has an unexpected parent id %s.', + 'unknown' => 'Album %s has an unknown error.', + ], +]; \ No newline at end of file diff --git a/lang/vi/gallery.php b/lang/vi/gallery.php new file mode 100644 index 00000000000..eb8008827e0 --- /dev/null +++ b/lang/vi/gallery.php @@ -0,0 +1,241 @@ + 'Gallery', + + 'smart_albums' => 'Smart albums', + 'albums' => 'Albums', + 'root' => 'Albums', + + 'original' => 'Original', + 'medium' => 'Medium', + 'medium_hidpi' => 'Medium HiDPI', + 'small' => 'Thumb', + 'small_hidpi' => 'Thumb HiDPI', + 'thumb' => 'Square thumb', + 'thumb_hidpi' => 'Square thumb HiDPI', + 'placeholder' => 'Low Quality Image Placeholder', + 'thumbnail' => 'Photo thumbnail', + 'live_video' => 'Video part of live-photo', + + 'camera_data' => 'Camera date', + 'album_reserved' => 'All Rights Reserved', + + 'map' => [ + 'error_gpx' => 'Error loading GPX file', + 'osm_contributors' => 'OpenStreetMap contributors', + ], + + 'search' => [ + 'title' => 'Search', + 'searching' => 'Searching…', + 'no_results' => 'Nothing matches your search query.', + 'searchbox' => 'Search…', + 'minimum_chars' => 'Minimum %s characters required.', + 'photos' => 'Photos (%s)', + 'albums' => 'Albums (%s)', + ], + + 'smart_album' => [ + 'unsorted' => 'Unsorted', + 'starred' => 'Starred', + 'recent' => 'Recent', + 'public' => 'Public', + 'on_this_day' => 'On This Day', + ], + + 'layout' => [ + 'squares' => 'Square thumbnails', + 'justified' => 'With aspect, justified', + 'masonry' => 'With aspect, masonry', + 'grid' => 'With aspect, grid', + ], + + 'overlay' => [ + 'none' => 'None', + 'exif' => 'EXIF data', + 'description' => 'Description', + 'date' => 'Date taken', + ], + + 'timeline' => [ + 'default' => 'default', + 'disabled' => 'disabled', + 'year' => 'Year', + 'month' => 'Month', + 'day' => 'Day', + 'hour' => 'Hour', + ], + + 'album' => [ + 'header_albums' => 'Albums', + 'header_photos' => 'Photos', + 'no_results' => 'Nothing to see here', + 'upload' => 'Upload photos', + + 'tabs' => [ + 'about' => 'About Album', + 'share' => 'Share Album', + 'move' => 'Move Album', + 'danger' => 'DANGER ZONE', + ], + + 'hero' => [ + 'created' => 'Created', + 'copyright' => 'Copyright', + 'subalbums' => 'Subalbums', + 'images' => 'Photos', + 'download' => 'Download Album', + 'share' => 'Share Album', + 'stats_only_se' => 'Statistics available in the Supporter Edition', + ], + + 'stats' => [ + 'lens' => 'Lens', + 'shutter' => 'Shutter speed', + 'iso' => 'ISO', + 'model' => 'Model', + 'aperture' => 'Aperture', + 'no_data' => 'No data', + ], + + 'properties' => [ + 'title' => 'Title', + 'description' => 'Description', + 'photo_ordering' => 'Order photos by', + 'children_ordering' => 'Order albums by', + 'asc/desc' => 'asc/desc', + 'header' => 'Set album header', + 'compact_header' => 'Use compact header', + 'license' => 'Set license', + 'copyright' => 'Set copyright', + 'aspect_ratio' => 'Set album thumbs aspect ratio', + 'album_timeline' => 'Set album timeline mode', + 'photo_timeline' => 'Set photo timeline mode', + 'layout' => 'Set photo layout', + 'show_tags' => 'Set tags to show', + 'tags_required' => 'Tags are required.', + ], + ], + + 'photo' => [ + 'actions' => [ + 'star' => 'Star', + 'unstar' => 'Unstar', + 'set_album_header' => 'Set as album header', + 'move' => 'Move', + 'delete' => 'Delete', + 'header_set' => 'Header set', + ], + + 'details' => [ + 'about' => 'About', + 'basics' => 'Basics', + 'title' => 'Title', + 'uploaded' => 'Uploaded', + 'description' => 'Description', + 'license' => 'License', + 'reuse' => 'Reuse', + 'latitude' => 'Latitude', + 'longitude' => 'Longitude', + 'altitude' => 'Altitude', + 'location' => 'Location', + 'image' => 'Image', + 'video' => 'Video', + 'size' => 'Size', + 'format' => 'Format', + 'resolution' => 'Resolution', + 'duration' => 'Duration', + 'fps' => 'Frame rate', + 'tags' => 'Tags', + 'camera' => 'Camera', + 'captured' => 'Captured', + 'make' => 'Make', + 'type' => 'Type/Model', + 'lens' => 'Lens', + 'shutter' => 'Shutter Speed', + 'aperture' => 'Aperture', + 'focal' => 'Focal Length', + 'iso' => 'ISO %s', + ], + + 'edit' => [ + 'set_title' => 'Set Title', + 'set_description' => 'Set Description', + 'set_license' => 'Set License', + 'no_tags' => 'No Tags', + 'set_tags' => 'Set Tags', + 'set_created_at' => 'Set Upload Date', + ], + ], + + 'nsfw' => [ + 'header' => 'Sensitive content', + 'description' => 'This album contains sensitive content which some people may find offensive or disturbing.', + 'consent' => 'Tap to consent.', + ], + + 'menus' => [ + 'star' => 'Star', + 'unstar' => 'Unstar', + 'star_all' => 'Star Selected', + 'unstar_all' => 'Unstar Selected', + 'tag' => 'Tag', + 'tag_all' => 'Tag Selected', + 'set_cover' => 'Set Album Cover', + 'remove_header' => 'Remove Album Header', + 'set_header' => 'Set Album Header', + 'copy_to' => 'Copy to …', + 'copy_all_to' => 'Copy Selected to …', + 'rename' => 'Rename', + 'move' => 'Move', + 'move_all' => 'Move Selected', + 'delete' => 'Delete', + 'delete_all' => 'Delete Selected', + 'download' => 'Download', + 'download_all' => 'Download Selected', + 'merge' => 'Merge', + 'merge_all' => 'Merge Selected', + + 'upload_photo' => 'Upload Photo', + 'import_link' => 'Import from Link', + 'import_dropbox' => 'Import from Dropbox', + 'new_album' => 'New Album', + 'new_tag_album' => 'New Tag Album', + 'upload_track' => 'Upload track', + 'delete_track' => 'Delete track', + ], + + 'sort' => [ + 'photo_select_1' => 'Upload Time', + 'photo_select_2' => 'Take Date', + 'photo_select_3' => 'Title', + 'photo_select_4' => 'Description', + 'photo_select_6' => 'Star', + 'photo_select_7' => 'Photo Format', + 'ascending' => 'Ascending', + 'descending' => 'Descending', + 'album_select_1' => 'Creation Time', + 'album_select_2' => 'Title', + 'album_select_3' => 'Description', + 'album_select_5' => 'Latest Take Date', + 'album_select_6' => 'Oldest Take Date', + ], + + 'albums_protection' => [ + 'private' => 'private', + 'public' => 'public', + 'inherit_from_parent' => 'inherit from parent', + ], +]; \ No newline at end of file diff --git a/lang/vi/jobs.php b/lang/vi/jobs.php new file mode 100644 index 00000000000..5d952b76012 --- /dev/null +++ b/lang/vi/jobs.php @@ -0,0 +1,18 @@ + 'Jobs', + + 'no_data' => 'No Jobs have been executed yet.', +]; \ No newline at end of file diff --git a/lang/vi/landing.php b/lang/vi/landing.php new file mode 100644 index 00000000000..fe6fe55b8ea --- /dev/null +++ b/lang/vi/landing.php @@ -0,0 +1,19 @@ + 'Gallery', + 'access_gallery' => 'Access the gallery', + 'hosted_with_lychee' => 'Hosted with Lychee', + 'copyright' => 'All images on this website are subject to copyright by %1$s © %2$s', +]; \ No newline at end of file diff --git a/lang/vi/left-menu.php b/lang/vi/left-menu.php new file mode 100644 index 00000000000..9a3e91f4037 --- /dev/null +++ b/lang/vi/left-menu.php @@ -0,0 +1,29 @@ + 'Back to Gallery', + + 'admin' => 'Admin', + 'clockwork' => 'Clockwork App', + 'logs' => 'Show Logs', + 'jobs' => 'Show Job History', + 'user' => 'User', + + 'sign_out' => 'Sign Out', + + 'about' => 'About', + 'api' => 'API Documentation', + 'source_code' => 'Source Code', + 'support' => 'Support', +]; \ No newline at end of file diff --git a/lang/vi/lychee.php b/lang/vi/lychee.php new file mode 100644 index 00000000000..8e43b9ef013 --- /dev/null +++ b/lang/vi/lychee.php @@ -0,0 +1,535 @@ + 'Tên đăng nhập', + 'PASSWORD' => 'Mật khẩu', + 'ENTER' => 'Ok', + 'CANCEL' => 'Bỏ', + 'CONFIRM' => 'Confirm', + 'SIGN_IN' => 'Đăng nhập', + 'CLOSE' => 'Đóng', + 'SETTINGS' => 'Cài đặt', + 'SEARCH' => 'Tìm kiếm …', + 'MORE' => 'Xem thêm', + 'DEFAULT' => 'Mặc định', + 'GALLERY' => 'Gallery', + + 'USERS' => 'Người dùng', + 'PROFILE' => 'Profile', + 'CREATE' => 'Tạo', + 'REMOVE' => 'Remove', + 'SHARE' => 'Share', + 'U2F' => 'Xác minh 2 Bước', + 'NOTIFICATIONS' => 'Thông báo', + 'SHARING' => 'Chia sẻ', + 'CHANGE_LOGIN' => 'Thay đổi thông tin đăng nhập', + 'CHANGE_SORTING' => 'Thay đổi cách sắp xếp', + 'SET_DROPBOX' => 'Chỉnh Dropbox', + 'ABOUT_LYCHEE' => 'Giới thiệu Lychee', + 'DIAGNOSTICS' => 'Thông tin hệ thống', + 'DIAGNOSTICS_GET_SIZE' => 'Xem dung lượng đã dùng', + 'JOBS' => 'Show job history', + 'LOGS' => 'Xem nhật ký thay đổi', + 'SIGN_OUT' => 'Thoát', + 'UPDATE_AVAILABLE' => 'Có phiên bản mới!', + 'MIGRATION_AVAILABLE' => 'Có thể sang chuyển hệ thống được rồi!', + 'CHECK_FOR_UPDATE' => 'Check for updates', + 'DEFAULT_LICENSE' => 'Bản quyền mặc định cho hình ảnh video tải lên:', + 'SET_LICENSE' => 'Cài đặt bản quyền', + 'SET_OVERLAY_TYPE' => 'Cài đặt thông tin hiển thị trên hình', + 'SET_ALBUM_DECORATION' => 'Set album decorations', + 'SET_MAP_PROVIDER' => 'Cài đặt nhà cung cấp ô bản đồ OpenStreetMap', + 'FULL_SETTINGS' => 'Toàn bộ cài đặt', + 'UPDATE' => 'Cập nhật', + 'RESET' => 'Reset', + 'DISABLE_TOKEN_TOOLTIP' => 'Disable', + 'ENABLE_TOKEN' => 'Enable API token', + 'DISABLED_TOKEN_STATUS_MSG' => 'Disabled', + 'TOKEN_BUTTON' => 'API Token ...', + 'TOKEN_NOT_AVAILABLE' => 'You have already viewed this token.', + 'TOKEN_WAIT' => 'Wait ...', + + 'SMART_ALBUMS' => 'Những album thông minh', + 'SHARED_ALBUMS' => 'Những album được chia sẻ', + 'ALBUMS' => 'Album', + 'PHOTOS' => 'Hình ảnh', + 'SEARCH_RESULTS' => 'Kết quả tìm kiếm', + + 'RENAME' => 'Đổi tên', + 'RENAME_ALL' => 'Đổi tên mục được chọn', + 'MERGE' => 'Gộp chung', + 'MERGE_ALL' => 'Gộp chung mục được chọn', + 'MAKE_PUBLIC' => 'Chia sẻ công cộng', + 'SHARE_ALBUM' => 'Chia sẻ album', + 'SHARE_PHOTO' => 'Chia sẻ hình ảnh', + 'VISIBILITY_ALBUM' => 'Chế độ hiển thị album', + 'VISIBILITY_PHOTO' => 'Chế độ hiển thị hình ảnh', + 'DOWNLOAD_ALBUM' => 'Tải album về máy', + 'ABOUT_ALBUM' => 'Giới thiệu về album', + 'DELETE_ALBUM' => 'Xóa album', + 'MOVE_ALBUM' => 'Di chuyển album', + 'FULLSCREEN_ENTER' => 'Vào xem toàn màn hình', + 'FULLSCREEN_EXIT' => 'Thoát chế độ xem toàn màn hình', + + 'SHARING_ALBUM_USERS' => 'Chia sẻ album này với những người dùng khác', + 'WAIT_FETCH_DATA' => 'Vui lòng chờ khi chúng tôi lấy dữ liệu …', + 'SHARING_ALBUM_USERS_NO_USERS' => 'Không có người dùng hiện hữu để chia sẻ album cùng với', + 'SHARING_ALBUM_USERS_LONG_MESSAGE' => 'Chọn những người dùng để chia sẻ album cùng', + + 'DELETE_ALBUM_QUESTION' => 'Xóa album và hình ảnh', + 'KEEP_ALBUM' => 'Giữ lại album', + 'DELETE_ALBUM_CONFIRMATION' => 'Bạn có chắc là bạn muốn xóa album này không “%s” và tất cả những hình ảnh chứa trong đó? Thao tác này sẽ không được phục hồi lại!', + + 'DELETE_TAG_ALBUM_QUESTION' => 'Xóa album', + 'DELETE_TAG_ALBUM_CONFIRMATION' => 'Bạn có chắc là bạn muốn xóa album này không “%s” (những hình ảnh bên trong album sẽ không bị xóa đi)? Thao tác này sẽ không được phục hồi lại!', + + 'DELETE_ALBUMS_QUESTION' => 'Xóa nhiều album và hình ảnh', + 'KEEP_ALBUMS' => 'Giữ lại các album này', + 'DELETE_ALBUMS_CONFIRMATION' => 'Bạn có chắc chắn là muốn xóa hết tất cả %d các album được chọn và cả những hình ảnh chứa trong đó? Thao tác này sẽ không được phục hồi lại!', + + 'DELETE_UNSORTED_CONFIRM' => 'Bạn có chắc là muốn xóa hết tất cả hình ảnh trong album “Chưa được phân loại”? Thao tác này sẽ không được phục hồi!', + 'CLEAR_UNSORTED' => 'Dọn trống album Chưa phân loại', + 'KEEP_UNSORTED' => 'Giữ nguyên hình trong album Chưa phân loại', + + 'EDIT_SHARING' => 'Chỉnh sửa mục Chia sẻ', + 'MAKE_PRIVATE' => 'Để ở chế độ riêng tư', + + 'CLOSE_ALBUM' => 'Đóng album', + 'CLOSE_PHOTO' => 'Đóng hình ảnh', + 'CLOSE_MAP' => 'Đóng bản đồ', + + 'ADD' => 'Thêm vào', + 'MOVE' => 'Di chuyển', + 'MOVE_ALL' => 'Di chuyển phần được chọn', + 'DUPLICATE' => 'Tạo 1 bản nữa', + 'DUPLICATE_ALL' => 'Tạo 1 bản nữa cho phần được chọn', + 'COPY_TO' => 'Sao chép đến …', + 'COPY_ALL_TO' => 'Sao chép phần được chọn đến …', + 'DELETE' => 'Xóa', + 'SAVE' => 'Lưu', + 'DELETE_ALL' => 'Xóa phần được chọn', + 'DOWNLOAD' => 'Tải về máy', + 'DOWNLOAD_ALL' => 'Tải phần được chọn về máy', + 'UPLOAD_PHOTO' => 'Đăng hình lên', + 'IMPORT_LINK' => 'Nhập hình vào từ đường link', + 'IMPORT_DROPBOX' => 'Nhập hình vào từ Dropbox', + 'IMPORT_SERVER' => 'Nhập hình vào từ máy chủ', + 'NEW_ALBUM' => 'Tạo album mới', + 'NEW_TAG_ALBUM' => 'Tạo album mới có thẻ tag', + 'UPLOAD_TRACK' => 'Tải track lên', + 'DELETE_TRACK' => 'Xóa track', + + 'TITLE_NEW_ALBUM' => 'Đặt tên cho album mới:', + 'UNTITLED' => 'Chưa có tên', + 'UNSORTED' => 'Chưa được sắp xếp', + 'STARRED' => 'Được dánh dấu sao', + 'RECENT' => 'Gần đây', + 'PUBLIC' => 'Công cộng', + 'ON_THIS_DAY' => 'On This Day', + 'NUM_PHOTOS' => 'Hình ảnh', + + 'CREATE_ALBUM' => 'Tạo album', + 'CREATE_TAG_ALBUM' => 'Tạo album có thẻ tag', + + 'STAR_PHOTO' => 'Đánh dấu sao hình ảnh', + 'STAR' => 'Đánh dấu sao', + 'UNSTAR' => 'Gỡ dấu sao', + 'STAR_ALL' => 'Đánh dấu sao mục được chọn', + 'UNSTAR_ALL' => 'Gỡ dấu sao mục được chọn', + 'TAG' => 'Đánh dấu tag', + 'TAG_ALL' => 'Đánh dấu tag mục được chọn', + 'UNSTAR_PHOTO' => 'Gỡ dấu sao hình ảnh', + 'SET_COVER' => 'Cài hình ảnh nền cho album', + 'REMOVE_COVER' => 'Xóa hình ảnh nền album', + 'SET_HEADER' => 'Set Album Header', + 'REMOVE_HEADER' => 'Remove Album Header', + 'SET_COMPACT_HEADER' => 'Use Compact Header', + + 'FULL_PHOTO' => 'Xem hình ảnh gốc', + 'ABOUT_PHOTO' => 'Giới thiệu về tấm ảnh', + 'DISPLAY_FULL_MAP' => 'Bản đồ', + 'DIRECT_LINK' => 'Đường link trực tiếp đến hình', + 'DIRECT_LINKS' => 'Những đường link trực tiếp đến hình', + 'QR_CODE' => 'Mã QR', + + 'ALBUM_ABOUT' => 'Giới thiệu', + 'ALBUM_BASICS' => 'Những phần cơ bản', + 'ALBUM_TITLE' => 'Tên album', + 'ALBUM_COPYRIGHT' => 'Copyright', + 'ALBUM_SET_COPYRIGHT' => 'Set copyright', + 'ALBUM_NEW_TITLE' => 'Đặt tên mới cho album này:', + 'ALBUMS_NEW_TITLE' => 'Đặt tên cho tất cả %d những album được chọn:', + 'ALBUM_SET_TITLE' => 'Đặt tên', + 'ALBUM_DESCRIPTION' => 'Mô tả', + 'ALBUM_SHOW_TAGS' => 'Những thẻ tag nào được hiển thị', + 'ALBUM_NEW_DESCRIPTION' => 'Nhập một mô tả mới giới thiệu về album này:', + 'ALBUM_SET_DESCRIPTION' => 'Đặt mô tả', + 'ALBUM_NEW_SHOWTAGS' => 'Nhập những thẻ tag của những hình ảnh sẽ được hiển thị trong album:', + 'ALBUM_SET_SHOWTAGS' => 'Đặt những thẻ tag được hiển thị', + 'ALBUM_ALBUM' => 'Album', + 'ALBUM_CREATED' => 'Đã được tạo', + 'ALBUM_IMAGES' => 'Hình ảnh', + 'ALBUM_VIDEOS' => 'Video', + 'ALBUM_SUBALBUMS' => 'Album con', + 'ALBUM_SHARING' => 'Chia sẻ', + 'ALBUM_SHR_YES' => 'Có', + 'ALBUM_SHR_NO' => 'Không', + 'ALBUM_PUBLIC' => 'Công cộng', + 'ALBUM_PUBLIC_EXPL' => 'Anonymous users can access this album, subject to the restrictions below.', + 'ALBUM_FULL' => 'Hình ảnh gốc', + 'ALBUM_FULL_EXPL' => 'Anonymous users can behold full-resolution photos.', + 'ALBUM_HIDDEN' => 'Đã ẩn', + 'ALBUM_HIDDEN_EXPL' => 'Anonymous users need a direct link to access this album.', + 'ALBUM_MARK_NSFW' => 'Đánh dấu đây là album nhạy cảm', + 'ALBUM_UNMARK_NSFW' => 'Gỡ đánh dấu đây là album nhạy cảm', + 'ALBUM_NSFW' => 'Nhạy cảm', + 'ALBUM_NSFW_EXPL' => 'Album được đánh dấu là chứa hình ảnh nhạy cảm.', + 'ALBUM_DOWNLOADABLE' => 'Có thể tải về máy', + 'ALBUM_DOWNLOADABLE_EXPL' => 'Anonymous users can download this album.', + 'ALBUM_SHARE_BUTTON_VISIBLE' => 'Nút chia sẻ được bật', + 'ALBUM_SHARE_BUTTON_VISIBLE_EXPL' => 'Hiển thị những đường link chia sẻ lên mạng xã hội.', + 'ALBUM_PASSWORD' => 'Mật khẩu', + 'ALBUM_PASSWORD_PROT' => 'Có mật khẩu mới xem được', + 'ALBUM_PASSWORD_PROT_EXPL' => 'Anonymous users need a shared password to access this album.', + 'ALBUM_PASSWORD_REQUIRED' => 'Album này phải có mật khẩu mới xem được. Bạn hãy nhập mật khẩu vào bên dưới để xem hình ảnh trong album nhé:', + 'ALBUM_MERGE' => 'Bạn có chắc chắn là muốn gộp chung album “%1$s” vào album “%2$s”?', + 'ALBUMS_MERGE' => 'Bạn có chắc là muốn gộp hết tất cả những album được chọn vào trong cùng album “%s”?', + 'MERGE_ALBUM' => 'Gộp những album lại với nhau', + 'DONT_MERGE' => 'Không gộp', + 'ALBUM_MOVE' => 'Bạn có chắc là muốn di chuyển album “%1$s” qua album “%2$s”?', + 'ALBUMS_MOVE' => 'Bạn có chắc là muốn di chuyển tất cả những album được chọn vào trong album “%s”?', + 'MOVE_ALBUMS' => 'Di chuyển những album này', + 'NOT_MOVE_ALBUMS' => 'Không di chuyển', + 'ROOT' => 'Albums', + 'ALBUM_REUSE' => 'Cho phép dùng hình ảnh', + 'ALBUM_LICENSE' => 'Bản quyền', + 'ALBUM_SET_LICENSE' => 'Cài đặt bản quyền', + 'ALBUM_LICENSE_HELP' => 'Cần hỗ trợ lựa chọn?', + 'ALBUM_LICENSE_NONE' => 'Không bản quyền', + 'ALBUM_RESERVED' => 'Toàn Quyền Bảo Lưu', + 'ALBUM_SET_ORDER' => 'Chỉnh thứ tự', + 'ALBUM_ORDERING' => 'Sắp xếp thứ tự hình ảnh', + 'ALBUM_PHOTO_ORDERING' => 'Order photos by', + 'ALBUM_CHILDREN_ORDERING' => 'Order albums by', + 'ALBUM_OWNER' => 'Chủ sở hữu', + + 'PHOTO_ABOUT' => 'Giới thiệu', + 'PHOTO_BASICS' => 'Những phần cơ bản', + 'PHOTO_TITLE' => 'Tên hình', + 'PHOTO_NEW_TITLE' => 'Đặt tên mới cho tấm hình này:', + 'PHOTO_SET_TITLE' => 'Đặt tên', + 'PHOTO_UPLOADED' => 'Đăng lên lúc', + 'PHOTO_DESCRIPTION' => 'Mô tả', + 'PHOTO_NEW_DESCRIPTION' => 'Nhập một mô tả mới cho tấm hình này:', + 'PHOTO_SET_DESCRIPTION' => 'Đặt mô tả', + 'PHOTO_NEW_LICENSE' => 'Thêm loại bản quyền', + 'PHOTO_SET_LICENSE' => 'Đặt bản quyền', + 'PHOTO_LICENSE' => 'Bản quyền', + 'PHOTO_LICENSE_HELP' => 'Cần hỗ trợ lựa chọn?', + 'PHOTO_REUSE' => 'Cho phép dùng hình ảnh', + 'PHOTO_LICENSE_NONE' => 'Không bản quyền', + 'PHOTO_RESERVED' => 'Toàn Quyền Bảo Lưu', + 'PHOTO_LATITUDE' => 'Vĩ độ', + 'PHOTO_LONGITUDE' => 'Kinh độ', + 'PHOTO_ALTITUDE' => 'Cao độ', + 'PHOTO_IMGDIRECTION' => 'Hướng hình chụp', + 'PHOTO_LOCATION' => 'Địa điểm', + 'PHOTO_IMAGE' => 'Hình ảnh', + 'PHOTO_VIDEO' => 'Video', + 'PHOTO_SIZE' => 'Kích cỡ', + 'PHOTO_FORMAT' => 'Định dạnh', + 'PHOTO_RESOLUTION' => 'Độ phân giải', + 'PHOTO_DURATION' => 'Thời lượng', + 'PHOTO_FPS' => 'Tốc độ khung hình trong video', + 'PHOTO_TAGS' => 'Thẻ tag', + 'PHOTO_NOTAGS' => 'Không có thẻ tag nào', + 'PHOTO_NEW_TAGS' => 'Nhập thẻ tag cho tấm hình này. Bạn có thể thêm vào nhiều tag cách ra bởi dấu phẩy:', + 'PHOTOS_NEW_TAGS' => 'Nhập thẻ tag của bạn cho tất cả %d những hình ảnh được chọn. Những thẻ tag đang có sẽ được cập nhật. Bạn có thể thêm vào nhiều tag cách ra bởi dấu phẩy:', + 'PHOTO_SET_TAGS' => 'Đặt thẻ tag', + 'PHOTO_CAMERA' => 'Máy ảnh', + 'PHOTO_CAPTURED' => 'Chụp lúc', + 'PHOTO_MAKE' => 'Hãng sản xuất', + 'PHOTO_TYPE' => 'Loại/Đời', + 'PHOTO_LENS' => 'Ống kính', + 'PHOTO_SHUTTER' => 'Tốc độ màn trập', + 'PHOTO_APERTURE' => 'Khẩu độ', + 'PHOTO_FOCAL' => 'Tiêu cự', + 'PHOTO_ISO' => 'Độ nhạy sáng %s', + 'PHOTO_SHARING' => 'Chia sẻ', + 'PHOTO_DELETE' => 'Xóa hình ảnh', + 'PHOTO_KEEP' => 'Giữ hình ảnh', + 'PHOTO_DELETE_CONFIRMATION' => 'Bạn có chắc là bạn muốn xóa hình ảnh này “%s”? Thao tác này sẽ không được khôi phục lại!', + 'PHOTO_DELETE_ALL' => 'Bạn có chắc là bạn muốn xóa tất cả %d hình ảnh được chọn? Thao tác này sẽ không khôi phục lại được!', + 'PHOTOS_NEW_TITLE' => 'Nhập tên cho tất cả %d những hình ảnh được chọn:', + 'PHOTO_MAKE_PRIVATE_ALBUM' => 'Tấm hình này đang có ở trong một album công cộng. Để chỉnh hình lại thành chế độ riêng tư, hãy chỉnh sửa chế độ hiển thị của album liên quan.', + 'PHOTO_SHOW_ALBUM' => 'Hiển thị album', + 'PHOTO_PUBLIC' => 'Chia sẻ công cộng', + 'PHOTO_PUBLIC_EXPL' => 'Người khác có thể xem tấm hình này, với những hạn chế bên dưới.', + 'PHOTO_FULL' => 'Hình ảnh gốc', + 'PHOTO_FULL_EXPL' => 'Anonymous users can behold full-resolution photo.', + 'PHOTO_HIDDEN' => 'Đã ẩn', + 'PHOTO_HIDDEN_EXPL' => 'Anonymous users need a direct link to view this photo.', + 'PHOTO_DOWNLOADABLE' => 'Có thể tải về máy', + 'PHOTO_DOWNLOADABLE_EXPL' => 'Anonymous users may download this photo.', + 'PHOTO_SHARE_BUTTON_VISIBLE' => 'Nút chia sẻ được bật', + 'PHOTO_SHARE_BUTTON_VISIBLE_EXPL' => 'Anonymous users can see social media sharing links.', + 'PHOTO_PASSWORD_PROT' => 'Có mật khẩu mới xem được', + 'PHOTO_PASSWORD_PROT_EXPL' => 'Anonymous users need a shared password to view this photo.', + 'PHOTO_EDIT_SHARING_TEXT' => 'Những đặc điểm chia sẻ của tấm hình này sẽ được thay đổi thành như sau:', + 'PHOTO_NO_EDIT_SHARING_TEXT' => 'Vì tấm hình này đang có trong một album công cộng, nó có chế độ hiển thị công cộng giống như cài đặt hiển thị của album. Chế độ hiển thị hiện tại của hình được hiển thị bên dưới để người dùng biết thêm thông tin.', + 'PHOTO_EDIT_GLOBAL_SHARING_TEXT' => 'Chế độ hiển thị của tấm hình này có thể được chỉnh chỉ tiết hơn với cài đặt tổng của Lychee. Chế độ hiển thị hiện thời của tấm hình được hiển thị để người dùng biết thêm thông tin.', + 'PHOTO_NEW_CREATED_AT' => 'Enter the upload date for this photo. mm/dd/yyyy, hh:mm [am/pm]', + 'PHOTO_SET_CREATED_AT' => 'Set upload date', + + 'LOADING' => 'Đang tải', + 'ERROR' => 'Bị lỗi', + 'ERROR_TEXT' => 'Èo, hình như có trục trặc gì đó xảy ra. Xin hãy thử tải lại trang và thử lại nhé!', + 'ERROR_UNKNOWN' => 'Có sự cố bất ngờ nào đó đã xảy ra. Xin hãy thử lại và kiểm tra phần cài đặt hệ thống và máy chủ của bạn. Hãy đọc phần readme để biết thêm thông tin chi tiết.', + 'ERROR_MAP_DEACTIVATED' => 'Tính năng hiển thị bản đồ đã tắt trong phần cài đặt.', + 'ERROR_SEARCH_DEACTIVATED' => 'Tính năng tìm kiếm đã tắt trong phần cài đặt', + 'SUCCESS' => 'OK', + 'CHANGE_SUCCESS' => 'Change successful.', + 'RETRY' => 'Thử lại', + 'OVERRIDE' => 'Override', + 'TAGS_OVERRIDE_INFO' => 'If this is unchecked, the tags will be added to the existing tags of the photo.', + + 'SETTINGS_SUCCESS_LOGIN' => 'Thông tin đăng nhập đã được cập nhật.', + 'SETTINGS_SUCCESS_SORT' => 'Thứ tự sắp xếp đã được cập nhật.', + 'SETTINGS_SUCCESS_DROPBOX' => 'Mã Dropbox đã được cập nhật.', + 'SETTINGS_SUCCESS_LANG' => 'Ngôn ngữ đã được cập nhật', + 'SETTINGS_SUCCESS_LAYOUT' => 'Cách bày trí đã được cập nhật', + 'SETTINGS_SUCCESS_IMAGE_OVERLAY' => 'Phần cài đặt thông tin EXIF hiển thị trên hình đã được cập nhật', + 'SETTINGS_SUCCESS_PUBLIC_SEARCH' => 'Đã cập nhật tính năng tìm kiếm công cộng', + 'SETTINGS_SUCCESS_LICENSE' => 'Đã cập nhật bản quyền sử dụng hình ảnh mặc định', + 'SETTINGS_SUCCESS_MAP_DISPLAY' => 'Đã cập nhật cài đặt hiển thị bản đồ', + 'SETTINGS_SUCCESS_MAP_DISPLAY_PUBLIC' => 'Đã cập nhật cài đặt hiển thị bản đồ cho những album chia sẻ công cộng', + 'SETTINGS_SUCCESS_MAP_PROVIDER' => 'Đã cập nhật cài đặt nhà cung cấp bản đồ', + 'SETTINGS_SUCCESS_CSS' => 'Stylesheets updated', + 'SETTINGS_SUCCESS_JS' => 'JS updated', + 'SETTINGS_SUCCESS_UPDATE' => 'Settings updated successfully', + 'SETTINGS_DROPBOX_KEY' => 'Mã API Dropbox', + 'SETTINGS_ADVANCED_WARNING_EXPL' => 'Changing these advanced settings can be harmful to the stability, security and performance of this application. You should only modify them if you are sure of what you are doing.', + 'SETTINGS_ADVANCED_SAVE' => 'Save my modifications, I accept the risk!', + + 'U2F_NOT_SUPPORTED' => 'Xác minh 2 Bước chưa được hỗ trợ. Thật xin lỗi nha.', + 'U2F_NOT_SECURE' => 'Môi trường đăng tải chưa được bảo mật. Xác minh 2 Bước chưa được cài đặt.', + 'U2F_REGISTER_KEY' => 'Đăng ký một thiết bị mới.', + 'U2F_REGISTRATION_SUCCESS' => 'Đã đăng ký thiết bị mới thành công!', + 'U2F_AUTHENTIFICATION_SUCCESS' => 'Xác minh thành công!', + 'U2F_CREDENTIALS' => 'Tên và Mật mã', + 'U2F_CREDENTIALS_DELETED' => 'Tên và mật mã đã được xóa!', + 'U2F_LOGIN' => 'Log in with WebAuthn', + + 'NEW_PHOTOS_NOTIFICATION' => 'Gửi email thông báo đăng hình ảnh mới.', + 'SETTINGS_SUCCESS_NEW_PHOTOS_NOTIFICATION' => 'Thông báo đăng hình mới được cập nhật', + 'USER_EMAIL_INSTRUCTION' => 'Thêm email của bạn vào bên dưới để nhận được thông báo qua email. Để ngưng nhận email, bạn chỉ cần xóa email của bạn ở bên dưới.', + + 'LOGIN_USERNAME' => 'Tên đăng nhập mới', + 'LOGIN_PASSWORD' => 'Mật khẩu mới', + 'LOGIN_PASSWORD_CONFIRM' => 'Xác nhận mật khẩu', + 'PASSWORD_TITLE' => 'Nhập mật khẩu cũ của bạn:', + 'PASSWORD_CURRENT' => 'Mật khẩu cũ', + 'PASSWORD_TEXT' => 'Tên đăng nhập và mật khẩu của bạn sẽ được thay đổi thành như sau:', + 'PASSWORD_CHANGE' => 'Thay đổi thông tin đăng nhập', + + 'EDIT_SHARING_TITLE' => 'Chỉnh sửa chế độ chia sẻ', + 'EDIT_SHARING_TEXT' => 'Những đặc điểm chia sẻ của tấm hình này sẽ được thay đổi thành như sau:', + 'SHARE_ALBUM_TEXT' => 'Album này sẽ được chia sẻ với những đặc điểm sau:', + + 'SORT_DIALOG_ATTRIBUTE_LABEL' => 'Attribute', + 'SORT_DIALOG_ORDER_LABEL' => 'Order', + + 'SORT_ALBUM_BY' => 'Sắp xếp album theo %1$s một %2$s thứ tự.', + + 'SORT_ALBUM_SELECT_1' => 'Lúc tạo album', + 'SORT_ALBUM_SELECT_2' => 'Tên', + 'SORT_ALBUM_SELECT_3' => 'Mô tả', + 'SORT_ALBUM_SELECT_5' => 'Lần chụp gần nhất', + 'SORT_ALBUM_SELECT_6' => 'Lần chụp lâu nhất', + + 'SORT_PHOTO_BY' => 'Sắp xếp hình ảnh theo %1$s một %2$s thứ tự.', + + 'SORT_PHOTO_SELECT_1' => 'Lúc hình được đăng', + 'SORT_PHOTO_SELECT_2' => 'Ngày chụp', + 'SORT_PHOTO_SELECT_3' => 'Tên', + 'SORT_PHOTO_SELECT_4' => 'Mô tả', + 'SORT_PHOTO_SELECT_6' => 'Được đánh dấu sao', + 'SORT_PHOTO_SELECT_7' => 'Định dạng hình ảnh', + + 'SORT_ASCENDING' => 'Tăng dần (A-Z, cũ-mới)', + 'SORT_DESCENDING' => 'Giảm dần (Z-A, mới-cũ)', + 'SORT_CHANGE' => 'Thay đổi cách sắp xếp', + + 'DROPBOX_TITLE' => 'Đặt mã Dropbox', + 'DROPBOX_TEXT' => "Để nhập hình ảnh của bạn vào từ Dropbox, bạn cần một mã nhập vào hợp lệ từ trang web của họ. Hãy tạo một mã cá nhân cho mình và nhập mã vào bên dưới nhé:", + + 'LANG_TEXT' => 'Thay đổi ngôn ngữ trên Lychee cho:', + 'LANG_TITLE' => 'Thay đổi ngôn ngữ', + + 'SETTING_RECENT_PUBLIC_TEXT' => 'Make "Recent" smart album accessible to anonymous users', + 'SETTING_STARRED_PUBLIC_TEXT' => 'Make "Starred" smart album accessible to anonymous users', + 'SETTING_ONTHISDAY_PUBLIC_TEXT' => 'Make "On This Day" smart album accessible to anonymous users', + + 'CSS_TEXT' => 'Personalize CSS:', + 'CSS_TITLE' => 'Change CSS', + 'JS_TEXT' => 'Custom JS:', + 'JS_TITLE' => 'Change JS', + 'PUBLIC_SEARCH_TEXT' => 'Cho phép tìm kiếm công cộng:', + 'OVERLAY_TYPE' => 'Thông tin hiển thị trên hình:', + 'OVERLAY_NONE' => 'Không hiển thị gì cả', + 'OVERLAY_EXIF' => 'Hiển thị dữ liệu EXIF', + 'OVERLAY_DESCRIPTION' => 'Hiển thị mô tả hình ảnh', + 'OVERLAY_DATE' => 'Hiển thị ngày chụp', + 'ALBUM_DECORATION' => 'Album decorations:', + 'ALBUM_DECORATION_NONE' => 'None', + 'ALBUM_DECORATION_ORIGINAL' => 'Sub-album marker', + 'ALBUM_DECORATION_ALBUM' => 'Number of sub-albums', + 'ALBUM_DECORATION_PHOTO' => 'Number of photos', + 'ALBUM_DECORATION_ALL' => 'Number of sub-albums and photos', + 'ALBUM_DECORATION_ORIENTATION' => 'Orientation of album decorations:', + 'ALBUM_DECORATION_ORIENTATION_ROW' => 'Horizontal (photos, albums)', + 'ALBUM_DECORATION_ORIENTATION_ROW_REVERSE' => 'Horizontal (albums, photos)', + 'ALBUM_DECORATION_ORIENTATION_COLUMN' => 'Vertical (top photos, albums)', + 'ALBUM_DECORATION_ORIENTATION_COLUMN_REVERSE' => 'Vertical (top albums, photos)', + 'MAP_DISPLAY_TEXT' => 'Bật chế độ bản đồ (nhà cung cấp OpenStreetMap):', + 'MAP_DISPLAY_PUBLIC_TEXT' => 'Bật chế độ bản đồ cho những album chia sẻ công cộng (nhà cung cấp OpenStreetMap):', + 'MAP_PROVIDER' => 'Nhà cung cấp của những ô bản đồ OpenStreetMap:', + 'MAP_PROVIDER_WIKIMEDIA' => 'Wikimedia', + 'MAP_PROVIDER_OSM_ORG' => 'OpenStreetMap.org (không có độ phân giải cao HiDPI)', + 'MAP_PROVIDER_OSM_DE' => 'OpenStreetMap.de (không có độ phân giải cao HiDPI)', + 'MAP_PROVIDER_OSM_FR' => 'OpenStreetMap.fr (không có độ phân giải cao HiDPI)', + 'MAP_PROVIDER_RRZE' => 'University of Erlangen, Germany (chỉ có HiDPI)', + 'MAP_INCLUDE_SUBALBUMS_TEXT' => 'Hiển thị hình ảnh của album con trên bản đồ:', + 'LOCATION_DECODING' => 'Giải mã dữ liệu GPS thành tên địa điểm', + 'LOCATION_SHOW' => 'Hiển thị tên địa điểm', + 'LOCATION_SHOW_PUBLIC' => 'Hiển thị tên địa điểm cho chế độ chia sẻ công cộng', + + 'LAYOUT_TYPE' => 'Cách trình bày hình ảnh:', + 'LAYOUT_SQUARES' => 'Ô vuông hình nhỏ', + 'LAYOUT_JUSTIFIED' => 'Theo tỷ lệ hình, canh đều hai bên', + 'LAYOUT_MASONRY' => 'Theo tỷ lệ hình, masonry', + 'LAYOUT_GRID' => 'Theo tỷ lệ hình, grid', + 'LAYOUT_UNJUSTIFIED' => 'Theo tỷ lệ hình, không canh đều hai bên', + 'SET_LAYOUT' => 'Thay đổi cách trình bày', + + 'NSFW_VISIBLE_TEXT_1' => 'Tự động cho hiển thị những album nhạy cảm.', + 'NSFW_VISIBLE_TEXT_2' => 'Nếu album được chia sẻ công cộng, album nhạy cảm vẫn có thể được truy cập, chỉ là tạm làm mờ khỏi tầm nhìn và có thể được hiện rõ ra bằng cách bấm phím H.', + 'SETTINGS_SUCCESS_NSFW_VISIBLE' => 'Chế độ hiển thị album nhạy cảm được cập nhật thành công.', + + 'NSFW_BANNER' => '

Sensitive content

This album contains sensitive content which some people may find offensive or disturbing.

Tap to consent.

', + 'NSFW_HEADER' => 'Sensitive content', + 'NSFW_EXPLANATION' => 'This album contains sensitive content which some people may find offensive or disturbing.', + 'TAP_CONSENT' => 'Tap to consent.', + + 'VIEW_NO_RESULT' => 'Không có kết quả nào', + 'VIEW_NO_PUBLIC_ALBUMS' => 'Chưa có album chia sẻ công cộng nào', + 'VIEW_NO_CONFIGURATION' => 'Chưa có thiết lập', + 'VIEW_PHOTO_NOT_FOUND' => 'Không tìm thấy hình ảnh', + + 'NO_TAGS' => 'Không có thẻ tag', + + 'UPLOAD_MANAGE_NEW_PHOTOS' => 'Giờ bạn có thể quản lý những hình ảnh mới của bạn rồi đó.', + 'UPLOAD_COMPLETE' => 'Đã tải hình lên xong', + 'UPLOAD_COMPLETE_FAILED' => 'Không thể tải một trong số các tấm hình lên', + 'UPLOAD_IMPORTING' => 'Đang nhập hình vào', + 'UPLOAD_IMPORTING_URL' => 'Đang nhập đường link vào', + 'UPLOAD_UPLOADING' => 'Đang tải lên', + 'UPLOAD_FINISHED' => 'Đã xong', + 'UPLOAD_PROCESSING' => 'Đang xử lý', + 'UPLOAD_FAILED' => 'Thất bại rồi', + 'UPLOAD_FAILED_ERROR' => 'Không thể tải hình lên được. Hệ thống báo lỗi!', + 'UPLOAD_FAILED_WARNING' => 'Không thể tải hình lên được. Hệ thống hiển thị một cảnh báo!', + 'UPLOAD_CANCELLED' => 'Đã hủy bỏ', + 'UPLOAD_SKIPPED' => 'Đã bỏ qua', + 'UPLOAD_UPDATED' => 'Đã cập nhật', + 'UPLOAD_GENERAL' => 'General', + 'UPLOAD_IMPORT_SKIPPED_DUPLICATE' => 'Hình ảnh này đã được bỏ qua vì nó đã có ở trong album ảnh của bạn rồi.', + 'UPLOAD_IMPORT_RESYNCED_DUPLICATE' => 'Hình ảnh này đã được bỏ qua vì nó đã có ở trong album ảnh của bạn rồi, nhưng thông tin meta về hình ảnh đã được cập nhật.', + 'UPLOAD_ERROR_CONSOLE' => 'Xin hãy xem qua thử bảng console ở trong trình duyệt của bạn để biết thêm thông tin chi tiết.', + 'UPLOAD_UNKNOWN' => 'Hệ thống báo một phản hồi chưa xác định. Xin hãy xem qua thử bảng console ở trong trình duyệt của bạn để biết thêm thông tin chi tiết.', + 'UPLOAD_ERROR_UNKNOWN' => 'Không thể tải hình lên được. Hệ thống báo lỗi chưa xác định!', + 'UPLOAD_ERROR_POSTSIZE' => 'Không thể tải hình lên được. Kích cỡ hình ảnh cho phép của PHP post_max_size có thể là quá nhỏ! Nếu không bạn hãy xem thử phần Câu hỏi thường gặp.', + 'UPLOAD_ERROR_FILESIZE' => 'Không thể tải hình lên được. Kích cỡ hình ảnh cho phép của PHP upload_max_filesize có thể là quá nhỏ! Nếu không bạn hãy xem thử phần Câu hỏi thường gặp. ', + 'UPLOAD_IN_PROGRESS' => 'Lychee hiện đang tải hình lên!', + 'UPLOAD_IMPORT_WARN_ERR' => 'Nhập hình vào đã xong, nhưng hệ thống trả kết quả cảnh báo hoặc có lỗi. Xin hãy xem phần nhật ký thay đổi (Cài đặ -> Hiển thị nhật ký thay đổi) để biết thêm thông tin chi tiết.', + 'UPLOAD_IMPORT_COMPLETE' => 'Đã nhập hình vào xong', + 'UPLOAD_IMPORT_INSTR' => 'Xin thả vào đây đường link trực tiếp của hình để nhập nó vào:', + 'UPLOAD_IMPORT' => 'Nhập hình', + 'UPLOAD_IMPORT_SERVER' => 'Nhập hình từ máy chủ', + 'UPLOAD_IMPORT_SERVER_FOLD' => 'Thư mục đang trống hoặc không có tập tin nào có thể đọc được để xử lý. Xin hãy xem phần nhật ký thay đổi (Cài đặ -> Hiển thị nhật ký thay đổi) để biết thêm thông tin chi tiết.', + 'UPLOAD_IMPORT_SERVER_INSTR' => 'Import all photos, folders and sub-folders located in the folders with the following absolute paths (on server). Paths are space separated, use \\ to escape a space in a path.', + 'UPLOAD_ABSOLUTE_PATH' => 'Absolute path to directories, space separated', + 'UPLOAD_IMPORT_SERVER_EMPT' => 'Không thể bắt đầu nhập hình vì thư mục không có hình ảnh gì cả!', + 'UPLOAD_IMPORT_DELETE_ORIGINALS' => 'Xóa hình ảnh gốc', + 'UPLOAD_IMPORT_DELETE_ORIGINALS_EXPL' => 'Những tập tin hình ảnh gốc sẽ được xóa đi sau lúc nhập hình khi có thể.', + 'UPLOAD_IMPORT_VIA_SYMLINK' => 'Đường link biểu trưng', + 'UPLOAD_IMPORT_VIA_SYMLINK_EXPL' => 'Nhập hình vào bằng đường link biểu trưng đến hình ảnh gốc.', + 'UPLOAD_IMPORT_SKIP_DUPLICATES' => 'Bỏ qua những tấm hình bị trùng', + 'UPLOAD_IMPORT_SKIP_DUPLICATES_EXPL' => 'Những tấm hình đã có trong album rồi sẽ được bỏ qua.', + 'UPLOAD_IMPORT_RESYNC_METADATA' => 'Đồng bộ lại thông tin meta', + 'UPLOAD_IMPORT_RESYNC_METADATA_EXPL' => 'Cập nhật thông tin meta của những tập tin hình ảnh đang có trong album.', + 'UPLOAD_IMPORT_LOW_MEMORY_EXPL' => 'Quá trình nhập hình trên hệ thống đang đến giới hạn bộ nhớ và có thể dừng bất thình lình bất cứ lúc nào.', + 'UPLOAD_WARNING' => 'Cảnh báo', + 'UPLOAD_IMPORT_NOT_A_DIRECTORY' => 'Đường dẫn bỏ vào không phải là một thư mục có thể đọc được!', + 'UPLOAD_IMPORT_PATH_RESERVED' => 'Đường dẫn bỏ vào là một đường dẫn dành riêng cho Lychee!', + 'UPLOAD_IMPORT_FAILED' => 'Không thể nhập tập tin hình ảnh vào!', + 'UPLOAD_IMPORT_UNSUPPORTED' => 'Định dạng hình ảnh không được hỗ trợ!', + 'UPLOAD_IMPORT_CANCELLED' => 'Quá trình nhập hình đã được hủy bỏ', + + 'ABOUT_SUBTITLE' => 'Phần mềm tự chạy quản lý hình ảnh đúng chuẩn', + 'ABOUT_DESCRIPTION' => 'Lychee là công cụ quản lý ảnh miễn phí, chạy trên chính hệ thống máy của bạn hoặc không gian web của bạn. Cài đặt nó chỉ mất vài giây. Tải hình lên, quản lý và chia sẻ hình ảnh như ứng dụng chính chủ của bạn. Lychee mang đến tất cả những gì bạn cần và những hình ảnh của bạn được lưu giữ một cách an toàn nhất có thể.', + 'FOOTER_COPYRIGHT' => 'Tất cả hình ảnh trên trang web này thuộc Bản quyền của %1$s © %2$s', + 'HOSTED_WITH_LYCHEE' => 'Được chạy bởi Lychee', + + 'URL_COPY_TO_CLIPBOARD' => 'Sao chép vào bộ nhớ tạm thời', + 'URL_COPIED_TO_CLIPBOARD' => 'Đã sao chép vào bộ nhớ tạm thời!', + 'PHOTO_DIRECT_LINKS_TO_IMAGES' => 'Đường link trực tiếp đến tập tin hình ảnh:', + 'PHOTO_ORIGINAL' => 'Original', + 'PHOTO_MEDIUM' => 'Vừa', + 'PHOTO_MEDIUM_HIDPI' => 'Độ phân giải cho hình vừa HiDPI', + 'PHOTO_SMALL' => 'Nhỏ', + 'PHOTO_SMALL_HIDPI' => 'Độ phân giải cho hình nhỏ HiDPI', + 'PHOTO_THUMB' => 'Ô vuông nhỏ', + 'PHOTO_THUMB_HIDPI' => 'Độ phân giải cho hình ô vuông nhỏ HiDPI', + 'PHOTO_PLACEHOLDER' => 'Low Quality Image Placeholder', + 'PHOTO_THUMBNAIL' => 'Hình thu nhỏ của hình ảnh gốc', + 'PHOTO_LIVE_VIDEO' => 'Phần video của hình động live-photo', + 'PHOTO_VIEW' => 'Khung hiển thị hình ảnh Lychee:', + + 'PHOTO_EDIT_ROTATECWISE' => 'Xoay thuận chiều kim đồng hồ', + 'PHOTO_EDIT_ROTATECCWISE' => 'Xoay ngược chiều kim đồng hồ', + + 'ERROR_GPX' => 'Có lỗi khi tải tập tin GPX: ', + 'ERROR_EITHER_ALBUMS_OR_PHOTOS' => 'Please select either albums or photos!', + 'ERROR_COULD_NOT_FIND' => 'Could not find what you want.', + 'ERROR_INVALID_EMAIL' => 'Not a valid email address.', + 'EMAIL_SUCCESS' => 'Email updated!', + 'ERROR_PHOTO_NOT_FOUND' => 'Error: photo %s not found !', + 'ERROR_EMPTY_USERNAME' => 'new username cannot be empty.', + 'ERROR_PASSWORD_DOES_NOT_MATCH' => 'new password does not match.', + 'ERROR_EMPTY_PASSWORD' => 'new password cannot be empty.', + 'ERROR_SELECT_ALBUM' => 'Select an album to share!', + 'ERROR_SELECT_USER' => 'Select a user to share with!', + 'ERROR_SELECT_SHARING' => 'Select a sharing to remove!', + 'SHARING_SUCCESS' => 'Sharing updated!', + 'SHARING_REMOVED' => 'Sharing removed!', + 'USER_CREATED' => 'User created!', + 'USER_DELETED' => 'User deleted!', + 'USER_UPDATED' => 'User updated!', + 'ENTER_EMAIL' => 'Enter your email address:', + 'ERROR_ALBUM_JSON_NOT_FOUND' => 'Error: Album json not found!', + 'ERROR_ALBUM_NOT_FOUND' => 'Error: album %s not found', + 'ERROR_DROPBOX_KEY' => 'Error: Dropbox key not set', + 'ERROR_SESSION' => 'Session expired.', + 'CAMERA_DATE' => 'Camera date', + 'NEW_PASSWORD' => 'new password', + 'ALLOW_UPLOADS' => 'Cho phép đăng hình ảnh video', + 'ALLOW_USER_SELF_EDIT' => 'Allow self-management of user account', + 'OSM_CONTRIBUTORS' => 'OpenStreetMap contributors', +]; diff --git a/lang/vi/maintenance.php b/lang/vi/maintenance.php new file mode 100644 index 00000000000..f86de3d6f46 --- /dev/null +++ b/lang/vi/maintenance.php @@ -0,0 +1,60 @@ + 'Maintenance', + 'description' => 'You will find on this page, all the required actions to keep your Lychee installation running smooth and nicely.', + 'cleaning' => [ + 'title' => 'Cleaning %s', + 'result' => '%s deleted.', + 'description' => 'Remove all contents from %s', + 'button' => 'Clean', + ], + 'fix-jobs' => [ + 'title' => 'Fixing Jobs History', + 'description' => 'Mark jobs with status %s or %s as %s.', + 'button' => 'Fix job history', + ], + 'gen-sizevariants' => [ + 'title' => 'Missing %s', + 'description' => 'Found %d %s that could be generated.', + 'button' => 'Generate!', + 'success' => 'Successfully generated %d %s.', + ], + 'fill-filesize-sizevariants' => [ + 'title' => 'File sizes missing', + 'description' => 'Found %d small variants without file size.', + 'button' => 'Fetch data!', + 'success' => 'Successfully computed sizes of %d small variants.', + ], + 'fix-tree' => [ + 'title' => 'Tree statistics', + 'Oddness' => 'Oddness', + 'Duplicates' => 'Duplicates', + 'Wrong parents' => 'Wrong parents', + 'Missing parents' => 'Missing parents', + 'button' => 'Fix tree', + ], + 'optimize' => [ + 'title' => 'Optimize Database', + 'description' => 'If you notice slowdown in your installation, it may be because your database does not + have all its needed index.', + 'button' => 'Optimize Database', + ], + 'update' => [ + 'title' => 'Updates', + 'check-button' => 'Check for updates', + 'update-button' => 'Update', + 'no-pending-updates' => 'No pending update.', + ], +]; \ No newline at end of file diff --git a/lang/vi/profile.php b/lang/vi/profile.php new file mode 100644 index 00000000000..cc24b97452c --- /dev/null +++ b/lang/vi/profile.php @@ -0,0 +1,64 @@ + 'Profile', + + 'login' => [ + 'header' => 'Profile', + 'enter_current_password' => 'Enter your current password:', + 'current_password' => 'Current password', + 'credentials_update' => 'Your credentials will be changed to the following:', + 'username' => 'Username', + 'new_password' => 'New password', + 'confirm_new_password' => 'Confirm new password', + 'email_instruction' => 'Add your email below to enable receiving email notifications. To stop receiving emails, simply remove your email below.', + 'email' => 'Email', + 'change' => 'Change Login', + 'api_token' => 'API Token ...', + + 'missing_fields' => 'Missing fields', + ], + + 'token' => [ + 'unavailable' => 'You have already viewed this token.', + 'no_data' => 'No token API have been generated.', + 'disable' => 'Disable', + 'disabled' => 'Token disabled', + 'warning' => 'This token will not be displayed again. Copy it and keep it in a safe place.', + 'reset' => 'Reset the token', + 'create' => 'Create a new token', + ], + + 'oauth' => [ + 'header' => 'OAuth', + 'header_not_available' => 'OAuth is not available', + 'setup_env' => 'Set up the credentials in your .env', + 'token_registered' => '%s token registered.', + 'setup' => 'Set up %s', + 'reset' => 'reset', + 'credential_deleted' => 'Credential deleted!', + ], + + 'u2f' => [ + 'header' => 'Passkey/MFA/2FA', + 'info' => 'This only provides the ability to use WebAuthn to authenticate instead of username & password.', + 'empty' => 'Credentials list is empty!', + 'not_secure' => 'Environment not secured. U2F not available.', + 'new' => 'Register new device.', + 'credential_deleted' => 'Credential deleted!', + 'credential_updated' => 'Credential updated!', + 'credential_registred' => 'Registration successful!', + '5_chars' => 'At least 5 chars.', + ], +]; \ No newline at end of file diff --git a/lang/vi/settings.php b/lang/vi/settings.php new file mode 100644 index 00000000000..fd197f11135 --- /dev/null +++ b/lang/vi/settings.php @@ -0,0 +1,92 @@ + 'Settings', + 'small_screen' => 'For better a experience on the Settings page,
we recommend you use a larger screen.', + 'tabs' => [ + 'basic' => 'Basic', + 'all_settings' => 'All settings', + ], + 'toasts' => [ + 'change_saved' => 'Change saved!', + 'details' => 'Settings have been modified as per request', + 'error' => 'Error!', + 'error_load_css' => 'Could not load dist/user.css', + 'error_load_js' => 'Could not load dist/custom.js', + 'error_save_css' => 'Could not save CSS', + 'error_save_js' => 'Could not save JS', + 'thank_you' => 'Thank you for your support.', + 'reload' => 'Reload your page for full functionalities.', + ], + 'system' => [ + 'header' => 'System', + 'use_dark_mode' => 'Use dark mode for Lychee', + 'language' => 'Language used by Lychee', + 'nsfw_album_visibility' => 'Make Sensitive albums visible by default.', + 'nsfw_album_explanation' => 'If the album is public, it is still accessible, just hidden from the view and can be revealed by pressing H.', + ], + 'lychee_se' => [ + 'header' => 'Lychee SE', + 'call4action' => 'Get exclusive features and support the development of Lychee. Unlock the SE edition.', + 'preview' => 'Enable preview of Lychee SE features', + 'hide_call4action' => 'Hide this Lychee SE registration form. I am happy with Lychee as-is. :)', + 'hide_warning' => 'If enabled, the only way to register your license key will be via the More tab above. Changes are applied on page reload.', + ], + 'dropbox' => [ + 'header' => 'Dropbox', + 'instruction' => 'In order to import photos from your Dropbox, you need a valid drop-ins app key from their website.', + 'api_key' => 'Dropbox API Key', + 'set_key' => 'Set Dropbox Key', + ], + 'gallery' => [ + 'header' => 'Gallery', + 'photo_order_column' => 'Default column used for sorting photos', + 'photo_order_direction' => 'Default order used for sorting photos', + 'album_order_column' => 'Default column used for sorting albums', + 'album_order_direction' => 'Default order used for sorting albums', + 'aspect_ratio' => 'Default aspect ratio for album thumbs', + 'photo_layout' => 'Layout for pictures', + 'album_decoration' => 'Show decorations on album cover (sub-album and/or photo count)', + 'album_decoration_direction' => 'Align album decorations horizontally or vertically', + 'photo_overlay' => 'Default image overlay information', + 'license_default' => 'Default license used for albums', + 'license_help' => 'Need help choosing?', + ], + 'geolocation' => [ + 'header' => 'Geo-location', + 'map_display' => 'Display the map given GPS coordinates', + 'map_display_public' => 'Allow anonymous users to access the map', + 'map_provider' => 'Defines the map provider', + 'map_include_subalbums' => 'Includes pictures of the sub albums on the map', + 'location_decoding' => 'Use GPS location decoding', + 'location_show' => 'Show location extracted from GPS coordinates', + 'location_show_public' => 'Anonymous users can access the extracted location from GPS coordinates', + ], + 'advanced' => [ + 'header' => 'Advanced Customization', + 'change_css' => 'Change CSS', + 'change_js' => 'Change JS', + ], + 'all' => [ + 'old_setting_style' => 'Old setting style', + 'change_detected' => 'Some settings changed.', + 'save' => 'Save', + ], + + 'tool_option' => [ + 'disabled' => 'disabled', + 'enabled' => 'enabled', + 'discover' => 'discover', + ], +]; \ No newline at end of file diff --git a/lang/vi/sharing.php b/lang/vi/sharing.php new file mode 100644 index 00000000000..69de18cc6d0 --- /dev/null +++ b/lang/vi/sharing.php @@ -0,0 +1,33 @@ + 'Sharing', + + 'info' => 'This page gives an overview of and the ability to edit the sharing rights associated with albums.', + 'album_title' => 'Album title', + 'username' => 'Username', + 'no_data' => 'Sharing list is empty.', + 'share' => 'Share', + 'permission_deleted' => 'Permission deleted!', + 'permission_created' => 'Permission created!', + + 'grants' => [ + 'read' => 'Grants read access', + 'original' => 'Grants access to original photo', + 'download' => 'Grants download', + 'upload' => 'Grants upload', + 'edit' => 'Grants edit', + 'delete' => 'Grants delete', + ], +]; \ No newline at end of file diff --git a/lang/vi/statistics.php b/lang/vi/statistics.php new file mode 100644 index 00000000000..2baf855bbd5 --- /dev/null +++ b/lang/vi/statistics.php @@ -0,0 +1,34 @@ + 'Statistics', + + 'preview_text' => 'This is a preview of the statistics page available in Lychee SE.
The data shown here are randomly generated and do not reflect your server.', + 'no_data' => 'User does not have data on server.', + 'collapse' => 'Collapse albums sizes', + + 'total' => [ + 'total' => 'Total', + 'albums' => 'Albums', + 'photos' => 'Photos', + 'size' => 'Size', + ], + 'table' => [ + 'username' => 'Owner', + 'title' => 'Title', + 'photos' => 'Photos', + 'descendants' => 'Children', + 'size' => 'Size', + ], +]; \ No newline at end of file diff --git a/lang/vi/toasts.php b/lang/vi/toasts.php new file mode 100644 index 00000000000..293d4b72594 --- /dev/null +++ b/lang/vi/toasts.php @@ -0,0 +1,17 @@ + 'Error', + 'success' => 'Success', +]; \ No newline at end of file diff --git a/lang/vi/users.php b/lang/vi/users.php new file mode 100644 index 00000000000..599bb833454 --- /dev/null +++ b/lang/vi/users.php @@ -0,0 +1,44 @@ + 'Users', + 'description' => 'Here you can manage the users of your Lychee installation. You can create, edit and delete users.', + 'create' => 'Create a new user', + 'username' => 'Username', + 'password' => 'Password', + 'legend' => 'Legend', + 'upload_rights' => 'When selected, the user can upload content.', + 'edit_rights' => 'When selected, the user can modify their profile (username, password).', + 'quota' => 'When set, the user has a space quota for pictures (in kB).', + + 'user_deleted' => 'User deleted', + 'user_created' => 'User created', + 'user_updated' => 'User updated', + 'change_saved' => 'Change saved!', + + 'create_edit' => [ + 'upload_rights' => 'User can upload content.', + 'edit_rights' => 'User can modify their profile (username, password).', + 'quota' => 'User has quota limit.', + 'quota_kb' => 'quota in kB (0 for default)', + 'note' => 'Admin note (not publically visible)', + 'create' => 'Create', + 'edit' => 'Edit', + ], + 'line' => [ + 'admin' => 'admin user', + 'edit' => 'Edit', + 'delete' => 'Delete', + ], +]; \ No newline at end of file diff --git a/lang/zh_CN/aspect_ratio.php b/lang/zh_CN/aspect_ratio.php new file mode 100644 index 00000000000..2c7e8fb56ac --- /dev/null +++ b/lang/zh_CN/aspect_ratio.php @@ -0,0 +1,21 @@ + '5/4 (instagram landscape)', + '4by5' => '4/5 (instagram portrait)', + '2by3' => '2/3 (portrait)', + '3by2' => '3/2 (landscape)', + '1by1' => 'square', + '1byx9' => '16/9 (landscape)', +]; \ No newline at end of file diff --git a/lang/zh_CN/diagnostics.php b/lang/zh_CN/diagnostics.php new file mode 100644 index 00000000000..0fadd640428 --- /dev/null +++ b/lang/zh_CN/diagnostics.php @@ -0,0 +1,30 @@ + 'Diagnostics', + + 'copy_to_clipboard' => 'Copy diagnostics to clipboard', + 'self-diagnosis' => 'Self-diagnosis', + 'info' => 'Info', + 'space' => 'Space', + 'load_space' => 'Load space usage.', + 'configuration' => 'Configuration', + 'loading' => 'Loading...', + 'identical_content' => 'Identical content', + + 'toast' => [ + 'info' => 'Info', + 'copy' => 'Diagnostics copied to clipboard!', + ], +]; \ No newline at end of file diff --git a/lang/zh_CN/dialogs.php b/lang/zh_CN/dialogs.php new file mode 100644 index 00000000000..4afd65fae3f --- /dev/null +++ b/lang/zh_CN/dialogs.php @@ -0,0 +1,221 @@ + [ + 'close' => 'Close', + 'cancel' => 'Cancel', + 'save' => 'Save', + 'delete' => 'Delete', + 'move' => 'Move', + ], + 'about' => [ + 'subtitle' => 'Self-hosted photo-management done right', + 'description' => 'Lychee is a free photo-management tool, which runs on your server or web-space. Installing is a matter of seconds. Upload, manage and share photos like from a native application. Lychee comes with everything you need and all your photos are stored securely.', + 'update_available' => 'Update available!', + 'thank_you' => 'Thank you for your support!', + 'get_supporter_or_register' => 'Get exclusive features and support the development of Lychee.
Unlock the Supporter Edition or register your License key', + 'here' => 'here', + ], + 'dropbox' => [ + 'not_configured' => 'Dropbox is not configured.', + ], + 'import_from_link' => [ + 'instructions' => 'Please enter the direct link to a photo to import it:', + 'import' => 'Import', + ], + 'keybindings' => [ + 'don_t_show_again' => 'Don\'t show this again', + 'side_wide' => 'Site-wide Shortcuts', + 'back_cancel' => 'Back/Cancel', + 'confirm' => 'Confirm', + 'login' => 'Login', + 'toggle_full_screen' => 'Toggle Full Screen', + 'toggle_sensitive_albums' => 'Toggle Sensitive Albums', + + 'albums' => 'Albums Shortcuts', + 'new_album' => 'New Album', + 'upload_photos' => 'Upload Photos', + 'search' => 'Search', + 'show_this_modal' => 'Show this modal', + 'select_all' => 'Select All', + 'move_selection' => 'Move Selection', + 'delete_selection' => 'Delete Selection', + + 'album' => 'Album Shortcuts', + 'slideshow' => 'Start/Stop Slideshow', + 'toggle' => 'Toggle panel', + + 'photo' => 'Photo Shortcuts', + 'previous' => 'Previous photo', + 'next' => 'Next photo', + 'cycle' => 'Cycle overlay mode', + 'star' => 'Star the photo', + 'move' => 'Move the photo', + 'delete' => 'Delete the photo', + 'edit' => 'Edit information', + 'show_hide_meta' => 'Show information', + + 'keep_hidden' => 'We will keep it hidden.', + ], + 'login' => [ + 'username' => 'Username', + 'password' => 'Password', + 'unknown_invalid' => 'Unknown user or invalid password.', + 'signin' => 'Sign-In', + ], + 'register' => [ + 'enter_license' => 'Enter your license key below:', + 'license_key' => 'License key', + 'invalid_license' => 'Invalid license key.', + 'register' => 'Register', + ], + 'share_album' => [ + 'url_copied' => 'Copied URL to clipboard!', + ], + 'upload' => [ + 'completed' => 'Completed', + 'uploaded' => 'Uploaded:', + 'release' => 'Release file to upload!', + 'select' => 'Click here to select files to upload', + 'drag' => '(Or drag files to the page)', + 'loading' => 'Loading', + 'resume' => 'Resume', + 'uploading' => 'Uploading', + 'finished' => 'Finished', + 'failed_error' => 'Upload failed. The server returned an error!', + ], + 'visibility' => [ + 'public' => 'Public', + 'public_expl' => 'Anonymous users can access this album, subject to the restrictions below.', + 'full' => 'Original', + 'full_expl' => 'Anonymous users can view full-resolution photos.', + 'hidden' => 'Hidden', + 'hidden_expl' => 'Anonymous users need a direct link to access this album.', + 'downloadable' => 'Downloadable', + 'downloadable_expl' => 'Anonymous users can download this album.', + 'password' => 'Password', + 'password_prot' => 'Password protected', + 'password_prot_expl' => 'Anonymous users need a shared password to access this album.', + 'nsfw' => 'Sensitive', + 'nsfw_expl' => 'Album contains sensitive content.', + 'visibility_updated' => 'Visibility updated.', + ], + 'move_album' => [ + 'confirm_single' => 'Are you sure you want to move the album “%1$s” into the album “%2$s”?', + 'confirm_multiple' => 'Are you sure you want to move all selected albums into the album “%s”?', + 'move_single' => 'Move Album', + 'move_to' => 'Move to', + 'move_to_single' => 'Move %s to:', + 'move_to_multiple' => 'Move %d albums to:', + 'no_album_target' => 'No album to move to', + 'moved_single' => 'Album moved!', + 'moved_single_details' => '%1$s moved to %2$s', + 'moved_details' => 'Album(s) moved to %s', + ], + 'new_album' => [ + 'menu' => 'Create Album', + 'info' => 'Enter a title for the new album:', + 'title' => 'title', + 'create' => 'Create Album', + ], + 'new_tag_album' => [ + 'menu' => 'Create Tag Album', + 'info' => 'Enter a title for the new tag album:', + 'title' => 'title', + 'set_tags' => 'Set tags to show', + 'warn' => 'Make sure to press enter after each tag', + 'create' => 'Create Tag Album', + ], + 'delete_album' => [ + 'confirmation' => 'Are you sure you want to delete the album “%s” and all of the photos it contains?', + 'confirmation_multiple' => 'Are you sure you want to delete all %d selected albums and all of the photos they contain?', + 'warning' => 'This action can not be undone!', + 'delete' => 'Delete Album and Photos', + ], + 'transfer' => [ + 'query' => 'Transfer ownership of album to', + 'confirmation' => 'Are you sure you want to transfer the ownership of album “%s” and all the photos it contains to "%s"?', + 'lost_access_warning' => 'Your access to this album will be lost.', + 'warning' => 'This action can not be undone!', + 'transfer' => 'Transfer ownership of album and photos', + ], + 'rename' => [ + 'photo' => 'Enter a new title for this photo:', + 'album' => 'Enter a new title for this album:', + 'rename' => 'Rename', + ], + 'merge' => [ + 'merge_to' => 'Merge %s to:', + 'merge_to_multiple' => 'Merge %d albums to:', + 'no_albums' => 'No albums to merge to.', + 'confirm' => 'Are you sure you want to merge the album “%1$s” into the album “%2$s”?', + 'confirm_multiple' => 'Are you sure you want to merge all selected albums into the album “%s”?', + 'merge' => 'Merge Albums', + 'merged' => 'Album(s) merged to %s!', + ], + 'unlock' => [ + 'password_required' => 'This album is protected by a password. Enter the password below to view the photos of this album:', + 'password' => 'Password', + 'unlock' => 'Unlock', + ], + 'photo_tags' => [ + 'question' => 'Enter your tags for this photo.', + 'question_multiple' => 'Enter your tags for all %d selected photos. Existing tags will be overwritten.', + 'no_tags' => 'No Tags', + 'set_tags' => 'Set Tags', + 'updated' => 'Tags updated!', + 'tags_override_info' => 'If this is unchecked, the tags will be added to the existing tags of the photo.', + ], + 'photo_copy' => [ + 'no_albums' => 'No albums to copy to', + 'copy_to' => 'Copy %s to:', + 'copy_to_multiple' => 'Copy %d photos to:', + 'confirm' => 'Copy %s to %s.', + 'confirm_multiple' => 'Copy %d photos to %s.', + 'copy' => 'Copy', + 'copied' => 'Photo(s) copied!', + ], + 'photo_delete' => [ + 'confirm' => 'Are you sure you want to delete the photo “%s”?', + 'confirm_multiple' => 'Are you sure you want to delete all %d selected photos?', + 'deleted' => 'Photo(s) deleted!', + ], + 'move_photo' => [ + 'move_single' => 'Move %s to:', + 'move_multiple' => 'Move %d photos to:', + 'confirm' => 'Move %s to %s.', + 'confirm_multiple' => 'Move %d photos to %s.', + 'moved' => 'Photo(s) moved to %s!', + ], + 'target_user' => [ + 'placeholder' => 'Select user', + ], + 'target_album' => [ + 'placeholder' => 'Select album', + ], + 'webauthn' => [ + 'u2f' => 'U2F', + 'success' => 'Authentication successful!', + 'error' => 'Whoops, it looks like something went wrong. Please reload the site and try again!', + ], + 'se' => [ + 'available' => 'Available in the Supporter Edition', + ], + 'session_expired' => [ + 'title' => 'Session expired', + 'message' => 'Your session has expired.
Please reload the page.', + 'reload' => 'Reload', + 'go_to_gallery' => 'Go to the Gallery', + ], +]; \ No newline at end of file diff --git a/lang/zh_CN/fix-tree.php b/lang/zh_CN/fix-tree.php new file mode 100644 index 00000000000..64803e310e6 --- /dev/null +++ b/lang/zh_CN/fix-tree.php @@ -0,0 +1,55 @@ + 'Maintenance', + 'intro' => 'This page allows you to re-order and fix your albums manually.
Before any modifications, we strongly recommend you to read about Nested Set tree structures.', + 'warning' => 'You can really break your Lychee installation here, modify values at your own risks.', + + 'help' => [ + 'header' => 'Help', + 'hover' => 'Hover ids or titles to highlight related albums.', + 'left' => 'Left', + 'right' => 'Right', + 'convenience' => 'For your convenience, the and buttons allow you to change the values of %s and %s by respectively +1 and -1 with propagation.', + 'left-right-warn' => 'The and indicates that the value of %s (and respectively %s) is duplicated somewhere.', + 'parent-marked' => 'Marked Parent Id indicates that the %s and %s do not satisfy the Nest Set tree structures. Edit either the Parent Id or the %s/%s values.', + 'slowness' => 'This page will be slow with a large number of albums.', + ], + + 'buttons' => [ + 'reset' => 'Reset', + 'check' => 'Check', + 'apply' => 'Apply', + ], + + 'table' => [ + 'title' => 'Title', + 'left' => 'Left', + 'right' => 'Right', + 'id' => 'Id', + 'parent' => 'Parent Id', + ], + + 'errors' => [ + 'invalid' => 'Invalid tree!', + 'invalid_details' => 'We are not applying this as it is guaranteed to be a broken state.', + 'invalid_left' => 'Album %s has an invalid left value.', + 'invalid_right' => 'Album %s has an invalid right value.', + 'invalid_left_right' => 'Album %s has an invalid left/right values. Left should be strictly smaller than right: %s < %s.', + 'duplicate_left' => 'Album %s has a duplicate left value %s.', + 'duplicate_right' => 'Album %s has a duplicate right value %s.', + 'parent' => 'Album %s has an unexpected parent id %s.', + 'unknown' => 'Album %s has an unknown error.', + ], +]; \ No newline at end of file diff --git a/lang/zh_CN/gallery.php b/lang/zh_CN/gallery.php new file mode 100644 index 00000000000..eb8008827e0 --- /dev/null +++ b/lang/zh_CN/gallery.php @@ -0,0 +1,241 @@ + 'Gallery', + + 'smart_albums' => 'Smart albums', + 'albums' => 'Albums', + 'root' => 'Albums', + + 'original' => 'Original', + 'medium' => 'Medium', + 'medium_hidpi' => 'Medium HiDPI', + 'small' => 'Thumb', + 'small_hidpi' => 'Thumb HiDPI', + 'thumb' => 'Square thumb', + 'thumb_hidpi' => 'Square thumb HiDPI', + 'placeholder' => 'Low Quality Image Placeholder', + 'thumbnail' => 'Photo thumbnail', + 'live_video' => 'Video part of live-photo', + + 'camera_data' => 'Camera date', + 'album_reserved' => 'All Rights Reserved', + + 'map' => [ + 'error_gpx' => 'Error loading GPX file', + 'osm_contributors' => 'OpenStreetMap contributors', + ], + + 'search' => [ + 'title' => 'Search', + 'searching' => 'Searching…', + 'no_results' => 'Nothing matches your search query.', + 'searchbox' => 'Search…', + 'minimum_chars' => 'Minimum %s characters required.', + 'photos' => 'Photos (%s)', + 'albums' => 'Albums (%s)', + ], + + 'smart_album' => [ + 'unsorted' => 'Unsorted', + 'starred' => 'Starred', + 'recent' => 'Recent', + 'public' => 'Public', + 'on_this_day' => 'On This Day', + ], + + 'layout' => [ + 'squares' => 'Square thumbnails', + 'justified' => 'With aspect, justified', + 'masonry' => 'With aspect, masonry', + 'grid' => 'With aspect, grid', + ], + + 'overlay' => [ + 'none' => 'None', + 'exif' => 'EXIF data', + 'description' => 'Description', + 'date' => 'Date taken', + ], + + 'timeline' => [ + 'default' => 'default', + 'disabled' => 'disabled', + 'year' => 'Year', + 'month' => 'Month', + 'day' => 'Day', + 'hour' => 'Hour', + ], + + 'album' => [ + 'header_albums' => 'Albums', + 'header_photos' => 'Photos', + 'no_results' => 'Nothing to see here', + 'upload' => 'Upload photos', + + 'tabs' => [ + 'about' => 'About Album', + 'share' => 'Share Album', + 'move' => 'Move Album', + 'danger' => 'DANGER ZONE', + ], + + 'hero' => [ + 'created' => 'Created', + 'copyright' => 'Copyright', + 'subalbums' => 'Subalbums', + 'images' => 'Photos', + 'download' => 'Download Album', + 'share' => 'Share Album', + 'stats_only_se' => 'Statistics available in the Supporter Edition', + ], + + 'stats' => [ + 'lens' => 'Lens', + 'shutter' => 'Shutter speed', + 'iso' => 'ISO', + 'model' => 'Model', + 'aperture' => 'Aperture', + 'no_data' => 'No data', + ], + + 'properties' => [ + 'title' => 'Title', + 'description' => 'Description', + 'photo_ordering' => 'Order photos by', + 'children_ordering' => 'Order albums by', + 'asc/desc' => 'asc/desc', + 'header' => 'Set album header', + 'compact_header' => 'Use compact header', + 'license' => 'Set license', + 'copyright' => 'Set copyright', + 'aspect_ratio' => 'Set album thumbs aspect ratio', + 'album_timeline' => 'Set album timeline mode', + 'photo_timeline' => 'Set photo timeline mode', + 'layout' => 'Set photo layout', + 'show_tags' => 'Set tags to show', + 'tags_required' => 'Tags are required.', + ], + ], + + 'photo' => [ + 'actions' => [ + 'star' => 'Star', + 'unstar' => 'Unstar', + 'set_album_header' => 'Set as album header', + 'move' => 'Move', + 'delete' => 'Delete', + 'header_set' => 'Header set', + ], + + 'details' => [ + 'about' => 'About', + 'basics' => 'Basics', + 'title' => 'Title', + 'uploaded' => 'Uploaded', + 'description' => 'Description', + 'license' => 'License', + 'reuse' => 'Reuse', + 'latitude' => 'Latitude', + 'longitude' => 'Longitude', + 'altitude' => 'Altitude', + 'location' => 'Location', + 'image' => 'Image', + 'video' => 'Video', + 'size' => 'Size', + 'format' => 'Format', + 'resolution' => 'Resolution', + 'duration' => 'Duration', + 'fps' => 'Frame rate', + 'tags' => 'Tags', + 'camera' => 'Camera', + 'captured' => 'Captured', + 'make' => 'Make', + 'type' => 'Type/Model', + 'lens' => 'Lens', + 'shutter' => 'Shutter Speed', + 'aperture' => 'Aperture', + 'focal' => 'Focal Length', + 'iso' => 'ISO %s', + ], + + 'edit' => [ + 'set_title' => 'Set Title', + 'set_description' => 'Set Description', + 'set_license' => 'Set License', + 'no_tags' => 'No Tags', + 'set_tags' => 'Set Tags', + 'set_created_at' => 'Set Upload Date', + ], + ], + + 'nsfw' => [ + 'header' => 'Sensitive content', + 'description' => 'This album contains sensitive content which some people may find offensive or disturbing.', + 'consent' => 'Tap to consent.', + ], + + 'menus' => [ + 'star' => 'Star', + 'unstar' => 'Unstar', + 'star_all' => 'Star Selected', + 'unstar_all' => 'Unstar Selected', + 'tag' => 'Tag', + 'tag_all' => 'Tag Selected', + 'set_cover' => 'Set Album Cover', + 'remove_header' => 'Remove Album Header', + 'set_header' => 'Set Album Header', + 'copy_to' => 'Copy to …', + 'copy_all_to' => 'Copy Selected to …', + 'rename' => 'Rename', + 'move' => 'Move', + 'move_all' => 'Move Selected', + 'delete' => 'Delete', + 'delete_all' => 'Delete Selected', + 'download' => 'Download', + 'download_all' => 'Download Selected', + 'merge' => 'Merge', + 'merge_all' => 'Merge Selected', + + 'upload_photo' => 'Upload Photo', + 'import_link' => 'Import from Link', + 'import_dropbox' => 'Import from Dropbox', + 'new_album' => 'New Album', + 'new_tag_album' => 'New Tag Album', + 'upload_track' => 'Upload track', + 'delete_track' => 'Delete track', + ], + + 'sort' => [ + 'photo_select_1' => 'Upload Time', + 'photo_select_2' => 'Take Date', + 'photo_select_3' => 'Title', + 'photo_select_4' => 'Description', + 'photo_select_6' => 'Star', + 'photo_select_7' => 'Photo Format', + 'ascending' => 'Ascending', + 'descending' => 'Descending', + 'album_select_1' => 'Creation Time', + 'album_select_2' => 'Title', + 'album_select_3' => 'Description', + 'album_select_5' => 'Latest Take Date', + 'album_select_6' => 'Oldest Take Date', + ], + + 'albums_protection' => [ + 'private' => 'private', + 'public' => 'public', + 'inherit_from_parent' => 'inherit from parent', + ], +]; \ No newline at end of file diff --git a/lang/zh_CN/jobs.php b/lang/zh_CN/jobs.php new file mode 100644 index 00000000000..5d952b76012 --- /dev/null +++ b/lang/zh_CN/jobs.php @@ -0,0 +1,18 @@ + 'Jobs', + + 'no_data' => 'No Jobs have been executed yet.', +]; \ No newline at end of file diff --git a/lang/zh_CN/landing.php b/lang/zh_CN/landing.php new file mode 100644 index 00000000000..fe6fe55b8ea --- /dev/null +++ b/lang/zh_CN/landing.php @@ -0,0 +1,19 @@ + 'Gallery', + 'access_gallery' => 'Access the gallery', + 'hosted_with_lychee' => 'Hosted with Lychee', + 'copyright' => 'All images on this website are subject to copyright by %1$s © %2$s', +]; \ No newline at end of file diff --git a/lang/zh_CN/left-menu.php b/lang/zh_CN/left-menu.php new file mode 100644 index 00000000000..9a3e91f4037 --- /dev/null +++ b/lang/zh_CN/left-menu.php @@ -0,0 +1,29 @@ + 'Back to Gallery', + + 'admin' => 'Admin', + 'clockwork' => 'Clockwork App', + 'logs' => 'Show Logs', + 'jobs' => 'Show Job History', + 'user' => 'User', + + 'sign_out' => 'Sign Out', + + 'about' => 'About', + 'api' => 'API Documentation', + 'source_code' => 'Source Code', + 'support' => 'Support', +]; \ No newline at end of file diff --git a/lang/zh_CN/lychee.php b/lang/zh_CN/lychee.php new file mode 100644 index 00000000000..0d0e0d3aaae --- /dev/null +++ b/lang/zh_CN/lychee.php @@ -0,0 +1,535 @@ + '用户名', + 'PASSWORD' => '密码', + 'ENTER' => '确定', + 'CANCEL' => '取消', + 'CONFIRM' => '确认', + 'SIGN_IN' => '登录', + 'CLOSE' => '关闭', + 'SETTINGS' => '设置', + 'SEARCH' => '搜索 …', + 'MORE' => '更多', + 'DEFAULT' => '默认', + 'GALLERY' => '图库', + + 'USERS' => '用户', + 'PROFILE' => '账户设置', + 'CREATE' => '创建', + 'REMOVE' => '移除', + 'SHARE' => '分享', + 'U2F' => '通用两步验证(U2F)', + 'NOTIFICATIONS' => '通知', + 'SHARING' => '共享', + 'CHANGE_LOGIN' => '修改登录信息', + 'CHANGE_SORTING' => '修改排序', + 'SET_DROPBOX' => '设置 Dropbox', + 'ABOUT_LYCHEE' => '关于 Lychee', + 'DIAGNOSTICS' => '诊断', + 'DIAGNOSTICS_GET_SIZE' => '请求空间占用信息', + 'JOBS' => '操作历史', + 'LOGS' => '查看日志', + 'SIGN_OUT' => '注销登录', + 'UPDATE_AVAILABLE' => '可用更新!', + 'MIGRATION_AVAILABLE' => '可用迁移!', + 'CHECK_FOR_UPDATE' => '检查更新', + 'DEFAULT_LICENSE' => '为新上传设置默认许可证:', + 'SET_LICENSE' => '设置许可证', + 'SET_OVERLAY_TYPE' => '设置叠层', + 'SET_ALBUM_DECORATION' => 'Set album decorations', + 'SET_MAP_PROVIDER' => '设置 OpenStreetMap 图层提供者', + 'FULL_SETTINGS' => 'Full Settings', + 'UPDATE' => 'Update', + 'RESET' => 'Reset', + 'DISABLE_TOKEN_TOOLTIP' => '禁用', + 'ENABLE_TOKEN' => '启用 API token', + 'DISABLED_TOKEN_STATUS_MSG' => '已禁用', + 'TOKEN_BUTTON' => 'API Token ...', + 'TOKEN_NOT_AVAILABLE' => '你已经看过了这个token', + 'TOKEN_WAIT' => '请等待 ...', + + 'SMART_ALBUMS' => '智能相册', + 'SHARED_ALBUMS' => '已共享的相册', + 'ALBUMS' => '相册', + 'PHOTOS' => '照片', + 'SEARCH_RESULTS' => '搜索结果', + + 'RENAME' => '重命名', + 'RENAME_ALL' => '重命名已选中', + 'MERGE' => '合并', + 'MERGE_ALL' => '合并选中', + 'MAKE_PUBLIC' => '设为公开', + 'SHARE_ALBUM' => '分享相册', + 'SHARE_PHOTO' => '分享照片', + 'VISIBILITY_ALBUM' => '相册可见性', + 'VISIBILITY_PHOTO' => '照片可见性', + 'DOWNLOAD_ALBUM' => '下载相册', + 'ABOUT_ALBUM' => '关于相册', + 'DELETE_ALBUM' => '删除相册', + 'MOVE_ALBUM' => '移动相册', + 'FULLSCREEN_ENTER' => '进入全屏幕', + 'FULLSCREEN_EXIT' => '退出全屏幕', + + 'SHARING_ALBUM_USERS' => '与用户共享此相册', + 'WAIT_FETCH_DATA' => '正在获取数据,请稍候 …', + 'SHARING_ALBUM_USERS_NO_USERS' => '没有可与之共享相册的用户', + 'SHARING_ALBUM_USERS_LONG_MESSAGE' => '选择要与之共享此相册的用户', + + 'DELETE_ALBUM_QUESTION' => '删除相册和照片', + 'KEEP_ALBUM' => '保留相册', + 'DELETE_ALBUM_CONFIRMATION' => '是否确认删除相册《%s》以及相册中包含的所有照片?操作后不可恢复!', + + 'DELETE_TAG_ALBUM_QUESTION' => '删除相册', + 'DELETE_TAG_ALBUM_CONFIRMATION' => '是否确认删除相册《%s》以及相册中包含的所有照片?操作后不可恢复!', + + 'DELETE_ALBUMS_QUESTION' => '删除相册和照片', + 'KEEP_ALBUMS' => '保留相册', + 'DELETE_ALBUMS_CONFIRMATION' => '是否确认删除全部 %d 选中的相册和其中的所有照片?操作后不可恢复!', + + 'DELETE_UNSORTED_CONFIRM' => '是否确认删除《未分类》的所有照片?操作后不可恢复!', + 'CLEAR_UNSORTED' => '清除未分类', + 'KEEP_UNSORTED' => '保留未分类', + + 'EDIT_SHARING' => '编辑共享', + 'MAKE_PRIVATE' => '设为私有', + + 'CLOSE_ALBUM' => '关闭相册', + 'CLOSE_PHOTO' => '关闭照片', + 'CLOSE_MAP' => '关闭地图', + + 'ADD' => '添加', + 'MOVE' => '移动', + 'MOVE_ALL' => '移动选中', + 'DUPLICATE' => '创建副本', + 'DUPLICATE_ALL' => '复制选定的', + 'COPY_TO' => '复制到 …', + 'COPY_ALL_TO' => '选定副本到 …', + 'DELETE' => '删除', + 'SAVE' => '保存', + 'DELETE_ALL' => '删除已选中', + 'DOWNLOAD' => '下载', + 'DOWNLOAD_ALL' => '下载已选中', + 'UPLOAD_PHOTO' => '上传相片', + 'IMPORT_LINK' => '从链接导入', + 'IMPORT_DROPBOX' => '从 Dropbox 导入', + 'IMPORT_SERVER' => '从服务器导入', + 'NEW_ALBUM' => '新建相册', + 'NEW_TAG_ALBUM' => '新建标签相册', + 'UPLOAD_TRACK' => 'Upload track', + 'DELETE_TRACK' => 'Delete track', + + 'TITLE_NEW_ALBUM' => '输入新相册的标题:', + 'UNTITLED' => '未命名', + 'UNSORTED' => '未分类', + 'STARRED' => '星标', + 'RECENT' => '最新', + 'PUBLIC' => '公开', + 'ON_THIS_DAY' => '历史上的今天', + 'NUM_PHOTOS' => '照片', + + 'CREATE_ALBUM' => '创建相册', + 'CREATE_TAG_ALBUM' => '创建标签相册', + + 'STAR_PHOTO' => '星标此照片', + 'STAR' => '星标', + 'UNSTAR' => '取消星标', + 'STAR_ALL' => '为所选照片加星标', + 'UNSTAR_ALL' => '取消已选照片星标', + 'TAG' => '标签', + 'TAG_ALL' => '为所选照片打标签', + 'UNSTAR_PHOTO' => '取消星标', + 'SET_COVER' => '设置为相册封面', + 'REMOVE_COVER' => '取消设置为相册封面', + 'SET_HEADER' => '设置为相册页眉', + 'REMOVE_HEADER' => '取消设置为相册页眉', + 'SET_COMPACT_HEADER' => '使用紧凑型页眉', + + 'FULL_PHOTO' => '打开原图', + 'ABOUT_PHOTO' => '关于照片', + 'DISPLAY_FULL_MAP' => '地图', + 'DIRECT_LINK' => '直链', + 'DIRECT_LINKS' => '直链', + 'QR_CODE' => 'QR Code', + + 'ALBUM_ABOUT' => '关于', + 'ALBUM_BASICS' => '基本信息', + 'ALBUM_TITLE' => '标题', + 'ALBUM_COPYRIGHT' => 'Copyright', + 'ALBUM_SET_COPYRIGHT' => '设置版权', + 'ALBUM_NEW_TITLE' => '输入新的相册标题:', + 'ALBUMS_NEW_TITLE' => '设置标题为 %d 已选择的相册:', + 'ALBUM_SET_TITLE' => '设置标题', + 'ALBUM_DESCRIPTION' => '描述', + 'ALBUM_SHOW_TAGS' => '要显示的标签', + 'ALBUM_NEW_DESCRIPTION' => '输入新的相册描述:', + 'ALBUM_SET_DESCRIPTION' => '设置描述', + 'ALBUM_NEW_SHOWTAGS' => '输入将在此相册中可见的照片的标签:', + 'ALBUM_SET_SHOWTAGS' => '设置要显示的标签', + 'ALBUM_ALBUM' => '相册', + 'ALBUM_CREATED' => '创建时间', + 'ALBUM_IMAGES' => '图片数', + 'ALBUM_VIDEOS' => '视频数', + 'ALBUM_SUBALBUMS' => '子相册数', + 'ALBUM_SHARING' => '共享', + 'ALBUM_SHR_YES' => '是', + 'ALBUM_SHR_NO' => '否', + 'ALBUM_PUBLIC' => '公开', + 'ALBUM_PUBLIC_EXPL' => '匿名用户可以访问此相册,但须遵守以下限制', + 'ALBUM_FULL' => '原始图像', + 'ALBUM_FULL_EXPL' => '匿名用户可以查看全分辨率照片', + 'ALBUM_HIDDEN' => '隐藏', + 'ALBUM_HIDDEN_EXPL' => '匿名用户需要直接链接才能访问此相册', + 'ALBUM_MARK_NSFW' => '将相册标记为敏感内容', + 'ALBUM_UNMARK_NSFW' => '取消相册的敏感内容标记', + 'ALBUM_NSFW' => '敏感内容', + 'ALBUM_NSFW_EXPL' => '相册被标记为包含敏感内容。', + 'ALBUM_DOWNLOADABLE' => '可下载', + 'ALBUM_DOWNLOADABLE_EXPL' => '匿名用户可以下载此相册', + 'ALBUM_SHARE_BUTTON_VISIBLE' => '分享按钮可见', + 'ALBUM_SHARE_BUTTON_VISIBLE_EXPL' => '匿名用户看到社交媒体共享链接', + 'ALBUM_PASSWORD' => '密码', + 'ALBUM_PASSWORD_PROT' => '受到密码保护', + 'ALBUM_PASSWORD_PROT_EXPL' => '匿名用户需要共享密码才能访问此相册', + 'ALBUM_PASSWORD_REQUIRED' => '此相册受到密码保护。请在下方输入密码以查看相册内的照片:', + 'ALBUM_MERGE' => '你确定要合并相册《%1$s》到相册《%2$s》?', + 'ALBUMS_MERGE' => '你确定要合并所有已选择的相册到相册《%s》?', + 'MERGE_ALBUM' => '合并相册', + 'DONT_MERGE' => '不要合并', + 'ALBUM_MOVE' => '你确定要移动相册《%1$s》到相册《%2$s》?', + 'ALBUMS_MOVE' => '你确定要移动所有已选择的相册到相册《%s》?', + 'MOVE_ALBUMS' => '移动相册', + 'NOT_MOVE_ALBUMS' => '不要移动', + 'ROOT' => '相册', + 'ALBUM_REUSE' => '重用', + 'ALBUM_LICENSE' => '许可证', + 'ALBUM_SET_LICENSE' => '设置许可证', + 'ALBUM_LICENSE_HELP' => '需要有关选择的帮助吗?', + 'ALBUM_LICENSE_NONE' => '无', + 'ALBUM_RESERVED' => '所有权利保留', + 'ALBUM_SET_ORDER' => '设置排序', + 'ALBUM_ORDERING' => '排序依据', + 'ALBUM_PHOTO_ORDERING' => '照片排序方式', + 'ALBUM_CHILDREN_ORDERING' => '相册排序方式', + 'ALBUM_OWNER' => 'Owner', + + 'PHOTO_ABOUT' => '关于', + 'PHOTO_BASICS' => '基本信息', + 'PHOTO_TITLE' => '标题', + 'PHOTO_NEW_TITLE' => '输入新的照片标题:', + 'PHOTO_SET_TITLE' => '设置标题', + 'PHOTO_UPLOADED' => '已上传', + 'PHOTO_DESCRIPTION' => '描述', + 'PHOTO_NEW_DESCRIPTION' => '输入新的照片描述', + 'PHOTO_SET_DESCRIPTION' => '设置描述', + 'PHOTO_NEW_LICENSE' => '添加许可证', + 'PHOTO_SET_LICENSE' => '设置许可证', + 'PHOTO_LICENSE' => '许可证', + 'PHOTO_LICENSE_HELP' => 'Need help choosing?', + 'PHOTO_REUSE' => '重用', + 'PHOTO_LICENSE_NONE' => '无', + 'PHOTO_RESERVED' => '所有权利保留', + 'PHOTO_LATITUDE' => '纬度', + 'PHOTO_LONGITUDE' => '经度', + 'PHOTO_ALTITUDE' => '海拔', + 'PHOTO_IMGDIRECTION' => '方向', + 'PHOTO_LOCATION' => '地点', + 'PHOTO_IMAGE' => '图片信息', + 'PHOTO_VIDEO' => '视频', + 'PHOTO_SIZE' => '大小', + 'PHOTO_FORMAT' => '格式', + 'PHOTO_RESOLUTION' => '分辨率', + 'PHOTO_DURATION' => '时长', + 'PHOTO_FPS' => '帧率', + 'PHOTO_TAGS' => '标签', + 'PHOTO_NOTAGS' => '无标签', + 'PHOTO_NEW_TAGS' => '为该照片添加标签。你可以用逗号分隔多个标签:', + 'PHOTOS_NEW_TAGS' => '设置你的标签于 %d 已选择的照片。已存在的标签会被覆盖。你可以用逗号分隔多个标签:', + 'PHOTO_SET_TAGS' => '设置标签', + 'PHOTO_CAMERA' => '相机信息', + 'PHOTO_CAPTURED' => '拍摄时间', + 'PHOTO_MAKE' => '设备', + 'PHOTO_TYPE' => '类型/型号', + 'PHOTO_LENS' => '镜头', + 'PHOTO_SHUTTER' => '快门速度', + 'PHOTO_APERTURE' => '光圈', + 'PHOTO_FOCAL' => '焦距', + 'PHOTO_ISO' => '感光度 %s', + 'PHOTO_SHARING' => '共享', + 'PHOTO_DELETE' => '删除照片', + 'PHOTO_KEEP' => '保留照片', + 'PHOTO_DELETE_CONFIRMATION' => '是否要删除照片《%s》?此操作不可恢复!', + 'PHOTO_DELETE_ALL' => '是否要删除全部 %d 已选择的照片?此操作不可恢复!', + 'PHOTOS_NEW_TITLE' => '设置照片标题于 %d 已选择的照片:', + 'PHOTO_MAKE_PRIVATE_ALBUM' => '此照片位于公开相册中。要使其私有或公开,请编辑所在相册的可见性。', + 'PHOTO_SHOW_ALBUM' => '显示相册', + 'PHOTO_PUBLIC' => '公开', + 'PHOTO_PUBLIC_EXPL' => '匿名用户可以查看此照片,但须遵守以下限制', + 'PHOTO_FULL' => '原始图像', + 'PHOTO_FULL_EXPL' => '匿名用户可以看到全分辨率照片', + 'PHOTO_HIDDEN' => '隐藏', + 'PHOTO_HIDDEN_EXPL' => '匿名用户需要直接链接才能查看此照片', + 'PHOTO_DOWNLOADABLE' => '可下载', + 'PHOTO_DOWNLOADABLE_EXPL' => '匿名用户可以下载此照片.', + 'PHOTO_SHARE_BUTTON_VISIBLE' => '分享按钮可见', + 'PHOTO_SHARE_BUTTON_VISIBLE_EXPL' => '匿名用户可以看到社交媒体共享链接', + 'PHOTO_PASSWORD_PROT' => '受到密码保护', + 'PHOTO_PASSWORD_PROT_EXPL' => '匿名用户需要共享密码才能查看此照片', + 'PHOTO_EDIT_SHARING_TEXT' => '此照片的共享属性将被修改为:', + 'PHOTO_NO_EDIT_SHARING_TEXT' => '因为此照片位于一个公开相册中,其继承了相册的可见性设置。其当前的可见性仅在下方作为提示的作用而显示。', + 'PHOTO_EDIT_GLOBAL_SHARING_TEXT' => '此照片的可见性可以使用全局的 Lychee 设置进行更细致的调整。其当前的可见性仅在下方作为提示的作用而显示。', + 'PHOTO_NEW_CREATED_AT' => '输入这张图片的上传日期 mm/dd/yyyy, hh:mm [am/pm]', + 'PHOTO_SET_CREATED_AT' => '设置上传日期', + + 'LOADING' => '载入中', + 'ERROR' => '错误', + 'ERROR_TEXT' => '噢,似乎出了一些问题。请刷新页面后再试!', + 'ERROR_UNKNOWN' => '发生未知问题。请再试一次,检查您的安装和服务器。请查看自述文件以获取更多信息。', + 'ERROR_MAP_DEACTIVATED' => '地图功能已在设置中停用。', + 'ERROR_SEARCH_DEACTIVATED' => '搜索功能已在设置中停用。', + 'SUCCESS' => 'OK', + 'CHANGE_SUCCESS' => '修改成功', + 'RETRY' => '重试', + 'OVERRIDE' => 'Override', + 'TAGS_OVERRIDE_INFO' => 'If this is unchecked, the tags will be added to the existing tags of the photo.', + + 'SETTINGS_SUCCESS_LOGIN' => '登录信息已更新.', + 'SETTINGS_SUCCESS_SORT' => '排序顺序已更新。', + 'SETTINGS_SUCCESS_DROPBOX' => 'Dropbox 密钥已更新。', + 'SETTINGS_SUCCESS_LANG' => '语言已更新。', + 'SETTINGS_SUCCESS_LAYOUT' => '布局已更新', + 'SETTINGS_SUCCESS_IMAGE_OVERLAY' => 'EXIF 叠层设置已更新', + 'SETTINGS_SUCCESS_PUBLIC_SEARCH' => '公开搜索已更新', + 'SETTINGS_SUCCESS_LICENSE' => '默认许可证已更新', + 'SETTINGS_SUCCESS_MAP_DISPLAY' => '地图显示设置已更新', + 'SETTINGS_SUCCESS_MAP_DISPLAY_PUBLIC' => '公开相册的地图显示设置已更新', + 'SETTINGS_SUCCESS_MAP_PROVIDER' => '地图供应商设置已更新', + 'SETTINGS_SUCCESS_CSS' => '样式表已更新', + 'SETTINGS_SUCCESS_JS' => 'JS 已更新', + 'SETTINGS_SUCCESS_UPDATE' => '设置更新成功', + 'SETTINGS_DROPBOX_KEY' => 'Dropbox API Key', + 'SETTINGS_ADVANCED_WARNING_EXPL' => '更改这些高级设置可能会损害此应用程序的稳定性、安全性和性能。只有当你确定自己在做什么时,你才应该修改它们。', + 'SETTINGS_ADVANCED_SAVE' => '保存我的修改,我接受风险!', + + 'U2F_NOT_SUPPORTED' => 'U2F 不被支持。 抱歉。', + 'U2F_NOT_SECURE' => '环境不安全。U2F 不可用', + 'U2F_REGISTER_KEY' => '注册新设备。', + 'U2F_REGISTRATION_SUCCESS' => '注册成功!', + 'U2F_AUTHENTIFICATION_SUCCESS' => '认证成功!', + 'U2F_CREDENTIALS' => '认证信息', + 'U2F_CREDENTIALS_DELETED' => '认证信息已删除!', + 'U2F_LOGIN' => 'Log in with WebAuthn', + + 'NEW_PHOTOS_NOTIFICATION' => '发送新照片通知电子邮件', + 'SETTINGS_SUCCESS_NEW_PHOTOS_NOTIFICATION' => '新照片通知设置已更新', + 'USER_EMAIL_INSTRUCTION' => 'Add your email below to enable receiving email notifications. To stop receiving emails, simply remove your email below.', + + 'LOGIN_USERNAME' => '新用户名', + 'LOGIN_PASSWORD' => '新密码', + 'LOGIN_PASSWORD_CONFIRM' => '确认密码', + 'PASSWORD_TITLE' => '输入您当前的密码:', + 'PASSWORD_CURRENT' => '当前密码', + 'PASSWORD_TEXT' => '您的用户名和密码将被修改为:', + 'PASSWORD_CHANGE' => '修改登录信息', + + 'EDIT_SHARING_TITLE' => '编辑共享', + 'EDIT_SHARING_TEXT' => '此相册的共享属性将被修改为:', + 'SHARE_ALBUM_TEXT' => '此相册将会以下列的属性共享:', + + 'SORT_DIALOG_ATTRIBUTE_LABEL' => 'Attribute', + 'SORT_DIALOG_ORDER_LABEL' => 'Order', + + 'SORT_ALBUM_BY' => '相册排序:根据 %1$s 的 %2$s 排序。', + + 'SORT_ALBUM_SELECT_1' => '创建时间', + 'SORT_ALBUM_SELECT_2' => '标题', + 'SORT_ALBUM_SELECT_3' => '描述', + 'SORT_ALBUM_SELECT_5' => '最新', + 'SORT_ALBUM_SELECT_6' => '最老', + + 'SORT_PHOTO_BY' => '照片排序:根据 %1$s 的 %2$s 排序。', + + 'SORT_PHOTO_SELECT_1' => '上传时间', + 'SORT_PHOTO_SELECT_2' => '创建时间', + 'SORT_PHOTO_SELECT_3' => '标题', + 'SORT_PHOTO_SELECT_4' => '描述', + 'SORT_PHOTO_SELECT_6' => '喜欢', + 'SORT_PHOTO_SELECT_7' => '照片格式', + + 'SORT_ASCENDING' => '升序', + 'SORT_DESCENDING' => '降序', + 'SORT_CHANGE' => '修改排序', + + 'DROPBOX_TITLE' => '设置 Dropbox 密钥', + 'DROPBOX_TEXT' => "要从 Dropbox 导入照片,您需要一个有效的插件应用密钥,请转到 他们的网站。为你自己生成个人密钥并输入到下面:", + + 'LANG_TEXT' => '将 Lychee 的语言修改为:', + 'LANG_TITLE' => '修改语言', + + 'SETTING_RECENT_PUBLIC_TEXT' => 'Make "Recent" smart album accessible to anonymous users', + 'SETTING_STARRED_PUBLIC_TEXT' => 'Make "Starred" smart album accessible to anonymous users', + 'SETTING_ONTHISDAY_PUBLIC_TEXT' => 'Make "On This Day" smart album accessible to anonymous users', + + 'CSS_TEXT' => '个性化CSS:', + 'CSS_TITLE' => '修改CSS', + 'JS_TEXT' => '自定义JS:', + 'JS_TITLE' => '修改JS', + 'PUBLIC_SEARCH_TEXT' => '允许公共搜索:', + 'OVERLAY_TYPE' => '用于图像叠层中的数据:', + 'OVERLAY_NONE' => 'No overlay', + 'OVERLAY_EXIF' => '照片 EXIF 数据', + 'OVERLAY_DESCRIPTION' => '照片描述', + 'OVERLAY_DATE' => '照片拍摄日期', + 'ALBUM_DECORATION' => 'Album decorations:', + 'ALBUM_DECORATION_NONE' => 'None', + 'ALBUM_DECORATION_ORIGINAL' => 'Sub-album marker', + 'ALBUM_DECORATION_ALBUM' => 'Number of sub-albums', + 'ALBUM_DECORATION_PHOTO' => 'Number of photos', + 'ALBUM_DECORATION_ALL' => 'Number of sub-albums and photos', + 'ALBUM_DECORATION_ORIENTATION' => 'Orientation of album decorations:', + 'ALBUM_DECORATION_ORIENTATION_ROW' => 'Horizontal (photos, albums)', + 'ALBUM_DECORATION_ORIENTATION_ROW_REVERSE' => 'Horizontal (albums, photos)', + 'ALBUM_DECORATION_ORIENTATION_COLUMN' => 'Vertical (top photos, albums)', + 'ALBUM_DECORATION_ORIENTATION_COLUMN_REVERSE' => 'Vertical (top albums, photos)', + 'MAP_DISPLAY_TEXT' => '启用地图(由 OpenStreetMap 提供):', + 'MAP_DISPLAY_PUBLIC_TEXT' => '为公共相册启用地图(由 OpenStreetMap 提供):', + 'MAP_PROVIDER' => 'OpenStreetMap 图层提供者:', + 'MAP_PROVIDER_WIKIMEDIA' => 'Wikimedia', + 'MAP_PROVIDER_OSM_ORG' => 'OpenStreetMap.org(无 HiDPI)', + 'MAP_PROVIDER_OSM_DE' => 'OpenStreetMap.de(无 HiDPI)', + 'MAP_PROVIDER_OSM_FR' => 'OpenStreetMap.fr(无 HiDPI)', + 'MAP_PROVIDER_RRZE' => 'University of Erlangen, Germany(仅 HiDPI)', + 'MAP_INCLUDE_SUBALBUMS_TEXT' => '在地图上包含子相册的图片:', + 'LOCATION_DECODING' => '将 GPS 数据解码为地点名称', + 'LOCATION_SHOW' => '显示地点名称', + 'LOCATION_SHOW_PUBLIC' => '为公开模式显示地点名称', + + 'LAYOUT_TYPE' => '照片布局:', + 'LAYOUT_SQUARES' => '方形缩略图', + 'LAYOUT_JUSTIFIED' => '保持长宽比,两端对齐', + 'LAYOUT_MASONRY' => '保持长宽比, masonry', + 'LAYOUT_GRID' => '保持长宽比, grid', + 'LAYOUT_UNJUSTIFIED' => '保持长宽比,不对齐', + 'SET_LAYOUT' => '更改布局', + + 'NSFW_VISIBLE_TEXT_1' => '使敏感相册默认可见。', + 'NSFW_VISIBLE_TEXT_2' => '如果相册是公开的,其将仍然可以访问,只是会从视图中隐藏并可以通过按下H键来显示。', + 'SETTINGS_SUCCESS_NSFW_VISIBLE' => '敏感相册的默认可见性成功更新。', + + 'NSFW_BANNER' => '

Sensitive content

This album contains sensitive content which some people may find offensive or disturbing.

Tap to consent.

', + 'NSFW_HEADER' => 'Sensitive content', + 'NSFW_EXPLANATION' => 'This album contains sensitive content which some people may find offensive or disturbing.', + 'TAP_CONSENT' => 'Tap to consent.', + + 'VIEW_NO_RESULT' => '无结果', + 'VIEW_NO_PUBLIC_ALBUMS' => '没有公开相册', + 'VIEW_NO_CONFIGURATION' => '没有配置', + 'VIEW_PHOTO_NOT_FOUND' => '照片未找到', + + 'NO_TAGS' => '没有标签', + + 'UPLOAD_MANAGE_NEW_PHOTOS' => '您现在可以管理您的新照片了。', + 'UPLOAD_COMPLETE' => '上传完成', + 'UPLOAD_COMPLETE_FAILED' => '有一个或多个照片上传失败。', + 'UPLOAD_IMPORTING' => '导入', + 'UPLOAD_IMPORTING_URL' => '导入 URL', + 'UPLOAD_UPLOADING' => '上传中', + 'UPLOAD_FINISHED' => '已完成', + 'UPLOAD_PROCESSING' => '处理中', + 'UPLOAD_FAILED' => '失败', + 'UPLOAD_FAILED_ERROR' => '上传失败。服务器返回了一个错误!', + 'UPLOAD_FAILED_WARNING' => '上传失败。服务器返回了一个警告!', + 'UPLOAD_CANCELLED' => 'Cancelled', + 'UPLOAD_SKIPPED' => '已跳过', + 'UPLOAD_UPDATED' => 'Updated', + 'UPLOAD_GENERAL' => 'General', + 'UPLOAD_IMPORT_SKIPPED_DUPLICATE' => 'This photo has been skipped because it’s already in your library.', + 'UPLOAD_IMPORT_RESYNCED_DUPLICATE' => 'This photo has been skipped because it’s already in your library, but its metadata has been updated.', + 'UPLOAD_ERROR_CONSOLE' => '请查看浏览器控制台获取详细信息。', + 'UPLOAD_UNKNOWN' => '服务器返回未知响应。请查看浏览器控制台获取详细信息。', + 'UPLOAD_ERROR_UNKNOWN' => '上传失败。服务器返回了一个未知错误!', + 'UPLOAD_ERROR_POSTSIZE' => '上传失败。PHP 的 post_max_size 限制过小!', + 'UPLOAD_ERROR_FILESIZE' => '上传失败。PHP 的 upload_max_filesize 限制过小!', + 'UPLOAD_IN_PROGRESS' => 'Lychee 当前正在上传!', + 'UPLOAD_IMPORT_WARN_ERR' => '导入已完成,但返回了警告或错误。请查看日志(设置->显示日志)以获取详细信息。', + 'UPLOAD_IMPORT_COMPLETE' => '导入完成', + 'UPLOAD_IMPORT_INSTR' => '输入照片的直链以导入:', + 'UPLOAD_IMPORT' => '导入', + 'UPLOAD_IMPORT_SERVER' => '从服务器导入', + 'UPLOAD_IMPORT_SERVER_FOLD' => '文件夹为空或其中没有可读的文件。请查看日志(设置->显示日志)以获取详细信息。', + 'UPLOAD_IMPORT_SERVER_INSTR' => 'Import all photos, folders and sub-folders located in the folders with the following absolute paths (on server). Paths are space separated, use \\ to escape a space in a path.', + 'UPLOAD_ABSOLUTE_PATH' => 'Absolute path to directories, space separated', + 'UPLOAD_IMPORT_SERVER_EMPT' => '无法导入空文件夹!', + 'UPLOAD_IMPORT_DELETE_ORIGINALS' => '删除原始图像', + 'UPLOAD_IMPORT_DELETE_ORIGINALS_EXPL' => '原始图像将在导入后尝试删除。', + 'UPLOAD_IMPORT_VIA_SYMLINK' => 'Symbolic links', + 'UPLOAD_IMPORT_VIA_SYMLINK_EXPL' => 'Import files using symbolic links to originals.', + 'UPLOAD_IMPORT_SKIP_DUPLICATES' => 'Skip duplicates', + 'UPLOAD_IMPORT_SKIP_DUPLICATES_EXPL' => 'Existing media files are skipped.', + 'UPLOAD_IMPORT_RESYNC_METADATA' => 'Re-sync metadata', + 'UPLOAD_IMPORT_RESYNC_METADATA_EXPL' => 'Update metadata of existing media files.', + 'UPLOAD_IMPORT_LOW_MEMORY_EXPL' => '此服务器上的导入进程已经接近内存上限并可能过早地被中断。', + 'UPLOAD_WARNING' => '警告', + 'UPLOAD_IMPORT_NOT_A_DIRECTORY' => '指定的路径不是一个可读的目录!', + 'UPLOAD_IMPORT_PATH_RESERVED' => '指定的路径是 Lychee 的保留目录!', + 'UPLOAD_IMPORT_FAILED' => '不能导入文件!', + 'UPLOAD_IMPORT_UNSUPPORTED' => '不支持的文件类型!', + 'UPLOAD_IMPORT_CANCELLED' => 'Import cancelled', + + 'ABOUT_SUBTITLE' => '自托管照片管理的正确之选', + 'ABOUT_DESCRIPTION' => 'Lychee 是一个自由的照片管理工具,其其运行于您的服务器或Web空间。仅需几分钟即可安装。Lychee 为您提供了像原生应用那样上传、管理和分享照片所需的一切,您的所有照片都将安全地存储。', + 'FOOTER_COPYRIGHT' => '本网站上的所有图像均受制于版权 %1$s © %2$s', + 'HOSTED_WITH_LYCHEE' => '由 Lychee 托管', + + 'URL_COPY_TO_CLIPBOARD' => '复制到剪贴板', + 'URL_COPIED_TO_CLIPBOARD' => 'URL 已经复制到剪贴板!', + 'PHOTO_DIRECT_LINKS_TO_IMAGES' => '图像文件的直链:', + 'PHOTO_ORIGINAL' => 'Original', + 'PHOTO_MEDIUM' => '中等尺寸', + 'PHOTO_MEDIUM_HIDPI' => '中等尺寸 HiDPI', + 'PHOTO_SMALL' => '缩略图', + 'PHOTO_SMALL_HIDPI' => '缩略图 HiDPI', + 'PHOTO_THUMB' => '方形缩略图', + 'PHOTO_THUMB_HIDPI' => '方形缩略图 HiDPI', + 'PHOTO_PLACEHOLDER' => 'Low Quality Image Placeholder', + 'PHOTO_THUMBNAIL' => 'Photo thumbnail', + 'PHOTO_LIVE_VIDEO' => '实况照片(Live-Photo)的视频部分', + 'PHOTO_VIEW' => 'Lychee 照片查看:', + + 'PHOTO_EDIT_ROTATECWISE' => '顺时针旋转', + 'PHOTO_EDIT_ROTATECCWISE' => '逆时针旋转', + + 'ERROR_GPX' => '载入GPX文件出错: ', + 'ERROR_EITHER_ALBUMS_OR_PHOTOS' => '请选择相册或照片!', + 'ERROR_COULD_NOT_FIND' => '找不到你想要的东西', + 'ERROR_INVALID_EMAIL' => '不是一个合法的邮箱地址', + 'EMAIL_SUCCESS' => '邮箱已更新', + 'ERROR_PHOTO_NOT_FOUND' => '错误: 照片 %s 找不到!', + 'ERROR_EMPTY_USERNAME' => '新用户名不能为空', + 'ERROR_PASSWORD_DOES_NOT_MATCH' => '新密码不匹配', + 'ERROR_EMPTY_PASSWORD' => '新密码不能为空', + 'ERROR_SELECT_ALBUM' => '选择要共享的相册!', + 'ERROR_SELECT_USER' => '选择要与之共享的用户!', + 'ERROR_SELECT_SHARING' => '选择要删除的共享!', + 'SHARING_SUCCESS' => '分享已更新!', + 'SHARING_REMOVED' => '共享已删除!', + 'USER_CREATED' => '用户已创建', + 'USER_DELETED' => '用户已删除', + 'USER_UPDATED' => '用户已更新', + 'ENTER_EMAIL' => '输入你的邮箱地址:', + 'ERROR_ALBUM_JSON_NOT_FOUND' => '错误:找不到相册json!', + 'ERROR_ALBUM_NOT_FOUND' => '错误:找不到相册 %s', + 'ERROR_DROPBOX_KEY' => '错误:未设置Dropbox密钥', + 'ERROR_SESSION' => '会话已过期', + 'CAMERA_DATE' => '相机日期', + 'NEW_PASSWORD' => '新密码', + 'ALLOW_UPLOADS' => '允许上传', + 'ALLOW_USER_SELF_EDIT' => '允许用户帐户的自我管理', + 'OSM_CONTRIBUTORS' => 'OpenStreetMap贡献者', +]; diff --git a/lang/zh_CN/maintenance.php b/lang/zh_CN/maintenance.php new file mode 100644 index 00000000000..f86de3d6f46 --- /dev/null +++ b/lang/zh_CN/maintenance.php @@ -0,0 +1,60 @@ + 'Maintenance', + 'description' => 'You will find on this page, all the required actions to keep your Lychee installation running smooth and nicely.', + 'cleaning' => [ + 'title' => 'Cleaning %s', + 'result' => '%s deleted.', + 'description' => 'Remove all contents from %s', + 'button' => 'Clean', + ], + 'fix-jobs' => [ + 'title' => 'Fixing Jobs History', + 'description' => 'Mark jobs with status %s or %s as %s.', + 'button' => 'Fix job history', + ], + 'gen-sizevariants' => [ + 'title' => 'Missing %s', + 'description' => 'Found %d %s that could be generated.', + 'button' => 'Generate!', + 'success' => 'Successfully generated %d %s.', + ], + 'fill-filesize-sizevariants' => [ + 'title' => 'File sizes missing', + 'description' => 'Found %d small variants without file size.', + 'button' => 'Fetch data!', + 'success' => 'Successfully computed sizes of %d small variants.', + ], + 'fix-tree' => [ + 'title' => 'Tree statistics', + 'Oddness' => 'Oddness', + 'Duplicates' => 'Duplicates', + 'Wrong parents' => 'Wrong parents', + 'Missing parents' => 'Missing parents', + 'button' => 'Fix tree', + ], + 'optimize' => [ + 'title' => 'Optimize Database', + 'description' => 'If you notice slowdown in your installation, it may be because your database does not + have all its needed index.', + 'button' => 'Optimize Database', + ], + 'update' => [ + 'title' => 'Updates', + 'check-button' => 'Check for updates', + 'update-button' => 'Update', + 'no-pending-updates' => 'No pending update.', + ], +]; \ No newline at end of file diff --git a/lang/zh_CN/profile.php b/lang/zh_CN/profile.php new file mode 100644 index 00000000000..cc24b97452c --- /dev/null +++ b/lang/zh_CN/profile.php @@ -0,0 +1,64 @@ + 'Profile', + + 'login' => [ + 'header' => 'Profile', + 'enter_current_password' => 'Enter your current password:', + 'current_password' => 'Current password', + 'credentials_update' => 'Your credentials will be changed to the following:', + 'username' => 'Username', + 'new_password' => 'New password', + 'confirm_new_password' => 'Confirm new password', + 'email_instruction' => 'Add your email below to enable receiving email notifications. To stop receiving emails, simply remove your email below.', + 'email' => 'Email', + 'change' => 'Change Login', + 'api_token' => 'API Token ...', + + 'missing_fields' => 'Missing fields', + ], + + 'token' => [ + 'unavailable' => 'You have already viewed this token.', + 'no_data' => 'No token API have been generated.', + 'disable' => 'Disable', + 'disabled' => 'Token disabled', + 'warning' => 'This token will not be displayed again. Copy it and keep it in a safe place.', + 'reset' => 'Reset the token', + 'create' => 'Create a new token', + ], + + 'oauth' => [ + 'header' => 'OAuth', + 'header_not_available' => 'OAuth is not available', + 'setup_env' => 'Set up the credentials in your .env', + 'token_registered' => '%s token registered.', + 'setup' => 'Set up %s', + 'reset' => 'reset', + 'credential_deleted' => 'Credential deleted!', + ], + + 'u2f' => [ + 'header' => 'Passkey/MFA/2FA', + 'info' => 'This only provides the ability to use WebAuthn to authenticate instead of username & password.', + 'empty' => 'Credentials list is empty!', + 'not_secure' => 'Environment not secured. U2F not available.', + 'new' => 'Register new device.', + 'credential_deleted' => 'Credential deleted!', + 'credential_updated' => 'Credential updated!', + 'credential_registred' => 'Registration successful!', + '5_chars' => 'At least 5 chars.', + ], +]; \ No newline at end of file diff --git a/lang/zh_CN/settings.php b/lang/zh_CN/settings.php new file mode 100644 index 00000000000..fd197f11135 --- /dev/null +++ b/lang/zh_CN/settings.php @@ -0,0 +1,92 @@ + 'Settings', + 'small_screen' => 'For better a experience on the Settings page,
we recommend you use a larger screen.', + 'tabs' => [ + 'basic' => 'Basic', + 'all_settings' => 'All settings', + ], + 'toasts' => [ + 'change_saved' => 'Change saved!', + 'details' => 'Settings have been modified as per request', + 'error' => 'Error!', + 'error_load_css' => 'Could not load dist/user.css', + 'error_load_js' => 'Could not load dist/custom.js', + 'error_save_css' => 'Could not save CSS', + 'error_save_js' => 'Could not save JS', + 'thank_you' => 'Thank you for your support.', + 'reload' => 'Reload your page for full functionalities.', + ], + 'system' => [ + 'header' => 'System', + 'use_dark_mode' => 'Use dark mode for Lychee', + 'language' => 'Language used by Lychee', + 'nsfw_album_visibility' => 'Make Sensitive albums visible by default.', + 'nsfw_album_explanation' => 'If the album is public, it is still accessible, just hidden from the view and can be revealed by pressing H.', + ], + 'lychee_se' => [ + 'header' => 'Lychee SE', + 'call4action' => 'Get exclusive features and support the development of Lychee. Unlock the SE edition.', + 'preview' => 'Enable preview of Lychee SE features', + 'hide_call4action' => 'Hide this Lychee SE registration form. I am happy with Lychee as-is. :)', + 'hide_warning' => 'If enabled, the only way to register your license key will be via the More tab above. Changes are applied on page reload.', + ], + 'dropbox' => [ + 'header' => 'Dropbox', + 'instruction' => 'In order to import photos from your Dropbox, you need a valid drop-ins app key from their website.', + 'api_key' => 'Dropbox API Key', + 'set_key' => 'Set Dropbox Key', + ], + 'gallery' => [ + 'header' => 'Gallery', + 'photo_order_column' => 'Default column used for sorting photos', + 'photo_order_direction' => 'Default order used for sorting photos', + 'album_order_column' => 'Default column used for sorting albums', + 'album_order_direction' => 'Default order used for sorting albums', + 'aspect_ratio' => 'Default aspect ratio for album thumbs', + 'photo_layout' => 'Layout for pictures', + 'album_decoration' => 'Show decorations on album cover (sub-album and/or photo count)', + 'album_decoration_direction' => 'Align album decorations horizontally or vertically', + 'photo_overlay' => 'Default image overlay information', + 'license_default' => 'Default license used for albums', + 'license_help' => 'Need help choosing?', + ], + 'geolocation' => [ + 'header' => 'Geo-location', + 'map_display' => 'Display the map given GPS coordinates', + 'map_display_public' => 'Allow anonymous users to access the map', + 'map_provider' => 'Defines the map provider', + 'map_include_subalbums' => 'Includes pictures of the sub albums on the map', + 'location_decoding' => 'Use GPS location decoding', + 'location_show' => 'Show location extracted from GPS coordinates', + 'location_show_public' => 'Anonymous users can access the extracted location from GPS coordinates', + ], + 'advanced' => [ + 'header' => 'Advanced Customization', + 'change_css' => 'Change CSS', + 'change_js' => 'Change JS', + ], + 'all' => [ + 'old_setting_style' => 'Old setting style', + 'change_detected' => 'Some settings changed.', + 'save' => 'Save', + ], + + 'tool_option' => [ + 'disabled' => 'disabled', + 'enabled' => 'enabled', + 'discover' => 'discover', + ], +]; \ No newline at end of file diff --git a/lang/zh_CN/sharing.php b/lang/zh_CN/sharing.php new file mode 100644 index 00000000000..69de18cc6d0 --- /dev/null +++ b/lang/zh_CN/sharing.php @@ -0,0 +1,33 @@ + 'Sharing', + + 'info' => 'This page gives an overview of and the ability to edit the sharing rights associated with albums.', + 'album_title' => 'Album title', + 'username' => 'Username', + 'no_data' => 'Sharing list is empty.', + 'share' => 'Share', + 'permission_deleted' => 'Permission deleted!', + 'permission_created' => 'Permission created!', + + 'grants' => [ + 'read' => 'Grants read access', + 'original' => 'Grants access to original photo', + 'download' => 'Grants download', + 'upload' => 'Grants upload', + 'edit' => 'Grants edit', + 'delete' => 'Grants delete', + ], +]; \ No newline at end of file diff --git a/lang/zh_CN/statistics.php b/lang/zh_CN/statistics.php new file mode 100644 index 00000000000..2baf855bbd5 --- /dev/null +++ b/lang/zh_CN/statistics.php @@ -0,0 +1,34 @@ + 'Statistics', + + 'preview_text' => 'This is a preview of the statistics page available in Lychee SE.
The data shown here are randomly generated and do not reflect your server.', + 'no_data' => 'User does not have data on server.', + 'collapse' => 'Collapse albums sizes', + + 'total' => [ + 'total' => 'Total', + 'albums' => 'Albums', + 'photos' => 'Photos', + 'size' => 'Size', + ], + 'table' => [ + 'username' => 'Owner', + 'title' => 'Title', + 'photos' => 'Photos', + 'descendants' => 'Children', + 'size' => 'Size', + ], +]; \ No newline at end of file diff --git a/lang/zh_CN/toasts.php b/lang/zh_CN/toasts.php new file mode 100644 index 00000000000..293d4b72594 --- /dev/null +++ b/lang/zh_CN/toasts.php @@ -0,0 +1,17 @@ + 'Error', + 'success' => 'Success', +]; \ No newline at end of file diff --git a/lang/zh_CN/users.php b/lang/zh_CN/users.php new file mode 100644 index 00000000000..599bb833454 --- /dev/null +++ b/lang/zh_CN/users.php @@ -0,0 +1,44 @@ + 'Users', + 'description' => 'Here you can manage the users of your Lychee installation. You can create, edit and delete users.', + 'create' => 'Create a new user', + 'username' => 'Username', + 'password' => 'Password', + 'legend' => 'Legend', + 'upload_rights' => 'When selected, the user can upload content.', + 'edit_rights' => 'When selected, the user can modify their profile (username, password).', + 'quota' => 'When set, the user has a space quota for pictures (in kB).', + + 'user_deleted' => 'User deleted', + 'user_created' => 'User created', + 'user_updated' => 'User updated', + 'change_saved' => 'Change saved!', + + 'create_edit' => [ + 'upload_rights' => 'User can upload content.', + 'edit_rights' => 'User can modify their profile (username, password).', + 'quota' => 'User has quota limit.', + 'quota_kb' => 'quota in kB (0 for default)', + 'note' => 'Admin note (not publically visible)', + 'create' => 'Create', + 'edit' => 'Edit', + ], + 'line' => [ + 'admin' => 'admin user', + 'edit' => 'Edit', + 'delete' => 'Delete', + ], +]; \ No newline at end of file diff --git a/lang/zh_TW/aspect_ratio.php b/lang/zh_TW/aspect_ratio.php new file mode 100644 index 00000000000..2c7e8fb56ac --- /dev/null +++ b/lang/zh_TW/aspect_ratio.php @@ -0,0 +1,21 @@ + '5/4 (instagram landscape)', + '4by5' => '4/5 (instagram portrait)', + '2by3' => '2/3 (portrait)', + '3by2' => '3/2 (landscape)', + '1by1' => 'square', + '1byx9' => '16/9 (landscape)', +]; \ No newline at end of file diff --git a/lang/zh_TW/diagnostics.php b/lang/zh_TW/diagnostics.php new file mode 100644 index 00000000000..0fadd640428 --- /dev/null +++ b/lang/zh_TW/diagnostics.php @@ -0,0 +1,30 @@ + 'Diagnostics', + + 'copy_to_clipboard' => 'Copy diagnostics to clipboard', + 'self-diagnosis' => 'Self-diagnosis', + 'info' => 'Info', + 'space' => 'Space', + 'load_space' => 'Load space usage.', + 'configuration' => 'Configuration', + 'loading' => 'Loading...', + 'identical_content' => 'Identical content', + + 'toast' => [ + 'info' => 'Info', + 'copy' => 'Diagnostics copied to clipboard!', + ], +]; \ No newline at end of file diff --git a/lang/zh_TW/dialogs.php b/lang/zh_TW/dialogs.php new file mode 100644 index 00000000000..4afd65fae3f --- /dev/null +++ b/lang/zh_TW/dialogs.php @@ -0,0 +1,221 @@ + [ + 'close' => 'Close', + 'cancel' => 'Cancel', + 'save' => 'Save', + 'delete' => 'Delete', + 'move' => 'Move', + ], + 'about' => [ + 'subtitle' => 'Self-hosted photo-management done right', + 'description' => 'Lychee is a free photo-management tool, which runs on your server or web-space. Installing is a matter of seconds. Upload, manage and share photos like from a native application. Lychee comes with everything you need and all your photos are stored securely.', + 'update_available' => 'Update available!', + 'thank_you' => 'Thank you for your support!', + 'get_supporter_or_register' => 'Get exclusive features and support the development of Lychee.
Unlock the Supporter Edition or register your License key', + 'here' => 'here', + ], + 'dropbox' => [ + 'not_configured' => 'Dropbox is not configured.', + ], + 'import_from_link' => [ + 'instructions' => 'Please enter the direct link to a photo to import it:', + 'import' => 'Import', + ], + 'keybindings' => [ + 'don_t_show_again' => 'Don\'t show this again', + 'side_wide' => 'Site-wide Shortcuts', + 'back_cancel' => 'Back/Cancel', + 'confirm' => 'Confirm', + 'login' => 'Login', + 'toggle_full_screen' => 'Toggle Full Screen', + 'toggle_sensitive_albums' => 'Toggle Sensitive Albums', + + 'albums' => 'Albums Shortcuts', + 'new_album' => 'New Album', + 'upload_photos' => 'Upload Photos', + 'search' => 'Search', + 'show_this_modal' => 'Show this modal', + 'select_all' => 'Select All', + 'move_selection' => 'Move Selection', + 'delete_selection' => 'Delete Selection', + + 'album' => 'Album Shortcuts', + 'slideshow' => 'Start/Stop Slideshow', + 'toggle' => 'Toggle panel', + + 'photo' => 'Photo Shortcuts', + 'previous' => 'Previous photo', + 'next' => 'Next photo', + 'cycle' => 'Cycle overlay mode', + 'star' => 'Star the photo', + 'move' => 'Move the photo', + 'delete' => 'Delete the photo', + 'edit' => 'Edit information', + 'show_hide_meta' => 'Show information', + + 'keep_hidden' => 'We will keep it hidden.', + ], + 'login' => [ + 'username' => 'Username', + 'password' => 'Password', + 'unknown_invalid' => 'Unknown user or invalid password.', + 'signin' => 'Sign-In', + ], + 'register' => [ + 'enter_license' => 'Enter your license key below:', + 'license_key' => 'License key', + 'invalid_license' => 'Invalid license key.', + 'register' => 'Register', + ], + 'share_album' => [ + 'url_copied' => 'Copied URL to clipboard!', + ], + 'upload' => [ + 'completed' => 'Completed', + 'uploaded' => 'Uploaded:', + 'release' => 'Release file to upload!', + 'select' => 'Click here to select files to upload', + 'drag' => '(Or drag files to the page)', + 'loading' => 'Loading', + 'resume' => 'Resume', + 'uploading' => 'Uploading', + 'finished' => 'Finished', + 'failed_error' => 'Upload failed. The server returned an error!', + ], + 'visibility' => [ + 'public' => 'Public', + 'public_expl' => 'Anonymous users can access this album, subject to the restrictions below.', + 'full' => 'Original', + 'full_expl' => 'Anonymous users can view full-resolution photos.', + 'hidden' => 'Hidden', + 'hidden_expl' => 'Anonymous users need a direct link to access this album.', + 'downloadable' => 'Downloadable', + 'downloadable_expl' => 'Anonymous users can download this album.', + 'password' => 'Password', + 'password_prot' => 'Password protected', + 'password_prot_expl' => 'Anonymous users need a shared password to access this album.', + 'nsfw' => 'Sensitive', + 'nsfw_expl' => 'Album contains sensitive content.', + 'visibility_updated' => 'Visibility updated.', + ], + 'move_album' => [ + 'confirm_single' => 'Are you sure you want to move the album “%1$s” into the album “%2$s”?', + 'confirm_multiple' => 'Are you sure you want to move all selected albums into the album “%s”?', + 'move_single' => 'Move Album', + 'move_to' => 'Move to', + 'move_to_single' => 'Move %s to:', + 'move_to_multiple' => 'Move %d albums to:', + 'no_album_target' => 'No album to move to', + 'moved_single' => 'Album moved!', + 'moved_single_details' => '%1$s moved to %2$s', + 'moved_details' => 'Album(s) moved to %s', + ], + 'new_album' => [ + 'menu' => 'Create Album', + 'info' => 'Enter a title for the new album:', + 'title' => 'title', + 'create' => 'Create Album', + ], + 'new_tag_album' => [ + 'menu' => 'Create Tag Album', + 'info' => 'Enter a title for the new tag album:', + 'title' => 'title', + 'set_tags' => 'Set tags to show', + 'warn' => 'Make sure to press enter after each tag', + 'create' => 'Create Tag Album', + ], + 'delete_album' => [ + 'confirmation' => 'Are you sure you want to delete the album “%s” and all of the photos it contains?', + 'confirmation_multiple' => 'Are you sure you want to delete all %d selected albums and all of the photos they contain?', + 'warning' => 'This action can not be undone!', + 'delete' => 'Delete Album and Photos', + ], + 'transfer' => [ + 'query' => 'Transfer ownership of album to', + 'confirmation' => 'Are you sure you want to transfer the ownership of album “%s” and all the photos it contains to "%s"?', + 'lost_access_warning' => 'Your access to this album will be lost.', + 'warning' => 'This action can not be undone!', + 'transfer' => 'Transfer ownership of album and photos', + ], + 'rename' => [ + 'photo' => 'Enter a new title for this photo:', + 'album' => 'Enter a new title for this album:', + 'rename' => 'Rename', + ], + 'merge' => [ + 'merge_to' => 'Merge %s to:', + 'merge_to_multiple' => 'Merge %d albums to:', + 'no_albums' => 'No albums to merge to.', + 'confirm' => 'Are you sure you want to merge the album “%1$s” into the album “%2$s”?', + 'confirm_multiple' => 'Are you sure you want to merge all selected albums into the album “%s”?', + 'merge' => 'Merge Albums', + 'merged' => 'Album(s) merged to %s!', + ], + 'unlock' => [ + 'password_required' => 'This album is protected by a password. Enter the password below to view the photos of this album:', + 'password' => 'Password', + 'unlock' => 'Unlock', + ], + 'photo_tags' => [ + 'question' => 'Enter your tags for this photo.', + 'question_multiple' => 'Enter your tags for all %d selected photos. Existing tags will be overwritten.', + 'no_tags' => 'No Tags', + 'set_tags' => 'Set Tags', + 'updated' => 'Tags updated!', + 'tags_override_info' => 'If this is unchecked, the tags will be added to the existing tags of the photo.', + ], + 'photo_copy' => [ + 'no_albums' => 'No albums to copy to', + 'copy_to' => 'Copy %s to:', + 'copy_to_multiple' => 'Copy %d photos to:', + 'confirm' => 'Copy %s to %s.', + 'confirm_multiple' => 'Copy %d photos to %s.', + 'copy' => 'Copy', + 'copied' => 'Photo(s) copied!', + ], + 'photo_delete' => [ + 'confirm' => 'Are you sure you want to delete the photo “%s”?', + 'confirm_multiple' => 'Are you sure you want to delete all %d selected photos?', + 'deleted' => 'Photo(s) deleted!', + ], + 'move_photo' => [ + 'move_single' => 'Move %s to:', + 'move_multiple' => 'Move %d photos to:', + 'confirm' => 'Move %s to %s.', + 'confirm_multiple' => 'Move %d photos to %s.', + 'moved' => 'Photo(s) moved to %s!', + ], + 'target_user' => [ + 'placeholder' => 'Select user', + ], + 'target_album' => [ + 'placeholder' => 'Select album', + ], + 'webauthn' => [ + 'u2f' => 'U2F', + 'success' => 'Authentication successful!', + 'error' => 'Whoops, it looks like something went wrong. Please reload the site and try again!', + ], + 'se' => [ + 'available' => 'Available in the Supporter Edition', + ], + 'session_expired' => [ + 'title' => 'Session expired', + 'message' => 'Your session has expired.
Please reload the page.', + 'reload' => 'Reload', + 'go_to_gallery' => 'Go to the Gallery', + ], +]; \ No newline at end of file diff --git a/lang/zh_TW/fix-tree.php b/lang/zh_TW/fix-tree.php new file mode 100644 index 00000000000..64803e310e6 --- /dev/null +++ b/lang/zh_TW/fix-tree.php @@ -0,0 +1,55 @@ + 'Maintenance', + 'intro' => 'This page allows you to re-order and fix your albums manually.
Before any modifications, we strongly recommend you to read about Nested Set tree structures.', + 'warning' => 'You can really break your Lychee installation here, modify values at your own risks.', + + 'help' => [ + 'header' => 'Help', + 'hover' => 'Hover ids or titles to highlight related albums.', + 'left' => 'Left', + 'right' => 'Right', + 'convenience' => 'For your convenience, the and buttons allow you to change the values of %s and %s by respectively +1 and -1 with propagation.', + 'left-right-warn' => 'The and indicates that the value of %s (and respectively %s) is duplicated somewhere.', + 'parent-marked' => 'Marked Parent Id indicates that the %s and %s do not satisfy the Nest Set tree structures. Edit either the Parent Id or the %s/%s values.', + 'slowness' => 'This page will be slow with a large number of albums.', + ], + + 'buttons' => [ + 'reset' => 'Reset', + 'check' => 'Check', + 'apply' => 'Apply', + ], + + 'table' => [ + 'title' => 'Title', + 'left' => 'Left', + 'right' => 'Right', + 'id' => 'Id', + 'parent' => 'Parent Id', + ], + + 'errors' => [ + 'invalid' => 'Invalid tree!', + 'invalid_details' => 'We are not applying this as it is guaranteed to be a broken state.', + 'invalid_left' => 'Album %s has an invalid left value.', + 'invalid_right' => 'Album %s has an invalid right value.', + 'invalid_left_right' => 'Album %s has an invalid left/right values. Left should be strictly smaller than right: %s < %s.', + 'duplicate_left' => 'Album %s has a duplicate left value %s.', + 'duplicate_right' => 'Album %s has a duplicate right value %s.', + 'parent' => 'Album %s has an unexpected parent id %s.', + 'unknown' => 'Album %s has an unknown error.', + ], +]; \ No newline at end of file diff --git a/lang/zh_TW/gallery.php b/lang/zh_TW/gallery.php new file mode 100644 index 00000000000..eb8008827e0 --- /dev/null +++ b/lang/zh_TW/gallery.php @@ -0,0 +1,241 @@ + 'Gallery', + + 'smart_albums' => 'Smart albums', + 'albums' => 'Albums', + 'root' => 'Albums', + + 'original' => 'Original', + 'medium' => 'Medium', + 'medium_hidpi' => 'Medium HiDPI', + 'small' => 'Thumb', + 'small_hidpi' => 'Thumb HiDPI', + 'thumb' => 'Square thumb', + 'thumb_hidpi' => 'Square thumb HiDPI', + 'placeholder' => 'Low Quality Image Placeholder', + 'thumbnail' => 'Photo thumbnail', + 'live_video' => 'Video part of live-photo', + + 'camera_data' => 'Camera date', + 'album_reserved' => 'All Rights Reserved', + + 'map' => [ + 'error_gpx' => 'Error loading GPX file', + 'osm_contributors' => 'OpenStreetMap contributors', + ], + + 'search' => [ + 'title' => 'Search', + 'searching' => 'Searching…', + 'no_results' => 'Nothing matches your search query.', + 'searchbox' => 'Search…', + 'minimum_chars' => 'Minimum %s characters required.', + 'photos' => 'Photos (%s)', + 'albums' => 'Albums (%s)', + ], + + 'smart_album' => [ + 'unsorted' => 'Unsorted', + 'starred' => 'Starred', + 'recent' => 'Recent', + 'public' => 'Public', + 'on_this_day' => 'On This Day', + ], + + 'layout' => [ + 'squares' => 'Square thumbnails', + 'justified' => 'With aspect, justified', + 'masonry' => 'With aspect, masonry', + 'grid' => 'With aspect, grid', + ], + + 'overlay' => [ + 'none' => 'None', + 'exif' => 'EXIF data', + 'description' => 'Description', + 'date' => 'Date taken', + ], + + 'timeline' => [ + 'default' => 'default', + 'disabled' => 'disabled', + 'year' => 'Year', + 'month' => 'Month', + 'day' => 'Day', + 'hour' => 'Hour', + ], + + 'album' => [ + 'header_albums' => 'Albums', + 'header_photos' => 'Photos', + 'no_results' => 'Nothing to see here', + 'upload' => 'Upload photos', + + 'tabs' => [ + 'about' => 'About Album', + 'share' => 'Share Album', + 'move' => 'Move Album', + 'danger' => 'DANGER ZONE', + ], + + 'hero' => [ + 'created' => 'Created', + 'copyright' => 'Copyright', + 'subalbums' => 'Subalbums', + 'images' => 'Photos', + 'download' => 'Download Album', + 'share' => 'Share Album', + 'stats_only_se' => 'Statistics available in the Supporter Edition', + ], + + 'stats' => [ + 'lens' => 'Lens', + 'shutter' => 'Shutter speed', + 'iso' => 'ISO', + 'model' => 'Model', + 'aperture' => 'Aperture', + 'no_data' => 'No data', + ], + + 'properties' => [ + 'title' => 'Title', + 'description' => 'Description', + 'photo_ordering' => 'Order photos by', + 'children_ordering' => 'Order albums by', + 'asc/desc' => 'asc/desc', + 'header' => 'Set album header', + 'compact_header' => 'Use compact header', + 'license' => 'Set license', + 'copyright' => 'Set copyright', + 'aspect_ratio' => 'Set album thumbs aspect ratio', + 'album_timeline' => 'Set album timeline mode', + 'photo_timeline' => 'Set photo timeline mode', + 'layout' => 'Set photo layout', + 'show_tags' => 'Set tags to show', + 'tags_required' => 'Tags are required.', + ], + ], + + 'photo' => [ + 'actions' => [ + 'star' => 'Star', + 'unstar' => 'Unstar', + 'set_album_header' => 'Set as album header', + 'move' => 'Move', + 'delete' => 'Delete', + 'header_set' => 'Header set', + ], + + 'details' => [ + 'about' => 'About', + 'basics' => 'Basics', + 'title' => 'Title', + 'uploaded' => 'Uploaded', + 'description' => 'Description', + 'license' => 'License', + 'reuse' => 'Reuse', + 'latitude' => 'Latitude', + 'longitude' => 'Longitude', + 'altitude' => 'Altitude', + 'location' => 'Location', + 'image' => 'Image', + 'video' => 'Video', + 'size' => 'Size', + 'format' => 'Format', + 'resolution' => 'Resolution', + 'duration' => 'Duration', + 'fps' => 'Frame rate', + 'tags' => 'Tags', + 'camera' => 'Camera', + 'captured' => 'Captured', + 'make' => 'Make', + 'type' => 'Type/Model', + 'lens' => 'Lens', + 'shutter' => 'Shutter Speed', + 'aperture' => 'Aperture', + 'focal' => 'Focal Length', + 'iso' => 'ISO %s', + ], + + 'edit' => [ + 'set_title' => 'Set Title', + 'set_description' => 'Set Description', + 'set_license' => 'Set License', + 'no_tags' => 'No Tags', + 'set_tags' => 'Set Tags', + 'set_created_at' => 'Set Upload Date', + ], + ], + + 'nsfw' => [ + 'header' => 'Sensitive content', + 'description' => 'This album contains sensitive content which some people may find offensive or disturbing.', + 'consent' => 'Tap to consent.', + ], + + 'menus' => [ + 'star' => 'Star', + 'unstar' => 'Unstar', + 'star_all' => 'Star Selected', + 'unstar_all' => 'Unstar Selected', + 'tag' => 'Tag', + 'tag_all' => 'Tag Selected', + 'set_cover' => 'Set Album Cover', + 'remove_header' => 'Remove Album Header', + 'set_header' => 'Set Album Header', + 'copy_to' => 'Copy to …', + 'copy_all_to' => 'Copy Selected to …', + 'rename' => 'Rename', + 'move' => 'Move', + 'move_all' => 'Move Selected', + 'delete' => 'Delete', + 'delete_all' => 'Delete Selected', + 'download' => 'Download', + 'download_all' => 'Download Selected', + 'merge' => 'Merge', + 'merge_all' => 'Merge Selected', + + 'upload_photo' => 'Upload Photo', + 'import_link' => 'Import from Link', + 'import_dropbox' => 'Import from Dropbox', + 'new_album' => 'New Album', + 'new_tag_album' => 'New Tag Album', + 'upload_track' => 'Upload track', + 'delete_track' => 'Delete track', + ], + + 'sort' => [ + 'photo_select_1' => 'Upload Time', + 'photo_select_2' => 'Take Date', + 'photo_select_3' => 'Title', + 'photo_select_4' => 'Description', + 'photo_select_6' => 'Star', + 'photo_select_7' => 'Photo Format', + 'ascending' => 'Ascending', + 'descending' => 'Descending', + 'album_select_1' => 'Creation Time', + 'album_select_2' => 'Title', + 'album_select_3' => 'Description', + 'album_select_5' => 'Latest Take Date', + 'album_select_6' => 'Oldest Take Date', + ], + + 'albums_protection' => [ + 'private' => 'private', + 'public' => 'public', + 'inherit_from_parent' => 'inherit from parent', + ], +]; \ No newline at end of file diff --git a/lang/zh_TW/jobs.php b/lang/zh_TW/jobs.php new file mode 100644 index 00000000000..5d952b76012 --- /dev/null +++ b/lang/zh_TW/jobs.php @@ -0,0 +1,18 @@ + 'Jobs', + + 'no_data' => 'No Jobs have been executed yet.', +]; \ No newline at end of file diff --git a/lang/zh_TW/landing.php b/lang/zh_TW/landing.php new file mode 100644 index 00000000000..fe6fe55b8ea --- /dev/null +++ b/lang/zh_TW/landing.php @@ -0,0 +1,19 @@ + 'Gallery', + 'access_gallery' => 'Access the gallery', + 'hosted_with_lychee' => 'Hosted with Lychee', + 'copyright' => 'All images on this website are subject to copyright by %1$s © %2$s', +]; \ No newline at end of file diff --git a/lang/zh_TW/left-menu.php b/lang/zh_TW/left-menu.php new file mode 100644 index 00000000000..9a3e91f4037 --- /dev/null +++ b/lang/zh_TW/left-menu.php @@ -0,0 +1,29 @@ + 'Back to Gallery', + + 'admin' => 'Admin', + 'clockwork' => 'Clockwork App', + 'logs' => 'Show Logs', + 'jobs' => 'Show Job History', + 'user' => 'User', + + 'sign_out' => 'Sign Out', + + 'about' => 'About', + 'api' => 'API Documentation', + 'source_code' => 'Source Code', + 'support' => 'Support', +]; \ No newline at end of file diff --git a/lang/zh_TW/lychee.php b/lang/zh_TW/lychee.php new file mode 100644 index 00000000000..8e0145946af --- /dev/null +++ b/lang/zh_TW/lychee.php @@ -0,0 +1,535 @@ + '帳號名稱', + 'PASSWORD' => '密碼', + 'ENTER' => '確定', + 'CANCEL' => '取消', + 'CONFIRM' => 'Confirm', + 'SIGN_IN' => '登入', + 'CLOSE' => '關閉', + 'SETTINGS' => '設定', + 'SEARCH' => '搜尋 …', + 'MORE' => '更多', + 'DEFAULT' => '默認', + 'GALLERY' => 'Gallery', + + 'USERS' => '使用者', + 'PROFILE' => 'Profile', + 'CREATE' => 'Create', + 'REMOVE' => 'Remove', + 'SHARE' => 'Share', + 'U2F' => 'U2F', + 'NOTIFICATIONS' => 'Notifications', + 'SHARING' => '分享', + 'CHANGE_LOGIN' => '修改登入訊息', + 'CHANGE_SORTING' => '修改排序', + 'SET_DROPBOX' => '設定Dropbox', + 'ABOUT_LYCHEE' => '關於Lychee', + 'DIAGNOSTICS' => '診斷', + 'DIAGNOSTICS_GET_SIZE' => '請求空間使用', + 'JOBS' => 'Show job history', + 'LOGS' => '查看日誌', + 'SIGN_OUT' => '登出', + 'UPDATE_AVAILABLE' => '可用更新!', + 'MIGRATION_AVAILABLE' => '可進行轉移!', + 'CHECK_FOR_UPDATE' => 'Check for updates', + 'DEFAULT_LICENSE' => '新上傳的默認許可證:', + 'SET_LICENSE' => '設置許可證', + 'SET_OVERLAY_TYPE' => '設置疊加', + 'SET_ALBUM_DECORATION' => 'Set album decorations', + 'SET_MAP_PROVIDER' => '設置OpenStreetMap圖層提供者', + 'FULL_SETTINGS' => 'Full Settings', + 'UPDATE' => 'Update', + 'RESET' => 'Reset', + 'DISABLE_TOKEN_TOOLTIP' => 'Disable', + 'ENABLE_TOKEN' => 'Enable API token', + 'DISABLED_TOKEN_STATUS_MSG' => 'Disabled', + 'TOKEN_BUTTON' => 'API Token ...', + 'TOKEN_NOT_AVAILABLE' => 'You have already viewed this token.', + 'TOKEN_WAIT' => 'Wait ...', + + 'SMART_ALBUMS' => '智能相簿', + 'SHARED_ALBUMS' => '共享的相簿', + 'ALBUMS' => '相簿', + 'PHOTOS' => '照片', + 'SEARCH_RESULTS' => '搜索結果', + + 'RENAME' => '重新命名', + 'RENAME_ALL' => '重新命名成功', + 'MERGE' => '合併', + 'MERGE_ALL' => '合併成功', + 'MAKE_PUBLIC' => '設為公開', + 'SHARE_ALBUM' => '分享相簿', + 'SHARE_PHOTO' => '分享照片', + 'VISIBILITY_ALBUM' => '相冊隱私設定', + 'VISIBILITY_PHOTO' => '照片隱私設定', + 'DOWNLOAD_ALBUM' => '下載相簿', + 'ABOUT_ALBUM' => '關於相簿', + 'DELETE_ALBUM' => '刪除相簿', + 'MOVE_ALBUM' => '移動相簿', + 'FULLSCREEN_ENTER' => '全螢幕模式', + 'FULLSCREEN_EXIT' => '結束全螢幕模式', + + 'SHARING_ALBUM_USERS' => 'Share this album with users', + 'WAIT_FETCH_DATA' => 'Please wait while we get the data …', + 'SHARING_ALBUM_USERS_NO_USERS' => 'There are no users to share the album with', + 'SHARING_ALBUM_USERS_LONG_MESSAGE' => 'Select the users to share this album with', + + 'DELETE_ALBUM_QUESTION' => '刪除相簿和照片', + 'KEEP_ALBUM' => '保留相簿', + 'DELETE_ALBUM_CONFIRMATION' => '確定要刪除相簿《%s》以及相簿內包含的所有照片?此動作無法還原!', + + 'DELETE_TAG_ALBUM_QUESTION' => 'Delete Album', + 'DELETE_TAG_ALBUM_CONFIRMATION' => 'Are you sure you want to delete the album “%s” (any photos inside will not be deleted)? This action can’t be undone!', + + 'DELETE_ALBUMS_QUESTION' => '刪除相簿和照片', + 'KEEP_ALBUMS' => '保留相簿', + 'DELETE_ALBUMS_CONFIRMATION' => '確定要刪除全部照片 %d 選取的相簿和其中的所有照片?此動作無法還原!', + + 'DELETE_UNSORTED_CONFIRM' => '確定刪除《未分類》的所有照片?此動作無法還原!', + 'CLEAR_UNSORTED' => '清除未分類', + 'KEEP_UNSORTED' => '保留未分類', + + 'EDIT_SHARING' => '編輯共享', + 'MAKE_PRIVATE' => '設為私人', + + 'CLOSE_ALBUM' => '關閉相簿', + 'CLOSE_PHOTO' => '關閉照片', + 'CLOSE_MAP' => '關閉地圖', + + 'ADD' => '添加', + 'MOVE' => '移動', + 'MOVE_ALL' => '移動已選項目', + 'DUPLICATE' => '創建副本', + 'DUPLICATE_ALL' => '複製已選項目', + 'COPY_TO' => '複製到 …', + 'COPY_ALL_TO' => '複製到 …', + 'DELETE' => '刪除', + 'SAVE' => 'Save', + 'DELETE_ALL' => '删除已選項目', + 'DOWNLOAD' => '下載', + 'DOWNLOAD_ALL' => '下載已選項目', + 'UPLOAD_PHOTO' => '上傳照片', + 'IMPORT_LINK' => '從連結導入', + 'IMPORT_DROPBOX' => '從Dropbox導入', + 'IMPORT_SERVER' => '從伺服器導入', + 'NEW_ALBUM' => '創建新相簿', + 'NEW_TAG_ALBUM' => '新的標籤相簿', + 'UPLOAD_TRACK' => 'Upload track', + 'DELETE_TRACK' => 'Delete track', + + 'TITLE_NEW_ALBUM' => '輸入相簿標題:', + 'UNTITLED' => '未命名', + 'UNSORTED' => '未分類', + 'STARRED' => '我的最愛', + 'RECENT' => '最新', + 'PUBLIC' => '公開', + 'ON_THIS_DAY' => 'On This Day', + 'NUM_PHOTOS' => '照片', + + 'CREATE_ALBUM' => '創建相簿', + 'CREATE_TAG_ALBUM' => '創建標籤相簿', + + 'STAR_PHOTO' => '加入我的最愛', + 'STAR' => '我的最愛', + 'UNSTAR' => 'Unstar', + 'STAR_ALL' => '將已選的標記為收藏夾', + 'UNSTAR_ALL' => 'Unstar Selected', + 'TAG' => '標籤', + 'TAG_ALL' => '批量標籤', + 'UNSTAR_PHOTO' => '從我的最愛中移除', + 'SET_COVER' => 'Set Album Cover', + 'REMOVE_COVER' => 'Remove Album Cover', + 'SET_HEADER' => 'Set Album Header', + 'REMOVE_HEADER' => 'Remove Album Header', + 'SET_COMPACT_HEADER' => 'Use Compact Header', + + 'FULL_PHOTO' => '打開原圖', + 'ABOUT_PHOTO' => '照片資訊', + 'DISPLAY_FULL_MAP' => '地圖', + 'DIRECT_LINK' => '外部連結', + 'DIRECT_LINKS' => '內部連結', + 'QR_CODE' => 'QR Code', + + 'ALBUM_ABOUT' => '關於', + 'ALBUM_BASICS' => '基本資訊', + 'ALBUM_TITLE' => '標題', + 'ALBUM_COPYRIGHT' => 'Copyright', + 'ALBUM_SET_COPYRIGHT' => 'Set copyright', + 'ALBUM_NEW_TITLE' => '輸入新的相簿標題:', + 'ALBUMS_NEW_TITLE' => '設定標題為 %d 已選擇的所有相簿:', + 'ALBUM_SET_TITLE' => '設定標題', + 'ALBUM_DESCRIPTION' => '描述', + 'ALBUM_SHOW_TAGS' => '顯示標籤', + 'ALBUM_NEW_DESCRIPTION' => '輸入新的相簿描述:', + 'ALBUM_SET_DESCRIPTION' => '編輯描述', + 'ALBUM_NEW_SHOWTAGS' => '輸入將在此相冊中顯示的照片標籤:', + 'ALBUM_SET_SHOWTAGS' => '設定顯示標籤', + 'ALBUM_ALBUM' => '相簿', + 'ALBUM_CREATED' => '創建時間', + 'ALBUM_IMAGES' => '圖片資訊', + 'ALBUM_VIDEOS' => '影片', + 'ALBUM_SUBALBUMS' => '子相簿', + 'ALBUM_SHARING' => '分享', + 'ALBUM_SHR_YES' => '是', + 'ALBUM_SHR_NO' => '否', + 'ALBUM_PUBLIC' => '公開', + 'ALBUM_PUBLIC_EXPL' => 'Anonymous users can access this album, subject to the restrictions below.', + 'ALBUM_FULL' => '原圖', + 'ALBUM_FULL_EXPL' => 'Anonymous users can behold full-resolution photos.', + 'ALBUM_HIDDEN' => '隱藏', + 'ALBUM_HIDDEN_EXPL' => 'Anonymous users need a direct link to access this album.', + 'ALBUM_MARK_NSFW' => 'Mark album as sensitive', + 'ALBUM_UNMARK_NSFW' => 'Unmark album as sensitive', + 'ALBUM_NSFW' => 'Sensitive', + 'ALBUM_NSFW_EXPL' => 'Album is marked to contain sensitive content.', + 'ALBUM_DOWNLOADABLE' => '下載', + 'ALBUM_DOWNLOADABLE_EXPL' => 'Anonymous users can download this album.', + 'ALBUM_SHARE_BUTTON_VISIBLE' => '顯示分享按鈕', + 'ALBUM_SHARE_BUTTON_VISIBLE_EXPL' => 'Anonymous users see social media sharing links.', + 'ALBUM_PASSWORD' => '密碼', + 'ALBUM_PASSWORD_PROT' => '密碼保護', + 'ALBUM_PASSWORD_PROT_EXPL' => 'Anonymous users need a shared password to access this album.', + 'ALBUM_PASSWORD_REQUIRED' => '此相簿設有密碼保護。請輸入密碼:', + 'ALBUM_MERGE' => '你確定要合併相簿《%1$s》到該相簿《%2$s》?', + 'ALBUMS_MERGE' => '你確定要合併所有已選擇的相簿到該相簿《%s》?', + 'MERGE_ALBUM' => '合併相簿', + 'DONT_MERGE' => '不要合併', + 'ALBUM_MOVE' => '您確定要移動相簿《%1$s》到該相簿《%2$s》?', + 'ALBUMS_MOVE' => '你確定要合併所有已選擇的相簿到該相簿?《%s》?', + 'MOVE_ALBUMS' => '相簿移動', + 'NOT_MOVE_ALBUMS' => '不要移動', + 'ROOT' => '相簿', + 'ALBUM_REUSE' => '重複利用', + 'ALBUM_LICENSE' => '許可證', + 'ALBUM_SET_LICENSE' => '設定許可證', + 'ALBUM_LICENSE_HELP' => '需要選擇幫助嗎?', + 'ALBUM_LICENSE_NONE' => '不須', + 'ALBUM_RESERVED' => '版權所有', + 'ALBUM_SET_ORDER' => '設定排序方式', + 'ALBUM_ORDERING' => '排序方式', + 'ALBUM_PHOTO_ORDERING' => 'Order photos by', + 'ALBUM_CHILDREN_ORDERING' => 'Order albums by', + 'ALBUM_OWNER' => 'Owner', + + 'PHOTO_ABOUT' => '關於', + 'PHOTO_BASICS' => '基本資訊', + 'PHOTO_TITLE' => '標題', + 'PHOTO_NEW_TITLE' => '輸入新的相片標題:', + 'PHOTO_SET_TITLE' => '設定標題', + 'PHOTO_UPLOADED' => '已上傳', + 'PHOTO_DESCRIPTION' => '描述', + 'PHOTO_NEW_DESCRIPTION' => '輸入新的照片描述', + 'PHOTO_SET_DESCRIPTION' => '編輯描述', + 'PHOTO_NEW_LICENSE' => '新增許可證', + 'PHOTO_SET_LICENSE' => '設定許可證', + 'PHOTO_LICENSE' => '許可證', + 'PHOTO_LICENSE_HELP' => 'Need help choosing?', + 'PHOTO_REUSE' => '重複利用', + 'PHOTO_LICENSE_NONE' => '無', + 'PHOTO_RESERVED' => '版權所有', + 'PHOTO_LATITUDE' => '緯度', + 'PHOTO_LONGITUDE' => '經度', + 'PHOTO_ALTITUDE' => '高度', + 'PHOTO_IMGDIRECTION' => '方向', + 'PHOTO_LOCATION' => '位置', + 'PHOTO_IMAGE' => '照片資訊', + 'PHOTO_VIDEO' => '影片', + 'PHOTO_SIZE' => '大小', + 'PHOTO_FORMAT' => '格式', + 'PHOTO_RESOLUTION' => '解析度', + 'PHOTO_DURATION' => '持續時間', + 'PHOTO_FPS' => '影格速率', + 'PHOTO_TAGS' => '標籤', + 'PHOTO_NOTAGS' => '無標籤', + 'PHOTO_NEW_TAGS' => '為該照片添加標籤(用逗號分隔):', + 'PHOTOS_NEW_TAGS' => '大量標籤 %d 標籤已選照片(已存在的標籤會被覆蓋;用逗號分隔):', + 'PHOTO_SET_TAGS' => '設定標籤', + 'PHOTO_CAMERA' => '相機資訊', + 'PHOTO_CAPTURED' => '拍攝時間', + 'PHOTO_MAKE' => '設備', + 'PHOTO_TYPE' => '類型/型號', + 'PHOTO_LENS' => '鏡片', + 'PHOTO_SHUTTER' => '快門速度', + 'PHOTO_APERTURE' => '光圈', + 'PHOTO_FOCAL' => '焦距', + 'PHOTO_ISO' => 'ISO感光度 %s', + 'PHOTO_SHARING' => '共享', + 'PHOTO_DELETE' => '刪除照片', + 'PHOTO_KEEP' => '保留照片', + 'PHOTO_DELETE_CONFIRMATION' => '是否要刪除照片《%s》?此動作無法還原!', + 'PHOTO_DELETE_ALL' => '是否要刪除除所有 %d 已選擇的照片?此動作無法還原!', + 'PHOTOS_NEW_TITLE' => '批量編輯照片標題 %d 已選的照片:', + 'PHOTO_MAKE_PRIVATE_ALBUM' => '此照片位於公開相簿中。編輯所在相簿的隱私設定,將其設置為公開或私有。', + 'PHOTO_SHOW_ALBUM' => '顯示相簿', + 'PHOTO_PUBLIC' => '公開', + 'PHOTO_PUBLIC_EXPL' => 'Anonymous users can view this photo, subject to the restrictions below.', + 'PHOTO_FULL' => '原圖', + 'PHOTO_FULL_EXPL' => 'Anonymous users can behold full-resolution photo.', + 'PHOTO_HIDDEN' => '隱藏', + 'PHOTO_HIDDEN_EXPL' => 'Anonymous users need a direct link to view this photo.', + 'PHOTO_DOWNLOADABLE' => '允許下載', + 'PHOTO_DOWNLOADABLE_EXPL' => 'Anonymous users may download this photo.', + 'PHOTO_SHARE_BUTTON_VISIBLE' => '顯示分享按鈕', + 'PHOTO_SHARE_BUTTON_VISIBLE_EXPL' => 'Anonymous users can see social media sharing links.', + 'PHOTO_PASSWORD_PROT' => '密碼保護', + 'PHOTO_PASSWORD_PROT_EXPL' => 'Anonymous users need a shared password to view this photo.', + 'PHOTO_EDIT_SHARING_TEXT' => '此照片的共享屬性將更改為以下內容:', + 'PHOTO_NO_EDIT_SHARING_TEXT' => '由於此照片位於公開相簿中,因此它會繼承該相冊的公開範圍設置。 下面顯示了它的當前可見性,僅供參考。', + 'PHOTO_EDIT_GLOBAL_SHARING_TEXT' => '可以使用全局Lychee設置微調這張照片的可見性。 下面顯示了它的當前可見性,僅供參考。', + 'PHOTO_NEW_CREATED_AT' => 'Enter the upload date for this photo. mm/dd/yyyy, hh:mm [am/pm]', + 'PHOTO_SET_CREATED_AT' => 'Set upload date', + + 'LOADING' => '載入中', + 'ERROR' => '錯誤', + 'ERROR_TEXT' => '噢,似乎出了一些問題。請重整頁面後再試一次!', + 'ERROR_UNKNOWN' => '發生未知問題!請再試一次,檢查您的安裝和伺服器。請查看自述文件以獲取更多信息。', + 'ERROR_MAP_DEACTIVATED' => '地圖功能已被設為停用。', + 'ERROR_SEARCH_DEACTIVATED' => '搜索功能已在設為停用。', + 'SUCCESS' => '好', + 'CHANGE_SUCCESS' => 'Change successful.', + 'RETRY' => '重試', + 'OVERRIDE' => 'Override', + 'TAGS_OVERRIDE_INFO' => 'If this is unchecked, the tags will be added to the existing tags of the photo.', + + 'SETTINGS_SUCCESS_LOGIN' => '登錄信息已更新', + 'SETTINGS_SUCCESS_SORT' => '排序順序已更新。', + 'SETTINGS_SUCCESS_DROPBOX' => 'Dropbox密鑰已更新', + 'SETTINGS_SUCCESS_LANG' => '語言已更新', + 'SETTINGS_SUCCESS_LAYOUT' => '佈局已更新', + 'SETTINGS_SUCCESS_IMAGE_OVERLAY' => 'EXIF覆蓋設置已更新', + 'SETTINGS_SUCCESS_PUBLIC_SEARCH' => '公開搜尋已更新', + 'SETTINGS_SUCCESS_LICENSE' => '默認許可證已更新', + 'SETTINGS_SUCCESS_MAP_DISPLAY' => '地圖顯示設置已更新', + 'SETTINGS_SUCCESS_MAP_DISPLAY_PUBLIC' => '公開相簿的地圖顯示設置已更新', + 'SETTINGS_SUCCESS_MAP_PROVIDER' => '地圖提供商設置已更新', + 'SETTINGS_SUCCESS_CSS' => 'Stylesheets updated', + 'SETTINGS_SUCCESS_JS' => 'JS updated', + 'SETTINGS_SUCCESS_UPDATE' => 'Settings updated successfully', + 'SETTINGS_DROPBOX_KEY' => 'Dropbox API Key', + 'SETTINGS_ADVANCED_WARNING_EXPL' => 'Changing these advanced settings can be harmful to the stability, security and performance of this application. You should only modify them if you are sure of what you are doing.', + 'SETTINGS_ADVANCED_SAVE' => 'Save my modifications, I accept the risk!', + + 'U2F_NOT_SUPPORTED' => 'U2F not supported. Sorry.', + 'U2F_NOT_SECURE' => 'Environment not secured. U2F not available.', + 'U2F_REGISTER_KEY' => 'Register new device.', + 'U2F_REGISTRATION_SUCCESS' => 'Registration successful!', + 'U2F_AUTHENTIFICATION_SUCCESS' => 'Authentication successful!', + 'U2F_CREDENTIALS' => 'Credentials', + 'U2F_CREDENTIALS_DELETED' => 'Credentials deleted!', + 'U2F_LOGIN' => 'Log in with WebAuthn', + + 'NEW_PHOTOS_NOTIFICATION' => 'Send new photos notification emails.', + 'SETTINGS_SUCCESS_NEW_PHOTOS_NOTIFICATION' => 'New photos notification updated', + 'USER_EMAIL_INSTRUCTION' => 'Add your email below to enable receiving email notifications. To stop receiving emails, simply remove your email below.', + + 'LOGIN_USERNAME' => '新用戶名', + 'LOGIN_PASSWORD' => '新密碼', + 'LOGIN_PASSWORD_CONFIRM' => '確認密碼', + 'PASSWORD_TITLE' => '當前密碼', + 'PASSWORD_CURRENT' => '當前密碼', + 'PASSWORD_TEXT' => '用戶名和密碼將被修改為:', + 'PASSWORD_CHANGE' => '修改登入訊息', + + 'EDIT_SHARING_TITLE' => '編輯共享', + 'EDIT_SHARING_TEXT' => '相簿的共享屬性將被修改為:', + 'SHARE_ALBUM_TEXT' => '該相簿的共享屬性:', + + 'SORT_DIALOG_ATTRIBUTE_LABEL' => 'Attribute', + 'SORT_DIALOG_ORDER_LABEL' => 'Order', + + 'SORT_ALBUM_BY' => '相簿排序 %1$s 在一個 %2$s 排序', + + 'SORT_ALBUM_SELECT_1' => '創建時間', + 'SORT_ALBUM_SELECT_2' => '標題', + 'SORT_ALBUM_SELECT_3' => '描述', + 'SORT_ALBUM_SELECT_5' => '最新', + 'SORT_ALBUM_SELECT_6' => '最老', + + 'SORT_PHOTO_BY' => '照片排序 %1$s 在一個 %2$s 排序', + + 'SORT_PHOTO_SELECT_1' => '發佈時間', + 'SORT_PHOTO_SELECT_2' => '創建時間', + 'SORT_PHOTO_SELECT_3' => '標題', + 'SORT_PHOTO_SELECT_4' => '描述', + 'SORT_PHOTO_SELECT_6' => '喜歡', + 'SORT_PHOTO_SELECT_7' => '照片格式', + + 'SORT_ASCENDING' => '升序', + 'SORT_DESCENDING' => '降序', + 'SORT_CHANGE' => '修改排序', + + 'DROPBOX_TITLE' => '設置Dropbox私鑰', + 'DROPBOX_TEXT' => "要從Dropbox導入照片,需要一個有效的應用私鑰,請到官網獲取。輸入你自己生成的私鑰:", + + 'LANG_TEXT' => '將Lychee語言更改為:', + 'LANG_TITLE' => '改變語言', + + 'SETTING_RECENT_PUBLIC_TEXT' => 'Make "Recent" smart album accessible to anonymous users', + 'SETTING_STARRED_PUBLIC_TEXT' => 'Make "Starred" smart album accessible to anonymous users', + 'SETTING_ONTHISDAY_PUBLIC_TEXT' => 'Make "On This Day" smart album accessible to anonymous users', + + 'CSS_TEXT' => 'Personalize CSS:', + 'CSS_TITLE' => 'Change CSS', + 'JS_TEXT' => 'Custom JS:', + 'JS_TITLE' => 'Change JS', + 'PUBLIC_SEARCH_TEXT' => '允許公共搜索:', + 'OVERLAY_TYPE' => '圖像疊加中要使用的數據:', + 'OVERLAY_NONE' => 'None', + 'OVERLAY_EXIF' => '照片EXIF數據', + 'OVERLAY_DESCRIPTION' => '照片說明', + 'OVERLAY_DATE' => '拍攝日期', + 'ALBUM_DECORATION' => 'Album decorations:', + 'ALBUM_DECORATION_NONE' => 'None', + 'ALBUM_DECORATION_ORIGINAL' => 'Sub-album marker', + 'ALBUM_DECORATION_ALBUM' => 'Number of sub-albums', + 'ALBUM_DECORATION_PHOTO' => 'Number of photos', + 'ALBUM_DECORATION_ALL' => 'Number of sub-albums and photos', + 'ALBUM_DECORATION_ORIENTATION' => 'Orientation of album decorations:', + 'ALBUM_DECORATION_ORIENTATION_ROW' => 'Horizontal (photos, albums)', + 'ALBUM_DECORATION_ORIENTATION_ROW_REVERSE' => 'Horizontal (albums, photos)', + 'ALBUM_DECORATION_ORIENTATION_COLUMN' => 'Vertical (top photos, albums)', + 'ALBUM_DECORATION_ORIENTATION_COLUMN_REVERSE' => 'Vertical (top albums, photos)', + 'MAP_DISPLAY_TEXT' => '啟用地圖(由OpenStreetMap提供):', + 'MAP_DISPLAY_PUBLIC_TEXT' => '為公開相冊啟用地圖(由OpenStreetMap提供):', + 'MAP_PROVIDER' => '地圖的提供者:', + 'MAP_PROVIDER_WIKIMEDIA' => '維基媒體', + 'MAP_PROVIDER_OSM_ORG' => 'OpenStreetMap.org (無 HiDPI)', + 'MAP_PROVIDER_OSM_DE' => 'OpenStreetMap.de (無 HiDPI)', + 'MAP_PROVIDER_OSM_FR' => 'OpenStreetMap.fr (無 HiDPI)', + 'MAP_PROVIDER_RRZE' => '德國埃爾蘭根大學 (只有 HiDPI)', + 'MAP_INCLUDE_SUBALBUMS_TEXT' => '在地圖上包括子相冊的照片:', + 'LOCATION_DECODING' => '將GPS數據解碼為位置名稱', + 'LOCATION_SHOW' => '顯示地點名稱', + 'LOCATION_SHOW_PUBLIC' => '顯示公共模式的位置名稱', + + 'LAYOUT_TYPE' => '照片佈局:', + 'LAYOUT_SQUARES' => '方形縮略圖', + 'LAYOUT_JUSTIFIED' => '有方面,有道理', + 'LAYOUT_MASONRY' => '有方面, masonry', + 'LAYOUT_GRID' => '有方面, grid', + 'LAYOUT_UNJUSTIFIED' => '有方面,沒有道理', + 'SET_LAYOUT' => '變更版面', + + 'NSFW_VISIBLE_TEXT_1' => 'Make Sensitive albums visible by default.', + 'NSFW_VISIBLE_TEXT_2' => 'If the album is public, it is still accessible, just hidden from the view and can be revealed by pressing H.', + 'SETTINGS_SUCCESS_NSFW_VISIBLE' => 'Default sensitive album visibility updated with success.', + + 'NSFW_BANNER' => '

Sensitive content

This album contains sensitive content which some people may find offensive or disturbing.

Tap to consent.

', + 'NSFW_HEADER' => 'Sensitive content', + 'NSFW_EXPLANATION' => 'This album contains sensitive content which some people may find offensive or disturbing.', + 'TAP_CONSENT' => 'Tap to consent.', + + 'VIEW_NO_RESULT' => '無結果', + 'VIEW_NO_PUBLIC_ALBUMS' => '沒有公開相簿', + 'VIEW_NO_CONFIGURATION' => '没有配置', + 'VIEW_PHOTO_NOT_FOUND' => '没找到照片', + + 'NO_TAGS' => '沒有標籤', + + 'UPLOAD_MANAGE_NEW_PHOTOS' => '現在可以管理你的新照片了', + 'UPLOAD_COMPLETE' => '上傳完成', + 'UPLOAD_COMPLETE_FAILED' => '有幾個照片上傳失敗了', + 'UPLOAD_IMPORTING' => '導入', + 'UPLOAD_IMPORTING_URL' => '導入 URL', + 'UPLOAD_UPLOADING' => '上傳中', + 'UPLOAD_FINISHED' => '已完成', + 'UPLOAD_PROCESSING' => '處理中', + 'UPLOAD_FAILED' => '失敗', + 'UPLOAD_FAILED_ERROR' => '上傳失敗。伺服器傳回了一個錯誤!', + 'UPLOAD_FAILED_WARNING' => '上傳失敗。伺服器傳回了一個警告!', + 'UPLOAD_CANCELLED' => 'Cancelled', + 'UPLOAD_SKIPPED' => '已跳過', + 'UPLOAD_UPDATED' => 'Updated', + 'UPLOAD_GENERAL' => 'General', + 'UPLOAD_IMPORT_SKIPPED_DUPLICATE' => 'This photo has been skipped because it’s already in your library.', + 'UPLOAD_IMPORT_RESYNCED_DUPLICATE' => 'This photo has been skipped because it’s already in your library, but its metadata has been updated.', + 'UPLOAD_ERROR_CONSOLE' => '請查看瀏覽器控制台獲取詳細信息。', + 'UPLOAD_UNKNOWN' => '伺服器傳回了未知響應。請查看瀏覽器控制台獲取詳細信息。', + 'UPLOAD_ERROR_UNKNOWN' => '上傳失敗。伺服器回傳了一個未知錯誤!', + 'UPLOAD_ERROR_POSTSIZE' => 'Upload failed. The PHP post_max_size may be too small! Otherwise check the FAQ.', + 'UPLOAD_ERROR_FILESIZE' => 'Upload failed. The PHP upload_max_filesize may be too small! Otherwise check the FAQ.', + 'UPLOAD_IN_PROGRESS' => 'Lychee當前正在上傳!', + 'UPLOAD_IMPORT_WARN_ERR' => '導入成功,但返回了的警告或錯誤。請查看日誌(設置->顯示日誌)以獲取詳細信息。', + 'UPLOAD_IMPORT_COMPLETE' => '導入完成', + 'UPLOAD_IMPORT_INSTR' => '輸入照片鏈接直接導入:', + 'UPLOAD_IMPORT' => '導入', + 'UPLOAD_IMPORT_SERVER' => '從伺服器導入', + 'UPLOAD_IMPORT_SERVER_FOLD' => '文件夾中沒有可讀的文件。請查看日誌(設置->顯示日誌)以獲取詳細信息。', + 'UPLOAD_IMPORT_SERVER_INSTR' => 'Import all photos, folders and sub-folders located in the folders with the following absolute paths (on server). Paths are space separated, use \\ to escape a space in a path.', + 'UPLOAD_ABSOLUTE_PATH' => 'Absolute path to directories, space separated', + 'UPLOAD_IMPORT_SERVER_EMPT' => '無法導入空文件夾!', + 'UPLOAD_IMPORT_DELETE_ORIGINALS' => '刪除原件', + 'UPLOAD_IMPORT_DELETE_ORIGINALS_EXPL' => '如果可能,原始文件將在導入後刪除。', + 'UPLOAD_IMPORT_VIA_SYMLINK' => 'Symbolic links', + 'UPLOAD_IMPORT_VIA_SYMLINK_EXPL' => 'Import files using symbolic links to originals.', + 'UPLOAD_IMPORT_SKIP_DUPLICATES' => 'Skip duplicates', + 'UPLOAD_IMPORT_SKIP_DUPLICATES_EXPL' => 'Existing media files are skipped.', + 'UPLOAD_IMPORT_RESYNC_METADATA' => 'Re-sync metadata', + 'UPLOAD_IMPORT_RESYNC_METADATA_EXPL' => 'Update metadata of existing media files.', + 'UPLOAD_IMPORT_LOW_MEMORY_EXPL' => '伺服器上的導入過程已接近內存限制,並可能最終被提前終止。', + 'UPLOAD_WARNING' => '警告', + 'UPLOAD_IMPORT_NOT_A_DIRECTORY' => '給定的路徑不是可讀目錄!', + 'UPLOAD_IMPORT_PATH_RESERVED' => '給定的路徑是Lychee的保留路徑!', + 'UPLOAD_IMPORT_FAILED' => '無法導入文件!', + 'UPLOAD_IMPORT_UNSUPPORTED' => '不支援的文件類型!', + 'UPLOAD_IMPORT_CANCELLED' => 'Import cancelled', + + 'ABOUT_SUBTITLE' => 'Lychee自主託管的照片管理程序', + 'ABOUT_DESCRIPTION' => 'Lychee 是一個免費的照片管理工具,可在您的伺服器或網站空間上運行。安裝僅需幾秒鐘。上傳,管理和分享照片(例如從本機應用程序)。Lychee提供您所需的一切,所有照片均安全存儲。', + 'FOOTER_COPYRIGHT' => '本網站的照片均受版權所有 %1$s © %2$s', + 'HOSTED_WITH_LYCHEE' => '使用Lychee託管照片(繁中由CYL翻譯)', + + 'URL_COPY_TO_CLIPBOARD' => '複製到剪貼板', + 'URL_COPIED_TO_CLIPBOARD' => '複製到剪貼板的URL!', + 'PHOTO_DIRECT_LINKS_TO_IMAGES' => '指向圖像文件的直接鏈接:', + 'PHOTO_ORIGINAL' => 'Original', + 'PHOTO_MEDIUM' => '中等', + 'PHOTO_MEDIUM_HIDPI' => '中等解析度', + 'PHOTO_SMALL' => '低', + 'PHOTO_SMALL_HIDPI' => '低解析度', + 'PHOTO_THUMB' => '方形圖', + 'PHOTO_THUMB_HIDPI' => '方形解析度', + 'PHOTO_PLACEHOLDER' => 'Low Quality Image Placeholder', + 'PHOTO_THUMBNAIL' => 'Photo thumbnail', + 'PHOTO_LIVE_VIDEO' => '實時照片的視頻部分', + 'PHOTO_VIEW' => 'Lychee照片瀏覽:', + + 'PHOTO_EDIT_ROTATECWISE' => '順時針旋轉', + 'PHOTO_EDIT_ROTATECCWISE' => '逆時針旋轉', + + 'ERROR_GPX' => 'Error loading GPX file: ', + 'ERROR_EITHER_ALBUMS_OR_PHOTOS' => 'Please select either albums or photos!', + 'ERROR_COULD_NOT_FIND' => 'Could not find what you want.', + 'ERROR_INVALID_EMAIL' => 'Not a valid email address.', + 'EMAIL_SUCCESS' => 'Email updated!', + 'ERROR_PHOTO_NOT_FOUND' => 'Error: photo %s not found !', + 'ERROR_EMPTY_USERNAME' => 'new username cannot be empty.', + 'ERROR_PASSWORD_DOES_NOT_MATCH' => 'new password does not match.', + 'ERROR_EMPTY_PASSWORD' => 'new password cannot be empty.', + 'ERROR_SELECT_ALBUM' => 'Select an album to share!', + 'ERROR_SELECT_USER' => 'Select a user to share with!', + 'ERROR_SELECT_SHARING' => 'Select a sharing to remove!', + 'SHARING_SUCCESS' => 'Sharing updated!', + 'SHARING_REMOVED' => 'Sharing removed!', + 'USER_CREATED' => 'User created!', + 'USER_DELETED' => 'User deleted!', + 'USER_UPDATED' => 'User updated!', + 'ENTER_EMAIL' => 'Enter your email address:', + 'ERROR_ALBUM_JSON_NOT_FOUND' => 'Error: Album json not found!', + 'ERROR_ALBUM_NOT_FOUND' => 'Error: album %s not found', + 'ERROR_DROPBOX_KEY' => 'Error: Dropbox key not set', + 'ERROR_SESSION' => 'Session expired.', + 'CAMERA_DATE' => 'Camera date', + 'NEW_PASSWORD' => 'new password', + 'ALLOW_UPLOADS' => 'Allow uploads', + 'ALLOW_USER_SELF_EDIT' => 'Allow self-management of user account', + 'OSM_CONTRIBUTORS' => 'OpenStreetMap contributors', +]; diff --git a/lang/zh_TW/maintenance.php b/lang/zh_TW/maintenance.php new file mode 100644 index 00000000000..f86de3d6f46 --- /dev/null +++ b/lang/zh_TW/maintenance.php @@ -0,0 +1,60 @@ + 'Maintenance', + 'description' => 'You will find on this page, all the required actions to keep your Lychee installation running smooth and nicely.', + 'cleaning' => [ + 'title' => 'Cleaning %s', + 'result' => '%s deleted.', + 'description' => 'Remove all contents from %s', + 'button' => 'Clean', + ], + 'fix-jobs' => [ + 'title' => 'Fixing Jobs History', + 'description' => 'Mark jobs with status %s or %s as %s.', + 'button' => 'Fix job history', + ], + 'gen-sizevariants' => [ + 'title' => 'Missing %s', + 'description' => 'Found %d %s that could be generated.', + 'button' => 'Generate!', + 'success' => 'Successfully generated %d %s.', + ], + 'fill-filesize-sizevariants' => [ + 'title' => 'File sizes missing', + 'description' => 'Found %d small variants without file size.', + 'button' => 'Fetch data!', + 'success' => 'Successfully computed sizes of %d small variants.', + ], + 'fix-tree' => [ + 'title' => 'Tree statistics', + 'Oddness' => 'Oddness', + 'Duplicates' => 'Duplicates', + 'Wrong parents' => 'Wrong parents', + 'Missing parents' => 'Missing parents', + 'button' => 'Fix tree', + ], + 'optimize' => [ + 'title' => 'Optimize Database', + 'description' => 'If you notice slowdown in your installation, it may be because your database does not + have all its needed index.', + 'button' => 'Optimize Database', + ], + 'update' => [ + 'title' => 'Updates', + 'check-button' => 'Check for updates', + 'update-button' => 'Update', + 'no-pending-updates' => 'No pending update.', + ], +]; \ No newline at end of file diff --git a/lang/zh_TW/profile.php b/lang/zh_TW/profile.php new file mode 100644 index 00000000000..cc24b97452c --- /dev/null +++ b/lang/zh_TW/profile.php @@ -0,0 +1,64 @@ + 'Profile', + + 'login' => [ + 'header' => 'Profile', + 'enter_current_password' => 'Enter your current password:', + 'current_password' => 'Current password', + 'credentials_update' => 'Your credentials will be changed to the following:', + 'username' => 'Username', + 'new_password' => 'New password', + 'confirm_new_password' => 'Confirm new password', + 'email_instruction' => 'Add your email below to enable receiving email notifications. To stop receiving emails, simply remove your email below.', + 'email' => 'Email', + 'change' => 'Change Login', + 'api_token' => 'API Token ...', + + 'missing_fields' => 'Missing fields', + ], + + 'token' => [ + 'unavailable' => 'You have already viewed this token.', + 'no_data' => 'No token API have been generated.', + 'disable' => 'Disable', + 'disabled' => 'Token disabled', + 'warning' => 'This token will not be displayed again. Copy it and keep it in a safe place.', + 'reset' => 'Reset the token', + 'create' => 'Create a new token', + ], + + 'oauth' => [ + 'header' => 'OAuth', + 'header_not_available' => 'OAuth is not available', + 'setup_env' => 'Set up the credentials in your .env', + 'token_registered' => '%s token registered.', + 'setup' => 'Set up %s', + 'reset' => 'reset', + 'credential_deleted' => 'Credential deleted!', + ], + + 'u2f' => [ + 'header' => 'Passkey/MFA/2FA', + 'info' => 'This only provides the ability to use WebAuthn to authenticate instead of username & password.', + 'empty' => 'Credentials list is empty!', + 'not_secure' => 'Environment not secured. U2F not available.', + 'new' => 'Register new device.', + 'credential_deleted' => 'Credential deleted!', + 'credential_updated' => 'Credential updated!', + 'credential_registred' => 'Registration successful!', + '5_chars' => 'At least 5 chars.', + ], +]; \ No newline at end of file diff --git a/lang/zh_TW/settings.php b/lang/zh_TW/settings.php new file mode 100644 index 00000000000..fd197f11135 --- /dev/null +++ b/lang/zh_TW/settings.php @@ -0,0 +1,92 @@ + 'Settings', + 'small_screen' => 'For better a experience on the Settings page,
we recommend you use a larger screen.', + 'tabs' => [ + 'basic' => 'Basic', + 'all_settings' => 'All settings', + ], + 'toasts' => [ + 'change_saved' => 'Change saved!', + 'details' => 'Settings have been modified as per request', + 'error' => 'Error!', + 'error_load_css' => 'Could not load dist/user.css', + 'error_load_js' => 'Could not load dist/custom.js', + 'error_save_css' => 'Could not save CSS', + 'error_save_js' => 'Could not save JS', + 'thank_you' => 'Thank you for your support.', + 'reload' => 'Reload your page for full functionalities.', + ], + 'system' => [ + 'header' => 'System', + 'use_dark_mode' => 'Use dark mode for Lychee', + 'language' => 'Language used by Lychee', + 'nsfw_album_visibility' => 'Make Sensitive albums visible by default.', + 'nsfw_album_explanation' => 'If the album is public, it is still accessible, just hidden from the view and can be revealed by pressing H.', + ], + 'lychee_se' => [ + 'header' => 'Lychee SE', + 'call4action' => 'Get exclusive features and support the development of Lychee. Unlock the SE edition.', + 'preview' => 'Enable preview of Lychee SE features', + 'hide_call4action' => 'Hide this Lychee SE registration form. I am happy with Lychee as-is. :)', + 'hide_warning' => 'If enabled, the only way to register your license key will be via the More tab above. Changes are applied on page reload.', + ], + 'dropbox' => [ + 'header' => 'Dropbox', + 'instruction' => 'In order to import photos from your Dropbox, you need a valid drop-ins app key from their website.', + 'api_key' => 'Dropbox API Key', + 'set_key' => 'Set Dropbox Key', + ], + 'gallery' => [ + 'header' => 'Gallery', + 'photo_order_column' => 'Default column used for sorting photos', + 'photo_order_direction' => 'Default order used for sorting photos', + 'album_order_column' => 'Default column used for sorting albums', + 'album_order_direction' => 'Default order used for sorting albums', + 'aspect_ratio' => 'Default aspect ratio for album thumbs', + 'photo_layout' => 'Layout for pictures', + 'album_decoration' => 'Show decorations on album cover (sub-album and/or photo count)', + 'album_decoration_direction' => 'Align album decorations horizontally or vertically', + 'photo_overlay' => 'Default image overlay information', + 'license_default' => 'Default license used for albums', + 'license_help' => 'Need help choosing?', + ], + 'geolocation' => [ + 'header' => 'Geo-location', + 'map_display' => 'Display the map given GPS coordinates', + 'map_display_public' => 'Allow anonymous users to access the map', + 'map_provider' => 'Defines the map provider', + 'map_include_subalbums' => 'Includes pictures of the sub albums on the map', + 'location_decoding' => 'Use GPS location decoding', + 'location_show' => 'Show location extracted from GPS coordinates', + 'location_show_public' => 'Anonymous users can access the extracted location from GPS coordinates', + ], + 'advanced' => [ + 'header' => 'Advanced Customization', + 'change_css' => 'Change CSS', + 'change_js' => 'Change JS', + ], + 'all' => [ + 'old_setting_style' => 'Old setting style', + 'change_detected' => 'Some settings changed.', + 'save' => 'Save', + ], + + 'tool_option' => [ + 'disabled' => 'disabled', + 'enabled' => 'enabled', + 'discover' => 'discover', + ], +]; \ No newline at end of file diff --git a/lang/zh_TW/sharing.php b/lang/zh_TW/sharing.php new file mode 100644 index 00000000000..69de18cc6d0 --- /dev/null +++ b/lang/zh_TW/sharing.php @@ -0,0 +1,33 @@ + 'Sharing', + + 'info' => 'This page gives an overview of and the ability to edit the sharing rights associated with albums.', + 'album_title' => 'Album title', + 'username' => 'Username', + 'no_data' => 'Sharing list is empty.', + 'share' => 'Share', + 'permission_deleted' => 'Permission deleted!', + 'permission_created' => 'Permission created!', + + 'grants' => [ + 'read' => 'Grants read access', + 'original' => 'Grants access to original photo', + 'download' => 'Grants download', + 'upload' => 'Grants upload', + 'edit' => 'Grants edit', + 'delete' => 'Grants delete', + ], +]; \ No newline at end of file diff --git a/lang/zh_TW/statistics.php b/lang/zh_TW/statistics.php new file mode 100644 index 00000000000..2baf855bbd5 --- /dev/null +++ b/lang/zh_TW/statistics.php @@ -0,0 +1,34 @@ + 'Statistics', + + 'preview_text' => 'This is a preview of the statistics page available in Lychee SE.
The data shown here are randomly generated and do not reflect your server.', + 'no_data' => 'User does not have data on server.', + 'collapse' => 'Collapse albums sizes', + + 'total' => [ + 'total' => 'Total', + 'albums' => 'Albums', + 'photos' => 'Photos', + 'size' => 'Size', + ], + 'table' => [ + 'username' => 'Owner', + 'title' => 'Title', + 'photos' => 'Photos', + 'descendants' => 'Children', + 'size' => 'Size', + ], +]; \ No newline at end of file diff --git a/lang/zh_TW/toasts.php b/lang/zh_TW/toasts.php new file mode 100644 index 00000000000..293d4b72594 --- /dev/null +++ b/lang/zh_TW/toasts.php @@ -0,0 +1,17 @@ + 'Error', + 'success' => 'Success', +]; \ No newline at end of file diff --git a/lang/zh_TW/users.php b/lang/zh_TW/users.php new file mode 100644 index 00000000000..599bb833454 --- /dev/null +++ b/lang/zh_TW/users.php @@ -0,0 +1,44 @@ + 'Users', + 'description' => 'Here you can manage the users of your Lychee installation. You can create, edit and delete users.', + 'create' => 'Create a new user', + 'username' => 'Username', + 'password' => 'Password', + 'legend' => 'Legend', + 'upload_rights' => 'When selected, the user can upload content.', + 'edit_rights' => 'When selected, the user can modify their profile (username, password).', + 'quota' => 'When set, the user has a space quota for pictures (in kB).', + + 'user_deleted' => 'User deleted', + 'user_created' => 'User created', + 'user_updated' => 'User updated', + 'change_saved' => 'Change saved!', + + 'create_edit' => [ + 'upload_rights' => 'User can upload content.', + 'edit_rights' => 'User can modify their profile (username, password).', + 'quota' => 'User has quota limit.', + 'quota_kb' => 'quota in kB (0 for default)', + 'note' => 'Admin note (not publically visible)', + 'create' => 'Create', + 'edit' => 'Edit', + ], + 'line' => [ + 'admin' => 'admin user', + 'edit' => 'Edit', + 'delete' => 'Delete', + ], +]; \ No newline at end of file diff --git a/makefile b/makefile deleted file mode 100644 index 9cae671f2a6..00000000000 --- a/makefile +++ /dev/null @@ -1,120 +0,0 @@ -.PHONY: dist-gen dist-clean dist clean - -composer: - rm -r vendor 2> /dev/null || true - composer install --prefer-dist --no-dev - -dist-gen: clean composer - @echo "packaging..." - @mkdir Lychee - @mkdir Lychee/public - @mkdir Lychee/public/dist - @mkdir Lychee/public/img - @mkdir Lychee/public/uploads - @mkdir Lychee/public/uploads/small - @mkdir Lychee/public/uploads/medium - @mkdir Lychee/public/uploads/big - @mkdir Lychee/public/uploads/thumb - @mkdir Lychee/public/uploads/raw - @mkdir Lychee/public/uploads/import - @mkdir Lychee/public/sym - @cp -r app Lychee - @cp -r bootstrap Lychee - @cp -r config Lychee - @cp -r composer-cache Lychee - @cp -r database Lychee - @cp -r public/dist Lychee/public - @cp -r public/installer Lychee/public - @cp -r public/img/* Lychee/public/img - @cp -r public/.htaccess Lychee/public - @cp -r public/.user.ini Lychee/public - @cp -r public/favicon.ico Lychee/public - @cp -r public/index.php Lychee/public - @cp -r public/robots.txt Lychee/public - @cp -r public/web.config Lychee/public - @cp -r resources Lychee - @cp -r routes Lychee - @cp -r scripts Lychee - @cp -r storage Lychee - @cp -r vendor Lychee 2> /dev/null || true - @cp -r .env.example Lychee - @cp -r artisan Lychee - @cp -r composer.json Lychee - @cp -r composer.lock Lychee - @cp -r index.php Lychee - @cp -r LICENSE Lychee - @cp -r readme.md Lychee - @cp -r server.php Lychee - @cp -r simple_error_template.html Lychee - @cp -r version.md Lychee - @touch Lychee/storage/logs/laravel.log - @touch Lychee/public/dist/user.css - @touch Lychee/public/uploads/big/index.html - @touch Lychee/public/uploads/small/index.html - @touch Lychee/public/uploads/medium/index.html - @touch Lychee/public/uploads/thumb/index.html - @touch Lychee/public/uploads/raw/index.html - @touch Lychee/public/uploads/import/index.html - @touch Lychee/public/sym/index.html - -dist-clean: dist-gen - find Lychee -wholename '*/[Tt]ests/*' -delete - find Lychee -wholename '*/[Tt]est/*' -delete - @rm -r Lychee/storage/framework/cache/data/* 2> /dev/null || true - @rm Lychee/storage/framework/sessions/* 2> /dev/null || true - @rm Lychee/storage/framework/views/* 2> /dev/null || true - @rm Lychee/storage/logs/* 2> /dev/null || true - -dist: dist-clean - @zip -r Lychee.zip Lychee - -contrib_add: - @echo "npx all-contributors-cli add " - -contrib_generate: - npx all-contributors-cli generate - -contrib_check: - npx all-contributors-cli check - -clean: - @rm -r Lychee 2> /dev/null || true - -test: - @if [ -x "vendor/bin/phpunit" ]; then \ - ./vendor/bin/phpunit --verbose --stop-on-failure; \ - else \ - echo ""; \ - echo "Please install phpunit:"; \ - echo ""; \ - echo " composer install"; \ - echo ""; \ - fi - -formatting: - @rm .php_cs.cache 2> /dev/null || true - @if [ -x "vendor/bin/php-cs-fixer" ]; then \ - ./vendor/bin/php-cs-fixer fix -v --config=.php_cs; \ - else \ - echo ""; \ - echo "Please install php-cs-fixer:"; \ - echo ""; \ - echo " composer install"; \ - echo ""; \ - fi - -gen_minor: - php gen_release.php - git add database - git add version.md - -release_minor: gen_minor - git commit -S -m "bump to version $(shell cat version.md)" - -gen_major: - php gen_release.php major - git add database - git add version.md - -release_major: gen_major - git commit -m "bump to version $(shell cat version.md)" diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000000..aea2c1719ca --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4784 @@ +{ + "name": "Lychee", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@fortawesome/fontawesome-free": "^6.7.1", + "@primevue/themes": "^4.2.4", + "@sidsbrmnn/scrollspy": "^1.1.0", + "@types/dropbox-chooser": "^1.0.8", + "@types/justified-layout": "^4.1.4", + "@vueuse/core": "^12.4.0", + "autoprefixer": "^10.4.19", + "axios": "^1.7.9", + "axios-cache-interceptor": "^1.5.3", + "justified-layout": "^4.1.0", + "laravel-vite-plugin": "^1.1.1", + "laravel-vue-i18n": "^2.7.8", + "leaflet": "^1.9.4", + "leaflet-gpx": "^2.1.2", + "leaflet-rotatedmarker": "^0.2.0", + "leaflet.markercluster": "^1.5.3", + "pinia": "^2.2.8", + "postcss": "^8.5.1", + "prettier": "^3.4.2", + "primeicons": "^7.0.0", + "primevue": "^4.2.5", + "qrcode": "^1.5.3", + "sprintf-js": "^1.1.3", + "tailwindcss": "^3.4.16", + "tailwindcss-primeui": "^0.4.0", + "tinygesture": "^3.0.0", + "ts-loader": "^9.5.2", + "typescript": "^5.7", + "vite": "^6.0.5", + "vite-plugin-commonjs": "^0.10.4", + "vue": "^3.5.13", + "vue-collapsed": "^1.3.3", + "vue-i18n": "^11.0.1", + "vue-router": "^4.5.0" + }, + "devDependencies": { + "@lychee-org/leaflet.photo": "^1.0.0", + "@popperjs/core": "^2.11.6", + "@tailwindcss/typography": "^0.5.16", + "@types/leaflet": "^1.9.16", + "@types/leaflet-rotatedmarker": "^0.2.5", + "@types/leaflet.markercluster": "^1.5.5", + "@types/mousetrap": "^1.6.15", + "@types/node": "^22.10.6", + "@types/photoswipe": "^4.1.6", + "@types/qrcode": "^1.5.5", + "@types/sprintf-js": "^1.1.4", + "@vitejs/plugin-vue": "^5.1.5", + "sass": "^1.83.4", + "vue-tsc": "^2.1.8" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz", + "integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.3" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", + "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", + "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", + "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", + "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", + "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", + "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", + "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", + "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", + "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", + "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", + "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", + "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", + "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", + "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", + "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", + "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", + "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", + "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", + "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", + "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", + "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", + "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", + "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", + "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", + "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", + "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fortawesome/fontawesome-free": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.7.2.tgz", + "integrity": "sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA==", + "license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)", + "engines": { + "node": ">=6" + } + }, + "node_modules/@intlify/core-base": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.0.1.tgz", + "integrity": "sha512-NAmhw1l/llM0HZRpagR/ChJTNymW4ll6/4EDSJML5c8L5Hl/+k6UyF8EIgE6DeHpfheQujkSRngauViHqq6jJQ==", + "license": "MIT", + "dependencies": { + "@intlify/message-compiler": "11.0.1", + "@intlify/shared": "11.0.1" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/message-compiler": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.0.1.tgz", + "integrity": "sha512-5RFH8x+Mn3mbjcHXnb6KCXGiczBdiQkWkv99iiA0JpKrNuTAQeW59Pjq/uObMB0eR0shnKYGTkIJxum+DbL3sw==", + "license": "MIT", + "dependencies": { + "@intlify/shared": "11.0.1", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/shared": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.0.1.tgz", + "integrity": "sha512-lH164+aDDptHZ3dBDbIhRa1dOPQUp+83iugpc+1upTOWCnwyC1PVis6rSWNMMJ8VQxvtHQB9JMib48K55y0PvQ==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@lychee-org/leaflet.photo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lychee-org/leaflet.photo/-/leaflet.photo-1.0.0.tgz", + "integrity": "sha512-0nUnOvcVxFdwJ7iqdpuZVT7t7ZLwluOTaRGZuGSf0KRd0f1Ebb6fed7uWaSa/TD+mXQIYagsoBtWYG0lxMakKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.0.tgz", + "integrity": "sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.0", + "@parcel/watcher-darwin-arm64": "2.5.0", + "@parcel/watcher-darwin-x64": "2.5.0", + "@parcel/watcher-freebsd-x64": "2.5.0", + "@parcel/watcher-linux-arm-glibc": "2.5.0", + "@parcel/watcher-linux-arm-musl": "2.5.0", + "@parcel/watcher-linux-arm64-glibc": "2.5.0", + "@parcel/watcher-linux-arm64-musl": "2.5.0", + "@parcel/watcher-linux-x64-glibc": "2.5.0", + "@parcel/watcher-linux-x64-musl": "2.5.0", + "@parcel/watcher-win32-arm64": "2.5.0", + "@parcel/watcher-win32-ia32": "2.5.0", + "@parcel/watcher-win32-x64": "2.5.0" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.0.tgz", + "integrity": "sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.0.tgz", + "integrity": "sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.0.tgz", + "integrity": "sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.0.tgz", + "integrity": "sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.0.tgz", + "integrity": "sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.0.tgz", + "integrity": "sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.0.tgz", + "integrity": "sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.0.tgz", + "integrity": "sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.0.tgz", + "integrity": "sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.0.tgz", + "integrity": "sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.0.tgz", + "integrity": "sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.0.tgz", + "integrity": "sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.0.tgz", + "integrity": "sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@primeuix/styled": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@primeuix/styled/-/styled-0.3.2.tgz", + "integrity": "sha512-ColZes0+/WKqH4ob2x8DyNYf1NENpe5ZguOvx5yCLxaP8EIMVhLjWLO/3umJiDnQU4XXMLkn2mMHHw+fhTX/mw==", + "license": "MIT", + "dependencies": { + "@primeuix/utils": "^0.3.2" + }, + "engines": { + "node": ">=12.11.0" + } + }, + "node_modules/@primeuix/utils": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@primeuix/utils/-/utils-0.3.2.tgz", + "integrity": "sha512-B+nphqTQeq+i6JuICLdVWnDMjONome2sNz0xI65qIOyeB4EF12CoKRiCsxuZ5uKAkHi/0d1LqlQ9mIWRSdkavw==", + "license": "MIT", + "engines": { + "node": ">=12.11.0" + } + }, + "node_modules/@primevue/core": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@primevue/core/-/core-4.2.5.tgz", + "integrity": "sha512-+oWBIQs5dLd2Ini4KEVOlvPILk989EHAskiFS3R/dz3jeOllJDMZFcSp8V9ddV0R3yDaPdLVkfHm2Q5t42kU2Q==", + "license": "MIT", + "dependencies": { + "@primeuix/styled": "^0.3.2", + "@primeuix/utils": "^0.3.2" + }, + "engines": { + "node": ">=12.11.0" + }, + "peerDependencies": { + "vue": "^3.3.0" + } + }, + "node_modules/@primevue/icons": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@primevue/icons/-/icons-4.2.5.tgz", + "integrity": "sha512-WFbUMZhQkXf/KmwcytkjGVeJ9aGEDXjP3uweOjQZMmRdEIxFnqYYpd90wE90JE1teZn3+TVnT4ZT7ejGyEXnFQ==", + "license": "MIT", + "dependencies": { + "@primeuix/utils": "^0.3.2", + "@primevue/core": "4.2.5" + }, + "engines": { + "node": ">=12.11.0" + } + }, + "node_modules/@primevue/themes": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@primevue/themes/-/themes-4.2.5.tgz", + "integrity": "sha512-8F7yA36xYIKtNuAuyBdZZEks/bKDwlhH5WjpqGGB0FdwfAEoBYsynQ5sdqcT2Lb/NsajHmS5lc++Ttlvr1g1Lw==", + "license": "MIT", + "dependencies": { + "@primeuix/styled": "^0.3.2" + }, + "engines": { + "node": ">=12.11.0" + } + }, + "node_modules/@sidsbrmnn/scrollspy": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@sidsbrmnn/scrollspy/-/scrollspy-1.1.0.tgz", + "integrity": "sha512-xLZdHlnPX3vlc/+SBxLLIIG/IXSaMpNTU0+AgUCwSQLpeWppn0IVtToLSV6o3GWoYakuofy1sE01xsEl6atzCA==", + "license": "MIT" + }, + "node_modules/@tailwindcss/typography": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz", + "integrity": "sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==", + "dev": true, + "dependencies": { + "lodash.castarray": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2", + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" + } + }, + "node_modules/@types/dropbox-chooser": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/dropbox-chooser/-/dropbox-chooser-1.0.8.tgz", + "integrity": "sha512-iXAJPSOgucUTAVOYipdzbA4VHFJ9AiAWBdAq0/fR+W3DUcPCH7lGZ2TAonK2WX4vYkSY7omLwfggX591HhsEVQ==", + "license": "MIT" + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "license": "MIT" + }, + "node_modules/@types/geojson": { + "version": "7946.0.15", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.15.tgz", + "integrity": "sha512-9oSxFzDCT2Rj6DfcHF8G++jxBKS7mBqXl5xrRW+Kbvjry6Uduya2iiwqHPhVXpasAVMBYKkEPGgKhd3+/HZ6xA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/justified-layout": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@types/justified-layout/-/justified-layout-4.1.4.tgz", + "integrity": "sha512-q2ybP0u0NVj87oMnGZOGxY2iUN8ddr48zPOBHBdbOLpsMTA/keGj+93ou+OMCnJk0xewzlNIaVEkxM6VBD3E2w==", + "license": "MIT" + }, + "node_modules/@types/leaflet": { + "version": "1.9.16", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.16.tgz", + "integrity": "sha512-wzZoyySUxkgMZ0ihJ7IaUIblG8Rdc8AbbZKLneyn+QjYsj5q1QU7TEKYqwTr10BGSzY5LI7tJk9Ifo+mEjdFRw==", + "dev": true, + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/leaflet-rotatedmarker": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@types/leaflet-rotatedmarker/-/leaflet-rotatedmarker-0.2.5.tgz", + "integrity": "sha512-GaKK1bdQ6NYGkVdZj2cHe8Eu1BVf40Jhtmd8pZj5gQSJcTy5iTog0hsMIhf6QQDKnaEgrRJzm4OES6B9vxi4dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/leaflet": "*" + } + }, + "node_modules/@types/leaflet.markercluster": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@types/leaflet.markercluster/-/leaflet.markercluster-1.5.5.tgz", + "integrity": "sha512-TkWOhSHDM1ANxmLi+uK0PjsVcjIKBr8CLV2WoF16dIdeFmC0Cj5P5axkI3C1Xsi4+ht6EU8+BfEbbqEF9icPrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/leaflet": "*" + } + }, + "node_modules/@types/mousetrap": { + "version": "1.6.15", + "resolved": "https://registry.npmjs.org/@types/mousetrap/-/mousetrap-1.6.15.tgz", + "integrity": "sha512-qL0hyIMNPow317QWW/63RvL1x5MVMV+Ru3NaY9f/CuEpCqrmb7WeuK2071ZY5hczOnm38qExWM2i2WtkXLSqFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.10.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.6.tgz", + "integrity": "sha512-qNiuwC4ZDAUNcY47xgaSuS92cjf8JbSUoaKS77bmLG1rU7MlATVSiw/IlrjtIyyskXBZ8KkNfjK/P5na7rgXbQ==", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/photoswipe": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@types/photoswipe/-/photoswipe-4.1.6.tgz", + "integrity": "sha512-6kN4KYjNF4sg79fSwZ46s4Pron4+YJxoW0DQOcHveUZc/3cWd8Q4B9OLlDmEYw9iI6fODU8kyyq8ZBy+8F/+zQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/qrcode": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.5.tgz", + "integrity": "sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/sprintf-js": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/sprintf-js/-/sprintf-js-1.1.4.tgz", + "integrity": "sha512-aWK1reDYWxcjgcIIPmQi3u+OQDuYa9b+lr6eIsGWrekJ9vr1NSjr4Eab8oQ1iKuH1ltFHpXGyerAv1a3FMKxzQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.1.tgz", + "integrity": "sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.11.tgz", + "integrity": "sha512-lN2C1+ByfW9/JRPpqScuZt/4OrUUse57GLI6TbLgTIqBVemdl1wNcZ1qYGEo2+Gw8coYLgCy7SuKqn6IrQcQgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.11" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.11.tgz", + "integrity": "sha512-ZQpmafIGvaZMn/8iuvCFGrW3smeqkq/IIh9F1SdSx9aUl0J4Iurzd6/FhmjNO5g2ejF3rT45dKskgXWiofqlZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.11.tgz", + "integrity": "sha512-2DT+Tdh88Spp5PyPbqhyoYavYCPDsqbHLFwcUI9K1NlY1YgUJvujGdrqUp0zWxnW7KWNTr3xSpMuv2WnaTKDAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.11", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz", + "integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.3", + "@vue/shared": "3.5.13", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz", + "integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.13", + "@vue/shared": "3.5.13" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz", + "integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.3", + "@vue/compiler-core": "3.5.13", + "@vue/compiler-dom": "3.5.13", + "@vue/compiler-ssr": "3.5.13", + "@vue/shared": "3.5.13", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.11", + "postcss": "^8.4.48", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz", + "integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.13", + "@vue/shared": "3.5.13" + } + }, + "node_modules/@vue/compiler-vue2": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", + "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", + "dev": true, + "license": "MIT", + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/language-core": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.0.tgz", + "integrity": "sha512-O1ZZFaaBGkKbsRfnVH1ifOK1/1BUkyK+3SQsfnh6PmMmD4qJcTU8godCeA96jjDRTL6zgnK7YzCHfaUlH2r0Mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "~2.4.11", + "@vue/compiler-dom": "^3.5.0", + "@vue/compiler-vue2": "^2.7.16", + "@vue/shared": "^3.5.0", + "alien-signals": "^0.4.9", + "minimatch": "^9.0.3", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz", + "integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.13" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz", + "integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.13", + "@vue/shared": "3.5.13" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz", + "integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.13", + "@vue/runtime-core": "3.5.13", + "@vue/shared": "3.5.13", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz", + "integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.13", + "@vue/shared": "3.5.13" + }, + "peerDependencies": { + "vue": "3.5.13" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz", + "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==", + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.4.0.tgz", + "integrity": "sha512-XnjQYcJwCsyXyIafyA6SvyN/OBtfPnjvJmbxNxQjCcyWD198urwm5TYvIUUyAxEAN0K7HJggOgT15cOlWFyLeA==", + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "12.4.0", + "@vueuse/shared": "12.4.0", + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/metadata": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.4.0.tgz", + "integrity": "sha512-AhPuHs/qtYrKHUlEoNO6zCXufu8OgbR8S/n2oMw1OQuBQJ3+HOLQ+EpvXs+feOlZMa0p8QVvDWNlmcJJY8rW2g==", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.4.0.tgz", + "integrity": "sha512-9yLgbHVIF12OSCojnjTIoZL1+UA10+O4E1aD6Hpfo/DKVm5o3SZIwz6CupqGy3+IcKI8d6Jnl26EQj/YucnW0Q==", + "dependencies": { + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "peer": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT", + "peer": true + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/alien-signals": { + "version": "0.4.12", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-0.4.12.tgz", + "integrity": "sha512-Og0PgAihxlp1R22bsoBsyhhMG4+qhU+fkkLPoGBQkYVc3qt9rYnrwYTf+M6kqUqUZpf3rXDnpL90iKa0QcSVVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axios": { + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios-cache-interceptor": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/axios-cache-interceptor/-/axios-cache-interceptor-1.6.2.tgz", + "integrity": "sha512-YLbAODIHZZIcD4b3WYFVQOa5W2TY/WnJ6sBHqAg6Z+hx+RVj8/OcjQyRopO6awn7/kOkGL5X9TP16AucnlJ/lw==", + "license": "MIT", + "dependencies": { + "cache-parser": "1.2.5", + "fast-defer": "1.1.8", + "object-code": "1.3.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/arthurfiorette/axios-cache-interceptor?sponsor=1" + }, + "peerDependencies": { + "axios": "^1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz", + "integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT", + "peer": true + }, + "node_modules/cache-parser": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/cache-parser/-/cache-parser-1.2.5.tgz", + "integrity": "sha512-Md/4VhAHByQ9frQ15WD6LrMNiVw9AEl/J7vWIXw+sxT6fSOpbtt6LHTp76vy8+bOESPBO94117Hm2bIjlI7XjA==", + "license": "MIT" + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001690", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz", + "integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "license": "Apache-2.0" + }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "license": "MIT" + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.76", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.76.tgz", + "integrity": "sha512-CjVQyG7n7Sr+eBXE86HIulnL5N8xZY1sgmOPGuq/F0Rr0FJq63lg0kEtOIDfZBk44FnDLf6FUJ+dsJcuiUDdDQ==", + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz", + "integrity": "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", + "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", + "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.24.2", + "@esbuild/android-arm": "0.24.2", + "@esbuild/android-arm64": "0.24.2", + "@esbuild/android-x64": "0.24.2", + "@esbuild/darwin-arm64": "0.24.2", + "@esbuild/darwin-x64": "0.24.2", + "@esbuild/freebsd-arm64": "0.24.2", + "@esbuild/freebsd-x64": "0.24.2", + "@esbuild/linux-arm": "0.24.2", + "@esbuild/linux-arm64": "0.24.2", + "@esbuild/linux-ia32": "0.24.2", + "@esbuild/linux-loong64": "0.24.2", + "@esbuild/linux-mips64el": "0.24.2", + "@esbuild/linux-ppc64": "0.24.2", + "@esbuild/linux-riscv64": "0.24.2", + "@esbuild/linux-s390x": "0.24.2", + "@esbuild/linux-x64": "0.24.2", + "@esbuild/netbsd-arm64": "0.24.2", + "@esbuild/netbsd-x64": "0.24.2", + "@esbuild/openbsd-arm64": "0.24.2", + "@esbuild/openbsd-x64": "0.24.2", + "@esbuild/sunos-x64": "0.24.2", + "@esbuild/win32-arm64": "0.24.2", + "@esbuild/win32-ia32": "0.24.2", + "@esbuild/win32-x64": "0.24.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT", + "peer": true + }, + "node_modules/fast-defer": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/fast-defer/-/fast-defer-1.1.8.tgz", + "integrity": "sha512-lEJeOH5VL5R09j6AA0D4Uvq7AgsHw0dAImQQ+F3iSyHZuAxyQfWobsagGpTcOPvJr3urmKRHrs+Gs9hV+/Qm/Q==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT", + "peer": true + }, + "node_modules/fast-uri": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.5.tgz", + "integrity": "sha512-5JnBCWpFlMo0a3ciDy/JckMzzv1U9coZrIhedq+HXxxUfDTAiS0LA8OKVao4G9BxmCVck/jtA5r3KAtRWEyD8Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/fastq": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz", + "integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "license": "BSD-2-Clause", + "peer": true + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/immutable": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz", + "integrity": "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT", + "peer": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT", + "peer": true + }, + "node_modules/justified-layout": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/justified-layout/-/justified-layout-4.1.0.tgz", + "integrity": "sha512-M5FimNMXgiOYerVRGsXZ2YK9YNCaTtwtYp7Hb2308U1Q9TXXHx5G0p08mcVR5O53qf8bWY4NJcPBxE6zuayXSg==", + "license": "ISC" + }, + "node_modules/laravel-vite-plugin": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-1.1.1.tgz", + "integrity": "sha512-HMZXpoSs1OR+7Lw1+g4Iy/s3HF3Ldl8KxxYT2Ot8pEB4XB/QRuZeWgDYJdu552UN03YRSRNK84CLC9NzYRtncA==", + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "vite-plugin-full-reload": "^1.1.0" + }, + "bin": { + "clean-orphaned-assets": "bin/clean.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0" + } + }, + "node_modules/laravel-vue-i18n": { + "version": "2.7.8", + "resolved": "https://registry.npmjs.org/laravel-vue-i18n/-/laravel-vue-i18n-2.7.8.tgz", + "integrity": "sha512-HY4yZl1fqApzJzoLFGcmiBusHIouZgT+grMtTCQu3gkilEbcqPOt6L4ksfpkZOYppPQkUlnXLqN8nDNc0gyLUQ==", + "license": "MIT", + "dependencies": { + "php-parser": "3.1.3", + "vue": "^3.2.45" + } + }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", + "license": "BSD-2-Clause" + }, + "node_modules/leaflet-gpx": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/leaflet-gpx/-/leaflet-gpx-2.1.2.tgz", + "integrity": "sha512-lKoEPlAWel9KXn9keg6Dmyt7gmj5IYyD8CKuxivN+77GpZr2bpKliwFvZJxLUHmNu4fICmCySyxhm5qjZuvvQg==", + "license": "BSD-2-Clause" + }, + "node_modules/leaflet-rotatedmarker": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/leaflet-rotatedmarker/-/leaflet-rotatedmarker-0.2.0.tgz", + "integrity": "sha512-yc97gxLXwbZa+Gk9VCcqI0CkvIBC9oNTTjFsHqq4EQvANrvaboib4UdeQLyTnEqDpaXHCqzwwVIDHtvz2mUiDg==", + "license": "MIT" + }, + "node_modules/leaflet.markercluster": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/leaflet.markercluster/-/leaflet.markercluster-1.5.3.tgz", + "integrity": "sha512-vPTw/Bndq7eQHjLBVlWpnGeLa3t+3zGiuM7fJwCkiMFq+nmRuG3RI3f7f4N4TDX7T4NpbAXpR2+NTRSEGfCSeA==", + "license": "MIT", + "peerDependencies": { + "leaflet": "^1.3.1" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.castarray": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", + "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT", + "peer": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT", + "peer": true + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-code": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/object-code/-/object-code-1.3.3.tgz", + "integrity": "sha512-/Ds4Xd5xzrtUOJ+xJQ57iAy0BZsZltOHssnDgcZ8DOhgh41q1YJCnTPnWdWSLkNGNnxYzhYChjc5dgC9mEERCA==", + "license": "MIT" + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/php-parser": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/php-parser/-/php-parser-3.1.3.tgz", + "integrity": "sha512-hPvBmnRYPqWEtMfIFOlyjQv1q75UUtxt4U+YscKIQViGmEE2Xa4BuS1B1/cZdjy7MVcwtnr0WkEsr915LgRKOw==", + "license": "BSD-3-Clause" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinia": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.0.tgz", + "integrity": "sha512-ohZj3jla0LL0OH5PlLTDMzqKiVw2XARmC1XYLdLWIPBMdhDW/123ZWr4zVAhtJm+aoSkFa13pYXskAvAscIkhQ==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.4.4", + "vue": "^2.7.0 || ^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/postcss": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", + "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-nested/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "node_modules/prettier": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", + "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/primeicons": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/primeicons/-/primeicons-7.0.0.tgz", + "integrity": "sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw==", + "license": "MIT" + }, + "node_modules/primevue": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/primevue/-/primevue-4.2.5.tgz", + "integrity": "sha512-7UMOIJvdFz4jQyhC76yhNdSlHtXvVpmE2JSo2ndUTBWjWJOkYyT562rQ4ayO+bMdJLtzBGqgY64I9ZfEvNd7vQ==", + "license": "MIT", + "dependencies": { + "@primeuix/styled": "^0.3.2", + "@primeuix/utils": "^0.3.2", + "@primevue/core": "4.2.5", + "@primevue/icons": "4.2.5" + }, + "engines": { + "node": ">=12.11.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "name": "@rollup/wasm-node", + "version": "4.30.0", + "resolved": "https://registry.npmjs.org/@rollup/wasm-node/-/wasm-node-4.30.0.tgz", + "integrity": "sha512-fRkB9VoRK/rWFVMw3eaBz8x3I74xoX9HXM01yM4qmm7Uptzq/jM8TJZEJPBGqyMZtEhU6HORUAOPX38wmXJj1g==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peer": true + }, + "node_modules/sass": { + "version": "1.83.4", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.83.4.tgz", + "integrity": "sha512-B1bozCeNQiOgDcLd33e2Cs2U60wZwjUUXzh900ZyQF5qUasvMdDZYbQ566LJu7cqR+sAHlAfO6RMkaID5s6qpA==", + "devOptional": true, + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "peer": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.6", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss-primeui": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/tailwindcss-primeui/-/tailwindcss-primeui-0.4.0.tgz", + "integrity": "sha512-YYC7B7Yyzm1/4pEGgpf1ABAhbrKY++LuPoUamnKE7fTPO5Ct/Qr/dT+Uq2yiVhQnaW1zHQpYnThxfksaxhlDfQ==", + "peerDependencies": { + "tailwindcss": ">=3.1.0" + } + }, + "node_modules/tailwindcss/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/tailwindcss/node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tailwindcss/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tailwindcss/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/terser": { + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz", + "integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.11", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.11.tgz", + "integrity": "sha512-RVCsMfuD0+cTt3EwX8hSl2Ks56EbFHWmhluwcqoPKtBnfjiT6olaq7PRIRfhyU8nnC2MrnDrBLfrD/RGE+cVXQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT", + "peer": true + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", + "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT", + "peer": true + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinygesture": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tinygesture/-/tinygesture-3.0.0.tgz", + "integrity": "sha512-UawUggtPCHy+N65ULpR/i6VLH8AzB7jWVvTNoXRFTJNh+ub6lP/SJCxzV/Ua7sJbCt9U9I79wKkKk3wbjcLdbQ==", + "license": "Apache-2.0" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "license": "Apache-2.0" + }, + "node_modules/ts-loader": { + "version": "9.5.2", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.2.tgz", + "integrity": "sha512-Qo4piXvOTWcMGIgRiuFa6nHNm+54HbYaZCKqc9eeZCLRy3XqafQgwX2F7mofrbJG3g7EEb+lkiR+z2Lic2s3Zw==", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vite": { + "version": "6.0.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.7.tgz", + "integrity": "sha512-RDt8r/7qx9940f8FcOIAH9PTViRrghKaK2K1jY3RaAURrEUbm9Du1mJ72G+jlhtG3WwodnfzY8ORQZbBavZEAQ==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.24.2", + "postcss": "^8.4.49", + "rollup": "^4.23.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-plugin-commonjs": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/vite-plugin-commonjs/-/vite-plugin-commonjs-0.10.4.tgz", + "integrity": "sha512-eWQuvQKCcx0QYB5e5xfxBNjQKyrjEWZIR9UOkOV6JAgxVhtbZvCOF+FNC2ZijBJ3U3Px04ZMMyyMyFBVWIJ5+g==", + "license": "MIT", + "dependencies": { + "acorn": "^8.12.1", + "magic-string": "^0.30.11", + "vite-plugin-dynamic-import": "^1.6.0" + } + }, + "node_modules/vite-plugin-dynamic-import": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/vite-plugin-dynamic-import/-/vite-plugin-dynamic-import-1.6.0.tgz", + "integrity": "sha512-TM0sz70wfzTIo9YCxVFwS8OA9lNREsh+0vMHGSkWDTZ7bgd1Yjs5RV8EgB634l/91IsXJReg0xtmuQqP0mf+rg==", + "license": "MIT", + "dependencies": { + "acorn": "^8.12.1", + "es-module-lexer": "^1.5.4", + "fast-glob": "^3.3.2", + "magic-string": "^0.30.11" + } + }, + "node_modules/vite-plugin-full-reload": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vite-plugin-full-reload/-/vite-plugin-full-reload-1.2.0.tgz", + "integrity": "sha512-kz18NW79x0IHbxRSHm0jttP4zoO9P9gXh+n6UTwlNKnviTTEpOlum6oS9SmecrTtSr+muHEn5TUuC75UovQzcA==", + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "picomatch": "^2.3.1" + } + }, + "node_modules/vscode-uri": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz", + "integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.13", + "@vue/compiler-sfc": "3.5.13", + "@vue/runtime-dom": "3.5.13", + "@vue/server-renderer": "3.5.13", + "@vue/shared": "3.5.13" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-collapsed": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/vue-collapsed/-/vue-collapsed-1.3.4.tgz", + "integrity": "sha512-W92b+QT3n5iTrfrH6kyvx3TsriYPfy/Ymsb6DaanjeMkJYMdVl1S4wzKRYVDsxWQPDxiC+5m+UwPwT/8YAYodA==", + "license": "MIT" + }, + "node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-i18n": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.0.1.tgz", + "integrity": "sha512-pWAT8CusK8q9/EpN7V3oxwHwxWm6+Kp2PeTZmRGvdZTkUzMQDpbbmHp0TwQ8xw04XKm23cr6B4GL72y3W8Yekg==", + "license": "MIT", + "dependencies": { + "@intlify/core-base": "11.0.1", + "@intlify/shared": "11.0.1", + "@vue/devtools-api": "^6.5.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/vue-router": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.0.tgz", + "integrity": "sha512-HDuk+PuH5monfNuY+ct49mNmkCRK4xJAV9Ts4z9UFc4rzdDnxQLyCMGGc8pKhZhHTVzfanpNwB/lwqevcBwI4w==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/vue-tsc": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.0.tgz", + "integrity": "sha512-gtmM1sUuJ8aSb0KoAFmK9yMxb8TxjewmxqTJ1aKphD5Cbu0rULFY6+UQT51zW7SpUcenfPUuflKyVwyx9Qdnxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "~2.4.11", + "@vue/language-core": "2.2.0" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, + "node_modules/watchpack": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", + "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", + "license": "MIT", + "peer": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.97.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", + "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", + "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/package.json b/package.json index b12978aaab4..804f2fed6f8 100644 --- a/package.json +++ b/package.json @@ -1,21 +1,69 @@ { "private": true, + "type": "module", "scripts": { - "dev": "npm run development", - "development": "mix", - "watch": "mix watch", - "watch-poll": "mix watch -- --watch-options-poll=1000", - "hot": "mix watch --hot", - "prod": "npm run production", - "production": "mix --production" + "check": "vue-tsc --noEmit -p tsconfig.json --composite false --skipLibCheck", + "dev": "vite", + "build": "vite build", + "check-formatting": "prettier --check resources/js/ resources/sass/", + "format": "prettier --write resources/js/ resources/sass/" }, "devDependencies": { - "axios": "^0.21", - "laravel-mix": "^6.0.6", - "lodash": "^4.17.19", - "postcss": "^8.1.14", - "resolve-url-loader": "^3.1.2", - "sass": "^1.32.11", - "sass-loader": "^11.0.1" + "@lychee-org/leaflet.photo": "^1.0.0", + "@popperjs/core": "^2.11.6", + "@tailwindcss/typography": "^0.5.16", + "@types/leaflet": "^1.9.16", + "@types/leaflet-rotatedmarker": "^0.2.5", + "@types/leaflet.markercluster": "^1.5.5", + "@types/mousetrap": "^1.6.15", + "@types/node": "^22.10.6", + "@types/photoswipe": "^4.1.6", + "@types/qrcode": "^1.5.5", + "@types/sprintf-js": "^1.1.4", + "@vitejs/plugin-vue": "^5.1.5", + "sass": "^1.83.4", + "vue-tsc": "^2.1.8" + }, + "dependencies": { + "@fortawesome/fontawesome-free": "^6.7.1", + "@primevue/themes": "^4.2.4", + "@sidsbrmnn/scrollspy": "^1.1.0", + "@types/dropbox-chooser": "^1.0.8", + "@types/justified-layout": "^4.1.4", + "@vueuse/core": "^12.4.0", + "autoprefixer": "^10.4.19", + "axios": "^1.7.9", + "axios-cache-interceptor": "^1.5.3", + "justified-layout": "^4.1.0", + "laravel-vite-plugin": "^1.1.1", + "laravel-vue-i18n": "^2.7.8", + "leaflet": "^1.9.4", + "leaflet-gpx": "^2.1.2", + "leaflet-rotatedmarker": "^0.2.0", + "leaflet.markercluster": "^1.5.3", + "pinia": "^2.2.8", + "postcss": "^8.5.1", + "prettier": "^3.4.2", + "primeicons": "^7.0.0", + "primevue": "^4.2.5", + "qrcode": "^1.5.3", + "sprintf-js": "^1.1.3", + "tailwindcss": "^3.4.16", + "tailwindcss-primeui": "^0.4.0", + "tinygesture": "^3.0.0", + "ts-loader": "^9.5.2", + "typescript": "^5.7", + "vite": "^6.0.5", + "vite-plugin-commonjs": "^0.10.4", + "vue": "^3.5.13", + "vue-collapsed": "^1.3.3", + "vue-i18n": "^11.0.1", + "vue-router": "^4.5.0" + }, + "browserslist": [ + "defaults and fully supports es6-module" + ], + "overrides": { + "rollup": "npm:@rollup/wasm-node" } } diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 00000000000..6f5bceed5da --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,176 @@ +includes: + - vendor/larastan/larastan/extension.neon + - vendor/lychee-org/phpstan-lychee/phpstan.neon + +parameters: + treatPhpDocTypesAsCertain: false + paths: + - app + - scripts + - lang + - database/migrations + - tests + - config + excludePaths: + - database/migrations/2021_12_04_181200_refactor_models.php + stubFiles: + # these can be removed after https://github.com/thecodingmachine/safe/issues/283 has been merged + - phpstan/stubs/Wireable.stub + - phpstan/stubs/image.stub + - phpstan/stubs/imageexception.stub + ignoreErrors: + # False positive php8.2 + phpstan on interface @property + - '#Access to an undefined property App\\Contracts\\Models\\AbstractAlbum::\$id#' + - '#Access to an undefined property App\\Contracts\\Models\\AbstractAlbum::\$title#' + - '#Access to an undefined property App\\Contracts\\Models\\AbstractAlbum::\$thumb#' + - '#Access to an undefined property App\\Contracts\\Models\\AbstractAlbum::\$photos#' + - '#Access to an undefined property App\\Contracts\\Image\\StreamStats::\$checksum#' + - '#Access to an undefined property App\\Contracts\\Image\\StreamStats::\$bytes#' + + # bunch of false positives from Eloquent + - '#Dynamic call to static method (Illuminate\\Database\\Query\\Builder|Illuminate\\Database\\Eloquent\\(Builder|Relations\\.*)|App\\Models\\Builders\\.*|App\\Eloquent\\FixedQueryBuilder|App\\Relations\\.*)(<.*>)?::from\(\).#' + - '#Dynamic call to static method (Illuminate\\Database\\Query\\Builder|Illuminate\\Database\\Eloquent\\(Builder|Relations\\.*)|App\\Models\\Builders\\.*|App\\Eloquent\\FixedQueryBuilder|App\\Relations\\.*)(<.*>)?::limit\(\).#' + - '#Dynamic call to static method (Illuminate\\Database\\Query\\Builder|Illuminate\\Database\\Eloquent\\(Builder|Relations\\.*)|App\\Models\\Builders\\.*|App\\Eloquent\\FixedQueryBuilder|App\\Relations\\.*)(<.*>)?::offset\(\).#' + - '#Dynamic call to static method (Illuminate\\Database\\Query\\Builder|Illuminate\\Database\\Eloquent\\(Builder|Relations\\.*)|App\\Models\\Builders\\.*|App\\Eloquent\\FixedQueryBuilder|App\\Relations\\.*)(<.*>)?::take\(\).#' + - '#Dynamic call to static method (Illuminate\\Database\\Query\\Builder|Illuminate\\Database\\Eloquent\\(Builder|Relations\\.*)|App\\Models\\Builders\\.*|App\\Eloquent\\FixedQueryBuilder|App\\Relations\\.*)(<.*>)?::truncate\(\).#' + - '#Dynamic call to static method (Illuminate\\Database\\Query\\Builder|Illuminate\\Database\\Eloquent\\(Builder|Relations\\.*)|App\\Models\\Builders\\.*|App\\Eloquent\\FixedQueryBuilder|App\\Relations\\.*)(<.*>)?::insert\(\).#' + - '#Dynamic call to static method (Illuminate\\Database\\Query\\Builder|Illuminate\\Database\\Eloquent\\(Builder|Relations\\.*)|App\\Models\\Builders\\.*|App\\Eloquent\\FixedQueryBuilder|App\\Relations\\.*)(<.*>)?::select\(\).#' + - '#Dynamic call to static method (Illuminate\\Database\\Query\\Builder|Illuminate\\Database\\Eloquent\\(Builder|Relations\\.*)|App\\Models\\Builders\\.*|App\\Eloquent\\FixedQueryBuilder|App\\Relations\\.*)(<.*>)?::orderBy\(\)#' + - '#Dynamic call to static method (Illuminate\\Database\\Query\\Builder|Illuminate\\Database\\Eloquent\\(Builder|Relations\\.*)|App\\Models\\Builders\\.*|App\\Eloquent\\FixedQueryBuilder|App\\Relations\\.*)(<.*>)?::where(Not)?(Null|In|Between|Exists|Column|Year|Month|Day)?\(\).#' + - '#Dynamic call to static method (Illuminate\\Database\\Query\\Builder|Illuminate\\Database\\Eloquent\\(Builder|Relations\\.*)|App\\Models\\Builders\\.*|App\\Eloquent\\FixedQueryBuilder|App\\Relations\\.*)(<.*>)?::delete\(\)#' + - '#Dynamic call to static method (Illuminate\\Database\\Query\\Builder|Illuminate\\Database\\Eloquent\\(Builder|Relations\\.*)|App\\Models\\Builders\\.*|App\\Eloquent\\FixedQueryBuilder|App\\Relations\\.*)(<.*>)?::without\(\)#' + # - '#Dynamic call to static method (Illuminate\\Database\\Query\\Builder|Illuminate\\Database\\Eloquent\\(Builder|Relations\\.*)|App\\Models\\Builders\\.*|App\\Eloquent\\FixedQueryBuilder|App\\Relations\\.*)(<.*>)?::with\(\)#' + - '#Dynamic call to static method (Illuminate\\Database\\Query\\Builder|Illuminate\\Database\\Eloquent\\(Builder|Relations\\.*)|App\\Models\\Builders\\.*|App\\Eloquent\\FixedQueryBuilder|App\\Relations\\.*)(<.*>)?::count\(\).#' + - '#Dynamic call to static method (Illuminate\\Database\\Query\\Builder|Illuminate\\Database\\Eloquent\\(Builder|Relations\\.*)|App\\Models\\Builders\\.*|App\\Eloquent\\FixedQueryBuilder|App\\Relations\\.*)(<.*>)?::update\(\).#' + - '#Dynamic call to static method (Illuminate\\Database\\Query\\Builder|Illuminate\\Database\\Eloquent\\(Builder|Relations\\.*)|App\\Models\\Builders\\.*|App\\Eloquent\\FixedQueryBuilder|App\\Relations\\.*)(<.*>)?::inRandomOrder\(\).#' + - '#Dynamic call to static method (Illuminate\\Database\\Query\\Builder|Illuminate\\Database\\Eloquent\\(Builder|Relations\\.*)|App\\Models\\Builders\\.*|App\\Eloquent\\FixedQueryBuilder|App\\Relations\\.*)(<.*>)?::groupBy\(\).#' + - '#Dynamic call to static method (Illuminate\\Database\\Query\\Builder|Illuminate\\Database\\Eloquent\\(Builder|Relations\\.*)|App\\Models\\Builders\\.*|App\\Eloquent\\FixedQueryBuilder|App\\Relations\\.*)(<.*>)?::latest\(\).#' + - '#Dynamic call to static method (Illuminate\\Database\\Query\\Builder|Illuminate\\Database\\Eloquent\\(Builder|Relations\\.*)|App\\Models\\Builders\\.*|App\\Eloquent\\FixedQueryBuilder|App\\Relations\\.*)(<.*>)?::first\(\).#' + - '#Dynamic call to static method (Illuminate\\Database\\Query\\Builder|Illuminate\\Database\\Eloquent\\(Builder|Relations\\.*)|App\\Models\\Builders\\.*|App\\Eloquent\\FixedQueryBuilder|App\\Relations\\.*)(<.*>)?::skip\(\).#' + - '#Dynamic call to static method (Illuminate\\Database\\Query\\Builder|Illuminate\\Database\\Eloquent\\(Builder|Relations\\.*)|App\\Models\\Builders\\.*|App\\Eloquent\\FixedQueryBuilder|App\\Relations\\.*)(<.*>)?::exists\(\).#' + - '#Dynamic call to static method App\\Models\\Builders\\.*::orderByDesc\(\).#' + - '#Dynamic call to static method App\\Models\\Builders\\.*::selectRaw\(\).#' + # - '#Call to an undefined method Illuminate\\Database\\Eloquent\\.*::with(Only)?\(\)#' + # - '#Call to an undefined method App\\Relations\\HasManyPhotosRecursively::whereNotNull\(\)#' + # - '#Call to an undefined method Illuminate\\Database\\Eloquent\\Builder|Illuminate\\Database\\Eloquent\\Relations\\Relation::whereNotNull\(\).#' + - '#Call to protected method asDateTime\(\) of class Illuminate\\Database\\Eloquent\\Model.#' + + # Covariance - LSP princinple: https://en.wikipedia.org/wiki/Liskov_substitution_principle + # - + # message: '#Parameter \#1 \$column \(array\|Closure\|Illuminate\\Database\\Query\\Expression\|string\) of method .* should be contravariant with parameter \$column \(array\|\(Closure.*\)\|\(Closure.*\)\|Illuminate\\Contracts\\Database\\Query\\Expression\|string\) of method .*#' + # paths: + # - app/Eloquent/FixedQueryBuilderTrait.php + # - '#Parameter \#1 \$models .* of method .*::initRelation\(\) should be contravariant with parameter \$models .* of method .*::initRelation\(\)#' + # - '#Parameter \#(1|2) \$(models|albums|photos|results) .* of method .*::(match|addEagerConstraints)\(\) should be contravariant with parameter \$(models|albums|photos|results) .* of method .*::(match|addEagerConstraints)\(\)#' + - '#Parameter .* of method .*::replicate\(\) should be contravariant with parameter .* of method .*::replicate\(\)#' + - '#Parameter .* of method .*::save\(\) should be contravariant with parameter .* of method .*::save\(\)#' + - '#Parameter .* of method .*::newEloquentBuilder\(\) should be contravariant with parameter .* of method Kalnoy\\Nestedset\\Node<.*>::newEloquentBuilder\(\)#' + - '#Parameter .* of method .*::isDirty\(\) should be contravariant with parameter .* of method .*::isDirty\(\)#' + + - '#Call to an undefined( static)? method Kalnoy\\Nestedset\\.*::(whereIn|select|join|leftJoin|orderBy|addSelect|without)\(\)#' + - '#Call to an undefined method Geocoder\\Location::getDisplayName\(\)#' + - '#Call to an undefined method COM::getfolder\(\).#' + - '#Dynamic call to static method Laragear\\WebAuthn\\Http\\Requests\\AssertionRequest::validate\(\).#' + - '#Dynamic call to static method Illuminate\\Container\\Container::refresh\(\).#' + - '#Dynamic call to static method Illuminate\\Foundation\\Application::getCachedConfigPath\(\).#' + - '#Dynamic call to static method Illuminate\\Foundation\\Application::getCachedRoutesPath\(\).#' + + + # - '#Access to an undefined property Laragear\\WebAuthn\\Models\\WebAuthnCredential::\$authenticatable_id#' + + # False positive as stub code for PHP has not yet been updated to 2nd parameter, see https://github.com/php/doc-en/issues/1529 and https://www.php.net/imagick.writeimagefile + # - '#Method Imagick::writeImageFile\(\) invoked with 2 parameters, 1 required#' + + # Synth + # - + # message: '#Variable property access on .*#' + # paths: + # - app/Livewire/Synth + + # - + # message: '#Parameter .* of method App\\Livewire\\Synth\\.* should be contravariant with parameter .* of method Livewire\\Mechanisms\\HandleComponents\\Synthesizers\\Synth::.*#' + # paths: + # - app/Livewire/Synth + + # Migrations + - + message: '#Function define is unsafe to use.#' + paths: + - database/migrations + - + message: '#Variable property access on (mixed|object).#' + paths: + - database/migrations + - + message: '#Access to an undefined property object::\$value.#' + paths: + - database/migrations + + # Configs + - + message: '#Cast to bool is forbidden.#' + paths: + - config + + # TESTS + - + message: '#no value type specified in iterable type array.#' + paths: + - tests + # - + # message: '#Dynamic call to static method Illuminate\\Testing\\TestResponse::assert(Forbidden|ViewIs|Unauthorized|Ok|Status)#' + # paths: + # - tests + # - + # message: '#Call to an undefined method Illuminate\\Testing\\TestResponse::(assert)?(SeeLivewire|dispatch|call|Set|Count)#' + # paths: + # - tests + - + message: '#Dynamic call to static method PHPUnit\\Framework\\Assert::assert(Is)?(Not)?(True|False|Equals|Int|Null|Empty|Count)\(\)#' + paths: + - tests + - + message: '#Dynamic call to static method PHPUnit\\Framework\\Assert::assertFile(DoesNot)?Exists?\(\)#' + paths: + - tests + - + message: '#Dynamic call to static method PHPUnit\\Framework\\Assert::assertString(Not)?(Contains|Ends|Starts)(String|With)\(\)#' + paths: + - tests + - + message: '#Dynamic call to static method Symfony\\Component\\HttpFoundation\\Response::get(Content|StatusCode)\(\)#' + paths: + - tests + - + message: '#Function "dump\(\)" cannot be used/left in the code: seems you missed some dump debugging function#' + paths: + - tests + - + message: '#Cannot call method .* on Illuminate\\Testing\\PendingCommand\|int.#' + paths: + - tests + + - + message: '#Access to private property App\\Models\\Extensions\\SizeVariants::\$(original|small(2x)?|thumb(2x)?|medium(2x)?|placeholder)#' + paths: + - tests + - + message: '#Call to an undefined method Mockery\\ExpectationInterface\|Mockery\\HigherOrderMessage::once\(\).#' + paths: + - tests + + - + message: '#Parameter \#3 \$fail of method App\\Rules\\.*::validate\(\) expects Closure\(string, string\|null=\): Illuminate\\Translation\\PotentiallyTranslatedString, Closure\(mixed\): void given.#' + paths: + - tests + + - + message: '#Dynamic call to static method Illuminate\\Session\\Store::(has|get|now|forget)\(\).#' + + - + message: '#Dynamic call to static method Kalnoy\\Nestedset\\QueryBuilder<.*>::(join|select|orderBy)\(\)#' + + - + message: '#Dynamic call to static method Illuminate\\Http\\Response::getContent\(\)#' + paths: + - tests \ No newline at end of file diff --git a/phpstan/stubs/Wireable.stub b/phpstan/stubs/Wireable.stub new file mode 100644 index 00000000000..a4dad489674 --- /dev/null +++ b/phpstan/stubs/Wireable.stub @@ -0,0 +1,22 @@ + + + + + + + + + + + ./tests/Unit + + + ./tests/Feature_v1 + ./tests/Feature_v1/Base/BasePhotoTest.php + ./tests/Feature_v1/Base/BasePhotosRotateTest.php + ./tests/Feature_v1/Base/BaseSharingTest.php + ./tests/Feature_v1/LibUnitTests/AlbumsUnitTest.php + ./tests/Feature_v1/LibUnitTests/PhotosUnitTest.php + ./tests/Feature_v1/LibUnitTests/RootAlbumUnitTest.php + ./tests/Feature_v1/LibUnitTests/SessionUnitTest.php + ./tests/Feature_v1/LibUnitTests/SharingUnitTest.php + ./tests/Feature_v1/LibUnitTests/UsersUnitTest.php + + + ./tests/Feature_v2 + ./tests/Feature_v2/Base/BaseApiV2Test.php + ./tests/Feature_v2/Base/BaseV2Test.php + + + + + + + + + + + + + + + + + + + + ./app + + + + diff --git a/phpunit.xml b/phpunit.xml index b58d78d47e9..3665cc969dd 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,37 +1,55 @@ - - - - ./app - + + - - ./tests/Feature + + ./tests/Unit + + + ./tests/Feature_v1 + ./tests/Feature_v1/Base/BasePhotoTest.php + ./tests/Feature_v1/Base/BasePhotosRotateTest.php + ./tests/Feature_v1/Base/BaseSharingTest.php + ./tests/Feature_v1/LibUnitTests/AlbumsUnitTest.php + ./tests/Feature_v1/LibUnitTests/PhotosUnitTest.php + ./tests/Feature_v1/LibUnitTests/RootAlbumUnitTest.php + ./tests/Feature_v1/LibUnitTests/SessionUnitTest.php + ./tests/Feature_v1/LibUnitTests/SharingUnitTest.php + ./tests/Feature_v1/LibUnitTests/UsersUnitTest.php + + + ./tests/Feature_v2 + ./tests/Feature_v2/Base/BaseApiV2Test.php + ./tests/Feature_v2/Base/BaseV2Test.php + + + + + + + + - + + + + + ./app + + + diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 00000000000..906537f6252 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,7 @@ +/** @type {import('postcss-load-config').Config} */ +export default { + plugins: { + tailwindcss: {}, + // autoprefixer: {}, + }, +} diff --git a/public/.htaccess b/public/.htaccess index 50f21a2aace..93f36bdeeee 100644 --- a/public/.htaccess +++ b/public/.htaccess @@ -14,6 +14,19 @@ RewriteCond %{REQUEST_URI} (.+)/$ RewriteRule ^ %1 [L,R=301] + # Any request to the public folders can be served directly. + # There is no need to direct them to the front-controller. + # Hence, we take a short-cut here, [L] means last rule. + # This also avoids some unneccessary execptions and exception logging + # inside Lychee, if a non-existing file is requested. + # Also disable compression for already highly compressed media files. + RewriteRule ^(css|dist|img|installer|js|src)/ - [L] + RewriteRule ^(sym|uploads)/ - [L,E=no-gzip:1] + + # Ensure that streamed responses are not cached, + # because Apache tries to ZIP them + RewriteRule (Album::getArchive|Photo::getArchive|Import::server|Zip) - [E=no-gzip:1] + # Send Requests To Front Controller... RewriteCond %{REQUEST_FILENAME} !-d RewriteCond %{REQUEST_FILENAME} !-f @@ -22,11 +35,13 @@ Options -Indexes - + php_value max_execution_time 200 php_value post_max_size 500M php_value upload_max_filesize 500M php_value max_file_uploads 100 + # A proper user agent is required by some web servers, when photos are imported via URL + php_value user_agent "Lychee/6 (https://lycheeorg.dev/)" # --- diff --git a/public/Lychee-front b/public/Lychee-front index 4f16f1facc8..a6eb679e3e1 160000 --- a/public/Lychee-front +++ b/public/Lychee-front @@ -1 +1 @@ -Subproject commit 4f16f1facc818538d109acc47fff1afc95b37bc6 +Subproject commit a6eb679e3e13a49239901ba87bd695ba1dc04cf6 diff --git a/public/css/app.css b/public/css/app.css deleted file mode 100644 index 630804aa6e0..00000000000 --- a/public/css/app.css +++ /dev/null @@ -1,3389 +0,0 @@ -@charset "UTF-8"; -html, -body, -div, -span, -applet, -object, -iframe, -h1, -h2, -h3, -h4, -h5, -h6, -p, -blockquote, -pre, -a, -abbr, -acronym, -address, -big, -cite, -code, -del, -dfn, -em, -img, -ins, -kbd, -q, -s, -samp, -small, -strike, -strong, -sub, -sup, -tt, -var, -b, -u, -i, -center, -dl, -dt, -dd, -ol, -ul, -li, -fieldset, -form, -label, -legend, -table, -caption, -tbody, -tfoot, -thead, -tr, -th, -td, -article, -aside, -canvas, -details, -embed, -figure, -figcaption, -footer, -header, -hgroup, -menu, -nav, -output, -ruby, -section, -summary, -time, -mark, -audio, -video { - margin: 0; - padding: 0; - border: 0; - font: inherit; - font-size: 100%; - vertical-align: baseline; -} - -article, -aside, -details, -figcaption, -figure, -footer, -header, -hgroup, -menu, -nav, -section { - display: block; -} - -body { - line-height: 1; -} - -ol, -ul { - list-style: none; -} - -blockquote, -q { - quotes: none; -} - -blockquote:before, -blockquote:after, -q:before, -q:after { - content: ""; - content: none; -} - -table { - border-collapse: collapse; - border-spacing: 0; -} - -em, -i { - font-style: italic; -} - -strong, -b { - font-weight: bold; -} - -* { - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - transition: color 0.3s, opacity 0.3s ease-out, transform 0.3s ease-out, box-shadow 0.3s; -} - -html, -body { - min-height: 100vh; - position: relative; -} - -body { - background-color: #1d1d1d; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 12px; - -webkit-font-smoothing: antialiased; - -moz-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} -body.view { - background-color: #0f0f0f; -} - -div#container { - position: relative; -} - -input { - -webkit-user-select: text !important; - -moz-user-select: text !important; - -ms-user-select: text !important; - user-select: text !important; -} - -.svgsprite { - display: none; -} - -.iconic { - width: 100%; - height: 100%; -} - -#upload { - display: none; -} - -.fadeIn { - -webkit-animation-name: fadeIn; - animation-name: fadeIn; - -webkit-animation-duration: 0.3s; - animation-duration: 0.3s; - -webkit-animation-fill-mode: forwards; - animation-fill-mode: forwards; - -webkit-animation-timing-function: cubic-bezier(0.51, 0.92, 0.24, 1); - animation-timing-function: cubic-bezier(0.51, 0.92, 0.24, 1); -} - -.fadeOut { - -webkit-animation-name: fadeOut; - animation-name: fadeOut; - -webkit-animation-duration: 0.3s; - animation-duration: 0.3s; - -webkit-animation-fill-mode: forwards; - animation-fill-mode: forwards; - -webkit-animation-timing-function: cubic-bezier(0.51, 0.92, 0.24, 1); - animation-timing-function: cubic-bezier(0.51, 0.92, 0.24, 1); -} - -@-webkit-keyframes fadeIn { - 0% { - opacity: 0; - } - 100% { - opacity: 1; - } -} - -@keyframes fadeIn { - 0% { - opacity: 0; - } - 100% { - opacity: 1; - } -} -@-webkit-keyframes fadeOut { - 0% { - opacity: 1; - } - 100% { - opacity: 0; - } -} -@keyframes fadeOut { - 0% { - opacity: 1; - } - 100% { - opacity: 0; - } -} -@-webkit-keyframes moveBackground { - 0% { - background-position-x: 0px; - } - 100% { - background-position-x: -100px; - } -} -@keyframes moveBackground { - 0% { - background-position-x: 0px; - } - 100% { - background-position-x: -100px; - } -} -@-webkit-keyframes zoomIn { - 0% { - opacity: 0; - transform: scale(0.8); - } - 100% { - opacity: 1; - transform: scale(1); - } -} -@keyframes zoomIn { - 0% { - opacity: 0; - transform: scale(0.8); - } - 100% { - opacity: 1; - transform: scale(1); - } -} -@-webkit-keyframes zoomOut { - 0% { - opacity: 1; - transform: scale(1); - } - 100% { - opacity: 0; - transform: scale(0.8); - } -} -@keyframes zoomOut { - 0% { - opacity: 1; - transform: scale(1); - } - 100% { - opacity: 0; - transform: scale(0.8); - } -} -@-webkit-keyframes pulse { - 0% { - opacity: 1; - } - 50% { - opacity: 0.3; - } - 100% { - opacity: 1; - } -} -@keyframes pulse { - 0% { - opacity: 1; - } - 50% { - opacity: 0.3; - } - 100% { - opacity: 1; - } -} -.content { - display: flex; - flex-wrap: wrap; - align-content: flex-start; - padding: 50px 30px 33px 0; - width: calc(100% - 30px); - transition: margin-left 0.5s; - -webkit-overflow-scrolling: touch; - max-width: calc(100vw - 10px); -} -.content::before { - content: ""; - position: absolute; - left: 0; - width: 100%; - height: 1px; - background: rgba(255, 255, 255, 0.02); -} -.content--sidebar { - width: calc(100% - 380px); -} -.content.contentZoomIn .album, .content.contentZoomIn .photo { - -webkit-animation-name: zoomIn; - animation-name: zoomIn; -} -.content.contentZoomIn .divider { - -webkit-animation-name: fadeIn; - animation-name: fadeIn; -} -.content.contentZoomOut .album, .content.contentZoomOut .photo { - -webkit-animation-name: zoomOut; - animation-name: zoomOut; -} -.content.contentZoomOut .divider { - -webkit-animation-name: fadeOut; - animation-name: fadeOut; -} -.content .album { - position: relative; - width: 202px; - height: 202px; - margin: 30px 0 0 30px; - cursor: default; - -webkit-animation-duration: 0.2s; - animation-duration: 0.2s; - -webkit-animation-fill-mode: forwards; - animation-fill-mode: forwards; - -webkit-animation-timing-function: cubic-bezier(0.51, 0.92, 0.24, 1); - animation-timing-function: cubic-bezier(0.51, 0.92, 0.24, 1); -} -.content .album .thumbimg { - position: absolute; - width: 100%; - height: 100%; - background: #222; - color: #222; - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5); - border: 1px solid rgba(255, 255, 255, 0.5); - transition: opacity 0.3s ease-out, transform 0.3s ease-out, border-color 0.3s ease-out; -} -.content .album .thumbimg > img { - width: 100%; - height: 100%; -} -.content .album:focus .thumbimg, .content .album.active .thumbimg { - border-color: #2293ec; -} -.content .album:active .thumbimg { - transition: none; - border-color: #0f6ab2; -} -.content .album.selected img { - outline: 1px solid #2293ec; -} -.content .album .video::before { - content: ""; - position: absolute; - display: block; - height: 100%; - width: 100%; - background: url("../img/play-icon.png") no-repeat 46% 50%; - transition: all 0.3s; - will-change: opacity, height; -} -.content .album .video:focus::before { - opacity: 0.75; -} -.content .album .livephoto::before { - content: ""; - position: absolute; - display: block; - height: 100%; - width: 100%; - background: url("../img/live-photo-icon.png") no-repeat 46% 50%; - background-position: 2% 2%; - transition: all 0.3s; - will-change: opacity, height; -} -.content .album .livephoto:focus::before { - opacity: 0.75; -} -.content .album .thumbimg:first-child, -.content .album .thumbimg:nth-child(2) { - transform: rotate(0) translateY(0) translateX(0); - opacity: 0; -} -.content .album:focus .thumbimg:nth-child(1), .content .album:focus .thumbimg:nth-child(2) { - opacity: 1; - will-change: transform; -} -.content .album:focus .thumbimg:nth-child(1) { - transform: rotate(-2deg) translateY(10px) translateX(-12px); -} -.content .album:focus .thumbimg:nth-child(2) { - transform: rotate(5deg) translateY(-8px) translateX(12px); -} -.content .blurred span { - overflow: hidden; -} -.content .blurred img { - /* Safari 6.0 - 9.0 */ - filter: blur(5px); -} -.content .album .overlay { - position: absolute; - margin: 0 1px; - width: 100%; - background: linear-gradient(to bottom, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.6)); - bottom: 0px; -} -.content .album .overlay h1 { - min-height: 19px; - width: 180px; - margin: 12px 0 5px 15px; - color: #fff; - text-shadow: 0 1px 3px rgba(0, 0, 0, 0.4); - font-size: 16px; - font-weight: bold; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; -} -.content .album .overlay a { - display: block; - margin: 0 0 12px 15px; - font-size: 11px; - color: #ccc; - text-shadow: 0 1px 3px rgba(0, 0, 0, 0.4); -} -.content .album .overlay a .iconic { - fill: #ccc; - margin: 0 5px 0 0; - width: 8px; - height: 8px; -} -.content .album .thumbimg[data-overlay=false] + .overlay { - background: none; -} -.content .album .thumbimg[data-overlay=false] + .overlay h1, .content .album .thumbimg[data-overlay=false] + .overlay a { - text-shadow: none; -} -.content .album .badges { - position: relative; - margin: -1px 0 0 6px; -} -.content .album .subalbum_badge { - position: absolute; - right: 0; - top: 0; -} -.content .album .badge { - display: none; - margin: 0 0 0 6px; - padding: 12px 8px 6px; - width: 18px; - background: #d92c34; - box-shadow: 0 0 2px rgba(0, 0, 0, 0.6); - border-radius: 0 0 5px 5px; - border: 1px solid #fff; - border-top: none; - color: #fff; - text-align: center; - text-shadow: 0 1px 0 rgba(0, 0, 0, 0.4); - opacity: 0.9; -} -.content .album .badge--visible { - display: inline-block; -} -.content .album .badge--not--hidden { - background: #00aa00; -} -.content .album .badge--hidden { - background: #ff9900; -} -.content .album .badge--cover { - display: "inline-block"; - background: #ff9900; -} -.content .album .badge--star { - display: inline-block; - background: #ffcc00; -} -.content .album .badge--nsfw { - display: inline-block; - background: #ff82ee; -} -.content .album .badge--list { - background: #2293ec; -} -.content .album .badge--tag { - display: inline-block; - background: #00aa00; -} -.content .album .badge .iconic { - fill: #fff; - width: 16px; - height: 16px; -} -.content .album .badge--folder { - display: inline-block; - box-shadow: none; - background: none; - border: none; -} -.content .album .badge--folder .iconic { - width: 12px; - height: 12px; -} -.content .divider { - margin: 50px 0 0; - padding: 10px 0 0; - width: 100%; - opacity: 0; - border-top: 1px solid rgba(255, 255, 255, 0.02); - box-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2); - -webkit-animation-duration: 0.2s; - animation-duration: 0.2s; - -webkit-animation-fill-mode: forwards; - animation-fill-mode: forwards; - -webkit-animation-timing-function: cubic-bezier(0.51, 0.92, 0.24, 1); - animation-timing-function: cubic-bezier(0.51, 0.92, 0.24, 1); -} -.content .divider:first-child { - margin-top: 10px; - border-top: 0; - box-shadow: none; -} -.content .divider h1 { - margin: 0 0 0 30px; - color: rgba(255, 255, 255, 0.6); - font-size: 14px; - font-weight: bold; -} - -@media only screen and (min-width: 320px) and (max-width: 567px) { - .content { - padding: 50px 0 33px 0; - width: 100%; - max-width: 100%; - } - .content .album { - --size: calc((100vw - 3px) / 3); - width: calc(var(--size) - 3px); - height: calc(var(--size) - 3px); - margin: 3px 0 0 3px; - } - .content .album .thumbimg { - width: calc(var(--size) - 5px); - height: calc(var(--size) - 5px); - } - .content .album .overlay { - width: calc(var(--size) - 5px); - } - .content .album .overlay h1 { - min-height: 14px; - width: calc(var(--size) - 19px); - margin: 8px 0 2px 6px; - font-size: 12px; - } - .content .album .overlay a { - display: none; - } - .content .album .badge { - padding: 4px 3px 3px; - width: 12px; - } - .content .album .badge .iconic { - width: 12px; - height: 12px; - } - .content .album .badge--folder .iconic { - width: 8px; - height: 8px; - } - .content .divider { - margin: 20px 0 0; - } - .content .divider:first-child { - margin-top: 0; - } - .content .divider h1 { - margin: 0 0 6px 8px; - font-size: 12px; - } -} -@media only screen and (min-width: 568px) and (max-width: 639px) { - .content { - padding: 50px 0 33px 0; - width: 100%; - max-width: 100%; - } - .content .album { - --size: calc((100vw - 3px) / 4); - width: calc(var(--size) - 3px); - height: calc(var(--size) - 3px); - margin: 3px 0 0 3px; - } - .content .album .thumbimg { - width: calc(var(--size) - 5px); - height: calc(var(--size) - 5px); - } - .content .album .overlay { - width: calc(var(--size) - 5px); - } - .content .album .overlay h1 { - min-height: 14px; - width: calc(var(--size) - 19px); - margin: 8px 0 2px 6px; - font-size: 12px; - } - .content .album .overlay a { - display: none; - } - .content .album .badge { - padding: 4px 3px 3px; - width: 14px; - } - .content .album .badge .iconic { - width: 14px; - height: 14px; - } - .content .album .badge--folder .iconic { - width: 9px; - height: 9px; - } - .content .divider { - margin: 24px 0 0; - } - .content .divider:first-child { - margin-top: 0; - } - .content .divider h1 { - margin: 0 0 6px 10px; - } -} -@media only screen and (min-width: 640px) and (max-width: 768px) { - .content { - padding: 50px 0 33px 0; - width: 100%; - max-width: 100%; - } - .content .album { - --size: calc((100vw - 5px) / 5); - width: calc(var(--size) - 5px); - height: calc(var(--size) - 5px); - margin: 5px 0 0 5px; - } - .content .album .thumbimg { - width: calc(var(--size) - 7px); - height: calc(var(--size) - 7px); - } - .content .album .overlay { - width: calc(var(--size) - 7px); - } - .content .album .overlay h1 { - min-height: 14px; - width: calc(var(--size) - 21px); - margin: 10px 0 3px 8px; - font-size: 12px; - } - .content .album .overlay a { - display: none; - } - .content .album .badge { - padding: 6px 4px 4px; - width: 16px; - } - .content .album .badge .iconic { - width: 16px; - height: 16px; - } - .content .album .badge--folder .iconic { - width: 10px; - height: 10px; - } - .content .divider { - margin: 28px 0 0; - } - .content .divider:first-child { - margin-top: 0; - } - .content .divider h1 { - margin: 0 0 6px 10px; - } -} -.no_content { - position: absolute; - top: 50%; - left: 50%; - padding-top: 20px; - color: rgba(255, 255, 255, 0.35); - text-align: center; - transform: translateX(-50%) translateY(-50%); -} -.no_content .iconic { - fill: rgba(255, 255, 255, 0.3); - margin: 0 0 10px; - width: 50px; - height: 50px; -} -.no_content p { - font-size: 16px; - font-weight: bold; -} - -.leftMenu__open { - margin-left: 250px; - width: calc(100% - 280px); -} - -@media (hover: hover) { - .content .album:hover .thumbimg, -.content .photo:hover .thumbimg { - border-color: #2293ec; - } - .content .album .video:hover::before, -.content .album .livephoto:hover::before, -.content .photo .video:hover::before, -.content .photo .livephoto:hover::before { - opacity: 0.75; - } - .content .album:hover .thumbimg:nth-child(1), -.content .album:hover .thumbimg:nth-child(2) { - opacity: 1; - will-change: transform; - } - .content .album:hover .thumbimg:nth-child(1) { - transform: rotate(-2deg) translateY(10px) translateX(-12px); - } - .content .album:hover .thumbimg:nth-child(2) { - transform: rotate(5deg) translateY(-8px) translateX(12px); - } - .content .photo:hover .overlay { - opacity: 1; - } -} -.photo { - cursor: default; - -webkit-animation-duration: 0.2s; - animation-duration: 0.2s; - -webkit-animation-fill-mode: forwards; - animation-fill-mode: forwards; - -webkit-animation-timing-function: cubic-bezier(0.51, 0.92, 0.24, 1); - animation-timing-function: cubic-bezier(0.51, 0.92, 0.24, 1); -} -.photo .thumbimg { - width: 100%; - height: 100%; - background: #222; - color: #222; - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5); - transition: opacity 0.3s ease-out, transform 0.3s ease-out, border-color 0.3s ease-out; -} -.photo .thumbimg > img { - width: 100%; - height: 100%; -} -.photo:focus .thumbimg, .photo.active .thumbimg { - border-color: #2293ec; -} -.photo:active .thumbimg { - transition: none; - border-color: #0f6ab2; -} -.photo.selected img { - outline: 1px solid #2293ec; -} -.photo .video::before { - content: ""; - position: absolute; - display: block; - height: 100%; - width: 100%; - background: url("../img/play-icon.png") no-repeat 46% 50%; - transition: all 0.3s; - will-change: opacity, height; -} -.photo .video:focus::before { - opacity: 0.75; -} -.photo .livephoto::before { - content: ""; - position: absolute; - display: block; - height: 100%; - width: 100%; - background: url("../img/live-photo-icon.png") no-repeat 46% 50%; - background-position: 2% 2%; - transition: all 0.3s; - will-change: opacity, height; -} -.photo .livephoto:focus::before { - opacity: 0.75; -} -.photo .overlay { - position: absolute; - margin: 0 0px; - width: 100%; - background: linear-gradient(to bottom, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.6)); - bottom: 0px; - opacity: 0; -} -.photo:focus .overlay, .photo.active .overlay { - opacity: 1; -} -.photo .overlay h1 { - min-height: 19px; - width: 180px; - margin: 12px 0 5px 15px; - color: #fff; - text-shadow: 0 1px 3px rgba(0, 0, 0, 0.4); - font-size: 16px; - font-weight: bold; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; -} -.photo .overlay a { - display: block; - margin: 0 0 12px 15px; - font-size: 11px; - color: #ccc; - text-shadow: 0 1px 3px rgba(0, 0, 0, 0.4); -} -.photo .overlay a .iconic { - fill: #ccc; - margin: 0 5px 0 0; - width: 8px; - height: 8px; -} -.photo .badges { - position: relative; - margin: -1px 0 0 6px; -} -.photo .badge { - display: none; - margin: 0 0 0 6px; - padding: 12px 8px 6px; - width: 18px; - background: #d92c34; - box-shadow: 0 0 2px rgba(0, 0, 0, 0.6); - border-radius: 0 0 5px 5px; - border: 1px solid #fff; - border-top: none; - color: #fff; - text-align: center; - text-shadow: 0 1px 0 rgba(0, 0, 0, 0.4); - opacity: 0.9; -} -.photo .badge--visible { - display: inline-block; -} -.photo .badge--not--hidden { - background: #00aa00; -} -.photo .badge--hidden { - background: #ff9900; -} -.photo .badge--cover { - display: "inline-block"; - background: #ff9900; -} -.photo .badge--star { - display: inline-block; - background: #ffcc00; -} -.photo .badge--nsfw { - display: inline-block; - background: #ff82ee; -} -.photo .badge--list { - background: #2293ec; -} -.photo .badge--tag { - display: inline-block; - background: #00aa00; -} -.photo .badge .iconic { - fill: #fff; - width: 16px; - height: 16px; -} -.photo .badge--folder { - display: inline-block; - box-shadow: none; - background: none; - border: none; -} -.photo .badge--folder .iconic { - width: 12px; - height: 12px; -} - -.flkr { - display: flex; - flex-wrap: wrap; - flex-grow: 1; - align-content: flex-start; - width: 100%; -} -@media (min-width: 768px) { - .flkr { - /* FLICKR like image display */ - margin: 30px 0 0 30px; - } - .flkr > div, .flkr::after { - --ratio: calc(var(--w) / var(--h)); - --row-height: 260px; - flex-basis: calc(var(--ratio) * var(--row-height)); - } - .flkr > div { - margin: 0.25rem; - flex-grow: calc(var(--ratio) * 100); - } - .flkr::after { - --w: 2; - --h: 1; - content: ""; - flex-grow: 1000000; - } - .flkr > div > span { - display: block; - height: 100%; - width: 100%; - } - .flkr > div > span > img { - width: 100%; - } -} - -@media only screen and (min-width: 320px) and (max-width: 567px) { - .block .content { - padding: 50px 0 33px 0; - width: 100%; - max-width: 100%; - } - .block .content .photo { - --size: calc((100vw - 3px) / 3); - width: calc(var(--size) - 3px); - height: calc(var(--size) - 3px); - margin: 3px 0 0 3px; - } - .block .content .photo .thumbimg { - width: calc(var(--size) - 5px); - height: calc(var(--size) - 5px); - } - .block .content .photo .overlay { - width: calc(var(--size) - 5px); - } - .block .content .photo .overlay h1 { - min-height: 14px; - width: calc(var(--size) - 19px); - margin: 8px 0 2px 6px; - font-size: 12px; - } - .block .content .photo .overlay a { - display: none; - } - .block .content .photo .badge { - padding: 4px 3px 3px; - width: 12px; - } - .block .content .photo .badge .iconic { - width: 12px; - height: 12px; - } - .block .content .photo .badge--folder .iconic { - width: 8px; - height: 8px; - } - .block .content .divider { - margin: 20px 0 0; - } - .block .content .divider:first-child { - margin-top: 0; - } - .block .content .divider h1 { - margin: 0 0 6px 8px; - font-size: 12px; - } -} -@media only screen and (min-width: 568px) and (max-width: 639px) { - .block .content { - padding: 50px 0 33px 0; - width: 100%; - max-width: 100%; - } - .block .content .photo { - --size: calc((100vw - 3px) / 4); - width: calc(var(--size) - 3px); - height: calc(var(--size) - 3px); - margin: 3px 0 0 3px; - } - .block .content .photo .thumbimg { - width: calc(var(--size) - 5px); - height: calc(var(--size) - 5px); - } - .block .content .photo .overlay { - width: calc(var(--size) - 5px); - } - .block .content .photo .overlay h1 { - min-height: 14px; - width: calc(var(--size) - 19px); - margin: 8px 0 2px 6px; - font-size: 12px; - } - .block .content .photo .overlay a { - display: none; - } - .block .content .photo .badge { - padding: 4px 3px 3px; - width: 14px; - } - .block .content .photo .badge .iconic { - width: 14px; - height: 14px; - } - .block .content .photo .badge--folder .iconic { - width: 9px; - height: 9px; - } - .block .content .divider { - margin: 24px 0 0; - } - .block .content .divider:first-child { - margin-top: 0; - } - .block .content .divider h1 { - margin: 0 0 6px 10px; - } -} -@media only screen and (min-width: 640px) and (max-width: 768px) { - .block .content { - padding: 50px 0 33px 0; - width: 100%; - max-width: 100%; - } - .block .content .photo { - --size: calc((100vw - 5px) / 5); - width: calc(var(--size) - 5px); - height: calc(var(--size) - 5px); - margin: 5px 0 0 5px; - } - .block .content .photo .thumbimg { - width: calc(var(--size) - 7px); - height: calc(var(--size) - 7px); - } - .block .content .photo .overlay { - width: calc(var(--size) - 7px); - } - .block .content .photo .overlay h1 { - min-height: 14px; - width: calc(var(--size) - 21px); - margin: 10px 0 3px 8px; - font-size: 12px; - } - .block .content .photo .overlay a { - display: none; - } - .block .content .photo .badge { - padding: 6px 4px 4px; - width: 16px; - } - .block .content .photo .badge .iconic { - width: 16px; - height: 16px; - } - .block .content .photo .badge--folder .iconic { - width: 10px; - height: 10px; - } - .block .content .divider { - margin: 28px 0 0; - } - .block .content .divider:first-child { - margin-top: 0; - } - .block .content .divider h1 { - margin: 0 0 6px 10px; - } -} - -.masonry { - -moz-column-gap: 16px; - column-gap: 16px; -} -@media (max-width: calc((240px + 16px)* 14)) { - .masonry { - -moz-columns: 14; - columns: 14; - } -} -@media (max-width: calc((240px + 16px)* 13)) { - .masonry { - -moz-columns: 13; - columns: 13; - } -} -@media (max-width: calc((240px + 16px)* 12)) { - .masonry { - -moz-columns: 12; - columns: 12; - } -} -@media (max-width: calc((240px + 16px)* 11)) { - .masonry { - -moz-columns: 11; - columns: 11; - } -} -@media (max-width: calc((240px + 16px)* 10)) { - .masonry { - -moz-columns: 10; - columns: 10; - } -} -@media (max-width: calc((240px + 16px)* 9)) { - .masonry { - -moz-columns: 9; - columns: 9; - } -} -@media (max-width: calc((240px + 16px)* 8)) { - .masonry { - -moz-columns: 8; - columns: 8; - } -} -@media (max-width: calc((240px + 16px)* 7)) { - .masonry { - -moz-columns: 7; - columns: 7; - } -} -@media (max-width: calc((240px + 16px)* 6)) { - .masonry { - -moz-columns: 6; - columns: 6; - } -} -@media (max-width: calc((240px + 16px)* 5)) { - .masonry { - -moz-columns: 5; - columns: 5; - } -} -@media (max-width: calc((240px + 16px)* 4)) { - .masonry { - -moz-columns: 4; - columns: 4; - } -} -@media (max-width: calc((240px + 16px)* 3)) { - .masonry { - -moz-columns: 3; - columns: 3; - } -} -@media (max-width: calc((240px + 16px)* 2)) { - .masonry { - -moz-columns: 2; - columns: 2; - } -} -.masonry .photo { - display: inline-block; - margin-bottom: 16px; - position: relative; -} -.masonry .photo .thumbimg { - width: 100%; - height: 100%; - display: grid; -} -.masonry .photo img { - width: 100%; - border-radius: 5px; -} -.masonry .photo .overlay { - opacity: 1; -} - -/* The side navigation menu */ -.leftMenu { - height: 100vh; - /* 100% Full-height */ - width: 0; - /* 0 width - change this with JavaScript */ - position: fixed; - /* Stay in place */ - z-index: 4; - /* Stay on top */ - top: 0; - /* Stay at the top */ - left: 0; - background-color: #111; - /* Black*/ - overflow-x: hidden; - /* Disable horizontal scroll */ - padding-top: 49px; - /* Place content 49px from the top (same as menu bar height) */ - transition: 0.5s; - /* 0.5 second transition effect to slide in the sidenav */ -} - -/* The navigation menu links */ -.leftMenu a { - padding: 8px 8px 8px 32px; - text-decoration: none; - font-size: 18px; - color: #818181; - display: block; - transition: 0.3s; -} -.leftMenu a.linkMenu { - white-space: nowrap; -} - -/* Position and style the close button (top right corner) */ -.leftMenu .closebtn { - position: absolute; - top: 0; - right: 25px; - font-size: 36px; - margin-left: 50px; -} - -.leftMenu .closetxt { - position: absolute; - top: 0; - left: 0; - font-size: 24px; - height: 28px; - padding-top: 16px; - color: #111; - display: inline-block; - width: 210px; -} - -.leftMenu .iconic { - display: inline-block; - margin: 0 10px 0 1px; - width: 15px; - height: 14px; - fill: #818181; -} - -.leftMenu .iconic.ionicons { - margin: 0 8px -2px 0; - width: 18px; - height: 18px; -} - -.leftMenu__visible { - width: 250px; -} - -@media only screen and (max-width: 567px), only screen and (max-width: 640px) and (orientation: portrait) { - .leftMenu { - display: none !important; - } -} -@media (hover: hover) { - .leftMenu { - /* When you mouse over the navigation links, change their color */ - } - .leftMenu .closetxt:hover { - color: #818181; - } - .leftMenu a:hover { - color: #f1f1f1; - } -} -@media (hover: none) { - .leftMenu a { - padding: 14px 8px 14px 32px; - } -} -.header { - position: fixed; - height: 49px; - width: 100%; - background: linear-gradient(to bottom, #222222, #1a1a1a); - border-bottom: 1px solid #0f0f0f; - z-index: 1; - transition: transform 0.3s ease-out; -} -.header--hidden { - transform: translateY(-60px); -} -.header--loading { - transform: translateY(2px); -} -.header--error { - transform: translateY(40px); -} -.header--view { - border-bottom: none; -} -.header--view.header--error { - background-color: rgba(10, 10, 10, 0.99); -} -.header__toolbar { - display: none; - align-items: center; - position: relative; - box-sizing: border-box; - width: 100%; - height: 100%; -} -.header__toolbar--visible { - display: flex; -} -.header__toolbar--config .button .iconic { - transform: rotate(45deg); -} -.header__toolbar--config .header__title { - padding-right: 80px; -} -.header__title { - width: 100%; - padding: 16px 0; - color: #fff; - font-size: 16px; - font-weight: bold; - text-align: center; - cursor: default; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - transition: margin-left 0.5s; -} -.header__title .iconic { - display: none; - margin: 0 0 0 5px; - width: 10px; - height: 10px; - fill: rgba(255, 255, 255, 0.5); - transition: fill 0.2s ease-out; -} -.header__title:active .iconic { - transition: none; - fill: rgba(255, 255, 255, 0.8); -} -.header__title--editable .iconic { - display: inline-block; -} -.header .button { - flex-shrink: 0; - padding: 16px 8px; - height: 15px; -} -.header .button .iconic { - width: 15px; - height: 15px; - fill: rgba(255, 255, 255, 0.5); - transition: fill 0.2s ease-out; -} -.header .button:active .iconic { - transition: none; - fill: rgba(255, 255, 255, 0.8); -} -.header .button--star.active .iconic { - fill: #f0ef77; -} -.header .button--eye.active .iconic { - fill: #d92c34; -} -.header .button--eye.active--not-hidden .iconic { - fill: #00aa00; -} -.header .button--eye.active--hidden .iconic { - fill: #ff9900; -} -.header .button--share .iconic.ionicons { - margin: -2px 0 -2px; - width: 18px; - height: 18px; -} -.header .button--nsfw.active .iconic { - fill: #ff82ee; -} -.header .button--info.active .iconic { - fill: #2293ec; -} -.header #button_back, -.header #button_back_home, -.header #button_settings, -.header #button_close_config, -.header #button_signin { - padding: 16px 12px 16px 18px; -} -.header .button_add { - padding: 16px 18px 16px 12px; -} -.header__divider { - flex-shrink: 0; - width: 14px; -} -.header__search { - flex-shrink: 0; - width: 80px; - margin: 0; - padding: 5px 12px 6px 12px; - background-color: #1d1d1d; - color: #fff; - border: 1px solid rgba(0, 0, 0, 0.9); - box-shadow: 0 1px 0 rgba(255, 255, 255, 0.04); - outline: none; - border-radius: 50px; - opacity: 0.6; - transition: opacity 0.3s ease-out, box-shadow 0.3s ease-out, width 0.2s ease-out; -} -.header__search:focus { - width: 140px; - border-color: #2293ec; - box-shadow: 0 1px 0 rgba(255, 255, 255, 0); - opacity: 1; -} -.header__search:focus ~ .header__clear { - opacity: 1; -} -.header__search::-ms-clear { - display: none; -} -.header__search__field { - position: relative; -} -.header__clear { - position: absolute; - top: -2px; - right: 8px; - padding: 0; - color: rgba(255, 255, 255, 0.5); - font-size: 24px; - opacity: 0; - transition: color 0.2s ease-out; - cursor: default; -} -.header__clear_nomap { - right: 60px; -} -.header__clear_public { - right: 17px; -} -.header__hostedwith { - flex-shrink: 0; - padding: 5px 10px; - margin: 11px 0; - color: #888; - font-size: 13px; - border-radius: 100px; - cursor: default; -} -.header .leftMenu__open { - margin-left: 250px; -} - -@media (hover: hover) { - .header__title:hover .iconic, -.header .button:hover .iconic { - fill: white; - } - .header__clear:hover { - color: white; - } - .header__hostedwith:hover { - background-color: rgba(0, 0, 0, 0.3); - } -} -@media only screen and (max-width: 640px) { - #button_move, -#button_move_album, -#button_trash, -#button_trash_album, -#button_visibility, -#button_visibility_album, -#button_nsfw_album { - display: none !important; - } -} -@media only screen and (max-width: 640px) and (max-width: 567px) { - #button_rotate_ccwise, -#button_rotate_cwise { - display: none !important; - } - - .header__divider { - width: 0; - } -} -#imageview { - position: fixed; - display: none; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba(10, 10, 10, 0.98); - transition: background-color 0.3s; -} -#imageview.view { - background-color: inherit; -} -#imageview.full { - background-color: black; - cursor: none; -} -#imageview #image, -#imageview #livephoto { - position: absolute; - top: 60px; - right: 30px; - bottom: 30px; - left: 30px; - margin: auto; - max-width: calc(100% - 60px); - max-height: calc(100% - 90px); - width: auto; - height: auto; - transition: top 0.3s, right 0.3s, bottom 0.3s, left 0.3s, max-width 0.3s, max-height 0.3s; - -webkit-animation-name: zoomIn; - animation-name: zoomIn; - -webkit-animation-duration: 0.3s; - animation-duration: 0.3s; - -webkit-animation-timing-function: cubic-bezier(0.51, 0.92, 0.24, 1.15); - animation-timing-function: cubic-bezier(0.51, 0.92, 0.24, 1.15); - background-size: contain; - background-position: center; - background-repeat: no-repeat; -} -#imageview.full #image, #imageview.full #livephoto { - top: 0; - right: 0; - bottom: 0; - left: 0; - max-width: 100%; - max-height: 100%; -} -#imageview.image--sidebar #image, #imageview.image--sidebar #livephoto { - right: 380px; - max-width: calc(100% - 410px); -} -#imageview #image_overlay { - position: absolute; - bottom: 30px; - left: 30px; - color: #ffffff; - text-shadow: 1px 1px 2px #000000; - z-index: 3; -} -#imageview #image_overlay h1 { - font-size: 28px; - font-weight: 500; - transition: visibility 0.3s linear, opacity 0.3s linear; -} -#imageview #image_overlay p { - margin-top: 5px; - font-size: 20px; - line-height: 24px; -} -#imageview #image_overlay a .iconic { - fill: #fff; - margin: 0 5px 0 0; - width: 14px; - height: 14px; -} -#imageview .arrow_wrapper { - position: fixed; - width: 15%; - height: calc(100% - 60px); - top: 60px; -} -#imageview .arrow_wrapper--previous { - left: 0; -} -#imageview .arrow_wrapper--next { - right: 0; -} -#imageview .arrow_wrapper a { - position: fixed; - top: 50%; - margin: -19px 0 0; - padding: 8px 12px; - width: 16px; - height: 22px; - background-size: 100% 100%; - border: 1px solid rgba(255, 255, 255, 0.8); - opacity: 0.6; - z-index: 2; - transition: transform 0.2s ease-out, opacity 0.2s ease-out; - will-change: transform; -} -#imageview .arrow_wrapper a#previous { - left: -1px; - transform: translateX(-100%); -} -#imageview .arrow_wrapper a#next { - right: -1px; - transform: translateX(100%); -} -#imageview .arrow_wrapper .iconic { - fill: rgba(255, 255, 255, 0.8); -} -#imageview.image--sidebar .arrow_wrapper--next { - right: 350px; -} -#imageview.image--sidebar .arrow_wrapper a#next { - right: 349px; -} -#imageview video { - z-index: 1; -} - -@media (hover: hover) { - #imageview .arrow_wrapper:hover a#previous, #imageview .arrow_wrapper:hover a#next { - transform: translateX(0); - } - #imageview .arrow_wrapper a:hover { - opacity: 1; - } -} -@media only screen and (max-width: 567px), only screen and (max-width: 640px) and (orientation: portrait) { - #imageview #image, -#imageview #livephoto { - top: 0; - right: 0; - bottom: 0; - left: 0; - max-width: 100%; - max-height: 100%; - } - #imageview.image--sidebar #image, -#imageview.image--sidebar #livephoto { - right: 0; - max-width: 100%; - } - #imageview.image--sidebar .arrow_wrapper--next { - right: 0; - } - #imageview.image--sidebar .arrow_wrapper a#next { - right: -1px; - } - #imageview #image_overlay h1 { - font-size: 14px; - } - #imageview #image_overlay p { - margin-top: 2px; - font-size: 11px; - line-height: 13px; - } - #imageview #image_overlay a .iconic { - width: 9px; - height: 9px; - } -} -@media only screen and (min-width: 568px) and (max-width: 768px), only screen and (min-width: 568px) and (max-width: 640px) and (orientation: landscape) { - #imageview #image, -#imageview #livephoto { - top: 0; - right: 0; - bottom: 0; - left: 0; - max-width: 100%; - max-height: 100%; - } - #imageview.image--sidebar #image, -#imageview.image--sidebar #livephoto { - top: 50px; - right: 280px; - max-width: calc(100% - 280px); - max-height: calc(100% - 50px); - } - #imageview.image--sidebar .arrow_wrapper--next { - right: 280px; - } - #imageview.image--sidebar .arrow_wrapper a#next { - right: 279px; - } - #imageview #image_overlay h1 { - font-size: 18px; - } - #imageview #image_overlay p { - margin-top: 4px; - font-size: 14px; - line-height: 16px; - } - #imageview #image_overlay a .iconic { - width: 12px; - height: 12px; - } -} -.sidebar { - position: fixed; - top: 49px; - right: -360px; - width: 350px; - height: calc(100% - 49px); - background-color: rgba(25, 25, 25, 0.98); - border-left: 1px solid rgba(0, 0, 0, 0.2); - transform: translateX(0); - transition: transform 0.3s cubic-bezier(0.51, 0.92, 0.24, 1); - z-index: 4; -} -.sidebar.active { - transform: translateX(-360px); -} -.sidebar__header { - float: left; - height: 49px; - width: 100%; - background: linear-gradient(to bottom, rgba(255, 255, 255, 0.02), rgba(0, 0, 0, 0)); - border-top: 1px solid #2293ec; -} -.sidebar__header h1 { - position: absolute; - margin: 15px 0 15px 0; - width: 100%; - color: #fff; - font-size: 16px; - font-weight: bold; - text-align: center; - -webkit-user-select: text; - -moz-user-select: text; - -ms-user-select: text; - user-select: text; -} -.sidebar__wrapper { - float: left; - height: calc(100% - 49px); - width: 350px; - overflow: auto; - -webkit-overflow-scrolling: touch; -} -.sidebar__divider { - float: left; - padding: 12px 0 8px; - width: 100%; - border-top: 1px solid rgba(255, 255, 255, 0.02); - box-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2); -} -.sidebar__divider:first-child { - border-top: 0; - box-shadow: none; -} -.sidebar__divider h1 { - margin: 0 0 0 20px; - color: rgba(255, 255, 255, 0.6); - font-size: 14px; - font-weight: bold; - -webkit-user-select: text; - -moz-user-select: text; - -ms-user-select: text; - user-select: text; -} -.sidebar .edit { - display: inline-block; - margin-left: 3px; - width: 10px; -} -.sidebar .edit .iconic { - width: 10px; - height: 10px; - fill: rgba(255, 255, 255, 0.5); - transition: fill 0.2s ease-out; -} -.sidebar .edit:active .iconic { - transition: none; - fill: rgba(255, 255, 255, 0.8); -} -.sidebar table { - float: left; - margin: 10px 0 15px 20px; - width: calc(100% - 20px); -} -.sidebar table tr td { - padding: 5px 0px; - color: #fff; - font-size: 14px; - line-height: 19px; - -webkit-user-select: text; - -moz-user-select: text; - -ms-user-select: text; - user-select: text; -} -.sidebar table tr td:first-child { - width: 110px; -} -.sidebar table tr td:last-child { - padding-right: 10px; -} -.sidebar table tr td span { - -webkit-user-select: text; - -moz-user-select: text; - -ms-user-select: text; - user-select: text; -} -.sidebar #tags { - width: calc(100% - 40px); - margin: 16px 20px 12px 20px; - color: #fff; - display: inline-block; -} -.sidebar #tags > div { - display: inline-block; -} -.sidebar #tags .empty { - font-size: 14px; - margin: 0 2px 8px 0; - -webkit-user-select: text; - -moz-user-select: text; - -ms-user-select: text; - user-select: text; -} -.sidebar #tags .edit { - margin-top: 6px; -} -.sidebar #tags .empty .edit { - margin-top: 0; -} -.sidebar #tags .tag { - cursor: default; - display: inline-block; - padding: 6px 10px; - margin: 0 6px 8px 0; - background-color: rgba(0, 0, 0, 0.5); - border-radius: 100px; - font-size: 12px; - transition: background-color 0.2s; - -webkit-user-select: text; - -moz-user-select: text; - -ms-user-select: text; - user-select: text; -} -.sidebar #tags .tag span { - float: right; - padding: 0; - margin: 0 0 -2px 0; - width: 0; - overflow: hidden; - transform: scale(0); - transition: width 0.2s, margin 0.2s, transform 0.2s, fill 0.2s ease-out; -} -.sidebar #tags .tag span .iconic { - fill: #d92c34; - width: 8px; - height: 8px; -} -.sidebar #tags .tag span:active .iconic { - transition: none; - fill: #b22027; -} -.sidebar #leaflet_map_single_photo { - margin: 10px 0px 0px 20px; - height: 180px; - width: calc(100% - 40px); - float: left; -} -.sidebar .attr_location.search { - cursor: pointer; -} - -@media (hover: hover) { - .sidebar .edit:hover .iconic { - fill: white; - } - .sidebar #tags .tag:hover { - background-color: rgba(0, 0, 0, 0.3); - } - .sidebar #tags .tag:hover.search { - cursor: pointer; - } - .sidebar #tags .tag:hover span { - width: 9px; - margin: 0 0 -2px 5px; - transform: scale(1); - } - .sidebar #tags .tag span:hover .iconic { - fill: #e1575e; - } -} -@media only screen and (max-width: 567px), only screen and (max-width: 640px) and (orientation: portrait) { - .sidebar { - width: 240px; - height: unset; - background-color: rgba(0, 0, 0, 0.6); - } - .sidebar__wrapper { - padding-bottom: 10px; - } - .sidebar__header { - height: 22px; - } - .sidebar__header h1 { - margin: 6px 0; - font-size: 13px; - } - .sidebar__divider { - padding: 6px 0 2px; - } - .sidebar__divider h1 { - margin: 0 0 0 10px; - font-size: 12px; - } - .sidebar table { - margin: 4px 0 6px 10px; - width: calc(100% - 16px); - } - .sidebar table tr td { - padding: 2px 0; - font-size: 11px; - line-height: 12px; - } - .sidebar table tr td:first-child { - width: 80px; - } - .sidebar #tags { - margin: 4px 0 6px 10px; - width: calc(100% - 16px); - } - .sidebar #tags .empty { - margin: 0; - font-size: 11px; - } -} -@media only screen and (min-width: 568px) and (max-width: 768px), only screen and (min-width: 568px) and (max-width: 640px) and (orientation: landscape) { - .sidebar { - width: 280px; - } - .sidebar__wrapper { - padding-bottom: 10px; - } - .sidebar__header { - height: 28px; - } - .sidebar__header h1 { - margin: 8px 0; - font-size: 15px; - } - .sidebar__divider { - padding: 8px 0 4px; - } - .sidebar__divider h1 { - margin: 0 0 0 10px; - font-size: 13px; - } - .sidebar table { - margin: 4px 0 6px 10px; - width: calc(100% - 16px); - } - .sidebar table tr td { - padding: 2px 0; - font-size: 12px; - line-height: 13px; - } - .sidebar table tr td:first-child { - width: 90px; - } - .sidebar #tags { - margin: 4px 0 6px 10px; - width: calc(100% - 16px); - } - .sidebar #tags .empty { - margin: 0; - font-size: 12px; - } -} -#sensitive_warning { - background: rgba(100, 0, 0, 0.95); - width: 100vw; - height: 100vh; - position: fixed; - top: 0px; - text-align: center; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - color: white; -} -#sensitive_warning h1 { - font-size: 36px; - font-weight: bold; - border-bottom: 2px solid white; - margin-bottom: 15px; -} -#sensitive_warning p { - font-size: 20px; - max-width: 40%; - margin-top: 15px; -} - -.settings_view { - width: 90%; - max-width: 700px; - margin-left: auto; - margin-right: auto; -} -.settings_view input.text { - padding: 9px 2px; - width: calc(50% - 4px); - background-color: transparent; - color: #fff; - border: none; - border-bottom: 1px solid #222; - border-radius: 0; - box-shadow: 0 1px 0 rgba(255, 255, 255, 0.05); - outline: 0; -} -.settings_view input.text:focus { - border-bottom-color: #2293ec; -} -.settings_view input.text .error { - border-bottom-color: #d92c34; -} -.settings_view .basicModal__button { - color: #2293ec; - display: inline-block; - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.02), inset 1px 0 0 rgba(0, 0, 0, 0.2); - border-radius: 5px; -} -.settings_view .basicModal__button_MORE, -.settings_view .basicModal__button_SAVE { - color: #b22027; - border-radius: 5px; -} -.settings_view > div { - font-size: 14px; - width: 100%; - padding: 12px 0; - /* The switch - the box around the slider */ - /* Hide default HTML checkbox */ - /* The slider */ - /* Rounded sliders */ -} -.settings_view > div p { - margin: 0 0 5%; - width: 100%; - color: #ccc; - line-height: 16px; -} -.settings_view > div p a { - color: rgba(255, 255, 255, 0.9); - text-decoration: none; - border-bottom: 1px dashed #888; -} -.settings_view > div p:last-of-type { - margin: 0; -} -.settings_view > div input.text { - width: 100%; -} -.settings_view > div textarea { - padding: 9px 9px; - width: calc(100% - 18px); - height: 100px; - background-color: transparent; - color: #fff; - border: 1px solid #666666; - border-radius: 0; - box-shadow: 0 1px 0 rgba(255, 255, 255, 0.05); - outline: 0; - resize: vertical; -} -.settings_view > div textarea:focus { - border-color: #2293ec; -} -.settings_view > div .choice { - padding: 0 30px 15px; - width: 100%; - color: #fff; -} -.settings_view > div .choice:last-child { - padding-bottom: 40px; -} -.settings_view > div .choice label { - float: left; - color: #fff; - font-size: 14px; - font-weight: 700; -} -.settings_view > div .choice label input { - position: absolute; - margin: 0; - opacity: 0; -} -.settings_view > div .choice label .checkbox { - float: left; - display: block; - width: 16px; - height: 16px; - background: rgba(0, 0, 0, 0.5); - border-radius: 3px; - box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.7); -} -.settings_view > div .choice label .checkbox .iconic { - box-sizing: border-box; - fill: #2293ec; - padding: 2px; - opacity: 0; - transform: scale(0); - transition: opacity 0.2s cubic-bezier(0.51, 0.92, 0.24, 1), transform 0.2s cubic-bezier(0.51, 0.92, 0.24, 1); -} -.settings_view > div .select { - position: relative; - margin: 1px 5px; - padding: 0; - width: 110px; - color: #fff; - border-radius: 3px; - border: 1px solid rgba(0, 0, 0, 0.2); - box-shadow: 0 1px 0 rgba(255, 255, 255, 0.02); - font-size: 11px; - line-height: 16px; - overflow: hidden; - outline: 0; - vertical-align: middle; - background: rgba(0, 0, 0, 0.3); - display: inline-block; -} -.settings_view > div .select select { - margin: 0; - padding: 4px 8px; - width: 120%; - color: #fff; - font-size: 11px; - line-height: 16px; - border: 0; - outline: 0; - box-shadow: none; - border-radius: 0; - background-color: transparent; - background-image: none; - -moz-appearance: none; - -webkit-appearance: none; - appearance: none; -} -.settings_view > div .select select option { - margin: 0; - padding: 0; - background: #fff; - color: #333; - transition: none; -} -.settings_view > div .select select:disabled { - color: #000; - cursor: not-allowed; -} -.settings_view > div .select::after { - position: absolute; - content: "≡"; - right: 8px; - top: 4px; - color: #2293ec; - font-size: 16px; - line-height: 16px; - font-weight: 700; - pointer-events: none; -} -.settings_view > div .switch { - position: relative; - display: inline-block; - width: 42px; - height: 22px; - bottom: -2px; - line-height: 24px; -} -.settings_view > div .switch input { - opacity: 0; - width: 0; - height: 0; -} -.settings_view > div .slider { - position: absolute; - cursor: pointer; - top: 0; - left: 0; - right: 0; - bottom: 0; - border: 1px solid rgba(0, 0, 0, 0.2); - box-shadow: 0 1px 0 rgba(255, 255, 255, 0.02); - background: rgba(0, 0, 0, 0.3); - transition: 0.4s; -} -.settings_view > div .slider:before { - position: absolute; - content: ""; - height: 14px; - width: 14px; - left: 3px; - bottom: 3px; - background-color: #2293ec; -} -.settings_view > div input:checked + .slider { - background-color: #2293ec; -} -.settings_view > div input:checked + .slider:before { - transform: translateX(20px); - background-color: #ffffff; -} -.settings_view > div .slider.round { - border-radius: 20px; -} -.settings_view > div .slider.round:before { - border-radius: 50%; -} -.settings_view .setting_category { - font-size: 20px; - width: 100%; - padding-top: 10px; - padding-left: 4px; - border-bottom: dotted 1px #222222; - margin-top: 20px; - color: #ffffff; - font-weight: bold; - text-transform: capitalize; -} -.settings_view .setting_line { - font-size: 14px; - width: 100%; -} -.settings_view .setting_line:last-child, .settings_view .setting_line:first-child { - padding-top: 50px; -} -.settings_view .setting_line p { - min-width: 550px; - margin: 0 0 0 0; - color: #ccc; - display: inline-block; - width: 100%; - overflow-wrap: break-word; -} -.settings_view .setting_line p a { - color: rgba(255, 255, 255, 0.9); - text-decoration: none; - border-bottom: 1px dashed #888; -} -.settings_view .setting_line p:last-of-type { - margin: 0; -} -.settings_view .setting_line p .warning { - margin-bottom: 30px; - color: #d92c34; - font-weight: bold; - font-size: 18px; - text-align: justify; - line-height: 22px; -} -.settings_view .setting_line span.text { - display: inline-block; - padding: 9px 4px; - width: calc(50% - 12px); - background-color: transparent; - color: #fff; - border: none; -} -.settings_view .setting_line span.text_icon { - width: 5%; -} -.settings_view .setting_line span.text_icon .iconic { - width: 15px; - height: 14px; - margin: 0 10px 0 1px; - fill: #ffffff; -} -.settings_view .setting_line input.text { - width: calc(50% - 4px); -} - -@media (hover: hover) { - .settings_view .basicModal__button:hover { - background: #2293ec; - color: #ffffff; - cursor: pointer; - } - .settings_view .basicModal__button_MORE:hover, -.settings_view .basicModal__button_SAVE:hover { - background: #b22027; - color: #ffffff; - } - .settings_view input:hover { - border-bottom: #2293ec solid 1px; - } -} -@media (hover: none) { - .settings_view input.text { - border-bottom: #2293ec solid 1px; - margin: 6px 0; - } - .settings_view > div { - padding: 16px 0; - } - .settings_view .basicModal__button { - background: #2293ec; - color: #ffffff; - max-width: 320px; - margin-top: 20px; - } - .settings_view .basicModal__button_MORE, .settings_view .basicModal__button_SAVE { - background: #b22027; - } -} -@media only screen and (max-width: 567px), only screen and (max-width: 640px) and (orientation: portrait) { - .settings_view { - max-width: 100%; - } - .settings_view .setting_category { - font-size: 14px; - padding-left: 0; - margin-bottom: 4px; - } - .settings_view .setting_line { - font-size: 12px; - } - .settings_view .setting_line:first-child { - padding-top: 20px; - } - .settings_view .setting_line p { - min-width: unset; - line-height: 20px; - } - .settings_view .setting_line p.warning { - font-size: 14px; - line-height: 16px; - margin-bottom: 0; - } - .settings_view .setting_line p span, -.settings_view .setting_line p input { - padding: 0; - } - .settings_view .basicModal__button_SAVE { - margin-top: 20px; - } -} -.users_view { - width: 90%; - max-width: 700px; - margin-left: auto; - margin-right: auto; -} - -.users_view_line { - font-size: 14px; - width: 100%; -} -.users_view_line:last-child, .users_view_line:first-child { - padding-top: 50px; -} -.users_view_line p { - width: 550px; - margin: 0 0 5%; - color: #ccc; - display: inline-block; -} -.users_view_line p a { - color: rgba(255, 255, 255, 0.9); - text-decoration: none; - border-bottom: 1px dashed #888; -} -.users_view_line p:last-of-type { - margin: 0; -} -.users_view_line p.line { - margin: 0 0 0 0; -} -.users_view_line span.text { - display: inline-block; - padding: 9px 6px 9px 0; - width: 40%; - background-color: transparent; - color: #fff; - border: none; -} -.users_view_line span.text_icon { - width: 5%; - min-width: 32px; -} -.users_view_line span.text_icon .iconic { - width: 15px; - height: 14px; - margin: 0 8px; - fill: #ffffff; -} -.users_view_line input.text { - padding: 9px 6px 9px 0; - width: 40%; - background-color: transparent; - color: #fff; - border: none; - border-bottom: 1px solid #222; - border-radius: 0; - box-shadow: 0 1px 0 rgba(255, 255, 255, 0.05); - outline: 0; - margin: 0 0 10px; -} -.users_view_line input.text:focus { - border-bottom-color: #2293ec; -} -.users_view_line input.text.error { - border-bottom-color: #d92c34; -} -.users_view_line .choice label input:checked ~ .checkbox .iconic { - opacity: 1; - transform: scale(1); -} -.users_view_line .choice { - display: inline-block; - width: 5%; - min-width: 32px; - color: #fff; -} -.users_view_line .choice input { - position: absolute; - margin: 0; - opacity: 0; -} -.users_view_line .choice .checkbox { - display: inline-block; - width: 16px; - height: 16px; - margin: 10px 8px 0; - background: rgba(0, 0, 0, 0.5); - border-radius: 3px; - box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.7); -} -.users_view_line .choice .checkbox .iconic { - box-sizing: border-box; - fill: #2293ec; - padding: 2px; - opacity: 0; - transform: scale(0); - transition: opacity 0.2s cubic-bezier(0.51, 0.92, 0.24, 1), transform 0.2s cubic-bezier(0.51, 0.92, 0.24, 1); -} -.users_view_line .basicModal__button { - display: inline-block; - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.02), inset 1px 0 0 rgba(0, 0, 0, 0.2); - width: 10%; - min-width: 72px; - border-radius: 0 0 0 0; -} -.users_view_line .basicModal__button_OK { - color: #2293ec; - border-radius: 5px 0 0 5px; - margin-right: -4px; -} -.users_view_line .basicModal__button_DEL { - color: #b22027; - border-radius: 0 5px 5px 0; -} -.users_view_line .basicModal__button_CREATE { - width: 20%; - color: #009900; - border-radius: 5px; - min-width: 144px; -} -.users_view_line .select { - position: relative; - margin: 1px 5px; - padding: 0; - width: 110px; - color: #fff; - border-radius: 3px; - border: 1px solid rgba(0, 0, 0, 0.2); - box-shadow: 0 1px 0 rgba(255, 255, 255, 0.02); - font-size: 11px; - line-height: 16px; - overflow: hidden; - outline: 0; - vertical-align: middle; - background: rgba(0, 0, 0, 0.3); - display: inline-block; -} -.users_view_line .select select { - margin: 0; - padding: 4px 8px; - width: 120%; - color: #fff; - font-size: 11px; - line-height: 16px; - border: 0; - outline: 0; - box-shadow: none; - border-radius: 0; - background: transparent none; - -moz-appearance: none; - -webkit-appearance: none; - appearance: none; -} -.users_view_line .select select option { - margin: 0; - padding: 0; - background: #fff; - color: #333; - transition: none; -} -.users_view_line .select::after { - position: absolute; - content: "≡"; - right: 8px; - top: 4px; - color: #2293ec; - font-size: 16px; - line-height: 16px; - font-weight: 700; - pointer-events: none; -} - -@media (hover: hover) { - .users_view_line .basicModal__button:hover { - cursor: pointer; - color: #ffffff; - } - .users_view_line .basicModal__button_OK:hover { - background: #2293ec; - } - .users_view_line .basicModal__button_DEL:hover { - background: #b22027; - } - .users_view_line .basicModal__button_CREATE:hover { - background: #009900; - } - .users_view_line input:hover { - border-bottom: #2293ec solid 1px; - } -} -@media (hover: none) { - .users_view_line .basicModal__button { - color: #ffffff; - } - .users_view_line .basicModal__button_OK { - background: #2293ec; - } - .users_view_line .basicModal__button_DEL { - background: #b22027; - } - .users_view_line .basicModal__button_CREATE { - background: #009900; - } - .users_view_line input { - border-bottom: #2293ec solid 1px; - } -} -@media only screen and (max-width: 567px), only screen and (max-width: 640px) and (orientation: portrait) { - .users_view { - width: 100%; - max-width: 100%; - padding: 20px; - } - - .users_view_line p { - width: 100%; - } - .users_view_line p .text, -.users_view_line p input.text { - width: 36%; - font-size: smaller; - } - .users_view_line .choice { - margin-left: -8px; - margin-right: 3px; - } -} -.u2f_view { - width: 90%; - max-width: 700px; - margin-left: auto; - margin-right: auto; -} - -.u2f_view_line { - font-size: 14px; - width: 100%; -} -.u2f_view_line:last-child, .u2f_view_line:first-child { - padding-top: 50px; -} -.u2f_view_line p { - width: 550px; - margin: 0 0 5%; - color: #ccc; - display: inline-block; -} -.u2f_view_line p a { - color: rgba(255, 255, 255, 0.9); - text-decoration: none; - border-bottom: 1px dashed #888; -} -.u2f_view_line p:last-of-type { - margin: 0; -} -.u2f_view_line p.line { - margin: 0 0 0 0; -} -.u2f_view_line p.single { - text-align: center; -} -.u2f_view_line span.text { - display: inline-block; - padding: 9px 4px; - width: 80%; - background-color: transparent; - color: #fff; - border: none; -} -.u2f_view_line span.text_icon { - width: 5%; -} -.u2f_view_line span.text_icon .iconic { - width: 15px; - height: 14px; - margin: 0 15px 0 1px; - fill: #ffffff; -} -.u2f_view_line .choice label input:checked ~ .checkbox .iconic { - opacity: 1; - transform: scale(1); -} -.u2f_view_line .choice { - display: inline-block; - width: 5%; - color: #fff; -} -.u2f_view_line .choice input { - position: absolute; - margin: 0; - opacity: 0; -} -.u2f_view_line .choice .checkbox { - display: inline-block; - width: 16px; - height: 16px; - margin-top: 10px; - margin-left: 2px; - background: rgba(0, 0, 0, 0.5); - border-radius: 3px; - box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.7); -} -.u2f_view_line .choice .checkbox .iconic { - box-sizing: border-box; - fill: #2293ec; - padding: 2px; - opacity: 0; - transform: scale(0); - transition: opacity 0.2s cubic-bezier(0.51, 0.92, 0.24, 1), transform 0.2s cubic-bezier(0.51, 0.92, 0.24, 1); -} -.u2f_view_line .basicModal__button { - display: inline-block; - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.02), inset 1px 0 0 rgba(0, 0, 0, 0.2); - width: 20%; - min-width: 50px; - border-radius: 0 0 0 0; -} -.u2f_view_line .basicModal__button_OK { - color: #2293ec; - border-radius: 5px 0 0 5px; -} -.u2f_view_line .basicModal__button_DEL { - color: #b22027; - border-radius: 0 5px 5px 0; -} -.u2f_view_line .basicModal__button_CREATE { - width: 100%; - color: #009900; - border-radius: 5px; -} -.u2f_view_line .select { - position: relative; - margin: 1px 5px; - padding: 0; - width: 110px; - color: #fff; - border-radius: 3px; - border: 1px solid rgba(0, 0, 0, 0.2); - box-shadow: 0 1px 0 rgba(255, 255, 255, 0.02); - font-size: 11px; - line-height: 16px; - overflow: hidden; - outline: 0; - vertical-align: middle; - background: rgba(0, 0, 0, 0.3); - display: inline-block; -} -.u2f_view_line .select select { - margin: 0; - padding: 4px 8px; - width: 120%; - color: #fff; - font-size: 11px; - line-height: 16px; - border: 0; - outline: 0; - box-shadow: none; - border-radius: 0; - background: transparent none; - -moz-appearance: none; - -webkit-appearance: none; - appearance: none; -} -.u2f_view_line .select select option { - margin: 0; - padding: 0; - background: #fff; - color: #333; - transition: none; -} -.u2f_view_line .select::after { - position: absolute; - content: "≡"; - right: 8px; - top: 4px; - color: #2293ec; - font-size: 16px; - line-height: 16px; - font-weight: 700; - pointer-events: none; -} - -/* When you mouse over the navigation links, change their color */ -.signInKeyLess { - display: block; - padding: 10px 10px; - position: absolute; - cursor: pointer; -} -.signInKeyLess .iconic { - display: inline-block; - margin: 0 0px 0 0px; - width: 20px; - height: 20px; - fill: #818181; -} -.signInKeyLess .iconic.ionicons { - margin: 0 8px -2px 0; - width: 18px; - height: 18px; -} - -@media (hover: hover) { - .u2f_view_line .basicModal__button:hover { - cursor: pointer; - } - .u2f_view_line .basicModal__button_OK:hover { - background: #2293ec; - color: #ffffff; - } - .u2f_view_line .basicModal__button_DEL:hover { - background: #b22027; - color: #ffffff; - } - .u2f_view_line .basicModal__button_CREATE:hover { - background: #009900; - color: #ffffff; - } - .u2f_view_line input:hover { - border-bottom: #2293ec solid 1px; - } - - .signInKeyLess:hover .iconic { - fill: #ffffff; - } -} -@media (hover: none) { - .u2f_view_line .basicModal__button { - color: #ffffff; - } - .u2f_view_line .basicModal__button_OK { - background: #2293ec; - } - .u2f_view_line .basicModal__button_DEL { - background: #b22027; - } - .u2f_view_line .basicModal__button_CREATE { - background: #009900; - } - .u2f_view_line input { - border-bottom: #2293ec solid 1px; - } -} -@media only screen and (max-width: 567px), only screen and (max-width: 640px) and (orientation: portrait) { - .u2f_view { - width: 100%; - max-width: 100%; - padding: 20px; - } - - .u2f_view_line p { - width: 100%; - } - .u2f_view_line .basicModal__button_CREATE { - width: 80%; - margin: 0 10%; - } -} -.logs_diagnostics_view { - width: 90%; - margin-left: auto; - margin-right: auto; - color: #ccc; - font-size: 12px; - line-height: 14px; -} -.logs_diagnostics_view pre { - font-family: monospace; - -webkit-user-select: text; - -moz-user-select: text; - -ms-user-select: text; - user-select: text; - width: -webkit-fit-content; - width: -moz-fit-content; - width: fit-content; - padding-right: 30px; -} - -.clear_logs_update { - margin-left: auto; - margin-right: auto; - margin-bottom: 20px; - margin-top: 20px; - padding-left: 30px; -} - -.clear_logs_update .basicModal__button, -.logs_diagnostics_view .basicModal__button { - color: #2293ec; - display: inline-block; - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.02), inset 1px 0 0 rgba(0, 0, 0, 0.2); - border-radius: 5px; -} -.clear_logs_update .iconic, -.logs_diagnostics_view .iconic { - display: inline-block; - margin: 0 10px 0 1px; - width: 13px; - height: 12px; - fill: #2293ec; -} -.clear_logs_update .button_left, -.logs_diagnostics_view .button_left { - margin-left: 24px; - width: 400px; -} - -@media (hover: none) { - .clear_logs_update .basicModal__button, -.logs_diagnostics_view .basicModal__button { - background: #2293ec; - color: #fff; - max-width: 320px; - margin-top: 20px; - } - .clear_logs_update .iconic, -.logs_diagnostics_view .iconic { - fill: #fff; - } -} -@media only screen and (max-width: 567px), only screen and (max-width: 640px) and (orientation: portrait) { - .logs_diagnostics_view, -.clear_logs_update { - width: 100%; - max-width: 100%; - font-size: 11px; - line-height: 12px; - } - .logs_diagnostics_view .basicModal__button, -.logs_diagnostics_view .button_left, -.clear_logs_update .basicModal__button, -.clear_logs_update .button_left { - width: 80%; - margin: 0 10%; - } - - .logs_diagnostics_view { - padding: 10px 10px 0 0; - } - - .clear_logs_update { - padding: 10px 10px 0 10px; - margin: 0; - } -} -.sharing_view { - width: 90%; - max-width: 700px; - margin-left: auto; - margin-right: auto; - margin-top: 20px; -} -.sharing_view .sharing_view_line { - width: 100%; - display: block; - clear: left; -} -.sharing_view .col-xs-1, -.sharing_view .col-xs-10, -.sharing_view .col-xs-11, -.sharing_view .col-xs-12, -.sharing_view .col-xs-2, -.sharing_view .col-xs-3, -.sharing_view .col-xs-4, -.sharing_view .col-xs-5, -.sharing_view .col-xs-6, -.sharing_view .col-xs-7, -.sharing_view .col-xs-8, -.sharing_view .col-xs-9 { - float: left; - position: relative; - min-height: 1px; -} -.sharing_view .col-xs-2 { - width: 10%; - padding-right: 3%; - padding-left: 3%; -} -.sharing_view .col-xs-5 { - width: 42%; -} -.sharing_view .btn-block + .btn-block { - margin-top: 5px; -} -.sharing_view .btn-block { - display: block; - width: 100%; -} -.sharing_view .btn-default { - color: #2293ec; - border-color: #2293ec; - background: rgba(0, 0, 0, 0.5); - border-radius: 3px; - box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.7); -} -.sharing_view .btn { - display: inline-block; - padding: 6px 12px; - margin-bottom: 0; - font-size: 14px; - font-weight: 400; - line-height: 1.42857143; - text-align: center; - white-space: nowrap; - vertical-align: middle; - touch-action: manipulation; - cursor: pointer; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - background-image: none; - border: 1px solid transparent; - border-radius: 4px; -} -.sharing_view select[multiple], -.sharing_view select[size] { - height: 150px; -} -.sharing_view .form-control { - display: block; - width: 100%; - height: 34px; - padding: 6px 12px; - font-size: 14px; - line-height: 1.42857143; - color: #555; - background-color: #fff; - background-image: none; - border: 1px solid #ccc; - border-radius: 4px; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; -} -.sharing_view .iconic { - display: inline-block; - width: 15px; - height: 14px; - fill: #2293ec; -} -.sharing_view .iconic .iconic.ionicons { - margin: 0 8px -2px 0; - width: 18px; - height: 18px; -} -.sharing_view .blue .iconic { - fill: #2293ec; -} -.sharing_view .grey .iconic { - fill: #b4b4b4; -} -.sharing_view p { - width: 100%; - color: #ccc; - text-align: center; - font-size: 14px; - display: block; -} -.sharing_view p.with { - padding: 15px 0; -} -.sharing_view span.text { - display: inline-block; - padding: 0 2px; - width: 40%; - background-color: transparent; - color: #fff; - border: none; -} -.sharing_view span.text:last-of-type { - width: 5%; -} -.sharing_view span.text .iconic { - width: 15px; - height: 14px; - margin: 0 10px 0 1px; - fill: #ffffff; -} -.sharing_view .basicModal__button { - margin-top: 10px; - color: #2293ec; - display: inline-block; - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.02), inset 1px 0 0 rgba(0, 0, 0, 0.2); - border-radius: 5px; -} -.sharing_view .choice label input:checked ~ .checkbox .iconic { - opacity: 1; - transform: scale(1); -} -.sharing_view .choice { - display: inline-block; - width: 5%; - margin: 0 10px; - color: #fff; -} -.sharing_view .choice input { - position: absolute; - margin: 0; - opacity: 0; -} -.sharing_view .choice .checkbox { - display: inline-block; - width: 16px; - height: 16px; - margin-top: 10px; - margin-left: 2px; - background: rgba(0, 0, 0, 0.5); - border-radius: 3px; - box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.7); -} -.sharing_view .choice .checkbox .iconic { - box-sizing: border-box; - fill: #2293ec; - padding: 2px; - opacity: 0; - transform: scale(0); - transition: opacity 0.2s cubic-bezier(0.51, 0.92, 0.24, 1), transform 0.2s cubic-bezier(0.51, 0.92, 0.24, 1); -} -.sharing_view .select { - position: relative; - padding: 0; - color: #fff; - border-radius: 3px; - border: 1px solid rgba(0, 0, 0, 0.2); - box-shadow: 0 1px 0 rgba(255, 255, 255, 0.02); - font-size: 14px; - line-height: 16px; - outline: 0; - vertical-align: middle; - background: rgba(0, 0, 0, 0.3); - display: inline-block; -} -.sharing_view .borderBlue { - border: 1px solid #2293ec; -} - -@media (hover: hover) { - .sharing_view .basicModal__button:hover { - background: #2293ec; - color: #ffffff; - cursor: pointer; - } - .sharing_view input:hover { - border-bottom: #2293ec solid 1px; - } -} -@media (hover: none) { - .sharing_view .basicModal__button { - background: #2293ec; - color: #ffffff; - } - .sharing_view input { - border-bottom: #2293ec solid 1px; - } -} -@media only screen and (max-width: 567px), only screen and (max-width: 640px) and (orientation: portrait) { - .sharing_view { - width: 100%; - max-width: 100%; - padding: 10px; - } - .sharing_view .select { - font-size: 12px; - } - .sharing_view .iconic { - margin-left: -4px; - } - - .sharing_view_line p { - width: 100%; - } - .sharing_view_line .basicModal__button { - width: 80%; - margin: 0 10%; - } -} -#multiselect { - position: absolute; - background-color: rgba(0, 94, 204, 0.3); - border: 1px solid #005ecc; - border-radius: 3px; - z-index: 5; -} - -.justified-layout { - margin: 30px 0 0 30px; - width: 100%; - position: relative; -} - -.unjustified-layout { - margin: 25px -5px -5px 25px; - width: 100%; - position: relative; - overflow: hidden; -} - -.justified-layout > .photo { - position: absolute; - --lychee-default-height: 320px; - margin: 0; -} - -.unjustified-layout > .photo { - float: left; - max-height: 240px; - margin: 5px; -} - -.justified-layout > .photo > .thumbimg > img, -.justified-layout > .photo > .thumbimg, -.unjustified-layout > .photo > .thumbimg > img, -.unjustified-layout > .photo > .thumbimg { - width: 100%; - height: 100%; - border: none; - -o-object-fit: cover; - object-fit: cover; -} - -.justified-layout > .photo > .overlay, -.unjustified-layout > .photo > .overlay { - width: 100%; - bottom: 0; - margin: 0 0 0 0; -} - -.justified-layout > .photo > .overlay > h1, -.unjustified-layout > .photo > .overlay > h1 { - width: auto; - margin-right: 15px; -} - -@media only screen and (min-width: 320px) and (max-width: 567px) { - .content > .justified-layout { - margin: 8px 8px 0 8px; - } - .content > .justified-layout .photo { - --lychee-default-height: 160px; - } -} -@media only screen and (min-width: 568px) and (max-width: 639px) { - .content > .justified-layout { - margin: 9px 9px 0 9px; - } - .content > .justified-layout .photo { - --lychee-default-height: 200px; - } -} -@media only screen and (min-width: 640px) and (max-width: 768px) { - .content > .justified-layout { - margin: 10px 10px 0 10px; - } - .content > .justified-layout .photo { - --lychee-default-height: 240px; - } -} -#footer { - z-index: 3; - left: 0; - right: 0; - bottom: 0; - text-align: center; - padding: 5px 0 5px 0; - transition: color 0.3s, opacity 0.3s ease-out, transform 0.3s ease-out, box-shadow 0.3s, margin-left 0.5s; - position: absolute; - background: #1d1d1d; -} -#footer p.hosted_by, -#footer p.home_copyright { - text-transform: uppercase; -} -#footer p { - color: #cccccc; - font-size: 0.75em; - line-height: 26px; -} -#footer p a { - color: #ccc; -} -#footer p a:visited { - color: #ccc; -} - -.hide_footer { - display: none; -} - -@font-face { - font-family: "socials"; - src: url("fonts/socials.eot?egvu10"); - src: url("fonts/socials.eot?egvu10#iefix") format("embedded-opentype"), url("fonts/socials.ttf?egvu10") format("truetype"), url("fonts/socials.woff?egvu10") format("woff"), url("fonts/socials.svg?egvu10#socials") format("svg"); - font-weight: normal; - font-style: normal; -} -#socials_footer { - padding: 0; - text-align: center; - left: 0; - right: 0; -} - -.socialicons { - display: inline-block; - font-size: 18px; - font-family: "socials" !important; - speak: none; - color: #cccccc; - text-decoration: none; - margin: 15px 15px 5px 15px; - transition: all 0.3s; - -webkit-transition: all 0.3s; - -moz-transition: all 0.3s; - -o-transition: all 0.3s; -} - -#twitter:before { - content: ""; -} - -#instagram:before { - content: ""; -} - -#youtube:before { - content: ""; -} - -#flickr:before { - content: ""; -} - -#facebook:before { - content: ""; -} - -@media (hover: hover) { - .socialicons:hover { - color: #b5b5b5; - transform: scale(1.3); - -webkit-transform: scale(1.3); - } -} -.directLinks input.text { - width: calc(100% - 30px); - color: rgba(255, 255, 255, 0.6); - padding: 2px; -} -.directLinks .basicModal__button { - display: inline-block; - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.02), inset 1px 0 0 rgba(0, 0, 0, 0.2); - width: 25px; - height: 25px; - border-radius: 5px; - border-bottom: 0; - padding: 3px 0 0; - margin-top: -5px; - float: right; -} -.directLinks .basicModal__button .iconic { - fill: #2293ec; - width: 16px; - height: 16px; -} -.directLinks .imageLinks { - margin-top: -30px; - padding-bottom: 40px; -} -.directLinks .imageLinks p { - padding: 10px 30px 0; - font-size: 12px; - line-height: 15px; -} -.directLinks .imageLinks .basicModal__button { - margin-top: -8px; -} - -.downloads { - padding: 30px; -} -.downloads .basicModal__button { - color: #2293ec; - display: inline-block; - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.02), inset 1px 0 0 rgba(0, 0, 0, 0.2); - border-radius: 5px; - border-bottom: 0; - margin: 5px 0; -} -.downloads .basicModal__button .iconic { - fill: #2293ec; - margin: 0 10px 0 1px; - width: 11px; - height: 10px; -} - -@media (hover: hover) { - .directLinks .basicModal__button:hover, -.downloads .basicModal__button:hover { - background: #2293ec; - cursor: pointer; - } - .directLinks .basicModal__button:hover .iconic, -.downloads .basicModal__button:hover .iconic { - fill: #ffffff; - } - - .downloads .basicModal__button:hover { - color: #ffffff; - } -} diff --git a/public/dist/Larapass.js b/public/dist/Larapass.js deleted file mode 100644 index 7459055b5be..00000000000 --- a/public/dist/Larapass.js +++ /dev/null @@ -1,295 +0,0 @@ -/** - * MIT License - * - * Copyright (c) Italo Israel Baeza Cabrera - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -class Larapass -{ - /** - * Headers to use in ALL requests done. - * - * @type {{Accept: string, "X-Requested-With": string, "Content-Type": string}} - */ - headers = { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'X-Requested-With': 'XMLHttpRequest' - }; - - /** - * Routes for WebAuthn assertion (login) and attestation (register). - * - * @type {{registerOptions: string, loginOptions: string, login: string, register: string}} - */ - routes = { - loginOptions: 'webauthn/login/options', - login: 'webauthn/login', - registerOptions: 'webauthn/register/options', - register: 'webauthn/register', - } - - /** - * Create a new Larapass instance. - * - * @param routes {{registerOptions: string, loginOptions: string, login: string, register: string}} - * @param headers {{string}} - */ - constructor(routes = {}, headers = {}) - { - this.routes = {...this.routes, ...routes}; - - this.headers = { - ...this.headers, - ...headers - } - - // If the developer didn't issue an XSRF token, we will find it ourselves. - if (headers['X-XSRF-TOKEN'] === undefined) { - this.headers['X-XSRF-TOKEN'] = Larapass.#getXsrfToken() - } - } - - /** - * Returns the XSRF token if it exists. - * - * @returns string|undefined - * @throws TypeError - */ - static #getXsrfToken() - { - let tokenContainer; - - // First, let's get the token if it exists as a cookie, since most apps use it by default. - tokenContainer = document.cookie.split('; ').find(row => row.startsWith('XSRF-TOKEN')) - if (tokenContainer !== undefined) { - return decodeURIComponent(tokenContainer.split('=')[1]); - } - - // If it doesn't exists, we will try to get it from the head meta tags as last resort. - tokenContainer = document.getElementsByName('csrf-token')[0]; - if (tokenContainer !== undefined) { - return tokenContainer.content; - } - - throw new TypeError('There is no cookie with "X-XSRF-TOKEN" or meta tag with "csrf-token".') - } - - /** - * Returns a fetch promise to resolve later. - * - * @param data {{string}} - * @param route {string} - * @param headers {{string}} - * @returns {Promise} - */ - #fetch(data, route, headers = {}) - { - return fetch(route, { - method: 'POST', - credentials: 'same-origin', - redirect: 'error', - headers: {...this.headers, ...headers}, - body: JSON.stringify(data) - }) - } - - /** - * Decodes a BASE64 URL string into a normal string. - * - * @param input {string} - * @returns {string|Iterable} - */ - static #base64UrlDecode(input) - { - input = input.replace(/-/g, '+').replace(/_/g, '/'); - - const pad = input.length % 4; - if (pad) { - if (pad === 1) { - throw new Error('InvalidLengthError: Input base64url string is the wrong length to determine padding'); - } - input += new Array(5-pad).join('='); - } - - return window.atob(input); - } - - /** - * Transform an string into Uint8Array instance. - * - * @param input {string} - * @param atob {boolean} - * @returns {Uint8Array} - */ - static #uint8Array(input, atob = false) - { - return Uint8Array.from( - atob ? window.atob(input) : Larapass.#base64UrlDecode(input), - c => c.charCodeAt(0) - ) - } - - /** - * Encodes an array of bytes to a BASE64 URL string - * - * @param arrayBuffer {ArrayBuffer|Uint8Array} - * @returns {string} - */ - static #arrayToBase64String(arrayBuffer) - { - return btoa(String.fromCharCode(...new Uint8Array(arrayBuffer))); - } - - /** - * Parses the Public Key Options received from the Server for the browser. - * - * @param publicKey {Object} - * @returns {Object} - */ - #parseIncomingServerOptions(publicKey) - { - publicKey.challenge = Larapass.#uint8Array(publicKey.challenge) - - if (publicKey.user !== undefined) { - publicKey.user = { - ...publicKey.user, - id: Larapass.#uint8Array(publicKey.user.id, true), - }; - } - - ['excludeCredentials', 'allowCredentials'] - .filter(key => publicKey[key] !== undefined) - .forEach(key => { - publicKey[key] = publicKey[key].map( - data => { - return { - ...data, - id: Larapass.#uint8Array(data.id), - }; - } - ) - }) - - return publicKey; - } - - /** - * Parses the outgoing credentials from the browser to the server. - * - * @param credentials {Credential|PublicKeyCredential} - * @return {{response: {string}, rawId: string, id: string, type: string}} - */ - #parseOutgoingCredentials(credentials) - { - let parseCredentials = { - id: credentials.id, - type: credentials.type, - rawId: Larapass.#arrayToBase64String(credentials.rawId), - response: {}, - }; - - ['clientDataJSON', 'attestationObject', 'authenticatorData', 'signature', 'userHandle'] - .filter(key => credentials.response[key] !== undefined) - .forEach(key => { - parseCredentials.response[key] = Larapass.#arrayToBase64String(credentials.response[key]); - }) - - return parseCredentials; - } - - /** - * Checks if the browser supports WebAuthn. - * - * @returns {boolean} - */ - static supportsWebAuthn() - { - return typeof(PublicKeyCredential) != 'undefined'; - } - - /** - * Handles the response from the Server. - * - * Throws the entire response if is not OK (HTTP 2XX). - * - * @param response {Response} - * @returns Promise - * @throws Response - */ - static #handleResponse(response) - { - if (! response.ok) { - throw response; - } - - // Here we will do a small trick. Since most of the responses from the server - // are JSON, we will automatically parse the JSON body from the response. If - // it's not JSON, we will push the body verbatim and let the dev handle it. - return new Promise(resolve => { - response.json() - .then(json => resolve(json)) - .catch(() => resolve(response.body)) - }) - } - - /** - * Log in an user with his credentials. - * - * If no credentials are given, Larapass can return a blank assertion for typeless login. - * - * @param data {{string}} - * @param headers {{string}} - * @returns Promise - */ - async login(data = {}, headers = {}) - { - const optionsResponse = await this.#fetch(data, this.routes.loginOptions) - const json = await optionsResponse.json() - const publicKey = this.#parseIncomingServerOptions(json) - const credentials = await navigator.credentials.get({publicKey}) - const publicKeyCredential = this.#parseOutgoingCredentials(credentials) - - return await this.#fetch(publicKeyCredential, this.routes.login, headers) - .then(Larapass.#handleResponse); - } - - /** - * Register the user credentials from the browser/device. - * - * You can add data if you are planning to register an user with WebAuthn from scratch. - * - * @param data {{string}} - * @param headers {{string}} - * @returns Promise - */ - async register(data = {}, headers = {}) - { - const optionsResponse = await this.#fetch(data, this.routes.registerOptions) - const json = await optionsResponse.json() - const publicKey = this.#parseIncomingServerOptions(json) - const credentials = await navigator.credentials.create({publicKey}) - const publicKeyCredential = this.#parseOutgoingCredentials(credentials) - - return await this.#fetch(publicKeyCredential, this.routes.register, headers) - .then(Larapass.#handleResponse); - } -} \ No newline at end of file diff --git a/public/dist/TV.css b/public/dist/TV.css deleted file mode 100644 index fe7513bd7d6..00000000000 --- a/public/dist/TV.css +++ /dev/null @@ -1,43 +0,0 @@ -.basicModal__button:focus { - background: #2293ec; - color: #ffffff; - cursor: pointer; - outline-style: none; -} - -.basicModal__button#basicModal__action:focus { - color: #ffffff; -} - -.content .photo:focus { - outline-style: solid; - outline-color: #ffffff; - outline-width: 10px; -} - -.content .album:focus { - outline-width: 0; -} - -.header .button:focus { - outline-width: 0; - background-color: #ffffff; -} - -.header__title:focus { - outline-width: 0; - background-color: #ffffff; - color: #000000; -} - -.header .button:focus .iconic { - fill: #000000; -} - -#imageview { - background-color: #000000; -} -#imageview #image, -#imageview #livephoto { - outline-width: 0; -} diff --git a/public/dist/cat.jpg b/public/dist/cat.jpg deleted file mode 100644 index e1e9fe7d563..00000000000 Binary files a/public/dist/cat.jpg and /dev/null differ diff --git a/public/dist/cat.webp b/public/dist/cat.webp new file mode 100644 index 00000000000..e313b6b9633 Binary files /dev/null and b/public/dist/cat.webp differ diff --git a/public/dist/frame.css b/public/dist/frame.css deleted file mode 100644 index 90fa891b21e..00000000000 --- a/public/dist/frame.css +++ /dev/null @@ -1 +0,0 @@ -body{padding:0;margin:0;background-color:#000;opacity:0;-webkit-transition:opacity 1s ease-in-out;-o-transition:opacity 1s ease-in-out;transition:opacity 1s ease-in-out}body.loaded{opacity:1}#background_canvas{width:100%;height:100%;position:absolute;z-index:-1}#background{width:100%;height:100%;position:absolute;display:none}#noise{position:absolute;top:0;left:0;width:100%;height:100%;background-image:url(../img/noise.png);background-repeat:repeat;background-position:44px 44px;z-index:-1}.image_container{margin:auto;width:95vw;height:95vh;text-align:center;line-height:100vh}.image_container img{vertical-align:middle;height:100%;width:100%;-o-object-fit:contain;object-fit:contain;-webkit-filter:drop-shadow(0 0 1px rgba(0, 0, 0, .3)) drop-shadow(0 0 10px rgba(0, 0, 0, .3));filter:drop-shadow(0 0 1px rgba(0, 0, 0, .3)) drop-shadow(0 0 10px rgba(0, 0, 0, .3));display:none} \ No newline at end of file diff --git a/public/dist/frame.js b/public/dist/frame.js deleted file mode 100644 index b6ab787d782..00000000000 --- a/public/dist/frame.js +++ /dev/null @@ -1,1227 +0,0 @@ -/*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */ -!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="
",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 049?function(){o(t,{timeout:n});if(n!==H.ricTimeout){n=H.ricTimeout}}:te(function(){I(t)},true);return function(e){var t;if(e=e===true){n=33}if(a){return}a=true;t=r-(f.now()-i);if(t<0){t=0}if(e||t<9){s()}else{I(s,t)}}},ie=function(e){var t,a;var i=99;var r=function(){t=null;e()};var n=function(){var e=f.now()-a;if(e0;if(r&&Z(i,"overflow")!="visible"){a=i.getBoundingClientRect();r=C>a.left&&pa.top-1&&g500&&O.clientWidth>500?500:370:H.expand;k._defEx=u;f=u*H.expFactor;c=H.hFac;A=null;if(w2&&h>2&&!D.hidden){w=f;N=0}else if(h>1&&N>1&&M<6){w=u}else{w=_}}if(l!==n){y=innerWidth+n*c;z=innerHeight+n;s=n*-1;l=n}a=d[t].getBoundingClientRect();if((b=a.bottom)>=s&&(g=a.top)<=z&&(C=a.right)>=s*c&&(p=a.left)<=y&&(b||C||p||g)&&(H.loadHidden||x(d[t]))&&(m&&M<3&&!o&&(h<3||N<4)||W(d[t],n))){R(d[t]);r=true;if(M>9){break}}else if(!r&&m&&!i&&M<4&&N<4&&h>2&&(v[0]||H.preloadAfterLoad)&&(v[0]||!o&&(b||C||p||g||d[t][$](H.sizesAttr)!="auto"))){i=v[0]||d[t]}}if(i&&!r){R(i)}}};var a=ae(t);var S=function(e){var t=e.target;if(t._lazyCache){delete t._lazyCache;return}L(e);K(t,H.loadedClass);Q(t,H.loadingClass);V(t,B);X(t,"lazyloaded")};var i=te(S);var B=function(e){i({target:e.target})};var T=function(e,t){var a=e.getAttribute("data-load-mode")||H.iframeLoadMode;if(a==0){e.contentWindow.location.replace(t)}else if(a==1){e.src=t}};var F=function(e){var t;var a=e[$](H.srcsetAttr);if(t=H.customMedia[e[$]("data-media")||e[$]("media")]){e.setAttribute("media",t)}if(a){e.setAttribute("srcset",a)}};var s=te(function(t,e,a,i,r){var n,s,o,l,u,f;if(!(u=X(t,"lazybeforeunveil",e)).defaultPrevented){if(i){if(a){K(t,H.autosizesClass)}else{t.setAttribute("sizes",i)}}s=t[$](H.srcsetAttr);n=t[$](H.srcAttr);if(r){o=t.parentNode;l=o&&j.test(o.nodeName||"")}f=e.firesLoad||"src"in t&&(s||n||l);u={target:t};K(t,H.loadingClass);if(f){clearTimeout(c);c=I(L,2500);V(t,B,true)}if(l){G.call(o.getElementsByTagName("source"),F)}if(s){t.setAttribute("srcset",s)}else if(n&&!l){if(d.test(t.nodeName)){T(t,n)}else{t.src=n}}if(r&&(s||l)){Y(t,{src:n})}}if(t._lazyRace){delete t._lazyRace}Q(t,H.lazyClass);ee(function(){var e=t.complete&&t.naturalWidth>1;if(!f||e){if(e){K(t,H.fastLoadedClass)}S(u);t._lazyCache=true;I(function(){if("_lazyCache"in t){delete t._lazyCache}},9)}if(t.loading=="lazy"){M--}},true)});var R=function(e){if(e._lazyRace){return}var t;var a=n.test(e.nodeName);var i=a&&(e[$](H.sizesAttr)||e[$]("sizes"));var r=i=="auto";if((r||!m)&&a&&(e[$]("src")||e.srcset)&&!e.complete&&!J(e,H.errorClass)&&J(e,H.lazyClass)){return}t=X(e,"lazyunveilread").detail;if(r){re.updateElem(e,true,e.offsetWidth)}e._lazyRace=true;M++;s(e,t,r,i,a)};var r=ie(function(){H.loadMode=3;a()});var o=function(){if(H.loadMode==3){H.loadMode=2}r()};var l=function(){if(m){return}if(f.now()-e<999){I(l,999);return}m=true;H.loadMode=3;a();q("scroll",o,true)};return{_:function(){e=f.now();k.elements=D.getElementsByClassName(H.lazyClass);v=D.getElementsByClassName(H.lazyClass+" "+H.preloadClass);q("scroll",a,true);q("resize",a,true);q("pageshow",function(e){if(e.persisted){var t=D.querySelectorAll("."+H.loadingClass);if(t.length&&t.forEach){U(function(){t.forEach(function(e){if(e.complete){R(e)}})})}}});if(u.MutationObserver){new MutationObserver(a).observe(O,{childList:true,subtree:true,attributes:true})}else{O[P]("DOMNodeInserted",a,true);O[P]("DOMAttrModified",a,true);setInterval(a,999)}q("hashchange",a,true);["focus","mouseover","click","load","transitionend","animationend"].forEach(function(e){D[P](e,a,true)});if(/d$|^c/.test(D.readyState)){l()}else{q("load",l);D[P]("DOMContentLoaded",a);I(l,2e4)}if(k.elements.length){t();ee._lsFlush()}else{a()}},checkElems:a,unveil:R,_aLSL:o}}(),re=function(){var a;var n=te(function(e,t,a,i){var r,n,s;e._lazysizesWidth=i;i+="px";e.setAttribute("sizes",i);if(j.test(t.nodeName||"")){r=t.getElementsByTagName("source");for(n=0,s=r.length;n> K), - 0 !== j - ? ((j = 255 / j), (T[b] = ((y * J) >> K) * j), (T[b + 1] = ((p * J) >> K) * j), (T[b + 2] = ((m * J) >> K) * j)) - : (T[b] = T[b + 1] = T[b + 2] = 0), - (y -= v), - (p -= w), - (m -= B), - (h -= C), - (v -= z.r), - (w -= z.g), - (B -= z.b), - (C -= z.a), - (s = (d + ((s = g + f + 1) < _ ? s : _)) << 2), - (y += E += z.r = T[s]), - (p += I += z.g = T[s + 1]), - (m += S += z.b = T[s + 2]), - (h += N += z.a = T[s + 3]), - (z = z.next), - (v += R = F.r), - (w += D = F.g), - (B += G = F.b), - (C += j = F.a), - (E -= R), - (I -= D), - (S -= G), - (N -= j), - (F = F.next), - (b += 4); - d += a; - } - for (g = 0; g < a; g++) { - for ( - I = S = N = E = p = m = h = y = 0, - v = M * (R = T[(b = g << 2)]), - w = M * (D = T[b + 1]), - B = M * (G = T[b + 2]), - C = M * (j = T[b + 3]), - y += O * R, - p += O * D, - m += O * G, - h += O * j, - q = P, - l = 0; - l < M; - l++ - ) - (q.r = R), (q.g = D), (q.b = G), (q.a = j), (q = q.next); - for (x = a, l = 1; l <= f; l++) - (b = (x + g) << 2), - (y += (q.r = R = T[b]) * (A = M - l)), - (p += (q.g = D = T[b + 1]) * A), - (m += (q.b = G = T[b + 2]) * A), - (h += (q.a = j = T[b + 3]) * A), - (E += R), - (I += D), - (S += G), - (N += j), - (q = q.next), - l < H && (x += a); - for (b = g, z = P, F = k, c = 0; c < i; c++) - (T[(s = b << 2) + 3] = j = (h * J) >> K), - j > 0 - ? ((j = 255 / j), (T[s] = ((y * J) >> K) * j), (T[s + 1] = ((p * J) >> K) * j), (T[s + 2] = ((m * J) >> K) * j)) - : (T[s] = T[s + 1] = T[s + 2] = 0), - (y -= v), - (p -= w), - (m -= B), - (h -= C), - (v -= z.r), - (w -= z.g), - (B -= z.b), - (C -= z.a), - (s = (g + ((s = c + M) < H ? s : H) * a) << 2), - (y += E += z.r = T[s]), - (p += I += z.g = T[s + 1]), - (m += S += z.b = T[s + 2]), - (h += N += z.a = T[s + 3]), - (z = z.next), - (v += R = F.r), - (w += D = F.g), - (B += G = F.b), - (C += j = F.a), - (E -= R), - (I -= D), - (S -= G), - (N -= j), - (F = F.next), - (b += a); - } - return t; - } - function f(t, e, n, r, a, i) { - if (!(isNaN(i) || i < 1)) { - i |= 0; - var f = o(t, e, n, r, a); - (f = g(f, e, n, r, a, i)), t.getContext("2d").putImageData(f, e, n); - } - } - function g(t, e, o, a, i, f) { - var g, - c, - l, - s, - x, - b, - d, - y, - p, - m, - h, - v, - w, - B, - C, - E, - I, - S, - N, - R, - D, - G = t.data, - j = 2 * f + 1, - A = a - 1, - k = i - 1, - T = f + 1, - W = (T * (T + 1)) / 2, - _ = new u(), - H = _; - for (l = 1; l < j; l++) (H = H.next = new u()), l === T && (D = H); - H.next = _; - var M = null, - O = null; - d = b = 0; - var P = n[f], - q = r[f]; - for (c = 0; c < i; c++) { - for ( - B = C = E = y = p = m = 0, - h = T * (I = G[b]), - v = T * (S = G[b + 1]), - w = T * (N = G[b + 2]), - y += W * I, - p += W * S, - m += W * N, - H = _, - l = 0; - l < T; - l++ - ) - (H.r = I), (H.g = S), (H.b = N), (H = H.next); - for (l = 1; l < T; l++) - (s = b + ((A < l ? A : l) << 2)), - (y += (H.r = I = G[s]) * (R = T - l)), - (p += (H.g = S = G[s + 1]) * R), - (m += (H.b = N = G[s + 2]) * R), - (B += I), - (C += S), - (E += N), - (H = H.next); - for (M = _, O = D, g = 0; g < a; g++) - (G[b] = (y * P) >> q), - (G[b + 1] = (p * P) >> q), - (G[b + 2] = (m * P) >> q), - (y -= h), - (p -= v), - (m -= w), - (h -= M.r), - (v -= M.g), - (w -= M.b), - (s = (d + ((s = g + f + 1) < A ? s : A)) << 2), - (y += B += M.r = G[s]), - (p += C += M.g = G[s + 1]), - (m += E += M.b = G[s + 2]), - (M = M.next), - (h += I = O.r), - (v += S = O.g), - (w += N = O.b), - (B -= I), - (C -= S), - (E -= N), - (O = O.next), - (b += 4); - d += a; - } - for (g = 0; g < a; g++) { - for ( - C = E = B = p = m = y = 0, - h = T * (I = G[(b = g << 2)]), - v = T * (S = G[b + 1]), - w = T * (N = G[b + 2]), - y += W * I, - p += W * S, - m += W * N, - H = _, - l = 0; - l < T; - l++ - ) - (H.r = I), (H.g = S), (H.b = N), (H = H.next); - for (x = a, l = 1; l <= f; l++) - (b = (x + g) << 2), - (y += (H.r = I = G[b]) * (R = T - l)), - (p += (H.g = S = G[b + 1]) * R), - (m += (H.b = N = G[b + 2]) * R), - (B += I), - (C += S), - (E += N), - (H = H.next), - l < k && (x += a); - for (b = g, M = _, O = D, c = 0; c < i; c++) - (G[(s = b << 2)] = (y * P) >> q), - (G[s + 1] = (p * P) >> q), - (G[s + 2] = (m * P) >> q), - (y -= h), - (p -= v), - (m -= w), - (h -= M.r), - (v -= M.g), - (w -= M.b), - (s = (g + ((s = c + T) < k ? s : k) * a) << 2), - (y += B += M.r = G[s]), - (p += C += M.g = G[s + 1]), - (m += E += M.b = G[s + 2]), - (M = M.next), - (h += I = O.r), - (v += S = O.g), - (w += N = O.b), - (B -= I), - (C -= S), - (E -= N), - (O = O.next), - (b += a); - } - return t; - } - var u = function t() { - !(function (t, e) { - if (!(t instanceof e)) throw new TypeError("Cannot call a class as a function"); - })(this, t), - (this.r = 0), - (this.g = 0), - (this.b = 0), - (this.a = 0), - (this.next = null); - }; - (t.BlurStack = u), - (t.image = function (t, e, n, r) { - if (("string" == typeof t && (t = document.getElementById(t)), t && "naturalWidth" in t)) { - var o = t.naturalWidth, - i = t.naturalHeight; - if (("string" == typeof e && (e = document.getElementById(e)), e && "getContext" in e)) { - (e.style.width = o + "px"), (e.style.height = i + "px"), (e.width = o), (e.height = i); - var g = e.getContext("2d"); - g.clearRect(0, 0, o, i), g.drawImage(t, 0, 0), isNaN(n) || n < 1 || (r ? a(e, 0, 0, o, i, n) : f(e, 0, 0, o, i, n)); - } - } - }), - (t.canvasRGBA = a), - (t.canvasRGB = f), - (t.imageDataRGBA = i), - (t.imageDataRGB = g), - Object.defineProperty(t, "__esModule", { value: !0 }); -}); -//# sourceMappingURL=stackblur.min.js.map - -"use strict"; - -function gup(b) { - b = b.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]"); - - var a = "[\\?&]" + b + "=([^&#]*)"; - var d = new RegExp(a); - var c = d.exec(window.location.href); - - if (c === null) return "";else return c[1]; -} - -/** - * @description This module communicates with Lychee's API - */ - -var api = { - onError: null -}; - -api.isTimeout = function (errorThrown, jqXHR) { - if (errorThrown && (errorThrown === "Bad Request" && jqXHR && jqXHR.responseJSON && jqXHR.responseJSON.error && jqXHR.responseJSON.error === "Session timed out" || errorThrown === "unknown status" && jqXHR && jqXHR.status && jqXHR.status === 419 && jqXHR.responseJSON && jqXHR.responseJSON.message && jqXHR.responseJSON.message === "CSRF token mismatch.")) { - return true; - } - - return false; -}; - -api.post = function (fn, params, callback) { - var responseProgressCB = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null; - - loadingBar.show(); - - params = $.extend({ function: fn }, params); - - var api_url = "api/" + fn; - - var success = function success(data) { - setTimeout(loadingBar.hide, 100); - - // Catch errors - if (typeof data === "string" && data.substring(0, 7) === "Error: ") { - api.onError(data.substring(7, data.length), params, data); - return false; - } - - callback(data); - }; - - var error = function error(jqXHR, textStatus, errorThrown) { - api.onError(api.isTimeout(errorThrown, jqXHR) ? "Session timed out." : "Server error or API not found.", params, errorThrown); - }; - - var ajaxParams = { - type: "POST", - url: api_url, - data: params, - dataType: "json", - success: success, - error: error - }; - - if (responseProgressCB !== null) { - ajaxParams.xhrFields = { - onprogress: responseProgressCB - }; - } - - $.ajax(ajaxParams); -}; - -api.get = function (url, callback) { - loadingBar.show(); - - var success = function success(data) { - setTimeout(loadingBar.hide, 100); - - // Catch errors - if (typeof data === "string" && data.substring(0, 7) === "Error: ") { - api.onError(data.substring(7, data.length), params, data); - return false; - } - - callback(data); - }; - - var error = function error(jqXHR, textStatus, errorThrown) { - api.onError(api.isTimeout(errorThrown, jqXHR) ? "Session timed out." : "Server error or API not found.", {}, errorThrown); - }; - - $.ajax({ - type: "GET", - url: url, - data: {}, - dataType: "text", - success: success, - error: error - }); -}; - -api.post_raw = function (fn, params, callback) { - loadingBar.show(); - - params = $.extend({ function: fn }, params); - - var api_url = "api/" + fn; - - var success = function success(data) { - setTimeout(loadingBar.hide, 100); - - // Catch errors - if (typeof data === "string" && data.substring(0, 7) === "Error: ") { - api.onError(data.substring(7, data.length), params, data); - return false; - } - - callback(data); - }; - - var error = function error(jqXHR, textStatus, errorThrown) { - api.onError(api.isTimeout(errorThrown, jqXHR) ? "Session timed out." : "Server error or API not found.", params, errorThrown); - }; - - $.ajax({ - type: "POST", - url: api_url, - data: params, - dataType: "text", - success: success, - error: error - }); -}; - -var csrf = {}; - -csrf.addLaravelCSRF = function (event, jqxhr, settings) { - if (settings.url !== lychee.updatePath) { - jqxhr.setRequestHeader("X-XSRF-TOKEN", csrf.getCookie("XSRF-TOKEN")); - } -}; - -csrf.escape = function (s) { - return s.replace(/([.*+?\^${}()|\[\]\/\\])/g, "\\$1"); -}; - -csrf.getCookie = function (name) { - // we stop the selection at = (default json) but also at % to prevent any %3D at the end of the string - var match = document.cookie.match(RegExp("(?:^|;\\s*)" + csrf.escape(name) + "=([^;^%]*)")); - return match ? match[1] : null; -}; - -csrf.bind = function () { - $(document).on("ajaxSend", csrf.addLaravelCSRF); -}; - -// Sub-implementation of lychee -------------------------------------------------------------- // - -var lychee = { - api_V2: true -}; - -lychee.content = $(".content"); - -lychee.escapeHTML = function () { - var html = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ""; - - // Ensure that html is a string - html += ""; - - // Escape all critical characters - html = html.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """).replace(/'/g, "'").replace(/`/g, "`"); - - return html; -}; - -lychee.html = function (literalSections) { - // Use raw literal sections: we don’t want - // backslashes (\n etc.) to be interpreted - var raw = literalSections.raw; - var result = ""; - - for (var _len = arguments.length, substs = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { - substs[_key - 1] = arguments[_key]; - } - - substs.forEach(function (subst, i) { - // Retrieve the literal section preceding - // the current substitution - var lit = raw[i]; - - // If the substitution is preceded by a dollar sign, - // we escape special characters in it - if (lit.slice(-1) === "$") { - subst = lychee.escapeHTML(subst); - lit = lit.slice(0, -1); - } - - result += lit; - result += subst; - }); - - // Take care of last literal section - // (Never fails, because an empty template string - // produces one literal section, an empty string) - result += raw[raw.length - 1]; - - return result; -}; - -lychee.getEventName = function () { - var touchendSupport = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent || navigator.vendor || window.opera) && "ontouchend" in document.documentElement; - return touchendSupport === true ? "touchend" : "click"; -}; - -// Sub-implementation of lychee -------------------------------------------------------------- // - -var frame = { - refresh: 30000 -}; - -frame.start_blur = function () { - var img = document.getElementById("background"); - var canvas = document.getElementById("background_canvas"); - StackBlur.image(img, canvas, 20); - canvas.style.width = "100%"; - canvas.style.height = "100%"; -}; - -frame.next = function () { - $("body").removeClass("loaded"); - setTimeout(function () { - frame.refreshPicture(); - }, 1000); -}; - -frame.refreshPicture = function () { - api.post("Photo::getRandom", {}, function (data) { - if (!data.url && (data.sizeVariants === null || data.sizeVariants.medium === null)) { - console.log("URL not found"); - } - if (data.sizeVariants.thumb === null) console.log("Thumb not found"); - - $("#background").attr("src", data.sizeVariants.thumb.url); - - var srcset = ""; - var src = ""; - this.frame.photo = null; - if (data.sizeVariants.medium !== null) { - src = data.sizeVariants.medium.url; - - if (data.sizeVariants.medium2x !== null) { - srcset = data.sizeVariants.medium.url + " " + data.sizeVariants.medium.width + "w, " + data.sizeVariants.medium2x.url + " " + data.sizeVariants.medium2x.width + "w"; - // We use it in the resize callback. - this.frame.photo = data; - } - } else { - src = data.url; - } - - $("#picture").attr("srcset", srcset); - frame.resize(); - $("#picture").attr("src", src).css("display", "inline"); - - setTimeout(function () { - frame.next(); - }, frame.refresh); - }); -}; - -frame.set = function (data) { - // console.log(data.refresh); - frame.refresh = data.refresh ? parseInt(data.refresh, 10) + 1000 : 31000; // 30 sec + 1 sec of blackout - // console.log(frame.refresh); - frame.refreshPicture(); -}; - -frame.resize = function () { - if (this.photo) { - var ratio = this.photo.height > 0 ? this.photo.width / this.photo.height : 1; - var winWidth = $(window).width(); - var winHeight = $(window).height(); - - // Our math assumes that the image occupies the whole frame. That's - // not quite the case (the default css sets it to 95%) but it's close - // enough. - var width = winWidth / ratio > winHeight ? winHeight * ratio : winWidth; - - $("#picture").attr("sizes", width + "px"); - } -}; - -frame.error = function (errorThrown) { - var params = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ""; - var data = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : ""; - - loadingBar.show("error", errorThrown); - - console.error({ - description: errorThrown, - params: params, - response: data - }); - alert(errorThrown); -}; - -// Main -------------------------------------------------------------- // - -var loadingBar = { - show: function show() {}, - hide: function hide() {} -}; - -var imageview = $("#imageview"); - -$(function () { - // set CSRF protection (Laravel) - csrf.bind(); - - // Set API error handler - api.onError = frame.error; - - $(window).on("resize", function () { - frame.resize(); - }); - - $("#background").on("load", function () { - frame.start_blur(); - }); - - $("#picture").on("load", function () { - $("body").addClass("loaded"); - }); - - api.post("Frame::getSettings", {}, function (data) { - frame.set(data); - }); -}); \ No newline at end of file diff --git a/public/dist/frontend.css b/public/dist/frontend.css new file mode 100644 index 00000000000..0682b7086a2 --- /dev/null +++ b/public/dist/frontend.css @@ -0,0 +1 @@ +@charset "UTF-8";@-webkit-keyframes basicModal__fadeIn{0%{opacity:0}100%{opacity:1}}@keyframes basicModal__fadeIn{0%{opacity:0}100%{opacity:1}}@-webkit-keyframes basicModal__fadeOut{0%{opacity:1}100%{opacity:0}}@keyframes basicModal__fadeOut{0%{opacity:1}100%{opacity:0}}@-webkit-keyframes basicModal__moveUpFade{0%{-webkit-transform:translateY(80px);transform:translateY(80px)}100%{-webkit-transform:translateY(0);transform:translateY(0)}}@keyframes basicModal__moveUpFade{0%{-webkit-transform:translateY(80px);transform:translateY(80px)}100%{-webkit-transform:translateY(0);transform:translateY(0)}}@-webkit-keyframes basicModal__shake{0%,100%{-webkit-transform:translateX(0);transform:translateX(0)}20%,60%{-webkit-transform:translateX(-10px);transform:translateX(-10px)}40%,80%{-webkit-transform:translateX(10px);transform:translateX(10px)}}@keyframes basicModal__shake{0%,100%{-webkit-transform:translateX(0);transform:translateX(0)}20%,60%{-webkit-transform:translateX(-10px);transform:translateX(-10px)}40%,80%{-webkit-transform:translateX(10px);transform:translateX(10px)}}.basicModalContainer{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;position:fixed;width:100%;height:100%;top:0;left:0;background-color:rgba(0,0,0,.4);z-index:1000;-webkit-box-sizing:border-box;box-sizing:border-box}.basicModalContainer *,.basicModalContainer :after,.basicModalContainer :before{-webkit-box-sizing:border-box;box-sizing:border-box}.basicModalContainer--fadeIn{-webkit-animation:.3s cubic-bezier(.51,.92,.24,1.15) basicModal__fadeIn;animation:.3s cubic-bezier(.51,.92,.24,1.15) basicModal__fadeIn}.basicModalContainer--fadeOut{-webkit-animation:.3s cubic-bezier(.51,.92,.24,1.15) basicModal__fadeOut;animation:.3s cubic-bezier(.51,.92,.24,1.15) basicModal__fadeOut}.basicModalContainer--fadeIn .basicModal--fadeIn{-webkit-animation:.3s cubic-bezier(.51,.92,.24,1.15) basicModal__moveUpFade;animation:.3s cubic-bezier(.51,.92,.24,1.15) basicModal__moveUpFade}.basicModalContainer--fadeIn .basicModal--shake{-webkit-animation:.3s cubic-bezier(.51,.92,.24,1.15) basicModal__shake;animation:.3s cubic-bezier(.51,.92,.24,1.15) basicModal__shake}.basicModal{position:relative;width:500px;background-color:#fff;border-radius:5px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.2);box-shadow:0 1px 2px rgba(0,0,0,.2)}.basicModal__content{padding:7%;max-height:70vh;overflow:auto;-webkit-overflow-scrolling:touch}.basicModal__buttons{display:-webkit-box;display:-ms-flexbox;display:flex;width:100%;-webkit-box-shadow:0 -1px 0 rgba(0,0,0,.1);box-shadow:0 -1px 0 rgba(0,0,0,.1)}.basicModal__button{display:inline-block;width:100%;font-weight:700;text-align:center;-webkit-transition:background-color .2s;transition:background-color .2s;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none}.basicModal__button:hover{background-color:rgba(0,0,0,.02)}.basicModal__button#basicModal__cancel{-ms-flex-negative:2;flex-shrink:2}.basicModal__button#basicModal__action{-ms-flex-negative:1;flex-shrink:1;-webkit-box-shadow:inset 1px 0 0 rgba(0,0,0,.1);box-shadow:inset 1px 0 0 rgba(0,0,0,.1)}.basicModal__button#basicModal__action:first-child{-webkit-box-shadow:none;box-shadow:none}.basicModal__button:first-child{border-radius:0 0 0 5px}.basicModal__button:last-child{border-radius:0 0 5px}.basicModal__small{max-width:340px;text-align:center}.basicModal__small .basicModal__content{padding:10% 5%}.basicModal__xclose#basicModal__cancel{position:absolute;top:-8px;right:-8px;margin:0;padding:0;width:40px;height:40px;background-color:#fff;border-radius:100%;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.2);box-shadow:0 1px 2px rgba(0,0,0,.2)}.basicModal__xclose#basicModal__cancel:after{content:"";position:absolute;left:-3px;top:8px;width:35px;height:34px;background:#fff}.basicModal__xclose#basicModal__cancel svg{position:relative;width:20px;height:39px;fill:#888;z-index:1;-webkit-transition:fill .2s;transition:fill .2s}.basicModal__xclose#basicModal__cancel:after:hover svg,.basicModal__xclose#basicModal__cancel:hover svg{fill:#2875ed}.basicModal__xclose#basicModal__cancel:active svg,.basicModal__xclose#basicModal__cancel:after:active svg{fill:#1364e3}.basicContextContainer{position:fixed;width:100%;height:100%;top:0;left:0;z-index:1000;-webkit-tap-highlight-color:transparent}.basicContext{position:absolute;opacity:0;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none;-webkit-box-sizing:border-box;box-sizing:border-box;-webkit-animation:.3s cubic-bezier(.51,.92,.24,1.2) basicContext__popIn;animation:.3s cubic-bezier(.51,.92,.24,1.2) basicContext__popIn}.basicContext *{-webkit-box-sizing:border-box;box-sizing:border-box}.basicContext__item{cursor:pointer}.basicContext__item--separator{float:left;width:100%;cursor:default}.basicContext__data{min-width:140px;text-align:left}.basicContext__icon{display:inline-block}.basicContext--scrollable{height:100%;-webkit-overflow-scrolling:touch;overflow-x:hidden;overflow-y:auto}.basicContext--scrollable .basicContext__data{min-width:160px}@-webkit-keyframes basicContext__popIn{0%{-webkit-transform:scale(0);transform:scale(0)}to{-webkit-transform:scale(1);transform:scale(1)}}@keyframes basicContext__popIn{0%{-webkit-transform:scale(0);transform:scale(0)}to{-webkit-transform:scale(1);transform:scale(1)}}a,abbr,acronym,address,applet,article,aside,audio,b,big,blockquote,body,canvas,caption,center,cite,code,dd,del,details,dfn,div,dl,dt,em,embed,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,header,hgroup,html,i,iframe,img,ins,kbd,label,legend,li,mark,menu,nav,object,ol,output,p,pre,q,ruby,s,samp,section,small,span,strike,strong,sub,summary,sup,table,tbody,td,tfoot,th,thead,time,tr,tt,u,ul,var,video{margin:0;padding:0;border:0;font:inherit;font-size:100%;vertical-align:baseline}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}body{line-height:1;background-color:#1d1d1d;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:12px;-webkit-font-smoothing:antialiased;-moz-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:after,blockquote:before,q:after,q:before{content:"";content:none}table{border-collapse:collapse;border-spacing:0}em,i{font-style:italic}b,strong{font-weight:700}*{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-transition:color .3s,opacity .3s ease-out,-webkit-transform .3s ease-out,-webkit-box-shadow .3s;transition:color .3s,opacity .3s ease-out,transform .3s ease-out,box-shadow .3s,-webkit-transform .3s ease-out,-webkit-box-shadow .3s}body,html{width:100%;height:100%;position:relative;overflow:clip}body.mode-frame div#container,body.mode-none div#container{display:none}input,textarea{-webkit-user-select:text!important;-moz-user-select:text!important;-ms-user-select:text!important;user-select:text!important}.svgsprite{display:none}.iconic{width:100%;height:100%}#upload{display:none}.fadeIn{-webkit-animation-name:fadeIn;animation-name:fadeIn;-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards;-webkit-animation-timing-function:cubic-bezier(.51,.92,.24,1);animation-timing-function:cubic-bezier(.51,.92,.24,1)}.fadeOut{-webkit-animation-name:fadeOut;animation-name:fadeOut;-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards;-webkit-animation-timing-function:cubic-bezier(.51,.92,.24,1);animation-timing-function:cubic-bezier(.51,.92,.24,1)}@-webkit-keyframes fadeIn{0%{opacity:0}100%{opacity:1}}@keyframes fadeIn{0%{opacity:0}100%{opacity:1}}@-webkit-keyframes fadeOut{0%{opacity:1}100%{opacity:0}}@keyframes fadeOut{0%{opacity:1}100%{opacity:0}}@-webkit-keyframes moveBackground{0%{background-position-x:0}100%{background-position-x:-100px}}@keyframes moveBackground{0%{background-position-x:0}100%{background-position-x:-100px}}@-webkit-keyframes zoomIn{0%{opacity:0;-webkit-transform:scale(.8);transform:scale(.8)}100%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}}@keyframes zoomIn{0%{opacity:0;-webkit-transform:scale(.8);transform:scale(.8)}100%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}}@-webkit-keyframes zoomOut{0%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}100%{opacity:0;-webkit-transform:scale(.8);transform:scale(.8)}}@keyframes zoomOut{0%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}100%{opacity:0;-webkit-transform:scale(.8);transform:scale(.8)}}@-webkit-keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}}@keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}}body.mode-frame #lychee_application_container,body.mode-none #lychee_application_container{display:none}.hflex-container,.hflex-item-rigid,.hflex-item-stretch,.vflex-container,.vflex-item-rigid,.vflex-item-stretch{position:relative;overflow:clip}.hflex-container,.vflex-container{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-line-pack:stretch;align-content:stretch;gap:0 0}.vflex-container{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.hflex-container{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.hflex-item-stretch,.vflex-item-stretch{-webkit-box-flex:1;-ms-flex:auto;flex:auto}.hflex-item-stretch{width:0;height:100%}.vflex-item-stretch{width:100%;height:0}.hflex-item-rigid,.vflex-item-rigid{-webkit-box-flex:0;-ms-flex:none;flex:none}.hflex-item-rigid{width:auto;height:100%}.vflex-item-rigid{width:100%;height:auto}.overlay-container{position:absolute;display:none;top:0;left:0;width:100%;height:100%;background-color:#000;-webkit-transition:background-color .3s;transition:background-color .3s}.overlay-container.full{cursor:none}.overlay-container.active{display:unset}#lychee_view_content{height:auto;-webkit-box-flex:1;-ms-flex:1 0 auto;flex:1 0 auto;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-line-pack:start;align-content:flex-start;padding-bottom:16px;-webkit-overflow-scrolling:touch}#lychee_view_content.contentZoomIn .album,#lychee_view_content.contentZoomIn .photo{-webkit-animation-name:zoomIn;animation-name:zoomIn}#lychee_view_content.contentZoomIn .divider{-webkit-animation-name:fadeIn;animation-name:fadeIn}#lychee_view_content.contentZoomOut .album,#lychee_view_content.contentZoomOut .photo{-webkit-animation-name:zoomOut;animation-name:zoomOut}#lychee_view_content.contentZoomOut .divider{-webkit-animation-name:fadeOut;animation-name:fadeOut}.album,.photo{position:relative;width:202px;height:202px;margin:30px 0 0 30px;cursor:default;-webkit-animation-duration:.2s;animation-duration:.2s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards;-webkit-animation-timing-function:cubic-bezier(.51,.92,.24,1);animation-timing-function:cubic-bezier(.51,.92,.24,1)}.album .thumbimg,.photo .thumbimg{position:absolute;width:200px;height:200px;background:#222;color:#222;-webkit-box-shadow:0 2px 5px rgba(0,0,0,.5);box-shadow:0 2px 5px rgba(0,0,0,.5);border:1px solid rgba(255,255,255,.5);-webkit-transition:opacity .3s ease-out,border-color .3s ease-out,-webkit-transform .3s ease-out;transition:opacity .3s ease-out,transform .3s ease-out,border-color .3s ease-out,-webkit-transform .3s ease-out}.album .thumbimg>img,.photo .thumbimg>img{width:100%;height:100%}.album.active .thumbimg,.album:focus .thumbimg,.photo.active .thumbimg,.photo:focus .thumbimg{border-color:#2293ec}.album:active .thumbimg,.photo:active .thumbimg{-webkit-transition:none;transition:none;border-color:#0f6ab2}.album.selected img,.photo.selected img{outline:#2293ec solid 1px}.album .video::before,.photo .video::before{content:"";position:absolute;display:block;height:100%;width:100%;background:url(../img/play-icon.png) 46% 50% no-repeat;-webkit-transition:.3s;transition:.3s;will-change:opacity,height}.album .video:focus::before,.photo .video:focus::before{opacity:.75}.album .livephoto::before,.photo .livephoto::before{content:"";position:absolute;display:block;height:100%;width:100%;background:url(../img/live-photo-icon.png) 2% 2% no-repeat;-webkit-transition:.3s;transition:.3s;will-change:opacity,height}.album .livephoto:focus::before,.photo .livephoto:focus::before{opacity:.75}.album .thumbimg:first-child,.album .thumbimg:nth-child(2){-webkit-transform:rotate(0) translateY(0) translateX(0);-ms-transform:rotate(0) translateY(0) translateX(0);transform:rotate(0) translateY(0) translateX(0);opacity:0}.album:focus .thumbimg:nth-child(1),.album:focus .thumbimg:nth-child(2){opacity:1;will-change:transform}.album:focus .thumbimg:nth-child(1){-webkit-transform:rotate(-2deg) translateY(10px) translateX(-12px);-ms-transform:rotate(-2deg) translateY(10px) translateX(-12px);transform:rotate(-2deg) translateY(10px) translateX(-12px)}.album:focus .thumbimg:nth-child(2){-webkit-transform:rotate(5deg) translateY(-8px) translateX(12px);-ms-transform:rotate(5deg) translateY(-8px) translateX(12px);transform:rotate(5deg) translateY(-8px) translateX(12px)}.blurred span{overflow:hidden}.blurred img{-webkit-filter:blur(5px);filter:blur(5px)}.album .album_counters{position:absolute;right:8px;top:8px;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;gap:4px 4px;-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end;text-align:right;font:bold 10px sans-serif;-webkit-filter:drop-shadow(0 0 4px rgba(0, 0, 0, .75));filter:drop-shadow(0 0 4px rgba(0, 0, 0, .75))}.album .album_counters .layers{position:relative;padding:6px 4px}.album .album_counters .layers .iconic{fill:#fff;width:12px;height:12px}.album .album_counters .folders,.album .album_counters .photos{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:distribute;justify-content:space-around;text-align:end}.album .album_counters .folders .iconic,.album .album_counters .photos .iconic{fill:#fff;width:15px;height:15px}.album .album_counters .folders span,.album .album_counters .photos span{position:absolute;bottom:0;color:#222;padding-right:1px;padding-left:1px}.album .album_counters .folders span{right:0;line-height:.9}.album .album_counters .photos span{right:4px;min-width:10px;background-color:#fff;padding-top:1px;line-height:1}.album .overlay,.photo .overlay{position:absolute;margin:0 1px;width:200px;background:-webkit-gradient(linear,left top,left bottom,from(rgba(0,0,0,0)),to(rgba(0,0,0,.6)));background:linear-gradient(to bottom,rgba(0,0,0,0),rgba(0,0,0,.6));bottom:1px}.album .thumbimg[data-overlay=false]+.overlay{background:0 0}.photo .overlay{opacity:0}.photo.active .overlay,.photo:focus .overlay{opacity:1}.album .overlay h1,.photo .overlay h1{min-height:19px;width:180px;margin:12px 0 5px 15px;color:#fff;text-shadow:0 1px 3px rgba(0,0,0,.4);font-size:16px;font-weight:700;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.album .overlay a,.photo .overlay a{display:block;margin:0 0 12px 15px;font-size:11px;color:#ccc;text-shadow:0 1px 3px rgba(0,0,0,.4)}.album .overlay a .iconic,.photo .overlay a .iconic{fill:#ccc;margin:0 5px 0 0;width:8px;height:8px}.album .thumbimg[data-overlay=false]+.overlay a,.album .thumbimg[data-overlay=false]+.overlay h1{text-shadow:none}.album .badges,.photo .badges{position:absolute;margin:-1px 0 0 6px}.album .subalbum_badge{position:absolute;right:0;top:0}.album .badge,.photo .badge{display:none;margin:0 0 0 6px;padding:12px 8px 6px;width:18px;background:#d92c34;-webkit-box-shadow:0 0 2px rgba(0,0,0,.6);box-shadow:0 0 2px rgba(0,0,0,.6);border-radius:0 0 5px 5px;border:1px solid #fff;border-top:none;color:#fff;text-align:center;text-shadow:0 1px 0 rgba(0,0,0,.4);opacity:.9}.album .badge--visible,.photo .badge--visible{display:inline-block}.album .badge--not--hidden,.photo .badge--not--hidden{background:#0a0}.album .badge--hidden,.photo .badge--hidden{background:#f90}.album .badge--cover,.photo .badge--cover{display:inline-block;background:#f90}.album .badge--star,.photo .badge--star{display:inline-block;background:#fc0}.album .badge--nsfw,.photo .badge--nsfw{display:inline-block;background:#ff82ee}.album .badge--list,.photo .badge--list{background:#2293ec}.album .badge--tag,.photo .badge--tag{display:inline-block;background:#0a0}.album .badge .iconic,.photo .badge .iconic{fill:#fff;width:16px;height:16px}.divider{margin:50px 0 0;padding:10px 0 0;width:100%;opacity:0;border-top:1px solid rgba(255,255,255,.02);-webkit-box-shadow:0 -1px 0 rgba(0,0,0,.2);box-shadow:0 -1px 0 rgba(0,0,0,.2);-webkit-animation-duration:.2s;animation-duration:.2s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards;-webkit-animation-timing-function:cubic-bezier(.51,.92,.24,1);animation-timing-function:cubic-bezier(.51,.92,.24,1)}.divider:first-child{margin-top:10px;border-top:0;-webkit-box-shadow:none;box-shadow:none}.divider h1{margin:0 0 0 30px;color:rgba(255,255,255,.6);font-size:14px;font-weight:700}@media only screen and (min-width:320px) and (max-width:567px){.album,.photo{--size:calc((100vw - 3px) / 3);width:calc(var(--size) - 3px);height:calc(var(--size) - 3px);margin:3px 0 0 3px}.album .thumbimg,.photo .thumbimg{width:calc(var(--size) - 5px);height:calc(var(--size) - 5px)}.album .overlay,.photo .overlay{width:calc(var(--size) - 5px)}.album .overlay h1,.photo .overlay h1{min-height:14px;width:calc(var(--size) - 19px);margin:8px 0 2px 6px;font-size:12px}.album .overlay a,.photo .overlay a{display:none}.album .badge,.photo .badge{padding:4px 3px 3px;width:12px}.album .badge .iconic,.photo .badge .iconic{width:12px;height:12px}.album .album_counters{-webkit-filter:drop-shadow(0 0 2px rgba(0, 0, 0, .75));filter:drop-shadow(0 0 2px rgba(0, 0, 0, .75));font:bold 7px sans-serif;gap:2px 2px;right:3px;top:3px}.album .album_counters .layers{position:relative;padding:3px 2px}.album .album_counters .layers .iconic{fill:#fff;width:8px;height:8px}.album .album_counters .folders .iconic,.album .album_counters .photos .iconic{width:11px;height:11px}.album .album_counters .photos span{right:3px;min-width:5px;line-height:.9;padding-top:2px}.divider{margin:20px 0 0}.divider:first-child{margin-top:0}.divider h1{margin:0 0 6px 8px;font-size:12px}}@media only screen and (min-width:568px) and (max-width:639px){.album,.photo{--size:calc((100vw - 3px) / 4);width:calc(var(--size) - 3px);height:calc(var(--size) - 3px);margin:3px 0 0 3px}.album .thumbimg,.photo .thumbimg{width:calc(var(--size) - 5px);height:calc(var(--size) - 5px)}.album .overlay,.photo .overlay{width:calc(var(--size) - 5px)}.album .overlay h1,.photo .overlay h1{min-height:14px;width:calc(var(--size) - 19px);margin:8px 0 2px 6px;font-size:12px}.album .overlay a,.photo .overlay a{display:none}.album .badge,.photo .badge{padding:4px 3px 3px;width:14px}.album .badge .iconic,.photo .badge .iconic{width:14px;height:14px}.album .album_counters{-webkit-filter:drop-shadow(0 0 2px rgba(0, 0, 0, .75));filter:drop-shadow(0 0 2px rgba(0, 0, 0, .75));font:bold 8px sans-serif;gap:3px 3px;right:4px;top:4px}.album .album_counters .layers{position:relative;padding:3px 2px}.album .album_counters .layers .iconic{fill:#fff;width:9px;height:9px}.album .album_counters .folders .iconic,.album .album_counters .photos .iconic{width:13px;height:13px}.album .album_counters .photos span{right:3px;min-width:8px;padding-top:2px}.divider{margin:24px 0 0}.divider:first-child{margin-top:0}.divider h1{margin:0 0 6px 10px}}@media only screen and (min-width:640px) and (max-width:768px){.album,.photo{--size:calc((100vw - 5px) / 5);width:calc(var(--size) - 5px);height:calc(var(--size) - 5px);margin:5px 0 0 5px}.album .thumbimg,.photo .thumbimg{width:calc(var(--size) - 7px);height:calc(var(--size) - 7px)}.album .overlay,.photo .overlay{width:calc(var(--size) - 7px)}.album .overlay h1,.photo .overlay h1{min-height:14px;width:calc(var(--size) - 21px);margin:10px 0 3px 8px;font-size:12px}.album .overlay a,.photo .overlay a{display:none}.album .badge,.photo .badge{padding:6px 4px 4px;width:16px}.album .badge .iconic,.photo .badge .iconic{width:16px;height:16px}.album .album_counters{-webkit-filter:drop-shadow(0 0 2px rgba(0, 0, 0, .75));filter:drop-shadow(0 0 2px rgba(0, 0, 0, .75));font:bold 9px sans-serif;gap:4px 4px;right:6px;top:6px}.album .album_counters .layers{position:relative;padding:3px 2px}.album .album_counters .layers .iconic{fill:#fff;width:11px;height:11px}.album .album_counters .folders .iconic,.album .album_counters .photos .iconic{width:15px;height:15px}.album .album_counters .folders span{line-height:1}.album .album_counters .photos span{right:3px;min-width:10px;padding-top:2px}.divider{margin:28px 0 0}.divider:first-child{margin-top:0}.divider h1{margin:0 0 6px 10px}}.no_content{position:absolute;top:50%;left:50%;padding-top:20px;color:rgba(255,255,255,.35);text-align:center;-webkit-transform:translateX(-50%) translateY(-50%);-ms-transform:translateX(-50%) translateY(-50%);transform:translateX(-50%) translateY(-50%)}.no_content .iconic{fill:rgba(255,255,255,.3);margin:0 0 10px;width:50px;height:50px}.no_content p{font-size:16px;font-weight:700}body.mode-gallery #lychee_frame_container,body.mode-none #lychee_frame_container,body.mode-view #lychee_frame_container{display:none}#lychee_frame_bg_canvas{width:100%;height:100%;position:absolute}#lychee_frame_bg_image{position:absolute;display:none}#lychee_frame_noise_layer{position:absolute;top:0;left:0;width:100%;height:100%;background-image:url(../img/noise.png);background-repeat:repeat;background-position:44px 44px}#lychee_frame_image_container{width:100%;height:100%;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-ms-flex-line-pack:center;align-content:center}#lychee_frame_image_container img{height:95%;width:95%;-o-object-fit:contain;object-fit:contain;-webkit-filter:drop-shadow(0 0 1px rgba(0, 0, 0, .3)) drop-shadow(0 0 10px rgba(0, 0, 0, .3));filter:drop-shadow(0 0 1px rgba(0, 0, 0, .3)) drop-shadow(0 0 10px rgba(0, 0, 0, .3))}#lychee_frame_shutter{position:absolute;width:100%;height:100%;top:0;left:0;padding:0;margin:0;background-color:#1d1d1d;opacity:1;-webkit-transition:opacity 1s ease-in-out;transition:opacity 1s ease-in-out}#lychee_frame_shutter.opened{opacity:0}#lychee_left_menu_container{width:0;background-color:#111;padding-top:16px;-webkit-transition:width .5s;transition:width .5s;height:100%;z-index:998}#lychee_left_menu,#lychee_left_menu_container.visible{width:250px}#lychee_left_menu a{padding:8px 8px 8px 32px;text-decoration:none;font-size:18px;color:#818181;display:block;cursor:pointer}#lychee_left_menu a.linkMenu{white-space:nowrap}#lychee_left_menu .iconic{display:inline-block;margin:0 10px 0 1px;width:15px;height:14px;fill:#818181}#lychee_left_menu .iconic.ionicons{margin:0 8px -2px 0;width:18px;height:18px}@media only screen and (max-width:567px),only screen and (max-width:640px) and (orientation:portrait){#lychee_left_menu,#lychee_left_menu_container{position:absolute;left:0}#lychee_left_menu_container.visible{width:100%}}@media (hover:hover){.album:hover .thumbimg,.photo:hover .thumbimg{border-color:#2293ec}.album .livephoto:hover::before,.album .video:hover::before,.photo .livephoto:hover::before,.photo .video:hover::before{opacity:.75}.album:hover .thumbimg:nth-child(1),.album:hover .thumbimg:nth-child(2),.album__dragover .thumbimg:nth-child(1),.album__dragover .thumbimg:nth-child(2){opacity:1;will-change:transform}.album:hover .thumbimg:nth-child(1),.album__dragover .thumbimg:nth-child(1){-webkit-transform:rotate(-2deg) translateY(10px) translateX(-12px);-ms-transform:rotate(-2deg) translateY(10px) translateX(-12px);transform:rotate(-2deg) translateY(10px) translateX(-12px)}.album:hover .thumbimg:nth-child(2),.album__dragover .thumbimg:nth-child(2){-webkit-transform:rotate(5deg) translateY(-8px) translateX(12px);-ms-transform:rotate(5deg) translateY(-8px) translateX(12px);transform:rotate(5deg) translateY(-8px) translateX(12px)}.photo:hover .overlay{opacity:1}#lychee_left_menu a:hover{color:#f1f1f1}}.basicContext{padding:5px 0 6px;background:-webkit-gradient(linear,left top,left bottom,from(#333),to(#252525));background:linear-gradient(to bottom,#333,#252525);-webkit-box-shadow:0 1px 4px rgba(0,0,0,.2),inset 0 1px 0 rgba(255,255,255,.05);box-shadow:0 1px 4px rgba(0,0,0,.2),inset 0 1px 0 rgba(255,255,255,.05);border-radius:5px;border:1px solid rgba(0,0,0,.7);border-bottom:1px solid rgba(0,0,0,.8);-webkit-transition:none;transition:none;max-width:240px}.basicContext__item{margin-bottom:2px;font-size:14px;color:#ccc}.basicContext__item--separator{margin:4px 0;height:2px;background:rgba(0,0,0,.2);border-bottom:1px solid rgba(255,255,255,.06)}.basicContext__item--disabled{cursor:default;opacity:.5}.basicContext__item:last-child{margin-bottom:0}.basicContext__data{min-width:auto;padding:6px 25px 7px 12px;white-space:normal;overflow-wrap:normal;-webkit-transition:none;transition:none;cursor:default}@media (hover:none) and (pointer:coarse){.basicContext__data{padding:12px 25px 12px 12px}}.basicContext__item:not(.basicContext__item--disabled):active .basicContext__data{background:-webkit-gradient(linear,left top,left bottom,from(#1178ca),to(#0f6ab2));background:linear-gradient(to bottom,#1178ca,#0f6ab2)}.basicContext__icon{margin-right:10px;width:12px;text-align:center}@media (hover:hover){.basicContext__item:not(.basicContext__item--disabled):hover .basicContext__data{background:-webkit-gradient(linear,left top,left bottom,from(#2293ec),to(#1386e1));background:linear-gradient(to bottom,#2293ec,#1386e1)}.basicContext__item:hover{color:#fff;-webkit-transition:.3s;transition:.3s;-webkit-transform:scale(1.05);-ms-transform:scale(1.05);transform:scale(1.05)}.basicContext__item:hover .iconic{fill:#fff}.basicContext__item--noHover:hover .basicContext__data{background:0 0!important}}#addMenu{top:48px!important;left:unset!important;right:4px}.basicContext__data{padding-left:40px}.basicContext__data .cover{position:absolute;background-color:#222;border-radius:2px;-webkit-box-shadow:0 0 0 1px rgba(0,0,0,.5);box-shadow:0 0 0 1px rgba(0,0,0,.5)}.basicContext__data .title{display:inline-block;margin:0 0 3px 26px}.basicContext__data .iconic{display:inline-block;margin:0 10px 0 -22px;width:11px;height:10px;fill:#fff}.basicContext__data .iconic.active{fill:#f90}.basicContext__data .iconic.ionicons{margin:0 8px -2px 0;width:14px;height:14px}.basicContext__data input#link{margin:-2px 0;padding:5px 7px 6px;width:100%;background:#333;color:#fff;-webkit-box-shadow:0 1px 0 rgba(255,255,255,.05);box-shadow:0 1px 0 rgba(255,255,255,.05);border:1px solid rgba(0,0,0,.4);border-radius:3px;outline:0}.basicContext__item--noHover .basicContext__data{padding-right:12px}div.basicModalContainer{background-color:rgba(0,0,0,.85);z-index:999}div.basicModalContainer--error{-webkit-transform:translateY(40px);-ms-transform:translateY(40px);transform:translateY(40px)}div.basicModal{background:-webkit-gradient(linear,left top,left bottom,from(#444),to(#333));background:linear-gradient(to bottom,#444,#333);-webkit-box-shadow:0 1px 4px rgba(0,0,0,.2),inset 0 1px 0 rgba(255,255,255,.05);box-shadow:0 1px 4px rgba(0,0,0,.2),inset 0 1px 0 rgba(255,255,255,.05);font-size:14px;line-height:17px}div.basicModal--error{-webkit-transform:translateY(-40px);-ms-transform:translateY(-40px);transform:translateY(-40px)}div.basicModal__buttons{-webkit-box-shadow:none;box-shadow:none}.basicModal__button{padding:13px 0 15px;background:0 0;color:#999;border-top:1px solid rgba(0,0,0,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.02);box-shadow:inset 0 1px 0 rgba(255,255,255,.02);cursor:default}.basicModal__button--busy,.basicModal__button:active{-webkit-transition:none;transition:none;background:rgba(0,0,0,.1);cursor:wait}.basicModal__button#basicModal__action{color:#2293ec;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.02),inset 1px 0 0 rgba(0,0,0,.2);box-shadow:inset 0 1px 0 rgba(255,255,255,.02),inset 1px 0 0 rgba(0,0,0,.2)}.basicModal__button#basicModal__action.red,.basicModal__button#basicModal__cancel.red{color:#d92c34}.basicModal__button.hidden{display:none}div.basicModal__content{padding:36px;color:#ececec;text-align:left}div.basicModal__content>*{display:block;width:100%;margin:24px 0;padding:0}div.basicModal__content>.force-first-child,div.basicModal__content>:first-child{margin-top:0}div.basicModal__content>.force-last-child,div.basicModal__content>:last-child{margin-bottom:0}div.basicModal__content .disabled{color:#999}div.basicModal__content b{font-weight:700;color:#fff}div.basicModal__content a{color:inherit;text-decoration:none;border-bottom:1px dashed #ececec}div.basicModal__content a.button{display:inline-block;margin:0 6px;padding:3px 12px;color:#2293ec;text-align:center;border-radius:5px;border:none;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.02),inset 1px 0 0 rgba(0,0,0,.2);box-shadow:inset 0 1px 0 rgba(255,255,255,.02),inset 1px 0 0 rgba(0,0,0,.2)}div.basicModal__content a.button .iconic{fill:#2293ec}div.basicModal__content>hr{border:none;border-top:1px solid rgba(0,0,0,.3)}#lychee_toolbar_container{-webkit-transition:height .3s ease-out;transition:height .3s ease-out}#lychee_toolbar_container.hidden{height:0}#lychee_toolbar_container,.toolbar{height:49px}.toolbar{background:-webkit-gradient(linear,left top,left bottom,from(#222),to(#1a1a1a));background:linear-gradient(to bottom,#222,#1a1a1a);border-bottom:1px solid #0f0f0f;display:none;-webkit-box-align:center;-ms-flex-align:center;align-items:center;position:relative;-webkit-box-sizing:border-box;box-sizing:border-box;width:100%}.toolbar.visible{display:-webkit-box;display:-ms-flexbox;display:flex}#lychee_toolbar_config .toolbar .button .iconic{-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg)}#lychee_toolbar_config .toolbar .header__title{padding-right:80px}.toolbar .header__title{width:100%;padding:16px 0;color:#fff;font-size:16px;font-weight:700;text-align:center;cursor:default;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;-webkit-transition:margin-left .5s;transition:margin-left .5s}.toolbar .header__title .iconic{display:none;margin:0 0 0 5px;width:10px;height:10px;fill:rgba(255,255,255,.5);-webkit-transition:fill .2s ease-out;transition:fill .2s ease-out}.toolbar .header__title:active .iconic{-webkit-transition:none;transition:none;fill:rgba(255,255,255,.8)}.toolbar .header__title--editable .iconic{display:inline-block}.toolbar .button{-ms-flex-negative:0;flex-shrink:0;padding:16px 8px;height:15px}.toolbar .button .iconic{width:15px;height:15px;fill:rgba(255,255,255,.5);-webkit-transition:fill .2s ease-out;transition:fill .2s ease-out}.toolbar .button:active .iconic{-webkit-transition:none;transition:none;fill:rgba(255,255,255,.8)}.toolbar .button--star.active .iconic{fill:#f0ef77}.toolbar .button--eye.active .iconic{fill:#d92c34}.toolbar .button--eye.active--not-hidden .iconic{fill:#0a0}.toolbar .button--eye.active--hidden .iconic{fill:#f90}.toolbar .button--share .iconic.ionicons{margin:-2px 0;width:18px;height:18px}.toolbar .button--nsfw.active .iconic{fill:#ff82ee}.toolbar .button--info.active .iconic{fill:#2293ec}.toolbar #button_back,.toolbar #button_back_home,.toolbar #button_close_config,.toolbar #button_settings,.toolbar #button_signin{padding:16px 12px 16px 18px}.toolbar .button_add{padding:16px 18px 16px 12px}.toolbar .header__divider{-ms-flex-negative:0;flex-shrink:0;width:14px}.toolbar .header__search__field{position:relative}.toolbar input[type=text].header__search{-ms-flex-negative:0;flex-shrink:0;width:80px;margin:0;padding:5px 12px 6px;background-color:#1d1d1d;color:#fff;border:1px solid rgba(0,0,0,.9);-webkit-box-shadow:0 1px 0 rgba(255,255,255,.04);box-shadow:0 1px 0 rgba(255,255,255,.04);outline:0;border-radius:50px;opacity:.6;-webkit-transition:opacity .3s ease-out,width .2s ease-out,-webkit-box-shadow .3s ease-out;transition:opacity .3s ease-out,box-shadow .3s ease-out,width .2s ease-out,-webkit-box-shadow .3s ease-out}.toolbar input[type=text].header__search:focus{width:140px;border-color:#2293ec;-webkit-box-shadow:0 1px 0 rgba(255,255,255,0);box-shadow:0 1px 0 rgba(255,255,255,0);opacity:1}.toolbar input[type=text].header__search:focus~.header__clear{opacity:1}.toolbar input[type=text].header__search::-ms-clear{display:none}.toolbar .header__clear{position:absolute;top:50%;-ms-transform:translateY(-50%);-webkit-transform:translateY(-50%);transform:translateY(-50%);right:8px;padding:0;color:rgba(255,255,255,.5);font-size:24px;opacity:0;-webkit-transition:color .2s ease-out;transition:color .2s ease-out;cursor:default}.toolbar .header__clear_nomap{right:60px}.toolbar .header__hostedwith{-ms-flex-negative:0;flex-shrink:0;padding:5px 10px;margin:11px 0;color:#888;font-size:13px;border-radius:100px;cursor:default}@media only screen and (max-width:640px){#button_move,#button_move_album,#button_nsfw_album,#button_trash,#button_trash_album,#button_visibility,#button_visibility_album{display:none!important}}@media only screen and (max-width:640px) and (max-width:567px){#button_rotate_ccwise,#button_rotate_cwise{display:none!important}.header__divider{width:0}}#imageview #image,#imageview #livephoto{position:absolute;top:30px;right:30px;bottom:30px;left:30px;margin:auto;max-width:calc(100% - 60px);max-height:calc(100% - 60px);width:auto;height:auto;-webkit-transition:top .3s,right .3s,bottom .3s,left .3s,max-width .3s,max-height .3s;transition:top .3s,right .3s,bottom .3s,left .3s,max-width .3s,max-height .3s;-webkit-animation-name:zoomIn;animation-name:zoomIn;-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-timing-function:cubic-bezier(.51,.92,.24,1.15);animation-timing-function:cubic-bezier(.51,.92,.24,1.15);background-size:contain;background-position:center;background-repeat:no-repeat}#imageview.full #image,#imageview.full #livephoto{top:0;right:0;bottom:0;left:0;max-width:100%;max-height:100%}#imageview #image_overlay{position:absolute;bottom:30px;left:30px;color:#fff;text-shadow:1px 1px 2px #000;z-index:3}#imageview #image_overlay h1{font-size:28px;font-weight:500;-webkit-transition:visibility .3s linear,opacity .3s linear;transition:visibility .3s linear,opacity .3s linear}#imageview #image_overlay p{margin-top:5px;font-size:20px;line-height:24px}#imageview #image_overlay a .iconic{fill:#fff;margin:0 5px 0 0;width:14px;height:14px}#imageview .arrow_wrapper{position:absolute;width:15%;height:calc(100% - 60px);top:60px}#imageview .arrow_wrapper--previous{left:0}#imageview .arrow_wrapper--next{right:0}#imageview .arrow_wrapper a{position:absolute;top:50%;margin:-19px 0 0;padding:8px 12px;width:16px;height:22px;background-size:100% 100%;border:1px solid rgba(255,255,255,.8);opacity:.6;z-index:2;-webkit-transition:opacity .2s ease-out,-webkit-transform .2s ease-out;transition:transform .2s ease-out,opacity .2s ease-out,-webkit-transform .2s ease-out;will-change:transform}#imageview .arrow_wrapper a#previous{left:-1px;-webkit-transform:translateX(-100%);-ms-transform:translateX(-100%);transform:translateX(-100%)}#imageview .arrow_wrapper a#next{right:-1px;-webkit-transform:translateX(100%);-ms-transform:translateX(100%);transform:translateX(100%)}#imageview .arrow_wrapper .iconic{fill:rgba(255,255,255,.8)}#imageview video{z-index:1}@media (hover:hover){.basicModal__button:hover{background:rgba(255,255,255,.02)}div.basicModal__content a.button:hover{color:#fff;background:#2293ec}.toolbar .button:hover .iconic,.toolbar .header__title:hover .iconic,div.basicModal__content a.button:hover .iconic{fill:#fff}.toolbar .header__clear:hover{color:#fff}.toolbar .header__hostedwith:hover{background-color:rgba(0,0,0,.3)}#imageview .arrow_wrapper:hover a#next,#imageview .arrow_wrapper:hover a#previous{-webkit-transform:translateX(0);-ms-transform:translateX(0);transform:translateX(0)}#imageview .arrow_wrapper a:hover{opacity:1}}@media only screen and (max-width:567px),only screen and (max-width:640px) and (orientation:portrait){#imageview #image,#imageview #livephoto{top:0;right:0;bottom:0;left:0;max-width:100%;max-height:100%}#imageview #image_overlay h1{font-size:14px}#imageview #image_overlay p{margin-top:2px;font-size:11px;line-height:13px}#imageview #image_overlay a .iconic{width:9px;height:9px}}@media only screen and (min-width:568px) and (max-width:768px),only screen and (min-width:568px) and (max-width:640px) and (orientation:landscape){#imageview #image,#imageview #livephoto{top:0;right:0;bottom:0;left:0;max-width:100%;max-height:100%}#imageview #image_overlay h1{font-size:18px}#imageview #image_overlay p{margin-top:4px;font-size:14px;line-height:16px}#imageview #image_overlay a .iconic{width:12px;height:12px}}.leaflet-marker-photo img{width:100%;height:100%}.image-leaflet-popup{width:100%}.leaflet-popup-content div{pointer-events:none;position:absolute;bottom:19px;left:22px;right:22px;padding-bottom:10px;background:-webkit-gradient(linear,left top,left bottom,from(rgba(0,0,0,0)),to(rgba(0,0,0,.6)));background:linear-gradient(to bottom,rgba(0,0,0,0),rgba(0,0,0,.6))}.leaflet-popup-content h1{top:0;position:relative;margin:12px 0 5px 15px;font-size:16px;font-weight:700;text-shadow:0 1px 3px rgba(255,255,255,.4);color:#fff;white-space:nowrap;text-overflow:ellipsis}.leaflet-popup-content span{margin-left:12px}.leaflet-popup-content svg{fill:#fff;vertical-align:middle}.leaflet-popup-content p{display:inline;font-size:11px;color:#fff}.leaflet-popup-content .iconic{width:20px;height:15px}#lychee_sidebar_container{width:0;-webkit-transition:width .3s cubic-bezier(.51,.92,.24,1);transition:width .3s cubic-bezier(.51,.92,.24,1)}#lychee_sidebar,#lychee_sidebar_container.active{width:350px}#lychee_sidebar{height:100%;background-color:rgba(25,25,25,.98);border-left:1px solid rgba(0,0,0,.2)}#lychee_sidebar_header{height:49px;background:-webkit-gradient(linear,left top,left bottom,from(rgba(255,255,255,.02)),to(rgba(0,0,0,0)));background:linear-gradient(to bottom,rgba(255,255,255,.02),rgba(0,0,0,0));border-top:1px solid #2293ec}#lychee_sidebar_header h1{margin:15px 0;color:#fff;font-size:16px;font-weight:700;text-align:center;-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;user-select:text}#lychee_sidebar_content{overflow:clip auto;-webkit-overflow-scrolling:touch}#lychee_sidebar_content .sidebar__divider{padding:12px 0 8px;width:100%;border-top:1px solid rgba(255,255,255,.02);-webkit-box-shadow:0 -1px 0 rgba(0,0,0,.2);box-shadow:0 -1px 0 rgba(0,0,0,.2)}#lychee_sidebar_content .sidebar__divider:first-child{border-top:0;-webkit-box-shadow:none;box-shadow:none}#lychee_sidebar_content .sidebar__divider h1{margin:0 0 0 20px;color:rgba(255,255,255,.6);font-size:14px;font-weight:700;-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;user-select:text}#lychee_sidebar_content .edit{display:inline-block;margin-left:3px;width:10px}#lychee_sidebar_content .edit .iconic{width:10px;height:10px;fill:rgba(255,255,255,.5);-webkit-transition:fill .2s ease-out;transition:fill .2s ease-out}#lychee_sidebar_content .edit:active .iconic{-webkit-transition:none;transition:none;fill:rgba(255,255,255,.8)}#lychee_sidebar_content table{margin:10px 0 15px 20px;width:calc(100% - 20px)}#lychee_sidebar_content table tr td{padding:5px 0;color:#fff;font-size:14px;line-height:19px;-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;user-select:text}#lychee_sidebar_content table tr td:first-child{width:110px}#lychee_sidebar_content table tr td:last-child{padding-right:10px}#lychee_sidebar_content table tr td span{-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;user-select:text}#lychee_sidebar_content #tags{width:calc(100% - 40px);margin:16px 20px 12px;color:#fff;display:inline-block}#lychee_sidebar_content #tags>div{display:inline-block}#lychee_sidebar_content #tags .empty{font-size:14px;margin:0 2px 8px 0;-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;user-select:text}#lychee_sidebar_content #tags .edit{margin-top:6px}#lychee_sidebar_content #tags .empty .edit{margin-top:0}#lychee_sidebar_content #tags .tag{cursor:default;display:inline-block;padding:6px 10px;margin:0 6px 8px 0;background-color:rgba(0,0,0,.5);border-radius:100px;font-size:12px;-webkit-transition:background-color .2s;transition:background-color .2s;-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;user-select:text}#lychee_sidebar_content #tags .tag span{display:inline-block;padding:0;margin:0 0 -2px;width:0;overflow:hidden;-webkit-transform:scale(0);-ms-transform:scale(0);transform:scale(0);-webkit-transition:width .2s,margin .2s,fill .2s ease-out,-webkit-transform .2s;transition:width .2s,margin .2s,transform .2s,fill .2s ease-out,-webkit-transform .2s}#lychee_sidebar_content #tags .tag span .iconic{fill:#d92c34;width:8px;height:8px}#lychee_sidebar_content #tags .tag span:active .iconic{-webkit-transition:none;transition:none;fill:#b22027}#lychee_sidebar_content #leaflet_map_single_photo{margin:10px 0 0 20px;height:180px;width:calc(100% - 40px)}#lychee_sidebar_content .attr_location.search{cursor:pointer}@media only screen and (max-width:567px),only screen and (max-width:640px) and (orientation:portrait){#lychee_sidebar_container{position:absolute;right:0}#lychee_sidebar{background-color:rgba(0,0,0,.6)}#lychee_sidebar,#lychee_sidebar_container.active{width:240px}#lychee_sidebar_header{height:22px}#lychee_sidebar_header h1{margin:6px 0;font-size:13px}#lychee_sidebar_content{padding-bottom:10px}#lychee_sidebar_content .sidebar__divider{padding:6px 0 2px}#lychee_sidebar_content .sidebar__divider h1{margin:0 0 0 10px;font-size:12px}#lychee_sidebar_content #tags,#lychee_sidebar_content table{margin:4px 0 6px 10px;width:calc(100% - 16px)}#lychee_sidebar_content table tr td{padding:2px 0;font-size:11px;line-height:12px}#lychee_sidebar_content table tr td:first-child{width:80px}#lychee_sidebar_content #tags .empty{margin:0;font-size:11px}}@media only screen and (min-width:568px) and (max-width:768px),only screen and (min-width:568px) and (max-width:640px) and (orientation:landscape){#lychee_sidebar,#lychee_sidebar_container.active{width:280px}#lychee_sidebar_header{height:28px}#lychee_sidebar_header h1{margin:8px 0;font-size:15px}#lychee_sidebar_content{padding-bottom:10px}#lychee_sidebar_content .sidebar__divider{padding:8px 0 4px}#lychee_sidebar_content .sidebar__divider h1{margin:0 0 0 10px;font-size:13px}#lychee_sidebar_content #tags,#lychee_sidebar_content table{margin:4px 0 6px 10px;width:calc(100% - 16px)}#lychee_sidebar_content table tr td{padding:2px 0;font-size:12px;line-height:13px}#lychee_sidebar_content table tr td:first-child{width:90px}#lychee_sidebar_content #tags .empty{margin:0;font-size:12px}}#lychee_loading{height:0;-webkit-transition:height .3s;transition:height .3s;background-size:100px 3px;background-repeat:repeat-x;-webkit-animation-name:moveBackground;animation-name:moveBackground;-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;-webkit-animation-timing-function:linear;animation-timing-function:linear}#lychee_loading.loading{height:3px;background-image:-webkit-gradient(linear,left top,right top,from(#153674),color-stop(47%,#153674),color-stop(53%,#2651ae),to(#2651ae));background-image:linear-gradient(to right,#153674 0,#153674 47%,#2651ae 53%,#2651ae 100%)}#lychee_loading.error{height:40px;background-color:#2f0d0e;background-image:-webkit-gradient(linear,left top,right top,from(#451317),color-stop(47%,#451317),color-stop(53%,#aa3039),to(#aa3039));background-image:linear-gradient(to right,#451317 0,#451317 47%,#aa3039 53%,#aa3039 100%)}#lychee_loading.success{height:40px;background-color:#070;background-image:-webkit-gradient(linear,left top,right top,from(#070),color-stop(47%,#090),color-stop(53%,#0a0),to(#0c0));background-image:linear-gradient(to right,#070 0,#090 47%,#0a0 53%,#0c0 100%)}#lychee_loading h1{margin:13px 13px 0;color:#ddd;font-size:14px;font-weight:700;text-shadow:0 1px 0 #000;text-transform:capitalize}#lychee_loading h1 span{margin-left:10px;font-weight:400;text-transform:none}div.select,input,output,select,textarea{display:inline-block;position:relative}div.select>select{display:block;width:100%}div.select,input,output,select,select option,textarea{color:#fff;background-color:#2c2c2c;margin:0;font-size:inherit;line-height:inherit;padding:0;border:none;-webkit-box-shadow:none;box-shadow:none;outline:0}input[type=password],input[type=text],select{padding-top:3px;padding-bottom:3px}input[type=password],input[type=text]{padding-left:2px;padding-right:2px;background-color:transparent;border-bottom:1px solid #222;border-radius:0;-webkit-box-shadow:0 1px 0 rgba(255,255,255,.05);box-shadow:0 1px 0 rgba(255,255,255,.05)}input[type=password]:focus,input[type=text]:focus{border-bottom-color:#2293ec}input[type=password].error,input[type=text].error{border-bottom-color:#d92c34}input[type=checkbox]{top:2px;height:16px;width:16px;-webkit-appearance:none;-moz-appearance:none;appearance:none;color:#2293ec;border:none;border-radius:3px;-webkit-box-shadow:0 0 0 1px rgba(0,0,0,.7);box-shadow:0 0 0 1px rgba(0,0,0,.7)}input[type=checkbox]::before{content:"✔";position:absolute;text-align:center;font-size:16px;line-height:16px;top:0;bottom:0;left:0;right:0;width:auto;height:auto;visibility:hidden}input[type=checkbox]:checked::before{visibility:visible}input[type=checkbox].slider{top:5px;height:22px;width:42px;border:1px solid rgba(0,0,0,.2);-webkit-box-shadow:0 1px 0 rgba(255,255,255,.02);box-shadow:0 1px 0 rgba(255,255,255,.02);border-radius:11px;background:#2c2c2c}input[type=checkbox].slider::before{content:"";background-color:#2293ec;height:14px;width:14px;left:3px;top:3px;border:none;border-radius:7px;visibility:visible}input[type=checkbox].slider:checked{background-color:#2293ec}input[type=checkbox].slider:checked::before{left:auto;right:3px;background-color:#fff}div.select{font-size:12px;background:#2c2c2c;border-radius:3px;border:1px solid rgba(0,0,0,.2);-webkit-box-shadow:0 1px 0 rgba(255,255,255,.02);box-shadow:0 1px 0 rgba(255,255,255,.02)}div.select::after{position:absolute;content:"≡";right:8px;top:3px;color:#2293ec;font-size:16px;font-weight:700;pointer-events:none}select{padding-left:8px;padding-right:8px;-webkit-appearance:none;-moz-appearance:none;appearance:none;background:0 0}select option{padding:2px 0;-webkit-transition:none;transition:none}form div.input-group{position:relative;margin:18px 0}form div.input-group:first-child{margin-top:0}form div.input-group:last-child{margin-bottom:0}form div.input-group.hidden{display:none}form div.input-group label{font-weight:700}form div.input-group p{display:block;margin:6px 0;font-size:13px;line-height:16px}form div.input-group p:last-child{margin-bottom:0}form div.input-group.stacked>label{display:block;margin-bottom:6px}form div.input-group.stacked>label>input[type=password],form div.input-group.stacked>label>input[type=text]{margin-top:12px}form div.input-group.stacked>div.select,form div.input-group.stacked>input,form div.input-group.stacked>output,form div.input-group.stacked>textarea{width:100%;display:block}form div.input-group.compact{padding-left:120px}form div.input-group.compact>label{display:block;position:absolute;margin:0;left:0;width:108px;height:auto;top:3px;bottom:0;overflow-y:hidden}form div.input-group.compact>div.select,form div.input-group.compact>input,form div.input-group.compact>output,form div.input-group.compact>textarea{display:block;width:100%}form div.input-group.compact-inverse{padding-left:36px}form div.input-group.compact-inverse label{display:block}form div.input-group.compact-inverse>div.select,form div.input-group.compact-inverse>input,form div.input-group.compact-inverse>output,form div.input-group.compact-inverse>textarea{display:block;position:absolute;width:16px;height:16px;top:2px;left:0}form div.input-group.compact-no-indent>label{display:inline}form div.input-group.compact-no-indent>div.select,form div.input-group.compact-no-indent>input,form div.input-group.compact-no-indent>output,form div.input-group.compact-no-indent>textarea{display:inline-block;margin-left:.3em;margin-right:.3em}div.basicModal.about-dialog div.basicModal__content h1{font-size:120%;font-weight:700;text-align:center;color:#fff}div.basicModal.about-dialog div.basicModal__content h2{font-weight:700;color:#fff}div.basicModal.about-dialog div.basicModal__content p.update-status.up-to-date-git,div.basicModal.about-dialog div.basicModal__content p.update-status.up-to-date-release{display:none}div.basicModal.about-dialog div.basicModal__content p.about-desc{line-height:1.4em}div.basicModal.downloads div.basicModal__content a.button{display:block;margin:12px 0;padding:12px;font-weight:700;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.02);box-shadow:inset 0 1px 0 rgba(255,255,255,.02);border:1px solid rgba(0,0,0,.2)}div.basicModal.downloads div.basicModal__content a.button .iconic{width:12px;height:12px;margin-right:12px}div.basicModal.qr-code{width:300px}div.basicModal.qr-code div.basicModal__content{padding:12px}.basicModal.import div.basicModal__content{padding:12px 8px}.basicModal.import div.basicModal__content h1{margin-bottom:12px;color:#fff;font-size:16px;line-height:19px;font-weight:700;text-align:center}.basicModal.import div.basicModal__content ol{margin-top:12px;height:300px;background-color:#2c2c2c;overflow:hidden;overflow-y:auto;border-radius:3px;-webkit-box-shadow:inset 0 0 3px rgba(0,0,0,.4);box-shadow:inset 0 0 3px rgba(0,0,0,.4)}.basicModal.import div.basicModal__content ol li{float:left;padding:8px 0;width:100%;background-color:rgba(255,255,255,.02)}.basicModal.import div.basicModal__content ol li:nth-child(2n){background-color:rgba(255,255,255,0)}.basicModal.import div.basicModal__content ol li h2{float:left;padding:5px 10px;width:70%;color:#fff;font-size:14px;white-space:nowrap;overflow:hidden}.basicModal.import div.basicModal__content ol li p.status{float:left;padding:5px 10px;width:30%;color:#999;font-size:14px;text-align:right;-webkit-animation-name:pulse;animation-name:pulse;-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}.basicModal.import div.basicModal__content ol li p.status.error,.basicModal.import div.basicModal__content ol li p.status.success,.basicModal.import div.basicModal__content ol li p.status.warning{-webkit-animation:none;animation:none}.basicModal.import div.basicModal__content ol li p.status.error{color:#d92c34}.basicModal.import div.basicModal__content ol li p.status.warning{color:#fc0}.basicModal.import div.basicModal__content ol li p.status.success{color:#0a0}.basicModal.import div.basicModal__content ol li p.notice{float:left;padding:2px 10px 5px;width:100%;color:#999;font-size:12px;overflow:hidden;line-height:16px}.basicModal.import div.basicModal__content ol li p.notice:empty{display:none}div.basicModal.login div.basicModal__content a.button#signInKeyLess{position:absolute;display:block;color:#999;top:8px;left:8px;width:30px;height:30px;margin:0;padding:5px;cursor:pointer;-webkit-box-shadow:inset 1px 1px 0 rgba(255,255,255,.02);box-shadow:inset 1px 1px 0 rgba(255,255,255,.02);border:1px solid rgba(0,0,0,.2)}div.basicModal.login div.basicModal__content a.button#signInKeyLess .iconic{width:100%;height:100%;fill:#999}div.basicModal.login div.basicModal__content p.version{font-size:12px;text-align:right}div.basicModal.login div.basicModal__content p.version span.update-status.up-to-date-git,div.basicModal.login div.basicModal__content p.version span.update-status.up-to-date-release{display:none}@media (hover:hover){#lychee_sidebar .edit:hover .iconic{fill:#fff}#lychee_sidebar #tags .tag:hover{background-color:rgba(0,0,0,.3)}#lychee_sidebar #tags .tag:hover.search{cursor:pointer}#lychee_sidebar #tags .tag:hover span{width:9px;margin:0 0 -2px 5px;-webkit-transform:scale(1);-ms-transform:scale(1);transform:scale(1)}#lychee_sidebar #tags .tag span:hover .iconic{fill:#e1575e}div.basicModal.login div.basicModal__content a.button#signInKeyLess:hover{color:#fff;background:inherit}div.basicModal.login div.basicModal__content a.button#signInKeyLess:hover .iconic{fill:#fff}}form.photo-links div.input-group{padding-right:30px}form.photo-links div.input-group a.button{display:block;position:absolute;margin:0;padding:4px;right:0;bottom:0;width:26px;height:26px;cursor:pointer;-webkit-box-shadow:inset 1px 1px 0 rgba(255,255,255,.02);box-shadow:inset 1px 1px 0 rgba(255,255,255,.02);border:1px solid rgba(0,0,0,.2)}form.photo-links div.input-group a.button .iconic{width:100%;height:100%}form.token div.input-group{padding-right:82px}form.token div.input-group input.disabled,form.token div.input-group input[disabled]{color:#999}form.token div.input-group div.button-group{display:block;position:absolute;margin:0;padding:0;right:0;bottom:0;width:78px}form.token div.input-group div.button-group a.button{display:block;float:right;margin:0;padding:4px;bottom:4px;width:26px;height:26px;cursor:pointer;-webkit-box-shadow:inset 1px 1px 0 rgba(255,255,255,.02);box-shadow:inset 1px 1px 0 rgba(255,255,255,.02);border:1px solid rgba(0,0,0,.2)}form.token div.input-group div.button-group a.button .iconic{width:100%;height:100%}#sensitive_warning{background:rgba(100,0,0,.95);text-align:center;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;color:#fff}#sensitive_warning.active{display:-webkit-box;display:-ms-flexbox;display:flex}#sensitive_warning h1{font-size:36px;font-weight:700;border-bottom:2px solid #fff;margin-bottom:15px}#sensitive_warning p{font-size:20px;max-width:40%;margin-top:15px}.settings_view{width:90%;max-width:700px;margin-left:auto;margin-right:auto}.settings_view input.text{padding:9px 2px;width:calc(50% - 4px);background-color:transparent;color:#fff;border:none;border-bottom:1px solid #222;border-radius:0;-webkit-box-shadow:0 1px 0 rgba(255,255,255,.05);box-shadow:0 1px 0 rgba(255,255,255,.05);outline:0}.settings_view input.text:focus{border-bottom-color:#2293ec}.settings_view input.text .error{border-bottom-color:#d92c34}.settings_view .basicModal__button{color:#2293ec;display:inline-block;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.02),inset 1px 0 0 rgba(0,0,0,.2);box-shadow:inset 0 1px 0 rgba(255,255,255,.02),inset 1px 0 0 rgba(0,0,0,.2);border-radius:5px}.settings_view .basicModal__button_MORE,.settings_view .basicModal__button_SAVE{color:#b22027;border-radius:5px}.settings_view>div{font-size:14px;width:100%;padding:12px 0}.settings_view>div p{margin:0 0 5%;width:100%;color:#ccc;line-height:16px}.settings_view>div p a{color:rgba(255,255,255,.9);text-decoration:none;border-bottom:1px dashed #888}.settings_view>div p:last-of-type{margin:0}.settings_view>div input.text{width:100%}.settings_view>div textarea{padding:9px;width:calc(100% - 18px);height:100px;background-color:transparent;color:#fff;border:1px solid #666;border-radius:0;-webkit-box-shadow:0 1px 0 rgba(255,255,255,.05);box-shadow:0 1px 0 rgba(255,255,255,.05);outline:0;resize:vertical}.settings_view>div textarea:focus{border-color:#2293ec}.settings_view>div .choice{padding:0 30px 15px;width:100%;color:#fff}.settings_view>div .choice:last-child{padding-bottom:40px}.settings_view>div .choice label{float:left;color:#fff;font-size:14px;font-weight:700}.settings_view>div .choice label input{position:absolute;margin:0;opacity:0}.settings_view>div .choice label .checkbox{float:left;display:block;width:16px;height:16px;background:rgba(0,0,0,.5);border-radius:3px;-webkit-box-shadow:0 0 0 1px rgba(0,0,0,.7);box-shadow:0 0 0 1px rgba(0,0,0,.7)}.settings_view>div .choice label .checkbox .iconic{-webkit-box-sizing:border-box;box-sizing:border-box;fill:#2293ec;padding:2px;opacity:0;-ms-transform:scale(0);-webkit-transform:scale(0);transform:scale(0);-webkit-transition:opacity .2s cubic-bezier(.51,.92,.24,1),-webkit-transform .2s cubic-bezier(.51,.92,.24,1);transition:opacity .2s cubic-bezier(.51,.92,.24,1),transform .2s cubic-bezier(.51,.92,.24,1),-webkit-transform .2s cubic-bezier(.51,.92,.24,1)}.settings_view>div .select{position:relative;margin:1px 5px;padding:0;width:110px;color:#fff;border-radius:3px;border:1px solid rgba(0,0,0,.2);-webkit-box-shadow:0 1px 0 rgba(255,255,255,.02);box-shadow:0 1px 0 rgba(255,255,255,.02);font-size:11px;line-height:16px;overflow:hidden;outline:0;vertical-align:middle;background:rgba(0,0,0,.3);display:inline-block}.settings_view>div .select select{margin:0;padding:4px 8px;width:120%;color:#fff;font-size:11px;line-height:16px;border:0;outline:0;-webkit-box-shadow:none;box-shadow:none;border-radius:0;background-color:transparent;background-image:none;-moz-appearance:none;-webkit-appearance:none;appearance:none}.settings_view>div .select select option{margin:0;padding:0;background:#fff;color:#333;-webkit-transition:none;transition:none}.settings_view>div .select select:disabled{color:#000;cursor:not-allowed}.settings_view>div .select::after{position:absolute;content:"≡";right:8px;top:4px;color:#2293ec;font-size:16px;line-height:16px;font-weight:700;pointer-events:none}.settings_view>div .switch{position:relative;display:inline-block;width:42px;height:22px;bottom:-2px;line-height:24px}.settings_view>div .switch input{opacity:0;width:0;height:0}.settings_view>div .slider{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;border:1px solid rgba(0,0,0,.2);-webkit-box-shadow:0 1px 0 rgba(255,255,255,.02);box-shadow:0 1px 0 rgba(255,255,255,.02);background:rgba(0,0,0,.3);-webkit-transition:.4s;transition:.4s}.settings_view>div .slider:before{position:absolute;content:"";height:14px;width:14px;left:3px;bottom:3px;background-color:#2293ec}.settings_view>div input:checked+.slider{background-color:#2293ec}.settings_view>div input:checked+.slider:before{-ms-transform:translateX(20px);-webkit-transform:translateX(20px);transform:translateX(20px);background-color:#fff}.settings_view>div .slider.round{border-radius:20px}.settings_view>div .slider.round:before{border-radius:50%}.settings_view .setting_category{font-size:20px;width:100%;padding-top:10px;padding-left:4px;border-bottom:1px dotted #222;margin-top:20px;color:#fff;font-weight:700;text-transform:capitalize}.settings_view .setting_line{font-size:14px;width:100%}.settings_view .setting_line:first-child,.settings_view .setting_line:last-child{padding-top:50px}.settings_view .setting_line p{min-width:550px;margin:0;color:#ccc;display:inline-block;width:100%;overflow-wrap:break-word}.settings_view .setting_line p a{color:rgba(255,255,255,.9);text-decoration:none;border-bottom:1px dashed #888}.settings_view .setting_line p:last-of-type{margin:0}.settings_view .setting_line p .warning{margin-bottom:30px;color:#d92c34;font-weight:700;font-size:18px;text-align:justify;line-height:22px}.settings_view .setting_line span.text{display:inline-block;padding:9px 4px;width:calc(50% - 12px);background-color:transparent;color:#fff;border:none}.settings_view .setting_line span.text_icon{width:5%}.settings_view .setting_line span.text_icon .iconic{width:15px;height:14px;margin:0 10px 0 1px;fill:#fff}.settings_view .setting_line input.text{width:calc(50% - 4px)}@media (hover:hover){.settings_view .basicModal__button:hover{background:#2293ec;color:#fff;cursor:pointer}.settings_view .basicModal__button_MORE:hover,.settings_view .basicModal__button_SAVE:hover{background:#b22027;color:#fff}.settings_view input:hover{border-bottom:1px solid #2293ec}}@media (hover:none){#lychee_left_menu a{padding:14px 8px 14px 32px}.settings_view input.text{border-bottom:1px solid #2293ec;margin:6px 0}.settings_view>div{padding:16px 0}.settings_view .basicModal__button{background:#2293ec;color:#fff;max-width:320px;margin-top:20px}.settings_view .basicModal__button_MORE,.settings_view .basicModal__button_SAVE{background:#b22027}}@media only screen and (max-width:567px),only screen and (max-width:640px) and (orientation:portrait){.settings_view{max-width:100%}.settings_view .setting_category{font-size:14px;padding-left:0;margin-bottom:4px}.settings_view .setting_line{font-size:12px}.settings_view .setting_line:first-child{padding-top:20px}.settings_view .setting_line p{min-width:unset;line-height:20px}.settings_view .setting_line p.warning{font-size:14px;line-height:16px;margin-bottom:0}.settings_view .setting_line p input,.settings_view .setting_line p span{padding:0}.settings_view .basicModal__button_SAVE{margin-top:20px}}.users_view{width:90%;max-width:700px;margin-left:auto;margin-right:auto}.users_view_line{font-size:14px;width:100%}.users_view_line:first-child,.users_view_line:last-child{padding-top:50px}.users_view_line p{width:550px;margin:0 0 5%;color:#ccc;display:inline-block}.users_view_line p a{color:rgba(255,255,255,.9);text-decoration:none;border-bottom:1px dashed #888}.users_view_line p.line,.users_view_line p:last-of-type{margin:0}.users_view_line span.text{display:inline-block;padding:9px 6px 9px 0;width:40%;background-color:transparent;color:#fff;border:none}.users_view_line span.text_icon{width:5%;min-width:32px}.users_view_line span.text_icon .iconic{width:15px;height:14px;margin:0 8px;fill:#fff}.users_view_line input.text{padding:9px 6px 9px 0;width:40%;background-color:transparent;color:#fff;border:none;border-bottom:1px solid #222;border-radius:0;-webkit-box-shadow:0 1px 0 rgba(255,255,255,.05);box-shadow:0 1px 0 rgba(255,255,255,.05);outline:0;margin:0 0 10px}.users_view_line input.text:focus{border-bottom-color:#2293ec}.users_view_line input.text.error{border-bottom-color:#d92c34}.users_view_line .choice label input:checked~.checkbox .iconic{opacity:1;-ms-transform:scale(1);-webkit-transform:scale(1);transform:scale(1)}.users_view_line .choice{display:inline-block;width:5%;min-width:32px;color:#fff}.users_view_line .choice input{position:absolute;margin:0;opacity:0}.users_view_line .choice .checkbox{display:inline-block;width:16px;height:16px;margin:10px 8px 0;background:rgba(0,0,0,.5);border-radius:3px;-webkit-box-shadow:0 0 0 1px rgba(0,0,0,.7);box-shadow:0 0 0 1px rgba(0,0,0,.7)}.users_view_line .choice .checkbox .iconic{-webkit-box-sizing:border-box;box-sizing:border-box;fill:#2293ec;padding:2px;opacity:0;-ms-transform:scale(0);-webkit-transform:scale(0);transform:scale(0);-webkit-transition:opacity .2s cubic-bezier(.51,.92,.24,1),-webkit-transform .2s cubic-bezier(.51,.92,.24,1);transition:opacity .2s cubic-bezier(.51,.92,.24,1),transform .2s cubic-bezier(.51,.92,.24,1),-webkit-transform .2s cubic-bezier(.51,.92,.24,1)}.users_view_line .basicModal__button{display:inline-block;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.02),inset 1px 0 0 rgba(0,0,0,.2);box-shadow:inset 0 1px 0 rgba(255,255,255,.02),inset 1px 0 0 rgba(0,0,0,.2);width:10%;min-width:72px;border-radius:0}.users_view_line .basicModal__button_OK{color:#2293ec;border-radius:5px 0 0 5px;margin-right:-4px}.users_view_line .basicModal__button_OK_no_DEL{border-radius:5px;min-width:144px;width:20%}.users_view_line .basicModal__button_DEL{color:#b22027;border-radius:0 5px 5px 0}.users_view_line .basicModal__button_CREATE{width:20%;color:#090;border-radius:5px;min-width:144px}.users_view_line .select{position:relative;margin:1px 5px;padding:0;width:110px;color:#fff;border-radius:3px;border:1px solid rgba(0,0,0,.2);-webkit-box-shadow:0 1px 0 rgba(255,255,255,.02);box-shadow:0 1px 0 rgba(255,255,255,.02);font-size:11px;line-height:16px;overflow:hidden;outline:0;vertical-align:middle;background:rgba(0,0,0,.3);display:inline-block}.users_view_line .select select{margin:0;padding:4px 8px;width:120%;color:#fff;font-size:11px;line-height:16px;border:0;outline:0;-webkit-box-shadow:none;box-shadow:none;border-radius:0;background:0 0;-moz-appearance:none;-webkit-appearance:none;appearance:none}.users_view_line .select select option{margin:0;padding:0;background:#fff;color:#333;-webkit-transition:none;transition:none}.users_view_line .select::after{position:absolute;content:"≡";right:8px;top:4px;color:#2293ec;font-size:16px;line-height:16px;font-weight:700;pointer-events:none}@media (hover:hover){.users_view_line .basicModal__button:hover{cursor:pointer;color:#fff}.users_view_line .basicModal__button_OK:hover{background:#2293ec}.users_view_line .basicModal__button_DEL:hover{background:#b22027}.users_view_line .basicModal__button_CREATE:hover{background:#090}.users_view_line input:hover{border-bottom:1px solid #2293ec}}@media (hover:none){.users_view_line .basicModal__button{color:#fff}.users_view_line .basicModal__button_OK{background:#2293ec}.users_view_line .basicModal__button_DEL{background:#b22027}.users_view_line .basicModal__button_CREATE{background:#090}.users_view_line input{border-bottom:1px solid #2293ec}}@media only screen and (max-width:567px),only screen and (max-width:640px) and (orientation:portrait){.users_view{width:100%;max-width:100%;padding:20px}.users_view_line p{width:100%}.users_view_line p .text,.users_view_line p input.text{width:36%;font-size:smaller}.users_view_line .choice{margin-left:-8px;margin-right:3px}}.u2f_view{width:90%;max-width:700px;margin-left:auto;margin-right:auto}.u2f_view_line{font-size:14px;width:100%}.u2f_view_line:first-child,.u2f_view_line:last-child{padding-top:50px}.u2f_view_line p{width:550px;margin:0 0 5%;color:#ccc;display:inline-block}.u2f_view_line p a{color:rgba(255,255,255,.9);text-decoration:none;border-bottom:1px dashed #888}.u2f_view_line p.line,.u2f_view_line p:last-of-type{margin:0}.u2f_view_line p.single{text-align:center}.u2f_view_line span.text{display:inline-block;padding:9px 4px;width:80%;background-color:transparent;color:#fff;border:none}.u2f_view_line span.text_icon{width:5%}.u2f_view_line span.text_icon .iconic{width:15px;height:14px;margin:0 15px 0 1px;fill:#fff}.u2f_view_line .choice label input:checked~.checkbox .iconic{opacity:1;-ms-transform:scale(1);-webkit-transform:scale(1);transform:scale(1)}.u2f_view_line .choice{display:inline-block;width:5%;color:#fff}.u2f_view_line .choice input{position:absolute;margin:0;opacity:0}.u2f_view_line .choice .checkbox{display:inline-block;width:16px;height:16px;margin-top:10px;margin-left:2px;background:rgba(0,0,0,.5);border-radius:3px;-webkit-box-shadow:0 0 0 1px rgba(0,0,0,.7);box-shadow:0 0 0 1px rgba(0,0,0,.7)}.u2f_view_line .choice .checkbox .iconic{-webkit-box-sizing:border-box;box-sizing:border-box;fill:#2293ec;padding:2px;opacity:0;-ms-transform:scale(0);-webkit-transform:scale(0);transform:scale(0);-webkit-transition:opacity .2s cubic-bezier(.51,.92,.24,1),-webkit-transform .2s cubic-bezier(.51,.92,.24,1);transition:opacity .2s cubic-bezier(.51,.92,.24,1),transform .2s cubic-bezier(.51,.92,.24,1),-webkit-transform .2s cubic-bezier(.51,.92,.24,1)}.u2f_view_line .basicModal__button{display:inline-block;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.02),inset 1px 0 0 rgba(0,0,0,.2);box-shadow:inset 0 1px 0 rgba(255,255,255,.02),inset 1px 0 0 rgba(0,0,0,.2);width:20%;min-width:50px;border-radius:0}.u2f_view_line .basicModal__button_OK{color:#2293ec;border-radius:5px 0 0 5px}.u2f_view_line .basicModal__button_DEL{color:#b22027;border-radius:0 5px 5px 0}.u2f_view_line .basicModal__button_CREATE{width:100%;color:#090;border-radius:5px}.u2f_view_line .select{position:relative;margin:1px 5px;padding:0;width:110px;color:#fff;border-radius:3px;border:1px solid rgba(0,0,0,.2);-webkit-box-shadow:0 1px 0 rgba(255,255,255,.02);box-shadow:0 1px 0 rgba(255,255,255,.02);font-size:11px;line-height:16px;overflow:hidden;outline:0;vertical-align:middle;background:rgba(0,0,0,.3);display:inline-block}.u2f_view_line .select select{margin:0;padding:4px 8px;width:120%;color:#fff;font-size:11px;line-height:16px;border:0;outline:0;-webkit-box-shadow:none;box-shadow:none;border-radius:0;background:0 0;-moz-appearance:none;-webkit-appearance:none;appearance:none}.u2f_view_line .select select option{margin:0;padding:0;background:#fff;color:#333;-webkit-transition:none;transition:none}.u2f_view_line .select::after{position:absolute;content:"≡";right:8px;top:4px;color:#2293ec;font-size:16px;line-height:16px;font-weight:700;pointer-events:none}@media (hover:hover){.u2f_view_line .basicModal__button:hover{cursor:pointer}.u2f_view_line .basicModal__button_OK:hover{background:#2293ec;color:#fff}.u2f_view_line .basicModal__button_DEL:hover{background:#b22027;color:#fff}.u2f_view_line .basicModal__button_CREATE:hover{background:#090;color:#fff}.u2f_view_line input:hover{border-bottom:1px solid #2293ec}}@media (hover:none){.u2f_view_line .basicModal__button{color:#fff}.u2f_view_line .basicModal__button_OK{background:#2293ec}.u2f_view_line .basicModal__button_DEL{background:#b22027}.u2f_view_line .basicModal__button_CREATE{background:#090}.u2f_view_line input{border-bottom:1px solid #2293ec}}@media only screen and (max-width:567px),only screen and (max-width:640px) and (orientation:portrait){.u2f_view{width:100%;max-width:100%;padding:20px}.u2f_view_line p{width:100%}.u2f_view_line .basicModal__button_CREATE{width:80%;margin:0 10%}}.logs_diagnostics_view{width:90%;margin-left:auto;margin-right:auto;color:#ccc;font-size:12px;line-height:14px}.logs_diagnostics_view pre{font-family:monospace;-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;user-select:text;width:-webkit-fit-content;width:-moz-fit-content;width:fit-content;padding-right:30px}.clear_logs_update{padding-left:30px;margin:20px auto}.clear_logs_update .basicModal__button,.logs_diagnostics_view .basicModal__button{color:#2293ec;display:inline-block;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.02),inset 1px 0 0 rgba(0,0,0,.2);box-shadow:inset 0 1px 0 rgba(255,255,255,.02),inset 1px 0 0 rgba(0,0,0,.2);border-radius:5px}.clear_logs_update .iconic,.logs_diagnostics_view .iconic{display:inline-block;margin:0 10px 0 1px;width:13px;height:12px;fill:#2293ec}.clear_logs_update .button_left,.logs_diagnostics_view .button_left{margin-left:24px;width:400px}@media (hover:none){.clear_logs_update .basicModal__button,.logs_diagnostics_view .basicModal__button{background:#2293ec;color:#fff;max-width:320px;margin-top:20px}.clear_logs_update .iconic,.logs_diagnostics_view .iconic{fill:#fff}}@media only screen and (max-width:567px),only screen and (max-width:640px) and (orientation:portrait){.clear_logs_update,.logs_diagnostics_view{width:100%;max-width:100%;font-size:11px;line-height:12px}.clear_logs_update .basicModal__button,.clear_logs_update .button_left,.logs_diagnostics_view .basicModal__button,.logs_diagnostics_view .button_left{width:80%;margin:0 10%}.logs_diagnostics_view{padding:10px 10px 0 0}.clear_logs_update{padding:10px 10px 0;margin:0}}.sharing_view{width:90%;max-width:700px;margin-left:auto;margin-right:auto;margin-top:20px}.sharing_view .sharing_view_line{width:100%;display:block;clear:left}.sharing_view .col-xs-1,.sharing_view .col-xs-10,.sharing_view .col-xs-11,.sharing_view .col-xs-12,.sharing_view .col-xs-2,.sharing_view .col-xs-3,.sharing_view .col-xs-4,.sharing_view .col-xs-5,.sharing_view .col-xs-6,.sharing_view .col-xs-7,.sharing_view .col-xs-8,.sharing_view .col-xs-9{float:left;position:relative;min-height:1px}.sharing_view .col-xs-2{width:10%;padding-right:3%;padding-left:3%}.sharing_view .col-xs-5{width:42%}.sharing_view .btn-block+.btn-block{margin-top:5px}.sharing_view .btn-block{display:block;width:100%}.sharing_view .btn-default{color:#2293ec;border-color:#2293ec;background:rgba(0,0,0,.5);border-radius:3px;-webkit-box-shadow:0 0 0 1px rgba(0,0,0,.7);box-shadow:0 0 0 1px rgba(0,0,0,.7)}.sharing_view .btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:400;line-height:1.42857143;text-align:center;white-space:nowrap;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-image:none;border:1px solid transparent;border-radius:4px}.sharing_view select[multiple],.sharing_view select[size]{height:150px}.sharing_view .form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color .15s ease-in-out,-webkit-box-shadow .15s ease-in-out;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-box-shadow .15s ease-in-out}.sharing_view .iconic{display:inline-block;width:15px;height:14px;fill:#2293ec}.sharing_view .iconic .iconic.ionicons{margin:0 8px -2px 0;width:18px;height:18px}.sharing_view .blue .iconic{fill:#2293ec}.sharing_view .grey .iconic{fill:#b4b4b4}.sharing_view p{width:100%;color:#ccc;text-align:center;font-size:14px;display:block}.sharing_view p.with{padding:15px 0}.sharing_view span.text{display:inline-block;padding:0 2px;width:40%;background-color:transparent;color:#fff;border:none}.sharing_view span.text:last-of-type{width:5%}.sharing_view span.text .iconic{width:15px;height:14px;margin:0 10px 0 1px;fill:#fff}.sharing_view .basicModal__button{margin-top:10px;color:#2293ec;display:inline-block;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.02),inset 1px 0 0 rgba(0,0,0,.2);box-shadow:inset 0 1px 0 rgba(255,255,255,.02),inset 1px 0 0 rgba(0,0,0,.2);border-radius:5px}.sharing_view .choice label input:checked~.checkbox .iconic{opacity:1;-ms-transform:scale(1);-webkit-transform:scale(1);transform:scale(1)}.sharing_view .choice{display:inline-block;width:5%;margin:0 10px;color:#fff}.sharing_view .choice input{position:absolute;margin:0;opacity:0}.sharing_view .choice .checkbox{display:inline-block;width:16px;height:16px;margin-top:10px;margin-left:2px;background:rgba(0,0,0,.5);border-radius:3px;-webkit-box-shadow:0 0 0 1px rgba(0,0,0,.7);box-shadow:0 0 0 1px rgba(0,0,0,.7)}.sharing_view .choice .checkbox .iconic{-webkit-box-sizing:border-box;box-sizing:border-box;fill:#2293ec;padding:2px;opacity:0;-ms-transform:scale(0);-webkit-transform:scale(0);transform:scale(0);-webkit-transition:opacity .2s cubic-bezier(.51,.92,.24,1),-webkit-transform .2s cubic-bezier(.51,.92,.24,1);transition:opacity .2s cubic-bezier(.51,.92,.24,1),transform .2s cubic-bezier(.51,.92,.24,1),-webkit-transform .2s cubic-bezier(.51,.92,.24,1)}.sharing_view .select{position:relative;padding:0;color:#fff;border-radius:3px;border:1px solid rgba(0,0,0,.2);-webkit-box-shadow:0 1px 0 rgba(255,255,255,.02);box-shadow:0 1px 0 rgba(255,255,255,.02);font-size:14px;line-height:16px;outline:0;vertical-align:middle;background:rgba(0,0,0,.3);display:inline-block}.sharing_view .borderBlue{border:1px solid #2293ec}@media (hover:none){.sharing_view .basicModal__button{background:#2293ec;color:#fff}.sharing_view input{border-bottom:1px solid #2293ec}}@media only screen and (max-width:567px),only screen and (max-width:640px) and (orientation:portrait){.sharing_view{width:100%;max-width:100%;padding:10px}.sharing_view .select{font-size:12px}.sharing_view .iconic{margin-left:-4px}.sharing_view_line p{width:100%}.sharing_view_line .basicModal__button{width:80%;margin:0 10%}}#multiselect{position:absolute;background-color:rgba(0,94,204,.3);border:1px solid #005ecc;border-radius:3px;z-index:5}.justified-layout,.unjustified-layout{margin:30px;width:100%;position:relative}.justified-layout.laying-out,.unjustified-layout.laying-out{display:none}.justified-layout>.photo{position:absolute;--lychee-default-height:320px;margin:0}.unjustified-layout>.photo{float:left;max-height:240px;margin:5px}.justified-layout>.photo>.thumbimg,.justified-layout>.photo>.thumbimg>img,.unjustified-layout>.photo>.thumbimg,.unjustified-layout>.photo>.thumbimg>img{width:100%;height:100%;border:none;-o-object-fit:cover;object-fit:cover}.justified-layout>.photo>.overlay,.unjustified-layout>.photo>.overlay{width:100%;bottom:0;margin:0}.justified-layout>.photo>.overlay>h1,.unjustified-layout>.photo>.overlay>h1{width:auto;margin-right:15px}@media only screen and (min-width:320px) and (max-width:567px){.justified-layout{margin:8px}.justified-layout .photo{--lychee-default-height:160px}}@media only screen and (min-width:568px) and (max-width:639px){.justified-layout{margin:9px}.justified-layout .photo{--lychee-default-height:200px}}@media only screen and (min-width:640px) and (max-width:768px){.justified-layout{margin:10px}.justified-layout .photo{--lychee-default-height:240px}}#lychee_footer{text-align:center;padding:5px 0;background:#1d1d1d;-webkit-transition:color .3s,opacity .3s ease-out,margin-left .5s,-webkit-transform .3s ease-out,-webkit-box-shadow .3s;transition:color .3s,opacity .3s ease-out,transform .3s ease-out,box-shadow .3s,margin-left .5s,-webkit-transform .3s ease-out,-webkit-box-shadow .3s}#lychee_footer p{color:#ccc;font-size:.75em;font-weight:400;line-height:26px}#lychee_footer p a,#lychee_footer p a:visited{color:#ccc}#lychee_footer p.home_copyright,#lychee_footer p.hosted_by{text-transform:uppercase}#lychee_footer #home_socials a[href=""],#lychee_footer p:empty,.hide_footer,body.mode-frame div#footer,body.mode-none div#footer{display:none}@font-face{font-family:socials;src:url(fonts/socials.eot?egvu10);src:url(fonts/socials.eot?egvu10#iefix) format("embedded-opentype"),url(fonts/socials.ttf?egvu10) format("truetype"),url(fonts/socials.woff?egvu10) format("woff"),url(fonts/socials.svg?egvu10#socials) format("svg");font-weight:400;font-style:normal}#socials_footer{padding:0;text-align:center;left:0;right:0}.socialicons{display:inline-block;font-size:18px;font-family:socials!important;speak:none;color:#ccc;text-decoration:none;margin:15px 15px 5px;transition:.3s;-webkit-transition:.3s;-moz-transition:.3s;-o-transition:.3s}#twitter:before{content:"\ea96"}#instagram:before{content:"\ea92"}#youtube:before{content:"\ea9d"}#flickr:before{content:"\eaa4"}#facebook:before{content:"\ea91"}@media (hover:hover){.sharing_view .basicModal__button:hover{background:#2293ec;color:#fff;cursor:pointer}.sharing_view input:hover{border-bottom:1px solid #2293ec}.socialicons:hover{color:#b5b5b5;-ms-transform:scale(1.3);transform:scale(1.3);-webkit-transform:scale(1.3)}}@media tv{.basicModal__button:focus{background:#2293ec;color:#fff;cursor:pointer;outline-style:none}.basicModal__button#basicModal__action:focus{color:#fff}.photo:focus{outline:#fff solid 10px}.album:focus{outline-width:0}.toolbar .button:focus{outline-width:0;background-color:#fff}.header__title:focus{outline-width:0;background-color:#fff;color:#000}.toolbar .button:focus .iconic{fill:#000}#imageview{background-color:#000}#imageview #image,#imageview #livephoto{outline-width:0}}#lychee_view_container{position:absolute;top:0;left:0;height:100%;width:100%;overflow:clip auto}.leaflet-image-layer,.leaflet-layer,.leaflet-marker-icon,.leaflet-marker-shadow,.leaflet-pane,.leaflet-pane>canvas,.leaflet-pane>svg,.leaflet-tile,.leaflet-tile-container,.leaflet-zoom-box{position:absolute;left:0;top:0}.leaflet-container{overflow:hidden;-webkit-tap-highlight-color:transparent;background:#ddd;outline-offset:1px;font-family:"Helvetica Neue",Arial,Helvetica,sans-serif;font-size:.75rem;line-height:1.5}.leaflet-marker-icon,.leaflet-marker-shadow,.leaflet-tile{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-user-drag:none}.leaflet-tile::-moz-selection{background:0 0}.leaflet-tile::selection{background:0 0}.leaflet-safari .leaflet-tile{image-rendering:-webkit-optimize-contrast}.leaflet-safari .leaflet-tile-container{width:1600px;height:1600px;-webkit-transform-origin:0 0}.leaflet-marker-icon,.leaflet-marker-shadow{display:block}.leaflet-container .leaflet-overlay-pane svg{max-width:none!important;max-height:none!important}.leaflet-container .leaflet-marker-pane img,.leaflet-container .leaflet-shadow-pane img,.leaflet-container .leaflet-tile,.leaflet-container .leaflet-tile-pane img,.leaflet-container img.leaflet-image-layer{max-width:none!important;max-height:none!important;width:auto;padding:0}.leaflet-container img.leaflet-tile{mix-blend-mode:plus-lighter}.leaflet-container.leaflet-touch-zoom{-ms-touch-action:pan-x pan-y;touch-action:pan-x pan-y}.leaflet-container.leaflet-touch-drag{-ms-touch-action:pinch-zoom;touch-action:none;touch-action:pinch-zoom}.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom{-ms-touch-action:none;touch-action:none}.leaflet-container a{-webkit-tap-highlight-color:rgba(51,181,229,.4);color:#0078a8}.leaflet-tile{-webkit-filter:inherit;filter:inherit;visibility:hidden}.leaflet-tile-loaded{visibility:inherit}.leaflet-zoom-box{width:0;height:0;-webkit-box-sizing:border-box;box-sizing:border-box;z-index:800}.leaflet-overlay-pane svg{-moz-user-select:none}.leaflet-pane{z-index:400}.leaflet-tile-pane{z-index:200}.leaflet-overlay-pane{z-index:400}.leaflet-shadow-pane{z-index:500}.leaflet-marker-pane{z-index:600}.leaflet-tooltip-pane{z-index:650}.leaflet-popup-pane{z-index:700}.leaflet-map-pane canvas{z-index:100}.leaflet-map-pane svg{z-index:200}.leaflet-vml-shape{width:1px;height:1px}.lvml{behavior:url(#default#VML);display:inline-block;position:absolute}.leaflet-control{position:relative;z-index:800;pointer-events:visiblePainted;pointer-events:auto;float:left;clear:both}.leaflet-bottom,.leaflet-top{position:absolute;z-index:1000;pointer-events:none}.leaflet-top{top:0}.leaflet-right{right:0}.leaflet-bottom{bottom:0}.leaflet-left{left:0}.leaflet-right .leaflet-control{float:right;margin-right:10px}.leaflet-top .leaflet-control{margin-top:10px}.leaflet-bottom .leaflet-control{margin-bottom:10px}.leaflet-left .leaflet-control{margin-left:10px}.leaflet-fade-anim .leaflet-popup{opacity:0;-webkit-transition:opacity .2s linear;transition:opacity .2s linear}.leaflet-fade-anim .leaflet-map-pane .leaflet-popup{opacity:1}.leaflet-zoom-animated{-webkit-transform-origin:0 0;-ms-transform-origin:0 0;transform-origin:0 0}svg.leaflet-zoom-animated{will-change:transform}.leaflet-zoom-anim .leaflet-zoom-animated{-webkit-transition:-webkit-transform .25s cubic-bezier(0,0,.25,1);transition:transform .25s cubic-bezier(0,0,.25,1);transition:transform .25s cubic-bezier(0,0,.25,1),-webkit-transform .25s cubic-bezier(0,0,.25,1)}.leaflet-pan-anim .leaflet-tile,.leaflet-zoom-anim .leaflet-tile{-webkit-transition:none;transition:none}.leaflet-zoom-anim .leaflet-zoom-hide{visibility:hidden}.leaflet-interactive{cursor:pointer}.leaflet-grab{cursor:-webkit-grab;cursor:grab}.leaflet-crosshair,.leaflet-crosshair .leaflet-interactive{cursor:crosshair}.leaflet-control,.leaflet-popup-pane{cursor:auto}.leaflet-dragging .leaflet-grab,.leaflet-dragging .leaflet-grab .leaflet-interactive,.leaflet-dragging .leaflet-marker-draggable{cursor:move;cursor:-webkit-grabbing;cursor:grabbing}.leaflet-image-layer,.leaflet-marker-icon,.leaflet-marker-shadow,.leaflet-pane>svg path,.leaflet-tile-container{pointer-events:none}.leaflet-image-layer.leaflet-interactive,.leaflet-marker-icon.leaflet-interactive,.leaflet-pane>svg path.leaflet-interactive,svg.leaflet-image-layer.leaflet-interactive path{pointer-events:visiblePainted;pointer-events:auto}.leaflet-zoom-box{border:2px dotted #38f;background:rgba(255,255,255,.5)}.leaflet-bar{-webkit-box-shadow:0 1px 5px rgba(0,0,0,.65);box-shadow:0 1px 5px rgba(0,0,0,.65);border-radius:4px}.leaflet-bar a{background-color:#fff;border-bottom:1px solid #ccc;width:26px;height:26px;line-height:26px;text-align:center;text-decoration:none;color:#000}.leaflet-bar a,.leaflet-control-layers-toggle{background-position:50% 50%;background-repeat:no-repeat;display:block}.leaflet-bar a:focus,.leaflet-bar a:hover{background-color:#f4f4f4}.leaflet-bar a:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.leaflet-bar a:last-child{border-bottom-left-radius:4px;border-bottom-right-radius:4px;border-bottom:none}.leaflet-bar a.leaflet-disabled{cursor:default;background-color:#f4f4f4;color:#bbb}.leaflet-touch .leaflet-bar a{width:30px;height:30px;line-height:30px}.leaflet-touch .leaflet-bar a:first-child{border-top-left-radius:2px;border-top-right-radius:2px}.leaflet-touch .leaflet-bar a:last-child{border-bottom-left-radius:2px;border-bottom-right-radius:2px}.leaflet-control-zoom-in,.leaflet-control-zoom-out{font:bold 18px "Lucida Console",Monaco,monospace;text-indent:1px}.leaflet-touch .leaflet-control-zoom-in,.leaflet-touch .leaflet-control-zoom-out{font-size:22px}.leaflet-control-layers{-webkit-box-shadow:0 1px 5px rgba(0,0,0,.4);box-shadow:0 1px 5px rgba(0,0,0,.4);background:#fff;border-radius:5px}.leaflet-control-layers-toggle{background-image:url(images/layers.png);width:36px;height:36px}.leaflet-retina .leaflet-control-layers-toggle{background-image:url(images/layers-2x.png);background-size:26px 26px}.leaflet-touch .leaflet-control-layers-toggle{width:44px;height:44px}.leaflet-control-layers .leaflet-control-layers-list,.leaflet-control-layers-expanded .leaflet-control-layers-toggle{display:none}.leaflet-control-layers-expanded .leaflet-control-layers-list{display:block;position:relative}.leaflet-control-layers-expanded{padding:6px 10px 6px 6px;color:#333;background:#fff}.leaflet-control-layers-scrollbar{overflow-y:scroll;overflow-x:hidden;padding-right:5px}.leaflet-control-layers-selector{margin-top:2px;position:relative;top:1px}.leaflet-control-layers label{display:block;font-size:1.08333em}.leaflet-control-layers-separator{height:0;border-top:1px solid #ddd;margin:5px -10px 5px -6px}.leaflet-default-icon-path{background-image:url(images/marker-icon.png)}.leaflet-container .leaflet-control-attribution{background:rgba(255,255,255,.8);margin:0}.leaflet-control-attribution,.leaflet-control-scale-line{padding:0 5px;color:#333;line-height:1.4}.leaflet-control-attribution a{text-decoration:none}.leaflet-control-attribution a:focus,.leaflet-control-attribution a:hover{text-decoration:underline}.leaflet-attribution-flag{display:inline!important;vertical-align:baseline!important;width:1em;height:.6669em}.leaflet-left .leaflet-control-scale{margin-left:5px}.leaflet-bottom .leaflet-control-scale{margin-bottom:5px}.leaflet-control-scale-line{border:2px solid #777;border-top:none;line-height:1.1;padding:2px 5px 1px;white-space:nowrap;-webkit-box-sizing:border-box;box-sizing:border-box;background:rgba(255,255,255,.8);text-shadow:1px 1px #fff}.leaflet-control-scale-line:not(:first-child){border-top:2px solid #777;border-bottom:none;margin-top:-2px}.leaflet-control-scale-line:not(:first-child):not(:last-child){border-bottom:2px solid #777}.leaflet-touch .leaflet-bar,.leaflet-touch .leaflet-control-attribution,.leaflet-touch .leaflet-control-layers{-webkit-box-shadow:none;box-shadow:none}.leaflet-touch .leaflet-bar,.leaflet-touch .leaflet-control-layers{border:2px solid rgba(0,0,0,.2);background-clip:padding-box}.leaflet-popup{position:absolute;text-align:center;margin-bottom:20px}.leaflet-popup-content-wrapper{padding:1px;text-align:left;border-radius:12px}.leaflet-popup-content{margin:13px 24px 13px 20px;line-height:1.3;font-size:1.08333em;min-height:1px}.leaflet-popup-content p{margin:1.3em 0}.leaflet-popup-tip-container{width:40px;height:20px;position:absolute;left:50%;margin-top:-1px;margin-left:-20px;overflow:hidden;pointer-events:none}.leaflet-popup-tip{width:17px;height:17px;padding:1px;margin:-10px auto 0;pointer-events:auto;-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg)}.leaflet-popup-content-wrapper,.leaflet-popup-tip{background:#fff;color:#333;-webkit-box-shadow:0 3px 14px rgba(0,0,0,.4);box-shadow:0 3px 14px rgba(0,0,0,.4)}.leaflet-container a.leaflet-popup-close-button{position:absolute;top:0;right:0;border:none;text-align:center;width:24px;height:24px;font:16px/24px Tahoma,Verdana,sans-serif;color:#757575;text-decoration:none;background:0 0}.leaflet-container a.leaflet-popup-close-button:focus,.leaflet-container a.leaflet-popup-close-button:hover{color:#585858}.leaflet-popup-scrolled{overflow:auto}.leaflet-oldie .leaflet-popup-content-wrapper{-ms-zoom:1}.leaflet-oldie .leaflet-popup-tip{width:24px;margin:0 auto}.leaflet-oldie .leaflet-control-layers,.leaflet-oldie .leaflet-control-zoom,.leaflet-oldie .leaflet-popup-content-wrapper,.leaflet-oldie .leaflet-popup-tip{border:1px solid #999}.leaflet-div-icon{background:#fff;border:1px solid #666}.leaflet-tooltip{position:absolute;padding:6px;background-color:#fff;border:1px solid #fff;border-radius:3px;color:#222;white-space:nowrap;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;pointer-events:none;-webkit-box-shadow:0 1px 3px rgba(0,0,0,.4);box-shadow:0 1px 3px rgba(0,0,0,.4)}.leaflet-tooltip.leaflet-interactive{cursor:pointer;pointer-events:auto}.leaflet-tooltip-bottom:before,.leaflet-tooltip-left:before,.leaflet-tooltip-right:before,.leaflet-tooltip-top:before{position:absolute;pointer-events:none;border:6px solid transparent;background:0 0;content:""}.leaflet-tooltip-bottom{margin-top:6px}.leaflet-tooltip-top{margin-top:-6px}.leaflet-tooltip-bottom:before,.leaflet-tooltip-top:before{left:50%;margin-left:-6px}.leaflet-tooltip-top:before{bottom:0;margin-bottom:-12px;border-top-color:#fff}.leaflet-tooltip-bottom:before{top:0;margin-top:-12px;margin-left:-6px;border-bottom-color:#fff}.leaflet-tooltip-left{margin-left:-6px}.leaflet-tooltip-right{margin-left:6px}.leaflet-tooltip-left:before,.leaflet-tooltip-right:before{top:50%;margin-top:-6px}.leaflet-tooltip-left:before{right:0;margin-right:-12px;border-left-color:#fff}.leaflet-tooltip-right:before{left:0;margin-left:-12px;border-right-color:#fff}@media print{.leaflet-control{-webkit-print-color-adjust:exact;print-color-adjust:exact}}.leaflet-cluster-anim .leaflet-marker-icon,.leaflet-cluster-anim .leaflet-marker-shadow{-webkit-transition:opacity .3s ease-in,-webkit-transform .3s ease-out;transition:transform .3s ease-out,opacity .3s ease-in,-webkit-transform .3s ease-out}.leaflet-cluster-spider-leg{-webkit-transition:stroke-dashoffset .3s ease-out,stroke-opacity .3s ease-in;transition:stroke-dashoffset .3s ease-out,stroke-opacity .3s ease-in}.leaflet-marker-photo{border:2px solid #fff;-webkit-box-shadow:3px 3px 10px #888;box-shadow:3px 3px 10px #888}.leaflet-marker-photo div{width:100%;height:100%;background-size:cover;background-position:center center;background-repeat:no-repeat}.leaflet-marker-photo b{position:absolute;top:-7px;right:-11px;color:#555;background-color:#fff;border-radius:8px;height:12px;min-width:12px;line-height:12px;text-align:center;padding:3px;-webkit-box-shadow:0 3px 14px rgba(0,0,0,.4);box-shadow:0 3px 14px rgba(0,0,0,.4)} \ No newline at end of file diff --git a/public/dist/frontend.html b/public/dist/frontend.html new file mode 100644 index 00000000000..025eba9e09d --- /dev/null +++ b/public/dist/frontend.html @@ -0,0 +1,425 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+
+ +
+ +
+
+ + +
+ + × +
+ +
+
+ + +
+ + × +
+ + + +
+
+ + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + +
+
+ + +
+
+ + +
+
+ +
+ +
+ +
+ +
+ + +
+ +
+ +
+ +

+ +
+ + +
+
+ +
+
+

+ +
+
+
+
+
+
+ +
+ image background + +
+
Random Image
+
+
diff --git a/public/dist/frontend.js b/public/dist/frontend.js new file mode 100644 index 00000000000..0c6e00da829 --- /dev/null +++ b/public/dist/frontend.js @@ -0,0 +1,5622 @@ +/*! jQuery v3.7.1 | (c) OpenJS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(ie,e){"use strict";var oe=[],r=Object.getPrototypeOf,ae=oe.slice,g=oe.flat?function(e){return oe.flat.call(e)}:function(e){return oe.concat.apply([],e)},s=oe.push,se=oe.indexOf,n={},i=n.toString,ue=n.hasOwnProperty,o=ue.toString,a=o.call(Object),le={},v=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},y=function(e){return null!=e&&e===e.window},C=ie.document,u={type:!0,src:!0,nonce:!0,noModule:!0};function m(e,t,n){var r,i,o=(n=n||C).createElement("script");if(o.text=e,t)for(r in u)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function x(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[i.call(e)]||"object":typeof e}var t="3.7.1",l=/HTML$/i,ce=function(e,t){return new ce.fn.init(e,t)};function c(e){var t=!!e&&"length"in e&&e.length,n=x(e);return!v(e)&&!y(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+ge+")"+ge+"*"),x=new RegExp(ge+"|>"),j=new RegExp(g),A=new RegExp("^"+t+"$"),D={ID:new RegExp("^#("+t+")"),CLASS:new RegExp("^\\.("+t+")"),TAG:new RegExp("^("+t+"|[*])"),ATTR:new RegExp("^"+p),PSEUDO:new RegExp("^"+g),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+ge+"*(even|odd|(([+-]|)(\\d*)n|)"+ge+"*(?:([+-]|)"+ge+"*(\\d+)|))"+ge+"*\\)|)","i"),bool:new RegExp("^(?:"+f+")$","i"),needsContext:new RegExp("^"+ge+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+ge+"*((?:-\\d)?\\d*)"+ge+"*\\)|)(?=[^-]|$)","i")},N=/^(?:input|select|textarea|button)$/i,q=/^h\d$/i,L=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,H=/[+~]/,O=new RegExp("\\\\[\\da-fA-F]{1,6}"+ge+"?|\\\\([^\\r\\n\\f])","g"),P=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},M=function(){V()},R=J(function(e){return!0===e.disabled&&fe(e,"fieldset")},{dir:"parentNode",next:"legend"});try{k.apply(oe=ae.call(ye.childNodes),ye.childNodes),oe[ye.childNodes.length].nodeType}catch(e){k={apply:function(e,t){me.apply(e,ae.call(t))},call:function(e){me.apply(e,ae.call(arguments,1))}}}function I(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(V(e),e=e||T,C)){if(11!==p&&(u=L.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return k.call(n,a),n}else if(f&&(a=f.getElementById(i))&&I.contains(e,a)&&a.id===i)return k.call(n,a),n}else{if(u[2])return k.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&e.getElementsByClassName)return k.apply(n,e.getElementsByClassName(i)),n}if(!(h[t+" "]||d&&d.test(t))){if(c=t,f=e,1===p&&(x.test(t)||m.test(t))){(f=H.test(t)&&U(e.parentNode)||e)==e&&le.scope||((s=e.getAttribute("id"))?s=ce.escapeSelector(s):e.setAttribute("id",s=S)),o=(l=Y(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+Q(l[o]);c=l.join(",")}try{return k.apply(n,f.querySelectorAll(c)),n}catch(e){h(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return re(t.replace(ve,"$1"),e,n,r)}function W(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function F(e){return e[S]=!0,e}function $(e){var t=T.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function B(t){return function(e){return fe(e,"input")&&e.type===t}}function _(t){return function(e){return(fe(e,"input")||fe(e,"button"))&&e.type===t}}function z(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&R(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function X(a){return F(function(o){return o=+o,F(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function U(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}function V(e){var t,n=e?e.ownerDocument||e:ye;return n!=T&&9===n.nodeType&&n.documentElement&&(r=(T=n).documentElement,C=!ce.isXMLDoc(T),i=r.matches||r.webkitMatchesSelector||r.msMatchesSelector,r.msMatchesSelector&&ye!=T&&(t=T.defaultView)&&t.top!==t&&t.addEventListener("unload",M),le.getById=$(function(e){return r.appendChild(e).id=ce.expando,!T.getElementsByName||!T.getElementsByName(ce.expando).length}),le.disconnectedMatch=$(function(e){return i.call(e,"*")}),le.scope=$(function(){return T.querySelectorAll(":scope")}),le.cssHas=$(function(){try{return T.querySelector(":has(*,:jqfake)"),!1}catch(e){return!0}}),le.getById?(b.filter.ID=function(e){var t=e.replace(O,P);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&C){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(O,P);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&C){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):t.querySelectorAll(e)},b.find.CLASS=function(e,t){if("undefined"!=typeof t.getElementsByClassName&&C)return t.getElementsByClassName(e)},d=[],$(function(e){var t;r.appendChild(e).innerHTML="",e.querySelectorAll("[selected]").length||d.push("\\["+ge+"*(?:value|"+f+")"),e.querySelectorAll("[id~="+S+"-]").length||d.push("~="),e.querySelectorAll("a#"+S+"+*").length||d.push(".#.+[+~]"),e.querySelectorAll(":checked").length||d.push(":checked"),(t=T.createElement("input")).setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),r.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&d.push(":enabled",":disabled"),(t=T.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||d.push("\\["+ge+"*name"+ge+"*="+ge+"*(?:''|\"\")")}),le.cssHas||d.push(":has"),d=d.length&&new RegExp(d.join("|")),l=function(e,t){if(e===t)return a=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!le.sortDetached&&t.compareDocumentPosition(e)===n?e===T||e.ownerDocument==ye&&I.contains(ye,e)?-1:t===T||t.ownerDocument==ye&&I.contains(ye,t)?1:o?se.call(o,e)-se.call(o,t):0:4&n?-1:1)}),T}for(e in I.matches=function(e,t){return I(e,null,null,t)},I.matchesSelector=function(e,t){if(V(e),C&&!h[t+" "]&&(!d||!d.test(t)))try{var n=i.call(e,t);if(n||le.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){h(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(O,P),e[3]=(e[3]||e[4]||e[5]||"").replace(O,P),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||I.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&I.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return D.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&j.test(n)&&(t=Y(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(O,P).toLowerCase();return"*"===e?function(){return!0}:function(e){return fe(e,t)}},CLASS:function(e){var t=s[e+" "];return t||(t=new RegExp("(^|"+ge+")"+e+"("+ge+"|$)"))&&s(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=I.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function T(e,n,r){return v(n)?ce.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?ce.grep(e,function(e){return e===n!==r}):"string"!=typeof n?ce.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(ce.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||k,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:S.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof ce?t[0]:t,ce.merge(this,ce.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:C,!0)),w.test(r[1])&&ce.isPlainObject(t))for(r in t)v(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=C.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):v(e)?void 0!==n.ready?n.ready(e):e(ce):ce.makeArray(e,this)}).prototype=ce.fn,k=ce(C);var E=/^(?:parents|prev(?:Until|All))/,j={children:!0,contents:!0,next:!0,prev:!0};function A(e,t){while((e=e[t])&&1!==e.nodeType);return e}ce.fn.extend({has:function(e){var t=ce(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,Ce=/^$|^module$|\/(?:java|ecma)script/i;xe=C.createDocumentFragment().appendChild(C.createElement("div")),(be=C.createElement("input")).setAttribute("type","radio"),be.setAttribute("checked","checked"),be.setAttribute("name","t"),xe.appendChild(be),le.checkClone=xe.cloneNode(!0).cloneNode(!0).lastChild.checked,xe.innerHTML="",le.noCloneChecked=!!xe.cloneNode(!0).lastChild.defaultValue,xe.innerHTML="",le.option=!!xe.lastChild;var ke={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function Se(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&fe(e,t)?ce.merge([e],n):n}function Ee(e,t){for(var n=0,r=e.length;n",""]);var je=/<|&#?\w+;/;function Ae(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function Re(e,t){return fe(e,"table")&&fe(11!==t.nodeType?t:t.firstChild,"tr")&&ce(e).children("tbody")[0]||e}function Ie(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function We(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Fe(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(_.hasData(e)&&(s=_.get(e).events))for(i in _.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),C.head.appendChild(r[0])},abort:function(){i&&i()}}});var Jt,Kt=[],Zt=/(=)\?(?=&|$)|\?\?/;ce.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Kt.pop()||ce.expando+"_"+jt.guid++;return this[e]=!0,e}}),ce.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Zt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Zt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=v(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Zt,"$1"+r):!1!==e.jsonp&&(e.url+=(At.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||ce.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=ie[r],ie[r]=function(){o=arguments},n.always(function(){void 0===i?ce(ie).removeProp(r):ie[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Kt.push(r)),o&&v(i)&&i(o[0]),o=i=void 0}),"script"}),le.createHTMLDocument=((Jt=C.implementation.createHTMLDocument("").body).innerHTML="
",2===Jt.childNodes.length),ce.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(le.createHTMLDocument?((r=(t=C.implementation.createHTMLDocument("")).createElement("base")).href=C.location.href,t.head.appendChild(r)):t=C),o=!n&&[],(i=w.exec(e))?[t.createElement(i[1])]:(i=Ae([e],t,o),o&&o.length&&ce(o).remove(),ce.merge([],i.childNodes)));var r,i,o},ce.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(ce.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},ce.expr.pseudos.animated=function(t){return ce.grep(ce.timers,function(e){return t===e.elem}).length},ce.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=ce.css(e,"position"),c=ce(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=ce.css(e,"top"),u=ce.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),v(t)&&(t=t.call(e,n,ce.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},ce.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){ce.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===ce.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===ce.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=ce(e).offset()).top+=ce.css(e,"borderTopWidth",!0),i.left+=ce.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-ce.css(r,"marginTop",!0),left:t.left-i.left-ce.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===ce.css(e,"position"))e=e.offsetParent;return e||J})}}),ce.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;ce.fn[t]=function(e){return M(this,function(e,t,n){var r;if(y(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),ce.each(["top","left"],function(e,n){ce.cssHooks[n]=Ye(le.pixelPosition,function(e,t){if(t)return t=Ge(e,n),_e.test(t)?ce(e).position()[n]+"px":t})}),ce.each({Height:"height",Width:"width"},function(a,s){ce.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){ce.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return M(this,function(e,t,n){var r;return y(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?ce.css(e,t,i):ce.style(e,t,n,i)},s,n?e:void 0,n)}})}),ce.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){ce.fn[t]=function(e){return this.on(t,e)}}),ce.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.on("mouseenter",e).on("mouseleave",t||e)}}),ce.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){ce.fn[n]=function(e,t){return 049?function(){o(t,{timeout:n});if(n!==H.ricTimeout){n=H.ricTimeout}}:te(function(){I(t)},true);return function(e){var t;if(e=e===true){n=33}if(a){return}a=true;t=r-(f.now()-i);if(t<0){t=0}if(e||t<9){s()}else{I(s,t)}}},ie=function(e){var t,a;var i=99;var r=function(){t=null;e()};var n=function(){var e=f.now()-a;if(e0;if(r&&Z(i,"overflow")!="visible"){a=i.getBoundingClientRect();r=C>a.left&&pa.top-1&&g500&&O.clientWidth>500?500:370:H.expand;k._defEx=u;f=u*H.expFactor;c=H.hFac;A=null;if(w2&&h>2&&!D.hidden){w=f;N=0}else if(h>1&&N>1&&M<6){w=u}else{w=_}}if(l!==n){y=innerWidth+n*c;z=innerHeight+n;s=n*-1;l=n}a=d[t].getBoundingClientRect();if((b=a.bottom)>=s&&(g=a.top)<=z&&(C=a.right)>=s*c&&(p=a.left)<=y&&(b||C||p||g)&&(H.loadHidden||x(d[t]))&&(m&&M<3&&!o&&(h<3||N<4)||W(d[t],n))){R(d[t]);r=true;if(M>9){break}}else if(!r&&m&&!i&&M<4&&N<4&&h>2&&(v[0]||H.preloadAfterLoad)&&(v[0]||!o&&(b||C||p||g||d[t][$](H.sizesAttr)!="auto"))){i=v[0]||d[t]}}if(i&&!r){R(i)}}};var a=ae(t);var S=function(e){var t=e.target;if(t._lazyCache){delete t._lazyCache;return}L(e);K(t,H.loadedClass);Q(t,H.loadingClass);V(t,B);X(t,"lazyloaded")};var i=te(S);var B=function(e){i({target:e.target})};var T=function(e,t){var a=e.getAttribute("data-load-mode")||H.iframeLoadMode;if(a==0){e.contentWindow.location.replace(t)}else if(a==1){e.src=t}};var F=function(e){var t;var a=e[$](H.srcsetAttr);if(t=H.customMedia[e[$]("data-media")||e[$]("media")]){e.setAttribute("media",t)}if(a){e.setAttribute("srcset",a)}};var s=te(function(t,e,a,i,r){var n,s,o,l,u,f;if(!(u=X(t,"lazybeforeunveil",e)).defaultPrevented){if(i){if(a){K(t,H.autosizesClass)}else{t.setAttribute("sizes",i)}}s=t[$](H.srcsetAttr);n=t[$](H.srcAttr);if(r){o=t.parentNode;l=o&&j.test(o.nodeName||"")}f=e.firesLoad||"src"in t&&(s||n||l);u={target:t};K(t,H.loadingClass);if(f){clearTimeout(c);c=I(L,2500);V(t,B,true)}if(l){G.call(o.getElementsByTagName("source"),F)}if(s){t.setAttribute("srcset",s)}else if(n&&!l){if(d.test(t.nodeName)){T(t,n)}else{t.src=n}}if(r&&(s||l)){Y(t,{src:n})}}if(t._lazyRace){delete t._lazyRace}Q(t,H.lazyClass);ee(function(){var e=t.complete&&t.naturalWidth>1;if(!f||e){if(e){K(t,H.fastLoadedClass)}S(u);t._lazyCache=true;I(function(){if("_lazyCache"in t){delete t._lazyCache}},9)}if(t.loading=="lazy"){M--}},true)});var R=function(e){if(e._lazyRace){return}var t;var a=n.test(e.nodeName);var i=a&&(e[$](H.sizesAttr)||e[$]("sizes"));var r=i=="auto";if((r||!m)&&a&&(e[$]("src")||e.srcset)&&!e.complete&&!J(e,H.errorClass)&&J(e,H.lazyClass)){return}t=X(e,"lazyunveilread").detail;if(r){re.updateElem(e,true,e.offsetWidth)}e._lazyRace=true;M++;s(e,t,r,i,a)};var r=ie(function(){H.loadMode=3;a()});var o=function(){if(H.loadMode==3){H.loadMode=2}r()};var l=function(){if(m){return}if(f.now()-e<999){I(l,999);return}m=true;H.loadMode=3;a();q("scroll",o,true)};return{_:function(){e=f.now();k.elements=D.getElementsByClassName(H.lazyClass);v=D.getElementsByClassName(H.lazyClass+" "+H.preloadClass);q("scroll",a,true);q("resize",a,true);q("pageshow",function(e){if(e.persisted){var t=D.querySelectorAll("."+H.loadingClass);if(t.length&&t.forEach){U(function(){t.forEach(function(e){if(e.complete){R(e)}})})}}});if(u.MutationObserver){new MutationObserver(a).observe(O,{childList:true,subtree:true,attributes:true})}else{O[P]("DOMNodeInserted",a,true);O[P]("DOMAttrModified",a,true);setInterval(a,999)}q("hashchange",a,true);["focus","mouseover","click","load","transitionend","animationend"].forEach(function(e){D[P](e,a,true)});if(/d$|^c/.test(D.readyState)){l()}else{q("load",l);D[P]("DOMContentLoaded",a);I(l,2e4)}if(k.elements.length){t();ee._lsFlush()}else{a()}},checkElems:a,unveil:R,_aLSL:o}}(),re=function(){var a;var n=te(function(e,t,a,i){var r,n,s;e._lazysizesWidth=i;i+="px";e.setAttribute("sizes",i);if(j.test(t.nodeName||"")){r=t.getElementsByTagName("source");for(n=0,s=r.length;nc||n.hasOwnProperty(c)&&(p[n[c]]=c)}g=p[e]?"keydown":"keypress"}"keypress"==g&&d.length&&(g="keydown");return{key:m,modifiers:d,action:g}}function D(a,b){return null===a||a===u?!1:a===b?!0:D(a.parentNode,b)}function d(a){function b(a){a= +a||{};var b=!1,l;for(l in p)a[l]?b=!0:p[l]=0;b||(x=!1)}function g(a,b,t,f,g,d){var l,E=[],h=t.type;if(!k._callbacks[a])return[];"keyup"==h&&w(a)&&(b=[a]);for(l=0;l":".","?":"/","|":"\\"},B={option:"alt",command:"meta","return":"enter", +escape:"esc",plus:"+",mod:/Mac|iPod|iPhone|iPad/.test(navigator.platform)?"meta":"ctrl"},p;for(c=1;20>c;++c)n[111+c]="f"+c;for(c=0;9>=c;++c)n[c+96]=c.toString();d.prototype.bind=function(a,b,c){a=a instanceof Array?a:[a];this._bindMultiple.call(this,a,b,c);return this};d.prototype.unbind=function(a,b){return this.bind.call(this,a,function(){},b)};d.prototype.trigger=function(a,b){if(this._directMap[a+":"+b])this._directMap[a+":"+b]({},a);return this};d.prototype.reset=function(){this._callbacks={}; +this._directMap={};return this};d.prototype.stopCallback=function(a,b){if(-1<(" "+b.className+" ").indexOf(" mousetrap ")||D(b,this.target))return!1;if("composedPath"in a&&"function"===typeof a.composedPath){var c=a.composedPath()[0];c!==a.target&&(b=c)}return"INPUT"==b.tagName||"SELECT"==b.tagName||"TEXTAREA"==b.tagName||b.isContentEditable};d.prototype.handleKey=function(){return this._handleKey.apply(this,arguments)};d.addKeycodes=function(a){for(var b in a)a.hasOwnProperty(b)&&(n[b]=a[b]);p=null}; +d.init=function(){var a=d(u),b;for(b in a)"_"!==b.charAt(0)&&(d[b]=function(b){return function(){return a[b].apply(a,arguments)}}(b))};d.init();q.Mousetrap=d;"undefined"!==typeof module&&module.exports&&(module.exports=d);"function"===typeof define&&define.amd&&define(function(){return d})}})("undefined"!==typeof window?window:null,"undefined"!==typeof window?document:null); + +(function(a){var c={},d=a.prototype.stopCallback;a.prototype.stopCallback=function(e,b,a,f){return this.paused?!0:c[a]||c[f]?!1:d.call(this,e,b,a)};a.prototype.bindGlobal=function(a,b,d){this.bind(a,b,d);if(a instanceof Array)for(b=0;bt.length)&&(n=t.length);for(var e=0,o=new Array(n);e'):(B.cancelButton=B.dialogButtons.appendChild(document.createElement("a")),B.cancelButton.textContent=e.buttons.cancel.title),B.cancelButton.id="basicModal__cancel",B.cancelButton.classList.add("basicModal__button"),(o=B.cancelButton.classList).add.apply(o,b(e.buttons.cancel.classList));for(var c=0,i=Object.entries(e.buttons.cancel.attributes);cs(e).data("position")?1:-1},right:function(t,e){return s(t).data("position")>s(e).data("position")?1:-1}}),o.$left.attachIndex(),o.$right.each(function(t,e){s(e).attachIndex()})),"function"==typeof o.callbacks.startUp&&o.callbacks.startUp(o.$left,o.$right),o.skipInitSort||("function"==typeof o.callbacks.sort.left&&o.$left.mSort(o.callbacks.sort.left),"function"==typeof o.callbacks.sort.right&&o.$right.each(function(t,e){s(e).mSort(o.callbacks.sort.right)})),o.options.search&&o.options.search.left&&(o.options.search.$left=s(o.options.search.left),o.$left.before(o.options.search.$left)),o.options.search&&o.options.search.right&&(o.options.search.$right=s(o.options.search.right),o.$right.before(s(o.options.search.$right))),o.events(),"function"==typeof o.callbacks.afterInit&&o.callbacks.afterInit()},events:function(){var o=this;o.options.search&&o.options.search.$left&&o.options.search.$left.on("keyup",function(t){o.callbacks.fireSearch(this.value)?(o.$left.find('option:search("'+this.value+'")').mShow(),o.$left.find('option:not(:search("'+this.value+'"))').mHide(),o.$left.find("option").closest("optgroup").mHide(),o.$left.find("option:not(.hidden)").parent("optgroup").mShow()):o.$left.find("option, optgroup").mShow()}),o.options.search&&o.options.search.$right&&o.options.search.$right.on("keyup",function(t){o.callbacks.fireSearch(this.value)?(o.$right.find('option:search("'+this.value+'")').mShow(),o.$right.find('option:not(:search("'+this.value+'"))').mHide(),o.$right.find("option").closest("optgroup").mHide(),o.$right.find("option:not(.hidden)").parent("optgroup").mShow()):o.$right.find("option, optgroup").mShow()}),o.$right.closest("form").on("submit",function(t){o.options.search&&(o.options.search.$left&&o.options.search.$left.val("").trigger("keyup"),o.options.search.$right&&o.options.search.$right.val("").trigger("keyup")),o.$left.find("option").prop("selected",o.options.submitAllLeft),o.$right.find("option").prop("selected",o.options.submitAllRight)}),o.$left.on("dblclick","option",function(t){t.preventDefault();var e=o.$left.find("option:selected:not(.hidden)");e.length&&o.moveToRight(e,t)}),o.$left.on("click","optgroup",function(t){"OPTGROUP"==s(t.target).prop("tagName")&&s(this).children().prop("selected",!0)}),o.$left.on("keypress",function(t){var e;13===t.keyCode&&(t.preventDefault(),(e=o.$left.find("option:selected:not(.hidden)")).length&&o.moveToRight(e,t))}),o.$right.on("dblclick","option",function(t){t.preventDefault();var e=o.$right.find("option:selected:not(.hidden)");e.length&&o.moveToLeft(e,t)}),o.$right.on("click","optgroup",function(t){"OPTGROUP"==s(t.target).prop("tagName")&&s(this).children().prop("selected",!0)}),o.$right.on("keydown",function(t){var e;8!==t.keyCode&&46!==t.keyCode||(t.preventDefault(),(e=o.$right.find("option:selected:not(.hidden)")).length&&o.moveToLeft(e,t))}),(navigator.userAgent.match(/MSIE/i)||0e.innerHTML?1:-1},fireSearch:function(t){return 1").hide()}),l&&this.prop("disabled",!0),this},i.fn.mSort=function(o){return this.children().sort(o).appendTo(this),this.find("optgroup").each(function(t,e){i(e).children().sort(o).appendTo(e)}),this},i.fn.attachIndex=function(){this.children().each(function(t,e){var o=i(e);o.is("optgroup")&&o.children().each(function(t,e){i(e).data("position",t)}),o.data("position",t)})},i.expr[":"].search=function(t,e,o){var n=new RegExp(o[3].replace(/([^a-zA-Z0-9])/g,"\\$1"),"i");return i(t).text().match(n)}}); +require=function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i=1){this.items.push(itemData);this.completeLayout(rowWidthWithoutSpacing/itemData.aspectRatio,"justify");return true}}}if(newAspectRatiothis.maxAspectRatio){if(this.items.length===0){this.items.push(Object.assign({},itemData));this.completeLayout(rowWidthWithoutSpacing/newAspectRatio,"justify");return true}previousRowWidthWithoutSpacing=this.width-(this.items.length-1)*this.spacing;previousAspectRatio=this.items.reduce(function(sum,item){return sum+item.aspectRatio},0);previousTargetAspectRatio=previousRowWidthWithoutSpacing/this.targetRowHeight;if(Math.abs(newAspectRatio-targetAspectRatio)>Math.abs(previousAspectRatio-previousTargetAspectRatio)){this.completeLayout(previousRowWidthWithoutSpacing/previousAspectRatio,"justify");return false}else{this.items.push(Object.assign({},itemData));this.completeLayout(rowWidthWithoutSpacing/newAspectRatio,"justify");return true}}else{this.items.push(Object.assign({},itemData));this.completeLayout(rowWidthWithoutSpacing/newAspectRatio,"justify");return true}},isLayoutComplete:function(){return this.height>0},completeLayout:function(newHeight,widowLayoutStyle){var itemWidthSum=this.left,rowWidthWithoutSpacing=this.width-(this.items.length-1)*this.spacing,clampedToNativeRatio,clampedHeight,errorWidthPerItem,roundedCumulativeErrors,singleItemGeometry,centerOffset;if(typeof widowLayoutStyle==="undefined"||["justify","center","left"].indexOf(widowLayoutStyle)<0){widowLayoutStyle="left"}clampedHeight=Math.max(this.edgeCaseMinRowHeight,Math.min(newHeight,this.edgeCaseMaxRowHeight));if(newHeight!==clampedHeight){this.height=clampedHeight;clampedToNativeRatio=rowWidthWithoutSpacing/clampedHeight/(rowWidthWithoutSpacing/newHeight)}else{this.height=newHeight;clampedToNativeRatio=1}this.items.forEach(function(item){item.top=this.top;item.width=item.aspectRatio*this.height*clampedToNativeRatio;item.height=this.height;item.left=itemWidthSum;itemWidthSum+=item.width+this.spacing},this);if(widowLayoutStyle==="justify"){itemWidthSum-=this.spacing+this.left;errorWidthPerItem=(itemWidthSum-this.width)/this.items.length;roundedCumulativeErrors=this.items.map(function(item,i){return Math.round((i+1)*errorWidthPerItem)});if(this.items.length===1){singleItemGeometry=this.items[0];singleItemGeometry.width-=Math.round(errorWidthPerItem)}else{this.items.forEach(function(item,i){if(i>0){item.left-=roundedCumulativeErrors[i-1];item.width-=roundedCumulativeErrors[i]-roundedCumulativeErrors[i-1]}else{item.width-=roundedCumulativeErrors[i]}})}}else if(widowLayoutStyle==="center"){centerOffset=(this.width-itemWidthSum)/2;this.items.forEach(function(item){item.left+=centerOffset+this.spacing},this)}},forceComplete:function(fitToWidth,rowHeight){if(typeof rowHeight==="number"){this.completeLayout(rowHeight,this.widowLayoutStyle)}else{this.completeLayout(this.targetRowHeight,this.widowLayoutStyle)}},getItems:function(){return this.items}}},{}],"justified-layout":[function(require,module,exports){ +/*! + * Copyright 2019 SmugMug, Inc. + * Licensed under the terms of the MIT license. Please see LICENSE file in the project root for terms. + * @license + */ +"use strict";var Row=require("./row");function createNewRow(layoutConfig,layoutData){var isBreakoutRow;if(layoutConfig.fullWidthBreakoutRowCadence!==false){if((layoutData._rows.length+1)%layoutConfig.fullWidthBreakoutRowCadence===0){isBreakoutRow=true}}return new Row({top:layoutData._containerHeight,left:layoutConfig.containerPadding.left,width:layoutConfig.containerWidth-layoutConfig.containerPadding.left-layoutConfig.containerPadding.right,spacing:layoutConfig.boxSpacing.horizontal,targetRowHeight:layoutConfig.targetRowHeight,targetRowHeightTolerance:layoutConfig.targetRowHeightTolerance,edgeCaseMinRowHeight:.5*layoutConfig.targetRowHeight,edgeCaseMaxRowHeight:2*layoutConfig.targetRowHeight,rightToLeft:false,isBreakoutRow:isBreakoutRow,widowLayoutStyle:layoutConfig.widowLayoutStyle})}function addRow(layoutConfig,layoutData,row){layoutData._rows.push(row);layoutData._layoutItems=layoutData._layoutItems.concat(row.getItems());layoutData._containerHeight+=row.height+layoutConfig.boxSpacing.vertical;return row.items}function computeLayout(layoutConfig,layoutData,itemLayoutData){var laidOutItems=[],itemAdded,currentRow,nextToLastRowHeight;if(layoutConfig.forceAspectRatio){itemLayoutData.forEach(function(itemData){itemData.forcedAspectRatio=true;itemData.aspectRatio=layoutConfig.forceAspectRatio})}itemLayoutData.some(function(itemData,i){if(isNaN(itemData.aspectRatio)){throw new Error("Item "+i+" has an invalid aspect ratio")}if(!currentRow){currentRow=createNewRow(layoutConfig,layoutData)}itemAdded=currentRow.addItem(itemData);if(currentRow.isLayoutComplete()){laidOutItems=laidOutItems.concat(addRow(layoutConfig,layoutData,currentRow));if(layoutData._rows.length>=layoutConfig.maxNumRows){currentRow=null;return true}currentRow=createNewRow(layoutConfig,layoutData);if(!itemAdded){itemAdded=currentRow.addItem(itemData);if(currentRow.isLayoutComplete()){laidOutItems=laidOutItems.concat(addRow(layoutConfig,layoutData,currentRow));if(layoutData._rows.length>=layoutConfig.maxNumRows){currentRow=null;return true}currentRow=createNewRow(layoutConfig,layoutData)}}}});if(currentRow&¤tRow.getItems().length&&layoutConfig.showWidows){if(layoutData._rows.length){if(layoutData._rows[layoutData._rows.length-1].isBreakoutRow){nextToLastRowHeight=layoutData._rows[layoutData._rows.length-1].targetRowHeight}else{nextToLastRowHeight=layoutData._rows[layoutData._rows.length-1].height}currentRow.forceComplete(false,nextToLastRowHeight)}else{currentRow.forceComplete(false)}laidOutItems=laidOutItems.concat(addRow(layoutConfig,layoutData,currentRow));layoutConfig._widowCount=currentRow.getItems().length}layoutData._containerHeight=layoutData._containerHeight-layoutConfig.boxSpacing.vertical;layoutData._containerHeight=layoutData._containerHeight+layoutConfig.containerPadding.bottom;return{containerHeight:layoutData._containerHeight,widowCount:layoutConfig._widowCount,boxes:layoutData._layoutItems}}module.exports=function(input,config){var layoutConfig={};var layoutData={};var defaults={containerWidth:1060,containerPadding:10,boxSpacing:10,targetRowHeight:320,targetRowHeightTolerance:.25,maxNumRows:Number.POSITIVE_INFINITY,forceAspectRatio:false,showWidows:true,fullWidthBreakoutRowCadence:false,widowLayoutStyle:"left"};var containerPadding={};var boxSpacing={};config=config||{};layoutConfig=Object.assign(defaults,config);containerPadding.top=!isNaN(parseFloat(layoutConfig.containerPadding.top))?layoutConfig.containerPadding.top:layoutConfig.containerPadding;containerPadding.right=!isNaN(parseFloat(layoutConfig.containerPadding.right))?layoutConfig.containerPadding.right:layoutConfig.containerPadding;containerPadding.bottom=!isNaN(parseFloat(layoutConfig.containerPadding.bottom))?layoutConfig.containerPadding.bottom:layoutConfig.containerPadding;containerPadding.left=!isNaN(parseFloat(layoutConfig.containerPadding.left))?layoutConfig.containerPadding.left:layoutConfig.containerPadding;boxSpacing.horizontal=!isNaN(parseFloat(layoutConfig.boxSpacing.horizontal))?layoutConfig.boxSpacing.horizontal:layoutConfig.boxSpacing;boxSpacing.vertical=!isNaN(parseFloat(layoutConfig.boxSpacing.vertical))?layoutConfig.boxSpacing.vertical:layoutConfig.boxSpacing;layoutConfig.containerPadding=containerPadding;layoutConfig.boxSpacing=boxSpacing;layoutData._layoutItems=[];layoutData._awakeItems=[];layoutData._inViewportItems=[];layoutData._leadingOrphans=[];layoutData._trailingOrphans=[];layoutData._containerHeight=layoutConfig.containerPadding.top;layoutData._rows=[];layoutData._orphans=[];layoutConfig._widowCount=0;return computeLayout(layoutConfig,layoutData,input.map(function(item){if(item.width&&item.height){return{aspectRatio:item.width/item.height}}else{return{aspectRatio:item}}}))}},{"./row":1}]},{},[]); +/* @preserve + * Leaflet 1.9.4, a JS library for interactive maps. https://leafletjs.com + * (c) 2010-2023 Vladimir Agafonkin, (c) 2010-2011 CloudMade + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).leaflet={})}(this,function(t){"use strict";function l(t){for(var e,i,n=1,o=arguments.length;n=this.min.x&&i.x<=this.max.x&&e.y>=this.min.y&&i.y<=this.max.y},intersects:function(t){t=_(t);var e=this.min,i=this.max,n=t.min,t=t.max,o=t.x>=e.x&&n.x<=i.x,t=t.y>=e.y&&n.y<=i.y;return o&&t},overlaps:function(t){t=_(t);var e=this.min,i=this.max,n=t.min,t=t.max,o=t.x>e.x&&n.xe.y&&n.y=n.lat&&i.lat<=o.lat&&e.lng>=n.lng&&i.lng<=o.lng},intersects:function(t){t=g(t);var e=this._southWest,i=this._northEast,n=t.getSouthWest(),t=t.getNorthEast(),o=t.lat>=e.lat&&n.lat<=i.lat,t=t.lng>=e.lng&&n.lng<=i.lng;return o&&t},overlaps:function(t){t=g(t);var e=this._southWest,i=this._northEast,n=t.getSouthWest(),t=t.getNorthEast(),o=t.lat>e.lat&&n.late.lng&&n.lng","http://www.w3.org/2000/svg"===(Wt.firstChild&&Wt.firstChild.namespaceURI));function y(t){return 0<=navigator.userAgent.toLowerCase().indexOf(t)}var b={ie:pt,ielt9:mt,edge:n,webkit:ft,android:gt,android23:vt,androidStock:yt,opera:xt,chrome:wt,gecko:bt,safari:Pt,phantom:Lt,opera12:o,win:Tt,ie3d:Mt,webkit3d:zt,gecko3d:_t,any3d:Ct,mobile:Zt,mobileWebkit:St,mobileWebkit3d:Et,msPointer:kt,pointer:Ot,touch:Bt,touchNative:At,mobileOpera:It,mobileGecko:Rt,retina:Nt,passiveEvents:Dt,canvas:jt,svg:Ht,vml:!Ht&&function(){try{var t=document.createElement("div"),e=(t.innerHTML='',t.firstChild);return e.style.behavior="url(#default#VML)",e&&"object"==typeof e.adj}catch(t){return!1}}(),inlineSvg:Wt,mac:0===navigator.platform.indexOf("Mac"),linux:0===navigator.platform.indexOf("Linux")},Ft=b.msPointer?"MSPointerDown":"pointerdown",Ut=b.msPointer?"MSPointerMove":"pointermove",Vt=b.msPointer?"MSPointerUp":"pointerup",qt=b.msPointer?"MSPointerCancel":"pointercancel",Gt={touchstart:Ft,touchmove:Ut,touchend:Vt,touchcancel:qt},Kt={touchstart:function(t,e){e.MSPOINTER_TYPE_TOUCH&&e.pointerType===e.MSPOINTER_TYPE_TOUCH&&O(e);ee(t,e)},touchmove:ee,touchend:ee,touchcancel:ee},Yt={},Xt=!1;function Jt(t,e,i){return"touchstart"!==e||Xt||(document.addEventListener(Ft,$t,!0),document.addEventListener(Ut,Qt,!0),document.addEventListener(Vt,te,!0),document.addEventListener(qt,te,!0),Xt=!0),Kt[e]?(i=Kt[e].bind(this,i),t.addEventListener(Gt[e],i,!1),i):(console.warn("wrong event specified:",e),u)}function $t(t){Yt[t.pointerId]=t}function Qt(t){Yt[t.pointerId]&&(Yt[t.pointerId]=t)}function te(t){delete Yt[t.pointerId]}function ee(t,e){if(e.pointerType!==(e.MSPOINTER_TYPE_MOUSE||"mouse")){for(var i in e.touches=[],Yt)e.touches.push(Yt[i]);e.changedTouches=[e],t(e)}}var ie=200;function ne(t,i){t.addEventListener("dblclick",i);var n,o=0;function e(t){var e;1!==t.detail?n=t.detail:"mouse"===t.pointerType||t.sourceCapabilities&&!t.sourceCapabilities.firesTouchEvents||((e=Ne(t)).some(function(t){return t instanceof HTMLLabelElement&&t.attributes.for})&&!e.some(function(t){return t instanceof HTMLInputElement||t instanceof HTMLSelectElement})||((e=Date.now())-o<=ie?2===++n&&i(function(t){var e,i,n={};for(i in t)e=t[i],n[i]=e&&e.bind?e.bind(t):e;return(t=n).type="dblclick",n.detail=2,n.isTrusted=!1,n._simulated=!0,n}(t)):n=1,o=e))}return t.addEventListener("click",e),{dblclick:i,simDblclick:e}}var oe,se,re,ae,he,le,ue=we(["transform","webkitTransform","OTransform","MozTransform","msTransform"]),ce=we(["webkitTransition","transition","OTransition","MozTransition","msTransition"]),de="webkitTransition"===ce||"OTransition"===ce?ce+"End":"transitionend";function _e(t){return"string"==typeof t?document.getElementById(t):t}function pe(t,e){var i=t.style[e]||t.currentStyle&&t.currentStyle[e];return"auto"===(i=i&&"auto"!==i||!document.defaultView?i:(t=document.defaultView.getComputedStyle(t,null))?t[e]:null)?null:i}function P(t,e,i){t=document.createElement(t);return t.className=e||"",i&&i.appendChild(t),t}function T(t){var e=t.parentNode;e&&e.removeChild(t)}function me(t){for(;t.firstChild;)t.removeChild(t.firstChild)}function fe(t){var e=t.parentNode;e&&e.lastChild!==t&&e.appendChild(t)}function ge(t){var e=t.parentNode;e&&e.firstChild!==t&&e.insertBefore(t,e.firstChild)}function ve(t,e){return void 0!==t.classList?t.classList.contains(e):0<(t=xe(t)).length&&new RegExp("(^|\\s)"+e+"(\\s|$)").test(t)}function M(t,e){var i;if(void 0!==t.classList)for(var n=F(e),o=0,s=n.length;othis.options.maxZoom)?this.setZoom(t):this},panInsideBounds:function(t,e){this._enforcingBounds=!0;var i=this.getCenter(),t=this._limitCenter(i,this._zoom,g(t));return i.equals(t)||this.panTo(t,e),this._enforcingBounds=!1,this},panInside:function(t,e){var i=m((e=e||{}).paddingTopLeft||e.padding||[0,0]),n=m(e.paddingBottomRight||e.padding||[0,0]),o=this.project(this.getCenter()),t=this.project(t),s=this.getPixelBounds(),i=_([s.min.add(i),s.max.subtract(n)]),s=i.getSize();return i.contains(t)||(this._enforcingBounds=!0,n=t.subtract(i.getCenter()),i=i.extend(t).getSize().subtract(s),o.x+=n.x<0?-i.x:i.x,o.y+=n.y<0?-i.y:i.y,this.panTo(this.unproject(o),e),this._enforcingBounds=!1),this},invalidateSize:function(t){if(!this._loaded)return this;t=l({animate:!1,pan:!0},!0===t?{animate:!0}:t);var e=this.getSize(),i=(this._sizeChanged=!0,this._lastCenter=null,this.getSize()),n=e.divideBy(2).round(),o=i.divideBy(2).round(),n=n.subtract(o);return n.x||n.y?(t.animate&&t.pan?this.panBy(n):(t.pan&&this._rawPanBy(n),this.fire("move"),t.debounceMoveend?(clearTimeout(this._sizeTimer),this._sizeTimer=setTimeout(a(this.fire,this,"moveend"),200)):this.fire("moveend")),this.fire("resize",{oldSize:e,newSize:i})):this},stop:function(){return this.setZoom(this._limitZoom(this._zoom)),this.options.zoomSnap||this.fire("viewreset"),this._stop()},locate:function(t){var e,i;return t=this._locateOptions=l({timeout:1e4,watch:!1},t),"geolocation"in navigator?(e=a(this._handleGeolocationResponse,this),i=a(this._handleGeolocationError,this),t.watch?this._locationWatchId=navigator.geolocation.watchPosition(e,i,t):navigator.geolocation.getCurrentPosition(e,i,t)):this._handleGeolocationError({code:0,message:"Geolocation not supported."}),this},stopLocate:function(){return navigator.geolocation&&navigator.geolocation.clearWatch&&navigator.geolocation.clearWatch(this._locationWatchId),this._locateOptions&&(this._locateOptions.setView=!1),this},_handleGeolocationError:function(t){var e;this._container._leaflet_id&&(e=t.code,t=t.message||(1===e?"permission denied":2===e?"position unavailable":"timeout"),this._locateOptions.setView&&!this._loaded&&this.fitWorld(),this.fire("locationerror",{code:e,message:"Geolocation error: "+t+"."}))},_handleGeolocationResponse:function(t){if(this._container._leaflet_id){var e,i,n=new v(t.coords.latitude,t.coords.longitude),o=n.toBounds(2*t.coords.accuracy),s=this._locateOptions,r=(s.setView&&(e=this.getBoundsZoom(o),this.setView(n,s.maxZoom?Math.min(e,s.maxZoom):e)),{latlng:n,bounds:o,timestamp:t.timestamp});for(i in t.coords)"number"==typeof t.coords[i]&&(r[i]=t.coords[i]);this.fire("locationfound",r)}},addHandler:function(t,e){return e&&(e=this[t]=new e(this),this._handlers.push(e),this.options[t]&&e.enable()),this},remove:function(){if(this._initEvents(!0),this.options.maxBounds&&this.off("moveend",this._panInsideMaxBounds),this._containerId!==this._container._leaflet_id)throw new Error("Map container is being reused by another instance");try{delete this._container._leaflet_id,delete this._containerId}catch(t){this._container._leaflet_id=void 0,this._containerId=void 0}for(var t in void 0!==this._locationWatchId&&this.stopLocate(),this._stop(),T(this._mapPane),this._clearControlPos&&this._clearControlPos(),this._resizeRequest&&(r(this._resizeRequest),this._resizeRequest=null),this._clearHandlers(),this._loaded&&this.fire("unload"),this._layers)this._layers[t].remove();for(t in this._panes)T(this._panes[t]);return this._layers=[],this._panes=[],delete this._mapPane,delete this._renderer,this},createPane:function(t,e){e=P("div","leaflet-pane"+(t?" leaflet-"+t.replace("Pane","")+"-pane":""),e||this._mapPane);return t&&(this._panes[t]=e),e},getCenter:function(){return this._checkIfLoaded(),this._lastCenter&&!this._moved()?this._lastCenter.clone():this.layerPointToLatLng(this._getCenterLayerPoint())},getZoom:function(){return this._zoom},getBounds:function(){var t=this.getPixelBounds();return new s(this.unproject(t.getBottomLeft()),this.unproject(t.getTopRight()))},getMinZoom:function(){return void 0===this.options.minZoom?this._layersMinZoom||0:this.options.minZoom},getMaxZoom:function(){return void 0===this.options.maxZoom?void 0===this._layersMaxZoom?1/0:this._layersMaxZoom:this.options.maxZoom},getBoundsZoom:function(t,e,i){t=g(t),i=m(i||[0,0]);var n=this.getZoom()||0,o=this.getMinZoom(),s=this.getMaxZoom(),r=t.getNorthWest(),t=t.getSouthEast(),i=this.getSize().subtract(i),t=_(this.project(t,n),this.project(r,n)).getSize(),r=b.any3d?this.options.zoomSnap:1,a=i.x/t.x,i=i.y/t.y,t=e?Math.max(a,i):Math.min(a,i),n=this.getScaleZoom(t,n);return r&&(n=Math.round(n/(r/100))*(r/100),n=e?Math.ceil(n/r)*r:Math.floor(n/r)*r),Math.max(o,Math.min(s,n))},getSize:function(){return this._size&&!this._sizeChanged||(this._size=new p(this._container.clientWidth||0,this._container.clientHeight||0),this._sizeChanged=!1),this._size.clone()},getPixelBounds:function(t,e){t=this._getTopLeftPoint(t,e);return new f(t,t.add(this.getSize()))},getPixelOrigin:function(){return this._checkIfLoaded(),this._pixelOrigin},getPixelWorldBounds:function(t){return this.options.crs.getProjectedBounds(void 0===t?this.getZoom():t)},getPane:function(t){return"string"==typeof t?this._panes[t]:t},getPanes:function(){return this._panes},getContainer:function(){return this._container},getZoomScale:function(t,e){var i=this.options.crs;return e=void 0===e?this._zoom:e,i.scale(t)/i.scale(e)},getScaleZoom:function(t,e){var i=this.options.crs,t=(e=void 0===e?this._zoom:e,i.zoom(t*i.scale(e)));return isNaN(t)?1/0:t},project:function(t,e){return e=void 0===e?this._zoom:e,this.options.crs.latLngToPoint(w(t),e)},unproject:function(t,e){return e=void 0===e?this._zoom:e,this.options.crs.pointToLatLng(m(t),e)},layerPointToLatLng:function(t){t=m(t).add(this.getPixelOrigin());return this.unproject(t)},latLngToLayerPoint:function(t){return this.project(w(t))._round()._subtract(this.getPixelOrigin())},wrapLatLng:function(t){return this.options.crs.wrapLatLng(w(t))},wrapLatLngBounds:function(t){return this.options.crs.wrapLatLngBounds(g(t))},distance:function(t,e){return this.options.crs.distance(w(t),w(e))},containerPointToLayerPoint:function(t){return m(t).subtract(this._getMapPanePos())},layerPointToContainerPoint:function(t){return m(t).add(this._getMapPanePos())},containerPointToLatLng:function(t){t=this.containerPointToLayerPoint(m(t));return this.layerPointToLatLng(t)},latLngToContainerPoint:function(t){return this.layerPointToContainerPoint(this.latLngToLayerPoint(w(t)))},mouseEventToContainerPoint:function(t){return De(t,this._container)},mouseEventToLayerPoint:function(t){return this.containerPointToLayerPoint(this.mouseEventToContainerPoint(t))},mouseEventToLatLng:function(t){return this.layerPointToLatLng(this.mouseEventToLayerPoint(t))},_initContainer:function(t){t=this._container=_e(t);if(!t)throw new Error("Map container not found.");if(t._leaflet_id)throw new Error("Map container is already initialized.");S(t,"scroll",this._onScroll,this),this._containerId=h(t)},_initLayout:function(){var t=this._container,e=(this._fadeAnimated=this.options.fadeAnimation&&b.any3d,M(t,"leaflet-container"+(b.touch?" leaflet-touch":"")+(b.retina?" leaflet-retina":"")+(b.ielt9?" leaflet-oldie":"")+(b.safari?" leaflet-safari":"")+(this._fadeAnimated?" leaflet-fade-anim":"")),pe(t,"position"));"absolute"!==e&&"relative"!==e&&"fixed"!==e&&"sticky"!==e&&(t.style.position="relative"),this._initPanes(),this._initControlPos&&this._initControlPos()},_initPanes:function(){var t=this._panes={};this._paneRenderers={},this._mapPane=this.createPane("mapPane",this._container),Z(this._mapPane,new p(0,0)),this.createPane("tilePane"),this.createPane("overlayPane"),this.createPane("shadowPane"),this.createPane("markerPane"),this.createPane("tooltipPane"),this.createPane("popupPane"),this.options.markerZoomAnimation||(M(t.markerPane,"leaflet-zoom-hide"),M(t.shadowPane,"leaflet-zoom-hide"))},_resetView:function(t,e,i){Z(this._mapPane,new p(0,0));var n=!this._loaded,o=(this._loaded=!0,e=this._limitZoom(e),this.fire("viewprereset"),this._zoom!==e);this._moveStart(o,i)._move(t,e)._moveEnd(o),this.fire("viewreset"),n&&this.fire("load")},_moveStart:function(t,e){return t&&this.fire("zoomstart"),e||this.fire("movestart"),this},_move:function(t,e,i,n){void 0===e&&(e=this._zoom);var o=this._zoom!==e;return this._zoom=e,this._lastCenter=t,this._pixelOrigin=this._getNewPixelOrigin(t),n?i&&i.pinch&&this.fire("zoom",i):((o||i&&i.pinch)&&this.fire("zoom",i),this.fire("move",i)),this},_moveEnd:function(t){return t&&this.fire("zoomend"),this.fire("moveend")},_stop:function(){return r(this._flyToFrame),this._panAnim&&this._panAnim.stop(),this},_rawPanBy:function(t){Z(this._mapPane,this._getMapPanePos().subtract(t))},_getZoomSpan:function(){return this.getMaxZoom()-this.getMinZoom()},_panInsideMaxBounds:function(){this._enforcingBounds||this.panInsideBounds(this.options.maxBounds)},_checkIfLoaded:function(){if(!this._loaded)throw new Error("Set map center and zoom first.")},_initEvents:function(t){this._targets={};var e=t?k:S;e((this._targets[h(this._container)]=this)._container,"click dblclick mousedown mouseup mouseover mouseout mousemove contextmenu keypress keydown keyup",this._handleDOMEvent,this),this.options.trackResize&&e(window,"resize",this._onResize,this),b.any3d&&this.options.transform3DLimit&&(t?this.off:this.on).call(this,"moveend",this._onMoveEnd)},_onResize:function(){r(this._resizeRequest),this._resizeRequest=x(function(){this.invalidateSize({debounceMoveend:!0})},this)},_onScroll:function(){this._container.scrollTop=0,this._container.scrollLeft=0},_onMoveEnd:function(){var t=this._getMapPanePos();Math.max(Math.abs(t.x),Math.abs(t.y))>=this.options.transform3DLimit&&this._resetView(this.getCenter(),this.getZoom())},_findEventTargets:function(t,e){for(var i,n=[],o="mouseout"===e||"mouseover"===e,s=t.target||t.srcElement,r=!1;s;){if((i=this._targets[h(s)])&&("click"===e||"preclick"===e)&&this._draggableMoved(i)){r=!0;break}if(i&&i.listens(e,!0)){if(o&&!We(s,t))break;if(n.push(i),o)break}if(s===this._container)break;s=s.parentNode}return n=n.length||r||o||!this.listens(e,!0)?n:[this]},_isClickDisabled:function(t){for(;t&&t!==this._container;){if(t._leaflet_disable_click)return!0;t=t.parentNode}},_handleDOMEvent:function(t){var e,i=t.target||t.srcElement;!this._loaded||i._leaflet_disable_events||"click"===t.type&&this._isClickDisabled(i)||("mousedown"===(e=t.type)&&Me(i),this._fireDOMEvent(t,e))},_mouseEvents:["click","dblclick","mouseover","mouseout","contextmenu"],_fireDOMEvent:function(t,e,i){"click"===t.type&&((a=l({},t)).type="preclick",this._fireDOMEvent(a,a.type,i));var n=this._findEventTargets(t,e);if(i){for(var o=[],s=0;sthis.options.zoomAnimationThreshold)return!1;var n=this.getZoomScale(e),n=this._getCenterOffset(t)._divideBy(1-1/n);if(!0!==i.animate&&!this.getSize().contains(n))return!1;x(function(){this._moveStart(!0,i.noMoveStart||!1)._animateZoom(t,e,!0)},this)}return!0},_animateZoom:function(t,e,i,n){this._mapPane&&(i&&(this._animatingZoom=!0,this._animateToCenter=t,this._animateToZoom=e,M(this._mapPane,"leaflet-zoom-anim")),this.fire("zoomanim",{center:t,zoom:e,noUpdate:n}),this._tempFireZoomEvent||(this._tempFireZoomEvent=this._zoom!==this._animateToZoom),this._move(this._animateToCenter,this._animateToZoom,void 0,!0),setTimeout(a(this._onZoomTransitionEnd,this),250))},_onZoomTransitionEnd:function(){this._animatingZoom&&(this._mapPane&&z(this._mapPane,"leaflet-zoom-anim"),this._animatingZoom=!1,this._move(this._animateToCenter,this._animateToZoom,void 0,!0),this._tempFireZoomEvent&&this.fire("zoom"),delete this._tempFireZoomEvent,this.fire("move"),this._moveEnd(!0))}});function Ue(t){return new B(t)}var B=et.extend({options:{position:"topright"},initialize:function(t){c(this,t)},getPosition:function(){return this.options.position},setPosition:function(t){var e=this._map;return e&&e.removeControl(this),this.options.position=t,e&&e.addControl(this),this},getContainer:function(){return this._container},addTo:function(t){this.remove(),this._map=t;var e=this._container=this.onAdd(t),i=this.getPosition(),t=t._controlCorners[i];return M(e,"leaflet-control"),-1!==i.indexOf("bottom")?t.insertBefore(e,t.firstChild):t.appendChild(e),this._map.on("unload",this.remove,this),this},remove:function(){return this._map&&(T(this._container),this.onRemove&&this.onRemove(this._map),this._map.off("unload",this.remove,this),this._map=null),this},_refocusOnMap:function(t){this._map&&t&&0",e=document.createElement("div");return e.innerHTML=t,e.firstChild},_addItem:function(t){var e,i=document.createElement("label"),n=this._map.hasLayer(t.layer),n=(t.overlay?((e=document.createElement("input")).type="checkbox",e.className="leaflet-control-layers-selector",e.defaultChecked=n):e=this._createRadioElement("leaflet-base-layers_"+h(this),n),this._layerControlInputs.push(e),e.layerId=h(t.layer),S(e,"click",this._onInputClick,this),document.createElement("span")),o=(n.innerHTML=" "+t.name,document.createElement("span"));return i.appendChild(o),o.appendChild(e),o.appendChild(n),(t.overlay?this._overlaysList:this._baseLayersList).appendChild(i),this._checkDisabledLayers(),i},_onInputClick:function(){if(!this._preventClick){var t,e,i=this._layerControlInputs,n=[],o=[];this._handlingClick=!0;for(var s=i.length-1;0<=s;s--)t=i[s],e=this._getLayer(t.layerId).layer,t.checked?n.push(e):t.checked||o.push(e);for(s=0;se.options.maxZoom},_expandIfNotCollapsed:function(){return this._map&&!this.options.collapsed&&this.expand(),this},_expandSafely:function(){var t=this._section,e=(this._preventClick=!0,S(t,"click",O),this.expand(),this);setTimeout(function(){k(t,"click",O),e._preventClick=!1})}})),qe=B.extend({options:{position:"topleft",zoomInText:'',zoomInTitle:"Zoom in",zoomOutText:'',zoomOutTitle:"Zoom out"},onAdd:function(t){var e="leaflet-control-zoom",i=P("div",e+" leaflet-bar"),n=this.options;return this._zoomInButton=this._createButton(n.zoomInText,n.zoomInTitle,e+"-in",i,this._zoomIn),this._zoomOutButton=this._createButton(n.zoomOutText,n.zoomOutTitle,e+"-out",i,this._zoomOut),this._updateDisabled(),t.on("zoomend zoomlevelschange",this._updateDisabled,this),i},onRemove:function(t){t.off("zoomend zoomlevelschange",this._updateDisabled,this)},disable:function(){return this._disabled=!0,this._updateDisabled(),this},enable:function(){return this._disabled=!1,this._updateDisabled(),this},_zoomIn:function(t){!this._disabled&&this._map._zoomthis._map.getMinZoom()&&this._map.zoomOut(this._map.options.zoomDelta*(t.shiftKey?3:1))},_createButton:function(t,e,i,n,o){i=P("a",i,n);return i.innerHTML=t,i.href="#",i.title=e,i.setAttribute("role","button"),i.setAttribute("aria-label",e),Ie(i),S(i,"click",Re),S(i,"click",o,this),S(i,"click",this._refocusOnMap,this),i},_updateDisabled:function(){var t=this._map,e="leaflet-disabled";z(this._zoomInButton,e),z(this._zoomOutButton,e),this._zoomInButton.setAttribute("aria-disabled","false"),this._zoomOutButton.setAttribute("aria-disabled","false"),!this._disabled&&t._zoom!==t.getMinZoom()||(M(this._zoomOutButton,e),this._zoomOutButton.setAttribute("aria-disabled","true")),!this._disabled&&t._zoom!==t.getMaxZoom()||(M(this._zoomInButton,e),this._zoomInButton.setAttribute("aria-disabled","true"))}}),Ge=(A.mergeOptions({zoomControl:!0}),A.addInitHook(function(){this.options.zoomControl&&(this.zoomControl=new qe,this.addControl(this.zoomControl))}),B.extend({options:{position:"bottomleft",maxWidth:100,metric:!0,imperial:!0},onAdd:function(t){var e="leaflet-control-scale",i=P("div",e),n=this.options;return this._addScales(n,e+"-line",i),t.on(n.updateWhenIdle?"moveend":"move",this._update,this),t.whenReady(this._update,this),i},onRemove:function(t){t.off(this.options.updateWhenIdle?"moveend":"move",this._update,this)},_addScales:function(t,e,i){t.metric&&(this._mScale=P("div",e,i)),t.imperial&&(this._iScale=P("div",e,i))},_update:function(){var t=this._map,e=t.getSize().y/2,t=t.distance(t.containerPointToLatLng([0,e]),t.containerPointToLatLng([this.options.maxWidth,e]));this._updateScales(t)},_updateScales:function(t){this.options.metric&&t&&this._updateMetric(t),this.options.imperial&&t&&this._updateImperial(t)},_updateMetric:function(t){var e=this._getRoundNum(t);this._updateScale(this._mScale,e<1e3?e+" m":e/1e3+" km",e/t)},_updateImperial:function(t){var e,i,t=3.2808399*t;5280'+(b.inlineSvg?' ':"")+"Leaflet"},initialize:function(t){c(this,t),this._attributions={}},onAdd:function(t){for(var e in(t.attributionControl=this)._container=P("div","leaflet-control-attribution"),Ie(this._container),t._layers)t._layers[e].getAttribution&&this.addAttribution(t._layers[e].getAttribution());return this._update(),t.on("layeradd",this._addAttribution,this),this._container},onRemove:function(t){t.off("layeradd",this._addAttribution,this)},_addAttribution:function(t){t.layer.getAttribution&&(this.addAttribution(t.layer.getAttribution()),t.layer.once("remove",function(){this.removeAttribution(t.layer.getAttribution())},this))},setPrefix:function(t){return this.options.prefix=t,this._update(),this},addAttribution:function(t){return t&&(this._attributions[t]||(this._attributions[t]=0),this._attributions[t]++,this._update()),this},removeAttribution:function(t){return t&&this._attributions[t]&&(this._attributions[t]--,this._update()),this},_update:function(){if(this._map){var t,e=[];for(t in this._attributions)this._attributions[t]&&e.push(t);var i=[];this.options.prefix&&i.push(this.options.prefix),e.length&&i.push(e.join(", ")),this._container.innerHTML=i.join(' ')}}}),n=(A.mergeOptions({attributionControl:!0}),A.addInitHook(function(){this.options.attributionControl&&(new Ke).addTo(this)}),B.Layers=Ve,B.Zoom=qe,B.Scale=Ge,B.Attribution=Ke,Ue.layers=function(t,e,i){return new Ve(t,e,i)},Ue.zoom=function(t){return new qe(t)},Ue.scale=function(t){return new Ge(t)},Ue.attribution=function(t){return new Ke(t)},et.extend({initialize:function(t){this._map=t},enable:function(){return this._enabled||(this._enabled=!0,this.addHooks()),this},disable:function(){return this._enabled&&(this._enabled=!1,this.removeHooks()),this},enabled:function(){return!!this._enabled}})),ft=(n.addTo=function(t,e){return t.addHandler(e,this),this},{Events:e}),Ye=b.touch?"touchstart mousedown":"mousedown",Xe=it.extend({options:{clickTolerance:3},initialize:function(t,e,i,n){c(this,n),this._element=t,this._dragStartTarget=e||t,this._preventOutline=i},enable:function(){this._enabled||(S(this._dragStartTarget,Ye,this._onDown,this),this._enabled=!0)},disable:function(){this._enabled&&(Xe._dragging===this&&this.finishDrag(!0),k(this._dragStartTarget,Ye,this._onDown,this),this._enabled=!1,this._moved=!1)},_onDown:function(t){var e,i;this._enabled&&(this._moved=!1,ve(this._element,"leaflet-zoom-anim")||(t.touches&&1!==t.touches.length?Xe._dragging===this&&this.finishDrag():Xe._dragging||t.shiftKey||1!==t.which&&1!==t.button&&!t.touches||((Xe._dragging=this)._preventOutline&&Me(this._element),Le(),re(),this._moving||(this.fire("down"),i=t.touches?t.touches[0]:t,e=Ce(this._element),this._startPoint=new p(i.clientX,i.clientY),this._startPos=Pe(this._element),this._parentScale=Ze(e),i="mousedown"===t.type,S(document,i?"mousemove":"touchmove",this._onMove,this),S(document,i?"mouseup":"touchend touchcancel",this._onUp,this)))))},_onMove:function(t){var e;this._enabled&&(t.touches&&1e&&(i.push(t[n]),o=n);oe.max.x&&(i|=2),t.ye.max.y&&(i|=8),i}function ri(t,e,i,n){var o=e.x,e=e.y,s=i.x-o,r=i.y-e,a=s*s+r*r;return 0this._layersMaxZoom&&this.setZoom(this._layersMaxZoom),void 0===this.options.minZoom&&this._layersMinZoom&&this.getZoom()t.y!=n.y>t.y&&t.x<(n.x-i.x)*(t.y-i.y)/(n.y-i.y)+i.x&&(l=!l);return l||yi.prototype._containsPoint.call(this,t,!0)}});var wi=ci.extend({initialize:function(t,e){c(this,e),this._layers={},t&&this.addData(t)},addData:function(t){var e,i,n,o=d(t)?t:t.features;if(o){for(e=0,i=o.length;es.x&&(r=i.x+a-s.x+o.x),i.x-r-n.x<(a=0)&&(r=i.x-n.x),i.y+e+o.y>s.y&&(a=i.y+e-s.y+o.y),i.y-a-n.y<0&&(a=i.y-n.y),(r||a)&&(this.options.keepInView&&(this._autopanning=!0),t.fire("autopanstart").panBy([r,a]))))},_getAnchor:function(){return m(this._source&&this._source._getPopupAnchor?this._source._getPopupAnchor():[0,0])}})),Ii=(A.mergeOptions({closePopupOnClick:!0}),A.include({openPopup:function(t,e,i){return this._initOverlay(Bi,t,e,i).openOn(this),this},closePopup:function(t){return(t=arguments.length?t:this._popup)&&t.close(),this}}),o.include({bindPopup:function(t,e){return this._popup=this._initOverlay(Bi,this._popup,t,e),this._popupHandlersAdded||(this.on({click:this._openPopup,keypress:this._onKeyPress,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!0),this},unbindPopup:function(){return this._popup&&(this.off({click:this._openPopup,keypress:this._onKeyPress,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!1,this._popup=null),this},openPopup:function(t){return this._popup&&(this instanceof ci||(this._popup._source=this),this._popup._prepareOpen(t||this._latlng)&&this._popup.openOn(this._map)),this},closePopup:function(){return this._popup&&this._popup.close(),this},togglePopup:function(){return this._popup&&this._popup.toggle(this),this},isPopupOpen:function(){return!!this._popup&&this._popup.isOpen()},setPopupContent:function(t){return this._popup&&this._popup.setContent(t),this},getPopup:function(){return this._popup},_openPopup:function(t){var e;this._popup&&this._map&&(Re(t),e=t.layer||t.target,this._popup._source!==e||e instanceof fi?(this._popup._source=e,this.openPopup(t.latlng)):this._map.hasLayer(this._popup)?this.closePopup():this.openPopup(t.latlng))},_movePopup:function(t){this._popup.setLatLng(t.latlng)},_onKeyPress:function(t){13===t.originalEvent.keyCode&&this._openPopup(t)}}),Ai.extend({options:{pane:"tooltipPane",offset:[0,0],direction:"auto",permanent:!1,sticky:!1,opacity:.9},onAdd:function(t){Ai.prototype.onAdd.call(this,t),this.setOpacity(this.options.opacity),t.fire("tooltipopen",{tooltip:this}),this._source&&(this.addEventParent(this._source),this._source.fire("tooltipopen",{tooltip:this},!0))},onRemove:function(t){Ai.prototype.onRemove.call(this,t),t.fire("tooltipclose",{tooltip:this}),this._source&&(this.removeEventParent(this._source),this._source.fire("tooltipclose",{tooltip:this},!0))},getEvents:function(){var t=Ai.prototype.getEvents.call(this);return this.options.permanent||(t.preclick=this.close),t},_initLayout:function(){var t="leaflet-tooltip "+(this.options.className||"")+" leaflet-zoom-"+(this._zoomAnimated?"animated":"hide");this._contentNode=this._container=P("div",t),this._container.setAttribute("role","tooltip"),this._container.setAttribute("id","leaflet-tooltip-"+h(this))},_updateLayout:function(){},_adjustPan:function(){},_setPosition:function(t){var e,i=this._map,n=this._container,o=i.latLngToContainerPoint(i.getCenter()),i=i.layerPointToContainerPoint(t),s=this.options.direction,r=n.offsetWidth,a=n.offsetHeight,h=m(this.options.offset),l=this._getAnchor(),i="top"===s?(e=r/2,a):"bottom"===s?(e=r/2,0):(e="center"===s?r/2:"right"===s?0:"left"===s?r:i.xthis.options.maxZoom||nthis.options.maxZoom||void 0!==this.options.minZoom&&oi.max.x)||!e.wrapLat&&(t.yi.max.y))return!1}return!this.options.bounds||(e=this._tileCoordsToBounds(t),g(this.options.bounds).overlaps(e))},_keyToBounds:function(t){return this._tileCoordsToBounds(this._keyToTileCoords(t))},_tileCoordsToNwSe:function(t){var e=this._map,i=this.getTileSize(),n=t.scaleBy(i),i=n.add(i);return[e.unproject(n,t.z),e.unproject(i,t.z)]},_tileCoordsToBounds:function(t){t=this._tileCoordsToNwSe(t),t=new s(t[0],t[1]);return t=this.options.noWrap?t:this._map.wrapLatLngBounds(t)},_tileCoordsToKey:function(t){return t.x+":"+t.y+":"+t.z},_keyToTileCoords:function(t){var t=t.split(":"),e=new p(+t[0],+t[1]);return e.z=+t[2],e},_removeTile:function(t){var e=this._tiles[t];e&&(T(e.el),delete this._tiles[t],this.fire("tileunload",{tile:e.el,coords:this._keyToTileCoords(t)}))},_initTile:function(t){M(t,"leaflet-tile");var e=this.getTileSize();t.style.width=e.x+"px",t.style.height=e.y+"px",t.onselectstart=u,t.onmousemove=u,b.ielt9&&this.options.opacity<1&&C(t,this.options.opacity)},_addTile:function(t,e){var i=this._getTilePos(t),n=this._tileCoordsToKey(t),o=this.createTile(this._wrapCoords(t),a(this._tileReady,this,t));this._initTile(o),this.createTile.length<2&&x(a(this._tileReady,this,t,null,o)),Z(o,i),this._tiles[n]={el:o,coords:t,current:!0},e.appendChild(o),this.fire("tileloadstart",{tile:o,coords:t})},_tileReady:function(t,e,i){e&&this.fire("tileerror",{error:e,tile:i,coords:t});var n=this._tileCoordsToKey(t);(i=this._tiles[n])&&(i.loaded=+new Date,this._map._fadeAnimated?(C(i.el,0),r(this._fadeFrame),this._fadeFrame=x(this._updateOpacity,this)):(i.active=!0,this._pruneTiles()),e||(M(i.el,"leaflet-tile-loaded"),this.fire("tileload",{tile:i.el,coords:t})),this._noTilesToLoad()&&(this._loading=!1,this.fire("load"),b.ielt9||!this._map._fadeAnimated?x(this._pruneTiles,this):setTimeout(a(this._pruneTiles,this),250)))},_getTilePos:function(t){return t.scaleBy(this.getTileSize()).subtract(this._level.origin)},_wrapCoords:function(t){var e=new p(this._wrapX?H(t.x,this._wrapX):t.x,this._wrapY?H(t.y,this._wrapY):t.y);return e.z=t.z,e},_pxBoundsToTileRange:function(t){var e=this.getTileSize();return new f(t.min.unscaleBy(e).floor(),t.max.unscaleBy(e).ceil().subtract([1,1]))},_noTilesToLoad:function(){for(var t in this._tiles)if(!this._tiles[t].loaded)return!1;return!0}});var Di=Ni.extend({options:{minZoom:0,maxZoom:18,subdomains:"abc",errorTileUrl:"",zoomOffset:0,tms:!1,zoomReverse:!1,detectRetina:!1,crossOrigin:!1,referrerPolicy:!1},initialize:function(t,e){this._url=t,(e=c(this,e)).detectRetina&&b.retina&&0')}}catch(t){}return function(t){return document.createElement("<"+t+' xmlns="urn:schemas-microsoft.com:vml" class="lvml">')}}(),zt={_initContainer:function(){this._container=P("div","leaflet-vml-container")},_update:function(){this._map._animatingZoom||(Wi.prototype._update.call(this),this.fire("update"))},_initPath:function(t){var e=t._container=Vi("shape");M(e,"leaflet-vml-shape "+(this.options.className||"")),e.coordsize="1 1",t._path=Vi("path"),e.appendChild(t._path),this._updateStyle(t),this._layers[h(t)]=t},_addPath:function(t){var e=t._container;this._container.appendChild(e),t.options.interactive&&t.addInteractiveTarget(e)},_removePath:function(t){var e=t._container;T(e),t.removeInteractiveTarget(e),delete this._layers[h(t)]},_updateStyle:function(t){var e=t._stroke,i=t._fill,n=t.options,o=t._container;o.stroked=!!n.stroke,o.filled=!!n.fill,n.stroke?(e=e||(t._stroke=Vi("stroke")),o.appendChild(e),e.weight=n.weight+"px",e.color=n.color,e.opacity=n.opacity,n.dashArray?e.dashStyle=d(n.dashArray)?n.dashArray.join(" "):n.dashArray.replace(/( *, *)/g," "):e.dashStyle="",e.endcap=n.lineCap.replace("butt","flat"),e.joinstyle=n.lineJoin):e&&(o.removeChild(e),t._stroke=null),n.fill?(i=i||(t._fill=Vi("fill")),o.appendChild(i),i.color=n.fillColor||n.color,i.opacity=n.fillOpacity):i&&(o.removeChild(i),t._fill=null)},_updateCircle:function(t){var e=t._point.round(),i=Math.round(t._radius),n=Math.round(t._radiusY||i);this._setPath(t,t._empty()?"M0 0":"AL "+e.x+","+e.y+" "+i+","+n+" 0,23592600")},_setPath:function(t,e){t._path.v=e},_bringToFront:function(t){fe(t._container)},_bringToBack:function(t){ge(t._container)}},qi=b.vml?Vi:ct,Gi=Wi.extend({_initContainer:function(){this._container=qi("svg"),this._container.setAttribute("pointer-events","none"),this._rootGroup=qi("g"),this._container.appendChild(this._rootGroup)},_destroyContainer:function(){T(this._container),k(this._container),delete this._container,delete this._rootGroup,delete this._svgSize},_update:function(){var t,e,i;this._map._animatingZoom&&this._bounds||(Wi.prototype._update.call(this),e=(t=this._bounds).getSize(),i=this._container,this._svgSize&&this._svgSize.equals(e)||(this._svgSize=e,i.setAttribute("width",e.x),i.setAttribute("height",e.y)),Z(i,t.min),i.setAttribute("viewBox",[t.min.x,t.min.y,e.x,e.y].join(" ")),this.fire("update"))},_initPath:function(t){var e=t._path=qi("path");t.options.className&&M(e,t.options.className),t.options.interactive&&M(e,"leaflet-interactive"),this._updateStyle(t),this._layers[h(t)]=t},_addPath:function(t){this._rootGroup||this._initContainer(),this._rootGroup.appendChild(t._path),t.addInteractiveTarget(t._path)},_removePath:function(t){T(t._path),t.removeInteractiveTarget(t._path),delete this._layers[h(t)]},_updatePath:function(t){t._project(),t._update()},_updateStyle:function(t){var e=t._path,t=t.options;e&&(t.stroke?(e.setAttribute("stroke",t.color),e.setAttribute("stroke-opacity",t.opacity),e.setAttribute("stroke-width",t.weight),e.setAttribute("stroke-linecap",t.lineCap),e.setAttribute("stroke-linejoin",t.lineJoin),t.dashArray?e.setAttribute("stroke-dasharray",t.dashArray):e.removeAttribute("stroke-dasharray"),t.dashOffset?e.setAttribute("stroke-dashoffset",t.dashOffset):e.removeAttribute("stroke-dashoffset")):e.setAttribute("stroke","none"),t.fill?(e.setAttribute("fill",t.fillColor||t.color),e.setAttribute("fill-opacity",t.fillOpacity),e.setAttribute("fill-rule",t.fillRule||"evenodd")):e.setAttribute("fill","none"))},_updatePoly:function(t,e){this._setPath(t,dt(t._parts,e))},_updateCircle:function(t){var e=t._point,i=Math.max(Math.round(t._radius),1),n="a"+i+","+(Math.max(Math.round(t._radiusY),1)||i)+" 0 1,0 ",e=t._empty()?"M0 0":"M"+(e.x-i)+","+e.y+n+2*i+",0 "+n+2*-i+",0 ";this._setPath(t,e)},_setPath:function(t,e){t._path.setAttribute("d",e)},_bringToFront:function(t){fe(t._path)},_bringToBack:function(t){ge(t._path)}});function Ki(t){return b.svg||b.vml?new Gi(t):null}b.vml&&Gi.include(zt),A.include({getRenderer:function(t){t=(t=t.options.renderer||this._getPaneRenderer(t.options.pane)||this.options.renderer||this._renderer)||(this._renderer=this._createRenderer());return this.hasLayer(t)||this.addLayer(t),t},_getPaneRenderer:function(t){var e;return"overlayPane"!==t&&void 0!==t&&(void 0===(e=this._paneRenderers[t])&&(e=this._createRenderer({pane:t}),this._paneRenderers[t]=e),e)},_createRenderer:function(t){return this.options.preferCanvas&&Ui(t)||Ki(t)}});var Yi=xi.extend({initialize:function(t,e){xi.prototype.initialize.call(this,this._boundsToLatLngs(t),e)},setBounds:function(t){return this.setLatLngs(this._boundsToLatLngs(t))},_boundsToLatLngs:function(t){return[(t=g(t)).getSouthWest(),t.getNorthWest(),t.getNorthEast(),t.getSouthEast()]}});Gi.create=qi,Gi.pointsToPath=dt,wi.geometryToLayer=bi,wi.coordsToLatLng=Li,wi.coordsToLatLngs=Ti,wi.latLngToCoords=Mi,wi.latLngsToCoords=zi,wi.getFeature=Ci,wi.asFeature=Zi,A.mergeOptions({boxZoom:!0});var _t=n.extend({initialize:function(t){this._map=t,this._container=t._container,this._pane=t._panes.overlayPane,this._resetStateTimeout=0,t.on("unload",this._destroy,this)},addHooks:function(){S(this._container,"mousedown",this._onMouseDown,this)},removeHooks:function(){k(this._container,"mousedown",this._onMouseDown,this)},moved:function(){return this._moved},_destroy:function(){T(this._pane),delete this._pane},_resetState:function(){this._resetStateTimeout=0,this._moved=!1},_clearDeferredResetState:function(){0!==this._resetStateTimeout&&(clearTimeout(this._resetStateTimeout),this._resetStateTimeout=0)},_onMouseDown:function(t){if(!t.shiftKey||1!==t.which&&1!==t.button)return!1;this._clearDeferredResetState(),this._resetState(),re(),Le(),this._startPoint=this._map.mouseEventToContainerPoint(t),S(document,{contextmenu:Re,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this)},_onMouseMove:function(t){this._moved||(this._moved=!0,this._box=P("div","leaflet-zoom-box",this._container),M(this._container,"leaflet-crosshair"),this._map.fire("boxzoomstart")),this._point=this._map.mouseEventToContainerPoint(t);var t=new f(this._point,this._startPoint),e=t.getSize();Z(this._box,t.min),this._box.style.width=e.x+"px",this._box.style.height=e.y+"px"},_finish:function(){this._moved&&(T(this._box),z(this._container,"leaflet-crosshair")),ae(),Te(),k(document,{contextmenu:Re,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this)},_onMouseUp:function(t){1!==t.which&&1!==t.button||(this._finish(),this._moved&&(this._clearDeferredResetState(),this._resetStateTimeout=setTimeout(a(this._resetState,this),0),t=new s(this._map.containerPointToLatLng(this._startPoint),this._map.containerPointToLatLng(this._point)),this._map.fitBounds(t).fire("boxzoomend",{boxZoomBounds:t})))},_onKeyDown:function(t){27===t.keyCode&&(this._finish(),this._clearDeferredResetState(),this._resetState())}}),Ct=(A.addInitHook("addHandler","boxZoom",_t),A.mergeOptions({doubleClickZoom:!0}),n.extend({addHooks:function(){this._map.on("dblclick",this._onDoubleClick,this)},removeHooks:function(){this._map.off("dblclick",this._onDoubleClick,this)},_onDoubleClick:function(t){var e=this._map,i=e.getZoom(),n=e.options.zoomDelta,i=t.originalEvent.shiftKey?i-n:i+n;"center"===e.options.doubleClickZoom?e.setZoom(i):e.setZoomAround(t.containerPoint,i)}})),Zt=(A.addInitHook("addHandler","doubleClickZoom",Ct),A.mergeOptions({dragging:!0,inertia:!0,inertiaDeceleration:3400,inertiaMaxSpeed:1/0,easeLinearity:.2,worldCopyJump:!1,maxBoundsViscosity:0}),n.extend({addHooks:function(){var t;this._draggable||(t=this._map,this._draggable=new Xe(t._mapPane,t._container),this._draggable.on({dragstart:this._onDragStart,drag:this._onDrag,dragend:this._onDragEnd},this),this._draggable.on("predrag",this._onPreDragLimit,this),t.options.worldCopyJump&&(this._draggable.on("predrag",this._onPreDragWrap,this),t.on("zoomend",this._onZoomEnd,this),t.whenReady(this._onZoomEnd,this))),M(this._map._container,"leaflet-grab leaflet-touch-drag"),this._draggable.enable(),this._positions=[],this._times=[]},removeHooks:function(){z(this._map._container,"leaflet-grab"),z(this._map._container,"leaflet-touch-drag"),this._draggable.disable()},moved:function(){return this._draggable&&this._draggable._moved},moving:function(){return this._draggable&&this._draggable._moving},_onDragStart:function(){var t,e=this._map;e._stop(),this._map.options.maxBounds&&this._map.options.maxBoundsViscosity?(t=g(this._map.options.maxBounds),this._offsetLimit=_(this._map.latLngToContainerPoint(t.getNorthWest()).multiplyBy(-1),this._map.latLngToContainerPoint(t.getSouthEast()).multiplyBy(-1).add(this._map.getSize())),this._viscosity=Math.min(1,Math.max(0,this._map.options.maxBoundsViscosity))):this._offsetLimit=null,e.fire("movestart").fire("dragstart"),e.options.inertia&&(this._positions=[],this._times=[])},_onDrag:function(t){var e,i;this._map.options.inertia&&(e=this._lastTime=+new Date,i=this._lastPos=this._draggable._absPos||this._draggable._newPos,this._positions.push(i),this._times.push(e),this._prunePositions(e)),this._map.fire("move",t).fire("drag",t)},_prunePositions:function(t){for(;1e.max.x&&(t.x=this._viscousLimit(t.x,e.max.x)),t.y>e.max.y&&(t.y=this._viscousLimit(t.y,e.max.y)),this._draggable._newPos=this._draggable._startPos.add(t))},_onPreDragWrap:function(){var t=this._worldWidth,e=Math.round(t/2),i=this._initialWorldOffset,n=this._draggable._newPos.x,o=(n-e+i)%t+e-i,n=(n+e+i)%t-e-i,t=Math.abs(o+i)e.getMaxZoom()&&1 + * All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/* + * Thanks to Pavel Shramov who provided the initial implementation and Leaflet + * integration. Original code was at https://github.com/shramov/leaflet-plugins. + * + * It was then cleaned-up and modified to record and make available more + * information about the GPX track while it is being parsed so that the result + * can be used to display additional information about the track that is + * rendered on the Leaflet map. + */ + +var L = L || require('leaflet'); + +var _MAX_POINT_INTERVAL_MS = 15000; +var _SECOND_IN_MILLIS = 1000; +var _MINUTE_IN_MILLIS = 60 * _SECOND_IN_MILLIS; +var _HOUR_IN_MILLIS = 60 * _MINUTE_IN_MILLIS; +var _DAY_IN_MILLIS = 24 * _HOUR_IN_MILLIS; + +var _GPX_STYLE_NS = 'http://www.topografix.com/GPX/gpx_style/0/2'; + +var _DEFAULT_MARKER_OPTS = { + startIconUrl: 'pin-icon-start.png', + endIconUrl: 'pin-icon-end.png', + shadowUrl: 'pin-shadow.png', + wptIcons: [], + wptIconsType: [], + wptIconUrls : { + '': 'pin-icon-wpt.png', + }, + wptIconTypeUrls : { + '': 'pin-icon-wpt.png', + }, + pointMatchers: [], + iconSize: [33, 45], + shadowSize: [50, 50], + iconAnchor: [16, 45], + shadowAnchor: [16, 47], + clickable: false +}; +var _DEFAULT_POLYLINE_OPTS = { + color: 'blue' +}; +var _DEFAULT_GPX_OPTS = { + parseElements: ['track', 'route', 'waypoint'], + joinTrackSegments: true +}; + +L.GPX = L.FeatureGroup.extend({ + initialize: function(gpx, options) { + options.max_point_interval = options.max_point_interval || _MAX_POINT_INTERVAL_MS; + options.marker_options = this._merge_objs( + _DEFAULT_MARKER_OPTS, + options.marker_options || {}); + options.polyline_options = options.polyline_options || {}; + options.gpx_options = this._merge_objs( + _DEFAULT_GPX_OPTS, + options.gpx_options || {}); + + L.Util.setOptions(this, options); + + // Base icon class for track pins. + L.GPXTrackIcon = L.Icon.extend({ options: options.marker_options }); + + this._gpx = gpx; + this._layers = {}; + this._init_info(); + + if (gpx) { + this._parse(gpx, options, this.options.async); + } + }, + + get_duration_string: function(duration, hidems) { + var s = ''; + + if (duration >= _DAY_IN_MILLIS) { + s += Math.floor(duration / _DAY_IN_MILLIS) + 'd '; + duration = duration % _DAY_IN_MILLIS; + } + + if (duration >= _HOUR_IN_MILLIS) { + s += Math.floor(duration / _HOUR_IN_MILLIS) + ':'; + duration = duration % _HOUR_IN_MILLIS; + } + + var mins = Math.floor(duration / _MINUTE_IN_MILLIS); + duration = duration % _MINUTE_IN_MILLIS; + if (mins < 10) s += '0'; + s += mins + '\''; + + var secs = Math.floor(duration / _SECOND_IN_MILLIS); + duration = duration % _SECOND_IN_MILLIS; + if (secs < 10) s += '0'; + s += secs; + + if (!hidems && duration > 0) s += '.' + Math.round(Math.floor(duration)*1000)/1000; + else s += '"'; + + return s; + }, + + get_duration_string_iso: function(duration, hidems) { + var s = this.get_duration_string(duration, hidems); + return s.replace("'",':').replace('"',''); + }, + + // Public methods + to_miles: function(v) { return v / 1.60934; }, + to_ft: function(v) { return v * 3.28084; }, + m_to_km: function(v) { return v / 1000; }, + m_to_mi: function(v) { return v / 1609.34; }, + ms_to_kmh: function(v) { return v * 3.6; }, + ms_to_mih: function(v) { return v / 1609.34 * 3600; }, + + get_name: function() { return this._info.name; }, + get_desc: function() { return this._info.desc; }, + get_author: function() { return this._info.author; }, + get_copyright: function() { return this._info.copyright; }, + get_distance: function() { return this._info.length; }, + get_distance_imp: function() { return this.to_miles(this.m_to_km(this.get_distance())); }, + + get_start_time: function() { return this._info.duration.start; }, + get_end_time: function() { return this._info.duration.end; }, + get_moving_time: function() { return this._info.duration.moving; }, + get_total_time: function() { return this._info.duration.total; }, + + get_moving_pace: function() { return this.get_moving_time() / this.m_to_km(this.get_distance()); }, + get_moving_pace_imp: function() { return this.get_moving_time() / this.get_distance_imp(); }, + + get_moving_speed: function() { return this.m_to_km(this.get_distance()) / (this.get_moving_time() / (3600 * 1000)) ; }, + get_moving_speed_imp:function() { return this.to_miles(this.m_to_km(this.get_distance())) / (this.get_moving_time() / (3600 * 1000)) ; }, + + get_total_speed: function() { return this.m_to_km(this.get_distance()) / (this.get_total_time() / (3600 * 1000)); }, + get_total_speed_imp: function() { return this.to_miles(this.m_to_km(this.get_distance())) / (this.get_total_time() / (3600 * 1000)); }, + + get_elevation_gain: function() { return this._info.elevation.gain; }, + get_elevation_loss: function() { return this._info.elevation.loss; }, + get_elevation_gain_imp: function() { return this.to_ft(this.get_elevation_gain()); }, + get_elevation_loss_imp: function() { return this.to_ft(this.get_elevation_loss()); }, + get_elevation_data: function() { + var _this = this; + return this._info.elevation._points.map( + function(p) { return _this._prepare_data_point(p, _this.m_to_km, null, + function(a, b) { return a.toFixed(2) + ' km, ' + b.toFixed(0) + ' m'; }); + }); + }, + get_elevation_data_imp: function() { + var _this = this; + return this._info.elevation._points.map( + function(p) { return _this._prepare_data_point(p, _this.m_to_mi, _this.to_ft, + function(a, b) { return a.toFixed(2) + ' mi, ' + b.toFixed(0) + ' ft'; }); + }); + }, + get_elevation_max: function() { return this._info.elevation.max; }, + get_elevation_min: function() { return this._info.elevation.min; }, + get_elevation_max_imp: function() { return this.to_ft(this.get_elevation_max()); }, + get_elevation_min_imp: function() { return this.to_ft(this.get_elevation_min()); }, + + get_speed_data: function() { + var _this = this; + return this._info.speed._points.map( + function(p) { return _this._prepare_data_point(p, _this.m_to_km, _this.ms_to_kmh, + function(a, b) { return a.toFixed(2) + ' km, ' + b.toFixed(2) + ' km/h'; }); + }); + }, + get_speed_data_imp: function() { + var _this = this; + return this._info.speed._points.map( + function(p) { return _this._prepare_data_point(p, _this.m_to_mi, _this.ms_to_mih, + function(a, b) { return a.toFixed(2) + ' mi, ' + b.toFixed(2) + ' mi/h'; }); + }); + }, + get_speed_max: function() { return this.m_to_km(this._info.speed.max) * 3600; }, + get_speed_max_imp: function() { return this.to_miles(this.get_speed_max()); }, + + get_average_hr: function() { return this._info.hr.avg; }, + get_average_temp: function() { return this._info.atemp.avg; }, + get_average_cadence: function() { return this._info.cad.avg; }, + get_heartrate_data: function() { + var _this = this; + return this._info.hr._points.map( + function(p) { return _this._prepare_data_point(p, _this.m_to_km, null, + function(a, b) { return a.toFixed(2) + ' km, ' + b.toFixed(0) + ' bpm'; }); + }); + }, + get_heartrate_data_imp: function() { + var _this = this; + return this._info.hr._points.map( + function(p) { return _this._prepare_data_point(p, _this.m_to_mi, null, + function(a, b) { return a.toFixed(2) + ' mi, ' + b.toFixed(0) + ' bpm'; }); + }); + }, + get_cadence_data: function() { + var _this = this; + return this._info.cad._points.map( + function(p) { return _this._prepare_data_point(p, _this.m_to_km, null, + function(a, b) { return a.toFixed(2) + ' km, ' + b.toFixed(0) + ' rpm'; }); + }); + }, + get_temp_data: function() { + var _this = this; + return this._info.atemp._points.map( + function(p) { return _this._prepare_data_point(p, _this.m_to_km, null, + function(a, b) { return a.toFixed(2) + ' km, ' + b.toFixed(0) + ' degrees'; }); + }); + }, + get_cadence_data_imp: function() { + var _this = this; + return this._info.cad._points.map( + function(p) { return _this._prepare_data_point(p, _this.m_to_mi, null, + function(a, b) { return a.toFixed(2) + ' mi, ' + b.toFixed(0) + ' rpm'; }); + }); + }, + get_temp_data_imp: function() { + var _this = this; + return this._info.atemp._points.map( + function(p) { return _this._prepare_data_point(p, _this.m_to_mi, null, + function(a, b) { return a.toFixed(2) + ' mi, ' + b.toFixed(0) + ' degrees'; }); + }); + }, + + reload: function() { + this._init_info(); + this.clearLayers(); + this._parse(this._gpx, this.options, this.options.async); + }, + + // Private methods + _merge_objs: function(a, b) { + var _ = {}; + for (var attr in a) { _[attr] = a[attr]; } + for (var attr in b) { _[attr] = b[attr]; } + return _; + }, + + _prepare_data_point: function(p, trans1, trans2, trans_tooltip) { + var r = [trans1 && trans1(p[0]) || p[0], trans2 && trans2(p[1]) || p[1]]; + r.push(trans_tooltip && trans_tooltip(r[0], r[1]) || (r[0] + ': ' + r[1])); + return r; + }, + + _init_info: function() { + this._info = { + name: null, + length: 0.0, + elevation: {gain: 0.0, loss: 0.0, max: 0.0, min: Infinity, _points: []}, + speed : {max: 0.0, _points: []}, + hr: {avg: 0, _total: 0, _points: []}, + duration: {start: null, end: null, moving: 0, total: 0}, + atemp: {avg: 0, _total: 0, _points: []}, + cad: {avg: 0, _total: 0, _points: []} + }; + }, + + _load_xml: function(url, cb, options, async) { + if (async == undefined) async = this.options.async; + if (options == undefined) options = this.options; + + var req = new window.XMLHttpRequest(); + req.open('GET', url, async); + try { + req.overrideMimeType('text/xml'); // unsupported by IE + } catch(e) {} + req.onreadystatechange = function() { + if (req.readyState != 4) return; + if(req.status == 200) cb(req.responseXML, options); + }; + req.send(null); + }, + + _parse: function(input, options, async) { + var _this = this; + var cb = function(gpx, options) { + var layers = _this._parse_gpx_data(gpx, options); + if (!layers) { + _this.fire('error', { err: 'No parseable layers of type(s) ' + JSON.stringify(options.gpx_options.parseElements) }); + return; + } + _this.addLayer(layers); + _this.fire('loaded', { layers: layers, element: gpx }); + } + if (input.substr(0,1)==='<') { // direct XML has to start with a < + var parser = new DOMParser(); + if (async) { + setTimeout(function() { + cb(parser.parseFromString(input, "text/xml"), options); + }); + } else { + cb(parser.parseFromString(input, "text/xml"), options); + } + } else { + this._load_xml(input, cb, options, async); + } + }, + + _parse_gpx_data: function(xml, options) { + var i, t, l, el, layers = []; + + var name = xml.getElementsByTagName('name'); + if (name.length > 0) { + this._info.name = name[0].textContent; + } + var desc = xml.getElementsByTagName('desc'); + if (desc.length > 0) { + this._info.desc = desc[0].textContent; + } + var author = xml.getElementsByTagName('author'); + if (author.length > 0) { + this._info.author = author[0].textContent; + } + var copyright = xml.getElementsByTagName('copyright'); + if (copyright.length > 0) { + this._info.copyright = copyright[0].textContent; + } + + var parseElements = options.gpx_options.parseElements; + if (parseElements.indexOf('route') > -1) { + // routes are tags inside sections + var routes = xml.getElementsByTagName('rte'); + for (i = 0; i < routes.length; i++) { + layers = layers.concat(this._parse_segment(routes[i], options, {}, 'rtept')); + } + } + + if (parseElements.indexOf('track') > -1) { + // tracks are tags in one or more sections in each + var tracks = xml.getElementsByTagName('trk'); + for (i = 0; i < tracks.length; i++) { + var track = tracks[i]; + var polyline_options = this._extract_styling(track); + + if (options.gpx_options.joinTrackSegments) { + layers = layers.concat(this._parse_segment(track, options, polyline_options, 'trkpt')); + } else { + var segments = track.getElementsByTagName('trkseg'); + for (j = 0; j < segments.length; j++) { + layers = layers.concat(this._parse_segment(segments[j], options, polyline_options, 'trkpt')); + } + } + } + } + + this._info.hr.avg = Math.round(this._info.hr._total / this._info.hr._points.length); + this._info.cad.avg = Math.round(this._info.cad._total / this._info.cad._points.length); + this._info.atemp.avg = Math.round(this._info.atemp._total / this._info.atemp._points.length); + + // parse waypoints and add markers for each of them + if (parseElements.indexOf('waypoint') > -1) { + el = xml.getElementsByTagName('wpt'); + for (i = 0; i < el.length; i++) { + var ll = new L.LatLng( + el[i].getAttribute('lat'), + el[i].getAttribute('lon')); + + var nameEl = el[i].getElementsByTagName('name'); + var name = nameEl.length > 0 ? nameEl[0].textContent : ''; + + var descEl = el[i].getElementsByTagName('desc'); + var desc = descEl.length > 0 ? descEl[0].textContent : ''; + + var symEl = el[i].getElementsByTagName('sym'); + var symKey = symEl.length > 0 ? symEl[0].textContent : null; + + var typeEl = el[i].getElementsByTagName('type'); + var typeKey = typeEl.length > 0 ? typeEl[0].textContent : null; + + /* + * Add waypoint marker based on the waypoint symbol key. + * + * First look for a configured icon for that symKey. If not found, look + * for a configured icon URL for that symKey and build an icon from it. + * If none of those match, look through the point matchers for a match + * on the waypoint's name. + * + * Otherwise, fall back to the default icon if one was configured, or + * finally to the default icon URL, if one was configured. + */ + var wptIcons = options.marker_options.wptIcons; + var wptIconUrls = options.marker_options.wptIconUrls; + var wptIconsType = options.marker_options.wptIconsType; + var wptIconTypeUrls = options.marker_options.wptIconTypeUrls; + var ptMatchers = options.marker_options.pointMatchers || []; + var symIcon; + if (wptIcons && symKey && wptIcons[symKey]) { + symIcon = wptIcons[symKey]; + } else if (wptIconsType && typeKey && wptIconsType[typeKey]) { + symIcon = wptIconsType[typeKey]; + } else if (wptIconUrls && symKey && wptIconUrls[symKey]) { + symIcon = new L.GPXTrackIcon({iconUrl: wptIconUrls[symKey]}); + } else if (wptIconTypeUrls && typeKey && wptIconTypeUrls[typeKey]) { + symIcon = new L.GPXTrackIcon({iconUrl: wptIconTypeUrls[typeKey]}); + } else if (ptMatchers.length > 0) { + for (var j = 0; j < ptMatchers.length; j++) { + if (ptMatchers[j].regex.test(name)) { + symIcon = ptMatchers[j].icon; + break; + } + } + } else if (wptIcons && wptIcons['']) { + symIcon = wptIcons['']; + } else if (wptIconUrls && wptIconUrls['']) { + symIcon = new L.GPXTrackIcon({iconUrl: wptIconUrls['']}); + } + + if (!symIcon) { + console.log( + 'No waypoint icon could be matched for symKey=%s,typeKey=%s,name=%s on waypoint %o', + symKey, typeKey, name, el[i]); + continue; + } + + var marker = new L.Marker(ll, { + clickable: options.marker_options.clickable, + title: name, + icon: symIcon, + type: 'waypoint' + }); + marker.bindPopup("" + name + "" + (desc.length > 0 ? '
' + desc : '')).openPopup(); + this.fire('addpoint', { point: marker, point_type: 'waypoint', element: el[i] }); + layers.push(marker); + } + } + + if (layers.length > 1) { + return new L.FeatureGroup(layers); + } else if (layers.length == 1) { + return layers[0]; + } + }, + + _parse_segment: function(line, options, polyline_options, tag) { + var el = line.getElementsByTagName(tag); + if (!el.length) return []; + + var coords = []; + var markers = []; + var layers = []; + var last = null; + + for (var i = 0; i < el.length; i++) { + var _, ll = new L.LatLng( + el[i].getAttribute('lat'), + el[i].getAttribute('lon')); + ll.meta = { time: null, ele: null, hr: null, cad: null, atemp: null, speed: null }; + + _ = el[i].getElementsByTagName('time'); + if (_.length > 0) { + ll.meta.time = new Date(Date.parse(_[0].textContent)); + } else { + ll.meta.time = new Date('1970-01-01T00:00:00'); + } + var time_diff = last != null ? Math.abs(ll.meta.time - last.meta.time) : 0; + + _ = el[i].getElementsByTagName('ele'); + if (_.length > 0) { + ll.meta.ele = parseFloat(_[0].textContent); + } else { + // If the point doesn't have an tag, assume it has the same + // elevation as the point before it (if it had one). + ll.meta.ele = last != null ? last.meta.ele : null; + } + var ele_diff = last != null ? ll.meta.ele - last.meta.ele : 0; + var dist_3d = last != null ? this._dist3d(last, ll) : 0; + + _ = el[i].getElementsByTagName('speed'); + if (_.length > 0) { + ll.meta.speed = parseFloat(_[0].textContent); + } else { + // speed in meter per second + ll.meta.speed = time_diff > 0 ? 1000.0 * dist_3d / time_diff : 0; + } + + _ = el[i].getElementsByTagName('name'); + if (_.length > 0) { + var name = _[0].textContent; + var ptMatchers = options.marker_options.pointMatchers || []; + + for (var j = 0; j < ptMatchers.length; j++) { + if (ptMatchers[j].regex.test(name)) { + markers.push({ label: name, coords: ll, icon: ptMatchers[j].icon, element: el[i] }); + break; + } + } + } + + _ = el[i].getElementsByTagNameNS('*', 'hr'); + if (_.length > 0) { + ll.meta.hr = parseInt(_[0].textContent); + this._info.hr._points.push([this._info.length, ll.meta.hr]); + this._info.hr._total += ll.meta.hr; + } + + _ = el[i].getElementsByTagNameNS('*', 'cad'); + if (_.length > 0) { + ll.meta.cad = parseInt(_[0].textContent); + this._info.cad._points.push([this._info.length, ll.meta.cad]); + this._info.cad._total += ll.meta.cad; + } + + _ = el[i].getElementsByTagNameNS('*', 'atemp'); + if (_.length > 0) { + ll.meta.atemp = parseInt(_[0].textContent); + this._info.atemp._points.push([this._info.length, ll.meta.atemp]); + this._info.atemp._total += ll.meta.atemp; + } + + if (ll.meta.ele > this._info.elevation.max) { + this._info.elevation.max = ll.meta.ele; + } + if (ll.meta.ele < this._info.elevation.min) { + this._info.elevation.min = ll.meta.ele; + } + this._info.elevation._points.push([this._info.length, ll.meta.ele]); + + if (ll.meta.speed > this._info.speed.max) { + this._info.speed.max = ll.meta.speed; + } + this._info.speed._points.push([this._info.length, ll.meta.speed]); + + if ((last == null) && (this._info.duration.start == null)) { + this._info.duration.start = ll.meta.time; + } + this._info.duration.end = ll.meta.time; + this._info.duration.total += time_diff; + if (time_diff < options.max_point_interval) { + this._info.duration.moving += time_diff; + } + + this._info.length += dist_3d; + + if (ele_diff > 0) { + this._info.elevation.gain += ele_diff; + } else { + this._info.elevation.loss += Math.abs(ele_diff); + } + + last = ll; + coords.push(ll); + } + + // add track + var l = new L.Polyline(coords, this._extract_styling(line, polyline_options, options.polyline_options)); + this.fire('addline', { line: l, element: line }); + layers.push(l); + + if (options.marker_options.startIcon || options.marker_options.startIconUrl) { + // add start pin + var marker = new L.Marker(coords[0], { + clickable: options.marker_options.clickable, + icon: options.marker_options.startIcon || new L.GPXTrackIcon({iconUrl: options.marker_options.startIconUrl}) + }); + this.fire('addpoint', { point: marker, point_type: 'start', element: el[0] }); + layers.push(marker); + } + + if (options.marker_options.endIcon || options.marker_options.endIconUrl) { + // add end pin + var marker = new L.Marker(coords[coords.length-1], { + clickable: options.marker_options.clickable, + icon: options.marker_options.endIcon || new L.GPXTrackIcon({iconUrl: options.marker_options.endIconUrl}) + }); + this.fire('addpoint', { point: marker, point_type: 'end', element: el[el.length-1] }); + layers.push(marker); + } + + // add named markers + for (var i = 0; i < markers.length; i++) { + var marker = new L.Marker(markers[i].coords, { + clickable: options.marker_options.clickable, + title: markers[i].label, + icon: markers[i].icon + }); + this.fire('addpoint', { point: marker, point_type: 'label', element: markers[i].element }); + layers.push(marker); + } + + return layers; + }, + + _extract_styling: function(el, base, overrides) { + var style = this._merge_objs(_DEFAULT_POLYLINE_OPTS, base); + var e = el.getElementsByTagNameNS(_GPX_STYLE_NS, 'line'); + if (e.length > 0) { + var _ = e[0].getElementsByTagName('color'); + if (_.length > 0) style.color = '#' + _[0].textContent; + var _ = e[0].getElementsByTagName('opacity'); + if (_.length > 0) style.opacity = _[0].textContent; + var _ = e[0].getElementsByTagName('weight'); + if (_.length > 0) style.weight = _[0].textContent; + var _ = e[0].getElementsByTagName('linecap'); + if (_.length > 0) style.lineCap = _[0].textContent; + var _ = e[0].getElementsByTagName('linejoin'); + if (_.length > 0) style.lineJoin = _[0].textContent; + var _ = e[0].getElementsByTagName('dasharray'); + if (_.length > 0) style.dashArray = _[0].textContent; + var _ = e[0].getElementsByTagName('dashoffset'); + if (_.length > 0) style.dashOffset = _[0].textContent; + } + return this._merge_objs(style, overrides) + }, + + _dist2d: function(a, b) { + var R = 6371000; + var dLat = this._deg2rad(b.lat - a.lat); + var dLon = this._deg2rad(b.lng - a.lng); + var r = Math.sin(dLat/2) * + Math.sin(dLat/2) + + Math.cos(this._deg2rad(a.lat)) * + Math.cos(this._deg2rad(b.lat)) * + Math.sin(dLon/2) * + Math.sin(dLon/2); + var c = 2 * Math.atan2(Math.sqrt(r), Math.sqrt(1-r)); + var d = R * c; + return d; + }, + + _dist3d: function(a, b) { + var planar = this._dist2d(a, b); + var height = Math.abs(b.meta.ele - a.meta.ele); + return Math.sqrt(Math.pow(planar, 2) + Math.pow(height, 2)); + }, + + _deg2rad: function(deg) { + return deg * Math.PI / 180; + } +}); + +if (typeof module === 'object' && typeof module.exports === 'object') { + module.exports = L; +} else if (typeof define === 'function' && define.amd) { + define(L); +} + +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t(((e=e||self).Leaflet=e.Leaflet||{},e.Leaflet.markercluster={}))}(this,function(e){"use strict";var t=L.MarkerClusterGroup=L.FeatureGroup.extend({options:{maxClusterRadius:80,iconCreateFunction:null,clusterPane:L.Marker.prototype.options.pane,spiderfyOnEveryZoom:!1,spiderfyOnMaxZoom:!0,showCoverageOnHover:!0,zoomToBoundsOnClick:!0,singleMarkerMode:!1,disableClusteringAtZoom:null,removeOutsideVisibleBounds:!0,animate:!0,animateAddingMarkers:!1,spiderfyShapePositions:null,spiderfyDistanceMultiplier:1,spiderLegPolylineOptions:{weight:1.5,color:"#222",opacity:.5},chunkedLoading:!1,chunkInterval:200,chunkDelay:50,chunkProgress:null,polygonOptions:{}},initialize:function(e){L.Util.setOptions(this,e),this.options.iconCreateFunction||(this.options.iconCreateFunction=this._defaultIconCreateFunction),this._featureGroup=L.featureGroup(),this._featureGroup.addEventParent(this),this._nonPointGroup=L.featureGroup(),this._nonPointGroup.addEventParent(this),this._inZoomAnimation=0,this._needsClustering=[],this._needsRemoving=[],this._currentShownBounds=null,this._queue=[],this._childMarkerEventHandlers={dragstart:this._childMarkerDragStart,move:this._childMarkerMoved,dragend:this._childMarkerDragEnd};var t=L.DomUtil.TRANSITION&&this.options.animate;L.extend(this,t?this._withAnimation:this._noAnimation),this._markerCluster=t?L.MarkerCluster:L.MarkerClusterNonAnimated},addLayer:function(e){if(e instanceof L.LayerGroup)return this.addLayers([e]);if(!e.getLatLng)return this._nonPointGroup.addLayer(e),this.fire("layeradd",{layer:e}),this;if(!this._map)return this._needsClustering.push(e),this.fire("layeradd",{layer:e}),this;if(this.hasLayer(e))return this;this._unspiderfy&&this._unspiderfy(),this._addLayer(e,this._maxZoom),this.fire("layeradd",{layer:e}),this._topClusterLevel._recalculateBounds(),this._refreshClustersIcons();var t=e,i=this._zoom;if(e.__parent)for(;t.__parent._zoom>=i;)t=t.__parent;return this._currentShownBounds.contains(t.getLatLng())&&(this.options.animateAddingMarkers?this._animationAddLayer(e,t):this._animationAddLayerNonAnimated(e,t)),this},removeLayer:function(e){return e instanceof L.LayerGroup?this.removeLayers([e]):(e.getLatLng?this._map?e.__parent&&(this._unspiderfy&&(this._unspiderfy(),this._unspiderfyLayer(e)),this._removeLayer(e,!0),this.fire("layerremove",{layer:e}),this._topClusterLevel._recalculateBounds(),this._refreshClustersIcons(),e.off(this._childMarkerEventHandlers,this),this._featureGroup.hasLayer(e)&&(this._featureGroup.removeLayer(e),e.clusterShow&&e.clusterShow())):(!this._arraySplice(this._needsClustering,e)&&this.hasLayer(e)&&this._needsRemoving.push({layer:e,latlng:e._latlng}),this.fire("layerremove",{layer:e})):(this._nonPointGroup.removeLayer(e),this.fire("layerremove",{layer:e})),this)},addLayers:function(n,s){if(!L.Util.isArray(n))return this.addLayer(n);var o,a=this._featureGroup,h=this._nonPointGroup,l=this.options.chunkedLoading,u=this.options.chunkInterval,_=this.options.chunkProgress,d=n.length,p=0,c=!0;if(this._map){var f=(new Date).getTime(),m=L.bind(function(){var e=(new Date).getTime();for(this._map&&this._unspiderfy&&this._unspiderfy();p"+t+"",className:"marker-cluster"+i,iconSize:new L.Point(40,40)})},_bindEvents:function(){var e=this._map,t=this.options.spiderfyOnMaxZoom,i=this.options.showCoverageOnHover,r=this.options.zoomToBoundsOnClick,n=this.options.spiderfyOnEveryZoom;(t||r||n)&&this.on("clusterclick clusterkeypress",this._zoomOrSpiderfy,this),i&&(this.on("clustermouseover",this._showCoverage,this),this.on("clustermouseout",this._hideCoverage,this),e.on("zoomend",this._hideCoverage,this))},_zoomOrSpiderfy:function(e){var t=e.layer,i=t;if("clusterkeypress"!==e.type||!e.originalEvent||13===e.originalEvent.keyCode){for(;1===i._childClusters.length;)i=i._childClusters[0];i._zoom===this._maxZoom&&i._childCount===t._childCount&&this.options.spiderfyOnMaxZoom?t.spiderfy():this.options.zoomToBoundsOnClick&&t.zoomToBounds(),this.options.spiderfyOnEveryZoom&&t.spiderfy(),e.originalEvent&&13===e.originalEvent.keyCode&&this._map._container.focus()}},_showCoverage:function(e){var t=this._map;this._inZoomAnimation||(this._shownPolygon&&t.removeLayer(this._shownPolygon),2h._zoom;r--)u=new this._markerCluster(this,r,u),n[r].addObject(u,this._map.project(a.getLatLng(),r));return h._addChild(u),void this._removeFromGridUnclustered(a,t)}s[t].addObject(e,i)}this._topClusterLevel._addChild(e),e.__parent=this._topClusterLevel},_refreshClustersIcons:function(){this._featureGroup.eachLayer(function(e){e instanceof L.MarkerCluster&&e._iconNeedsUpdate&&e._updateIcon()})},_enqueue:function(e){this._queue.push(e),this._queueTimeout||(this._queueTimeout=setTimeout(L.bind(this._processQueue,this),300))},_processQueue:function(){for(var e=0;ee?(this._animationStart(),this._animationZoomOut(this._zoom,e)):this._moveEnd()},_getExpandedVisibleBounds:function(){return this.options.removeOutsideVisibleBounds?L.Browser.mobile?this._checkBoundsMaxLat(this._map.getBounds()):this._checkBoundsMaxLat(this._map.getBounds().pad(1)):this._mapBoundsInfinite},_checkBoundsMaxLat:function(e){var t=this._maxLat;return void 0!==t&&(e.getNorth()>=t&&(e._northEast.lat=1/0),e.getSouth()<=-t&&(e._southWest.lat=-1/0)),e},_animationAddLayerNonAnimated:function(e,t){if(t===e)this._featureGroup.addLayer(e);else if(2===t._childCount){t._addToMap();var i=t.getAllChildMarkers();this._featureGroup.removeLayer(i[0]),this._featureGroup.removeLayer(i[1])}else t._updateIcon()},_extractNonGroupLayers:function(e,t){var i,r=e.getLayers(),n=0;for(t=t||[];ni)&&(i=(o=d).lat),(!1===r||d.latn)&&(n=(h=d).lng),(!1===s||d.lng=this._circleSpiralSwitchover?this._generatePointsSpiral(t.length,i):(i.y+=10,this._generatePointsCircle(t.length,i)),this._animationSpiderfy(t,e)}},unspiderfy:function(e){this._group._inZoomAnimation||(this._animationUnspiderfy(e),this._group._spiderfied=null)},_generatePointsCircle:function(e,t){var i,r,n=this._group.options.spiderfyDistanceMultiplier*this._circleFootSeparation*(2+e)/this._2PI,s=this._2PI/e,o=[];for(n=Math.max(n,35),o.length=e,i=0;i1){r=F.arrayPool.get();for(var i=1,n=arguments.length;i1)for(var i=0;i1?t-1:0),i=1;i1?t-1:0),n=1;n1?t-1:0),n=1;n1?t-1:0),n=1;n1?t-1:0),i=1;i1?t-1:0),i=1;i0&&void 0!==arguments[0]?arguments[0]:{};d.a&&!e.videoSrc&&e.photoSrc?s.a.warn("Changing a `photoSrc` independent of its `videoSrc` can result in unexpected behavior"):d.a&&e.videoSrc&&!e.photoSrc&&s.a.warn("Changing a `videoSrc` independent of its `photoSrc` can result in unexpected behavior");var t=F?{photoSrc:F.photo,videoSrc:F.videoSrc,effectType:F.effectType,autoplay:F.autoplay,proactivelyLoadsVideo:F.proactivelyLoadsVideo}:{},r=c({},t,e),i=(r.photoSrc,r.videoSrc,r.effectType),n=r.autoplay,f=r.proactivelyLoadsVideo;C=o.a.objectPool.get(),r.preloadedEffectType=i,r.autoplay=!1!==n;var v=i||l.a.default;l.a.toPlaybackStyle(v)===u.a.LOOP&&r.autoplay&&(d.a&&!f&&s.a.warn("When using a looping asset you should set `proactivelyLoadsVideo` to `true` unless `autoplay` is also set to `false`"),r.proactivelyLoadsVideo=!0);for(var y in r)Object.prototype.hasOwnProperty.call(r,y)&&(p[y]===h?C[y]=r[y]:s.a.warn("LivePhotosKit.Player: Initial configuration for `"+y+"` was ignored, because the property is not a writable property."));if(F)for(var m in C){var g=C[m];F[m]=g}else F=a.a.create(R,C);o.a.objectPool.ret(C),C=null};R.setProperties=L,R.setProperties(t);for(var E,A,I=0;(E=f[I])&&(A=m[I]);I++)!function(e,t,r){"method"===r?(g.value=F[t].bind(F),Object.defineProperty(R,t,g)):(b.set=r===h?function(e){F[t]=e}:function(){},b.get=function(){return F[t]},Object.defineProperty(R,t,b))}(0,E,A);g.value=function(){var e=arguments.length,t=arguments[e-1];if(e<1||!(t instanceof Function))throw new Error("Invalid arguments passed to `observe`. Form: key, [key, …], callback.");for(var r=o.a.arrayPool.get(),i=0,n=e;i=3||"string"==typeof arguments[0]&&"string"==typeof arguments[1])throw new Error("LivePhotosKit.Player: Creating a new Player using arguments of the form 'photoSrc, videoSrc, [targetElement, [options]]' is no longer supported. Instead, use the new signature, '[targetElement, [options]]");return s.a.warn("The `LivePhotosKit.Player` method will be deprecated in an upcoming release. Please use the `LivePhotosKit.augementElementAsPlayer` or `LivePhotosKit.createPlayer` methods, instead."),e?_(e,t):P(t)},T=function e(t,r){i(this,e),this.fire=function(){r[t.keyOnObject]()},this.disconnect=function(){t.unregisterFromDefinition(r)},this.connect=function(){t.registerOnDefinition(r)}}},function(e,t,r){"use strict";var i=/_lpk_debug=true/i;t.a=i.test(window.location.search)||i.test(window.location.hash)},function(e,t,r){"use strict";var i={setUpForRender:function(){this.attachInto(this.renderer)},tearDownFromRender:function(){this.detach(),this._super()},renderStyles:function(e){for(var t,r=this.element,i=r.style,n=0;t=e[n];n++){var a=t,o=a.styleKey,s=a.value;i[o]!==s&&(i[o]=s)}}};t.a=i},function(e,t,r){"use strict";var i=r(55),n=r(56),a=r(57);t.a={APP_NAME:"LivePhotosKit",BUILD_NUMBER:i.a,MASTERING_NUMBER:n.a,FEEDBACK_URL_PREFIX:"https://feedbackws.icloud.com",LIVEPHOTOSKIT_LOADED:"livephotoskitloaded",URL_PREFIX:"https://cdn.apple-livephotoskit.com",VERSION:a.a}},function(e,t,r){"use strict";function i(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}var n=r(3),a=r(50),o=r(18),s=r(10),u=r(1);r.d(t,"a",function(){return c});var l=function(){function e(e,t){for(var r=0;r0&&void 0!==arguments[0]?arguments[0]:{};i(this,e),this._setInstanceProps(r),this._createCanvas(),this.redraw(),this._addEventListeners(),s.a.observe("locale",function(){return t.updateBadgeText()})}return l(e,[{key:"attachPlayerInstance",value:function(e){e.attachBadgeView(this),this.updateBadgeText(e.effectType)}},{key:"redraw",value:function(){var e=this.progress;e>0&&this.shouldAnimateProgressRing?this._animateProgressRing():this._redraw(e)}},{key:"reset",value:function(){var e=this._requestedFrame;e&&cancelAnimationFrame(e),this._progress=0,this._previousProgress=0,this.redraw()}},{key:"appendTo",value:function(e){e.appendChild(this.element)}},{key:"updateAriaLabel",value:function(){var e=n.a.toLocalizedString(this.effectType),t=s.a.getString("VideoEffects.Badge");this.element.setAttribute("aria-label",t+": "+e)}},{key:"updateBadgeText",value:function(e){e?this.effectType=e:e=this.effectType,this.label=e?n.a.toBadgeText(e):"",this.playbackStyle=n.a.toPlaybackStyle(e),this.updateAriaLabel(),this._redraw()}},{key:"_createCanvas",value:function(){var e=this.element;if(e){if("canvas"!==e.tagName.toLowerCase())throw new Error("Backing element for LivePhotoBadge needs to be an HTMLCanvasElement.")}else e=this.element=document.createElement("canvas");e.setAttribute("role","button"),this.updateAriaLabel(),e.classList.add("lpk-badge"),this._context=e.getContext("2d")}},{key:"_setCanvasSize",value:function(){var e=this.element,t=o.a(),r=this.height,i=this.width;e.height=r*t,e.width=i*t,e.style.height=r+"px",e.style.width=i+"px"}},{key:"_setInstanceProps",value:function(e){var t={};for(var r in d)t.hasOwnProperty.call(d,r)&&(this[r]=e.hasOwnProperty(r)?e[r]:d[r]);this.defaultProps=d}},{key:"_redraw",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:0,t=(this.element,this.label),r=t.toLowerCase()||n.a.default;this._setCanvasSize(),this._context.clearRect(0,0,this.width,this.height),this._drawBackground(),this._drawLabel(),this.shouldShowError||(this._drawInnerCircle(),n.a.toPlaybackStyle(r)!==u.a.LOOP?this._drawPlayArrow():this._drawLoopCircle()),this.shouldShowError?(this._drawProgressRing(1),this._drawErrorSlash()):this.progress>0?this._drawProgressRing(e):this._drawDottedCircle()}},{key:"_drawBackground",value:function(){var e=o.a(),t=this._context,r=this.borderRadius*e,i=this.width*e,n=this.height*e;t.beginPath(),t.moveTo(r,0),t.lineTo(i-r,0),t.quadraticCurveTo(i,0,i,r),t.lineTo(i,n-r),t.quadraticCurveTo(i,n,i-r,n),t.lineTo(r,n),t.quadraticCurveTo(0,n,0,n-r),t.lineTo(0,r),t.quadraticCurveTo(0,0,r,0),t.closePath(),t.fillStyle=this.backgroundColor,t.fill()}},{key:"_drawDottedCircle",value:function(){for(var t=e.numberOfDots,r=this.dottedRadius*o.a(),i=0;i0?s.width:0;return this._width=(u>2?a:-2)+2*t+2*n+Math.ceil(u/o.a())}},{key:"fontStyle",get:function(){return this.fontSize*o.a()+'pt/1 system, -apple-system, BlinkMacSystemFont, "Helvetica Neue", Helvetica'}},{key:"x0",get:function(){return(this.dottedRadius+this.leftPadding)*o.a()}},{key:"y0",get:function(){return this.height/2*o.a()}},{key:"progress",set:function(e){"number"==typeof e&&(this._previousProgress=this._progress,this._progress=e,this.redraw())},get:function(){return this._progress}},{key:"shouldShowError",set:function(e){this._shouldShowError=!!e,this._redraw(this.progress)},get:function(){return this._shouldShowError}}],[{key:"numberOfDots",get:function(){return 1===o.a()?17:26}}]),e}()},function(e,t,r){"use strict";var i=r(30),n=r(0),a=r(6),o=i.a.extend({mimeType:n.a.observableProperty({dependencies:["_mimeTypeFromXHR"],get:function(e){return this._mimeTypeFromXHR||e||null}}),_mimeTypeFromXHR:n.a.observableProperty(),requiresMimeTypeForRawArrayBufferSrc:!0,exposedMimeTypeKeyForErrorStrings:"mimeType",exposedSrcKeyForErrorStrings:"src",abortCurrentLoad:function(){this.__xhr&&(this._detachXHR(),this._xhr.abort()),this._mimeTypeFromXHR=null,this.abortCurrentSecondaryLoad()},loadSrc:function(e){if("string"==typeof e){this._mimeTypeFromXHR=null,this._attachXHR();var t=this._xhr;t.open("GET",e),t.responseType="arraybuffer",t.send(null)}else if(e instanceof ArrayBuffer){if(!this.mimeType&&this.requiresMimeTypeForRawArrayBufferSrc)throw new Error("MIME Type must be assigned to `"+this.exposedMimeTypeKeyForErrorStrings+"` prior to assigning a raw ArrayBuffer to `"+this.exposedSrcKeyForErrorStrings+"`.");this.beginSecondaryLoad(e,this.mimeType)}},get _xhr(){var e=this.__xhr;return e||(e=this.__xhr=new XMLHttpRequest),e},_detachXHR:function(){var e=this._xhr;e.removeEventListener("progress",this._xhrProgress),e.removeEventListener("readystatechange",this._xhrReadyStateChanged)},_attachXHR:function(){var e=this._xhr;e.addEventListener("progress",this._xhrProgress),e.addEventListener("readystatechange",this._xhrReadyStateChanged)},_xhrReadyStateChanged:function(){if("loading"===this.state){if(this._xhr.readyState>=2&&200!==this._xhr.status){var e=new Error("Failed to download resource from URL assigned to '"+this.exposedSrcKeyForErrorStrings+"'.");return e.errCode=a.a.FAILED_TO_DOWNLOAD_RESOURCE,this.loadDidFail(e)}return 4===this._xhr.readyState&&200===this._xhr.status?this._xhrLoadDidFinish():void 0}},_xhrProgress:function(e){if(e&&e.total){var t=(+e.loaded||0)/e.total;+t===t&&(this.progress=Math.max(0,Math.min(1,t)))}},_xhrLoadDidFinish:function(){this._mimeTypeFromXHR=this._xhr.getResponseHeader("Content-Type"),this.beginSecondaryLoad(this._xhr.response,this.mimeType)},beginSecondaryLoad:function(e,t){this._defaultSecondaryLoadTimeout=setTimeout(this.loadDidSucceed.bind(this,e),0)},abortCurrentSecondaryLoad:function(){this._defaultSecondaryLoadTimeout&&(clearTimeout(this._defaultSecondaryLoadTimeout),this._defaultSecondaryLoadTimeout=null)},init:function(){this._xhrReadyStateChanged=this._xhrReadyStateChanged.bind(this),this._xhrProgress=this._xhrProgress.bind(this),this._super()}});t.a=o},function(e,t,r){"use strict";var i=r(2);t.a=i.a.isEdge||i.a.isIE},function(e,t,r){"use strict";function i(){u.forEach(function(e){return e()})}function n(e){u.push(e)}function a(){return window.devicePixelRatio}function o(){return Math.ceil(a())}t.b=n,t.a=o;var s=void 0,u=[];!function(){window.matchMedia&&(s=window.matchMedia("only screen and (-webkit-min-device-pixel-ratio:1.3),only screen and (-o-min-device-pixel-ratio:13/10),only screen and (min-resolution:120dpi)"),s.addListener(i))}()},function(e,t,r){"use strict";function i(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}var n=function(){function e(e,t){for(var r=0;r0&&(this._k.length=0,this._v.length=0)}}]),e}();t.a=a},function(e,t,r){"use strict";function i(e){if(null===e)return"_null";if(void 0===e)return"_undefined";if(e.hasOwnProperty("_LPKGUID"))return e._LPKGUID;var t=void 0===e?"undefined":n(e);switch(t){case"number":Object.is(e,-0)&&(e="-0");case"string":case"boolean":return t+e;case"object":case"function":o++;var r=t+o;return a.value=r,Object.defineProperty(e,"_LPKGUID",a),r;default:throw"unrecognized object type"}}t.a=i;var n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},a={value:"",enumerable:!1,writable:!1,configurable:!1},o=0},function(e,t,r){function i(e){return r(n(e))}function n(e){var t=a[e];if(!(t+1))throw new Error("Cannot find module '"+e+"'.");return t}var a={"./en-us.lproj/strings.json":22};i.keys=function(){return Object.keys(a)},i.resolve=n,e.exports=i,i.id=21},function(e,t){e.exports={"VideoEffects.Badge":"Badge","VideoEffects.Badge.Title.Loop":"Loop","VideoEffects.Badge.Title.Bounce":"Bounce","VideoEffects.Badge.Title.LongExposure":"Long Exposure"}},function(e,t,r){"use strict";var i=r(28),n=r(32),a=r(34),o=r(37),s=r(35),u=r(4),l=r(0),d=r(8),c=r(5),h=r(1);a.a.register(),o.a.register(),s.a.register();var p=d.a.extend({approach:"",autoplay:!0,caption:"",_hasInitialized:!1,_lastRecipe:null,recipe:l.a.observableProperty({get:function(){var e=u.a.getRecipeFromPlaybackStyle(this.playbackStyle);return this._setRecipe(e),e},set:function(e){this._setRecipe(e)}}),_setRecipe:function(e){e&&e!==this._lastRecipe&&(this._lastRecipe=e,this.setUpRenderLayers())},requestMoreCompatibleRecipe:function(){this.recipe=this.recipe.requestMoreCompatibleRecipe()},duration:l.a.observableProperty({dependencies:["recipe","provider.videoDuration","provider.photoTime"],get:function(e){var t=this.recipe,r=this.provider,i=r.photoTime,n=r.videoDuration;return t?t.calculateAnimationDuration(e,n,i):0}}),displayWidth:0,displayHeight:0,get backingWidth(){return Math.round(this.displayWidth*devicePixelRatio)},get backingHeight(){return Math.round(this.displayHeight*devicePixelRatio)},get renderLayerWidth(){return this.displayWidth},get renderLayerHeight(){return this.displayHeight},get videoWidth(){return this.videoDecoder.videoWidth},get videoHeight(){return this.videoDecoder.videoHeight},photoWidth:l.a.proxyProperty("photo.width"),photoHeight:l.a.proxyProperty("photo.height"),photo:l.a.proxyProperty("provider.photo"),video:l.a.proxyProperty("provider.video"),photoTime:l.a.proxyProperty("provider.photoTime"),frameTimes:l.a.proxyProperty("provider.frameTimes"),effectType:l.a.proxyProperty("provider.effectType"),preloadedEffectType:l.a.proxyProperty("provider.preloadedEffectType"),playbackStyle:l.a.proxyProperty("provider.playbackStyle"),currentTime:l.a.observableProperty({defaultValue:0,dependencies:["duration"],get:function(e){return Math.min(this.duration||0,Math.max(0,e||0))},didChange:function(e){this.prepareToRenderAtTime(e)}}),canRenderCurrentTime:l.a.observableProperty({readOnly:!0,dependencies:["currentTime"],get:function(){return this.canRenderAtTime(this.currentTime)}}),_currentTimeRenderObserver:l.a.observer("currentTime","canRenderCurrentTime",function(e,t){t&&(this.renderedTime=e)}),renderedTime:l.a.observableProperty({defaultValue:0,didChange:function(e){this.renderAtTime(e),this.currentTime=e}}),areAllRenderLayersPrepared:l.a.observableProperty({defaultValue:!1}),isFullyPreparedForPlayback:l.a.observableProperty({readOnly:!0,dependencies:["video","areAllRenderLayersPrepared","photoTime","frameTimes","playbackStyle"],get:function(){return Boolean(this.video&&this.areAllRenderLayersPrepared&&(this.photoTime||this.playbackStyle!==h.a.HINT)&&Array.isArray(this.frameTimes))}}),cannotRenderDueToMissingPhotoTimeOrFrameTimes:l.a.observableProperty({readOnly:!0,dependencies:["video","areAllRenderLayersPrepared","photoTime","frameTimes","playbackStyle"],get:function(){return Boolean(this.video&&this.areAllRenderLayersPrepared&&(!this.photoTime&&this.playbackStyle===h.a.HINT||!Array.isArray(this.frameTimes)))}}),renderLayers:l.a.property(function(){return[]}),videoDecoder:l.a.observableProperty(function(){return this._videoDecoderClass.create({owner:this})}),_videoDecoderClass:i.a.extend({owner:l.a.observableProperty(),provider:l.a.proxyProperty("owner.provider")}),provider:l.a.observableProperty(function(){return n.a.create()}),init:function(){this._super(),this.element.className=((this.element.className||"")+" lpk-live-photo-renderer").trim(),this.element.style.position="absolute",this.element.style.overflow="hidden",this.element.style.textAlign="left"},updateSize:function(e,t){if(!arguments.length)return void(this.displayWidth&&this.displayHeight&&this.updateSize(this.displayWidth,this.displayHeight));this.displayWidth=e=Math.round(e),this.displayHeight=t=Math.round(t),this.element.style.width=e+"px",this.element.style.height=t+"px";for(var r,i=0;r=this.renderLayers[i];i++)r.updateSize(this.renderLayerWidth,this.renderLayerHeight)},_imageOrVideoDidEnterOrLeave:l.a.observer("videoDecoder.canProvideFrames","photo",function(){this.prepareToRenderAtTime(this.currentTime)}),prepareToRenderAtTime:l.a.boundFunction(function(e){this.propertyChanged("canRenderCurrentTime");for(var t,r=!0,i=0;t=this.renderLayers[i];i++)r=t.prepareToRenderAtTime(e)&&r;this.areAllRenderLayersPrepared=r}),canRenderAtTime:function(e){if(0===e)return!0;if(!this.duration&&e)return!1;for(var t,r=!0,i="",n=0;t=this.renderLayers[n];n++)t.canRenderAtTime(e)||(r=!1,i+=(i?", ":"Cannot render; waiting for ")+t.layerName);return i&&c.a.log(i+"."),r},renderAtTime:function(e){if(this.duration)for(var t,r=0;t=this.renderLayers[r];r++)t.renderAtTime(e)},getNewRenderLayers:function(){return this.recipe.getRenderLayers(this)},setUpRenderLayers:function(){var e=this.renderLayers;e&&this._cleanUpRenderLayers(e),this.renderLayers=this.getNewRenderLayers(),this.updateSize(),this.currentTime=0,this.prepareToRenderAtTime(0)},_cleanUpRenderLayers:function(e){for(var t,r=0;t=e[r];r++)t.dispose(),t.tearDownFromRender()},reduceMemoryFootprint:function(){for(var e,t=0;e=this.renderLayers[t];t++)e.reduceMemoryFootprint()},_clearRetainedFramesWhenNecessary:l.a.observer("provider.videoRotation","provider.frameTimes",function(){this.reduceMemoryFootprint(),this.prepareToRenderAtTime(this.currentTime)})});t.a=p},function(e,t,r){"use strict";var i=r(23),n=i.a.extend({approach:"dom"});t.a=n},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var i=r(14),n=r(9),a=r(10),o=r(11);r.d(t,"augmentElementAsPlayer",function(){return o.a}),r.d(t,"createPlayer",function(){return o.b}),r.d(t,"Player",function(){return o.c});var s=r(6);r.d(t,"Errors",function(){return s.a});var u=r(15);r.d(t,"LivePhotoBadge",function(){return u.a});var l=r(1);r.d(t,"PlaybackStyle",function(){return l.a}),r.d(t,"Localization",function(){return d}),r.d(t,"BUILD_NUMBER",function(){return c}),r.d(t,"MASTERING_NUMBER",function(){return h}),r.d(t,"VERSION",function(){return p}),r.d(t,"LIVEPHOTOSKIT_LOADED",function(){return f});var d={get locale(){return a.a.locale},set locale(e){a.a.locale=e}},c=i.a.BUILD_NUMBER,h=i.a.MASTERING_NUMBER,p=i.a.VERSION,f=i.a.LIVEPHOTOSKIT_LOADED,v="undefined"!=typeof window&&"undefined"!=typeof document;if(v){var y=window.document;setTimeout(function(){return y.dispatchEvent(r.i(n.a)())});if(y.styleSheets&&document.head){for(var m=null,g=null,b=0;b0)}),init:function(){this._super.apply(this,arguments),e.attachBadgeView(this.badgeView)}}).create()):null},didChange:function(e){this._nativeControls_previousValue&&this._nativeControls_previousValue.detach(),this._nativeControls_previousValue=e,e&&e.attachInto(this)}}),init:function(e,t){var i=this;if(e&&!n(e))throw"Any pre-existing element provided for use as a LivePhotosKit.Player must be able to append child DOM nodes.";e&&e.childNodes.length&&(e.innerHTML="");for(var a in t)Object.prototype.hasOwnProperty.call(t,a)&&(this[a]=t[a]);this._super(e);switch(this.element.className.indexOf("lpk-live-photo-player")<0&&(this.element.className=this.element.className+" lpk-live-photo-player"),this.element.setAttribute("role","image"),r.i(c.a)(this.element,"position")||this.element.style.position){case"absolute":case"fixed":case"relative":break;default:this.element.style.position="relative"}switch(r.i(c.a)(this.element,"display")||this.element.style.display){case"block":case"inline-block":case"table":case"table-caption":case"table-column-group":case"table-header-group":case"table-footer-group":case"table-row-group":case"table-cell":case"table-column":case"table-row":break;default:this.element.style.display="inline-block"}this.renderer.attachInto(this),this.renderer.eventDispatchingElement=this.element,window.addEventListener("resize",this.updateSize),"ontouchstart"in document.documentElement&&(this.addEventListener("touchstart",function(){return i.play()},!1),this.addEventListener("touchend",function(){return i.beginFinishingPlaybackEarly()},!1))},play:function(){if(!this.isPlaying){var e=this.provider;e.video||(e.needsLoadedVideoForPlayback=!0),this.wantsToPlay=!0,this.canPlay&&(this.isPlaying=!0,this._lastFrameNow=Date.now(),this._nextFrame())}return this.isPlaying},pause:function(){this.isPlaying=!1,this.wantsToPlay=!1,this._cancelNextFrame()},stop:function(){this.pause(),this.currentTime=0,this.renderer.duration=NaN},toggle:function(){this.wantsToPlay?this.pause():this.play()},beginFinishingPlaybackEarly:function(){this.recipe.beginFinishingPlaybackEarly(this)},_stopWhenAnotherPlayerStarts:l.a.observer("_constructor.activeInstance",function(e){e&&e!==this&&(this.stop(),this.renderer.reduceMemoryFootprint())}),_constructor:l.a.observableProperty(function(){return p}),_stopPlaybackWhenItemsLoadOrUnload:l.a.observer("video","photo",function(){!this.isPlaying||this.playbackStyle===h.a.LOOP&&this.autoplay||this.stop()}),addEventListener:function(e,t,r){var i=this.element;i.addEventListener.call(i,e,t,r)},removeEventListener:function(e,t,r){var i=this.element;i.removeEventListener.call(i,e,t,r)},_nextFrame:function(){var e=Date.now(),t=(e-this._lastFrameNow)*this.playbackRate;this._lastFrameNow=e,this.currentTime===this.renderedTime&&(this.currentTime+=t/1e3),this.recipe&&this.recipe.continuePlayback(this)},_cancelNextFrame:function(){cancelAnimationFrame(this._rafID)},updateSize:l.a.boundFunction(function(e,t){if(this.photoWidth&&this.photoHeight){var i=!0===e?void 0:e,n=!0===e?e:void 0;if(isNaN(i)||isNaN(t)?(i=this.element.offsetWidth,t=this.element.offsetHeight):(i=Math.round(i),t=Math.round(t),this.element.style.width=i+"px",this.element.style.height=t+"px"),i&&t){if(!(this._lastUpdateChangeToken!==(this._lastUpdateChangeToken=i+":"+t))&&!n)return!1;var a=r.i(u.a)(this.photoWidth,this.photoHeight,i,t),o=Math.ceil(a.height),s=Math.ceil(a.width),l=Math.floor(i/2-s/2),d=Math.round(t/2-o/2),c=this.renderer;c.element.style.top=d+"px",c.element.style.left=l+"px",c.updateSize(s,o),this.displayWidth=i,this.displayHeight=t,this.nativeControls&&this.nativeControls.updateToRendererLayout(l,d,s,o)}}}),_dispatchPhotoLoadEventOnNewPhoto:l.a.observer("photo",function(e){e&&this.dispatchEvent(r.i(d.c)())}),_dispatchVideoLoadEventOnNewVideo:l.a.observer("video",function(e){e&&this.dispatchEvent(r.i(d.d)())}),throwError:function(e){this.dispatchEvent(r.i(d.e)({error:e,errorCode:e.errCode}))}}),f=document.createElement("div");t.a=p},function(e,t,r){"use strict";function i(){f=!1}function n(){}function a(e,t){return-(e.importance-t.importance)||e.number-t.number}function o(e,t){for(var r=0,i=e.length,n=0;n=this.frameTimes.length)return this.duration;var t=0|e,r=Math.ceil(e);if(t===r)return this.frameTimes[t];var i=this.frameTimes[t],n=r=u&&l.numberr&&c.number<=r+2&&f;if(h||(p=!1),p){if(!this._isPlaying){this._isPlaying=!0;try{var v=this.video.play();v&&v.then instanceof Function&&v.then(n,i)}catch(e){f=!1}}this._expectedNextSeenFrameNumber=c.number,this._scheduleArtificialSeek()}else this._isPlaying&&(this._isPlaying=!1,this.video.pause()),this._expectedNextSeenFrameNumber=NaN,this.video.currentTime=c.time+1e-4,this._isSeeking=!0}}),_frameWillDispose:function(e){this._removePendingFrame(e)},_removePendingFrame:function(e){o(this._pendingFrames,e),this._pendingFrames.length||this._unscheduleArtificialSeek()}});t.a=v},function(e,t,r){"use strict";function i(e){e.container=document.createElement("div"),e.container.frame=e,e.container.innerHTML='
',e.textBox=e.container.lastChild,e.container.insertBefore(e.image,e.textBox),e.image.style.position="absolute",e.container.style.cssText="position:relative; display:inline-block; border: 1px solid black;";var t=e._debug_aspect||(e._debug_aspect=e.videoDecoder&&(e.videoDecoder.videoWidth>e.videoDecoder.videoHeight?"landscape":"portrait"));e.container.style.width=e.image.style.width="landscape"===t?"40px":"30px",e.container.style.height=e.image.style.height="landscape"===t?"30px":"40px",document.body.appendChild(e.container)}var n=r(12),a=r(48),o=r(5),s=r(0),u=r(46),l=r(2);r.d(t,"a",function(){return d});var d=s.a.Object.extend(u.a,a.a,{staticMembers:{getPoolingCacheKey:function(e,t){return"f"+t+"_in_"+e.id}},container:null,image:null,_context:null,number:-1,time:-1,importance:0,videoDecoder:null,readyState:0,_poolingCacheKey:null,_debugShowInDOM:n.a,lacksOwnPixelData:!1,_postDispose:function(){this.image.width=this.image.height=0},get backingFrame(){return this.lacksOwnPixelData?this.videoDecoder.getNearestDecodedFrame(this.number)||this:this},init:function(){this._postDispose=this._postDispose.bind(this);var e=this.image=document.createElement("canvas");this._context=this.image.getContext("2d"),this._super(),this._debugShowInDOM?i(this):h&&(h.appendChild(e),e.style.cssText="position: absolute; top: 0px; width:1px; height: 1px; display: inline-block;",e.style.left=c+++"px")},initFromPool:function(e,t){clearTimeout(this._postDisposalTimeout),this.videoDecoder=e,this.number=t,this.time=e.frameTimes[t],this._debugShowInDOM&&(this.textBox.innerHTML=this.number)},dispose:function(){this.resetReadiness(),this.videoDecoder._frameWillDispose(this),this.number=this.time=-1,this.importance=0,this.videoDecoder=null,this.readyState=0,this.lacksOwnPixelData=!1,this._postDisposalTimeout=setTimeout(this._postDispose,3e3),this.constructor._disposeInstance(this),this._debugShowInDOM&&(this.textBox.innerHTML="x",this.textBox.style.color="#FF0000",this._context.clearRect(0,0,this.image.width,this.image.height))},didPend:function(){this.readyState=1,this._debugShowInDOM&&(this.textBox.style.color="#FF8800")},didDecode:function(){this.obtainPixelData(),this.readyState=2,this.resolveReadiness(this),this._debugShowInDOM&&(this.textBox.style.color="#00FF00")},obtainPixelData:function(){var e=this.image,t=this._context,r=this.videoDecoder,i=r.videoRotation,n=r.videoWidth,a=r.videoHeight,o=i%180==0?n:a,s=i%180==0?a:n;e.width===n&&e.height===a||(e.width=n,e.height=a),l.a.isFirefox&&t.getImageData(0,0,1,1);for(var u=0;u=2,a=0,o=e.length;a>r)*(0!=(i&1<0)switch(t.metaData.values.items[m]){case 1:g=h.a.LOOP;break;case 2:g=h.a.BOUNCE;break;case 3:g=h.a.EXPOSURE}this.effectType=g}}),y=[0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,64,0,0,0],m=[[1,0,0,0,1,0,0,0,1],[0,1,0,-1,0,0,0,0,1],[-1,0,0,0,-1,0,0,0,1],[0,-1,0,1,0,0,0,0,1]];t.a=v},function(e,t,r){"use strict";var i=r(4),n=r(1),a=r(2),o=a.a.isSafari,s=i.a.create({correspondingPlaybackStyle:n.a.FULL,get minimumShortenedDuration(){return this.enterDuration+this.exitDuration+.01},get spontaneousFinishDuration(){return this.exitDuration},enterDuration:1/3,exitDuration:.5,videoBeginTime:.15,zoomScaleFactor:1.075,blurRadius:5,blurRadiusStep:.2,requiresInterpolation:!0,quantizeRadius:function(e){return this.blurRadiusStep?Math.round(e/this.blurRadiusStep)*this.blurRadiusStep:e},easeInOut:function(e){return e<0?0:e>1?1:.5-.5*Math.cos(e*Math.PI)},calculateAnimationDuration:function(e,t,r){var i=t?t+this.videoBeginTime+this.exitDuration:0;return Math.max(0,Math.min(e||1/0,i))},getEntranceExitParameter:function(e,t){return Math.min(Math.max(0,Math.min(1,1-this.easeInOut((e-(t-this.exitDuration))/this.exitDuration))),1-Math.max(0,Math.min(1,1-this.easeInOut(e/this.enterDuration))))||0},getTransform:function(e,t,r,i){var n=arguments.length>4&&void 0!==arguments[4]?arguments[4]:1,a=arguments.length>5&&void 0!==arguments[5]?arguments[5]:1,o=arguments.length>6&&void 0!==arguments[6]?arguments[6]:1,s=1+(this.zoomScaleFactor-1)*this.getEntranceExitParameter(e,t),u=-(s-1)/2*r,l=-(s-1)/2*i,d=Math.round(u*devicePixelRatio)/devicePixelRatio,c=Math.round(l*devicePixelRatio)/devicePixelRatio;return Math.abs(s-n)<1e-5?"translate3d("+d+"px, "+c+"px, 0) scale3d("+a+", "+o+", 1)":u||l||s?"translate3d("+u+"px, "+l+"px, 0) scale3d("+s+", "+s+", 1)":"translate3d(0, 0, 0)"},photo:i.a.PhotoIngredient.create({opacity:i.a.computedStyle(function(e){if(ethis.recipe.enterDuration&&e1?1:.5-.5*Math.cos(e*Math.PI)},calculateAnimationDuration:function(e,t,r){var i=t?t-r+this.exitBlurDuration:0;return Math.max(0,Math.min(e||1/0,i))},photo:i.a.PhotoIngredient.create({hideDuration:.06,get returnDuration(){return this.recipe.exitBlurDuration},opacity:i.a.computedStyle(function(e){if(ethis.hideDuration&&e0?"none":""})}),video:i.a.InterpolatedVideoIngredient.create({lookaheadTime:.01+7/15,videoTimeAtTime:function(e){return e%this.renderer.duration},prepareVideoFramesFromTime:function(e){this.retainFramesForTime(e,e+this.lookaheadTime)},display:i.a.computedStyle(function(e){return""})}),beginFinishingPlaybackEarly:function(e){e.autoplay||(e.isPlaying?e.pause():e.wantsToPlay=!1)},continuePlayback:function(e){var t=e.currentTime,r=e.duration;t>=r&&(e.currentTime=t%r),e._rafID=requestAnimationFrame(e._nextFrame.bind(e))}}));t.a=a},function(e,t,r){"use strict";var i=r(4),n=r(36),a=r(1);n.a.register();var o=i.a.create({correspondingPlaybackStyle:a.a.LOOP,photo:i.a.PhotoIngredient.create({display:i.a.computedStyle(function(e){return this.isPlaying||e>0?"none":""})}),video:i.a.VideoIngredient.create({display:i.a.computedStyle(function(e){return""})}),beginFinishingPlaybackEarly:function(e){e.autoplay||(e.isPlaying?e.pause():e.wantsToPlay=!1)},continuePlayback:function(e){var t=e.currentTime,r=e.duration;t>=r&&(e.currentTime=t%r),e._rafID=requestAnimationFrame(e._nextFrame.bind(e))},requestMoreCompatibleRecipe:function(e){return i.a.registerRecipeWithPlaybackStyle(n.a,this.correspondingPlaybackStyle),n.a}});t.a=o},function(e,t,r){"use strict";var i=r(0),n=r(41),a=r(1),o=r(13),s=n.a.extend(o.a,{_loCanvas:null,_hiCanvas:null,backingScaleFactor:1,setUpForRender:function(){var e=this.element,t=(this.isPlaying,this.renderer),r=t.autoplay,n=t.parentView,o=t.playbackStyle,s=t.video;if(!this._loCanvas||!this._hiCanvas){e.innerHTML&&(e.innerHTML="");var u=this._loCanvas=i.a.canvasPool.get(),l=this._hiCanvas=i.a.canvasPool.get();u._context=u.getContext("2d"),l._context=l.getContext("2d"),u.style.cssText=l.style.cssText="position: absolute; left: 0; top: 0; width: 100%; height: 100%; transform: translateZ(0);",e.appendChild(u),e.appendChild(l),this._swapCanvases()}e.className="lpk-render-layer lpk-video",e.style.position="absolute",e.style.transformOrigin="0 0",e.style.zIndex=1,this._super(),o===a.a.LOOP&&(this.shouldLoop=!0),this.shouldLoop&&requestAnimationFrame(function(){s.currentTime=-1,r&&n.play()}),window.test=this},updateSize:function(e,t){if(!arguments.length)return this._super();this._super(e,t);var r=Math.ceil(e*this.backingScaleFactor),i=Math.ceil(t*this.backingScaleFactor);this.backingScaleX=r/e,this.backingScaleY=i/t,this.element.style.width=r+"px",this.element.style.height=i+"px",this._loCanvas&&this._hiCanvas&&(this._loCanvas.width=this._hiCanvas.width=r*devicePixelRatio,this._loCanvas.height=this._hiCanvas.height=i*devicePixelRatio,this._loCanvas._drawnFrameNumber=this._hiCanvas._drawnFrameNumber=-1,this.renderAtTime())},renderAtTime:function(e){if(!arguments.length)return this._super();this._super(e);var t=this.backingScaleX,r=this.backingScaleY;1===t&&1===r||(this.element.style.transform+=" scale3d("+1/t+", "+1/r+", 1)")},renderFramePair:function(e,t,r){(e&&this._hiCanvas._drawnFrameNumber===e.number||t&&this._loCanvas._drawnFrameNumber===t.number)&&this._swapCanvases(),this._putFrameInCanvasIfNeeded(e,this._loCanvas),this._putFrameInCanvasIfNeeded(t,this._hiCanvas),t&&(this._hiCanvas.style.opacity=r)},_swapCanvases:function(){var e=this._hiCanvas;this._hiCanvas=this._loCanvas,this._loCanvas=e,this._loCanvas.style.opacity="",this._loCanvas.style.zIndex=1,this._hiCanvas.style.zIndex=2},_putFrameInCanvasIfNeeded:function(e,t){t._drawnFrameNumber!==(t._drawnFrameNumber=e?e.number:-1)&&(t.setAttribute("data-frame-number",t._drawnFrameNumber.toString()),e?t._context.drawImage(e.image,0,0,t.width,t.height):t._context.clearRect(0,0,t.width,t.height))},dispose:function(){this._super(),this._loCanvas&&i.a.canvasPool.ret(this._loCanvas),this._hiCanvas&&i.a.canvasPool.ret(this._hiCanvas)},tearDownFromRender:function(){var e=this.renderer,t=e.parentView;this.shouldLoop=!1,t&&t.stop(),this._clearAllRetainedFrames(),this._super()}});t.a=s},function(e,t,r){"use strict";var i=r(42),n=r(13),a=r(49),o=i.a.extend(n.a,{tagName:"canvas",get _canvas(){return this.element},get _context(){return this.__context||(this.__context=this._canvas.getContext("2d"))},init:function(){this._super.apply(this,arguments),this.element.className="lpk-render-layer lpk-photo",this.element.style.position="absolute",this.element.style.width=this.element.style.height="100%",this.element.style.transformOrigin="0 0",this.element.style.zIndex=2},tearDownFromRender:function(){this._super(),this._canvas.width=this._canvas.height=0},updateSize:function(e,t){if(!arguments.length)return this._super();this._super(e,t);var i=Math.ceil(e*devicePixelRatio),n=Math.ceil(t*devicePixelRatio),o=this.photo,s=this._canvas;this._lastPhoto===(this._lastPhoto=o)&&s.width===i&&s.height===n||(s.width=i,s.height=n,o&&r.i(a.a)(this._context,o,0,0,i,n))}});t.a=o},function(e,t,r){"use strict";var i=r(0),n=r(2),a=r(13),o=r(43),s=o.a.extend(a.a,{_isPlayingChanged:i.a.observer("isPlaying",function(e){this._video&&(e?(this.duration=1/0,this.play()):this.pause())}),_isVisible:!1,applyStyles:function(){var e=this.element,t=this.video,r=this.videoRotation,i=t.videoHeight,n=t.videoWidth,a=1;[90,270].indexOf(r)>=0&&(a=n/i);var o="\n height: 100%;\n position: absolute;\n width: 100%;\n -moz-transform: scale("+a+") rotate("+r+"deg);\n -webkit-transform: scale("+a+") rotate("+r+"deg);\n -o-transform: scale("+a+") rotate("+r+"deg);\n -ms-transform: scale("+a+") rotate("+r+"deg);\n transform: scale("+a+") rotate("+r+"deg);\n z-index: 1;\n ";e.setAttribute("style",o),e.className="lpk-render-layer lpk-video",t.style.height="100%",t.style.width="100%"},cleanupElement:function(){var e=this.element,t=this.renderer,r=this._video,i=t.parentView;e.innerHtml&&(e.innerHtml=""),r&&(r.loop=!1,r.muted=!1,r.removeEventListener("pause",this.playIfPlaying)),i&&i.stop(),delete this._video},pause:function(){var e=this._isVisible,t=this._video;e&&t.pause()},play:function(){if(this._isVisible){var e=this._video,t=e.play();t?t.catch(this._handlePlayFailure):n.a.isIE||n.a.isEdge||(e.pause(),setTimeout(this._handlePlayFailure))}},_handlePlayFailure:i.a.boundFunction(function(){this.renderer.requestMoreCompatibleRecipe()}),playIfPlaying:i.a.boundFunction(function(){var e=this.isPlaying,t=this._video;if(e&&t.paused){var r=t.play();r&&r.catch(function(){})}}),setUpForRender:function(){var e=this.element,t=(this.isPlaying,this.renderer),r=t.autoplay,i=t.parentView,n=t.video;this.cleanupElement(),e.appendChild(n),this.applyStyles(),n.loop=!0,n.muted=!0,this._video=n,this._isVisible=!0,this._super(),r&&(n.addEventListener("pause",this.playIfPlaying),i.play())},tearDownFromRender:function(){this.cleanupElement(),this._isVisible=!1,this._super()}});t.a=s},function(e,t,r){"use strict";function i(e){e.retain()}function n(e){e.release()}var a=r(0),o=r(7),s=r(17),u=o.a.extend({videoDecoder:a.a.proxyProperty("renderer.videoDecoder"),videoDuration:a.a.proxyProperty("videoDecoder.duration"),canRender:a.a.proxyProperty({readOnly:!0,proxyPath:"videoDecoder.canProvideFrames"}),init:function(){this._super.apply(this,arguments);var e=this.layerName,t=this.recipe;this._framePrepIDKey=t.name+"_"+e+"_framePrepID"},videoTimeAtTime:function(e){return e},_videoTimeAtTime:function(e){return isNaN(e)?e:this.videoTimeAtTime(e)},prepareToRenderAtTime:function(e){var t=this._currentPrepID=++l;if(!this.canRender)return!1;this.prepareVideoFramesFromTime(e);for(var r,i=this._retainedFrames,n=0,a=0;r=i[a];a++)2!==r.readyState&&(r[this._framePrepIDKey]=t,r.onReadyOrFail(this._frameDidPrepare),n++);return this._preppingFrameCount=n,!n},reduceMemoryFootprint:function(){this._super(),this._clearAllRetainedFrames()},_clearAllRetainedFrames:function(){this._clearExtraRetainedFrames(),this._clearRetainedInstantaneousFrames()},_clearExtraRetainedFrames:function(){var e=this._retainedFrames;e&&(e.forEach(n),e.length=0)},_clearRetainedInstantaneousFrames:function(){this._retainedLoFrame&&this._retainedLoFrame.release(),this._retainedHiFrame&&this._retainedHiFrame.release(),this._retainedLoFrame=this._retainedHiFrame=null},_frameDidPrepare:a.a.boundFunction(function(e){e[this._framePrepIDKey]===this._currentPrepID&&(e[this._framePrepIDKey]=void 0,--this._preppingFrameCount||this.renderer.prepareToRenderAtTime(this.renderer.currentTime))}),prepareVideoFramesFromTime:function(e){this.retainFramesForTime(e)},canRenderAtTime:function(e){if("none"===this.display(e))return!0;if(!this.canRender)return!1;for(var t,r=!0,i=this.requiredFramesForTime(e),n=0;t=i[n];n++)r=r&&2===t.readyState,t.retain().release();return r},renderAtTime:function(e){if(!arguments.length)return this._super();if("none"===this.display(e))return this._clearRetainedInstantaneousFrames(),this._super(e);var t=this._videoTimeAtTime(e),r=this.requiredFramesForVideoTime(t),i=r[0]||null,n=r[1]||null;if(i&&i.retain(),n&&n.retain(),this._clearRetainedInstantaneousFrames(),this._retainedLoFrame=i,this._retainedHiFrame=n,i&&(i=i.backingFrame),n&&(n=n.backingFrame),i&&n&&i.number>n.number){var a=i;n=i,i=a}i===n&&(n=null);var o=!i||n?this.videoDecoder.fractionalIndexForTime(t):i.frameNumber,s=o-(0|o);this.renderFramePair(i,n,s),this._super(e)},renderFramePair:function(){},requiredFramesForVideoTime:function(e,t,r){isNaN(t)&&(t=e);var i=this.videoDecoder,n=this.videoDuration,a=i.frameCount,o=d;if(o.length=0,t<0||e>n||isNaN(e)||isNaN(t))return o;var u=Math.max(0,Math.floor(i.fractionalIndexForTime(e))),l=Math.min(i.frameCount,Math.ceil(i.fractionalIndexForTime(t))),c=l=0;l--){var d=u[l],c=d.time;(!o||c>a/2)&&(n(d),u.splice(l,1))}u.push.apply(u,s)},retainFramesForTime:function(e,t,r){return this.retainFramesForVideoTime(this._videoTimeAtTime(e),this._videoTimeAtTime(t),r)},dispose:function(){this.retainFramesForVideoTime(NaN),this._super()}}),l=1,d=[];t.a=u},function(e,t,r){"use strict";var i=r(7),n=r(0),a=i.a.extend({isPlaying:n.a.proxyProperty({readOnly:!0,proxyPath:"renderer.parentView.isPlaying"}),photo:n.a.proxyProperty({readOnly:!0,proxyPath:"renderer.photo"}),canRender:n.a.proxyProperty("photo"),canRenderAtTime:function(e){var t=this.photo;return!("none"!==this.display(e)&&(!t||t instanceof Image&&!t.complete))}});t.a=a},function(e,t,r){"use strict";var i=r(7),n=r(0),a=i.a.extend({canRender:n.a.proxyProperty({readOnly:!0,proxyPath:"video"}),isPlaying:n.a.proxyProperty({readOnly:!0,proxyPath:"renderer.parentView.isPlaying"}),video:n.a.proxyProperty({readOnly:!0,proxyPath:"renderer.video"}),videoRotation:n.a.proxyProperty({readOnly:!0,proxyPath:"renderer.provider.videoRotation"})});t.a=a},function(e,t,r){"use strict";function i(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function n(e){var t=r.i(o.a)(e),i=l.get(t);if(i)return i;var n=e.map(function(e){if("i"===e[0]&&h(e[1]))return"I"+e.substring(1)});return e=e.concat(n.filter(function(e){return!!e})),i=new RegExp(e.join("|"),"g"),l.set(t,i),i}function a(e,t){var r=e.charCodeAt(0),i=t.charCodeAt(0),n=new Map;return function(e){var t=n.get(e);if(void 0!==t)return t;var a=e.charCodeAt(0);return t=a>=r&&a<=i,n.set(e,t),t}}var o=r(20),s=function(){function e(e,t){for(var r=0;r>>1,this.h>>>1)}},{key:"length",value:function(){return this.w*this.h}}])}(),function(){function e(t,r,n){i(this,e),this.bytes=new Uint8Array(t),this.start=r||0,this.pos=this.start,this.end=r+n||this.bytes.length}return s(e,[{key:"readU8Array",value:function(e){if(this.pos>this.end-e)return null;var t=this.bytes.subarray(this.pos,this.pos+e);return this.pos+=e,t}},{key:"readU32Array",value:function(e,t,r){if(t=t||1,this.pos>this.end-e*t*4)return null;if(1===t){for(var i=new Uint32Array(e),n=0;n>24}},{key:"readU8",value:function(){return this.pos>=this.end?null:this.bytes[this.pos++]}},{key:"read16",value:function(){return this.readU16()<<16>>16}},{key:"readU16",value:function(){if(this.pos>=this.end-1)return null;var e=this.bytes[this.pos+0]<<8|this.bytes[this.pos+1];return this.pos+=2,e}},{key:"read24",value:function(){return this.readU24()<<8>>8}},{key:"readU24",value:function(){var e=this.pos,t=this.bytes;if(e>this.end-3)return null;var r=t[e+0]<<16|t[e+1]<<8|t[e+2];return this.pos+=3,r}},{key:"peek32",value:function(e){var t=this.pos,r=this.bytes;if(t>this.end-4)return null;var i=r[t+0]<<24|r[t+1]<<16|r[t+2]<<8|r[t+3];return e&&(this.pos+=4),i}},{key:"read32",value:function(){return this.peek32(!0)}},{key:"readU32",value:function(){return this.peek32(!0)>>>0}},{key:"read4CC",value:function(){var e=this.pos;if(e>this.end-4)return null;for(var t="",r=0;r<4;r++)t+=String.fromCharCode(this.bytes[e+r]);return this.pos+=4,t}},{key:"readFP16",value:function(){return this.read32()/65536}},{key:"readFP8",value:function(){return this.read16()/256}},{key:"readISO639",value:function(){for(var e=this.readU16(),t="",r=0;r<3;r++){var i=e>>>5*(2-r)&31;t+=String.fromCharCode(i+96)}return t}},{key:"readUTF8",value:function(e){for(var t="",r=0;rthis.end)&&a("Index out of bounds (bounds: [0, "+this.end+"], index: "+e+")."),this.pos=e}},{key:"subStream",value:function(t,r){return new e(this.bytes.buffer,t,r)}},{key:"uint",value:function(e){for(var t=this.position,r=t+e,i=0,n=t;n0&&(T.name=e.readUTF8(l));break;case"minf":o.name="Media Information Box",a();break;case"stbl":o.name="Sample Table Box",a();break;case"stsd":var x=o;x.name="Sample Description Box",t(),x.sd=[],e.readU32(),a();break;case"avc1":var S=o;e.reserved(6,0),S.dataReferenceIndex=e.readU16(),n(0==e.readU16()),n(0==e.readU16()),e.readU32(),e.readU32(),e.readU32(),S.width=e.readU16(),S.height=e.readU16(),S.horizontalResolution=e.readFP16(),S.verticalResolution=e.readFP16(),n(0==e.readU32()),S.frameCount=e.readU16(),S.compressorName=e.readPString(32),S.depth=e.readU16(),n(65535==e.readU16()),a();break;case"mp4a":var w=o;if(e.reserved(6,0),w.dataReferenceIndex=e.readU16(),w.version=e.readU16(),0!==w.version){i();break}e.skip(2),e.skip(4),w.channelCount=e.readU16(),w.sampleSize=e.readU16(),w.compressionId=e.readU16(),w.packetSize=e.readU16(),w.sampleRate=e.readU32()>>>16,a();break;case"esds":o.name="Elementary Stream Descriptor",t(),i();break;case"avcC":var O=o;O.name="AVC Configuration Box",O.configurationVersion=e.readU8(),O.avcProfileIndicaation=e.readU8(),O.profileCompatibility=e.readU8(),O.avcLevelIndication=e.readU8(),O.lengthSizeMinusOne=3&e.readU8(),n(3==O.lengthSizeMinusOne,"TODO"),u=31&e.readU8(),O.sps=[];for(var C=0;C=8,"Cannot parse large media data yet."),j.data=e.readU8Array(r());break;case"mebx":o.name="Mebx",a();break;case"meta":o.name="Metadata",a();break;case"keys":var U=o;U.name="Metadata Item Keys",t();var V=U.keyCount=e.read32(),N=U.offset-U.size;U.keyList=new Map;for(var B=1;B<=V;B++){var z=e.read32()-8;z<1||z>N||(e.skip(4),U.keyList.set(e.readUTF8(z),B))}this.metaData.keys=U;break;case"ilst":var H=o;H.name="Metadata Item List",H.items=[];for(var K=H.offset+H.size;e.position0){var s=t[a-1],u=o.firstChunk-s.firstChunk,l=s.samplesPerChunk*u;if(!(e>=l))return{index:i+Math.floor(e/s.samplesPerChunk),offset:e%s.samplesPerChunk};if(e-=l,a===t.length-1)return{index:i+u+Math.floor(e/o.samplesPerChunk),offset:e%o.samplesPerChunk};i+=u}}n(!1)}},{key:"chunkToOffset",value:function(e){return this.trak.mdia.minf.stbl.stco.table[e]}},{key:"sampleToOffset",value:function(e){var t=this.sampleToChunk(e);return this.chunkToOffset(t.index)+this.sampleToSize(e-t.offset,t.offset)}},{key:"timeToSample",value:function(e){for(var t=this.trak.mdia.minf.stbl.stts.table,r=0,i=0;i=n))return r+Math.floor(e/t[i].delta);e-=n,r+=t[i].count}}},{key:"sampleToTime",value:function(e){for(var t=this.trak.mdia.minf.stbl.stts.table,r=0,i=0,n=0;n0;){var a=new u(t.buffer,r).readU32();n.push(t.subarray(r+4,r+a+4)),r=r+a+4}return n}}]),e}()},function(e,t,r){"use strict";var i={staticMembers:{_pool:null,_cache:null,init:function(){this._pool=[],this._cache={},this._super()},getPoolingCacheKey:function(){throw"Must implement `getPoolingCacheKey` to use PoolCaching."},getCached:function(){var e=this.getPoolingCacheKey.apply(this,arguments),t=this._cache[e];return t||(t=this._cache[e]=this._pool.pop()||this.create(),t._poolingCacheKey=e,t.initFromPool.apply(t,arguments)),t},peekCached:function(){var e=this.getPoolingCacheKey.apply(this,arguments);return this._cache[e]||null},_disposeInstance:function(e){delete this._cache[e._poolingCacheKey],e._poolingCacheKey=void 0,e._poolingLifecycleCount=1+(0|e._poolingLifecycleCount),this._pool.push(e)}},dispose:function(){},_poolingCacheKey:null,initFromPool:function(){},_retainCount:0,retain:function(){return this._retainCount++,this},release:function(){return this._retainCount--,this._retainCount||this.dispose(),this}};t.a=i},function(e,t,r){"use strict";function i(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function n(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function a(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}var o=r(19);r.d(t,"a",function(){return d}),r.d(t,"b",function(){return h}),r.d(t,"c",function(){return f}),r.d(t,"d",function(){return y});var s=function(){function e(e,t){for(var r=0;r2?r-2:0),p=2;p=1)return e.drawImage.apply(e,i.apply(h,arguments)),!0;var R=void 0;if(f){R="_cachedSmoothDownsample_from"+g+","+b+","+_+","+P+"@"+F+"x";var L=t[R];if(L)return e.drawImage(L,0,0,L.width,L.height,k,T,x,S),!0}if(v)return e.drawImage.apply(e,i.apply(h,arguments)),!1;var E=1,A=_,I=P,D=Math.max(Math.pow(2,Math.ceil(Math.log(A)/Math.log(2))),a.width),M=Math.max(Math.pow(2,Math.ceil(Math.log(I)/Math.log(2))),a.height);for(a.width===D&&a.height===M||(a.width=s.width=D,a.height=s.height=M),o.drawImage(t,g,b,_,P,0,0,_,P);E>F;){u.drawImage(a,0,0,A,I,0,0,A=Math.ceil(A/2),I=Math.ceil(I/2)),o.clearRect(0,0,A,I);var j=a;a=s,s=j;var U=o;o=u,u=U,E/=2}if(f){var V=document.createElement("canvas");V.width=A,V.height=I,V.getContext("2d").drawImage(a,0,0),t[R]=V}return e.drawImage(a,0,0,A,I,k,T,x,S),o.clearRect(0,0,_,P),u.clearRect(0,0,_,P),!0}};c.usingCache=function(){return l=!0,this},c.avoidingWorkIf=function(e){return d=e,this};var h=[];t.a=c},function(e,t,r){"use strict";function i(){var e="_callbacksForEventHandler"+ ++n;return function(t){var r=this[e]||(this[e]=[]);if("function"==typeof t)return r.push(t);if(r)for(var i=0,n=r.length;ig;return f=f||{},f.width=b?h:p*m,f.height=b?h/m:p,f}function n(e,t,r,n,a){return i(!1,e,t,r,n,a,arguments.length)}t.a=n},function(e,t,r){"use strict";t.a="current"},function(e,t,r){"use strict";t.a="Mcurrent"},function(e,t,r){"use strict";t.a="1.5.6"}])}); +//# sourceMappingURL=resources/livephotoskit.js.map +/* + jquery-qrcode v0.14.0 - https://larsjung.de/jquery-qrcode/ */ +'use strict';let G=null;class H{}H.render=function(w,B){G(w,B)};self.QrCreator=H; +(function(w){function B(t,c,a,e){var b={},h=w(a,c);h.u(t);h.J();e=e||0;var r=h.h(),d=h.h()+2*e;b.text=t;b.level=c;b.version=a;b.O=d;b.a=function(b,a){b-=e;a-=e;return 0>b||b>=r||0>a||a>=r?!1:h.a(b,a)};return b}function C(t,c,a,e,b,h,r,d,g,x){function u(b,a,f,c,d,r,g){b?(t.lineTo(a+r,f+g),t.arcTo(a,f,c,d,h)):t.lineTo(a,f)}r?t.moveTo(c+h,a):t.moveTo(c,a);u(d,e,a,e,b,-h,0);u(g,e,b,c,b,0,-h);u(x,c,b,c,a,h,0);u(r,c,a,e,a,0,h)}function z(t,c,a,e,b,h,r,d,g,x){function u(b,a,c,d){t.moveTo(b+c,a);t.lineTo(b, +a);t.lineTo(b,a+d);t.arcTo(b,a,b+c,a,h)}r&&u(c,a,h,h);d&&u(e,a,-h,h);g&&u(e,b,-h,-h);x&&u(c,b,h,-h)}function A(t,c){var a=c.fill;if("string"===typeof a)t.fillStyle=a;else{var e=a.type,b=a.colorStops;a=a.position.map((b)=>Math.round(b*c.size));if("linear-gradient"===e)var h=t.createLinearGradient.apply(t,a);else if("radial-gradient"===e)h=t.createRadialGradient.apply(t,a);else throw Error("Unsupported fill");b.forEach(([b,a])=>{h.addColorStop(b,a)});t.fillStyle=h}}function y(t,c){a:{var a=c.text,e= +c.v,b=c.N,h=c.K,r=c.P;b=Math.max(1,b||1);for(h=Math.min(40,h||40);b<=h;b+=1)try{var d=B(a,e,b,r);break a}catch(J){}d=void 0}if(!d)return null;a=t.getContext("2d");c.background&&(a.fillStyle=c.background,a.fillRect(c.left,c.top,c.size,c.size));e=d.O;h=c.size/e;a.beginPath();for(r=0;r>>7-b%8&1)},put:function(b,h){for(var a=0;a>>h-a-1&1))},f:function(){return a},m:function(b){var h=Math.floor(a/8);c.length<=h&&c.push(0);b&&(c[h]|=128>>>a%8);a+=1}};return e}function C(c,a){function e(b,h){for(var a=-1;7>=a;a+=1)if(!(-1>=b+a||d<=b+a))for(var c=-1;7>=c;c+=1)-1>=h+c||d<=h+c||(r[b+a][h+c]=0<=a&&6>=a&&(0==c||6==c)||0<=c&&6>=c&&(0==a||6==a)||2<=a&&4>=a&&2<=c&&4>=c?!0:!1)}function b(b,a){for(var f=d=4*c+17,k=Array(f),m=0;m< +f;m+=1){k[m]=Array(f);for(var p=0;p=n;n+=1)for(var l=-2;2>=l;l+=1)r[p+n][q+l]=-2==n||2==n||-2==l||2==l||0==n&&0==l}for(f=8;fk;k+=1)m=!b&&1==(f>>k&1),r[6>k?k:8>k?k+1:d-15+k][8]=m,r[8][8>k?d-k-1:9>k?15-k:14-k]=m;r[d-8][8]=!b;if(7<= +c){f=y.A(c);for(k=0;18>k;k+=1)m=!b&&1==(f>>k&1),r[Math.floor(k/3)][k%3+d-8-3]=m;for(k=0;18>k;k+=1)m=!b&&1==(f>>k&1),r[k%3+d-8-3][Math.floor(k/3)]=m}if(null==g){b=t.I(c,h);f=B();for(k=0;k8*m)throw Error("code length overflow. ("+f.f()+">"+8*m+")");for(f.f()+4<=8*m&&f.put(0,4);0!=f.f()%8;)f.m(!1);for(;!(f.f()>=8*m);){f.put(236,8);if(f.f()>=8*m)break;f.put(17,8)}var u=0;m=k=0;p=Array(b.length); +q=Array(b.length);for(n=0;nn;n+=1)null==r[k][q-n]&&(l=!1,p>>m&1)),a(k,q-n)&&(l=!l),r[k][q-n]=l,--m,-1==m&&(p+=1,m=7));k+=f;if(0>k||d<=k){k-=f;f=-f;break}}}var h=A[a],r=null,d=0,g=null,x=[],u={u:function(b){b=w(b);x.push(b);g=null},a:function(b,a){if(0>b||d<=b||0>a||d<=a)throw Error(b+","+a);return r[b][a]},h:function(){return d},J:function(){for(var a=0,h=0,c=0;8>c;c+=1){b(!0,c);var d=y.D(u);if(0==c||a>d)a=d,h=c}b(!1,h)}};return u} +function z(c,a){if("undefined"==typeof c.length)throw Error(c.length+"/"+a);var e=function(){for(var b=0;bb.b()-a.b())return b;for(var c=v.g(b.c(0))-v.g(a.c(0)),h=Array(b.b()), +g=0;gb?a.push(b):2048>b?a.push(192|b>>6,128|b&63):55296>b||57344<=b?a.push(224|b>>12,128|b>>6&63,128|b&63):(e++,b=65536+((b&1023)<<10|c.charCodeAt(e)&1023),a.push(240|b>>18,128|b>>12&63,128|b>>6&63,128|b&63))}return a};var A={L:1,M:0,Q:3,H:2},y=function(){function c(b){for(var a=0;0!=b;)a+=1,b>>>=1;return a}var a=[[],[6,18], +[6,22],[6,26],[6,30],[6,34],[6,22,38],[6,24,42],[6,26,46],[6,28,50],[6,30,54],[6,32,58],[6,34,62],[6,26,46,66],[6,26,48,70],[6,26,50,74],[6,30,54,78],[6,30,56,82],[6,30,58,86],[6,34,62,90],[6,28,50,72,94],[6,26,50,74,98],[6,30,54,78,102],[6,28,54,80,106],[6,32,58,84,110],[6,30,58,86,114],[6,34,62,90,118],[6,26,50,74,98,122],[6,30,54,78,102,126],[6,26,52,78,104,130],[6,30,56,82,108,134],[6,34,60,86,112,138],[6,30,58,86,114,142],[6,34,62,90,118,146],[6,30,54,78,102,126,150],[6,24,50,76,102,128,154], +[6,28,54,80,106,132,158],[6,32,58,84,110,136,162],[6,26,54,82,110,138,166],[6,30,58,86,114,142,170]],e={w:function(b){for(var a=b<<10;0<=c(a)-c(1335);)a^=1335<a||40a?8:16},D:function(b){for(var a=b.h(),c=0, +d=0;d=p;p+=1)if(!(0>d+p||a<=d+p))for(var q=-1;1>=q;q+=1)0>g+q||a<=g+q||(0!=p||0!=q)&&t==b.a(d+p,g+q)&&(e+=1);5e;e+=1)c[e]=1<e;e+=1)c[e]=c[e-4]^c[e-5]^c[e-6]^c[e-8];for(e=0;255>e;e+=1)a[c[e]]=e;return{g:function(b){if(1>b)throw Error("glog("+b+")");return a[b]},i:function(b){for(;0>b;)b+=255;for(;256<=b;)b-=255;return c[b]}}}(),t=function(){function c(b,c){switch(c){case A.L:return a[4* +(b-1)];case A.M:return a[4*(b-1)+1];case A.Q:return a[4*(b-1)+2];case A.H:return a[4*(b-1)+3]}}var a=[[1,26,19],[1,26,16],[1,26,13],[1,26,9],[1,44,34],[1,44,28],[1,44,22],[1,44,16],[1,70,55],[1,70,44],[2,35,17],[2,35,13],[1,100,80],[2,50,32],[2,50,24],[4,25,9],[1,134,108],[2,67,43],[2,33,15,2,34,16],[2,33,11,2,34,12],[2,86,68],[4,43,27],[4,43,19],[4,43,15],[2,98,78],[4,49,31],[2,32,14,4,33,15],[4,39,13,1,40,14],[2,121,97],[2,60,38,2,61,39],[4,40,18,2,41,19],[4,40,14,2,41,15],[2,146,116],[3,58,36, +2,59,37],[4,36,16,4,37,17],[4,36,12,4,37,13],[2,86,68,2,87,69],[4,69,43,1,70,44],[6,43,19,2,44,20],[6,43,15,2,44,16],[4,101,81],[1,80,50,4,81,51],[4,50,22,4,51,23],[3,36,12,8,37,13],[2,116,92,2,117,93],[6,58,36,2,59,37],[4,46,20,6,47,21],[7,42,14,4,43,15],[4,133,107],[8,59,37,1,60,38],[8,44,20,4,45,21],[12,33,11,4,34,12],[3,145,115,1,146,116],[4,64,40,5,65,41],[11,36,16,5,37,17],[11,36,12,5,37,13],[5,109,87,1,110,88],[5,65,41,5,66,42],[5,54,24,7,55,25],[11,36,12,7,37,13],[5,122,98,1,123,99],[7,73, +45,3,74,46],[15,43,19,2,44,20],[3,45,15,13,46,16],[1,135,107,5,136,108],[10,74,46,1,75,47],[1,50,22,15,51,23],[2,42,14,17,43,15],[5,150,120,1,151,121],[9,69,43,4,70,44],[17,50,22,1,51,23],[2,42,14,19,43,15],[3,141,113,4,142,114],[3,70,44,11,71,45],[17,47,21,4,48,22],[9,39,13,16,40,14],[3,135,107,5,136,108],[3,67,41,13,68,42],[15,54,24,5,55,25],[15,43,15,10,44,16],[4,144,116,4,145,117],[17,68,42],[17,50,22,6,51,23],[19,46,16,6,47,17],[2,139,111,7,140,112],[17,74,46],[7,54,24,16,55,25],[34,37,13],[4, +151,121,5,152,122],[4,75,47,14,76,48],[11,54,24,14,55,25],[16,45,15,14,46,16],[6,147,117,4,148,118],[6,73,45,14,74,46],[11,54,24,16,55,25],[30,46,16,2,47,17],[8,132,106,4,133,107],[8,75,47,13,76,48],[7,54,24,22,55,25],[22,45,15,13,46,16],[10,142,114,2,143,115],[19,74,46,4,75,47],[28,50,22,6,51,23],[33,46,16,4,47,17],[8,152,122,4,153,123],[22,73,45,3,74,46],[8,53,23,26,54,24],[12,45,15,28,46,16],[3,147,117,10,148,118],[3,73,45,23,74,46],[4,54,24,31,55,25],[11,45,15,31,46,16],[7,146,116,7,147,117], +[21,73,45,7,74,46],[1,53,23,37,54,24],[19,45,15,26,46,16],[5,145,115,10,146,116],[19,75,47,10,76,48],[15,54,24,25,55,25],[23,45,15,25,46,16],[13,145,115,3,146,116],[2,74,46,29,75,47],[42,54,24,1,55,25],[23,45,15,28,46,16],[17,145,115],[10,74,46,23,75,47],[10,54,24,35,55,25],[19,45,15,35,46,16],[17,145,115,1,146,116],[14,74,46,21,75,47],[29,54,24,19,55,25],[11,45,15,46,46,16],[13,145,115,6,146,116],[14,74,46,23,75,47],[44,54,24,7,55,25],[59,46,16,1,47,17],[12,151,121,7,152,122],[12,75,47,26,76,48], +[39,54,24,14,55,25],[22,45,15,41,46,16],[6,151,121,14,152,122],[6,75,47,34,76,48],[46,54,24,10,55,25],[2,45,15,64,46,16],[17,152,122,4,153,123],[29,74,46,14,75,47],[49,54,24,10,55,25],[24,45,15,46,46,16],[4,152,122,18,153,123],[13,74,46,32,75,47],[48,54,24,14,55,25],[42,45,15,32,46,16],[20,147,117,4,148,118],[40,75,47,7,76,48],[43,54,24,22,55,25],[10,45,15,67,46,16],[19,148,118,6,149,119],[18,75,47,31,76,48],[34,54,24,34,55,25],[20,45,15,61,46,16]],e={I:function(b,a){var e=c(b,a);if("undefined"== +typeof e)throw Error("bad rs block @ typeNumber:"+b+"/errorCorrectLevel:"+a);b=e.length/3;a=[];for(var d=0;d | BSD-3-Clause */ +!function(){"use strict";var g={not_string:/[^s]/,not_bool:/[^t]/,not_type:/[^T]/,not_primitive:/[^v]/,number:/[diefg]/,numeric_arg:/[bcdiefguxX]/,json:/[j]/,not_json:/[^j]/,text:/^[^\x25]+/,modulo:/^\x25{2}/,placeholder:/^\x25(?:([1-9]\d*)\$|\(([^)]+)\))?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-gijostTuvxX])/,key:/^([a-z_][a-z_\d]*)/i,key_access:/^\.([a-z_][a-z_\d]*)/i,index_access:/^\[(\d+)\]/,sign:/^[+-]/};function y(e){return function(e,t){var r,n,i,s,a,o,p,c,l,u=1,f=e.length,d="";for(n=0;n>>0).toString(8);break;case"s":r=String(r),r=s.precision?r.substring(0,s.precision):r;break;case"t":r=String(!!r),r=s.precision?r.substring(0,s.precision):r;break;case"T":r=Object.prototype.toString.call(r).slice(8,-1).toLowerCase(),r=s.precision?r.substring(0,s.precision):r;break;case"u":r=parseInt(r,10)>>>0;break;case"v":r=r.valueOf(),r=s.precision?r.substring(0,s.precision):r;break;case"x":r=(parseInt(r,10)>>>0).toString(16);break;case"X":r=(parseInt(r,10)>>>0).toString(16).toUpperCase()}g.json.test(s.type)?d+=r:(!g.number.test(s.type)||c&&!s.sign?l="":(l=c?"+":"-",r=r.toString().replace(g.sign,"")),o=s.pad_char?"0"===s.pad_char?"0":s.pad_char.charAt(1):" ",p=s.width-(l+r).length,a=s.width&&0>E;if(l[B+3]=Y,0!==Y){var Z=255/Y;l[B]=(O*C>>E)*Z,l[B+1]=(P*C>>E)*Z,l[B+2]=(q*C>>E)*Z}else l[B]=l[B+1]=l[B+2]=0;O-=k,P-=H,q-=_,z-=M,k-=p.r,H-=p.g,_-=p.b,M-=p.a;var $=X+f+1;$=w+($>E,ut>0?(ut=255/ut,l[Nt]=(bt*C>>E)*ut,l[Nt+1]=(xt*C>>E)*ut,l[Nt+2]=(dt*C>>E)*ut):l[Nt]=l[Nt+1]=l[Nt+2]=0,bt-=lt,xt-=ct,dt-=st,yt-=vt,lt-=p.r,ct-=p.g,st-=p.b,vt-=p.a,Nt=ot+((Nt=St+b)>E,l[S+1]=k*C>>E,l[S+2]=H*C>>E,W-=T,k-=j,H-=A,T-=w.r,j-=w.g,A-=w.b,p=I+((p=z+f+1)>E,l[p+1]=Y*C>>E,l[p+2]=Z*C>>E,X-=Q,Y-=U,Z-=V,Q-=w.r,U-=w.g,V-=w.b,p=F+((p=ot+b)​', + className: "leaflet-marker-photo", + }, + photo, + this.options.icon + ) + ), + title: photo.caption || "", + }); + marker.photo = photo; + return marker; + }, +}); + +L.photo = function (photos, options) { + return new L.Photo(photos, options); +}; + +if (L.MarkerClusterGroup) { + L.Photo.Cluster = L.MarkerClusterGroup.extend({ + options: { + featureGroup: L.photo, + maxClusterRadius: 100, + showCoverageOnHover: false, + iconCreateFunction: function (cluster) { + return new L.DivIcon( + L.extend( + { + className: "leaflet-marker-photo", + html: + '
' + + cluster.getChildCount() + + "", + }, + this.icon + ) + ); + }, + icon: { + iconSize: [40, 40], + }, + }, + + initialize: function (options) { + options = L.Util.setOptions(this, options); + L.MarkerClusterGroup.prototype.initialize.call(this); + this._photos = options.featureGroup(null, options); + }, + + add: function (photos) { + this.addLayer(this._photos.addLayers(photos)); + return this; + }, + + clear: function () { + this._photos.clearLayers(); + this.clearLayers(); + }, + }); + + L.photo.cluster = function (options) { + return new L.Photo.Cluster(options); + }; +} + +!function(t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).basicContext=t()}(function(){return function o(i,c,l){function r(n,t){if(!c[n]){if(!i[n]){var e="function"==typeof require&&require;if(!t&&e)return e(n,!0);if(a)return a(n,!0);throw(t=new Error("Cannot find module '"+n+"'")).code="MODULE_NOT_FOUND",t}e=c[n]={exports:{}},i[n][0].call(e.exports,function(t){return r(i[n][1][t]||t)},e,e.exports,o,i,c,l)}return c[n].exports}for(var a="function"==typeof require&&require,t=0;t")),t.type===i?e="\n\t\t\t\t \n\t\t\t\t\t\t ").concat(o).concat(t.title,"\n\t\t\t\t \n\t\t\t\t "):t.type===c&&(e="\n\t\t\t\t \n\t\t\t\t "),e)},v=function(){var t=0\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t",t.forEach(function(t,n){return i+=x(t,n)});var i,c,l,r,a,s,u,f,d=i+="\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\n\t\t\t\t",d=(document.body.insertAdjacentHTML("beforeend",d),b()),p=(l=d,c=v(c=n),r=c.x,a=c.y,s=document.querySelector(".basicContextContainer"),p=s.offsetWidth,s=s.offsetHeight,u=l.offsetWidth,f=l.offsetHeight,p} arr + * @param {*} key + * @returns {number} + */ + function getIndex(arr, key) { + var result = -1; + arr.some(function (entry, index) { + if (entry[0] === key) { + result = index; + return true; + } + return false; + }); + return result; + } + return /** @class */ (function () { + function class_1() { + this.__entries__ = []; + } + Object.defineProperty(class_1.prototype, "size", { + /** + * @returns {boolean} + */ + get: function () { + return this.__entries__.length; + }, + enumerable: true, + configurable: true + }); + /** + * @param {*} key + * @returns {*} + */ + class_1.prototype.get = function (key) { + var index = getIndex(this.__entries__, key); + var entry = this.__entries__[index]; + return entry && entry[1]; + }; + /** + * @param {*} key + * @param {*} value + * @returns {void} + */ + class_1.prototype.set = function (key, value) { + var index = getIndex(this.__entries__, key); + if (~index) { + this.__entries__[index][1] = value; + } + else { + this.__entries__.push([key, value]); + } + }; + /** + * @param {*} key + * @returns {void} + */ + class_1.prototype.delete = function (key) { + var entries = this.__entries__; + var index = getIndex(entries, key); + if (~index) { + entries.splice(index, 1); + } + }; + /** + * @param {*} key + * @returns {void} + */ + class_1.prototype.has = function (key) { + return !!~getIndex(this.__entries__, key); + }; + /** + * @returns {void} + */ + class_1.prototype.clear = function () { + this.__entries__.splice(0); + }; + /** + * @param {Function} callback + * @param {*} [ctx=null] + * @returns {void} + */ + class_1.prototype.forEach = function (callback, ctx) { + if (ctx === void 0) { ctx = null; } + for (var _i = 0, _a = this.__entries__; _i < _a.length; _i++) { + var entry = _a[_i]; + callback.call(ctx, entry[1], entry[0]); + } + }; + return class_1; + }()); + })(); + + /** + * Detects whether window and document objects are available in current environment. + */ + var isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined' && window.document === document; + + // Returns global object of a current environment. + var global$1 = (function () { + if (typeof global !== 'undefined' && global.Math === Math) { + return global; + } + if (typeof self !== 'undefined' && self.Math === Math) { + return self; + } + if (typeof window !== 'undefined' && window.Math === Math) { + return window; + } + // eslint-disable-next-line no-new-func + return Function('return this')(); + })(); + + /** + * A shim for the requestAnimationFrame which falls back to the setTimeout if + * first one is not supported. + * + * @returns {number} Requests' identifier. + */ + var requestAnimationFrame$1 = (function () { + if (typeof requestAnimationFrame === 'function') { + // It's required to use a bounded function because IE sometimes throws + // an "Invalid calling object" error if rAF is invoked without the global + // object on the left hand side. + return requestAnimationFrame.bind(global$1); + } + return function (callback) { return setTimeout(function () { return callback(Date.now()); }, 1000 / 60); }; + })(); + + // Defines minimum timeout before adding a trailing call. + var trailingTimeout = 2; + /** + * Creates a wrapper function which ensures that provided callback will be + * invoked only once during the specified delay period. + * + * @param {Function} callback - Function to be invoked after the delay period. + * @param {number} delay - Delay after which to invoke callback. + * @returns {Function} + */ + function throttle (callback, delay) { + var leadingCall = false, trailingCall = false, lastCallTime = 0; + /** + * Invokes the original callback function and schedules new invocation if + * the "proxy" was called during current request. + * + * @returns {void} + */ + function resolvePending() { + if (leadingCall) { + leadingCall = false; + callback(); + } + if (trailingCall) { + proxy(); + } + } + /** + * Callback invoked after the specified delay. It will further postpone + * invocation of the original function delegating it to the + * requestAnimationFrame. + * + * @returns {void} + */ + function timeoutCallback() { + requestAnimationFrame$1(resolvePending); + } + /** + * Schedules invocation of the original function. + * + * @returns {void} + */ + function proxy() { + var timeStamp = Date.now(); + if (leadingCall) { + // Reject immediately following calls. + if (timeStamp - lastCallTime < trailingTimeout) { + return; + } + // Schedule new call to be in invoked when the pending one is resolved. + // This is important for "transitions" which never actually start + // immediately so there is a chance that we might miss one if change + // happens amids the pending invocation. + trailingCall = true; + } + else { + leadingCall = true; + trailingCall = false; + setTimeout(timeoutCallback, delay); + } + lastCallTime = timeStamp; + } + return proxy; + } + + // Minimum delay before invoking the update of observers. + var REFRESH_DELAY = 20; + // A list of substrings of CSS properties used to find transition events that + // might affect dimensions of observed elements. + var transitionKeys = ['top', 'right', 'bottom', 'left', 'width', 'height', 'size', 'weight']; + // Check if MutationObserver is available. + var mutationObserverSupported = typeof MutationObserver !== 'undefined'; + /** + * Singleton controller class which handles updates of ResizeObserver instances. + */ + var ResizeObserverController = /** @class */ (function () { + /** + * Creates a new instance of ResizeObserverController. + * + * @private + */ + function ResizeObserverController() { + /** + * Indicates whether DOM listeners have been added. + * + * @private {boolean} + */ + this.connected_ = false; + /** + * Tells that controller has subscribed for Mutation Events. + * + * @private {boolean} + */ + this.mutationEventsAdded_ = false; + /** + * Keeps reference to the instance of MutationObserver. + * + * @private {MutationObserver} + */ + this.mutationsObserver_ = null; + /** + * A list of connected observers. + * + * @private {Array} + */ + this.observers_ = []; + this.onTransitionEnd_ = this.onTransitionEnd_.bind(this); + this.refresh = throttle(this.refresh.bind(this), REFRESH_DELAY); + } + /** + * Adds observer to observers list. + * + * @param {ResizeObserverSPI} observer - Observer to be added. + * @returns {void} + */ + ResizeObserverController.prototype.addObserver = function (observer) { + if (!~this.observers_.indexOf(observer)) { + this.observers_.push(observer); + } + // Add listeners if they haven't been added yet. + if (!this.connected_) { + this.connect_(); + } + }; + /** + * Removes observer from observers list. + * + * @param {ResizeObserverSPI} observer - Observer to be removed. + * @returns {void} + */ + ResizeObserverController.prototype.removeObserver = function (observer) { + var observers = this.observers_; + var index = observers.indexOf(observer); + // Remove observer if it's present in registry. + if (~index) { + observers.splice(index, 1); + } + // Remove listeners if controller has no connected observers. + if (!observers.length && this.connected_) { + this.disconnect_(); + } + }; + /** + * Invokes the update of observers. It will continue running updates insofar + * it detects changes. + * + * @returns {void} + */ + ResizeObserverController.prototype.refresh = function () { + var changesDetected = this.updateObservers_(); + // Continue running updates if changes have been detected as there might + // be future ones caused by CSS transitions. + if (changesDetected) { + this.refresh(); + } + }; + /** + * Updates every observer from observers list and notifies them of queued + * entries. + * + * @private + * @returns {boolean} Returns "true" if any observer has detected changes in + * dimensions of it's elements. + */ + ResizeObserverController.prototype.updateObservers_ = function () { + // Collect observers that have active observations. + var activeObservers = this.observers_.filter(function (observer) { + return observer.gatherActive(), observer.hasActive(); + }); + // Deliver notifications in a separate cycle in order to avoid any + // collisions between observers, e.g. when multiple instances of + // ResizeObserver are tracking the same element and the callback of one + // of them changes content dimensions of the observed target. Sometimes + // this may result in notifications being blocked for the rest of observers. + activeObservers.forEach(function (observer) { return observer.broadcastActive(); }); + return activeObservers.length > 0; + }; + /** + * Initializes DOM listeners. + * + * @private + * @returns {void} + */ + ResizeObserverController.prototype.connect_ = function () { + // Do nothing if running in a non-browser environment or if listeners + // have been already added. + if (!isBrowser || this.connected_) { + return; + } + // Subscription to the "Transitionend" event is used as a workaround for + // delayed transitions. This way it's possible to capture at least the + // final state of an element. + document.addEventListener('transitionend', this.onTransitionEnd_); + window.addEventListener('resize', this.refresh); + if (mutationObserverSupported) { + this.mutationsObserver_ = new MutationObserver(this.refresh); + this.mutationsObserver_.observe(document, { + attributes: true, + childList: true, + characterData: true, + subtree: true + }); + } + else { + document.addEventListener('DOMSubtreeModified', this.refresh); + this.mutationEventsAdded_ = true; + } + this.connected_ = true; + }; + /** + * Removes DOM listeners. + * + * @private + * @returns {void} + */ + ResizeObserverController.prototype.disconnect_ = function () { + // Do nothing if running in a non-browser environment or if listeners + // have been already removed. + if (!isBrowser || !this.connected_) { + return; + } + document.removeEventListener('transitionend', this.onTransitionEnd_); + window.removeEventListener('resize', this.refresh); + if (this.mutationsObserver_) { + this.mutationsObserver_.disconnect(); + } + if (this.mutationEventsAdded_) { + document.removeEventListener('DOMSubtreeModified', this.refresh); + } + this.mutationsObserver_ = null; + this.mutationEventsAdded_ = false; + this.connected_ = false; + }; + /** + * "Transitionend" event handler. + * + * @private + * @param {TransitionEvent} event + * @returns {void} + */ + ResizeObserverController.prototype.onTransitionEnd_ = function (_a) { + var _b = _a.propertyName, propertyName = _b === void 0 ? '' : _b; + // Detect whether transition may affect dimensions of an element. + var isReflowProperty = transitionKeys.some(function (key) { + return !!~propertyName.indexOf(key); + }); + if (isReflowProperty) { + this.refresh(); + } + }; + /** + * Returns instance of the ResizeObserverController. + * + * @returns {ResizeObserverController} + */ + ResizeObserverController.getInstance = function () { + if (!this.instance_) { + this.instance_ = new ResizeObserverController(); + } + return this.instance_; + }; + /** + * Holds reference to the controller's instance. + * + * @private {ResizeObserverController} + */ + ResizeObserverController.instance_ = null; + return ResizeObserverController; + }()); + + /** + * Defines non-writable/enumerable properties of the provided target object. + * + * @param {Object} target - Object for which to define properties. + * @param {Object} props - Properties to be defined. + * @returns {Object} Target object. + */ + var defineConfigurable = (function (target, props) { + for (var _i = 0, _a = Object.keys(props); _i < _a.length; _i++) { + var key = _a[_i]; + Object.defineProperty(target, key, { + value: props[key], + enumerable: false, + writable: false, + configurable: true + }); + } + return target; + }); + + /** + * Returns the global object associated with provided element. + * + * @param {Object} target + * @returns {Object} + */ + var getWindowOf = (function (target) { + // Assume that the element is an instance of Node, which means that it + // has the "ownerDocument" property from which we can retrieve a + // corresponding global object. + var ownerGlobal = target && target.ownerDocument && target.ownerDocument.defaultView; + // Return the local global object if it's not possible extract one from + // provided element. + return ownerGlobal || global$1; + }); + + // Placeholder of an empty content rectangle. + var emptyRect = createRectInit(0, 0, 0, 0); + /** + * Converts provided string to a number. + * + * @param {number|string} value + * @returns {number} + */ + function toFloat(value) { + return parseFloat(value) || 0; + } + /** + * Extracts borders size from provided styles. + * + * @param {CSSStyleDeclaration} styles + * @param {...string} positions - Borders positions (top, right, ...) + * @returns {number} + */ + function getBordersSize(styles) { + var positions = []; + for (var _i = 1; _i < arguments.length; _i++) { + positions[_i - 1] = arguments[_i]; + } + return positions.reduce(function (size, position) { + var value = styles['border-' + position + '-width']; + return size + toFloat(value); + }, 0); + } + /** + * Extracts paddings sizes from provided styles. + * + * @param {CSSStyleDeclaration} styles + * @returns {Object} Paddings box. + */ + function getPaddings(styles) { + var positions = ['top', 'right', 'bottom', 'left']; + var paddings = {}; + for (var _i = 0, positions_1 = positions; _i < positions_1.length; _i++) { + var position = positions_1[_i]; + var value = styles['padding-' + position]; + paddings[position] = toFloat(value); + } + return paddings; + } + /** + * Calculates content rectangle of provided SVG element. + * + * @param {SVGGraphicsElement} target - Element content rectangle of which needs + * to be calculated. + * @returns {DOMRectInit} + */ + function getSVGContentRect(target) { + var bbox = target.getBBox(); + return createRectInit(0, 0, bbox.width, bbox.height); + } + /** + * Calculates content rectangle of provided HTMLElement. + * + * @param {HTMLElement} target - Element for which to calculate the content rectangle. + * @returns {DOMRectInit} + */ + function getHTMLElementContentRect(target) { + // Client width & height properties can't be + // used exclusively as they provide rounded values. + var clientWidth = target.clientWidth, clientHeight = target.clientHeight; + // By this condition we can catch all non-replaced inline, hidden and + // detached elements. Though elements with width & height properties less + // than 0.5 will be discarded as well. + // + // Without it we would need to implement separate methods for each of + // those cases and it's not possible to perform a precise and performance + // effective test for hidden elements. E.g. even jQuery's ':visible' filter + // gives wrong results for elements with width & height less than 0.5. + if (!clientWidth && !clientHeight) { + return emptyRect; + } + var styles = getWindowOf(target).getComputedStyle(target); + var paddings = getPaddings(styles); + var horizPad = paddings.left + paddings.right; + var vertPad = paddings.top + paddings.bottom; + // Computed styles of width & height are being used because they are the + // only dimensions available to JS that contain non-rounded values. It could + // be possible to utilize the getBoundingClientRect if only it's data wasn't + // affected by CSS transformations let alone paddings, borders and scroll bars. + var width = toFloat(styles.width), height = toFloat(styles.height); + // Width & height include paddings and borders when the 'border-box' box + // model is applied (except for IE). + if (styles.boxSizing === 'border-box') { + // Following conditions are required to handle Internet Explorer which + // doesn't include paddings and borders to computed CSS dimensions. + // + // We can say that if CSS dimensions + paddings are equal to the "client" + // properties then it's either IE, and thus we don't need to subtract + // anything, or an element merely doesn't have paddings/borders styles. + if (Math.round(width + horizPad) !== clientWidth) { + width -= getBordersSize(styles, 'left', 'right') + horizPad; + } + if (Math.round(height + vertPad) !== clientHeight) { + height -= getBordersSize(styles, 'top', 'bottom') + vertPad; + } + } + // Following steps can't be applied to the document's root element as its + // client[Width/Height] properties represent viewport area of the window. + // Besides, it's as well not necessary as the itself neither has + // rendered scroll bars nor it can be clipped. + if (!isDocumentElement(target)) { + // In some browsers (only in Firefox, actually) CSS width & height + // include scroll bars size which can be removed at this step as scroll + // bars are the only difference between rounded dimensions + paddings + // and "client" properties, though that is not always true in Chrome. + var vertScrollbar = Math.round(width + horizPad) - clientWidth; + var horizScrollbar = Math.round(height + vertPad) - clientHeight; + // Chrome has a rather weird rounding of "client" properties. + // E.g. for an element with content width of 314.2px it sometimes gives + // the client width of 315px and for the width of 314.7px it may give + // 314px. And it doesn't happen all the time. So just ignore this delta + // as a non-relevant. + if (Math.abs(vertScrollbar) !== 1) { + width -= vertScrollbar; + } + if (Math.abs(horizScrollbar) !== 1) { + height -= horizScrollbar; + } + } + return createRectInit(paddings.left, paddings.top, width, height); + } + /** + * Checks whether provided element is an instance of the SVGGraphicsElement. + * + * @param {Element} target - Element to be checked. + * @returns {boolean} + */ + var isSVGGraphicsElement = (function () { + // Some browsers, namely IE and Edge, don't have the SVGGraphicsElement + // interface. + if (typeof SVGGraphicsElement !== 'undefined') { + return function (target) { return target instanceof getWindowOf(target).SVGGraphicsElement; }; + } + // If it's so, then check that element is at least an instance of the + // SVGElement and that it has the "getBBox" method. + // eslint-disable-next-line no-extra-parens + return function (target) { return (target instanceof getWindowOf(target).SVGElement && + typeof target.getBBox === 'function'); }; + })(); + /** + * Checks whether provided element is a document element (). + * + * @param {Element} target - Element to be checked. + * @returns {boolean} + */ + function isDocumentElement(target) { + return target === getWindowOf(target).document.documentElement; + } + /** + * Calculates an appropriate content rectangle for provided html or svg element. + * + * @param {Element} target - Element content rectangle of which needs to be calculated. + * @returns {DOMRectInit} + */ + function getContentRect(target) { + if (!isBrowser) { + return emptyRect; + } + if (isSVGGraphicsElement(target)) { + return getSVGContentRect(target); + } + return getHTMLElementContentRect(target); + } + /** + * Creates rectangle with an interface of the DOMRectReadOnly. + * Spec: https://drafts.fxtf.org/geometry/#domrectreadonly + * + * @param {DOMRectInit} rectInit - Object with rectangle's x/y coordinates and dimensions. + * @returns {DOMRectReadOnly} + */ + function createReadOnlyRect(_a) { + var x = _a.x, y = _a.y, width = _a.width, height = _a.height; + // If DOMRectReadOnly is available use it as a prototype for the rectangle. + var Constr = typeof DOMRectReadOnly !== 'undefined' ? DOMRectReadOnly : Object; + var rect = Object.create(Constr.prototype); + // Rectangle's properties are not writable and non-enumerable. + defineConfigurable(rect, { + x: x, y: y, width: width, height: height, + top: y, + right: x + width, + bottom: height + y, + left: x + }); + return rect; + } + /** + * Creates DOMRectInit object based on the provided dimensions and the x/y coordinates. + * Spec: https://drafts.fxtf.org/geometry/#dictdef-domrectinit + * + * @param {number} x - X coordinate. + * @param {number} y - Y coordinate. + * @param {number} width - Rectangle's width. + * @param {number} height - Rectangle's height. + * @returns {DOMRectInit} + */ + function createRectInit(x, y, width, height) { + return { x: x, y: y, width: width, height: height }; + } + + /** + * Class that is responsible for computations of the content rectangle of + * provided DOM element and for keeping track of it's changes. + */ + var ResizeObservation = /** @class */ (function () { + /** + * Creates an instance of ResizeObservation. + * + * @param {Element} target - Element to be observed. + */ + function ResizeObservation(target) { + /** + * Broadcasted width of content rectangle. + * + * @type {number} + */ + this.broadcastWidth = 0; + /** + * Broadcasted height of content rectangle. + * + * @type {number} + */ + this.broadcastHeight = 0; + /** + * Reference to the last observed content rectangle. + * + * @private {DOMRectInit} + */ + this.contentRect_ = createRectInit(0, 0, 0, 0); + this.target = target; + } + /** + * Updates content rectangle and tells whether it's width or height properties + * have changed since the last broadcast. + * + * @returns {boolean} + */ + ResizeObservation.prototype.isActive = function () { + var rect = getContentRect(this.target); + this.contentRect_ = rect; + return (rect.width !== this.broadcastWidth || + rect.height !== this.broadcastHeight); + }; + /** + * Updates 'broadcastWidth' and 'broadcastHeight' properties with a data + * from the corresponding properties of the last observed content rectangle. + * + * @returns {DOMRectInit} Last observed content rectangle. + */ + ResizeObservation.prototype.broadcastRect = function () { + var rect = this.contentRect_; + this.broadcastWidth = rect.width; + this.broadcastHeight = rect.height; + return rect; + }; + return ResizeObservation; + }()); + + var ResizeObserverEntry = /** @class */ (function () { + /** + * Creates an instance of ResizeObserverEntry. + * + * @param {Element} target - Element that is being observed. + * @param {DOMRectInit} rectInit - Data of the element's content rectangle. + */ + function ResizeObserverEntry(target, rectInit) { + var contentRect = createReadOnlyRect(rectInit); + // According to the specification following properties are not writable + // and are also not enumerable in the native implementation. + // + // Property accessors are not being used as they'd require to define a + // private WeakMap storage which may cause memory leaks in browsers that + // don't support this type of collections. + defineConfigurable(this, { target: target, contentRect: contentRect }); + } + return ResizeObserverEntry; + }()); + + var ResizeObserverSPI = /** @class */ (function () { + /** + * Creates a new instance of ResizeObserver. + * + * @param {ResizeObserverCallback} callback - Callback function that is invoked + * when one of the observed elements changes it's content dimensions. + * @param {ResizeObserverController} controller - Controller instance which + * is responsible for the updates of observer. + * @param {ResizeObserver} callbackCtx - Reference to the public + * ResizeObserver instance which will be passed to callback function. + */ + function ResizeObserverSPI(callback, controller, callbackCtx) { + /** + * Collection of resize observations that have detected changes in dimensions + * of elements. + * + * @private {Array} + */ + this.activeObservations_ = []; + /** + * Registry of the ResizeObservation instances. + * + * @private {Map} + */ + this.observations_ = new MapShim(); + if (typeof callback !== 'function') { + throw new TypeError('The callback provided as parameter 1 is not a function.'); + } + this.callback_ = callback; + this.controller_ = controller; + this.callbackCtx_ = callbackCtx; + } + /** + * Starts observing provided element. + * + * @param {Element} target - Element to be observed. + * @returns {void} + */ + ResizeObserverSPI.prototype.observe = function (target) { + if (!arguments.length) { + throw new TypeError('1 argument required, but only 0 present.'); + } + // Do nothing if current environment doesn't have the Element interface. + if (typeof Element === 'undefined' || !(Element instanceof Object)) { + return; + } + if (!(target instanceof getWindowOf(target).Element)) { + throw new TypeError('parameter 1 is not of type "Element".'); + } + var observations = this.observations_; + // Do nothing if element is already being observed. + if (observations.has(target)) { + return; + } + observations.set(target, new ResizeObservation(target)); + this.controller_.addObserver(this); + // Force the update of observations. + this.controller_.refresh(); + }; + /** + * Stops observing provided element. + * + * @param {Element} target - Element to stop observing. + * @returns {void} + */ + ResizeObserverSPI.prototype.unobserve = function (target) { + if (!arguments.length) { + throw new TypeError('1 argument required, but only 0 present.'); + } + // Do nothing if current environment doesn't have the Element interface. + if (typeof Element === 'undefined' || !(Element instanceof Object)) { + return; + } + if (!(target instanceof getWindowOf(target).Element)) { + throw new TypeError('parameter 1 is not of type "Element".'); + } + var observations = this.observations_; + // Do nothing if element is not being observed. + if (!observations.has(target)) { + return; + } + observations.delete(target); + if (!observations.size) { + this.controller_.removeObserver(this); + } + }; + /** + * Stops observing all elements. + * + * @returns {void} + */ + ResizeObserverSPI.prototype.disconnect = function () { + this.clearActive(); + this.observations_.clear(); + this.controller_.removeObserver(this); + }; + /** + * Collects observation instances the associated element of which has changed + * it's content rectangle. + * + * @returns {void} + */ + ResizeObserverSPI.prototype.gatherActive = function () { + var _this = this; + this.clearActive(); + this.observations_.forEach(function (observation) { + if (observation.isActive()) { + _this.activeObservations_.push(observation); + } + }); + }; + /** + * Invokes initial callback function with a list of ResizeObserverEntry + * instances collected from active resize observations. + * + * @returns {void} + */ + ResizeObserverSPI.prototype.broadcastActive = function () { + // Do nothing if observer doesn't have active observations. + if (!this.hasActive()) { + return; + } + var ctx = this.callbackCtx_; + // Create ResizeObserverEntry instance for every active observation. + var entries = this.activeObservations_.map(function (observation) { + return new ResizeObserverEntry(observation.target, observation.broadcastRect()); + }); + this.callback_.call(ctx, entries, ctx); + this.clearActive(); + }; + /** + * Clears the collection of active observations. + * + * @returns {void} + */ + ResizeObserverSPI.prototype.clearActive = function () { + this.activeObservations_.splice(0); + }; + /** + * Tells whether observer has active observations. + * + * @returns {boolean} + */ + ResizeObserverSPI.prototype.hasActive = function () { + return this.activeObservations_.length > 0; + }; + return ResizeObserverSPI; + }()); + + // Registry of internal observers. If WeakMap is not available use current shim + // for the Map collection as it has all required methods and because WeakMap + // can't be fully polyfilled anyway. + var observers = typeof WeakMap !== 'undefined' ? new WeakMap() : new MapShim(); + /** + * ResizeObserver API. Encapsulates the ResizeObserver SPI implementation + * exposing only those methods and properties that are defined in the spec. + */ + var ResizeObserver = /** @class */ (function () { + /** + * Creates a new instance of ResizeObserver. + * + * @param {ResizeObserverCallback} callback - Callback that is invoked when + * dimensions of the observed elements change. + */ + function ResizeObserver(callback) { + if (!(this instanceof ResizeObserver)) { + throw new TypeError('Cannot call a class as a function.'); + } + if (!arguments.length) { + throw new TypeError('1 argument required, but only 0 present.'); + } + var controller = ResizeObserverController.getInstance(); + var observer = new ResizeObserverSPI(callback, controller, this); + observers.set(this, observer); + } + return ResizeObserver; + }()); + // Expose public methods of ResizeObserver. + [ + 'observe', + 'unobserve', + 'disconnect' + ].forEach(function (method) { + ResizeObserver.prototype[method] = function () { + var _a; + return (_a = observers.get(this))[method].apply(_a, arguments); + }; + }); + + var index = (function () { + // Export existing implementation if available. + if (typeof global$1.ResizeObserver !== 'undefined') { + return global$1.ResizeObserver; + } + return ResizeObserver; + })(); + + return index; + +}))); + +"use strict";var _templateObject,_templateObject2,_templateObject3,_templateObject4,_templateObject5,_templateObject6,_templateObject7,_templateObject8,_templateObject9,_templateObject10,_templateObject11,_templateObject12,_templateObject13,_templateObject14,_templateObject15,_templateObject16,_templateObject17,_templateObject18,_templateObject19,_templateObject20,_templateObject21,_templateObject22,_templateObject23,_templateObject24,_templateObject25,_templateObject26,_templateObject27,_templateObject28,_templateObject29,_templateObject30,_templateObject31,_templateObject32,_templateObject33,_templateObject34,_templateObject35,_templateObject36,_templateObject37,_templateObject38,_templateObject39,_templateObject40,_templateObject41,_templateObject42,_templateObject43,_templateObject44,_templateObject45,_templateObject46,_templateObject47,_templateObject48,_templateObject49,_templateObject50,_templateObject51,_templateObject52,_templateObject53,_templateObject54,_templateObject55,_templateObject56,_templateObject57,_templateObject58,_templateObject59,_templateObject60,_templateObject61,_templateObject62,_templateObject63,_templateObject64,_templateObject65,_templateObject66,_templateObject67,_templateObject68,_templateObject69,_class;function _toConsumableArray(arr){return _arrayWithoutHoles(arr)||_iterableToArray(arr)||_unsupportedIterableToArray(arr)||_nonIterableSpread();}function _nonIterableSpread(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");}function _iterableToArray(iter){if(typeof Symbol!=="undefined"&&iter[Symbol.iterator]!=null||iter["@@iterator"]!=null)return Array.from(iter);}function _arrayWithoutHoles(arr){if(Array.isArray(arr))return _arrayLikeToArray(arr);}function ownKeys(e,r){var t=Object.keys(e);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);r&&(o=o.filter(function(r){return Object.getOwnPropertyDescriptor(e,r).enumerable;})),t.push.apply(t,o);}return t;}function _objectSpread(e){for(var r=1;r=0;--o){var i=this.tryEntries[o],a=i.completion;if("root"===i.tryLoc)return handle("end");if(i.tryLoc<=this.prev){var c=n.call(i,"catchLoc"),u=n.call(i,"finallyLoc");if(c&&u){if(this.prev=0;--r){var o=this.tryEntries[r];if(o.tryLoc<=this.prev&&n.call(o,"finallyLoc")&&this.prev=0;--e){var r=this.tryEntries[e];if(r.finallyLoc===t)return this.complete(r.completion,r.afterLoc),resetTryEntry(r),y;}},"catch":function _catch(t){for(var e=this.tryEntries.length-1;e>=0;--e){var r=this.tryEntries[e];if(r.tryLoc===t){var n=r.completion;if("throw"===n.type){var o=n.arg;resetTryEntry(r);}return o;}}throw new Error("illegal catch attempt");},delegateYield:function delegateYield(e,r,n){return this.delegate={iterator:values(e),resultName:r,nextLoc:n},"next"===this.method&&(this.arg=t),y;}},e;}function asyncGeneratorStep(gen,resolve,reject,_next,_throw,key,arg){try{var info=gen[key](arg);var value=info.value;}catch(error){reject(error);return;}if(info.done){resolve(value);}else{Promise.resolve(value).then(_next,_throw);}}function _asyncToGenerator(fn){return function(){var self=this,args=arguments;return new Promise(function(resolve,reject){var gen=fn.apply(self,args);function _next(value){asyncGeneratorStep(gen,resolve,reject,_next,_throw,"next",value);}function _throw(err){asyncGeneratorStep(gen,resolve,reject,_next,_throw,"throw",err);}_next(undefined);});};}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor)){throw new TypeError("Cannot call a class as a function");}}function _defineProperties(target,props){for(var i=0;i]+)>/g,function(e,r){var t=o[r];return"$"+(Array.isArray(t)?t.join("$"):t);}));}if("function"==typeof p){var i=this;return e[Symbol.replace].call(this,t,function(){var e=arguments;return"object"!=_typeof(e[e.length-1])&&(e=[].slice.call(e)).push(buildGroups(e,i)),p.apply(this,e);});}return e[Symbol.replace].call(this,t,p);},_wrapRegExp.apply(this,arguments);}function _inherits(subClass,superClass){if(typeof superClass!=="function"&&superClass!==null){throw new TypeError("Super expression must either be null or a function");}subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,writable:true,configurable:true}});Object.defineProperty(subClass,"prototype",{writable:false});if(superClass)_setPrototypeOf(subClass,superClass);}function _setPrototypeOf(o,p){_setPrototypeOf=Object.setPrototypeOf?Object.setPrototypeOf.bind():function _setPrototypeOf(o,p){o.__proto__=p;return o;};return _setPrototypeOf(o,p);}function _typeof(o){"@babel/helpers - typeof";return _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(o){return typeof o;}:function(o){return o&&"function"==typeof Symbol&&o.constructor===Symbol&&o!==Symbol.prototype?"symbol":typeof o;},_typeof(o);}function _taggedTemplateLiteral(strings,raw){if(!raw){raw=strings.slice(0);}return Object.freeze(Object.defineProperties(strings,{raw:{value:Object.freeze(raw)}}));}function _slicedToArray(arr,i){return _arrayWithHoles(arr)||_iterableToArrayLimit(arr,i)||_unsupportedIterableToArray(arr,i)||_nonIterableRest();}function _nonIterableRest(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");}function _unsupportedIterableToArray(o,minLen){if(!o)return;if(typeof o==="string")return _arrayLikeToArray(o,minLen);var n=Object.prototype.toString.call(o).slice(8,-1);if(n==="Object"&&o.constructor)n=o.constructor.name;if(n==="Map"||n==="Set")return Array.from(o);if(n==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return _arrayLikeToArray(o,minLen);}function _arrayLikeToArray(arr,len){if(len==null||len>arr.length)len=arr.length;for(var i=0,arr2=new Array(len);i2&&arguments[2]!==undefined?arguments[2]:null;var responseProgressCB=arguments.length>3&&arguments[3]!==undefined?arguments[3]:null;var errorCallback=arguments.length>4&&arguments[4]!==undefined?arguments[4]:null;loadingBar.show();/** + * The success handler + * @param {Object} data the decoded JSON object of the response + */var successHandler=function successHandler(data){setTimeout(loadingBar.hide,100);if(successCallback)successCallback(data);};/** + * The error handler + * @param {XMLHttpRequest} jqXHR the jQuery XMLHttpRequest object, see {@link https://api.jquery.com/jQuery.ajax/#jqXHR}. + */var errorHandler=function errorHandler(jqXHR){/** + * @type {?LycheeException} + */var lycheeException=jqXHR.responseJSON;if(errorCallback){var isHandled=errorCallback(jqXHR,params,lycheeException);if(isHandled){setTimeout(loadingBar.hide,100);return;}}// Call global error handler for unhandled errors +api.onError(jqXHR,params,lycheeException);};var ajaxParams={type:"POST",url:"api/"+fn,contentType:"application/json",data:JSON.stringify(params),dataType:"json",headers:{"X-XSRF-TOKEN":csrf.getCSRFCookieValue()},success:successHandler,error:errorHandler};if(responseProgressCB!==null){ajaxParams.xhrFields={onprogress:responseProgressCB};}$.ajax(ajaxParams);};/** + * Given a URL return the text raw content of the file. + * + * @param {string} url + * @param {APISuccessCB} callback + * @returns {void} + */api.getRawContent=function(url,callback){loadingBar.show();/** + * The success handler + * @param {Object} data the decoded JSON object of the response + */var successHandler=function successHandler(data){setTimeout(loadingBar.hide,100);callback(data);};/** + * The error handler + * @param {XMLHttpRequest} jqXHR the jQuery XMLHttpRequest object, see {@link https://api.jquery.com/jQuery.ajax/#jqXHR}. + */var errorHandler=function errorHandler(jqXHR){api.onError(jqXHR,{},null);};$.ajax({type:"GET",url:url,data:{},dataType:"text",headers:{"X-XSRF-TOKEN":csrf.getCSRFCookieValue()},success:successHandler,error:errorHandler});};var csrf={};/** + * Returns the value of the CSRF token. + * + * Inspired by https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie#example_2_get_a_sample_cookie_named_test2 + * + * @returns {?string} + */csrf.getCSRFCookieValue=function(){var cookie=document.cookie.split(";").find(function(row){return /^\s*(X-)?[XC]SRF-TOKEN\s*=/.test(row);});// We must remove all '%3D' from the end of the string. +// Background: +// The actual binary value of the CSFR value is encoded in Base64. +// If the length of original, binary value is not a multiple of 3 bytes, +// the encoding gets padded with `=` on the right; i.e. there might be +// zero, one or two `=` at the end of the encoded value. +// If the value is sent from the server to the client as part of a cookie, +// the `=` character is URL-encoded as `%3D`, because `=` is already used +// to separate a cookie key from its value. +// When we send back the value to the server as part of an AJAX request, +// Laravel expects an unpadded value. +// Hence, we must remove the `%3D`. +return cookie?cookie.split("=")[1].trim().replace(/%3D/g,""):null;};(function($){var Swipe=function Swipe(el){var self=this;this.el=$(el);this.pos={start:{x:0,y:0},end:{x:0,y:0}};this.startTime=null;el.on("touchstart",function(e){self.touchStart(e);});el.on("touchmove",function(e){self.touchMove(e);});el.on("touchend",function(){self.swipeEnd();});el.on("mousedown",function(e){self.mouseDown(e);});};Swipe.prototype={touchStart:function touchStart(e){var touch=e.originalEvent.touches[0];this.swipeStart(e,touch.pageX,touch.pageY);},touchMove:function touchMove(e){var touch=e.originalEvent.touches[0];this.swipeMove(e,touch.pageX,touch.pageY);},mouseDown:function mouseDown(e){var self=this;this.swipeStart(e,e.pageX,e.pageY);this.el.on("mousemove",function(_e){self.mouseMove(_e);});this.el.on("mouseup",function(){self.mouseUp();});},mouseMove:function mouseMove(e){this.swipeMove(e,e.pageX,e.pageY);},mouseUp:function mouseUp(e){this.swipeEnd(e);this.el.off("mousemove");this.el.off("mouseup");},swipeStart:function swipeStart(e,x,y){this.pos.start.x=x;this.pos.start.y=y;this.pos.end.x=x;this.pos.end.y=y;this.startTime=new Date().getTime();this.trigger("swipeStart",e);},swipeMove:function swipeMove(e,x,y){this.pos.end.x=x;this.pos.end.y=y;this.trigger("swipeMove",e);},swipeEnd:function swipeEnd(e){this.trigger("swipeEnd",e);},trigger:function trigger(e,originalEvent){var self=this;var event=$.Event(e),x=self.pos.start.x-self.pos.end.x,y=self.pos.end.y-self.pos.start.y,radians=Math.atan2(y,x),direction="up",distance=Math.round(Math.sqrt(Math.pow(x,2)+Math.pow(y,2))),angle=Math.round(radians*180/Math.PI),speed=Math.round(distance/(new Date().getTime()-self.startTime)*1000);if(angle<0){angle=360-Math.abs(angle);}if(angle<=45&&angle>=0||angle<=360&&angle>=315){direction="left";}else if(angle>=135&&angle<=225){direction="right";}else if(angle>45&&angle<135){direction="down";}event.originalEvent=originalEvent;event.swipe={x:x,y:y,direction:direction,distance:distance,angle:angle,speed:speed};$(self.el).trigger(event);}};$.fn.swipe=function(){// let swipe = new Swipe(this); +new Swipe(this);return this;};})(jQuery);/** + * @description Takes care of every action an album can handle and execute. + */var album={/** @type {(?Album|?TagAlbum|?SearchAlbum)} */json:null};/** + * @param {?string} id + * @returns {boolean} + */album.isSmartID=function(id){return id===SmartAlbumID.UNSORTED||id===SmartAlbumID.STARRED||id===SmartAlbumID.PUBLIC||id===SmartAlbumID.RECENT||id===SmartAlbumID.ON_THIS_DAY;};/** + * @param {?string} id + * @returns {boolean} + */album.isSearchID=function(id){return id!==null&&(id===SearchAlbumIDPrefix||id.startsWith(SearchAlbumIDPrefix+"/"));};/** + * @param {?string} id + * @returns {boolean} + */album.isModelID=function(id){return typeof id==="string"&&/^[-_0-9a-zA-Z]{24}$/.test(id);};/** + * @returns {?string} + */album.getParentID=function(){if(album.json===null||album.isSmartID(album.json.id)||album.isSearchID(album.json.id)||!album.json.parent_id){return null;}return album.json.parent_id;};/** + * @returns {?string} the album ID + */album.getID=function(){/** @type {?string} */var id=null;/** @param {?string} _id */var isID=function isID(_id){return album.isSmartID(_id)||album.isSearchID(_id)||album.isModelID(_id);};if(_photo3.json)id=_photo3.json.album_id;else if(album.json)id=album.json.id;else if(mapview.albumID)id=mapview.albumID;if(isID(id)===false){var active=$(".album:hover, .album.active");if(active.length===1){id=active.attr("data-id")||null;}}if(isID(id)===false){var _active=$(".photo:hover, .photo.active");if(_active.length===1){id=_active.attr("data-album-id")||null;}}if(isID(id)===true)return id;else return null;};/** + * @returns {boolean} + */album.isTagAlbum=function(){return album.json&&album.json.is_tag_album&&album.json.is_tag_album===true;};/** + * @param {?string} photoID + * @returns {?Photo} the photo model + */album.getByID=function(photoID){if(photoID==null||!album.json||!album.json.photos){loadingBar.show("error","Error: Album json not found !");return null;}var i=0;while(i1&&arguments[1]!==undefined?arguments[1]:null;var parentID=arguments.length>2&&arguments[2]!==undefined?arguments[2]:null;/** + * @param {Album} data + */var processAlbum=function processAlbum(data){album.json=data;if(parentID!==null){// Used with search so that the back button sends back to the +// search results. +album.json.original_parent_id=album.json.parent_id;album.json.parent_id=parentID;}if(albumLoadedCB===null){lychee.animate(lychee.content,"contentZoomOut");}var waitTime=300;// Skip delay when we have a callback `albumLoadedCB` +// Skip delay when opening a blank Lychee +if(albumLoadedCB)waitTime=0;if(!visible.albums()&&!visible.photo()&&!visible.album())waitTime=0;setTimeout(function(){view.album.init();if(albumLoadedCB===null){lychee.animate(lychee.content,"contentZoomIn");header.setMode("album");}tabindex.makeFocusable(lychee.content);if(lychee.active_focus_on_page_load){// Put focus on first element - either album or photo +var first_album=$(".album:first");if(first_album.length!==0){first_album.focus();}else{var first_photo=$(".photo:first");if(first_photo.length!==0){first_photo.focus();}}}},waitTime);};/** + * @param {Album} data + */var successHandler=function successHandler(data){processAlbum(data);tabindex.makeFocusable(lychee.content);if(lychee.active_focus_on_page_load){// Put focus on first element - either album or photo +var first_album=$(".album:first");if(first_album.length!==0){first_album.focus();}else{var first_photo=$(".photo:first");if(first_photo.length!==0){first_photo.focus();}}}if(albumLoadedCB)albumLoadedCB(true);};/** + * @param {XMLHttpRequest} jqXHR + * @param {Object} params the original JSON parameters of the request + * @param {?LycheeException} lycheeException the Lychee exception + * @returns {boolean} + */var errorHandler=function errorHandler(jqXHR,params,lycheeException){if(jqXHR.status!==401&&jqXHR.status!==403){// Any other error then unauthenticated or unauthorized +// shall be handled by the global error handler. +return false;}if(lycheeException.exception.endsWith("PasswordRequiredException")){// If a password is required, then try to unlock the album +// and in case of success, try again to load album with same +// parameters +password.getDialog(albumID,function(){albums.refresh();album.load(albumID,albumLoadedCB);});return true;}else if(lycheeException.exception.endsWith("UnauthenticatedException")&&!albumLoadedCB){// If no password is required, but we still get a 401 error +// try to properly log in as a user +// We only try this, if `albumLoadedCB` is not set. +// This is not optimal, but the best we can do without too much +// refactoring for now. +// `albumLoadedCB` is set, if the user directly jumps to a photo +// in an album via a direct link. +// Even though the album might be private, the photo could still +// be visible. +// If we caught users for a direct link to a public photo +// within a private album, we would "trap" the users in a login +// dialog which they cannot pass by. +lychee.loginDialog();return true;}else if(albumLoadedCB){// In case we could not successfully load and unlock the album, +// but we have a callback, we call that and consider the error +// handled. +// Note: This case occurs for a single public photo on an +// otherwise non-public album. +album.json=null;albumLoadedCB(false);return true;}else{// In any other case, let the global error handler deal with the +// problem. +return false;}};api.post("Album::get",{albumID:albumID},successHandler,null,errorHandler);};/** + * Creates a new album. + * + * The method optionally calls the provided callback after the new album + * has been created and passes the ID of the newly created album plus the + * provided `IDs`. + * + * Actually, the callback should enclose all additional parameter it needs. + * The parameter `IDs` is not needed by this method itself. + * TODO: Refactor callbacks. + * Also see comments for {@link TargetAlbumSelectedCB} and + * {@link contextMenu.move}. + * + * @param {string[]} [IDs=null] some IDs which are passed on to the callback + * @param {TargetAlbumSelectedCB} [callback=null] called upon successful creation of the album + * + * @returns {void} + */album.add=function(){var IDs=arguments.length>0&&arguments[0]!==undefined?arguments[0]:null;var callback=arguments.length>1&&arguments[1]!==undefined?arguments[1]:null;/** + * @param {{title: string}} data + * @returns {void} + */var action=function action(data){if(!data.title.trim()){basicModal.focusError("title");return;}basicModal.close();var params={title:data.title,parent_id:null};if(visible.albums()||album.isSmartID(album.json.id)||album.isSearchID(album.json.id)){params.parent_id=null;}else if(visible.album()){params.parent_id=album.json.id;}else if(visible.photo()){params.parent_id=_photo3.json.album_id;}api.post("Album::add",params,/** @param {Album} _data */function(_data){if(IDs!=null&&callback!=null){callback(IDs,_data.id,false);// we do not confirm +}else{albums.refresh();lychee["goto"](_data.id);}});};/** + * @param {ModalDialogFormElements} formElements + * @param {HTMLDivElement} dialog + * @returns {void} + */var initAddAlbumDialog=function initAddAlbumDialog(formElements,dialog){dialog.querySelector("p").textContent=lychee.locale["TITLE_NEW_ALBUM"];formElements.title.placeholder="Title";formElements.title.value=lychee.locale["UNTITLED"];};var addAlbumDialogBody="\n\t\t

\n\t\t
\n\t\t\t
\n\t\t
\n\t";basicModal.show({body:addAlbumDialogBody,readyCB:initAddAlbumDialog,buttons:{action:{title:lychee.locale["CREATE_ALBUM"],fn:action},cancel:{title:lychee.locale["CANCEL"],fn:basicModal.close}}});};/** + * @returns {void} + */album.addByTags=function(){/** @param {{title: string, tags: string}} data */var action=function action(data){if(!data.title.trim()){basicModal.focusError("title");return;}if(!data.tags.trim()){basicModal.focusError("tags");return;}basicModal.close();api.post("Album::addByTags",{title:data.title,tags:data.tags.split(",")},/** @param {TagAlbum} _data */function(_data){albums.refresh();lychee["goto"](_data.id);});};var addTagAlbumDialogBody="\n\t\t

\n\t\t
\n\t\t\t
\n\t\t\t
\n\t\t
";/** + * @param {ModalDialogFormElements} formElements + * @param {HTMLDivElement} dialog + * @returns {void} + */var initAddTagAlbumDialog=function initAddTagAlbumDialog(formElements,dialog){dialog.querySelector("p").textContent=lychee.locale["TITLE_NEW_ALBUM"];formElements.title.placeholder="Title";formElements.title.value=lychee.locale["UNTITLED"];formElements.tags.placeholder="Tags";};basicModal.show({body:addTagAlbumDialogBody,readyCB:initAddTagAlbumDialog,buttons:{action:{title:lychee.locale["CREATE_TAG_ALBUM"],fn:action},cancel:{title:lychee.locale["CANCEL"],fn:basicModal.close}}});};/** + * @param {string} albumID + * @returns {void} + */album.setShowTags=function(albumID){/** @param {{show_tags: string}} data */var action=function action(data){if(!data.show_tags.trim()){basicModal.focusError("show_tags");return;}var new_show_tags=data.show_tags.split(",").map(function(tag){return tag.trim();}).filter(function(tag){return tag!==""&&tag.indexOf(",")===-1;}).sort();basicModal.close();if(visible.album()){album.json.show_tags=new_show_tags;view.album.show_tags();}api.post("Album::setShowTags",{albumID:albumID,show_tags:new_show_tags},function(){return album.reload();});};var setShowTagDialogBody="\n\t\t

\n\t\t
\n\t\t\t
\n\t\t
";/** + * @param {ModalDialogFormElements} formElements + * @param {HTMLDivElement} dialog + * @returns {void} + */var initShowTagAlbumDialog=function initShowTagAlbumDialog(formElements,dialog){dialog.querySelector("p").textContent=lychee.locale["ALBUM_NEW_SHOWTAGS"];formElements.show_tags.placeholder="Tags";formElements.show_tags.value=album.json.show_tags.sort().join(", ");};basicModal.show({body:setShowTagDialogBody,readyCB:initShowTagAlbumDialog,buttons:{action:{title:lychee.locale["ALBUM_SET_SHOWTAGS"],fn:action},cancel:{title:lychee.locale["CANCEL"],fn:basicModal.close}}});};/** + * + * @param {string[]} albumIDs + * @returns {boolean} + */album.setTitle=function(albumIDs){var oldTitle="";if(albumIDs.length===1){// Get old title if only one album is selected +if(album.json){if(album.getID()===albumIDs[0]){oldTitle=album.json.title;}else oldTitle=album.getSubByID(albumIDs[0]).title;}if(!oldTitle){var a=albums.getByID(albumIDs[0]);if(a)oldTitle=a.title;}}/** @param {{title: string}} data */var action=function action(data){if(!data.title.trim()){basicModal.focusError("title");return;}basicModal.close();var newTitle=data.title;if(visible.album()){if(albumIDs.length===1&&album.getID()===albumIDs[0]){// Rename only one album +album.json.title=newTitle;view.album.title();var _a=albums.getByID(albumIDs[0]);if(_a)_a.title=newTitle;}else{albumIDs.forEach(function(id){album.getSubByID(id).title=newTitle;view.album.content.titleSub(id);var a=albums.getByID(id);if(a)a.title=newTitle;});}}else if(visible.albums()){// Rename all albums +albumIDs.forEach(function(id){var a=albums.getByID(id);if(a)a.title=newTitle;view.albums.content.title(id);});}api.post("Album::setTitle",{albumIDs:albumIDs,title:newTitle});};var setAlbumTitleDialogBody="\n\t\t

\n\t\t
\n\t\t\t
\n\t\t
";/** + * @param {ModalDialogFormElements} formElements + * @param {HTMLDivElement} dialog + * @returns {void} + */var initSetAlbumTitleDialog=function initSetAlbumTitleDialog(formElements,dialog){dialog.querySelector("p").textContent=albumIDs.length===1?lychee.locale["ALBUM_NEW_TITLE"]:sprintf(lychee.locale["ALBUMS_NEW_TITLE"],albumIDs.length);formElements.title.placeholder=lychee.locale["ALBUM_TITLE"];formElements.title.value=oldTitle;};basicModal.show({body:setAlbumTitleDialogBody,readyCB:initSetAlbumTitleDialog,buttons:{action:{title:lychee.locale["ALBUM_SET_TITLE"],fn:action},cancel:{title:lychee.locale["CANCEL"],fn:basicModal.close}}});};/** + * @param {string} albumID + * @returns {void} + */album.setDescription=function(albumID){/** @param {{description: string}} data */var action=function action(data){var description=data.description?data.description:null;basicModal.close();if(visible.album()){album.json.description=description;view.album.description();}api.post("Album::setDescription",{albumID:albumID,description:description});};var setAlbumDescriptionDialogBody="\n\t\t

\n\t\t
\n\t\t\t
\n\t\t
";/** + * @param {ModalDialogFormElements} formElements + * @param {HTMLDivElement} dialog + * @returns {void} + */var initSetAlbumDescriptionDialog=function initSetAlbumDescriptionDialog(formElements,dialog){dialog.querySelector("p").textContent=lychee.locale["ALBUM_NEW_DESCRIPTION"];formElements.description.placeholder=lychee.locale["ALBUM_DESCRIPTION"];formElements.description.value=album.json.description?album.json.description:"";};basicModal.show({body:setAlbumDescriptionDialogBody,readyCB:initSetAlbumDescriptionDialog,buttons:{action:{title:lychee.locale["ALBUM_SET_DESCRIPTION"],fn:action},cancel:{title:lychee.locale["CANCEL"],fn:basicModal.close}}});};/** + * @param {string} photoID + * @returns {void} + */album.toggleCover=function(photoID){album.json.cover_id=album.json.cover_id===photoID?null:photoID;var params={albumID:album.json.id,photoID:album.json.cover_id};api.post("Album::setCover",params,function(){view.album.content.cover(photoID);if(!album.getParentID()){albums.refresh();}});};/** + * @param {string} albumID + * @returns {void} + */album.setLicense=function(albumID){/** @param {{license: string}} data */var action=function action(data){basicModal.close();api.post("Album::setLicense",{albumID:albumID,license:data.license},function(){if(visible.album()){album.json.license=data.license;view.album.license();}});};var setAlbumLicenseDialogBody="\n\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t
\n\t\t\t\t

\n\t\t\t
\n\t\t
";/** + * @param {ModalDialogFormElements} formElements + * @param {HTMLDivElement} dialog + * @returns {void} + */var initSetAlbumLicenseDialog=function initSetAlbumLicenseDialog(formElements,dialog){dialog.querySelector("label").textContent=lychee.locale["ALBUM_LICENSE"];formElements.license.item(0).textContent=lychee.locale["ALBUM_LICENSE_NONE"];formElements.license.item(1).textContent=lychee.locale["ALBUM_RESERVED"];formElements.license.value=album.json.license===""?"none":album.json.license;dialog.querySelector("p a").textContent=lychee.locale["ALBUM_LICENSE_HELP"];};basicModal.show({body:setAlbumLicenseDialogBody,readyCB:initSetAlbumLicenseDialog,buttons:{action:{title:lychee.locale["ALBUM_SET_LICENSE"],fn:action},cancel:{title:lychee.locale["CANCEL"],fn:basicModal.close}}});};/** + * @param {string} albumID + * @returns {void} + */album.setSorting=function(albumID){/** @param {{sorting_col: string, sorting_order: string}} data */var action=function action(data){basicModal.close();api.post("Album::setSorting",{albumID:albumID,sorting_column:data.sorting_col,sorting_order:data.sorting_order},function(){if(visible.album()){album.reload();}});};var setAlbumSortingDialogBody="\n\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t
\n\t\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t
\n\t\t\t
\n\t\t
";/** + * @param {ModalDialogFormElements} formElements + * @param {HTMLDivElement} dialog + * @returns {void} + */var initSetAlbumSortingDialog=function initSetAlbumSortingDialog(formElements,dialog){formElements.sorting_col.parentElement.previousElementSibling.textContent=lychee.locale["SORT_DIALOG_ATTRIBUTE_LABEL"];formElements.sorting_col.item(1).textContent=lychee.locale["SORT_PHOTO_SELECT_1"];formElements.sorting_col.item(2).textContent=lychee.locale["SORT_PHOTO_SELECT_2"];formElements.sorting_col.item(3).textContent=lychee.locale["SORT_PHOTO_SELECT_3"];formElements.sorting_col.item(4).textContent=lychee.locale["SORT_PHOTO_SELECT_4"];formElements.sorting_col.item(5).textContent=lychee.locale["SORT_PHOTO_SELECT_5"];formElements.sorting_col.item(6).textContent=lychee.locale["SORT_PHOTO_SELECT_6"];formElements.sorting_col.item(7).textContent=lychee.locale["SORT_PHOTO_SELECT_7"];formElements.sorting_order.parentElement.previousElementSibling.textContent=lychee.locale["SORT_DIALOG_ORDER_LABEL"];formElements.sorting_order.item(1).textContent=lychee.locale["SORT_ASCENDING"];formElements.sorting_order.item(2).textContent=lychee.locale["SORT_DESCENDING"];if(album.json.sorting){formElements.sorting_col.value=album.json.sorting.column;formElements.sorting_order.value=album.json.sorting.order;}else{formElements.sorting_col.value="";formElements.sorting_order.value="";}};basicModal.show({body:setAlbumSortingDialogBody,readyCB:initSetAlbumSortingDialog,buttons:{action:{title:lychee.locale["ALBUM_SET_ORDER"],fn:action},cancel:{title:lychee.locale["CANCEL"],fn:basicModal.close}}});};/** + * Sets the accessibility attributes of an album. + * + * @param {string} albumID + * @returns {void} + */album.setProtectionPolicy=function(albumID){/** + * @param {ModalDialogResult} data + */var action=function action(data){basicModal.close();albums.refresh();album.json.policy.is_nsfw=data.is_nsfw;album.json.policy.is_public=data.is_public;album.json.policy.grants_full_photo_access=data.grants_full_photo_access;album.json.policy.is_link_required=data.is_link_required;album.json.policy.grants_download=data.grants_download;album.json.policy.is_password_required=data.is_password_required;// Set data and refresh view +if(visible.album()){view.album.nsfw();view.album["public"]();view.album.requiresLink();view.album.downloadable();view.album.password();}var params={albumID:albumID,grants_full_photo_access:album.json.policy.grants_full_photo_access,is_public:album.json.policy.is_public,is_nsfw:album.json.policy.is_nsfw,is_link_required:album.json.policy.is_link_required,grants_download:album.json.policy.grants_download};if(album.json.policy.is_password_required){if(data.password){// We send the password only if there's been a change; that way the +// server will keep the current password if it wasn't changed. +params.password=data.password;}}else{params.password=null;}api.post("Album::setProtectionPolicy",params);};var setAlbumProtectionPolicyBody="\n\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t

\n\t\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t

\n\t\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t

\n\t\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t

\n\t\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t

\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t
\n\t\t
\n\t\t
\n\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t

\n\t\t\t
\n\t\t
";/** + * @typedef ProtectionPolicyDialogFormElements + * @property {HTMLInputElement} is_public + * @property {HTMLInputElement} grants_full_photo_access + * @property {HTMLInputElement} is_link_required + * @property {HTMLInputElement} grants_download + * @property {HTMLInputElement} is_password_required + * @property {HTMLInputElement} password + * @property {HTMLInputElement} is_nsfw + */ /** + * @param {ProtectionPolicyDialogFormElements} formElements + * @param {HTMLDivElement} dialog + * @returns {void} + */var initAlbumProtectionPolicyDialog=function initAlbumProtectionPolicyDialog(formElements,dialog){formElements.is_public.previousElementSibling.textContent=lychee.locale["ALBUM_PUBLIC"];formElements.is_public.nextElementSibling.textContent=lychee.locale["ALBUM_PUBLIC_EXPL"];formElements.grants_full_photo_access.previousElementSibling.textContent=lychee.locale["ALBUM_FULL"];formElements.grants_full_photo_access.nextElementSibling.textContent=lychee.locale["ALBUM_FULL_EXPL"];formElements.is_link_required.previousElementSibling.textContent=lychee.locale["ALBUM_HIDDEN"];formElements.is_link_required.nextElementSibling.textContent=lychee.locale["ALBUM_HIDDEN_EXPL"];formElements.grants_download.previousElementSibling.textContent=lychee.locale["ALBUM_DOWNLOADABLE"];formElements.grants_download.nextElementSibling.textContent=lychee.locale["ALBUM_DOWNLOADABLE_EXPL"];formElements.is_password_required.previousElementSibling.textContent=lychee.locale["ALBUM_PASSWORD_PROT"];formElements.is_password_required.nextElementSibling.textContent=lychee.locale["ALBUM_PASSWORD_PROT_EXPL"];formElements.password.placeholder=lychee.locale["PASSWORD"];formElements.is_nsfw.previousElementSibling.textContent=lychee.locale["ALBUM_NSFW"];formElements.is_nsfw.nextElementSibling.textContent=lychee.locale["ALBUM_NSFW_EXPL"];formElements.is_public.checked=album.json.is_public;formElements.is_nsfw.checked=album.json.is_nsfw;/** + * Array of checkboxes which are enable/disabled wrt. the state of `is_public` + * @type {HTMLInputElement[]} + */var tristateCheckboxes=[formElements.grants_full_photo_access,formElements.is_link_required,formElements.grants_download,formElements.is_password_required];formElements.is_public.checked=album.json.policy.is_public;if(album.json.policy.is_public){tristateCheckboxes.forEach(function(checkbox){checkbox.parentElement.classList.remove("disabled");checkbox.disabled=false;});// Initialize options based on album settings. +formElements.grants_full_photo_access.checked=album.json.policy.grants_full_photo_access;formElements.is_link_required.checked=album.json.policy.is_link_required;formElements.grants_download.checked=album.json.policy.grants_download;formElements.is_password_required.checked=album.json.policy.is_password_required;if(album.json.policy.is_password_required){formElements.password.parentElement.classList.remove("hidden");}else{formElements.password.parentElement.classList.add("hidden");}}else{tristateCheckboxes.forEach(function(checkbox){checkbox.parentElement.classList.add("disabled");checkbox.disabled=true;});// Initialize options based on global settings. +formElements.grants_full_photo_access.checked=lychee.grants_full_photo_access;formElements.is_link_required.checked=false;formElements.grants_download.checked=lychee.grants_download;formElements.is_password_required.checked=false;formElements.password.parentElement.classList.add("hidden");}formElements.is_public.addEventListener("change",function(){tristateCheckboxes.forEach(function(checkbox){checkbox.parentElement.classList.toggle("disabled");checkbox.disabled=!formElements.is_public.checked;});});formElements.is_password_required.addEventListener("change",function(){if(formElements.is_password_required.checked){formElements.password.parentElement.classList.remove("hidden");formElements.password.focus();}else{formElements.password.parentElement.classList.add("hidden");}});};basicModal.show({body:setAlbumProtectionPolicyBody,readyCB:initAlbumProtectionPolicyDialog,buttons:{action:{title:lychee.locale["SAVE"],fn:action},cancel:{title:lychee.locale["CANCEL"],fn:basicModal.close}}});};/** + * Lets a user update the sharing settings of an album. + * + * @param {string} albumID + * @returns {void} + */album.shareUsers=function(albumID){/** + * @param {ModalDialogResult} data + */var action=function action(data){basicModal.close();/** @type {number[]} */var selectedUserIds=Object.entries(data).filter(function(_ref){var _ref2=_slicedToArray(_ref,2),userId=_ref2[0],isChecked=_ref2[1];return isChecked;}).map(function(_ref3){var _ref4=_slicedToArray(_ref3,2),userId=_ref4[0],isChecked=_ref4[1];return parseInt(userId,10);});api.post("Sharing::setByAlbum",{albumID:albumID,userIDs:selectedUserIds});};/** + * @param {ModalDialogFormElements} formElements + * @param {HTMLDivElement} dialog + * @returns {void} + */var initSharingDialog=function initSharingDialog(formElements,dialog){/** @type {HTMLParagraphElement} */var p=dialog.querySelector("p");p.textContent=lychee.locale["WAIT_FETCH_DATA"];/** @param {SharingInfo} data */var successCallback=function successCallback(data){if(data.users.length===0){p.textContent=lychee.locale["SHARING_ALBUM_USERS_NO_USERS"];return;}p.textContent=lychee.locale["SHARING_ALBUM_USERS_LONG_MESSAGE"];/** @type {HTMLFormElement} */var form=document.createElement("form");var existingShares=new Set(data.shared.map(function(value){return value.user_id;}));// Create a list with one checkbox per user +data.users.forEach(function(user){var div=form.appendChild(document.createElement("div"));div.classList.add("input-group","compact-inverse");var label=div.appendChild(document.createElement("label"));label.htmlFor="share_dialog_user_"+user.id;label.textContent=user.username;var input=div.appendChild(document.createElement("input"));input.type="checkbox";input.id=label.htmlFor;input.name=user.id.toString();input.checked=existingShares.has(user.id);});// Append the pre-constructed form to the dialog after the paragraph +dialog.appendChild(form);basicModal.cacheFormElements();};api.post("Sharing::list",{albumID:albumID},successCallback);};basicModal.show({body:"

",readyCB:initSharingDialog,buttons:{action:{title:lychee.locale["SAVE"],fn:action},cancel:{title:lychee.locale["CANCEL"],fn:basicModal.close}}});};/** + * Toggles the NSFW attribute of the currently loaded album. + * + * @returns {void} + */album.toggleNSFW=function(){album.json.policy.is_nsfw=!album.json.policy.is_nsfw;view.album.nsfw();api.post("Album::setNSFW",{albumID:album.json.id,is_nsfw:album.json.policy.is_nsfw},function(){return albums.refresh();});};/** + * @param {string} service - either `"twitter"`, `"facebook"` or `"mail"` + * @returns {void} + */album.share=function(service){if(!lychee.share_button_visible){return;}var url=location.href;switch(service){case"twitter":window.open("https://twitter.com/share?url=".concat(encodeURI(url)));break;case"facebook":window.open("https://www.facebook.com/sharer.php?u=".concat(encodeURI(url),"&t=").concat(encodeURI(album.json.title)));break;case"mail":location.href="mailto:?subject=".concat(encodeURI(album.json.title),"&body=").concat(encodeURI(url));break;}};/** + * @returns {void} + */album.qrCode=function(){if(!lychee.share_button_visible){return;}// We need this indirection based on a resize observer, because the ready +// callback of the dialog is invoked _before_ the dialog is made visible +// in order to allow the ready callback to make initializations of +// form elements without causing flicker. +// However, for invisible elements `.clientWidth` returns zero, hence +// we cannot paint the QR code onto the canvas before it becomes visible. +var qrCodeCanvasObserver=function(){var width=0;return new ResizeObserver(function(entries,observer){var qrCodeCanvas=entries[0].target;// Avoid infinite resize events due to clearing and repainting +// the same QR code on the canvas. +if(width===qrCodeCanvas.clientWidth){return;}width=qrCodeCanvas.clientWidth;QrCreator.render({text:location.href,radius:0.0,ecLevel:"H",fill:"#000000",background:"#FFFFFF",size:width},qrCodeCanvas);});}();basicModal.show({body:"",classList:["qr-code"],readyCB:function readyCB(formElements,dialog){var qrCodeCanvas=dialog.querySelector("canvas");qrCodeCanvasObserver.observe(qrCodeCanvas);},buttons:{cancel:{title:lychee.locale["CLOSE"],fn:basicModal.close}}});};/** + * @param {string[]} albumIDs + * @returns {void} + */album.getArchive=function(albumIDs){location.href="api/Album::getArchive?albumIDs="+albumIDs.join();};/** + * @param {string[]} albumIDs + * @param {?string} albumID + * @param {string} op1 + * @param {string} ops + * @returns {string} the message + */album.buildMessage=function(albumIDs,albumID,op1,ops){var targetTitle=lychee.locale["UNTITLED"];var sourceTitle=lychee.locale["UNTITLED"];var msg="";// Get title of target album +if(albumID===null){targetTitle=lychee.locale["ROOT"];}else{var targetAlbum=albums.getByID(albumID)||album.getSubByID(albumID);if(targetAlbum){targetTitle=targetAlbum.title;}}if(albumIDs.length===1){// Get title of the unique source album +var sourceAlbum=albums.getByID(albumIDs[0])||album.getSubByID(albumIDs[0]);if(sourceAlbum){sourceTitle=sourceAlbum.title;}msg=sprintf(lychee.locale[op1],sourceTitle,targetTitle);}else{msg=sprintf(lychee.locale[ops],targetTitle);}return msg;};/** + * @param {string[]} albumIDs + * @returns {void} + */album["delete"]=function(albumIDs){var isTagAlbum=albumIDs.length===1&&albums.isTagAlbum(albumIDs[0]);var handleSuccessfulDeletion=function handleSuccessfulDeletion(){if(visible.albums()){albumIDs.forEach(function(id){view.albums.content["delete"](id);albums.deleteByID(id);});}else if(visible.album()){albums.refresh();if(albumIDs.length===1&&album.getID()===albumIDs[0]){lychee["goto"](album.getParentID());}else{albumIDs.forEach(function(id){album.deleteSubByID(id);view.album.content.deleteSub(id);});}}};var action=function action(){basicModal.close();api.post("Album::delete",{albumIDs:albumIDs},handleSuccessfulDeletion);};/** + * @param {ModalDialogFormElements} formElements + * @param {HTMLDivElement} dialog + */var initConfirmDeletionDialog=function initConfirmDeletionDialog(formElements,dialog){/** @type {HTMLParagraphElement} */var p=dialog.querySelector("p");if(albumIDs.length===1&&albumIDs[0]===SmartAlbumID.UNSORTED){p.textContent=lychee.locale["DELETE_UNSORTED_CONFIRM"];}else if(albumIDs.length===1){var albumTitle="";// Get title +if(album.json){if(album.getID()===albumIDs[0]){albumTitle=album.json.title;}else albumTitle=album.getSubByID(albumIDs[0]).title;}if(!albumTitle){var a=albums.getByID(albumIDs[0]);if(a)albumTitle=a.title;}// Fallback for album without a title +if(!albumTitle)albumTitle=lychee.locale["UNTITLED"];p.textContent=isTagAlbum?sprintf(lychee.locale["DELETE_TAG_ALBUM_CONFIRMATION"],albumTitle):sprintf(lychee.locale["DELETE_ALBUM_CONFIRMATION"],albumTitle);}else{p.textContent=sprintf(lychee.locale["DELETE_ALBUMS_CONFIRMATION"],albumIDs.length);}};var actionButtonLabel=albumIDs.length===1?albumIDs[0]===SmartAlbumID.UNSORTED?lychee.locale["CLEAR_UNSORTED"]:isTagAlbum?lychee.locale["DELETE_TAG_ALBUM_QUESTION"]:lychee.locale["DELETE_ALBUM_QUESTION"]:lychee.locale["DELETE_ALBUMS_QUESTION"];var cancelButtonLabel=albumIDs.length===1?albumIDs[0]===SmartAlbumID.UNSORTED?lychee.locale["KEEP_UNSORTED"]:lychee.locale["KEEP_ALBUM"]:lychee.locale["KEEP_ALBUMS"];basicModal.show({body:"

",readyCB:initConfirmDeletionDialog,buttons:{action:{title:actionButtonLabel,fn:action,classList:["red"]},cancel:{title:cancelButtonLabel,fn:basicModal.close}}});};/** + * @param {string[]} albumIDs + * @param {string} albumID + * @param {boolean} confirm + */album.merge=function(albumIDs,albumID){var confirm=arguments.length>2&&arguments[2]!==undefined?arguments[2]:true;var action=function action(){basicModal.close();api.post("Album::merge",{albumID:albumID,albumIDs:albumIDs},function(){return album.reload();});};if(confirm){basicModal.show({body:"

",readyCB:function readyCB(formElements,dialog){return dialog.querySelector("p").textContent=album.buildMessage(albumIDs,albumID,"ALBUM_MERGE","ALBUMS_MERGE");},buttons:{action:{title:lychee.locale["MERGE_ALBUM"],fn:action,classList:["red"]},cancel:{title:lychee.locale["DONT_MERGE"],fn:basicModal.close}}});}else{action();}};/** + * @param {string[]} albumIDs source IDs + * @param {string} albumID target ID + * @param {boolean} confirm show confirmation dialog? + */album.setAlbum=function(albumIDs,albumID){var confirm=arguments.length>2&&arguments[2]!==undefined?arguments[2]:true;var action=function action(){basicModal.close();api.post("Album::move",{albumID:albumID,albumIDs:albumIDs},function(){return album.reload();});};if(confirm){basicModal.show({body:"

",readyCB:function readyCB(formElements,dialog){return dialog.querySelector("p").textContent=album.buildMessage(albumIDs,albumID,"ALBUM_MOVE","ALBUMS_MOVE");},buttons:{action:{title:lychee.locale["MOVE_ALBUMS"],fn:action,classList:["red"]},cancel:{title:lychee.locale["NOT_MOVE_ALBUMS"],fn:basicModal.close}}});}else{action();}};/** + * @returns {void} + */album.apply_nsfw_filter=function(){if(lychee.nsfw_visible){$('.album[data-nsfw="1"]').show();}else{$('.album[data-nsfw="1"]').hide();}};/** + * Determines whether the user can upload to the currently active album. + * + * It is safe to call this method even if no album is loaded at all. + * + * If no user is authenticated or the authenticated user has no upload + * capabilities, the method returns `false` (even for albums owned by the + * user). + * For admin users the method returns `true`. + * + * For non-admin, authenticated users with the upload capability + * + * - the method returns `true` for smart albums + * - the method returns `true` for regular albums if and only if the album is + * owned by the currently authenticated user. + * + * Note, for the time being this method contains a work-around in case + * no album is loaded, but the root view is visible. + * Currently, this is necessary, because this method is (erroneously) called + * for the root view as well. + * In order to determine whether the work-around for the root view needs to + * be applied, this method checks if the root view is visible based on the + * visibility of the corresponding headers. + * Hence, the caller must ensure that the appropriate header is set first + * in order to obtain a correct result from this method. + * + * @returns {boolean} + */album.isUploadable=function(){if(album.json!==null&&album.json.rights.can_upload){return true;}if(album.json===null&&lychee.rights.root_album.can_upload){return true;}return false;};/** + * @param {Photo} data + */album.updatePhoto=function(data){/** + * @param {?SizeVariant} src + * @returns {?SizeVariant} + */var deepCopySizeVariant=function deepCopySizeVariant(src){if(src===undefined||src===null)return null;return{type:src.type,url:src.url,width:src.width,height:src.height,filesize:src.filesize};};if(album.json&&album.json.photos){var _photo2=album.json.photos.find(function(p){return p.id===data.id;});// Deep copy size variants +_photo2.size_variants={thumb:deepCopySizeVariant(data.size_variants.thumb),thumb2x:deepCopySizeVariant(data.size_variants.thumb2x),small:deepCopySizeVariant(data.size_variants.small),small2x:deepCopySizeVariant(data.size_variants.small2x),medium:deepCopySizeVariant(data.size_variants.medium),medium2x:deepCopySizeVariant(data.size_variants.medium2x),original:deepCopySizeVariant(data.size_variants.original)};view.album.content.updatePhoto(_photo2);albums.refresh();}};/** + * @returns {void} + */album.reload=function(){var albumID=album.getID();album.refresh();albums.refresh();if(visible.album())lychee["goto"](albumID);else lychee["goto"]();};/** + * @returns {void} + */album.refresh=function(){album.json=null;};/** + * @returns {void} + */album.deleteTrack=function(){album.json.track_url=null;api.post("Album::deleteTrack",{albumID:album.json.id});};/** + * @description Takes care of every action albums can handle and execute. + */var albums={/** @type {?Albums} */json:null};/** + * @returns {void} + */albums.load=function(){var showRootAlbum=function showRootAlbum(){// DO NOT change the order of `header.setMode` and `view.albums.init`. +// The latter relies on the header being set correctly. +// +// `view.albums.init` builds the HTML of the albums view (note the +// plural-s). +// Internally, this exploits code for regular albums which in +// turn calls `album.isUploadabe` (note the missing plural-s) to +// check whether the current album supports drag-&-drop. +// In order to return the correct value `album.isUploadabe` resorts +// to a hack: if no (regular) album is loaded `album.isUploadabe` +// normally returns `false` except the root album is visible. +// In that case `album.isUploadabe` returns a "fake" `true`. +// However, in order to do so `album.isUploadabe` needs to check +// whether the root album is visible which is determined by the +// visibility of the corresponding header. +// That is why the header needs to be set first. +// +// However, the actual bug is to call `album.isUploadable` for the +// root view. +// TODO: Fix the bug described above. +header.setMode("albums");view.albums.init();lychee.animate(lychee.content,"contentZoomIn");tabindex.makeFocusable(lychee.content);if(lychee.active_focus_on_page_load){// Put focus on first element - either album or photo +var first_album=$(".album:first");if(first_album.length!==0){first_album.focus();}else{var first_photo=$(".photo:first");if(first_photo.length!==0){first_photo.focus();}}}setTimeout(function(){lychee.footer_show();},300);// If no user is authenticated and there is nothing to see in the +// root album, we automatically show the login dialog +if(lychee.publicMode===true&&lychee.viewMode===false&&albums.isEmpty()){lychee.loginDialog();}};var startTime=new Date().getTime();lychee.animate(lychee.content,"contentZoomOut");/** + * @param {Albums} data + */var successCallback=function successCallback(data){albums.json=data;// Skip delay when opening a blank Lychee +var skipDelay=!visible.albums()&&!visible.photo()&&!visible.album()||visible.album()&&lychee.content.html()==="";// Calculate delay +var durationTime=new Date().getTime()-startTime;var waitTime=durationTime>300||skipDelay?0:300-durationTime;setTimeout(function(){showRootAlbum();},waitTime);};if(albums.json===null){api.post("Albums::get",{},successCallback);}else{setTimeout(function(){showRootAlbum();},300);}};/** + * @param {(Album|TagAlbum|SmartAlbum)} album + * @returns {void} + */albums.parse=function(album){if(!album.thumb){album.thumb={id:"",thumb:album.policy.is_password_required?"img/password.svg":"img/no_images.svg",type:"image/svg+xml",thumb2x:null};}};/** + * @param {?string} albumID + * @returns {boolean} + */albums.isShared=function(albumID){if(albumID==null)return false;if(!albums.json)return false;if(!albums.json.albums)return false;var found=false;/** + * @this {Album} + * @returns {boolean} + */var func=function func(){if(this.id===albumID){found=true;return false;// stop the loop +}if(this.albums){$.each(this.albums,func);}};if(albums.json.shared_albums!==null)$.each(albums.json.shared_albums,func);return found;};/** + * @param {?string} albumID + * @returns {(null|Album|TagAlbum|SmartAlbum)} + */albums.getByID=function(albumID){if(albumID==null)return null;if(!albums.json)return null;if(!albums.json.albums)return null;if(albums.json.smart_albums.hasOwnProperty(albumID)){return albums.json.smart_albums[albumID];}var result=albums.json.tag_albums.find(function(tagAlbum){return tagAlbum.id===albumID;});if(result){return result;}result=albums.json.albums.find(function(album){return album.id===albumID;});if(result){return result;}result=albums.json.shared_albums.find(function(album){return album.id===albumID;});if(result){return result;}return null;};/** + * Deletes a top-level album by ID from the cached JSON for albums. + * + * The method is called by {@link album.delete} after a top-level album has + * successfully been deleted at the server-side. + * + * @param {?string} albumID + * @returns {void} + */albums.deleteByID=function(albumID){if(albumID==null)return;if(!albums.json)return;if(!albums.json.albums)return;var idx=albums.json.albums.findIndex(function(a){return a.id===albumID;});albums.json.albums.splice(idx,1);if(idx!==-1)return;idx=albums.json.shared_albums.findIndex(function(a){return a.id===albumID;});albums.json.shared_albums.splice(idx,1);if(idx!==-1)return;idx=albums.json.tag_albums.findIndex(function(a){return a.id===albumID;});albums.json.tag_albums.splice(idx,1);};/** + * @returns {void} + */albums.refresh=function(){albums.json=null;};/** + * @param {?string} albumID + * @returns {boolean} + */albums.isTagAlbum=function(albumID){return albums.json&&albums.json.tag_albums.find(function(tagAlbum){return tagAlbum.id===albumID;});};/** + * Returns true if the root album is empty in the sense that there is no + * visible user content. + * + * @returns {boolean} + */albums.isEmpty=function(){return albums.json===null||albums.isSmartAlbumEmpty(albums.json.smart_albums["public"])&&albums.isSmartAlbumEmpty(albums.json.smart_albums.recent)&&albums.isSmartAlbumEmpty(albums.json.smart_albums.starred)&&albums.isSmartAlbumEmpty(albums.json.smart_albums.unsorted)&&albums.isSmartAlbumEmpty(albums.json.smart_albums.on_this_day)&&albums.json.albums.length===0&&albums.json.shared_albums.length===0&&albums.json.tag_albums.length===0;};/** + * @param {?SmartAlbum} smartAlbum + * @returns {boolean} + */albums.isSmartAlbumEmpty=function(smartAlbum){return!smartAlbum||!smartAlbum.photos||smartAlbum.photos.length===0;};//noinspection HtmlUnknownTarget +/** + * @description This module is used to generate HTML-Code. + */var build={};/** + * @param {string} icon + * @param {string} [classes=""] + * + * @returns {string} + */build.iconic=function(icon){var classes=arguments.length>1&&arguments[1]!==undefined?arguments[1]:"";return lychee.html(_templateObject||(_templateObject=_taggedTemplateLiteral([""])),classes,icon);};/** + * @param {string} title + * @returns {string} + */build.divider=function(title){return lychee.html(_templateObject2||(_templateObject2=_taggedTemplateLiteral(["

$","

"])),title);};/** + * @param {string} id + * @returns {string} + */build.editIcon=function(id){return lychee.html(_templateObject3||(_templateObject3=_taggedTemplateLiteral(["
","
"])),id,build.iconic("pencil"));};/** + * @param {number} top + * @param {number} left + * @returns {string} + */build.multiselect=function(top,left){return lychee.html(_templateObject4||(_templateObject4=_taggedTemplateLiteral(["
"])),top,left);};/** + * Returns HTML for the thumb of an album. + * + * @param {(Album|TagAlbum)} data + * + * @returns {string} + */build.getAlbumThumb=function(data){var isVideo=data.thumb.type&&data.thumb.type.indexOf("video")>-1;var isRaw=data.thumb.type&&data.thumb.type.indexOf("raw")>-1;var thumb=data.thumb.thumb;var thumb2x=data.thumb.thumb2x;if(thumb==="uploads/thumb/"&&isVideo){return"".concat(lychee.locale["PHOTO_THUMBNAIL"],"");}if(thumb==="uploads/thumb/"&&isRaw){return"".concat(lychee.locale["PHOTO_THUMBNAIL"],"");}return"").concat(lychee.locale["PHOTO_THUMBNAIL"],"");};/** + * @param {(Album|TagAlbum|SmartAlbum)} data + * @param {boolean} disabled + * + * @returns {string} HTML for the album + */build.album=function(data){var disabled=arguments.length>1&&arguments[1]!==undefined?arguments[1]:false;var formattedCreationTs=lychee.locale.printMonthYear(data.created_at);var formattedMinTs=lychee.locale.printMonthYear(data.min_taken_at);var formattedMaxTs=lychee.locale.printMonthYear(data.max_taken_at);// The condition below is faulty wrt. to two issues: +// +// a) The condition only checks whether the owning/current album is +// uploadable (aka "editable"), but it does not check whether the +// album at hand whose icon is built is editable. +// But this is of similar importance. +// Currently, we only check whether the album at hand is a smart +// album or tag album which are always considered non-editable. +// But this is only half of the story. +// For example, a regular album might still be non-editable, if the +// current user is not the owner of that album. +// b) This method is not only called if the owning/current album is a +// proper album, but also for the root view. +// However, `album.isUploadable` should not be called for the root +// view. +// +// Moreover, we have to distinguish between "drag" and "drop". +// Doing so would also solve the problems above: +// +// - "Drag": If the current child album at hand can be dragged (away) +// is mostly determined by the user's rights on the parent album. +// Instead of (erroneously) using `album.isUploadable()` for that +// (even for the root view), the "right to drag" should be passed to +// this method as a parameter very much like `disabled` such that this +// method can be used for both regular albums and the root view. +// - "Drop": If something (e.g. a photo) can be dropped onto the child +// album at hand is independent of the user's rights on the containing +// album. +// Whether the child album supports the drop event depends on the type +// of the album (i.e. it must not be a smart or tag album), but also +// on the ownership of the album. +var disableDragDrop=!data.rights.can_edit||disabled||album.isSmartID(data.id)||data.is_tag_album;var subtitle=formattedCreationTs;// check setting album_subtitle_type: +// takedate: date range (min/max_takedate from EXIF; if missing defaults to creation) +// creation: creation date of album +// description: album description +// default: any other type defaults to old style setting subtitles based of album sorting +switch(lychee.album_subtitle_type){case"description":subtitle=data.description?lychee.escapeHTML(data.description):"";break;case"takedate":if(formattedMinTs!==""||formattedMaxTs!==""){// either min_taken_at or max_taken_at is set +subtitle=formattedMinTs===formattedMaxTs?formattedMaxTs:formattedMinTs+" - "+formattedMaxTs;subtitle=lychee.html(_templateObject5||(_templateObject5=_taggedTemplateLiteral(["","$",""])),lychee.locale["CAMERA_DATE"],build.iconic("camera-slr"),subtitle);break;}// fall through +case"creation":break;case"oldstyle":default:if(lychee.sorting_albums&&data.min_taken_at&&data.max_taken_at){if(lychee.sorting_albums.column==="max_taken_at"||lychee.sorting_albums.column==="min_taken_at"){if(formattedMinTs!==""&&formattedMaxTs!==""){subtitle=formattedMinTs===formattedMaxTs?formattedMaxTs:formattedMinTs+" - "+formattedMaxTs;}else if(formattedMinTs!==""&&lychee.sorting_albums.column==="min_taken_at"){subtitle=formattedMinTs;}else if(formattedMaxTs!==""&&lychee.sorting_albums.column==="max_taken_at"){subtitle=formattedMaxTs;}}}}var html=lychee.html(_templateObject6||(_templateObject6=_taggedTemplateLiteral(["\n\t\t\t
\n\t\t\t\t ","\n\t\t\t\t ","\n\t\t\t\t ","\n\t\t\t\t
\n\t\t\t\t\t

$","

\n\t\t\t\t\t","\n\t\t\t\t
\n\t\t\t"])),disabled?"disabled":"",data.policy.is_nsfw&&lychee.nsfw_blur?"blurred":"",data.id,data.policy.is_nsfw?"1":"0",tabindex.get_next_tab_index(),disableDragDrop?"false":"true",disableDragDrop?"":"ondragstart='lychee.startDrag(event)'\n\t\t\t\tondragover='lychee.overDrag(event)'\n\t\t\t\tondragleave='lychee.leaveDrag(event)'\n\t\t\t\tondragend='lychee.endDrag(event)'\n\t\t\t\tondrop='lychee.finishDrag(event)'",build.getAlbumThumb(data),build.getAlbumThumb(data),build.getAlbumThumb(data),data.title,data.title,subtitle);if(data.rights.can_edit&&!disabled){var isCover=album.json&&album.json.cover_id&&data.thumb.id===album.json.cover_id;html+=lychee.html(_templateObject7||(_templateObject7=_taggedTemplateLiteral(["\n\t\t\t\t
\n\t\t\t\t\t","\n\t\t\t\t\t","\n\t\t\t\t\t","\n\t\t\t\t\t","\n\t\t\t\t\t","\n\t\t\t\t\t","\n\t\t\t\t\t","\n\t\t\t\t\t","\n\t\t\t\t\t","\n\t\t\t\t
\n\t\t\t\t"])),data.policy&&data.policy.is_nsfw?"badge--nsfw":"",build.iconic("warning"),data.id===SmartAlbumID.STARRED?"badge--star":"",build.iconic("star"),data.id===SmartAlbumID.RECENT?"badge--visible badge--list":"",build.iconic("clock"),data.id===SmartAlbumID.ON_THIS_DAY?"badge--tag badge--list":"",build.iconic("calendar"),data.id===SmartAlbumID.PUBLIC||data.policy&&data.policy.is_public?"badge--visible":"",data.policy&&data.policy.is_link_required?"badge--hidden":"badge--not--hidden",build.iconic("eye"),data.id===SmartAlbumID.UNSORTED?"badge--visible":"",build.iconic("list"),data.policy&&data.policy.is_password_required?"badge--visible":"",build.iconic("lock-unlocked"),data.is_tag_album?"badge--tag":"",build.iconic("tag"),isCover?"badge--cover":"",build.iconic("folder-cover"));}var albumcount=data.num_subalbums;switch(lychee.album_decoration){case"none":// no decorations +break;case"photo":// photos only +if(data.num_photos>0){html+=lychee.html(_templateObject8||(_templateObject8=_taggedTemplateLiteral(["\n\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t","\n\t\t\t\t\t\t\t","\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t
"])),build.iconic("puzzle-piece"),data.num_photos);}break;case"layers":// sub-albums only and only marker without count (as in old v4 behaviour) +if(albumcount>0){html+=lychee.html(_templateObject9||(_templateObject9=_taggedTemplateLiteral(["\n\t\t\t\t\t
\n\t\t\t\t\t\t","\n\t\t\t\t\t
"])),build.iconic("layers"));}break;case"album":// sub-albums only +if(albumcount>0){html+=lychee.html(_templateObject10||(_templateObject10=_taggedTemplateLiteral(["\n\t\t\t\t\t"])));}break;case"all":// sub-albums and photos +if(albumcount>0||data.num_photos>0){html+=lychee.html(_templateObject13||(_templateObject13=_taggedTemplateLiteral(["\n\t\t\t\t\t
"])),lychee.album_decoration_orientation);if(data.num_photos>0){html+=lychee.html(_templateObject14||(_templateObject14=_taggedTemplateLiteral(["\n\t\t\t\t\t\t\t","\n\t\t\t\t\t\t\t\t","\n\t\t\t\t\t\t\t"])),build.iconic("puzzle-piece"),data.num_photos);}if(albumcount>0){html+=lychee.html(_templateObject15||(_templateObject15=_taggedTemplateLiteral(["\n\t\t\t\t\t\t",""])),build.iconic("folder"));if(albumcount>1)html+=lychee.html(_templateObject16||(_templateObject16=_taggedTemplateLiteral(["\n\t\t\t\t\t\t\t",""])),albumcount);html+=lychee.html(_templateObject17||(_templateObject17=_taggedTemplateLiteral(["\n\t\t\t\t\t\t"])));}html+=lychee.html(_templateObject18||(_templateObject18=_taggedTemplateLiteral(["\n\t\t\t\t\t
"])));}}html+="
";// close 'album' +return html;};/** + * @param {Photo} data + * @param {boolean} disabled + * + * @returns {string} HTML for the photo + */build.photo=function(data){var disabled=arguments.length>1&&arguments[1]!==undefined?arguments[1]:false;var html="";var thumbnail="";var thumb2x="";// Note, album.json might not be loaded, if +// a) the photo is a single public photo in a private album +// b) the photo is part of a search result +var isCover=album.json&&album.json.cover_id===data.id;var isVideo=data.type&&data.type.indexOf("video")>-1;var isRaw=data.type&&data.type.indexOf("raw")>-1;var isLivePhoto=data.live_photo_url!==""&&data.live_photo_url!==null;if(data.size_variants.thumb===null){if(isLivePhoto){thumbnail="".concat(lychee.locale["PHOTO_THUMBNAIL"],"");}if(isVideo){thumbnail="".concat(lychee.locale["PHOTO_THUMBNAIL"],"");}else if(isRaw){thumbnail="".concat(lychee.locale["PHOTO_THUMBNAIL"],"");}}else if(lychee.layout==="square"){if(data.size_variants.thumb2x!==null){thumb2x=data.size_variants.thumb2x.url;}if(thumb2x!==""){thumb2x="data-srcset='".concat(thumb2x," 2x'");}thumbnail="");thumbnail+="".concat(lychee.locale["PHOTO_THUMBNAIL"],"");thumbnail+="";}else{if(data.size_variants.small!==null){if(data.size_variants.small2x!==null){thumb2x="data-srcset='".concat(data.size_variants.small.url," ").concat(data.size_variants.small.width,"w, ").concat(data.size_variants.small2x.url," ").concat(data.size_variants.small2x.width,"w'");}thumbnail="");thumbnail+="".concat(lychee.locale["PHOTO_THUMBNAIL"],"");thumbnail+="";}else if(data.size_variants.medium!==null){if(data.size_variants.medium2x!==null){thumb2x="data-srcset='".concat(data.size_variants.medium.url," ").concat(data.size_variants.medium.width,"w, ").concat(data.size_variants.medium2x.url," ").concat(data.size_variants.medium2x.width,"w'");}thumbnail="");thumbnail+="".concat(lychee.locale["PHOTO_THUMBNAIL"],"");thumbnail+="";}else if(!isVideo){// Fallback for images with no small or medium. +thumbnail="");thumbnail+="").concat(lychee.locale["PHOTO_THUMBNAIL"],"");thumbnail+="";}else{// Fallback for videos with no small (the case of no thumb is +// handled at the top of this function). +if(data.size_variants.thumb2x!==null){thumb2x=data.size_variants.thumb2x.url;}if(thumb2x!==""){thumb2x="data-srcset='".concat(data.size_variants.thumb.url," ").concat(data.size_variants.thumb.width,"w, ").concat(thumb2x," ").concat(data.size_variants.thumb2x.width,"w'");}thumbnail="";thumbnail+="".concat(lychee.locale["PHOTO_THUMBNAIL"],"");thumbnail+="";}}html+=lychee.html(_templateObject19||(_templateObject19=_taggedTemplateLiteral(["\n\t\t\t
\n\t\t\t\t","\n\t\t\t\t
\n\t\t\t\t\t

$","

\n\t\t\t"])),disabled?"disabled":"",data.album_id,data.id,tabindex.get_next_tab_index(),!album.isUploadable()||disabled?"false":"true",thumbnail,data.title,data.title);if(data.taken_at!==null)html+=lychee.html(_templateObject20||(_templateObject20=_taggedTemplateLiteral(["","",""])),lychee.locale["CAMERA_DATE"],build.iconic("camera-slr"),lychee.locale.printDateTime(data.taken_at));else html+=lychee.html(_templateObject21||(_templateObject21=_taggedTemplateLiteral(["",""])),lychee.locale.printDateTime(data.created_at));html+="
";if(album.isUploadable()){// Note, `album.json` might be null, if the photo is displayed as +// part of a search result and therefore the actual parent album +// is not loaded. (The "parent" album is the virtual "search album" +// in this case). +// This also means that the displayed variant of the public badge of +// a photo depends on the availability of the parent album. +// This seems to be an undesired but unavoidable side effect. +html+=lychee.html(_templateObject22||(_templateObject22=_taggedTemplateLiteral(["\n\t\t\t\t
\n\t\t\t\t","\n\t\t\t\t","\n\t\t\t\t","\n\t\t\t\t
\n\t\t\t\t"])),data.is_starred?"badge--star":"",build.iconic("star"),data.is_public&&album.json&&album.json.policy&&!album.json.policy.is_public?"badge--visible badge--hidden":"",build.iconic("eye"),isCover?"badge--cover":"",build.iconic("folder-cover"));}html+="
";return html;};/** + * @param {Photo} data + * @param {string} overlay_type + * @param {boolean} [next=false] + * + * @returns {string} + */build.check_overlay_type=function(data,overlay_type){var next=arguments.length>2&&arguments[2]!==undefined?arguments[2]:false;var types=["desc","date","exif","none"];var idx=types.indexOf(overlay_type);if(idx<0)return"none";if(next)idx++;var exifHash=data.make+data.model+data.shutter+data.iso+(data.type.indexOf("video")!==0?data.aperture+data.focal:"");for(var i=0;i").concat(build.iconic("camera-slr"),"").concat(lychee.locale.printDateTime(data.taken_at),"");else overlay=lychee.locale.printDateTime(data.created_at);break;case"exif":var exifHash=data.make+data.model+data.shutter+data.aperture+data.focal+data.iso;if(exifHash!==""){if(data.shutter&&data.shutter!=="")overlay=data.shutter.replace("s","sec");if(data.aperture&&data.aperture!==""){if(overlay!=="")overlay+=" at ";overlay+=data.aperture.replace("f/","ƒ / ");}if(data.iso&&data.iso!==""){if(overlay!=="")overlay+=", ";overlay+=sprintf(lychee.locale["PHOTO_ISO"],data.iso);}if(data.focal&&data.focal!==""){if(overlay!=="")overlay+="
";overlay+=data.focal+(data.lens&&data.lens!==""?" ("+data.lens+")":"");}}break;case"none":default:return"";}return lychee.html(_templateObject23||(_templateObject23=_taggedTemplateLiteral(["\n\t\t
\n\t\t

$","

\n\t\t"])),data.title?data.title:lychee.locale["UNTITLED"])+(overlay!==""?"

".concat(overlay,"

"):"")+"\n\t\t
\n\t\t";};/** + * @param {Photo} data + * @param {boolean} areControlsVisible + * @param {boolean} autoplay + * @returns {{thumb: string, html: string}} + */build.imageview=function(data,areControlsVisible,autoplay){var html="";var thumb="";if(data.type.indexOf("video")>-1){html+=lychee.html(_templateObject24||(_templateObject24=_taggedTemplateLiteral([""])),areControlsVisible?"":"full",autoplay?"autoplay":"",tabindex.get_next_tab_index(),data.size_variants.original.url);}else if(data.type.indexOf("raw")>-1&&data.size_variants.medium===null){html+=lychee.html(_templateObject25||(_templateObject25=_taggedTemplateLiteral(["big"])),areControlsVisible?"":"full",tabindex.get_next_tab_index());}else{var img="";if(data.live_photo_url===""||data.live_photo_url===null){// It's normal photo +// See if we have the thumbnail loaded... +$(".photo").each(function(){if($(this).attr("data-id")&&$(this).attr("data-id")===data.id){var thumbimg=$(this).find("img");if(thumbimg.length>0){thumb=thumbimg[0].currentSrc?thumbimg[0].currentSrc:thumbimg[0].src;return false;}}});if(data.size_variants.medium!==null){var medium="";if(data.size_variants.medium2x!==null){medium="srcset='".concat(data.size_variants.medium.url," ").concat(data.size_variants.medium.width,"w, ").concat(data.size_variants.medium2x.url," ").concat(data.size_variants.medium2x.width,"w'");}img="medium");}else{img="big");}}else{if(data.size_variants.medium!==null){var medium_width=data.size_variants.medium.width;var medium_height=data.size_variants.medium.height;// It's a live photo +img="
");}else{// It's a live photo +img="
");}}html+=lychee.html(_templateObject26||(_templateObject26=_taggedTemplateLiteral(["",""])),img);}html+=build.overlay_image(data)+"\n\t\t\t\n\t\t\t\n\t\t\t");return{html:html,thumb:thumb};};/** + * @param {string} type - either `"magnifying-glass"`, `"eye"`, `"cog"` or `"question-marks"` + * @returns {string} + */build.no_content=function(type){var html="";html+=lychee.html(_templateObject27||(_templateObject27=_taggedTemplateLiteral(["
",""])),build.iconic(type));switch(type){case"magnifying-glass":html+=lychee.html(_templateObject28||(_templateObject28=_taggedTemplateLiteral(["

","

"])),lychee.locale["VIEW_NO_RESULT"]);break;case"eye":html+=lychee.html(_templateObject29||(_templateObject29=_taggedTemplateLiteral(["

","

"])),lychee.locale["VIEW_NO_PUBLIC_ALBUMS"]);break;case"cog":html+=lychee.html(_templateObject30||(_templateObject30=_taggedTemplateLiteral(["

","

"])),lychee.locale["VIEW_NO_CONFIGURATION"]);break;case"question-mark":html+=lychee.html(_templateObject31||(_templateObject31=_taggedTemplateLiteral(["

","

"])),lychee.locale["VIEW_PHOTO_NOT_FOUND"]);break;}html+="
";return html;};/** + * @param {string[]} tags + * @returns {string} return safe HTMl code + */build.tags=function(tags){var html="";var editable=album.isUploadable();// Search is enabled if logged in (not publicMode) or public search is enabled +var searchable=!lychee.publicMode||lychee.public_search;// build class_string for tag +var a_class=searchable?"tag search":"tag";if(tags.length!==0){tags.forEach(function(tag,index){if(editable){html+=lychee.html(_templateObject32||(_templateObject32=_taggedTemplateLiteral(["$","",""])),a_class,tag,index,build.iconic("x"));}else{html+=lychee.html(_templateObject33||(_templateObject33=_taggedTemplateLiteral(["$",""])),a_class,tag);}});}else{html=lychee.html(_templateObject34||(_templateObject34=_taggedTemplateLiteral(["
","
"])),lychee.locale["NO_TAGS"]);}return html;};/** + * @param {User} user + * @returns {string} + */build.user=function(user){return lychee.html(_templateObject35||(_templateObject35=_taggedTemplateLiteral(["
\n\t\t\t

\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t

\n\t\t\t","\n\t\t\t","\n\t\t
\n\t\t"])),user.id,user.id,user.username,lychee.locale["USERNAME"],lychee.locale["NEW_PASSWORD"],lychee.locale["ALLOW_UPLOADS"],lychee.locale["ALLOW_USER_SELF_EDIT"],user.id,user.id!==lychee.user.id?"":"basicModal__button_OK_no_DEL",lychee.locale["SAVE"],user.id!==lychee.user.id?"").concat(lychee.locale["DELETE"],""):"");};/** + * @param {WebAuthnCredential} credential + * @returns {string} + */build.u2f=function(credential){return lychee.html(_templateObject36||(_templateObject36=_taggedTemplateLiteral(["
\n\t\t\t

\n\t\t\t\n\t\t\t","\n\t\t\t

\n\t\t\tDelete\n\t\t
\n\t\t"])),credential.id,credential.id,credential.id.slice(0,30),credential.id);};/** + * @description This module is used for the context menu. + */var contextMenu={};/** + * Note, the 'Add' menu in the upper right corner is effectively + * a 'More...' menu (at least on small screens). More importantly, + * it is not a context menu (even though it uses the same code). + * + * @param {jQuery.Event} e + */contextMenu.add=function(e){var items=[{title:build.iconic("image")+lychee.locale["UPLOAD_PHOTO"],fn:function fn(){return $("#upload_files").click();}},{},{title:build.iconic("link-intact")+lychee.locale["IMPORT_LINK"],fn:function fn(){return upload.start.url();}},{title:build.iconic("dropbox","ionicons")+lychee.locale["IMPORT_DROPBOX"],fn:function fn(){return upload.start.dropbox();}},{title:build.iconic("terminal")+lychee.locale["IMPORT_SERVER"],fn:function fn(){return upload.start.server();}},{},{title:build.iconic("folder")+lychee.locale["NEW_ALBUM"],fn:function fn(){return album.add();}}];if(visible.albums()){items.push({title:build.iconic("tags")+lychee.locale["NEW_TAG_ALBUM"],fn:function fn(){return album.addByTags();}});}else if(album.isSmartID(album.getID())||album.isSearchID(album.getID())){// remove Import and New album if smart album or search results +items.splice(1);}if(!lychee.rights.settings.can_edit){// remove import from dropbox and server if not admin +items.splice(3,2);}else if(!lychee.dropboxKey||lychee.dropboxKey===""){// remove import from dropbox if dropboxKey not set +items.splice(3,1);}if(visible.album()&&album.isUploadable()){// prepend further buttons if menu bar is reduced on small screens +var albumID=album.getID();if(album.isTagAlbum()){// For tag albums the context menu is normally not used. +items=[];}if(albumID.length===24||albumID===SmartAlbumID.UNSORTED){if(albumID!==SmartAlbumID.UNSORTED){var button_visibility_album=$("#button_visibility_album");if(button_visibility_album&&button_visibility_album.css("display")==="none"){items.unshift({title:build.iconic("eye")+lychee.locale["VISIBILITY_ALBUM"],visible:lychee.enable_button_visibility,fn:function fn(){return album.setProtectionPolicy(albumID);}});}}var button_trash_album=$("#button_trash_album");if(button_trash_album&&button_trash_album.css("display")==="none"){items.unshift({title:build.iconic("trash")+lychee.locale["DELETE_ALBUM"],visible:lychee.enable_button_trash,fn:function fn(){return album["delete"]([albumID]);}});}if(albumID!==SmartAlbumID.UNSORTED){if(!album.isTagAlbum()){var button_move_album=$("#button_move_album");if(button_move_album&&button_move_album.css("display")==="none"){items.unshift({title:build.iconic("folder")+lychee.locale["MOVE_ALBUM"],visible:lychee.enable_button_move,fn:function fn(event){return contextMenu.move([albumID],event,album.setAlbum,"ROOT",album.getParentID()!==null);}});}}var button_nsfw_album=$("#button_nsfw_album");if(button_nsfw_album&&button_nsfw_album.css("display")==="none"){items.unshift({title:build.iconic("warning")+lychee.locale["ALBUM_MARK_NSFW"],visible:true,fn:function fn(){return album.toggleNSFW();}});}}if(!album.isSmartID(albumID)&&lychee.map_display){// display track add button if it's a regular album +items.push({},{title:build.iconic("location")+lychee.locale["UPLOAD_TRACK"],fn:function fn(){return $("#upload_track_file").click();}});if(album.json.track_url){items.push({title:build.iconic("trash")+lychee.locale["DELETE_TRACK"],fn:album.deleteTrack});}}}}basicContext.show(items,e.originalEvent,null,function(){// use callback of basicContext to add an id and use CSS +// formatting to override hardcoded positioning of the +// menu based on mouse/tap location and more. +var menu=document.querySelector(".basicContext");menu.setAttribute("id","addMenu");});upload.notify();};/** + * @param {string} albumID + * @param {jQuery.Event} e + * + * @returns {void} + */contextMenu.album=function(albumID,e){// Notice for 'Merge': +// fn must call basicContext.close() first, +// in order to keep the selection +if(album.isSmartID(albumID)||album.isSearchID(albumID))return;var showMergeMove=!albums.isTagAlbum(albumID);var items=[{title:build.iconic("pencil")+lychee.locale["RENAME"],fn:function fn(){return album.setTitle([albumID]);}},{title:build.iconic("collapse-left")+lychee.locale["MERGE"],visible:showMergeMove,fn:function fn(){basicContext.close();contextMenu.move([albumID],e,album.merge,"ROOT",false);}},{title:build.iconic("folder")+lychee.locale["MOVE"],visible:showMergeMove,fn:function fn(){basicContext.close();contextMenu.move([albumID],e,album.setAlbum,"ROOT");}},{title:build.iconic("trash")+lychee.locale["DELETE"],fn:function fn(){return album["delete"]([albumID]);}},{title:build.iconic("cloud-download")+lychee.locale["DOWNLOAD"],fn:function fn(){return album.getArchive([albumID]);}}];if(visible.album()){// not top level +var myalbum=album.getSubByID(albumID);if(myalbum.thumb.id){var coverActive=myalbum.thumb.id===album.json.cover_id;// prepend context menu item +items.unshift({title:build.iconic("folder-cover",coverActive?"active":"")+lychee.locale[coverActive?"REMOVE_COVER":"SET_COVER"],fn:function fn(){return album.toggleCover(myalbum.thumb.id);}});}}$('.album[data-id="'+albumID+'"]').addClass("active");basicContext.show(items,e.originalEvent,contextMenu.close);};/** + * Handles drop event of an album onto an album and shows context menu to let the user pick the actions. + * + * @param {string} sourceAlbumID source album (which is being dragged) + * @param {string} targetAlbumID target album (where it is dropped) + * @param {DragEvent} e + * + * @returns {void} + */contextMenu.albumDrop=function(sourceAlbumID,targetAlbumID,e){var items=[{title:build.iconic("collapse-left")+lychee.locale["MERGE"],fn:function fn(){album.merge([sourceAlbumID],targetAlbumID);}},{title:build.iconic("folder")+lychee.locale["MOVE"],visible:true,fn:function fn(){basicContext.close();album.setAlbum([sourceAlbumID],targetAlbumID);}}];basicContext.show(items,e,contextMenu.close);};/** + * @param {string[]} albumIDs + * @param {jQuery.Event} e + * + * @returns {void} + */contextMenu.albumMulti=function(albumIDs,e){multiselect.stopResize();// Automatically merge selected albums when albumIDs contains more than one album +// Show list of albums otherwise +var autoMerge=albumIDs.length>1;var showMergeMove=albumIDs.every(function(albumID){return!albums.isTagAlbum(albumID);});var items=[{title:build.iconic("pencil")+lychee.locale["RENAME_ALL"],fn:function fn(){return album.setTitle(albumIDs);}},{title:build.iconic("collapse-left")+lychee.locale["MERGE_ALL"],visible:showMergeMove&&autoMerge,fn:function fn(){var albumID=albumIDs.shift();album.merge(albumIDs,albumID);}},{title:build.iconic("collapse-left")+lychee.locale["MERGE"],visible:showMergeMove&&!autoMerge,fn:function fn(){basicContext.close();contextMenu.move(albumIDs,e,album.merge,"ROOT",false);}},{title:build.iconic("folder")+lychee.locale["MOVE_ALL"],visible:showMergeMove,fn:function fn(){basicContext.close();contextMenu.move(albumIDs,e,album.setAlbum,"ROOT");}},{title:build.iconic("trash")+lychee.locale["DELETE_ALL"],fn:function fn(){return album["delete"](albumIDs);}},{title:build.iconic("cloud-download")+lychee.locale["DOWNLOAD_ALL"],fn:function fn(){return album.getArchive(albumIDs);}}];basicContext.show(items,e.originalEvent,contextMenu.close);};/** + * @callback ContextMenuActionCB + * + * @param {(Photo|Album)} entity + */ /** + * @callback ContextMenuEventCB + * + * @param {jQuery.Event} [e] + * @returns {void} + */ /** + * @param {(Photo|Album)[]} lists + * @param {string[]} exclude list of IDs to exclude + * @param {ContextMenuActionCB} action + * @param {?string} [parentID=null] parentID + * @param {number} [layer=0] + * + * @returns {{title: string, disabled: boolean, fn: ContextMenuEventCB}[]} + */contextMenu.buildList=function(lists,exclude,action){var parentID=arguments.length>3&&arguments[3]!==undefined?arguments[3]:null;var layer=arguments.length>4&&arguments[4]!==undefined?arguments[4]:0;var items=[];lists.forEach(function(item){if((layer!==0||item.parent_id)&&item.parent_id!==parentID)return;var thumb="img/no_cover.svg";if(item.thumb&&item.thumb.thumb){if(item.thumb.thumb==="uploads/thumb/"){if(item.thumb.type&&item.thumb.type.indexOf("video")>-1){thumb="img/play-icon.png";}}else{thumb=item.thumb.thumb;}}else if(item.size_variants){if(item.size_variants.thumb===null){if(item.type&&item.type.indexOf("video")>-1){thumb="img/play-icon.png";}}else{thumb=item.size_variants.thumb.url;}}if(!item.title)item.title=lychee.locale["UNTITLED"];var prefix=layer>0?"  ".repeat(layer-1)+"└ ":"";var html=lychee.html(_templateObject37||(_templateObject37=_taggedTemplateLiteral(["\n\t\t\t ","\n\t\t\t \"thumbnail\"\n\t\t\t
$","
\n\t\t\t "])),prefix,thumb,item.title);items.push({title:html,disabled:exclude.findIndex(function(id){return id===item.id;})!==-1,fn:function fn(){return action(item);}});if(item.albums&&item.albums.length>0){items=items.concat(contextMenu.buildList(item.albums,exclude,action,item.id,layer+1));}else{// Fallback for flat tree representation. Should not be +// needed anymore but shouldn't hurt either. +items=items.concat(contextMenu.buildList(lists,exclude,action,item.id,layer+1));}});return items;};/** + * @param {?string} albumID + * @param {jQuery.Event} e + * + * @returns {void} + */contextMenu.albumTitle=function(albumID,e){api.post("Albums::tree",{},function(data){var items=[];items=items.concat({title:lychee.locale["ROOT"],disabled:albumID===null,fn:function fn(){return lychee["goto"]();}});if(data.albums&&data.albums.length>0){items=items.concat({});items=items.concat(contextMenu.buildList(data.albums,albumID!==null?[albumID]:[],function(a){return lychee["goto"](a.id);}));}if(data.shared_albums&&data.shared_albums.length>0){items=items.concat({});items=items.concat(contextMenu.buildList(data.shared_albums,albumID!==null?[albumID]:[],function(a){return lychee["goto"](a.id);}));}if(albumID!==null&&!album.isSmartID(albumID)&&!album.isSearchID(albumID)&&album.isUploadable()){if(items.length>0){items.unshift({});}items.unshift({title:build.iconic("pencil")+lychee.locale["RENAME"],fn:function fn(){return album.setTitle([albumID]);}});}basicContext.show(items,e.originalEvent,contextMenu.close);});};/** + * @param {string} photoID + * @param {jQuery.Event} e + * + * @returns {void} + */contextMenu.photo=function(photoID,e){var coverActive=photoID===album.json.cover_id;var isPhotoStarred=album.getByID(photoID).is_starred;var items=[{title:build.iconic("star")+(isPhotoStarred?lychee.locale["UNSTAR"]:lychee.locale["STAR"]),fn:function fn(){return _photo3.setStar([photoID],!isPhotoStarred);}},{title:build.iconic("tag")+lychee.locale["TAG"],fn:function fn(){return _photo3.editTags([photoID]);}},// for future work, use a list of all the ancestors. +{title:build.iconic("folder-cover",coverActive?"active":"")+lychee.locale[coverActive?"REMOVE_COVER":"SET_COVER"],fn:function fn(){return album.toggleCover(photoID);}},{},{title:build.iconic("pencil")+lychee.locale["RENAME"],fn:function fn(){return _photo3.setTitle([photoID]);}},{title:build.iconic("layers")+lychee.locale["COPY_TO"],fn:function fn(){basicContext.close();contextMenu.move([photoID],e,_photo3.copyTo);}},// Notice for 'Move': +// fn must call basicContext.close() first, +// in order to keep the selection +{title:build.iconic("folder")+lychee.locale["MOVE"],fn:function fn(){basicContext.close();contextMenu.move([photoID],e,_photo3.setAlbum);}},{title:build.iconic("trash")+lychee.locale["DELETE"],fn:function fn(){return _photo3["delete"]([photoID]);}},{title:build.iconic("cloud-download")+lychee.locale["DOWNLOAD"],fn:function fn(){return _photo3.getArchive([photoID]);}}];if(album.isSmartID(album.getID())||album.isSearchID(album.getID())||album.isTagAlbum()){// Cover setting not supported for smart or tag albums and search results. +items.splice(2,1);}$('.photo[data-id="'+photoID+'"]').addClass("active");basicContext.show(items,e.originalEvent,contextMenu.close);};/** + * @param {string} photoID + * @param {string} albumID + * @param {DragEvent} e + * + * @returns {void} + */contextMenu.photoDrop=function(photoID,albumID,e){var items=[{title:build.iconic("folder")+lychee.locale["MOVE"],fn:function fn(){_photo3.setAlbum([photoID],albumID);}}];$('.photo[data-id="'+photoID+'"]').addClass("active");basicContext.show(items,e,contextMenu.close);};/** + * @param {string[]} photoIDs + * @param {jQuery.Event} e + */contextMenu.photoMulti=function(photoIDs,e){multiselect.stopResize();var arePhotosStarred=false;var arePhotosNotStarred=false;photoIDs.forEach(function(id){if(album.getByID(id).is_starred){arePhotosStarred=true;}else{arePhotosNotStarred=true;}});var items=[// Only show the star/unstar menu item when the selected photos are +// consistently either all starred or all not starred. +{title:build.iconic("star")+(arePhotosNotStarred?lychee.locale["STAR_ALL"]:lychee.locale["UNSTAR_ALL"]),visible:!(arePhotosStarred&&arePhotosNotStarred),fn:function fn(){return _photo3.setStar(photoIDs,arePhotosNotStarred);}},{title:build.iconic("tag")+lychee.locale["TAG_ALL"],fn:function fn(){return _photo3.editTags(photoIDs);}},{},{title:build.iconic("pencil")+lychee.locale["RENAME_ALL"],fn:function fn(){return _photo3.setTitle(photoIDs);}},{title:build.iconic("layers")+lychee.locale["COPY_ALL_TO"],fn:function fn(){basicContext.close();contextMenu.move(photoIDs,e,_photo3.copyTo);}},{title:build.iconic("folder")+lychee.locale["MOVE_ALL"],fn:function fn(){basicContext.close();contextMenu.move(photoIDs,e,_photo3.setAlbum);}},{title:build.iconic("trash")+lychee.locale["DELETE_ALL"],fn:function fn(){return _photo3["delete"](photoIDs);}},{title:build.iconic("cloud-download")+lychee.locale["DOWNLOAD_ALL"],fn:function fn(){return _photo3.getArchive(photoIDs,"ORIGINAL");}}];basicContext.show(items,e.originalEvent,contextMenu.close);};/** + * @param {string} albumID + * @param {string} photoID + * @param {jQuery.Event} e + */contextMenu.photoTitle=function(albumID,photoID,e){var items=[{title:build.iconic("pencil")+lychee.locale["RENAME"],fn:function fn(){return _photo3.setTitle([photoID]);}}];// Note: We can also have a photo without its parent album being loaded +// if the photo is a public photo within a private album +var photos=album.json?album.json.photos:[];if(photos.length>0){items.push({});items=items.concat(contextMenu.buildList(photos,[photoID],function(a){return lychee["goto"](albumID+"/"+a.id);}));}if(!album.isUploadable()){// Remove Rename and the spacer. +items.splice(0,2);}basicContext.show(items,e.originalEvent,contextMenu.close);};/** + * @param {string} photoID + * @param {jQuery.Event} e + */contextMenu.photoMore=function(photoID,e){// Show download-item when +// a) We are allowed to upload to the album +// b) the photo is explicitly marked as downloadable (v4-only) +// c) or, the album is explicitly marked as downloadable +var showDownload=album.isUploadable()||_photo3.json.grant_download;var showFull=!!(_photo3.json.size_variants.original.url&&_photo3.json.size_variants.original.url!=="");var items=[{title:build.iconic("fullscreen-enter")+lychee.locale["FULL_PHOTO"],visible:showFull,fn:function fn(){return window.open(_photo3.getDirectLink());}},{title:build.iconic("cloud-download")+lychee.locale["DOWNLOAD"],visible:showDownload,fn:function fn(){return _photo3.getArchive([photoID]);}}];if(album.isUploadable()){// prepend further buttons if menu bar is reduced on small screens +var button_visibility=$("#button_visibility");if(button_visibility&&button_visibility.css("display")==="none"){items.unshift({title:build.iconic("eye")+lychee.locale["VISIBILITY_PHOTO"],visible:lychee.enable_button_visibility,fn:function fn(){return _photo3.setProtectionPolicy(_photo3.getID());}});}var button_trash=$("#button_trash");if(button_trash&&button_trash.css("display")==="none"){items.unshift({title:build.iconic("trash")+lychee.locale["DELETE"],visible:lychee.enable_button_trash,fn:function fn(){return _photo3["delete"]([_photo3.getID()]);}});}var button_move=$("#button_move");if(button_move&&button_move.css("display")==="none"){items.unshift({title:build.iconic("folder")+lychee.locale["MOVE"],visible:lychee.enable_button_move,fn:function fn(event){return contextMenu.move([_photo3.getID()],event,_photo3.setAlbum);}});}/* The condition below is copied from view.photo.header() */if(!(_photo3.json.type&&(_photo3.json.type.indexOf("video")===0||_photo3.json.type==="raw")||_photo3.json.live_photo_url!==""&&_photo3.json.live_photo_url!==null)){var button_rotate_cwise=$("#button_rotate_cwise");if(button_rotate_cwise&&button_rotate_cwise.css("display")==="none"){items.unshift({title:build.iconic("clockwise")+lychee.locale["PHOTO_EDIT_ROTATECWISE"],visible:lychee.enable_button_move,fn:function fn(){return photoeditor.rotate(_photo3.getID(),1);}});}var button_rotate_ccwise=$("#button_rotate_ccwise");if(button_rotate_ccwise&&button_rotate_ccwise.css("display")==="none"){items.unshift({title:build.iconic("counterclockwise")+lychee.locale["PHOTO_EDIT_ROTATECCWISE"],visible:lychee.enable_button_move,fn:function fn(){return photoeditor.rotate(_photo3.getID(),-1);}});}}}basicContext.show(items,e.originalEvent);};/** + * @param {Album[]} albums + * @param {string} albumID + * + * @returns {string[]} + */contextMenu.getSubIDs=function(albums,albumID){var ids=[albumID];albums.forEach(function(album){if(album.parent_id===albumID){ids=ids.concat(contextMenu.getSubIDs(albums,album.id));}if(album.albums&&album.albums.length>0){ids=ids.concat(contextMenu.getSubIDs(album.albums,albumID));}});return ids;};/** + * @callback TargetAlbumSelectedCB + * + * Called by {@link contextMenu.move} after the user has selected a target ID. + * In most cases, {@link album.setAlbum} or {@link photo.setAlbum} are + * directly used as the callback. + * This design decision is the only reason, why this callback gets more + * parameters than the selected target ID. + * The parameter signature of this callback matches {@link album.setAlbum}. + * + * However, the callback should actually enclose all other parameters it + * needs and only receive the target ID. + * + * TODO: Re-factor callbacks. + * + * @param {string[]} IDs the source IDs + * @param {?string} targetID the ID of the target album + * @param {boolean} [confirm] indicates whether the callback shall show a + * confirmation dialog to the user for whatever to + * callback is going to do + * @returns {void} + */ /** + * Shows the context menu with the album tree and allows the user to select a target album. + * + * **ATTENTION:** The name `move` of this method is very badly chosen. + * The method does not move anything, but only shows the menu and reports + * the selected album. + * In particular, the method is used by any operation which needs a target + * album (i.e. merge, copy-to, etc.) + * + * TODO: Find a better name for this function. + * + * The method calls the provided callback after the user has selected a + * target album and passes the ID of the target album together with the + * source `IDs` and the event `e` to the callback. + * + * TODO: Actually the callbacks should enclose all additional parameters (e.g., `IDs`) they need. Refactor the callbacks. + * + * The name of the root node in the context menu may be provided by the caller + * depending on the use-case. + * Keep in mind, that the root album is not visible to the user during normal + * browsing. + * Photos on the root level are stashed away into a virtual album called + * "Unsorted". + * Albums on the root level are shown as siblings, but the root node itself + * is invisible. + * So the user actually sees a forest. + * Hence, the root node should be named differently to meet the user's + * expectations. + * When the user moves/copies/merges photos, then the root node should be + * called "Unsorted". + * When the user moves/copies/merges albums, then the root node should be + * called "Root". + * + * @param {string[]} IDs - IDs of source objects (either album or photo IDs) + * @param {jQuery.Event} e - Some (?) event + * @param {TargetAlbumSelectedCB} callback - to be called after the user has selected a target ID + * @param {string} [kind=UNSORTED] - Name of root album; either "UNSORTED" or "ROOT" + * @param {boolean} [display_root=true] - Whether the root (aka unsorted) album shall be shown + */contextMenu.move=function(IDs,e,callback){var kind=arguments.length>3&&arguments[3]!==undefined?arguments[3]:"UNSORTED";var display_root=arguments.length>4&&arguments[4]!==undefined?arguments[4]:true;var items=[];api.post("Albums::tree",{},function(data){var addItems=function addItems(albums){// Disable all children +// It's not possible to move us into them +var i,s;var exclude=[];for(i=0;i0){// items = items.concat(contextMenu.buildList(data.albums, [ album.getID() ], (a) => callback(IDs, a.id))); //photo.setAlbum +addItems(data.albums);}if(data.shared_albums&&data.shared_albums.length>0&&lychee.rights.settings.can_edit){items=items.concat({});addItems(data.shared_albums);}// Show Unsorted when unsorted is not the current album +if(display_root&&album.getID()!=="unsorted"&&!visible.albums()){items.unshift({});items.unshift({title:lychee.locale[kind],fn:function fn(){return callback(IDs,null);}});}// Don't allow to move the current album to a newly created subalbum +// (creating a cycle). +if(IDs.length!==1||IDs[0]!==(album.json?album.json.id:null)||callback!==album.setAlbum){items.unshift({});items.unshift({title:lychee.locale["NEW_ALBUM"],fn:function fn(){return album.add(IDs,callback);}});}basicContext.show(items,e.originalEvent,contextMenu.close);});};/** + * @param {string} photoID + * @param {jQuery.Event} e + * + * @returns {void} + */contextMenu.sharePhoto=function(photoID,e){if(!lychee.share_button_visible){return;}var iconClass="ionicons";var items=[{title:build.iconic("twitter",iconClass)+"Twitter",fn:function fn(){return _photo3.share(photoID,"twitter");}},{title:build.iconic("facebook",iconClass)+"Facebook",fn:function fn(){return _photo3.share(photoID,"facebook");}},{title:build.iconic("envelope-closed")+"Mail",fn:function fn(){return _photo3.share(photoID,"mail");}},{title:build.iconic("dropbox",iconClass)+"Dropbox",visible:lychee.rights.settings.can_edit===true,fn:function fn(){return _photo3.share(photoID,"dropbox");}},{title:build.iconic("link-intact")+lychee.locale["DIRECT_LINKS"],fn:function fn(){return _photo3.showDirectLinks(photoID);}},{title:build.iconic("grid-two-up")+lychee.locale["QR_CODE"],fn:function fn(){return _photo3.qrCode(photoID);}}];basicContext.show(items,e.originalEvent);};/** + * @param {string} albumID + * @param {jQuery.Event} e + * + * @returns {void} + */contextMenu.shareAlbum=function(albumID,e){if(!lychee.share_button_visible){return;}var iconClass="ionicons";var items=[{title:build.iconic("twitter",iconClass)+"Twitter",fn:function fn(){return album.share("twitter");}},{title:build.iconic("facebook",iconClass)+"Facebook",fn:function fn(){return album.share("facebook");}},{title:build.iconic("envelope-closed")+"Mail",fn:function fn(){return album.share("mail");}},{title:build.iconic("link-intact")+lychee.locale["DIRECT_LINK"],fn:function fn(){var url=lychee.getBaseUrl()+"r/"+albumID;if(album.json.policy.is_password_required){// Copy the url with prefilled password param +url+="?password=";}navigator.clipboard.writeText(url).then(function(){return loadingBar.show("success",lychee.locale["URL_COPIED_TO_CLIPBOARD"]);});}},{title:build.iconic("grid-two-up")+lychee.locale["QR_CODE"],fn:function fn(){return album.qrCode();}}];basicContext.show(items,e.originalEvent);};/** + * @returns {void} + */contextMenu.close=function(){if(!visible.contextMenu())return;basicContext.close();multiselect.clearSelection();if(visible.multiselect()){multiselect.close();}};var frame={/** @type {?Photo} */photo:null,/** @type {Number} */nextTimeOutId:0,_dom:{/** + * Hidden image element with thumb variant of current image used + * as a source for blurring. + * @type {?HTMLImageElement} + */bgImage:null,/** + * Canvas element which shows the blurry variant of `bgImage`. + * @type {?HTMLCanvasElement} + */canvas:null,/** + * Image element which displays the full-size image + * @type {?HTMLImageElement} + */image:null,/** + * Div element which works as a shutter to blend over between + * subsequent photos + * @type {?HTMLDivElement} + */shutter:null}};/** + * Determines whether the photo loading loop of the frame mode is currently + * running. + * @returns {boolean} + */frame.isRunning=function(){return frame.nextTimeOutId!==0;};/** + * Stops loading images for frame mode. + * @returns {void} + */frame.stop=function(){if(frame.nextTimeOutId!==0){clearTimeout(frame.nextTimeOutId);}frame.photo=null;frame.nextTimeOutId=0;};/** + * Initializes the DOM (if called for the very first time), sets the frontend + * into "frame mode" and enters the photo loading loop. + * + * @returns {void} + */frame.initAndStart=function(){lychee.setMode("frame");if(frame._dom.bgImage===null){frame._dom.bgImage=document.getElementById("lychee_frame_bg_image");frame._dom.bgImage.addEventListener("load",function(){// After a new background image has been loaded, draw a blurry +// version on the canvas. +StackBlur.image(frame._dom.bgImage,frame._dom.canvas,20);// We must reset the canvas to its originally defined dimensions +// as StackBlur resets it. +frame._dom.canvas.style.width=null;frame._dom.canvas.style.height=null;});}if(frame._dom.canvas===null){frame._dom.canvas=document.getElementById("lychee_frame_bg_canvas");}if(frame._dom.image===null){frame._dom.image=document.getElementById("lychee_frame_image");frame._dom.image.addEventListener("load",function(){// After a new image has been loaded, open the shutter +frame._dom.shutter.classList.add("opened");});}if(frame._dom.shutter===null){frame._dom.shutter=document.getElementById("lychee_frame_shutter");}// We also must call the very first invocation of `runPhotoLoop` +// asynchronously to ensure that `nextTimeOutId` is also set for the first +// call, otherwise `frame.isRunning` and `frame.stop` will report false +// results and not work during the first invocation. +frame.nextTimeOutId=setTimeout(function(){return frame.runPhotoLoop();},0);};/** + * Repeatedly loads random photos every {@link lychee.mod_frame_refresh} + * interval. + * + * The method stops loading photos when {@link frame.stop} is called. + * + * @returns {void} + */frame.runPhotoLoop=function(){/** + * Forwards loaded photo to handler and recalls this method after the + * refresh timeout unless the loop hasn't been stopped in the meantime. + * + * @param {Photo} data + * @returns {void} + */var onSuccess=function onSuccess(data){frame.onRandomPhotoLoaded(data);if(frame.nextTimeOutId!==0){frame.nextTimeOutId=setTimeout(function(){return frame.runPhotoLoop();},1000*lychee.mod_frame_refresh);}};// Closes the shutter and loads a new, random photo after that. +// The CSS defines that the shutter takes 1s to close; hence the +// 1s of timeout here and the duration of the animation as defined in the +// CSS must be aligned to for a pleasant visual experience. +frame._dom.shutter.classList.remove("opened");// Only set the timeout, if the loop hasn't been stopped in the +// meantime +if(frame.nextTimeOutId!==0){frame.nextTimeOutId=setTimeout(function(){return api.post("Photo::getRandom",{},onSuccess);},1000);}};/** + * Attempts to load a random photo from the backend. + * + * Upon success, the method calls {@link frame.onRandomPhotoLoaded} followed + * by `successCallback` in that order. + * Upon error, the method calls `errorCallback`. + * + * @param {APISuccessCB} successCallback + * @param {APIErrorCB} errorCallback + * @returns {void} + */frame.loadRandomPhoto=function(successCallback,errorCallback){api.post("Photo::getRandom",{},/** @param {Photo} data */function(data){frame.onRandomPhotoLoaded(data);successCallback(data);},null,errorCallback);};/** + * Displays the given photo in the central image area of the frame mode. + * + * This method is called by {@link frame.runPhotoLoop} for each successfully + * loaded, random photo. + * + * @param {Photo} photo + * + * @returns {void} + */frame.onRandomPhotoLoaded=function(photo){if(photo.size_variants.thumb){frame._dom.bgImage.src=photo.size_variants.thumb.url;}else{frame._dom.bgImage.src="";console.log("Thumb not found");}frame.photo=photo;frame._dom.image.src=photo.size_variants.medium!==null?photo.size_variants.medium.url:photo.size_variants.original.url;frame._dom.image.srcset=photo.size_variants.medium!==null&&photo.size_variants.medium2x!==null?"".concat(photo.size_variants.medium.url," ").concat(photo.size_variants.medium.width,"w, ").concat(photo.size_variants.medium2x.url," ").concat(photo.size_variants.medium2x.width,"w"):"";frame.resize();};/** + * @returns {void} + */frame.resize=function(){if(frame.photo&&frame._dom.image){var ratio=frame.photo.size_variants.original.height>0?frame.photo.size_variants.original.width/frame.photo.size_variants.original.height:1;// Our math assumes that the image occupies the whole frame. That's +// not quite the case (the default css sets it to 95%) but it's close +// enough. +var width=window.innerWidth/ratio>window.innerHeight?window.innerHeight*ratio:window.innerWidth;frame._dom.image.sizes=""+width+"px";}};/** + * @description This module takes care of the header. + */ /** + * @namespace + * @property {jQuery} _dom + */var header={_dom:$("#lychee_toolbar_container")};/** + * @param {?string} [selector=null] + * @returns {jQuery} + */header.dom=function(selector){if(selector==null||selector==="")return header._dom;return header._dom.find(selector);};/** + * @returns {void} + */header.bind=function(){// Event Name +var eventName="click";header.dom(".header__title").on(eventName,function(e){if($(this).hasClass("header__title--editable")===false)return false;if(lychee.enable_contextmenu_header===false)return false;if(visible.photo())contextMenu.photoTitle(album.getID(),_photo3.getID(),e);else contextMenu.albumTitle(album.getID(),e);});header.dom("#button_visibility").on(eventName,function(){_photo3.setProtectionPolicy(_photo3.getID());});header.dom("#button_share").on(eventName,function(e){contextMenu.sharePhoto(_photo3.getID(),e);});header.dom("#button_visibility_album").on(eventName,function(){album.setProtectionPolicy(album.getID());});header.dom("#button_sharing_album_users").on(eventName,function(){album.shareUsers(album.getID());});header.dom("#button_share_album").on(eventName,function(e){contextMenu.shareAlbum(album.getID(),e);});header.dom("#button_signin").on(eventName,lychee.loginDialog);header.dom("#button_settings").on(eventName,function(e){leftMenu.open();});header.dom("#button_close_config").on(eventName,function(){tabindex.makeFocusable(header.dom());tabindex.makeFocusable(lychee.content);tabindex.makeUnfocusable(leftMenu._dom);multiselect.bind();lychee.load();});header.dom("#button_info_album").on(eventName,function(){_sidebar.toggle(true);});header.dom("#button_info").on(eventName,function(){_sidebar.toggle(true);});header.dom(".button--map-albums").on(eventName,function(){lychee.gotoMap();});header.dom("#button_map_album").on(eventName,function(){lychee.gotoMap(album.getID());});header.dom("#button_map").on(eventName,function(){lychee.gotoMap(album.getID());});header.dom(".button_add").on(eventName,contextMenu.add);header.dom("#button_more").on(eventName,function(e){contextMenu.photoMore(_photo3.getID(),e);});header.dom("#button_move_album").on(eventName,function(e){contextMenu.move([album.getID()],e,album.setAlbum,"ROOT",album.getParentID()!=null);});header.dom("#button_nsfw_album").on(eventName,function(){album.toggleNSFW();});header.dom("#button_move").on(eventName,function(e){contextMenu.move([_photo3.getID()],e,_photo3.setAlbum);});header.dom(".header__hostedwith").on(eventName,function(){window.open(lychee.website);});header.dom("#button_trash_album").on(eventName,function(){album["delete"]([album.getID()]);});header.dom("#button_trash").on(eventName,function(){_photo3["delete"]([_photo3.getID()]);});header.dom("#button_archive").on(eventName,function(){album.getArchive([album.getID()]);});header.dom("#button_star").on(eventName,function(){_photo3.toggleStar();});header.dom("#button_rotate_ccwise").on(eventName,function(){photoeditor.rotate(_photo3.getID(),-1);});header.dom("#button_rotate_cwise").on(eventName,function(){photoeditor.rotate(_photo3.getID(),1);});header.dom("#button_back_home").on(eventName,function(){if(!album.json.parent_id){lychee["goto"]();}else{lychee["goto"](album.getParentID());}});header.dom("#button_back").on(eventName,function(){lychee["goto"](album.getID());});header.dom("#button_back_map").on(eventName,function(){lychee["goto"](album.getID());});header.dom("#button_fs_album_enter,#button_fs_enter").on(eventName,lychee.fullscreenEnter);header.dom("#button_fs_album_exit,#button_fs_exit").on(eventName,lychee.fullscreenExit).hide();header.dom(".header__search").on("keyup click",function(){if($(this).val().length>0){lychee["goto"](SearchAlbumIDPrefix+"/"+encodeURIComponent($(this).val()));}else if(search.json!==null){search.reset();}});header.dom(".header__clear").on(eventName,function(){search.reset();});header.bind_back();};/** + * @returns {void} + */header.bind_back=function(){header.dom(".header__title").on("click touchend",function(){if(lychee.landing_page_enable&&visible.albums()){window.location.href=".";}else{return false;}});};/** + * @returns {void} + */header.show=function(){lychee.imageview.removeClass("full");header.dom().removeClass("hidden");tabindex.restoreSettings(header.dom());};/** + * @returns {void} + */header.hideIfLivePhotoNotPlaying=function(){// Hides the header, if current live photo is not playing +if(!_photo3.isLivePhotoPlaying())header.hide();};/** + * @returns {void} + */header.hide=function(){if(visible.photo()&&!visible.sidebar()&&!visible.contextMenu()&&basicModal.isVisible()===false){tabindex.saveSettings(header.dom());tabindex.makeUnfocusable(header.dom());lychee.imageview.addClass("full");header.dom().addClass("hidden");}};/** + * @param {string} title + * @returns {void} + */header.setTitle=function(title){var $title=header.dom(".header__title");var html=lychee.html(_templateObject38||(_templateObject38=_taggedTemplateLiteral(["$","",""])),title,build.iconic("caret-bottom"));$title.html(html);};/** + * Applies the "mode" of the application to the header. + * + * Note, in contrast to {@link lychee.setMode} this method does **not** + * support the mode "view". + * Reminder: The mode "view" is used to display a single photo in Lychee's + * special "view" mode which is somewhat similar to "public" for albums. + * In lack of a dedicated "view" mode here, the method must be called with + * `mode === "photo"` (even in "view" mode) and then this method internally + * depends on {@link lychee.publicMode} being set to hide certain buttons. + * Note: This strange design decision has (assumingly) been made, because + * both the view and photo mode call {@link view.photo.show} which in turn + * calls this method and passes `"photo"` as the parameter in both cases. + * + * @param {string} mode either one out of `"public"`, `"albums"`, `"album"`, + * `"photo"`, `"map"` or `"config"` + * @returns {void} + */header.setMode=function(mode){if(mode==="albums"&&lychee.publicMode===true)mode="public";switch(mode){case"public":header.dom(".toolbar").removeClass("visible");header.dom("#lychee_toolbar_public").addClass("visible");tabindex.makeUnfocusable(header.dom(".toolbar"));tabindex.makeFocusable(header.dom("#lychee_toolbar_public"));if(lychee.public_search){var e=$(".header__search, .header__clear","#lychee_toolbar_public");e.show();tabindex.makeFocusable(e);}else{var _e2=$(".header__search, .header__clear","#lychee_toolbar_public");_e2.hide();tabindex.makeUnfocusable(_e2);}// Set icon in Public mode +if(lychee.map_display_public){var _e3=$(".button--map-albums","#lychee_toolbar_public");_e3.show();tabindex.makeFocusable(_e3);}else{var _e4=$(".button--map-albums","#lychee_toolbar_public");_e4.hide();tabindex.makeUnfocusable(_e4);}// Set focus on login button +if(lychee.active_focus_on_page_load){$("#button_signin").focus();}return;case"albums":header.dom(".toolbar").removeClass("visible");header.dom("#lychee_toolbar_albums").addClass("visible");tabindex.makeUnfocusable(header.dom(".toolbar"));tabindex.makeFocusable(header.dom("#lychee_toolbar_albums"));// If map is disabled, we should hide the icon +if(lychee.map_display){var _e5=$(".button--map-albums","#lychee_toolbar_albums");_e5.show();tabindex.makeFocusable(_e5);}else{var _e6=$(".button--map-albums","#lychee_toolbar_albums");_e6.hide();tabindex.makeUnfocusable(_e6);}if(lychee.enable_button_add&&lychee.rights.root_album.can_upload){var _e7=$(".button_add","#lychee_toolbar_albums");_e7.show();tabindex.makeFocusable(_e7);}else{var _e8=$(".button_add","#lychee_toolbar_albums");_e8.remove();}return;case"album":var albumID=album.getID();header.dom(".toolbar").removeClass("visible");header.dom("#lychee_toolbar_album").addClass("visible");tabindex.makeUnfocusable(header.dom(".toolbar"));tabindex.makeFocusable(header.dom("#lychee_toolbar_album"));// Hide download button when album empty or we are not allowed to +// upload to it and it's not explicitly marked as downloadable. +if(!album.json||album.json.photos.length===0&&album.json.albums&&album.json.albums.length===0||!album.json.rights.can_download){var _e9=$("#button_archive");_e9.hide();tabindex.makeUnfocusable(_e9);}else{var _e10=$("#button_archive");_e10.show();tabindex.makeFocusable(_e10);}if(!lychee.share_button_visible){var _e11=$("#button_share_album");_e11.hide();tabindex.makeUnfocusable(_e11);}else{var _e12=$("#button_share_album");_e12.show();tabindex.makeFocusable(_e12);}// If map is disabled, we should hide the icon +if(lychee.publicMode===true?lychee.map_display_public:lychee.map_display){var _e13=$("#button_map_album");_e13.show();tabindex.makeFocusable(_e13);}else{var _e14=$("#button_map_album");_e14.hide();tabindex.makeUnfocusable(_e14);}if(albumID===SmartAlbumID.STARRED||albumID===SmartAlbumID.PUBLIC||albumID===SmartAlbumID.RECENT||albumID===SmartAlbumID.ON_THIS_DAY){$("#button_nsfw_album, #button_info_album, #button_trash_album, #button_visibility_album, #button_sharing_album_users, #button_move_album").hide();if(album.isUploadable()){$(".button_add, .header__divider","#lychee_toolbar_album").show();tabindex.makeFocusable($(".button_add, .header__divider","#lychee_toolbar_album"));}else{$(".button_add, .header__divider","#lychee_toolbar_album").hide();tabindex.makeUnfocusable($(".button_add, .header__divider","#lychee_toolbar_album"));}tabindex.makeUnfocusable($("#button_nsfw_album, #button_info_album, #button_trash_album, #button_visibility_album, #button_sharing_album_users, #button_move_album"));}else if(albumID===SmartAlbumID.UNSORTED){$("#button_nsfw_album, #button_info_album, #button_visibility_album, #button_sharing_album_users, #button_move_album").hide();$("#button_trash_album, .button_add, .header__divider","#lychee_toolbar_album").show();tabindex.makeFocusable($("#button_trash_album, .button_add, .header__divider","#lychee_toolbar_album"));tabindex.makeUnfocusable($("#button_nsfw_album, #button_info_album, #button_visibility_album, #button_sharing_album_users, #button_move_album"));}else if(album.isTagAlbum()){$("#button_info_album").show();if(_sidebar.keepSidebarVisible()&&!visible.sidebar())_sidebar.toggle(false);$("#button_move_album").hide();$(".button_add, .header__divider","#lychee_toolbar_album").hide();tabindex.makeFocusable($("#button_info_album"));tabindex.makeUnfocusable($("#button_move_album"));tabindex.makeUnfocusable($(".button_add, .header__divider","#lychee_toolbar_album"));if(album.isUploadable()){$("#button_nsfw_album, #button_visibility_album, #button_sharing_album_users, #button_trash_album").show();tabindex.makeFocusable($("#button_nsfw_album, #button_visibility_album, #button_sharing_album_users, #button_trash_album"));if($("#button_visibility_album").is(":hidden")){// This can happen with narrow screens. In that +// case we re-enable the add button which will +// contain the overflow items. +$(".button_add, .header__divider","#lychee_toolbar_album").show();tabindex.makeFocusable($(".button_add, .header__divider","#lychee_toolbar_album"));}}else{$("#button_nsfw_album, #button_visibility_album, #button_sharing_album_users, #button_trash_album").hide();tabindex.makeUnfocusable($("#button_nsfw_album, #button_visibility_album, #button_sharing_album_users, #button_trash_album"));}}else{$("#button_info_album").show();if(_sidebar.keepSidebarVisible()&&!visible.sidebar())_sidebar.toggle(false);tabindex.makeFocusable($("#button_info_album"));if(album.isUploadable()){$("#button_nsfw_album, #button_trash_album, #button_move_album, #button_visibility_album, #button_sharing_album_users, .button_add, .header__divider","#lychee_toolbar_album").show();tabindex.makeFocusable($("#button_nsfw_album, #button_trash_album, #button_move_album, #button_visibility_album, #button_sharing_album_users, .button_add, .header__divider","#lychee_toolbar_album"));}else{$("#button_nsfw_album, #button_trash_album, #button_move_album, #button_visibility_album, #button_sharing_album_users, .button_add, .header__divider","#lychee_toolbar_album").hide();tabindex.makeUnfocusable($("#button_nsfw_album, #button_trash_album, #button_move_album, #button_visibility_album, #button_sharing_album_users, .button_add, .header__divider","#lychee_toolbar_album"));}}// Remove buttons if needed +if(!lychee.enable_button_visibility){var _e15=$("#button_visibility_album","#button_sharing_album_users","#lychee_toolbar_album");_e15.remove();}if(!lychee.enable_button_share||!lychee.share_button_visible){var _e16=$("#button_share_album","#lychee_toolbar_album");_e16.remove();}if(!lychee.enable_button_archive){var _e17=$("#button_archive","#lychee_toolbar_album");_e17.remove();}if(!lychee.enable_button_move){var _e18=$("#button_move_album","#lychee_toolbar_album");_e18.remove();}if(!lychee.enable_button_trash){var _e19=$("#button_trash_album","#lychee_toolbar_album");_e19.remove();}if(!lychee.enable_button_fullscreen||!lychee.fullscreenAvailable()){var _e20=$("#button_fs_album_enter","#lychee_toolbar_album");_e20.remove();}if(!lychee.enable_button_add){var _e21=$(".button_add","#lychee_toolbar_album");_e21.remove();}return;case"photo":header.dom(".toolbar").removeClass("visible");header.dom("#lychee_toolbar_photo").addClass("visible");tabindex.makeUnfocusable(header.dom(".toolbar"));tabindex.makeFocusable(header.dom("#lychee_toolbar_photo"));// If map is disabled, we should hide the icon +if(lychee.publicMode===true?lychee.map_display_public:lychee.map_display){var _e22=$("#button_map");_e22.show();tabindex.makeFocusable(_e22);}else{var _e23=$("#button_map");_e23.hide();tabindex.makeUnfocusable(_e23);}if(lychee.enable_button_move&&album.isUploadable()){var _e24=$("#button_move");_e24.show();tabindex.makeFocusable(_e24);}else{var _e25=$("#button_move");_e25.hide();tabindex.makeUnfocusable(_e25);}if(!lychee.share_button_visible){var _e26=$("#button_share");_e26.hide();tabindex.makeUnfocusable(_e26);}else{var _e27=$("#button_share");_e27.show();tabindex.makeFocusable(_e27);}if(lychee.enable_button_trash&&album.isUploadable()){var _e28=$("#button_trash");_e28.show();tabindex.makeFocusable(_e28);}else{var _e29=$("#button_trash");_e29.hide();tabindex.makeUnfocusable(_e29);}if(lychee.enable_button_fullscreen&&lychee.fullscreenAvailable()){var _e30=$("#button_fs_enter");_e30.show();}else{var _e31=$("#button_fs_enter");_e31.hide();}// Hide More menu if +// - empty (see contextMenu.photoMore) +// - not enabled +if(!lychee.enable_button_more||!(// +album.isUploadable()||_photo3.json&&(_photo3.json.rights.can_download||_photo3.json.rights.can_access_full_photo&&_photo3.json.size_variants.original.url))){var _e32=$("#button_more");_e32.hide();tabindex.makeUnfocusable(_e32);}else{var _e33=$("#button_more");_e33.show();tabindex.makeFocusable(_e33);}// Hide buttons if needed +if(lychee.publicMode){var _e34=$("#button_star","#lychee_toolbar_photo");_e34.hide();tabindex.makeUnfocusable(_e34);}else{var _e35=$("#button_star","#lychee_toolbar_photo");_e35.show();}if(!lychee.enable_button_visibility||lychee.publicMode){var _e36=$("#button_visibility","#lychee_toolbar_photo");_e36.hide();}else{var _e37=$("#button_visibility","#lychee_toolbar_photo");_e37.show();}if(!lychee.enable_button_share||!lychee.share_button_visible){var _e38=$("#button_share","#lychee_toolbar_photo");_e38.hide();}else{var _e39=$("#button_share","#lychee_toolbar_photo");_e39.show();}if(!lychee.enable_button_move||lychee.publicMode){var _e40=$("#button_move","#lychee_toolbar_photo");_e40.hide();}else{var _e41=$("#button_move","#lychee_toolbar_photo");_e41.show();}if(!lychee.enable_button_trash||lychee.publicMode){var _e42=$("#button_trash","#lychee_toolbar_photo");_e42.hide();}else{var _e43=$("#button_trash","#lychee_toolbar_photo");_e43.show();}if(!lychee.enable_button_fullscreen||!lychee.fullscreenAvailable()||lychee.publicMode){var _e44=$("#button_fs_enter","#lychee_toolbar_photo");_e44.hide();}else{var _e45=$("#button_fs_enter","#lychee_toolbar_photo");_e45.show();}if(!lychee.enable_button_rotate||lychee.publicMode){var _e46=$("#button_rotate_cwise","#lychee_toolbar_photo");_e46.hide();_e46=$("#button_rotate_ccwise","#lychee_toolbar_photo");_e46.hide();}else{var _e47=$("#button_rotate_cwise","#lychee_toolbar_photo");_e47.show();_e47=$("#button_rotate_ccwise","#lychee_toolbar_photo");_e47.show();tabindex.makeFocusable(_e47);}return;case"map":header.dom(".toolbar").removeClass("visible");header.dom("#lychee_toolbar_map").addClass("visible");tabindex.makeUnfocusable(header.dom(".toolbar"));tabindex.makeFocusable(header.dom("#lychee_toolbar_map"));return;case"config":header.dom(".toolbar").removeClass("visible");header.dom("#lychee_toolbar_config").addClass("visible");return;}};/** + * Note that the pull-down menu is now enabled not only for editable + * items but for all of public/albums/album/photo views, so 'editable' is a + * bit of a misnomer at this point... + * + * @param {boolean} editable + * @returns {void} + */header.setEditable=function(editable){var $title=header.dom(".header__title");if(editable&&!lychee.publicMode)$title.addClass("header__title--editable");else $title.removeClass("header__title--editable");};/** + * @description This module is used for bindings. + */$(document).ready(function(){// Event Name +var eventName="click touchend";// Set API error handler +api.onError=lychee.handleAPIError;// Make the application visible; initially the `` has an inline +// style `display: none` to avoid an ugly flash of massively over-sized +// icons from the header in case the HTML engine starts rendering before +// the (asynchronously loaded) CSS becomes available. +document.querySelector("body").style.display=null;// Multiselect +multiselect.bind();// Header +header.bind();// Image View +lychee.imageview.on(eventName,".arrow_wrapper--previous",function(){return _photo3.previous(false);}).on(eventName,".arrow_wrapper--next",function(){return _photo3.next(false);}).on(eventName,"img, #livephoto",function(){return _photo3.cycle_display_overlay();});// Keyboard +Mousetrap.addKeycodes({18:"ContextMenu",179:"play_pause",227:"rewind",228:"forward"});Mousetrap.bind(["l"],function(){lychee.loginDialog();return false;}).bind(["k"],function(){u2f.login();return false;}).bind(["left"],function(){if(visible.photo()&&(!visible.header()||$("img#image").is(":focus")||$("img#livephoto").is(":focus")||$(":focus").length===0)){$("#imageview a#previous").click();return false;}return true;}).bind(["right"],function(){if(visible.photo()&&(!visible.header()||$("img#image").is(":focus")||$("img#livephoto").is(":focus")||$(":focus").length===0)){$("#imageview a#next").click();return false;}return true;}).bind(["u"],function(){if(!visible.photo()&&album.isUploadable()&&!album.isTagAlbum()){$("#upload_files").click();return false;}}).bind(["n"],function(){if(!visible.photo()&&album.isUploadable()){album.add();return false;}}).bind(["s"],function(){if(visible.photo()&&album.isUploadable()){header.dom("#button_star").click();return false;}else if(visible.albums()){header.dom(".header__search").focus();return false;}}).bind(["r"],function(){if(album.isUploadable()){if(visible.album()){album.setTitle([album.getID()]);return false;}else if(visible.photo()){_photo3.setTitle([_photo3.getID()]);return false;}}}).bind(["h"],function(){lychee.nsfw_visible=!lychee.nsfw_visible;album.apply_nsfw_filter();return false;}).bind(["d"],function(){if(album.isUploadable()){if(visible.photo()){_photo3.setDescription(_photo3.getID());return false;}else if(visible.album()){album.setDescription(album.getID());return false;}}}).bind(["t"],function(){if(visible.photo()&&album.isUploadable()){_photo3.editTags([_photo3.getID()]);return false;}}).bind(["i","ContextMenu"],function(){if(!visible.multiselect()){_sidebar.toggle(true);return false;}}).bind(["command+backspace","ctrl+backspace"],function(){if(album.isUploadable()){if(visible.photo()&&basicModal.isVisible()===false){_photo3["delete"]([_photo3.getID()]);return false;}else if(visible.album()&&basicModal.isVisible()===false){album["delete"]([album.getID()]);return false;}}}).bind(["command+a","ctrl+a"],function(){if(visible.album()&&basicModal.isVisible()===false){multiselect.selectAll();return false;}else if(visible.albums()&&basicModal.isVisible()===false){multiselect.selectAll();return false;}}).bind(["o"],function(){if(visible.photo()){_photo3.cycle_display_overlay();return false;}}).bind(["f"],function(){if(visible.album()||visible.photo()){lychee.fullscreenToggle();return false;}});Mousetrap.bind(["play_pause"],function(){// If it's a video, we toggle play/pause +var video=$("video");if(video.length!==0){if(video[0].paused){video[0].play();}else{video[0].pause();}}});Mousetrap.bindGlobal("enter",function(){if(basicModal.isVisible()===true){// check if any of the input fields is focussed +// apply action, other do nothing +if($(".basicModal__content input").is(":focus")){basicModal.action();return false;}}else if(visible.photo()&&!lychee.header_auto_hide&&($("img#image").is(":focus")||$("img#livephoto").is(":focus")||$(":focus").length===0)){if(visible.header()){header.hide();}else{header.show();}return false;}var clicked=false;$(":focus").each(function(){if(!$(this).is("input")&&!$(this).is("textarea")){$(this).click();clicked=true;}});if(clicked){return false;}});// Prevent 'esc keyup' event to trigger 'go back in history' +// and 'alt keyup' to show a webapp context menu for Fire TV +Mousetrap.bindGlobal(["esc","ContextMenu"],function(){return false;},"keyup");Mousetrap.bindGlobal(["esc","command+up"],function(){if(basicModal.isVisible()===true)basicModal.cancel();else if(visible.config()||visible.leftMenu())leftMenu.close();else if(visible.contextMenu())contextMenu.close();else if(visible.photo())lychee["goto"](album.getID());else if(visible.album()&&!album.json.parent_id)lychee["goto"]();else if(visible.album())lychee["goto"](album.getParentID());else if(visible.albums()&&search.json!==null)search.reset();else if(visible.mapview())mapview.close();else if(visible.albums()&&lychee.enable_close_tab_on_esc){window.open("","_self").close();}return false;});$(document)// Fullscreen on mobile +.on("touchend","#imageview #image",function(){// prevent triggering event 'mousemove' +// why? this also prevents 'click' from firing which results in unexpected behaviour +// unable to reproduce problems arising from 'mousemove' on iOS devices +// e.preventDefault(); +if(_typeof(swipe.obj)===null||Math.abs(swipe.offsetX)<=5&&Math.abs(swipe.offsetY)<=5){// Toggle header only if we're not moving to next/previous photo; +// In this case, swipe.preventNextHeaderToggle is set to true +if(!swipe.preventNextHeaderToggle){if(visible.header()){header.hide();}else{header.show();}}// For next 'touchend', behave again as normal and toggle header +swipe.preventNextHeaderToggle=false;}});$("#imageview")// Swipe on mobile +.swipe().on("swipeStart",function(){if(visible.photo())swipe.start($("#imageview #image, #imageview #livephoto"));}).swipe().on("swipeMove",/** @param {jQuery.Event} e */function(e){if(visible.photo())swipe.move(e.swipe);}).swipe().on("swipeEnd",/** @param {jQuery.Event} e */function(e){if(visible.photo())swipe.stop(e.swipe,_photo3.previous,_photo3.next);});// Document +$(document)// Navigation +.on("click",".album",/** @param {jQuery.Event} e */function(e){multiselect.albumClick(e,$(this));}).on("click",".photo",/** @param {jQuery.Event} e */function(e){multiselect.photoClick(e,$(this));})// Context Menu +.on("contextmenu",".photo",/** @param {jQuery.Event} e */function(e){multiselect.photoContextMenu(e,$(this));}).on("contextmenu",".album",/** @param {jQuery.Event} e */function(e){multiselect.albumContextMenu(e,$(this));})// Upload +.on("change","#upload_files",function(){var _this=this;basicModal.close(false,function(){return upload.start.local(_this.files);});}).on("change","#upload_track_file",function(){var _this2=this;basicModal.close(false,function(){return upload.uploadTrack(_this2.files);});})// Drag and Drop upload +.on("dragover",function(){return false;},false)// In the long run, the "drop" event should not be defined on the +// global document element, but on the `DIV` which corresponds to the +// view onto which something is dropped. +// This would also avoid this highly fragile condition below. +// For example, in order to avoid that a photo unintentionally ends +// up in the root album when someone drops a photo while the +// setting screen is opened, we check for `!visible.config()`. +// This would simply not be necessary, if the drop event was directly +// defined on the albums view where it belongs. +// The conditions whether a user is allowed to upload to the root +// album (cp. `visible.albums()` below) or to a regular album +// (cp. `visible.album()` below) are slightly different. +// Nonetheless, we only have a single method `album.isUploadable` +// which tries to cover both cases and is prone to fail in certain +// corner cases. +// If the drop event was defined on the DIV for the root view and on +// the DIV for an album view, the whole problem would not exist. +// TODO: Fix that +.on("drop",/** @param {jQuery.Event} e */function(e){if(album.isUploadable()&&!visible.contextMenu()&&!basicModal.isVisible()&&!visible.leftMenu()&&!visible.config()&&(visible.album()||visible.albums())){// Detect if dropped item is a file or a link +if(e.originalEvent.dataTransfer.files.length>0){upload.start.local(e.originalEvent.dataTransfer.files);}else if(e.originalEvent.dataTransfer.getData("Text").length>3&&!e.originalEvent.dataTransfer.getData("Text").startsWith("photo-")&&// block drag and drop from albums/photos in web UI +!e.originalEvent.dataTransfer.getData("Text").startsWith("album-")){upload.start.url(e.originalEvent.dataTransfer.getData("Text"));}}return false;})// click on thumbnail on map +.on("click",".image-leaflet-popup",function(){mapview["goto"]($(this));})// Paste upload +.on("paste",/** @param {jQuery.Event} e */function(e){if(e.originalEvent.clipboardData.items){var items=e.originalEvent.clipboardData.items;var filesToUpload=[];// Search clipboard items for an image +for(var i=0;i0&&album.isUploadable()&&!visible.contextMenu()&&!basicModal.isVisible()&&!visible.leftMenu()&&!visible.config()&&(visible.album()||visible.albums())){upload.start.local(filesToUpload);return false;}else{return true;}}});// Fullscreen +if(lychee.fullscreenAvailable())$(document).on("fullscreenchange mozfullscreenchange webkitfullscreenchange msfullscreenchange",lychee.fullscreenUpdate);$("#sensitive_warning").on("click",view.album.nsfw_warning.next);/** + * @returns {void} + */var rememberScrollPage=function rememberScrollPage(){if(visible.albums()&&!visible.search()||visible.album()){var urls=JSON.parse(sessionStorage.getItem("scroll"));if(urls==null||urls.length<1){urls={};}var urlWindow=window.location.href;var urlScroll=$("#lychee_view_container").scrollTop();urls[urlWindow]=urlScroll;if(urlScroll<1){delete urls[urlWindow];}sessionStorage.setItem("scroll",JSON.stringify(urls));}};$(window).on("resize",function(){if(visible.photo())view.photo.onresize();frame.resize();});$("#lychee_view_container").on("scroll",function(){rememberScrollPage();});// Init +lychee.init();});/** + * @description This module is used for the context menu. + */ /** + * @namespace + * @property {jQuery} _dom + */var leftMenu={_dom:$("#lychee_left_menu_container")};/** + * @param {?string} [selector=null] + * @returns {jQuery} + */leftMenu.dom=function(selector){if(selector==null||selector==="")return leftMenu._dom;return leftMenu._dom.find(selector);};/** + * Build left menu + * @returns {void} + */leftMenu.build=function(){var html=lychee.html(_templateObject39||(_templateObject39=_taggedTemplateLiteral(["\n\t\t","","\n\t"])),build.iconic("chevron-left"),lychee.locale["CLOSE"]);if(lychee.rights.settings.can_edit||lychee.rights.user.can_edit){html+=lychee.html(_templateObject40||(_templateObject40=_taggedTemplateLiteral(["\n\t\t","\n\t\t"])),lychee.locale["SETTINGS"]);}if(lychee.new_photos_notification){html+=lychee.html(_templateObject41||(_templateObject41=_taggedTemplateLiteral(["\n\t\t",""," \n\t\t"])),build.iconic("bell"),lychee.locale["NOTIFICATIONS"]);}if(lychee.rights.user_management.can_edit){html+=lychee.html(_templateObject42||(_templateObject42=_taggedTemplateLiteral(["\n\t\t",""," \n\t\t"])),build.iconic("person"),lychee.locale["USERS"]);}if(lychee.rights.user.can_use_2fa){html+=lychee.html(_templateObject43||(_templateObject43=_taggedTemplateLiteral(["\n\t\t",""," \n\t\t"])),build.iconic("key"),lychee.locale["U2F"]);}if(lychee.rights.root_album.can_upload){html+=lychee.html(_templateObject44||(_templateObject44=_taggedTemplateLiteral(["\n\t\t","","\n\t\t"])),build.iconic("cloud"),lychee.locale["SHARING"]);}if(lychee.rights.settings.can_see_logs){html+=lychee.html(_templateObject45||(_templateObject45=_taggedTemplateLiteral(["\n\t\t","","\n\t\t"])),build.iconic("align-left"),lychee.locale["LOGS"]);}if(lychee.rights.settings.can_see_diagnostics){html+=lychee.html(_templateObject46||(_templateObject46=_taggedTemplateLiteral(["\n\t\t","","\n\t\t"])),build.iconic("wrench"),lychee.locale["DIAGNOSTICS"]);}html+=lychee.html(_templateObject47||(_templateObject47=_taggedTemplateLiteral(["\n\t\t","","\n\t\t","",""])),build.iconic("info"),lychee.locale["ABOUT_LYCHEE"],build.iconic("account-logout"),lychee.locale["SIGN_OUT"]);if(lychee.rights.settings.can_update&&lychee.update_available){html+=lychee.html(_templateObject48||(_templateObject48=_taggedTemplateLiteral(["\n\t\t","","\n\t\t"])),build.iconic("timer"),lychee.locale["UPDATE_AVAILABLE"]);}leftMenu.dom("#lychee_left_menu").html(html);};/** Set the width of the side navigation to 250px and the left margin of the page content to 250px + * + * @returns {void} + */leftMenu.open=function(){leftMenu.dom().addClass("visible");// Make background unfocusable +tabindex.makeUnfocusable(header.dom());tabindex.makeUnfocusable(lychee.content);tabindex.makeFocusable(leftMenu.dom());$("#button_signout").focus();multiselect.unbind();};/** + * Set the width of the side navigation to 0 and the left margin of the page content to 0 + * + * @returns {void} + */leftMenu.close=function(){leftMenu.dom().removeClass("visible");tabindex.makeFocusable(header.dom());tabindex.makeFocusable(lychee.content);tabindex.makeUnfocusable(leftMenu.dom());multiselect.bind();lychee.load();};/** + * Close the menu if it's in responsive mode. + * + * @returns {void} + */leftMenu.closeIfResponsive=function(){if(window.matchMedia("only screen and (max-width: 567px), only screen and (max-width: 640px) and (orientation: portrait)").matches){leftMenu.dom().removeClass("visible");tabindex.makeFocusable(header.dom());tabindex.makeFocusable(lychee.content);tabindex.makeUnfocusable(leftMenu.dom());}};/** + * @returns {void} + */leftMenu.bind=function(){// Event Name +var eventName="click";leftMenu.dom("#button_settings_close").on(eventName,leftMenu.close);leftMenu.dom("#button_settings_open").on(eventName,function(){leftMenu.closeIfResponsive();settings.open();});leftMenu.dom("#button_signout").on(eventName,lychee.logout);leftMenu.dom("#button_logs").on(eventName,leftMenu.Logs);leftMenu.dom("#button_diagnostics").on(eventName,leftMenu.Diagnostics);leftMenu.dom("#button_about").on(eventName,lychee.aboutDialog);leftMenu.dom("#button_notifications").on(eventName,leftMenu.Notifications);leftMenu.dom("#button_users").on(eventName,leftMenu.Users);leftMenu.dom("#button_u2f").on(eventName,leftMenu.u2f);leftMenu.dom("#button_sharing").on(eventName,leftMenu.Sharing);leftMenu.dom("#button_update").on(eventName,leftMenu.Update);};/** + * @returns {void} + */leftMenu.Logs=function(){leftMenu.closeIfResponsive();window.open("Logs");};/** + * @returns {void} + */leftMenu.Diagnostics=function(){leftMenu.closeIfResponsive();view.diagnostics.init();};/** + * @returns {void} + */leftMenu.Update=function(){leftMenu.closeIfResponsive();view.update.init();};/** + * @returns {void} + */leftMenu.Notifications=function(){leftMenu.closeIfResponsive();notifications.load();};/** + * @returns {void} + */leftMenu.Users=function(){leftMenu.closeIfResponsive();users.list();};/** + * @returns {void} + */leftMenu.u2f=function(){leftMenu.closeIfResponsive();u2f.list();};/** + * @returns {void} + */leftMenu.Sharing=function(){leftMenu.closeIfResponsive();sharing.list();};/** + * @description This module is used to show and hide the loading bar. + */var loadingBar={/** @type {?string} */status:null,/** @type {jQuery} */_dom:$("#lychee_loading")};/** + * @param {string} [selector=""] + * @returns {jQuery} + */loadingBar.dom=function(selector){if(selector==null||selector==="")return loadingBar._dom;return loadingBar._dom.find(selector);};/** + * @param {?string} [status=null] the status, either `null`, `"error"` or `"success"` + * @param {?string} [errorText=null] the error text to show + * @returns {void} + */loadingBar.show=function(){var status=arguments.length>0&&arguments[0]!==undefined?arguments[0]:null;var errorText=arguments.length>1&&arguments[1]!==undefined?arguments[1]:null;if(status==="error"){// Set status +loadingBar.status="error";// Parse text +if(errorText)errorText=errorText.replace("
","");if(!errorText)errorText=lychee.locale["ERROR_TEXT"];// Modify loading +loadingBar.dom().removeClass().html("

"+lychee.locale["ERROR"]+": ".concat(errorText,"

")).addClass(status);// Set timeout +clearTimeout(loadingBar._timeout);loadingBar._timeout=setTimeout(function(){return loadingBar.hide(true);},3000);return;}if(status==="success"){// Set status +loadingBar.status="success";// Parse text +if(errorText)errorText=errorText.replace("
","");if(!errorText)errorText=lychee.locale["ERROR_TEXT"];// Modify loading +loadingBar.dom().removeClass().html("

"+lychee.locale["SUCCESS"]+": ".concat(errorText,"

")).addClass(status);// Set timeout +clearTimeout(loadingBar._timeout);loadingBar._timeout=setTimeout(function(){return loadingBar.hide(true);},2000);return;}if(loadingBar.status===null){// Set status +loadingBar.status=lychee.locale["LOADING"];// Set timeout +clearTimeout(loadingBar._timeout);loadingBar._timeout=setTimeout(function(){// Modify loading +loadingBar.dom().removeClass().html("").addClass("loading");},1000);}};/** + * @param {boolean} force + * @returns {void} + */loadingBar.hide=function(force){if(loadingBar.status!=="error"&&loadingBar.status!=="success"&&loadingBar.status!=null||force){// Remove status +loadingBar.status=null;// Also move up the dark background +$(".basicModalContainer").removeClass("basicModalContainer--error");$(".basicModal").removeClass("basicModal--error");// Set timeout +clearTimeout(loadingBar._timeout);setTimeout(function(){return loadingBar.dom().removeClass();},300);}};/** + * @description This module provides the basic functions of Lychee. + */var lychee={/** + * The version of the backend in human-readable + * @type {Version} + */version:null,updateGit:"https://github.com/LycheeOrg/Lychee",updateURL:"https://github.com/LycheeOrg/Lychee/releases",website:"https://lycheeorg.dev",publicMode:false,viewMode:false,grants_full_photo_access:true,grants_download:false,public_photos_hidden:true,share_button_visible:false,/** + * The authenticated user or `null` if unauthenticated + * @type {?User} + */user:null,/** + * The rights granted by the backend + * @type {?GlobalRightsDTO} + */rights:null,/** + * Values: + * + * - `square`: Use default, "square" layout. + * - `justified`: Use Flickr-like "justified" layout. + * - `unjustified`: Use Google-like "unjustified" layout + * + * @type {string} + */layout:"justified",/** + * Display search in public mode. + * @type {boolean} + */public_search:false,/** + * Overlay display type + * @type {string} + */image_overlay_type:"exif",/** + * Image overlay type default type + * @type {string} + */image_overlay_type_default:"exif",/** + * Display photo coordinates on map + * @type {boolean} + */map_display:false,/** + * Display photos of public album on map (user not logged in) + * @type {boolean} + */map_display_public:false,/** + * Use the GPS direction data on displayed maps + * @type {boolean} + */map_display_direction:true,/** + * Provider of OSM Tiles + * @type {string} + */map_provider:"Wikimedia",/** + * Include photos of subalbums on map + * @type {boolean} + */map_include_subalbums:false,/** + * Retrieve location name from GPS data + * @type {boolean} + */location_decoding:false,/** + * Caching mode for GPS data decoding + * @type {string} + */location_decoding_caching_type:"Harddisk",/** + * Show location name + * @type {boolean} + */location_show:false,/** + * Show location name for public albums + * @type {boolean} + */location_show_public:false,/** + * Tolerance for navigating when swiping images to the left and right on mobile + * @type {number} + */swipe_tolerance_x:150,/** + * Tolerance for navigating when swiping images up and down + * @type {number} + */swipe_tolerance_y:250,/** + * Is landing page enabled? + * @type {boolean} + */landing_page_enabled:false,delete_imported:false,import_via_symlink:false,skip_duplicates:false,nsfw_visible:true,nsfw_visible_saved:true,nsfw_blur:false,nsfw_warning:false,/** @type {string} */nsfw_banner_override:"",album_subtitle_type:"oldstyle",album_decoration:"layers",album_decoration_orientation:"row",upload_processing_limit:4,/** + * Allow users to change their username + * @type {boolean} + */allow_username_change:true,/** + * The URL to the Facebook page related to this site + * @type {string} + */sm_facebook_url:"",/** + * The URL to the Flickr page related to this site + * @type {string} + */sm_flickr_url:"",/** + * The URL to the Instagram page related to this site + * @type {string} + */sm_instagram_url:"",/** + * The URL to the Twitter page related to this site + * @type {string} + */sm_twitter_url:"",/** + * The URL to the YouTube channel related to this site + * @type {string} + */sm_youtube_url:"",/** + * Indicates whether RSS feeds are enabled or not + * @type {boolean} + */rss_enable:false,/** + * An array of RSS feeds provided by the site + * @type {Feed[]} + */rss_feeds:[],/** + * The site title. + * @type {string} + */site_title:"",/** + * The name of the site owner. + * @type {string} + */site_owner:"",/** + * Begin of copyright. + * @type {string} + */site_copyright_begin:"",/** + * End of copyright. + * @type {string} + */site_copyright_end:"",/** + * Determines if social media links are shown in footer. + * @type {boolean} + */footer_show_social_media:false,/** + * Determines if copyright notice is shown in footer. + * @type {boolean} + */footer_show_copyright:false,/** + * An optional line of text to be shown in the footer. + * @type {string} + */footer_additional_text:"",/** + * Determines whether frame mode is enabled or not + * @type {boolean} + */mod_frame_enabled:false,/** + * Refresh rate in seconds for the frame mode. + * @type {number} + */mod_frame_refresh:30,// this is device specific config, in this case default is Desktop. +header_auto_hide:true,active_focus_on_page_load:false,enable_button_visibility:true,enable_button_share:true,enable_button_archive:true,enable_button_move:true,enable_button_trash:true,enable_button_fullscreen:true,enable_button_download:true,enable_button_add:true,enable_button_more:true,enable_button_rotate:true,enable_close_tab_on_esc:false,enable_tabindex:false,enable_contextmenu_header:true,hide_content_during_imgview:false,checkForUpdates:true,update_json:false,update_available:false,new_photos_notification:false,/** @type {?SortingCriterion} */sorting_photos:null,/** @type {?SortingCriterion} */sorting_albums:null,/** + * The absolute path of the server-side installation directory of Lychee, e.g. `/var/www/lychee` + * @type {string} + */location:"",lang:"",/** @type {string[]} */lang_available:[],/** + * The visibility status of recent, starred, on_this_day + */smart_album_visibilty:[],dropbox:false,dropboxKey:"",content:$("#lychee_view_content"),imageview:$("#imageview"),footer:$("#lychee_footer"),/** @type {Locale} */locale:{},nsfw_unlocked_albums:[]};/** + * @returns {string} + */lychee.diagnostics=function(){return"/Diagnostics";};/** + * @returns {string} + */lychee.logs=function(){return"/Logs";};/** + * @returns {void} + */lychee.aboutDialog=function(){var aboutDialogBody="\n\t\t

Lychee

\n\t\t

\n\t\t

\n\t\t

\n\t\t

");/** + * @param {ModalDialogFormElements} formElements + * @param {HTMLDivElement} dialog + * @returns {void} + */var initAboutDialog=function initAboutDialog(formElements,dialog){dialog.querySelector("span.version-number").textContent=lychee.version.major+"."+lychee.version.minor+"."+lychee.version.patch;// If Release is available : show release +// If Git is available : show git +if(lychee.update_available){dialog.querySelector("p.up-to-date-release a").textContent=lychee.locale["UPDATE_AVAILABLE"];dialog.querySelector("p.up-to-date-release").classList.remove("up-to-date-release");}else if(lychee.update_json){dialog.querySelector("p.up-to-date-git a").textContent=lychee.locale["UPDATE_AVAILABLE"];dialog.querySelector("p.up-to-date-git").classList.remove("up-to-date-git");}dialog.querySelector("h2").textContent=lychee.locale["ABOUT_SUBTITLE"];// We should not use `innerHTML`, but either hard-code HTML or build it +// programmatically. +// Also, localized strings should not contain HTML tags. +// TODO: Find a better solution for this. +dialog.querySelector("p.about-desc").innerHTML=sprintf(lychee.locale["ABOUT_DESCRIPTION"],lychee.website);};basicModal.show({body:aboutDialogBody,readyCB:initAboutDialog,classList:["about-dialog"],buttons:{cancel:{title:lychee.locale["CLOSE"],fn:basicModal.close}}});};/** + * @param {boolean} isFirstInitialization must be set to `false` if called + * for re-initialization to prevent + * multiple registrations of global + * event handlers + * @returns {void} + */lychee.init=function(){var isFirstInitialization=arguments.length>0&&arguments[0]!==undefined?arguments[0]:true;api.post("Session::init",{},/** @param {InitializationData} data */function(data){lychee.parseInitializationData(data);if(data.user!==null||data.rights.settings.can_edit){// Authenticated or no admin is registered +leftMenu.build();leftMenu.bind();lychee.setMode("logged_in");}else{lychee.setMode("public");}if(isFirstInitialization){$(window).on("popstate",function(){var autoplay=history.state&&history.state.hasOwnProperty("autoplay")?history.state.autoplay:true;lychee.load(autoplay);});lychee.load();}});};/** + * @param {InitializationData} data + * @returns {void} + */lychee.parseInitializationData=function(data){lychee.user=data.user;lychee.rights=data.rights;lychee.update_json=data.update_json;lychee.update_available=data.update_available;lychee.version=data.config.version;// we copy the locale that exists only. +// This ensures forward and backward compatibility. +// e.g. if the front localization is unfinished in a language +// or if we need to change some locale string +for(var key in data.locale){lychee.locale[key]=data.locale[key];}lychee.parsePublicInitializationData(data);if(lychee.user!==null||lychee.rights.settings.can_edit){lychee.parseProtectedInitializationData(data);}lychee.initHtmlHeader();lychee.localizeStaticGuiElements();};/** + * Initializes the HTML header of the page according to the loaded + * configuration. + * + * This method is comparable to {@link lychee.setMetaData} except that this + * method sets data in the HTML header which does not change for each page + * but is static for the entire site. + */lychee.initHtmlHeader=function(){// General Meta Data +document.querySelector('meta[name="author"]').content=lychee.site_owner;document.querySelector('meta[name="publisher"]').content=lychee.site_owner;// RSS feeds +if(lychee.rss_enable){var head=document.querySelector("head");lychee.rss_feeds.forEach(function(feed){var link=document.createElement("link");link.rel="alternate";link.type=feed.mimetype;link.href=feed.url;link.title=feed.title;head.appendChild(link);});}};/** + * Applies the current `lychee.locale` to those GUI elements which are + * static part of the HTML. + * + * Note, `lychee.setMode` removes some elements (e.g. the input element + * for search) depending on the mode. + * Hence, we must take some precautions as some elements might be `null`. + * TODO: Fix that. + * + * @return {void} + */lychee.localizeStaticGuiElements=function(){// Toolbars in the header +var tbPublic=document.querySelector("div#lychee_toolbar_public");tbPublic.querySelector("a#button_signin").title=lychee.locale["SIGN_IN"];var tbPublicSearch=tbPublic.querySelector("input.header__search");if(tbPublicSearch instanceof HTMLInputElement){// See remark about `lychee.setMode` in the jsDoc comment of this method. +tbPublicSearch.placeholder=lychee.locale["SEARCH"];}tbPublic.querySelector("a.button--map-albums").title=lychee.locale["DISPLAY_FULL_MAP"];var tbAlbums=document.querySelector("div#lychee_toolbar_albums");tbAlbums.querySelector("a#button_settings").title=lychee.locale["SETTINGS"];var tbAlbumsSearch=tbAlbums.querySelector("input.header__search");if(tbAlbumsSearch instanceof HTMLInputElement){// See remark about `lychee.setMode` in the jsDoc comment of this method. +tbAlbumsSearch.placeholder=lychee.locale["SEARCH"];}tbAlbums.querySelector("a.button--map-albums").title=lychee.locale["DISPLAY_FULL_MAP"];tbAlbums.querySelector("a.button_add").title=lychee.locale["ADD"];var tbAlbum=document.querySelector("div#lychee_toolbar_album");tbAlbum.querySelector("a#button_back_home").title=lychee.locale["CLOSE_ALBUM"];tbAlbum.querySelector("a#button_visibility_album").title=lychee.locale["VISIBILITY_ALBUM"];tbAlbum.querySelector("a#button_sharing_album_users").title=lychee.locale["SHARING_ALBUM_USERS"];tbAlbum.querySelector("a#button_nsfw_album").title=lychee.locale["ALBUM_MARK_NSFW"];tbAlbum.querySelector("a#button_share_album").title=lychee.locale["SHARE_ALBUM"];tbAlbum.querySelector("a#button_archive").title=lychee.locale["DOWNLOAD_ALBUM"];tbAlbum.querySelector("a#button_info_album").title=lychee.locale["ABOUT_ALBUM"];tbAlbum.querySelector("a#button_map_album").title=lychee.locale["DISPLAY_FULL_MAP"];tbAlbum.querySelector("a#button_move_album").title=lychee.locale["MOVE_ALBUM"];tbAlbum.querySelector("a#button_trash_album").title=lychee.locale["DELETE_ALBUM"];tbAlbum.querySelector("a#button_fs_album_enter").title=lychee.locale["FULLSCREEN_ENTER"];tbAlbum.querySelector("a#button_fs_album_exit").title=lychee.locale["FULLSCREEN_EXIT"];tbAlbum.querySelector("a.button_add").title=lychee.locale["ADD"];var tbPhoto=document.querySelector("div#lychee_toolbar_photo");tbPhoto.querySelector("a#button_back").title=lychee.locale["CLOSE_PHOTO"];tbPhoto.querySelector("a#button_star").title=lychee.locale["STAR_PHOTO"];tbPhoto.querySelector("a#button_visibility").title=lychee.locale["VISIBILITY_PHOTO"];tbPhoto.querySelector("a#button_rotate_ccwise").title=lychee.locale["PHOTO_EDIT_ROTATECCWISE"];tbPhoto.querySelector("a#button_rotate_cwise").title=lychee.locale["PHOTO_EDIT_ROTATECWISE"];tbPhoto.querySelector("a#button_share").title=lychee.locale["SHARE_PHOTO"];tbPhoto.querySelector("a#button_info").title=lychee.locale["ABOUT_PHOTO"];tbPhoto.querySelector("a#button_map").title=lychee.locale["DISPLAY_FULL_MAP"];tbPhoto.querySelector("a#button_move").title=lychee.locale["MOVE"];tbPhoto.querySelector("a#button_trash").title=lychee.locale["DELETE"];tbPhoto.querySelector("a#button_fs_enter").title=lychee.locale["FULLSCREEN_ENTER"];tbPhoto.querySelector("a#button_fs_exit").title=lychee.locale["FULLSCREEN_EXIT"];tbPhoto.querySelector("a#button_more").title=lychee.locale["MORE"];var tbMap=document.querySelector("div#lychee_toolbar_map");tbMap.querySelector("a#button_back_map").title=lychee.locale["CLOSE_MAP"];var tbConfig=document.querySelector("div#lychee_toolbar_config");tbConfig.querySelector("a#button_close_config").title=lychee.locale["CLOSE"];// Sidebar +document.querySelector("#lychee_sidebar_header h1").textContent=lychee.locale["PHOTO_ABOUT"];// NSFW Warning Banner +/** @type {HTMLDivElement} */var nsfwBanner=document.querySelector("#sensitive_warning");nsfwBanner.innerHTML=lychee.nsfw_banner_override?lychee.nsfw_banner_override:lychee.locale["NSFW_BANNER"];// Footer +var footer=document.querySelector("#lychee_footer");footer.querySelector("p.home_copyright").textContent=lychee.footer_show_copyright?sprintf(lychee.locale["FOOTER_COPYRIGHT"],lychee.site_owner,lychee.site_copyright_begin===lychee.site_copyright_end?lychee.site_copyright_begin:lychee.site_copyright_begin+"–"+lychee.site_copyright_end):"";footer.querySelector("p.personal_text").innerHTML=lychee.footer_additional_text;footer.querySelector("p.hosted_by a").textContent=lychee.locale["HOSTED_WITH_LYCHEE"];/** @type {HTMLDivElement} */var footerSocialMedia=footer.querySelector("div#home_socials");if(lychee.footer_show_social_media){footerSocialMedia.style.display=null;footerSocialMedia.querySelector("a#facebook").href=lychee.sm_facebook_url;footerSocialMedia.querySelector("a#flickr").href=lychee.sm_flickr_url;footerSocialMedia.querySelector("a#instagram").href=lychee.sm_instagram_url;footerSocialMedia.querySelector("a#twitter").href=lychee.sm_twitter_url;footerSocialMedia.querySelector("a#youtube").href=lychee.sm_youtube_url;}else{footerSocialMedia.style.display="none";}};/** + * Parses the configuration settings which are always available. + * + * TODO: If configuration management is re-factored on the backend, remember to use proper types in the first place + * + * @param {InitializationData} data + * @returns {void} + */lychee.parsePublicInitializationData=function(data){lychee.sorting_photos=data.config.sorting_photos;lychee.sorting_albums=data.config.sorting_albums;lychee.share_button_visible=data.config.share_button_visible;lychee.album_subtitle_type=data.config.album_subtitle_type||"oldstyle";lychee.album_decoration=data.config.album_decoration||"layers";lychee.album_decoration_orientation=data.config.album_decoration_orientation||"row";lychee.checkForUpdates=data.config.check_for_updates;lychee.layout=data.config.layout||"justified";lychee.landing_page_enable=data.config.landing_page_enable;lychee.public_search=data.config.public_search;lychee.image_overlay_type=data.config.image_overlay_type||"exif";lychee.image_overlay_type_default=lychee.image_overlay_type;lychee.map_display=data.config.map_display;lychee.map_display_public=data.config.map_display_public;lychee.map_display_direction=data.config.map_display_direction==="1";lychee.map_provider=data.config.map_provider||"Wikimedia";lychee.map_include_subalbums=data.config.map_include_subalbums;lychee.location_show=data.config.location_show;lychee.location_show_public=data.config.location_show_public;lychee.swipe_tolerance_x=Number.parseInt(data.config.swipe_tolerance_x,10)||150;lychee.swipe_tolerance_y=Number.parseInt(data.config.swipe_tolerance_y,10)||250;lychee.nsfw_visible=data.config.nsfw_visible;lychee.nsfw_visible_saved=lychee.nsfw_visible;lychee.nsfw_blur=data.config.nsfw_blur;lychee.nsfw_warning=data.config.nsfw_warning;lychee.nsfw_banner_override=data.config.nsfw_banner_override||"";lychee.sm_facebook_url=data.config.sm_facebook_url;lychee.sm_flickr_url=data.config.sm_flickr_url;lychee.sm_instagram_url=data.config.sm_instagram_url;lychee.sm_twitter_url=data.config.sm_twitter_url;lychee.sm_youtube_url=data.config.sm_youtube_url;lychee.rss_enable=data.config.rss_enable;lychee.rss_feeds=data.config.rss_feeds;lychee.site_title=data.config.site_title;lychee.site_owner=data.config.site_owner;lychee.site_copyright_begin=data.config.site_copyright_begin;lychee.site_copyright_end=data.config.site_copyright_end;lychee.footer_show_social_media=data.config.footer_show_social_media;lychee.footer_show_copyright=data.config.footer_show_copyright;lychee.footer_additional_text=data.config.footer_additional_text;lychee.mod_frame_enabled=data.config.mod_frame_enabled;lychee.mod_frame_refresh=Number.parseInt(data.config.mod_frame_refresh,10)||30;var isTv=window.matchMedia("tv").matches;lychee.header_auto_hide=!isTv;lychee.active_focus_on_page_load=isTv;lychee.enable_button_visibility=!isTv;lychee.enable_button_share=!isTv;lychee.enable_button_archive=!isTv;lychee.enable_button_move=!isTv;lychee.enable_button_trash=!isTv;lychee.enable_button_fullscreen=!isTv;lychee.enable_button_download=!isTv;lychee.enable_button_add=!isTv;lychee.enable_button_more=!isTv;lychee.enable_button_rotate=!isTv;lychee.enable_close_tab_on_esc=isTv;lychee.enable_tabindex=isTv;lychee.enable_contextmenu_header=!isTv;lychee.hide_content_during_imgview=isTv;};/** + * Parses the configuration settings which are only available, if a user is authenticated. + * + * TODO: If configuration management is re-factored on the backend, remember to use proper types in the first place + * + * @param {InitializationData} data + * @returns {void} + */lychee.parseProtectedInitializationData=function(data){lychee.allow_username_change=data.config.allow_username_change;lychee.dropboxKey=data.config.dropbox_key||"";lychee.location=data.config.location||"";lychee.checkForUpdates=data.config.check_for_updates;lychee.lang=data.config.lang||"";lychee.lang_available=data.config.lang_available||[];lychee.location_decoding=data.config.location_decoding;lychee.default_license=data.config.default_license||"none";lychee.css=data.config.css||"";lychee.grants_full_photo_access=data.config.grants_full_photo_access;lychee.grants_download=data.config.grants_download;lychee.public_photos_hidden=data.config.public_photos_hidden;lychee.delete_imported=data.config.delete_imported;lychee.import_via_symlink=data.config.import_via_symlink;lychee.skip_duplicates=data.config.skip_duplicates;lychee.editor_enabled=data.config.editor_enabled;lychee.new_photos_notification=data.config.new_photos_notification;lychee.upload_processing_limit=Number.parseInt(data.config.upload_processing_limit,10)||4;lychee.smart_album_visibilty=data.config.smart_album_visibilty;};/** + * @param {{username: string, password: string}} data + * @returns {void} + */lychee.login=function(data){if(!data.username.trim()){basicModal.focusError("username");return;}if(!data.password.trim()){basicModal.focusError("password");return;}api.post("Session::login",data,function(){return window.location.reload();},null,function(jqXHR){if(jqXHR.status===401){basicModal.focusError("password");return true;}else{return false;}});};/** + * @returns {void} + */lychee.loginDialog=function(){var loginDialogBody="\n\t\t\n\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t
\n\t\t
\n\t\t

Lychee \n\t\t\t\n\t\t\t\n\t\t

\n\t\t");/** + * @param {ModalDialogFormElements} formElements + * @param {HTMLDivElement} dialog + * @returns {void} + */var initLoginDialog=function initLoginDialog(formElements,dialog){tabindex.makeUnfocusable(header.dom());tabindex.makeUnfocusable(lychee.content);tabindex.makeUnfocusable(lychee.imageview);tabindex.makeFocusable($(dialog));formElements.username.placeholder=lychee.locale["USERNAME"];formElements.password.placeholder=lychee.locale["PASSWORD"];if(!!lychee.version){dialog.querySelector("span.version-number").textContent=lychee.version.major+"."+lychee.version.minor+"."+lychee.version.patch;}else{dialog.querySelector("span.version-number").textContent="";}// If Release is available : show release +// If Git is available : show git +if(lychee.update_available){dialog.querySelector("span.up-to-date-release a").textContent=lychee.locale["UPDATE_AVAILABLE"];dialog.querySelector("span.up-to-date-release").classList.remove("up-to-date-release");}else if(lychee.update_json){dialog.querySelector("span.up-to-date-git a").textContent=lychee.locale["UPDATE_AVAILABLE"];dialog.querySelector("span.up-to-date-git").classList.remove("up-to-date-git");}// This feels awkward, because this hooks into the modal dialog in some +// unpredictable way. +// It would be better to have a checkbox for password-less login in the +// dialog and then let the action handler of the modal dialog, i.e. +// `lychee.login` handle both cases. +// TODO: Refactor this. +dialog.querySelector("#signInKeyLess").addEventListener("click",u2f.login);};basicModal.show({body:loginDialogBody,readyCB:initLoginDialog,classList:["login"],buttons:{action:{title:lychee.locale["SIGN_IN"],fn:lychee.login,attributes:{"data-tabindex":tabindex.get_next_tab_index()}},cancel:{title:lychee.locale["CANCEL"],fn:basicModal.close,attributes:{"data-tabindex":tabindex.get_next_tab_index()}}}});};/** + * @returns {void} + */lychee.logout=function(){api.post("Session::logout",{},function(){return window.location.reload();});};/** + * @param {?string} [url=null] + * @param {boolean} [autoplay=true] + * + * @returns {void} + */lychee["goto"]=function(){var url=arguments.length>0&&arguments[0]!==undefined?arguments[0]:null;var autoplay=arguments.length>1&&arguments[1]!==undefined?arguments[1]:true;url="gallery#"+(url!==null?url:"");history.pushState({autoplay:autoplay},null,url);lychee.load(autoplay);};/** + * @param {?string} [albumID=null] + * @param {boolean} [autoplay=true] + * + * @returns {void} + */lychee.gotoMap=function(){var albumID=arguments.length>0&&arguments[0]!==undefined?arguments[0]:null;var autoplay=arguments.length>1&&arguments[1]!==undefined?arguments[1]:true;// If map functionality is disabled -> go to album +if(!lychee.map_display){loadingBar.show("error",lychee.locale["ERROR_MAP_DEACTIVATED"]);return;}lychee["goto"]("map/"+(albumID!==null?albumID:""),autoplay);};/** + * Triggers a reload, if the given IDs are in legacy format. + * + * If any of the IDs is in legacy format, the method first translates the IDs + * into the modern format via an AJAX call to the backend and then triggers + * an asynchronous reloading of the page with the resolved, modern IDs. + * The function returns `true` in this case. + * + * If the IDs are already in modern format (and thus neither a translation + * nor a reloading is required), the function returns `false`. + * In this case this function is basically a no-op. + * + * @param {?string} albumID the album ID + * @param {?string} photoID the photo ID + * @param {boolean} autoplay indicates whether playback should start + * automatically, if the indicated photo is a video + * + * @returns {boolean} `true`, if any of the IDs has been in legacy format + * and an asynchronous reloading has been scheduled + */lychee.reloadIfLegacyIDs=function(albumID,photoID,autoplay){/** @param {?string} id the inspected ID */var isLegacyID=function isLegacyID(id){// The legacy IDs were pure numeric values. We exclude values which +// have 24 digits, because these could also be modern IDs. +// A modern IDs is a 24 character long, base64 encoded value and thus +// could also match 24 digits by accident. +return id&&id.length!==24&&parseInt(id,10).toString()===id;};if(!isLegacyID(albumID)&&!isLegacyID(photoID)){// this function is a no-op if neither ID is in legacy format +return false;}/** + * Callback to be called asynchronously which executes the actual reloading. + * + * @param {?string} newAlbumID + * @param {?string} newPhotoID + * + * @returns {void} + */var reloadWithNewIDs=function reloadWithNewIDs(newAlbumID,newPhotoID){var newUrl="";if(newAlbumID){newUrl+=newAlbumID;newUrl+=newPhotoID?"/"+newPhotoID:"";}lychee["goto"](newUrl,autoplay);};// We have to deal with three cases: +// 1. the album and photo ID need to be translated +// 2. only the album ID needs to be translated +// 3. only the photo ID needs to be translated +var params={};if(isLegacyID(albumID))params.albumID=parseInt(albumID,10);if(isLegacyID(photoID))params.photoID=parseInt(photoID,10);api.post("Legacy::translateLegacyModelIDs",params,function(data){reloadWithNewIDs(data.hasOwnProperty("albumID")?data.albumID:albumID,data.hasOwnProperty("photoID")?data.photoID:photoID);});return true;};/** + * This is a "God method" that is used to load pretty much anything, based + * on what's in the web browser's URL. + * + * Traditionally, Lychee has been using client-side navigation based on + * URL fragments (i.e. based on the part after the '#' character) + * Fragments can match one of the following cases: + * + * - (nothing): load root album, assign `null` to `albumID` and `photoID` + * - `{albumID}`: load the album; `albumID` equals the given ID, `photoID` is + * null + * - `{albumID}/{photoID}`: load album (if not already loaded) and then the + * corresponding photo, assign the respective values to `albumID` and + * `photoID` + * - `map`: load the map of all albums + * - `map/{albumID}`: load the map of the respective album + * - `search/{term}`: load or go back to "search" album for the given term, + * assign `search/{term}` as fictitious `albumID` and assign `null` to + * `photoID` + * - `search/{term}/{photoID}`: load photo within fictitious search album, + * assign `search/{term}` as fictitious `albumID` and assign the given ID + * to `photoID` + * - `view/{photoID}`: load the photo in "view" mode, i.e. a special photo + * view which displays the photo as standalone (not in an album carousel) + * which assumes that the user is always unauthenticated. + * - `frame`: shows random, starred photos in a kiosk mode + * + * Additionally, Lychee supports the following proper paths: + * + * - `/view/{photoID}` and `/view?p={photoID}`: See `view/{photoID}` above + * for the fragment-based approach + * - `/frame`: See `frame` above for the fragment-based approach. + * + * @param {boolean} [autoplay=true] + * @returns {void} + */lychee.load=function(){var autoplay=arguments.length>0&&arguments[0]!==undefined?arguments[0]:true;var albumID="";var photoID="";var viewMatch=document.location.href.match(/*#__PURE__*/_wrapRegExp(/\/view(?:\/|(\?p=))([-_0-9A-Za-z]+)$/,{photoID:2}));var hashMatch=document.location.hash.replace("#","").split("/");if(/\/frame\/?$/.test(document.location.href)){albumID="frame";photoID="";}else if(viewMatch!==null&&viewMatch.groups.photoID){albumID="view";photoID=viewMatch.groups.photoID;}else{albumID=hashMatch[0];if(albumID===SearchAlbumIDPrefix&&hashMatch.length>1){albumID+="/"+decodeURIComponent(hashMatch[1]);}photoID=hashMatch[album.isSearchID(albumID)?2:1];}contextMenu.close();multiselect.close();tabindex.reset();// If Lychee is currently in frame or view mode, we need to re-initialize. +// Note, this is a temporary nasty hack. +// In an optimal world, we would simply call `lychee.setMode` to leave +// view or frame mode and to enter gallery or public mode. +// However, `lychee.setMode` does not support that direction (see comment +// here). +// Hence, in order to get back to a "full" mode, we need to re-initialize +// completely. +var bodyClasses=document.querySelector("body").classList;if(bodyClasses.contains("mode-frame")||bodyClasses.contains("mode-view")){lychee.init(false);return;}if(albumID&&photoID){if(albumID==="map"){// If map functionality is disabled -> do nothing +if(!lychee.map_display){loadingBar.show("error",lychee.locale["ERROR_MAP_DEACTIVATED"]);return;}$(".no_content").remove();// show map +// albumID has been stored in photoID due to URL format #map/albumID +albumID=photoID;photoID=null;// Trash data +_photo3.json=null;// Show Album -> it's below the map +if(visible.photo())view.photo.hide();if(visible.sidebar())_sidebar.toggle(false);if(album.json&&albumID===album.json.id){view.album.title();}mapview.open(albumID);lychee.footer_hide();}else{if(lychee.reloadIfLegacyIDs(albumID,photoID,autoplay)){return;}$(".no_content").remove();// Show photo +// Trash data +_photo3.json=null;/** + * @param {boolean} isParentAlbumAccessible + * @returns {void} + */var loadPhoto=function loadPhoto(isParentAlbumAccessible){if(!isParentAlbumAccessible){lychee.setMode("view");}_photo3.load(photoID,albumID,autoplay);// Make imageview focusable +tabindex.makeFocusable(lychee.imageview);// Make thumbnails unfocusable and store which element had focus +tabindex.makeUnfocusable(lychee.content,true);// hide contentview if requested +if(lychee.hide_content_during_imgview)lychee.content.hide();lychee.footer_hide();};// Load Photo +if(albumID==="view"){// If the photo shall be displayed in "view" mode, delete +// any album which we possibly have and load the photo as +// if the parent album was inaccessible (even if a user is +// authenticated). +albumID=null;album.refresh();lychee.content.empty();loadPhoto(false);}else if(lychee.content.html()===""||album.json===null||album.json.id!==albumID){// If we don't have an album or the wrong album load the album +// first and let the album loader load the photo afterwards or +// load the photo directly. +lychee.content.hide();album.load(albumID,loadPhoto);}else{loadPhoto(true);}}}else if(albumID){if(albumID==="map"){$(".no_content").remove();// Show map of all albums +// If map functionality is disabled -> do nothing +if(!lychee.map_display){loadingBar.show("error",lychee.locale["ERROR_MAP_DEACTIVATED"]);return;}// Trash data +_photo3.json=null;// Show Album -> it's below the map +if(visible.photo())view.photo.hide();if(visible.sidebar())_sidebar.toggle(false);mapview.open();lychee.footer_hide();}else if(albumID==="frame"){if(lychee.mod_frame_enabled){frame.initAndStart();}else{loadingBar.show("error","Frame mode disabled");}}else{if(lychee.reloadIfLegacyIDs(albumID,photoID,autoplay)){return;}$(".no_content").remove();// Trash data +_photo3.json=null;// Show Album +if(visible.photo()){view.photo.hide();tabindex.makeUnfocusable(lychee.imageview);}if(visible.mapview())mapview.close();if(visible.sidebar()&&(album.isSmartID(albumID)||album.isSearchID(albumID)))_sidebar.toggle(false);$("#sensitive_warning").removeClass("active");if(album.json&&albumID===album.json.id){if(album.isSearchID(albumID)){if($(".settings_view, .users_view, .sharing_view, .logs_diagnostics_view, .u2f_view").length>0){search.reset();history.back();}else{// We are probably coming back to the search results from +// viewing an image. Because search results is not a +// regular album, it needs to be treated a little +// differently. +header.setMode("albums");lychee.setMetaData(lychee.locale["SEARCH_RESULTS"]);}}else{if($(".settings_view, .users_view, .sharing_view, .logs_diagnostics_view, .u2f_view").length>0){album.load(albumID);}else{view.album.title();}}lychee.content.show();tabindex.makeFocusable(lychee.content,true);// If the album was loaded in the background (when content is +// hidden), scrolling may not have worked. +view.album.content.restoreScroll();}else if(album.isSearchID(albumID)){// Search has been triggered +var search_string=decodeURIComponent(hashMatch[1]).trim();if(search_string===""){// do nothing on "only space" search strings +return;}// If public search is disabled -> do nothing +if(lychee.publicMode===true&&!lychee.public_search){loadingBar.show("error",lychee.locale["ERROR_SEARCH_DEACTIVATED"]);return;}header.dom(".header__search").val(search_string);search.find(search_string);}else if(visible.search()){// Somebody clicked on an album in search results. We +// will alter the parent_id of that album once it's loaded +// so that the back button sends us back to the search +// results. +// Trash data so that it's reloaded if needed (just as we +// would for a regular parent album). +search.json=null;album.load(albumID,null,album.getID());}else{album.load(albumID);}lychee.footer_show();}}else{$(".no_content").remove();// Trash data +search.json=null;album.json=null;_photo3.json=null;// Hide sidebar +if(visible.sidebar())_sidebar.toggle(false);// Show Albums +if(visible.photo()){view.photo.hide();tabindex.makeUnfocusable(lychee.imageview);}if(visible.mapview())mapview.close();$("#sensitive_warning").removeClass("active");lychee.content.show();lychee.footer_show();albums.load();}};/** + * Sets the title and various other meta for the current page. + * + * The title is shown in the browser window and in the header bar. + * The window title is prefixed by the value of the configuration setting + * `lychee.site_title`. + * If both, the prefix `lychee.site_title` and the given title, are not empty, + * they are seperated by an en-dash. + * + * The description is postfixed with `" – via Lychee"` if not empty. + * + * @param {string=""} title + * @param {boolean=false} isTitleEditable + * @param {string=""} description + * @param {string=""} photoUrl + */lychee.setMetaData=function(){var title=arguments.length>0&&arguments[0]!==undefined?arguments[0]:"";var isTitleEditable=arguments.length>1&&arguments[1]!==undefined?arguments[1]:false;var description=arguments.length>2&&arguments[2]!==undefined?arguments[2]:"";var photoUrl=arguments.length>3&&arguments[3]!==undefined?arguments[3]:"";var pageTitle=lychee.site_title+(lychee.site_title&&title?" – ":"")+title;var pageDescription=description?description+" – via Lychee":"";// General Meta Data +document.title=pageTitle;document.querySelector('meta[name="description"]').content=pageDescription;// Twitter Meta Data +document.querySelector('meta[name="twitter:title"]').content=pageTitle;document.querySelector('meta[name="twitter:description"]').content=pageDescription;document.querySelector('meta[name="twitter:image"]').content=photoUrl;// OpenGraph Meta Data (e.g. used by Facebook) +document.querySelector('meta[property="og:title"]').content=pageTitle;document.querySelector('meta[property="og:description"]').content=pageDescription;document.querySelector('meta[property="og:image"]').content=photoUrl;document.querySelector('meta[property="og:url"]').content=window.location.href;header.setEditable(isTitleEditable);header.setTitle(title);};/** + * Sets the "view mode" of the application. + * + * Note, this method is asymmetric and therewith causes a major problem. + * It assumes that it is only called once and that the new mode is always + * more restrictive than the previous mode. + * This method only hides elements and unbinds events, but does not support + * to show elements and bind events. + * This method relies on {@link lychee.init} to have bound particular events + * which can be unbound here. + * TODO: Refactor this. There should be one (or several) methods to change modes, but each of the methods should be symmetric. + * + * TODO: FIX ME WITH NEW RIGHTS + * + * @param {string} mode - one out of: `public`, `view`, `logged_in`, `frame` + */lychee.setMode=function(mode){if(!lychee.rights.settings.can_edit&&!lychee.rights.user.can_edit||mode==="view"||mode==="frame"){$("#button_settings_open").hide();}if(!lychee.rights.root_album.can_upload||mode==="view"||mode==="frame"){$("#button_sharing").hide();$(document).off("click",".header__title--editable").off("touchend",".header__title--editable").off("contextmenu",".photo").off("contextmenu",".album").off("drop");Mousetrap.unbind(["u"]).unbind(["s"]).unbind(["n"]).unbind(["r"]).unbind(["d"]).unbind(["t"]).unbind(["command+backspace","ctrl+backspace"]).unbind(["command+a","ctrl+a"]);}if(!lychee.rights.user_management.can_list||mode==="view"||mode==="frame"){$("#button_users").hide();}if(!lychee.rights.settings.can_see_diagnostics||mode==="view"||mode==="frame"){$("#button_diagnostics").hide();}if(!lychee.rights.settings.can_see_logs||mode==="view"||mode==="frame"){$("#button_logs").hide();}var bodyClasses=document.querySelector("body").classList;if(mode==="logged_in"){if(!bodyClasses.contains("mode-gallery")){bodyClasses.replace("mode-none","mode-gallery");bodyClasses.replace("mode-frame","mode-gallery");bodyClasses.replace("mode-view","mode-gallery");}// After login the keyboard short-cuts to login by password (l) and +// by key (k) are not required anymore, so we unbind them. +Mousetrap.unbind(["l"]).unbind(["k"]);// The code searches by class, so remove the other instance. +$(".header__search, .header__clear","#lychee_toolbar_public").hide();if(!lychee.editor_enabled){$("#button_rotate_cwise").hide();$("#button_rotate_ccwise").hide();}return;}$(".header__search, .header__clear","#lychee_toolbar_albums").hide();$("#button_rotate_cwise").hide();$("#button_rotate_ccwise").hide();$("#button_settings, .header__divider, #lychee_left_menu_container").hide();if(mode==="public"){if(!bodyClasses.contains("mode-gallery")){bodyClasses.replace("mode-none","mode-gallery");bodyClasses.replace("mode-frame","mode-gallery");bodyClasses.replace("mode-view","mode-gallery");}lychee.publicMode=true;}else if(mode==="view"){if(!bodyClasses.contains("mode-view")){bodyClasses.replace("mode-none","mode-view");bodyClasses.replace("mode-frame","mode-view");bodyClasses.replace("mode-gallery","mode-view");}Mousetrap.unbind(["esc","command+up"]);$("#button_back, a#next, a#previous").hide();$(".no_content").hide();lychee.publicMode=true;lychee.viewMode=true;}else if(mode==="frame"){if(!bodyClasses.contains("mode-frame")){bodyClasses.replace("mode-none","mode-frame");bodyClasses.replace("mode-view","mode-frame");bodyClasses.replace("mode-gallery","mode-frame");}Mousetrap.unbind(["esc","command+up"]);$("#button_back, a#next, a#previous").hide();$(".no_content").hide();lychee.publicMode=true;lychee.viewMode=true;}// just mak +header.bind_back();};/** + * @param {jQuery} obj + * @param {string} animation + * + * @returns {void} + */lychee.animate=function(obj,animation){var animations=[["fadeIn","fadeOut"],["contentZoomIn","contentZoomOut"]];for(var i=0;i0&&arguments[0]!==undefined?arguments[0]:"";// Ensure that html is a string +html+="";// Escape all critical characters +html=html.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'").replace(/`/g,"`");return html;};/** + * Creates a HTML string with some fancy variable substitution. + * + * Actually, this method should not be required in the year 2022. + * jQuery and even native JS should probably provide a suitable alternative. + * But this method is used so ubiquitous that it might be difficult to get + * rid of it. + * + * TODO: Try it nonetheless. + * + * @param literalSections + * @param substs + * @returns {string} + */lychee.html=function(literalSections){// Use raw literal sections: we don’t want +// backslashes (\n etc.) to be interpreted +var raw=literalSections.raw;var result="";for(var _len=arguments.length,substs=new Array(_len>1?_len-1:0),_key=1;_key<_len;_key++){substs[_key-1]=arguments[_key];}substs.forEach(function(subst,i){// Retrieve the literal section preceding +// the current substitution +var lit=raw[i];// If the substitution is preceded by a dollar sign, +// we escape special characters in it +if(lit.slice(-1)==="$"){subst=lychee.escapeHTML(subst);lit=lit.slice(0,-1);}result+=lit;result+=subst;});// Take care of last literal section +// (Never fails, because an empty template string +// produces one literal section, an empty string) +result+=raw[raw.length-1];return result;};/** + * @param {XMLHttpRequest} jqXHR + * @param {Object} params the original JSON parameters of the request + * @param {?LycheeException} lycheeException the Lychee Exception + * @returns {boolean} + */lychee.handleAPIError=function(jqXHR,params,lycheeException){if(api.hasSessionExpired(jqXHR,lycheeException)){loadingBar.show("error",lychee.locale["ERROR_SESSION"]);setTimeout(function(){lychee["goto"]();window.location.reload();},3000);}else{var msg=jqXHR.statusText+(lycheeException?" - "+lycheeException.message:"");loadingBar.show("error",msg);console.error("The server returned an error response",{description:msg,params:params,response:lycheeException});}return true;};/** + * @returns {void} + */lychee.fullscreenEnter=function(){var elem=document.documentElement;if(elem.requestFullscreen){elem.requestFullscreen();}else if(elem.mozRequestFullScreen){/* Firefox */elem.mozRequestFullScreen();}else if(elem.webkitRequestFullscreen){/* Chrome, Safari and Opera */elem.webkitRequestFullscreen();}else if(elem.msRequestFullscreen){/* IE/Edge */elem.msRequestFullscreen();}};/** + * @returns {void} + */lychee.fullscreenExit=function(){if(document.exitFullscreen){document.exitFullscreen();}else if(document.mozCancelFullScreen){/* Firefox */document.mozCancelFullScreen();}else if(document.webkitExitFullscreen){/* Chrome, Safari and Opera */document.webkitExitFullscreen();}else if(document.msExitFullscreen){/* IE/Edge */document.msExitFullscreen();}};/** + * @returns {void} + */lychee.fullscreenToggle=function(){if(lychee.fullscreenStatus()){lychee.fullscreenExit();}else{lychee.fullscreenEnter();}};/** + * @returns {boolean} + */lychee.fullscreenStatus=function(){var elem=document.fullscreenElement||document.mozFullScreenElement||document.webkitFullscreenElement||document.msFullscreenElement;return!!elem;};/** + * @returns {boolean} + */lychee.fullscreenAvailable=function(){return document.fullscreenEnabled||document.mozFullscreenEnabled||document.webkitFullscreenEnabled||document.msFullscreenEnabled;};/** + * @returns {void} + */lychee.fullscreenUpdate=function(){if(lychee.fullscreenStatus()){$("#button_fs_album_enter,#button_fs_enter").hide();$("#button_fs_album_exit,#button_fs_exit").show();}else{$("#button_fs_album_enter,#button_fs_enter").show();$("#button_fs_album_exit,#button_fs_exit").hide();}};/** + * @returns {void} + */lychee.footer_show=function(){setTimeout(function(){lychee.footer.removeClass("hide_footer");},200);};/** + * @returns {void} + */lychee.footer_hide=function(){lychee.footer.addClass("hide_footer");};/** + * @returns {string} + */lychee.getBaseUrl=function(){if(location.href.includes("index.html")){return location.href.replace("index.html"+location.hash,"");}else if(location.href.includes("gallery#")){return location.href.replace("gallery"+location.hash,"");}else{return location.href.replace(location.hash,"");}};/** + * drag album to another one + * @param {DragEvent} ev + * @returns {void} + */lychee.startDrag=function(ev){/** @type ?HTMLDivElement */var div=ev.target.closest("div.album,div.photo");if(!div)return;var type=div.classList.contains("album")?"album":"photo";ev.dataTransfer.setData("text/plain","".concat(type,"-").concat(div.dataset.id));};/** + * drop album + * @param {DragEvent} ev + * @returns {void} + */lychee.finishDrag=function(ev){ev.preventDefault();/** @type string */var data=ev.dataTransfer.getData("text/plain");/** @type string */var targetId=ev.target.closest("div.album").dataset.id;if(!targetId||data.substring(6)===targetId)return;if(data.startsWith("photo-")){// photo is dragged +contextMenu.photoDrop(data.substring(6),targetId,ev);}else{// album is dragged +contextMenu.albumDrop(data.substring(6),targetId,ev);}};/** + * Album drag-over callback + * @param {DragEvent} ev + * @returns {void} + */lychee.overDrag=function(ev){ev.preventDefault();/** @type ?HTMLDivElement */var div=ev.target.closest("div.album");if(div){div.classList.add("album__dragover");}};/** + * Album drag-leave callback + * @param {DragEvent} ev + * @returns {void} + */lychee.leaveDrag=function(ev){/** @type ?HTMLDivElement */var div=ev.target.closest("div.album");if(div){div.classList.remove("album__dragover");}};/** + * drag-end callback + * @param {DragEvent} ev + * @returns {void} + */lychee.endDrag=function(ev){$("div.album").removeClass("album__dragover");};/** + * Adds the given event listener to the event target for both a `"click"` and + * `"touchend"` event. + * + * @param {Element} eventTarget + * @param {EventListenerOrEventListenerObject} listener + * @param {boolean|AddEventListenerOptions} [options] + * @return {void} + */lychee.addClickOrTouchListener=function(eventTarget,listener,options){eventTarget.addEventListener("click",listener,options);eventTarget.addEventListener("touchend",listener,options);};/** + * @typedef {Object.} Locale + * @property {function} printFilesizeLocalized + * @property {function} printDateTime + * @property {function} printMonthYear + */lychee.locale={USERNAME:"Username",PASSWORD:"Password",ENTER:"Enter",CANCEL:"Cancel",SIGN_IN:"Sign In",CLOSE:"Close",SETTINGS:"Settings",SEARCH:"Search …",MORE:"More",DEFAULT:"Default",GALLERY:"Gallery",USERS:"Users",CREATE:"Create",REMOVE:"Remove",SHARE:"Share",U2F:"U2F",NOTIFICATIONS:"Notifications",SHARING:"Sharing",CHANGE_LOGIN:"Change Login",CHANGE_SORTING:"Change Sorting",SET_DROPBOX:"Set Dropbox",ABOUT_LYCHEE:"About Lychee",DIAGNOSTICS:"Diagnostics",DIAGNOSTICS_GET_SIZE:"Request space usage",LOGS:"Show Logs",SIGN_OUT:"Sign Out",UPDATE_AVAILABLE:"Update available!",MIGRATION_AVAILABLE:"Migration available!",CHECK_FOR_UPDATE:"Check for updates",DEFAULT_LICENSE:"Default license for new uploads:",SET_LICENSE:"Set License",SET_OVERLAY_TYPE:"Set Overlay",SET_ALBUM_DECORATION:"Set album decorations",SET_MAP_PROVIDER:"Set OpenStreetMap tiles provider",FULL_SETTINGS:"Full Settings",UPDATE:"Update",RESET:"Reset",DISABLE_TOKEN_TOOLTIP:"Disable",ENABLE_TOKEN:"Enable API token",DISABLED_TOKEN_STATUS_MSG:"Disabled",TOKEN_BUTTON:"API Token ...",TOKEN_NOT_AVAILABLE:"You have already viewed this token.",TOKEN_WAIT:"Wait ...",SMART_ALBUMS:"Smart albums",SHARED_ALBUMS:"Shared albums",ALBUMS:"Albums",PHOTOS:"Pictures",SEARCH_RESULTS:"Search results",RENAME:"Rename",RENAME_ALL:"Rename Selected",MERGE:"Merge",MERGE_ALL:"Merge Selected",MAKE_PUBLIC:"Make Public",SHARE_ALBUM:"Share Album",SHARE_PHOTO:"Share Photo",VISIBILITY_ALBUM:"Album Visibility",VISIBILITY_PHOTO:"Photo Visibility",DOWNLOAD_ALBUM:"Download Album",ABOUT_ALBUM:"About Album",DELETE_ALBUM:"Delete Album",MOVE_ALBUM:"Move Album",FULLSCREEN_ENTER:"Enter Fullscreen",FULLSCREEN_EXIT:"Exit Fullscreen",SHARING_ALBUM_USERS:"Share this album with users",WAIT_FETCH_DATA:"Please wait while we get the data …",SHARING_ALBUM_USERS_NO_USERS:"There are no users to share the album with",SHARING_ALBUM_USERS_LONG_MESSAGE:"Select the users to share this album with",DELETE_ALBUM_QUESTION:"Delete Album and Photos",KEEP_ALBUM:"Keep Album",DELETE_ALBUM_CONFIRMATION:"Are you sure you want to delete the album “%s” and all of the photos it contains? This action can’t be undone!",DELETE_TAG_ALBUM_QUESTION:"Delete Album",DELETE_TAG_ALBUM_CONFIRMATION:"Are you sure you want to delete the album “%s” (any photos inside will not be deleted)? This action can’t be undone!",DELETE_ALBUMS_QUESTION:"Delete Albums and Photos",KEEP_ALBUMS:"Keep Albums",DELETE_ALBUMS_CONFIRMATION:"Are you sure you want to delete all %d selected albums and all of the photos they contain? This action can’t be undone!",DELETE_UNSORTED_CONFIRM:"Are you sure you want to delete all photos from “Unsorted”? This action can’t be undone!",CLEAR_UNSORTED:"Clear Unsorted",KEEP_UNSORTED:"Keep Unsorted",EDIT_SHARING:"Edit Sharing",MAKE_PRIVATE:"Make Private",CLOSE_ALBUM:"Close Album",CLOSE_PHOTO:"Close Photo",CLOSE_MAP:"Close Map",ADD:"Add",MOVE:"Move",MOVE_ALL:"Move Selected",DUPLICATE:"Duplicate",DUPLICATE_ALL:"Duplicate Selected",COPY_TO:"Copy to …",COPY_ALL_TO:"Copy Selected to …",DELETE:"Delete",SAVE:"Save",DELETE_ALL:"Delete Selected",DOWNLOAD:"Download",DOWNLOAD_ALL:"Download Selected",UPLOAD_PHOTO:"Upload Photo",IMPORT_LINK:"Import from Link",IMPORT_DROPBOX:"Import from Dropbox",IMPORT_SERVER:"Import from Server",NEW_ALBUM:"New Album",NEW_TAG_ALBUM:"New Tag Album",UPLOAD_TRACK:"Upload track",DELETE_TRACK:"Delete track",TITLE_NEW_ALBUM:"Enter a title for the new album:",UNTITLED:"Untitled",UNSORTED:"Unsorted",STARRED:"Starred",RECENT:"Recent",PUBLIC:"Public",ON_THIS_DAY:"On This Day",NUM_PHOTOS:"Photos",CREATE_ALBUM:"Create Album",CREATE_TAG_ALBUM:"Create Tag Album",STAR_PHOTO:"Star Photo",STAR:"Star",UNSTAR:"Unstar",STAR_ALL:"Star Selected",UNSTAR_ALL:"Unstar Selected",TAG:"Tag",TAG_ALL:"Tag Selected",UNSTAR_PHOTO:"Unstar Photo",SET_COVER:"Set Album Cover",REMOVE_COVER:"Remove Album Cover",FULL_PHOTO:"Open Original",ABOUT_PHOTO:"About Photo",DISPLAY_FULL_MAP:"Map",DIRECT_LINK:"Direct Link",DIRECT_LINKS:"Direct Links",QR_CODE:"QR Code",ALBUM_ABOUT:"About",ALBUM_BASICS:"Basics",ALBUM_TITLE:"Title",ALBUM_NEW_TITLE:"Enter a new title for this album:",ALBUMS_NEW_TITLE:"Enter a title for all %d selected albums:",ALBUM_SET_TITLE:"Set Title",ALBUM_DESCRIPTION:"Description",ALBUM_SHOW_TAGS:"Tags to show",ALBUM_NEW_DESCRIPTION:"Enter a new description for this album:",ALBUM_SET_DESCRIPTION:"Set Description",ALBUM_NEW_SHOWTAGS:"Enter tags of photos that will be visible in this album:",ALBUM_SET_SHOWTAGS:"Set tags to show",ALBUM_ALBUM:"Album",ALBUM_CREATED:"Created",ALBUM_IMAGES:"Images",ALBUM_VIDEOS:"Videos",ALBUM_SUBALBUMS:"Subalbums",ALBUM_SHARING:"Share",ALBUM_SHR_YES:"YES",ALBUM_SHR_NO:"No",ALBUM_PUBLIC:"Public",ALBUM_PUBLIC_EXPL:"Anonymous users can access this album, subject to the restrictions below.",ALBUM_FULL:"Original",ALBUM_FULL_EXPL:"Anonymous users can behold full-resolution photos.",ALBUM_HIDDEN:"Hidden",ALBUM_HIDDEN_EXPL:"Anonymous users need a direct link to access this album.",ALBUM_MARK_NSFW:"Mark album as sensitive",ALBUM_UNMARK_NSFW:"Unmark album as sensitive",ALBUM_NSFW:"Sensitive",ALBUM_NSFW_EXPL:"Album contains sensitive content.",ALBUM_DOWNLOADABLE:"Downloadable",ALBUM_DOWNLOADABLE_EXPL:"Anonymous users can download this album.",ALBUM_SHARE_BUTTON_VISIBLE:"Share button is visible",ALBUM_SHARE_BUTTON_VISIBLE_EXPL:"Anonymous users can see social media sharing links.",ALBUM_PASSWORD:"Password",ALBUM_PASSWORD_PROT:"Password protected",ALBUM_PASSWORD_PROT_EXPL:"Anonymous users need a shared password to access this album.",ALBUM_PASSWORD_REQUIRED:"This album is protected by a password. Enter the password below to view the photos of this album:",ALBUM_MERGE:"Are you sure you want to merge the album “%1$s” into the album “%2$s”?",ALBUMS_MERGE:"Are you sure you want to merge all selected albums into the album “%s”?",MERGE_ALBUM:"Merge Albums",DONT_MERGE:"Don’t Merge",ALBUM_MOVE:"Are you sure you want to move the album “%1$s” into the album “%2$s”?",ALBUMS_MOVE:"Are you sure you want to move all selected albums into the album “%s”?",MOVE_ALBUMS:"Move Albums",NOT_MOVE_ALBUMS:"Don’t Move",ROOT:"Albums",ALBUM_REUSE:"Reuse",ALBUM_LICENSE:"License",ALBUM_SET_LICENSE:"Set License",ALBUM_LICENSE_HELP:"Need help choosing?",ALBUM_LICENSE_NONE:"None",ALBUM_RESERVED:"All Rights Reserved",ALBUM_SET_ORDER:"Set Order",ALBUM_ORDERING:"Order by",ALBUM_OWNER:"Owner",PHOTO_ABOUT:"About",PHOTO_BASICS:"Basics",PHOTO_TITLE:"Title",PHOTO_NEW_TITLE:"Enter a new title for this photo:",PHOTO_SET_TITLE:"Set Title",PHOTO_UPLOADED:"Uploaded",PHOTO_DESCRIPTION:"Description",PHOTO_NEW_DESCRIPTION:"Enter a new description for this photo:",PHOTO_SET_DESCRIPTION:"Set Description",PHOTO_NEW_LICENSE:"Add a License",PHOTO_SET_LICENSE:"Set License",PHOTO_LICENSE:"License",PHOTO_LICENSE_HELP:"Need help choosing?",PHOTO_REUSE:"Reuse",PHOTO_LICENSE_NONE:"None",PHOTO_RESERVED:"All Rights Reserved",PHOTO_LATITUDE:"Latitude",PHOTO_LONGITUDE:"Longitude",PHOTO_ALTITUDE:"Altitude",PHOTO_IMGDIRECTION:"Direction",PHOTO_LOCATION:"Location",PHOTO_IMAGE:"Image",PHOTO_VIDEO:"Video",PHOTO_SIZE:"Size",PHOTO_FORMAT:"Format",PHOTO_RESOLUTION:"Resolution",PHOTO_DURATION:"Duration",PHOTO_FPS:"Frame rate",PHOTO_TAGS:"Tags",PHOTO_NOTAGS:"No Tags",PHOTO_NEW_TAGS:"Enter your tags for this photo. You can add multiple tags by separating them with a comma:",PHOTOS_NEW_TAGS:"Enter your tags for all %d selected photos. Existing tags will be overwritten. You can add multiple tags by separating them with a comma:",PHOTO_SET_TAGS:"Set Tags",TAGS_OVERRIDE_INFO:"If this is unchecked, the tags will be added to the existing tags of the photo.",PHOTO_CAMERA:"Camera",PHOTO_CAPTURED:"Captured",PHOTO_MAKE:"Make",PHOTO_TYPE:"Type/Model",PHOTO_LENS:"Lens",PHOTO_SHUTTER:"Shutter Speed",PHOTO_APERTURE:"Aperture",PHOTO_FOCAL:"Focal Length",PHOTO_ISO:"ISO %s",PHOTO_SHARING:"Sharing",PHOTO_SHR_PUBLIC:"Public",PHOTO_SHR_ALB:"Yes (Album)",PHOTO_SHR_PHT:"Yes (Photo)",PHOTO_SHR_NO:"No",PHOTO_DELETE:"Delete Photo",PHOTO_KEEP:"Keep Photo",PHOTO_DELETE_CONFIRMATION:"Are you sure you want to delete the photo “%s”? This action can’t be undone!",PHOTO_DELETE_ALL:"Are you sure you want to delete all %d selected photo? This action can’t be undone!",PHOTOS_NEW_TITLE:"Enter a title for all %d selected photos:",PHOTO_MAKE_PRIVATE_ALBUM:"This photo is located in a public album. To make this photo private or public, edit the visibility of the associated album.",PHOTO_SHOW_ALBUM:"Show Album",PHOTO_PUBLIC:"Public",PHOTO_PUBLIC_EXPL:"Anonymous users can view this photo, subject to the restrictions below.",PHOTO_FULL:"Original",PHOTO_FULL_EXPL:"Anonymous users can behold full-resolution photo.",PHOTO_HIDDEN:"Hidden",PHOTO_HIDDEN_EXPL:"Anonymous users need a direct link to view this photo.",PHOTO_DOWNLOADABLE:"Downloadable",PHOTO_DOWNLOADABLE_EXPL:"Anonymous users may download this photo.",PHOTO_SHARE_BUTTON_VISIBLE:"Share button is visible",PHOTO_SHARE_BUTTON_VISIBLE_EXPL:"Anonymous users can see social media sharing links.",PHOTO_PASSWORD_PROT:"Password protected",PHOTO_PASSWORD_PROT_EXPL:"Anonymous users need a shared password to view this photo.",PHOTO_EDIT_SHARING_TEXT:"The sharing properties of this photo will be changed to the following:",PHOTO_NO_EDIT_SHARING_TEXT:"Because this photo is located in a public album, it inherits that album’s visibility settings. Its current visibility is shown below for informational purposes only.",PHOTO_EDIT_GLOBAL_SHARING_TEXT:"The visibility of this photo can be fine-tuned using global Lychee settings. Its current visibility is shown below for informational purposes only.",PHOTO_NEW_CREATED_AT:"Enter the upload date for this photo. mm/dd/yyyy, hh:mm [am/pm]",PHOTO_SET_CREATED_AT:"Set upload date",LOADING:"Loading",ERROR:"Error",ERROR_TEXT:"Whoops, it looks like something went wrong. Please reload the site and try again!",ERROR_UNKNOWN:"Something unexpected happened. Please try again and check your installation and server. Take a look at the readme for more information.",ERROR_MAP_DEACTIVATED:"Map functionality has been deactivated under settings.",ERROR_SEARCH_DEACTIVATED:"Search functionality has been deactivated under settings.",SUCCESS:"OK",RETRY:"Retry",OVERRIDE:"Override",SETTINGS_SUCCESS_LOGIN:"Login Info updated.",SETTINGS_SUCCESS_SORT:"Sorting order updated.",SETTINGS_SUCCESS_DROPBOX:"Dropbox Key updated.",SETTINGS_SUCCESS_LANG:"Language updated",SETTINGS_SUCCESS_LAYOUT:"Layout updated",SETTINGS_SUCCESS_IMAGE_OVERLAY:"Image overlay setting updated",SETTINGS_SUCCESS_ALBUM_DECORATION:"Album decorations updated",SETTINGS_SUCCESS_PUBLIC_SEARCH:"Public search updated",SETTINGS_SUCCESS_LICENSE:"Default license updated",SETTINGS_SUCCESS_MAP_DISPLAY:"Map display settings updated",SETTINGS_SUCCESS_MAP_DISPLAY_PUBLIC:"Map display settings for public albums updated",SETTINGS_SUCCESS_MAP_PROVIDER:"Map provider settings updated",SETTINGS_SUCCESS_CSS:"CSS updated",SETTINGS_SUCCESS_JS:"JS updated",SETTINGS_SUCCESS_UPDATE:"Settings updated successfully",SETTINGS_DROPBOX_KEY:"Dropbox API Key",SETTINGS_ADVANCED_WARNING_EXPL:"Changing these advanced settings can be harmful to the stability, security and performance of this application. You should only modify them if you are sure of what you are doing.",SETTINGS_ADVANCED_SAVE:"Save my modifications, I accept the risk!",U2F_NOT_SUPPORTED:"U2F not supported. Sorry.",U2F_NOT_SECURE:"Environment not secured. U2F not available.",U2F_REGISTER_KEY:"Register new device.",U2F_REGISTRATION_SUCCESS:"Registration successful!",U2F_AUTHENTIFICATION_SUCCESS:"Authentication successful!",U2F_CREDENTIALS:"Credentials",U2F_CREDENTIALS_DELETED:"Credentials deleted!",NEW_PHOTOS_NOTIFICATION:"Send new photos notification emails.",SETTINGS_SUCCESS_NEW_PHOTOS_NOTIFICATION:"New photos notification updated",USER_EMAIL_INSTRUCTION:"Add your email below to enable receiving email notifications. To stop receiving emails, simply remove your email below.",LOGIN_USERNAME:"New Username",LOGIN_PASSWORD:"New Password",LOGIN_PASSWORD_CONFIRM:"Confirm Password",PASSWORD_TITLE:"Enter your current password:",PASSWORD_CURRENT:"Current Password",PASSWORD_TEXT:"Your credentials will be changed to the following:",PASSWORD_CHANGE:"Change Login",EDIT_SHARING_TITLE:"Edit Sharing",EDIT_SHARING_TEXT:"The sharing properties of this album will be changed to the following:",SHARE_ALBUM_TEXT:"This album will be shared with the following properties:",SORT_DIALOG_ATTRIBUTE_LABEL:"Attribute",SORT_DIALOG_ORDER_LABEL:"Order",SORT_ALBUM_BY:"Sort albums by %1$s in an %2$s order.",SORT_ALBUM_SELECT_1:"Creation Time",SORT_ALBUM_SELECT_2:"Title",SORT_ALBUM_SELECT_3:"Description",SORT_ALBUM_SELECT_5:"Latest Take Date",SORT_ALBUM_SELECT_6:"Oldest Take Date",SORT_PHOTO_BY:"Sort photos by %1$s in an %2$s order.",SORT_PHOTO_SELECT_1:"Upload Time",SORT_PHOTO_SELECT_2:"Take Date",SORT_PHOTO_SELECT_3:"Title",SORT_PHOTO_SELECT_4:"Description",SORT_PHOTO_SELECT_5:"Public",SORT_PHOTO_SELECT_6:"Star",SORT_PHOTO_SELECT_7:"Photo Format",SORT_ASCENDING:"Ascending",SORT_DESCENDING:"Descending",SORT_CHANGE:"Change Sorting",DROPBOX_TITLE:"Set Dropbox Key",DROPBOX_TEXT:"In order to import photos from your Dropbox, you need a valid drop-ins app key from their website. Generate yourself a personal key and enter it below:",LANG_TEXT:"Change Lychee language for:",LANG_TITLE:"Change Language",SETTING_RECENT_PUBLIC_TEXT:'Make "Recent" smart album accessible to anonymous users',SETTING_STARRED_PUBLIC_TEXT:'Make "Starred" smart album accessible to anonymous users',SETTING_ONTHISDAY_PUBLIC_TEXT:'Make "On This Day" smart album accessible to anonymous users',CSS_TEXT:"Personalize CSS:",CSS_TITLE:"Change CSS",JS_TEXT:"Custom JS:",JS_TITLE:"Change JS",PUBLIC_SEARCH_TEXT:"Public search allowed:",OVERLAY_TYPE:"Photo overlay:",OVERLAY_NONE:"None",OVERLAY_EXIF:"EXIF data",OVERLAY_DESCRIPTION:"Description",OVERLAY_DATE:"Date taken",ALBUM_DECORATION:"Album decorations:",ALBUM_DECORATION_NONE:"No badges",ALBUM_DECORATION_ORIGINAL:"Sub-album badge, no count",ALBUM_DECORATION_ALBUM:"Sub-album badge with count",ALBUM_DECORATION_PHOTO:"Photo badge with count",ALBUM_DECORATION_ALL:"Sub-album and photo badges with counts",ALBUM_DECORATION_ORIENTATION:"Orientation of album decorations:",ALBUM_DECORATION_ORIENTATION_ROW:"Horizontal decorations (photos, albums)",ALBUM_DECORATION_ORIENTATION_ROW_REVERSE:"Horizontal decorations (albums, photos)",ALBUM_DECORATION_ORIENTATION_COLUMN:"Vertical decorations (top photos, albums)",ALBUM_DECORATION_ORIENTATION_COLUMN_REVERSE:"Vertical decorations (top albums, photos)",MAP_DISPLAY_TEXT:"Enable maps (provided by OpenStreetMap):",MAP_DISPLAY_PUBLIC_TEXT:"Enable maps for public albums (provided by OpenStreetMap):",MAP_PROVIDER:"Provider of OpenStreetMap tiles:",MAP_PROVIDER_WIKIMEDIA:"Wikimedia",MAP_PROVIDER_OSM_ORG:"OpenStreetMap.org (no HiDPI)",MAP_PROVIDER_OSM_DE:"OpenStreetMap.de (no HiDPI)",MAP_PROVIDER_OSM_FR:"OpenStreetMap.fr (no HiDPI)",MAP_PROVIDER_RRZE:"University of Erlangen, Germany (only HiDPI)",MAP_INCLUDE_SUBALBUMS_TEXT:"Include photos of subalbums on map:",LOCATION_DECODING:"Decode GPS data into location name",LOCATION_SHOW:"Show location name",LOCATION_SHOW_PUBLIC:"Show location name for public mode",LAYOUT_TYPE:"Layout of photos:",LAYOUT_SQUARES:"Square thumbnails",LAYOUT_JUSTIFIED:"With aspect, justified",LAYOUT_UNJUSTIFIED:"With aspect, unjustified",SET_LAYOUT:"Change layout",NSFW_VISIBLE_TEXT_1:"Make Sensitive albums visible by default.",NSFW_VISIBLE_TEXT_2:"If the album is public, it is still accessible, just hidden from the view and can be revealed by pressing H.",SETTINGS_SUCCESS_NSFW_VISIBLE:"Default sensitive album visibility updated with success.",NSFW_BANNER:"

Sensitive content

This album contains sensitive content which some people may find offensive or disturbing.

Tap to consent.

",VIEW_NO_RESULT:"No results",VIEW_NO_PUBLIC_ALBUMS:"No public albums",VIEW_NO_CONFIGURATION:"No configuration",VIEW_PHOTO_NOT_FOUND:"Photo not found",NO_TAGS:"No Tags",UPLOAD_MANAGE_NEW_PHOTOS:"You can now manage your new photo(s).",UPLOAD_COMPLETE:"Upload complete",UPLOAD_COMPLETE_FAILED:"Failed to upload one or more photos.",UPLOAD_IMPORTING:"Importing",UPLOAD_IMPORTING_URL:"Importing URL",UPLOAD_UPLOADING:"Uploading",UPLOAD_FINISHED:"Finished",UPLOAD_PROCESSING:"Processing",UPLOAD_FAILED:"Failed",UPLOAD_FAILED_ERROR:"Upload failed. The server returned an error!",UPLOAD_FAILED_WARNING:"Upload failed. The server returned a warning!",UPLOAD_CANCELLED:"Cancelled",UPLOAD_SKIPPED:"Skipped",UPLOAD_UPDATED:"Updated",UPLOAD_GENERAL:"General",UPLOAD_IMPORT_SKIPPED_DUPLICATE:"This photo has been skipped because it’s already in your library.",UPLOAD_IMPORT_RESYNCED_DUPLICATE:"This photo has been skipped because it’s already in your library, but its metadata has been updated.",UPLOAD_ERROR_CONSOLE:"Please take a look at the console of your browser for further details.",UPLOAD_UNKNOWN:"Server returned an unknown response. Please take a look at the console of your browser for further details.",UPLOAD_ERROR_UNKNOWN:"Upload failed. The server returned an unknown error!",UPLOAD_ERROR_POSTSIZE:"Upload failed. The PHP post_max_size may be too small! Otherwise check the FAQ.",UPLOAD_ERROR_FILESIZE:"Upload failed. The PHP upload_max_filesize may be too small! Otherwise check the FAQ.",UPLOAD_IN_PROGRESS:"Lychee is currently uploading!",UPLOAD_IMPORT_WARN_ERR:"The import has been finished, but returned warnings or errors. Please take a look at the log (Settings -> Show Log) for further details.",UPLOAD_IMPORT_COMPLETE:"Import complete",UPLOAD_IMPORT_INSTR:"Please enter the direct link to a photo to import it:",UPLOAD_IMPORT:"Import",UPLOAD_IMPORT_SERVER:"Importing from server",UPLOAD_IMPORT_SERVER_FOLD:"Folder empty or no readable files to process. Please take a look at the log (Settings -> Show Log) for further details.",UPLOAD_IMPORT_SERVER_INSTR:"Import all photos, folders, and sub-folders located in the folders with the following absolute paths (on the server). Paths are space-separated, use \\ to escape a space in a path.",UPLOAD_ABSOLUTE_PATH:"Absolute path to directories, space separated",UPLOAD_IMPORT_SERVER_EMPT:"Could not start import because the folder was empty!",UPLOAD_IMPORT_DELETE_ORIGINALS:"Delete originals",UPLOAD_IMPORT_DELETE_ORIGINALS_EXPL:"Original files will be deleted after the import when possible.",UPLOAD_IMPORT_VIA_SYMLINK:"Symbolic links",UPLOAD_IMPORT_VIA_SYMLINK_EXPL:"Import files using symbolic links to originals.",UPLOAD_IMPORT_SKIP_DUPLICATES:"Skip duplicates",UPLOAD_IMPORT_SKIP_DUPLICATES_EXPL:"Existing media files are skipped.",UPLOAD_IMPORT_RESYNC_METADATA:"Re-sync metadata",UPLOAD_IMPORT_RESYNC_METADATA_EXPL:"Update metadata of existing media files.",UPLOAD_IMPORT_LOW_MEMORY_EXPL:"The import process on the server is approaching the memory limit and may end up being terminated prematurely.",UPLOAD_WARNING:"Warning",UPLOAD_IMPORT_NOT_A_DIRECTORY:"The given path is not a readable directory!",UPLOAD_IMPORT_PATH_RESERVED:"The given path is a reserved path of Lychee!",UPLOAD_IMPORT_FAILED:"Could not import the file!",UPLOAD_IMPORT_UNSUPPORTED:"Unsupported file type!",UPLOAD_IMPORT_CANCELLED:"Import cancelled",ABOUT_SUBTITLE:"Self-hosted photo-management done right",ABOUT_DESCRIPTION:"Lychee is a free photo-management tool, which runs on your server or web-space. Installing is a matter of seconds. Upload, manage and share photos like from a native application. Lychee comes with everything you need and all your photos are stored securely.",FOOTER_COPYRIGHT:"All images on this website are subject to copyright by %1$s © %2$s",HOSTED_WITH_LYCHEE:"Hosted with Lychee",URL_COPY_TO_CLIPBOARD:"Copy to clipboard",URL_COPIED_TO_CLIPBOARD:"Copied URL to clipboard!",PHOTO_DIRECT_LINKS_TO_IMAGES:"Direct links to image files:",PHOTO_ORIGINAL:"Original",PHOTO_MEDIUM:"Medium",PHOTO_MEDIUM_HIDPI:"Medium HiDPI",PHOTO_SMALL:"Thumb",PHOTO_SMALL_HIDPI:"Thumb HiDPI",PHOTO_THUMB:"Square thumb",PHOTO_THUMB_HIDPI:"Square thumb HiDPI",PHOTO_THUMBNAIL:"Photo thumbnail",PHOTO_LIVE_VIDEO:"Video part of live-photo",PHOTO_VIEW:"Lychee Photo View:",PHOTO_EDIT_ROTATECWISE:"Rotate clockwise",PHOTO_EDIT_ROTATECCWISE:"Rotate counter-clockwise",ERROR_GPX:"Error loading GPX file: ",ERROR_EITHER_ALBUMS_OR_PHOTOS:"Please select either albums or photos!",ERROR_COULD_NOT_FIND:"Could not find what you want.",ERROR_INVALID_EMAIL:"Not a valid email address.",EMAIL_SUCCESS:"Email updated!",ERROR_PHOTO_NOT_FOUND:"Error: photo %s not found!",ERROR_EMPTY_USERNAME:"new username cannot be empty.",ERROR_PASSWORD_DOES_NOT_MATCH:"new password does not match.",ERROR_EMPTY_PASSWORD:"new password cannot be empty.",ERROR_SELECT_ALBUM:"Select an album to share!",ERROR_SELECT_USER:"Select a user to share with!",ERROR_SELECT_SHARING:"Select a sharing to remove!",SHARING_SUCCESS:"Sharing updated!",SHARING_REMOVED:"Sharing removed!",USER_CREATED:"User created!",USER_DELETED:"User deleted!",USER_UPDATED:"User updated!",ENTER_EMAIL:"Enter your email address:",ERROR_ALBUM_JSON_NOT_FOUND:"Error: Album JSON not found!",ERROR_ALBUM_NOT_FOUND:"Error: album %s not found",ERROR_DROPBOX_KEY:"Error: Dropbox key not set",ERROR_SESSION:"Session expired.",CAMERA_DATE:"Camera date",NEW_PASSWORD:"new password",ALLOW_UPLOADS:"Allow uploads",ALLOW_USER_SELF_EDIT:"Allow self-management of user account",OSM_CONTRIBUTORS:"OpenStreetMap contributors",dateTimeFormatter:new Intl.DateTimeFormat("default",{dateStyle:"medium",timeStyle:"medium"}),/** + * Formats a number representing a filesize in bytes as a localized string + * @param {!number} filesize + * @returns {string} A formatted and localized string + */printFilesizeLocalized:function printFilesizeLocalized(filesize){var suffix=[" B"," kB"," MB"," GB"];var i=0;// Sic! We check if the number is larger than 1000 but divide by 1024 by intention +// We aim at a number which has at most 3 non-decimal digits, i.e. the result shall be in the interval +// [1000/1024, 1000) = [0.977, 1000) (lower bound included, upper bound excluded) +while(filesize>=1000.0&&i=100.0){filesize=Math.round(filesize);}else if(filesize>=10.0){filesize=Math.round(filesize*10.0)/10.0;}else{filesize=Math.round(filesize*100.0)/100.0;}return Number(filesize).toLocaleString()+suffix[i];},/** + * Converts a JSON encoded date/time into a localized string relative to + * the original timezone + * + * The localized string uses the JS "medium" verbosity. + * The precise definition of "medium verbosity" depends on the current locale, but for Western languages this + * means that the date portion is fully printed with digits (e.g. something like 03/30/2021 for English, + * 30/03/2021 for French and 30.03.2021 for German), and that the time portion is printed with a resolution of + * seconds with two digits for all parts either in 24h or 12h scheme (e.g. something like 02:24:13pm for English + * and 14:24:13 for French/German). + * + * @param {?string} jsonDateTime + * @returns {string} A formatted and localized time + */printDateTime:function printDateTime(jsonDateTime){if(typeof jsonDateTime!=="string"||jsonDateTime==="")return"";// Unfortunately, the built-in JS Date object is rather dumb. +// It is only required to support the timezone of the runtime +// environment and UTC. +// Moreover, the method `toLocalString` may or may not convert +// the represented time to the timezone of the runtime environment +// before formatting it as a string. +// However, we want to keep the printed time in the original timezone, +// because this facilitates human interaction with a photo. +// To this end we apply a "dirty" trick here. +// We first cut off any explicit timezone indication from the JSON +// string and only pass a date/time of the form `YYYYMMDDThhmmss` to +// `Date`. +// `Date` is required to interpret those time values according to the +// local timezone (see [MDN Web Docs - Date Time String Format](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse#date_time_string_format)). +// Most likely, the resulting `Date` object will represent the +// wrong instant in time (given in seconds since epoch), but we only +// want to call `toLocalString` which is fine and don't do any time +// arithmetics. +// Then we add the original timezone to the string manually. +var splitDateTime=/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}([,.]\d{1,6})?)([-Z+])(\d{2}:\d{2})?$/.exec(jsonDateTime);// The capturing groups are: +// - 0: the whole string +// - 1: the whole date/time segment incl. fractional seconds +// - 2: the fractional seconds (if present) +// - 3: the timezone separator, i.e. "Z", "-" or "+" (if present) +// - 4: the absolute timezone offset without the sign (if present) +console.assert(splitDateTime.length===5,"'jsonDateTime' is not formatted acc. to ISO 8601; passed string was: "+jsonDateTime);var result=lychee.locale.dateTimeFormatter.format(new Date(splitDateTime[1]));if(splitDateTime[3]==="Z"||splitDateTime[4]==="00:00"){result+=" UTC";}else{result+=" UTC"+splitDateTime[3]+splitDateTime[4];}return result;},/** + * Converts a JSON encoded date/time into a localized string which only displays month and year. + * + * The month is printed as a shortened word with 3/4 letters, the year is printed with 4 digits (e.g. something like + * "Aug 2020" in English or "Août 2020" in French). + * + * @param {?string} jsonDateTime + * @returns {string} A formatted and localized month and year + */printMonthYear:function printMonthYear(jsonDateTime){if(typeof jsonDateTime!=="string"||jsonDateTime==="")return"";var locale="default";// use the user's browser settings +var format={month:"short",year:"numeric"};return new Date(jsonDateTime).toLocaleDateString(locale,format);}};/** + * @description This module takes care of the map view of a full album and its sub-albums. + */ /** + * @typedef MapProvider + * @property {string} layer - URL pattern for map tile + * @property {string} attribution - HTML with attribution + */var map_provider_layer_attribution={/** + * @type {MapProvider} + */Wikimedia:{layer:"https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}{r}.png",attribution:'Wikimedia'},/** + * @type {MapProvider} + */"OpenStreetMap.org":{layer:"https://tile.openstreetmap.org/{z}/{x}/{y}.png",attribution:"© ".concat(lychee.locale["OSM_CONTRIBUTORS"],"")},/** + * @type {MapProvider} + */"OpenStreetMap.de":{layer:"https://tile.openstreetmap.de/{z}/{x}/{y}.png ",attribution:"© ".concat(lychee.locale["OSM_CONTRIBUTORS"],"")},/** + * @type {MapProvider} + */"OpenStreetMap.fr":{layer:"https://{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png ",attribution:"© ".concat(lychee.locale["OSM_CONTRIBUTORS"],"")},/** + * @type {MapProvider} + */RRZE:{layer:"https://{s}.osm.rrze.fau.de/osmhd/{z}/{x}/{y}.png",attribution:"© ".concat(lychee.locale["OSM_CONTRIBUTORS"],"")}};var mapview={/** @type {?L.Map} */map:null,photoLayer:null,trackLayer:null,/** @type {(?LatLngBounds|?number[][])} */bounds:null,/** @type {?string} */albumID:null,/** @type {?string} */map_provider:null};/** + * @typedef MapPhotoEntry + * + * @property {number} [lat] - latitude + * @property {number} [lng] - longitude + * @property {string} [thumbnail] - URL to the thumbnail + * @property {string} [thumbnail2x] - URL to the high-res thumbnail + * @property {string} url - URL to the small size-variant; quite a misnomer + * @property {string} url2x - URL to the small, high-res size-variant; quite a misnomer + * @property {string} name - the title of the photo + * @property {string} taken_at - the takedate of the photo, formatted as a locale string + * @property {string} albumID - the album ID + * @property {string} photoID - the photo ID + */ /** + * @returns {boolean} + */mapview.isInitialized=function(){return!(mapview.map===null||mapview.photoLayer===null);};/** + * @param {?string} _albumID + * @param {string} _albumTitle + * + * @returns {void} + */mapview.title=function(_albumID,_albumTitle){switch(_albumID){case SmartAlbumID.STARRED:lychee.setMetaData(lychee.locale["STARRED"]);break;case SmartAlbumID.PUBLIC:lychee.setMetaData(lychee.locale["PUBLIC"]);break;case SmartAlbumID.RECENT:lychee.setMetaData(lychee.locale["RECENT"]);break;case SmartAlbumID.UNSORTED:lychee.setMetaData(lychee.locale["UNSORTED"]);break;case SmartAlbumID.ON_THIS_DAY:lychee.setMetaData(lychee.locale["ON_THIS_DAY"]);break;case null:lychee.setMetaData(lychee.locale["ALBUMS"]);break;default:lychee.setMetaData(_albumTitle?_albumTitle:lychee.locale["UNTITLED"]);break;}};/** + * Opens the map view + * + * @param {?string} [albumID=null] + * @returns {void} + */mapview.open=function(){var albumID=arguments.length>0&&arguments[0]!==undefined?arguments[0]:null;// If map functionality is disabled -> do nothing +if(!lychee.map_display||lychee.publicMode===true&&!lychee.map_display_public){loadingBar.show("error",lychee.locale["ERROR_MAP_DEACTIVATED"]);return;}var mapContainer=$("#lychee_map_container");lychee.animate(mapContainer,"fadeIn");mapContainer.addClass("active");header.setMode("map");mapview.albumID=albumID;// initialize container only once +if(!mapview.isInitialized()){// Leaflet searches for icon in same directory as js file -> paths need +// to be overwritten +delete L.Icon.Default.prototype._getIconUrl;L.Icon.Default.mergeOptions({iconRetinaUrl:"img/marker-icon-2x.png",iconUrl:"img/marker-icon.png",shadowUrl:"img/marker-shadow.png"});// Set initial view to (0,0) +mapview.map=L.map("lychee_map_container").setView([0.0,0.0],13);L.tileLayer(map_provider_layer_attribution[lychee.map_provider].layer,{attribution:map_provider_layer_attribution[lychee.map_provider].attribution}).addTo(mapview.map);mapview.map_provider=lychee.map_provider;}else{if(mapview.map_provider!==lychee.map_provider){// removew all layers +mapview.map.eachLayer(function(layer){mapview.map.removeLayer(layer);});L.tileLayer(map_provider_layer_attribution[lychee.map_provider].layer,{attribution:map_provider_layer_attribution[lychee.map_provider].attribution}).addTo(mapview.map);mapview.map_provider=lychee.map_provider;}else{// Mapview has already shown data -> remove only photoLayer and trackLayer showing photos and tracks +mapview.photoLayer.clear();if(mapview.trackLayer!==null){mapview.map.removeLayer(mapview.trackLayer);}}// Reset bounds +mapview.bounds=null;}// Define how the photos on the map should look like +mapview.photoLayer=L.photo.cluster().on("click",function(e){/** @type {MapPhotoEntry} */var photo={photoID:e.layer.photo.photoID,albumID:e.layer.photo.albumID,name:e.layer.photo.name,url:e.layer.photo.url,url2x:e.layer.photo.url2x,taken_at:lychee.locale.printDateTime(e.layer.photo.taken_at)};var template="";// Retina version if available +if(photo.url2x!==""){template=template.concat('

{name}

',build.iconic("camera-slr"),"

{taken_at}

");}else{template=template.concat('

{name}

',build.iconic("camera-slr"),"

{taken_at}

");}e.layer.bindPopup(L.Util.template(template,photo),{minWidth:400}).openPopup();});// Adjusts zoom and position of map to show all images +var updateZoom=function updateZoom(){if(mapview.bounds){mapview.map.fitBounds(mapview.bounds);}else{mapview.map.fitWorld();}};/** + * Adds photos to the map. + * + * @param {(Album|TagAlbum|PositionData)} album + * + * @returns {void} + */var addContentsToMap=function addContentsToMap(album){// check if empty +if(!album.photos)return;/** @type {MapPhotoEntry[]} */var photos=[];/** @type {?number} */var min_lat=null;/** @type {?number} */var min_lng=null;/** @type {?number} */var max_lat=null;/** @type {?number} */var max_lng=null;album.photos.forEach(/** @param {Photo} element */function(element){if(element.latitude||element.longitude){photos.push({lat:element.latitude,lng:element.longitude,thumbnail:element.size_variants.thumb!==null?element.size_variants.thumb.url:"img/placeholder.png",thumbnail2x:element.size_variants.thumb2x!==null?element.size_variants.thumb2x.url:null,url:element.size_variants.small!==null?element.size_variants.small.url:element.url,url2x:element.size_variants.small2x!==null?element.size_variants.small2x.url:null,name:element.title,taken_at:element.taken_at,albumID:element.album_id,photoID:element.id});// Update min/max lat/lng +if(min_lat===null||min_lat>element.latitude){min_lat=element.latitude;}if(min_lng===null||min_lng>element.longitude){min_lng=element.longitude;}if(max_lat===null||max_lat0){// update map bounds +var dist_lat=max_lat-min_lat;var dist_lng=max_lng-min_lng;mapview.bounds=[[min_lat-0.1*dist_lat,min_lng-0.1*dist_lng],[max_lat+0.1*dist_lat,max_lng+0.1*dist_lng]];}// add track +if(album.track_url){mapview.trackLayer=new L.GPX(album.track_url,{async:true,marker_options:{startIconUrl:null,endIconUrl:null,shadowUrl:null}}).on("error",function(e){lychee.error(lycche.locale["ERROR_GPX"]+e.err);}).on("loaded",function(e){if(photos.length===0){// no photos, update map bound to center track +mapview.bounds=e.target.getBounds();updateZoom();}});mapview.trackLayer.addTo(mapview.map);}// Update Zoom and Position +updateZoom();};/** + * Calls backend, retrieves information about photos and displays them. + * + * This function is called recursively to retrieve data for sub-albums. + * Possible enhancement could be to only have a single ajax call. + * + * @param {?string} _albumID + * @param {boolean} [_includeSubAlbums=true] + */var getAlbumData=function getAlbumData(_albumID){var _includeSubAlbums=arguments.length>1&&arguments[1]!==undefined?arguments[1]:true;/** + * @param {PositionData} data + */var successHandler=function successHandler(data){addContentsToMap(data);mapview.title(_albumID,data.title);};if(_albumID!==""&&_albumID!==null){// _albumID has been specified +var params={albumID:_albumID,includeSubAlbums:_includeSubAlbums};api.post("Album::getPositionData",params,successHandler);}else{// AlbumID is empty -> fetch all photos of all albums +api.post("Albums::getPositionData",{},successHandler);}};// If sub-albums are not requested and album.json already has all data, +// we reuse it +if(lychee.map_include_subalbums===false&&album.json!==null&&album.json.photos!==null){addContentsToMap(album.json);}else{// Not all needed data has been preloaded - we need to load everything +getAlbumData(albumID,lychee.map_include_subalbums);}// Update Zoom and Position once more (for empty map) +updateZoom();};/** + * @returns {void} + */mapview.close=function(){// If map functionality is disabled -> do nothing +if(!lychee.map_display)return;var mapContainer=$("#lychee_map_container");lychee.animate(mapContainer,"fadeOut");// TODO: Reconsider the line below +// The line below is inconsistent to the corresponding code for +// the photo view (cp. `view.photo.hide()`). +// Here, we remove the `active` class immediately, in `view.photo.hide()` +// we remove that class after the animation has ended. +mapContainer.removeClass("active");// TODO: Fix the line below +// The map view can also be opened from a single photo and probably a +// users expect to go back to the photo if they close the photo. +// Currently, Lychee jumps back to the album of that photo. +header.setMode(mapview.albumID?"album":"albums");// Make album focusable +tabindex.makeFocusable(lychee.content);};/** + * @param {jQuery} elem + * @returns {void} + */mapview["goto"]=function(elem){// If map functionality is disabled -> do nothing +if(!lychee.map_display)return;var photoID=elem.attr("data-id");var albumID=elem.attr("data-album-id");if(albumID==="null")albumID="unsorted";lychee["goto"](albumID+"/"+photoID);};/** + * @description Select multiple albums or photos. + */ /** + * @param {jQuery.Event} e + * @returns {boolean} + */var isSelectKeyPressed=function isSelectKeyPressed(e){return e.metaKey||e.ctrlKey;};var multiselect={/** @type {string[]} */ids:[],albumsSelected:0,photosSelected:0,/** @type {?jQuery} */lastClicked:null};/** + * @typedef SelectionPosition + * + * @property {number} top + * @property {number} right + * @property {number} bottom + * @property {number} left + */ /** + * @type {?SelectionPosition} + */multiselect.position=null;multiselect.bind=function(){$("#lychee_view_content").on("mousedown",function(e){if(e.which===1)multiselect.show(e);});return true;};/** + * @returns {void} + */multiselect.unbind=function(){$("#lychee_view_content").off("mousedown");};/** + * @param {string} id + * @returns {{position: number, selected: boolean}} + */multiselect.isSelected=function(id){var pos=multiselect.ids.indexOf(id);return{selected:pos!==-1,position:pos};};/** + * @param {jQuery} object + * @param {string} id + */multiselect.toggleItem=function(object,id){if(album.isSmartID(id)||album.isSearchID(id))return;var selected=multiselect.isSelected(id).selected;if(selected===false)multiselect.addItem(object,id);else multiselect.removeItem(object,id);};/** + * @param {jQuery} object + * @param {string} id + */multiselect.addItem=function(object,id){if(album.isSmartID(id)||album.isSearchID(id))return;if(!lychee.rights.settings.can_edit&&albums.isShared(id))return;if(multiselect.isSelected(id).selected===true)return;var isAlbum=object.hasClass("album");if(isAlbum&&multiselect.photosSelected>0||!isAlbum&&multiselect.albumsSelected>0){loadingBar.show("error",lychee.locale["ERROR_EITHER_ALBUMS_OR_PHOTOS"]);return;}multiselect.ids.push(id);multiselect.select(object);if(isAlbum){multiselect.albumsSelected++;}else{multiselect.photosSelected++;}multiselect.lastClicked=object;};/** + * @param {jQuery} object + * @param {string} id + */multiselect.removeItem=function(object,id){var _multiselect$isSelect=multiselect.isSelected(id),selected=_multiselect$isSelect.selected,position=_multiselect$isSelect.position;if(selected===false)return;multiselect.ids.splice(position,1);multiselect.deselect(object);var isAlbum=object.hasClass("album");if(isAlbum){multiselect.albumsSelected--;}else{multiselect.photosSelected--;}multiselect.lastClicked=object;};/** + * @param {jQuery.Event} e + * @param {jQuery} albumObj + * + * @returns {void} + */multiselect.albumClick=function(e,albumObj){var id=albumObj.attr("data-id");if((isSelectKeyPressed(e)||e.shiftKey)&&album.isUploadable()){if(albumObj.hasClass("disabled"))return;if(isSelectKeyPressed(e)){multiselect.toggleItem(albumObj,id);}else{if(multiselect.albumsSelected>0){// Click with Shift. Select all elements between the current +// element and the last clicked-on one. +if(albumObj.prevAll(".album").toArray().includes(multiselect.lastClicked[0])){albumObj.prevUntil(multiselect.lastClicked,".album").each(function(){multiselect.addItem($(this),$(this).attr("data-id"));});}else if(albumObj.nextAll(".album").toArray().includes(multiselect.lastClicked[0])){albumObj.nextUntil(multiselect.lastClicked,".album").each(function(){multiselect.addItem($(this),$(this).attr("data-id"));});}}multiselect.addItem(albumObj,id);}}else{lychee["goto"](id);}};/** + * @param {jQuery.Event} e + * @param {jQuery} photoObj + * + * @returns {void} + */multiselect.photoClick=function(e,photoObj){var id=photoObj.attr("data-id");if((isSelectKeyPressed(e)||e.shiftKey)&&album.isUploadable()){if(photoObj.hasClass("disabled"))return;if(isSelectKeyPressed(e)){multiselect.toggleItem(photoObj,id);}else{if(multiselect.photosSelected>0){// Click with Shift. Select all elements between the current +// element and the last clicked-on one. +if(photoObj.prevAll(".photo").toArray().includes(multiselect.lastClicked[0])){photoObj.prevUntil(multiselect.lastClicked,".photo").each(function(){multiselect.addItem($(this),$(this).attr("data-id"));});}else if(photoObj.nextAll(".photo").toArray().includes(multiselect.lastClicked[0])){photoObj.nextUntil(multiselect.lastClicked,".photo").each(function(){multiselect.addItem($(this),$(this).attr("data-id"));});}}multiselect.addItem(photoObj,id);}}else{lychee["goto"](album.getID()+"/"+id);}};/** + * @param {jQuery.Event} e + * @param {jQuery} albumObj + * + * @returns {void} + */multiselect.albumContextMenu=function(e,albumObj){var id=albumObj.attr("data-id");var selected=multiselect.isSelected(id).selected;if(albumObj.hasClass("disabled"))return;if(selected!==false&&multiselect.ids.length>1){contextMenu.albumMulti(multiselect.ids,e);}else{contextMenu.album(id,e);}};/** + * @param {jQuery.Event} e + * @param {jQuery} photoObj + * + * @returns {void} + */multiselect.photoContextMenu=function(e,photoObj){var id=photoObj.attr("data-id");var selected=multiselect.isSelected(id).selected;if(photoObj.hasClass("disabled"))return;if(selected!==false&&multiselect.ids.length>1){contextMenu.photoMulti(multiselect.ids,e);}else if(visible.album()||visible.search()){contextMenu.photo(id,e);}else if(visible.photo()){// should not happen... but you never know... +contextMenu.photo(_photo3.getID(),e);}else{loadingBar.show("error",lychee.locale["ERROR_COULD_NOT_FIND"]);}};/** + * @returns {void} + */multiselect.clearSelection=function(){multiselect.deselect($(".photo.active, .album.active"));multiselect.ids=[];multiselect.albumsSelected=0;multiselect.photosSelected=0;multiselect.lastClicked=null;};/** + * @param {jQuery.Event} e + * @returns {boolean} + */multiselect.show=function(e){if(!album.isUploadable())return false;if(!visible.albums()&&!visible.album())return false;if($(".album:hover, .photo:hover").length!==0)return false;if(visible.search())return false;if(visible.multiselect())$("#multiselect").remove();_sidebar.setSelectable(false);if(!isSelectKeyPressed(e)&&!e.shiftKey){multiselect.clearSelection();}multiselect.position={top:e.pageY,right:$(document).width()-e.pageX,bottom:$(document).height()-e.pageY,left:e.pageX};$("body").append(build.multiselect(multiselect.position.top,multiselect.position.left));$(document).on("mousemove",multiselect.resize).on("mouseup",function(_e){if(_e.which===1){multiselect.getSelection(_e);}});};/** + * @param {jQuery.Event} e + * @returns {boolean} + */multiselect.resize=function(e){if(multiselect.position===null)return false;// Default CSS +var newCSS={top:null,bottom:null,height:null,left:null,right:null,width:null};if(e.pageY>=multiselect.position.top){newCSS.top=multiselect.position.top;newCSS.bottom="inherit";newCSS.height=Math.min(e.pageY,$(document).height()-3)-multiselect.position.top;}else{newCSS.top="inherit";newCSS.bottom=multiselect.position.bottom;newCSS.height=multiselect.position.top-Math.max(e.pageY,2);}if(e.pageX>=multiselect.position.left){newCSS.right="inherit";newCSS.left=multiselect.position.left;newCSS.width=Math.min(e.pageX,$(document).width()-3)-multiselect.position.left;}else{newCSS.right=multiselect.position.right;newCSS.left="inherit";newCSS.width=multiselect.position.left-Math.max(e.pageX,2);}// Updated all CSS properties at once +$("#multiselect").css(newCSS);};/** + * @returns {void} + */multiselect.stopResize=function(){if(multiselect.position!==null)$(document).off("mousemove mouseup");};/** + * @returns {null|{top: number, left: number, width: number, height: number}} + */multiselect.getSize=function(){if(!visible.multiselect())return null;var $elem=$("#multiselect");var offset=$elem.offset();return{top:offset.top,left:offset.left,width:parseFloat($elem.css("width")),height:parseFloat($elem.css("height"))};};/** + * TODO: This method is called **`get...`** but it doesn't get anything. + * + * @param {jQuery.Event} e + * @returns {void} + */multiselect.getSelection=function(e){var size=multiselect.getSize();if(visible.contextMenu())return;if(!visible.multiselect())return;$(".photo, .album").each(function(){// We select if there's even a slightest overlap. Overlap between +// an object and the selection occurs if the left edge of the +// object is to the left of the right edge of the selection *and* +// the right edge of the object is to the right of the left edge of +// the selection; analogous for top/bottom. +if($(this).offset().leftsize.left&&$(this).offset().topsize.top){var id=$(this).attr("data-id");if(isSelectKeyPressed(e)){multiselect.toggleItem($(this),id);}else{multiselect.addItem($(this),id);}}});multiselect.hide();};/** + * @param {jQuery} elem + * @returns {void} + */multiselect.select=function(elem){elem.addClass("selected");elem.addClass("active");};/** + * @param {jQuery} elem + * @returns {void} + */multiselect.deselect=function(elem){elem.removeClass("selected");elem.removeClass("active");};/** + * Note, identical to {@link multiselect.close} + * @returns {void} + */multiselect.hide=function(){_sidebar.setSelectable(true);multiselect.stopResize();multiselect.position=null;lychee.animate($("#multiselect"),"fadeOut");setTimeout(function(){return $("#multiselect").remove();},300);};/** + * Note, identical to {@link multiselect.hide} + * @returns {void} + */multiselect.close=function(){_sidebar.setSelectable(true);multiselect.stopResize();multiselect.position=null;lychee.animate($("#multiselect"),"fadeOut");setTimeout(function(){return $("#multiselect").remove();},300);};/** + * @returns {void} + */multiselect.selectAll=function(){if(!album.isUploadable())return;if(visible.search())return;if(!visible.albums()&&!visible.album)return;if(visible.multiselect())$("#multiselect").remove();_sidebar.setSelectable(false);multiselect.clearSelection();$(".photo").each(function(){multiselect.addItem($(this),$(this).attr("data-id"));});if(multiselect.photosSelected===0){// There are no pictures. Try albums then. +$(".album").each(function(){multiselect.addItem($(this),$(this).attr("data-id"));});}};var notifications={/** @type {?EMailData} */json:null};/** + * @param {EMailData} params + * @returns {void} + */notifications.update=function(params){if(params.email&¶ms.email.length>1){var regexp=/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;if(!regexp.test(String(params.email).toLowerCase())){loadingBar.show("error",lychee.locale["ERROR_INVALID_EMAIL"]);return;}}api.post("User::setEmail",params,function(){loadingBar.show("success",lychee.locale["EMAIL_SUCCESS"]);});};notifications.load=function(){api.post("User::getAuthenticatedUser",{},/** @param {EMailData} data */function(data){notifications.json=data.email;view.notifications.init();});};/** + * @description Controls the access to password-protected albums and photos. + */var password={};/** + * @callback UnlockSuccessCB + * @returns {void} + */ /** + * Shows the "album unlock"-dialog, tries to unlock the album and calls + * the provided callback in case of success. + * + * @param {string} albumID - the ID of the album which shall be unlocked + * @param {UnlockSuccessCB} callback - called in case of success + */password.getDialog=function(albumID,callback){/** @param {{password: string}} data */var action=function action(data){var params={albumID:albumID,password:data.password};api.post("Album::unlock",params,function(){basicModal.close(false,callback);},null,function(jqXHR,params2,lycheeException){if((jqXHR.status===401||jqXHR.status===403)&&lycheeException.message.includes("Password is invalid")){basicModal.focusError("password");return true;}basicModal.close();return false;});};var cancel=function cancel(){basicModal.close(false,function(){if(!visible.albums()&&!visible.album())lychee["goto"]();});};var enterPasswordDialogBody="\n\t\t

\n\t\t
\n\t\t \t
\n\t\t
";/** + * @param {ModalDialogFormElements} formElements + * @param {HTMLDivElement} dialog + * @returns {void} + */var initEnterPasswordDialog=function initEnterPasswordDialog(formElements,dialog){dialog.querySelector("p").textContent=lychee.locale["ALBUM_PASSWORD_REQUIRED"];formElements.password.placeholder=lychee.locale["PASSWORD"];};basicModal.show({body:enterPasswordDialogBody,readyCB:initEnterPasswordDialog,buttons:{action:{title:lychee.locale["ENTER"],fn:action},cancel:{title:lychee.locale["CANCEL"],fn:cancel}}});};/** + * @description Takes care of every action a photo can handle and execute. + */var _photo3={/** @type {?Photo} */json:null,cache:null,/** @type {?boolean} indicates whether the browser supports prefetching of images; `null` if support hasn't been determined yet */supportsPrefetch:null,/** @type {?LivePhotosKit.Player} */livePhotosObject:null};/** + * @returns {?string} - the photo ID + */_photo3.getID=function(){var id=_photo3.json?_photo3.json.id:$(".photo:hover, .photo.active").attr("data-id");id=typeof id==="string"&&/^[-_0-9a-zA-Z]{24}$/.test(id)?id:null;return id;};/** + * + * @param {string} photoID + * @param {string} albumID + * @param {boolean} autoplay - automatically start playback, if the photo is a video or live photo + * + * @returns {void} + */_photo3.load=function(photoID,albumID,autoplay){/** + * @param {Photo} data + * @returns {void} + */var successHandler=function successHandler(data){_photo3.json=data;// TODO: `photo.json.original_album_id` is set only, but never read; do we need it? +_photo3.json.original_album_id=_photo3.json.album_id;// TODO: Why do we overwrite the true album ID of a photo, by the externally provided one? I guess we need it, because the album which the user came from might also be a smart album or a tag album. However, in this case I would prefer to leave the `album_id untouched (don't rename it to `original_album_id`) and call this one `effective_album_id` instead. +_photo3.json.album_id=albumID;view.photo.show();view.photo.init(autoplay);if(!lychee.hide_content_during_imgview){setTimeout(function(){lychee.content.show();tabindex.makeUnfocusable(lychee.content);},300);}};api.post("Photo::get",{photoID:photoID},successHandler);};/** + * @returns {boolean} + */_photo3.hasExif=function(){return!!_photo3.json.make||!!_photo3.json.model||!!_photo3.json.shutter||!!_photo3.json.aperture||!!_photo3.json.focal||!!_photo3.json.iso;};/** + * @returns {boolean} + */_photo3.hasTakestamp=function(){return!!_photo3.json.taken_at;};/** + * @returns {boolean} + */_photo3.hasDesc=function(){return!!_photo3.json.description;};/** + * @returns {boolean} + */_photo3.isLivePhoto=function(){return!!_photo3.json&&// In case it's called, but not initialized +!!_photo3.json.live_photo_url;};/** + * @returns {boolean} + */_photo3.isLivePhotoInitialized=function(){return!!_photo3.livePhotosObject;};/** + * @returns {boolean} + */_photo3.isLivePhotoPlaying=function(){return _photo3.isLivePhotoInitialized()&&_photo3.livePhotosObject.isPlaying;};/** + * @returns {void} + */_photo3.cycle_display_overlay=function(){var oldType=build.check_overlay_type(_photo3.json,lychee.image_overlay_type);var newType=build.check_overlay_type(_photo3.json,oldType,true);if(oldType!==newType){lychee.image_overlay_type=newType;$("#image_overlay").remove();var newOverlay=build.overlay_image(_photo3.json);if(newOverlay!=="")lychee.imageview.append(newOverlay);}};/** + * Preloads the next and previous photos for better response time + * + * @param {string} photoID + * @returns {void} + */_photo3.preloadNextPrev=function(photoID){if(!album.json||!album.json.photos)return;var photo=album.getByID(photoID);if(!photo)return;var imgs=$("img#image");// TODO: consider replacing the test for "@2x." by a simple comparison to photo.size_variants.medium2x.url. +var isUsing2xCurrently=imgs.length>0&&imgs[0].currentSrc!==null&&imgs[0].currentSrc.includes("@2x.");$("head [data-prefetch]").remove();/** + * @param {string} preloadID + * @returns {void} + */var preload=function preload(preloadID){var preloadPhoto=album.getByID(preloadID);var href="";if(preloadPhoto.size_variants.medium!=null){href=preloadPhoto.size_variants.medium.url;if(preloadPhoto.size_variants.medium2x!=null&&isUsing2xCurrently){// If the currently displayed image uses the 2x variant, +// chances are that so will the next one. +href=preloadPhoto.size_variants.medium2x.url;}}else if(preloadPhoto.type&&preloadPhoto.type.indexOf("video")===-1){// Preload the original size, but only if it's not a video +href=preloadPhoto.size_variants.original.url;}if(href!==""){if(photo.supportsPrefetch===null){/** + * Copied from https://www.smashingmagazine.com/2016/02/preload-what-is-it-good-for/ + * + * TODO: This method should not be defined dynamically, but defined and executed upon initialization once + * + * @param {DOMTokenList} tokenList + * @param {string} token + * @returns {boolean} + */var DOMTokenListSupports=function DOMTokenListSupports(tokenList,token){try{if(!tokenList||!tokenList.supports){return false;}return tokenList.supports(token);}catch(e){if(e instanceof TypeError){console.log("The DOMTokenList doesn't have a supported tokens list");}else{console.error("That shouldn't have happened");}return false;}};photo.supportsPrefetch=DOMTokenListSupports(document.createElement("link").relList,"prefetch");}if(photo.supportsPrefetch){$("head").append(lychee.html(_templateObject49||(_templateObject49=_taggedTemplateLiteral([""])),href));}else{// According to https://caniuse.com/#feat=link-rel-prefetch, +// as of mid-2019 it's mainly Safari (both on desktop and mobile) +new Image().src=href;}}};if(photo.next_photo_id){preload(photo.next_photo_id);}if(photo.previous_photo_id){preload(photo.previous_photo_id);}};/** + * @param {boolean} animate + * @returns {void} + */_photo3.previous=function(animate){var curPhoto=_photo3.getID()!==null&&album.json?album.getByID(_photo3.getID()):null;if(!curPhoto||!curPhoto.previous_photo_id)return;var delay=animate?200:0;if(animate){$("#imageview #image").css({WebkitTransform:"translateX(100%)",MozTransform:"translateX(100%)",transform:"translateX(100%)",opacity:0});}setTimeout(function(){_photo3.livePhotosObject=null;lychee["goto"](album.getID()+"/"+curPhoto.previous_photo_id,false);},delay);};/** + * @param {boolean} animate + * @returns {void} + */_photo3.next=function(animate){var curPhoto=_photo3.getID()!==null&&album.json?album.getByID(_photo3.getID()):null;if(!curPhoto||!curPhoto.next_photo_id)return;var delay=animate?200:0;if(animate===true){$("#imageview #image").css({WebkitTransform:"translateX(-100%)",MozTransform:"translateX(-100%)",transform:"translateX(-100%)",opacity:0});}setTimeout(function(){_photo3.livePhotosObject=null;lychee["goto"](album.getID()+"/"+curPhoto.next_photo_id,false);},delay);};/** + * @param {string[]} photoIDs + * @returns {boolean} + */_photo3["delete"]=function(photoIDs){var deletePhotos=function deletePhotos(){var nextPhotoID=null;var previousPhotoID=null;basicModal.close();photoIDs.forEach(function(id,index){// Change reference for the next and previous photo +var curPhoto=album.getByID(id);if(curPhoto.next_photo_id!==null||curPhoto.previous_photo_id!==null){nextPhotoID=curPhoto.next_photo_id;previousPhotoID=curPhoto.previous_photo_id;if(previousPhotoID!==null){album.getByID(previousPhotoID).next_photo_id=nextPhotoID;}if(nextPhotoID!==null){album.getByID(nextPhotoID).previous_photo_id=previousPhotoID;}}album.deleteByID(id);view.album.content["delete"](id,index===photoIDs.length-1);});albums.refresh();// Go to next photo if there is a next photo and +// next photo is not the current one. Also try the previous one. +// Show album otherwise. +if(visible.photo()){if(nextPhotoID!==null&&nextPhotoID!==_photo3.getID()){lychee["goto"](album.getID()+"/"+nextPhotoID,false);}else if(previousPhotoID!==null&&previousPhotoID!==_photo3.getID()){lychee["goto"](album.getID()+"/"+previousPhotoID,false);}else{lychee["goto"](album.getID());}}else if(!visible.albums()){lychee["goto"](album.getID());}api.post("Photo::delete",{photoIDs:photoIDs});};/** + * @param {ModalDialogFormElements} formElements + * @param {HTMLDivElement} dialog + * @returns {void} + */var initDeletePhotoDialog=function initDeletePhotoDialog(formElements,dialog){if(photoIDs.length===1){var photoTitle=(visible.photo()?_photo3.json.title:album.getByID(photoIDs[0]).title)||lychee.locale["UNTITLED"];dialog.querySelector("p").textContent=sprintf(lychee.locale["PHOTO_DELETE_CONFIRMATION"],photoTitle);}else{dialog.querySelector("p").textContent=sprintf(lychee.locale["PHOTO_DELETE_ALL"],photoIDs.length);}};basicModal.show({body:"

",readyCB:initDeletePhotoDialog,buttons:{action:{title:lychee.locale["PHOTO_DELETE"],fn:deletePhotos,classList:["red"]},cancel:{title:lychee.locale["PHOTO_KEEP"],fn:basicModal.close}}});};/** + * + * @param {string[]} photoIDs + * @returns {void} + */_photo3.setTitle=function(photoIDs){/** + * @param {{title: string}} data + * @returns {void} + */var action=function action(data){if(!data.title.trim()){basicModal.focusError("title");return;}basicModal.close();var newTitle=data.title?data.title:null;if(visible.photo()){_photo3.json.title=newTitle;view.photo.title();}photoIDs.forEach(function(id){// TODO: The line below looks suspicious: It is inconsistent to the code some lines above. +album.getByID(id).title=newTitle;view.album.content.title(id);});api.post("Photo::setTitle",{photoIDs:photoIDs,title:newTitle});};var setPhotoTitleDialogBody="\n\t\t

\n\t\t
\n\t\t\t
\n\t\t
";/** + * @param {ModalDialogFormElements} formElements + * @param {HTMLDivElement} dialog + * @returns {void} + */var initSetPhotoTitleDialog=function initSetPhotoTitleDialog(formElements,dialog){var oldTitle=photoIDs.length===1?_photo3.json?_photo3.json.title:album.getByID(photoIDs[0]).title:"";dialog.querySelector("p").textContent=photoIDs.length===1?lychee.locale["PHOTO_NEW_TITLE"]:sprintf(lychee.locale["PHOTOS_NEW_TITLE"],photoIDs.length);formElements.title.placeholder="Title";formElements.title.value=oldTitle;};basicModal.show({body:setPhotoTitleDialogBody,readyCB:initSetPhotoTitleDialog,buttons:{action:{title:lychee.locale["PHOTO_SET_TITLE"],fn:action},cancel:{title:lychee.locale["CANCEL"],fn:basicModal.close}}});};/** + * + * @param {string[]} photoIDs IDs of photos to be copied + * @param {?string} albumID ID of destination album; `null` means root album + * @returns {void} + */_photo3.copyTo=function(photoIDs,albumID){api.post("Photo::duplicate",{photoIDs:photoIDs,albumID:albumID},function(){return album.reload();});};/** + * @param {string[]} photoIDs + * @param {string} albumID + * @returns {void} + */_photo3.setAlbum=function(photoIDs,albumID){var nextPhotoID=null;var previousPhotoID=null;photoIDs.forEach(function(id,index){// Change reference for the next and previous photo +var curPhoto=album.getByID(id);if(curPhoto.next_photo_id!==null||curPhoto.previous_photo_id!==null){nextPhotoID=curPhoto.next_photo_id;previousPhotoID=curPhoto.previous_photo_id;if(previousPhotoID!==null){album.getByID(previousPhotoID).next_photo_id=nextPhotoID;}if(nextPhotoID!==null){album.getByID(nextPhotoID).previous_photo_id=previousPhotoID;}}album.deleteByID(id);view.album.content["delete"](id,index===photoIDs.length-1);});albums.refresh();// Go to next photo if there is a next photo and +// next photo is not the current one. Also try the previous one. +// Show album otherwise. +if(visible.photo()){if(nextPhotoID!==null&&nextPhotoID!==_photo3.getID()){lychee["goto"](album.getID()+"/"+nextPhotoID);}else if(previousPhotoID!==null&&previousPhotoID!==_photo3.getID()){lychee["goto"](album.getID()+"/"+previousPhotoID);}else{lychee["goto"](album.getID());}}api.post("Photo::setAlbum",{photoIDs:photoIDs,albumID:albumID},function(){// We only really need to do anything here if the destination +// is a (possibly nested) subalbum of the current album; but +// since we have no way of figuring it out (albums.json is +// null), we need to reload. +if(visible.album()){album.reload();}});};/** + * Toggles the star-property of the currently visible photo. + * + * @returns {void} + */_photo3.toggleStar=function(){_photo3.json.is_starred=!_photo3.json.is_starred;view.photo.star();album.getByID(_photo3.json.id).is_starred=_photo3.json.is_starred;view.album.content.star(_photo3.json.id);albums.refresh();api.post("Photo::setStar",{photoIDs:[_photo3.json.id],is_starred:_photo3.json.is_starred});};/** + * Sets the star-property of the given photos. + * + * @param {string[]} photoIDs + * @param {boolean} isStarred + * @returns {void} + */_photo3.setStar=function(photoIDs,isStarred){photoIDs.forEach(function(id){album.getByID(id).is_starred=isStarred;view.album.content.star(id);});albums.refresh();api.post("Photo::setStar",{photoIDs:photoIDs,is_starred:isStarred});};/** + * Edits the protection policy of a photo. + * + * This method is a misnomer, it does not only set the policy, it also creates + * and handles the edit dialog + * + * @param {string} photoID + * @returns {void} + */_photo3.setProtectionPolicy=function(photoID){/** + * @param {{is_public: boolean}} data + */var action=function action(data){if(data.is_public!==_photo3.json.is_public){if(visible.photo()){_photo3.json.is_public=data.is_public;view.photo["public"]();}album.getByID(photoID).is_public=data.is_public;view.album.content["public"](photoID);albums.refresh();api.post("Photo::setPublic",{photoID:photoID,is_public:data.is_public});}basicModal.close();};var setPhotoProtectionPolicyBody="\n\t\t

\n\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t

\n\t\t\t
\n\t\t\t

\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t

\n\t\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t

\n\t\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t

\n\t\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t

\n\t\t\t
\n\t\t
";/** + * @typedef PhotoProtectionPolicyDialogFormElements + * @property {HTMLInputElement} is_public + * @property {HTMLInputElement} grants_full_photo_access + * @property {HTMLInputElement} is_link_required + * @property {HTMLInputElement} grants_download + * @property {HTMLInputElement} is_password_required + */ /** + * @param {PhotoProtectionPolicyDialogFormElements} formElements + * @param {HTMLDivElement} dialog + * @returns {void} + */var initPhotoProtectionPolicyDialog=function initPhotoProtectionPolicyDialog(formElements,dialog){formElements.is_public.previousElementSibling.textContent=lychee.locale["PHOTO_PUBLIC"];formElements.is_public.nextElementSibling.textContent=lychee.locale["PHOTO_PUBLIC_EXPL"];formElements.grants_full_photo_access.previousElementSibling.textContent=lychee.locale["PHOTO_FULL"];formElements.grants_full_photo_access.nextElementSibling.textContent=lychee.locale["PHOTO_FULL_EXPL"];formElements.is_link_required.previousElementSibling.textContent=lychee.locale["PHOTO_HIDDEN"];formElements.is_link_required.nextElementSibling.textContent=lychee.locale["PHOTO_HIDDEN_EXPL"];formElements.grants_download.previousElementSibling.textContent=lychee.locale["PHOTO_DOWNLOADABLE"];formElements.grants_download.nextElementSibling.textContent=lychee.locale["PHOTO_DOWNLOADABLE_EXPL"];formElements.is_password_required.previousElementSibling.textContent=lychee.locale["PHOTO_PASSWORD_PROT"];formElements.is_password_required.nextElementSibling.textContent=lychee.locale["PHOTO_PASSWORD_PROT_EXPL"];if(_photo3.json.album_id===null){// No album +dialog.querySelector("p#ppp_dialog_no_edit_expl").remove();dialog.querySelector("p#ppp_dialog_global_expl").textContent=lychee.locale["PHOTO_EDIT_GLOBAL_SHARING_TEXT"];// Initialize values of detailed settings according to global +// configuration. +formElements.is_public.checked=_photo3.json.is_public;formElements.grants_full_photo_access.checked=lychee.grants_full_photo_access;formElements.is_link_required.checked=lychee.public_photos_hidden;formElements.grants_download.checked=lychee.grants_download;formElements.is_password_required.checked=false;}else if(album.json&&album.json.policy.is_public===false){// Private album +dialog.querySelector("p#ppp_dialog_no_edit_expl").remove();dialog.querySelector("p#ppp_dialog_global_expl").textContent=lychee.locale["PHOTO_EDIT_GLOBAL_SHARING_TEXT"];// Initialize values of detailed settings according to global +// configuration. +formElements.is_public.checked=_photo3.json.is_public;formElements.grants_full_photo_access.checked=lychee.grants_full_photo_access;formElements.is_link_required.checked=lychee.public_photos_hidden;formElements.grants_download.checked=lychee.grants_download;formElements.is_password_required.checked=false;}else{// Public album. +dialog.querySelector("p#ppp_dialog_no_edit_expl").textContent=lychee.locale["PHOTO_NO_EDIT_SHARING_TEXT"];dialog.querySelector("p#ppp_dialog_global_expl").remove();// Initialize values of detailed settings according to album +// settings and hide action button as we can't actually change +// anything. +formElements.is_public.disabled=true;formElements.is_public.checked=album.json.policy.is_public;formElements.is_public.parentElement.classList.add("disabled");formElements.grants_full_photo_access.checked=album.json.policy.grants_full_photo_access;// Photos in public albums are never hidden as such. It's the +// album that's hidden. Or is that distinction irrelevant to end +// users? +formElements.is_link_required.checked=album.json.policy.is_link_required;formElements.grants_download.checked=album.json.policy.grants_download;formElements.is_password_required.checked=album.json.policy.is_password_required;basicModal.hideActionButton();}};basicModal.show({body:setPhotoProtectionPolicyBody,readyCB:initPhotoProtectionPolicyDialog,buttons:{action:{title:lychee.locale["SAVE"],fn:action},cancel:{title:lychee.locale["CANCEL"],fn:basicModal.close}}});};/** + * Edits the description of a photo. + * + * This method is a misnomer, it does not only set the description, it also creates and handles the edit dialog + * + * @param {string} photoID + * @returns {void} + */_photo3.setDescription=function(photoID){/** + * @param {{description: string}} data + */var action=function action(data){basicModal.close();var description=data.description?data.description:null;if(visible.photo()){_photo3.json.description=description;view.photo.description();}api.post("Photo::setDescription",{photoID:photoID,description:description});};var setPhotoDescriptionDialogBody="\n\t\t

\n\t\t
\n\t\t\t
\n\t\t
";/** + * @param {ModalDialogFormElements} formElements + * @param {HTMLDivElement} dialog + * @returns {void} + */var initSetPhotoDescriptionDialog=function initSetPhotoDescriptionDialog(formElements,dialog){dialog.querySelector("p#ppp_dialog_description_expl").textContent=lychee.locale["PHOTO_NEW_DESCRIPTION"];formElements.description.placeholder=lychee.locale["PHOTO_DESCRIPTION"];formElements.description.value=_photo3.json.description?_photo3.json.description:"";};basicModal.show({body:setPhotoDescriptionDialogBody,readyCB:initSetPhotoDescriptionDialog,buttons:{action:{title:lychee.locale["PHOTO_SET_DESCRIPTION"],fn:action},cancel:{title:lychee.locale["CANCEL"],fn:basicModal.close}}});};/** + * Edits the upload date of a photo. + * + * This method is a misnomer, it does not only set the description, it also creates and handles the edit dialog + * + * @param {string} photoID + * @returns {void} + */_photo3.setCreatedAt=function(photoID){/** + * @param {{date: string}} data + */var action=function action(data){basicModal.close();var created_at=data.created_at?data.created_at.concat(":",data.tz):null;if(visible.photo()){_photo3.json.created_at=created_at;view.photo.uploaded();}api.post("Photo::setUploadDate",{photoID:photoID,date:created_at});};var setPhotoCreatedAtDialogBody="\n\t\t

\n\t\t
\n\t\t\t
\n\t\t\t\n\t\t\t
\n\t\t
";/** + * @param {ModalDialogFormElements} formElements + * @param {HTMLDivElement} dialog + * @returns {void} + */var initSetPhotoCreatedAtDialog=function initSetPhotoCreatedAtDialog(formElements,dialog){dialog.querySelector("p#ppp_dialog_uploaddate_expl").textContent=lychee.locale["PHOTO_NEW_CREATED_AT"];formElements.created_at.value=_photo3.json.created_at?_photo3.json.created_at.slice(0,16):"";formElements.tz.value=_photo3.json.created_at?_photo3.json.created_at.slice(17):"";};basicModal.show({body:setPhotoCreatedAtDialogBody,readyCB:initSetPhotoCreatedAtDialog,buttons:{action:{title:lychee.locale["PHOTO_SET_CREATED_AT"],fn:action},cancel:{title:lychee.locale["CANCEL"],fn:basicModal.close}}});};/** + * @param {string[]} photoIDs + * @returns {void} + */_photo3.editTags=function(photoIDs){/** @type {string[]} */var oldTags=[];// Get tags +if(visible.photo())oldTags=_photo3.json.tags.sort();else if(visible.album()&&photoIDs.length===1)oldTags=album.getByID(photoIDs[0]).tags.sort();else if(visible.search()&&photoIDs.length===1)oldTags=album.getByID(photoIDs[0]).tags.sort();else if(visible.album()&&photoIDs.length>1){oldTags=album.getByID(photoIDs[0]).tags.sort();var areIdentical=photoIDs.every(function(id){var oldTags2=album.getByID(id).tags.sort();if(oldTags.length!==oldTags2.length)return false;for(var tagIdx=0;tagIdx!==oldTags.length;tagIdx++){if(oldTags[tagIdx]!==oldTags2[tagIdx])return false;}return true;});if(!areIdentical)oldTags=[];}/** + * @param {{tags: string, override: boolean}} data + * @returns {void} + */var action=function action(data){basicModal.close();var newTags=data.tags.split(",").map(function(tag){return tag.trim();}).filter(function(tag){return tag!==""&&tag.indexOf(",")===-1;}).sort();_photo3.setTags(photoIDs,newTags,data.override);};var setTagDialogBody="\n\t\t

\n\t\t
\n\t\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t

\n\t\t\t
\n\t\t
";/** + * @param {ModalDialogFormElements} formElements + * @param {HTMLDivElement} dialog + * @returns {void} + */var initSetTagAlbumDialog=function initSetTagAlbumDialog(formElements,dialog){dialog.querySelector("p").textContent=photoIDs.length===1?lychee.locale["PHOTO_NEW_TAGS"]:sprintf(lychee.locale["PHOTOS_NEW_TAGS"],photoIDs.length);formElements.tags.placeholder="Tags";formElements.tags.value=oldTags.join(", ");formElements.override.previousElementSibling.textContent=lychee.locale["OVERRIDE"];formElements.override.nextElementSibling.textContent=lychee.locale["TAGS_OVERRIDE_INFO"];};basicModal.show({body:setTagDialogBody,readyCB:initSetTagAlbumDialog,buttons:{action:{title:lychee.locale["PHOTO_SET_TAGS"],fn:action},cancel:{title:lychee.locale["CANCEL"],fn:basicModal.close}}});};/** + * @param {string[]} photoIDs + * @param {string[]} tags + * @param {boolean} shall_override + * @returns {void} + */_photo3.setTags=function(photoIDs,tags,shall_override){if(visible.photo()){_photo3.json.tags=shall_override?tags:_photo3.json.tags.concat(tags.filter(function(t){return!_photo3.json.tags.includes(t);}));view.photo.tags();}photoIDs.forEach(function(id){album.getByID(id).tags=tags;});api.post("Photo::setTags",{photoIDs:photoIDs,tags:tags,shall_override:shall_override},function(){// If we have any tag albums, force a refresh. +if(albums.json&&albums.json.tag_albums.length!==0){albums.refresh();}});};/** + * Deletes the tag at the given index from the photo. + * + * @param {string} photoID + * @param {number} index + */_photo3.deleteTag=function(photoID,index){_photo3.json.tags.splice(index,1);_photo3.setTags([photoID],_photo3.json.tags,true);};/** + * @param {string} photoID + * @param {string} service - one out of `"twitter"`, `"facebook"`, `"mail"` or `"dropbox"` + * @returns {void} + */_photo3.share=function(photoID,service){if(!lychee.share_button_visible){return;}var url=_photo3.getViewLink(photoID);switch(service){case"twitter":window.open("https://twitter.com/share?url=".concat(encodeURI(url)));break;case"facebook":window.open("https://www.facebook.com/sharer.php?u=".concat(encodeURI(url),"&t=").concat(encodeURI(_photo3.json.title)));break;case"mail":location.href="mailto:?subject=".concat(encodeURI(_photo3.json.title),"&body=").concat(encodeURI(url));break;case"dropbox":lychee.loadDropbox(function(){var filename=_photo3.json.title+"."+_photo3.getDirectLink().split(".").pop();Dropbox.save(_photo3.getDirectLink(),filename);});break;}};/** + * @param {string} photoID + * @returns {void} + */_photo3.setLicense=function(photoID){/** + * @param {{license: string}} data + */var action=function action(data){basicModal.close();api.post("Photo::setLicense",{photoID:photoID,license:data.license},function(){// update the photo JSON and reload the license in the sidebar +_photo3.json.license=data.license;view.photo.license();});};var setPhotoLicenseDialogBody="\n\t\t
\n\t\t\t\n\t\t\t
\n\t\t\t

\n\t\t
";/** + * @param {ModalDialogFormElements} formElements + * @param {HTMLDivElement} dialog + * @returns {void} + */var initSetPhotoLicenseDialog=function initSetPhotoLicenseDialog(formElements,dialog){dialog.querySelector("label").textContent=lychee.locale["PHOTO_LICENSE"];formElements.license.item(0).textContent=lychee.locale["PHOTO_LICENSE_NONE"];formElements.license.item(1).textContent=lychee.locale["PHOTO_RESERVED"];formElements.license.value=_photo3.json.license===""?"none":_photo3.json.license;dialog.querySelector("p a").textContent=lychee.locale["PHOTO_LICENSE_HELP"];};basicModal.show({body:setPhotoLicenseDialogBody,readyCB:initSetPhotoLicenseDialog,buttons:{action:{title:lychee.locale["PHOTO_SET_LICENSE"],fn:action},cancel:{title:lychee.locale["CANCEL"],fn:basicModal.close}}});};/** + * @param {string[]} photoIDs + * @param {?string} [kind=null] - the type of size variant; one out of + * `"ORIGINAL"`, `"MEDIUM2X"`, `"MEDIUM"`, + * `"SMALL2X"`, `"SMALL"`, `"THUMB2X"` or + * `"THUMB"`, + * @returns {void} + */_photo3.getArchive=function(photoIDs){var kind=arguments.length>1&&arguments[1]!==undefined?arguments[1]:null;if(photoIDs.length!==1||kind!==null){location.href="api/Photo::getArchive?photoIDs="+photoIDs.join()+"&kind="+kind;return;}// For a single photo without a specified kind, allow to pick the kind +// via a dialog box and re-call this method later on. +var myPhoto=_photo3.json&&_photo3.json.id===photoIDs[0]?_photo3.json:album.getByID(photoIDs[0]);var kind2VariantAndLocalizedLabel={ORIGINAL:["original",lychee.locale["PHOTO_ORIGINAL"]],MEDIUM2X:["medium2x",lychee.locale["PHOTO_MEDIUM_HIDPI"]],MEDIUM:["medium",lychee.locale["PHOTO_MEDIUM"]],SMALL2X:["small2x",lychee.locale["PHOTO_SMALL_HIDPI"]],SMALL:["small",lychee.locale["PHOTO_SMALL"]],THUMB2X:["thumb2x",lychee.locale["PHOTO_THUMB_HIDPI"]],THUMB:["thumb",lychee.locale["PHOTO_THUMB"]]};/** + * @param {string} kind - the kind this button is for, used to construct the ID + * @returns {string} - HTML + */var buildButton=function buildButton(kind){return"\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t");};var getPhotoArchiveDialogBody=Object.entries(kind2VariantAndLocalizedLabel).reduce(function(html,_ref5){var _ref6=_slicedToArray(_ref5,1),kind=_ref6[0];return html+buildButton(kind);},"")+buildButton("LIVEPHOTOVIDEO");/** @param {TouchEvent|MouseEvent} ev */var onClickOrTouch=function onClickOrTouch(ev){if(ev.currentTarget instanceof HTMLAnchorElement){basicModal.close();_photo3.getArchive(photoIDs,ev.currentTarget.dataset.photoKind);ev.stopPropagation();}};/** + * @param {ModalDialogFormElements} formElements + * @param {HTMLDivElement} dialog + */var initGetPhotoArchiveDialog=function initGetPhotoArchiveDialog(formElements,dialog){Object.entries(kind2VariantAndLocalizedLabel).forEach(function(_ref7){var _ref8=_slicedToArray(_ref7,2),kind=_ref8[0],_ref8$=_slicedToArray(_ref8[1],2),variant=_ref8$[0],lLabel=_ref8$[1];/** @type {HTMLAnchorElement} */var button=dialog.querySelector('a[data-photo-kind="'+kind+'"]');/** @type {?SizeVariant} */var sv=myPhoto.size_variants[variant];if(sv){button.title=lychee.locale["DOWNLOAD"];lychee.addClickOrTouchListener(button,onClickOrTouch);button.lastElementChild.textContent=lLabel+" ("+sv.width+"×"+sv.height+", "+lychee.locale.printFilesizeLocalized(sv.filesize)+")";}else{button.remove();}});/** @type {HTMLAnchorElement} */var liveButton=dialog.querySelector('a[data-photo-kind="LIVEPHOTOVIDEO"]');if(myPhoto.live_photo_url!==null){liveButton.title=lychee.locale["DOWNLOAD"];lychee.addClickOrTouchListener(liveButton,onClickOrTouch);liveButton.lastElementChild.textContent=lychee.locale["PHOTO_LIVE_VIDEO"];}else{liveButton.remove();}};basicModal.show({body:getPhotoArchiveDialogBody,readyCB:initGetPhotoArchiveDialog,classList:["downloads"],buttons:{cancel:{title:lychee.locale["CLOSE"],fn:basicModal.close}}});};/** + * Shows a dialog to share the view URL via a QR code. + * + * @param {string} photoID + * @returns {void} + */_photo3.qrCode=function(photoID){/** @type {?Photo} */var myPhoto=_photo3.json&&_photo3.json.id===photoID?_photo3.json:album.getByID(photoID);if(myPhoto==null){lychee.error(sprintf(lychee.locale["ERROR_PHOTO_NOT_FOUND"],photoID));return;}// We need this indirection based on a resize observer, because the ready +// callback of the dialog is invoked _before_ the dialog is made visible +// in order to allow the ready callback to make initializations of +// form elements without causing flicker. +// However, for invisible elements `.clientWidth` returns zero, hence +// we cannot paint the QR code onto the canvas before it becomes visible. +var qrCodeCanvasObserver=function(){var width=0;return new ResizeObserver(function(entries,observer){var qrCodeCanvas=entries[0].target;// Avoid infinite resize events due to clearing and repainting +// the same QR code on the canvas. +if(width===qrCodeCanvas.clientWidth){return;}width=qrCodeCanvas.clientWidth;QrCreator.render({text:_photo3.getViewLink(myPhoto.id),radius:0.0,ecLevel:"H",fill:"#000000",background:"#FFFFFF",size:width},qrCodeCanvas);});}();basicModal.show({body:"",classList:["qr-code"],readyCB:function readyCB(formElements,dialog){var qrCodeCanvas=dialog.querySelector("canvas");qrCodeCanvasObserver.observe(qrCodeCanvas);},buttons:{cancel:{title:lychee.locale["CLOSE"],fn:function fn(){qrCodeCanvasObserver.disconnect();basicModal.close();}}}});};/** + * @returns {string} + */_photo3.getDirectLink=function(){return _photo3.json&&_photo3.json.size_variants&&_photo3.json.size_variants.original&&_photo3.json.size_variants.original.url?_photo3.json.size_variants.original.url:"";};/** + * @param {string} photoID + * @returns {string} + */_photo3.getViewLink=function(photoID){return lychee.getBaseUrl()+"view?p="+photoID;};/** + * @param photoID + * @returns {void} + */_photo3.showDirectLinks=function(photoID){if(!_photo3.json||_photo3.json.id!==photoID){return;}/** + * @param {string} name - name of the HTML input element + * @returns {string} - HTML + */var buildLine=function buildLine(name){return"\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t
");};var localizations={original:lychee.locale["PHOTO_FULL"],medium2x:lychee.locale["PHOTO_MEDIUM_HIDPI"],medium:lychee.locale["PHOTO_MEDIUM"],small2x:lychee.locale["PHOTO_SMALL_HIDPI"],small:lychee.locale["PHOTO_SMALL"],thumb2x:lychee.locale["PHOTO_THUMB_HIDPI"],thumb:lychee.locale["PHOTO_THUMB"]};var showDirectLinksDialogBody='

";/** + * @param {ModalDialogFormElements} formElements + * @param {HTMLDivElement} dialog + */var initShowDirectLinksDialog=function initShowDirectLinksDialog(formElements,dialog){formElements.view.value=_photo3.getViewLink(photoID);formElements.view.previousElementSibling.textContent=lychee.locale["PHOTO_VIEW"];formElements.view.nextElementSibling.title=lychee.locale["URL_COPY_TO_CLIPBOARD"];dialog.querySelector("p").textContent=lychee.locale["PHOTO_DIRECT_LINKS_TO_IMAGES"];for(var type in localizations){/** @type {?SizeVariant} */var sv=_photo3.json.size_variants[type];if(sv!==null){formElements[type].value=lychee.getBaseUrl()+sv.url;formElements[type].previousElementSibling.textContent=localizations[type]+" ("+sv.width+"×"+sv.height+")";formElements[type].nextElementSibling.title=lychee.locale["URL_COPY_TO_CLIPBOARD"];}else{// The form element is the `` element, the parent +// element is the `
` which binds the label, the input +// and the button together. +// We remove that `
` for non-existing variants. +formElements[type].parentElement.remove();}}if(_photo3.json.live_photo_url!==null){formElements.live.value=lychee.getBaseUrl()+_photo3.json.live_photo_url;formElements.live.previousElementSibling.textContent=lychee.locale["PHOTO_LIVE_VIDEO"];formElements.live.nextElementSibling.title=lychee.locale["URL_COPY_TO_CLIPBOARD"];}else{formElements.live.parentElement.remove();}/** @param {TouchEvent|MouseEvent} ev */var onClickOrTouch=function onClickOrTouch(ev){navigator.clipboard.writeText(ev.currentTarget.previousElementSibling.value).then(function(){return loadingBar.show("success",lychee.locale["URL_COPIED_TO_CLIPBOARD"]);});ev.stopPropagation();};dialog.querySelectorAll("a.button").forEach(function(a){return lychee.addClickOrTouchListener(a,onClickOrTouch);});};basicModal.show({body:showDirectLinksDialogBody,readyCB:initShowDirectLinksDialog,buttons:{cancel:{title:lychee.locale["CLOSE"],fn:basicModal.close}}});};/** + * @description Takes care of every action a photoeditor can handle and execute. + */photoeditor={};/** + * @param {string} photoID + * @param {number} direction - either `1` or `-1` + * @returns {void} + */photoeditor.rotate=function(photoID,direction){api.post("PhotoEditor::rotate",{photoID:photoID,direction:direction},/** @param {Photo} data */function(data){_photo3.json=data;// TODO: `photo.json.original_album_id` is set only, but never read; do we need it? +_photo3.json.original_album_id=_photo3.json.album_id;if(album.json){// TODO: Why do we overwrite the true album ID of a photo, by the externally provided one? I guess we need it, because the album which the user came from might also be a smart album or a tag album. However, in this case I would prefer to leave the `album_id untouched (don't rename it to `original_album_id`) and call this one `effective_album_id` instead. +_photo3.json.album_id=album.json.id;}var image=$("img#image");if(_photo3.json.size_variants.medium2x!==null){image.prop("srcset","".concat(_photo3.json.size_variants.medium.url," ").concat(_photo3.json.size_variants.medium.width,"w, ").concat(_photo3.json.size_variants.medium2x.url," ").concat(_photo3.json.size_variants.medium2x.width,"w"));}else{image.prop("srcset","");}image.prop("src",_photo3.json.size_variants.medium!==null?_photo3.json.size_variants.medium.url:_photo3.json.size_variants.original.url);view.photo.onresize();view.photo.sidebar();album.updatePhoto(data);});};/** + * @description Searches through your photos and albums. + */ /** + * The ID of the search album + * + * Constant `'search'`. + * + * @type {string} + */var SearchAlbumIDPrefix="search";/** + * @typedef SearchAlbum + * + * A "virtual" album which holds the search results in a form which is + * mostly compatible with the other album types, i.e. + * {@link Album}, {@link TagAlbum} and {@link SmartAlbum}. + * + * @property {string} id - always equals `SearchAlbumIDPrefix/search-term` + * @property {string} title - always equals `lychee.locale["SEARCH_RESULTS"]` + * @property {Photo[]} photos - the found photos + * @property {Album[]} albums - the found albums + * @property {TagAlbum[]} tag_albums - the found tag albums + * @property {?Thumb} thumb - always `null`; just a dummy entry, because all other albums {@link Album}, {@link TagAlbum}, {@link SmartAlbum} have it + * @property {boolean} is_public - always `false`; just a dummy entry, because all other albums {@link Album}, {@link TagAlbum}, {@link SmartAlbum} have it + * @property {boolean} grant_download - always `false`; just a dummy entry, because all other albums {@link Album}, {@link TagAlbum}, {@link SmartAlbum} have it + */ /** + * The search object + */var search={/** @type {?SearchResult} */json:null};/** + * @param {string} term + * @returns {void} + */search.find=function(term){if(term.trim()==="")return;/** @param {SearchResult} data */var successHandler=function successHandler(data){if(search.json&&search.json.checksum===data.checksum){// If search result is identical to previous result, just +// update the album id with the new search term and bail out. +album.json.id=SearchAlbumIDPrefix+"/"+term;return;}search.json=data;// Create and assign a `SearchAlbum` +album.json={id:SearchAlbumIDPrefix+"/"+term,title:lychee.locale["SEARCH_RESULTS"],photos:search.json.photos,albums:search.json.albums,tag_albums:search.json.tag_albums,thumb:null,rights:{can_download:false},policy:{is_public:false}};var albumsData="";var photosData="";// Build HTML for album +search.json.tag_albums.forEach(function(album){albums.parse(album);albumsData+=build.album(album);});search.json.albums.forEach(function(album){albums.parse(album);albumsData+=build.album(album);});// Build HTML for photo +search.json.photos.forEach(function(photo){photosData+=build.photo(photo);});var albums_divider=lychee.locale["ALBUMS"];var photos_divider=lychee.locale["PHOTOS"];if(albumsData!=="")albums_divider+=" ("+(search.json.tag_albums.length+search.json.albums.length)+")";if(photosData!==""){photos_divider+=" ("+search.json.photos.length+")";if(lychee.layout==="justified"){photosData='
'+photosData+"
";}else if(lychee.layout==="unjustified"){photosData='
'+photosData+"
";}}// 1. No albums and photos +// 2. Only photos +// 3. Only albums +// 4. Albums and photos +var html=albumsData===""&&photosData===""?"":albumsData===""?build.divider(photos_divider)+photosData:photosData===""?build.divider(albums_divider)+albumsData:build.divider(albums_divider)+albumsData+build.divider(photos_divider)+photosData;$(".no_content").remove();lychee.animate(lychee.content,"contentZoomOut");setTimeout(function(){if(visible.photo())view.photo.hide();if(visible.sidebar())_sidebar.toggle(false);if(visible.mapview())mapview.close();header.setMode("albums");if(html===""){lychee.content.html("");lychee.content.append(build.no_content("magnifying-glass"));}else{lychee.content.html(html);// Here we exploit the layout method of an album although +// the search result is not a proper album. +// It would be much better to have a component like +// `view.photos` (note the plural form) which takes care of +// all photo listings independent of the surrounding "thing" +// (i.e. regular album, tag album, search result) +setTimeout(function(){view.album.content.justify();lychee.animate(lychee.content,"contentZoomIn");$("#lychee_view_container").scrollTop(0);},0);}lychee.setMetaData(lychee.locale["SEARCH_RESULTS"]);},300);};/** @returns {void} */var timeoutHandler=function timeoutHandler(){if(header.dom(".header__search").val().length!==0){api.post("Search::run",{term:term},successHandler);}else{search.reset();}};clearTimeout($(window).data("timeout"));$(window).data("timeout",setTimeout(timeoutHandler,250));};search.reset=function(){header.dom(".header__search").val("");$(".no_content").remove();if(search.json!==null){// Trash data +album.json=null;_photo3.json=null;search.json=null;lychee.animate($(".divider"),"fadeOut");lychee["goto"]();}};/** + * @description Lets you change settings. + */var settings={};/** + * @returns {void} + */settings.open=function(){view.settings.init();};/** + * A dictionary of (name,value)-pairs of the form. + * + * @typedef SettingsFormData + * + * @type {Object.} + */ /** + * From https://github.com/electerious/basicModal/blob/master/src/scripts/main.js + * + * @param {string} formSelector + * @returns {SettingsFormData} + */settings.getValues=function(formSelector){var values={};/** @type {?NodeListOf} */var inputElements=document.querySelectorAll(formSelector+" input[name]");// Get value from all inputs +inputElements.forEach(function(inputElement){switch(inputElement.type){case"checkbox":case"radio":values[inputElement.name]=inputElement.checked;break;case"number":case"range":values[inputElement.name]=parseInt(inputElement.value,10);break;case"file":values[inputElement.name]=inputElement.files;break;default:switch(inputElement.getAttribute("inputmode")){case"numeric":values[inputElement.name]=parseInt(inputElement.value,10);break;case"decimal":values[inputElement.name]=parseFloat(inputElement.value);break;default:values[inputElement.name]=inputElement.value;}}});/** @type {?NodeListOf} */var selectElements=document.querySelectorAll(formSelector+" select[name]");// Get name of selected option from all selects +selectElements.forEach(function(selectElement){values[selectElement.name]=selectElement.selectedIndex!==-1?selectElement.options[selectElement.selectedIndex].value:null;});return values;};/** + * @callback SettingClickCB + * + * @param {SettingsFormData} formData + * @returns {void} + */ /** + * From https://github.com/electerious/basicModal/blob/master/src/scripts/main.js. + * + * @param {string} inputSelector + * @param {string} formSelector + * @param {SettingClickCB} settingClickCB + */settings.bind=function(inputSelector,formSelector,settingClickCB){$(inputSelector).on("click",function(){settingClickCB(settings.getValues(formSelector));});};/** + * @param {SettingsFormData} params + * @returns {void} + */settings.changeLogin=function(params){if(params.username===""){params.username=null;}if(params.password.length<1){loadingBar.show("error",lychee.locale["ERROR_EMPTY_PASSWORD"]);$("input[name=password]").addClass("error");return;}else{$("input[name=password]").removeClass("error");}if(params.password!==params.confirm){loadingBar.show("error",lychee.locale["ERROR_PASSWORD_DOES_NOT_MATCH"]);$("input[name=confirm]").addClass("error");return;}else{$("input[name=confirm]").removeClass("error");}api.post("User::updateLogin",params,/** @param {User} updatedUser */function(updatedUser){$("input[name]").removeClass("error");loadingBar.show("success",lychee.locale["SETTINGS_SUCCESS_LOGIN"]);view.settings.content.clearLogin();lychee.user=updatedUser;});};/** + * @param {SettingsFormData} params + * @returns {void} + */settings.changeSorting=function(params){api.post("Settings::setSorting",params,function(){lychee.sorting_albums.column=params["sorting_albums_column"];lychee.sorting_albums.order=params["sorting_albums_order"];lychee.sorting_photos.column=params["sorting_photos_column"];lychee.sorting_photos.order=params["sorting_photos_order"];albums.refresh();loadingBar.show("success",lychee.locale["SETTINGS_SUCCESS_SORT"]);});};/** + * @param {SettingsFormData} params + * @returns {void} + */settings.changeDropboxKey=function(params){// if params.key == "" key is cleared +api.post("Settings::setDropboxKey",params,function(){lychee.dropboxKey=params.key;// if (callback) lychee.loadDropbox(callback) +loadingBar.show("success",lychee.locale["SETTINGS_SUCCESS_DROPBOX"]);});};/** + * @param {SettingsFormData} params + * @returns {void} + */settings.changeLang=function(params){api.post("Settings::setLang",params,function(){loadingBar.show("success",lychee.locale["SETTINGS_SUCCESS_LANG"]);lychee.init();});};/** + * @param {SettingsFormData} params + * @returns {void} + */settings.setDefaultLicense=function(params){api.post("Settings::setDefaultLicense",params,function(){lychee.default_license=params.license;loadingBar.show("success",lychee.locale["SETTINGS_SUCCESS_LICENSE"]);});};/** + * @param {SettingsFormData} params + * @returns {void} + */settings.setLayout=function(params){api.post("Settings::setLayout",params,function(){lychee.layout=params.layout;loadingBar.show("success",lychee.locale["SETTINGS_SUCCESS_LAYOUT"]);});};/** + * @param {SettingsFormData} params + * @returns {void} + */settings.changePublicSearch=function(params){api.post("Settings::setPublicSearch",params,function(){loadingBar.show("success",lychee.locale["SETTINGS_SUCCESS_PUBLIC_SEARCH"]);lychee.public_search=params.public_search;});};/** + * @param {SettingsFormData} params + * @returns {void} + */settings.setAlbumDecoration=function(params){api.post("Settings::setAlbumDecoration",params,function(){loadingBar.show("success",lychee.locale["SETTINGS_SUCCESS_ALBUM_DECORATION"]);albums.refresh();lychee.album_decoration=params.album_decoration;lychee.album_decoration_orientation=params.album_decoration_orientation;});};/** + * @param {SettingsFormData} params + * @returns {void} + */settings.setOverlayType=function(params){api.post("Settings::setOverlayType",params,function(){loadingBar.show("success",lychee.locale["SETTINGS_SUCCESS_IMAGE_OVERLAY"]);lychee.image_overlay_type=params.image_overlay_type;lychee.image_overlay_type_default=params.image_overlay_type;});};/** + * @param {SettingsFormData} params + * @returns {void} + */settings.changeMapDisplay=function(params){api.post("Settings::setMapDisplay",params,function(){loadingBar.show("success",lychee.locale["SETTINGS_SUCCESS_MAP_DISPLAY"]);lychee.map_display=params.map_display;// Map functionality is disabled +// -> map for public albums also needs to be disabled +if(!lychee.map_display&&lychee.map_display_public){$("#MapDisplayPublic").click();}});};/** + * @param {SettingsFormData} params + * @returns {void} + */settings.changeMapDisplayPublic=function(params){api.post("Settings::setMapDisplayPublic",params,function(){loadingBar.show("success",lychee.locale["SETTINGS_SUCCESS_MAP_DISPLAY_PUBLIC"]);lychee.map_display_public=params.map_display_public;// If public map functionality is enabled, but map in general is disabled +// General map functionality needs to be enabled +if(lychee.map_display_public&&!lychee.map_display){$("#MapDisplay").click();}});};/** + * @param {SettingsFormData} params + * @returns {void} + */settings.setMapProvider=function(params){api.post("Settings::setMapProvider",params,function(){loadingBar.show("success",lychee.locale["SETTINGS_SUCCESS_MAP_PROVIDER"]);lychee.map_provider=params.map_provider;});};/** + * @param {SettingsFormData} params + * @returns {void} + */settings.changeMapIncludeSubAlbums=function(params){api.post("Settings::setMapIncludeSubAlbums",params,function(){loadingBar.show("success",lychee.locale["SETTINGS_SUCCESS_MAP_DISPLAY"]);lychee.map_include_subalbums=params.map_include_subalbums;});};/** + * @param {SettingsFormData} params + * @returns {void} + */settings.changeLocationDecoding=function(params){api.post("Settings::setLocationDecoding",params,function(){loadingBar.show("success",lychee.locale["SETTINGS_SUCCESS_MAP_DISPLAY"]);lychee.location_decoding=params.location_decoding;});};/** + * @param {SettingsFormData} params + * @returns {void} + */settings.changeNSFWVisible=function(params){api.post("Settings::setNSFWVisible",params,function(){loadingBar.show("success",lychee.locale["SETTINGS_SUCCESS_NSFW_VISIBLE"]);lychee.nsfw_visible=params.nsfw_visible;lychee.nsfw_visible_saved=lychee.nsfw_visible;});};/** + * @param {SettingsFormData} params + * @returns {void} + */settings.changeSmartAlbumVisibility=function(params){api.post("Settings::setSmartAlbumVisibility",params,function(){loadingBar.show("success",lychee.locale["SUCCESS"]);var albumId=params.albumID;lychee.smart_album_visibilty[albumId]=params.is_public;});};//TODO : later +// lychee.nsfw_blur = (data.config.nsfw_blur && data.config.nsfw_blur === '1') || false; +// lychee.nsfw_warning = (data.config.nsfw_warning && data.config.nsfw_warning === '1') || false; +// lychee.nsfw_warning_text = data.config.nsfw_warning_text || 'Sensitive content

This album contains sensitive content which some people may find offensive or disturbing.

'; +/** + * @param {SettingsFormData} params + * @returns {void} + */settings.changeLocationShow=function(params){api.post("Settings::setLocationShow",params,function(){loadingBar.show("success",lychee.locale["SETTINGS_SUCCESS_MAP_DISPLAY"]);lychee.location_show=params.location_show;// Don't show location +// -> location for public albums also needs to be disabled +if(!lychee.location_show&&lychee.location_show_public){$("#LocationShowPublic").click();}});};/** + * @param {SettingsFormData} params + * @returns {void} + */settings.changeLocationShowPublic=function(params){api.post("Settings::setLocationShowPublic",params,function(){loadingBar.show("success",lychee.locale["SETTINGS_SUCCESS_MAP_DISPLAY"]);lychee.location_show_public=params.location_show_public;// If public map functionality is enabled, but map in general is disabled +// General map functionality needs to be enabled +if(lychee.location_show_public&&!lychee.location_show){$("#LocationShow").click();}});};/** + * @param {SettingsFormData} params + * @returns {void} + */settings.changeNewPhotosNotification=function(params){api.post("Settings::setNewPhotosNotification",params,function(){loadingBar.show("success",lychee.locale["SETTINGS_SUCCESS_NEW_PHOTOS_NOTIFICATION"]);lychee.new_photos_notification=params.new_photos_notification;});};/** + * @returns {void} + */settings.changeCSS=function(){var params={css:$("#css").val()};api.post("Settings::setCSS",params,function(){lychee.css=params.css;loadingBar.show("success",lychee.locale["SETTINGS_SUCCESS_CSS"]);});};/** + * @returns {void} + */settings.changeJS=function(){var params={js:$("#js").val()};api.post("Settings::setJS",params,function(){lychee.js=params.js;loadingBar.show("success",lychee.locale["SETTINGS_SUCCESS_JS"]);});};/** + * @param {SettingsFormData} params + * @returns {void} + */settings.save=function(params){api.post("Settings::saveAll",params,function(){loadingBar.show("success",lychee.locale["SETTINGS_SUCCESS_UPDATE"]);view.full_settings.init();// re-read settings +lychee.init(false);});};/** + * @param {jQuery.Event} e + * @returns {void} + */settings.save_enter=function(e){// We only handle "enter" +if(e.which!==13)return;var saveSettingsConfirmationDialogBody=// TODO: move the style to the style file, where it belongs. +'

';basicModal.show({body:saveSettingsConfirmationDialogBody,readyCB:function readyCB(formElements,dialog){dialog.querySelector("p").textContent=lychee.locale["SETTINGS_ADVANCED_SAVE"];},buttons:{action:{title:lychee.locale["ENTER"],fn:function fn(){settings.save(settings.getValues("#fullSettings"));basicModal.close();},classList:["red"]},cancel:{title:lychee.locale["CANCEL"],fn:basicModal.close}}});};/** + * @returns {void} + */settings.openTokenDialog=function(){/** @type {string} */var tokenValue="";/** @type {?HTMLAnchorElement} */var resetTokenButton=null;/** @type {?HTMLAnchorElement} */var copyTokenButton=null;/** @type {?HTMLAnchorElement} */var disableTokenButton=null;/** @type {?HTMLInputElement} */var tokenInputElement=null;var bodyHtml="\n\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t
\n\t\t
";/** + * @returns {void} + */var updateTokenDialog=function updateTokenDialog(){if(lychee.user.has_token){disableTokenButton.style.display=null;if(!!tokenValue){tokenInputElement.value=tokenValue;tokenInputElement.disabled=false;copyTokenButton.style.display=null;}else{tokenInputElement.value=lychee.locale["TOKEN_NOT_AVAILABLE"];tokenInputElement.disabled=true;copyTokenButton.style.display="none";}}else{tokenInputElement.value=lychee.locale["DISABLED_TOKEN_STATUS_MSG"];tokenInputElement.disabled=true;copyTokenButton.style.display="none";disableTokenButton.style.display="none";}};/** + * @param {MouseEvent|TouchEvent} ev + */var onCopyToken=function onCopyToken(ev){navigator.clipboard.writeText(tokenValue);ev.stopPropagation();};/** + * @param {MouseEvent|TouchEvent} ev + */var onResetToken=function onResetToken(ev){tokenInputElement.value="";ev.stopPropagation();api.post("User::resetToken",{},/** + * @param {{token: string}} data + */function(data){tokenValue=data.token;lychee.user.has_token=true;updateTokenDialog();});};/** + * @param {MouseEvent|TouchEvent} ev + */var onDisableToken=function onDisableToken(ev){tokenInputElement.value="";ev.stopPropagation();api.post("User::unsetToken",{},function(){tokenValue="";lychee.user.has_token=false;updateTokenDialog();});};/** + * @param {ModalDialogFormElements} formElements + * @param {HTMLDivElement} dialog + * @returns {void} + */var initTokenDialog=function initTokenDialog(formElements,dialog){resetTokenButton=dialog.querySelector("a#button_reset_token");resetTokenButton.title=lychee.locale["RESET"];copyTokenButton=dialog.querySelector("a#button_copy_token");copyTokenButton.title=lychee.locale["URL_COPY_TO_CLIPBOARD"];disableTokenButton=dialog.querySelector("a#button_disable_token");disableTokenButton.title=lychee.locale["DISABLE_TOKEN_TOOLTIP"];tokenInputElement=formElements.token;tokenInputElement.placeholder=lychee.locale["TOKEN_WAIT"];dialog.querySelector("label").textContent="Token";tokenInputElement.blur();updateTokenDialog();lychee.addClickOrTouchListener(copyTokenButton,onCopyToken);lychee.addClickOrTouchListener(resetTokenButton,onResetToken);lychee.addClickOrTouchListener(disableTokenButton,onDisableToken);};basicModal.show({body:bodyHtml,readyCB:initTokenDialog,buttons:{cancel:{title:lychee.locale["CLOSE"],fn:basicModal.close}}});};var sharing={/** @type {?SharingInfo} */json:null};/** + * @returns {void} + */sharing.add=function(){var params={/** @type {string[]} */albumIDs:[],/** @type {number[]} */userIDs:[]};$("#albums_list_to option").each(function(){params.albumIDs.push(this.value);});$("#user_list_to option").each(function(){params.userIDs.push(Number.parseInt(this.value,10));});if(params.albumIDs.length===0){loadingBar.show("error",lychee.locale["ERROR_SELECT_ALBUM"]);return;}if(params.userIDs.length===0){loadingBar.show("error",lychee.locale["ERROR_SELECT_USER"]);return;}api.post("Sharing::add",params,function(){loadingBar.show("success",lychee.locale["SHARING_SUCCESS"]);sharing.list();// reload user list +});};/** + * @returns {void} + */sharing["delete"]=function(){var params={/** @type {number[]} */shareIDs:[]};$('input[name="remove_id"]:checked').each(function(){params.shareIDs.push(Number.parseInt(this.value,10));});if(params.shareIDs.length===0){loadingBar.show("error",lychee.locale["ERROR_SELECT_SHARING"]);return;}api.post("Sharing::delete",params,function(){loadingBar.show("success",lychee.locale["SHARING_REMOVED"]);sharing.list();// reload user list +});};/** + * Queries the backend for a list of active shares, sharable albums and users + * with whom albums can be shared. + * + * For admin user, the query is unrestricted, for non-admin user the + * query is restricted to albums which are owned by the currently + * authenticated user. + * The latter is required as the backend forbids unrestricted queries for + * non-admin users. + * + * @returns {void} + */sharing.list=function(){api.post("Sharing::list",lychee.rights.is_admin?{}:{ownerID:lychee.user.id},/** @param {SharingInfo} data */function(data){sharing.json=data;view.sharing.init();});};/** + * @description This module takes care of the sidebar. + */ /** + * @namespace + */var _sidebar={/** @type {jQuery} */_dom:$("#lychee_sidebar_container"),types:{DEFAULT:0,TAGS:1},createStructure:{}};/** + * @param {?string} [selector=null] + * @returns {jQuery} + */_sidebar.dom=function(selector){if(selector==null||selector==="")return _sidebar._dom;return _sidebar._dom.find(selector);};/** + * This function should be called after building and appending + * the sidebars content to the DOM. + * This function can be called multiple times, therefore + * event handlers should be removed before binding a new one. + * + * @returns {void} + */_sidebar.bind=function(){var eventName="click";_sidebar.dom("#edit_title").off(eventName).on(eventName,function(){if(visible.photo())_photo3.setTitle([_photo3.getID()]);else if(visible.album())album.setTitle([album.getID()]);});_sidebar.dom("#edit_description").off(eventName).on(eventName,function(){if(visible.photo())_photo3.setDescription(_photo3.getID());else if(visible.album())album.setDescription(album.getID());});_sidebar.dom("#edit_uploaded").off(eventName).on(eventName,function(){if(visible.photo())_photo3.setCreatedAt(_photo3.getID());});_sidebar.dom("#edit_showtags").off(eventName).on(eventName,function(){album.setShowTags(album.getID());});_sidebar.dom("#edit_tags").off(eventName).on(eventName,function(){_photo3.editTags([_photo3.getID()]);});_sidebar.dom("#tags .tag").off(eventName).on(eventName,function(){_sidebar.triggerSearch($(this).text());});_sidebar.dom("#tags .tag span").off(eventName).on(eventName,function(){_photo3.deleteTag(_photo3.getID(),$(this).data("index"));});_sidebar.dom("#edit_license").off(eventName).on(eventName,function(){if(visible.photo())_photo3.setLicense(_photo3.getID());else if(visible.album())album.setLicense(album.getID());});_sidebar.dom("#edit_sorting").off(eventName).on(eventName,function(){album.setSorting(album.getID());});_sidebar.dom(".attr_location").off(eventName).on(eventName,function(){_sidebar.triggerSearch($(this).text());});};/** + * @param {string} search_string + * @returns {void} + */_sidebar.triggerSearch=function(search_string){// If public search is disabled -> do nothing +if(lychee.publicMode&&!lychee.public_search){// Do not display an error -> just do nothing to not confuse the user +return;}search.json=null;// We're either logged in or public search is allowed +lychee["goto"](SearchAlbumIDPrefix+"/"+encodeURIComponent(search_string));};/** + * @returns {boolean} + */_sidebar.keepSidebarVisible=function(){var v=sessionStorage.getItem("keepSidebarVisible");return v!==null?v==="true":false;};/** + * @param {boolean} is_user_initiated - indicates if the user requested to + * toggle and hence the new state shall + * be saved in session storage + * @returns {void} + */_sidebar.toggle=function(is_user_initiated){if(visible.sidebar()||visible.sidebarbutton()){header.dom(".button--info").toggleClass("active");_sidebar.dom().toggleClass("active");if(is_user_initiated)sessionStorage.setItem("keepSidebarVisible",visible.sidebar()?"true":"false");}};/** + * Attributes/Values inside the sidebar are selectable by default. + * Selection needs to be deactivated to prevent an unwanted selection + * while using multiselect. + * + * @param {boolean} [selectable=true] + * @returns {void} + */_sidebar.setSelectable=function(){var selectable=arguments.length>0&&arguments[0]!==undefined?arguments[0]:true;if(selectable)_sidebar.dom().removeClass("notSelectable");else _sidebar.dom().addClass("notSelectable");};/** + * @param {string} attr - selector of attribute without the `attr_` prefix + * @param {?string} value - a `null` value is replaced by the empty string + * @param {boolean} [dangerouslySetInnerHTML=false] + * + * @returns {void} + */_sidebar.changeAttr=function(attr){var value=arguments.length>1&&arguments[1]!==undefined?arguments[1]:"";var dangerouslySetInnerHTML=arguments.length>2&&arguments[2]!==undefined?arguments[2]:false;if(!attr)return;if(!value)value="";// TODO: Don't use our home-brewed `escapeHTML` method; use `jQuery#text` instead +// Escape value +if(!dangerouslySetInnerHTML)value=lychee.escapeHTML(value);_sidebar.dom(".attr_"+attr).html(value);};/** + * @param {string} attr - selector of attribute without the `attr_` prefix + * @returns {void} + */_sidebar.hideAttr=function(attr){_sidebar.dom(".attr_"+attr).closest("tr").hide();};/** + * Converts integer seconds into "hours:minutes:seconds". + * + * TODO: Consider to make this method part of `lychee.locale`. + * + * @param {(number|string)} d + * @returns {string} + */_sidebar.secondsToHMS=function(d){d=Number(d);var h=Math.floor(d/3600);var m=Math.floor(d%3600/60);var s=Math.floor(d%60);return(h>0?h.toString()+"h":"")+(m>0?m.toString()+"m":"")+(s>0||h===0&&m===0?s.toString()+"s":"");};/** + * @typedef Section + * + * @property {string} title + * @property {number} type + * @property {SectionRow[]} rows + */ /** + * @typedef SectionRow + * + * @property {string} title + * @property {string} kind + * @property {(string|string[])} value + * @property {boolean} [editable] + */ /** + * @param {?Photo} data + * @returns {Section[]} + */_sidebar.createStructure.photo=function(data){if(!data)return[];var editable=data.rights.can_edit;var hasExif=!!data.taken_at||!!data.make||!!data.model||!!data.shutter||!!data.aperture||!!data.focal||!!data.iso;// Attributes for geo-position are nullable floats. +// The geo-position 0°00'00'', 0°00'00'' at zero altitude is very unlikely +// but valid (it's south of the coast of Ghana in the Atlantic) +// So we must not calculate the sum and compare for zero. +var hasLocation=data.longitude!==null||data.latitude!==null||data.altitude!==null;var structure={};var isPublic="";var isVideo=data.type&&data.type.indexOf("video")>-1;var license;// Set the license string for a photo +switch(data.license){// if the photo doesn't have a license +case"none":license="";break;// Localize All Rights Reserved +case"reserved":license=lychee.locale["PHOTO_RESERVED"];break;// Display anything else that's set +default:license=data.license;break;}// Set value for public +switch(data.is_public){case 0:isPublic=lychee.locale["PHOTO_SHR_NO"];break;case 1:isPublic=lychee.locale["PHOTO_SHR_PHT"];break;case 2:isPublic=lychee.locale["PHOTO_SHR_ALB"];break;default:isPublic="-";break;}structure.basics={title:lychee.locale["PHOTO_BASICS"],type:_sidebar.types.DEFAULT,rows:[{title:lychee.locale["PHOTO_TITLE"],kind:"title",value:data.title,editable:editable},{title:lychee.locale["PHOTO_UPLOADED"],kind:"uploaded",value:lychee.locale.printDateTime(data.created_at),editable:editable},{title:lychee.locale["PHOTO_DESCRIPTION"],kind:"description",value:data.description?data.description:"",editable:editable}]};structure.image={title:lychee.locale[isVideo?"PHOTO_VIDEO":"PHOTO_IMAGE"],type:_sidebar.types.DEFAULT,rows:[{title:lychee.locale["PHOTO_SIZE"],kind:"size",value:lychee.locale.printFilesizeLocalized(data.size_variants.original.filesize)},{title:lychee.locale["PHOTO_FORMAT"],kind:"type",value:data.type},{title:lychee.locale["PHOTO_RESOLUTION"],kind:"resolution",value:data.size_variants.original.width+" x "+data.size_variants.original.height}]};if(isVideo){if(data.size_variants.original.width===0||data.size_variants.original.height===0){// Remove the "Resolution" line if we don't have the data. +structure.image.rows.splice(-1,1);}// We overload the database, storing duration (in full seconds) in +// "aperture" and frame rate (floating point with three digits after +// the decimal point) in "focal". +if(data.aperture){structure.image.rows.push({title:lychee.locale["PHOTO_DURATION"],kind:"duration",value:_sidebar.secondsToHMS(data.aperture)});}if(data.focal){structure.image.rows.push({title:lychee.locale["PHOTO_FPS"],kind:"fps",value:data.focal+" fps"});}}// Always create tags section - behaviour for editing +// tags handled when constructing the html code for tags +// TODO: IDE warns, that `value` is not property and `rows` is missing; the tags should actually be stored in a row for consistency +// TODO: Consider to NOT call `build.tags` here, but simply pass the plain JSON. `build.tags` should be called in `sidebar.render` below +structure.tags={title:lychee.locale["PHOTO_TAGS"],type:_sidebar.types.TAGS,value:build.tags(data.tags),editable:editable};// Only create EXIF section when EXIF data available +if(hasExif){structure.exif={title:lychee.locale["PHOTO_CAMERA"],type:_sidebar.types.DEFAULT,rows:isVideo?[{title:lychee.locale["PHOTO_CAPTURED"],kind:"takedate",value:lychee.locale.printDateTime(data.taken_at)},{title:lychee.locale["PHOTO_MAKE"],kind:"make",value:data.make},{title:lychee.locale["PHOTO_TYPE"],kind:"model",value:data.model}]:[{title:lychee.locale["PHOTO_CAPTURED"],kind:"takedate",value:lychee.locale.printDateTime(data.taken_at)},{title:lychee.locale["PHOTO_MAKE"],kind:"make",value:data.make},{title:lychee.locale["PHOTO_TYPE"],kind:"model",value:data.model},{title:lychee.locale["PHOTO_LENS"],kind:"lens",value:data.lens},{title:lychee.locale["PHOTO_SHUTTER"],kind:"shutter",value:data.shutter},{title:lychee.locale["PHOTO_APERTURE"],kind:"aperture",value:data.aperture},{title:lychee.locale["PHOTO_FOCAL"],kind:"focal",value:data.focal},{title:sprintf(lychee.locale["PHOTO_ISO"],""),kind:"iso",value:data.iso}]};}else{structure.exif={};}structure.sharing={title:lychee.locale["PHOTO_SHARING"],type:_sidebar.types.DEFAULT,rows:[{title:lychee.locale["PHOTO_SHR_PUBLIC"],kind:"public",value:isPublic}]};structure.license={title:lychee.locale["PHOTO_REUSE"],type:_sidebar.types.DEFAULT,rows:[{title:lychee.locale["PHOTO_LICENSE"],kind:"license",value:license,editable:editable}]};if(hasLocation){structure.location={title:lychee.locale["PHOTO_LOCATION"],type:_sidebar.types.DEFAULT,rows:[{title:lychee.locale["PHOTO_LATITUDE"],kind:"latitude",value:data.latitude?DecimalToDegreeMinutesSeconds(data.latitude,true):""},{title:lychee.locale["PHOTO_LONGITUDE"],kind:"longitude",value:data.longitude?DecimalToDegreeMinutesSeconds(data.longitude,false):""},// No point in displaying sub-mm precision; 10cm is more than enough. +{title:lychee.locale["PHOTO_ALTITUDE"],kind:"altitude",value:data.altitude?(Math.round(data.altitude*10)/10).toString()+"m":""},{title:lychee.locale["PHOTO_LOCATION"],kind:"location",// Explode location string into an array to keep street, city etc. separate +// TODO: We should consider to keep the components apart on the server-side and send an structured object to the front-end. +value:data.location?data.location.split(",").map(function(item){return item.trim();}):""}]};if(data.img_direction!==null){// No point in display sub-degree precision. +structure.location.rows.push({title:lychee.locale["PHOTO_IMGDIRECTION"],kind:"imgDirection",value:Math.round(data.img_direction).toString()+"°"});}}else{structure.location={};}// Construct all parts of the structure +var structure_ret=[structure.basics,structure.image,structure.tags,structure.exif,structure.location];if(license){structure_ret.push(structure.license);}if(!lychee.publicMode){structure_ret.push(structure.sharing);}return structure_ret;};/** + * @param {(Album|TagAlbum|SmartAlbum)} data + * @returns {Section[]} + */_sidebar.createStructure.album=function(data){if(!data)return[];var editable=data.rights.can_edit;var structure={};var isPublic=!!data.policy&&data.policy.is_public?lychee.locale["ALBUM_SHR_YES"]:lychee.locale["ALBUM_SHR_NO"];var requiresLink=!!data.policy&&data.policy.is_link_required?lychee.locale["ALBUM_SHR_YES"]:lychee.locale["ALBUM_SHR_NO"];var isDownloadable=!!data.policy&&data.policy.grant_download?lychee.locale["ALBUM_SHR_YES"]:lychee.locale["ALBUM_SHR_NO"];var hasPassword=!!data.policy&&data.policy.is_password_required?lychee.locale["ALBUM_SHR_YES"]:lychee.locale["ALBUM_SHR_NO"];var license="";var sorting="";// Set license string +switch(data.license){case"none":license="";// consistency +break;case"reserved":license=lychee.locale["ALBUM_RESERVED"];break;default:license=data.license;break;}if(!lychee.publicMode){if(!data.sorting){sorting=lychee.locale["DEFAULT"];}else{sorting=data.sorting.column+" "+data.sorting.order;}}structure.basics={title:lychee.locale["ALBUM_BASICS"],type:_sidebar.types.DEFAULT,rows:[{title:lychee.locale["ALBUM_TITLE"],kind:"title",value:data.title,editable:editable},{title:lychee.locale["ALBUM_DESCRIPTION"],kind:"description",value:data.description?data.description:"",editable:editable}]};if(album.isTagAlbum()){structure.basics.rows.push({title:lychee.locale["ALBUM_SHOW_TAGS"],kind:"showtags",value:data.show_tags,editable:editable});}var videoCount=data.photos.reduce(function(count,photo){return count+(photo.type.indexOf("video")>-1?1:0);},0);structure.album={title:lychee.locale["ALBUM_ALBUM"],type:_sidebar.types.DEFAULT,rows:[{title:lychee.locale["ALBUM_CREATED"],kind:"created",value:lychee.locale.printDateTime(data.created_at)}]};if(data.albums&&data.albums.length>0){structure.album.rows.push({title:lychee.locale["ALBUM_SUBALBUMS"],kind:"subalbums",value:data.albums.length});}if(data.photos){if(data.photos.length-videoCount>0){structure.album.rows.push({title:lychee.locale["ALBUM_IMAGES"],kind:"images",value:data.photos.length-videoCount});}}if(videoCount>0){structure.album.rows.push({title:lychee.locale["ALBUM_VIDEOS"],kind:"videos",value:videoCount});}if(data.photos&&sorting!==""){structure.album.rows.push({title:lychee.locale["ALBUM_ORDERING"],kind:"sorting",value:sorting,editable:editable});}structure.share={title:lychee.locale["ALBUM_SHARING"],type:_sidebar.types.DEFAULT,rows:[{title:lychee.locale["ALBUM_PUBLIC"],kind:"public",value:isPublic},{title:lychee.locale["ALBUM_HIDDEN"],kind:"hidden",value:requiresLink},{title:lychee.locale["ALBUM_DOWNLOADABLE"],kind:"downloadable",value:isDownloadable},{title:lychee.locale["ALBUM_PASSWORD"],kind:"password",value:hasPassword}]};if(data.owner_name){structure.share.rows.push({title:lychee.locale["ALBUM_OWNER"],kind:"owner",value:data.owner_name});}structure.license={title:lychee.locale["ALBUM_REUSE"],type:_sidebar.types.DEFAULT,rows:[{title:lychee.locale["ALBUM_LICENSE"],kind:"license",value:license,editable:editable}]};// Construct all parts of the structure +var structure_ret=[structure.basics,structure.album];if(license){structure_ret.push(structure.license);}if(!lychee.publicMode){structure_ret.push(structure.share);}return structure_ret;};/** + * @param {Section[]} structure + * @returns {boolean} - true if the passed structure contains a "location" section + */_sidebar.has_location=function(structure){var _has_location=false;structure.forEach(function(section){if(section.title===lychee.locale["PHOTO_LOCATION"]){_has_location=true;}});return _has_location;};/** + * @param {Section[]} structure + * @returns {string} - HTML + */_sidebar.render=function(structure){/** + * @param {Section} section + * @returns {string} + */var renderDefault=function renderDefault(section){var _html=lychee.html(_templateObject50||(_templateObject50=_taggedTemplateLiteral(["\n\t\t\t\t \n\t\t\t\t \n\t\t\t\t "])),section.title);if(section.title===lychee.locale["PHOTO_LOCATION"]){var _has_latitude=section.rows.findIndex(function(row){return row.kind==="latitude"&&row.value;})!==-1;var _has_longitude=section.rows.findIndex(function(row){return row.kind==="longitude"&&row.value;})!==-1;var idxLocation=section.rows.findIndex(function(row){return row.kind==="location";});// Do not show location if not enabled +if(idxLocation!==-1&&(lychee.publicMode===true&&!lychee.location_show_public||!lychee.location_show)){section.rows.splice(idxLocation,1);}// Show map if we have coordinates +if(_has_latitude&&_has_longitude&&lychee.map_display){_html+="\n\t\t\t\t\t\t
\n\t\t\t\t\t\t ";}}section.rows.forEach(function(row){var rawValue=row.value;// don't show rows which are empty and cannot be edited +if((rawValue===""||rawValue==null)&&row.editable!==true){return;}/** @type {string} */var htmlValue;// Wrap span-element around value for easier selecting on change +if(Array.isArray(rawValue)){htmlValue=rawValue.reduce(/** + * @param {string} prev + * @param {string} cur + */function(prev,cur){// Add separator if needed +if(prev!==""){prev+=lychee.html(_templateObject51||(_templateObject51=_taggedTemplateLiteral([", "])),row.kind);}return prev+lychee.html(_templateObject52||(_templateObject52=_taggedTemplateLiteral(["$",""])),row.kind,cur);},"");}else{htmlValue=lychee.html(_templateObject53||(_templateObject53=_taggedTemplateLiteral(["$",""])),row.kind,rawValue);}// Add edit-icon to the value when editable +if(row.editable===true)htmlValue+=" "+build.editIcon("edit_"+row.kind);_html+=lychee.html(_templateObject54||(_templateObject54=_taggedTemplateLiteral([""])),row.title,htmlValue);});_html+="
$","","
";return _html;};/** + * @param {Section} section + * @returns {string} + */var renderTags=function renderTags(section){// TODO: IDE warns me that the `Section` has no properties `editable` nor `value`; cause of the problem is that the section `tags` is built differently, see above +// Add edit-icon to the value when editable +var htmlEditable=section.editable===true?build.editIcon("edit_tags"):"";// Note: In case of tags `section.value` already contains proper +// HTML (with each tag wrapped into a ``-element), because +// `section.value` is the result of `build.renderTags`. +return lychee.html(_templateObject55||(_templateObject55=_taggedTemplateLiteral(["\n\t\t\t\t \n\t\t\t\t
\n\t\t\t\t\t
","
\n\t\t\t\t\t ","\n\t\t\t\t
\n\t\t\t\t "])),section.title,section.title.toLowerCase(),section.value,htmlEditable);};var html="";structure.forEach(function(section){if(section.type===_sidebar.types.DEFAULT)html+=renderDefault(section);else if(section.type===_sidebar.types.TAGS)html+=renderTags(section);});return html;};/** + * Converts a decimal degree into integer degree, minutes and seconds. + * + * TODO: Consider to make this method part of `lychee.locale`. + * + * @param {number} decimal + * @param {boolean} type - indicates if the passed decimal indicates a + * latitude (`true`) or a longitude (`false`) + * @returns {string} + */function DecimalToDegreeMinutesSeconds(decimal,type){var d=Math.abs(decimal);var degrees=0;var minutes=0;var seconds=0;var direction;// absolute value of decimal must be smaller than 180; +if(d>180){return"";}// set direction; north assumed +if(type&&decimal<0){direction="S";}else if(!type&&decimal<0){direction="W";}else if(!type){direction="E";}else{direction="N";}//get degrees +degrees=Math.floor(d);//get seconds +seconds=(d-degrees)*3600;//get minutes +minutes=Math.floor(seconds/60);//reset seconds +seconds=Math.floor(seconds-minutes*60);return degrees+"° "+minutes+"' "+seconds+'" '+direction;}/** + * @description Swipes and moves an object. + */var swipe={/** @type {?jQuery} */obj:null,/** @type {number} */offsetX:0,/** @type {number} */offsetY:0,/** @type {boolean} */preventNextHeaderToggle:false};/** + * @param {jQuery} obj + * @returns {void} + */swipe.start=function(obj){swipe.obj=obj;};/** + * @param {jQuery.Event} e + * @returns {void} + */swipe.move=function(e){if(swipe.obj===null){return;}if(Math.abs(e.x)>Math.abs(e.y)){swipe.offsetX=-1*e.x;swipe.offsetY=0.0;}else{swipe.offsetX=0.0;swipe.offsetY=+1*e.y;}var value="translate("+swipe.offsetX+"px, "+swipe.offsetY+"px)";swipe.obj.css({WebkitTransform:value,MozTransform:value,transform:value});};/** + * @callback SwipeStoppedCB + * + * Find a better name for that, but I have no idea what this callback is + * supposed to do. + * + * @param {boolean} animate + * @returns {void} + */ /** + * @param {{x: number, y: number, direction: number, distance: number, angle: number, speed: number, }} e + * @param {SwipeStoppedCB} left + * @param {SwipeStoppedCB} right + * @returns {void} + */swipe.stop=function(e,left,right){// Only execute once +if(swipe.obj===null){return;}if(e.y<=-lychee.swipe_tolerance_y){lychee["goto"](album.getID());}else if(e.y>=lychee.swipe_tolerance_y){lychee["goto"](album.getID());}else if(e.x<=-lychee.swipe_tolerance_x){left(true);// 'touchend' will be called after 'swipeEnd' +// in case of moving to next image, we want to skip +// the toggling of the header +swipe.preventNextHeaderToggle=true;}else if(e.x>=lychee.swipe_tolerance_x){right(true);// 'touchend' will be called after 'swipeEnd' +// in case of moving to next image, we want to skip +// the toggling of the header +swipe.preventNextHeaderToggle=true;}else{var value="translate(0px, 0px)";swipe.obj.css({WebkitTransform:value,MozTransform:value,transform:value});}swipe.obj=null;swipe.offsetX=0;swipe.offsetY=0;};/** + * @description Helper class to manage tabindex + */var tabindex={offset_for_header:100,next_tab_index:100};/** + * @param {jQuery} elem + * @returns {void} + */tabindex.saveSettings=function(elem){if(!lychee.enable_tabindex)return;// Todo: Make shorter notation +// Get all elements which have a tabindex +// TODO @Hallenser: What did you intended by the TODO above? It seems as if the jQuery selector is already as short as possible? +var tmp=elem.find("[tabindex]");// iterate over all elements and set tabindex to stored value (i.e. make is not focusable) +tmp.each(/** + * @param {number} i - the index + * @param {Element} e - the HTML element + * @this {Element} - identical to `e` + */function(i,e){// TODO: shorter notation +// TODO @Hallenser: What do you intended by the TODO `short notation`? Moreover: Why do we use `this` and `e`? They refer to the identical instance of a HTML element. +var a=$(e).attr("tabindex");$(this).data("tabindex-saved",a);});};tabindex.restoreSettings=function(elem){if(!lychee.enable_tabindex)return;// Todo: Make shorter notation +// Get all elements which have a tabindex +// TODO @Hallenser: What did you intended by the TODO above? It seems as if the jQuery selector is already as short as possible? +var tmp=$(elem).find("[tabindex]");// iterate over all elements and set tabindex to stored value (i.e. make is not focussable) +tmp.each(/** + * @param {number} i - the index + * @param {Element} e - the HTML element + * @this {Element} - identical to `e` + */function(i,e){// TODO: shorter notation +// TODO @Hallenser: What do you intended by the TODO `short notation`? Moreover: Why do we use `this` and `e`? They refer to the identical instance of a HTML element. +var a=$(e).data("tabindex-saved");$(e).attr("tabindex",a);});};/** + * @param {jQuery} elem + * @param {boolean} [saveFocusElement=false] + * @returns {void} + */tabindex.makeUnfocusable=function(elem){var saveFocusElement=arguments.length>1&&arguments[1]!==undefined?arguments[1]:false;if(!lychee.enable_tabindex)return;// Todo: Make shorter notation +// Get all elements which have a tabindex +var tmp=elem.find("[tabindex]");// iterate over all elements and set tabindex to -1 (i.e. make is not focussable) +tmp.each(/** + * @param {number} i - the index + * @param {Element} e - the HTML element + */function(i,e){$(e).attr("tabindex","-1");// Save which element had focus before we make it unfocusable +if(saveFocusElement&&$(e).is(":focus")){$(e).data("tabindex-focus",true);// Remove focus +$(e).blur();}});// Disable input fields +elem.find("input").attr("disabled","disabled");};/** + * @param {jQuery} elem + * @param {boolean} [restoreFocusElement=false] + * @returns {void} + */tabindex.makeFocusable=function(elem){var restoreFocusElement=arguments.length>1&&arguments[1]!==undefined?arguments[1]:false;if(!lychee.enable_tabindex)return;// Todo: Make shorter notation +// Get all elements which have a tabindex +var tmp=elem.find("[data-tabindex]");// iterate over all elements and set tabindex to stored value +tmp.each(/** + * @param {number} i + * @param {Element} e + */function(i,e){$(e).attr("tabindex",$(e).data("tabindex"));// restore focus element if wanted +if(restoreFocusElement){if($(e).data("tabindex-focus")&&lychee.active_focus_on_page_load){$(e).focus();$(e).removeData("tabindex-focus");}}});// Enable input fields +elem.find("input").removeAttr("disabled");};/** + * @returns {number} + */tabindex.get_next_tab_index=function(){tabindex.next_tab_index=tabindex.next_tab_index+1;return tabindex.next_tab_index-1;};/** + * @returns {void} + */tabindex.reset=function(){tabindex.next_tab_index=tabindex.offset_for_header;};var u2f={/** @type {?WebAuthnCredential[]} */json:null};/** + * @returns {boolean} + */u2f.is_available=function(){if(!window.isSecureContext&&window.location.hostname!=="localhost"&&window.location.hostname!=="127.0.0.1"){basicModal.show({body:"

",readyCB:function readyCB(formElements,dialog){dialog.querySelector("p").textContent=lychee.locale["U2F_NOT_SECURE"];},buttons:{cancel:{title:lychee.locale["CLOSE"],fn:basicModal.close}}});return false;}return true;};/** + * @returns {void} + */u2f.login=function(){if(!u2f.is_available()){return;}new WebAuthn({login:"/api/WebAuthn::login",loginOptions:"/api/WebAuthn::login/options"},{},false).login({user_id:1// for now it is only available to Admin user via a secret key shortcut. +}).then(function(){loadingBar.show("success",lychee.locale["U2F_AUTHENTIFICATION_SUCCESS"]);window.location.reload();})["catch"](function(){return loadingBar.show("error",lychee.locale["ERROR_TEXT"]);});};/** + * @returns {void} + */u2f.register=function(){if(!u2f.is_available()){return;}var webauthn=new WebAuthn({register:"/api/WebAuthn::register",registerOptions:"/api/WebAuthn::register/options"},{},false);if(WebAuthn.supportsWebAuthn()){webauthn.register().then(function(){loadingBar.show("success",lychee.locale["U2F_REGISTRATION_SUCCESS"]);u2f.list();// reload credential list +})["catch"](function(){return loadingBar.show("error",lychee.locale["ERROR_TEXT"]);});}else{loadingBar.show("error",lychee.locale["U2F_NOT_SUPPORTED"]);}};/** + * @param {{id: string}} params - ID of WebAuthn credential + */u2f["delete"]=function(params){api.post("WebAuthn::delete",params,function(){loadingBar.show("success",lychee.locale["U2F_CREDENTIALS_DELETED"]);u2f.list();// reload credential list +});};u2f.list=function(){api.post("WebAuthn::list",{},/** @param {WebAuthnCredential[]} data*/function(data){u2f.json=data;view.u2f.init();});};/** + * @description Takes care of every action an album can handle and execute. + */ /** + * @typedef ProgressReportDialogRow + * @property {HTMLLIElement} listEntry + * @property {HTMLHeadingElement} header + * @property {HTMLParagraphElement} status + * @property {HTMLParagraphElement} notice + */var upload={SCROLL_OPTIONS:{inline:"nearest",block:"nearest",behavior:"smooth"},_dom:{/** + * Holds the ordered list (`
    `) with the individual reports + * of a Progress Report dialog. + * + * @type {HTMLOListElement|null} + */reportList:null,/** + * Maps a path (as the unique identifier) to a tuple of UI elements + * which visualize the report row for that path. + * + * Note, rows for event reports which are not associated to a + * particular file or directory are not kept in this map, but + * of course they are visualized inside the list of reports. + * + * This map allows fast access to the rows without running + * (inefficient) CSS selector queries and/or relying on a specific + * order (i.e. no need for `nth-child`-selector). + * + * @type {Map|null} + */progressRowsByPath:null}};upload.showProgressReportCloseButton=function(){basicModal.showActionButton();basicModal.hideCancelButton();// Re-activate cancel button to close modal panel if needed +basicModal.markActionButtonAsIdle();};upload.closeProgressReportDialog=function(){basicModal.close();upload._dom.reportList=null;upload._dom.progressRowsByPath=null;};/** + * Builds the HTML snippet for a single entry in the Progress Report dialog. + * + * Constructs an entry for the list of reports made up of a caption, + * a status and a notice. + * + * @param {string} caption the caption of the list entry; for reports about + * files this is typically the filename + * @returns {ProgressReportDialogRow} + */upload.buildReportRow=function(caption){var listEntry=document.createElement("li");var header=listEntry.appendChild(document.createElement("h2"));header.textContent=caption.length<=40?caption:caption.substring(0,19)+"…"+caption.substring(caption.length-20,caption.length);var status=listEntry.appendChild(document.createElement("p"));status.classList.add("status");var notice=listEntry.appendChild(document.createElement("p"));notice.classList.add("notice");return{listEntry:listEntry,header:header,status:status,notice:notice};};/** + * Builds the HTML snippet for the list of reports in the Progress Report dialog. + * + * The list is initially filled with the given list of files. + * More items to this list may be added on-the-fly during an ongoing import. + * + * Note: This is used for downloading files from a remote URL, importing from + * Dropbox or uploading, i.e. whenever the list is known in advance. + * For importing from server, the list initially only contains the selected + * server directory and more items are added while the backend scans the + * directory on the server. + * + * @param {(FileList|File[]|DropboxFile[]|{name: string}[])} files + * @returns {void} + */upload.buildReportList=function(files){upload._dom.reportList=document.createElement("ol");upload._dom.progressRowsByPath=new Map();for(var idx=0;idx!==files.length;idx++){var row=upload.buildReportRow(files[idx].name);upload._dom.progressRowsByPath.set(files[idx].name,row);upload._dom.reportList.appendChild(row.listEntry);}};/** + * @param {string} title + * @param {(FileList|File[]|DropboxFile[]|{name: string}[])} files + * @param {ModalDialogReadyCB} run_callback + * @param {?ModalDialogButtonCB} cancel_callback + */upload.showProgressReportDialog=function(title,files,run_callback){var cancel_callback=arguments.length>3&&arguments[3]!==undefined?arguments[3]:null;/** + * @param {ModalDialogFormElements} formElements + * @param {HTMLDivElement} dialog + * @returns {void} + */var initImportProgressReportDialog=function initImportProgressReportDialog(formElements,dialog){// Initially, the normal Action (aka "Close") button is hidden and +// remains hidden as long as an import is running. +// Users must use the Cancel button to interrupt an ongoing import. +// The Action button becomes visible after the import has been +// terminated (either successfully, with error or due to interruption). +basicModal.hideActionButton();var caption=dialog.querySelector("h1");caption.textContent=title;upload.buildReportList(files);dialog.appendChild(upload._dom.reportList);setTimeout(function(){return run_callback(formElements,dialog);},0);};basicModal.show({body:"

    ",classList:["import"],readyCB:initImportProgressReportDialog,buttons:{action:{title:lychee.locale["CLOSE"],fn:function fn(){return upload.closeProgressReportDialog();}},cancel:{title:lychee.locale["CANCEL"],classList:["red"],fn:function fn(resultData){// If Action button is visible, the Cancel button behaves +// like the Close button; otherwise the button only calls +// the callback to cancel the import +if(basicModal.isActionButtonVisible()){upload.closeProgressReportDialog();}else{if(cancel_callback){cancel_callback(resultData);}}}}}});};/** + * @param {string} title + * @param {string} [text=""] + * @returns {void} + */upload.notify=function(title){var text=arguments.length>1&&arguments[1]!==undefined?arguments[1]:"";if(text==="")text=lychee.locale["UPLOAD_MANAGE_NEW_PHOTOS"];if(!window.webkitNotifications)return;if(window.webkitNotifications.checkPermission()!==0)window.webkitNotifications.requestPermission();if(window.webkitNotifications.checkPermission()===0&&title){var popup=window.webkitNotifications.createNotification("",title,text);popup.show();}};upload.start={/** + * @param {(FileList|File[])} files + */local:function local(files){if(files.length<=0)return;var albumID=album.getID();var hasErrorOccurred=false;var hasWarningOccurred=false;/** + * The number of requests which are "on the fly", i.e. for which a + * response has not yet completely been received. + * + * Note, that Lychee supports a restricted kind of "parallelism" + * which is limited by the configuration option + * `lychee.upload_processing_limit`: + * While always only a single file is uploaded at once, upload of the + * next file already starts after transmission of the previous file + * has been finished, the response to the previous file might still be + * outstanding as the uploaded file is processed at the server-side. + * + * @type {number} + */var outstandingResponsesCount=0;/** + * The latest (aka highest) index of a file which is being or has + * been uploaded to the server. + * + * @type {number} + */var latestFileIdx=0;/** + * Semaphore whether a file is currently being uploaded. + * + * This is used as a semaphore to serialize the upload transmissions + * between several instances of the method {@link process}. + * + * @type {boolean} + */var isUploadRunning=false;/** + * Semaphore whether a further upload shall be cancelled on the next + * occasion. + * + * @type {boolean} + */var shallCancelUpload=false;/** + * This callback is invoked when the last file has been processed. + * + * It closes the modal dialog or shows the close button and + * reloads the album. + */var finish=function finish(){window.onbeforeunload=null;$("#upload_files").val("");if(!hasErrorOccurred&&!hasWarningOccurred){// Success +upload.closeProgressReportDialog();upload.notify(lychee.locale["UPLOAD_COMPLETE"]);}else if(!hasErrorOccurred&&hasWarningOccurred){// Warning +upload.showProgressReportCloseButton();upload.notify(lychee.locale["UPLOAD_COMPLETE"]);}else{// Error +upload.showProgressReportCloseButton();if(shallCancelUpload){var row=upload.buildReportRow(lychee.locale["UPLOAD_GENERAL"]);row.status.textContent=lychee.locale["UPLOAD_CANCELLED"];row.status.classList.add("warning");upload._dom.reportList.appendChild(row.listEntry);}upload.notify(lychee.locale["UPLOAD_COMPLETE"],lychee.locale["UPLOAD_COMPLETE_FAILED"]);}album.reload();};/** + * Processes the upload and response for a single file. + * + * Note that up to `lychee.upload_processing_limit` "instances" of + * this method can be "alive" simultaneously. + * The parameter `fileIdx` is limited by `latestFileIdx`. + * + * @param {number} fileIdx the index of the file being processed + */var process=function process(fileIdx){/** + * The upload progress of the file with index `fileIdx` so far. + * + * @type {number} + */var uploadProgress=0;/** + * A function to be called when the upload has transmitted more data. + * + * This method updates the upload percentage counter in the dialog. + * + * If the progress equals 100%, i.e. if the upload has been + * completed, this method + * + * - unsets the semaphore for a running upload, + * - scrolls the dialog such that the file with index `fileIdx` + * becomes visible, and + * - changes the status text to "Upload processing". + * + * After the current upload has reached 100%, this method starts a + * new upload, if + * + * - there are more files to be uploaded, + * - no other upload is currently running, and + * - the number of outstanding responses does not exceed the + * processing limit of Lychee. + * + * @param {ProgressEvent} e + * @this XMLHttpRequest + */var onUploadProgress=function onUploadProgress(e){if(e.lengthComputable!==true)return;// Calculate progress +var progress=e.loaded/e.total*100|0;// Set progress when progress has changed +if(progress>uploadProgress){uploadProgress=progress;var row=upload._dom.progressRowsByPath.get(files[fileIdx].name);row.listEntry.scrollIntoView(upload.SCROLL_OPTIONS);row.status.textContent=""+uploadProgress+"%";if(progress>=100){row.status.textContent=lychee.locale["UPLOAD_PROCESSING"];isUploadRunning=false;// Start a new upload, if there are still pending +// files +if(!isUploadRunning&&!shallCancelUpload&&(outstandingResponsesCount=400?this.response:null;switch(this.status){case 200:case 201:case 204:row.status.textContent=lychee.locale["UPLOAD_FINISHED"];row.status.classList.add("success");break;case 409:row.status.textContent=lychee.locale["UPLOAD_SKIPPED"];row.status.classList.add("warning");row.notice.textContent=lycheeException?lycheeException.message:lychee.locale["UPLOAD_ERROR_UNKNOWN"];hasWarningOccurred=true;break;case 413:row.status.textContent=lychee.locale["UPLOAD_FAILED"];row.status.classList.add("error");row.notice.textContent=lychee.locale["UPLOAD_ERROR_POSTSIZE"];hasErrorOccurred=true;api.onError(this,{albumID:albumID},lycheeException);break;default:row.status.textContent=lychee.locale["UPLOAD_FAILED"];row.status.classList.add("error");row.notice.textContent=lycheeException?lycheeException.message:lychee.locale["UPLOAD_ERROR_UNKNOWN"];hasErrorOccurred=true;api.onError(this,{albumID:albumID},lycheeException);break;}};/** + * A function to be called when any response has been received + * (after specific success and error callbacks have been executed) + * + * This method starts a new upload, if + * + * - there are more files to be uploaded, + * - no other upload is currently running, and + * - the number of outstanding responses does not exceed the + * processing limit of Lychee. + * + * This method calls {@link finish}, if + * + * - the process shall be cancelled or no more files are left for processing, + * - no upload is running anymore, and + * - no response is outstanding + * + * @this XMLHttpRequest + */var onComplete=function onComplete(){outstandingResponsesCount--;if(!isUploadRunning&&!shallCancelUpload&&(outstandingResponsesCount0&&arguments[0]!==undefined?arguments[0]:"";var albumID=album.getID();/** @param {{url: string}} data */var importFromUrl=function importFromUrl(data){var runImport=function runImport(){var successHandler=function successHandler(){// Same code as in import.dropbox() +upload.closeProgressReportDialog();upload.notify(lychee.locale["UPLOAD_IMPORT_COMPLETE"]);album.reload();};/** + * @param {XMLHttpRequest} jqXHR + * @param {Object} params + * @param {?LycheeException} lycheeException + * @returns {boolean} + */var errorHandler=function errorHandler(jqXHR,params,lycheeException){// Same code as in import.dropbox() +/** @type {ProgressReportDialogRow} */var row=upload._dom.progressRowsByPath.get(data.url);switch(jqXHR.status){case 409:row.status.textContent=lychee.locale["UPLOAD_SKIPPED"];row.status.classList.add("warning");row.notice.textContent=lycheeException?lycheeException.message:lychee.locale["UPLOAD_IMPORT_WARN_ERR"];break;default:row.status.textContent=lychee.locale["UPLOAD_FAILED"];row.status.classList.add("error");row.notice.textContent=lycheeException?lycheeException.message:lychee.locale["UPLOAD_IMPORT_WARN_ERR"];break;}// Show close button +basicModal.showActionButton();upload.notify(lychee.locale["UPLOAD_IMPORT_WARN_ERR"]);album.reload();return true;};upload._dom.progressRowsByPath.get(data.url).status.textContent=lychee.locale["UPLOAD_IMPORTING"];// In theory, the backend is prepared to download a list of +// URLs (note that `data.url`) is wrapped into an array. +// However, we need a better dialog which allows input of a +// list of URLs. +// Another problem which already exists even for a single +// URL concerns timeouts. +// Below, we transmit a single HTTP request which must respond +// within about 500ms either with a success or error response. +// Otherwise, JS assumes that the AJAX request just timed out. +// But the server, first need to download the image from the +// specified URL, process it and then generate a HTTP response. +// Probably, it would be much better to use a streamed +// response here like we already have for imports from the +// local server. +// This way, the server could also report its own progress of +// downloading the images. +// TODO: Use a streamed response (see description above). +api.post("Import::url",{urls:[data.url],albumID:albumID},successHandler,null,errorHandler);};upload.showProgressReportDialog(lychee.locale["UPLOAD_IMPORTING_URL"],[{name:data.url}],runImport);};/** @param {{url: string}} data */var processImportFromUrlDialog=function processImportFromUrlDialog(data){if(data.url&&data.url.trim().length>3){basicModal.close(false,function(){return importFromUrl(data);});}else basicModal.focusError("url");};var importFromUrlDialogBody="\n\t\t\t

    \n\t\t\t
    \n\t\t\t\t
    \n\t\t\t
    ";/** + * @param {ModalDialogFormElements} formElements + * @param {HTMLDivElement} dialog + * @returns {void} + */var initImportFromUrlDialog=function initImportFromUrlDialog(formElements,dialog){dialog.querySelector("p").textContent=lychee.locale["UPLOAD_IMPORT_INSTR"];formElements.url.placeholder="https://";formElements.url.value=preselectedUrl;};basicModal.show({body:importFromUrlDialogBody,readyCB:initImportFromUrlDialog,buttons:{action:{title:lychee.locale["UPLOAD_IMPORT"],fn:processImportFromUrlDialog},cancel:{title:lychee.locale["CANCEL"],fn:basicModal.close}}});},server:function server(){var albumID=album.getID();/** + * @typedef ImportFromServerDialogFormElements + * + * @property {HTMLInputElement} paths + * @property {HTMLInputElement} delete_imported + * @property {HTMLInputElement} import_via_symlink + * @property {HTMLInputElement} skip_duplicates + * @property {HTMLInputElement} resync_metadata + */ /** + * @param {ImportFromServerDialogFormElements} formElements + * @param {HTMLDivElement} dialog + * @returns {void} + */var initImportFromServerDialog=function initImportFromServerDialog(formElements,dialog){dialog.querySelector("p").textContent=lychee.locale["UPLOAD_IMPORT_SERVER_INSTR"];formElements.paths.placeholder=lychee.locale["UPLOAD_ABSOLUTE_PATH"];formElements.paths.value=lychee.location+"uploads/import/";formElements.delete_imported.previousElementSibling.textContent=lychee.locale["UPLOAD_IMPORT_DELETE_ORIGINALS"];formElements.delete_imported.nextElementSibling.textContent=lychee.locale["UPLOAD_IMPORT_DELETE_ORIGINALS_EXPL"];formElements.import_via_symlink.previousElementSibling.textContent=lychee.locale["UPLOAD_IMPORT_VIA_SYMLINK"];formElements.import_via_symlink.nextElementSibling.textContent=lychee.locale["UPLOAD_IMPORT_VIA_SYMLINK_EXPL"];formElements.skip_duplicates.previousElementSibling.textContent=lychee.locale["UPLOAD_IMPORT_SKIP_DUPLICATES"];formElements.skip_duplicates.nextElementSibling.textContent=lychee.locale["UPLOAD_IMPORT_SKIP_DUPLICATES_EXPL"];formElements.resync_metadata.previousElementSibling.textContent=lychee.locale["UPLOAD_IMPORT_RESYNC_METADATA"];formElements.resync_metadata.nextElementSibling.textContent=lychee.locale["UPLOAD_IMPORT_RESYNC_METADATA_EXPL"];// Initialize form elements (and dependent form elements) based on +// global configuration settings. +if(lychee.delete_imported){formElements.delete_imported.checked=true;formElements.import_via_symlink.checked=false;formElements.import_via_symlink.disabled=true;formElements.import_via_symlink.parentElement.classList.add("disabled");}else{if(lychee.import_via_symlink){formElements.delete_imported.checked=false;formElements.delete_imported.disabled=true;formElements.delete_imported.parentElement.classList.add("disabled");formElements.import_via_symlink.checked=true;}}if(lychee.skip_duplicates){formElements.skip_duplicates.checked=true;formElements.resync_metadata.checked=lychee.resync_metadata;}else{formElements.skip_duplicates.checked=false;formElements.resync_metadata.checked=false;formElements.resync_metadata.disabled=true;formElements.resync_metadata.parentElement.classList.add("disabled");}// Checkbox action handler to visualize contradictory settings +formElements.delete_imported.addEventListener("change",function(){if(formElements.delete_imported.checked){formElements.import_via_symlink.checked=false;formElements.import_via_symlink.disabled=true;formElements.import_via_symlink.parentElement.classList.add("disabled");}else{formElements.import_via_symlink.disabled=false;formElements.import_via_symlink.parentElement.classList.remove("disabled");}});formElements.import_via_symlink.addEventListener("change",function(){if(formElements.import_via_symlink.checked){formElements.delete_imported.checked=false;formElements.delete_imported.disabled=true;formElements.delete_imported.parentElement.classList.add("disabled");}else{formElements.delete_imported.disabled=false;formElements.delete_imported.parentElement.classList.remove("disabled");}});formElements.skip_duplicates.addEventListener("change",function(){if(formElements.skip_duplicates.checked){formElements.resync_metadata.disabled=false;formElements.resync_metadata.parentElement.classList.remove("disabled");}else{formElements.resync_metadata.checked=false;formElements.resync_metadata.disabled=true;formElements.resync_metadata.parentElement.classList.add("disabled");}});};/** + * @typedef ServerImportDialogResult + * @property {string|string[]} paths + * @property {boolean} delete_imported + * @property {boolean} import_via_symlink + * @property {boolean} skip_duplicates + * @property {boolean} resync_metadata + */ /** @param {ServerImportDialogResult} data */var importFromServer=function importFromServer(data){var isUploadCancelled=false;var cancelUpload=function cancelUpload(){if(!isUploadCancelled){api.post("Import::serverCancel",{},function(){isUploadCancelled=true;});}};var runUpload=function runUpload(){basicModal.showCancelButton();// Variables holding state across the invocations of +// processIncremental(). +var lastReadIdx=0;var encounteredProblems=false;/** + * Worker function invoked from both the response progress + * callback and the completion callback. + * + * @param {(ImportProgressReport|ImportEventReport)[]} reports + */var processIncremental=function processIncremental(reports){reports.slice(lastReadIdx).forEach(function(report){if(report.type==="progress"){// Gets existing row for the current path or creates a new one +/** @type {ProgressReportDialogRow} */var row=upload._dom.progressRowsByPath.get(report.path)||upload.buildReportRow(report.path);upload._dom.progressRowsByPath.set(report.path,row);// Always unconditionally append the list entry to +// the end of the list even if the `reportList` +// already contains `listEntry`. +// 1. If `listEntry` is not yet an element of +// `reportList` (e.g. this happens for +// new directories), then appending the +// element does the obvious thing +// 2. If `listEntry` is already an element +// of `reportList` (e.g. this happens for +// follow-up reports), then `appendChild` +// *moves* `listEntry` the end of the list. +// We don't need to take care of accidentally +// duplicating the entry, the DOM tree is +// clever enough. +// Moving `listEntry` is an intended effect, +// as we always want the most recent entry at +// the end of the list. +upload._dom.reportList.appendChild(row.listEntry);row.listEntry.scrollIntoView(upload.SCROLL_OPTIONS);if(report.progress!==100){row.status.textContent=""+report.progress+"%";}else{// Final status report for this directory. +row.status.textContent=lychee.locale["UPLOAD_FINISHED"];row.status.classList.add("success");}}else if(report.type==="event"){var _row;if(!!report.path){// The event report refers to a specific path, +// hence get the existing row for that path +// or create a new one. +/** @type {ProgressReportDialogRow} */_row=upload._dom.progressRowsByPath.get(report.path)||upload.buildReportRow(report.path);upload._dom.progressRowsByPath.set(report.path,_row);// Always unconditionally append the list entry to +// the end of the list even if the `reportList` +// already contains `listEntry`. +// 1. If `listEntry` is not yet an element of +// `reportList` (e.g. this happens for +// new directories), then appending the +// element does the obvious thing +// 2. If `listEntry` is already an element +// of `reportList` (e.g. this happens for +// follow-up reports), then `appendChild` +// *moves* `listEntry` the end of the list. +// We don't need to take care of accidentally +// duplicating the entry, the DOM tree is +// clever enough. +// Moving `listEntry` is an intended effect, +// as we always want the most recent entry at +// the end of the list. +upload._dom.reportList.appendChild(_row.listEntry);}else{// The event report does not refer to a +// specific directory. +_row=upload.buildReportRow(lychee.locale["UPLOAD_GENERAL"]);upload._dom.reportList.appendChild(_row.listEntry);}_row.listEntry.scrollIntoView(upload.SCROLL_OPTIONS);var severityClass="";var statusText="";var noteText="";switch(report.severity){case"debug":case"info":break;case"notice":case"warning":severityClass="warning";break;case"error":case"critical":case"emergency":severityClass="error";break;}switch(report.subtype){case"mem_limit":statusText=lychee.locale["UPLOAD_WARNING"];noteText=lychee.locale["UPLOAD_IMPORT_LOW_MEMORY_EXPL"];break;case"FileOperationException":case"MediaFileOperationException":statusText=lychee.locale["UPLOAD_SKIPPED"];noteText=lychee.locale["UPLOAD_IMPORT_FAILED"];break;case"MediaFileUnsupportedException":statusText=lychee.locale["UPLOAD_SKIPPED"];noteText=lychee.locale["UPLOAD_IMPORT_UNSUPPORTED"];break;case"InvalidDirectoryException":statusText=lychee.locale["UPLOAD_FAILED"];noteText=lychee.locale["UPLOAD_IMPORT_NOT_A_DIRECTORY"];break;case"ReservedDirectoryException":statusText=lychee.locale["UPLOAD_FAILED"];noteText=lychee.locale["UPLOAD_IMPORT_PATH_RESERVED"];break;case"PhotoSkippedException":statusText=lychee.locale["UPLOAD_SKIPPED"];noteText=lychee.locale["UPLOAD_IMPORT_SKIPPED_DUPLICATE"];break;case"PhotoResyncedException":statusText=lychee.locale["UPLOAD_UPDATED"];noteText=lychee.locale["UPLOAD_IMPORT_RESYNCED_DUPLICATE"];break;case"ImportCancelledException":statusText=lychee.locale["UPLOAD_CANCELLED"];noteText=lychee.locale["UPLOAD_IMPORT_CANCELLED"];break;default:statusText=lychee.locale["UPLOAD_SKIPPED"];noteText=report.message;break;}_row.notice.textContent=noteText;_row.status.textContent=statusText;_row.status.classList.add(severityClass);encounteredProblems=true;}});// forEach (resp) +lastReadIdx=reports.length;};// processIncremental +/** + * @param {ImportReport[]} reports + */var successHandler=function successHandler(reports){// reports is already JSON-parsed. +processIncremental(reports);upload.notify(lychee.locale["UPLOAD_IMPORT_COMPLETE"],encounteredProblems?lychee.locale["UPLOAD_COMPLETE_FAILED"]:null);album.reload();if(encounteredProblems)upload.showProgressReportCloseButton();else upload.closeProgressReportDialog();};/** + * @this {XMLHttpRequest} + */var progressHandler=function progressHandler(){/** @type {string} */var response=this.response;/** @type {ImportReport[]} */var reports=[];// We received a possibly partial response. +// We must ensure that the last object in the +// array is complete and terminate the array. +while(response.length>2&&reports.length===0){// Search the last '}', assume that this terminates +// the last JSON object, cut the string and terminate +// the array with `]`. +var fixedResponse=response.substring(0,response.lastIndexOf("}")+1)+"]";try{// If the assumption is wrong and the last found +// '}' does not terminate the last object, then +// `JSON.parse` will fail and tell us where the +// problem occurred. +reports=JSON.parse(fixedResponse);}catch(e){if(e instanceof SyntaxError){var errorPos=e.columnNumber;var lastBrace=response.lastIndexOf("}");var cutResponse=errorPos\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t

\n\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t

\n\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t

\n\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t

\n\t\t\t\t
\n\t\t\t";basicModal.show({body:importFromServerDialogBody,readyCB:initImportFromServerDialog,buttons:{action:{title:lychee.locale["UPLOAD_IMPORT"],fn:processImportFromServerDialog},cancel:{title:lychee.locale["CANCEL"],fn:basicModal.close}}});},dropbox:function dropbox(){var albumID=album.getID();/** + * @param {DropboxFile[]} files + */var action=function action(files){var runImport=function runImport(){var successHandler=function successHandler(){// Same code as in import.url() +upload.closeProgressReportDialog();upload.notify(lychee.locale["UPLOAD_IMPORT_COMPLETE"]);album.reload();};/** + * @param {XMLHttpRequest} jqXHR + * @param {Object} params + * @param {?LycheeException} lycheeException + * @returns {boolean} + */var errorHandler=function errorHandler(jqXHR,params,lycheeException){// Same code as in import.url() +// Note, this is complete rubbish: +// Dropbox allows to import several photos at once, but +// here we assume that `files` has only a single entry. +// This seems to be a long-standing, open bug +/** @type {ProgressReportDialogRow} */var row=upload._dom.progressRowsByPath.get(files[0].link);switch(jqXHR.status){case 409:row.status.textContent=lychee.locale["UPLOAD_SKIPPED"];row.status.classList.add("warning");row.notice.textContent=lycheeException?lycheeException.message:lychee.locale["UPLOAD_IMPORT_WARN_ERR"];break;default:row.status.textContent=lychee.locale["UPLOAD_FAILED"];row.status.classList.add("error");row.notice.textContent=lycheeException?lycheeException.message:lychee.locale["UPLOAD_IMPORT_WARN_ERR"];break;}// Show close button +basicModal.showActionButton();upload.notify(lychee.locale["UPLOAD_IMPORT_WARN_ERR"]);album.reload();return true;};upload._dom.progressRowsByPath.get(files[0].link).status.textContent=lychee.locale["UPLOAD_IMPORTING"];// TODO: Use a streamed response; see long comment in `import.url()` for the reasons +api.post("Import::url",{urls:files.map(function(file){return file.link;}),albumID:albumID},successHandler,null,errorHandler);};files.forEach(function(file){return file.name=file.link;});upload.showProgressReportDialog("Importing from Dropbox",files,runImport);};lychee.loadDropbox(function(){Dropbox.choose({linkType:"direct",multiselect:true,success:action});});}};/** + * @param {(FileList|File[])} files + * + * @returns {void} + */upload.uploadTrack=function(files){var albumID=album.getID();if(files.length<=0||albumID===null)return;var runUpload=function runUpload(){// Only a single track can be uploaded at once, hence the only +// file is at position 0. +var row=upload._dom.progressRowsByPath.get(files[0].name);/** + * A function to be called when a response has been received. + * + * It closes the modal dialog or shows the close button and + * reloads the album. + * + * @this XMLHttpRequest + */var finish=function finish(){/** @type {?LycheeException} */var lycheeException=this.status>=400?this.response:null;var errorText="";var statusText;var statusClass;$("#upload_track_file").val("");switch(this.status){case 200:case 201:case 204:statusText=lychee.locale["UPLOAD_FINISHED"];statusClass="success";break;case 413:statusText=lychee.locale["UPLOAD_FAILED"];errorText=lychee.locale["UPLOAD_ERROR_POSTSIZE"];statusClass="error";break;default:statusText=lychee.locale["UPLOAD_FAILED"];errorText=lycheeException?lycheeException.message:lychee.locale["UPLOAD_ERROR_UNKNOWN"];statusClass="error";break;}row.status.textContent=statusText;if(errorText!==""){row.notice.textContent=errorText;api.onError(this,{albumID:albumID},lycheeException);upload.showProgressReportCloseButton();upload.notify(lychee.locale["UPLOAD_COMPLETE"],lychee.locale["UPLOAD_COMPLETE_FAILED"]);}else{upload.closeProgressReportDialog();upload.notify(lychee.locale["UPLOAD_COMPLETE"]);}album.reload();};// finish +row.status.textContent=lychee.locale["UPLOAD_UPLOADING"];var formData=new FormData();var xhr=new XMLHttpRequest();formData.append("albumID",albumID);formData.append("file",files[0]);xhr.onload=finish;xhr.responseType="json";xhr.open("POST","api/Album::setTrack");xhr.setRequestHeader("X-XSRF-TOKEN",csrf.getCSRFCookieValue());xhr.setRequestHeader("Accept","application/json");xhr.send(formData);};// runUpload +upload.showProgressReportDialog(lychee.locale["UPLOAD_UPLOADING"],files,runUpload);};var users={/** @type {?UserDTO[]} */json:null};/** + * Updates a user account. + * + * The object `params` must be kept in sync with the HTML form constructed + * by {@link build.user}. + * + * @param {{id: number, username: string, password: string, may_upload: boolean, may_edit_own_settings: boolean}} params + * @returns {void} + */users.update=function(params){if(params.username.length<1){loadingBar.show("error",lychee.locale["ERROR_EMPTY_USERNAME"]);return;}// If the password is empty, then the password shall not be changed. +// In this case, the password must not be an attribute of the object at +// all. +// An existing, but empty password, would indicate to clear the password. +if(params.password.length===0){delete params.password;}api.post("Users::save",params,function(){loadingBar.show("success",lychee.locale["USER_UPDATED"]);users.list();// reload user list +});};/** + * Creates a new user account. + * + * The object `params` must be kept in sync with the HTML form constructed + * by {@link view.users.content}. + * + * @param {{id: string, username: string, password: string, may_upload: boolean, may_edit_own_settings: boolean}} params + * @returns {void} + */users.create=function(params){if(params.username.length<1){loadingBar.show("error",lychee.locale["ERROR_EMPTY_USERNAME"]);return;}if(params.password.length<1){loadingBar.show("error",lychee.locale["ERROR_EMPTY_PASSWORD"]);return;}api.post("Users::create",params,function(){loadingBar.show("success",lychee.locale["USER_CREATED"]);users.list();// reload user list +});};/** + * Deletes a user account. + * + * The object `params` must be kept in sync with the HTML form constructed + * by {@link build.user}. + * + * @param {{id: number}} params + * @returns {boolean} + */users["delete"]=function(params){api.post("Users::delete",params,function(){loadingBar.show("success",lychee.locale["USER_DELETED"]);users.list();// reload user list +});};/** + * @returns {void} + */users.list=function(){api.post("Users::list",{},/** @param {UserDTO[]} data */function(data){users.json=data;view.users.init();});};/** + * @description Responsible to reflect data changes to the UI. + */var view={/** @type {ResizeObserver} */resizeObserver:function(){/** + * @type {HTMLDivElement} + */var viewContainer=document.getElementById("lychee_view_container");var resizeHandler=function(){var viewContainerWidth=0;return function(){// Avoid infinite loops +// The layout method `view.album.content.justify()` below will +// change the height of the container as the height depends on +// the amount of content. +// Hence, `view.album.content.justify()` re-triggers the +// event handler. +// However, we are only interested into changes of the width, +// which is independent of content but solely depends on +// window size. +// We bail out early if the width has not changed since last +// time. +if(viewContainer.clientWidth===viewContainerWidth)return;viewContainerWidth=viewContainer.clientWidth;view.album.content.justify();if(_photo3.isLivePhotoInitialized()){_photo3.livePhotosObject.updateSize();}};}();var observer=new ResizeObserver(resizeHandler);observer.observe(viewContainer);return observer;}()};view.albums={/** @returns {void} */init:function init(){multiselect.clearSelection();view.albums.title();view.albums.content.init();},/** @returns {void} */title:function title(){if(lychee.landing_page_enable){lychee.setMetaData();}else{lychee.setMetaData(lychee.locale["ALBUMS"]);}},content:{/** @returns {void} */init:function init(){var smartData="";var tagAlbumsData="";var albumsData="";var sharedData="";// Smart Albums +if(lychee.publicMode===false&&(albums.json.smart_albums["public"]||albums.json.smart_albums.recent||albums.json.smart_albums.starred||albums.json.smart_albums.unsorted||albums.json.smart_albums.on_this_day||albums.json.tag_albums.length>0)){smartData=build.divider(lychee.locale["SMART_ALBUMS"]);}if(albums.json.smart_albums.unsorted){albums.parse(albums.json.smart_albums.unsorted);smartData+=build.album(albums.json.smart_albums.unsorted,!lychee.rights.root_album.can_edit);}if(albums.json.smart_albums["public"]){albums.parse(albums.json.smart_albums["public"]);smartData+=build.album(albums.json.smart_albums["public"],!lychee.rights.root_album.can_edit);}if(albums.json.smart_albums.starred){albums.parse(albums.json.smart_albums.starred);smartData+=build.album(albums.json.smart_albums.starred,!lychee.rights.root_album.can_edit);}if(albums.json.smart_albums.recent){albums.parse(albums.json.smart_albums.recent);smartData+=build.album(albums.json.smart_albums.recent,!lychee.rights.root_album.can_edit);}if(albums.json.smart_albums.on_this_day){albums.parse(albums.json.smart_albums.on_this_day);smartData+=build.album(albums.json.smart_albums.on_this_day);}// Tag albums +tagAlbumsData+=albums.json.tag_albums.reduce(function(html,tagAlbum){albums.parse(tagAlbum);return html+build.album(tagAlbum,!lychee.rights.root_album.can_edit);},"");// Albums +if(lychee.publicMode===false&&albums.json.albums.length>0)albumsData=build.divider(lychee.locale["ALBUMS"]);albumsData+=albums.json.albums.reduce(function(html,album){albums.parse(album);return html+build.album(album,!lychee.rights.root_album.can_edit);},"");var current_owner="";// Shared +sharedData+=albums.json.shared_albums.reduce(function(html,album){albums.parse(album);if(current_owner!==album.owner_name&&lychee.publicMode===false){html+=build.divider(album.owner_name);current_owner=album.owner_name;}return html+build.album(album,!lychee.rights.settings.can_edit);},"");if(smartData===""&&tagAlbumsData===""&&albumsData===""&&sharedData===""){lychee.content.html("");lychee.content.append(build.no_content("eye"));}else{lychee.content.html(smartData+tagAlbumsData+albumsData+sharedData);}album.apply_nsfw_filter();view.album.content.restoreScroll();},/** + * @param {string} albumID + * @returns {void} + */title:function title(albumID){var album=albums.getByID(albumID);var title=album.title?album.title:lychee.locale["UNTITLED"];$('.album[data-id="'+albumID+'"] .overlay h1').text(title).attr("title",title);},/** + * @param {string} albumID + * @returns {void} + */"delete":function _delete(albumID){$('.album[data-id="'+albumID+'"]').css("opacity",0).animate({width:0,marginLeft:0},300,function(){$(this).remove();if(albums.json.albums.length<=0)lychee.content.find(".divider:last-child").remove();});}}};view.album={/** @returns {void} */init:function init(){multiselect.clearSelection();view.album.sidebar();view.album.title();view.album["public"]();view.album.nsfw();view.album.nsfw_warning.init();view.album.content.init();// TODO: `init` is not a property of the Album JSON; this is a property of the view. Consider to move it to `view.album.isInitialized` +album.json.init=true;},/** @returns {void} */title:function title(){if((visible.album()||!album.json.init)&&!visible.photo()){switch(album.getID()){case SmartAlbumID.STARRED:lychee.setMetaData(lychee.locale["STARRED"]);break;case SmartAlbumID.PUBLIC:lychee.setMetaData(lychee.locale["PUBLIC"]);break;case SmartAlbumID.RECENT:lychee.setMetaData(lychee.locale["RECENT"]);break;case SmartAlbumID.UNSORTED:lychee.setMetaData(lychee.locale["UNSORTED"]);break;case SmartAlbumID.ON_THIS_DAY:lychee.setMetaData(lychee.locale["ON_THIS_DAY"]);break;default:if(album.json.init)_sidebar.changeAttr("title",album.json.title);lychee.setMetaData(album.json.title,true,album.json.description);break;}}},nsfw_warning:{/** @returns {void} */init:function init(){if(!lychee.nsfw_warning){$("#sensitive_warning").removeClass("active");return;}if(album.json.policy.is_nsfw&&!lychee.nsfw_unlocked_albums.includes(album.json.id)){$("#sensitive_warning").addClass("active");}else{$("#sensitive_warning").removeClass("active");}},/** @returns {void} */next:function next(){lychee.nsfw_unlocked_albums.push(album.json.id);$("#sensitive_warning").removeClass("active");}},content:{/** @returns {void} */init:function init(){var photosData="";var albumsData="";var html="";if(album.json.albums){album.json.albums.forEach(function(_album){albums.parse(_album);albumsData+=build.album(_album,!album.json.rights.can_edit);});}if(album.json.photos){// Build photos +album.json.photos.forEach(function(_photo){photosData+=build.photo(_photo,!album.json.rights.can_edit);});}if(photosData!==""){if(lychee.layout==="justified"){// The CSS class 'laying-out' prevents the DIV from being +// rendered. +// The CSS class will eventually be removed by the +// layout routine `view.album.content.justify` after all +// child nodes have been arranged. +// ---- Update 2022-10-20, temporary fix ---- +// However, the reported width of hidden elements is zero. +// Hence, using the CSS class `laying-out` currently +// prevent `view.album.content.justify` from calculating +// the correct width of the container. +// TODO: Re-add the CSS class `laying-out` here after https://github.com/LycheeOrg/Lychee-front/pull/335 has been merged. +photosData='
'+photosData+"
";}else if(lychee.layout==="unjustified"){photosData='
'+photosData+"
";}}if(albumsData!==""&&photosData!==""){html=build.divider(lychee.locale["ALBUMS"]);}html+=albumsData;if(albumsData!==""&&photosData!==""){html+=build.divider(lychee.locale["PHOTOS"]);}html+=photosData;// Add photos to view +lychee.content.html(html);album.apply_nsfw_filter();setTimeout(function(){view.album.content.justify();},0);},/** @returns {void} */restoreScroll:function restoreScroll(){// Restore scroll position +var urls=JSON.parse(sessionStorage.getItem("scroll"));var urlWindow=window.location.href;$("#lychee_view_container").scrollTop(urls!=null&&urls[urlWindow]?urls[urlWindow]:0);},/** + * @param {string} photoID + * @returns {void} + */title:function title(photoID){var photo=album.getByID(photoID);var title=photo.title?photo.title:lychee.locale["UNTITLED"];$('.photo[data-id="'+photoID+'"] .overlay h1').text(title).attr("title",title);},/** + * @param {string} albumID + * @returns {void} + */titleSub:function titleSub(albumID){var subalbum=album.getSubByID(albumID);var title=subalbum.title?subalbum.title:lychee.locale["UNTITLED"];$('.album[data-id="'+albumID+'"] .overlay h1').text(title).attr("title",title);},/** + * @param {string} photoID + * @returns {void} + */star:function star(photoID){var $badge=$('.photo[data-id="'+photoID+'"] .icn-star');if(album.getByID(photoID).is_starred)$badge.addClass("badge--star");else $badge.removeClass("badge--star");},/** + * @param {string} photoID + * @returns {void} + */"public":function _public(photoID){var $badge=$('.photo[data-id="'+photoID+'"] .icn-share');if(album.getByID(photoID).is_public===1)$badge.addClass("badge--visible badge--hidden");else $badge.removeClass("badge--visible badge--hidden");},/** + * @param {string} photoID + * @returns {void} + */cover:function cover(photoID){$(".album .icn-cover").removeClass("badge--cover");$(".photo .icn-cover").removeClass("badge--cover");if(album.json.cover_id===photoID){var badge=$('.photo[data-id="'+photoID+'"] .icn-cover');if(badge.length>0){badge.addClass("badge--cover");}else{$.each(album.json.albums,function(){if(this.thumb.id===photoID){$('.album[data-id="'+this.id+'"] .icn-cover').addClass("badge--cover");return false;}});}}},/** + * @param {Photo} data + * @returns {void} + */updatePhoto:function updatePhoto(data){var src,srcset="";// This mimicks the structure of build.photo +if(lychee.layout==="square"){src=data.size_variants.thumb.url;if(data.size_variants.thumb2x!==null){srcset="".concat(data.size_variants.thumb2x.url," 2x");}}else{if(data.size_variants.small!==null){src=data.size_variants.small.url;if(data.size_variants.small2x!==null){srcset="".concat(data.size_variants.small.url," ").concat(data.size_variants.small.width,"w, ").concat(data.size_variants.small2x.url," ").concat(data.size_variants.small2x.width,"w");}}else if(data.size_variants.medium!==null){src=data.size_variants.medium.url;if(data.size_variants.medium2x!==null){srcset="".concat(data.size_variants.medium.url," ").concat(data.size_variants.medium.width,"w, ").concat(data.size_variants.medium2x.url," ").concat(data.size_variants.medium2x.width,"w");}}else if(!data.type||data.type.indexOf("video")!==0){src=data.size_variants.original.url;}else{src=data.size_variants.thumb.url;if(data.size_variants.thumb2x!==null){srcset="".concat(data.size_variants.thumb.url," ").concat(data.size_variants.thumb.width,"w, ").concat(data.size_variants.thumb2x.url," ").concat(data.size_variants.thumb2x.width,"w");}}}$('.photo[data-id="'+data.id+'"] > span.thumbimg > img').attr("data-src",src).attr("data-srcset",srcset).addClass("lazyload");setTimeout(function(){return view.album.content.justify();},0);},/** + * @param {string} photoID + * @param {boolean} [justify=false] + * @returns {void} + */"delete":function _delete(photoID){var justify=arguments.length>1&&arguments[1]!==undefined?arguments[1]:false;$('.photo[data-id="'+photoID+'"]').css("opacity",0).animate({width:0,marginLeft:0},300,function(){$(this).remove();// Only when search is not active +if(album.json){if(visible.sidebar()){var videoCount=0;$.each(album.json.photos,function(){if(this.type&&this.type.indexOf("video")>-1){videoCount++;}});if(album.json.photos.length-videoCount>0){_sidebar.changeAttr("images",(album.json.photos.length-videoCount).toString());}else{_sidebar.hideAttr("images");}if(videoCount>0){_sidebar.changeAttr("videos",videoCount.toString());}else{_sidebar.hideAttr("videos");}}if(album.json.photos.length<=0){lychee.content.find(".divider").remove();}if(justify){setTimeout(function(){return view.album.content.justify();},0);}}});},/** + * @param {string} albumID + * @returns {void} + */deleteSub:function deleteSub(albumID){$('.album[data-id="'+albumID+'"]').css("opacity",0).animate({width:0,marginLeft:0},300,function(){$(this).remove();if(album.json){if(album.json.albums.length<=0){lychee.content.find(".divider").remove();}if(visible.sidebar()){if(album.json.albums.length>0){_sidebar.changeAttr("subalbums",album.json.albums.length.toString());}else{_sidebar.hideAttr("subalbums");}}}});},/** + * Lays out the photos inside an album or a search result. + * + * This method is a misnomer, because it does not necessarily + * create a justified layout, but the configured layout as specified + * by `lychee.layout` which can also be a non-justified layout. + * + * Also note that this method is bastardized by `search.find`. + * Hence, this method would better not be part of `view.album.content`, + * because it is not exclusively used for an album. + * + * TODO: Livewire front-end will make this a pure CSS solution. + * + * @returns {void} + */justify:function justify(){// Note, this also works for search results as the search creates +// a virtual "search smart album" which fills `album.json`. +if(album.json===null||album.json.photos.length===0)return;/** + * @type {Photo[]} + */var photos=album.json.photos;if(lychee.layout==="justified"){/** @type {jQuery} */var jqJustifiedLayout=$(".justified-layout");var containerWidth=parseFloat(jqJustifiedLayout.width());if(containerWidth===0){// The reported width is zero, if `.justified-layout` +// or any parent element is hidden via `display: none`. +// Currently, this happens when a page reload is triggered +// in photo view due to dorky timing constraints. +// (In short: `lychee.load` initially hides the parent +// container `.content`, and the parent container only +// becomes visible _after_ the photo has been loaded which +// is too late for this method.) +// Also note, that this container and the parent +// container are normally always visible, even if a photo +// is shown as the photo view is drawn in the foreground +// and covers this container. +// Hence, this edge case here is really only a problem +// during a full page reload in combination with +// `lychee.load`. +// Also note that the code below is wrong and outdated. +// The alternative way to calculate the container width +// depends on the window width and (falsely) assumes that +// neither the left menu nor the right sidebar are open, +// but that the `.content` box covers the whole viewport. +// That was a correct assumption in the past, as the +// sidebar was always closed after a full page reload, but +// this assumption isn't true anymore since Lychee +// remembers the state of the sidebar. +// Luckily, this whole problem vanishes with the new +// box model after +// https://github.com/LycheeOrg/Lychee-front/pull/335 has been merged. +// Then, we can use the view of the view container which +// is always visible and always has the correct width +// even for opened sidebars. +// TODO: Unconditionally use the width of the view container and remove this alternative width calculation after https://github.com/LycheeOrg/Lychee-front/pull/335 has been merged +containerWidth=$(window).width()-2*parseFloat(jqJustifiedLayout.css("margin"));}/** @type {number[]} */var ratio=photos.map(function(_photo){var height=_photo.size_variants.original.height;var width=_photo.size_variants.original.width;var ratio=height>0?width/height:1;// If there is no small and medium size variants for videos, +// we have to fall back to square thumbs +return _photo.type&&_photo.type.indexOf("video")!==-1&&_photo.size_variants.small===null&&_photo.size_variants.medium===null?1:ratio;});/** + * An album listing has potentially hundreds of photos, hence + * only query for them once. + * @type {jQuery} + */var jqPhotoElements=$(".justified-layout > div.photo");var photoDefaultHeight=parseFloat(jqPhotoElements.css("--lychee-default-height"));var layoutGeometry=require("justified-layout")(ratio,{containerWidth:containerWidth,containerPadding:0,targetRowHeight:photoDefaultHeight});// if (lychee.rights.settings.can_edit) console.log(layoutGeometry); +$(".justified-layout").css("height",layoutGeometry.containerHeight+"px");$(".justified-layout > div").each(function(i){if(!layoutGeometry.boxes[i]){// Race condition in search.find -- window content +// and `photos` can get out of sync as search +// query is being modified. +return false;}var imgs=$(this).css({top:layoutGeometry.boxes[i].top+"px",width:layoutGeometry.boxes[i].width+"px",height:layoutGeometry.boxes[i].height+"px",left:layoutGeometry.boxes[i].left+"px"}).find(".thumbimg > img");if(imgs.length>0&&imgs[0].getAttribute("data-srcset")){imgs[0].setAttribute("sizes",layoutGeometry.boxes[i].width+"px");}});// Show updated layout +jqJustifiedLayout.removeClass("laying-out");}else if(lychee.layout==="unjustified"){/** @type {jQuery} */var jqUnjustifiedLayout=$(".unjustified-layout");var _containerWidth=parseFloat(jqUnjustifiedLayout.width());if(_containerWidth===0){// Triggered on Reload in photo view. +_containerWidth=$(window).width()-2*parseFloat(jqUnjustifiedLayout.css("margin"));}/** + * An album listing has potentially hundreds of photos, hence + * only query for them once. + * @type {jQuery} + */var _jqPhotoElements=$(".unjustified-layout > div.photo");var photoMaxHeight=parseFloat(_jqPhotoElements.css("max-height"));var photoMargin=parseFloat(_jqPhotoElements.css("margin-right"));// Temporarily hide the container such that not each +// modification of every photo triggers a UI update. +jqUnjustifiedLayout.addClass("laying-out");_jqPhotoElements.each(function(i){if(!photos[i]){// Race condition in search.find -- window content +// and `photos` can get out of sync as search +// query is being modified. +return false;}var ratio=photos[i].size_variants.original.height>0?photos[i].size_variants.original.width/photos[i].size_variants.original.height:1;if(photos[i].type&&photos[i].type.indexOf("video")>-1){// Video. If there's no small and medium, we have +// to fall back to the square thumb. +if(photos[i].size_variants.small===null&&photos[i].size_variants.medium===null){ratio=1;}}var height=photoMaxHeight;var width=height*ratio;if(width>_containerWidth-photoMargin){width=_containerWidth-photoMargin;height=width/ratio;}var imgs=$(this).css({width:width+"px",height:height+"px"}).find(".thumbimg > img");if(imgs.length>0&&imgs[0].getAttribute("data-srcset")){imgs[0].setAttribute("sizes",width+"px");}});// Show updated layout +jqUnjustifiedLayout.removeClass("laying-out");}view.album.content.restoreScroll();}},/** + * @returns {void} + */description:function description(){_sidebar.changeAttr("description",album.json.description?album.json.description:"");},/** + * @returns {void} + */show_tags:function show_tags(){_sidebar.changeAttr("show_tags",album.json.show_tags.join(", "));},/** + * @returns {void} + */license:function license(){var license;switch(album.json.license){case"none":// TODO: If we do not use `"none"` as a literal string, we should convert `license` to a nullable DB attribute and use `null` for none to be consistent which everything else +license="";// none is displayed as - thus is empty. +break;case"reserved":license=lychee.locale["ALBUM_RESERVED"];break;default:license=album.json.license;// console.log('default'); +break;}_sidebar.changeAttr("license",license);},/** + * @returns {void} + */"public":function _public(){$("#button_visibility_album, #button_sharing_album_users").removeClass("active--not-hidden active--hidden");if(album.json.policy.is_public){if(album.json.policy.is_link_required){$("#button_visibility_album, #button_sharing_album_users").addClass("active--hidden");}else{$("#button_visibility_album, #button_sharing_album_users").addClass("active--not-hidden");}$(".photo .iconic-share").remove();if(album.json.init)_sidebar.changeAttr("public",lychee.locale["ALBUM_SHR_YES"]);}else{if(album.json.init)_sidebar.changeAttr("public",lychee.locale["ALBUM_SHR_NO"]);}},/** + * @returns {void} + */requiresLink:function requiresLink(){if(album.json.policy.is_link_required)_sidebar.changeAttr("hidden",lychee.locale["ALBUM_SHR_YES"]);else _sidebar.changeAttr("hidden",lychee.locale["ALBUM_SHR_NO"]);},/** + * @returns {void} + */nsfw:function nsfw(){if(album.json.policy.is_nsfw){// Sensitive +$("#button_nsfw_album").addClass("active").attr("title",lychee.locale["ALBUM_UNMARK_NSFW"]);}else{// Not Sensitive +$("#button_nsfw_album").removeClass("active").attr("title",lychee.locale["ALBUM_MARK_NSFW"]);}},/** + * @returns {void} + */downloadable:function downloadable(){if(album.json.policy.grants_download)_sidebar.changeAttr("downloadable",lychee.locale["ALBUM_SHR_YES"]);else _sidebar.changeAttr("downloadable",lychee.locale["ALBUM_SHR_NO"]);},/** + * @returns {void} + */password:function password(){if(album.json.policy.is_password_required)_sidebar.changeAttr("password",lychee.locale["ALBUM_SHR_YES"]);else _sidebar.changeAttr("password",lychee.locale["ALBUM_SHR_NO"]);},/** + * @returns {void} + */sidebar:function sidebar(){if((visible.album()||album.json&&!album.json.init)&&!visible.photo()){var structure=_sidebar.createStructure.album(album.json);var html=_sidebar.render(structure);_sidebar.dom("#lychee_sidebar_content").html(html);_sidebar.bind();}}};view.photo={/** + * @param {boolean} autoplay + * @returns {void} + */init:function init(autoplay){multiselect.clearSelection();view.photo.sidebar();view.photo.title();view.photo.star();view.photo["public"]();view.photo.header();view.photo.photo(autoplay);// TODO: `init` is not a property of the Photo JSON; this is a property of the view. Consider to move it to `view.photo.isInitialized` +_photo3.json.init=true;},/** + * @returns {void} + */show:function show(){// Change header +lychee.content.addClass("view");header.setMode("photo");if(!visible.photo()){// Fullscreen +var timeout=null;$(document).bind("mousemove",function(){clearTimeout(timeout);// For live Photos: header animation only if LivePhoto is not playing +if(!_photo3.isLivePhotoPlaying()&&lychee.header_auto_hide){header.show();timeout=setTimeout(header.hideIfLivePhotoNotPlaying,2500);}});// we also put this timeout to enable it by default when you directly click on a picture. +if(lychee.header_auto_hide){setTimeout(header.hideIfLivePhotoNotPlaying,2500);}lychee.animate(lychee.imageview,"fadeIn");lychee.imageview.addClass("active");}},/** + * @returns {void} + */hide:function hide(){header.show();lychee.content.removeClass("view");header.setMode("album");// Disable Fullscreen +$(document).unbind("mousemove");if($("video").length){$("video")[$("video").length-1].pause();}// Hide Photo +lychee.animate(lychee.imageview,"fadeOut");// TODO: Reconsider the lines below +// The lines below are inconsistent to the corresponding code for +// the mapview (cp. `mapview.close()`). +// Here, we remove the `active` class after the animation has ended, +// in `mapview.close()` we remove that class immediately. +setTimeout(function(){lychee.imageview.removeClass("active");view.album.sidebar();},300);},/** + * @returns {void} + */title:function title(){if(_photo3.json.init)_sidebar.changeAttr("title",_photo3.json.title?_photo3.json.title:"");var photoUrl=_photo3.json.size_variants.medium?_photo3.json.size_variants.medium.url:_photo3.json.size_variants.original.url;lychee.setMetaData(_photo3.json.title?_photo3.json.title:lychee.locale["UNTITLED"],true,_photo3.json.description,photoUrl);},/** + * @returns {void} + */description:function description(){if(_photo3.json.init)_sidebar.changeAttr("description",_photo3.json.description?_photo3.json.description:"");},/** + * @returns {void} + */uploaded:function uploaded(){if(_photo3.json.init)_sidebar.changeAttr("uploaded",_photo3.json.created_at?lychee.locale.printDateTime(_photo3.json.created_at):"");},/** + * @returns {void} + */license:function license(){var license;// Process key to display correct string +switch(_photo3.json.license){case"none":// TODO: If we do not use `"none"` as a literal string, we should convert `license` to a nullable DB attribute and use `null` for none to be consistent which everything else +license="";// none is displayed as - thus is empty (uniformity of the display). +break;case"reserved":license=lychee.locale["PHOTO_RESERVED"];break;default:license=_photo3.json.license;break;}// Update the sidebar if the photo is visible +if(_photo3.json.init)_sidebar.changeAttr("license",license);},/** + * @returns {void} + */star:function star(){if(_photo3.json.is_starred){// Starred +$("#button_star").addClass("active").attr("title",lychee.locale["UNSTAR_PHOTO"]);}else{// Unstarred +$("#button_star").removeClass("active").attr("title",lychee.locale["STAR_PHOTO"]);}},/** + * @returns {void} + */"public":function _public(){$("#button_visibility").removeClass("active--hidden active--not-hidden");if(_photo3.json.is_public===true){$("#button_visibility").addClass("active--hidden");if(_photo3.json.init)_sidebar.changeAttr("public",lychee.locale["PHOTO_SHR_YES"]);}else if(_photo3.json.album_id!==null&&album.json.policy.is_public===true){// part of a visible album +$("#button_visibility").addClass("active--not-hidden");if(_photo3.json.init)_sidebar.changeAttr("public",lychee.locale["PHOTO_SHR_YES"]);}else{// Photo private +if(_photo3.json.init)_sidebar.changeAttr("public",lychee.locale["PHOTO_SHR_NO"]);}},/** + * @returns {void} + */tags:function tags(){_sidebar.changeAttr("tags",build.tags(_photo3.json.tags),true);_sidebar.bind();},/** + * @param {boolean} autoplay + * @returns {void} + */photo:function photo(autoplay){var ret=build.imageview(_photo3.json,visible.header(),autoplay);lychee.imageview.html(ret.html);tabindex.makeFocusable(lychee.imageview);// Init Live Photo if needed +if(_photo3.isLivePhoto()){// Package gives warning that function will be remove and +// shoud be replaced by LivePhotosKit.augementElementAsPlayer +// But, LivePhotosKit.augementElementAsPlayer is not yet available +_photo3.livePhotosObject=LivePhotosKit.Player(document.getElementById("livephoto"));}view.photo.onresize();var $nextArrow=lychee.imageview.find("a#next");var $previousArrow=lychee.imageview.find("a#previous");var photoID=_photo3.getID();/** @type {?Photo} */var photoInAlbum=album.json&&album.json.photos?album.getByID(photoID):null;/** @type {?Photo} */var nextPhotoInAlbum=photoInAlbum&&photoInAlbum.next_photo_id?album.getByID(photoInAlbum.next_photo_id):null;/** @type {?Photo} */var prevPhotoInAlbum=photoInAlbum&&photoInAlbum.previous_photo_id?album.getByID(photoInAlbum.previous_photo_id):null;var img=$("img#image");if(img.length>0){if(!img[0].complete||img[0].currentSrc!==null&&img[0].currentSrc===""){// Image is still loading. Display the thumb version in the +// background. +if(ret.thumb!==""){img.css("background-image",lychee.html(_templateObject56||(_templateObject56=_taggedTemplateLiteral(["url(\"","\")"])),ret.thumb));}// Don't preload next/prev until the requested image is +// fully loaded. +img.on("load",function(){_photo3.preloadNextPrev(_photo3.getID());});}else{_photo3.preloadNextPrev(_photo3.getID());}}if(nextPhotoInAlbum===null||lychee.viewMode===true){$nextArrow.hide();}else{// Check if thumbUrl exists (for videos w/o ffmpeg, we add a play-icon) +var thumbUrl="img/placeholder.png";if(nextPhotoInAlbum.size_variants.thumb!==null){thumbUrl=nextPhotoInAlbum.size_variants.thumb.url;}else if(nextPhotoInAlbum.type.indexOf("video")>-1){thumbUrl="img/play-icon.png";}$nextArrow.css("background-image",lychee.html(_templateObject57||(_templateObject57=_taggedTemplateLiteral(["linear-gradient(to bottom, rgba(0, 0, 0, .4), rgba(0, 0, 0, .4)), url(\"","\")"])),thumbUrl));}if(prevPhotoInAlbum===null||lychee.viewMode===true){$previousArrow.hide();}else{// Check if thumbUrl exists (for videos w/o ffmpeg, we add a play-icon) +var _thumbUrl="img/placeholder.png";if(prevPhotoInAlbum.size_variants.thumb!==null){_thumbUrl=prevPhotoInAlbum.size_variants.thumb.url;}else if(prevPhotoInAlbum.type.indexOf("video")>-1){_thumbUrl="img/play-icon.png";}$previousArrow.css("background-image",lychee.html(_templateObject58||(_templateObject58=_taggedTemplateLiteral(["linear-gradient(to bottom, rgba(0, 0, 0, .4), rgba(0, 0, 0, .4)), url(\"","\")"])),_thumbUrl));}},/** + * @returns {void} + */sidebar:function sidebar(){var structure=_sidebar.createStructure.photo(_photo3.json);var html=_sidebar.render(structure);var has_location=!!(_photo3.json.latitude&&_photo3.json.longitude);_sidebar.dom("#lychee_sidebar_content").html(html);_sidebar.bind();if(has_location&&lychee.map_display){// Leaflet searches for icon in same directory as js file -> paths needs +// to be overwritten +delete L.Icon.Default.prototype._getIconUrl;L.Icon.Default.mergeOptions({iconRetinaUrl:"img/marker-icon-2x.png",iconUrl:"img/marker-icon.png",shadowUrl:"img/marker-shadow.png"});var myMap=L.map("leaflet_map_single_photo").setView([_photo3.json.latitude,_photo3.json.longitude],13);L.tileLayer(map_provider_layer_attribution[lychee.map_provider].layer,{attribution:map_provider_layer_attribution[lychee.map_provider].attribution}).addTo(myMap);if(!lychee.map_display_direction||!_photo3.json.img_direction){// Add Marker to map, direction is not set +L.marker([_photo3.json.latitude,_photo3.json.longitude]).addTo(myMap);}else{// Add Marker, direction has been set +var viewDirectionIcon=L.icon({iconUrl:"img/view-angle-icon.png",iconRetinaUrl:"img/view-angle-icon-2x.png",iconSize:[100,58],// size of the icon +iconAnchor:[50,49]// point of the icon which will correspond to marker's location +});var marker=L.marker([_photo3.json.latitude,_photo3.json.longitude],{icon:viewDirectionIcon}).addTo(myMap);marker.setRotationAngle(_photo3.json.img_direction);}}},/** + * @returns {void} + */header:function header(){/* Note: the condition below is duplicated in contextMenu.photoMore() */if(_photo3.json.type&&(_photo3.json.type.indexOf("video")===0||_photo3.json.type==="raw")||_photo3.json.live_photo_url!==""&&_photo3.json.live_photo_url!==null){$("#button_rotate_cwise, #button_rotate_ccwise").hide();}},/** + * @returns {void} + */onresize:function onresize(){if(!_photo3.json||_photo3.json.size_variants.medium===null||_photo3.json.size_variants.medium2x===null)return;// Calculate the width of the image in the current window without +// borders and set 'sizes' to it. +var imgWidth=_photo3.json.size_variants.medium.width;var imgHeight=_photo3.json.size_variants.medium.height;var containerWidth=$(window).outerWidth();var containerHeight=$(window).outerHeight();// Image can be no larger than its natural size, but it can be +// smaller depending on the size of the window. +var width=imgWidthcontainerHeight){width=containerHeight*imgWidth/imgHeight;}$("img#image").attr("sizes",width+"px");}};view.settings={/** + * @returns {void} + */init:function init(){multiselect.clearSelection();if(visible.photo())view.photo.hide();view.settings.title();header.setMode("config");view.settings.content.init();},/** + * @returns {void} + */title:function title(){lychee.setMetaData(lychee.locale["SETTINGS"]);},/** + * @returns {void} + */clearContent:function clearContent(){lychee.content.html('
');},content:{/** + * @returns {void} + */init:function init(){view.settings.clearContent();if(lychee.rights.user.can_edit){view.settings.content.setLogin();}if(lychee.rights.settings.can_edit){view.settings.content.setSorting();view.settings.content.setDropboxKey();view.settings.content.setLang();view.settings.content.setDefaultLicense();view.settings.content.setLayout();view.settings.content.setPublicSearch();view.settings.content.setAlbumDecoration();view.settings.content.setOverlayType();view.settings.content.setMapDisplay();view.settings.content.setNSFWVisible();view.settings.content.setRecentPublic();view.settings.content.setStarredPublic();view.settings.content.setOnThisDayPublic();view.settings.content.setNotification();view.settings.content.setCSS();view.settings.content.setJS();view.settings.content.moreButton();}},/** + * @returns {void} + */setLogin:function setLogin(){var username_type="hidden";if(lychee.allow_username_change){username_type="text";}var msg=lychee.html(_templateObject59||(_templateObject59=_taggedTemplateLiteral(["\n\t\t\t
\n\t\t\t
\n\t\t\t

$","\n\t\t\t\t \n\t\t\t

\n\t\t\t

$","\n\t\t\t\t \n\t\t\t\t \n\t\t\t\t \n\t\t\t

\n\t\t\t
\n\t\t\t\t\n\t\t\t\t$","\n\t\t\t\t$","\n\t\t\t
\n\t\t\t
\n\t\t\t
"])),lychee.locale["PASSWORD_TITLE"],lychee.locale["PASSWORD_CURRENT"],lychee.locale["PASSWORD_TEXT"],username_type,lychee.locale["LOGIN_USERNAME"],lychee.locale["LOGIN_PASSWORD"],lychee.locale["LOGIN_PASSWORD_CONFIRM"],lychee.locale["PASSWORD_CHANGE"],lychee.locale["TOKEN_BUTTON"]);$(".settings_view").append(msg);settings.bind("#basicModal__action_password_change",".setLogin",settings.changeLogin);settings.bind("#basicModal__action_token",".setLogin",settings.openTokenDialog);},/** + * @returns {void} + */clearLogin:function clearLogin(){$("input[name=oldUsername], input[name=oldPassword], input[name=username], input[name=password], input[name=confirm]").val("");},/** + * Renders the area of the settings related to sorting + * + * TODO: Note, the method is a misnomer. + * It does not **set** any sorting, see {@link settings.changeSorting} + * for that. + * This method only creates the HTML GUI. + * + * @returns {void} + */setSorting:function setSorting(){var msg=lychee.html(_templateObject60||(_templateObject60=_taggedTemplateLiteral(["\n\t\t\t\t
\n\t\t\t\t\t

\n\t\t\t\t\t\t","\n\t\t\t\t\t

\n\t\t\t\t\t

\n\t\t\t\t\t\t","\n\t\t\t\t\t

\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t$","\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t"])),sprintf(lychee.locale["SORT_ALBUM_BY"],"\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t"),"\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t")),sprintf(lychee.locale["SORT_PHOTO_BY"],"\n\t\t\t\t\t\t\t\n\t\t\t\t \t\t"),"\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t")),lychee.locale["SORT_CHANGE"]);$(".settings_view").append(msg);if(lychee.sorting_albums){$(".setSorting select#settings_albums_sorting_column").val(lychee.sorting_albums.column);$(".setSorting select#settings_albums_sorting_order").val(lychee.sorting_albums.order);}if(lychee.sorting_photos){$(".setSorting select#settings_photos_sorting_column").val(lychee.sorting_photos.column);$(".setSorting select#settings_photos_sorting_order").val(lychee.sorting_photos.order);}settings.bind("#basicModal__action_sorting_change",".setSorting",settings.changeSorting);},/** + * @returns {void} + */setDropboxKey:function setDropboxKey(){var msg="\n\t\t\t
\n\t\t\t

".concat(lychee.locale["DROPBOX_TEXT"],"\n\t\t\t \n\t\t\t

\n\t\t\t\t\n\t\t\t
\n\t\t\t ");$(".settings_view").append(msg);settings.bind("#basicModal__action_dropbox_change",".setDropBox",settings.changeDropboxKey);},/** + * @returns {void} + */setLang:function setLang(){var msg="\n\t\t\t\t
\n\t\t\t\t\t

\n\t\t\t\t\t\t".concat(lychee.locale["LANG_TEXT"],"\n\t\t\t \t\t\t\n\t\t\t\t\t\t\t\n\t\t\t \t\t\t\n\t\t\t\t\t

\n\t\t\t\t\t
\n\t\t\t\t\t\t").concat(lychee.locale["LANG_TITLE"],"\n\t\t\t\t\t
\n\t\t\t\t
");$(".settings_view").append(msg);settings.bind("#basicModal__action_set_lang",".setLang",settings.changeLang);},/** + * @returns {void} + */setDefaultLicense:function setDefaultLicense(){var msg="\n\t\t\t
\n\t\t\t

".concat(lychee.locale["DEFAULT_LICENSE"],"\n\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t
\n\t\t\t").concat(lychee.locale["PHOTO_LICENSE_HELP"],"\n\t\t\t

\n\t\t\t\n\t\t\t
\n\t\t\t");$(".settings_view").append(msg);$("select#license").val(lychee.default_license===""?"none":lychee.default_license);settings.bind("#basicModal__action_set_license",".setDefaultLicense",settings.setDefaultLicense);},/** + * @returns {void} + */setLayout:function setLayout(){var msg="\n\t\t\t
\n\t\t\t

".concat(lychee.locale["LAYOUT_TYPE"],"\n\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t

\n\t\t\t\n\t\t\t
\n\t\t\t");$(".settings_view").append(msg);$("select#layout").val(lychee.layout);settings.bind("#basicModal__action_set_layout",".setLayout",settings.setLayout);},/** + * @returns {void} + */setPublicSearch:function setPublicSearch(){var msg="\n\t\t\t
\n\t\t\t

".concat(lychee.locale["PUBLIC_SEARCH_TEXT"],"\n\t\t\t\n\t\t\t

\n\t\t\t
\n\t\t\t");$(".settings_view").append(msg);if(lychee.public_search)$("#PublicSearch").click();settings.bind("#PublicSearch",".setPublicSearch",settings.changePublicSearch);},/** + * @returns {void} + */setNSFWVisible:function setNSFWVisible(){var msg="\n\t\t\t
\n\t\t\t

".concat(lychee.locale["NSFW_VISIBLE_TEXT_1"],"\n\t\t\t

\n\t\t\t

").concat(lychee.locale["NSFW_VISIBLE_TEXT_2"],"\n\t\t\t

\n\t\t\t
\n\t\t\t");$(".settings_view").append(msg);if(lychee.nsfw_visible_saved){$("#NSFWVisible").click();}settings.bind("#NSFWVisible",".setNSFWVisible",settings.changeNSFWVisible);},// TODO: extend to the other settings. +/** + * @returns {void} + */setRecentPublic:function setRecentPublic(){var msg="\n\t\t\t
\n\t\t\t

".concat(lychee.locale["SETTING_RECENT_PUBLIC_TEXT"],"\n\t\t\t

\n\t\t\t\n\t\t\t
\n\t\t\t");$(".settings_view").append(msg);console.log(lychee.smart_album_visibilty);if(lychee.smart_album_visibilty.recent===true){$("#RecentPublic").click();}settings.bind("#RecentPublic",".setRecentPublic",settings.changeSmartAlbumVisibility);},/** + * @returns {void} + */setStarredPublic:function setStarredPublic(){var msg="\n\t\t\t
\n\t\t\t

".concat(lychee.locale["SETTING_STARRED_PUBLIC_TEXT"],"\n\t\t\t

\n\t\t\t\n\t\t\t
\n\t\t\t");$(".settings_view").append(msg);if(lychee.smart_album_visibilty.starred){$("#StarredPublic").click();}settings.bind("#StarredPublic",".setStarredPublic",settings.changeSmartAlbumVisibility);},/** + * @returns {void} + */setOnThisDayPublic:function setOnThisDayPublic(){var msg="\n\t\t\t
\n\t\t\t

".concat(lychee.locale["SETTING_ONTHISDAY_PUBLIC_TEXT"],"\n\t\t\t

\n\t\t\t\n\t\t\t
\n\t\t\t");$(".settings_view").append(msg);if(lychee.smart_album_visibilty.on_this_day){$("#OnThisDayPublic").click();}settings.bind("#OnThisDayPublic",".setOnThisDayPublic",settings.changeSmartAlbumVisibility);},/** + * @returns {void} + */setAlbumDecoration:function setAlbumDecoration(){var msg="\n\t\t\t
\n\t\t\t

".concat(lychee.locale["ALBUM_DECORATION"],"\n\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t

").concat(lychee.locale["ALBUM_DECORATION_ORIENTATION"],"\n\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t

\n\t\t\t
\n\t\t\t");$(".settings_view").append(msg);$("select#AlbumDecorationType").val(!lychee.album_decoration?"layers":lychee.album_decoration);$("select#AlbumDecorationOrientation").val(!lychee.album_decoration_orientation?"row":lychee.album_decoration_orientation);settings.bind("#basicModal__action_set_album_decoration",".setAlbumDecoration",settings.setAlbumDecoration);},/** + * @returns {void} + */setOverlayType:function setOverlayType(){var msg="\n\t\t\t
\n\t\t\t

".concat(lychee.locale["OVERLAY_TYPE"],"\n\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t

\n\t\t\t
\n\t\t\t");$(".settings_view").append(msg);$("select#ImgOverlayType").val(!lychee.image_overlay_type_default?"exif":lychee.image_overlay_type_default);settings.bind("#basicModal__action_set_overlay_type",".setOverlayType",settings.setOverlayType);},/** + * @returns {void} + */setMapDisplay:function setMapDisplay(){var msg="\n\t\t\t
\n\t\t\t

".concat(lychee.locale["MAP_DISPLAY_TEXT"],"\n\t\t\t\n\t\t\t

\n\t\t\t
\n\t\t\t");$(".settings_view").append(msg);if(lychee.map_display)$("#MapDisplay").click();settings.bind("#MapDisplay",".setMapDisplay",settings.changeMapDisplay);msg="\n\t\t\t
\n\t\t\t

".concat(lychee.locale["MAP_DISPLAY_PUBLIC_TEXT"],"\n\t\t\t\n\t\t\t

\n\t\t\t
\n\t\t\t");$(".settings_view").append(msg);if(lychee.map_display_public)$("#MapDisplayPublic").click();settings.bind("#MapDisplayPublic",".setMapDisplayPublic",settings.changeMapDisplayPublic);msg="\n\t\t\t
\n\t\t\t

".concat(lychee.locale["MAP_PROVIDER"],"\n\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t

\n\t\t\t
\n\t\t\t");$(".settings_view").append(msg);$("select#MapProvider").val(!lychee.map_provider?"Wikimedia":lychee.map_provider);settings.bind("#basicModal__action_set_map_provider",".setMapProvider",settings.setMapProvider);msg="\n\t\t\t
\n\t\t\t

".concat(lychee.locale["MAP_INCLUDE_SUBALBUMS_TEXT"],"\n\t\t\t\n\t\t\t

\n\t\t\t
\n\t\t\t");$(".settings_view").append(msg);if(lychee.map_include_subalbums)$("#MapIncludeSubAlbums").click();settings.bind("#MapIncludeSubAlbums",".setMapIncludeSubAlbums",settings.changeMapIncludeSubAlbums);msg="\n\t\t\t
\n\t\t\t

".concat(lychee.locale["LOCATION_DECODING"],"\n\t\t\t\n\t\t\t

\n\t\t\t
\n\t\t\t");$(".settings_view").append(msg);if(lychee.location_decoding)$("#LocationDecoding").click();settings.bind("#LocationDecoding",".setLocationDecoding",settings.changeLocationDecoding);msg="\n\t\t\t
\n\t\t\t

".concat(lychee.locale["LOCATION_SHOW"],"\n\t\t\t\n\t\t\t

\n\t\t\t
\n\t\t\t");$(".settings_view").append(msg);if(lychee.location_show)$("#LocationShow").click();settings.bind("#LocationShow",".setLocationShow",settings.changeLocationShow);msg="\n\t\t\t
\n\t\t\t

".concat(lychee.locale["LOCATION_SHOW_PUBLIC"],"\n\t\t\t\n\t\t\t

\n\t\t\t
\n\t\t\t");$(".settings_view").append(msg);if(lychee.location_show_public)$("#LocationShowPublic").click();settings.bind("#LocationShowPublic",".setLocationShowPublic",settings.changeLocationShowPublic);},/** + * @returns {void} + */setNotification:function setNotification(){var msg="\n\t\t\t
\n\t\t\t

".concat(lychee.locale["NEW_PHOTOS_NOTIFICATION"],"\n\t\t\t\n\t\t\t

\n\t\t\t
\n\t\t\t");$(".settings_view").append(msg);if(lychee.new_photos_notification)$("#NewPhotosNotification").click();settings.bind("#NewPhotosNotification",".setNewPhotosNotification",settings.changeNewPhotosNotification);},/** + * @returns {void} + */setCSS:function setCSS(){var msg="\n\t\t\t
\n\t\t\t

".concat(lychee.locale["CSS_TEXT"],"

\n\t\t\t\n\t\t\t\n\t\t\t
");$(".settings_view").append(msg);var css_addr=$($("link")[1]).attr("href");api.getRawContent(css_addr,function(data){$("#css").html(data);});settings.bind("#basicModal__action_set_css",".setCSS",settings.changeCSS);},/** + * @returns {void} + */setJS:function setJS(){var msg="\n\t\t\t
\n\t\t\t

".concat(lychee.locale["JS_TEXT"],"

\n\t\t\t\n\t\t\t\n\t\t\t
");$(".settings_view").append(msg);var js_addr=$("script[src]:last").attr("src");api.getRawContent(js_addr,function(data){$("#js").html(data);});settings.bind("#basicModal__action_set_js",".setJS",settings.changeJS);},/** + * @returns {void} + */moreButton:function moreButton(){var msg=lychee.html(_templateObject61||(_templateObject61=_taggedTemplateLiteral(["\n\t\t\t
\n\t\t\t\t","\n\t\t\t
\n\t\t\t"])),lychee.locale["MORE"]);$(".settings_view").append(msg);$("#basicModal__action_more").on("click",view.full_settings.init);}}};view.full_settings={/** + * @returns {void} + */init:function init(){multiselect.clearSelection();view.full_settings.title();view.full_settings.content.init();},/** + * @returns {void} + */title:function title(){lychee.setMetaData(lychee.locale["FULL_SETTINGS"]);},/** + * @returns {void} + */clearContent:function clearContent(){lychee.content.html('
');},content:{init:function init(){view.full_settings.clearContent();api.post("Settings::getAll",{},/** @param {ConfigSetting[]} data */function(data){var msg=lychee.html(_templateObject62||(_templateObject62=_taggedTemplateLiteral(["\n\t\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t

\n\t\t\t\t\t\t","\n\t\t\t\t\t\t

\n\t\t\t\t\t\t
\n\t\t\t\t\t\t"])),lychee.locale["SETTINGS_ADVANCED_WARNING_EXPL"]);var prev="";data.forEach(function(_config){if(_config.cat&&prev!==_config.cat){msg+=lychee.html(_templateObject63||(_templateObject63=_taggedTemplateLiteral(["\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t

$","

\n\t\t\t\t\t\t\t\t
"])),_config.cat);prev=_config.cat;}// prevent 'null' string for empty values +var val=_config.value?_config.value:"";msg+=lychee.html(_templateObject64||(_templateObject64=_taggedTemplateLiteral(["\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t

\n\t\t\t\t\t\t\t\t\t$","\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t

\n\t\t\t\t\t\t\t
"])),_config.key,_config.key,val);});msg+=lychee.html(_templateObject65||(_templateObject65=_taggedTemplateLiteral(["\n\t\t\t\t\t\t","\n\t\t\t\t\t\t
"])),lychee.locale["SETTINGS_ADVANCED_SAVE"]);$(".settings_view").append(msg);settings.bind("#FullSettingsSave_button","#fullSettings",settings.save);$("#fullSettings").on("keypress",function(e){settings.save_enter(e);});});}}};view.notifications={/** @returns {void} */init:function init(){multiselect.clearSelection();if(visible.photo())view.photo.hide();view.notifications.title();header.setMode("config");view.notifications.content.init();},/** @returns {void} */title:function title(){lychee.setMetaData(lychee.locale["NOTIFICATIONS"]);},/** @returns {void} */clearContent:function clearContent(){lychee.content.html('
');},content:{/** @returns {void} */init:function init(){view.notifications.clearContent();var html="\n\t\t\t\t
\n\t\t\t\t\t

".concat(lychee.locale["USER_EMAIL_INSTRUCTION"],"

\n\t\t\t\t
\n\t\t\t\t\t

\n\t\t\t\t\t\t").concat(lychee.locale["ENTER_EMAIL"],"\n\t\t\t\t\t\t\n\t\t\t\t\t

\n\t\t\t\t\t
\n\t\t\t\t\t\t").concat(lychee.locale["SAVE"],"\n\t\t\t\t\t
\n\t\t\t\t
");$(".settings_view").append(html);settings.bind("#UserUpdate_button","#UserUpdate",notifications.update);}}};view.users={/** @returns {void} */init:function init(){multiselect.clearSelection();if(visible.photo())view.photo.hide();view.users.title();header.setMode("config");view.users.content.init();},/** @returns {void} */title:function title(){lychee.setMetaData(lychee.locale["USERS"]);},/** @returns {void} */clearContent:function clearContent(){lychee.content.html('
');},content:{/** @returns {void} */init:function init(){view.users.clearContent();if(users.json.length===0){$(".users_view").append('

User list is empty!

');}var html="\n\t\t\t\t

\n\t\t\t\t\t".concat(lychee.locale["USERNAME"],"\n\t\t\t\t\t").concat(lychee.locale["NEW_PASSWORD"],"\n\t\t\t\t\t\n\t\t\t\t\t\t").concat(build.iconic("data-transfer-upload"),"\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t").concat(build.iconic("lock-unlocked"),"\n\t\t\t\t\t\n\t\t\t\t

");$(".users_view").append(html);users.json.forEach(function(_user){$(".users_view").append(build.user(_user));// TODO: Instead of binding an event handler to each input element it would be much more efficient, to bind a single event handler to the common parent view, let the event bubble up the DOM tree and use the `originalElement` property of the event to get the input element which caused the event. +settings.bind("#UserUpdate"+_user.id,"#UserData"+_user.id,users.update);settings.bind("#UserDelete"+_user.id,"#UserData"+_user.id,users["delete"]);if(_user.may_upload){$("#UserData"+_user.id+' .choice input[name="may_upload"]').click();}if(_user.may_edit_own_settings){$("#UserData"+_user.id+' .choice input[name="may_edit_own_settings"]').click();}});html="\n\t\t\t\t
\n\t\t\t\t\t

\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t

\n\t\t\t\t\t").concat(lychee.locale["CREATE"],"\n\t\t\t\t
");$(".users_view").append(html);settings.bind("#UserCreate_button","#UserCreate",users.create);}}};view.sharing={/** @returns {void} */init:function init(){multiselect.clearSelection();if(visible.photo())view.photo.hide();view.sharing.title();header.setMode("config");view.sharing.content.init();},/** @returns {void} */title:function title(){lychee.setMetaData(lychee.locale["SHARING"]);},/** @returns {void} */clearContent:function clearContent(){lychee.content.html('');},content:{/** @returns {void} */init:function init(){view.sharing.clearContent();if(sharing.json.shared.length===0){$(".sharing_view").append('');}var albumOptions=sharing.json.albums.reduce(function(acc,_album){return acc+lychee.html(_templateObject66||(_templateObject66=_taggedTemplateLiteral([""])),_album.id,_album.title);},"");var userOptions=sharing.json.users.reduce(function(acc,_user){return acc+lychee.html(_templateObject67||(_templateObject67=_taggedTemplateLiteral([""])),_user.id,_user.username);},"");var sharingOptions=sharing.json.shared.reduce(function(acc,_shareInfo){return acc+lychee.html(_templateObject68||(_templateObject68=_taggedTemplateLiteral(["\n\t\t\t\t\t\t

\n\t\t\t\t\t\t\t$","\n\t\t\t\t\t\t\t$","\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t

"])),_shareInfo.title,_shareInfo.username,_shareInfo.id);},"");var html="\n\t\t\t\t

Share

\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t

with

\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\n\t\t\t\t
\n\t\t\t\t\t").concat(sharingOptions,"\n\t\t\t\t
");if(sharing.json.shared.length!==0){html+="");}$(".sharing_view").append(html);$("#albums_list").multiselect();$("#user_list").multiselect();$("#Share_button").on("click",sharing.add).on("mouseenter",function(){$("#albums_list_to, #user_list_to").addClass("borderBlue");}).on("mouseleave",function(){$("#albums_list_to, #user_list_to").removeClass("borderBlue");});$("#Remove_button").on("click",sharing["delete"]);}}};view.diagnostics={/** @returns {void} */init:function init(){multiselect.clearSelection();if(visible.photo())view.photo.hide();view.diagnostics.title();header.setMode("config");view.diagnostics.content.init();},/** @returns {void} */title:function title(){lychee.setMetaData(lychee.locale["DIAGNOSTICS"]);},/** + * @param {number} update - The update status: `0`: not on master branch; + * `1`: up-to-date; `2`: not up-to-date; + * `3`: requires migration + * @returns {void} + */clearContent:function clearContent(update){var html="";if(update===2){html=view.diagnostics.button("",lychee.locale["UPDATE_AVAILABLE"]);}else if(update===3){html=view.diagnostics.button("",lychee.locale["MIGRATION_AVAILABLE"]);}else if(update>0){html=view.diagnostics.button("Check_",lychee.locale["CHECK_FOR_UPDATE"]);}html+='
';lychee.content.html(html);},/**
+	 * @param {string} type
+	 * @param {string} locale
+	 * @returns {string} - HTML
+	 */button:function button(type,locale){return"\n\t\t\t
\n\t\t\t\t").concat(locale,"\n\t\t\t
");},/** @returns {string} */bind:function bind(){$("#Update_Lychee").on("click",view.diagnostics.call_apply_update);$("#Check_Update_Lychee").on("click",view.diagnostics.call_check_update);},content:{/** @returns {void} */init:function init(){view.diagnostics.clearContent(0);api.post("Diagnostics::get",{},view.diagnostics.content.parseResponse);},/** + * @param {DiagnosticInfo} data + * @returns {void} + */parseResponse:function parseResponse(data){view.diagnostics.clearContent(data.update);var html="";html+=view.diagnostics.content.block("error","Diagnostics",data.errors);html+=view.diagnostics.content.block("sys","System Information",data.infos);html+='';html+='';html+=lychee.html(_templateObject69||(_templateObject69=_taggedTemplateLiteral(["",""])),lychee.locale["DIAGNOSTICS_GET_SIZE"]);html+="";html+=view.diagnostics.content.block("conf","Config Information",data.configs);$(".logs_diagnostics_view").html(html);view.diagnostics.bind();$("#Get_Size_Lychee").on("click",view.diagnostics.call_get_size);},/** + * @param {string} id + * @param {string} title + * @param {string[]} arr + * @returns {string} - HTML + */block:function block(id,title,arr){var html="";html+='
\n\n\n\n';html+="    "+title+"\n";html+="    ".padEnd(title.length,"-")+"\n";html+=arr.reduce(function(acc,line){return acc+"    "+line+"\n";},"");html+="
\n";return html;}},/** @returns {void} */call_check_update:function call_check_update(){api.post("Update::check",{},/** @param {{updateStatus: string}} data */function(data){loadingBar.show("success",data.updateStatus);$("#Check_Update_Lychee").remove();});},/** @returns {void} */call_apply_update:function call_apply_update(){api.post("Update::apply",{},/** @param {{updateMsgs: string[]}} data */function(data){var html=view.preify(data.updateMsgs,"");$("#Update_Lychee").remove();$(html).prependTo(".logs_diagnostics_view");});},/** @returns {void} */call_get_size:function call_get_size(){api.post("Diagnostics::getSize",{},/** @param {string[]} data */function(data){var html=view.preify(data,"");$("#Get_Size_Lychee").remove();$(html).appendTo("#content_diag_sys");});}};view.update={/** @returns {void} */init:function init(){multiselect.clearSelection();if(visible.photo())view.photo.hide();view.update.title();header.setMode("config");view.update.content.init();},/** @returns {void} */title:function title(){lychee.setMetaData(lychee.locale["UPDATE"]);},/** @returns {void} */clearContent:function clearContent(){var html='
';lychee.content.html(html);},content:{init:function init(){view.update.clearContent();// code duplicate
+api.post("Update::apply",{},/** @param {{updateMsgs: string[]}} data */function(data){var html=view.preify(data.updateMsgs,"");lychee.content.html(html);});}}};/**
+ * @param {string[]} data
+ * @param {string} cssClass
+ * @returns {string} - HTML which wraps `data` into a `
`-tag
+ */view.preify=function(data,cssClass){return data.reduce(function(acc,line){return acc+"    "+line+"\n";},'
')+"
";};view.u2f={/** @returns {void} */init:function init(){multiselect.clearSelection();if(visible.photo())view.photo.hide();view.u2f.title();header.setMode("config");view.u2f.content.init();},/** @returns {void} */title:function title(){lychee.setMetaData(lychee.locale["U2F"]);},/** @returns {void} */clearContent:function clearContent(){lychee.content.html('
');},content:{/** @returns {void} */init:function init(){view.u2f.clearContent();if(u2f.json.length===0){$(".u2f_view").append('

Credentials list is empty!

');}else{var _html2="\n\t\t\t\t\t

\n\t\t\t\t\t\t".concat(lychee.locale["U2F_CREDENTIALS"],"\n\t\t\t\t\t

");$(".u2f_view").append(_html2);u2f.json.forEach(function(credential){// TODO: Don't query the DOM for the same element in each loop iteration +$(".u2f_view").append(build.u2f(credential));settings.bind("#CredentialDelete"+credential.id,"#CredentialData"+credential.id,u2f["delete"]);});}var html="\n\t\t\t\t");$(".u2f_view").append(html);$("#RegisterU2FButton").on("click",u2f.register);}}};/** + * @description This module is used to check if elements are visible or not. + */var visible={};/** + * TODO: Whether the albums view is visible or not should not be determined based on the visibility of a toolbar, especially as this does not work for the photo view in full screen mode which makes this approach inconsistent. + * @returns {boolean} + */visible.albums=function(){return!!header.dom("#lychee_toolbar_public").hasClass("visible")||!!header.dom("#lychee_toolbar_albums").hasClass("visible");};/** @returns {boolean} */visible.album=function(){return!!header.dom("#lychee_toolbar_album").hasClass("visible");};/** @returns {boolean} */visible.photo=function(){return $("#imageview.fadeIn").length>0;};/** @returns {boolean} */visible.mapview=function(){return $("#lychee_map_container.fadeIn").length>0;};/** @returns {boolean} */visible.config=function(){return!!header.dom("#lychee_toolbar_config").hasClass("visible");};/** @returns {boolean} */visible.search=function(){return visible.albums()&&album.json!==null&&album.isSearchID(album.json.id);};/** @returns {boolean} */visible.sidebar=function(){return!!_sidebar.dom().hasClass("active");};/** @returns {boolean} */visible.sidebarbutton=function(){return visible.photo()||visible.album()&&$("#button_info_album:visible").length>0;};/** @returns {boolean} */visible.header=function(){return!header.dom().hasClass("hidden");};/** @returns {boolean} */visible.contextMenu=function(){return basicContext.visible();};/** @returns {boolean} */visible.multiselect=function(){return $("#multiselect").length>0;};/** @returns {boolean} */visible.leftMenu=function(){return!!leftMenu.dom().hasClass("visible");};/** + * @typedef {Object} LycheeException + * @property {string} message the message of the exception + * @property {string} exception the (base) name of the exception class; in developer mode the backend reports the full class name, in productive mode only the base name + * @property {string} [file] the file name where the exception has been thrown; only in developer mode + * @property {number} [line] the line number where the exception has been thrown; only in developer mode + * @property {Array} [trace] the backtrace; only in developer mode + * @property {?LycheeException} [previous_exception] the previous exception, if any; only in developer mode + */ /** + * @typedef Version + * + * @property {int} major + * @property {int} minor + * @property {int} patch + */ /** + * @typedef Photo + * + * @property {string} id + * @property {string} title + * @property {?string} description + * @property {string[]} tags + * @property {boolean} is_public + * @property {?string} type + * @property {?string} iso + * @property {?string} aperture + * @property {?string} make + * @property {?string} model + * @property {?string} lens + * @property {?string} shutter + * @property {?string} focal + * @property {?number} latitude + * @property {?number} longitude + * @property {?number} altitude + * @property {?number} img_direction + * @property {?string} location + * @property {?string} taken_at + * @property {?string} taken_at_orig_tz + * @property {boolean} is_starred + * @property {?string} live_photo_url + * @property {?string} album_id + * @property {string} checksum + * @property {string} license + * @property {string} created_at + * @property {string} updated_at + * @property {?string} live_photo_content_id + * @property {?string} live_photo_checksum + * @property {SizeVariants} size_variants + * @property {?string} [next_photo_id] + * @property {?string} [previous_photo_id] + * @property {PhotoRightsDTO} rights + */ /** + * @typedef SizeVariants + * + * @property {SizeVariant} original + * @property {?SizeVariant} medium2x + * @property {?SizeVariant} medium + * @property {?SizeVariant} small2x + * @property {?SizeVariant} small + * @property {?SizeVariant} thumb2x + * @property {?SizeVariant} thumb + */ /** + * @typedef SizeVariant + * + * @property {number} type + * @property {string} url + * @property {number} width + * @property {number} height + * @property {number} filesize + */ /** + * @typedef SortingCriterion + * + * @property {string} column + * @property {string} order + */ /** + * @typedef Album + * + * @property {string} id + * @property {string} parent_id + * @property {string} created_at + * @property {string} updated_at + * @property {string} title + * @property {?string} description + * @property {string} license + * @property {Photo[]} photos + * @property {Album[]} [albums] + * @property {?string} cover_id + * @property {?Thumb} thumb + * @property {string} [owner_name] optional, only shown in authenticated mode + * @property {boolean} is_nsfw + * @property {AlbumRightsDTO} rights + * @property {AlbumProtectionPolicy} policy + * @property {boolean} num_albums + * @property {boolean} num_photos + * @property {?string} min_taken_at + * @property {?string} max_taken_at + * @property {?SortingCriterion} sorting + */ /** + * @typedef TagAlbum + * + * @property {string} id + * @property {string} created_at + * @property {string} updated_at + * @property {string} title + * @property {?string} description + * @property {string[]} show_tags + * @property {Photo[]} photos + * @property {?Thumb} thumb + * @property {string} [owner_name] optional, only shown in authenticated mode + * @property {boolean} is_nsfw + * @property {AlbumRightsDTO} rights + * @property {AlbumProtectionPolicy} policy + * @property {?string} min_taken_at + * @property {?string} max_taken_at + * @property {?SortingCriterion} sorting + * @property {boolean} is_tag_album always true + */ /** + * @typedef SmartAlbum + * + * @property {string} id + * @property {string} title + * @property {Photo[]} [photos] + * @property {?Thumb} thumb + * @property {AlbumRightsDTO} rights + * @property {AlbumProtectionPolicy} policy + */ /** + * @typedef Thumb + * + * @property {string} id + * @property {string} type + * @property {string} thumb + * @property {?string} thumb2x + */ /** + * @typedef SharingInfo + * + * DTO returned by `Sharing::list` + * + * @property {{id: number, album_id: string, user_id: number, username: string, title: string}[]} shared + * @property {{id: string, title: string}[]} albums + * @property {{id: number, username: string}[]} users + */ /** + * @typedef SearchResult + * + * DTO returned by `Search::run` + * + * @property {Album[]} albums + * @property {TagAlbum[]} tag_albums + * @property {Photo[]} photos + * @property {string} checksum - checksum of the search result to + * efficiently determine if the result has + * changed since the last time + */ /** + * @typedef Albums + * + * @property {SmartAlbums} smart_albums + * @property {TagAlbum[]} tag_albums + * @property {Album[]} albums + * @property {Album[]} shared_albums + */ /** + * @typedef SmartAlbums + * + * @property {?SmartAlbum} unsorted + * @property {?SmartAlbum} starred + * @property {?SmartAlbum} public + * @property {?SmartAlbum} recent + * @property {?SmartAlbum} on_this_day + */ /** + * The IDs of the built-in, smart albums. + * + * @type {Readonly<{RECENT: string, STARRED: string, PUBLIC: string, UNSORTED: string, ON_THIS_DAY: string}>} + */var SmartAlbumID=Object.freeze({UNSORTED:"unsorted",STARRED:"starred",PUBLIC:"public",RECENT:"recent",ON_THIS_DAY:"on_this_day"});/** + * @typedef User + * + * @property {number} id + * @property {string} username + * @property {string} email + * @property {boolean} has_token + */ /** + * @typedef UserWithCapabilitiesDTO + * + * @property {number} id + * @property {string} username + * @property {boolean} may_administrate + * @property {boolean} may_upload + * @property {boolean} may_edit_own_settings + */ /** + * @typedef WebAuthnCredential + * + * @property {string} id + */ /** + * @typedef PositionData + * + * @property {?string} id - album ID + * @property {?string} title - album title + * @property {Photo[]} photos + * @property {?string} track_url - URL to GPX track + */ /** + * @typedef ConfigSetting + * + * @property {number} id + * @property {string} key + * @property {?string} value - TODO: this should have the correct type depending on `type_range` + * @property {string} cat + * @property {string} type_range + * @property {number} confidentiality - `0`: public setting, `2`: informational, `3`: admin only + * @property {string} description + */ /** + * @typedef LogEntry + * + * @property {number} id + * @property {string} created_at + * @property {string} updated_at + * @property {string} type + * @property {string} function + * @property {number} line + * @property {string} text + */ /** + * @typedef DiagnosticInfo + * + * @property {string[]} errors + * @property {string[]} infos + * @property {string[]} configs + * @property {number} update - `0`: not on master branch; `1`: up-to-date; `2`: not up-to-date; `3`: requires migration + */ /** + * @typedef FrameSettings + * + * @property {number} refresh + */ /** + * @typedef InitializationData + * + * @property {?User} user + * @property {GlobalRightsDTO} rights + * @property {boolean} update_json + * @property {boolean} update_available + * @property {Object.} locale + * @property {ConfigurationData} config + */ /** + * @typedef Feed + * + * @property {string} url + * @property {string} mimetype + * @property {string} title + */ /** + * @typedef ConfigurationData + * + * @property {string} album_decoration + * @property {string} album_decoration_orientation + * @property {string} album_subtitle_type + * @property {string} allow_username_change - actually a boolean + * @property {string} check_for_updates - actually a boolean + * @property {string} [default_license] + * @property {string} [delete_imported] - actually a boolean + * @property {string} grants_download - actually a boolean + * @property {string} [dropbox_key] + * @property {string} editor_enabled - actually a boolean + * @property {string} rss_enable - actually a boolean + * @property {Feed[]} rss_feeds - array of RSS feeds + * @property {string} grants_full_photo_access - actually a boolean + * @property {string} image_overlay_type + * @property {string} landing_page_enable - actually a boolean + * @property {string} lang + * @property {string[]} lang_available + * @property {string} layout - `square`, `justified` or `unjustified` + * @property {string} [location] + * @property {string} location_decoding - actually a boolean + * @property {string} location_show - actually a boolean + * @property {string} location_show_public - actually a boolean + * @property {string} map_display - actually a boolean + * @property {string} map_display_direction - actually a boolean + * @property {string} map_display_public - actually a boolean + * @property {string} map_include_subalbums - actually a boolean + * @property {string} map_provider + * @property {string} new_photos_notification - actually a boolean + * @property {string} nsfw_blur - actually a boolean + * @property {string} nsfw_visible - actually a boolean + * @property {string} nsfw_warning - actually a boolean + * @property {string} nsfw_warning_admin - actually a boolean + * @property {string} nsfw_banner_override - custom HTML instead of the default NSFW banner + * @property {string} public_photos_hidden - actually a boolean + * @property {string} public_search - actually a boolean + * @property {string} share_button_visible - actually a boolean + * @property {string} [skip_duplicates] - actually a boolean + * @property {SortingCriterion} sorting_albums + * @property {SortingCriterion} sorting_photos + * @property {string} swipe_tolerance_x - actually a number + * @property {string} swipe_tolerance_y - actually a number + * @property {string} upload_processing_limit - actually a number + * @property {?Version} version - Version number + * @property {SmartAlbumVisibility} smart_album_visibilty - visibility of smart albums + */ /** + * The JSON object for incremental reports sent by the + * back-end within a streamed response. + * + * @typedef ImportReport + * + * @property {string} type - indicates the type of report; + * `'progress'`: {@link ImportProgressReport}, + * `'event'`: {@link ImportEventReport} + */ /** + * The JSON object for cumulative progress reports sent by the + * back-end within a streamed response. + * + * @typedef ImportProgressReport + * + * @property {string} type - `'progress'` + * @property {string} path + * @property {number} progress + */ /** + * The JSON object for events sent by the back-end within a streamed response. + * + * @typedef ImportEventReport + * + * @property {string} type - `'event'` + * @property {string} subtype - the subtype of event; equals the base name of the exception class which caused this event on the back-end + * @property {number} severity - either `'debug'`, `'info'`, `'notice'`, `'warning'`, `'error'`, `'critical'` or `'emergency'` + * @property {?string} path - the path to the affected file or directory + * @property {string} message - a message text + */ /** + * The JSON object for Policy on Albums + * + * @typedef AlbumProtectionPolicy + * + * @property {is_nsfw} boolean + * @property {boolean} is_public + * @property {boolean} is_link_required + * @property {boolean} is_password_required + * @property {boolean} grants_full_photo_access + * @property {boolean} grants_download + */ /** + * The JSON object for Rights on users management + * + * @typedef UserManagementRightsDTO + * + * @property {boolean} can_create + * @property {boolean} can_list + * @property {boolean} can_edit + * @property {boolean} can_delete + */ /** + * The JSON object for Rights on a User + * + * @typedef UserRightsDTO + * + * @property {boolean} can_edit + * @property {boolean} can_use_2fa + */ /** + * The JSON object for Rights on Settings + * + * @typedef SettingsRightsDTO + * + * @property {boolean} can_edit + * @property {boolean} can_see_logs + * @property {boolean} can_clear_logs + * @property {boolean} can_see_diagnostics + * @property {boolean} can_update + */ /** + * The JSON object for Rights on Settings + * + * @typedef RootAlbumRightsDTO + * + * @property {boolean} can_edit + * @property {boolean} can_upload + * @property {boolean} can_download + * @property {boolean} can_import_from_server + */ /** + * The JSON object for Rights on Photos + * + * @typedef PhotoRightsDTO + * + * @property {boolean} can_edit + * @property {boolean} can_download + * @property {boolean} can_access_full_photo + */ /** + * The JSON object for Rights on Album + * + * @typedef AlbumRightsDTO + * + * @property {boolean} can_edit + * @property {boolean} can_share_with_users + * @property {boolean} can_download + * @property {boolean} can_upload + */ /** + * The JSON object for Rights on Global Application + * + * @typedef GlobalRightsDTO + * + * @property {RootAlbumRightsDTO} root_album + * @property {SettingsRightsDTO} settings + * @property {UserManagementRightsDTO} user_management + * @property {UserRightsDTO} user + */ /** + * The JSON object containing the visibility of smart albums + * + * @typedef SmartAlbumVisibility + * + * @property {boolean} recent + * @property {boolean} starred + * @property {boolean} on_this_day + */ /** + * MIT License + * + * Copyright (c) Italo Israel Baeza Cabrera + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */var _routes=/*#__PURE__*/new WeakMap();var _headers=/*#__PURE__*/new WeakMap();var _includeCredentials=/*#__PURE__*/new WeakMap();var _fetch=/*#__PURE__*/new WeakSet();var _parseIncomingServerOptions=/*#__PURE__*/new WeakSet();var _parseOutgoingCredentials=/*#__PURE__*/new WeakSet();var WebAuthn=/*#__PURE__*/function(_register,_login){/** + * Create a new WebAuthn instance. + * + * @param routes {{registerOptions: string, register: string, loginOptions: string, login: string}} + * @param headers {{string}} + * @param includeCredentials {boolean} + * @param xcsrfToken {string|null} Either a csrf token (40 chars) or xsrfToken (224 chars) + */function WebAuthn(){var routes=arguments.length>0&&arguments[0]!==undefined?arguments[0]:{};var _headers2=arguments.length>1&&arguments[1]!==undefined?arguments[1]:{};var includeCredentials=arguments.length>2&&arguments[2]!==undefined?arguments[2]:false;var xcsrfToken=arguments.length>3&&arguments[3]!==undefined?arguments[3]:null;_classCallCheck(this,WebAuthn);/** + * Parses the outgoing credentials from the browser to the server. + * + * @param credentials {Credential|PublicKeyCredential} + * @return {{response: {string}, rawId: string, id: string, type: string}} + */_classPrivateMethodInitSpec(this,_parseOutgoingCredentials);/** + * Parses the Public Key Options received from the Server for the browser. + * + * @param publicKey {Object} + * @returns {Object} + */_classPrivateMethodInitSpec(this,_parseIncomingServerOptions);/** + * Returns a fetch promise to resolve later. + * + * @param data {Object} + * @param route {string} + * @param headers {{string}} + * @returns {Promise} + */_classPrivateMethodInitSpec(this,_fetch);/** + * Routes for WebAuthn assertion (login) and attestation (register). + * + * @type {{registerOptions: string, register: string, loginOptions: string, login: string, }} + */_classPrivateFieldInitSpec(this,_routes,{writable:true,value:{registerOptions:"webauthn/register/options",register:"webauthn/register",loginOptions:"webauthn/login/options",login:"webauthn/login"}});/** + * Headers to use in ALL requests done. + * + * @type {{Accept: string, "Content-Type": string, "X-Requested-With": string}} + */_classPrivateFieldInitSpec(this,_headers,{writable:true,value:{Accept:"application/json","Content-Type":"application/json","X-Requested-With":"XMLHttpRequest"}});/** + * If set to true, the credentials option will be set to 'include' on all fetch calls, + * or else it will use the default 'same-origin'. Use this if the backend is not the + * same origin as the client or the XSRF protection will break without the session. + * + * @type {boolean} + */_classPrivateFieldInitSpec(this,_includeCredentials,{writable:true,value:false});Object.assign(_classPrivateFieldGet(this,_routes),routes);Object.assign(_classPrivateFieldGet(this,_headers),_headers2);_classPrivateFieldSet(this,_includeCredentials,includeCredentials);var xsrfToken;var csrfToken;if(xcsrfToken===null){// If the developer didn't issue an XSRF token, we will find it ourselves. +xsrfToken=_classStaticPrivateFieldSpecGet(WebAuthn,WebAuthn,_XsrfToken);csrfToken=_classStaticPrivateFieldSpecGet(WebAuthn,WebAuthn,_firstInputWithCsrfToken);}else{// Check if it is a CSRF or XSRF token +if(xcsrfToken.length===40){csrfToken=xcsrfToken;}else if(xcsrfToken.length===224){xsrfToken=xcsrfToken;}else{throw new TypeError("CSRF token or XSRF token provided does not match requirements. Must be 40 or 224 characters.");}}if(xsrfToken!==null){var _classPrivateFieldGet2,_XXSRFTOKEN,_classPrivateFieldGet3;(_classPrivateFieldGet3=(_classPrivateFieldGet2=_classPrivateFieldGet(this,_headers))[_XXSRFTOKEN="X-XSRF-TOKEN"])!==null&&_classPrivateFieldGet3!==void 0?_classPrivateFieldGet3:_classPrivateFieldGet2[_XXSRFTOKEN]=xsrfToken;}else if(csrfToken!==null){var _classPrivateFieldGet4,_XCSRFTOKEN,_classPrivateFieldGet5;(_classPrivateFieldGet5=(_classPrivateFieldGet4=_classPrivateFieldGet(this,_headers))[_XCSRFTOKEN="X-CSRF-TOKEN"])!==null&&_classPrivateFieldGet5!==void 0?_classPrivateFieldGet5:_classPrivateFieldGet4[_XCSRFTOKEN]=csrfToken;}else{// We didn't find it, and since is required, we will bail out. +throw new TypeError('Ensure a CSRF/XSRF token is manually set, or provided in a cookie "XSRF-TOKEN" or or there is meta tag named "csrf-token".');}}/** + * Returns the CSRF token if it exists as a form input tag. + * + * @returns string + * @throws TypeError + */_createClass(WebAuthn,[{key:"register",value:/** + * Register the user credentials from the browser/device. + * + * You can add request input if you are planning to register a user with WebAuthn from scratch. + * + * @param request {{string}} + * @param response {{string}} + * @returns Promise + */function register(){return(_register=_register||_asyncToGenerator(/*#__PURE__*/_regeneratorRuntime().mark(function _callee(){var request,response,optionsResponse,json,publicKey,credentials,publicKeyCredential,_args=arguments;return _regeneratorRuntime().wrap(function _callee$(_context){while(1)switch(_context.prev=_context.next){case 0:request=_args.length>0&&_args[0]!==undefined?_args[0]:{};response=_args.length>1&&_args[1]!==undefined?_args[1]:{};_context.next=4;return _classPrivateMethodGet(this,_fetch,_fetch2).call(this,request,_classPrivateFieldGet(this,_routes).registerOptions);case 4:optionsResponse=_context.sent;_context.next=7;return optionsResponse.json();case 7:json=_context.sent;publicKey=_classPrivateMethodGet(this,_parseIncomingServerOptions,_parseIncomingServerOptions2).call(this,json);_context.next=11;return navigator.credentials.create({publicKey:publicKey});case 11:credentials=_context.sent;publicKeyCredential=_classPrivateMethodGet(this,_parseOutgoingCredentials,_parseOutgoingCredentials2).call(this,credentials);Object.assign(publicKeyCredential,response);Object.assign(publicKeyCredential,request);_context.next=17;return _classPrivateMethodGet(this,_fetch,_fetch2).call(this,publicKeyCredential,_classPrivateFieldGet(this,_routes).register).then(_classStaticPrivateMethodGet(WebAuthn,WebAuthn,_handleResponse));case 17:return _context.abrupt("return",_context.sent);case 18:case"end":return _context.stop();}},_callee,this);}))).apply(this,arguments);}/** + * Log in a user with his credentials. + * + * If no credentials are given, the app may return a blank assertion for userless login. + * + * @param request {{string}} + * @param response {{string}} + * @returns Promise + */},{key:"login",value:function login(){return(_login=_login||_asyncToGenerator(/*#__PURE__*/_regeneratorRuntime().mark(function _callee2(){var request,response,optionsResponse,json,publicKey,credentials,publicKeyCredential,_args2=arguments;return _regeneratorRuntime().wrap(function _callee2$(_context2){while(1)switch(_context2.prev=_context2.next){case 0:request=_args2.length>0&&_args2[0]!==undefined?_args2[0]:{};response=_args2.length>1&&_args2[1]!==undefined?_args2[1]:{};_context2.next=4;return _classPrivateMethodGet(this,_fetch,_fetch2).call(this,request,_classPrivateFieldGet(this,_routes).loginOptions);case 4:optionsResponse=_context2.sent;_context2.next=7;return optionsResponse.json();case 7:json=_context2.sent;publicKey=_classPrivateMethodGet(this,_parseIncomingServerOptions,_parseIncomingServerOptions2).call(this,json);_context2.next=11;return navigator.credentials.get({publicKey:publicKey});case 11:credentials=_context2.sent;publicKeyCredential=_classPrivateMethodGet(this,_parseOutgoingCredentials,_parseOutgoingCredentials2).call(this,credentials);Object.assign(publicKeyCredential,response);console.log(publicKeyCredential);_context2.next=17;return _classPrivateMethodGet(this,_fetch,_fetch2).call(this,publicKeyCredential,_classPrivateFieldGet(this,_routes).login,response).then(_classStaticPrivateMethodGet(WebAuthn,WebAuthn,_handleResponse));case 17:return _context2.abrupt("return",_context2.sent);case 18:case"end":return _context2.stop();}},_callee2,this);}))).apply(this,arguments);}/** + * Checks if the browser supports WebAuthn. + * + * @returns {boolean} + */}],[{key:"supportsWebAuthn",value:function supportsWebAuthn(){return typeof PublicKeyCredential!="undefined";}/** + * Checks if the browser doesn't support WebAuthn. + * + * @returns {boolean} + */},{key:"doesntSupportWebAuthn",value:function doesntSupportWebAuthn(){return!this.supportsWebAuthn();}}]);return WebAuthn;}();_class=WebAuthn;function _get_firstInputWithCsrfToken(){// First, try finding an CSRF Token in the head. +var token=Array.from(document.head.getElementsByTagName("meta")).find(function(element){return element.name==="csrf-token";});if(token){return token.content;}// Then, try to find a hidden input containing the CSRF token. +token=Array.from(document.getElementsByTagName("input")).find(function(input){return input.name==="_token"&&input.type==="hidden";});if(token){return token.value;}return null;}/** + * Returns the value of the XSRF token if it exists in a cookie. + * + * Inspired by https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie#example_2_get_a_sample_cookie_named_test2 + * + * @returns {?string} + */function _get_XsrfToken(){var cookie=document.cookie.split(";").find(function(row){return /^\s*(X-)?[XC]SRF-TOKEN\s*=/.test(row);});// We must remove all '%3D' from the end of the string. +// Background: +// The actual binary value of the CSFR value is encoded in Base64. +// If the length of original, binary value is not a multiple of 3 bytes, +// the encoding gets padded with `=` on the right; i.e. there might be +// zero, one or two `=` at the end of the encoded value. +// If the value is sent from the server to the client as part of a cookie, +// the `=` character is URL-encoded as `%3D`, because `=` is already used +// to separate a cookie key from its value. +// When we send back the value to the server as part of an AJAX request, +// Laravel expects an unpadded value. +// Hence, we must remove the `%3D`. +return cookie?cookie.split("=")[1].trim().replaceAll("%3D",""):null;}function _fetch2(data,route){var headers=arguments.length>2&&arguments[2]!==undefined?arguments[2]:{};var url=new URL(route,window.location.origin).href;return fetch(url,{method:"POST",credentials:_classPrivateFieldGet(this,_includeCredentials)?"include":"same-origin",redirect:"error",headers:_objectSpread(_objectSpread({},_classPrivateFieldGet(this,_headers)),headers),body:JSON.stringify(data)});}/** + * Decodes a BASE64 URL string into a normal string. + * + * @param input {string} + * @returns {string|Iterable} + */function _base64UrlDecode(input){input=input.replace(/-/g,"+").replace(/_/g,"/");var pad=input.length%4;if(pad){if(pad===1){throw new Error("InvalidLengthError: Input base64url string is the wrong length to determine padding");}input+=new Array(5-pad).join("=");}return atob(input);}/** + * Transform a string into Uint8Array instance. + * + * @param input {string} + * @param useAtob {boolean} + * @returns {Uint8Array} + */function _uint8Array(input){var useAtob=arguments.length>1&&arguments[1]!==undefined?arguments[1]:false;return Uint8Array.from(useAtob?atob(input):_classStaticPrivateMethodGet(_class,_class,_base64UrlDecode).call(_class,input),function(c){return c.charCodeAt(0);});}/** + * Encodes an array of bytes to a BASE64 URL string + * + * @param arrayBuffer {ArrayBuffer|Uint8Array} + * @returns {string} + */function _arrayToBase64String(arrayBuffer){return btoa(String.fromCharCode.apply(String,_toConsumableArray(new Uint8Array(arrayBuffer))));}function _parseIncomingServerOptions2(publicKey){console.debug(publicKey);publicKey.challenge=_classStaticPrivateMethodGet(_class,_class,_uint8Array).call(_class,publicKey.challenge);if("user"in publicKey){publicKey.user=_objectSpread(_objectSpread({},publicKey.user),{},{id:_classStaticPrivateMethodGet(_class,_class,_uint8Array).call(_class,publicKey.user.id)});}["excludeCredentials","allowCredentials"].filter(function(key){return key in publicKey;}).forEach(function(key){publicKey[key]=publicKey[key].map(function(data){return _objectSpread(_objectSpread({},data),{},{id:_classStaticPrivateMethodGet(_class,_class,_uint8Array).call(_class,data.id)});});});console.log(publicKey);return publicKey;}function _parseOutgoingCredentials2(credentials){var parseCredentials={id:credentials.id,type:credentials.type,rawId:_classStaticPrivateMethodGet(_class,_class,_arrayToBase64String).call(_class,credentials.rawId),response:{}};["clientDataJSON","attestationObject","authenticatorData","signature","userHandle"].filter(function(key){return key in credentials.response;}).forEach(function(key){return parseCredentials.response[key]=_classStaticPrivateMethodGet(_class,_class,_arrayToBase64String).call(_class,credentials.response[key]);});return parseCredentials;}/** + * Handles the response from the Server. + * + * Throws the entire response if is not OK (HTTP 2XX). + * + * @param response {Response} + * @returns Promise + * @throws Response + */function _handleResponse(response){if(!response.ok){throw response;}// Here we will do a small trick. Since most of the responses from the server +// are JSON, we will automatically parse the JSON body from the response. If +// it's not JSON, we will push the body verbatim and let the dev handle it. +return new Promise(function(resolve){response.json().then(function(json){return resolve(json);})["catch"](function(){return resolve(response.body);});});}var _XsrfToken={get:_get_XsrfToken,set:void 0};var _firstInputWithCsrfToken={get:_get_firstInputWithCsrfToken,set:void 0}; \ No newline at end of file diff --git a/public/dist/landing.css b/public/dist/landing.css index 0b2be92a568..6eef016d8f6 100644 --- a/public/dist/landing.css +++ b/public/dist/landing.css @@ -1,4 +1,24 @@ -@import url("https://fonts.googleapis.com/css?family=Roboto:300,400,700,900"); +/* roboto-300 - vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic */ +@font-face { + font-family: "Roboto"; + font-style: normal; + font-weight: 300; + src: local(""), url("../fonts/roboto-v29-300.woff2") format("woff2"); +} +/* roboto-regular - vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic */ +@font-face { + font-family: "Roboto"; + font-style: normal; + font-weight: 400; + src: local(""), url("../fonts/roboto-v29-400.woff2") format("woff2"); +} +/* roboto-700 - vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic */ +@font-face { + font-family: "Roboto"; + font-style: normal; + font-weight: 700; + src: local(""), url("../fonts/roboto-v29-700.woff2") format("woff2"); +} html, body, div, @@ -85,7 +105,8 @@ video { border: 0; font: inherit; font-size: 100%; - vertical-align: baseline; } + vertical-align: baseline; +} article, aside, @@ -98,37 +119,45 @@ hgroup, menu, nav, section { - display: block; } + display: block; +} body { - line-height: 1; } + line-height: 1; +} ol, ul { - list-style: none; } + list-style: none; +} blockquote, q { - quotes: none; } + quotes: none; +} blockquote:before, blockquote:after, q:before, q:after { content: ""; - content: none; } + content: none; +} table { border-collapse: collapse; - border-spacing: 0; } + border-spacing: 0; +} em, i { - font-style: italic; } + font-style: italic; +} strong, b { - font-weight: bold; } + font-weight: bold; +} * { -webkit-user-select: none; @@ -137,31 +166,34 @@ b { user-select: none; -webkit-transition: color 0.3s, opacity 0.3s ease-out, -webkit-transform 0.3s ease-out, -webkit-box-shadow 0.3s; transition: color 0.3s, opacity 0.3s ease-out, -webkit-transform 0.3s ease-out, -webkit-box-shadow 0.3s; - -o-transition: color 0.3s, opacity 0.3s ease-out, transform 0.3s ease-out, box-shadow 0.3s; transition: color 0.3s, opacity 0.3s ease-out, transform 0.3s ease-out, box-shadow 0.3s; - transition: color 0.3s, opacity 0.3s ease-out, transform 0.3s ease-out, box-shadow 0.3s, -webkit-transform 0.3s ease-out, -webkit-box-shadow 0.3s; } + transition: color 0.3s, opacity 0.3s ease-out, transform 0.3s ease-out, box-shadow 0.3s, -webkit-transform 0.3s ease-out, -webkit-box-shadow 0.3s; +} html, body { font-family: "Roboto", sans-serif; background: #000000; - overflow: hidden; } + overflow: hidden; +} ol, ul { - list-style: none; } + list-style: none; +} a { - text-decoration: none; } + text-decoration: none; +} @font-face { font-family: "socials"; src: url("fonts/socials.eot?egvu10"); src: url("fonts/socials.eot?egvu10#iefix") format("embedded-opentype"), url("fonts/socials.ttf?egvu10") format("truetype"), url("fonts/socials.woff?egvu10") format("woff"), url("fonts/socials.svg?egvu10#socials") format("svg"); font-weight: normal; - font-style: normal; } - -[class^="icon-"], + font-style: normal; +} +[class^=icon-], [class*=" icon-"] { /* use !important to prevent issues with browser extensions that change fonts */ font-family: "socials" !important; @@ -173,31 +205,37 @@ a { line-height: 1; /* Better Font Rendering =========== */ -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; } + -moz-osx-font-smoothing: grayscale; +} .icon-facebook2:before { - content: "\ea91"; } + content: "\ea91"; +} .icon-instagram:before { - content: "\ea92"; } + content: "\ea92"; +} .icon-twitter:before { - content: "\ea96"; } + content: "\ea96"; +} .icon-youtube:before { - content: "\ea9d"; } + content: "\ea9d"; +} .icon-flickr2:before { - content: "\eaa4"; } + content: "\eaa4"; +} @font-face { font-family: "icomoon"; src: url("fonts/icomoon.eot?mqsjq9"); src: url("fonts/icomoon.eot?mqsjq9#iefix") format("embedded-opentype"), url("fonts/icomoon.ttf?mqsjq9") format("truetype"), url("fonts/icomoon.woff?mqsjq9") format("woff"), url("fonts/icomoon.svg?mqsjq9#icomoon") format("svg"); font-weight: normal; - font-style: normal; } - -[class^="icon-"], + font-style: normal; +} +[class^=icon-], [class*=" icon-"] { /* use !important to prevent issues with browser extensions that change fonts */ font-family: "icomoon" !important; @@ -209,2542 +247,3388 @@ a { line-height: 1; /* Better Font Rendering =========== */ -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; } + -moz-osx-font-smoothing: grayscale; +} .icon-3d_rotation:before { - content: "\e84d"; } + content: "\e84d"; +} .icon-ac_unit:before { - content: "\eb3b"; } + content: "\eb3b"; +} .icon-alarm:before { - content: "\e855"; } + content: "\e855"; +} .icon-access_alarms:before { - content: "\e191"; } + content: "\e191"; +} .icon-schedule:before { - content: "\e8b5"; } + content: "\e8b5"; +} .icon-accessibility:before { - content: "\e84e"; } + content: "\e84e"; +} .icon-accessible:before { - content: "\e914"; } + content: "\e914"; +} .icon-account_balance:before { - content: "\e84f"; } + content: "\e84f"; +} .icon-account_balance_wallet:before { - content: "\e850"; } + content: "\e850"; +} .icon-account_box:before { - content: "\e851"; } + content: "\e851"; +} .icon-account_circle:before { - content: "\e853"; } + content: "\e853"; +} .icon-adb:before { - content: "\e60e"; } + content: "\e60e"; +} .icon-add:before { - content: "\e145"; } + content: "\e145"; +} .icon-add_a_photo:before { - content: "\e439"; } + content: "\e439"; +} .icon-alarm_add:before { - content: "\e856"; } + content: "\e856"; +} .icon-add_alert:before { - content: "\e003"; } + content: "\e003"; +} .icon-add_box:before { - content: "\e146"; } + content: "\e146"; +} .icon-add_circle:before { - content: "\e147"; } + content: "\e147"; +} .icon-control_point:before { - content: "\e3ba"; } + content: "\e3ba"; +} .icon-add_location:before { - content: "\e567"; } + content: "\e567"; +} .icon-add_shopping_cart:before { - content: "\e854"; } + content: "\e854"; +} .icon-queue:before { - content: "\e03c"; } + content: "\e03c"; +} .icon-add_to_queue:before { - content: "\e05c"; } + content: "\e05c"; +} .icon-adjust:before { - content: "\e39e"; } + content: "\e39e"; +} .icon-airline_seat_flat:before { - content: "\e630"; } + content: "\e630"; +} .icon-airline_seat_flat_angled:before { - content: "\e631"; } + content: "\e631"; +} .icon-airline_seat_individual_suite:before { - content: "\e632"; } + content: "\e632"; +} .icon-airline_seat_legroom_extra:before { - content: "\e633"; } + content: "\e633"; +} .icon-airline_seat_legroom_normal:before { - content: "\e634"; } + content: "\e634"; +} .icon-airline_seat_legroom_reduced:before { - content: "\e635"; } + content: "\e635"; +} .icon-airline_seat_recline_extra:before { - content: "\e636"; } + content: "\e636"; +} .icon-airline_seat_recline_normal:before { - content: "\e637"; } + content: "\e637"; +} .icon-flight:before { - content: "\e539"; } + content: "\e539"; +} .icon-airplanemode_inactive:before { - content: "\e194"; } + content: "\e194"; +} .icon-airplay:before { - content: "\e055"; } + content: "\e055"; +} .icon-airport_shuttle:before { - content: "\eb3c"; } + content: "\eb3c"; +} .icon-alarm_off:before { - content: "\e857"; } + content: "\e857"; +} .icon-alarm_on:before { - content: "\e858"; } + content: "\e858"; +} .icon-album:before { - content: "\e019"; } + content: "\e019"; +} .icon-all_inclusive:before { - content: "\eb3d"; } + content: "\eb3d"; +} .icon-all_out:before { - content: "\e90b"; } + content: "\e90b"; +} .icon-android:before { - content: "\e859"; } + content: "\e859"; +} .icon-announcement:before { - content: "\e85a"; } + content: "\e85a"; +} .icon-apps:before { - content: "\e5c3"; } + content: "\e5c3"; +} .icon-archive:before { - content: "\e149"; } + content: "\e149"; +} .icon-arrow_back:before { - content: "\e5c4"; } + content: "\e5c4"; +} .icon-arrow_downward:before { - content: "\e5db"; } + content: "\e5db"; +} .icon-arrow_drop_down:before { - content: "\e5c5"; } + content: "\e5c5"; +} .icon-arrow_drop_down_circle:before { - content: "\e5c6"; } + content: "\e5c6"; +} .icon-arrow_drop_up:before { - content: "\e5c7"; } + content: "\e5c7"; +} .icon-arrow_forward:before { - content: "\e5c8"; } + content: "\e5c8"; +} .icon-arrow_upward:before { - content: "\e5d8"; } + content: "\e5d8"; +} .icon-art_track:before { - content: "\e060"; } + content: "\e060"; +} .icon-aspect_ratio:before { - content: "\e85b"; } + content: "\e85b"; +} .icon-poll:before { - content: "\e801"; } + content: "\e801"; +} .icon-assignment:before { - content: "\e85d"; } + content: "\e85d"; +} .icon-assignment_ind:before { - content: "\e85e"; } + content: "\e85e"; +} .icon-assignment_late:before { - content: "\e85f"; } + content: "\e85f"; +} .icon-assignment_return:before { - content: "\e860"; } + content: "\e860"; +} .icon-assignment_returned:before { - content: "\e861"; } + content: "\e861"; +} .icon-assignment_turned_in:before { - content: "\e862"; } + content: "\e862"; +} .icon-assistant:before { - content: "\e39f"; } + content: "\e39f"; +} .icon-flag:before { - content: "\e153"; } + content: "\e153"; +} .icon-attach_file:before { - content: "\e226"; } + content: "\e226"; +} .icon-attach_money:before { - content: "\e227"; } + content: "\e227"; +} .icon-attachment:before { - content: "\e2bc"; } + content: "\e2bc"; +} .icon-audiotrack:before { - content: "\e3a1"; } + content: "\e3a1"; +} .icon-autorenew:before { - content: "\e863"; } + content: "\e863"; +} .icon-av_timer:before { - content: "\e01b"; } + content: "\e01b"; +} .icon-backspace:before { - content: "\e14a"; } + content: "\e14a"; +} .icon-cloud_upload:before { - content: "\e2c3"; } + content: "\e2c3"; +} .icon-battery_alert:before { - content: "\e19c"; } + content: "\e19c"; +} .icon-battery_charging_full:before { - content: "\e1a3"; } + content: "\e1a3"; +} .icon-battery_std:before { - content: "\e1a5"; } + content: "\e1a5"; +} .icon-battery_unknown:before { - content: "\e1a6"; } + content: "\e1a6"; +} .icon-beach_access:before { - content: "\eb3e"; } + content: "\eb3e"; +} .icon-beenhere:before { - content: "\e52d"; } + content: "\e52d"; +} .icon-block:before { - content: "\e14b"; } + content: "\e14b"; +} .icon-bluetooth:before { - content: "\e1a7"; } + content: "\e1a7"; +} .icon-bluetooth_searching:before { - content: "\e1aa"; } + content: "\e1aa"; +} .icon-bluetooth_connected:before { - content: "\e1a8"; } + content: "\e1a8"; +} .icon-bluetooth_disabled:before { - content: "\e1a9"; } + content: "\e1a9"; +} .icon-blur_circular:before { - content: "\e3a2"; } + content: "\e3a2"; +} .icon-blur_linear:before { - content: "\e3a3"; } + content: "\e3a3"; +} .icon-blur_off:before { - content: "\e3a4"; } + content: "\e3a4"; +} .icon-blur_on:before { - content: "\e3a5"; } + content: "\e3a5"; +} .icon-class:before { - content: "\e86e"; } + content: "\e86e"; +} .icon-turned_in:before { - content: "\e8e6"; } + content: "\e8e6"; +} .icon-turned_in_not:before { - content: "\e8e7"; } + content: "\e8e7"; +} .icon-border_all:before { - content: "\e228"; } + content: "\e228"; +} .icon-border_bottom:before { - content: "\e229"; } + content: "\e229"; +} .icon-border_clear:before { - content: "\e22a"; } + content: "\e22a"; +} .icon-border_color:before { - content: "\e22b"; } + content: "\e22b"; +} .icon-border_horizontal:before { - content: "\e22c"; } + content: "\e22c"; +} .icon-border_inner:before { - content: "\e22d"; } + content: "\e22d"; +} .icon-border_left:before { - content: "\e22e"; } + content: "\e22e"; +} .icon-border_outer:before { - content: "\e22f"; } + content: "\e22f"; +} .icon-border_right:before { - content: "\e230"; } + content: "\e230"; +} .icon-border_style:before { - content: "\e231"; } + content: "\e231"; +} .icon-border_top:before { - content: "\e232"; } + content: "\e232"; +} .icon-border_vertical:before { - content: "\e233"; } + content: "\e233"; +} .icon-branding_watermark:before { - content: "\e06b"; } + content: "\e06b"; +} .icon-brightness_1:before { - content: "\e3a6"; } + content: "\e3a6"; +} .icon-brightness_2:before { - content: "\e3a7"; } + content: "\e3a7"; +} .icon-brightness_3:before { - content: "\e3a8"; } + content: "\e3a8"; +} .icon-brightness_4:before { - content: "\e3a9"; } + content: "\e3a9"; +} .icon-brightness_low:before { - content: "\e1ad"; } + content: "\e1ad"; +} .icon-brightness_medium:before { - content: "\e1ae"; } + content: "\e1ae"; +} .icon-brightness_high:before { - content: "\e1ac"; } + content: "\e1ac"; +} .icon-brightness_auto:before { - content: "\e1ab"; } + content: "\e1ab"; +} .icon-broken_image:before { - content: "\e3ad"; } + content: "\e3ad"; +} .icon-brush:before { - content: "\e3ae"; } + content: "\e3ae"; +} .icon-bubble_chart:before { - content: "\e6dd"; } + content: "\e6dd"; +} .icon-bug_report:before { - content: "\e868"; } + content: "\e868"; +} .icon-build:before { - content: "\e869"; } + content: "\e869"; +} .icon-burst_mode:before { - content: "\e43c"; } + content: "\e43c"; +} .icon-domain:before { - content: "\e7ee"; } + content: "\e7ee"; +} .icon-business_center:before { - content: "\eb3f"; } + content: "\eb3f"; +} .icon-cached:before { - content: "\e86a"; } + content: "\e86a"; +} .icon-cake:before { - content: "\e7e9"; } + content: "\e7e9"; +} .icon-phone:before { - content: "\e0cd"; } + content: "\e0cd"; +} .icon-call_end:before { - content: "\e0b1"; } + content: "\e0b1"; +} .icon-call_made:before { - content: "\e0b2"; } + content: "\e0b2"; +} .icon-merge_type:before { - content: "\e252"; } + content: "\e252"; +} .icon-call_missed:before { - content: "\e0b4"; } + content: "\e0b4"; +} .icon-call_missed_outgoing:before { - content: "\e0e4"; } + content: "\e0e4"; +} .icon-call_received:before { - content: "\e0b5"; } + content: "\e0b5"; +} .icon-call_split:before { - content: "\e0b6"; } + content: "\e0b6"; +} .icon-call_to_action:before { - content: "\e06c"; } + content: "\e06c"; +} .icon-camera:before { - content: "\e3af"; } + content: "\e3af"; +} .icon-photo_camera:before { - content: "\e412"; } + content: "\e412"; +} .icon-camera_enhance:before { - content: "\e8fc"; } + content: "\e8fc"; +} .icon-camera_front:before { - content: "\e3b1"; } + content: "\e3b1"; +} .icon-camera_rear:before { - content: "\e3b2"; } + content: "\e3b2"; +} .icon-camera_roll:before { - content: "\e3b3"; } + content: "\e3b3"; +} .icon-cancel:before { - content: "\e5c9"; } + content: "\e5c9"; +} .icon-redeem:before { - content: "\e8b1"; } + content: "\e8b1"; +} .icon-card_membership:before { - content: "\e8f7"; } + content: "\e8f7"; +} .icon-card_travel:before { - content: "\e8f8"; } + content: "\e8f8"; +} .icon-casino:before { - content: "\eb40"; } + content: "\eb40"; +} .icon-cast:before { - content: "\e307"; } + content: "\e307"; +} .icon-cast_connected:before { - content: "\e308"; } + content: "\e308"; +} .icon-center_focus_strong:before { - content: "\e3b4"; } + content: "\e3b4"; +} .icon-center_focus_weak:before { - content: "\e3b5"; } + content: "\e3b5"; +} .icon-change_history:before { - content: "\e86b"; } + content: "\e86b"; +} .icon-chat:before { - content: "\e0b7"; } + content: "\e0b7"; +} .icon-chat_bubble:before { - content: "\e0ca"; } + content: "\e0ca"; +} .icon-chat_bubble_outline:before { - content: "\e0cb"; } + content: "\e0cb"; +} .icon-check:before { - content: "\e5ca"; } + content: "\e5ca"; +} .icon-check_box:before { - content: "\e834"; } + content: "\e834"; +} .icon-check_box_outline_blank:before { - content: "\e835"; } + content: "\e835"; +} .icon-check_circle:before { - content: "\e86c"; } + content: "\e86c"; +} .icon-navigate_before:before { - content: "\e408"; } + content: "\e408"; +} .icon-navigate_next:before { - content: "\e409"; } + content: "\e409"; +} .icon-child_care:before { - content: "\eb41"; } + content: "\eb41"; +} .icon-child_friendly:before { - content: "\eb42"; } + content: "\eb42"; +} .icon-chrome_reader_mode:before { - content: "\e86d"; } + content: "\e86d"; +} .icon-close:before { - content: "\e5cd"; } + content: "\e5cd"; +} .icon-clear_all:before { - content: "\e0b8"; } + content: "\e0b8"; +} .icon-closed_caption:before { - content: "\e01c"; } + content: "\e01c"; +} .icon-wb_cloudy:before { - content: "\e42d"; } + content: "\e42d"; +} .icon-cloud_circle:before { - content: "\e2be"; } + content: "\e2be"; +} .icon-cloud_done:before { - content: "\e2bf"; } + content: "\e2bf"; +} .icon-cloud_download:before { - content: "\e2c0"; } + content: "\e2c0"; +} .icon-cloud_off:before { - content: "\e2c1"; } + content: "\e2c1"; +} .icon-cloud_queue:before { - content: "\e2c2"; } + content: "\e2c2"; +} .icon-code:before { - content: "\e86f"; } + content: "\e86f"; +} .icon-photo_library:before { - content: "\e413"; } + content: "\e413"; +} .icon-collections_bookmark:before { - content: "\e431"; } + content: "\e431"; +} .icon-palette:before { - content: "\e40a"; } + content: "\e40a"; +} .icon-colorize:before { - content: "\e3b8"; } + content: "\e3b8"; +} .icon-comment:before { - content: "\e0b9"; } + content: "\e0b9"; +} .icon-compare:before { - content: "\e3b9"; } + content: "\e3b9"; +} .icon-compare_arrows:before { - content: "\e915"; } + content: "\e915"; +} .icon-laptop:before { - content: "\e31e"; } + content: "\e31e"; +} .icon-confirmation_number:before { - content: "\e638"; } + content: "\e638"; +} .icon-contact_mail:before { - content: "\e0d0"; } + content: "\e0d0"; +} .icon-contact_phone:before { - content: "\e0cf"; } + content: "\e0cf"; +} .icon-contacts:before { - content: "\e0ba"; } + content: "\e0ba"; +} .icon-content_copy:before { - content: "\e14d"; } + content: "\e14d"; +} .icon-content_cut:before { - content: "\e14e"; } + content: "\e14e"; +} .icon-content_paste:before { - content: "\e14f"; } + content: "\e14f"; +} .icon-control_point_duplicate:before { - content: "\e3bb"; } + content: "\e3bb"; +} .icon-copyright:before { - content: "\e90c"; } + content: "\e90c"; +} .icon-mode_edit:before { - content: "\e254"; } + content: "\e254"; +} .icon-create_new_folder:before { - content: "\e2cc"; } + content: "\e2cc"; +} .icon-payment:before { - content: "\e8a1"; } + content: "\e8a1"; +} .icon-crop:before { - content: "\e3be"; } + content: "\e3be"; +} .icon-crop_16_9:before { - content: "\e3bc"; } + content: "\e3bc"; +} .icon-crop_3_2:before { - content: "\e3bd"; } + content: "\e3bd"; +} .icon-crop_landscape:before { - content: "\e3c3"; } + content: "\e3c3"; +} .icon-crop_7_5:before { - content: "\e3c0"; } + content: "\e3c0"; +} .icon-crop_din:before { - content: "\e3c1"; } + content: "\e3c1"; +} .icon-crop_free:before { - content: "\e3c2"; } + content: "\e3c2"; +} .icon-crop_original:before { - content: "\e3c4"; } + content: "\e3c4"; +} .icon-crop_portrait:before { - content: "\e3c5"; } + content: "\e3c5"; +} .icon-crop_rotate:before { - content: "\e437"; } + content: "\e437"; +} .icon-crop_square:before { - content: "\e3c6"; } + content: "\e3c6"; +} .icon-dashboard:before { - content: "\e871"; } + content: "\e871"; +} .icon-data_usage:before { - content: "\e1af"; } + content: "\e1af"; +} .icon-date_range:before { - content: "\e916"; } + content: "\e916"; +} .icon-dehaze:before { - content: "\e3c7"; } + content: "\e3c7"; +} .icon-delete:before { - content: "\e872"; } + content: "\e872"; +} .icon-delete_forever:before { - content: "\e92b"; } + content: "\e92b"; +} .icon-delete_sweep:before { - content: "\e16c"; } + content: "\e16c"; +} .icon-description:before { - content: "\e873"; } + content: "\e873"; +} .icon-desktop_mac:before { - content: "\e30b"; } + content: "\e30b"; +} .icon-desktop_windows:before { - content: "\e30c"; } + content: "\e30c"; +} .icon-details:before { - content: "\e3c8"; } + content: "\e3c8"; +} .icon-developer_board:before { - content: "\e30d"; } + content: "\e30d"; +} .icon-developer_mode:before { - content: "\e1b0"; } + content: "\e1b0"; +} .icon-device_hub:before { - content: "\e335"; } + content: "\e335"; +} .icon-phonelink:before { - content: "\e326"; } + content: "\e326"; +} .icon-devices_other:before { - content: "\e337"; } + content: "\e337"; +} .icon-dialer_sip:before { - content: "\e0bb"; } + content: "\e0bb"; +} .icon-dialpad:before { - content: "\e0bc"; } + content: "\e0bc"; +} .icon-directions:before { - content: "\e52e"; } + content: "\e52e"; +} .icon-directions_bike:before { - content: "\e52f"; } + content: "\e52f"; +} .icon-directions_boat:before { - content: "\e532"; } + content: "\e532"; +} .icon-directions_bus:before { - content: "\e530"; } + content: "\e530"; +} .icon-directions_car:before { - content: "\e531"; } + content: "\e531"; +} .icon-directions_railway:before { - content: "\e534"; } + content: "\e534"; +} .icon-directions_run:before { - content: "\e566"; } + content: "\e566"; +} .icon-directions_transit:before { - content: "\e535"; } + content: "\e535"; +} .icon-directions_walk:before { - content: "\e536"; } + content: "\e536"; +} .icon-disc_full:before { - content: "\e610"; } + content: "\e610"; +} .icon-dns:before { - content: "\e875"; } + content: "\e875"; +} .icon-not_interested:before { - content: "\e033"; } + content: "\e033"; +} .icon-do_not_disturb_alt:before { - content: "\e611"; } + content: "\e611"; +} .icon-do_not_disturb_off:before { - content: "\e643"; } + content: "\e643"; +} .icon-remove_circle:before { - content: "\e15c"; } + content: "\e15c"; +} .icon-dock:before { - content: "\e30e"; } + content: "\e30e"; +} .icon-done:before { - content: "\e876"; } + content: "\e876"; +} .icon-done_all:before { - content: "\e877"; } + content: "\e877"; +} .icon-donut_large:before { - content: "\e917"; } + content: "\e917"; +} .icon-donut_small:before { - content: "\e918"; } + content: "\e918"; +} .icon-drafts:before { - content: "\e151"; } + content: "\e151"; +} .icon-drag_handle:before { - content: "\e25d"; } + content: "\e25d"; +} .icon-time_to_leave:before { - content: "\e62c"; } + content: "\e62c"; +} .icon-dvr:before { - content: "\e1b2"; } + content: "\e1b2"; +} .icon-edit_location:before { - content: "\e568"; } + content: "\e568"; +} .icon-eject:before { - content: "\e8fb"; } + content: "\e8fb"; +} .icon-markunread:before { - content: "\e159"; } + content: "\e159"; +} .icon-enhanced_encryption:before { - content: "\e63f"; } + content: "\e63f"; +} .icon-equalizer:before { - content: "\e01d"; } + content: "\e01d"; +} .icon-error:before { - content: "\e000"; } + content: "\e000"; +} .icon-error_outline:before { - content: "\e001"; } + content: "\e001"; +} .icon-euro_symbol:before { - content: "\e926"; } + content: "\e926"; +} .icon-ev_station:before { - content: "\e56d"; } + content: "\e56d"; +} .icon-insert_invitation:before { - content: "\e24f"; } + content: "\e24f"; +} .icon-event_available:before { - content: "\e614"; } + content: "\e614"; +} .icon-event_busy:before { - content: "\e615"; } + content: "\e615"; +} .icon-event_note:before { - content: "\e616"; } + content: "\e616"; +} .icon-event_seat:before { - content: "\e903"; } + content: "\e903"; +} .icon-exit_to_app:before { - content: "\e879"; } + content: "\e879"; +} .icon-expand_less:before { - content: "\e5ce"; } + content: "\e5ce"; +} .icon-expand_more:before { - content: "\e5cf"; } + content: "\e5cf"; +} .icon-explicit:before { - content: "\e01e"; } + content: "\e01e"; +} .icon-explore:before { - content: "\e87a"; } + content: "\e87a"; +} .icon-exposure:before { - content: "\e3ca"; } + content: "\e3ca"; +} .icon-exposure_neg_1:before { - content: "\e3cb"; } + content: "\e3cb"; +} .icon-exposure_neg_2:before { - content: "\e3cc"; } + content: "\e3cc"; +} .icon-exposure_plus_1:before { - content: "\e3cd"; } + content: "\e3cd"; +} .icon-exposure_plus_2:before { - content: "\e3ce"; } + content: "\e3ce"; +} .icon-exposure_zero:before { - content: "\e3cf"; } + content: "\e3cf"; +} .icon-extension:before { - content: "\e87b"; } + content: "\e87b"; +} .icon-face:before { - content: "\e87c"; } + content: "\e87c"; +} .icon-fast_forward:before { - content: "\e01f"; } + content: "\e01f"; +} .icon-fast_rewind:before { - content: "\e020"; } + content: "\e020"; +} .icon-favorite:before { - content: "\e87d"; } + content: "\e87d"; +} .icon-favorite_border:before { - content: "\e87e"; } + content: "\e87e"; +} .icon-featured_play_list:before { - content: "\e06d"; } + content: "\e06d"; +} .icon-featured_video:before { - content: "\e06e"; } + content: "\e06e"; +} .icon-sms_failed:before { - content: "\e626"; } + content: "\e626"; +} .icon-fiber_dvr:before { - content: "\e05d"; } + content: "\e05d"; +} .icon-fiber_manual_record:before { - content: "\e061"; } + content: "\e061"; +} .icon-fiber_new:before { - content: "\e05e"; } + content: "\e05e"; +} .icon-fiber_pin:before { - content: "\e06a"; } + content: "\e06a"; +} .icon-fiber_smart_record:before { - content: "\e062"; } + content: "\e062"; +} .icon-get_app:before { - content: "\e884"; } + content: "\e884"; +} .icon-file_upload:before { - content: "\e2c6"; } + content: "\e2c6"; +} .icon-filter:before { - content: "\e3d3"; } + content: "\e3d3"; +} .icon-filter_1:before { - content: "\e3d0"; } + content: "\e3d0"; +} .icon-filter_2:before { - content: "\e3d1"; } + content: "\e3d1"; +} .icon-filter_3:before { - content: "\e3d2"; } + content: "\e3d2"; +} .icon-filter_4:before { - content: "\e3d4"; } + content: "\e3d4"; +} .icon-filter_5:before { - content: "\e3d5"; } + content: "\e3d5"; +} .icon-filter_6:before { - content: "\e3d6"; } + content: "\e3d6"; +} .icon-filter_7:before { - content: "\e3d7"; } + content: "\e3d7"; +} .icon-filter_8:before { - content: "\e3d8"; } + content: "\e3d8"; +} .icon-filter_9:before { - content: "\e3d9"; } + content: "\e3d9"; +} .icon-filter_9_plus:before { - content: "\e3da"; } + content: "\e3da"; +} .icon-filter_b_and_w:before { - content: "\e3db"; } + content: "\e3db"; +} .icon-filter_center_focus:before { - content: "\e3dc"; } + content: "\e3dc"; +} .icon-filter_drama:before { - content: "\e3dd"; } + content: "\e3dd"; +} .icon-filter_frames:before { - content: "\e3de"; } + content: "\e3de"; +} .icon-terrain:before { - content: "\e564"; } + content: "\e564"; +} .icon-filter_list:before { - content: "\e152"; } + content: "\e152"; +} .icon-filter_none:before { - content: "\e3e0"; } + content: "\e3e0"; +} .icon-filter_tilt_shift:before { - content: "\e3e2"; } + content: "\e3e2"; +} .icon-filter_vintage:before { - content: "\e3e3"; } + content: "\e3e3"; +} .icon-find_in_page:before { - content: "\e880"; } + content: "\e880"; +} .icon-find_replace:before { - content: "\e881"; } + content: "\e881"; +} .icon-fingerprint:before { - content: "\e90d"; } + content: "\e90d"; +} .icon-first_page:before { - content: "\e5dc"; } + content: "\e5dc"; +} .icon-fitness_center:before { - content: "\eb43"; } + content: "\eb43"; +} .icon-flare:before { - content: "\e3e4"; } + content: "\e3e4"; +} .icon-flash_auto:before { - content: "\e3e5"; } + content: "\e3e5"; +} .icon-flash_off:before { - content: "\e3e6"; } + content: "\e3e6"; +} .icon-flash_on:before { - content: "\e3e7"; } + content: "\e3e7"; +} .icon-flight_land:before { - content: "\e904"; } + content: "\e904"; +} .icon-flight_takeoff:before { - content: "\e905"; } + content: "\e905"; +} .icon-flip:before { - content: "\e3e8"; } + content: "\e3e8"; +} .icon-flip_to_back:before { - content: "\e882"; } + content: "\e882"; +} .icon-flip_to_front:before { - content: "\e883"; } + content: "\e883"; +} .icon-folder:before { - content: "\e2c7"; } + content: "\e2c7"; +} .icon-folder_open:before { - content: "\e2c8"; } + content: "\e2c8"; +} .icon-folder_shared:before { - content: "\e2c9"; } + content: "\e2c9"; +} .icon-folder_special:before { - content: "\e617"; } + content: "\e617"; +} .icon-font_download:before { - content: "\e167"; } + content: "\e167"; +} .icon-format_align_center:before { - content: "\e234"; } + content: "\e234"; +} .icon-format_align_justify:before { - content: "\e235"; } + content: "\e235"; +} .icon-format_align_left:before { - content: "\e236"; } + content: "\e236"; +} .icon-format_align_right:before { - content: "\e237"; } + content: "\e237"; +} .icon-format_bold:before { - content: "\e238"; } + content: "\e238"; +} .icon-format_clear:before { - content: "\e239"; } + content: "\e239"; +} .icon-format_color_fill:before { - content: "\e23a"; } + content: "\e23a"; +} .icon-format_color_reset:before { - content: "\e23b"; } + content: "\e23b"; +} .icon-format_color_text:before { - content: "\e23c"; } + content: "\e23c"; +} .icon-format_indent_decrease:before { - content: "\e23d"; } + content: "\e23d"; +} .icon-format_indent_increase:before { - content: "\e23e"; } + content: "\e23e"; +} .icon-format_italic:before { - content: "\e23f"; } + content: "\e23f"; +} .icon-format_line_spacing:before { - content: "\e240"; } + content: "\e240"; +} .icon-format_list_bulleted:before { - content: "\e241"; } + content: "\e241"; +} .icon-format_list_numbered:before { - content: "\e242"; } + content: "\e242"; +} .icon-format_paint:before { - content: "\e243"; } + content: "\e243"; +} .icon-format_quote:before { - content: "\e244"; } + content: "\e244"; +} .icon-format_shapes:before { - content: "\e25e"; } + content: "\e25e"; +} .icon-format_size:before { - content: "\e245"; } + content: "\e245"; +} .icon-format_strikethrough:before { - content: "\e246"; } + content: "\e246"; +} .icon-format_textdirection_l_to_r:before { - content: "\e247"; } + content: "\e247"; +} .icon-format_textdirection_r_to_l:before { - content: "\e248"; } + content: "\e248"; +} .icon-format_underlined:before { - content: "\e249"; } + content: "\e249"; +} .icon-question_answer:before { - content: "\e8af"; } + content: "\e8af"; +} .icon-forward:before { - content: "\e154"; } + content: "\e154"; +} .icon-forward_10:before { - content: "\e056"; } + content: "\e056"; +} .icon-forward_30:before { - content: "\e057"; } + content: "\e057"; +} .icon-forward_5:before { - content: "\e058"; } + content: "\e058"; +} .icon-free_breakfast:before { - content: "\eb44"; } + content: "\eb44"; +} .icon-fullscreen:before { - content: "\e5d0"; } + content: "\e5d0"; +} .icon-fullscreen_exit:before { - content: "\e5d1"; } + content: "\e5d1"; +} .icon-functions:before { - content: "\e24a"; } + content: "\e24a"; +} .icon-g_translate:before { - content: "\e927"; } + content: "\e927"; +} .icon-games:before { - content: "\e021"; } + content: "\e021"; +} .icon-gavel:before { - content: "\e90e"; } + content: "\e90e"; +} .icon-gesture:before { - content: "\e155"; } + content: "\e155"; +} .icon-gif:before { - content: "\e908"; } + content: "\e908"; +} .icon-goat:before { - content: "\e900"; } + content: "\e900"; +} .icon-golf_course:before { - content: "\eb45"; } + content: "\eb45"; +} .icon-my_location:before { - content: "\e55c"; } + content: "\e55c"; +} .icon-location_searching:before { - content: "\e1b7"; } + content: "\e1b7"; +} .icon-location_disabled:before { - content: "\e1b6"; } + content: "\e1b6"; +} .icon-star:before { - content: "\e838"; } + content: "\e838"; +} .icon-gradient:before { - content: "\e3e9"; } + content: "\e3e9"; +} .icon-grain:before { - content: "\e3ea"; } + content: "\e3ea"; +} .icon-graphic_eq:before { - content: "\e1b8"; } + content: "\e1b8"; +} .icon-grid_off:before { - content: "\e3eb"; } + content: "\e3eb"; +} .icon-grid_on:before { - content: "\e3ec"; } + content: "\e3ec"; +} .icon-people:before { - content: "\e7fb"; } + content: "\e7fb"; +} .icon-group_add:before { - content: "\e7f0"; } + content: "\e7f0"; +} .icon-group_work:before { - content: "\e886"; } + content: "\e886"; +} .icon-hd:before { - content: "\e052"; } + content: "\e052"; +} .icon-hdr_off:before { - content: "\e3ed"; } + content: "\e3ed"; +} .icon-hdr_on:before { - content: "\e3ee"; } + content: "\e3ee"; +} .icon-hdr_strong:before { - content: "\e3f1"; } + content: "\e3f1"; +} .icon-hdr_weak:before { - content: "\e3f2"; } + content: "\e3f2"; +} .icon-headset:before { - content: "\e310"; } + content: "\e310"; +} .icon-headset_mic:before { - content: "\e311"; } + content: "\e311"; +} .icon-healing:before { - content: "\e3f3"; } + content: "\e3f3"; +} .icon-hearing:before { - content: "\e023"; } + content: "\e023"; +} .icon-help:before { - content: "\e887"; } + content: "\e887"; +} .icon-help_outline:before { - content: "\e8fd"; } + content: "\e8fd"; +} .icon-high_quality:before { - content: "\e024"; } + content: "\e024"; +} .icon-highlight:before { - content: "\e25f"; } + content: "\e25f"; +} .icon-highlight_off:before { - content: "\e888"; } + content: "\e888"; +} .icon-restore:before { - content: "\e8b3"; } + content: "\e8b3"; +} .icon-home:before { - content: "\e88a"; } + content: "\e88a"; +} .icon-hot_tub:before { - content: "\eb46"; } + content: "\eb46"; +} .icon-local_hotel:before { - content: "\e549"; } + content: "\e549"; +} .icon-hourglass_empty:before { - content: "\e88b"; } + content: "\e88b"; +} .icon-hourglass_full:before { - content: "\e88c"; } + content: "\e88c"; +} .icon-http:before { - content: "\e902"; } + content: "\e902"; +} .icon-lock:before { - content: "\e897"; } + content: "\e897"; +} .icon-photo:before { - content: "\e410"; } + content: "\e410"; +} .icon-image_aspect_ratio:before { - content: "\e3f5"; } + content: "\e3f5"; +} .icon-import_contacts:before { - content: "\e0e0"; } + content: "\e0e0"; +} .icon-import_export:before { - content: "\e0c3"; } + content: "\e0c3"; +} .icon-important_devices:before { - content: "\e912"; } + content: "\e912"; +} .icon-inbox:before { - content: "\e156"; } + content: "\e156"; +} .icon-indeterminate_check_box:before { - content: "\e909"; } + content: "\e909"; +} .icon-info:before { - content: "\e88e"; } + content: "\e88e"; +} .icon-info_outline:before { - content: "\e88f"; } + content: "\e88f"; +} .icon-input:before { - content: "\e890"; } + content: "\e890"; +} .icon-insert_comment:before { - content: "\e24c"; } + content: "\e24c"; +} .icon-insert_drive_file:before { - content: "\e24d"; } + content: "\e24d"; +} .icon-tag_faces:before { - content: "\e420"; } + content: "\e420"; +} .icon-link:before { - content: "\e157"; } + content: "\e157"; +} .icon-invert_colors:before { - content: "\e891"; } + content: "\e891"; +} .icon-invert_colors_off:before { - content: "\e0c4"; } + content: "\e0c4"; +} .icon-iso:before { - content: "\e3f6"; } + content: "\e3f6"; +} .icon-keyboard:before { - content: "\e312"; } + content: "\e312"; +} .icon-keyboard_arrow_down:before { - content: "\e313"; } + content: "\e313"; +} .icon-keyboard_arrow_left:before { - content: "\e314"; } + content: "\e314"; +} .icon-keyboard_arrow_right:before { - content: "\e315"; } + content: "\e315"; +} .icon-keyboard_arrow_up:before { - content: "\e316"; } + content: "\e316"; +} .icon-keyboard_backspace:before { - content: "\e317"; } + content: "\e317"; +} .icon-keyboard_capslock:before { - content: "\e318"; } + content: "\e318"; +} .icon-keyboard_hide:before { - content: "\e31a"; } + content: "\e31a"; +} .icon-keyboard_return:before { - content: "\e31b"; } + content: "\e31b"; +} .icon-keyboard_tab:before { - content: "\e31c"; } + content: "\e31c"; +} .icon-keyboard_voice:before { - content: "\e31d"; } + content: "\e31d"; +} .icon-kitchen:before { - content: "\eb47"; } + content: "\eb47"; +} .icon-label:before { - content: "\e892"; } + content: "\e892"; +} .icon-label_outline:before { - content: "\e893"; } + content: "\e893"; +} .icon-language:before { - content: "\e894"; } + content: "\e894"; +} .icon-laptop_chromebook:before { - content: "\e31f"; } + content: "\e31f"; +} .icon-laptop_mac:before { - content: "\e320"; } + content: "\e320"; +} .icon-laptop_windows:before { - content: "\e321"; } + content: "\e321"; +} .icon-last_page:before { - content: "\e5dd"; } + content: "\e5dd"; +} .icon-open_in_new:before { - content: "\e89e"; } + content: "\e89e"; +} .icon-layers:before { - content: "\e53b"; } + content: "\e53b"; +} .icon-layers_clear:before { - content: "\e53c"; } + content: "\e53c"; +} .icon-leak_add:before { - content: "\e3f8"; } + content: "\e3f8"; +} .icon-leak_remove:before { - content: "\e3f9"; } + content: "\e3f9"; +} .icon-lens:before { - content: "\e3fa"; } + content: "\e3fa"; +} .icon-library_books:before { - content: "\e02f"; } + content: "\e02f"; +} .icon-library_music:before { - content: "\e030"; } + content: "\e030"; +} .icon-lightbulb_outline:before { - content: "\e90f"; } + content: "\e90f"; +} .icon-line_style:before { - content: "\e919"; } + content: "\e919"; +} .icon-line_weight:before { - content: "\e91a"; } + content: "\e91a"; +} .icon-linear_scale:before { - content: "\e260"; } + content: "\e260"; +} .icon-linked_camera:before { - content: "\e438"; } + content: "\e438"; +} .icon-list:before { - content: "\e896"; } + content: "\e896"; +} .icon-live_help:before { - content: "\e0c6"; } + content: "\e0c6"; +} .icon-live_tv:before { - content: "\e639"; } + content: "\e639"; +} .icon-local_play:before { - content: "\e553"; } + content: "\e553"; +} .icon-local_airport:before { - content: "\e53d"; } + content: "\e53d"; +} .icon-local_atm:before { - content: "\e53e"; } + content: "\e53e"; +} .icon-local_bar:before { - content: "\e540"; } + content: "\e540"; +} .icon-local_cafe:before { - content: "\e541"; } + content: "\e541"; +} .icon-local_car_wash:before { - content: "\e542"; } + content: "\e542"; +} .icon-local_convenience_store:before { - content: "\e543"; } + content: "\e543"; +} .icon-restaurant_menu:before { - content: "\e561"; } + content: "\e561"; +} .icon-local_drink:before { - content: "\e544"; } + content: "\e544"; +} .icon-local_florist:before { - content: "\e545"; } + content: "\e545"; +} .icon-local_gas_station:before { - content: "\e546"; } + content: "\e546"; +} .icon-shopping_cart:before { - content: "\e8cc"; } + content: "\e8cc"; +} .icon-local_hospital:before { - content: "\e548"; } + content: "\e548"; +} .icon-local_laundry_service:before { - content: "\e54a"; } + content: "\e54a"; +} .icon-local_library:before { - content: "\e54b"; } + content: "\e54b"; +} .icon-local_mall:before { - content: "\e54c"; } + content: "\e54c"; +} .icon-theaters:before { - content: "\e8da"; } + content: "\e8da"; +} .icon-local_offer:before { - content: "\e54e"; } + content: "\e54e"; +} .icon-local_parking:before { - content: "\e54f"; } + content: "\e54f"; +} .icon-local_pharmacy:before { - content: "\e550"; } + content: "\e550"; +} .icon-local_pizza:before { - content: "\e552"; } + content: "\e552"; +} .icon-print:before { - content: "\e8ad"; } + content: "\e8ad"; +} .icon-local_shipping:before { - content: "\e558"; } + content: "\e558"; +} .icon-local_taxi:before { - content: "\e559"; } + content: "\e559"; +} .icon-location_city:before { - content: "\e7f1"; } + content: "\e7f1"; +} .icon-location_off:before { - content: "\e0c7"; } + content: "\e0c7"; +} .icon-room:before { - content: "\e8b4"; } + content: "\e8b4"; +} .icon-lock_open:before { - content: "\e898"; } + content: "\e898"; +} .icon-lock_outline:before { - content: "\e899"; } + content: "\e899"; +} .icon-looks:before { - content: "\e3fc"; } + content: "\e3fc"; +} .icon-looks_3:before { - content: "\e3fb"; } + content: "\e3fb"; +} .icon-looks_4:before { - content: "\e3fd"; } + content: "\e3fd"; +} .icon-looks_5:before { - content: "\e3fe"; } + content: "\e3fe"; +} .icon-looks_6:before { - content: "\e3ff"; } + content: "\e3ff"; +} .icon-looks_one:before { - content: "\e400"; } + content: "\e400"; +} .icon-looks_two:before { - content: "\e401"; } + content: "\e401"; +} .icon-sync:before { - content: "\e627"; } + content: "\e627"; +} .icon-loupe:before { - content: "\e402"; } + content: "\e402"; +} .icon-low_priority:before { - content: "\e16d"; } + content: "\e16d"; +} .icon-loyalty:before { - content: "\e89a"; } + content: "\e89a"; +} .icon-mail_outline:before { - content: "\e0e1"; } + content: "\e0e1"; +} .icon-map:before { - content: "\e55b"; } + content: "\e55b"; +} .icon-markunread_mailbox:before { - content: "\e89b"; } + content: "\e89b"; +} .icon-memory:before { - content: "\e322"; } + content: "\e322"; +} .icon-menu:before { - content: "\e5d2"; } + content: "\e5d2"; +} .icon-message:before { - content: "\e0c9"; } + content: "\e0c9"; +} .icon-mic:before { - content: "\e029"; } + content: "\e029"; +} .icon-mic_none:before { - content: "\e02a"; } + content: "\e02a"; +} .icon-mic_off:before { - content: "\e02b"; } + content: "\e02b"; +} .icon-mms:before { - content: "\e618"; } + content: "\e618"; +} .icon-mode_comment:before { - content: "\e253"; } + content: "\e253"; +} .icon-monetization_on:before { - content: "\e263"; } + content: "\e263"; +} .icon-money_off:before { - content: "\e25c"; } + content: "\e25c"; +} .icon-monochrome_photos:before { - content: "\e403"; } + content: "\e403"; +} .icon-mood_bad:before { - content: "\e7f3"; } + content: "\e7f3"; +} .icon-more:before { - content: "\e619"; } + content: "\e619"; +} .icon-more_horiz:before { - content: "\e5d3"; } + content: "\e5d3"; +} .icon-more_vert:before { - content: "\e5d4"; } + content: "\e5d4"; +} .icon-motorcycle:before { - content: "\e91b"; } + content: "\e91b"; +} .icon-mouse:before { - content: "\e323"; } + content: "\e323"; +} .icon-move_to_inbox:before { - content: "\e168"; } + content: "\e168"; +} .icon-movie_creation:before { - content: "\e404"; } + content: "\e404"; +} .icon-movie_filter:before { - content: "\e43a"; } + content: "\e43a"; +} .icon-multiline_chart:before { - content: "\e6df"; } + content: "\e6df"; +} .icon-music_note:before { - content: "\e405"; } + content: "\e405"; +} .icon-music_video:before { - content: "\e063"; } + content: "\e063"; +} .icon-nature:before { - content: "\e406"; } + content: "\e406"; +} .icon-nature_people:before { - content: "\e407"; } + content: "\e407"; +} .icon-navigation:before { - content: "\e55d"; } + content: "\e55d"; +} .icon-near_me:before { - content: "\e569"; } + content: "\e569"; +} .icon-network_cell:before { - content: "\e1b9"; } + content: "\e1b9"; +} .icon-network_check:before { - content: "\e640"; } + content: "\e640"; +} .icon-network_locked:before { - content: "\e61a"; } + content: "\e61a"; +} .icon-network_wifi:before { - content: "\e1ba"; } + content: "\e1ba"; +} .icon-new_releases:before { - content: "\e031"; } + content: "\e031"; +} .icon-next_week:before { - content: "\e16a"; } + content: "\e16a"; +} .icon-nfc:before { - content: "\e1bb"; } + content: "\e1bb"; +} .icon-no_encryption:before { - content: "\e641"; } + content: "\e641"; +} .icon-signal_cellular_no_sim:before { - content: "\e1ce"; } + content: "\e1ce"; +} .icon-note:before { - content: "\e06f"; } + content: "\e06f"; +} .icon-note_add:before { - content: "\e89c"; } + content: "\e89c"; +} .icon-notifications:before { - content: "\e7f4"; } + content: "\e7f4"; +} .icon-notifications_active:before { - content: "\e7f7"; } + content: "\e7f7"; +} .icon-notifications_none:before { - content: "\e7f5"; } + content: "\e7f5"; +} .icon-notifications_off:before { - content: "\e7f6"; } + content: "\e7f6"; +} .icon-notifications_paused:before { - content: "\e7f8"; } + content: "\e7f8"; +} .icon-offline_pin:before { - content: "\e90a"; } + content: "\e90a"; +} .icon-ondemand_video:before { - content: "\e63a"; } + content: "\e63a"; +} .icon-opacity:before { - content: "\e91c"; } + content: "\e91c"; +} .icon-open_in_browser:before { - content: "\e89d"; } + content: "\e89d"; +} .icon-open_with:before { - content: "\e89f"; } + content: "\e89f"; +} .icon-pages:before { - content: "\e7f9"; } + content: "\e7f9"; +} .icon-pageview:before { - content: "\e8a0"; } + content: "\e8a0"; +} .icon-pan_tool:before { - content: "\e925"; } + content: "\e925"; +} .icon-panorama:before { - content: "\e40b"; } + content: "\e40b"; +} .icon-radio_button_unchecked:before { - content: "\e836"; } + content: "\e836"; +} .icon-panorama_horizontal:before { - content: "\e40d"; } + content: "\e40d"; +} .icon-panorama_vertical:before { - content: "\e40e"; } + content: "\e40e"; +} .icon-panorama_wide_angle:before { - content: "\e40f"; } + content: "\e40f"; +} .icon-party_mode:before { - content: "\e7fa"; } + content: "\e7fa"; +} .icon-pause:before { - content: "\e034"; } + content: "\e034"; +} .icon-pause_circle_filled:before { - content: "\e035"; } + content: "\e035"; +} .icon-pause_circle_outline:before { - content: "\e036"; } + content: "\e036"; +} .icon-people_outline:before { - content: "\e7fc"; } + content: "\e7fc"; +} .icon-perm_camera_mic:before { - content: "\e8a2"; } + content: "\e8a2"; +} .icon-perm_contact_calendar:before { - content: "\e8a3"; } + content: "\e8a3"; +} .icon-perm_data_setting:before { - content: "\e8a4"; } + content: "\e8a4"; +} .icon-perm_device_information:before { - content: "\e8a5"; } + content: "\e8a5"; +} .icon-person_outline:before { - content: "\e7ff"; } + content: "\e7ff"; +} .icon-perm_media:before { - content: "\e8a7"; } + content: "\e8a7"; +} .icon-perm_phone_msg:before { - content: "\e8a8"; } + content: "\e8a8"; +} .icon-perm_scan_wifi:before { - content: "\e8a9"; } + content: "\e8a9"; +} .icon-person:before { - content: "\e7fd"; } + content: "\e7fd"; +} .icon-person_add:before { - content: "\e7fe"; } + content: "\e7fe"; +} .icon-person_pin:before { - content: "\e55a"; } + content: "\e55a"; +} .icon-person_pin_circle:before { - content: "\e56a"; } + content: "\e56a"; +} .icon-personal_video:before { - content: "\e63b"; } + content: "\e63b"; +} .icon-pets:before { - content: "\e91d"; } + content: "\e91d"; +} .icon-phone_android:before { - content: "\e324"; } + content: "\e324"; +} .icon-phone_bluetooth_speaker:before { - content: "\e61b"; } + content: "\e61b"; +} .icon-phone_forwarded:before { - content: "\e61c"; } + content: "\e61c"; +} .icon-phone_in_talk:before { - content: "\e61d"; } + content: "\e61d"; +} .icon-phone_iphone:before { - content: "\e325"; } + content: "\e325"; +} .icon-phone_locked:before { - content: "\e61e"; } + content: "\e61e"; +} .icon-phone_missed:before { - content: "\e61f"; } + content: "\e61f"; +} .icon-phone_paused:before { - content: "\e620"; } + content: "\e620"; +} .icon-phonelink_erase:before { - content: "\e0db"; } + content: "\e0db"; +} .icon-phonelink_lock:before { - content: "\e0dc"; } + content: "\e0dc"; +} .icon-phonelink_off:before { - content: "\e327"; } + content: "\e327"; +} .icon-phonelink_ring:before { - content: "\e0dd"; } + content: "\e0dd"; +} .icon-phonelink_setup:before { - content: "\e0de"; } + content: "\e0de"; +} .icon-photo_album:before { - content: "\e411"; } + content: "\e411"; +} .icon-photo_filter:before { - content: "\e43b"; } + content: "\e43b"; +} .icon-photo_size_select_actual:before { - content: "\e432"; } + content: "\e432"; +} .icon-photo_size_select_large:before { - content: "\e433"; } + content: "\e433"; +} .icon-photo_size_select_small:before { - content: "\e434"; } + content: "\e434"; +} .icon-picture_as_pdf:before { - content: "\e415"; } + content: "\e415"; +} .icon-picture_in_picture:before { - content: "\e8aa"; } + content: "\e8aa"; +} .icon-picture_in_picture_alt:before { - content: "\e911"; } + content: "\e911"; +} .icon-pie_chart:before { - content: "\e6c4"; } + content: "\e6c4"; +} .icon-pie_chart_outlined:before { - content: "\e6c5"; } + content: "\e6c5"; +} .icon-pin_drop:before { - content: "\e55e"; } + content: "\e55e"; +} .icon-play_arrow:before { - content: "\e037"; } + content: "\e037"; +} .icon-play_circle_filled:before { - content: "\e038"; } + content: "\e038"; +} .icon-play_circle_outline:before { - content: "\e039"; } + content: "\e039"; +} .icon-play_for_work:before { - content: "\e906"; } + content: "\e906"; +} .icon-playlist_add:before { - content: "\e03b"; } + content: "\e03b"; +} .icon-playlist_add_check:before { - content: "\e065"; } + content: "\e065"; +} .icon-playlist_play:before { - content: "\e05f"; } + content: "\e05f"; +} .icon-plus_one:before { - content: "\e800"; } + content: "\e800"; +} .icon-polymer:before { - content: "\e8ab"; } + content: "\e8ab"; +} .icon-pool:before { - content: "\eb48"; } + content: "\eb48"; +} .icon-portable_wifi_off:before { - content: "\e0ce"; } + content: "\e0ce"; +} .icon-portrait:before { - content: "\e416"; } + content: "\e416"; +} .icon-power:before { - content: "\e63c"; } + content: "\e63c"; +} .icon-power_input:before { - content: "\e336"; } + content: "\e336"; +} .icon-power_settings_new:before { - content: "\e8ac"; } + content: "\e8ac"; +} .icon-pregnant_woman:before { - content: "\e91e"; } + content: "\e91e"; +} .icon-present_to_all:before { - content: "\e0df"; } + content: "\e0df"; +} .icon-priority_high:before { - content: "\e645"; } + content: "\e645"; +} .icon-public:before { - content: "\e80b"; } + content: "\e80b"; +} .icon-publish:before { - content: "\e255"; } + content: "\e255"; +} .icon-queue_music:before { - content: "\e03d"; } + content: "\e03d"; +} .icon-queue_play_next:before { - content: "\e066"; } + content: "\e066"; +} .icon-radio:before { - content: "\e03e"; } + content: "\e03e"; +} .icon-radio_button_checked:before { - content: "\e837"; } + content: "\e837"; +} .icon-rate_review:before { - content: "\e560"; } + content: "\e560"; +} .icon-receipt:before { - content: "\e8b0"; } + content: "\e8b0"; +} .icon-recent_actors:before { - content: "\e03f"; } + content: "\e03f"; +} .icon-record_voice_over:before { - content: "\e91f"; } + content: "\e91f"; +} .icon-redo:before { - content: "\e15a"; } + content: "\e15a"; +} .icon-refresh:before { - content: "\e5d5"; } + content: "\e5d5"; +} .icon-remove:before { - content: "\e15b"; } + content: "\e15b"; +} .icon-remove_circle_outline:before { - content: "\e15d"; } + content: "\e15d"; +} .icon-remove_from_queue:before { - content: "\e067"; } + content: "\e067"; +} .icon-visibility:before { - content: "\e8f4"; } + content: "\e8f4"; +} .icon-remove_shopping_cart:before { - content: "\e928"; } + content: "\e928"; +} .icon-reorder:before { - content: "\e8fe"; } + content: "\e8fe"; +} .icon-repeat:before { - content: "\e040"; } + content: "\e040"; +} .icon-repeat_one:before { - content: "\e041"; } + content: "\e041"; +} .icon-replay:before { - content: "\e042"; } + content: "\e042"; +} .icon-replay_10:before { - content: "\e059"; } + content: "\e059"; +} .icon-replay_30:before { - content: "\e05a"; } + content: "\e05a"; +} .icon-replay_5:before { - content: "\e05b"; } + content: "\e05b"; +} .icon-reply:before { - content: "\e15e"; } + content: "\e15e"; +} .icon-reply_all:before { - content: "\e15f"; } + content: "\e15f"; +} .icon-report:before { - content: "\e160"; } + content: "\e160"; +} .icon-warning:before { - content: "\e002"; } + content: "\e002"; +} .icon-restaurant:before { - content: "\e56c"; } + content: "\e56c"; +} .icon-restore_page:before { - content: "\e929"; } + content: "\e929"; +} .icon-ring_volume:before { - content: "\e0d1"; } + content: "\e0d1"; +} .icon-room_service:before { - content: "\eb49"; } + content: "\eb49"; +} .icon-rotate_90_degrees_ccw:before { - content: "\e418"; } + content: "\e418"; +} .icon-rotate_left:before { - content: "\e419"; } + content: "\e419"; +} .icon-rotate_right:before { - content: "\e41a"; } + content: "\e41a"; +} .icon-rounded_corner:before { - content: "\e920"; } + content: "\e920"; +} .icon-router:before { - content: "\e328"; } + content: "\e328"; +} .icon-rowing:before { - content: "\e921"; } + content: "\e921"; +} .icon-rss_feed:before { - content: "\e0e5"; } + content: "\e0e5"; +} .icon-rv_hookup:before { - content: "\e642"; } + content: "\e642"; +} .icon-satellite:before { - content: "\e562"; } + content: "\e562"; +} .icon-save:before { - content: "\e161"; } + content: "\e161"; +} .icon-scanner:before { - content: "\e329"; } + content: "\e329"; +} .icon-school:before { - content: "\e80c"; } + content: "\e80c"; +} .icon-screen_lock_landscape:before { - content: "\e1be"; } + content: "\e1be"; +} .icon-screen_lock_portrait:before { - content: "\e1bf"; } + content: "\e1bf"; +} .icon-screen_lock_rotation:before { - content: "\e1c0"; } + content: "\e1c0"; +} .icon-screen_rotation:before { - content: "\e1c1"; } + content: "\e1c1"; +} .icon-screen_share:before { - content: "\e0e2"; } + content: "\e0e2"; +} .icon-sd_storage:before { - content: "\e1c2"; } + content: "\e1c2"; +} .icon-search:before { - content: "\e8b6"; } + content: "\e8b6"; +} .icon-security:before { - content: "\e32a"; } + content: "\e32a"; +} .icon-select_all:before { - content: "\e162"; } + content: "\e162"; +} .icon-send:before { - content: "\e163"; } + content: "\e163"; +} .icon-sentiment_dissatisfied:before { - content: "\e811"; } + content: "\e811"; +} .icon-sentiment_neutral:before { - content: "\e812"; } + content: "\e812"; +} .icon-sentiment_satisfied:before { - content: "\e813"; } + content: "\e813"; +} .icon-sentiment_very_dissatisfied:before { - content: "\e814"; } + content: "\e814"; +} .icon-sentiment_very_satisfied:before { - content: "\e815"; } + content: "\e815"; +} .icon-settings:before { - content: "\e8b8"; } + content: "\e8b8"; +} .icon-settings_applications:before { - content: "\e8b9"; } + content: "\e8b9"; +} .icon-settings_backup_restore:before { - content: "\e8ba"; } + content: "\e8ba"; +} .icon-settings_bluetooth:before { - content: "\e8bb"; } + content: "\e8bb"; +} .icon-settings_brightness:before { - content: "\e8bd"; } + content: "\e8bd"; +} .icon-settings_cell:before { - content: "\e8bc"; } + content: "\e8bc"; +} .icon-settings_ethernet:before { - content: "\e8be"; } + content: "\e8be"; +} .icon-settings_input_antenna:before { - content: "\e8bf"; } + content: "\e8bf"; +} .icon-settings_input_composite:before { - content: "\e8c1"; } + content: "\e8c1"; +} .icon-settings_input_hdmi:before { - content: "\e8c2"; } + content: "\e8c2"; +} .icon-settings_input_svideo:before { - content: "\e8c3"; } + content: "\e8c3"; +} .icon-settings_overscan:before { - content: "\e8c4"; } + content: "\e8c4"; +} .icon-settings_phone:before { - content: "\e8c5"; } + content: "\e8c5"; +} .icon-settings_power:before { - content: "\e8c6"; } + content: "\e8c6"; +} .icon-settings_remote:before { - content: "\e8c7"; } + content: "\e8c7"; +} .icon-settings_system_daydream:before { - content: "\e1c3"; } + content: "\e1c3"; +} .icon-settings_voice:before { - content: "\e8c8"; } + content: "\e8c8"; +} .icon-share:before { - content: "\e80d"; } + content: "\e80d"; +} .icon-shop:before { - content: "\e8c9"; } + content: "\e8c9"; +} .icon-shop_two:before { - content: "\e8ca"; } + content: "\e8ca"; +} .icon-shopping_basket:before { - content: "\e8cb"; } + content: "\e8cb"; +} .icon-short_text:before { - content: "\e261"; } + content: "\e261"; +} .icon-show_chart:before { - content: "\e6e1"; } + content: "\e6e1"; +} .icon-shuffle:before { - content: "\e043"; } + content: "\e043"; +} .icon-signal_cellular_4_bar:before { - content: "\e1c8"; } + content: "\e1c8"; +} .icon-signal_cellular_connected_no_internet_4_bar:before { - content: "\e1cd"; } + content: "\e1cd"; +} .icon-signal_cellular_null:before { - content: "\e1cf"; } + content: "\e1cf"; +} .icon-signal_cellular_off:before { - content: "\e1d0"; } + content: "\e1d0"; +} .icon-signal_wifi_4_bar:before { - content: "\e1d8"; } + content: "\e1d8"; +} .icon-signal_wifi_4_bar_lock:before { - content: "\e1d9"; } + content: "\e1d9"; +} .icon-signal_wifi_off:before { - content: "\e1da"; } + content: "\e1da"; +} .icon-sim_card:before { - content: "\e32b"; } + content: "\e32b"; +} .icon-sim_card_alert:before { - content: "\e624"; } + content: "\e624"; +} .icon-skip_next:before { - content: "\e044"; } + content: "\e044"; +} .icon-skip_previous:before { - content: "\e045"; } + content: "\e045"; +} .icon-slideshow:before { - content: "\e41b"; } + content: "\e41b"; +} .icon-slow_motion_video:before { - content: "\e068"; } + content: "\e068"; +} .icon-stay_primary_portrait:before { - content: "\e0d6"; } + content: "\e0d6"; +} .icon-smoke_free:before { - content: "\eb4a"; } + content: "\eb4a"; +} .icon-smoking_rooms:before { - content: "\eb4b"; } + content: "\eb4b"; +} .icon-textsms:before { - content: "\e0d8"; } + content: "\e0d8"; +} .icon-snooze:before { - content: "\e046"; } + content: "\e046"; +} .icon-sort:before { - content: "\e164"; } + content: "\e164"; +} .icon-sort_by_alpha:before { - content: "\e053"; } + content: "\e053"; +} .icon-spa:before { - content: "\eb4c"; } + content: "\eb4c"; +} .icon-space_bar:before { - content: "\e256"; } + content: "\e256"; +} .icon-speaker:before { - content: "\e32d"; } + content: "\e32d"; +} .icon-speaker_group:before { - content: "\e32e"; } + content: "\e32e"; +} .icon-speaker_notes:before { - content: "\e8cd"; } + content: "\e8cd"; +} .icon-speaker_notes_off:before { - content: "\e92a"; } + content: "\e92a"; +} .icon-speaker_phone:before { - content: "\e0d2"; } + content: "\e0d2"; +} .icon-spellcheck:before { - content: "\e8ce"; } + content: "\e8ce"; +} .icon-star_border:before { - content: "\e83a"; } + content: "\e83a"; +} .icon-star_half:before { - content: "\e839"; } + content: "\e839"; +} .icon-stars:before { - content: "\e8d0"; } + content: "\e8d0"; +} .icon-stay_primary_landscape:before { - content: "\e0d5"; } + content: "\e0d5"; +} .icon-stop:before { - content: "\e047"; } + content: "\e047"; +} .icon-stop_screen_share:before { - content: "\e0e3"; } + content: "\e0e3"; +} .icon-storage:before { - content: "\e1db"; } + content: "\e1db"; +} .icon-store_mall_directory:before { - content: "\e563"; } + content: "\e563"; +} .icon-straighten:before { - content: "\e41c"; } + content: "\e41c"; +} .icon-streetview:before { - content: "\e56e"; } + content: "\e56e"; +} .icon-strikethrough_s:before { - content: "\e257"; } + content: "\e257"; +} .icon-style:before { - content: "\e41d"; } + content: "\e41d"; +} .icon-subdirectory_arrow_left:before { - content: "\e5d9"; } + content: "\e5d9"; +} .icon-subdirectory_arrow_right:before { - content: "\e5da"; } + content: "\e5da"; +} .icon-subject:before { - content: "\e8d2"; } + content: "\e8d2"; +} .icon-subscriptions:before { - content: "\e064"; } + content: "\e064"; +} .icon-subtitles:before { - content: "\e048"; } + content: "\e048"; +} .icon-subway:before { - content: "\e56f"; } + content: "\e56f"; +} .icon-supervisor_account:before { - content: "\e8d3"; } + content: "\e8d3"; +} .icon-surround_sound:before { - content: "\e049"; } + content: "\e049"; +} .icon-swap_calls:before { - content: "\e0d7"; } + content: "\e0d7"; +} .icon-swap_horiz:before { - content: "\e8d4"; } + content: "\e8d4"; +} .icon-swap_vert:before { - content: "\e8d5"; } + content: "\e8d5"; +} .icon-swap_vertical_circle:before { - content: "\e8d6"; } + content: "\e8d6"; +} .icon-switch_camera:before { - content: "\e41e"; } + content: "\e41e"; +} .icon-switch_video:before { - content: "\e41f"; } + content: "\e41f"; +} .icon-sync_disabled:before { - content: "\e628"; } + content: "\e628"; +} .icon-sync_problem:before { - content: "\e629"; } + content: "\e629"; +} .icon-system_update:before { - content: "\e62a"; } + content: "\e62a"; +} .icon-system_update_alt:before { - content: "\e8d7"; } + content: "\e8d7"; +} .icon-tab:before { - content: "\e8d8"; } + content: "\e8d8"; +} .icon-tab_unselected:before { - content: "\e8d9"; } + content: "\e8d9"; +} .icon-tablet:before { - content: "\e32f"; } + content: "\e32f"; +} .icon-tablet_android:before { - content: "\e330"; } + content: "\e330"; +} .icon-tablet_mac:before { - content: "\e331"; } + content: "\e331"; +} .icon-tap_and_play:before { - content: "\e62b"; } + content: "\e62b"; +} .icon-text_fields:before { - content: "\e262"; } + content: "\e262"; +} .icon-text_format:before { - content: "\e165"; } + content: "\e165"; +} .icon-texture:before { - content: "\e421"; } + content: "\e421"; +} .icon-thumb_down:before { - content: "\e8db"; } + content: "\e8db"; +} .icon-thumb_up:before { - content: "\e8dc"; } + content: "\e8dc"; +} .icon-thumbs_up_down:before { - content: "\e8dd"; } + content: "\e8dd"; +} .icon-timelapse:before { - content: "\e422"; } + content: "\e422"; +} .icon-timeline:before { - content: "\e922"; } + content: "\e922"; +} .icon-timer:before { - content: "\e425"; } + content: "\e425"; +} .icon-timer_10:before { - content: "\e423"; } + content: "\e423"; +} .icon-timer_3:before { - content: "\e424"; } + content: "\e424"; +} .icon-timer_off:before { - content: "\e426"; } + content: "\e426"; +} .icon-title:before { - content: "\e264"; } + content: "\e264"; +} .icon-toc:before { - content: "\e8de"; } + content: "\e8de"; +} .icon-today:before { - content: "\e8df"; } + content: "\e8df"; +} .icon-toll:before { - content: "\e8e0"; } + content: "\e8e0"; +} .icon-tonality:before { - content: "\e427"; } + content: "\e427"; +} .icon-touch_app:before { - content: "\e913"; } + content: "\e913"; +} .icon-toys:before { - content: "\e332"; } + content: "\e332"; +} .icon-track_changes:before { - content: "\e8e1"; } + content: "\e8e1"; +} .icon-traffic:before { - content: "\e565"; } + content: "\e565"; +} .icon-train:before { - content: "\e570"; } + content: "\e570"; +} .icon-tram:before { - content: "\e571"; } + content: "\e571"; +} .icon-transfer_within_a_station:before { - content: "\e572"; } + content: "\e572"; +} .icon-transform:before { - content: "\e428"; } + content: "\e428"; +} .icon-translate:before { - content: "\e8e2"; } + content: "\e8e2"; +} .icon-trending_down:before { - content: "\e8e3"; } + content: "\e8e3"; +} .icon-trending_flat:before { - content: "\e8e4"; } + content: "\e8e4"; +} .icon-trending_up:before { - content: "\e8e5"; } + content: "\e8e5"; +} .icon-tune:before { - content: "\e429"; } + content: "\e429"; +} .icon-tv:before { - content: "\e333"; } + content: "\e333"; +} .icon-unarchive:before { - content: "\e169"; } + content: "\e169"; +} .icon-undo:before { - content: "\e166"; } + content: "\e166"; +} .icon-unfold_less:before { - content: "\e5d6"; } + content: "\e5d6"; +} .icon-unfold_more:before { - content: "\e5d7"; } + content: "\e5d7"; +} .icon-update:before { - content: "\e923"; } + content: "\e923"; +} .icon-usb:before { - content: "\e1e0"; } + content: "\e1e0"; +} .icon-verified_user:before { - content: "\e8e8"; } + content: "\e8e8"; +} .icon-vertical_align_bottom:before { - content: "\e258"; } + content: "\e258"; +} .icon-vertical_align_center:before { - content: "\e259"; } + content: "\e259"; +} .icon-vertical_align_top:before { - content: "\e25a"; } + content: "\e25a"; +} .icon-vibration:before { - content: "\e62d"; } + content: "\e62d"; +} .icon-video_call:before { - content: "\e070"; } + content: "\e070"; +} .icon-video_label:before { - content: "\e071"; } + content: "\e071"; +} .icon-video_library:before { - content: "\e04a"; } + content: "\e04a"; +} .icon-videocam:before { - content: "\e04b"; } + content: "\e04b"; +} .icon-videocam_off:before { - content: "\e04c"; } + content: "\e04c"; +} .icon-videogame_asset:before { - content: "\e338"; } + content: "\e338"; +} .icon-view_agenda:before { - content: "\e8e9"; } + content: "\e8e9"; +} .icon-view_array:before { - content: "\e8ea"; } + content: "\e8ea"; +} .icon-view_carousel:before { - content: "\e8eb"; } + content: "\e8eb"; +} .icon-view_column:before { - content: "\e8ec"; } + content: "\e8ec"; +} .icon-view_comfy:before { - content: "\e42a"; } + content: "\e42a"; +} .icon-view_compact:before { - content: "\e42b"; } + content: "\e42b"; +} .icon-view_day:before { - content: "\e8ed"; } + content: "\e8ed"; +} .icon-view_headline:before { - content: "\e8ee"; } + content: "\e8ee"; +} .icon-view_list:before { - content: "\e8ef"; } + content: "\e8ef"; +} .icon-view_module:before { - content: "\e8f0"; } + content: "\e8f0"; +} .icon-view_quilt:before { - content: "\e8f1"; } + content: "\e8f1"; +} .icon-view_stream:before { - content: "\e8f2"; } + content: "\e8f2"; +} .icon-view_week:before { - content: "\e8f3"; } + content: "\e8f3"; +} .icon-vignette:before { - content: "\e435"; } + content: "\e435"; +} .icon-visibility_off:before { - content: "\e8f5"; } + content: "\e8f5"; +} .icon-voice_chat:before { - content: "\e62e"; } + content: "\e62e"; +} .icon-voicemail:before { - content: "\e0d9"; } + content: "\e0d9"; +} .icon-volume_down:before { - content: "\e04d"; } + content: "\e04d"; +} .icon-volume_mute:before { - content: "\e04e"; } + content: "\e04e"; +} .icon-volume_off:before { - content: "\e04f"; } + content: "\e04f"; +} .icon-volume_up:before { - content: "\e050"; } + content: "\e050"; +} .icon-vpn_key:before { - content: "\e0da"; } + content: "\e0da"; +} .icon-vpn_lock:before { - content: "\e62f"; } + content: "\e62f"; +} .icon-wallpaper:before { - content: "\e1bc"; } + content: "\e1bc"; +} .icon-watch:before { - content: "\e334"; } + content: "\e334"; +} .icon-watch_later:before { - content: "\e924"; } + content: "\e924"; +} .icon-wb_auto:before { - content: "\e42c"; } + content: "\e42c"; +} .icon-wb_incandescent:before { - content: "\e42e"; } + content: "\e42e"; +} .icon-wb_iridescent:before { - content: "\e436"; } + content: "\e436"; +} .icon-wb_sunny:before { - content: "\e430"; } + content: "\e430"; +} .icon-wc:before { - content: "\e63d"; } + content: "\e63d"; +} .icon-web:before { - content: "\e051"; } + content: "\e051"; +} .icon-web_asset:before { - content: "\e069"; } + content: "\e069"; +} .icon-weekend:before { - content: "\e16b"; } + content: "\e16b"; +} .icon-whatshot:before { - content: "\e80e"; } + content: "\e80e"; +} .icon-widgets:before { - content: "\e1bd"; } + content: "\e1bd"; +} .icon-wifi:before { - content: "\e63e"; } + content: "\e63e"; +} .icon-wifi_lock:before { - content: "\e1e1"; } + content: "\e1e1"; +} .icon-wifi_tethering:before { - content: "\e1e2"; } + content: "\e1e2"; +} .icon-work:before { - content: "\e8f9"; } + content: "\e8f9"; +} .icon-wrap_text:before { - content: "\e25b"; } + content: "\e25b"; +} .icon-youtube_searched_for:before { - content: "\e8fa"; } + content: "\e8fa"; +} .icon-zoom_in:before { - content: "\e8ff"; } + content: "\e8ff"; +} .icon-zoom_out:before { - content: "\e901"; } + content: "\e901"; +} .icon-zoom_out_map:before { - content: "\e56b"; } + content: "\e56b"; +} .pop-in.toggled, .pop-out.toggled, @@ -2754,7 +3638,8 @@ a { transform: scale(1); -webkit-transform: scale(1); -moz-transform: scale(1); - -o-transform: scale(1); } + -o-transform: scale(1); +} .pop-in, .pop-in-last { @@ -2763,13 +3648,15 @@ a { transform: scale(1.1); -webkit-transform: scale(1.1); -moz-transform: scale(1.1); - -o-transform: scale(1.1); } + -o-transform: scale(1.1); +} .animate_slower { transition: all 2s ease-in-out !important; -webkit-transition: all 2s ease-in-out !important; -moz-transition: all 2s ease-in-out !important; - -o-transition: all 2s ease-in-out !important; } + -o-transition: all 2s ease-in-out !important; +} .animate-up.toggled, .animate-down.toggled { @@ -2778,7 +3665,8 @@ a { transform: translateY(0px); -webkit-transform: translateY(0px); -moz-transform: translateY(0px); - -o-transform: translateY(0px); } + -o-transform: translateY(0px); +} .animate-down { opacity: 0; @@ -2786,7 +3674,8 @@ a { transform: translateY(-300px); -webkit-transform: translateY(-300px); -moz-transform: translateY(-300px); - -o-transform: translateY(-300px); } + -o-transform: translateY(-300px); +} .animate-up { opacity: 0; @@ -2794,13 +3683,15 @@ a { transform: translateY(300px); -webkit-transform: translateY(300px); -moz-transform: translateY(300px); - -o-transform: translateY(300px); } + -o-transform: translateY(300px); +} .animate { transition: all 1s ease-in-out; -webkit-transition: all 1s ease-in-out; -moz-transition: all 1s ease-in-out; - -o-transition: all 1s ease-in-out; } + -o-transition: all 1s ease-in-out; +} #home_socials { position: fixed; @@ -2808,18 +3699,21 @@ a { left: 0; right: 0; text-align: center; - z-index: 2; } - #home_socials .socialicons { - display: inline-block; - font-size: 1.4em; - margin: 15px 20px 15px 20px; } + z-index: 2; +} +#home_socials .socialicons { + display: inline-block; + font-size: 1.4em; + margin: 15px 20px 15px 20px; +} #socials { position: fixed; left: 0; top: 37%; background: rgba(0, 0, 0, 0.8); - z-index: 2; } + z-index: 2; +} .socialicons { display: block; @@ -2837,30 +3731,37 @@ a { transition: all 0.3s; -webkit-transition: all 0.3s; -moz-transition: all 0.3s; - -o-transition: all 0.3s; } + -o-transition: all 0.3s; +} #twitter:before { - content: "\ea96"; } + content: "\ea96"; +} #instagram:before { - content: "\ea92"; } + content: "\ea92"; +} #youtube:before { - content: "\ea9d"; } + content: "\ea9d"; +} #flickr:before { - content: "\eaa4"; } + content: "\eaa4"; +} #facebook:before { - content: "\ea91"; } + content: "\ea91"; +} @media (hover: hover) { .socialicons:hover { color: #b5b5b5; -ms-transform: scale(1.3); transform: scale(1.3); - -webkit-transform: scale(1.3); } } - + -webkit-transform: scale(1.3); + } +} #footer { z-index: 3; left: 0; @@ -2870,75 +3771,89 @@ a { padding: 5px 0 5px 0; -webkit-transition: color 0.3s, opacity 0.3s ease-out, margin-left 0.5s, -webkit-transform 0.3s ease-out, -webkit-box-shadow 0.3s; transition: color 0.3s, opacity 0.3s ease-out, margin-left 0.5s, -webkit-transform 0.3s ease-out, -webkit-box-shadow 0.3s; - -o-transition: color 0.3s, opacity 0.3s ease-out, transform 0.3s ease-out, box-shadow 0.3s, margin-left 0.5s; transition: color 0.3s, opacity 0.3s ease-out, transform 0.3s ease-out, box-shadow 0.3s, margin-left 0.5s; - transition: color 0.3s, opacity 0.3s ease-out, transform 0.3s ease-out, box-shadow 0.3s, margin-left 0.5s, -webkit-transform 0.3s ease-out, -webkit-box-shadow 0.3s; } - #footer p { - color: #cccccc; - font-size: 0.5em; - font-weight: 400; } - #footer p a { - color: #ccc; } - #footer p a:visited { - color: #ccc; } - #footer p.hosted_by, - #footer p.home_copyright { - text-transform: uppercase; } + transition: color 0.3s, opacity 0.3s ease-out, transform 0.3s ease-out, box-shadow 0.3s, margin-left 0.5s, -webkit-transform 0.3s ease-out, -webkit-box-shadow 0.3s; +} +#footer p { + color: #cccccc; + font-size: 0.5em; + font-weight: 400; +} +#footer p a { + color: #ccc; +} +#footer p a:visited { + color: #ccc; +} +#footer p.hosted_by, +#footer p.home_copyright { + text-transform: uppercase; +} #menu { - width: 100%; } - #menu li { - position: relative; - display: block; - float: right; - padding: 22px 1.5% 20px 1.5%; } - #menu a { - display: block; - font-size: 0.8em; - color: #ffffff; - text-transform: uppercase; - font-weight: 400; - transition: all 0.3s; - -webkit-transition: all 0.3s; - -moz-transition: all 0.3s; - -o-transition: all 0.3s; } - #menu .current-menu-item a { - color: #b5b5b5 !important; } + width: 100%; +} +#menu li { + position: relative; + display: block; + float: right; + padding: 22px 1.5% 20px 1.5%; +} +#menu a { + display: block; + font-size: 0.8em; + color: #ffffff; + text-transform: uppercase; + font-weight: 400; + transition: all 0.3s; + -webkit-transition: all 0.3s; + -moz-transition: all 0.3s; + -o-transition: all 0.3s; +} +#menu .current-menu-item a { + color: #b5b5b5 !important; +} #menu_wrap { position: fixed; right: 0; top: 0; z-index: 98; - width: 80%; } + width: 80%; +} @media (hover: hover) { #menu a:hover { - color: #b5b5b5 !important; } } - + color: #b5b5b5 !important; + } +} #header { position: fixed; left: 0; top: 0; right: 0; - z-index: 98; } + z-index: 98; +} #logo { float: left; - padding: 15px; } - #logo h1 { - color: #ffffff; - font-size: 1em; - text-transform: uppercase; - font-weight: 700; - text-align: center; } - #logo h1 span { - font-family: "Roboto", sans-serif; - font-size: 0.6em; - display: block; - font-weight: 300; - letter-spacing: 1px; - padding: 0 0 0 0; } + padding: 15px; +} +#logo h1 { + color: #ffffff; + font-size: 1em; + text-transform: uppercase; + font-weight: 700; + text-align: center; +} +#logo h1 span { + font-family: "Roboto", sans-serif; + font-size: 0.6em; + display: block; + font-weight: 300; + letter-spacing: 1px; + padding: 0 0 0 0; +} #intro { position: fixed; @@ -2947,40 +3862,48 @@ a { bottom: 0; right: 0; z-index: 1000; - background: #000000; } - #intro h1 { - text-align: center; - font-size: 1.5em; - color: #ffffff; - text-transform: uppercase; - font-weight: 200; } - #intro h2 { - text-align: center; - font-size: 1em; - color: #ececec; - text-transform: uppercase; - font-weight: 200; } + background: #000000; +} +#intro h1 { + text-align: center; + font-size: 1.5em; + color: #ffffff; + text-transform: uppercase; + font-weight: 200; +} +#intro h2 { + text-align: center; + font-size: 1em; + color: #ececec; + text-transform: uppercase; + font-weight: 200; +} #slides { position: absolute; left: 0; top: 0; width: 100vw; - height: 98vh; } - #slides .slides-container, - #slides li, - #slides img { - height: 100%; - width: 100%; } - #slides img { - top: 0; - left: 0; - position: absolute; - -o-object-fit: cover; - object-fit: cover; } + height: 98vh; +} +#slides .slides-container, +#slides li, +#slides img { + height: 100%; + width: 100%; +} +#slides img { + top: 0; + left: 0; + position: absolute; + -o-object-fit: cover; + object-fit: cover; +} #footer { position: absolute; - background: #000000; } - #footer p.home_copyright { - color: #ffffff; } + background: #000000; +} +#footer p.home_copyright { + color: #ffffff; +} \ No newline at end of file diff --git a/public/dist/landing.js b/public/dist/landing.js index 3c5a8529ecc..8cb2444f061 100644 --- a/public/dist/landing.js +++ b/public/dist/landing.js @@ -1,251 +1,202 @@ -/*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */ -!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="
",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0+~]|"+ge+")"+ge+"*"),x=new RegExp(ge+"|>"),j=new RegExp(g),A=new RegExp("^"+t+"$"),D={ID:new RegExp("^#("+t+")"),CLASS:new RegExp("^\\.("+t+")"),TAG:new RegExp("^("+t+"|[*])"),ATTR:new RegExp("^"+p),PSEUDO:new RegExp("^"+g),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+ge+"*(even|odd|(([+-]|)(\\d*)n|)"+ge+"*(?:([+-]|)"+ge+"*(\\d+)|))"+ge+"*\\)|)","i"),bool:new RegExp("^(?:"+f+")$","i"),needsContext:new RegExp("^"+ge+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+ge+"*((?:-\\d)?\\d*)"+ge+"*\\)|)(?=[^-]|$)","i")},N=/^(?:input|select|textarea|button)$/i,q=/^h\d$/i,L=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,H=/[+~]/,O=new RegExp("\\\\[\\da-fA-F]{1,6}"+ge+"?|\\\\([^\\r\\n\\f])","g"),P=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},M=function(){V()},R=J(function(e){return!0===e.disabled&&fe(e,"fieldset")},{dir:"parentNode",next:"legend"});try{k.apply(oe=ae.call(ye.childNodes),ye.childNodes),oe[ye.childNodes.length].nodeType}catch(e){k={apply:function(e,t){me.apply(e,ae.call(t))},call:function(e){me.apply(e,ae.call(arguments,1))}}}function I(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(V(e),e=e||T,C)){if(11!==p&&(u=L.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return k.call(n,a),n}else if(f&&(a=f.getElementById(i))&&I.contains(e,a)&&a.id===i)return k.call(n,a),n}else{if(u[2])return k.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&e.getElementsByClassName)return k.apply(n,e.getElementsByClassName(i)),n}if(!(h[t+" "]||d&&d.test(t))){if(c=t,f=e,1===p&&(x.test(t)||m.test(t))){(f=H.test(t)&&U(e.parentNode)||e)==e&&le.scope||((s=e.getAttribute("id"))?s=ce.escapeSelector(s):e.setAttribute("id",s=S)),o=(l=Y(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+Q(l[o]);c=l.join(",")}try{return k.apply(n,f.querySelectorAll(c)),n}catch(e){h(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return re(t.replace(ve,"$1"),e,n,r)}function W(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function F(e){return e[S]=!0,e}function $(e){var t=T.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function B(t){return function(e){return fe(e,"input")&&e.type===t}}function _(t){return function(e){return(fe(e,"input")||fe(e,"button"))&&e.type===t}}function z(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&R(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function X(a){return F(function(o){return o=+o,F(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function U(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}function V(e){var t,n=e?e.ownerDocument||e:ye;return n!=T&&9===n.nodeType&&n.documentElement&&(r=(T=n).documentElement,C=!ce.isXMLDoc(T),i=r.matches||r.webkitMatchesSelector||r.msMatchesSelector,r.msMatchesSelector&&ye!=T&&(t=T.defaultView)&&t.top!==t&&t.addEventListener("unload",M),le.getById=$(function(e){return r.appendChild(e).id=ce.expando,!T.getElementsByName||!T.getElementsByName(ce.expando).length}),le.disconnectedMatch=$(function(e){return i.call(e,"*")}),le.scope=$(function(){return T.querySelectorAll(":scope")}),le.cssHas=$(function(){try{return T.querySelector(":has(*,:jqfake)"),!1}catch(e){return!0}}),le.getById?(b.filter.ID=function(e){var t=e.replace(O,P);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&C){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(O,P);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&C){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):t.querySelectorAll(e)},b.find.CLASS=function(e,t){if("undefined"!=typeof t.getElementsByClassName&&C)return t.getElementsByClassName(e)},d=[],$(function(e){var t;r.appendChild(e).innerHTML="",e.querySelectorAll("[selected]").length||d.push("\\["+ge+"*(?:value|"+f+")"),e.querySelectorAll("[id~="+S+"-]").length||d.push("~="),e.querySelectorAll("a#"+S+"+*").length||d.push(".#.+[+~]"),e.querySelectorAll(":checked").length||d.push(":checked"),(t=T.createElement("input")).setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),r.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&d.push(":enabled",":disabled"),(t=T.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||d.push("\\["+ge+"*name"+ge+"*="+ge+"*(?:''|\"\")")}),le.cssHas||d.push(":has"),d=d.length&&new RegExp(d.join("|")),l=function(e,t){if(e===t)return a=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!le.sortDetached&&t.compareDocumentPosition(e)===n?e===T||e.ownerDocument==ye&&I.contains(ye,e)?-1:t===T||t.ownerDocument==ye&&I.contains(ye,t)?1:o?se.call(o,e)-se.call(o,t):0:4&n?-1:1)}),T}for(e in I.matches=function(e,t){return I(e,null,null,t)},I.matchesSelector=function(e,t){if(V(e),C&&!h[t+" "]&&(!d||!d.test(t)))try{var n=i.call(e,t);if(n||le.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){h(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(O,P),e[3]=(e[3]||e[4]||e[5]||"").replace(O,P),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||I.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&I.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return D.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&j.test(n)&&(t=Y(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(O,P).toLowerCase();return"*"===e?function(){return!0}:function(e){return fe(e,t)}},CLASS:function(e){var t=s[e+" "];return t||(t=new RegExp("(^|"+ge+")"+e+"("+ge+"|$)"))&&s(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=I.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function T(e,n,r){return v(n)?ce.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?ce.grep(e,function(e){return e===n!==r}):"string"!=typeof n?ce.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(ce.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||k,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:S.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof ce?t[0]:t,ce.merge(this,ce.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:C,!0)),w.test(r[1])&&ce.isPlainObject(t))for(r in t)v(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=C.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):v(e)?void 0!==n.ready?n.ready(e):e(ce):ce.makeArray(e,this)}).prototype=ce.fn,k=ce(C);var E=/^(?:parents|prev(?:Until|All))/,j={children:!0,contents:!0,next:!0,prev:!0};function A(e,t){while((e=e[t])&&1!==e.nodeType);return e}ce.fn.extend({has:function(e){var t=ce(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,Ce=/^$|^module$|\/(?:java|ecma)script/i;xe=C.createDocumentFragment().appendChild(C.createElement("div")),(be=C.createElement("input")).setAttribute("type","radio"),be.setAttribute("checked","checked"),be.setAttribute("name","t"),xe.appendChild(be),le.checkClone=xe.cloneNode(!0).cloneNode(!0).lastChild.checked,xe.innerHTML="",le.noCloneChecked=!!xe.cloneNode(!0).lastChild.defaultValue,xe.innerHTML="",le.option=!!xe.lastChild;var ke={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function Se(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&fe(e,t)?ce.merge([e],n):n}function Ee(e,t){for(var n=0,r=e.length;n",""]);var je=/<|&#?\w+;/;function Ae(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function Re(e,t){return fe(e,"table")&&fe(11!==t.nodeType?t:t.firstChild,"tr")&&ce(e).children("tbody")[0]||e}function Ie(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function We(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Fe(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(_.hasData(e)&&(s=_.get(e).events))for(i in _.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),C.head.appendChild(r[0])},abort:function(){i&&i()}}});var Jt,Kt=[],Zt=/(=)\?(?=&|$)|\?\?/;ce.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Kt.pop()||ce.expando+"_"+jt.guid++;return this[e]=!0,e}}),ce.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Zt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Zt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=v(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Zt,"$1"+r):!1!==e.jsonp&&(e.url+=(At.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||ce.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=ie[r],ie[r]=function(){o=arguments},n.always(function(){void 0===i?ce(ie).removeProp(r):ie[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Kt.push(r)),o&&v(i)&&i(o[0]),o=i=void 0}),"script"}),le.createHTMLDocument=((Jt=C.implementation.createHTMLDocument("").body).innerHTML="
",2===Jt.childNodes.length),ce.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(le.createHTMLDocument?((r=(t=C.implementation.createHTMLDocument("")).createElement("base")).href=C.location.href,t.head.appendChild(r)):t=C),o=!n&&[],(i=w.exec(e))?[t.createElement(i[1])]:(i=Ae([e],t,o),o&&o.length&&ce(o).remove(),ce.merge([],i.childNodes)));var r,i,o},ce.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(ce.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},ce.expr.pseudos.animated=function(t){return ce.grep(ce.timers,function(e){return t===e.elem}).length},ce.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=ce.css(e,"position"),c=ce(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=ce.css(e,"top"),u=ce.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),v(t)&&(t=t.call(e,n,ce.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},ce.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){ce.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===ce.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===ce.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=ce(e).offset()).top+=ce.css(e,"borderTopWidth",!0),i.left+=ce.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-ce.css(r,"marginTop",!0),left:t.left-i.left-ce.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===ce.css(e,"position"))e=e.offsetParent;return e||J})}}),ce.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;ce.fn[t]=function(e){return M(this,function(e,t,n){var r;if(y(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),ce.each(["top","left"],function(e,n){ce.cssHooks[n]=Ye(le.pixelPosition,function(e,t){if(t)return t=Ge(e,n),_e.test(t)?ce(e).position()[n]+"px":t})}),ce.each({Height:"height",Width:"width"},function(a,s){ce.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){ce.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return M(this,function(e,t,n){var r;return y(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?ce.css(e,t,i):ce.style(e,t,n,i)},s,n?e:void 0,n)}})}),ce.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){ce.fn[t]=function(e){return this.on(t,e)}}),ce.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.on("mouseenter",e).on("mouseleave",t||e)}}),ce.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){ce.fn[n]=function(e,t){return 049?function(){o(t,{timeout:n});if(n!==H.ricTimeout){n=H.ricTimeout}}:te(function(){I(t)},true);return function(e){var t;if(e=e===true){n=33}if(a){return}a=true;t=r-(f.now()-i);if(t<0){t=0}if(e||t<9){s()}else{I(s,t)}}},ie=function(e){var t,a;var i=99;var r=function(){t=null;e()};var n=function(){var e=f.now()-a;if(e0;if(r&&Z(i,"overflow")!="visible"){a=i.getBoundingClientRect();r=C>a.left&&pa.top-1&&g500&&O.clientWidth>500?500:370:H.expand;k._defEx=u;f=u*H.expFactor;c=H.hFac;A=null;if(w2&&h>2&&!D.hidden){w=f;N=0}else if(h>1&&N>1&&M<6){w=u}else{w=_}}if(l!==n){y=innerWidth+n*c;z=innerHeight+n;s=n*-1;l=n}a=d[t].getBoundingClientRect();if((b=a.bottom)>=s&&(g=a.top)<=z&&(C=a.right)>=s*c&&(p=a.left)<=y&&(b||C||p||g)&&(H.loadHidden||x(d[t]))&&(m&&M<3&&!o&&(h<3||N<4)||W(d[t],n))){R(d[t]);r=true;if(M>9){break}}else if(!r&&m&&!i&&M<4&&N<4&&h>2&&(v[0]||H.preloadAfterLoad)&&(v[0]||!o&&(b||C||p||g||d[t][$](H.sizesAttr)!="auto"))){i=v[0]||d[t]}}if(i&&!r){R(i)}}};var a=ae(t);var S=function(e){var t=e.target;if(t._lazyCache){delete t._lazyCache;return}L(e);K(t,H.loadedClass);Q(t,H.loadingClass);V(t,B);X(t,"lazyloaded")};var i=te(S);var B=function(e){i({target:e.target})};var T=function(e,t){var a=e.getAttribute("data-load-mode")||H.iframeLoadMode;if(a==0){e.contentWindow.location.replace(t)}else if(a==1){e.src=t}};var F=function(e){var t;var a=e[$](H.srcsetAttr);if(t=H.customMedia[e[$]("data-media")||e[$]("media")]){e.setAttribute("media",t)}if(a){e.setAttribute("srcset",a)}};var s=te(function(t,e,a,i,r){var n,s,o,l,u,f;if(!(u=X(t,"lazybeforeunveil",e)).defaultPrevented){if(i){if(a){K(t,H.autosizesClass)}else{t.setAttribute("sizes",i)}}s=t[$](H.srcsetAttr);n=t[$](H.srcAttr);if(r){o=t.parentNode;l=o&&j.test(o.nodeName||"")}f=e.firesLoad||"src"in t&&(s||n||l);u={target:t};K(t,H.loadingClass);if(f){clearTimeout(c);c=I(L,2500);V(t,B,true)}if(l){G.call(o.getElementsByTagName("source"),F)}if(s){t.setAttribute("srcset",s)}else if(n&&!l){if(d.test(t.nodeName)){T(t,n)}else{t.src=n}}if(r&&(s||l)){Y(t,{src:n})}}if(t._lazyRace){delete t._lazyRace}Q(t,H.lazyClass);ee(function(){var e=t.complete&&t.naturalWidth>1;if(!f||e){if(e){K(t,H.fastLoadedClass)}S(u);t._lazyCache=true;I(function(){if("_lazyCache"in t){delete t._lazyCache}},9)}if(t.loading=="lazy"){M--}},true)});var R=function(e){if(e._lazyRace){return}var t;var a=n.test(e.nodeName);var i=a&&(e[$](H.sizesAttr)||e[$]("sizes"));var r=i=="auto";if((r||!m)&&a&&(e[$]("src")||e.srcset)&&!e.complete&&!J(e,H.errorClass)&&J(e,H.lazyClass)){return}t=X(e,"lazyunveilread").detail;if(r){re.updateElem(e,true,e.offsetWidth)}e._lazyRace=true;M++;s(e,t,r,i,a)};var r=ie(function(){H.loadMode=3;a()});var o=function(){if(H.loadMode==3){H.loadMode=2}r()};var l=function(){if(m){return}if(f.now()-e<999){I(l,999);return}m=true;H.loadMode=3;a();q("scroll",o,true)};return{_:function(){e=f.now();k.elements=D.getElementsByClassName(H.lazyClass);v=D.getElementsByClassName(H.lazyClass+" "+H.preloadClass);q("scroll",a,true);q("resize",a,true);q("pageshow",function(e){if(e.persisted){var t=D.querySelectorAll("."+H.loadingClass);if(t.length&&t.forEach){U(function(){t.forEach(function(e){if(e.complete){R(e)}})})}}});if(u.MutationObserver){new MutationObserver(a).observe(O,{childList:true,subtree:true,attributes:true})}else{O[P]("DOMNodeInserted",a,true);O[P]("DOMAttrModified",a,true);setInterval(a,999)}q("hashchange",a,true);["focus","mouseover","click","load","transitionend","animationend"].forEach(function(e){D[P](e,a,true)});if(/d$|^c/.test(D.readyState)){l()}else{q("load",l);D[P]("DOMContentLoaded",a);I(l,2e4)}if(k.elements.length){t();ee._lsFlush()}else{a()}},checkElems:a,unveil:R,_aLSL:o}}(),re=function(){var a;var n=te(function(e,t,a,i){var r,n,s;e._lazysizesWidth=i;i+="px";e.setAttribute("sizes",i);if(j.test(t.nodeName||"")){r=t.getElementsByTagName("source");for(n=0,s=r.length;n 0) { - $("#loader_wrap").fadeOut(1000); - } - - if ($(".animate-down").length > 0) { - $(".animate-down").each(function (index) { - var $this = $(this); - setTimeout(function () { - $this.addClass("toggled"); - }, 100 * index); - }); - } - - if ($(".animate-up").length > 0) { - $(".animate-up").each(function (index) { - var $this = $(this); - setTimeout(function () { - $this.addClass("toggled"); - }, 100 * index); - }); - } - - if ($(".pop-in").length > 0) { - $(".pop-in").each(function (index) { - var $this = $(this); - setTimeout(function () { - $this.addClass("toggled"); - }, 100 * index); - }); - } - - if ($(".pop-out").length > 0) { - $(".pop-out").each(function (index) { - var $this = $(this); - setTimeout(function () { - $this.addClass("toggled"); - }, 100 * index); - }); - } + $("#loader_wrap").fadeOut(1000); + $(".animate-down").each(function (index) { + setTimeout(function (elem) { + return elem.addClass("toggled"); + }, 100 * index, $(this)); + }); + $(".animate-up").each(function (index) { + setTimeout(function (elem) { + return elem.addClass("toggled"); + }, 100 * index, $(this)); + }); + $(".pop-in").each(function (index) { + setTimeout(function (elem) { + return elem.addClass("toggled"); + }, 100 * index, $(this)); + }); + $(".pop-out").each(function (index) { + setTimeout(function (elem) { + return elem.addClass("toggled"); + }, 100 * index, $(this)); + }); }; +/** + * @returns {void} + */ landing.runInitAnimationsHome = function () { - if ($(".pop-in").length > 0) { - $(".pop-in").each(function (index) { - var $this = $(this); - setTimeout(function () { - $this.addClass("toggled"); - }, 100 * index); - }); - } - - setTimeout(function () { - $("#intro").fadeOut(1000, function () { - if ($(".pop-in-last").length > 0) { - $(".pop-in-last").each(function (index) { - var $this = $(this); - setTimeout(function () { - $this.addClass("toggled"); - }, 100 * index); - }); - } - if ($(".animate-down").length > 0) { - $(".animate-down").each(function (index) { - var $this = $(this); - setTimeout(function () { - $this.addClass("toggled"); - }, 100 * index); - }); - } - - if ($(".animate-up").length > 0) { - $(".animate-up").each(function (index) { - var $this = $(this); - setTimeout(function () { - $this.addClass("toggled"); - }, 100 * index); - }); - } - }); - }, 2500); + $(".pop-in").each(function (index) { + setTimeout(function (elem) { + return elem.addClass("toggled"); + }, 100 * index, $(this)); + }); + var onFadedOut = function onFadedOut() { + $(".pop-in-last").each(function (index) { + setTimeout(function (elem) { + return elem.addClass("toggled"); + }, 100 * index, $(this)); + }); + $(".animate-down").each(function (index) { + setTimeout(function (elem) { + return elem.addClass("toggled"); + }, 100 * index, $(this)); + }); + $(".animate-up").each(function (index) { + setTimeout(function (elem) { + return elem.addClass("toggled"); + }, 100 * index, $(this)); + }); + }; + setTimeout(function () { + return $("#intro").fadeOut(1000, onFadedOut); + }, 2500); }; - $(document).ready(function () { - // Prevent users from saving images - - /* - $("body").on("contextmenu",function(){ - return false; - }); - */ - - // Toggle menu and menu setup - - $("#intro_content").css({ - paddingTop: ($(window).height() - 50) / 2 + "px" - }); - - $(".sub-menu").hide(); - - // $('#menu a').each(function() { - - // var $this = $(this); - // var href = $(this).attr("href"); - // var text = $(this).html(); - - // // if ( $this.html() == "Store" || $this.closest("ul").hasClass("sub-menu") ) { - // // - // // } else { - // // $("#mobile_menu_wrap").prepend('' + text + ''); - // // } - - // }); - - // $('.sub-menu a').each(function() { - // - // var $this = $(this); - // var href = $(this).attr("href"); - // var text = $(this).html(); - // - // $("#mobile_menu_wrap").append('' + text + ''); - // - // }); - - $("#menu li").hover(function () { - if ($(this).find(".sub-menu").length > 0) { - $(this).find(".sub-menu").show(); - } - }, function () { - if ($(this).find(".sub-menu").length > 0) { - $(this).find(".sub-menu").hide(); - } - }); - - // $('.hamburger').on("click", function() { - // - // $(this).toggleClass("is-active"); - // - // if ( $(this).hasClass("is-active") == true ) { - // $("#mobile_menu_wrap").fadeIn(800); - // - // $("#mobile_menu_wrap a").each(function(index) { - // var $this = $(this); - // setTimeout(function() { - // $this.addClass("popped"); - // }, 100 * index); - // }); - // - // } else { - // $("#mobile_menu_wrap").fadeOut(800); - // $("#mobile_menu_wrap a").removeClass("popped"); - // } - // - // return false; - // }); - - // var homeslider = $('#slides'); - // - // if( homeslider ) { - // - // var homeslider_slides = homeslider.find("li").length; - // var playSpeed = 0; - // - // if ( homeslider_slides > 1 ) { - // playSpeed = 5000; - // } - // - // homeslider.superslides({ - // play : playSpeed, - // pagination : true, - // animation : "fade", - // animation_speed : 1500 - // }); - // } - - // Gallery page - - // $('#gallery_nav a').on("click", function() { - // - // var targets = $(this).data("category"); - // - // $(this).addClass("active").parent().siblings("li").find('a').removeClass("active"); - // - // if ( targets == "all" ) { - // - // $('.grid-item').show(); - // - // } else { - // - // $('.grid-item').each(function() { - // - // var $this = $(this); - // var thisCat = $(this).data("category"); - // - // if ( thisCat.indexOf(targets) >= 0 ) { - // $this.show(); - // } else { - // $this.hide(); - // } - // - // }); - // - // } - // - // galleryGrid.masonry(); - // - // console.log(targets); - // - // return false; - // }); - - if ($("#intro").length > 0) { - landing.runInitAnimationsHome(); - } else { - landing.runInitAnimations(); - } + // Prevent users from saving images + + /* + $("body").on("contextmenu",function(){ + return false; + }); + */ + + // Toggle menu and menu setup + + $("#intro_content").css({ + paddingTop: ($(window).height() - 50) / 2 + "px" + }); + $(".sub-menu").hide(); + + // $('#menu a').each(function() { + + // var $this = $(this); + // var href = $(this).attr("href"); + // var text = $(this).html(); + + // // if ( $this.html() == "Store" || $this.closest("ul").hasClass("sub-menu") ) { + // // + // // } else { + // // $("#mobile_menu_wrap").prepend('' + text + ''); + // // } + + // }); + + // $('.sub-menu a').each(function() { + // + // var $this = $(this); + // var href = $(this).attr("href"); + // var text = $(this).html(); + // + // $("#mobile_menu_wrap").append('' + text + ''); + // + // }); + + $("#menu li").hover(function () { + if ($(this).find(".sub-menu").length > 0) { + $(this).find(".sub-menu").show(); + } + }, function () { + if ($(this).find(".sub-menu").length > 0) { + $(this).find(".sub-menu").hide(); + } + }); + + // $('.hamburger').on("click", function() { + // + // $(this).toggleClass("is-active"); + // + // if ( $(this).hasClass("is-active") == true ) { + // $("#mobile_menu_wrap").fadeIn(800); + // + // $("#mobile_menu_wrap a").each(function(index) { + // var $this = $(this); + // setTimeout(function() { + // $this.addClass("popped"); + // }, 100 * index); + // }); + // + // } else { + // $("#mobile_menu_wrap").fadeOut(800); + // $("#mobile_menu_wrap a").removeClass("popped"); + // } + // + // return false; + // }); + + // var homeslider = $('#slides'); + // + // if( homeslider ) { + // + // var homeslider_slides = homeslider.find("li").length; + // var playSpeed = 0; + // + // if ( homeslider_slides > 1 ) { + // playSpeed = 5000; + // } + // + // homeslider.superslides({ + // play : playSpeed, + // pagination : true, + // animation : "fade", + // animation_speed : 1500 + // }); + // } + + // Gallery page + + // $('#gallery_nav a').on("click", function() { + // + // var targets = $(this).data("category"); + // + // $(this).addClass("active").parent().siblings("li").find('a').removeClass("active"); + // + // if ( targets == "all" ) { + // + // $('.grid-item').show(); + // + // } else { + // + // $('.grid-item').each(function() { + // + // var $this = $(this); + // var thisCat = $(this).data("category"); + // + // if ( thisCat.indexOf(targets) >= 0 ) { + // $this.show(); + // } else { + // $this.hide(); + // } + // + // }); + // + // } + // + // galleryGrid.masonry(); + // + // console.log(targets); + // + // return false; + // }); + + if ($("#intro").length > 0) { + landing.runInitAnimationsHome(); + } else { + landing.runInitAnimations(); + } }); // $(window).load(function() { diff --git a/public/dist/leaflet.markercluster.js.map b/public/dist/leaflet.markercluster.js.map deleted file mode 100644 index c676243a9d8..00000000000 --- a/public/dist/leaflet.markercluster.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["../src/MarkerClusterGroup.js","../src/MarkerCluster.js","../src/MarkerOpacity.js","../src/DistanceGrid.js","../src/MarkerCluster.QuickHull.js","../src/MarkerCluster.Spiderfier.js","../src/MarkerClusterGroup.Refresh.js"],"names":["MarkerClusterGroup","L","FeatureGroup","extend","options","maxClusterRadius","iconCreateFunction","clusterPane","Marker","prototype","pane","spiderfyOnMaxZoom","showCoverageOnHover","zoomToBoundsOnClick","singleMarkerMode","disableClusteringAtZoom","removeOutsideVisibleBounds","animate","animateAddingMarkers","spiderfyShapePositions","spiderfyDistanceMultiplier","spiderLegPolylineOptions","weight","color","opacity","chunkedLoading","chunkInterval","chunkDelay","chunkProgress","polygonOptions","initialize","Util","setOptions","this","_defaultIconCreateFunction","_featureGroup","featureGroup","addEventParent","_nonPointGroup","_inZoomAnimation","_needsClustering","_needsRemoving","_currentShownBounds","_queue","_childMarkerEventHandlers","dragstart","_childMarkerDragStart","move","_childMarkerMoved","dragend","_childMarkerDragEnd","DomUtil","TRANSITION","_withAnimation","_noAnimation","_markerCluster","MarkerCluster","MarkerClusterNonAnimated","addLayer","layer","LayerGroup","addLayers","getLatLng","fire","_map","push","hasLayer","_unspiderfy","_addLayer","_maxZoom","_topClusterLevel","_recalculateBounds","_refreshClustersIcons","visibleLayer","currentZoom","_zoom","__parent","contains","_animationAddLayer","_animationAddLayerNonAnimated","removeLayer","removeLayers","_unspiderfyLayer","_removeLayer","off","clusterShow","_arraySplice","latlng","_latlng","layersArray","skipLayerAddEvent","isArray","m","fg","npg","chunked","l","length","offset","originalArray","started","Date","getTime","process","bind","start","elapsed","slice","_extractNonGroupLayers","getChildCount","markers","getAllChildMarkers","otherMarker","_recursivelyAddChildrenToMap","setTimeout","needsClustering","i","layersArray2","l2","clearLayers","_gridClusters","_gridUnclustered","_noanimationUnspiderfy","eachLayer","marker","_generateInitialClusters","getBounds","bounds","LatLngBounds","_bounds","method","context","thisNeedsRemoving","j","needsRemoving","call","getLayers","layers","getLayer","id","result","parseInt","stamp","anArray","_group","zoomToShowLayer","callback","map","showMarker","_icon","once","spiderfy","Math","round","on","panTo","zoomToBounds","onAdd","isFinite","getMaxZoom","addTo","_maxLat","crs","projection","MAX_LATITUDE","newlatlng","_getExpandedVisibleBounds","_zoomEnd","_moveEnd","_spiderfierOnAdd","_bindEvents","onRemove","_unbindEvents","_mapPane","className","replace","_spiderfierOnRemove","_hideCoverage","remove","getVisibleParent","vMarker","obj","splice","_removeFromGridUnclustered","z","gridUnclustered","minZoom","floor","getMinZoom","removeObject","project","e","target","__dragStart","_ignoreMove","isPopupOpen","_popup","isOpen","_moveChild","oldLatLng","openPopup","from","to","dragStart","removeFromDistanceGrid","dontUpdateMap","gridClusters","cluster","_markers","_childCount","_boundsNeedUpdate","_cLatLng","addObject","_childClusters","_iconNeedsUpdate","_isOrIsParent","el","oel","parentNode","type","data","propagate","originalEvent","relatedTarget","listens","childCount","c","DivIcon","html","iconSize","Point","_zoomOrSpiderfy","_showCoverage","bottomCluster","keyCode","_container","focus","_shownPolygon","_spiderfied","Polygon","getConvexHull","_mergeSplitClusters","newBounds","_recursivelyRemoveChildrenFromMap","maxZoom","ceil","radius","radiusFn","zoom","DistanceGrid","markerPoint","_overrideMarkerIcon","closest","getNearObject","_addChild","parent","newCluster","lastParent","_updateIcon","_enqueue","fn","_queueTimeout","_processQueue","clearTimeout","mapZoom","intersects","_animationStart","_animationZoomIn","_animationZoomOut","Browser","mobile","_checkBoundsMaxLat","pad","_mapBoundsInfinite","maxLat","undefined","getNorth","_northEast","lat","Infinity","getSouth","_southWest","_addToMap","group","output","icon","include","LatLng","previousZoomLevel","newZoomLevel","_recursively","startPos","_isSingleParent","clusterHide","_forceLayout","_recursivelyBecomeVisible","n","_recursivelyRestoreChildPositions","_animationEnd","_animationZoomOutSingle","me","_setPos","latLngToLayerPoint","_recursivelyAnimateChildrenInAndAddSelfToMap","setLatLng","falseFn","document","body","offsetWidth","markerClusterGroup","Icon","a","b","storageArray","ignoreDraggedMarker","fitBoundsOptions","childClusters","boundsZoom","getBoundsZoom","getZoom","newClusters","concat","setView","fitBounds","setIcon","createIcon","_iconObj","createShadow","new1","isNotificationFromChild","_setClusterCenter","child","_resetBounds","lng","childLatLng","latSum","lngSum","totalCount","_wLatLng","_backupLatlng","_recursivelyAnimateChildrenIn","center","cm","mapMinZoom","zoomLevel","nm","_restorePosition","k","previousBounds","exceptBounds","boundsToApplyTo","zoomLevelToStart","zoomLevelToStop","runAtEveryLevel","runAtBottomLevel","backup","setOpacity","cellSize","_cellSize","_sqCellSize","_grid","_objectPoint","point","x","_getCoord","y","grid","row","cell","updateObject","len","eachObject","dist","objectPoint","closestDistSq","_sqDist","coord","p","p2","dx","dy","QuickHull","getDistant","cpt","bl","vY","findMostDistantPointFromBaseLine","baseLine","latLngs","pt","d","maxD","maxPt","newPoints","maxPoint","buildConvexHull","convexHullBaseLines","t","minLat","maxLng","minLng","maxLatPt","minLatPt","maxLngPt","minLngPt","minPt","childMarkers","points","_2PI","PI","_circleFootSeparation","_circleStartAngle","_spiralFootSeparation","_spiralLengthStart","_spiralLengthFactor","_circleSpiralSwitchover","positions","_generatePointsSpiral","_generatePointsCircle","_animationSpiderfy","unspiderfy","zoomDetails","_animationUnspiderfy","count","centerPt","angle","legLength","angleStep","res","max","cos","sin","_round","separation","lengthFactor","_preSpiderfyLatlng","setZIndexOffset","_spiderLeg","leg","newPos","legOptions","layerPointToLatLng","Polyline","legPath","thisLayerLatLng","thisLayerPos","svg","Path","SVG","finalLegOpacity","_path","getTotalLength","style","strokeDasharray","strokeDashoffset","setStyle","nonAnimatable","_latLngToNewLayerPoint","closePopup","stillThereChildCount","apply","arguments","_unspiderfyWrapper","zoomAnimation","_unspiderfyZoomStart","touch","getRenderer","_unspiderfyZoomAnim","hasClass","refreshClusters","_layers","_flagParentsIconsNeedUpdate","_refreshSingleMarkerModeMarkers","refreshIconOptions","directlyRefreshClusters"],"mappings":"4OAIA,IAAWA,EAAqBC,EAAED,mBAAqBC,EAAEC,aAAaC,OAAO,CAE5EC,QAAS,CACRC,iBAAkB,GAClBC,mBAAoB,KACpBC,YAAaN,EAAEO,OAAOC,UAAUL,QAAQM,KAExCC,mBAAmB,EACnBC,qBAAqB,EACrBC,qBAAqB,EACrBC,kBAAkB,EAElBC,wBAAyB,KAIzBC,4BAA4B,EAK5BC,SAAS,EAITC,sBAAsB,EAGtBC,uBAAwB,KAGxBC,2BAA4B,EAG5BC,yBAA0B,CAAEC,OAAQ,IAAKC,MAAO,OAAQC,QAAS,IAGjEC,gBAAgB,EAChBC,cAAe,IACfC,WAAY,GACZC,cAAe,KAGfC,eAAgB,IAGjBC,WAAY,SAAU1B,GACrBH,EAAE8B,KAAKC,WAAWC,KAAM7B,GACnB6B,KAAK7B,QAAQE,qBACjB2B,KAAK7B,QAAQE,mBAAqB2B,KAAKC,4BAGxCD,KAAKE,cAAgBlC,EAAEmC,eACvBH,KAAKE,cAAcE,eAAeJ,MAElCA,KAAKK,eAAiBrC,EAAEmC,eACxBH,KAAKK,eAAeD,eAAeJ,MAEnCA,KAAKM,iBAAmB,EACxBN,KAAKO,iBAAmB,GACxBP,KAAKQ,eAAiB,GAEtBR,KAAKS,oBAAsB,KAE3BT,KAAKU,OAAS,GAEdV,KAAKW,0BAA4B,CAChCC,UAAaZ,KAAKa,sBAClBC,KAAQd,KAAKe,kBACbC,QAAWhB,KAAKiB,qBAIjB,IAAIjC,EAAUhB,EAAEkD,QAAQC,YAAcnB,KAAK7B,QAAQa,QACnDhB,EAAEE,OAAO8B,KAAMhB,EAAUgB,KAAKoB,eAAiBpB,KAAKqB,cAEpDrB,KAAKsB,eAAiBtC,EAAUhB,EAAEuD,cAAgBvD,EAAEwD,0BAGrDC,SAAU,SAAUC,GAEnB,GAAIA,aAAiB1D,EAAE2D,WACtB,OAAO3B,KAAK4B,UAAU,CAACF,IAIxB,IAAKA,EAAMG,UAGV,OAFA7B,KAAKK,eAAeoB,SAASC,GAC7B1B,KAAK8B,KAAK,WAAY,CAAEJ,MAAOA,IACxB1B,KAGR,IAAKA,KAAK+B,KAGT,OAFA/B,KAAKO,iBAAiByB,KAAKN,GAC3B1B,KAAK8B,KAAK,WAAY,CAAEJ,MAAOA,IACxB1B,KAGR,GAAIA,KAAKiC,SAASP,GACjB,OAAO1B,KAMJA,KAAKkC,aACRlC,KAAKkC,cAGNlC,KAAKmC,UAAUT,EAAO1B,KAAKoC,UAC3BpC,KAAK8B,KAAK,WAAY,CAAEJ,MAAOA,IAG/B1B,KAAKqC,iBAAiBC,qBAEtBtC,KAAKuC,wBAGL,IAAIC,EAAed,EACfe,EAAczC,KAAK0C,MACvB,GAAIhB,EAAMiB,SACT,KAAOH,EAAaG,SAASD,OAASD,GACrCD,EAAeA,EAAaG,SAW9B,OAPI3C,KAAKS,oBAAoBmC,SAASJ,EAAaX,eAC9C7B,KAAK7B,QAAQc,qBAChBe,KAAK6C,mBAAmBnB,EAAOc,GAE/BxC,KAAK8C,8BAA8BpB,EAAOc,IAGrCxC,MAGR+C,YAAa,SAAUrB,GAEtB,OAAIA,aAAiB1D,EAAE2D,WACf3B,KAAKgD,aAAa,CAACtB,KAItBA,EAAMG,UAMN7B,KAAK+B,KAQLL,EAAMiB,WAIP3C,KAAKkC,cACRlC,KAAKkC,cACLlC,KAAKiD,iBAAiBvB,IAIvB1B,KAAKkD,aAAaxB,GAAO,GACzB1B,KAAK8B,KAAK,cAAe,CAAEJ,MAAOA,IAGlC1B,KAAKqC,iBAAiBC,qBAEtBtC,KAAKuC,wBAELb,EAAMyB,IAAInD,KAAKW,0BAA2BX,MAEtCA,KAAKE,cAAc+B,SAASP,KAC/B1B,KAAKE,cAAc6C,YAAYrB,GAC3BA,EAAM0B,aACT1B,EAAM0B,kBA9BFpD,KAAKqD,aAAarD,KAAKO,iBAAkBmB,IAAU1B,KAAKiC,SAASP,IACrE1B,KAAKQ,eAAewB,KAAK,CAAEN,MAAOA,EAAO4B,OAAQ5B,EAAM6B,UAExDvD,KAAK8B,KAAK,cAAe,CAAEJ,MAAOA,MATlC1B,KAAKK,eAAe0C,YAAYrB,GAChC1B,KAAK8B,KAAK,cAAe,CAAEJ,MAAOA,KAuC5B1B,OAIR4B,UAAW,SAAU4B,EAAaC,GACjC,IAAKzF,EAAE8B,KAAK4D,QAAQF,GACnB,OAAOxD,KAAKyB,SAAS+B,GAGtB,IAQIG,EARAC,EAAK5D,KAAKE,cACV2D,EAAM7D,KAAKK,eACXyD,EAAU9D,KAAK7B,QAAQqB,eACvBC,EAAgBO,KAAK7B,QAAQsB,cAC7BE,EAAgBK,KAAK7B,QAAQwB,cAC7BoE,EAAIP,EAAYQ,OAChBC,EAAS,EACTC,GAAgB,EAGpB,GAAIlE,KAAK+B,KAAM,CACd,IAAIoC,GAAU,IAAKC,MAAQC,UACvBC,EAAUtG,EAAEuG,KAAK,WACpB,IAAIC,GAAQ,IAAKJ,MAAQC,UAOzB,IAJIrE,KAAK+B,MAAQ/B,KAAKkC,aACrBlC,KAAKkC,cAGC+B,EAASF,EAAGE,IAAU,CAC5B,GAAIH,GAAWG,EAAS,KAAQ,EAAG,CAElC,IAAIQ,GAAU,IAAKL,MAAQC,UAAYG,EACvC,GAAc/E,EAAVgF,EACH,MAYF,IARAd,EAAIH,EAAYS,cAQCjG,EAAE2D,WACduC,IACHV,EAAcA,EAAYkB,QAC1BR,GAAgB,GAEjBlE,KAAK2E,uBAAuBhB,EAAGH,GAC/BO,EAAIP,EAAYQ,YAKjB,GAAKL,EAAE9B,WAQP,IAAI7B,KAAKiC,SAAS0B,KAIlB3D,KAAKmC,UAAUwB,EAAG3D,KAAKoC,UAClBqB,GACJzD,KAAK8B,KAAK,WAAY,CAAEJ,MAAOiC,IAI5BA,EAAEhB,UAC8B,IAA/BgB,EAAEhB,SAASiC,iBAAuB,CACrC,IAAIC,EAAUlB,EAAEhB,SAASmC,qBACrBC,EAAcF,EAAQ,KAAOlB,EAAIkB,EAAQ,GAAKA,EAAQ,GAC1DjB,EAAGb,YAAYgC,SArBhBlB,EAAIpC,SAASkC,GACRF,GACJzD,KAAK8B,KAAK,WAAY,CAAEJ,MAAOiC,IAwB9BhE,GAEHA,EAAcsE,EAAQF,GAAG,IAAKK,MAAQC,UAAYF,GAI/CF,IAAWF,GAGd/D,KAAKqC,iBAAiBC,qBAEtBtC,KAAKuC,wBAELvC,KAAKqC,iBAAiB2C,6BAA6B,KAAMhF,KAAK0C,MAAO1C,KAAKS,sBAE1EwE,WAAWX,EAAStE,KAAK7B,QAAQuB,aAEhCM,MAEHsE,SAIA,IAFA,IAAIY,EAAkBlF,KAAKO,iBAEpB0D,EAASF,EAAGE,KAClBN,EAAIH,EAAYS,cAGCjG,EAAE2D,YACduC,IACHV,EAAcA,EAAYkB,QAC1BR,GAAgB,GAEjBlE,KAAK2E,uBAAuBhB,EAAGH,GAC/BO,EAAIP,EAAYQ,QAKZL,EAAE9B,UAKH7B,KAAKiC,SAAS0B,IAIlBuB,EAAgBlD,KAAK2B,GARpBE,EAAIpC,SAASkC,GAWhB,OAAO3D,MAIRgD,aAAc,SAAUQ,GACvB,IAAI2B,EAAGxB,EACHI,EAAIP,EAAYQ,OAChBJ,EAAK5D,KAAKE,cACV2D,EAAM7D,KAAKK,eACX6D,GAAgB,EAEpB,IAAKlE,KAAK+B,KAAM,CACf,IAAKoD,EAAI,EAAGA,EAAIpB,EAAGoB,KAClBxB,EAAIH,EAAY2B,cAGCnH,EAAE2D,YACduC,IACHV,EAAcA,EAAYkB,QAC1BR,GAAgB,GAEjBlE,KAAK2E,uBAAuBhB,EAAGH,GAC/BO,EAAIP,EAAYQ,SAIjBhE,KAAKqD,aAAarD,KAAKO,iBAAkBoD,GACzCE,EAAId,YAAYY,GACZ3D,KAAKiC,SAAS0B,IACjB3D,KAAKQ,eAAewB,KAAK,CAAEN,MAAOiC,EAAGL,OAAQK,EAAEJ,UAEhDvD,KAAK8B,KAAK,cAAe,CAAEJ,MAAOiC,KAEnC,OAAO3D,KAGR,GAAIA,KAAKkC,YAAa,CACrBlC,KAAKkC,cAGL,IAAIkD,EAAe5B,EAAYkB,QAC3BW,EAAKtB,EACT,IAAKoB,EAAI,EAAGA,EAAIE,EAAIF,KACnBxB,EAAIyB,EAAaD,cAGAnH,EAAE2D,YAClB3B,KAAK2E,uBAAuBhB,EAAGyB,GAC/BC,EAAKD,EAAapB,QAInBhE,KAAKiD,iBAAiBU,GAIxB,IAAKwB,EAAI,EAAGA,EAAIpB,EAAGoB,KAClBxB,EAAIH,EAAY2B,cAGCnH,EAAE2D,YACduC,IACHV,EAAcA,EAAYkB,QAC1BR,GAAgB,GAEjBlE,KAAK2E,uBAAuBhB,EAAGH,GAC/BO,EAAIP,EAAYQ,QAIZL,EAAEhB,UAMP3C,KAAKkD,aAAaS,GAAG,GAAM,GAC3B3D,KAAK8B,KAAK,cAAe,CAAEJ,MAAOiC,IAE9BC,EAAG3B,SAAS0B,KACfC,EAAGb,YAAYY,GACXA,EAAEP,aACLO,EAAEP,iBAXHS,EAAId,YAAYY,GAChB3D,KAAK8B,KAAK,cAAe,CAAEJ,MAAOiC,KAuBpC,OAPA3D,KAAKqC,iBAAiBC,qBAEtBtC,KAAKuC,wBAGLvC,KAAKqC,iBAAiB2C,6BAA6B,KAAMhF,KAAK0C,MAAO1C,KAAKS,qBAEnET,MAIRsF,YAAa,WA6BZ,OAzBKtF,KAAK+B,OACT/B,KAAKO,iBAAmB,GACxBP,KAAKQ,eAAiB,UACfR,KAAKuF,qBACLvF,KAAKwF,kBAGTxF,KAAKyF,wBACRzF,KAAKyF,yBAINzF,KAAKE,cAAcoF,cACnBtF,KAAKK,eAAeiF,cAEpBtF,KAAK0F,UAAU,SAAUC,GACxBA,EAAOxC,IAAInD,KAAKW,0BAA2BX,aACpC2F,EAAOhD,UACZ3C,MAECA,KAAK+B,MAER/B,KAAK4F,2BAGC5F,MAIR6F,UAAW,WACV,IAAIC,EAAS,IAAI9H,EAAE+H,aAEf/F,KAAKqC,kBACRyD,EAAO5H,OAAO8B,KAAKqC,iBAAiB2D,SAGrC,IAAK,IAAIb,EAAInF,KAAKO,iBAAiByD,OAAS,EAAQ,GAALmB,EAAQA,IACtDW,EAAO5H,OAAO8B,KAAKO,iBAAiB4E,GAAGtD,aAKxC,OAFAiE,EAAO5H,OAAO8B,KAAKK,eAAewF,aAE3BC,GAIRJ,UAAW,SAAUO,EAAQC,GAC5B,IAECC,EAAmBhB,EAAGiB,EAFnBvB,EAAU7E,KAAKO,iBAAiBmE,QACnC2B,EAAgBrG,KAAKQ,eAOtB,IAJIR,KAAKqC,kBACRrC,KAAKqC,iBAAiByC,mBAAmBD,GAGrCM,EAAIN,EAAQb,OAAS,EAAQ,GAALmB,EAAQA,IAAK,CAGzC,IAFAgB,GAAoB,EAEfC,EAAIC,EAAcrC,OAAS,EAAQ,GAALoC,EAAQA,IAC1C,GAAIC,EAAcD,GAAG1E,QAAUmD,EAAQM,GAAI,CAC1CgB,GAAoB,EACpB,MAIEA,GACHF,EAAOK,KAAKJ,EAASrB,EAAQM,IAI/BnF,KAAKK,eAAeqF,UAAUO,EAAQC,IAIvCK,UAAW,WACV,IAAIC,EAAS,GAIb,OAHAxG,KAAK0F,UAAU,SAAU3B,GACxByC,EAAOxE,KAAK+B,KAENyC,GAIRC,SAAU,SAAUC,GACnB,IAAIC,EAAS,KAUb,OARAD,EAAKE,SAASF,EAAI,IAElB1G,KAAK0F,UAAU,SAAU3B,GACpB/F,EAAE6I,MAAM9C,KAAO2C,IAClBC,EAAS5C,KAIJ4C,GAIR1E,SAAU,SAAUP,GACnB,IAAKA,EACJ,OAAO,EAGR,IAAIyD,EAAG2B,EAAU9G,KAAKO,iBAEtB,IAAK4E,EAAI2B,EAAQ9C,OAAS,EAAQ,GAALmB,EAAQA,IACpC,GAAI2B,EAAQ3B,KAAOzD,EAClB,OAAO,EAKT,IAAKyD,GADL2B,EAAU9G,KAAKQ,gBACEwD,OAAS,EAAQ,GAALmB,EAAQA,IACpC,GAAI2B,EAAQ3B,GAAGzD,QAAUA,EACxB,OAAO,EAIT,SAAUA,EAAMiB,UAAYjB,EAAMiB,SAASoE,SAAW/G,OAASA,KAAKK,eAAe4B,SAASP,IAI7FsF,gBAAiB,SAAUtF,EAAOuF,GAEjC,IAAIC,EAAMlH,KAAK+B,KAES,mBAAbkF,IACVA,EAAW,cAGZ,IAAIE,EAAa,YAGXD,EAAIjF,SAASP,KAAUwF,EAAIjF,SAASP,EAAMiB,WAAe3C,KAAKM,mBAClEN,KAAK+B,KAAKoB,IAAI,UAAWgE,EAAYnH,MACrCA,KAAKmD,IAAI,eAAgBgE,EAAYnH,MAEjCkH,EAAIjF,SAASP,GAChBuF,IACUvF,EAAMiB,SAASyE,QACzBpH,KAAKqH,KAAK,aAAcJ,EAAUjH,MAClC0B,EAAMiB,SAAS2E,cAKd5F,EAAM0F,OAASpH,KAAK+B,KAAK8D,YAAYjD,SAASlB,EAAMG,aAEvDoF,IACUvF,EAAMiB,SAASD,MAAQ6E,KAAKC,MAAMxH,KAAK+B,KAAKW,QAEtD1C,KAAK+B,KAAK0F,GAAG,UAAWN,EAAYnH,MACpCA,KAAK+B,KAAK2F,MAAMhG,EAAMG,eAEtB7B,KAAK+B,KAAK0F,GAAG,UAAWN,EAAYnH,MACpCA,KAAKyH,GAAG,eAAgBN,EAAYnH,MACpC0B,EAAMiB,SAASgF,iBAKjBC,MAAO,SAAUV,GAEhB,IAAI/B,EAAGpB,EAAGrC,EAEV,GAHA1B,KAAK+B,KAAOmF,GAGPW,SAAS7H,KAAK+B,KAAK+F,cACvB,KAAM,+BAaP,IAVA9H,KAAKE,cAAc6H,MAAMb,GACzBlH,KAAKK,eAAe0H,MAAMb,GAErBlH,KAAKuF,eACTvF,KAAK4F,2BAGN5F,KAAKgI,QAAUd,EAAI/I,QAAQ8J,IAAIC,WAAWC,aAGrChD,EAAI,EAAGpB,EAAI/D,KAAKQ,eAAewD,OAAQmB,EAAIpB,EAAGoB,KAClDzD,EAAQ1B,KAAKQ,eAAe2E,IACtBiD,UAAY1G,EAAMA,MAAM6B,QAC9B7B,EAAMA,MAAM6B,QAAU7B,EAAM4B,OAG7B,IAAK6B,EAAI,EAAGpB,EAAI/D,KAAKQ,eAAewD,OAAQmB,EAAIpB,EAAGoB,IAClDzD,EAAQ1B,KAAKQ,eAAe2E,GAC5BnF,KAAKkD,aAAaxB,EAAMA,OAAO,GAC/BA,EAAMA,MAAM6B,QAAU7B,EAAM0G,UAE7BpI,KAAKQ,eAAiB,GAGtBR,KAAK0C,MAAQ6E,KAAKC,MAAMxH,KAAK+B,KAAKW,OAClC1C,KAAKS,oBAAsBT,KAAKqI,4BAEhCrI,KAAK+B,KAAK0F,GAAG,UAAWzH,KAAKsI,SAAUtI,MACvCA,KAAK+B,KAAK0F,GAAG,UAAWzH,KAAKuI,SAAUvI,MAEnCA,KAAKwI,kBACRxI,KAAKwI,mBAGNxI,KAAKyI,cAGL1E,EAAI/D,KAAKO,iBACTP,KAAKO,iBAAmB,GACxBP,KAAK4B,UAAUmC,GAAG,IAInB2E,SAAU,SAAUxB,GACnBA,EAAI/D,IAAI,UAAWnD,KAAKsI,SAAUtI,MAClCkH,EAAI/D,IAAI,UAAWnD,KAAKuI,SAAUvI,MAElCA,KAAK2I,gBAGL3I,KAAK+B,KAAK6G,SAASC,UAAY7I,KAAK+B,KAAK6G,SAASC,UAAUC,QAAQ,wBAAyB,IAEzF9I,KAAK+I,qBACR/I,KAAK+I,6BAGC/I,KAAKgI,QAGZhI,KAAKgJ,gBACLhJ,KAAKE,cAAc+I,SACnBjJ,KAAKK,eAAe4I,SAEpBjJ,KAAKE,cAAcoF,cAEnBtF,KAAK+B,KAAO,MAGbmH,iBAAkB,SAAUvD,GAE3B,IADA,IAAIwD,EAAUxD,EACPwD,IAAYA,EAAQ/B,OAC1B+B,EAAUA,EAAQxG,SAEnB,OAAOwG,GAAW,MAInB9F,aAAc,SAAUyD,EAASsC,GAChC,IAAK,IAAIjE,EAAI2B,EAAQ9C,OAAS,EAAQ,GAALmB,EAAQA,IACxC,GAAI2B,EAAQ3B,KAAOiE,EAElB,OADAtC,EAAQuC,OAAOlE,EAAG,IACX,GAWVmE,2BAA4B,SAAU3D,EAAQ4D,GAK7C,IAJA,IAAIrC,EAAMlH,KAAK+B,KACXyH,EAAkBxJ,KAAKwF,iBAC1BiE,EAAUlC,KAAKmC,MAAM1J,KAAK+B,KAAK4H,cAEpBF,GAALF,GACDC,EAAgBD,GAAGK,aAAajE,EAAQuB,EAAI2C,QAAQlE,EAAO9D,YAAa0H,IADzDA,OAOtB1I,sBAAuB,SAAUiJ,GAChCA,EAAEC,OAAOC,YAAcF,EAAEC,OAAOxG,SAGjCxC,kBAAmB,SAAU+I,GAC5B,IAAK9J,KAAKiK,cAAgBH,EAAEC,OAAOC,YAAa,CAC/C,IAAIE,EAAcJ,EAAEC,OAAOI,QAAUL,EAAEC,OAAOI,OAAOC,SAErDpK,KAAKqK,WAAWP,EAAEC,OAAQD,EAAEQ,UAAWR,EAAExG,QAErC4G,GACHJ,EAAEC,OAAOQ,cAKZF,WAAY,SAAU3I,EAAO8I,EAAMC,GAClC/I,EAAM6B,QAAUiH,EAChBxK,KAAK+C,YAAYrB,GAEjBA,EAAM6B,QAAUkH,EAChBzK,KAAKyB,SAASC,IAGfT,oBAAqB,SAAU6I,GAC9B,IAAIY,EAAYZ,EAAEC,OAAOC,mBAClBF,EAAEC,OAAOC,YACZU,GACH1K,KAAKqK,WAAWP,EAAEC,OAAQW,EAAWZ,EAAEC,OAAOxG,UAOhDL,aAAc,SAAUyC,EAAQgF,EAAwBC,GACvD,IAAIC,EAAe7K,KAAKuF,cACvBiE,EAAkBxJ,KAAKwF,iBACvB5B,EAAK5D,KAAKE,cACVgH,EAAMlH,KAAK+B,KACX0H,EAAUlC,KAAKmC,MAAM1J,KAAK+B,KAAK4H,cAG5BgB,GACH3K,KAAKsJ,2BAA2B3D,EAAQ3F,KAAKoC,UAI9C,IAEC2C,EAFG+F,EAAUnF,EAAOhD,SACpBkC,EAAUiG,EAAQC,SAMnB,IAFA/K,KAAKqD,aAAawB,EAASc,GAEpBmF,IACNA,EAAQE,cACRF,EAAQG,mBAAoB,IAExBH,EAAQpI,MAAQ+G,KAGTkB,GAA0BG,EAAQE,aAAe,GAE3DjG,EAAc+F,EAAQC,SAAS,KAAOpF,EAASmF,EAAQC,SAAS,GAAKD,EAAQC,SAAS,GAGtFF,EAAaC,EAAQpI,OAAOkH,aAAakB,EAAS5D,EAAI2C,QAAQiB,EAAQI,SAAUJ,EAAQpI,QACxF8G,EAAgBsB,EAAQpI,OAAOyI,UAAUpG,EAAamC,EAAI2C,QAAQ9E,EAAYlD,YAAaiJ,EAAQpI,QAGnG1C,KAAKqD,aAAayH,EAAQnI,SAASyI,eAAgBN,GACnDA,EAAQnI,SAASoI,SAAS/I,KAAK+C,GAC/BA,EAAYpC,SAAWmI,EAAQnI,SAE3BmI,EAAQ1D,QAEXxD,EAAGb,YAAY+H,GACVF,GACJhH,EAAGnC,SAASsD,KAId+F,EAAQO,kBAAmB,EAG5BP,EAAUA,EAAQnI,gBAGZgD,EAAOhD,UAGf2I,cAAe,SAAUC,EAAIC,GAC5B,KAAOA,GAAK,CACX,GAAID,IAAOC,EACV,OAAO,EAERA,EAAMA,EAAIC,WAEX,OAAO,GAIR3J,KAAM,SAAU4J,EAAMC,EAAMC,GAC3B,GAAID,GAAQA,EAAKjK,iBAAiB1D,EAAEuD,cAAe,CAElD,GAAIoK,EAAKE,eAAiB7L,KAAKsL,cAAcK,EAAKjK,MAAM0F,MAAOuE,EAAKE,cAAcC,eACjF,OAEDJ,EAAO,UAAYA,EAGpB1N,EAAEC,aAAaO,UAAUsD,KAAKwE,KAAKtG,KAAM0L,EAAMC,EAAMC,IAItDG,QAAS,SAAUL,EAAME,GACxB,OAAO5N,EAAEC,aAAaO,UAAUuN,QAAQzF,KAAKtG,KAAM0L,EAAME,IAAc5N,EAAEC,aAAaO,UAAUuN,QAAQzF,KAAKtG,KAAM,UAAY0L,EAAME,IAItI3L,2BAA4B,SAAU6K,GACrC,IAAIkB,EAAalB,EAAQlG,gBAErBqH,EAAI,mBASR,OAPCA,GADGD,EAAa,GACX,QACKA,EAAa,IAClB,SAEA,QAGC,IAAIhO,EAAEkO,QAAQ,CAAEC,KAAM,cAAgBH,EAAa,gBAAiBnD,UAAW,iBAAmBoD,EAAGG,SAAU,IAAIpO,EAAEqO,MAAM,GAAI,OAGvI5D,YAAa,WACZ,IAAIvB,EAAMlH,KAAK+B,KACXrD,EAAoBsB,KAAK7B,QAAQO,kBACjCC,EAAsBqB,KAAK7B,QAAQQ,oBACnCC,EAAsBoB,KAAK7B,QAAQS,qBAGnCF,GAAqBE,IACxBoB,KAAKyH,GAAG,eAAgBzH,KAAKsM,gBAAiBtM,MAI3CrB,IACHqB,KAAKyH,GAAG,mBAAoBzH,KAAKuM,cAAevM,MAChDA,KAAKyH,GAAG,kBAAmBzH,KAAKgJ,cAAehJ,MAC/CkH,EAAIO,GAAG,UAAWzH,KAAKgJ,cAAehJ,QAIxCsM,gBAAiB,SAAUxC,GAI1B,IAHA,IAAIgB,EAAUhB,EAAEpI,MACZ8K,EAAgB1B,EAE2B,IAAxC0B,EAAcpB,eAAepH,QACnCwI,EAAgBA,EAAcpB,eAAe,GAG1CoB,EAAc9J,QAAU1C,KAAKoC,UAChCoK,EAAcxB,cAAgBF,EAAQE,aACtChL,KAAK7B,QAAQO,kBAGboM,EAAQxD,WACEtH,KAAK7B,QAAQS,qBACvBkM,EAAQnD,eAILmC,EAAE+B,eAA6C,KAA5B/B,EAAE+B,cAAcY,SACtCzM,KAAK+B,KAAK2K,WAAWC,SAIvBJ,cAAe,SAAUzC,GACxB,IAAI5C,EAAMlH,KAAK+B,KACX/B,KAAKM,mBAGLN,KAAK4M,eACR1F,EAAInE,YAAY/C,KAAK4M,eAEQ,EAA1B9C,EAAEpI,MAAMkD,iBAAuBkF,EAAEpI,QAAU1B,KAAK6M,cACnD7M,KAAK4M,cAAgB,IAAI5O,EAAE8O,QAAQhD,EAAEpI,MAAMqL,gBAAiB/M,KAAK7B,QAAQyB,gBACzEsH,EAAIzF,SAASzB,KAAK4M,kBAIpB5D,cAAe,WACVhJ,KAAK4M,gBACR5M,KAAK+B,KAAKgB,YAAY/C,KAAK4M,eAC3B5M,KAAK4M,cAAgB,OAIvBjE,cAAe,WACd,IAAIjK,EAAoBsB,KAAK7B,QAAQO,kBACpCC,EAAsBqB,KAAK7B,QAAQQ,oBACnCC,EAAsBoB,KAAK7B,QAAQS,oBACnCsI,EAAMlH,KAAK+B,MAERrD,GAAqBE,IACxBoB,KAAKmD,IAAI,eAAgBnD,KAAKsM,gBAAiBtM,MAE5CrB,IACHqB,KAAKmD,IAAI,mBAAoBnD,KAAKuM,cAAevM,MACjDA,KAAKmD,IAAI,kBAAmBnD,KAAKgJ,cAAehJ,MAChDkH,EAAI/D,IAAI,UAAWnD,KAAKgJ,cAAehJ,QAIzCsI,SAAU,WACJtI,KAAK+B,OAGV/B,KAAKgN,sBAELhN,KAAK0C,MAAQ6E,KAAKC,MAAMxH,KAAK+B,KAAKW,OAClC1C,KAAKS,oBAAsBT,KAAKqI,8BAGjCE,SAAU,WACT,IAAIvI,KAAKM,iBAAT,CAIA,IAAI2M,EAAYjN,KAAKqI,4BAErBrI,KAAKqC,iBAAiB6K,kCAAkClN,KAAKS,oBAAqB8G,KAAKmC,MAAM1J,KAAK+B,KAAK4H,cAAe3J,KAAK0C,MAAOuK,GAClIjN,KAAKqC,iBAAiB2C,6BAA6B,KAAMuC,KAAKC,MAAMxH,KAAK+B,KAAKW,OAAQuK,GAEtFjN,KAAKS,oBAAsBwM,IAI5BrH,yBAA0B,WACzB,IAAIuH,EAAU5F,KAAK6F,KAAKpN,KAAK+B,KAAK+F,cACjC2B,EAAUlC,KAAKmC,MAAM1J,KAAK+B,KAAK4H,cAC/B0D,EAASrN,KAAK7B,QAAQC,iBACtBkP,EAAWD,EAKU,mBAAXA,IACVC,EAAW,WAAc,OAAOD,IAGY,OAAzCrN,KAAK7B,QAAQW,0BAChBqO,EAAUnN,KAAK7B,QAAQW,wBAA0B,GAElDkB,KAAKoC,SAAW+K,EAChBnN,KAAKuF,cAAgB,GACrBvF,KAAKwF,iBAAmB,GAGxB,IAAK,IAAI+H,EAAOJ,EAAiB1D,GAAR8D,EAAiBA,IACzCvN,KAAKuF,cAAcgI,GAAQ,IAAIvP,EAAEwP,aAAaF,EAASC,IACvDvN,KAAKwF,iBAAiB+H,GAAQ,IAAIvP,EAAEwP,aAAaF,EAASC,IAI3DvN,KAAKqC,iBAAmB,IAAIrC,KAAKsB,eAAetB,KAAMyJ,EAAU,IAIjEtH,UAAW,SAAUT,EAAO6L,GAC3B,IAGIE,EAAalE,EAHbsB,EAAe7K,KAAKuF,cACpBiE,EAAkBxJ,KAAKwF,iBAC1BiE,EAAUlC,KAAKmC,MAAM1J,KAAK+B,KAAK4H,cAUhC,IAPI3J,KAAK7B,QAAQU,kBAChBmB,KAAK0N,oBAAoBhM,GAG1BA,EAAM+F,GAAGzH,KAAKW,0BAA2BX,MAG1ByJ,GAAR8D,EAAiBA,IAAQ,CAC/BE,EAAczN,KAAK+B,KAAK8H,QAAQnI,EAAMG,YAAa0L,GAGnD,IAAII,EAAU9C,EAAa0C,GAAMK,cAAcH,GAC/C,GAAIE,EAGH,OAFAA,EAAQE,UAAUnM,QAClBA,EAAMiB,SAAWgL,GAMlB,GADAA,EAAUnE,EAAgB+D,GAAMK,cAAcH,GACjC,CACZ,IAAIK,EAASH,EAAQhL,SACjBmL,GACH9N,KAAKkD,aAAayK,GAAS,GAK5B,IAAII,EAAa,IAAI/N,KAAKsB,eAAetB,KAAMuN,EAAMI,EAASjM,GAC9DmJ,EAAa0C,GAAMpC,UAAU4C,EAAY/N,KAAK+B,KAAK8H,QAAQkE,EAAW7C,SAAUqC,IAChFI,EAAQhL,SAAWoL,EAInB,IAAIC,EAHJtM,EAAMiB,SAAWoL,EAIjB,IAAKxE,EAAIgE,EAAO,EAAGhE,EAAIuE,EAAOpL,MAAO6G,IACpCyE,EAAa,IAAIhO,KAAKsB,eAAetB,KAAMuJ,EAAGyE,GAC9CnD,EAAatB,GAAG4B,UAAU6C,EAAYhO,KAAK+B,KAAK8H,QAAQ8D,EAAQ9L,YAAa0H,IAO9E,OALAuE,EAAOD,UAAUG,QAGjBhO,KAAKsJ,2BAA2BqE,EAASJ,GAM1C/D,EAAgB+D,GAAMpC,UAAUzJ,EAAO+L,GAIxCzN,KAAKqC,iBAAiBwL,UAAUnM,GAChCA,EAAMiB,SAAW3C,KAAKqC,kBASvBE,sBAAuB,WACtBvC,KAAKE,cAAcwF,UAAU,SAAUuG,GAClCA,aAAajO,EAAEuD,eAAiB0K,EAAEZ,kBACrCY,EAAEgC,iBAMLC,SAAU,SAAUC,GACnBnO,KAAKU,OAAOsB,KAAKmM,GACZnO,KAAKoO,gBACTpO,KAAKoO,cAAgBnJ,WAAWjH,EAAEuG,KAAKvE,KAAKqO,cAAerO,MAAO,OAGpEqO,cAAe,WACd,IAAK,IAAIlJ,EAAI,EAAGA,EAAInF,KAAKU,OAAOsD,OAAQmB,IACvCnF,KAAKU,OAAOyE,GAAGmB,KAAKtG,MAErBA,KAAKU,OAAOsD,OAAS,EACrBsK,aAAatO,KAAKoO,eAClBpO,KAAKoO,cAAgB,MAItBpB,oBAAqB,WACpB,IAAIuB,EAAUhH,KAAKC,MAAMxH,KAAK+B,KAAKW,OAGnC1C,KAAKqO,gBAEDrO,KAAK0C,MAAQ6L,GAAWvO,KAAKS,oBAAoB+N,WAAWxO,KAAKqI,8BACpErI,KAAKyO,kBAELzO,KAAKqC,iBAAiB6K,kCAAkClN,KAAKS,oBAAqB8G,KAAKmC,MAAM1J,KAAK+B,KAAK4H,cAAe3J,KAAK0C,MAAO1C,KAAKqI,6BAEvIrI,KAAK0O,iBAAiB1O,KAAK0C,MAAO6L,IAExBvO,KAAK0C,MAAQ6L,GACvBvO,KAAKyO,kBAELzO,KAAK2O,kBAAkB3O,KAAK0C,MAAO6L,IAEnCvO,KAAKuI,YAKPF,0BAA2B,WAC1B,OAAKrI,KAAK7B,QAAQY,2BAEPf,EAAE4Q,QAAQC,OACb7O,KAAK8O,mBAAmB9O,KAAK+B,KAAK8D,aAGnC7F,KAAK8O,mBAAmB9O,KAAK+B,KAAK8D,YAAYkJ,IAAI,IALjD/O,KAAKgP,oBAkBdF,mBAAoB,SAAUhJ,GAC7B,IAAImJ,EAASjP,KAAKgI,QAWlB,YATekH,IAAXD,IACCnJ,EAAOqJ,YAAcF,IACxBnJ,EAAOsJ,WAAWC,IAAMC,EAAAA,GAErBxJ,EAAOyJ,aAAeN,IACzBnJ,EAAO0J,WAAWH,KAAOC,EAAAA,IAIpBxJ,GAIRhD,8BAA+B,SAAUpB,EAAOqM,GAC/C,GAAIA,IAAerM,EAClB1B,KAAKE,cAAcuB,SAASC,QACtB,GAA+B,IAA3BqM,EAAW/C,YAAmB,CACxC+C,EAAW0B,YAEX,IAAI5K,EAAUkJ,EAAWjJ,qBACzB9E,KAAKE,cAAc6C,YAAY8B,EAAQ,IACvC7E,KAAKE,cAAc6C,YAAY8B,EAAQ,SAEvCkJ,EAAWE,eAWbtJ,uBAAwB,SAAU+K,EAAOC,GACxC,IAEIjO,EAFA8E,EAASkJ,EAAMnJ,YACfpB,EAAI,EAKR,IAFAwK,EAASA,GAAU,GAEZxK,EAAIqB,EAAOxC,OAAQmB,KACzBzD,EAAQ8E,EAAOrB,cAEMnH,EAAE2D,WACtB3B,KAAK2E,uBAAuBjD,EAAOiO,GAIpCA,EAAO3N,KAAKN,GAGb,OAAOiO,GASRjC,oBAAqB,SAAUhM,GAU9B,OATWA,EAAMvD,QAAQyR,KAAO5P,KAAK7B,QAAQE,mBAAmB,CAC/DuG,cAAe,WACd,OAAO,GAERE,mBAAoB,WACnB,MAAO,CAACpD,SASZ1D,EAAED,mBAAmB8R,QAAQ,CAC5Bb,mBAAoB,IAAIhR,EAAE+H,aAAa,IAAI/H,EAAE8R,QAAQR,EAAAA,GAAWA,EAAAA,GAAW,IAAItR,EAAE8R,OAAOR,EAAAA,EAAUA,EAAAA,MAGnGtR,EAAED,mBAAmB8R,QAAQ,CAC5BxO,aAAc,CAEboN,gBAAiB,aAGjBC,iBAAkB,SAAUqB,EAAmBC,GAC9ChQ,KAAKqC,iBAAiB6K,kCAAkClN,KAAKS,oBAAqB8G,KAAKmC,MAAM1J,KAAK+B,KAAK4H,cAAeoG,GACtH/P,KAAKqC,iBAAiB2C,6BAA6B,KAAMgL,EAAchQ,KAAKqI,6BAG5ErI,KAAK8B,KAAK,iBAEX6M,kBAAmB,SAAUoB,EAAmBC,GAC/ChQ,KAAKqC,iBAAiB6K,kCAAkClN,KAAKS,oBAAqB8G,KAAKmC,MAAM1J,KAAK+B,KAAK4H,cAAeoG,GACtH/P,KAAKqC,iBAAiB2C,6BAA6B,KAAMgL,EAAchQ,KAAKqI,6BAG5ErI,KAAK8B,KAAK,iBAEXe,mBAAoB,SAAUnB,EAAOqM,GACpC/N,KAAK8C,8BAA8BpB,EAAOqM,KAI5C3M,eAAgB,CAEfqN,gBAAiB,WAChBzO,KAAK+B,KAAK6G,SAASC,WAAa,wBAChC7I,KAAKM,oBAGNoO,iBAAkB,SAAUqB,EAAmBC,GAC9C,IAGI7K,EAHAW,EAAS9F,KAAKqI,4BACdzE,EAAK5D,KAAKE,cACbuJ,EAAUlC,KAAKmC,MAAM1J,KAAK+B,KAAK4H,cAGhC3J,KAAKiK,aAAc,EAGnBjK,KAAKqC,iBAAiB4N,aAAanK,EAAQiK,EAAmBtG,EAAS,SAAUwC,GAChF,IAEItI,EAFAuM,EAAWjE,EAAE1I,QACbsB,EAAWoH,EAAElB,SAkBjB,IAfKjF,EAAOlD,SAASsN,KACpBA,EAAW,MAGRjE,EAAEkE,mBAAqBJ,EAAoB,IAAMC,GACpDpM,EAAGb,YAAYkJ,GACfA,EAAEjH,6BAA6B,KAAMgL,EAAclK,KAGnDmG,EAAEmE,cACFnE,EAAEjH,6BAA6BkL,EAAUF,EAAclK,IAKnDX,EAAIN,EAAQb,OAAS,EAAQ,GAALmB,EAAQA,IACpCxB,EAAIkB,EAAQM,GACPW,EAAOlD,SAASe,EAAEJ,UACtBK,EAAGb,YAAYY,KAMlB3D,KAAKqQ,eAGLrQ,KAAKqC,iBAAiBiO,0BAA0BxK,EAAQkK,GAExDpM,EAAG8B,UAAU,SAAU6K,GAChBA,aAAavS,EAAEuD,gBAAkBgP,EAAEnJ,OACxCmJ,EAAEnN,gBAKJpD,KAAKqC,iBAAiB4N,aAAanK,EAAQiK,EAAmBC,EAAc,SAAU/D,GACrFA,EAAEuE,kCAAkCR,KAGrChQ,KAAKiK,aAAc,EAGnBjK,KAAKkO,SAAS,WAEblO,KAAKqC,iBAAiB4N,aAAanK,EAAQiK,EAAmBtG,EAAS,SAAUwC,GAChFrI,EAAGb,YAAYkJ,GACfA,EAAE7I,gBAGHpD,KAAKyQ,mBAIP9B,kBAAmB,SAAUoB,EAAmBC,GAC/ChQ,KAAK0Q,wBAAwB1Q,KAAKqC,iBAAkB0N,EAAoB,EAAGC,GAG3EhQ,KAAKqC,iBAAiB2C,6BAA6B,KAAMgL,EAAchQ,KAAKqI,6BAE5ErI,KAAKqC,iBAAiB6K,kCAAkClN,KAAKS,oBAAqB8G,KAAKmC,MAAM1J,KAAK+B,KAAK4H,cAAeoG,EAAmB/P,KAAKqI,8BAG/IxF,mBAAoB,SAAUnB,EAAOqM,GACpC,IAAI4C,EAAK3Q,KACL4D,EAAK5D,KAAKE,cAEd0D,EAAGnC,SAASC,GACRqM,IAAerM,IACW,EAAzBqM,EAAW/C,aAEd+C,EAAWE,cACXjO,KAAKqQ,eACLrQ,KAAKyO,kBAEL/M,EAAMkP,QAAQ5Q,KAAK+B,KAAK8O,mBAAmB9C,EAAWlM,cACtDH,EAAM0O,cAENpQ,KAAKkO,SAAS,WACbtK,EAAGb,YAAYrB,GACfA,EAAM0B,cAENuN,EAAGF,oBAIJzQ,KAAKqQ,eAELM,EAAGlC,kBACHkC,EAAGD,wBAAwB3C,EAAY/N,KAAK+B,KAAK+F,aAAc9H,KAAK0C,WAOxEgO,wBAAyB,SAAU5F,EAASiF,EAAmBC,GAC9D,IAAIlK,EAAS9F,KAAKqI,4BACjBoB,EAAUlC,KAAKmC,MAAM1J,KAAK+B,KAAK4H,cAGhCmB,EAAQgG,6CAA6ChL,EAAQ2D,EAASsG,EAAoB,EAAGC,GAE7F,IAAIW,EAAK3Q,KAGTA,KAAKqQ,eACLvF,EAAQwF,0BAA0BxK,EAAQkK,GAI1ChQ,KAAKkO,SAAS,WAGb,GAA4B,IAAxBpD,EAAQE,YAAmB,CAC9B,IAAIrH,EAAImH,EAAQC,SAAS,GAEzB/K,KAAKiK,aAAc,EACnBtG,EAAEoN,UAAUpN,EAAE9B,aACd7B,KAAKiK,aAAc,EACftG,EAAEP,aACLO,EAAEP,mBAGH0H,EAAQmF,aAAanK,EAAQkK,EAAcvG,EAAS,SAAUwC,GAC7DA,EAAEiB,kCAAkCpH,EAAQ2D,EAASsG,EAAoB,KAG3EY,EAAGF,mBAILA,cAAe,WACVzQ,KAAK+B,OACR/B,KAAK+B,KAAK6G,SAASC,UAAY7I,KAAK+B,KAAK6G,SAASC,UAAUC,QAAQ,wBAAyB,KAE9F9I,KAAKM,mBACLN,KAAK8B,KAAK,iBAKXuO,aAAc,WAIbrS,EAAE8B,KAAKkR,QAAQC,SAASC,KAAKC,gBAI/BnT,EAAEoT,mBAAqB,SAAUjT,GAChC,OAAO,IAAIH,EAAED,mBAAmBI,ICz2CvB,IAACoD,EAAgBvD,EAAEuD,cAAgBvD,EAAEO,OAAOL,OAAO,CAC5DC,QAASH,EAAEqT,KAAK7S,UAAUL,QAE1B0B,WAAY,SAAU6P,EAAOnC,EAAM+D,EAAGC,GAErCvT,EAAEO,OAAOC,UAAUqB,WAAWyG,KAAKtG,KAAMsR,EAAKA,EAAEpG,UAAYoG,EAAEzP,YAAe,IAAI7D,EAAE8R,OAAO,EAAG,GACnF,CAAEF,KAAM5P,KAAMvB,KAAMiR,EAAMvR,QAAQG,cAE5C0B,KAAK+G,OAAS2I,EACd1P,KAAK0C,MAAQ6K,EAEbvN,KAAK+K,SAAW,GAChB/K,KAAKoL,eAAiB,GACtBpL,KAAKgL,YAAc,EACnBhL,KAAKqL,kBAAmB,EACxBrL,KAAKiL,mBAAoB,EAEzBjL,KAAKgG,QAAU,IAAIhI,EAAE+H,aAEjBuL,GACHtR,KAAK6N,UAAUyD,GAEZC,GACHvR,KAAK6N,UAAU0D,IAKjBzM,mBAAoB,SAAU0M,EAAcC,GAC3CD,EAAeA,GAAgB,GAE/B,IAAK,IAAIrM,EAAInF,KAAKoL,eAAepH,OAAS,EAAQ,GAALmB,EAAQA,IACpDnF,KAAKoL,eAAejG,GAAGL,mBAAmB0M,GAG3C,IAAK,IAAIpL,EAAIpG,KAAK+K,SAAS/G,OAAS,EAAQ,GAALoC,EAAQA,IAC1CqL,GAAuBzR,KAAK+K,SAAS3E,GAAG4D,aAG5CwH,EAAaxP,KAAKhC,KAAK+K,SAAS3E,IAGjC,OAAOoL,GAIR5M,cAAe,WACd,OAAO5E,KAAKgL,aAIbrD,aAAc,SAAU+J,GASvB,IARA,IAKCvM,EALGwM,EAAgB3R,KAAKoL,eAAe1G,QACvCwC,EAAMlH,KAAK+G,OAAOhF,KAClB6P,EAAa1K,EAAI2K,cAAc7R,KAAKgG,SACpCuH,EAAOvN,KAAK0C,MAAQ,EACpB6L,EAAUrH,EAAI4K,UAIe,EAAvBH,EAAc3N,QAA2BuJ,EAAbqE,GAAmB,CACrDrE,IACA,IAAIwE,EAAc,GAClB,IAAK5M,EAAI,EAAGA,EAAIwM,EAAc3N,OAAQmB,IACrC4M,EAAcA,EAAYC,OAAOL,EAAcxM,GAAGiG,gBAEnDuG,EAAgBI,EAGAxE,EAAbqE,EACH5R,KAAK+G,OAAOhF,KAAKkQ,QAAQjS,KAAKuD,QAASgK,GAC7BqE,GAAcrD,EACxBvO,KAAK+G,OAAOhF,KAAKkQ,QAAQjS,KAAKuD,QAASgL,EAAU,GAEjDvO,KAAK+G,OAAOhF,KAAKmQ,UAAUlS,KAAKgG,QAAS0L,IAI3C7L,UAAW,WACV,IAAIC,EAAS,IAAI9H,EAAE+H,aAEnB,OADAD,EAAO5H,OAAO8B,KAAKgG,SACZF,GAGRmI,YAAa,WACZjO,KAAKqL,kBAAmB,EACpBrL,KAAKoH,OACRpH,KAAKmS,QAAQnS,OAKfoS,WAAY,WAKX,OAJIpS,KAAKqL,mBACRrL,KAAKqS,SAAWrS,KAAK+G,OAAO5I,QAAQE,mBAAmB2B,MACvDA,KAAKqL,kBAAmB,GAElBrL,KAAKqS,SAASD,cAEtBE,aAAc,WACb,OAAOtS,KAAKqS,SAASC,gBAItBzE,UAAW,SAAU0E,EAAMC,GAE1BxS,KAAKqL,kBAAmB,EAExBrL,KAAKiL,mBAAoB,EACzBjL,KAAKyS,kBAAkBF,GAEnBA,aAAgBvU,EAAEuD,eAChBiR,IACJxS,KAAKoL,eAAepJ,KAAKuQ,GACzBA,EAAK5P,SAAW3C,MAEjBA,KAAKgL,aAAeuH,EAAKvH,cAEpBwH,GACJxS,KAAK+K,SAAS/I,KAAKuQ,GAEpBvS,KAAKgL,eAGFhL,KAAK2C,UACR3C,KAAK2C,SAASkL,UAAU0E,GAAM,IAShCE,kBAAmB,SAAUC,GACvB1S,KAAKkL,WAETlL,KAAKkL,SAAWwH,EAAMxH,UAAYwH,EAAMnP,UAU1CoP,aAAc,WACb,IAAI7M,EAAS9F,KAAKgG,QAEdF,EAAO0J,aACV1J,EAAO0J,WAAWH,IAAMC,EAAAA,EACxBxJ,EAAO0J,WAAWoD,IAAMtD,EAAAA,GAErBxJ,EAAOsJ,aACVtJ,EAAOsJ,WAAWC,KAAOC,EAAAA,EACzBxJ,EAAOsJ,WAAWwD,KAAOtD,EAAAA,IAI3BhN,mBAAoB,WACnB,IAKI6C,EAAGuN,EAAOG,EAAa7G,EALvBnH,EAAU7E,KAAK+K,SACf4G,EAAgB3R,KAAKoL,eACrB0H,EAAS,EACTC,EAAS,EACTC,EAAahT,KAAKgL,YAItB,GAAmB,IAAfgI,EAAJ,CAQA,IAHAhT,KAAK2S,eAGAxN,EAAI,EAAGA,EAAIN,EAAQb,OAAQmB,IAC/B0N,EAAchO,EAAQM,GAAG5B,QAEzBvD,KAAKgG,QAAQ9H,OAAO2U,GAEpBC,GAAUD,EAAYxD,IACtB0D,GAAUF,EAAYD,IAIvB,IAAKzN,EAAI,EAAGA,EAAIwM,EAAc3N,OAAQmB,KACrCuN,EAAQf,EAAcxM,IAGZ8F,mBACTyH,EAAMpQ,qBAGPtC,KAAKgG,QAAQ9H,OAAOwU,EAAM1M,SAE1B6M,EAAcH,EAAMO,SACpBjH,EAAa0G,EAAM1H,YAEnB8H,GAAUD,EAAYxD,IAAMrD,EAC5B+G,GAAUF,EAAYD,IAAM5G,EAG7BhM,KAAKuD,QAAUvD,KAAKiT,SAAW,IAAIjV,EAAE8R,OAAOgD,EAASE,EAAYD,EAASC,GAG1EhT,KAAKiL,mBAAoB,IAI1BwE,UAAW,SAAUS,GAChBA,IACHlQ,KAAKkT,cAAgBlT,KAAKuD,QAC1BvD,KAAK+Q,UAAUb,IAEhBlQ,KAAK+G,OAAO7G,cAAcuB,SAASzB,OAGpCmT,8BAA+B,SAAUrN,EAAQsN,EAAQjG,GACxDnN,KAAKiQ,aAAanK,EAAQ9F,KAAK+G,OAAOhF,KAAK4H,aAAcwD,EAAU,EAClE,SAAUlB,GACT,IACC9G,EAAGxB,EADAkB,EAAUoH,EAAElB,SAEhB,IAAK5F,EAAIN,EAAQb,OAAS,EAAQ,GAALmB,EAAQA,KACpCxB,EAAIkB,EAAQM,IAGNiC,QACLzD,EAAEiN,QAAQwC,GACVzP,EAAEyM,gBAIL,SAAUnE,GACT,IACC7F,EAAGiN,EADA1B,EAAgB1F,EAAEb,eAEtB,IAAKhF,EAAIuL,EAAc3N,OAAS,EAAQ,GAALoC,EAAQA,KAC1CiN,EAAK1B,EAAcvL,IACZgB,QACNiM,EAAGzC,QAAQwC,GACXC,EAAGjD,kBAORU,6CAA8C,SAAUhL,EAAQwN,EAAYvD,EAAmBC,GAC9FhQ,KAAKiQ,aAAanK,EAAQkK,EAAcsD,EACvC,SAAUrH,GACTA,EAAEkH,8BAA8BrN,EAAQmG,EAAElF,OAAOhF,KAAK8O,mBAAmB5E,EAAEpK,aAAa2F,QAASuI,GAI7F9D,EAAEkE,mBAAqBJ,EAAoB,IAAMC,GACpD/D,EAAE7I,cACF6I,EAAEiB,kCAAkCpH,EAAQwN,EAAYvD,IAExD9D,EAAEmE,cAGHnE,EAAEwD,eAKLa,0BAA2B,SAAUxK,EAAQyN,GAC5CvT,KAAKiQ,aAAanK,EAAQ9F,KAAK+G,OAAOhF,KAAK4H,aAAc4J,EAAW,KAAM,SAAUtH,GACnFA,EAAE7I,iBAIJ4B,6BAA8B,SAAUkL,EAAUqD,EAAWzN,GAC5D9F,KAAKiQ,aAAanK,EAAQ9F,KAAK+G,OAAOhF,KAAK4H,aAAe,EAAG4J,EAC5D,SAAUtH,GACT,GAAIsH,IAActH,EAAEvJ,MAKpB,IAAK,IAAIyC,EAAI8G,EAAElB,SAAS/G,OAAS,EAAQ,GAALmB,EAAQA,IAAK,CAChD,IAAIqO,EAAKvH,EAAElB,SAAS5F,GAEfW,EAAOlD,SAAS4Q,EAAGjQ,WAIpB2M,IACHsD,EAAGN,cAAgBM,EAAG3R,YAEtB2R,EAAGzC,UAAUb,GACTsD,EAAGpD,aACNoD,EAAGpD,eAILnE,EAAElF,OAAO7G,cAAcuB,SAAS+R,MAGlC,SAAUvH,GACTA,EAAEwD,UAAUS,MAKfM,kCAAmC,SAAU+C,GAE5C,IAAK,IAAIpO,EAAInF,KAAK+K,SAAS/G,OAAS,EAAQ,GAALmB,EAAQA,IAAK,CACnD,IAAIqO,EAAKxT,KAAK+K,SAAS5F,GACnBqO,EAAGN,gBACNM,EAAGzC,UAAUyC,EAAGN,sBACTM,EAAGN,eAIZ,GAAIK,EAAY,IAAMvT,KAAK0C,MAE1B,IAAK,IAAI0D,EAAIpG,KAAKoL,eAAepH,OAAS,EAAQ,GAALoC,EAAQA,IACpDpG,KAAKoL,eAAehF,GAAGqN,wBAGxB,IAAK,IAAIC,EAAI1T,KAAKoL,eAAepH,OAAS,EAAQ,GAAL0P,EAAQA,IACpD1T,KAAKoL,eAAesI,GAAGlD,kCAAkC+C,IAK5DE,iBAAkB,WACbzT,KAAKkT,gBACRlT,KAAK+Q,UAAU/Q,KAAKkT,sBACblT,KAAKkT,gBAKdhG,kCAAmC,SAAUyG,EAAgBL,EAAYC,EAAWK,GACnF,IAAIjQ,EAAGwB,EACPnF,KAAKiQ,aAAa0D,EAAgBL,EAAa,EAAGC,EAAY,EAC7D,SAAUtH,GAET,IAAK9G,EAAI8G,EAAElB,SAAS/G,OAAS,EAAQ,GAALmB,EAAQA,IACvCxB,EAAIsI,EAAElB,SAAS5F,GACVyO,GAAiBA,EAAahR,SAASe,EAAEJ,WAC7C0I,EAAElF,OAAO7G,cAAc6C,YAAYY,GAC/BA,EAAEP,aACLO,EAAEP,gBAKN,SAAU6I,GAET,IAAK9G,EAAI8G,EAAEb,eAAepH,OAAS,EAAQ,GAALmB,EAAQA,IAC7CxB,EAAIsI,EAAEb,eAAejG,GAChByO,GAAiBA,EAAahR,SAASe,EAAEJ,WAC7C0I,EAAElF,OAAO7G,cAAc6C,YAAYY,GAC/BA,EAAEP,aACLO,EAAEP,kBAcR6M,aAAc,SAAU4D,EAAiBC,EAAkBC,EAAiBC,EAAiBC,GAC5F,IAEI9O,EAAG8G,EAFH0F,EAAgB3R,KAAKoL,eACrBmC,EAAOvN,KAAK0C,MAYhB,GATIoR,GAAoBvG,IACnByG,GACHA,EAAgBhU,MAEbiU,GAAoB1G,IAASwG,GAChCE,EAAiBjU,OAIfuN,EAAOuG,GAAoBvG,EAAOwG,EACrC,IAAK5O,EAAIwM,EAAc3N,OAAS,EAAQ,GAALmB,EAAQA,KAC1C8G,EAAI0F,EAAcxM,IACZ8F,mBACLgB,EAAE3J,qBAECuR,EAAgBrF,WAAWvC,EAAEjG,UAChCiG,EAAEgE,aAAa4D,EAAiBC,EAAkBC,EAAiBC,EAAiBC,IAOxF9D,gBAAiB,WAEhB,OAAoC,EAA7BnQ,KAAKoL,eAAepH,QAAchE,KAAKoL,eAAe,GAAGJ,cAAgBhL,KAAKgL,eC1YvFhN,EAAEO,OAAOsR,QAAQ,CAChBO,YAAa,WACZ,IAAI8D,EAASlU,KAAK7B,QAAQoB,QAG1B,OAFAS,KAAKmU,WAAW,GAChBnU,KAAK7B,QAAQoB,QAAU2U,EAChBlU,MAGRoD,YAAa,WACZ,OAAOpD,KAAKmU,WAAWnU,KAAK7B,QAAQoB,YChBtCvB,EAAEwP,aAAe,SAAU4G,GAC1BpU,KAAKqU,UAAYD,EACjBpU,KAAKsU,YAAcF,EAAWA,EAC9BpU,KAAKuU,MAAQ,GACbvU,KAAKwU,aAAe,IAGrBxW,EAAEwP,aAAahP,UAAY,CAE1B2M,UAAW,SAAU/B,EAAKqL,GACzB,IAAIC,EAAI1U,KAAK2U,UAAUF,EAAMC,GACzBE,EAAI5U,KAAK2U,UAAUF,EAAMG,GACzBC,EAAO7U,KAAKuU,MACZO,EAAMD,EAAKD,GAAKC,EAAKD,IAAM,GAC3BG,EAAOD,EAAIJ,GAAKI,EAAIJ,IAAM,GAC1B7N,EAAQ7I,EAAE8B,KAAK+G,MAAMuC,GAEzBpJ,KAAKwU,aAAa3N,GAAS4N,EAE3BM,EAAK/S,KAAKoH,IAGX4L,aAAc,SAAU5L,EAAKqL,GAC5BzU,KAAK4J,aAAaR,GAClBpJ,KAAKmL,UAAU/B,EAAKqL,IAIrB7K,aAAc,SAAUR,EAAKqL,GAC5B,IAKItP,EAAG8P,EALHP,EAAI1U,KAAK2U,UAAUF,EAAMC,GACzBE,EAAI5U,KAAK2U,UAAUF,EAAMG,GACzBC,EAAO7U,KAAKuU,MACZO,EAAMD,EAAKD,GAAKC,EAAKD,IAAM,GAC3BG,EAAOD,EAAIJ,GAAKI,EAAIJ,IAAM,GAK9B,WAFO1U,KAAKwU,aAAaxW,EAAE8B,KAAK+G,MAAMuC,IAEjCjE,EAAI,EAAG8P,EAAMF,EAAK/Q,OAAQmB,EAAI8P,EAAK9P,IACvC,GAAI4P,EAAK5P,KAAOiE,EAQf,OANA2L,EAAK1L,OAAOlE,EAAG,GAEH,IAAR8P,UACIH,EAAIJ,IAGL,GAMVQ,WAAY,SAAU/G,EAAIjI,GACzB,IAAIf,EAAGiB,EAAGsN,EAAGuB,EAAKH,EAAKC,EACnBF,EAAO7U,KAAKuU,MAEhB,IAAKpP,KAAK0P,EAGT,IAAKzO,KAFL0O,EAAMD,EAAK1P,GAKV,IAAKuO,EAAI,EAAGuB,GAFZF,EAAOD,EAAI1O,IAEYpC,OAAQ0P,EAAIuB,EAAKvB,IAC7BvF,EAAG7H,KAAKJ,EAAS6O,EAAKrB,MAE/BA,IACAuB,MAOLrH,cAAe,SAAU6G,GACxB,IAEItP,EAAGiB,EAAGsN,EAAGoB,EAAKC,EAAME,EAAK7L,EAAK+L,EAF9BT,EAAI1U,KAAK2U,UAAUF,EAAMC,GACzBE,EAAI5U,KAAK2U,UAAUF,EAAMG,GAEzBQ,EAAcpV,KAAKwU,aACnBa,EAAgBrV,KAAKsU,YACrB3G,EAAU,KAEd,IAAKxI,EAAIyP,EAAI,EAAGzP,GAAKyP,EAAI,EAAGzP,IAE3B,GADA2P,EAAM9U,KAAKuU,MAAMpP,GAGhB,IAAKiB,EAAIsO,EAAI,EAAGtO,GAAKsO,EAAI,EAAGtO,IAE3B,GADA2O,EAAOD,EAAI1O,GAGV,IAAKsN,EAAI,EAAGuB,EAAMF,EAAK/Q,OAAQ0P,EAAIuB,EAAKvB,IACvCtK,EAAM2L,EAAKrB,KACXyB,EAAOnV,KAAKsV,QAAQF,EAAYpX,EAAE8B,KAAK+G,MAAMuC,IAAOqL,IACzCY,GACVF,GAAQE,GAA6B,OAAZ1H,KACzB0H,EAAgBF,EAChBxH,EAAUvE,GAOhB,OAAOuE,GAGRgH,UAAW,SAAUD,GACpB,IAAIa,EAAQhO,KAAKmC,MAAMgL,EAAI1U,KAAKqU,WAChC,OAAOxM,SAAS0N,GAASA,EAAQb,GAGlCY,QAAS,SAAUE,EAAGC,GACrB,IAAIC,EAAKD,EAAGf,EAAIc,EAAEd,EACdiB,EAAKF,EAAGb,EAAIY,EAAEZ,EAClB,OAAOc,EAAKA,EAAKC,EAAKA,ICxFvB3X,EAAE4X,UAAY,CAQbC,WAAY,SAAUC,EAAKC,GAC1B,IAAIC,EAAKD,EAAG,GAAG1G,IAAM0G,EAAG,GAAG1G,IAE3B,OADM0G,EAAG,GAAGnD,IAAMmD,EAAG,GAAGnD,MACVkD,EAAIzG,IAAM0G,EAAG,GAAG1G,KAAO2G,GAAMF,EAAIlD,IAAMmD,EAAG,GAAGnD,MAU5DqD,iCAAkC,SAAUC,EAAUC,GACrD,IAGChR,EAAGiR,EAAIC,EAHJC,EAAO,EACVC,EAAQ,KACRC,EAAY,GAGb,IAAKrR,EAAIgR,EAAQnS,OAAS,EAAQ,GAALmB,EAAQA,IACpCiR,EAAKD,EAAQhR,GAGL,GAFRkR,EAAIrW,KAAK6V,WAAWO,EAAIF,MAGvBM,EAAUxU,KAAKoU,GAKRE,EAAJD,IACHC,EAAOD,EACPE,EAAQH,IAIV,MAAO,CAAEK,SAAUF,EAAOC,UAAWA,IAWtCE,gBAAiB,SAAUR,EAAUC,GACpC,IAAIQ,EAAsB,GACzBC,EAAI5W,KAAKiW,iCAAiCC,EAAUC,GAErD,OAAIS,EAAEH,SAKLE,GAJAA,EACCA,EAAoB3E,OACnBhS,KAAK0W,gBAAgB,CAACR,EAAS,GAAIU,EAAEH,UAAWG,EAAEJ,aAG/BxE,OACnBhS,KAAK0W,gBAAgB,CAACE,EAAEH,SAAUP,EAAS,IAAKU,EAAEJ,YAI7C,CAACN,EAAS,KAWnBnJ,cAAe,SAAUoJ,GAExB,IAKChR,EALG8J,GAAS,EAAO4H,GAAS,EAC5BC,GAAS,EAAOC,GAAS,EACzBC,EAAW,KAAMC,EAAW,KAC5BC,EAAW,KAAMC,EAAW,KAC5BZ,EAAQ,KAAMa,EAAQ,KAGvB,IAAKjS,EAAIgR,EAAQnS,OAAS,EAAQ,GAALmB,EAAQA,IAAK,CACzC,IAAIiR,EAAKD,EAAQhR,KACF,IAAX8J,GAAoBmH,EAAG/G,IAAMJ,KAEhCA,GADA+H,EAAWZ,GACC/G,OAEE,IAAXwH,GAAoBT,EAAG/G,IAAMwH,KAEhCA,GADAI,EAAWb,GACC/G,OAEE,IAAXyH,GAAoBV,EAAGxD,IAAMkE,KAEhCA,GADAI,EAAWd,GACCxD,OAEE,IAAXmE,GAAoBX,EAAGxD,IAAMmE,KAEhCA,GADAI,EAAWf,GACCxD,KAcd,OARC2D,EAFGM,IAAW5H,GACdmI,EAAQH,EACAD,IAERI,EAAQD,EACAD,GAGA,GAAGlF,OAAOhS,KAAK0W,gBAAgB,CAACU,EAAOb,GAAQJ,GACnDnW,KAAK0W,gBAAgB,CAACH,EAAOa,GAAQjB,MAM7CnY,EAAEuD,cAAcsO,QAAQ,CACvB9C,cAAe,WACd,IAECyI,EAAGrQ,EAFAkS,EAAerX,KAAK8E,qBACvBwS,EAAS,GAGV,IAAKnS,EAAIkS,EAAarT,OAAS,EAAQ,GAALmB,EAAQA,IACzCqQ,EAAI6B,EAAalS,GAAGtD,YACpByV,EAAOtV,KAAKwT,GAGb,OAAOxX,EAAE4X,UAAU7I,cAAcuK,MC/JnCtZ,EAAEuD,cAAcsO,QAAQ,CAEvB0H,KAAgB,EAAVhQ,KAAKiQ,GACXC,sBAAuB,GACvBC,kBAAmB,EAEnBC,sBAAwB,GACxBC,mBAAoB,GACpBC,oBAAqB,EAErBC,wBAAyB,EAGzBxQ,SAAU,WACT,GAAItH,KAAK+G,OAAO8F,cAAgB7M,OAAQA,KAAK+G,OAAOzG,iBAApD,CAIA,IAICyX,EAJGV,EAAerX,KAAK8E,mBAAmB,MAAM,GAGhDsO,EAFQpT,KAAK+G,OACDhF,KACC8O,mBAAmB7Q,KAAKuD,SAGtCvD,KAAK+G,OAAO7E,cAMX6V,GALD/X,KAAK+G,OAAO8F,YAAc7M,MAIjB+G,OAAO5I,QAAQe,uBACXc,KAAK+G,OAAO5I,QAAQe,uBAAuBmY,EAAarT,OAAQoP,GAClEiE,EAAarT,QAAUhE,KAAK8X,wBAC1B9X,KAAKgY,sBAAsBX,EAAarT,OAAQoP,IAE5DA,EAAOwB,GAAK,GACA5U,KAAKiY,sBAAsBZ,EAAarT,OAAQoP,IAG7DpT,KAAKkY,mBAAmBb,EAAcU,KAGvCI,WAAY,SAAUC,GAEjBpY,KAAK+G,OAAOzG,mBAGhBN,KAAKqY,qBAAqBD,GAE1BpY,KAAK+G,OAAO8F,YAAc,OAG3BoL,sBAAuB,SAAUK,EAAOC,GACvC,IAICpT,EAAGqT,EAHHC,EADmBzY,KAAK+G,OAAO5I,QAAQgB,2BAA6Ba,KAAKyX,uBAAyB,EAAIa,GAC1EtY,KAAKuX,KACjCmB,EAAY1Y,KAAKuX,KAAOe,EACxBK,EAAM,GAOP,IAJAF,EAAYlR,KAAKqR,IAAIH,EAAW,IAEhCE,EAAI3U,OAASsU,EAERnT,EAAI,EAAGA,EAAImT,EAAOnT,IACtBqT,EAAQxY,KAAK0X,kBAAoBvS,EAAIuT,EACrCC,EAAIxT,GAAK,IAAInH,EAAEqO,MAAMkM,EAAS7D,EAAI+D,EAAYlR,KAAKsR,IAAIL,GAAQD,EAAS3D,EAAI6D,EAAYlR,KAAKuR,IAAIN,IAAQO,SAG1G,OAAOJ,GAGRX,sBAAuB,SAAUM,EAAOC,GACvC,IAMCpT,EANGhG,EAA6Ba,KAAK+G,OAAO5I,QAAQgB,2BACpDsZ,EAAYtZ,EAA6Ba,KAAK4X,mBAC9CoB,EAAa7Z,EAA6Ba,KAAK2X,sBAC/CsB,EAAe9Z,EAA6Ba,KAAK6X,oBAAsB7X,KAAKuX,KAC5EiB,EAAQ,EACRG,EAAM,GAMP,IAAKxT,EAHLwT,EAAI3U,OAASsU,EAGQ,GAALnT,EAAQA,IAGnBA,EAAImT,IACPK,EAAIxT,GAAK,IAAInH,EAAEqO,MAAMkM,EAAS7D,EAAI+D,EAAYlR,KAAKsR,IAAIL,GAAQD,EAAS3D,EAAI6D,EAAYlR,KAAKuR,IAAIN,IAAQO,UAG1GN,GAAaQ,GADbT,GAASQ,EAAaP,EAAgB,KAAJtT,GAGnC,OAAOwT,GAGRlT,uBAAwB,WACvB,IAIC9B,EAAGwB,EAJAuK,EAAQ1P,KAAK+G,OAChBG,EAAMwI,EAAM3N,KACZ6B,EAAK8L,EAAMxP,cACXmX,EAAerX,KAAK8E,mBAAmB,MAAM,GAM9C,IAHA4K,EAAMzF,aAAc,EAEpBjK,KAAKmU,WAAW,GACXhP,EAAIkS,EAAarT,OAAS,EAAQ,GAALmB,EAAQA,IACzCxB,EAAI0T,EAAalS,GAEjBvB,EAAGb,YAAYY,GAEXA,EAAEuV,qBACLvV,EAAEoN,UAAUpN,EAAEuV,2BACPvV,EAAEuV,oBAENvV,EAAEwV,iBACLxV,EAAEwV,gBAAgB,GAGfxV,EAAEyV,aACLlS,EAAInE,YAAYY,EAAEyV,mBACXzV,EAAEyV,YAIX1J,EAAM5N,KAAK,eAAgB,CAC1BgJ,QAAS9K,KACT6E,QAASwS,IAEV3H,EAAMzF,aAAc,EACpByF,EAAM7C,YAAc,QAKtB7O,EAAEwD,yBAA2BxD,EAAEuD,cAAcrD,OAAO,CACnDga,mBAAoB,SAAUb,EAAcU,GAC3C,IAIC5S,EAAGxB,EAAG0V,EAAKC,EAJR5J,EAAQ1P,KAAK+G,OAChBG,EAAMwI,EAAM3N,KACZ6B,EAAK8L,EAAMxP,cACXqZ,EAAavZ,KAAK+G,OAAO5I,QAAQiB,yBAOlC,IAJAsQ,EAAMzF,aAAc,EAIf9E,EAAI,EAAGA,EAAIkS,EAAarT,OAAQmB,IACpCmU,EAASpS,EAAIsS,mBAAmBzB,EAAU5S,IAC1CxB,EAAI0T,EAAalS,GAGjBkU,EAAM,IAAIrb,EAAEyb,SAAS,CAACzZ,KAAKuD,QAAS+V,GAASC,GAC7CrS,EAAIzF,SAAS4X,GACb1V,EAAEyV,WAAaC,EAGf1V,EAAEuV,mBAAqBvV,EAAEJ,QACzBI,EAAEoN,UAAUuI,GACR3V,EAAEwV,iBACLxV,EAAEwV,gBAAgB,KAGnBvV,EAAGnC,SAASkC,GAEb3D,KAAKmU,WAAW,IAEhBzE,EAAMzF,aAAc,EACpByF,EAAM5N,KAAK,aAAc,CACxBgJ,QAAS9K,KACT6E,QAASwS,KAIXgB,qBAAsB,WACrBrY,KAAKyF,4BAKPzH,EAAEuD,cAAcsO,QAAQ,CAEvBqI,mBAAoB,SAAUb,EAAcU,GAC3C,IASC5S,EAAGxB,EAAG0V,EAAKK,EAASjB,EAAWa,EAT5B3I,EAAK3Q,KACR0P,EAAQ1P,KAAK+G,OACbG,EAAMwI,EAAM3N,KACZ6B,EAAK8L,EAAMxP,cACXyZ,EAAkB3Z,KAAKuD,QACvBqW,EAAe1S,EAAI2J,mBAAmB8I,GACtCE,EAAM7b,EAAE8b,KAAKC,IACbR,EAAavb,EAAEE,OAAO,GAAI8B,KAAK+G,OAAO5I,QAAQiB,0BAC9C4a,EAAkBT,EAAWha,QAuB9B,SApBwB2P,IAApB8K,IACHA,EAAkBhc,EAAED,mBAAmBS,UAAUL,QAAQiB,yBAAyBG,SAG/Esa,GAEHN,EAAWha,QAAU,EAGrBga,EAAW1Q,WAAa0Q,EAAW1Q,WAAa,IAAM,+BAGtD0Q,EAAWha,QAAUya,EAGtBtK,EAAMzF,aAAc,EAKf9E,EAAI,EAAGA,EAAIkS,EAAarT,OAAQmB,IACpCxB,EAAI0T,EAAalS,GAEjBmU,EAASpS,EAAIsS,mBAAmBzB,EAAU5S,IAG1CkU,EAAM,IAAIrb,EAAEyb,SAAS,CAACE,EAAiBL,GAASC,GAChDrS,EAAIzF,SAAS4X,GACb1V,EAAEyV,WAAaC,EAIXQ,IAEHpB,GADAiB,EAAUL,EAAIY,OACMC,iBAAmB,GACvCR,EAAQS,MAAMC,gBAAkB3B,EAChCiB,EAAQS,MAAME,iBAAmB5B,GAI9B9U,EAAEwV,iBACLxV,EAAEwV,gBAAgB,KAEfxV,EAAEyM,aACLzM,EAAEyM,cAIHxM,EAAGnC,SAASkC,GAERA,EAAEiN,SACLjN,EAAEiN,QAAQgJ,GAQZ,IAJAlK,EAAMW,eACNX,EAAMjB,kBAGDtJ,EAAIkS,EAAarT,OAAS,EAAQ,GAALmB,EAAQA,IACzCmU,EAASpS,EAAIsS,mBAAmBzB,EAAU5S,KAC1CxB,EAAI0T,EAAalS,IAGf+T,mBAAqBvV,EAAEJ,QACzBI,EAAEoN,UAAUuI,GAER3V,EAAEP,aACLO,EAAEP,cAICyW,KAEHH,GADAL,EAAM1V,EAAEyV,YACMa,OACNE,MAAME,iBAAmB,EAEjChB,EAAIiB,SAAS,CAAC/a,QAASya,KAGzBha,KAAKmU,WAAW,IAEhBzE,EAAMzF,aAAc,EAEpBhF,WAAW,WACVyK,EAAMe,gBACNf,EAAM5N,KAAK,aAAc,CACxBgJ,QAAS6F,EACT9L,QAASwS,KAER,MAGJgB,qBAAsB,SAAUD,GAC/B,IAOCzU,EAAGwB,EAAGkU,EAAKK,EAASjB,EAAW8B,EAP5B5J,EAAK3Q,KACR0P,EAAQ1P,KAAK+G,OACbG,EAAMwI,EAAM3N,KACZ6B,EAAK8L,EAAMxP,cACX0Z,EAAexB,EAAclR,EAAIsT,uBAAuBxa,KAAKuD,QAAS6U,EAAY7K,KAAM6K,EAAYhF,QAAUlM,EAAI2J,mBAAmB7Q,KAAKuD,SAC1I8T,EAAerX,KAAK8E,mBAAmB,MAAM,GAC7C+U,EAAM7b,EAAE8b,KAAKC,IAQd,IALArK,EAAMzF,aAAc,EACpByF,EAAMjB,kBAGNzO,KAAKmU,WAAW,GACXhP,EAAIkS,EAAarT,OAAS,EAAQ,GAALmB,EAAQA,KACzCxB,EAAI0T,EAAalS,IAGV+T,qBAKPvV,EAAE8W,aAGF9W,EAAEoN,UAAUpN,EAAEuV,2BACPvV,EAAEuV,mBAGTqB,GAAgB,EACZ5W,EAAEiN,UACLjN,EAAEiN,QAAQgJ,GACVW,GAAgB,GAEb5W,EAAEyM,cACLzM,EAAEyM,cACFmK,GAAgB,GAEbA,GACH3W,EAAGb,YAAYY,GAIZkW,IAGHpB,GADAiB,GADAL,EAAM1V,EAAEyV,YACMa,OACMC,iBAAmB,GACvCR,EAAQS,MAAME,iBAAmB5B,EACjCY,EAAIiB,SAAS,CAAC/a,QAAS,MAIzBmQ,EAAMzF,aAAc,EAEpBhF,WAAW,WAEV,IAAIyV,EAAuB,EAC3B,IAAKvV,EAAIkS,EAAarT,OAAS,EAAQ,GAALmB,EAAQA,KACzCxB,EAAI0T,EAAalS,IACXiU,YACLsB,IAKF,IAAKvV,EAAIkS,EAAarT,OAAS,EAAQ,GAALmB,EAAQA,KACzCxB,EAAI0T,EAAalS,IAEViU,aAIHzV,EAAEP,aACLO,EAAEP,cAECO,EAAEwV,iBACLxV,EAAEwV,gBAAgB,GAGQ,EAAvBuB,GACH9W,EAAGb,YAAYY,GAGhBuD,EAAInE,YAAYY,EAAEyV,mBACXzV,EAAEyV,YAEV1J,EAAMe,gBACNf,EAAM5N,KAAK,eAAgB,CAC1BgJ,QAAS6F,EACT9L,QAASwS,KAER,QAKLrZ,EAAED,mBAAmB8R,QAAQ,CAE5BhD,YAAa,KAEbsL,WAAY,WACXnY,KAAKkC,YAAYyY,MAAM3a,KAAM4a,YAG9BpS,iBAAkB,WACjBxI,KAAK+B,KAAK0F,GAAG,QAASzH,KAAK6a,mBAAoB7a,MAE3CA,KAAK+B,KAAK5D,QAAQ2c,eACrB9a,KAAK+B,KAAK0F,GAAG,YAAazH,KAAK+a,qBAAsB/a,MAGtDA,KAAK+B,KAAK0F,GAAG,UAAWzH,KAAKyF,uBAAwBzF,MAEhDhC,EAAE4Q,QAAQoM,OACdhb,KAAK+B,KAAKkZ,YAAYjb,OAOxB+I,oBAAqB,WACpB/I,KAAK+B,KAAKoB,IAAI,QAASnD,KAAK6a,mBAAoB7a,MAChDA,KAAK+B,KAAKoB,IAAI,YAAanD,KAAK+a,qBAAsB/a,MACtDA,KAAK+B,KAAKoB,IAAI,WAAYnD,KAAKkb,oBAAqBlb,MACpDA,KAAK+B,KAAKoB,IAAI,UAAWnD,KAAKyF,uBAAwBzF,MAItDA,KAAKyF,0BAKNsV,qBAAsB,WAChB/a,KAAK+B,MAIV/B,KAAK+B,KAAK0F,GAAG,WAAYzH,KAAKkb,oBAAqBlb,OAGpDkb,oBAAqB,SAAU9C,GAE1Bpa,EAAEkD,QAAQia,SAASnb,KAAK+B,KAAK6G,SAAU,sBAI3C5I,KAAK+B,KAAKoB,IAAI,WAAYnD,KAAKkb,oBAAqBlb,MACpDA,KAAKkC,YAAYkW,KAGlByC,mBAAoB,WAEnB7a,KAAKkC,eAGNA,YAAa,SAAUkW,GAClBpY,KAAK6M,aACR7M,KAAK6M,YAAYsL,WAAWC,IAI9B3S,uBAAwB,WACnBzF,KAAK6M,aACR7M,KAAK6M,YAAYpH,0BAKnBxC,iBAAkB,SAAUvB,GACvBA,EAAM0X,aACTpZ,KAAKE,cAAc6C,YAAYrB,GAE3BA,EAAM0B,aACT1B,EAAM0B,cAGH1B,EAAMyX,iBACTzX,EAAMyX,gBAAgB,GAGvBnZ,KAAK+B,KAAKgB,YAAYrB,EAAM0X,mBACrB1X,EAAM0X,eCjdhBpb,EAAED,mBAAmB8R,QAAQ,CAS5BuL,gBAAiB,SAAU5U,GAoB1B,OAnBKA,EAEMA,aAAkBxI,EAAED,mBAC9ByI,EAASA,EAAOnE,iBAAiByC,qBACvB0B,aAAkBxI,EAAE2D,WAC9B6E,EAASA,EAAO6U,QACN7U,aAAkBxI,EAAEuD,cAC9BiF,EAASA,EAAO1B,qBACN0B,aAAkBxI,EAAEO,SAC9BiI,EAAS,CAACA,IARVA,EAASxG,KAAKqC,iBAAiByC,qBAUhC9E,KAAKsb,4BAA4B9U,GACjCxG,KAAKuC,wBAGDvC,KAAK7B,QAAQU,kBAChBmB,KAAKub,gCAAgC/U,GAG/BxG,MAQRsb,4BAA6B,SAAU9U,GACtC,IAAIE,EAAIoH,EAGR,IAAKpH,KAAMF,EAOV,IADAsH,EAAStH,EAAOE,GAAI/D,SACbmL,GACNA,EAAOzC,kBAAmB,EAC1ByC,EAASA,EAAOnL,UAWnB4Y,gCAAiC,SAAU/U,GAC1C,IAAIE,EAAIhF,EAER,IAAKgF,KAAMF,EACV9E,EAAQ8E,EAAOE,GAGX1G,KAAKiC,SAASP,IAEjBA,EAAMyQ,QAAQnS,KAAK0N,oBAAoBhM,OAM3C1D,EAAEO,OAAOsR,QAAQ,CAQhB2L,mBAAoB,SAAUrd,EAASsd,GACtC,IAAI7L,EAAO5P,KAAK7B,QAAQyR,KAcxB,OAZA5R,EAAE+B,WAAW6P,EAAMzR,GAEnB6B,KAAKmS,QAAQvC,GAMT6L,GAA2Bzb,KAAK2C,UACnC3C,KAAK2C,SAASoE,OAAOqU,gBAAgBpb,MAG/BA","file":"dist/leaflet.markercluster.js.map"} \ No newline at end of file diff --git a/public/dist/main.css b/public/dist/main.css deleted file mode 100755 index ef17ba9ec5a..00000000000 --- a/public/dist/main.css +++ /dev/null @@ -1 +0,0 @@ -@charset "UTF-8";@-webkit-keyframes basicModal__fadeIn{0%{opacity:0}100%{opacity:1}}@keyframes basicModal__fadeIn{0%{opacity:0}100%{opacity:1}}@-webkit-keyframes basicModal__fadeOut{0%{opacity:1}100%{opacity:0}}@keyframes basicModal__fadeOut{0%{opacity:1}100%{opacity:0}}@-webkit-keyframes basicModal__moveUpFade{0%{-webkit-transform:translateY(80px);transform:translateY(80px)}100%{-webkit-transform:translateY(0);transform:translateY(0)}}@keyframes basicModal__moveUpFade{0%{-webkit-transform:translateY(80px);transform:translateY(80px)}100%{-webkit-transform:translateY(0);transform:translateY(0)}}@-webkit-keyframes basicModal__shake{0%,100%{-webkit-transform:translateX(0);transform:translateX(0)}20%,60%{-webkit-transform:translateX(-10px);transform:translateX(-10px)}40%,80%{-webkit-transform:translateX(10px);transform:translateX(10px)}}@keyframes basicModal__shake{0%,100%{-webkit-transform:translateX(0);transform:translateX(0)}20%,60%{-webkit-transform:translateX(-10px);transform:translateX(-10px)}40%,80%{-webkit-transform:translateX(10px);transform:translateX(10px)}}.basicModalContainer{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;position:fixed;width:100%;height:100%;top:0;left:0;z-index:1000;-webkit-box-sizing:border-box;box-sizing:border-box}.basicModalContainer *,.basicModalContainer :after,.basicModalContainer :before{-webkit-box-sizing:border-box;box-sizing:border-box}.basicModalContainer--fadeIn{-webkit-animation:.3s cubic-bezier(.51,.92,.24,1.15) basicModal__fadeIn;animation:.3s cubic-bezier(.51,.92,.24,1.15) basicModal__fadeIn}.basicModalContainer--fadeOut{-webkit-animation:.3s cubic-bezier(.51,.92,.24,1.15) basicModal__fadeOut;animation:.3s cubic-bezier(.51,.92,.24,1.15) basicModal__fadeOut}.basicModalContainer--fadeIn .basicModal--fadeIn{-webkit-animation:.3s cubic-bezier(.51,.92,.24,1.15) basicModal__moveUpFade;animation:.3s cubic-bezier(.51,.92,.24,1.15) basicModal__moveUpFade}.basicModalContainer--fadeIn .basicModal--shake{-webkit-animation:.3s cubic-bezier(.51,.92,.24,1.15) basicModal__shake;animation:.3s cubic-bezier(.51,.92,.24,1.15) basicModal__shake}.basicModal{position:relative;width:500px;background-color:#fff;font-size:14px;border-radius:5px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.2);box-shadow:0 1px 2px rgba(0,0,0,.2)}.basicModal__content{max-height:70vh;overflow:auto;-webkit-overflow-scrolling:touch}.basicModal__buttons{display:-webkit-box;display:-ms-flexbox;display:flex;width:100%;-webkit-box-shadow:0 -1px 0 rgba(0,0,0,.1);box-shadow:0 -1px 0 rgba(0,0,0,.1)}.basicModal__button{display:inline-block;width:100%;font-weight:700;text-align:center;-webkit-transition:background-color .2s;-o-transition:background-color .2s;transition:background-color .2s;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none}.basicModal__button:hover{background-color:rgba(0,0,0,.02)}.basicModal__button#basicModal__cancel{-ms-flex-negative:2;flex-shrink:2}.basicModal__button#basicModal__action{-ms-flex-negative:1;flex-shrink:1;-webkit-box-shadow:inset 1px 0 0 rgba(0,0,0,.1);box-shadow:inset 1px 0 0 rgba(0,0,0,.1)}.basicModal__button#basicModal__action:first-child{-webkit-box-shadow:none;box-shadow:none}.basicModal__button:first-child{border-radius:0 0 0 5px}.basicModal__button:last-child{border-radius:0 0 5px}.basicModal__text{width:100%;margin:0;padding:14px 10px;background-color:rgba(0,0,0,0);color:#333;border:none;-webkit-box-shadow:0 1px 0 #c8c8c8;box-shadow:0 1px 0 #c8c8c8;border-radius:0;outline:0;-webkit-transition:background-color .2s,-webkit-box-shadow .2s;transition:background-color .2s,box-shadow .2s,-webkit-box-shadow .2s;-o-transition:background-color .2s,box-shadow .2s}.basicModal__text:hover{background-color:rgba(0,0,0,.02);-webkit-box-shadow:0 1px 0 #b4b4b4;box-shadow:0 1px 0 #b4b4b4}.basicModal__text:focus{background-color:rgba(40,117,237,.05);-webkit-box-shadow:0 1px 0 #2875ed;box-shadow:0 1px 0 #2875ed}.basicModal__text.error{background-color:rgba(255,36,16,.05);-webkit-box-shadow:0 1px 0 #ff2410;box-shadow:0 1px 0 #ff2410}.basicModal p{margin:0 0 5%;width:100%}.basicModal p:last-child{margin:0}.basicModal__small{max-width:340px;text-align:center}.basicModal__small .basicModal__content{padding:10% 5%}.basicModal__xclose#basicModal__cancel{position:absolute;top:-8px;right:-8px;margin:0;padding:0;width:40px;height:40px;background-color:#fff;border-radius:100%;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.2);box-shadow:0 1px 2px rgba(0,0,0,.2)}.basicModal__xclose#basicModal__cancel:after{content:'';position:absolute;left:-3px;top:8px;width:35px;height:34px;background:#fff}.basicModal__xclose#basicModal__cancel svg{position:relative;width:20px;height:39px;fill:#888;z-index:1;-webkit-transition:fill .2s;-o-transition:fill .2s;transition:fill .2s}.basicModal__xclose#basicModal__cancel:after:hover svg,.basicModal__xclose#basicModal__cancel:hover svg{fill:#2875ed}.basicModal__xclose#basicModal__cancel:active svg,.basicModal__xclose#basicModal__cancel:after:active svg{fill:#1364e3}.basicContextContainer{position:fixed;width:100%;height:100%;top:0;left:0;z-index:1000;-webkit-tap-highlight-color:transparent}.basicContext{position:absolute;opacity:0;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none;-webkit-box-sizing:border-box;box-sizing:border-box;-webkit-animation:.3s cubic-bezier(.51,.92,.24,1.2) basicContext__popIn;animation:.3s cubic-bezier(.51,.92,.24,1.2) basicContext__popIn}.basicContext *{-webkit-box-sizing:border-box;box-sizing:border-box}.basicContext__item{cursor:pointer}.basicContext__item--separator{float:left;width:100%;cursor:default}.basicContext__item--disabled{cursor:default;opacity:.5}.basicContext__data{min-width:140px;text-align:left;white-space:nowrap}.basicContext__icon{display:inline-block}.basicContext--scrollable{height:100%;-webkit-overflow-scrolling:touch;overflow-x:hidden;overflow-y:auto}.basicContext--scrollable .basicContext__data{min-width:160px}@-webkit-keyframes basicContext__popIn{0%{-webkit-transform:scale(0);transform:scale(0)}100%{-webkit-transform:scale(1);transform:scale(1)}}@keyframes basicContext__popIn{0%{-webkit-transform:scale(0);transform:scale(0)}100%{-webkit-transform:scale(1);transform:scale(1)}}a,abbr,acronym,address,applet,article,aside,audio,b,big,blockquote,body,canvas,caption,center,cite,code,dd,del,details,dfn,div,dl,dt,em,embed,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,header,hgroup,html,i,iframe,img,ins,kbd,label,legend,li,mark,menu,nav,object,ol,output,p,pre,q,ruby,s,samp,section,small,span,strike,strong,sub,summary,sup,table,tbody,td,tfoot,th,thead,time,tr,tt,u,ul,var,video{margin:0;padding:0;border:0;font:inherit;font-size:100%;vertical-align:baseline}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}body{line-height:1;background-color:#1d1d1d;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:12px;-webkit-font-smoothing:antialiased;-moz-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:after,blockquote:before,q:after,q:before{content:"";content:none}table{border-collapse:collapse;border-spacing:0}em,i{font-style:italic}b,strong{font-weight:700}*{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-transition:color .3s,opacity .3s ease-out,-webkit-transform .3s ease-out,-webkit-box-shadow .3s;transition:color .3s,opacity .3s ease-out,transform .3s ease-out,box-shadow .3s,-webkit-transform .3s ease-out,-webkit-box-shadow .3s;-o-transition:color .3s,opacity .3s ease-out,transform .3s ease-out,box-shadow .3s}body,html{min-height:100vh;position:relative}body.view{background-color:#0f0f0f}div#container{position:relative}input{-webkit-user-select:text!important;-moz-user-select:text!important;-ms-user-select:text!important;user-select:text!important}.svgsprite{display:none}.iconic{width:100%;height:100%}#upload{display:none}.fadeIn{-webkit-animation-name:fadeIn;animation-name:fadeIn;-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards;-webkit-animation-timing-function:cubic-bezier(.51,.92,.24,1);animation-timing-function:cubic-bezier(.51,.92,.24,1)}.fadeOut{-webkit-animation-name:fadeOut;animation-name:fadeOut;-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards;-webkit-animation-timing-function:cubic-bezier(.51,.92,.24,1);animation-timing-function:cubic-bezier(.51,.92,.24,1)}@-webkit-keyframes fadeIn{0%{opacity:0}100%{opacity:1}}@keyframes fadeIn{0%{opacity:0}100%{opacity:1}}@-webkit-keyframes fadeOut{0%{opacity:1}100%{opacity:0}}@keyframes fadeOut{0%{opacity:1}100%{opacity:0}}@-webkit-keyframes moveBackground{0%{background-position-x:0}100%{background-position-x:-100px}}@keyframes moveBackground{0%{background-position-x:0}100%{background-position-x:-100px}}@-webkit-keyframes zoomIn{0%{opacity:0;-webkit-transform:scale(.8);transform:scale(.8)}100%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}}@keyframes zoomIn{0%{opacity:0;-webkit-transform:scale(.8);transform:scale(.8)}100%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}}@-webkit-keyframes zoomOut{0%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}100%{opacity:0;-webkit-transform:scale(.8);transform:scale(.8)}}@keyframes zoomOut{0%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}100%{opacity:0;-webkit-transform:scale(.8);transform:scale(.8)}}@-webkit-keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}}@keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}}.content{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-line-pack:start;align-content:flex-start;padding:50px 30px 33px 0;width:calc(100% - 30px);-webkit-transition:margin-left .5s;-o-transition:margin-left .5s;transition:margin-left .5s;-webkit-overflow-scrolling:touch;max-width:calc(100vw - 10px)}.content::before{content:"";position:absolute;left:0;width:100%;height:1px;background:rgba(255,255,255,.02)}.content--sidebar{width:calc(100% - 380px)}.content.contentZoomIn .album,.content.contentZoomIn .photo{-webkit-animation-name:zoomIn;animation-name:zoomIn}.content.contentZoomIn .divider{-webkit-animation-name:fadeIn;animation-name:fadeIn}.content.contentZoomOut .album,.content.contentZoomOut .photo{-webkit-animation-name:zoomOut;animation-name:zoomOut}.content.contentZoomOut .divider{-webkit-animation-name:fadeOut;animation-name:fadeOut}.content .album,.content .photo{position:relative;width:202px;height:202px;margin:30px 0 0 30px;cursor:default;-webkit-animation-duration:.2s;animation-duration:.2s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards;-webkit-animation-timing-function:cubic-bezier(.51,.92,.24,1);animation-timing-function:cubic-bezier(.51,.92,.24,1)}.content .album .thumbimg,.content .photo .thumbimg{position:absolute;width:200px;height:200px;background:#222;color:#222;-webkit-box-shadow:0 2px 5px rgba(0,0,0,.5);box-shadow:0 2px 5px rgba(0,0,0,.5);border:1px solid rgba(255,255,255,.5);-webkit-transition:opacity .3s ease-out,border-color .3s ease-out,-webkit-transform .3s ease-out;transition:opacity .3s ease-out,transform .3s ease-out,border-color .3s ease-out,-webkit-transform .3s ease-out;-o-transition:opacity .3s ease-out,transform .3s ease-out,border-color .3s ease-out}.content .album .thumbimg>img,.content .photo .thumbimg>img{width:100%;height:100%}.content .album.active .thumbimg,.content .album:focus .thumbimg,.content .photo.active .thumbimg,.content .photo:focus .thumbimg{border-color:#2293ec}.content .album:active .thumbimg,.content .photo:active .thumbimg{-webkit-transition:none;-o-transition:none;transition:none;border-color:#0f6ab2}.content .album.selected img,.content .photo.selected img{outline:#2293ec solid 1px}.content .album .video::before,.content .photo .video::before{content:"";position:absolute;display:block;height:100%;width:100%;background:url(../img/play-icon.png) 46% 50% no-repeat;-webkit-transition:.3s;-o-transition:.3s;transition:.3s;will-change:opacity,height}.content .album .video:focus::before,.content .photo .video:focus::before{opacity:.75}.content .album .livephoto::before,.content .photo .livephoto::before{content:"";position:absolute;display:block;height:100%;width:100%;background:url(../img/live-photo-icon.png) 2% 2% no-repeat;-webkit-transition:.3s;-o-transition:.3s;transition:.3s;will-change:opacity,height}.content .album .livephoto:focus::before,.content .photo .livephoto:focus::before{opacity:.75}.content .album .thumbimg:first-child,.content .album .thumbimg:nth-child(2){-webkit-transform:rotate(0) translateY(0) translateX(0);-ms-transform:rotate(0) translateY(0) translateX(0);transform:rotate(0) translateY(0) translateX(0);opacity:0}.content .album:focus .thumbimg:nth-child(1),.content .album:focus .thumbimg:nth-child(2){opacity:1;will-change:transform}.content .album:focus .thumbimg:nth-child(1){-webkit-transform:rotate(-2deg) translateY(10px) translateX(-12px);-ms-transform:rotate(-2deg) translateY(10px) translateX(-12px);transform:rotate(-2deg) translateY(10px) translateX(-12px)}.content .album:focus .thumbimg:nth-child(2){-webkit-transform:rotate(5deg) translateY(-8px) translateX(12px);-ms-transform:rotate(5deg) translateY(-8px) translateX(12px);transform:rotate(5deg) translateY(-8px) translateX(12px)}.content .blurred span{overflow:hidden}.content .blurred img{-webkit-filter:blur(5px);filter:blur(5px)}.content .album .overlay,.content .photo .overlay{position:absolute;margin:0 1px;width:200px;background:-webkit-gradient(linear,left top,left bottom,from(rgba(0,0,0,0)),to(rgba(0,0,0,.6)));background:-o-linear-gradient(top,rgba(0,0,0,0),rgba(0,0,0,.6));background:linear-gradient(to bottom,rgba(0,0,0,0),rgba(0,0,0,.6));bottom:1px}.content .album .thumbimg[data-overlay=false]+.overlay{background:0 0}.content .photo .overlay{opacity:0}.content .photo.active .overlay,.content .photo:focus .overlay{opacity:1}.content .album .overlay h1,.content .photo .overlay h1{min-height:19px;width:180px;margin:12px 0 5px 15px;color:#fff;text-shadow:0 1px 3px rgba(0,0,0,.4);font-size:16px;font-weight:700;overflow:hidden;white-space:nowrap;-o-text-overflow:ellipsis;text-overflow:ellipsis}.content .album .overlay a,.content .photo .overlay a{display:block;margin:0 0 12px 15px;font-size:11px;color:#ccc;text-shadow:0 1px 3px rgba(0,0,0,.4)}.content .album .overlay a .iconic,.content .photo .overlay a .iconic{fill:#ccc;margin:0 5px 0 0;width:8px;height:8px}.content .album .thumbimg[data-overlay=false]+.overlay a,.content .album .thumbimg[data-overlay=false]+.overlay h1{text-shadow:none}.content .album .badges,.content .photo .badges{position:relative;margin:-1px 0 0 6px}.content .album .subalbum_badge{position:absolute;right:0;top:0}.content .album .badge,.content .photo .badge{display:none;margin:0 0 0 6px;padding:12px 8px 6px;width:18px;background:#d92c34;-webkit-box-shadow:0 0 2px rgba(0,0,0,.6);box-shadow:0 0 2px rgba(0,0,0,.6);border-radius:0 0 5px 5px;border:1px solid #fff;border-top:none;color:#fff;text-align:center;text-shadow:0 1px 0 rgba(0,0,0,.4);opacity:.9}.content .album .badge--visible,.content .photo .badge--visible{display:inline-block}.content .album .badge--not--hidden,.content .photo .badge--not--hidden{background:#0a0}.content .album .badge--hidden,.content .photo .badge--hidden{background:#f90}.content .album .badge--cover,.content .photo .badge--cover{display:inline-block;background:#f90}.content .album .badge--star,.content .photo .badge--star{display:inline-block;background:#fc0}.content .album .badge--nsfw,.content .photo .badge--nsfw{display:inline-block;background:#ff82ee}.content .album .badge--list,.content .photo .badge--list{background:#2293ec}.content .album .badge--tag,.content .photo .badge--tag{display:inline-block;background:#0a0}.content .album .badge .iconic,.content .photo .badge .iconic{fill:#fff;width:16px;height:16px}.content .album .badge--folder,.content .photo .badge--folder{display:inline-block;-webkit-box-shadow:none;box-shadow:none;background:0 0;border:none}.content .album .badge--folder .iconic,.content .photo .badge--folder .iconic{width:12px;height:12px}.content .divider{margin:50px 0 0;padding:10px 0 0;width:100%;opacity:0;border-top:1px solid rgba(255,255,255,.02);-webkit-box-shadow:0 -1px 0 rgba(0,0,0,.2);box-shadow:0 -1px 0 rgba(0,0,0,.2);-webkit-animation-duration:.2s;animation-duration:.2s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards;-webkit-animation-timing-function:cubic-bezier(.51,.92,.24,1);animation-timing-function:cubic-bezier(.51,.92,.24,1)}.content .divider:first-child{margin-top:10px;border-top:0;-webkit-box-shadow:none;box-shadow:none}.content .divider h1{margin:0 0 0 30px;color:rgba(255,255,255,.6);font-size:14px;font-weight:700}@media only screen and (min-width:320px) and (max-width:567px){.content{padding:50px 0 33px;width:100%;max-width:100%}.content .album,.content .photo{--size:calc((100vw - 3px) / 3);width:calc(var(--size) - 3px);height:calc(var(--size) - 3px);margin:3px 0 0 3px}.content .album .thumbimg,.content .photo .thumbimg{width:calc(var(--size) - 5px);height:calc(var(--size) - 5px)}.content .album .overlay,.content .photo .overlay{width:calc(var(--size) - 5px)}.content .album .overlay h1,.content .photo .overlay h1{min-height:14px;width:calc(var(--size) - 19px);margin:8px 0 2px 6px;font-size:12px}.content .album .overlay a,.content .photo .overlay a{display:none}.content .album .badge,.content .photo .badge{padding:4px 3px 3px;width:12px}.content .album .badge .iconic,.content .photo .badge .iconic{width:12px;height:12px}.content .album .badge--folder .iconic,.content .photo .badge--folder .iconic{width:8px;height:8px}.content .divider{margin:20px 0 0}.content .divider:first-child{margin-top:0}.content .divider h1{margin:0 0 6px 8px;font-size:12px}}@media only screen and (min-width:568px) and (max-width:639px){.content{padding:50px 0 33px;width:100%;max-width:100%}.content .album,.content .photo{--size:calc((100vw - 3px) / 4);width:calc(var(--size) - 3px);height:calc(var(--size) - 3px);margin:3px 0 0 3px}.content .album .thumbimg,.content .photo .thumbimg{width:calc(var(--size) - 5px);height:calc(var(--size) - 5px)}.content .album .overlay,.content .photo .overlay{width:calc(var(--size) - 5px)}.content .album .overlay h1,.content .photo .overlay h1{min-height:14px;width:calc(var(--size) - 19px);margin:8px 0 2px 6px;font-size:12px}.content .album .overlay a,.content .photo .overlay a{display:none}.content .album .badge,.content .photo .badge{padding:4px 3px 3px;width:14px}.content .album .badge .iconic,.content .photo .badge .iconic{width:14px;height:14px}.content .album .badge--folder .iconic,.content .photo .badge--folder .iconic{width:9px;height:9px}.content .divider{margin:24px 0 0}.content .divider:first-child{margin-top:0}.content .divider h1{margin:0 0 6px 10px}}@media only screen and (min-width:640px) and (max-width:768px){.content{padding:50px 0 33px;width:100%;max-width:100%}.content .album,.content .photo{--size:calc((100vw - 5px) / 5);width:calc(var(--size) - 5px);height:calc(var(--size) - 5px);margin:5px 0 0 5px}.content .album .thumbimg,.content .photo .thumbimg{width:calc(var(--size) - 7px);height:calc(var(--size) - 7px)}.content .album .overlay,.content .photo .overlay{width:calc(var(--size) - 7px)}.content .album .overlay h1,.content .photo .overlay h1{min-height:14px;width:calc(var(--size) - 21px);margin:10px 0 3px 8px;font-size:12px}.content .album .overlay a,.content .photo .overlay a{display:none}.content .album .badge,.content .photo .badge{padding:6px 4px 4px;width:16px}.content .album .badge .iconic,.content .photo .badge .iconic{width:16px;height:16px}.content .album .badge--folder .iconic,.content .photo .badge--folder .iconic{width:10px;height:10px}.content .divider{margin:28px 0 0}.content .divider:first-child{margin-top:0}.content .divider h1{margin:0 0 6px 10px}}.no_content{position:absolute;top:50%;left:50%;padding-top:20px;color:rgba(255,255,255,.35);text-align:center;-webkit-transform:translateX(-50%) translateY(-50%);-ms-transform:translateX(-50%) translateY(-50%);transform:translateX(-50%) translateY(-50%)}.no_content .iconic{fill:rgba(255,255,255,.3);margin:0 0 10px;width:50px;height:50px}.no_content p{font-size:16px;font-weight:700}.leftMenu__open{margin-left:250px;width:calc(100% - 280px)}.leftMenu{height:100vh;width:0;position:fixed;z-index:4;top:0;left:0;background-color:#111;overflow-x:hidden;padding-top:49px;-webkit-transition:.5s;-o-transition:.5s;transition:.5s}.leftMenu a{padding:8px 8px 8px 32px;text-decoration:none;font-size:18px;color:#818181;display:block;-webkit-transition:.3s;-o-transition:.3s;transition:.3s}.leftMenu a.linkMenu{white-space:nowrap}.leftMenu .closebtn{position:absolute;top:0;right:25px;font-size:36px;margin-left:50px}.leftMenu .closetxt{position:absolute;top:0;left:0;font-size:24px;height:28px;padding-top:16px;color:#111;display:inline-block;width:210px}.leftMenu .iconic{display:inline-block;margin:0 10px 0 1px;width:15px;height:14px;fill:#818181}.leftMenu .iconic.ionicons{margin:0 8px -2px 0;width:18px;height:18px}.leftMenu__visible{width:250px}@media (hover:hover){.content .album:hover .thumbimg,.content .photo:hover .thumbimg{border-color:#2293ec}.content .album .livephoto:hover::before,.content .album .video:hover::before,.content .photo .livephoto:hover::before,.content .photo .video:hover::before{opacity:.75}.content .album:hover .thumbimg:nth-child(1),.content .album:hover .thumbimg:nth-child(2){opacity:1;will-change:transform}.content .album:hover .thumbimg:nth-child(1){-webkit-transform:rotate(-2deg) translateY(10px) translateX(-12px);-ms-transform:rotate(-2deg) translateY(10px) translateX(-12px);transform:rotate(-2deg) translateY(10px) translateX(-12px)}.content .album:hover .thumbimg:nth-child(2){-webkit-transform:rotate(5deg) translateY(-8px) translateX(12px);-ms-transform:rotate(5deg) translateY(-8px) translateX(12px);transform:rotate(5deg) translateY(-8px) translateX(12px)}.content .photo:hover .overlay{opacity:1}.leftMenu .closetxt:hover{color:#818181}.leftMenu a:hover{color:#f1f1f1}}@media (hover:none){.leftMenu a{padding:14px 8px 14px 32px}}.basicContext{padding:5px 0 6px;background:-webkit-gradient(linear,left top,left bottom,from(#333),to(#252525));background:-o-linear-gradient(top,#333,#252525);background:linear-gradient(to bottom,#333,#252525);-webkit-box-shadow:0 1px 4px rgba(0,0,0,.2),inset 0 1px 0 rgba(255,255,255,.05);box-shadow:0 1px 4px rgba(0,0,0,.2),inset 0 1px 0 rgba(255,255,255,.05);border-radius:5px;border:1px solid rgba(0,0,0,.7);border-bottom:1px solid rgba(0,0,0,.8);-webkit-transition:none;-o-transition:none;transition:none}.basicContext__item{margin-bottom:2px;font-size:14px;color:#ccc}.basicContext__item--separator{margin:4px 0;height:2px;background:rgba(0,0,0,.2);border-bottom:1px solid rgba(255,255,255,.06)}.basicContext__item:last-child{margin-bottom:0}.basicContext__data{min-width:auto;padding:6px 25px 7px 12px;-webkit-transition:none;-o-transition:none;transition:none;cursor:default}@media (hover:none) and (pointer:coarse){.basicContext__data{padding:12px 25px 12px 12px}}.basicContext__item:not(.basicContext__item--disabled):active .basicContext__data{background:-webkit-gradient(linear,left top,left bottom,from(#1178ca),to(#0f6ab2));background:-o-linear-gradient(top,#1178ca,#0f6ab2);background:linear-gradient(to bottom,#1178ca,#0f6ab2)}.basicContext__icon{margin-right:10px;width:12px;text-align:center}@media (hover:hover){.basicContext__item:not(.basicContext__item--disabled):hover __data{background:-webkit-gradient(linear,left top,left bottom,from(#2293ec),to(#1386e1));background:-o-linear-gradient(top,#2293ec,#1386e1);background:linear-gradient(to bottom,#2293ec,#1386e1)}.basicContext__item:hover{color:#fff;-webkit-transition:.3s;-o-transition:.3s;transition:.3s;-webkit-transform:scale(1.05);-ms-transform:scale(1.05);transform:scale(1.05)}.basicContext__item:hover .iconic{fill:#fff}}.basicContext__data .cover{position:absolute;background-color:#222;border-radius:2px;-webkit-box-shadow:0 0 0 1px rgba(0,0,0,.5);box-shadow:0 0 0 1px rgba(0,0,0,.5)}.basicContext__data .title{display:inline-block;margin:0 0 3px 26px}.basicContext__data .iconic{display:inline-block;margin:0 10px 0 1px;width:11px;height:10px;fill:#fff}.basicContext__data .iconic.active{fill:#f90}.basicContext__data .iconic.ionicons{margin:0 8px -2px 0;width:14px;height:14px}.basicContext__data input#link{margin:-2px 0;padding:5px 7px 6px;width:100%;background:#333;color:#fff;-webkit-box-shadow:0 1px 0 rgba(255,255,255,.05);box-shadow:0 1px 0 rgba(255,255,255,.05);border:1px solid rgba(0,0,0,.4);border-radius:3px;outline:0}.basicContext__item--noHover .basicContext__data{padding-right:12px}@media (hover:hover){.basicContext__item--noHover:hover __data{background:0 0!important}}.basicModal .switch:last-child{padding-bottom:42px}.basicModal .hr{padding:0 30px 15px;width:100%}.basicModal .hr hr{border:none;border-top:1px solid rgba(0,0,0,.2)}@media only screen and (max-width:567px),only screen and (max-width:640px) and (orientation:portrait){.leftMenu{display:none!important}.basicModal{max-width:90%}.basicModal .basicModal__content .choice h1,.basicModal .basicModal__content .choice p,.basicModal .basicModal__content .switch h1,.basicModal .basicModal__content .switch p,.basicModal .basicModal__content h1,.basicModal .basicModal__content p{padding-left:20px;padding-right:20px}.basicModal .basicModal__content .choice p,.basicModal .basicModal__content .switch p,.basicModal .basicModal__content p{font-size:12px;line-height:14px}.basicModal .basicModal__content .choice h1,.basicModal .basicModal__content .switch h1,.basicModal .basicModal__content h1{font-size:14px;line-height:16px}}.header{position:fixed;height:49px;width:100%;background:-webkit-gradient(linear,left top,left bottom,from(#222),to(#1a1a1a));background:-o-linear-gradient(top,#222,#1a1a1a);background:linear-gradient(to bottom,#222,#1a1a1a);border-bottom:1px solid #0f0f0f;z-index:1;-webkit-transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out;-o-transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out}.header--hidden{-webkit-transform:translateY(-60px);-ms-transform:translateY(-60px);transform:translateY(-60px)}.header--loading{-webkit-transform:translateY(2px);-ms-transform:translateY(2px);transform:translateY(2px)}.header--error{-webkit-transform:translateY(40px);-ms-transform:translateY(40px);transform:translateY(40px)}.header--view{border-bottom:none}.header--view.header--error{background-color:rgba(10,10,10,.99)}.header__toolbar{display:none;-webkit-box-align:center;-ms-flex-align:center;align-items:center;position:relative;-webkit-box-sizing:border-box;box-sizing:border-box;width:100%;height:100%}.header__toolbar--visible{display:-webkit-box;display:-ms-flexbox;display:flex}.header__toolbar--config .button .iconic{-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg)}.header__toolbar--config .header__title{padding-right:80px}.header__title{width:100%;padding:16px 0;color:#fff;font-size:16px;font-weight:700;text-align:center;cursor:default;overflow:hidden;white-space:nowrap;-o-text-overflow:ellipsis;text-overflow:ellipsis;-webkit-transition:margin-left .5s;-o-transition:margin-left .5s;transition:margin-left .5s}.header__title .iconic{display:none;margin:0 0 0 5px;width:10px;height:10px;fill:rgba(255,255,255,.5);-webkit-transition:fill .2s ease-out;-o-transition:fill .2s ease-out;transition:fill .2s ease-out}.header__title:active .iconic{-webkit-transition:none;-o-transition:none;transition:none;fill:rgba(255,255,255,.8)}.header__title--editable .iconic{display:inline-block}.header .button{-ms-flex-negative:0;flex-shrink:0;padding:16px 8px;height:15px}.header .button .iconic{width:15px;height:15px;fill:rgba(255,255,255,.5);-webkit-transition:fill .2s ease-out;-o-transition:fill .2s ease-out;transition:fill .2s ease-out}.header .button:active .iconic{-webkit-transition:none;-o-transition:none;transition:none;fill:rgba(255,255,255,.8)}.header .button--star.active .iconic{fill:#f0ef77}.header .button--eye.active .iconic{fill:#d92c34}.header .button--eye.active--not-hidden .iconic{fill:#0a0}.header .button--eye.active--hidden .iconic{fill:#f90}.header .button--share .iconic.ionicons{margin:-2px 0;width:18px;height:18px}.header .button--nsfw.active .iconic{fill:#ff82ee}.header .button--info.active .iconic{fill:#2293ec}.header #button_back,.header #button_back_home,.header #button_close_config,.header #button_settings,.header #button_signin{padding:16px 12px 16px 18px}.header .button_add{padding:16px 18px 16px 12px}.header__divider{-ms-flex-negative:0;flex-shrink:0;width:14px}.header__search{-ms-flex-negative:0;flex-shrink:0;width:80px;margin:0;padding:5px 12px 6px;background-color:#1d1d1d;color:#fff;border:1px solid rgba(0,0,0,.9);-webkit-box-shadow:0 1px 0 rgba(255,255,255,.04);box-shadow:0 1px 0 rgba(255,255,255,.04);outline:0;border-radius:50px;opacity:.6;-webkit-transition:opacity .3s ease-out,width .2s ease-out,-webkit-box-shadow .3s ease-out;transition:opacity .3s ease-out,box-shadow .3s ease-out,width .2s ease-out,-webkit-box-shadow .3s ease-out;-o-transition:opacity .3s ease-out,box-shadow .3s ease-out,width .2s ease-out}.header__search:focus{width:140px;border-color:#2293ec;-webkit-box-shadow:0 1px 0 rgba(255,255,255,0);box-shadow:0 1px 0 rgba(255,255,255,0);opacity:1}.header__search:focus~.header__clear{opacity:1}.header__search::-ms-clear{display:none}.header__search__field{position:relative}.header__clear{position:absolute;top:-2px;right:8px;padding:0;color:rgba(255,255,255,.5);font-size:24px;opacity:0;-webkit-transition:color .2s ease-out;-o-transition:color .2s ease-out;transition:color .2s ease-out;cursor:default}.header__clear_nomap{right:60px}.header__clear_public{right:17px}.header__hostedwith{-ms-flex-negative:0;flex-shrink:0;padding:5px 10px;margin:11px 0;color:#888;font-size:13px;border-radius:100px;cursor:default}.header .leftMenu__open{margin-left:250px}@media only screen and (max-width:640px){#button_move,#button_move_album,#button_nsfw_album,#button_trash,#button_trash_album,#button_visibility,#button_visibility_album{display:none!important}}@media only screen and (max-width:640px) and (max-width:567px){#button_rotate_ccwise,#button_rotate_cwise{display:none!important}.header__divider{width:0}}#imageview{position:fixed;display:none;top:0;left:0;width:100%;height:100%;background-color:rgba(10,10,10,.98);-webkit-transition:background-color .3s;-o-transition:background-color .3s;transition:background-color .3s}#imageview.view{background-color:inherit}#imageview.full{background-color:#000;cursor:none}#imageview #image,#imageview #livephoto{position:absolute;top:60px;right:30px;bottom:30px;left:30px;margin:auto;max-width:calc(100% - 60px);max-height:calc(100% - 90px);width:auto;height:auto;-webkit-transition:top .3s,right .3s,bottom .3s,left .3s,max-width .3s,max-height .3s;-o-transition:top .3s,right .3s,bottom .3s,left .3s,max-width .3s,max-height .3s;transition:top .3s,right .3s,bottom .3s,left .3s,max-width .3s,max-height .3s;-webkit-animation-name:zoomIn;animation-name:zoomIn;-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-timing-function:cubic-bezier(.51,.92,.24,1.15);animation-timing-function:cubic-bezier(.51,.92,.24,1.15);background-size:contain;background-position:center;background-repeat:no-repeat}#imageview.full #image,#imageview.full #livephoto{top:0;right:0;bottom:0;left:0;max-width:100%;max-height:100%}#imageview.image--sidebar #image,#imageview.image--sidebar #livephoto{right:380px;max-width:calc(100% - 410px)}#imageview #image_overlay{position:absolute;bottom:30px;left:30px;color:#fff;text-shadow:1px 1px 2px #000;z-index:3}#imageview #image_overlay h1{font-size:28px;font-weight:500;-webkit-transition:visibility .3s linear,opacity .3s linear;-o-transition:visibility .3s linear,opacity .3s linear;transition:visibility .3s linear,opacity .3s linear}#imageview #image_overlay p{margin-top:5px;font-size:20px;line-height:24px}#imageview #image_overlay a .iconic{fill:#fff;margin:0 5px 0 0;width:14px;height:14px}#imageview .arrow_wrapper{position:fixed;width:15%;height:calc(100% - 60px);top:60px}#imageview .arrow_wrapper--previous{left:0}#imageview .arrow_wrapper--next{right:0}#imageview .arrow_wrapper a{position:fixed;top:50%;margin:-19px 0 0;padding:8px 12px;width:16px;height:22px;background-size:100% 100%;border:1px solid rgba(255,255,255,.8);opacity:.6;z-index:2;-webkit-transition:opacity .2s ease-out,-webkit-transform .2s ease-out;transition:transform .2s ease-out,opacity .2s ease-out,-webkit-transform .2s ease-out;-o-transition:transform .2s ease-out,opacity .2s ease-out;will-change:transform}#imageview .arrow_wrapper a#previous{left:-1px;-webkit-transform:translateX(-100%);-ms-transform:translateX(-100%);transform:translateX(-100%)}#imageview .arrow_wrapper a#next{right:-1px;-webkit-transform:translateX(100%);-ms-transform:translateX(100%);transform:translateX(100%)}#imageview .arrow_wrapper .iconic{fill:rgba(255,255,255,.8)}#imageview.image--sidebar .arrow_wrapper--next{right:350px}#imageview.image--sidebar .arrow_wrapper a#next{right:349px}#imageview video{z-index:1}@media (hover:hover){.header .button:hover .iconic,.header__title:hover .iconic{fill:#fff}.header__clear:hover{color:#fff}.header__hostedwith:hover{background-color:rgba(0,0,0,.3)}#imageview .arrow_wrapper:hover a#next,#imageview .arrow_wrapper:hover a#previous{-webkit-transform:translateX(0);-ms-transform:translateX(0);transform:translateX(0)}#imageview .arrow_wrapper a:hover{opacity:1}}@media only screen and (max-width:567px),only screen and (max-width:640px) and (orientation:portrait){#imageview #image,#imageview #livephoto{top:0;right:0;bottom:0;left:0;max-width:100%;max-height:100%}#imageview.image--sidebar #image,#imageview.image--sidebar #livephoto{right:0;max-width:100%}#imageview.image--sidebar .arrow_wrapper--next{right:0}#imageview.image--sidebar .arrow_wrapper a#next{right:-1px}#imageview #image_overlay h1{font-size:14px}#imageview #image_overlay p{margin-top:2px;font-size:11px;line-height:13px}#imageview #image_overlay a .iconic{width:9px;height:9px}}#mapview{position:fixed;display:none;top:0;left:0;width:100%;height:100%;background-color:rgba(100,10,10,.98);-webkit-transition:background-color .3s;-o-transition:background-color .3s;transition:background-color .3s}#mapview.view{background-color:inherit}#mapview.full{background-color:#000;cursor:none}#mapview #leaflet_map_full{top:50px;height:100%;width:100%;float:left}.leaflet-marker-photo img{width:100%;height:100%}.image-leaflet-popup{width:100%}.leaflet-popup-content div{pointer-events:none;position:absolute;bottom:19px;left:22px;right:22px;padding-bottom:10px;background:-webkit-gradient(linear,left top,left bottom,from(rgba(0,0,0,0)),to(rgba(0,0,0,.6)));background:-o-linear-gradient(top,rgba(0,0,0,0),rgba(0,0,0,.6));background:linear-gradient(to bottom,rgba(0,0,0,0),rgba(0,0,0,.6))}.leaflet-popup-content h1{top:0;position:relative;margin:12px 0 5px 15px;font-size:16px;font-weight:700;text-shadow:0 1px 3px rgba(255,255,255,.4);color:#fff;white-space:nowrap;-o-text-overflow:ellipsis;text-overflow:ellipsis}.leaflet-popup-content span{margin-left:12px}.leaflet-popup-content svg{fill:#fff;vertical-align:middle}.leaflet-popup-content p{display:inline;font-size:11px;color:#fff}.leaflet-popup-content .iconic{width:20px;height:15px}.sidebar{position:fixed;top:49px;right:-360px;width:350px;height:calc(100% - 49px);background-color:rgba(25,25,25,.98);border-left:1px solid rgba(0,0,0,.2);-webkit-transform:translateX(0);-ms-transform:translateX(0);transform:translateX(0);-webkit-transition:-webkit-transform .3s cubic-bezier(.51,.92,.24,1);transition:transform .3s cubic-bezier(.51,.92,.24,1);-o-transition:transform .3s cubic-bezier(.51,.92,.24,1);transition:transform .3s cubic-bezier(.51,.92,.24,1),-webkit-transform .3s cubic-bezier(.51,.92,.24,1);z-index:4}.sidebar.active{-webkit-transform:translateX(-360px);-ms-transform:translateX(-360px);transform:translateX(-360px)}.sidebar__header{float:left;height:49px;width:100%;background:-webkit-gradient(linear,left top,left bottom,from(rgba(255,255,255,.02)),to(rgba(0,0,0,0)));background:-o-linear-gradient(top,rgba(255,255,255,.02),rgba(0,0,0,0));background:linear-gradient(to bottom,rgba(255,255,255,.02),rgba(0,0,0,0));border-top:1px solid #2293ec}.sidebar__header h1{position:absolute;margin:15px 0;width:100%;color:#fff;font-size:16px;font-weight:700;text-align:center;-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;user-select:text}.sidebar__wrapper{float:left;height:calc(100% - 49px);width:350px;overflow:auto;-webkit-overflow-scrolling:touch}.sidebar__divider{float:left;padding:12px 0 8px;width:100%;border-top:1px solid rgba(255,255,255,.02);-webkit-box-shadow:0 -1px 0 rgba(0,0,0,.2);box-shadow:0 -1px 0 rgba(0,0,0,.2)}.sidebar__divider:first-child{border-top:0;-webkit-box-shadow:none;box-shadow:none}.sidebar__divider h1{margin:0 0 0 20px;color:rgba(255,255,255,.6);font-size:14px;font-weight:700;-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;user-select:text}.sidebar .edit{display:inline-block;margin-left:3px;width:10px}.sidebar .edit .iconic{width:10px;height:10px;fill:rgba(255,255,255,.5);-webkit-transition:fill .2s ease-out;-o-transition:fill .2s ease-out;transition:fill .2s ease-out}.sidebar .edit:active .iconic{-webkit-transition:none;-o-transition:none;transition:none;fill:rgba(255,255,255,.8)}.sidebar table{float:left;margin:10px 0 15px 20px;width:calc(100% - 20px)}.sidebar table tr td{padding:5px 0;color:#fff;font-size:14px;line-height:19px;-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;user-select:text}.sidebar table tr td:first-child{width:110px}.sidebar table tr td:last-child{padding-right:10px}.sidebar table tr td span{-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;user-select:text}.sidebar #tags{width:calc(100% - 40px);margin:16px 20px 12px;color:#fff;display:inline-block}.sidebar #tags>div{display:inline-block}.sidebar #tags .empty{font-size:14px;margin:0 2px 8px 0;-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;user-select:text}.sidebar #tags .edit{margin-top:6px}.sidebar #tags .empty .edit{margin-top:0}.sidebar #tags .tag{cursor:default;display:inline-block;padding:6px 10px;margin:0 6px 8px 0;background-color:rgba(0,0,0,.5);border-radius:100px;font-size:12px;-webkit-transition:background-color .2s;-o-transition:background-color .2s;transition:background-color .2s;-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;user-select:text}.sidebar #tags .tag span{float:right;padding:0;margin:0 0 -2px;width:0;overflow:hidden;-webkit-transform:scale(0);-ms-transform:scale(0);transform:scale(0);-webkit-transition:width .2s,margin .2s,fill .2s ease-out,-webkit-transform .2s;transition:width .2s,margin .2s,transform .2s,fill .2s ease-out,-webkit-transform .2s;-o-transition:width .2s,margin .2s,transform .2s,fill .2s ease-out}.sidebar #tags .tag span .iconic{fill:#d92c34;width:8px;height:8px}.sidebar #tags .tag span:active .iconic{-webkit-transition:none;-o-transition:none;transition:none;fill:#b22027}.sidebar #leaflet_map_single_photo{margin:10px 0 0 20px;height:180px;width:calc(100% - 40px);float:left}.sidebar .attr_location.search{cursor:pointer}@media (hover:hover){.sidebar .edit:hover .iconic{fill:#fff}.sidebar #tags .tag:hover{background-color:rgba(0,0,0,.3)}.sidebar #tags .tag:hover.search{cursor:pointer}.sidebar #tags .tag:hover span{width:9px;margin:0 0 -2px 5px;-webkit-transform:scale(1);-ms-transform:scale(1);transform:scale(1)}.sidebar #tags .tag span:hover .iconic{fill:#e1575e}}@media only screen and (max-width:567px),only screen and (max-width:640px) and (orientation:portrait){.sidebar{width:240px;height:unset;background-color:rgba(0,0,0,.6)}.sidebar__wrapper{padding-bottom:10px}.sidebar__header{height:22px}.sidebar__header h1{margin:6px 0;font-size:13px}.sidebar__divider{padding:6px 0 2px}.sidebar__divider h1{margin:0 0 0 10px;font-size:12px}.sidebar table{margin:4px 0 6px 10px;width:calc(100% - 16px)}.sidebar table tr td{padding:2px 0;font-size:11px;line-height:12px}.sidebar table tr td:first-child{width:80px}.sidebar #tags{margin:4px 0 6px 10px;width:calc(100% - 16px)}.sidebar #tags .empty{margin:0;font-size:11px}}@media only screen and (min-width:568px) and (max-width:768px),only screen and (min-width:568px) and (max-width:640px) and (orientation:landscape){#imageview #image,#imageview #livephoto{top:0;right:0;bottom:0;left:0;max-width:100%;max-height:100%}#imageview.image--sidebar #image,#imageview.image--sidebar #livephoto{top:50px;right:280px;max-width:calc(100% - 280px);max-height:calc(100% - 50px)}#imageview.image--sidebar .arrow_wrapper--next{right:280px}#imageview.image--sidebar .arrow_wrapper a#next{right:279px}#imageview #image_overlay h1{font-size:18px}#imageview #image_overlay p{margin-top:4px;font-size:14px;line-height:16px}#imageview #image_overlay a .iconic{width:12px;height:12px}.sidebar{width:280px}.sidebar__wrapper{padding-bottom:10px}.sidebar__header{height:28px}.sidebar__header h1{margin:8px 0;font-size:15px}.sidebar__divider{padding:8px 0 4px}.sidebar__divider h1{margin:0 0 0 10px;font-size:13px}.sidebar table{margin:4px 0 6px 10px;width:calc(100% - 16px)}.sidebar table tr td{padding:2px 0;font-size:12px;line-height:13px}.sidebar table tr td:first-child{width:90px}.sidebar #tags{margin:4px 0 6px 10px;width:calc(100% - 16px)}.sidebar #tags .empty{margin:0;font-size:12px}}#loading{display:none;position:fixed;width:100%;height:3px;background-size:100px 3px;background-repeat:repeat-x;border-bottom:1px solid rgba(0,0,0,.3);-webkit-animation-name:moveBackground;animation-name:moveBackground;-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;-webkit-animation-timing-function:linear;animation-timing-function:linear}#loading.loading{height:3px;background-image:-webkit-gradient(linear,left top,right top,from(#153674),color-stop(47%,#153674),color-stop(53%,#2651ae),to(#2651ae));background-image:-o-linear-gradient(left,#153674 0,#153674 47%,#2651ae 53%,#2651ae 100%);background-image:linear-gradient(to right,#153674 0,#153674 47%,#2651ae 53%,#2651ae 100%);z-index:2}#loading.error{height:40px;background-color:#2f0d0e;background-image:-webkit-gradient(linear,left top,right top,from(#451317),color-stop(47%,#451317),color-stop(53%,#aa3039),to(#aa3039));background-image:-o-linear-gradient(left,#451317 0,#451317 47%,#aa3039 53%,#aa3039 100%);background-image:linear-gradient(to right,#451317 0,#451317 47%,#aa3039 53%,#aa3039 100%);z-index:1}#loading.success{height:40px;background-color:#070;background-image:-webkit-gradient(linear,left top,right top,from(#070),color-stop(47%,#090),color-stop(53%,#0a0),to(#0c0));background-image:-o-linear-gradient(left,#070 0,#090 47%,#0a0 53%,#0c0 100%);background-image:linear-gradient(to right,#070 0,#090 47%,#0a0 53%,#0c0 100%);z-index:1}#loading .leftMenu__open{padding-left:250px}#loading h1{margin:13px 13px 0;color:#ddd;font-size:14px;font-weight:700;text-shadow:0 1px 0 #000;text-transform:capitalize}#loading h1 span{margin-left:10px;font-weight:400;text-transform:none}.basicModalContainer{background-color:rgba(0,0,0,.85)}.basicModalContainer--error{-webkit-transform:translateY(40px);-ms-transform:translateY(40px);transform:translateY(40px)}.basicModal{background:-webkit-gradient(linear,left top,left bottom,from(#444),to(#333));background:-o-linear-gradient(top,#444,#333);background:linear-gradient(to bottom,#444,#333);-webkit-box-shadow:0 1px 4px rgba(0,0,0,.2),inset 0 1px 0 rgba(255,255,255,.05);box-shadow:0 1px 4px rgba(0,0,0,.2),inset 0 1px 0 rgba(255,255,255,.05)}.basicModal--error{-webkit-transform:translateY(-40px);-ms-transform:translateY(-40px);transform:translateY(-40px)}.basicModal__content{padding:0}.basicModal__content p{margin:0}.basicModal__buttons{-webkit-box-shadow:none;box-shadow:none}.basicModal p{padding:10px 30px;color:rgba(255,255,255,.9);font-size:14px;text-align:left;line-height:20px}.basicModal p b{font-weight:700;color:#fff}.basicModal p a{color:rgba(255,255,255,.9);text-decoration:none;border-bottom:1px dashed #888}.basicModal p:first-of-type{padding-top:42px}.basicModal p:last-of-type{padding-bottom:40px}.basicModal p.signIn:first-of-type{padding-top:30px}.basicModal p.less,.basicModal p.signIn:last-of-type{padding-bottom:30px}.basicModal p.photoPublic{padding:0 30px;margin:30px 0}.basicModal p.importServer:last-of-type{padding-bottom:0}.basicModal__button{padding:13px 0 15px;background:rgba(0,0,0,.02);color:rgba(255,255,255,.5);border-top:1px solid rgba(0,0,0,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.02);box-shadow:inset 0 1px 0 rgba(255,255,255,.02);cursor:default}.basicModal__button--active,.basicModal__button:active{-webkit-transition:none;-o-transition:none;transition:none;background:rgba(0,0,0,.1)}.basicModal__button#basicModal__action{color:#2293ec;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.02),inset 1px 0 0 rgba(0,0,0,.2);box-shadow:inset 0 1px 0 rgba(255,255,255,.02),inset 1px 0 0 rgba(0,0,0,.2)}.basicModal__button#basicModal__action.red,.basicModal__button#basicModal__cancel.red{color:#d92c34}.basicModal__button.hidden{display:none}.basicModal__button.busy{cursor:wait}.basicModal input.text{padding:9px 2px;width:100%;background-color:transparent;color:#fff;border:none;border-bottom:1px solid #222;border-radius:0;-webkit-box-shadow:0 1px 0 rgba(255,255,255,.05);box-shadow:0 1px 0 rgba(255,255,255,.05);outline:0}.basicModal input.text:focus{border-bottom-color:#2293ec}.basicModal input.text.error{border-bottom-color:#d92c34}.basicModal input.text:first-child{margin-top:10px}.basicModal input.text:last-child{margin-bottom:10px}.basicModal .choice{padding:0 30px 15px;width:100%;color:#fff}.basicModal .choice:first-child{padding-top:42px}.basicModal .choice:last-child{padding-bottom:40px}.basicModal .choice label{float:left;color:#fff;font-size:14px;font-weight:700}.basicModal .choice label input{position:absolute;margin:0;opacity:0}.basicModal .choice label .checkbox{float:left;display:block;width:16px;height:16px;background:rgba(0,0,0,.5);border-radius:3px;-webkit-box-shadow:0 0 0 1px rgba(0,0,0,.7);box-shadow:0 0 0 1px rgba(0,0,0,.7)}.basicModal .choice label .checkbox .iconic{-webkit-box-sizing:border-box;box-sizing:border-box;fill:#2293ec;padding:2px;opacity:0;-webkit-transform:scale(0);-ms-transform:scale(0);transform:scale(0);-webkit-transition:opacity .2s cubic-bezier(.51,.92,.24,1),-webkit-transform .2s cubic-bezier(.51,.92,.24,1);transition:opacity .2s cubic-bezier(.51,.92,.24,1),transform .2s cubic-bezier(.51,.92,.24,1),-webkit-transform .2s cubic-bezier(.51,.92,.24,1);-o-transition:opacity .2s cubic-bezier(.51,.92,.24,1),transform .2s cubic-bezier(.51,.92,.24,1)}.basicModal .choice label input:checked~.checkbox{background:rgba(0,0,0,.5)}.basicModal .choice label input:checked~.checkbox .iconic{opacity:1;-webkit-transform:scale(1);-ms-transform:scale(1);transform:scale(1)}.basicModal .choice label input:active~.checkbox{background:rgba(0,0,0,.3)}.basicModal .choice label input:active~.checkbox .iconic{opacity:.8}.basicModal .choice label input:disabled~.checkbox{background:rgba(0,0,0,.2);cursor:not-allowed}.basicModal .choice label input:disabled~.checkbox .iconic{opacity:.3}.basicModal .choice label input:disabled~.label{color:rgba(255,255,255,.3)}.basicModal .choice label .label{margin:0 0 0 18px}.basicModal .choice p{clear:both;padding:2px 0 0 35px;margin:0;width:100%;color:rgba(255,255,255,.6);font-size:13px}.basicModal .choice input.text{display:none;margin-top:5px;margin-left:35px;width:calc(100% - 35px)}.basicModal .choice input.text:disabled{cursor:not-allowed}.basicModal .select{display:inline-block;position:relative;margin:5px 7px;padding:0;width:210px;background:rgba(0,0,0,.3);color:#fff;border-radius:3px;border:1px solid rgba(0,0,0,.2);-webkit-box-shadow:0 1px 0 rgba(255,255,255,.02);box-shadow:0 1px 0 rgba(255,255,255,.02);font-size:11px;line-height:16px;overflow:hidden;outline:0;vertical-align:middle}.basicModal .select::after{position:absolute;content:"≡";right:8px;top:4px;color:#2293ec;font-size:16px;line-height:16px;font-weight:700;pointer-events:none}.basicModal .select select{margin:0;padding:4px 8px;width:120%;color:#fff;font-size:11px;line-height:16px;border:0;outline:0;-webkit-box-shadow:none;box-shadow:none;border-radius:0;background-color:transparent;background-image:none;-moz-appearance:none;-webkit-appearance:none;appearance:none}.basicModal .select select:focus{outline:0}.basicModal .select select option{background:#333!important;color:#fff!important;margin:0;padding:0;-webkit-transition:none;-o-transition:none;transition:none}.basicModal .version{margin:-5px 0 0;padding:0 30px 30px!important;color:rgba(255,255,255,.3);font-size:12px;text-align:right}.basicModal .version span{display:none}.basicModal .version span a{color:rgba(255,255,255,.3)}.basicModal div.version{position:absolute;top:20px;right:0}.basicModal h1{float:left;width:100%;padding:12px 0;color:#fff;font-size:16px;font-weight:700;text-align:center}.basicModal .rows{margin:0 8px 8px;width:calc(100% - 16px);height:300px;background-color:rgba(0,0,0,.4);overflow:hidden;overflow-y:auto;border-radius:3px;-webkit-box-shadow:inset 0 0 3px rgba(0,0,0,.4);box-shadow:inset 0 0 3px rgba(0,0,0,.4)}.basicModal .rows .row{float:left;padding:8px 0;width:100%;background-color:rgba(255,255,255,.02)}.basicModal .rows .row:nth-child(2n){background-color:rgba(255,255,255,0)}.basicModal .rows .row a.name{float:left;padding:5px 10px;width:70%;color:#fff;font-size:14px;white-space:nowrap;overflow:hidden}.basicModal .rows .row a.status{float:left;padding:5px 10px;width:30%;color:rgba(255,255,255,.5);font-size:14px;text-align:right;-webkit-animation-name:pulse;animation-name:pulse;-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}.basicModal .rows .row a.status.error,.basicModal .rows .row a.status.success,.basicModal .rows .row a.status.warning{-webkit-animation:none;animation:none}.basicModal .rows .row a.status.error{color:#e92a00}.basicModal .rows .row a.status.warning{color:#e4e900}.basicModal .rows .row a.status.success{color:#7ee900}.basicModal .rows .row p.notice{display:none;float:left;padding:2px 10px 5px;width:100%;color:rgba(255,255,255,.5);font-size:12px;overflow:hidden;line-height:16px}.basicModal .switch{padding:0 30px;margin-bottom:15px;width:100%;color:#fff}.basicModal .switch:first-child{padding-top:42px}.basicModal .switch input{opacity:0;width:0;height:0;margin:0}.basicModal .switch label{float:left;color:#fff;font-size:14px;font-weight:700}.basicModal .switch .slider{display:inline-block;width:42px;height:22px;left:-9px;bottom:-6px;position:relative;cursor:pointer;border:1px solid rgba(0,0,0,.2);-webkit-box-shadow:0 1px 0 rgba(255,255,255,.02);box-shadow:0 1px 0 rgba(255,255,255,.02);background:rgba(0,0,0,.3);-webkit-transition:.4s;-o-transition:.4s;transition:.4s}.basicModal .switch .slider:before{position:absolute;content:"";height:14px;width:14px;left:3px;bottom:3px;background-color:#2293ec;-webkit-transition:.4s;-o-transition:.4s;transition:.4s}.basicModal .switch input:checked+.slider{background-color:#2293ec}.basicModal .switch input:checked+.slider:before{-ms-transform:translateX(20px);-webkit-transform:translateX(20px);transform:translateX(20px);background-color:#fff}.basicModal .switch .slider.round{border-radius:20px}.basicModal .switch .slider.round:before{border-radius:50%}.basicModal .switch label input:disabled~.slider{background:rgba(0,0,0,.2);cursor:not-allowed}.basicModal .switch label input:disabled~.slider .iconic{opacity:.3}.basicModal .switch .label--disabled{color:rgba(255,255,255,.6)}.basicModal .switch p{clear:both;padding:2px 0 0;margin:0;width:100%;color:rgba(255,255,255,.6);font-size:13px}#sensitive_warning{background:rgba(100,0,0,.95);width:100vw;height:100vh;position:fixed;top:0;text-align:center;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;color:#fff}#sensitive_warning h1{font-size:36px;font-weight:700;border-bottom:2px solid #fff;margin-bottom:15px}#sensitive_warning p{font-size:20px;max-width:40%;margin-top:15px}.settings_view{width:90%;max-width:700px;margin-left:auto;margin-right:auto}.settings_view input.text{padding:9px 2px;width:calc(50% - 4px);background-color:transparent;color:#fff;border:none;border-bottom:1px solid #222;border-radius:0;-webkit-box-shadow:0 1px 0 rgba(255,255,255,.05);box-shadow:0 1px 0 rgba(255,255,255,.05);outline:0}.settings_view input.text:focus{border-bottom-color:#2293ec}.settings_view input.text .error{border-bottom-color:#d92c34}.settings_view .basicModal__button{color:#2293ec;display:inline-block;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.02),inset 1px 0 0 rgba(0,0,0,.2);box-shadow:inset 0 1px 0 rgba(255,255,255,.02),inset 1px 0 0 rgba(0,0,0,.2);border-radius:5px}.settings_view .basicModal__button_MORE,.settings_view .basicModal__button_SAVE{color:#b22027;border-radius:5px}.settings_view>div{font-size:14px;width:100%;padding:12px 0}.settings_view>div p{margin:0 0 5%;width:100%;color:#ccc;line-height:16px}.settings_view>div p a{color:rgba(255,255,255,.9);text-decoration:none;border-bottom:1px dashed #888}.settings_view>div p:last-of-type{margin:0}.settings_view>div input.text{width:100%}.settings_view>div textarea{padding:9px;width:calc(100% - 18px);height:100px;background-color:transparent;color:#fff;border:1px solid #666;border-radius:0;-webkit-box-shadow:0 1px 0 rgba(255,255,255,.05);box-shadow:0 1px 0 rgba(255,255,255,.05);outline:0;resize:vertical}.settings_view>div textarea:focus{border-color:#2293ec}.settings_view>div .choice{padding:0 30px 15px;width:100%;color:#fff}.settings_view>div .choice:last-child{padding-bottom:40px}.settings_view>div .choice label{float:left;color:#fff;font-size:14px;font-weight:700}.settings_view>div .choice label input{position:absolute;margin:0;opacity:0}.settings_view>div .choice label .checkbox{float:left;display:block;width:16px;height:16px;background:rgba(0,0,0,.5);border-radius:3px;-webkit-box-shadow:0 0 0 1px rgba(0,0,0,.7);box-shadow:0 0 0 1px rgba(0,0,0,.7)}.settings_view>div .choice label .checkbox .iconic{-webkit-box-sizing:border-box;box-sizing:border-box;fill:#2293ec;padding:2px;opacity:0;-ms-transform:scale(0);-webkit-transform:scale(0);transform:scale(0);-webkit-transition:opacity .2s cubic-bezier(.51,.92,.24,1),-webkit-transform .2s cubic-bezier(.51,.92,.24,1);transition:opacity .2s cubic-bezier(.51,.92,.24,1),transform .2s cubic-bezier(.51,.92,.24,1),-webkit-transform .2s cubic-bezier(.51,.92,.24,1);-o-transition:opacity .2s cubic-bezier(.51,.92,.24,1),transform .2s cubic-bezier(.51,.92,.24,1)}.settings_view>div .select{position:relative;margin:1px 5px;padding:0;width:110px;color:#fff;border-radius:3px;border:1px solid rgba(0,0,0,.2);-webkit-box-shadow:0 1px 0 rgba(255,255,255,.02);box-shadow:0 1px 0 rgba(255,255,255,.02);font-size:11px;line-height:16px;overflow:hidden;outline:0;vertical-align:middle;background:rgba(0,0,0,.3);display:inline-block}.settings_view>div .select select{margin:0;padding:4px 8px;width:120%;color:#fff;font-size:11px;line-height:16px;border:0;outline:0;-webkit-box-shadow:none;box-shadow:none;border-radius:0;background-color:transparent;background-image:none;-moz-appearance:none;-webkit-appearance:none;appearance:none}.settings_view>div .select select option{margin:0;padding:0;background:#fff;color:#333;-webkit-transition:none;-o-transition:none;transition:none}.settings_view>div .select select:disabled{color:#000;cursor:not-allowed}.settings_view>div .select::after{position:absolute;content:"≡";right:8px;top:4px;color:#2293ec;font-size:16px;line-height:16px;font-weight:700;pointer-events:none}.settings_view>div .switch{position:relative;display:inline-block;width:42px;height:22px;bottom:-2px;line-height:24px}.settings_view>div .switch input{opacity:0;width:0;height:0}.settings_view>div .slider{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;border:1px solid rgba(0,0,0,.2);-webkit-box-shadow:0 1px 0 rgba(255,255,255,.02);box-shadow:0 1px 0 rgba(255,255,255,.02);background:rgba(0,0,0,.3);-webkit-transition:.4s;-o-transition:.4s;transition:.4s}.settings_view>div .slider:before{position:absolute;content:"";height:14px;width:14px;left:3px;bottom:3px;background-color:#2293ec}.settings_view>div input:checked+.slider{background-color:#2293ec}.settings_view>div input:checked+.slider:before{-ms-transform:translateX(20px);-webkit-transform:translateX(20px);transform:translateX(20px);background-color:#fff}.settings_view>div .slider.round{border-radius:20px}.settings_view>div .slider.round:before{border-radius:50%}.settings_view .setting_category{font-size:20px;width:100%;padding-top:10px;padding-left:4px;border-bottom:1px dotted #222;margin-top:20px;color:#fff;font-weight:700;text-transform:capitalize}.settings_view .setting_line{font-size:14px;width:100%}.settings_view .setting_line:first-child,.settings_view .setting_line:last-child{padding-top:50px}.settings_view .setting_line p{min-width:550px;margin:0;color:#ccc;display:inline-block;width:100%;overflow-wrap:break-word}.settings_view .setting_line p a{color:rgba(255,255,255,.9);text-decoration:none;border-bottom:1px dashed #888}.settings_view .setting_line p:last-of-type{margin:0}.settings_view .setting_line p .warning{margin-bottom:30px;color:#d92c34;font-weight:700;font-size:18px;text-align:justify;line-height:22px}.settings_view .setting_line span.text{display:inline-block;padding:9px 4px;width:calc(50% - 12px);background-color:transparent;color:#fff;border:none}.settings_view .setting_line span.text_icon{width:5%}.settings_view .setting_line span.text_icon .iconic{width:15px;height:14px;margin:0 10px 0 1px;fill:#fff}.settings_view .setting_line input.text{width:calc(50% - 4px)}@media (hover:hover){.basicModal__button:hover{background:rgba(255,255,255,.02)}.settings_view .basicModal__button:hover{background:#2293ec;color:#fff;cursor:pointer}.settings_view .basicModal__button_MORE:hover,.settings_view .basicModal__button_SAVE:hover{background:#b22027;color:#fff}.settings_view input:hover{border-bottom:1px solid #2293ec}}@media (hover:none){.settings_view input.text{border-bottom:1px solid #2293ec;margin:6px 0}.settings_view>div{padding:16px 0}.settings_view .basicModal__button{background:#2293ec;color:#fff;max-width:320px;margin-top:20px}.settings_view .basicModal__button_MORE,.settings_view .basicModal__button_SAVE{background:#b22027}}@media only screen and (max-width:567px),only screen and (max-width:640px) and (orientation:portrait){.settings_view{max-width:100%}.settings_view .setting_category{font-size:14px;padding-left:0;margin-bottom:4px}.settings_view .setting_line{font-size:12px}.settings_view .setting_line:first-child{padding-top:20px}.settings_view .setting_line p{min-width:unset;line-height:20px}.settings_view .setting_line p.warning{font-size:14px;line-height:16px;margin-bottom:0}.settings_view .setting_line p input,.settings_view .setting_line p span{padding:0}.settings_view .basicModal__button_SAVE{margin-top:20px}}.users_view{width:90%;max-width:700px;margin-left:auto;margin-right:auto}.users_view_line{font-size:14px;width:100%}.users_view_line:first-child,.users_view_line:last-child{padding-top:50px}.users_view_line p{width:550px;margin:0 0 5%;color:#ccc;display:inline-block}.users_view_line p a{color:rgba(255,255,255,.9);text-decoration:none;border-bottom:1px dashed #888}.users_view_line p.line,.users_view_line p:last-of-type{margin:0}.users_view_line span.text{display:inline-block;padding:9px 6px 9px 0;width:40%;background-color:transparent;color:#fff;border:none}.users_view_line span.text_icon{width:5%;min-width:32px}.users_view_line span.text_icon .iconic{width:15px;height:14px;margin:0 8px;fill:#fff}.users_view_line input.text{padding:9px 6px 9px 0;width:40%;background-color:transparent;color:#fff;border:none;border-bottom:1px solid #222;border-radius:0;-webkit-box-shadow:0 1px 0 rgba(255,255,255,.05);box-shadow:0 1px 0 rgba(255,255,255,.05);outline:0;margin:0 0 10px}.users_view_line input.text:focus{border-bottom-color:#2293ec}.users_view_line input.text.error{border-bottom-color:#d92c34}.users_view_line .choice label input:checked~.checkbox .iconic{opacity:1;-ms-transform:scale(1);-webkit-transform:scale(1);transform:scale(1)}.users_view_line .choice{display:inline-block;width:5%;min-width:32px;color:#fff}.users_view_line .choice input{position:absolute;margin:0;opacity:0}.users_view_line .choice .checkbox{display:inline-block;width:16px;height:16px;margin:10px 8px 0;background:rgba(0,0,0,.5);border-radius:3px;-webkit-box-shadow:0 0 0 1px rgba(0,0,0,.7);box-shadow:0 0 0 1px rgba(0,0,0,.7)}.users_view_line .choice .checkbox .iconic{-webkit-box-sizing:border-box;box-sizing:border-box;fill:#2293ec;padding:2px;opacity:0;-ms-transform:scale(0);-webkit-transform:scale(0);transform:scale(0);-webkit-transition:opacity .2s cubic-bezier(.51,.92,.24,1),-webkit-transform .2s cubic-bezier(.51,.92,.24,1);transition:opacity .2s cubic-bezier(.51,.92,.24,1),transform .2s cubic-bezier(.51,.92,.24,1),-webkit-transform .2s cubic-bezier(.51,.92,.24,1);-o-transition:opacity .2s cubic-bezier(.51,.92,.24,1),transform .2s cubic-bezier(.51,.92,.24,1)}.users_view_line .basicModal__button{display:inline-block;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.02),inset 1px 0 0 rgba(0,0,0,.2);box-shadow:inset 0 1px 0 rgba(255,255,255,.02),inset 1px 0 0 rgba(0,0,0,.2);width:10%;min-width:72px;border-radius:0}.users_view_line .basicModal__button_OK{color:#2293ec;border-radius:5px 0 0 5px;margin-right:-4px}.users_view_line .basicModal__button_DEL{color:#b22027;border-radius:0 5px 5px 0}.users_view_line .basicModal__button_CREATE{width:20%;color:#090;border-radius:5px;min-width:144px}.users_view_line .select{position:relative;margin:1px 5px;padding:0;width:110px;color:#fff;border-radius:3px;border:1px solid rgba(0,0,0,.2);-webkit-box-shadow:0 1px 0 rgba(255,255,255,.02);box-shadow:0 1px 0 rgba(255,255,255,.02);font-size:11px;line-height:16px;overflow:hidden;outline:0;vertical-align:middle;background:rgba(0,0,0,.3);display:inline-block}.users_view_line .select select{margin:0;padding:4px 8px;width:120%;color:#fff;font-size:11px;line-height:16px;border:0;outline:0;-webkit-box-shadow:none;box-shadow:none;border-radius:0;background:0 0;-moz-appearance:none;-webkit-appearance:none;appearance:none}.users_view_line .select select option{margin:0;padding:0;background:#fff;color:#333;-webkit-transition:none;-o-transition:none;transition:none}.users_view_line .select::after{position:absolute;content:"≡";right:8px;top:4px;color:#2293ec;font-size:16px;line-height:16px;font-weight:700;pointer-events:none}@media (hover:hover){.users_view_line .basicModal__button:hover{cursor:pointer;color:#fff}.users_view_line .basicModal__button_OK:hover{background:#2293ec}.users_view_line .basicModal__button_DEL:hover{background:#b22027}.users_view_line .basicModal__button_CREATE:hover{background:#090}.users_view_line input:hover{border-bottom:1px solid #2293ec}}@media (hover:none){.users_view_line .basicModal__button{color:#fff}.users_view_line .basicModal__button_OK{background:#2293ec}.users_view_line .basicModal__button_DEL{background:#b22027}.users_view_line .basicModal__button_CREATE{background:#090}.users_view_line input{border-bottom:1px solid #2293ec}}@media only screen and (max-width:567px),only screen and (max-width:640px) and (orientation:portrait){.users_view{width:100%;max-width:100%;padding:20px}.users_view_line p{width:100%}.users_view_line p .text,.users_view_line p input.text{width:36%;font-size:smaller}.users_view_line .choice{margin-left:-8px;margin-right:3px}}.u2f_view{width:90%;max-width:700px;margin-left:auto;margin-right:auto}.u2f_view_line{font-size:14px;width:100%}.u2f_view_line:first-child,.u2f_view_line:last-child{padding-top:50px}.u2f_view_line p{width:550px;margin:0 0 5%;color:#ccc;display:inline-block}.u2f_view_line p a{color:rgba(255,255,255,.9);text-decoration:none;border-bottom:1px dashed #888}.u2f_view_line p.line,.u2f_view_line p:last-of-type{margin:0}.u2f_view_line p.single{text-align:center}.u2f_view_line span.text{display:inline-block;padding:9px 4px;width:80%;background-color:transparent;color:#fff;border:none}.u2f_view_line span.text_icon{width:5%}.u2f_view_line span.text_icon .iconic{width:15px;height:14px;margin:0 15px 0 1px;fill:#fff}.u2f_view_line .choice label input:checked~.checkbox .iconic{opacity:1;-ms-transform:scale(1);-webkit-transform:scale(1);transform:scale(1)}.u2f_view_line .choice{display:inline-block;width:5%;color:#fff}.u2f_view_line .choice input{position:absolute;margin:0;opacity:0}.u2f_view_line .choice .checkbox{display:inline-block;width:16px;height:16px;margin-top:10px;margin-left:2px;background:rgba(0,0,0,.5);border-radius:3px;-webkit-box-shadow:0 0 0 1px rgba(0,0,0,.7);box-shadow:0 0 0 1px rgba(0,0,0,.7)}.u2f_view_line .choice .checkbox .iconic{-webkit-box-sizing:border-box;box-sizing:border-box;fill:#2293ec;padding:2px;opacity:0;-ms-transform:scale(0);-webkit-transform:scale(0);transform:scale(0);-webkit-transition:opacity .2s cubic-bezier(.51,.92,.24,1),-webkit-transform .2s cubic-bezier(.51,.92,.24,1);transition:opacity .2s cubic-bezier(.51,.92,.24,1),transform .2s cubic-bezier(.51,.92,.24,1),-webkit-transform .2s cubic-bezier(.51,.92,.24,1);-o-transition:opacity .2s cubic-bezier(.51,.92,.24,1),transform .2s cubic-bezier(.51,.92,.24,1)}.u2f_view_line .basicModal__button{display:inline-block;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.02),inset 1px 0 0 rgba(0,0,0,.2);box-shadow:inset 0 1px 0 rgba(255,255,255,.02),inset 1px 0 0 rgba(0,0,0,.2);width:20%;min-width:50px;border-radius:0}.u2f_view_line .basicModal__button_OK{color:#2293ec;border-radius:5px 0 0 5px}.u2f_view_line .basicModal__button_DEL{color:#b22027;border-radius:0 5px 5px 0}.u2f_view_line .basicModal__button_CREATE{width:100%;color:#090;border-radius:5px}.u2f_view_line .select{position:relative;margin:1px 5px;padding:0;width:110px;color:#fff;border-radius:3px;border:1px solid rgba(0,0,0,.2);-webkit-box-shadow:0 1px 0 rgba(255,255,255,.02);box-shadow:0 1px 0 rgba(255,255,255,.02);font-size:11px;line-height:16px;overflow:hidden;outline:0;vertical-align:middle;background:rgba(0,0,0,.3);display:inline-block}.u2f_view_line .select select{margin:0;padding:4px 8px;width:120%;color:#fff;font-size:11px;line-height:16px;border:0;outline:0;-webkit-box-shadow:none;box-shadow:none;border-radius:0;background:0 0;-moz-appearance:none;-webkit-appearance:none;appearance:none}.u2f_view_line .select select option{margin:0;padding:0;background:#fff;color:#333;-webkit-transition:none;-o-transition:none;transition:none}.u2f_view_line .select::after{position:absolute;content:"≡";right:8px;top:4px;color:#2293ec;font-size:16px;line-height:16px;font-weight:700;pointer-events:none}.signInKeyLess{display:block;padding:10px;position:absolute;cursor:pointer}.signInKeyLess .iconic{display:inline-block;margin:0;width:20px;height:20px;fill:#818181}.signInKeyLess .iconic.ionicons{margin:0 8px -2px 0;width:18px;height:18px}@media (hover:hover){.u2f_view_line .basicModal__button:hover{cursor:pointer}.u2f_view_line .basicModal__button_OK:hover{background:#2293ec;color:#fff}.u2f_view_line .basicModal__button_DEL:hover{background:#b22027;color:#fff}.u2f_view_line .basicModal__button_CREATE:hover{background:#090;color:#fff}.u2f_view_line input:hover{border-bottom:1px solid #2293ec}.signInKeyLess:hover .iconic{fill:#fff}}@media (hover:none){.u2f_view_line .basicModal__button{color:#fff}.u2f_view_line .basicModal__button_OK{background:#2293ec}.u2f_view_line .basicModal__button_DEL{background:#b22027}.u2f_view_line .basicModal__button_CREATE{background:#090}.u2f_view_line input{border-bottom:1px solid #2293ec}}@media only screen and (max-width:567px),only screen and (max-width:640px) and (orientation:portrait){.u2f_view{width:100%;max-width:100%;padding:20px}.u2f_view_line p{width:100%}.u2f_view_line .basicModal__button_CREATE{width:80%;margin:0 10%}}.logs_diagnostics_view{width:90%;margin-left:auto;margin-right:auto;color:#ccc;font-size:12px;line-height:14px}.logs_diagnostics_view pre{font-family:monospace;-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;user-select:text;width:-webkit-fit-content;width:-moz-fit-content;width:fit-content;padding-right:30px}.clear_logs_update{padding-left:30px;margin:20px auto}.clear_logs_update .basicModal__button,.logs_diagnostics_view .basicModal__button{color:#2293ec;display:inline-block;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.02),inset 1px 0 0 rgba(0,0,0,.2);box-shadow:inset 0 1px 0 rgba(255,255,255,.02),inset 1px 0 0 rgba(0,0,0,.2);border-radius:5px}.clear_logs_update .iconic,.logs_diagnostics_view .iconic{display:inline-block;margin:0 10px 0 1px;width:13px;height:12px;fill:#2293ec}.clear_logs_update .button_left,.logs_diagnostics_view .button_left{margin-left:24px;width:400px}@media (hover:none){.clear_logs_update .basicModal__button,.logs_diagnostics_view .basicModal__button{background:#2293ec;color:#fff;max-width:320px;margin-top:20px}.clear_logs_update .iconic,.logs_diagnostics_view .iconic{fill:#fff}}@media only screen and (max-width:567px),only screen and (max-width:640px) and (orientation:portrait){.clear_logs_update,.logs_diagnostics_view{width:100%;max-width:100%;font-size:11px;line-height:12px}.clear_logs_update .basicModal__button,.clear_logs_update .button_left,.logs_diagnostics_view .basicModal__button,.logs_diagnostics_view .button_left{width:80%;margin:0 10%}.logs_diagnostics_view{padding:10px 10px 0 0}.clear_logs_update{padding:10px 10px 0;margin:0}}.sharing_view{width:90%;max-width:700px;margin-left:auto;margin-right:auto;margin-top:20px}.sharing_view .sharing_view_line{width:100%;display:block;clear:left}.sharing_view .col-xs-1,.sharing_view .col-xs-10,.sharing_view .col-xs-11,.sharing_view .col-xs-12,.sharing_view .col-xs-2,.sharing_view .col-xs-3,.sharing_view .col-xs-4,.sharing_view .col-xs-5,.sharing_view .col-xs-6,.sharing_view .col-xs-7,.sharing_view .col-xs-8,.sharing_view .col-xs-9{float:left;position:relative;min-height:1px}.sharing_view .col-xs-2{width:10%;padding-right:3%;padding-left:3%}.sharing_view .col-xs-5{width:42%}.sharing_view .btn-block+.btn-block{margin-top:5px}.sharing_view .btn-block{display:block;width:100%}.sharing_view .btn-default{color:#2293ec;border-color:#2293ec;background:rgba(0,0,0,.5);border-radius:3px;-webkit-box-shadow:0 0 0 1px rgba(0,0,0,.7);box-shadow:0 0 0 1px rgba(0,0,0,.7)}.sharing_view .btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:400;line-height:1.42857143;text-align:center;white-space:nowrap;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-image:none;border:1px solid transparent;border-radius:4px}.sharing_view select[multiple],.sharing_view select[size]{height:150px}.sharing_view .form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color .15s ease-in-out,-webkit-box-shadow .15s ease-in-out;-o-transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-box-shadow .15s ease-in-out}.sharing_view .iconic{display:inline-block;width:15px;height:14px;fill:#2293ec}.sharing_view .iconic .iconic.ionicons{margin:0 8px -2px 0;width:18px;height:18px}.sharing_view .blue .iconic{fill:#2293ec}.sharing_view .grey .iconic{fill:#b4b4b4}.sharing_view p{width:100%;color:#ccc;text-align:center;font-size:14px;display:block}.sharing_view p.with{padding:15px 0}.sharing_view span.text{display:inline-block;padding:0 2px;width:40%;background-color:transparent;color:#fff;border:none}.sharing_view span.text:last-of-type{width:5%}.sharing_view span.text .iconic{width:15px;height:14px;margin:0 10px 0 1px;fill:#fff}.sharing_view .basicModal__button{margin-top:10px;color:#2293ec;display:inline-block;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.02),inset 1px 0 0 rgba(0,0,0,.2);box-shadow:inset 0 1px 0 rgba(255,255,255,.02),inset 1px 0 0 rgba(0,0,0,.2);border-radius:5px}.sharing_view .choice label input:checked~.checkbox .iconic{opacity:1;-ms-transform:scale(1);-webkit-transform:scale(1);transform:scale(1)}.sharing_view .choice{display:inline-block;width:5%;margin:0 10px;color:#fff}.sharing_view .choice input{position:absolute;margin:0;opacity:0}.sharing_view .choice .checkbox{display:inline-block;width:16px;height:16px;margin-top:10px;margin-left:2px;background:rgba(0,0,0,.5);border-radius:3px;-webkit-box-shadow:0 0 0 1px rgba(0,0,0,.7);box-shadow:0 0 0 1px rgba(0,0,0,.7)}.sharing_view .choice .checkbox .iconic{-webkit-box-sizing:border-box;box-sizing:border-box;fill:#2293ec;padding:2px;opacity:0;-ms-transform:scale(0);-webkit-transform:scale(0);transform:scale(0);-webkit-transition:opacity .2s cubic-bezier(.51,.92,.24,1),-webkit-transform .2s cubic-bezier(.51,.92,.24,1);transition:opacity .2s cubic-bezier(.51,.92,.24,1),transform .2s cubic-bezier(.51,.92,.24,1),-webkit-transform .2s cubic-bezier(.51,.92,.24,1);-o-transition:opacity .2s cubic-bezier(.51,.92,.24,1),transform .2s cubic-bezier(.51,.92,.24,1)}.sharing_view .select{position:relative;padding:0;color:#fff;border-radius:3px;border:1px solid rgba(0,0,0,.2);-webkit-box-shadow:0 1px 0 rgba(255,255,255,.02);box-shadow:0 1px 0 rgba(255,255,255,.02);font-size:14px;line-height:16px;outline:0;vertical-align:middle;background:rgba(0,0,0,.3);display:inline-block}.sharing_view .borderBlue{border:1px solid #2293ec}@media (hover:none){.sharing_view .basicModal__button{background:#2293ec;color:#fff}.sharing_view input{border-bottom:1px solid #2293ec}}@media only screen and (max-width:567px),only screen and (max-width:640px) and (orientation:portrait){.sharing_view{width:100%;max-width:100%;padding:10px}.sharing_view .select{font-size:12px}.sharing_view .iconic{margin-left:-4px}.sharing_view_line p{width:100%}.sharing_view_line .basicModal__button{width:80%;margin:0 10%}}#multiselect{position:absolute;background-color:rgba(0,94,204,.3);border:1px solid #005ecc;border-radius:3px;z-index:5}.justified-layout{margin:30px 0 0 30px;width:100%;position:relative}.unjustified-layout{margin:25px -5px -5px 25px;width:100%;position:relative;overflow:hidden}.justified-layout>.photo{position:absolute;--lychee-default-height:320px;margin:0}.unjustified-layout>.photo{float:left;max-height:240px;margin:5px}.justified-layout>.photo>.thumbimg,.justified-layout>.photo>.thumbimg>img,.unjustified-layout>.photo>.thumbimg,.unjustified-layout>.photo>.thumbimg>img{width:100%;height:100%;border:none;-o-object-fit:cover;object-fit:cover}.justified-layout>.photo>.overlay,.unjustified-layout>.photo>.overlay{width:100%;bottom:0;margin:0}.justified-layout>.photo>.overlay>h1,.unjustified-layout>.photo>.overlay>h1{width:auto;margin-right:15px}@media only screen and (min-width:320px) and (max-width:567px){.content>.justified-layout{margin:8px 8px 0}.content>.justified-layout .photo{--lychee-default-height:160px}}@media only screen and (min-width:568px) and (max-width:639px){.content>.justified-layout{margin:9px 9px 0}.content>.justified-layout .photo{--lychee-default-height:200px}}@media only screen and (min-width:640px) and (max-width:768px){.content>.justified-layout{margin:10px 10px 0}.content>.justified-layout .photo{--lychee-default-height:240px}}#footer{z-index:3;left:0;right:0;bottom:0;-webkit-transition:color .3s,opacity .3s ease-out,margin-left .5s,-webkit-transform .3s ease-out,-webkit-box-shadow .3s;transition:color .3s,opacity .3s ease-out,transform .3s ease-out,box-shadow .3s,margin-left .5s,-webkit-transform .3s ease-out,-webkit-box-shadow .3s;-o-transition:color .3s,opacity .3s ease-out,transform .3s ease-out,box-shadow .3s,margin-left .5s;padding:5px 0;text-align:center;position:absolute;background:#1d1d1d}#footer p{color:#ccc;font-weight:400;font-size:.75em;line-height:26px}#footer p a,#footer p a:visited{color:#ccc}#footer p.home_copyright,#footer p.hosted_by{text-transform:uppercase}.hide_footer{display:none}@font-face{font-family:socials;src:url(fonts/socials.eot?egvu10);src:url(fonts/socials.eot?egvu10#iefix) format("embedded-opentype"),url(fonts/socials.ttf?egvu10) format("truetype"),url(fonts/socials.woff?egvu10) format("woff"),url(fonts/socials.svg?egvu10#socials) format("svg");font-weight:400;font-style:normal}#socials_footer{padding:0;text-align:center;left:0;right:0}.socialicons{display:inline-block;font-size:18px;font-family:socials!important;speak:none;color:#ccc;text-decoration:none;margin:15px 15px 5px;transition:.3s;-webkit-transition:.3s;-moz-transition:.3s;-o-transition:.3s}#twitter:before{content:"\ea96"}#instagram:before{content:"\ea92"}#youtube:before{content:"\ea9d"}#flickr:before{content:"\eaa4"}#facebook:before{content:"\ea91"}@media (hover:hover){.sharing_view .basicModal__button:hover{background:#2293ec;color:#fff;cursor:pointer}.sharing_view input:hover{border-bottom:1px solid #2293ec}.socialicons:hover{color:#b5b5b5;-ms-transform:scale(1.3);transform:scale(1.3);-webkit-transform:scale(1.3)}.directLinks .basicModal__button:hover,.downloads .basicModal__button:hover{background:#2293ec;cursor:pointer}.directLinks .basicModal__button:hover .iconic,.downloads .basicModal__button:hover .iconic{fill:#fff}.downloads .basicModal__button:hover{color:#fff}}.directLinks input.text{width:calc(100% - 30px);color:rgba(255,255,255,.6);padding:2px}.directLinks .basicModal__button{display:inline-block;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.02),inset 1px 0 0 rgba(0,0,0,.2);box-shadow:inset 0 1px 0 rgba(255,255,255,.02),inset 1px 0 0 rgba(0,0,0,.2);width:25px;height:25px;border-radius:5px;border-bottom:0;padding:3px 0 0;margin-top:-5px;float:right}.directLinks .basicModal__button .iconic{fill:#2293ec;width:16px;height:16px}.directLinks .imageLinks{margin-top:-30px;padding-bottom:40px}.directLinks .imageLinks p{padding:10px 30px 0;font-size:12px;line-height:15px}.directLinks .imageLinks .basicModal__button{margin-top:-8px}.downloads{padding:30px}.downloads .basicModal__button{color:#2293ec;display:inline-block;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.02),inset 1px 0 0 rgba(0,0,0,.2);box-shadow:inset 0 1px 0 rgba(255,255,255,.02),inset 1px 0 0 rgba(0,0,0,.2);border-radius:5px;border-bottom:0;margin:5px 0}.downloads .basicModal__button .iconic{fill:#2293ec;margin:0 10px 0 1px;width:11px;height:10px}.leaflet-image-layer,.leaflet-layer,.leaflet-marker-icon,.leaflet-marker-shadow,.leaflet-pane,.leaflet-pane>canvas,.leaflet-pane>svg,.leaflet-tile,.leaflet-tile-container,.leaflet-zoom-box{position:absolute;left:0;top:0}.leaflet-container{overflow:hidden;-webkit-tap-highlight-color:transparent;background:#ddd;outline:0;font:12px/1.5 "Helvetica Neue",Arial,Helvetica,sans-serif}.leaflet-marker-icon,.leaflet-marker-shadow,.leaflet-tile{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-user-drag:none}.leaflet-tile::-moz-selection{background:0 0}.leaflet-tile::selection{background:0 0}.leaflet-safari .leaflet-tile{image-rendering:-webkit-optimize-contrast}.leaflet-safari .leaflet-tile-container{width:1600px;height:1600px;-webkit-transform-origin:0 0}.leaflet-marker-icon,.leaflet-marker-shadow{display:block}.leaflet-container .leaflet-marker-pane img,.leaflet-container .leaflet-overlay-pane svg,.leaflet-container .leaflet-shadow-pane img,.leaflet-container .leaflet-tile,.leaflet-container .leaflet-tile-pane img,.leaflet-container img.leaflet-image-layer{max-width:none!important;max-height:none!important}.leaflet-container.leaflet-touch-zoom{-ms-touch-action:pan-x pan-y;touch-action:pan-x pan-y}.leaflet-container.leaflet-touch-drag{-ms-touch-action:pinch-zoom;touch-action:none;touch-action:pinch-zoom}.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom{-ms-touch-action:none;touch-action:none}.leaflet-container a{-webkit-tap-highlight-color:rgba(51,181,229,.4);color:#0078a8}.leaflet-tile{-webkit-filter:inherit;filter:inherit;visibility:hidden}.leaflet-tile-loaded{visibility:inherit}.leaflet-zoom-box{width:0;height:0;-webkit-box-sizing:border-box;box-sizing:border-box;z-index:800}.leaflet-overlay-pane svg{-moz-user-select:none}.leaflet-pane{z-index:400}.leaflet-tile-pane{z-index:200}.leaflet-overlay-pane{z-index:400}.leaflet-shadow-pane{z-index:500}.leaflet-marker-pane{z-index:600}.leaflet-tooltip-pane{z-index:650}.leaflet-popup-pane{z-index:700}.leaflet-map-pane canvas{z-index:100}.leaflet-map-pane svg{z-index:200}.leaflet-vml-shape{width:1px;height:1px}.lvml{behavior:url(#default#VML);display:inline-block;position:absolute}.leaflet-control{position:relative;z-index:800;pointer-events:visiblePainted;pointer-events:auto;float:left;clear:both}.leaflet-bottom,.leaflet-top{position:absolute;z-index:1000;pointer-events:none}.leaflet-top{top:0}.leaflet-right{right:0}.leaflet-bottom{bottom:0}.leaflet-left{left:0}.leaflet-right .leaflet-control{float:right;margin-right:10px}.leaflet-top .leaflet-control{margin-top:10px}.leaflet-bottom .leaflet-control{margin-bottom:10px}.leaflet-left .leaflet-control{margin-left:10px}.leaflet-fade-anim .leaflet-tile{will-change:opacity}.leaflet-fade-anim .leaflet-popup{opacity:0;-webkit-transition:opacity .2s linear;-o-transition:opacity .2s linear;transition:opacity .2s linear}.leaflet-fade-anim .leaflet-map-pane .leaflet-popup{opacity:1}.leaflet-zoom-animated{-webkit-transform-origin:0 0;-ms-transform-origin:0 0;transform-origin:0 0}.leaflet-zoom-anim .leaflet-zoom-animated{will-change:transform;-webkit-transition:-webkit-transform .25s cubic-bezier(0,0,.25,1);transition:transform .25s cubic-bezier(0,0,.25,1);-o-transition:transform .25s cubic-bezier(0,0,.25,1);transition:transform .25s cubic-bezier(0,0,.25,1),-webkit-transform .25s cubic-bezier(0,0,.25,1)}.leaflet-pan-anim .leaflet-tile,.leaflet-zoom-anim .leaflet-tile{-webkit-transition:none;-o-transition:none;transition:none}.leaflet-zoom-anim .leaflet-zoom-hide{visibility:hidden}.leaflet-interactive{cursor:pointer}.leaflet-grab{cursor:-webkit-grab;cursor:grab}.leaflet-crosshair,.leaflet-crosshair .leaflet-interactive{cursor:crosshair}.leaflet-control,.leaflet-popup-pane{cursor:auto}.leaflet-dragging .leaflet-grab,.leaflet-dragging .leaflet-grab .leaflet-interactive,.leaflet-dragging .leaflet-marker-draggable{cursor:move;cursor:-webkit-grabbing;cursor:grabbing}.leaflet-image-layer,.leaflet-marker-icon,.leaflet-marker-shadow,.leaflet-pane>svg path,.leaflet-tile-container{pointer-events:none}.leaflet-image-layer.leaflet-interactive,.leaflet-marker-icon.leaflet-interactive,.leaflet-pane>svg path.leaflet-interactive,svg.leaflet-image-layer.leaflet-interactive path{pointer-events:visiblePainted;pointer-events:auto}.leaflet-container a.leaflet-active{outline:orange solid 2px}.leaflet-zoom-box{border:2px dotted #38f;background:rgba(255,255,255,.5)}.leaflet-bar{-webkit-box-shadow:0 1px 5px rgba(0,0,0,.65);box-shadow:0 1px 5px rgba(0,0,0,.65);border-radius:4px}.leaflet-bar a,.leaflet-bar a:hover{background-color:#fff;border-bottom:1px solid #ccc;width:26px;height:26px;line-height:26px;display:block;text-align:center;text-decoration:none;color:#000}.leaflet-bar a,.leaflet-control-layers-toggle{background-position:50% 50%;background-repeat:no-repeat;display:block}.leaflet-bar a:hover{background-color:#f4f4f4}.leaflet-bar a:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.leaflet-bar a:last-child{border-bottom-left-radius:4px;border-bottom-right-radius:4px;border-bottom:none}.leaflet-bar a.leaflet-disabled{cursor:default;background-color:#f4f4f4;color:#bbb}.leaflet-touch .leaflet-bar a{width:30px;height:30px;line-height:30px}.leaflet-touch .leaflet-bar a:first-child{border-top-left-radius:2px;border-top-right-radius:2px}.leaflet-touch .leaflet-bar a:last-child{border-bottom-left-radius:2px;border-bottom-right-radius:2px}.leaflet-control-zoom-in,.leaflet-control-zoom-out{font:bold 18px 'Lucida Console',Monaco,monospace;text-indent:1px}.leaflet-touch .leaflet-control-zoom-in,.leaflet-touch .leaflet-control-zoom-out{font-size:22px}.leaflet-control-layers{-webkit-box-shadow:0 1px 5px rgba(0,0,0,.4);box-shadow:0 1px 5px rgba(0,0,0,.4);background:#fff;border-radius:5px}.leaflet-control-layers-toggle{background-image:url(images/layers.png);width:36px;height:36px}.leaflet-retina .leaflet-control-layers-toggle{background-image:url(images/layers-2x.png);background-size:26px 26px}.leaflet-touch .leaflet-control-layers-toggle{width:44px;height:44px}.leaflet-control-layers .leaflet-control-layers-list,.leaflet-control-layers-expanded .leaflet-control-layers-toggle{display:none}.leaflet-control-layers-expanded .leaflet-control-layers-list{display:block;position:relative}.leaflet-control-layers-expanded{padding:6px 10px 6px 6px;color:#333;background:#fff}.leaflet-control-layers-scrollbar{overflow-y:scroll;overflow-x:hidden;padding-right:5px}.leaflet-control-layers-selector{margin-top:2px;position:relative;top:1px}.leaflet-control-layers label{display:block}.leaflet-control-layers-separator{height:0;border-top:1px solid #ddd;margin:5px -10px 5px -6px}.leaflet-default-icon-path{background-image:url(images/marker-icon.png)}.leaflet-container .leaflet-control-attribution{background:rgba(255,255,255,.7);margin:0}.leaflet-control-attribution,.leaflet-control-scale-line{padding:0 5px;color:#333}.leaflet-control-attribution a{text-decoration:none}.leaflet-control-attribution a:hover{text-decoration:underline}.leaflet-container .leaflet-control-attribution,.leaflet-container .leaflet-control-scale{font-size:11px}.leaflet-left .leaflet-control-scale{margin-left:5px}.leaflet-bottom .leaflet-control-scale{margin-bottom:5px}.leaflet-control-scale-line{border:2px solid #777;border-top:none;line-height:1.1;padding:2px 5px 1px;font-size:11px;white-space:nowrap;overflow:hidden;-webkit-box-sizing:border-box;box-sizing:border-box;background:rgba(255,255,255,.5)}.leaflet-control-scale-line:not(:first-child){border-top:2px solid #777;border-bottom:none;margin-top:-2px}.leaflet-control-scale-line:not(:first-child):not(:last-child){border-bottom:2px solid #777}.leaflet-touch .leaflet-bar,.leaflet-touch .leaflet-control-attribution,.leaflet-touch .leaflet-control-layers{-webkit-box-shadow:none;box-shadow:none}.leaflet-touch .leaflet-bar,.leaflet-touch .leaflet-control-layers{border:2px solid rgba(0,0,0,.2);background-clip:padding-box}.leaflet-popup{position:absolute;text-align:center;margin-bottom:20px}.leaflet-popup-content-wrapper{padding:1px;text-align:left;border-radius:12px}.leaflet-popup-content{margin:13px 19px;line-height:1.4}.leaflet-popup-content p{margin:18px 0}.leaflet-popup-tip-container{width:40px;height:20px;position:absolute;left:50%;margin-left:-20px;overflow:hidden;pointer-events:none}.leaflet-popup-tip{width:17px;height:17px;padding:1px;margin:-10px auto 0;-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg)}.leaflet-popup-content-wrapper,.leaflet-popup-tip{background:#fff;color:#333;-webkit-box-shadow:0 3px 14px rgba(0,0,0,.4);box-shadow:0 3px 14px rgba(0,0,0,.4)}.leaflet-container a.leaflet-popup-close-button{position:absolute;top:0;right:0;padding:4px 4px 0 0;border:none;text-align:center;width:18px;height:14px;font:700 16px/14px Tahoma,Verdana,sans-serif;color:#c3c3c3;text-decoration:none;background:0 0}.leaflet-container a.leaflet-popup-close-button:hover{color:#999}.leaflet-popup-scrolled{overflow:auto;border-bottom:1px solid #ddd;border-top:1px solid #ddd}.leaflet-oldie .leaflet-popup-content-wrapper{-ms-zoom:1}.leaflet-oldie .leaflet-popup-tip{width:24px;margin:0 auto}.leaflet-oldie .leaflet-popup-tip-container{margin-top:-1px}.leaflet-oldie .leaflet-control-layers,.leaflet-oldie .leaflet-control-zoom,.leaflet-oldie .leaflet-popup-content-wrapper,.leaflet-oldie .leaflet-popup-tip{border:1px solid #999}.leaflet-div-icon{background:#fff;border:1px solid #666}.leaflet-tooltip{position:absolute;padding:6px;background-color:#fff;border:1px solid #fff;border-radius:3px;color:#222;white-space:nowrap;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;pointer-events:none;-webkit-box-shadow:0 1px 3px rgba(0,0,0,.4);box-shadow:0 1px 3px rgba(0,0,0,.4)}.leaflet-tooltip.leaflet-clickable{cursor:pointer;pointer-events:auto}.leaflet-tooltip-bottom:before,.leaflet-tooltip-left:before,.leaflet-tooltip-right:before,.leaflet-tooltip-top:before{position:absolute;pointer-events:none;border:6px solid transparent;background:0 0;content:""}.leaflet-tooltip-bottom{margin-top:6px}.leaflet-tooltip-top{margin-top:-6px}.leaflet-tooltip-bottom:before,.leaflet-tooltip-top:before{left:50%;margin-left:-6px}.leaflet-tooltip-top:before{bottom:0;margin-bottom:-12px;border-top-color:#fff}.leaflet-tooltip-bottom:before{top:0;margin-top:-12px;margin-left:-6px;border-bottom-color:#fff}.leaflet-tooltip-left{margin-left:-6px}.leaflet-tooltip-right{margin-left:6px}.leaflet-tooltip-left:before,.leaflet-tooltip-right:before{top:50%;margin-top:-6px}.leaflet-tooltip-left:before{right:0;margin-right:-12px;border-left-color:#fff}.leaflet-tooltip-right:before{left:0;margin-left:-12px;border-right-color:#fff}.leaflet-cluster-anim .leaflet-marker-icon,.leaflet-cluster-anim .leaflet-marker-shadow{-webkit-transition:opacity .3s ease-in,-webkit-transform .3s ease-out;-o-transition:transform .3s ease-out,opacity .3s ease-in;transition:transform .3s ease-out,opacity .3s ease-in,-webkit-transform .3s ease-out}.leaflet-cluster-spider-leg{-webkit-transition:stroke-dashoffset .3s ease-out,stroke-opacity .3s ease-in;-o-transition:stroke-dashoffset .3s ease-out,stroke-opacity .3s ease-in;transition:stroke-dashoffset .3s ease-out,stroke-opacity .3s ease-in}.leaflet-marker-photo{border:2px solid #fff;-webkit-box-shadow:3px 3px 10px #888;box-shadow:3px 3px 10px #888}.leaflet-marker-photo div{width:100%;height:100%;background-size:cover;background-position:center center;background-repeat:no-repeat}.leaflet-marker-photo b{position:absolute;top:-7px;right:-11px;color:#555;background-color:#fff;border-radius:8px;height:12px;min-width:12px;line-height:12px;text-align:center;padding:3px;-webkit-box-shadow:0 3px 14px rgba(0,0,0,.4);box-shadow:0 3px 14px rgba(0,0,0,.4)} \ No newline at end of file diff --git a/public/dist/main.js b/public/dist/main.js deleted file mode 100644 index 4854567b3cc..00000000000 --- a/public/dist/main.js +++ /dev/null @@ -1,11505 +0,0 @@ -/*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */ -!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="
",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 049?function(){o(t,{timeout:n});if(n!==H.ricTimeout){n=H.ricTimeout}}:te(function(){I(t)},true);return function(e){var t;if(e=e===true){n=33}if(a){return}a=true;t=r-(f.now()-i);if(t<0){t=0}if(e||t<9){s()}else{I(s,t)}}},ie=function(e){var t,a;var i=99;var r=function(){t=null;e()};var n=function(){var e=f.now()-a;if(e0;if(r&&Z(i,"overflow")!="visible"){a=i.getBoundingClientRect();r=C>a.left&&pa.top-1&&g500&&O.clientWidth>500?500:370:H.expand;k._defEx=u;f=u*H.expFactor;c=H.hFac;A=null;if(w2&&h>2&&!D.hidden){w=f;N=0}else if(h>1&&N>1&&M<6){w=u}else{w=_}}if(l!==n){y=innerWidth+n*c;z=innerHeight+n;s=n*-1;l=n}a=d[t].getBoundingClientRect();if((b=a.bottom)>=s&&(g=a.top)<=z&&(C=a.right)>=s*c&&(p=a.left)<=y&&(b||C||p||g)&&(H.loadHidden||x(d[t]))&&(m&&M<3&&!o&&(h<3||N<4)||W(d[t],n))){R(d[t]);r=true;if(M>9){break}}else if(!r&&m&&!i&&M<4&&N<4&&h>2&&(v[0]||H.preloadAfterLoad)&&(v[0]||!o&&(b||C||p||g||d[t][$](H.sizesAttr)!="auto"))){i=v[0]||d[t]}}if(i&&!r){R(i)}}};var a=ae(t);var S=function(e){var t=e.target;if(t._lazyCache){delete t._lazyCache;return}L(e);K(t,H.loadedClass);Q(t,H.loadingClass);V(t,B);X(t,"lazyloaded")};var i=te(S);var B=function(e){i({target:e.target})};var T=function(e,t){var a=e.getAttribute("data-load-mode")||H.iframeLoadMode;if(a==0){e.contentWindow.location.replace(t)}else if(a==1){e.src=t}};var F=function(e){var t;var a=e[$](H.srcsetAttr);if(t=H.customMedia[e[$]("data-media")||e[$]("media")]){e.setAttribute("media",t)}if(a){e.setAttribute("srcset",a)}};var s=te(function(t,e,a,i,r){var n,s,o,l,u,f;if(!(u=X(t,"lazybeforeunveil",e)).defaultPrevented){if(i){if(a){K(t,H.autosizesClass)}else{t.setAttribute("sizes",i)}}s=t[$](H.srcsetAttr);n=t[$](H.srcAttr);if(r){o=t.parentNode;l=o&&j.test(o.nodeName||"")}f=e.firesLoad||"src"in t&&(s||n||l);u={target:t};K(t,H.loadingClass);if(f){clearTimeout(c);c=I(L,2500);V(t,B,true)}if(l){G.call(o.getElementsByTagName("source"),F)}if(s){t.setAttribute("srcset",s)}else if(n&&!l){if(d.test(t.nodeName)){T(t,n)}else{t.src=n}}if(r&&(s||l)){Y(t,{src:n})}}if(t._lazyRace){delete t._lazyRace}Q(t,H.lazyClass);ee(function(){var e=t.complete&&t.naturalWidth>1;if(!f||e){if(e){K(t,H.fastLoadedClass)}S(u);t._lazyCache=true;I(function(){if("_lazyCache"in t){delete t._lazyCache}},9)}if(t.loading=="lazy"){M--}},true)});var R=function(e){if(e._lazyRace){return}var t;var a=n.test(e.nodeName);var i=a&&(e[$](H.sizesAttr)||e[$]("sizes"));var r=i=="auto";if((r||!m)&&a&&(e[$]("src")||e.srcset)&&!e.complete&&!J(e,H.errorClass)&&J(e,H.lazyClass)){return}t=X(e,"lazyunveilread").detail;if(r){re.updateElem(e,true,e.offsetWidth)}e._lazyRace=true;M++;s(e,t,r,i,a)};var r=ie(function(){H.loadMode=3;a()});var o=function(){if(H.loadMode==3){H.loadMode=2}r()};var l=function(){if(m){return}if(f.now()-e<999){I(l,999);return}m=true;H.loadMode=3;a();q("scroll",o,true)};return{_:function(){e=f.now();k.elements=D.getElementsByClassName(H.lazyClass);v=D.getElementsByClassName(H.lazyClass+" "+H.preloadClass);q("scroll",a,true);q("resize",a,true);q("pageshow",function(e){if(e.persisted){var t=D.querySelectorAll("."+H.loadingClass);if(t.length&&t.forEach){U(function(){t.forEach(function(e){if(e.complete){R(e)}})})}}});if(u.MutationObserver){new MutationObserver(a).observe(O,{childList:true,subtree:true,attributes:true})}else{O[P]("DOMNodeInserted",a,true);O[P]("DOMAttrModified",a,true);setInterval(a,999)}q("hashchange",a,true);["focus","mouseover","click","load","transitionend","animationend"].forEach(function(e){D[P](e,a,true)});if(/d$|^c/.test(D.readyState)){l()}else{q("load",l);D[P]("DOMContentLoaded",a);I(l,2e4)}if(k.elements.length){t();ee._lsFlush()}else{a()}},checkElems:a,unveil:R,_aLSL:o}}(),re=function(){var a;var n=te(function(e,t,a,i){var r,n,s;e._lazysizesWidth=i;i+="px";e.setAttribute("sizes",i);if(j.test(t.nodeName||"")){r=t.getElementsByTagName("source");for(n=0,s=r.length;nc||n.hasOwnProperty(c)&&(p[n[c]]=c)}g=p[e]?"keydown":"keypress"}"keypress"==g&&d.length&&(g="keydown");return{key:m,modifiers:d,action:g}}function D(a,b){return null===a||a===u?!1:a===b?!0:D(a.parentNode,b)}function d(a){function b(a){a= -a||{};var b=!1,l;for(l in p)a[l]?b=!0:p[l]=0;b||(x=!1)}function g(a,b,t,f,g,d){var l,E=[],h=t.type;if(!k._callbacks[a])return[];"keyup"==h&&w(a)&&(b=[a]);for(l=0;l":".","?":"/","|":"\\"},B={option:"alt",command:"meta","return":"enter", -escape:"esc",plus:"+",mod:/Mac|iPod|iPhone|iPad/.test(navigator.platform)?"meta":"ctrl"},p;for(c=1;20>c;++c)n[111+c]="f"+c;for(c=0;9>=c;++c)n[c+96]=c.toString();d.prototype.bind=function(a,b,c){a=a instanceof Array?a:[a];this._bindMultiple.call(this,a,b,c);return this};d.prototype.unbind=function(a,b){return this.bind.call(this,a,function(){},b)};d.prototype.trigger=function(a,b){if(this._directMap[a+":"+b])this._directMap[a+":"+b]({},a);return this};d.prototype.reset=function(){this._callbacks={}; -this._directMap={};return this};d.prototype.stopCallback=function(a,b){if(-1<(" "+b.className+" ").indexOf(" mousetrap ")||D(b,this.target))return!1;if("composedPath"in a&&"function"===typeof a.composedPath){var c=a.composedPath()[0];c!==a.target&&(b=c)}return"INPUT"==b.tagName||"SELECT"==b.tagName||"TEXTAREA"==b.tagName||b.isContentEditable};d.prototype.handleKey=function(){return this._handleKey.apply(this,arguments)};d.addKeycodes=function(a){for(var b in a)a.hasOwnProperty(b)&&(n[b]=a[b]);p=null}; -d.init=function(){var a=d(u),b;for(b in a)"_"!==b.charAt(0)&&(d[b]=function(b){return function(){return a[b].apply(a,arguments)}}(b))};d.init();q.Mousetrap=d;"undefined"!==typeof module&&module.exports&&(module.exports=d);"function"===typeof define&&define.amd&&define(function(){return d})}})("undefined"!==typeof window?window:null,"undefined"!==typeof window?document:null); - -(function(a){var c={},d=a.prototype.stopCallback;a.prototype.stopCallback=function(e,b,a,f){return this.paused?!0:c[a]||c[f]?!1:d.call(this,e,b,a)};a.prototype.bindGlobal=function(a,b,d){this.bind(a,b,d);if(a instanceof Array)for(b=0;b0&&void 0!==arguments[0]?arguments[0]:"";return!0===(arguments.length>1&&void 0!==arguments[1]&&arguments[1])?document.querySelectorAll(".basicModal "+t):document.querySelector(".basicModal "+t)}),a=function(t,n){return null!=t&&(t.constructor===Object?Array.prototype.forEach.call(Object.keys(t),function(e){return n(t[e],e,t)}):Array.prototype.forEach.call(t,function(e,l){return n(e,l,t)}))},c=function(t){return null==t||0===Object.keys(t).length?(console.error("Missing or empty modal configuration object"),!1):(null==t.body&&(t.body=""),null==t.class&&(t.class=""),!1!==t.closable&&(t.closable=!0),null==t.buttons?(console.error("basicModal requires at least one button"),!1):null!=t.buttons.action&&(null==t.buttons.action.class&&(t.buttons.action.class=""),null==t.buttons.action.title&&(t.buttons.action.title="OK"),null==t.buttons.action.fn)?(console.error("Missing fn for action-button"),!1):null==t.buttons.cancel||(null==t.buttons.cancel.class&&(t.buttons.cancel.class=""),null==t.buttons.cancel.title&&(t.buttons.cancel.title="Cancel"),null!=t.buttons.cancel.fn)||(console.error("Missing fn for cancel-button"),!1))},s=function(t){var n="";if(n+="\n\t
\n\t
\n\t
\n\t "+t.body+"\n\t
\n\t
\n\t ",null!=t.buttons.cancel){var e="";null!=t.buttons.cancel.attributes&&t.buttons.cancel.attributes.forEach(function(t,n){e+=t[0]+"='"+t[1]+"' "}),-1===t.buttons.cancel.class.indexOf("basicModal__xclose")?n+=""+t.buttons.cancel.title+"":n+="
'}if(null!=t.buttons.action){var l="";null!=t.buttons.action.attributes&&t.buttons.action.attributes.forEach(function(t,n){l+=t[0]+"='"+t[1]+"' "}),n+=""+t.buttons.action.title+""}return n+="\n\t
\n\t
\n\t
\n\t "},i=e.getValues=function(){var t={},n=o("input[name]",!0),e=o("select[name]",!0);return a(n,function(n){var e=n.getAttribute("name"),l=n.value;t[e]=l}),a(e,function(n){var e=n.getAttribute("name"),l=n.options[n.selectedIndex].value;t[e]=l}),0===Object.keys(t).length?null:t},u=function(t){return null!=t.buttons.cancel&&(o("#basicModal__cancel").onclick=function(){if(!0===this.classList.contains("basicModal__button--active"))return!1;this.classList.add("basicModal__button--active"),t.buttons.cancel.fn()}),null!=t.buttons.action&&(o("#basicModal__action").onclick=function(){if(!0===this.classList.contains("basicModal__button--active"))return!1;this.classList.add("basicModal__button--active"),t.buttons.action.fn(i())}),a(o("input",!0),function(t){t.oninput=t.onblur=function(){this.classList.remove("error")}}),a(o("select",!0),function(t){t.onchange=t.onblur=function(){this.classList.remove("error")}}),!0},r=(e.show=function t(n){if(!1===c(n))return!1;if(null!=o())return d(!0),setTimeout(function(){return t(n)},301),!1;l=document.activeElement;var e=s(n);document.body.insertAdjacentHTML("beforeend",e),u(n);var a=o("input");null!=a&&a.select();var i=o("select");null==a&&null!=i&&i.focus();var r=o("#basicModal__action");null==a&&null==i&&null!=r&&r.focus();var b=o("#basicModal__cancel");return null==a&&null==i&&null==r&&null!=b&&b.focus(),null!=n.callback&&n.callback(n),!0},e.error=function(t){b();var n=o("input[name='"+t+"']")||o("select[name='"+t+"']");if(null==n)return!1;n.classList.add("error"),"function"==typeof n.select?n.select():n.focus(),o().classList.remove("basicModal--fadeIn","basicModal--shake"),setTimeout(function(){return o().classList.add("basicModal--shake")},1)},e.visible=function(){return null!=o()}),b=(e.action=function(){var t=o("#basicModal__action");return null!=t&&(t.click(),!0)},e.cancel=function(){var t=o("#basicModal__cancel");return null!=t&&(t.click(),!0)},e.reset=function(){var t=o(".basicModal__button",!0);a(t,function(t){return t.classList.remove("basicModal__button--active")});var n=o("input",!0);a(n,function(t){return t.classList.remove("error")});var e=o("select",!0);return a(e,function(t){return t.classList.remove("error")}),!0}),d=e.close=function(){var t=arguments.length>0&&void 0!==arguments[0]&&arguments[0];if(!1===r())return!1;var n=o().parentElement;return("false"!==n.getAttribute("data-closable")||!1!==t)&&(n.classList.remove("basicModalContainer--fadeIn"),n.classList.add("basicModalContainer--fadeOut"),setTimeout(function(){return null!=n&&(null!=n.parentElement&&void n.parentElement.removeChild(n))},300),null!=l&&(l.focus(),l=null),!0)}},{}]},{},[1])(1)}); -!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.scrollLock=t():e.scrollLock=t()}(this,function(){return function(l){var r={};function o(e){if(r[e])return r[e].exports;var t=r[e]={i:e,l:!1,exports:{}};return l[e].call(t.exports,t,t.exports,o),t.l=!0,t.exports}return o.m=l,o.c=r,o.d=function(e,t,l){o.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:l})},o.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},o.t=function(t,e){if(1&e&&(t=o(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var l=Object.create(null);if(o.r(l),Object.defineProperty(l,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var r in t)o.d(l,r,function(e){return t[e]}.bind(null,r));return l},o.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return o.d(t,"a",t),t},o.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},o.p="",o(o.s=0)}([function(e,t,l){"use strict";l.r(t);var r=function(e){return Array.isArray(e)?e:[e]},a=function(e){return e instanceof Node},o=function(e,t){if(e&&t){e=e instanceof NodeList?e:[e];for(var l=0;ls(e).data("position")?1:-1},right:function(t,e){return s(t).data("position")>s(e).data("position")?1:-1}}),o.$left.attachIndex(),o.$right.each(function(t,e){s(e).attachIndex()})),"function"==typeof o.callbacks.startUp&&o.callbacks.startUp(o.$left,o.$right),o.skipInitSort||("function"==typeof o.callbacks.sort.left&&o.$left.mSort(o.callbacks.sort.left),"function"==typeof o.callbacks.sort.right&&o.$right.each(function(t,e){s(e).mSort(o.callbacks.sort.right)})),o.options.search&&o.options.search.left&&(o.options.search.$left=s(o.options.search.left),o.$left.before(o.options.search.$left)),o.options.search&&o.options.search.right&&(o.options.search.$right=s(o.options.search.right),o.$right.before(s(o.options.search.$right))),o.events(),"function"==typeof o.callbacks.afterInit&&o.callbacks.afterInit()},events:function(){var o=this;o.options.search&&o.options.search.$left&&o.options.search.$left.on("keyup",function(t){o.callbacks.fireSearch(this.value)?(o.$left.find('option:search("'+this.value+'")').mShow(),o.$left.find('option:not(:search("'+this.value+'"))').mHide(),o.$left.find("option").closest("optgroup").mHide(),o.$left.find("option:not(.hidden)").parent("optgroup").mShow()):o.$left.find("option, optgroup").mShow()}),o.options.search&&o.options.search.$right&&o.options.search.$right.on("keyup",function(t){o.callbacks.fireSearch(this.value)?(o.$right.find('option:search("'+this.value+'")').mShow(),o.$right.find('option:not(:search("'+this.value+'"))').mHide(),o.$right.find("option").closest("optgroup").mHide(),o.$right.find("option:not(.hidden)").parent("optgroup").mShow()):o.$right.find("option, optgroup").mShow()}),o.$right.closest("form").on("submit",function(t){o.options.search&&(o.options.search.$left&&o.options.search.$left.val("").trigger("keyup"),o.options.search.$right&&o.options.search.$right.val("").trigger("keyup")),o.$left.find("option").prop("selected",o.options.submitAllLeft),o.$right.find("option").prop("selected",o.options.submitAllRight)}),o.$left.on("dblclick","option",function(t){t.preventDefault();var e=o.$left.find("option:selected:not(.hidden)");e.length&&o.moveToRight(e,t)}),o.$left.on("click","optgroup",function(t){"OPTGROUP"==s(t.target).prop("tagName")&&s(this).children().prop("selected",!0)}),o.$left.on("keypress",function(t){var e;13===t.keyCode&&(t.preventDefault(),(e=o.$left.find("option:selected:not(.hidden)")).length&&o.moveToRight(e,t))}),o.$right.on("dblclick","option",function(t){t.preventDefault();var e=o.$right.find("option:selected:not(.hidden)");e.length&&o.moveToLeft(e,t)}),o.$right.on("click","optgroup",function(t){"OPTGROUP"==s(t.target).prop("tagName")&&s(this).children().prop("selected",!0)}),o.$right.on("keydown",function(t){var e;8!==t.keyCode&&46!==t.keyCode||(t.preventDefault(),(e=o.$right.find("option:selected:not(.hidden)")).length&&o.moveToLeft(e,t))}),(navigator.userAgent.match(/MSIE/i)||0e.innerHTML?1:-1},fireSearch:function(t){return 1").hide()}),l&&this.prop("disabled",!0),this},i.fn.mSort=function(o){return this.children().sort(o).appendTo(this),this.find("optgroup").each(function(t,e){i(e).children().sort(o).appendTo(e)}),this},i.fn.attachIndex=function(){this.children().each(function(t,e){var o=i(e);o.is("optgroup")&&o.children().each(function(t,e){i(e).data("position",t)}),o.data("position",t)})},i.expr[":"].search=function(t,e,o){var n=new RegExp(o[3].replace(/([^a-zA-Z0-9])/g,"\\$1"),"i");return i(t).text().match(n)}}); -require=function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o=1){this.items.push(itemData);this.completeLayout(rowWidthWithoutSpacing/itemData.aspectRatio,"justify");return true}}}if(newAspectRatiothis.maxAspectRatio){if(this.items.length===0){this.items.push(merge(itemData));this.completeLayout(rowWidthWithoutSpacing/newAspectRatio,"justify");return true}previousRowWidthWithoutSpacing=this.width-(this.items.length-1)*this.spacing;previousAspectRatio=this.items.reduce(function(sum,item){return sum+item.aspectRatio},0);previousTargetAspectRatio=previousRowWidthWithoutSpacing/this.targetRowHeight;if(Math.abs(newAspectRatio-targetAspectRatio)>Math.abs(previousAspectRatio-previousTargetAspectRatio)){this.completeLayout(previousRowWidthWithoutSpacing/previousAspectRatio,"justify");return false}else{this.items.push(merge(itemData));this.completeLayout(rowWidthWithoutSpacing/newAspectRatio,"justify");return true}}else{this.items.push(merge(itemData));this.completeLayout(rowWidthWithoutSpacing/newAspectRatio,"justify");return true}},isLayoutComplete:function(){return this.height>0},completeLayout:function(newHeight,widowLayoutStyle){var itemWidthSum=this.left,rowWidthWithoutSpacing=this.width-(this.items.length-1)*this.spacing,clampedToNativeRatio,clampedHeight,errorWidthPerItem,roundedCumulativeErrors,singleItemGeometry,centerOffset;if(typeof widowLayoutStyle==="undefined"||["justify","center","left"].indexOf(widowLayoutStyle)<0){widowLayoutStyle="left"}clampedHeight=Math.max(this.edgeCaseMinRowHeight,Math.min(newHeight,this.edgeCaseMaxRowHeight));if(newHeight!==clampedHeight){this.height=clampedHeight;clampedToNativeRatio=rowWidthWithoutSpacing/clampedHeight/(rowWidthWithoutSpacing/newHeight)}else{this.height=newHeight;clampedToNativeRatio=1}this.items.forEach(function(item){item.top=this.top;item.width=item.aspectRatio*this.height*clampedToNativeRatio;item.height=this.height;item.left=itemWidthSum;itemWidthSum+=item.width+this.spacing},this);if(widowLayoutStyle==="justify"){itemWidthSum-=this.spacing+this.left;errorWidthPerItem=(itemWidthSum-this.width)/this.items.length;roundedCumulativeErrors=this.items.map(function(item,i){return Math.round((i+1)*errorWidthPerItem)});if(this.items.length===1){singleItemGeometry=this.items[0];singleItemGeometry.width-=Math.round(errorWidthPerItem)}else{this.items.forEach(function(item,i){if(i>0){item.left-=roundedCumulativeErrors[i-1];item.width-=roundedCumulativeErrors[i]-roundedCumulativeErrors[i-1]}else{item.width-=roundedCumulativeErrors[i]}})}}else if(widowLayoutStyle==="center"){centerOffset=(this.width-itemWidthSum)/2;this.items.forEach(function(item){item.left+=centerOffset+this.spacing},this)}},forceComplete:function(fitToWidth,rowHeight){if(typeof rowHeight==="number"){this.completeLayout(rowHeight,this.widowLayoutStyle)}else{this.completeLayout(this.targetRowHeight,this.widowLayoutStyle)}},getItems:function(){return this.items}}},{merge:2}],2:[function(require,module,exports){(function(isNode){var Public=function(clone){return merge(clone===true,false,arguments)},publicName="merge";Public.recursive=function(clone){return merge(clone===true,true,arguments)};Public.clone=function(input){var output=input,type=typeOf(input),index,size;if(type==="array"){output=[];size=input.length;for(index=0;index=layoutConfig.maxNumRows){currentRow=null;return true}currentRow=createNewRow(layoutConfig,layoutData);if(!itemAdded){itemAdded=currentRow.addItem(itemData);if(currentRow.isLayoutComplete()){laidOutItems=laidOutItems.concat(addRow(layoutConfig,layoutData,currentRow));if(layoutData._rows.length>=layoutConfig.maxNumRows){currentRow=null;return true}currentRow=createNewRow(layoutConfig,layoutData)}}}});if(currentRow&¤tRow.getItems().length&&layoutConfig.showWidows){if(layoutData._rows.length){if(layoutData._rows[layoutData._rows.length-1].isBreakoutRow){nextToLastRowHeight=layoutData._rows[layoutData._rows.length-1].targetRowHeight}else{nextToLastRowHeight=layoutData._rows[layoutData._rows.length-1].height}currentRow.forceComplete(false,nextToLastRowHeight)}else{currentRow.forceComplete(false)}laidOutItems=laidOutItems.concat(addRow(layoutConfig,layoutData,currentRow));layoutConfig._widowCount=currentRow.getItems().length}layoutData._containerHeight=layoutData._containerHeight-layoutConfig.boxSpacing.vertical;layoutData._containerHeight=layoutData._containerHeight+layoutConfig.containerPadding.bottom;return{containerHeight:layoutData._containerHeight,widowCount:layoutConfig._widowCount,boxes:layoutData._layoutItems}}module.exports=function(input,config){var layoutConfig={};var layoutData={};var defaults={containerWidth:1060,containerPadding:10,boxSpacing:10,targetRowHeight:320,targetRowHeightTolerance:.25,maxNumRows:Number.POSITIVE_INFINITY,forceAspectRatio:false,showWidows:true,fullWidthBreakoutRowCadence:false,widowLayoutStyle:"left"};var containerPadding={};var boxSpacing={};config=config||{};layoutConfig=merge(defaults,config);containerPadding.top=!isNaN(parseFloat(layoutConfig.containerPadding.top))?layoutConfig.containerPadding.top:layoutConfig.containerPadding;containerPadding.right=!isNaN(parseFloat(layoutConfig.containerPadding.right))?layoutConfig.containerPadding.right:layoutConfig.containerPadding;containerPadding.bottom=!isNaN(parseFloat(layoutConfig.containerPadding.bottom))?layoutConfig.containerPadding.bottom:layoutConfig.containerPadding;containerPadding.left=!isNaN(parseFloat(layoutConfig.containerPadding.left))?layoutConfig.containerPadding.left:layoutConfig.containerPadding;boxSpacing.horizontal=!isNaN(parseFloat(layoutConfig.boxSpacing.horizontal))?layoutConfig.boxSpacing.horizontal:layoutConfig.boxSpacing;boxSpacing.vertical=!isNaN(parseFloat(layoutConfig.boxSpacing.vertical))?layoutConfig.boxSpacing.vertical:layoutConfig.boxSpacing;layoutConfig.containerPadding=containerPadding;layoutConfig.boxSpacing=boxSpacing;layoutData._layoutItems=[];layoutData._awakeItems=[];layoutData._inViewportItems=[];layoutData._leadingOrphans=[];layoutData._trailingOrphans=[];layoutData._containerHeight=layoutConfig.containerPadding.top;layoutData._rows=[];layoutData._orphans=[];layoutConfig._widowCount=0;return computeLayout(layoutConfig,layoutData,input.map(function(item){if(item.width&&item.height){return{aspectRatio:item.width/item.height}}else{return{aspectRatio:item}}}))}},{"./row":1,merge:2}]},{},[]); -/* @preserve - * Leaflet 1.7.1, a JS library for interactive maps. http://leafletjs.com - * (c) 2010-2019 Vladimir Agafonkin, (c) 2010-2011 CloudMade - */ -!function(t,i){"object"==typeof exports&&"undefined"!=typeof module?i(exports):"function"==typeof define&&define.amd?define(["exports"],i):i(t.L={})}(this,function(t){"use strict";function h(t){for(var i,e,n=1,o=arguments.length;n=this.min.x&&e.x<=this.max.x&&i.y>=this.min.y&&e.y<=this.max.y},intersects:function(t){t=O(t);var i=this.min,e=this.max,n=t.min,o=t.max,s=o.x>=i.x&&n.x<=e.x,r=o.y>=i.y&&n.y<=e.y;return s&&r},overlaps:function(t){t=O(t);var i=this.min,e=this.max,n=t.min,o=t.max,s=o.x>i.x&&n.xi.y&&n.y=n.lat&&e.lat<=o.lat&&i.lng>=n.lng&&e.lng<=o.lng},intersects:function(t){t=N(t);var i=this._southWest,e=this._northEast,n=t.getSouthWest(),o=t.getNorthEast(),s=o.lat>=i.lat&&n.lat<=e.lat,r=o.lng>=i.lng&&n.lng<=e.lng;return s&&r},overlaps:function(t){t=N(t);var i=this._southWest,e=this._northEast,n=t.getSouthWest(),o=t.getNorthEast(),s=o.lat>i.lat&&n.lati.lng&&n.lng';var i=t.firstChild;return i.style.behavior="url(#default#VML)",i&&"object"==typeof i.adj}catch(t){return!1}}();function kt(t){return 0<=navigator.userAgent.toLowerCase().indexOf(t)}var Bt={ie:tt,ielt9:it,edge:et,webkit:nt,android:ot,android23:st,androidStock:at,opera:ht,chrome:ut,gecko:lt,safari:ct,phantom:_t,opera12:dt,win:pt,ie3d:mt,webkit3d:ft,gecko3d:gt,any3d:vt,mobile:yt,mobileWebkit:xt,mobileWebkit3d:wt,msPointer:Pt,pointer:Lt,touch:bt,mobileOpera:Tt,mobileGecko:Mt,retina:zt,passiveEvents:Ct,canvas:St,svg:Zt,vml:Et},At=Pt?"MSPointerDown":"pointerdown",It=Pt?"MSPointerMove":"pointermove",Ot=Pt?"MSPointerUp":"pointerup",Rt=Pt?"MSPointerCancel":"pointercancel",Nt={},Dt=!1;function jt(t,i,e,n){function o(t){Ut(t,r)}var s,r,a,h,u,l,c,_;function d(t){t.pointerType===(t.MSPOINTER_TYPE_MOUSE||"mouse")&&0===t.buttons||Ut(t,h)}return"touchstart"===i?(u=t,l=e,c=n,_=p(function(t){t.MSPOINTER_TYPE_TOUCH&&t.pointerType===t.MSPOINTER_TYPE_TOUCH&&Ri(t),Ut(t,l)}),u["_leaflet_touchstart"+c]=_,u.addEventListener(At,_,!1),Dt||(document.addEventListener(At,Wt,!0),document.addEventListener(It,Ht,!0),document.addEventListener(Ot,Ft,!0),document.addEventListener(Rt,Ft,!0),Dt=!0)):"touchmove"===i?(h=e,(a=t)["_leaflet_touchmove"+n]=d,a.addEventListener(It,d,!1)):"touchend"===i&&(r=e,(s=t)["_leaflet_touchend"+n]=o,s.addEventListener(Ot,o,!1),s.addEventListener(Rt,o,!1)),this}function Wt(t){Nt[t.pointerId]=t}function Ht(t){Nt[t.pointerId]&&(Nt[t.pointerId]=t)}function Ft(t){delete Nt[t.pointerId]}function Ut(t,i){for(var e in t.touches=[],Nt)t.touches.push(Nt[e]);t.changedTouches=[t],i(t)}var Vt=Pt?"MSPointerDown":Lt?"pointerdown":"touchstart",qt=Pt?"MSPointerUp":Lt?"pointerup":"touchend",Gt="_leaflet_";var Kt,Yt,Xt,Jt,$t,Qt,ti=fi(["transform","webkitTransform","OTransform","MozTransform","msTransform"]),ii=fi(["webkitTransition","transition","OTransition","MozTransition","msTransition"]),ei="webkitTransition"===ii||"OTransition"===ii?ii+"End":"transitionend";function ni(t){return"string"==typeof t?document.getElementById(t):t}function oi(t,i){var e,n=t.style[i]||t.currentStyle&&t.currentStyle[i];return n&&"auto"!==n||!document.defaultView||(n=(e=document.defaultView.getComputedStyle(t,null))?e[i]:null),"auto"===n?null:n}function si(t,i,e){var n=document.createElement(t);return n.className=i||"",e&&e.appendChild(n),n}function ri(t){var i=t.parentNode;i&&i.removeChild(t)}function ai(t){for(;t.firstChild;)t.removeChild(t.firstChild)}function hi(t){var i=t.parentNode;i&&i.lastChild!==t&&i.appendChild(t)}function ui(t){var i=t.parentNode;i&&i.firstChild!==t&&i.insertBefore(t,i.firstChild)}function li(t,i){if(void 0!==t.classList)return t.classList.contains(i);var e=pi(t);return 0this.options.maxZoom)?this.setZoom(t):this},panInsideBounds:function(t,i){this._enforcingBounds=!0;var e=this.getCenter(),n=this._limitCenter(e,this._zoom,N(t));return e.equals(n)||this.panTo(n,i),this._enforcingBounds=!1,this},panInside:function(t,i){var e,n,o=A((i=i||{}).paddingTopLeft||i.padding||[0,0]),s=A(i.paddingBottomRight||i.padding||[0,0]),r=this.getCenter(),a=this.project(r),h=this.project(t),u=this.getPixelBounds(),l=u.getSize().divideBy(2),c=O([u.min.add(o),u.max.subtract(s)]);return c.contains(h)||(this._enforcingBounds=!0,e=a.subtract(h),n=A(h.x+e.x,h.y+e.y),(h.xc.max.x)&&(n.x=a.x-e.x,0c.max.y)&&(n.y=a.y-e.y,0=this.options.transform3DLimit&&this._resetView(this.getCenter(),this.getZoom())},_findEventTargets:function(t,i){for(var e,n=[],o="mouseout"===i||"mouseover"===i,s=t.target||t.srcElement,r=!1;s;){if((e=this._targets[m(s)])&&("click"===i||"preclick"===i)&&!t._simulated&&this._draggableMoved(e)){r=!0;break}if(e&&e.listens(i,!0)){if(o&&!Vi(s,t))break;if(n.push(e),o)break}if(s===this._container)break;s=s.parentNode}return n.length||r||o||!Vi(s,t)||(n=[this]),n},_handleDOMEvent:function(t){var i;this._loaded&&!Ui(t)&&("mousedown"!==(i=t.type)&&"keypress"!==i&&"keyup"!==i&&"keydown"!==i||Pi(t.target||t.srcElement),this._fireDOMEvent(t,i))},_mouseEvents:["click","dblclick","mouseover","mouseout","contextmenu"],_fireDOMEvent:function(t,i,e){var n;if("click"===t.type&&((n=h({},t)).type="preclick",this._fireDOMEvent(n,n.type,e)),!t._stopped&&(e=(e||[]).concat(this._findEventTargets(t,i))).length){var o=e[0];"contextmenu"===i&&o.listens(i,!0)&&Ri(t);var s,r={originalEvent:t};"keypress"!==t.type&&"keydown"!==t.type&&"keyup"!==t.type&&(s=o.getLatLng&&(!o._radius||o._radius<=10),r.containerPoint=s?this.latLngToContainerPoint(o.getLatLng()):this.mouseEventToContainerPoint(t),r.layerPoint=this.containerPointToLayerPoint(r.containerPoint),r.latlng=s?o.getLatLng():this.layerPointToLatLng(r.layerPoint));for(var a=0;athis.options.zoomAnimationThreshold)return!1;var n=this.getZoomScale(i),o=this._getCenterOffset(t)._divideBy(1-1/n);return!(!0!==e.animate&&!this.getSize().contains(o))&&(M(function(){this._moveStart(!0,!1)._animateZoom(t,i,!0)},this),!0)},_animateZoom:function(t,i,e,n){this._mapPane&&(e&&(this._animatingZoom=!0,this._animateToCenter=t,this._animateToZoom=i,ci(this._mapPane,"leaflet-zoom-anim")),this.fire("zoomanim",{center:t,zoom:i,noUpdate:n}),setTimeout(p(this._onZoomTransitionEnd,this),250))},_onZoomTransitionEnd:function(){this._animatingZoom&&(this._mapPane&&_i(this._mapPane,"leaflet-zoom-anim"),this._animatingZoom=!1,this._move(this._animateToCenter,this._animateToZoom),M(function(){this._moveEnd(!0)},this))}});function Yi(t){return new Xi(t)}var Xi=S.extend({options:{position:"topright"},initialize:function(t){c(this,t)},getPosition:function(){return this.options.position},setPosition:function(t){var i=this._map;return i&&i.removeControl(this),this.options.position=t,i&&i.addControl(this),this},getContainer:function(){return this._container},addTo:function(t){this.remove(),this._map=t;var i=this._container=this.onAdd(t),e=this.getPosition(),n=t._controlCorners[e];return ci(i,"leaflet-control"),-1!==e.indexOf("bottom")?n.insertBefore(i,n.firstChild):n.appendChild(i),this._map.on("unload",this.remove,this),this},remove:function(){return this._map&&(ri(this._container),this.onRemove&&this.onRemove(this._map),this._map.off("unload",this.remove,this),this._map=null),this},_refocusOnMap:function(t){this._map&&t&&0",n=document.createElement("div");return n.innerHTML=e,n.firstChild},_addItem:function(t){var i,e=document.createElement("label"),n=this._map.hasLayer(t.layer);t.overlay?((i=document.createElement("input")).type="checkbox",i.className="leaflet-control-layers-selector",i.defaultChecked=n):i=this._createRadioElement("leaflet-base-layers_"+m(this),n),this._layerControlInputs.push(i),i.layerId=m(t.layer),zi(i,"click",this._onInputClick,this);var o=document.createElement("span");o.innerHTML=" "+t.name;var s=document.createElement("div");return e.appendChild(s),s.appendChild(i),s.appendChild(o),(t.overlay?this._overlaysList:this._baseLayersList).appendChild(e),this._checkDisabledLayers(),e},_onInputClick:function(){var t,i,e=this._layerControlInputs,n=[],o=[];this._handlingClick=!0;for(var s=e.length-1;0<=s;s--)t=e[s],i=this._getLayer(t.layerId).layer,t.checked?n.push(i):t.checked||o.push(i);for(s=0;si.options.maxZoom},_expandIfNotCollapsed:function(){return this._map&&!this.options.collapsed&&this.expand(),this},_expand:function(){return this.expand()},_collapse:function(){return this.collapse()}}),$i=Xi.extend({options:{position:"topleft",zoomInText:"+",zoomInTitle:"Zoom in",zoomOutText:"−",zoomOutTitle:"Zoom out"},onAdd:function(t){var i="leaflet-control-zoom",e=si("div",i+" leaflet-bar"),n=this.options;return this._zoomInButton=this._createButton(n.zoomInText,n.zoomInTitle,i+"-in",e,this._zoomIn),this._zoomOutButton=this._createButton(n.zoomOutText,n.zoomOutTitle,i+"-out",e,this._zoomOut),this._updateDisabled(),t.on("zoomend zoomlevelschange",this._updateDisabled,this),e},onRemove:function(t){t.off("zoomend zoomlevelschange",this._updateDisabled,this)},disable:function(){return this._disabled=!0,this._updateDisabled(),this},enable:function(){return this._disabled=!1,this._updateDisabled(),this},_zoomIn:function(t){!this._disabled&&this._map._zoomthis._map.getMinZoom()&&this._map.zoomOut(this._map.options.zoomDelta*(t.shiftKey?3:1))},_createButton:function(t,i,e,n,o){var s=si("a",e,n);return s.innerHTML=t,s.href="#",s.title=i,s.setAttribute("role","button"),s.setAttribute("aria-label",i),Oi(s),zi(s,"click",Ni),zi(s,"click",o,this),zi(s,"click",this._refocusOnMap,this),s},_updateDisabled:function(){var t=this._map,i="leaflet-disabled";_i(this._zoomInButton,i),_i(this._zoomOutButton,i),!this._disabled&&t._zoom!==t.getMinZoom()||ci(this._zoomOutButton,i),!this._disabled&&t._zoom!==t.getMaxZoom()||ci(this._zoomInButton,i)}});Ki.mergeOptions({zoomControl:!0}),Ki.addInitHook(function(){this.options.zoomControl&&(this.zoomControl=new $i,this.addControl(this.zoomControl))});var Qi=Xi.extend({options:{position:"bottomleft",maxWidth:100,metric:!0,imperial:!0},onAdd:function(t){var i="leaflet-control-scale",e=si("div",i),n=this.options;return this._addScales(n,i+"-line",e),t.on(n.updateWhenIdle?"moveend":"move",this._update,this),t.whenReady(this._update,this),e},onRemove:function(t){t.off(this.options.updateWhenIdle?"moveend":"move",this._update,this)},_addScales:function(t,i,e){t.metric&&(this._mScale=si("div",i,e)),t.imperial&&(this._iScale=si("div",i,e))},_update:function(){var t=this._map,i=t.getSize().y/2,e=t.distance(t.containerPointToLatLng([0,i]),t.containerPointToLatLng([this.options.maxWidth,i]));this._updateScales(e)},_updateScales:function(t){this.options.metric&&t&&this._updateMetric(t),this.options.imperial&&t&&this._updateImperial(t)},_updateMetric:function(t){var i=this._getRoundNum(t),e=i<1e3?i+" m":i/1e3+" km";this._updateScale(this._mScale,e,i/t)},_updateImperial:function(t){var i,e,n,o=3.2808399*t;5280Leaflet'},initialize:function(t){c(this,t),this._attributions={}},onAdd:function(t){for(var i in(t.attributionControl=this)._container=si("div","leaflet-control-attribution"),Oi(this._container),t._layers)t._layers[i].getAttribution&&this.addAttribution(t._layers[i].getAttribution());return this._update(),this._container},setPrefix:function(t){return this.options.prefix=t,this._update(),this},addAttribution:function(t){return t&&(this._attributions[t]||(this._attributions[t]=0),this._attributions[t]++,this._update()),this},removeAttribution:function(t){return t&&this._attributions[t]&&(this._attributions[t]--,this._update()),this},_update:function(){if(this._map){var t=[];for(var i in this._attributions)this._attributions[i]&&t.push(i);var e=[];this.options.prefix&&e.push(this.options.prefix),t.length&&e.push(t.join(", ")),this._container.innerHTML=e.join(" | ")}}});Ki.mergeOptions({attributionControl:!0}),Ki.addInitHook(function(){this.options.attributionControl&&(new te).addTo(this)});Xi.Layers=Ji,Xi.Zoom=$i,Xi.Scale=Qi,Xi.Attribution=te,Yi.layers=function(t,i,e){return new Ji(t,i,e)},Yi.zoom=function(t){return new $i(t)},Yi.scale=function(t){return new Qi(t)},Yi.attribution=function(t){return new te(t)};var ie=S.extend({initialize:function(t){this._map=t},enable:function(){return this._enabled||(this._enabled=!0,this.addHooks()),this},disable:function(){return this._enabled&&(this._enabled=!1,this.removeHooks()),this},enabled:function(){return!!this._enabled}});ie.addTo=function(t,i){return t.addHandler(i,this),this};var ee,ne={Events:Z},oe=bt?"touchstart mousedown":"mousedown",se={mousedown:"mouseup",touchstart:"touchend",pointerdown:"touchend",MSPointerDown:"touchend"},re={mousedown:"mousemove",touchstart:"touchmove",pointerdown:"touchmove",MSPointerDown:"touchmove"},ae=E.extend({options:{clickTolerance:3},initialize:function(t,i,e,n){c(this,n),this._element=t,this._dragStartTarget=i||t,this._preventOutline=e},enable:function(){this._enabled||(zi(this._dragStartTarget,oe,this._onDown,this),this._enabled=!0)},disable:function(){this._enabled&&(ae._dragging===this&&this.finishDrag(),Si(this._dragStartTarget,oe,this._onDown,this),this._enabled=!1,this._moved=!1)},_onDown:function(t){var i,e;!t._simulated&&this._enabled&&(this._moved=!1,li(this._element,"leaflet-zoom-anim")||ae._dragging||t.shiftKey||1!==t.which&&1!==t.button&&!t.touches||((ae._dragging=this)._preventOutline&&Pi(this._element),xi(),Xt(),this._moving||(this.fire("down"),i=t.touches?t.touches[0]:t,e=bi(this._element),this._startPoint=new k(i.clientX,i.clientY),this._parentScale=Ti(e),zi(document,re[t.type],this._onMove,this),zi(document,se[t.type],this._onUp,this))))},_onMove:function(t){var i,e;!t._simulated&&this._enabled&&(t.touches&&1i&&(e.push(t[n]),o=n);oi.max.x&&(e|=2),t.yi.max.y&&(e|=8),e}function de(t,i,e,n){var o,s=i.x,r=i.y,a=e.x-s,h=e.y-r,u=a*a+h*h;return 0this._layersMaxZoom&&this.setZoom(this._layersMaxZoom),void 0===this.options.minZoom&&this._layersMinZoom&&this.getZoom()t.y!=n.y>t.y&&t.x<(n.x-e.x)*(t.y-e.y)/(n.y-e.y)+e.x&&(u=!u);return u||Oe.prototype._containsPoint.call(this,t,!0)}});var Ne=Ce.extend({initialize:function(t,i){c(this,i),this._layers={},t&&this.addData(t)},addData:function(t){var i,e,n,o=g(t)?t:t.features;if(o){for(i=0,e=o.length;iu.x&&(l=s.x+n-u.x+h.x),s.x-l-a.x<0&&(l=s.x-a.x),s.y+e+h.y>u.y&&(c=s.y+e-u.y+h.y),s.y-c-a.y<0&&(c=s.y-a.y),(l||c)&&t.fire("autopanstart").panBy([l,c]))},_onCloseButtonClick:function(t){this._close(),Ni(t)},_getAnchor:function(){return A(this._source&&this._source._getPopupAnchor?this._source._getPopupAnchor():[0,0])}});Ki.mergeOptions({closePopupOnClick:!0}),Ki.include({openPopup:function(t,i,e){return t instanceof tn||(t=new tn(e).setContent(t)),i&&t.setLatLng(i),this.hasLayer(t)?this:(this._popup&&this._popup.options.autoClose&&this.closePopup(),this._popup=t,this.addLayer(t))},closePopup:function(t){return t&&t!==this._popup||(t=this._popup,this._popup=null),t&&this.removeLayer(t),this}}),Me.include({bindPopup:function(t,i){return t instanceof tn?(c(t,i),(this._popup=t)._source=this):(this._popup&&!i||(this._popup=new tn(i,this)),this._popup.setContent(t)),this._popupHandlersAdded||(this.on({click:this._openPopup,keypress:this._onKeyPress,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!0),this},unbindPopup:function(){return this._popup&&(this.off({click:this._openPopup,keypress:this._onKeyPress,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!1,this._popup=null),this},openPopup:function(t,i){return this._popup&&this._map&&(i=this._popup._prepareOpen(this,t,i),this._map.openPopup(this._popup,i)),this},closePopup:function(){return this._popup&&this._popup._close(),this},togglePopup:function(t){return this._popup&&(this._popup._map?this.closePopup():this.openPopup(t)),this},isPopupOpen:function(){return!!this._popup&&this._popup.isOpen()},setPopupContent:function(t){return this._popup&&this._popup.setContent(t),this},getPopup:function(){return this._popup},_openPopup:function(t){var i=t.layer||t.target;this._popup&&this._map&&(Ni(t),i instanceof Be?this.openPopup(t.layer||t.target,t.latlng):this._map.hasLayer(this._popup)&&this._popup._source===i?this.closePopup():this.openPopup(i,t.latlng))},_movePopup:function(t){this._popup.setLatLng(t.latlng)},_onKeyPress:function(t){13===t.originalEvent.keyCode&&this._openPopup(t)}});var en=Qe.extend({options:{pane:"tooltipPane",offset:[0,0],direction:"auto",permanent:!1,sticky:!1,interactive:!1,opacity:.9},onAdd:function(t){Qe.prototype.onAdd.call(this,t),this.setOpacity(this.options.opacity),t.fire("tooltipopen",{tooltip:this}),this._source&&this._source.fire("tooltipopen",{tooltip:this},!0)},onRemove:function(t){Qe.prototype.onRemove.call(this,t),t.fire("tooltipclose",{tooltip:this}),this._source&&this._source.fire("tooltipclose",{tooltip:this},!0)},getEvents:function(){var t=Qe.prototype.getEvents.call(this);return bt&&!this.options.permanent&&(t.preclick=this._close),t},_close:function(){this._map&&this._map.closeTooltip(this)},_initLayout:function(){var t="leaflet-tooltip "+(this.options.className||"")+" leaflet-zoom-"+(this._zoomAnimated?"animated":"hide");this._contentNode=this._container=si("div",t)},_updateLayout:function(){},_adjustPan:function(){},_setPosition:function(t){var i,e=this._map,n=this._container,o=e.latLngToContainerPoint(e.getCenter()),s=e.layerPointToContainerPoint(t),r=this.options.direction,a=n.offsetWidth,h=n.offsetHeight,u=A(this.options.offset),l=this._getAnchor(),c="top"===r?(i=a/2,h):"bottom"===r?(i=a/2,0):(i="center"===r?a/2:"right"===r?0:"left"===r?a:s.xthis.options.maxZoom||nthis.options.maxZoom||void 0!==this.options.minZoom&&oe.max.x)||!i.wrapLat&&(t.ye.max.y))return!1}if(!this.options.bounds)return!0;var n=this._tileCoordsToBounds(t);return N(this.options.bounds).overlaps(n)},_keyToBounds:function(t){return this._tileCoordsToBounds(this._keyToTileCoords(t))},_tileCoordsToNwSe:function(t){var i=this._map,e=this.getTileSize(),n=t.scaleBy(e),o=n.add(e);return[i.unproject(n,t.z),i.unproject(o,t.z)]},_tileCoordsToBounds:function(t){var i=this._tileCoordsToNwSe(t),e=new R(i[0],i[1]);return this.options.noWrap||(e=this._map.wrapLatLngBounds(e)),e},_tileCoordsToKey:function(t){return t.x+":"+t.y+":"+t.z},_keyToTileCoords:function(t){var i=t.split(":"),e=new k(+i[0],+i[1]);return e.z=+i[2],e},_removeTile:function(t){var i=this._tiles[t];i&&(ri(i.el),delete this._tiles[t],this.fire("tileunload",{tile:i.el,coords:this._keyToTileCoords(t)}))},_initTile:function(t){ci(t,"leaflet-tile");var i=this.getTileSize();t.style.width=i.x+"px",t.style.height=i.y+"px",t.onselectstart=a,t.onmousemove=a,it&&this.options.opacity<1&&mi(t,this.options.opacity),ot&&!st&&(t.style.WebkitBackfaceVisibility="hidden")},_addTile:function(t,i){var e=this._getTilePos(t),n=this._tileCoordsToKey(t),o=this.createTile(this._wrapCoords(t),p(this._tileReady,this,t));this._initTile(o),this.createTile.length<2&&M(p(this._tileReady,this,t,null,o)),vi(o,e),this._tiles[n]={el:o,coords:t,current:!0},i.appendChild(o),this.fire("tileloadstart",{tile:o,coords:t})},_tileReady:function(t,i,e){i&&this.fire("tileerror",{error:i,tile:e,coords:t});var n=this._tileCoordsToKey(t);(e=this._tiles[n])&&(e.loaded=+new Date,this._map._fadeAnimated?(mi(e.el,0),z(this._fadeFrame),this._fadeFrame=M(this._updateOpacity,this)):(e.active=!0,this._pruneTiles()),i||(ci(e.el,"leaflet-tile-loaded"),this.fire("tileload",{tile:e.el,coords:t})),this._noTilesToLoad()&&(this._loading=!1,this.fire("load"),it||!this._map._fadeAnimated?M(this._pruneTiles,this):setTimeout(p(this._pruneTiles,this),250)))},_getTilePos:function(t){return t.scaleBy(this.getTileSize()).subtract(this._level.origin)},_wrapCoords:function(t){var i=new k(this._wrapX?o(t.x,this._wrapX):t.x,this._wrapY?o(t.y,this._wrapY):t.y);return i.z=t.z,i},_pxBoundsToTileRange:function(t){var i=this.getTileSize();return new I(t.min.unscaleBy(i).floor(),t.max.unscaleBy(i).ceil().subtract([1,1]))},_noTilesToLoad:function(){for(var t in this._tiles)if(!this._tiles[t].loaded)return!1;return!0}});var sn=on.extend({options:{minZoom:0,maxZoom:18,subdomains:"abc",errorTileUrl:"",zoomOffset:0,tms:!1,zoomReverse:!1,detectRetina:!1,crossOrigin:!1},initialize:function(t,i){this._url=t,(i=c(this,i)).detectRetina&&zt&&0')}}catch(t){return function(t){return document.createElement("<"+t+' xmlns="urn:schemas-microsoft.com:vml" class="lvml">')}}}(),_n={_initContainer:function(){this._container=si("div","leaflet-vml-container")},_update:function(){this._map._animatingZoom||(hn.prototype._update.call(this),this.fire("update"))},_initPath:function(t){var i=t._container=cn("shape");ci(i,"leaflet-vml-shape "+(this.options.className||"")),i.coordsize="1 1",t._path=cn("path"),i.appendChild(t._path),this._updateStyle(t),this._layers[m(t)]=t},_addPath:function(t){var i=t._container;this._container.appendChild(i),t.options.interactive&&t.addInteractiveTarget(i)},_removePath:function(t){var i=t._container;ri(i),t.removeInteractiveTarget(i),delete this._layers[m(t)]},_updateStyle:function(t){var i=t._stroke,e=t._fill,n=t.options,o=t._container;o.stroked=!!n.stroke,o.filled=!!n.fill,n.stroke?(i=i||(t._stroke=cn("stroke")),o.appendChild(i),i.weight=n.weight+"px",i.color=n.color,i.opacity=n.opacity,n.dashArray?i.dashStyle=g(n.dashArray)?n.dashArray.join(" "):n.dashArray.replace(/( *, *)/g," "):i.dashStyle="",i.endcap=n.lineCap.replace("butt","flat"),i.joinstyle=n.lineJoin):i&&(o.removeChild(i),t._stroke=null),n.fill?(e=e||(t._fill=cn("fill")),o.appendChild(e),e.color=n.fillColor||n.color,e.opacity=n.fillOpacity):e&&(o.removeChild(e),t._fill=null)},_updateCircle:function(t){var i=t._point.round(),e=Math.round(t._radius),n=Math.round(t._radiusY||e);this._setPath(t,t._empty()?"M0 0":"AL "+i.x+","+i.y+" "+e+","+n+" 0,23592600")},_setPath:function(t,i){t._path.v=i},_bringToFront:function(t){hi(t._container)},_bringToBack:function(t){ui(t._container)}},dn=Et?cn:J,pn=hn.extend({getEvents:function(){var t=hn.prototype.getEvents.call(this);return t.zoomstart=this._onZoomStart,t},_initContainer:function(){this._container=dn("svg"),this._container.setAttribute("pointer-events","none"),this._rootGroup=dn("g"),this._container.appendChild(this._rootGroup)},_destroyContainer:function(){ri(this._container),Si(this._container),delete this._container,delete this._rootGroup,delete this._svgSize},_onZoomStart:function(){this._update()},_update:function(){var t,i,e;this._map._animatingZoom&&this._bounds||(hn.prototype._update.call(this),i=(t=this._bounds).getSize(),e=this._container,this._svgSize&&this._svgSize.equals(i)||(this._svgSize=i,e.setAttribute("width",i.x),e.setAttribute("height",i.y)),vi(e,t.min),e.setAttribute("viewBox",[t.min.x,t.min.y,i.x,i.y].join(" ")),this.fire("update"))},_initPath:function(t){var i=t._path=dn("path");t.options.className&&ci(i,t.options.className),t.options.interactive&&ci(i,"leaflet-interactive"),this._updateStyle(t),this._layers[m(t)]=t},_addPath:function(t){this._rootGroup||this._initContainer(),this._rootGroup.appendChild(t._path),t.addInteractiveTarget(t._path)},_removePath:function(t){ri(t._path),t.removeInteractiveTarget(t._path),delete this._layers[m(t)]},_updatePath:function(t){t._project(),t._update()},_updateStyle:function(t){var i=t._path,e=t.options;i&&(e.stroke?(i.setAttribute("stroke",e.color),i.setAttribute("stroke-opacity",e.opacity),i.setAttribute("stroke-width",e.weight),i.setAttribute("stroke-linecap",e.lineCap),i.setAttribute("stroke-linejoin",e.lineJoin),e.dashArray?i.setAttribute("stroke-dasharray",e.dashArray):i.removeAttribute("stroke-dasharray"),e.dashOffset?i.setAttribute("stroke-dashoffset",e.dashOffset):i.removeAttribute("stroke-dashoffset")):i.setAttribute("stroke","none"),e.fill?(i.setAttribute("fill",e.fillColor||e.color),i.setAttribute("fill-opacity",e.fillOpacity),i.setAttribute("fill-rule",e.fillRule||"evenodd")):i.setAttribute("fill","none"))},_updatePoly:function(t,i){this._setPath(t,$(t._parts,i))},_updateCircle:function(t){var i=t._point,e=Math.max(Math.round(t._radius),1),n="a"+e+","+(Math.max(Math.round(t._radiusY),1)||e)+" 0 1,0 ",o=t._empty()?"M0 0":"M"+(i.x-e)+","+i.y+n+2*e+",0 "+n+2*-e+",0 ";this._setPath(t,o)},_setPath:function(t,i){t._path.setAttribute("d",i)},_bringToFront:function(t){hi(t._path)},_bringToBack:function(t){ui(t._path)}});function mn(t){return Zt||Et?new pn(t):null}Et&&pn.include(_n),Ki.include({getRenderer:function(t){var i=(i=t.options.renderer||this._getPaneRenderer(t.options.pane)||this.options.renderer||this._renderer)||(this._renderer=this._createRenderer());return this.hasLayer(i)||this.addLayer(i),i},_getPaneRenderer:function(t){if("overlayPane"===t||void 0===t)return!1;var i=this._paneRenderers[t];return void 0===i&&(i=this._createRenderer({pane:t}),this._paneRenderers[t]=i),i},_createRenderer:function(t){return this.options.preferCanvas&&ln(t)||mn(t)}});var fn=Re.extend({initialize:function(t,i){Re.prototype.initialize.call(this,this._boundsToLatLngs(t),i)},setBounds:function(t){return this.setLatLngs(this._boundsToLatLngs(t))},_boundsToLatLngs:function(t){return[(t=N(t)).getSouthWest(),t.getNorthWest(),t.getNorthEast(),t.getSouthEast()]}});pn.create=dn,pn.pointsToPath=$,Ne.geometryToLayer=De,Ne.coordsToLatLng=We,Ne.coordsToLatLngs=He,Ne.latLngToCoords=Fe,Ne.latLngsToCoords=Ue,Ne.getFeature=Ve,Ne.asFeature=qe,Ki.mergeOptions({boxZoom:!0});var gn=ie.extend({initialize:function(t){this._map=t,this._container=t._container,this._pane=t._panes.overlayPane,this._resetStateTimeout=0,t.on("unload",this._destroy,this)},addHooks:function(){zi(this._container,"mousedown",this._onMouseDown,this)},removeHooks:function(){Si(this._container,"mousedown",this._onMouseDown,this)},moved:function(){return this._moved},_destroy:function(){ri(this._pane),delete this._pane},_resetState:function(){this._resetStateTimeout=0,this._moved=!1},_clearDeferredResetState:function(){0!==this._resetStateTimeout&&(clearTimeout(this._resetStateTimeout),this._resetStateTimeout=0)},_onMouseDown:function(t){if(!t.shiftKey||1!==t.which&&1!==t.button)return!1;this._clearDeferredResetState(),this._resetState(),Xt(),xi(),this._startPoint=this._map.mouseEventToContainerPoint(t),zi(document,{contextmenu:Ni,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this)},_onMouseMove:function(t){this._moved||(this._moved=!0,this._box=si("div","leaflet-zoom-box",this._container),ci(this._container,"leaflet-crosshair"),this._map.fire("boxzoomstart")),this._point=this._map.mouseEventToContainerPoint(t);var i=new I(this._point,this._startPoint),e=i.getSize();vi(this._box,i.min),this._box.style.width=e.x+"px",this._box.style.height=e.y+"px"},_finish:function(){this._moved&&(ri(this._box),_i(this._container,"leaflet-crosshair")),Jt(),wi(),Si(document,{contextmenu:Ni,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this)},_onMouseUp:function(t){var i;1!==t.which&&1!==t.button||(this._finish(),this._moved&&(this._clearDeferredResetState(),this._resetStateTimeout=setTimeout(p(this._resetState,this),0),i=new R(this._map.containerPointToLatLng(this._startPoint),this._map.containerPointToLatLng(this._point)),this._map.fitBounds(i).fire("boxzoomend",{boxZoomBounds:i})))},_onKeyDown:function(t){27===t.keyCode&&this._finish()}});Ki.addInitHook("addHandler","boxZoom",gn),Ki.mergeOptions({doubleClickZoom:!0});var vn=ie.extend({addHooks:function(){this._map.on("dblclick",this._onDoubleClick,this)},removeHooks:function(){this._map.off("dblclick",this._onDoubleClick,this)},_onDoubleClick:function(t){var i=this._map,e=i.getZoom(),n=i.options.zoomDelta,o=t.originalEvent.shiftKey?e-n:e+n;"center"===i.options.doubleClickZoom?i.setZoom(o):i.setZoomAround(t.containerPoint,o)}});Ki.addInitHook("addHandler","doubleClickZoom",vn),Ki.mergeOptions({dragging:!0,inertia:!st,inertiaDeceleration:3400,inertiaMaxSpeed:1/0,easeLinearity:.2,worldCopyJump:!1,maxBoundsViscosity:0});var yn=ie.extend({addHooks:function(){var t;this._draggable||(t=this._map,this._draggable=new ae(t._mapPane,t._container),this._draggable.on({dragstart:this._onDragStart,drag:this._onDrag,dragend:this._onDragEnd},this),this._draggable.on("predrag",this._onPreDragLimit,this),t.options.worldCopyJump&&(this._draggable.on("predrag",this._onPreDragWrap,this),t.on("zoomend",this._onZoomEnd,this),t.whenReady(this._onZoomEnd,this))),ci(this._map._container,"leaflet-grab leaflet-touch-drag"),this._draggable.enable(),this._positions=[],this._times=[]},removeHooks:function(){_i(this._map._container,"leaflet-grab"),_i(this._map._container,"leaflet-touch-drag"),this._draggable.disable()},moved:function(){return this._draggable&&this._draggable._moved},moving:function(){return this._draggable&&this._draggable._moving},_onDragStart:function(){var t,i=this._map;i._stop(),this._map.options.maxBounds&&this._map.options.maxBoundsViscosity?(t=N(this._map.options.maxBounds),this._offsetLimit=O(this._map.latLngToContainerPoint(t.getNorthWest()).multiplyBy(-1),this._map.latLngToContainerPoint(t.getSouthEast()).multiplyBy(-1).add(this._map.getSize())),this._viscosity=Math.min(1,Math.max(0,this._map.options.maxBoundsViscosity))):this._offsetLimit=null,i.fire("movestart").fire("dragstart"),i.options.inertia&&(this._positions=[],this._times=[])},_onDrag:function(t){var i,e;this._map.options.inertia&&(i=this._lastTime=+new Date,e=this._lastPos=this._draggable._absPos||this._draggable._newPos,this._positions.push(e),this._times.push(i),this._prunePositions(i)),this._map.fire("move",t).fire("drag",t)},_prunePositions:function(t){for(;1i.max.x&&(t.x=this._viscousLimit(t.x,i.max.x)),t.y>i.max.y&&(t.y=this._viscousLimit(t.y,i.max.y)),this._draggable._newPos=this._draggable._startPos.add(t))},_onPreDragWrap:function(){var t=this._worldWidth,i=Math.round(t/2),e=this._initialWorldOffset,n=this._draggable._newPos.x,o=(n-i+e)%t+i-e,s=(n+i+e)%t-i-e,r=Math.abs(o+e)i.getMaxZoom()&&1b;b++)a.appendChild(arguments[b]);return a}function c(a,b,c,d){var e=["opacity",b,~~(100*a),c,d].join("-"),f=.01+c/d*100,g=Math.max(1-(1-a)/b*(100-f),a),h=j.substring(0,j.indexOf("Animation")).toLowerCase(),i=h&&"-"+h+"-"||"";return m[e]||(k.insertRule("@"+i+"keyframes "+e+"{0%{opacity:"+g+"}"+f+"%{opacity:"+a+"}"+(f+.01)+"%{opacity:1}"+(f+b)%100+"%{opacity:"+a+"}100%{opacity:"+g+"}}",k.cssRules.length),m[e]=1),e}function d(a,b){var c,d,e=a.style;if(b=b.charAt(0).toUpperCase()+b.slice(1),void 0!==e[b])return b;for(d=0;d',c)}k.addRule(".spin-vml","behavior:url(#default#VML)"),h.prototype.lines=function(a,d){function f(){return e(c("group",{coordsize:k+" "+k,coordorigin:-j+" "+-j}),{width:k,height:k})}function h(a,h,i){b(m,b(e(f(),{rotation:360/d.lines*a+"deg",left:~~h}),b(e(c("roundrect",{arcsize:d.corners}),{width:j,height:d.scale*d.width,left:d.scale*d.radius,top:-d.scale*d.width>>1,filter:i}),c("fill",{color:g(d.color,a),opacity:d.opacity}),c("stroke",{opacity:0}))))}var i,j=d.scale*(d.length+d.width),k=2*d.scale*j,l=-(d.width+d.length)*d.scale*2+"px",m=e(f(),{position:"absolute",top:l,left:l});if(d.shadow)for(i=1;i<=d.lines;i++)h(i,-2,"progid:DXImageTransform.Microsoft.Blur(pixelradius=2,makeshadow=1,shadowopacity=.3)");for(i=1;i<=d.lines;i++)h(i);return b(a,m)},h.prototype.opacity=function(a,b,c,d){var e=a.firstChild;d=d.shadow&&d.lines||0,e&&b+d>1)+"px"})}for(var i,k=0,l=(f.lines-1)*(1-f.direction)/2;k=i;)t=t.__parent;return this._currentShownBounds.contains(t.getLatLng())&&(this.options.animateAddingMarkers?this._animationAddLayer(e,t):this._animationAddLayerNonAnimated(e,t)),this},removeLayer:function(e){return e instanceof L.LayerGroup?this.removeLayers([e]):(e.getLatLng?this._map?e.__parent&&(this._unspiderfy&&(this._unspiderfy(),this._unspiderfyLayer(e)),this._removeLayer(e,!0),this.fire("layerremove",{layer:e}),this._topClusterLevel._recalculateBounds(),this._refreshClustersIcons(),e.off(this._childMarkerEventHandlers,this),this._featureGroup.hasLayer(e)&&(this._featureGroup.removeLayer(e),e.clusterShow&&e.clusterShow())):(!this._arraySplice(this._needsClustering,e)&&this.hasLayer(e)&&this._needsRemoving.push({layer:e,latlng:e._latlng}),this.fire("layerremove",{layer:e})):(this._nonPointGroup.removeLayer(e),this.fire("layerremove",{layer:e})),this)},addLayers:function(n,s){if(!L.Util.isArray(n))return this.addLayer(n);var o,a=this._featureGroup,h=this._nonPointGroup,l=this.options.chunkedLoading,u=this.options.chunkInterval,_=this.options.chunkProgress,d=n.length,p=0,c=!0;if(this._map){var f=(new Date).getTime(),m=L.bind(function(){var e=(new Date).getTime();for(this._map&&this._unspiderfy&&this._unspiderfy();p"+t+"
",className:"marker-cluster"+i,iconSize:new L.Point(40,40)})},_bindEvents:function(){var e=this._map,t=this.options.spiderfyOnMaxZoom,i=this.options.showCoverageOnHover,r=this.options.zoomToBoundsOnClick;(t||r)&&this.on("clusterclick",this._zoomOrSpiderfy,this),i&&(this.on("clustermouseover",this._showCoverage,this),this.on("clustermouseout",this._hideCoverage,this),e.on("zoomend",this._hideCoverage,this))},_zoomOrSpiderfy:function(e){for(var t=e.layer,i=t;1===i._childClusters.length;)i=i._childClusters[0];i._zoom===this._maxZoom&&i._childCount===t._childCount&&this.options.spiderfyOnMaxZoom?t.spiderfy():this.options.zoomToBoundsOnClick&&t.zoomToBounds(),e.originalEvent&&13===e.originalEvent.keyCode&&this._map._container.focus()},_showCoverage:function(e){var t=this._map;this._inZoomAnimation||(this._shownPolygon&&t.removeLayer(this._shownPolygon),2h._zoom;r--)u=new this._markerCluster(this,r,u),n[r].addObject(u,this._map.project(a.getLatLng(),r));return h._addChild(u),void this._removeFromGridUnclustered(a,t)}s[t].addObject(e,i)}this._topClusterLevel._addChild(e),e.__parent=this._topClusterLevel},_refreshClustersIcons:function(){this._featureGroup.eachLayer(function(e){e instanceof L.MarkerCluster&&e._iconNeedsUpdate&&e._updateIcon()})},_enqueue:function(e){this._queue.push(e),this._queueTimeout||(this._queueTimeout=setTimeout(L.bind(this._processQueue,this),300))},_processQueue:function(){for(var e=0;ee?(this._animationStart(),this._animationZoomOut(this._zoom,e)):this._moveEnd()},_getExpandedVisibleBounds:function(){return this.options.removeOutsideVisibleBounds?L.Browser.mobile?this._checkBoundsMaxLat(this._map.getBounds()):this._checkBoundsMaxLat(this._map.getBounds().pad(1)):this._mapBoundsInfinite},_checkBoundsMaxLat:function(e){var t=this._maxLat;return void 0!==t&&(e.getNorth()>=t&&(e._northEast.lat=1/0),e.getSouth()<=-t&&(e._southWest.lat=-1/0)),e},_animationAddLayerNonAnimated:function(e,t){if(t===e)this._featureGroup.addLayer(e);else if(2===t._childCount){t._addToMap();var i=t.getAllChildMarkers();this._featureGroup.removeLayer(i[0]),this._featureGroup.removeLayer(i[1])}else t._updateIcon()},_extractNonGroupLayers:function(e,t){var i,r=e.getLayers(),n=0;for(t=t||[];ni)&&(i=(o=d).lat),(!1===r||d.latn)&&(n=(h=d).lng),(!1===s||d.lng=this._circleSpiralSwitchover?this._generatePointsSpiral(t.length,i):(i.y+=10,this._generatePointsCircle(t.length,i)),this._animationSpiderfy(t,e)}},unspiderfy:function(e){this._group._inZoomAnimation||(this._animationUnspiderfy(e),this._group._spiderfied=null)},_generatePointsCircle:function(e,t){var i,r,n=this._group.options.spiderfyDistanceMultiplier*this._circleFootSeparation*(2+e)/this._2PI,s=this._2PI/e,o=[];for(n=Math.max(n,35),o.length=e,i=0;i1){r=F.arrayPool.get();for(var i=1,n=arguments.length;i1)for(var i=0;i1?t-1:0),i=1;i1?t-1:0),n=1;n1?t-1:0),n=1;n1?t-1:0),n=1;n1?t-1:0),i=1;i1?t-1:0),i=1;i0&&void 0!==arguments[0]?arguments[0]:{};d.a&&!e.videoSrc&&e.photoSrc?s.a.warn("Changing a `photoSrc` independent of its `videoSrc` can result in unexpected behavior"):d.a&&e.videoSrc&&!e.photoSrc&&s.a.warn("Changing a `videoSrc` independent of its `photoSrc` can result in unexpected behavior");var t=F?{photoSrc:F.photo,videoSrc:F.videoSrc,effectType:F.effectType,autoplay:F.autoplay,proactivelyLoadsVideo:F.proactivelyLoadsVideo}:{},r=c({},t,e),i=(r.photoSrc,r.videoSrc,r.effectType),n=r.autoplay,f=r.proactivelyLoadsVideo;C=o.a.objectPool.get(),r.preloadedEffectType=i,r.autoplay=!1!==n;var v=i||l.a.default;l.a.toPlaybackStyle(v)===u.a.LOOP&&r.autoplay&&(d.a&&!f&&s.a.warn("When using a looping asset you should set `proactivelyLoadsVideo` to `true` unless `autoplay` is also set to `false`"),r.proactivelyLoadsVideo=!0);for(var y in r)Object.prototype.hasOwnProperty.call(r,y)&&(p[y]===h?C[y]=r[y]:s.a.warn("LivePhotosKit.Player: Initial configuration for `"+y+"` was ignored, because the property is not a writable property."));if(F)for(var m in C){var g=C[m];F[m]=g}else F=a.a.create(R,C);o.a.objectPool.ret(C),C=null};R.setProperties=L,R.setProperties(t);for(var E,A,I=0;(E=f[I])&&(A=m[I]);I++)!function(e,t,r){"method"===r?(g.value=F[t].bind(F),Object.defineProperty(R,t,g)):(b.set=r===h?function(e){F[t]=e}:function(){},b.get=function(){return F[t]},Object.defineProperty(R,t,b))}(0,E,A);g.value=function(){var e=arguments.length,t=arguments[e-1];if(e<1||!(t instanceof Function))throw new Error("Invalid arguments passed to `observe`. Form: key, [key, …], callback.");for(var r=o.a.arrayPool.get(),i=0,n=e;i=3||"string"==typeof arguments[0]&&"string"==typeof arguments[1])throw new Error("LivePhotosKit.Player: Creating a new Player using arguments of the form 'photoSrc, videoSrc, [targetElement, [options]]' is no longer supported. Instead, use the new signature, '[targetElement, [options]]");return s.a.warn("The `LivePhotosKit.Player` method will be deprecated in an upcoming release. Please use the `LivePhotosKit.augementElementAsPlayer` or `LivePhotosKit.createPlayer` methods, instead."),e?_(e,t):P(t)},T=function e(t,r){i(this,e),this.fire=function(){r[t.keyOnObject]()},this.disconnect=function(){t.unregisterFromDefinition(r)},this.connect=function(){t.registerOnDefinition(r)}}},function(e,t,r){"use strict";var i=/_lpk_debug=true/i;t.a=i.test(window.location.search)||i.test(window.location.hash)},function(e,t,r){"use strict";var i={setUpForRender:function(){this.attachInto(this.renderer)},tearDownFromRender:function(){this.detach(),this._super()},renderStyles:function(e){for(var t,r=this.element,i=r.style,n=0;t=e[n];n++){var a=t,o=a.styleKey,s=a.value;i[o]!==s&&(i[o]=s)}}};t.a=i},function(e,t,r){"use strict";var i=r(55),n=r(56),a=r(57);t.a={APP_NAME:"LivePhotosKit",BUILD_NUMBER:i.a,MASTERING_NUMBER:n.a,FEEDBACK_URL_PREFIX:"https://feedbackws.icloud.com",LIVEPHOTOSKIT_LOADED:"livephotoskitloaded",URL_PREFIX:"https://cdn.apple-livephotoskit.com",VERSION:a.a}},function(e,t,r){"use strict";function i(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}var n=r(3),a=r(50),o=r(18),s=r(10),u=r(1);r.d(t,"a",function(){return c});var l=function(){function e(e,t){for(var r=0;r0&&void 0!==arguments[0]?arguments[0]:{};i(this,e),this._setInstanceProps(r),this._createCanvas(),this.redraw(),this._addEventListeners(),s.a.observe("locale",function(){return t.updateBadgeText()})}return l(e,[{key:"attachPlayerInstance",value:function(e){e.attachBadgeView(this),this.updateBadgeText(e.effectType)}},{key:"redraw",value:function(){var e=this.progress;e>0&&this.shouldAnimateProgressRing?this._animateProgressRing():this._redraw(e)}},{key:"reset",value:function(){var e=this._requestedFrame;e&&cancelAnimationFrame(e),this._progress=0,this._previousProgress=0,this.redraw()}},{key:"appendTo",value:function(e){e.appendChild(this.element)}},{key:"updateAriaLabel",value:function(){var e=n.a.toLocalizedString(this.effectType),t=s.a.getString("VideoEffects.Badge");this.element.setAttribute("aria-label",t+": "+e)}},{key:"updateBadgeText",value:function(e){e?this.effectType=e:e=this.effectType,this.label=e?n.a.toBadgeText(e):"",this.playbackStyle=n.a.toPlaybackStyle(e),this.updateAriaLabel(),this._redraw()}},{key:"_createCanvas",value:function(){var e=this.element;if(e){if("canvas"!==e.tagName.toLowerCase())throw new Error("Backing element for LivePhotoBadge needs to be an HTMLCanvasElement.")}else e=this.element=document.createElement("canvas");e.setAttribute("role","button"),this.updateAriaLabel(),e.classList.add("lpk-badge"),this._context=e.getContext("2d")}},{key:"_setCanvasSize",value:function(){var e=this.element,t=o.a(),r=this.height,i=this.width;e.height=r*t,e.width=i*t,e.style.height=r+"px",e.style.width=i+"px"}},{key:"_setInstanceProps",value:function(e){var t={};for(var r in d)t.hasOwnProperty.call(d,r)&&(this[r]=e.hasOwnProperty(r)?e[r]:d[r]);this.defaultProps=d}},{key:"_redraw",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:0,t=(this.element,this.label),r=t.toLowerCase()||n.a.default;this._setCanvasSize(),this._context.clearRect(0,0,this.width,this.height),this._drawBackground(),this._drawLabel(),this.shouldShowError||(this._drawInnerCircle(),n.a.toPlaybackStyle(r)!==u.a.LOOP?this._drawPlayArrow():this._drawLoopCircle()),this.shouldShowError?(this._drawProgressRing(1),this._drawErrorSlash()):this.progress>0?this._drawProgressRing(e):this._drawDottedCircle()}},{key:"_drawBackground",value:function(){var e=o.a(),t=this._context,r=this.borderRadius*e,i=this.width*e,n=this.height*e;t.beginPath(),t.moveTo(r,0),t.lineTo(i-r,0),t.quadraticCurveTo(i,0,i,r),t.lineTo(i,n-r),t.quadraticCurveTo(i,n,i-r,n),t.lineTo(r,n),t.quadraticCurveTo(0,n,0,n-r),t.lineTo(0,r),t.quadraticCurveTo(0,0,r,0),t.closePath(),t.fillStyle=this.backgroundColor,t.fill()}},{key:"_drawDottedCircle",value:function(){for(var t=e.numberOfDots,r=this.dottedRadius*o.a(),i=0;i0?s.width:0;return this._width=(u>2?a:-2)+2*t+2*n+Math.ceil(u/o.a())}},{key:"fontStyle",get:function(){return this.fontSize*o.a()+'pt/1 system, -apple-system, BlinkMacSystemFont, "Helvetica Neue", Helvetica'}},{key:"x0",get:function(){return(this.dottedRadius+this.leftPadding)*o.a()}},{key:"y0",get:function(){return this.height/2*o.a()}},{key:"progress",set:function(e){"number"==typeof e&&(this._previousProgress=this._progress,this._progress=e,this.redraw())},get:function(){return this._progress}},{key:"shouldShowError",set:function(e){this._shouldShowError=!!e,this._redraw(this.progress)},get:function(){return this._shouldShowError}}],[{key:"numberOfDots",get:function(){return 1===o.a()?17:26}}]),e}()},function(e,t,r){"use strict";var i=r(30),n=r(0),a=r(6),o=i.a.extend({mimeType:n.a.observableProperty({dependencies:["_mimeTypeFromXHR"],get:function(e){return this._mimeTypeFromXHR||e||null}}),_mimeTypeFromXHR:n.a.observableProperty(),requiresMimeTypeForRawArrayBufferSrc:!0,exposedMimeTypeKeyForErrorStrings:"mimeType",exposedSrcKeyForErrorStrings:"src",abortCurrentLoad:function(){this.__xhr&&(this._detachXHR(),this._xhr.abort()),this._mimeTypeFromXHR=null,this.abortCurrentSecondaryLoad()},loadSrc:function(e){if("string"==typeof e){this._mimeTypeFromXHR=null,this._attachXHR();var t=this._xhr;t.open("GET",e),t.responseType="arraybuffer",t.send(null)}else if(e instanceof ArrayBuffer){if(!this.mimeType&&this.requiresMimeTypeForRawArrayBufferSrc)throw new Error("MIME Type must be assigned to `"+this.exposedMimeTypeKeyForErrorStrings+"` prior to assigning a raw ArrayBuffer to `"+this.exposedSrcKeyForErrorStrings+"`.");this.beginSecondaryLoad(e,this.mimeType)}},get _xhr(){var e=this.__xhr;return e||(e=this.__xhr=new XMLHttpRequest),e},_detachXHR:function(){var e=this._xhr;e.removeEventListener("progress",this._xhrProgress),e.removeEventListener("readystatechange",this._xhrReadyStateChanged)},_attachXHR:function(){var e=this._xhr;e.addEventListener("progress",this._xhrProgress),e.addEventListener("readystatechange",this._xhrReadyStateChanged)},_xhrReadyStateChanged:function(){if("loading"===this.state){if(this._xhr.readyState>=2&&200!==this._xhr.status){var e=new Error("Failed to download resource from URL assigned to '"+this.exposedSrcKeyForErrorStrings+"'.");return e.errCode=a.a.FAILED_TO_DOWNLOAD_RESOURCE,this.loadDidFail(e)}return 4===this._xhr.readyState&&200===this._xhr.status?this._xhrLoadDidFinish():void 0}},_xhrProgress:function(e){if(e&&e.total){var t=(+e.loaded||0)/e.total;+t===t&&(this.progress=Math.max(0,Math.min(1,t)))}},_xhrLoadDidFinish:function(){this._mimeTypeFromXHR=this._xhr.getResponseHeader("Content-Type"),this.beginSecondaryLoad(this._xhr.response,this.mimeType)},beginSecondaryLoad:function(e,t){this._defaultSecondaryLoadTimeout=setTimeout(this.loadDidSucceed.bind(this,e),0)},abortCurrentSecondaryLoad:function(){this._defaultSecondaryLoadTimeout&&(clearTimeout(this._defaultSecondaryLoadTimeout),this._defaultSecondaryLoadTimeout=null)},init:function(){this._xhrReadyStateChanged=this._xhrReadyStateChanged.bind(this),this._xhrProgress=this._xhrProgress.bind(this),this._super()}});t.a=o},function(e,t,r){"use strict";var i=r(2);t.a=i.a.isEdge||i.a.isIE},function(e,t,r){"use strict";function i(){u.forEach(function(e){return e()})}function n(e){u.push(e)}function a(){return window.devicePixelRatio}function o(){return Math.ceil(a())}t.b=n,t.a=o;var s=void 0,u=[];!function(){window.matchMedia&&(s=window.matchMedia("only screen and (-webkit-min-device-pixel-ratio:1.3),only screen and (-o-min-device-pixel-ratio:13/10),only screen and (min-resolution:120dpi)"),s.addListener(i))}()},function(e,t,r){"use strict";function i(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}var n=function(){function e(e,t){for(var r=0;r0&&(this._k.length=0,this._v.length=0)}}]),e}();t.a=a},function(e,t,r){"use strict";function i(e){if(null===e)return"_null";if(void 0===e)return"_undefined";if(e.hasOwnProperty("_LPKGUID"))return e._LPKGUID;var t=void 0===e?"undefined":n(e);switch(t){case"number":Object.is(e,-0)&&(e="-0");case"string":case"boolean":return t+e;case"object":case"function":o++;var r=t+o;return a.value=r,Object.defineProperty(e,"_LPKGUID",a),r;default:throw"unrecognized object type"}}t.a=i;var n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},a={value:"",enumerable:!1,writable:!1,configurable:!1},o=0},function(e,t,r){function i(e){return r(n(e))}function n(e){var t=a[e];if(!(t+1))throw new Error("Cannot find module '"+e+"'.");return t}var a={"./en-us.lproj/strings.json":22};i.keys=function(){return Object.keys(a)},i.resolve=n,e.exports=i,i.id=21},function(e,t){e.exports={"VideoEffects.Badge":"Badge","VideoEffects.Badge.Title.Loop":"Loop","VideoEffects.Badge.Title.Bounce":"Bounce","VideoEffects.Badge.Title.LongExposure":"Long Exposure"}},function(e,t,r){"use strict";var i=r(28),n=r(32),a=r(34),o=r(37),s=r(35),u=r(4),l=r(0),d=r(8),c=r(5),h=r(1);a.a.register(),o.a.register(),s.a.register();var p=d.a.extend({approach:"",autoplay:!0,caption:"",_hasInitialized:!1,_lastRecipe:null,recipe:l.a.observableProperty({get:function(){var e=u.a.getRecipeFromPlaybackStyle(this.playbackStyle);return this._setRecipe(e),e},set:function(e){this._setRecipe(e)}}),_setRecipe:function(e){e&&e!==this._lastRecipe&&(this._lastRecipe=e,this.setUpRenderLayers())},requestMoreCompatibleRecipe:function(){this.recipe=this.recipe.requestMoreCompatibleRecipe()},duration:l.a.observableProperty({dependencies:["recipe","provider.videoDuration","provider.photoTime"],get:function(e){var t=this.recipe,r=this.provider,i=r.photoTime,n=r.videoDuration;return t?t.calculateAnimationDuration(e,n,i):0}}),displayWidth:0,displayHeight:0,get backingWidth(){return Math.round(this.displayWidth*devicePixelRatio)},get backingHeight(){return Math.round(this.displayHeight*devicePixelRatio)},get renderLayerWidth(){return this.displayWidth},get renderLayerHeight(){return this.displayHeight},get videoWidth(){return this.videoDecoder.videoWidth},get videoHeight(){return this.videoDecoder.videoHeight},photoWidth:l.a.proxyProperty("photo.width"),photoHeight:l.a.proxyProperty("photo.height"),photo:l.a.proxyProperty("provider.photo"),video:l.a.proxyProperty("provider.video"),photoTime:l.a.proxyProperty("provider.photoTime"),frameTimes:l.a.proxyProperty("provider.frameTimes"),effectType:l.a.proxyProperty("provider.effectType"),preloadedEffectType:l.a.proxyProperty("provider.preloadedEffectType"),playbackStyle:l.a.proxyProperty("provider.playbackStyle"),currentTime:l.a.observableProperty({defaultValue:0,dependencies:["duration"],get:function(e){return Math.min(this.duration||0,Math.max(0,e||0))},didChange:function(e){this.prepareToRenderAtTime(e)}}),canRenderCurrentTime:l.a.observableProperty({readOnly:!0,dependencies:["currentTime"],get:function(){return this.canRenderAtTime(this.currentTime)}}),_currentTimeRenderObserver:l.a.observer("currentTime","canRenderCurrentTime",function(e,t){t&&(this.renderedTime=e)}),renderedTime:l.a.observableProperty({defaultValue:0,didChange:function(e){this.renderAtTime(e),this.currentTime=e}}),areAllRenderLayersPrepared:l.a.observableProperty({defaultValue:!1}),isFullyPreparedForPlayback:l.a.observableProperty({readOnly:!0,dependencies:["video","areAllRenderLayersPrepared","photoTime","frameTimes","playbackStyle"],get:function(){return Boolean(this.video&&this.areAllRenderLayersPrepared&&(this.photoTime||this.playbackStyle!==h.a.HINT)&&Array.isArray(this.frameTimes))}}),cannotRenderDueToMissingPhotoTimeOrFrameTimes:l.a.observableProperty({readOnly:!0,dependencies:["video","areAllRenderLayersPrepared","photoTime","frameTimes","playbackStyle"],get:function(){return Boolean(this.video&&this.areAllRenderLayersPrepared&&(!this.photoTime&&this.playbackStyle===h.a.HINT||!Array.isArray(this.frameTimes)))}}),renderLayers:l.a.property(function(){return[]}),videoDecoder:l.a.observableProperty(function(){return this._videoDecoderClass.create({owner:this})}),_videoDecoderClass:i.a.extend({owner:l.a.observableProperty(),provider:l.a.proxyProperty("owner.provider")}),provider:l.a.observableProperty(function(){return n.a.create()}),init:function(){this._super(),this.element.className=((this.element.className||"")+" lpk-live-photo-renderer").trim(),this.element.style.position="absolute",this.element.style.overflow="hidden",this.element.style.textAlign="left"},updateSize:function(e,t){if(!arguments.length)return void(this.displayWidth&&this.displayHeight&&this.updateSize(this.displayWidth,this.displayHeight));this.displayWidth=e=Math.round(e),this.displayHeight=t=Math.round(t),this.element.style.width=e+"px",this.element.style.height=t+"px";for(var r,i=0;r=this.renderLayers[i];i++)r.updateSize(this.renderLayerWidth,this.renderLayerHeight)},_imageOrVideoDidEnterOrLeave:l.a.observer("videoDecoder.canProvideFrames","photo",function(){this.prepareToRenderAtTime(this.currentTime)}),prepareToRenderAtTime:l.a.boundFunction(function(e){this.propertyChanged("canRenderCurrentTime");for(var t,r=!0,i=0;t=this.renderLayers[i];i++)r=t.prepareToRenderAtTime(e)&&r;this.areAllRenderLayersPrepared=r}),canRenderAtTime:function(e){if(0===e)return!0;if(!this.duration&&e)return!1;for(var t,r=!0,i="",n=0;t=this.renderLayers[n];n++)t.canRenderAtTime(e)||(r=!1,i+=(i?", ":"Cannot render; waiting for ")+t.layerName);return i&&c.a.log(i+"."),r},renderAtTime:function(e){if(this.duration)for(var t,r=0;t=this.renderLayers[r];r++)t.renderAtTime(e)},getNewRenderLayers:function(){return this.recipe.getRenderLayers(this)},setUpRenderLayers:function(){var e=this.renderLayers;e&&this._cleanUpRenderLayers(e),this.renderLayers=this.getNewRenderLayers(),this.updateSize(),this.currentTime=0,this.prepareToRenderAtTime(0)},_cleanUpRenderLayers:function(e){for(var t,r=0;t=e[r];r++)t.dispose(),t.tearDownFromRender()},reduceMemoryFootprint:function(){for(var e,t=0;e=this.renderLayers[t];t++)e.reduceMemoryFootprint()},_clearRetainedFramesWhenNecessary:l.a.observer("provider.videoRotation","provider.frameTimes",function(){this.reduceMemoryFootprint(),this.prepareToRenderAtTime(this.currentTime)})});t.a=p},function(e,t,r){"use strict";var i=r(23),n=i.a.extend({approach:"dom"});t.a=n},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var i=r(14),n=r(9),a=r(10),o=r(11);r.d(t,"augmentElementAsPlayer",function(){return o.a}),r.d(t,"createPlayer",function(){return o.b}),r.d(t,"Player",function(){return o.c});var s=r(6);r.d(t,"Errors",function(){return s.a});var u=r(15);r.d(t,"LivePhotoBadge",function(){return u.a});var l=r(1);r.d(t,"PlaybackStyle",function(){return l.a}),r.d(t,"Localization",function(){return d}),r.d(t,"BUILD_NUMBER",function(){return c}),r.d(t,"MASTERING_NUMBER",function(){return h}),r.d(t,"VERSION",function(){return p}),r.d(t,"LIVEPHOTOSKIT_LOADED",function(){return f});var d={get locale(){return a.a.locale},set locale(e){a.a.locale=e}},c=i.a.BUILD_NUMBER,h=i.a.MASTERING_NUMBER,p=i.a.VERSION,f=i.a.LIVEPHOTOSKIT_LOADED,v="undefined"!=typeof window&&"undefined"!=typeof document;if(v){var y=window.document;setTimeout(function(){return y.dispatchEvent(r.i(n.a)())});if(y.styleSheets&&document.head){for(var m=null,g=null,b=0;b0)}),init:function(){this._super.apply(this,arguments),e.attachBadgeView(this.badgeView)}}).create()):null},didChange:function(e){this._nativeControls_previousValue&&this._nativeControls_previousValue.detach(),this._nativeControls_previousValue=e,e&&e.attachInto(this)}}),init:function(e,t){var i=this;if(e&&!n(e))throw"Any pre-existing element provided for use as a LivePhotosKit.Player must be able to append child DOM nodes.";e&&e.childNodes.length&&(e.innerHTML="");for(var a in t)Object.prototype.hasOwnProperty.call(t,a)&&(this[a]=t[a]);this._super(e);switch(this.element.className.indexOf("lpk-live-photo-player")<0&&(this.element.className=this.element.className+" lpk-live-photo-player"),this.element.setAttribute("role","image"),r.i(c.a)(this.element,"position")||this.element.style.position){case"absolute":case"fixed":case"relative":break;default:this.element.style.position="relative"}switch(r.i(c.a)(this.element,"display")||this.element.style.display){case"block":case"inline-block":case"table":case"table-caption":case"table-column-group":case"table-header-group":case"table-footer-group":case"table-row-group":case"table-cell":case"table-column":case"table-row":break;default:this.element.style.display="inline-block"}this.renderer.attachInto(this),this.renderer.eventDispatchingElement=this.element,window.addEventListener("resize",this.updateSize),"ontouchstart"in document.documentElement&&(this.addEventListener("touchstart",function(){return i.play()},!1),this.addEventListener("touchend",function(){return i.beginFinishingPlaybackEarly()},!1))},play:function(){if(!this.isPlaying){var e=this.provider;e.video||(e.needsLoadedVideoForPlayback=!0),this.wantsToPlay=!0,this.canPlay&&(this.isPlaying=!0,this._lastFrameNow=Date.now(),this._nextFrame())}return this.isPlaying},pause:function(){this.isPlaying=!1,this.wantsToPlay=!1,this._cancelNextFrame()},stop:function(){this.pause(),this.currentTime=0,this.renderer.duration=NaN},toggle:function(){this.wantsToPlay?this.pause():this.play()},beginFinishingPlaybackEarly:function(){this.recipe.beginFinishingPlaybackEarly(this)},_stopWhenAnotherPlayerStarts:l.a.observer("_constructor.activeInstance",function(e){e&&e!==this&&(this.stop(),this.renderer.reduceMemoryFootprint())}),_constructor:l.a.observableProperty(function(){return p}),_stopPlaybackWhenItemsLoadOrUnload:l.a.observer("video","photo",function(){!this.isPlaying||this.playbackStyle===h.a.LOOP&&this.autoplay||this.stop()}),addEventListener:function(e,t,r){var i=this.element;i.addEventListener.call(i,e,t,r)},removeEventListener:function(e,t,r){var i=this.element;i.removeEventListener.call(i,e,t,r)},_nextFrame:function(){var e=Date.now(),t=(e-this._lastFrameNow)*this.playbackRate;this._lastFrameNow=e,this.currentTime===this.renderedTime&&(this.currentTime+=t/1e3),this.recipe&&this.recipe.continuePlayback(this)},_cancelNextFrame:function(){cancelAnimationFrame(this._rafID)},updateSize:l.a.boundFunction(function(e,t){if(this.photoWidth&&this.photoHeight){var i=!0===e?void 0:e,n=!0===e?e:void 0;if(isNaN(i)||isNaN(t)?(i=this.element.offsetWidth,t=this.element.offsetHeight):(i=Math.round(i),t=Math.round(t),this.element.style.width=i+"px",this.element.style.height=t+"px"),i&&t){if(!(this._lastUpdateChangeToken!==(this._lastUpdateChangeToken=i+":"+t))&&!n)return!1;var a=r.i(u.a)(this.photoWidth,this.photoHeight,i,t),o=Math.ceil(a.height),s=Math.ceil(a.width),l=Math.floor(i/2-s/2),d=Math.round(t/2-o/2),c=this.renderer;c.element.style.top=d+"px",c.element.style.left=l+"px",c.updateSize(s,o),this.displayWidth=i,this.displayHeight=t,this.nativeControls&&this.nativeControls.updateToRendererLayout(l,d,s,o)}}}),_dispatchPhotoLoadEventOnNewPhoto:l.a.observer("photo",function(e){e&&this.dispatchEvent(r.i(d.c)())}),_dispatchVideoLoadEventOnNewVideo:l.a.observer("video",function(e){e&&this.dispatchEvent(r.i(d.d)())}),throwError:function(e){this.dispatchEvent(r.i(d.e)({error:e,errorCode:e.errCode}))}}),f=document.createElement("div");t.a=p},function(e,t,r){"use strict";function i(){f=!1}function n(){}function a(e,t){return-(e.importance-t.importance)||e.number-t.number}function o(e,t){for(var r=0,i=e.length,n=0;n=this.frameTimes.length)return this.duration;var t=0|e,r=Math.ceil(e);if(t===r)return this.frameTimes[t];var i=this.frameTimes[t],n=r=u&&l.numberr&&c.number<=r+2&&f;if(h||(p=!1),p){if(!this._isPlaying){this._isPlaying=!0;try{var v=this.video.play();v&&v.then instanceof Function&&v.then(n,i)}catch(e){f=!1}}this._expectedNextSeenFrameNumber=c.number,this._scheduleArtificialSeek()}else this._isPlaying&&(this._isPlaying=!1,this.video.pause()),this._expectedNextSeenFrameNumber=NaN,this.video.currentTime=c.time+1e-4,this._isSeeking=!0}}),_frameWillDispose:function(e){this._removePendingFrame(e)},_removePendingFrame:function(e){o(this._pendingFrames,e),this._pendingFrames.length||this._unscheduleArtificialSeek()}});t.a=v},function(e,t,r){"use strict";function i(e){e.container=document.createElement("div"),e.container.frame=e,e.container.innerHTML='
',e.textBox=e.container.lastChild,e.container.insertBefore(e.image,e.textBox),e.image.style.position="absolute",e.container.style.cssText="position:relative; display:inline-block; border: 1px solid black;";var t=e._debug_aspect||(e._debug_aspect=e.videoDecoder&&(e.videoDecoder.videoWidth>e.videoDecoder.videoHeight?"landscape":"portrait"));e.container.style.width=e.image.style.width="landscape"===t?"40px":"30px",e.container.style.height=e.image.style.height="landscape"===t?"30px":"40px",document.body.appendChild(e.container)}var n=r(12),a=r(48),o=r(5),s=r(0),u=r(46),l=r(2);r.d(t,"a",function(){return d});var d=s.a.Object.extend(u.a,a.a,{staticMembers:{getPoolingCacheKey:function(e,t){return"f"+t+"_in_"+e.id}},container:null,image:null,_context:null,number:-1,time:-1,importance:0,videoDecoder:null,readyState:0,_poolingCacheKey:null,_debugShowInDOM:n.a,lacksOwnPixelData:!1,_postDispose:function(){this.image.width=this.image.height=0},get backingFrame(){return this.lacksOwnPixelData?this.videoDecoder.getNearestDecodedFrame(this.number)||this:this},init:function(){this._postDispose=this._postDispose.bind(this);var e=this.image=document.createElement("canvas");this._context=this.image.getContext("2d"),this._super(),this._debugShowInDOM?i(this):h&&(h.appendChild(e),e.style.cssText="position: absolute; top: 0px; width:1px; height: 1px; display: inline-block;",e.style.left=c+++"px")},initFromPool:function(e,t){clearTimeout(this._postDisposalTimeout),this.videoDecoder=e,this.number=t,this.time=e.frameTimes[t],this._debugShowInDOM&&(this.textBox.innerHTML=this.number)},dispose:function(){this.resetReadiness(),this.videoDecoder._frameWillDispose(this),this.number=this.time=-1,this.importance=0,this.videoDecoder=null,this.readyState=0,this.lacksOwnPixelData=!1,this._postDisposalTimeout=setTimeout(this._postDispose,3e3),this.constructor._disposeInstance(this),this._debugShowInDOM&&(this.textBox.innerHTML="x",this.textBox.style.color="#FF0000",this._context.clearRect(0,0,this.image.width,this.image.height))},didPend:function(){this.readyState=1,this._debugShowInDOM&&(this.textBox.style.color="#FF8800")},didDecode:function(){this.obtainPixelData(),this.readyState=2,this.resolveReadiness(this),this._debugShowInDOM&&(this.textBox.style.color="#00FF00")},obtainPixelData:function(){var e=this.image,t=this._context,r=this.videoDecoder,i=r.videoRotation,n=r.videoWidth,a=r.videoHeight,o=i%180==0?n:a,s=i%180==0?a:n;e.width===n&&e.height===a||(e.width=n,e.height=a),l.a.isFirefox&&t.getImageData(0,0,1,1);for(var u=0;u=2,a=0,o=e.length;a>r)*(0!=(i&1<0)switch(t.metaData.values.items[m]){case 1:g=h.a.LOOP;break;case 2:g=h.a.BOUNCE;break;case 3:g=h.a.EXPOSURE}this.effectType=g}}),y=[0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,64,0,0,0],m=[[1,0,0,0,1,0,0,0,1],[0,1,0,-1,0,0,0,0,1],[-1,0,0,0,-1,0,0,0,1],[0,-1,0,1,0,0,0,0,1]];t.a=v},function(e,t,r){"use strict";var i=r(4),n=r(1),a=r(2),o=a.a.isSafari,s=i.a.create({correspondingPlaybackStyle:n.a.FULL,get minimumShortenedDuration(){return this.enterDuration+this.exitDuration+.01},get spontaneousFinishDuration(){return this.exitDuration},enterDuration:1/3,exitDuration:.5,videoBeginTime:.15,zoomScaleFactor:1.075,blurRadius:5,blurRadiusStep:.2,requiresInterpolation:!0,quantizeRadius:function(e){return this.blurRadiusStep?Math.round(e/this.blurRadiusStep)*this.blurRadiusStep:e},easeInOut:function(e){return e<0?0:e>1?1:.5-.5*Math.cos(e*Math.PI)},calculateAnimationDuration:function(e,t,r){var i=t?t+this.videoBeginTime+this.exitDuration:0;return Math.max(0,Math.min(e||1/0,i))},getEntranceExitParameter:function(e,t){return Math.min(Math.max(0,Math.min(1,1-this.easeInOut((e-(t-this.exitDuration))/this.exitDuration))),1-Math.max(0,Math.min(1,1-this.easeInOut(e/this.enterDuration))))||0},getTransform:function(e,t,r,i){var n=arguments.length>4&&void 0!==arguments[4]?arguments[4]:1,a=arguments.length>5&&void 0!==arguments[5]?arguments[5]:1,o=arguments.length>6&&void 0!==arguments[6]?arguments[6]:1,s=1+(this.zoomScaleFactor-1)*this.getEntranceExitParameter(e,t),u=-(s-1)/2*r,l=-(s-1)/2*i,d=Math.round(u*devicePixelRatio)/devicePixelRatio,c=Math.round(l*devicePixelRatio)/devicePixelRatio;return Math.abs(s-n)<1e-5?"translate3d("+d+"px, "+c+"px, 0) scale3d("+a+", "+o+", 1)":u||l||s?"translate3d("+u+"px, "+l+"px, 0) scale3d("+s+", "+s+", 1)":"translate3d(0, 0, 0)"},photo:i.a.PhotoIngredient.create({opacity:i.a.computedStyle(function(e){if(ethis.recipe.enterDuration&&e1?1:.5-.5*Math.cos(e*Math.PI)},calculateAnimationDuration:function(e,t,r){var i=t?t-r+this.exitBlurDuration:0;return Math.max(0,Math.min(e||1/0,i))},photo:i.a.PhotoIngredient.create({hideDuration:.06,get returnDuration(){return this.recipe.exitBlurDuration},opacity:i.a.computedStyle(function(e){if(ethis.hideDuration&&e0?"none":""})}),video:i.a.InterpolatedVideoIngredient.create({lookaheadTime:.01+7/15,videoTimeAtTime:function(e){return e%this.renderer.duration},prepareVideoFramesFromTime:function(e){this.retainFramesForTime(e,e+this.lookaheadTime)},display:i.a.computedStyle(function(e){return""})}),beginFinishingPlaybackEarly:function(e){e.autoplay||(e.isPlaying?e.pause():e.wantsToPlay=!1)},continuePlayback:function(e){var t=e.currentTime,r=e.duration;t>=r&&(e.currentTime=t%r),e._rafID=requestAnimationFrame(e._nextFrame.bind(e))}}));t.a=a},function(e,t,r){"use strict";var i=r(4),n=r(36),a=r(1);n.a.register();var o=i.a.create({correspondingPlaybackStyle:a.a.LOOP,photo:i.a.PhotoIngredient.create({display:i.a.computedStyle(function(e){return this.isPlaying||e>0?"none":""})}),video:i.a.VideoIngredient.create({display:i.a.computedStyle(function(e){return""})}),beginFinishingPlaybackEarly:function(e){e.autoplay||(e.isPlaying?e.pause():e.wantsToPlay=!1)},continuePlayback:function(e){var t=e.currentTime,r=e.duration;t>=r&&(e.currentTime=t%r),e._rafID=requestAnimationFrame(e._nextFrame.bind(e))},requestMoreCompatibleRecipe:function(e){return i.a.registerRecipeWithPlaybackStyle(n.a,this.correspondingPlaybackStyle),n.a}});t.a=o},function(e,t,r){"use strict";var i=r(0),n=r(41),a=r(1),o=r(13),s=n.a.extend(o.a,{_loCanvas:null,_hiCanvas:null,backingScaleFactor:1,setUpForRender:function(){var e=this.element,t=(this.isPlaying,this.renderer),r=t.autoplay,n=t.parentView,o=t.playbackStyle,s=t.video;if(!this._loCanvas||!this._hiCanvas){e.innerHTML&&(e.innerHTML="");var u=this._loCanvas=i.a.canvasPool.get(),l=this._hiCanvas=i.a.canvasPool.get();u._context=u.getContext("2d"),l._context=l.getContext("2d"),u.style.cssText=l.style.cssText="position: absolute; left: 0; top: 0; width: 100%; height: 100%; transform: translateZ(0);",e.appendChild(u),e.appendChild(l),this._swapCanvases()}e.className="lpk-render-layer lpk-video",e.style.position="absolute",e.style.transformOrigin="0 0",e.style.zIndex=1,this._super(),o===a.a.LOOP&&(this.shouldLoop=!0),this.shouldLoop&&requestAnimationFrame(function(){s.currentTime=-1,r&&n.play()}),window.test=this},updateSize:function(e,t){if(!arguments.length)return this._super();this._super(e,t);var r=Math.ceil(e*this.backingScaleFactor),i=Math.ceil(t*this.backingScaleFactor);this.backingScaleX=r/e,this.backingScaleY=i/t,this.element.style.width=r+"px",this.element.style.height=i+"px",this._loCanvas&&this._hiCanvas&&(this._loCanvas.width=this._hiCanvas.width=r*devicePixelRatio,this._loCanvas.height=this._hiCanvas.height=i*devicePixelRatio,this._loCanvas._drawnFrameNumber=this._hiCanvas._drawnFrameNumber=-1,this.renderAtTime())},renderAtTime:function(e){if(!arguments.length)return this._super();this._super(e);var t=this.backingScaleX,r=this.backingScaleY;1===t&&1===r||(this.element.style.transform+=" scale3d("+1/t+", "+1/r+", 1)")},renderFramePair:function(e,t,r){(e&&this._hiCanvas._drawnFrameNumber===e.number||t&&this._loCanvas._drawnFrameNumber===t.number)&&this._swapCanvases(),this._putFrameInCanvasIfNeeded(e,this._loCanvas),this._putFrameInCanvasIfNeeded(t,this._hiCanvas),t&&(this._hiCanvas.style.opacity=r)},_swapCanvases:function(){var e=this._hiCanvas;this._hiCanvas=this._loCanvas,this._loCanvas=e,this._loCanvas.style.opacity="",this._loCanvas.style.zIndex=1,this._hiCanvas.style.zIndex=2},_putFrameInCanvasIfNeeded:function(e,t){t._drawnFrameNumber!==(t._drawnFrameNumber=e?e.number:-1)&&(t.setAttribute("data-frame-number",t._drawnFrameNumber.toString()),e?t._context.drawImage(e.image,0,0,t.width,t.height):t._context.clearRect(0,0,t.width,t.height))},dispose:function(){this._super(),this._loCanvas&&i.a.canvasPool.ret(this._loCanvas),this._hiCanvas&&i.a.canvasPool.ret(this._hiCanvas)},tearDownFromRender:function(){var e=this.renderer,t=e.parentView;this.shouldLoop=!1,t&&t.stop(),this._clearAllRetainedFrames(),this._super()}});t.a=s},function(e,t,r){"use strict";var i=r(42),n=r(13),a=r(49),o=i.a.extend(n.a,{tagName:"canvas",get _canvas(){return this.element},get _context(){return this.__context||(this.__context=this._canvas.getContext("2d"))},init:function(){this._super.apply(this,arguments),this.element.className="lpk-render-layer lpk-photo",this.element.style.position="absolute",this.element.style.width=this.element.style.height="100%",this.element.style.transformOrigin="0 0",this.element.style.zIndex=2},tearDownFromRender:function(){this._super(),this._canvas.width=this._canvas.height=0},updateSize:function(e,t){if(!arguments.length)return this._super();this._super(e,t);var i=Math.ceil(e*devicePixelRatio),n=Math.ceil(t*devicePixelRatio),o=this.photo,s=this._canvas;this._lastPhoto===(this._lastPhoto=o)&&s.width===i&&s.height===n||(s.width=i,s.height=n,o&&r.i(a.a)(this._context,o,0,0,i,n))}});t.a=o},function(e,t,r){"use strict";var i=r(0),n=r(2),a=r(13),o=r(43),s=o.a.extend(a.a,{_isPlayingChanged:i.a.observer("isPlaying",function(e){this._video&&(e?(this.duration=1/0,this.play()):this.pause())}),_isVisible:!1,applyStyles:function(){var e=this.element,t=this.video,r=this.videoRotation,i=t.videoHeight,n=t.videoWidth,a=1;[90,270].indexOf(r)>=0&&(a=n/i);var o="\n height: 100%;\n position: absolute;\n width: 100%;\n -moz-transform: scale("+a+") rotate("+r+"deg);\n -webkit-transform: scale("+a+") rotate("+r+"deg);\n -o-transform: scale("+a+") rotate("+r+"deg);\n -ms-transform: scale("+a+") rotate("+r+"deg);\n transform: scale("+a+") rotate("+r+"deg);\n z-index: 1;\n ";e.setAttribute("style",o),e.className="lpk-render-layer lpk-video",t.style.height="100%",t.style.width="100%"},cleanupElement:function(){var e=this.element,t=this.renderer,r=this._video,i=t.parentView;e.innerHtml&&(e.innerHtml=""),r&&(r.loop=!1,r.muted=!1,r.removeEventListener("pause",this.playIfPlaying)),i&&i.stop(),delete this._video},pause:function(){var e=this._isVisible,t=this._video;e&&t.pause()},play:function(){if(this._isVisible){var e=this._video,t=e.play();t?t.catch(this._handlePlayFailure):n.a.isIE||n.a.isEdge||(e.pause(),setTimeout(this._handlePlayFailure))}},_handlePlayFailure:i.a.boundFunction(function(){this.renderer.requestMoreCompatibleRecipe()}),playIfPlaying:i.a.boundFunction(function(){var e=this.isPlaying,t=this._video;if(e&&t.paused){var r=t.play();r&&r.catch(function(){})}}),setUpForRender:function(){var e=this.element,t=(this.isPlaying,this.renderer),r=t.autoplay,i=t.parentView,n=t.video;this.cleanupElement(),e.appendChild(n),this.applyStyles(),n.loop=!0,n.muted=!0,this._video=n,this._isVisible=!0,this._super(),r&&(n.addEventListener("pause",this.playIfPlaying),i.play())},tearDownFromRender:function(){this.cleanupElement(),this._isVisible=!1,this._super()}});t.a=s},function(e,t,r){"use strict";function i(e){e.retain()}function n(e){e.release()}var a=r(0),o=r(7),s=r(17),u=o.a.extend({videoDecoder:a.a.proxyProperty("renderer.videoDecoder"),videoDuration:a.a.proxyProperty("videoDecoder.duration"),canRender:a.a.proxyProperty({readOnly:!0,proxyPath:"videoDecoder.canProvideFrames"}),init:function(){this._super.apply(this,arguments);var e=this.layerName,t=this.recipe;this._framePrepIDKey=t.name+"_"+e+"_framePrepID"},videoTimeAtTime:function(e){return e},_videoTimeAtTime:function(e){return isNaN(e)?e:this.videoTimeAtTime(e)},prepareToRenderAtTime:function(e){var t=this._currentPrepID=++l;if(!this.canRender)return!1;this.prepareVideoFramesFromTime(e);for(var r,i=this._retainedFrames,n=0,a=0;r=i[a];a++)2!==r.readyState&&(r[this._framePrepIDKey]=t,r.onReadyOrFail(this._frameDidPrepare),n++);return this._preppingFrameCount=n,!n},reduceMemoryFootprint:function(){this._super(),this._clearAllRetainedFrames()},_clearAllRetainedFrames:function(){this._clearExtraRetainedFrames(),this._clearRetainedInstantaneousFrames()},_clearExtraRetainedFrames:function(){var e=this._retainedFrames;e&&(e.forEach(n),e.length=0)},_clearRetainedInstantaneousFrames:function(){this._retainedLoFrame&&this._retainedLoFrame.release(),this._retainedHiFrame&&this._retainedHiFrame.release(),this._retainedLoFrame=this._retainedHiFrame=null},_frameDidPrepare:a.a.boundFunction(function(e){e[this._framePrepIDKey]===this._currentPrepID&&(e[this._framePrepIDKey]=void 0,--this._preppingFrameCount||this.renderer.prepareToRenderAtTime(this.renderer.currentTime))}),prepareVideoFramesFromTime:function(e){this.retainFramesForTime(e)},canRenderAtTime:function(e){if("none"===this.display(e))return!0;if(!this.canRender)return!1;for(var t,r=!0,i=this.requiredFramesForTime(e),n=0;t=i[n];n++)r=r&&2===t.readyState,t.retain().release();return r},renderAtTime:function(e){if(!arguments.length)return this._super();if("none"===this.display(e))return this._clearRetainedInstantaneousFrames(),this._super(e);var t=this._videoTimeAtTime(e),r=this.requiredFramesForVideoTime(t),i=r[0]||null,n=r[1]||null;if(i&&i.retain(),n&&n.retain(),this._clearRetainedInstantaneousFrames(),this._retainedLoFrame=i,this._retainedHiFrame=n,i&&(i=i.backingFrame),n&&(n=n.backingFrame),i&&n&&i.number>n.number){var a=i;n=i,i=a}i===n&&(n=null);var o=!i||n?this.videoDecoder.fractionalIndexForTime(t):i.frameNumber,s=o-(0|o);this.renderFramePair(i,n,s),this._super(e)},renderFramePair:function(){},requiredFramesForVideoTime:function(e,t,r){isNaN(t)&&(t=e);var i=this.videoDecoder,n=this.videoDuration,a=i.frameCount,o=d;if(o.length=0,t<0||e>n||isNaN(e)||isNaN(t))return o;var u=Math.max(0,Math.floor(i.fractionalIndexForTime(e))),l=Math.min(i.frameCount,Math.ceil(i.fractionalIndexForTime(t))),c=l=0;l--){var d=u[l],c=d.time;(!o||c>a/2)&&(n(d),u.splice(l,1))}u.push.apply(u,s)},retainFramesForTime:function(e,t,r){return this.retainFramesForVideoTime(this._videoTimeAtTime(e),this._videoTimeAtTime(t),r)},dispose:function(){this.retainFramesForVideoTime(NaN),this._super()}}),l=1,d=[];t.a=u},function(e,t,r){"use strict";var i=r(7),n=r(0),a=i.a.extend({isPlaying:n.a.proxyProperty({readOnly:!0,proxyPath:"renderer.parentView.isPlaying"}),photo:n.a.proxyProperty({readOnly:!0,proxyPath:"renderer.photo"}),canRender:n.a.proxyProperty("photo"),canRenderAtTime:function(e){var t=this.photo;return!("none"!==this.display(e)&&(!t||t instanceof Image&&!t.complete))}});t.a=a},function(e,t,r){"use strict";var i=r(7),n=r(0),a=i.a.extend({canRender:n.a.proxyProperty({readOnly:!0,proxyPath:"video"}),isPlaying:n.a.proxyProperty({readOnly:!0,proxyPath:"renderer.parentView.isPlaying"}),video:n.a.proxyProperty({readOnly:!0,proxyPath:"renderer.video"}),videoRotation:n.a.proxyProperty({readOnly:!0,proxyPath:"renderer.provider.videoRotation"})});t.a=a},function(e,t,r){"use strict";function i(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function n(e){var t=r.i(o.a)(e),i=l.get(t);if(i)return i;var n=e.map(function(e){if("i"===e[0]&&h(e[1]))return"I"+e.substring(1)});return e=e.concat(n.filter(function(e){return!!e})),i=new RegExp(e.join("|"),"g"),l.set(t,i),i}function a(e,t){var r=e.charCodeAt(0),i=t.charCodeAt(0),n=new Map;return function(e){var t=n.get(e);if(void 0!==t)return t;var a=e.charCodeAt(0);return t=a>=r&&a<=i,n.set(e,t),t}}var o=r(20),s=function(){function e(e,t){for(var r=0;r>>1,this.h>>>1)}},{key:"length",value:function(){return this.w*this.h}}])}(),function(){function e(t,r,n){i(this,e),this.bytes=new Uint8Array(t),this.start=r||0,this.pos=this.start,this.end=r+n||this.bytes.length}return s(e,[{key:"readU8Array",value:function(e){if(this.pos>this.end-e)return null;var t=this.bytes.subarray(this.pos,this.pos+e);return this.pos+=e,t}},{key:"readU32Array",value:function(e,t,r){if(t=t||1,this.pos>this.end-e*t*4)return null;if(1===t){for(var i=new Uint32Array(e),n=0;n>24}},{key:"readU8",value:function(){return this.pos>=this.end?null:this.bytes[this.pos++]}},{key:"read16",value:function(){return this.readU16()<<16>>16}},{key:"readU16",value:function(){if(this.pos>=this.end-1)return null;var e=this.bytes[this.pos+0]<<8|this.bytes[this.pos+1];return this.pos+=2,e}},{key:"read24",value:function(){return this.readU24()<<8>>8}},{key:"readU24",value:function(){var e=this.pos,t=this.bytes;if(e>this.end-3)return null;var r=t[e+0]<<16|t[e+1]<<8|t[e+2];return this.pos+=3,r}},{key:"peek32",value:function(e){var t=this.pos,r=this.bytes;if(t>this.end-4)return null;var i=r[t+0]<<24|r[t+1]<<16|r[t+2]<<8|r[t+3];return e&&(this.pos+=4),i}},{key:"read32",value:function(){return this.peek32(!0)}},{key:"readU32",value:function(){return this.peek32(!0)>>>0}},{key:"read4CC",value:function(){var e=this.pos;if(e>this.end-4)return null;for(var t="",r=0;r<4;r++)t+=String.fromCharCode(this.bytes[e+r]);return this.pos+=4,t}},{key:"readFP16",value:function(){return this.read32()/65536}},{key:"readFP8",value:function(){return this.read16()/256}},{key:"readISO639",value:function(){for(var e=this.readU16(),t="",r=0;r<3;r++){var i=e>>>5*(2-r)&31;t+=String.fromCharCode(i+96)}return t}},{key:"readUTF8",value:function(e){for(var t="",r=0;rthis.end)&&a("Index out of bounds (bounds: [0, "+this.end+"], index: "+e+")."),this.pos=e}},{key:"subStream",value:function(t,r){return new e(this.bytes.buffer,t,r)}},{key:"uint",value:function(e){for(var t=this.position,r=t+e,i=0,n=t;n0&&(T.name=e.readUTF8(l));break;case"minf":o.name="Media Information Box",a();break;case"stbl":o.name="Sample Table Box",a();break;case"stsd":var x=o;x.name="Sample Description Box",t(),x.sd=[],e.readU32(),a();break;case"avc1":var S=o;e.reserved(6,0),S.dataReferenceIndex=e.readU16(),n(0==e.readU16()),n(0==e.readU16()),e.readU32(),e.readU32(),e.readU32(),S.width=e.readU16(),S.height=e.readU16(),S.horizontalResolution=e.readFP16(),S.verticalResolution=e.readFP16(),n(0==e.readU32()),S.frameCount=e.readU16(),S.compressorName=e.readPString(32),S.depth=e.readU16(),n(65535==e.readU16()),a();break;case"mp4a":var w=o;if(e.reserved(6,0),w.dataReferenceIndex=e.readU16(),w.version=e.readU16(),0!==w.version){i();break}e.skip(2),e.skip(4),w.channelCount=e.readU16(),w.sampleSize=e.readU16(),w.compressionId=e.readU16(),w.packetSize=e.readU16(),w.sampleRate=e.readU32()>>>16,a();break;case"esds":o.name="Elementary Stream Descriptor",t(),i();break;case"avcC":var O=o;O.name="AVC Configuration Box",O.configurationVersion=e.readU8(),O.avcProfileIndicaation=e.readU8(),O.profileCompatibility=e.readU8(),O.avcLevelIndication=e.readU8(),O.lengthSizeMinusOne=3&e.readU8(),n(3==O.lengthSizeMinusOne,"TODO"),u=31&e.readU8(),O.sps=[];for(var C=0;C=8,"Cannot parse large media data yet."),j.data=e.readU8Array(r());break;case"mebx":o.name="Mebx",a();break;case"meta":o.name="Metadata",a();break;case"keys":var U=o;U.name="Metadata Item Keys",t();var V=U.keyCount=e.read32(),N=U.offset-U.size;U.keyList=new Map;for(var B=1;B<=V;B++){var z=e.read32()-8;z<1||z>N||(e.skip(4),U.keyList.set(e.readUTF8(z),B))}this.metaData.keys=U;break;case"ilst":var H=o;H.name="Metadata Item List",H.items=[];for(var K=H.offset+H.size;e.position0){var s=t[a-1],u=o.firstChunk-s.firstChunk,l=s.samplesPerChunk*u;if(!(e>=l))return{index:i+Math.floor(e/s.samplesPerChunk),offset:e%s.samplesPerChunk};if(e-=l,a===t.length-1)return{index:i+u+Math.floor(e/o.samplesPerChunk),offset:e%o.samplesPerChunk};i+=u}}n(!1)}},{key:"chunkToOffset",value:function(e){return this.trak.mdia.minf.stbl.stco.table[e]}},{key:"sampleToOffset",value:function(e){var t=this.sampleToChunk(e);return this.chunkToOffset(t.index)+this.sampleToSize(e-t.offset,t.offset)}},{key:"timeToSample",value:function(e){for(var t=this.trak.mdia.minf.stbl.stts.table,r=0,i=0;i=n))return r+Math.floor(e/t[i].delta);e-=n,r+=t[i].count}}},{key:"sampleToTime",value:function(e){for(var t=this.trak.mdia.minf.stbl.stts.table,r=0,i=0,n=0;n0;){var a=new u(t.buffer,r).readU32();n.push(t.subarray(r+4,r+a+4)),r=r+a+4}return n}}]),e}()},function(e,t,r){"use strict";var i={staticMembers:{_pool:null,_cache:null,init:function(){this._pool=[],this._cache={},this._super()},getPoolingCacheKey:function(){throw"Must implement `getPoolingCacheKey` to use PoolCaching."},getCached:function(){var e=this.getPoolingCacheKey.apply(this,arguments),t=this._cache[e];return t||(t=this._cache[e]=this._pool.pop()||this.create(),t._poolingCacheKey=e,t.initFromPool.apply(t,arguments)),t},peekCached:function(){var e=this.getPoolingCacheKey.apply(this,arguments);return this._cache[e]||null},_disposeInstance:function(e){delete this._cache[e._poolingCacheKey],e._poolingCacheKey=void 0,e._poolingLifecycleCount=1+(0|e._poolingLifecycleCount),this._pool.push(e)}},dispose:function(){},_poolingCacheKey:null,initFromPool:function(){},_retainCount:0,retain:function(){return this._retainCount++,this},release:function(){return this._retainCount--,this._retainCount||this.dispose(),this}};t.a=i},function(e,t,r){"use strict";function i(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function n(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function a(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}var o=r(19);r.d(t,"a",function(){return d}),r.d(t,"b",function(){return h}),r.d(t,"c",function(){return f}),r.d(t,"d",function(){return y});var s=function(){function e(e,t){for(var r=0;r2?r-2:0),p=2;p=1)return e.drawImage.apply(e,i.apply(h,arguments)),!0;var R=void 0;if(f){R="_cachedSmoothDownsample_from"+g+","+b+","+_+","+P+"@"+F+"x";var L=t[R];if(L)return e.drawImage(L,0,0,L.width,L.height,k,T,x,S),!0}if(v)return e.drawImage.apply(e,i.apply(h,arguments)),!1;var E=1,A=_,I=P,D=Math.max(Math.pow(2,Math.ceil(Math.log(A)/Math.log(2))),a.width),M=Math.max(Math.pow(2,Math.ceil(Math.log(I)/Math.log(2))),a.height);for(a.width===D&&a.height===M||(a.width=s.width=D,a.height=s.height=M),o.drawImage(t,g,b,_,P,0,0,_,P);E>F;){u.drawImage(a,0,0,A,I,0,0,A=Math.ceil(A/2),I=Math.ceil(I/2)),o.clearRect(0,0,A,I);var j=a;a=s,s=j;var U=o;o=u,u=U,E/=2}if(f){var V=document.createElement("canvas");V.width=A,V.height=I,V.getContext("2d").drawImage(a,0,0),t[R]=V}return e.drawImage(a,0,0,A,I,k,T,x,S),o.clearRect(0,0,_,P),u.clearRect(0,0,_,P),!0}};c.usingCache=function(){return l=!0,this},c.avoidingWorkIf=function(e){return d=e,this};var h=[];t.a=c},function(e,t,r){"use strict";function i(){var e="_callbacksForEventHandler"+ ++n;return function(t){var r=this[e]||(this[e]=[]);if("function"==typeof t)return r.push(t);if(r)for(var i=0,n=r.length;ig;return f=f||{},f.width=b?h:p*m,f.height=b?h/m:p,f}function n(e,t,r,n,a){return i(!1,e,t,r,n,a,arguments.length)}t.a=n},function(e,t,r){"use strict";t.a="current"},function(e,t,r){"use strict";t.a="Mcurrent"},function(e,t,r){"use strict";t.a="1.5.6"}])}); -//# sourceMappingURL=resources/livephotoskit.js.map -L.Photo = L.FeatureGroup.extend({ - options: { - icon: { - iconSize: [40, 40], - }, - }, - - initialize: function (photos, options) { - L.setOptions(this, options); - L.FeatureGroup.prototype.initialize.call(this, photos); - }, - - addLayers: function (photos) { - if (photos) { - for (var i = 0, len = photos.length; i < len; i++) { - this.addLayer(photos[i]); - } - } - return this; - }, - - addLayer: function (photo) { - L.FeatureGroup.prototype.addLayer.call(this, this.createMarker(photo)); - }, - - createMarker: function (photo) { - var marker = L.marker(photo, { - icon: L.divIcon( - L.extend( - { - html: - '​", - className: "leaflet-marker-photo", - }, - photo, - this.options.icon - ) - ), - title: photo.caption || "", - }); - marker.photo = photo; - return marker; - }, -}); - -L.photo = function (photos, options) { - return new L.Photo(photos, options); -}; - -if (L.MarkerClusterGroup) { - L.Photo.Cluster = L.MarkerClusterGroup.extend({ - options: { - featureGroup: L.photo, - maxClusterRadius: 100, - showCoverageOnHover: false, - iconCreateFunction: function (cluster) { - return new L.DivIcon( - L.extend( - { - className: "leaflet-marker-photo", - html: - '" + - cluster.getChildCount() + - "", - }, - this.icon - ) - ); - }, - icon: { - iconSize: [40, 40], - }, - }, - - initialize: function (options) { - options = L.Util.setOptions(this, options); - L.MarkerClusterGroup.prototype.initialize.call(this); - this._photos = options.featureGroup(null, options); - }, - - add: function (photos) { - this.addLayer(this._photos.addLayers(photos)); - return this; - }, - - clear: function () { - this._photos.clearLayers(); - this.clearLayers(); - }, - }); - - L.photo.cluster = function (options) { - return new L.Photo.Cluster(options); - }; -} - -"use strict"; - -var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); - -var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; - -var _templateObject = _taggedTemplateLiteral(["

", "

"], ["

", "

"]), - _templateObject2 = _taggedTemplateLiteral(["

", "\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t

"], ["

", "\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t

"]), - _templateObject3 = _taggedTemplateLiteral(["

", "\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t

"], ["

", "\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t

"]), - _templateObject4 = _taggedTemplateLiteral([""], [""]), - _templateObject5 = _taggedTemplateLiteral(["

", " ", "

"], ["

", " ", "

"]), - _templateObject6 = _taggedTemplateLiteral(["

", " $", " ", " ", "

"], ["

", " $", " ", " ", "

"]), - _templateObject7 = _taggedTemplateLiteral(["

", "

"], ["

", "

"]), - _templateObject8 = _taggedTemplateLiteral(["\n\t
\n\t\t

", "\n\t\t\n\t\t\t\n\t\t\n\t\t
\n\t\t", "\n\t\t

\n\t
"], ["\n\t
\n\t\t

", "\n\t\t\n\t\t\t\n\t\t\n\t\t
\n\t\t", "\n\t\t

\n\t
"]), - _templateObject9 = _taggedTemplateLiteral(["\n\t
\n\t\t

"], ["\n\t

\n\t\t

"]), - _templateObject10 = _taggedTemplateLiteral(["\n\t\t\t

\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t

", "

\n\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t

", "

\n\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t

", "

\n\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t

", "

\n\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t

", "

\n\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t

", "

\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t\t

\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t

", "

\n\t\t\t\t
\n\t\t\t
\n\t\t"], ["\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t

", "

\n\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t

", "

\n\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t

", "

\n\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t

", "

\n\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t

", "

\n\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t

", "

\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t\t

\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t

", "

\n\t\t\t\t
\n\t\t\t
\n\t\t"]), - _templateObject11 = _taggedTemplateLiteral(["
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t

\n\t\t\t\t\t\t
"], ["
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t

\n\t\t\t\t\t\t
"]), - _templateObject12 = _taggedTemplateLiteral(["?albumIDs=", ""], ["?albumIDs=", ""]), - _templateObject13 = _taggedTemplateLiteral(["

", " '$", "' ", " '$", "'?

"], ["

", " '$", "' ", " '$", "'?

"]), - _templateObject14 = _taggedTemplateLiteral(["

", " '$", "'?

"], ["

", " '$", "'?

"]), - _templateObject15 = _taggedTemplateLiteral(["

", " '$", "' ", "

"], ["

", " '$", "' ", "

"]), - _templateObject16 = _taggedTemplateLiteral(["

", " $", " ", "

"], ["

", " $", " ", "

"]), - _templateObject17 = _taggedTemplateLiteral([""], [""]), - _templateObject18 = _taggedTemplateLiteral(["

", "

"], ["

", "

"]), - _templateObject19 = _taggedTemplateLiteral(["
", "
"], ["
", "
"]), - _templateObject20 = _taggedTemplateLiteral(["
"], ["
"]), - _templateObject21 = _taggedTemplateLiteral(["\n\t\t\t
\n\t\t\t\t ", "\n\t\t\t\t ", "\n\t\t\t\t ", "\n\t\t\t\t
\n\t\t\t\t\t

$", "

\n\t\t\t\t\t", "\n\t\t\t\t
\n\t\t\t"], ["\n\t\t\t
\n\t\t\t\t ", "\n\t\t\t\t ", "\n\t\t\t\t ", "\n\t\t\t\t
\n\t\t\t\t\t

$", "

\n\t\t\t\t\t", "\n\t\t\t\t
\n\t\t\t"]), - _templateObject22 = _taggedTemplateLiteral(["\n\t\t\t\t
\n\t\t\t\t\t", "\n\t\t\t\t\t", "\n\t\t\t\t\t", "\n\t\t\t\t\t", "\n\t\t\t\t\t", "\n\t\t\t\t\t", "\n\t\t\t\t\t", "\n\t\t\t\t\t", "\n\t\t\t\t
\n\t\t\t\t"], ["\n\t\t\t\t
\n\t\t\t\t\t", "\n\t\t\t\t\t", "\n\t\t\t\t\t", "\n\t\t\t\t\t", "\n\t\t\t\t\t", "\n\t\t\t\t\t", "\n\t\t\t\t\t", "\n\t\t\t\t\t", "\n\t\t\t\t
\n\t\t\t\t"]), - _templateObject23 = _taggedTemplateLiteral(["\n\t\t\t\t
\n\t\t\t\t\t", "\n\t\t\t\t
"], ["\n\t\t\t\t
\n\t\t\t\t\t", "\n\t\t\t\t
"]), - _templateObject24 = _taggedTemplateLiteral(["\n\t\t\t
\n\t\t\t\t", "\n\t\t\t\t
\n\t\t\t\t\t

$", "

\n\t\t\t"], ["\n\t\t\t
\n\t\t\t\t", "\n\t\t\t\t
\n\t\t\t\t\t

$", "

\n\t\t\t"]), - _templateObject25 = _taggedTemplateLiteral(["", "", ""], ["", "", ""]), - _templateObject26 = _taggedTemplateLiteral(["", ""], ["", ""]), - _templateObject27 = _taggedTemplateLiteral(["\n\t\t\t\t
\n\t\t\t\t", "\n\t\t\t\t", "\n\t\t\t\t", "\n\t\t\t\t
\n\t\t\t\t"], ["\n\t\t\t\t
\n\t\t\t\t", "\n\t\t\t\t", "\n\t\t\t\t", "\n\t\t\t\t
\n\t\t\t\t"]), - _templateObject28 = _taggedTemplateLiteral(["\n\t\t
\n\t\t

$", "

\n\t\t"], ["\n\t\t
\n\t\t

$", "

\n\t\t"]), - _templateObject29 = _taggedTemplateLiteral([""], [""]), - _templateObject30 = _taggedTemplateLiteral(["big"], ["big"]), - _templateObject31 = _taggedTemplateLiteral(["", ""], ["", ""]), - _templateObject32 = _taggedTemplateLiteral(["
", ""], ["
", ""]), - _templateObject33 = _taggedTemplateLiteral(["

", "

"], ["

", "

"]), - _templateObject34 = _taggedTemplateLiteral(["\n\t\t\t

$", "

\n\t\t\t
\n\t\t\t"], ["\n\t\t\t

$", "

\n\t\t\t
\n\t\t\t"]), - _templateObject35 = _taggedTemplateLiteral(["\n\t\t\t\t
\n\t\t\t\t\t", "\n\t\t\t\t\t\n\t\t\t\t\t

\n\t\t\t\t
\n\t\t\t\t"], ["\n\t\t\t\t
\n\t\t\t\t\t", "\n\t\t\t\t\t\n\t\t\t\t\t

\n\t\t\t\t
\n\t\t\t\t"]), - _templateObject36 = _taggedTemplateLiteral(["\n\t\t
\n\t\t\t", "\n\t\t\t\n\t\t\t

\n\t\t
\n\t\t"], ["\n\t\t
\n\t\t\t", "\n\t\t\t\n\t\t\t

\n\t\t
\n\t\t"]), - _templateObject37 = _taggedTemplateLiteral(["$", "", ""], ["$", "", ""]), - _templateObject38 = _taggedTemplateLiteral(["$", ""], ["$", ""]), - _templateObject39 = _taggedTemplateLiteral(["
", "
"], ["
", "
"]), - _templateObject40 = _taggedTemplateLiteral(["
\n\t\t\t

\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t

\n\t\t\tSave\n\t\t\tDelete\n\t\t
\n\t\t"], ["
\n\t\t\t

\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t

\n\t\t\tSave\n\t\t\tDelete\n\t\t
\n\t\t"]), - _templateObject41 = _taggedTemplateLiteral(["
\n\t\t\t

\n\t\t\t\n\t\t\t", "\n\t\t\t\n\t\t\t

\n\t\t\tDelete\n\t\t
\n\t\t"], ["
\n\t\t\t

\n\t\t\t\n\t\t\t", "\n\t\t\t\n\t\t\t

\n\t\t\tDelete\n\t\t
\n\t\t"]), - _templateObject42 = _taggedTemplateLiteral(["\n\t\t\t ", "\n\t\t\t \n\t\t\t
$", "
\n\t\t\t "], ["\n\t\t\t ", "\n\t\t\t \n\t\t\t
$", "
\n\t\t\t "]), - _templateObject43 = _taggedTemplateLiteral(["$", "", ""], ["$", "", ""]), - _templateObject44 = _taggedTemplateLiteral(["\n\t\t", "\n\t\t×\n\t\t", ""], ["\n\t\t", "\n\t\t×\n\t\t", ""]), - _templateObject45 = _taggedTemplateLiteral(["\n\t\t", "", " \n\t\t"], ["\n\t\t", "", " \n\t\t"]), - _templateObject46 = _taggedTemplateLiteral(["\n\t\t", "", " \n\t\t", "", " \n\t\t", "", ""], ["\n\t\t", "", " \n\t\t", "", " \n\t\t", "", ""]), - _templateObject47 = _taggedTemplateLiteral(["\n\t\t", "", "\n\t\t", "", "\n\t\t", "", "\n\t\t", "", ""], ["\n\t\t", "", "\n\t\t", "", "\n\t\t", "", "\n\t\t", "", ""]), - _templateObject48 = _taggedTemplateLiteral(["\n\t\t", "", "\n\t\t"], ["\n\t\t", "", "\n\t\t"]), - _templateObject49 = _taggedTemplateLiteral(["\n\t\t\t\t

Lychee ", "

\n\t\t\t\t\n\t\t\t\t

", "

\n\t\t\t\t

Lychee ", "

\n\t\t\t "], ["\n\t\t\t\t

Lychee ", "

\n\t\t\t\t\n\t\t\t\t

", "

\n\t\t\t\t

Lychee ", "

\n\t\t\t "]), - _templateObject50 = _taggedTemplateLiteral(["\n\t\t\t", "\n\t\t\t
\n\t\t\t\t\n\t\t\t\t

Lychee ", "", "

\n\t\t\t
\n\t\t\t"], ["\n\t\t\t", "\n\t\t\t
\n\t\t\t\t\n\t\t\t\t

Lychee ", "", "

\n\t\t\t
\n\t\t\t"]), - _templateObject51 = _taggedTemplateLiteral([""], [""]), - _templateObject52 = _taggedTemplateLiteral(["

", " '", "'", "

"], ["

", " '", "'", "

"]), - _templateObject53 = _taggedTemplateLiteral(["

", " ", " ", "

"], ["

", " ", " ", "

"]), - _templateObject54 = _taggedTemplateLiteral([""], [""]), - _templateObject55 = _taggedTemplateLiteral(["

", " ", " ", " ", "

"], ["

", " ", " ", " ", "

"]), - _templateObject56 = _taggedTemplateLiteral(["\n\t\t
\n\t\t\t\n\t\t\t

", "

\n\t\t
\n\t"], ["\n\t\t
\n\t\t\t\n\t\t\t

", "

\n\t\t
\n\t"]), - _templateObject57 = _taggedTemplateLiteral(["\n\t\t
\n\t\t\t\n\t\t\t

", "

\n\t\t
\n\t\t
\n\t\t\t\n\t\t\t

", "

\n\t\t
\n\t\t
\n\t\t\t\n\t\t\t

", "

\n\t\t
\n\t\t
\n\t\t\t\n\t\t\t

", "

\n\t\t
\n\t\t
\n\t\t\t\n\t\t\t

", "

\n\t\t
\n\t"], ["\n\t\t
\n\t\t\t\n\t\t\t

", "

\n\t\t
\n\t\t
\n\t\t\t\n\t\t\t

", "

\n\t\t
\n\t\t
\n\t\t\t\n\t\t\t

", "

\n\t\t
\n\t\t
\n\t\t\t\n\t\t\t

", "

\n\t\t
\n\t\t
\n\t\t\t\n\t\t\t

", "

\n\t\t
\n\t"]), - _templateObject58 = _taggedTemplateLiteral(["\n\t\t\t

", "

\n\t\t\t", "\n\t\t\t", "\n\t\t"], ["\n\t\t\t

", "

\n\t\t\t", "\n\t\t\t", "\n\t\t"]), - _templateObject59 = _taggedTemplateLiteral(["\n\t\t\t", "\n\t\t\t

", "

\n\t\t\t", "\n\t\t"], ["\n\t\t\t", "\n\t\t\t

", "

\n\t\t\t", "\n\t\t"]), - _templateObject60 = _taggedTemplateLiteral(["

", "

"], ["

", "

"]), - _templateObject61 = _taggedTemplateLiteral([""], [""]), - _templateObject62 = _taggedTemplateLiteral(["\n\t\t\t\t\n\t\t\t\t\t", "", "\n\t\t\t\t\n\t\t\t"], ["\n\t\t\t\t\n\t\t\t\t\t", "", "\n\t\t\t\t\n\t\t\t"]), - _templateObject63 = _taggedTemplateLiteral(["\n\t\t\t
\n\t\t"], ["\n\t\t\t
\n\t\t"]), - _templateObject64 = _taggedTemplateLiteral(["\n\t\t\t
\n\t\t"], ["\n\t\t\t
\n\t\t"]), - _templateObject65 = _taggedTemplateLiteral(["?photoIDs=", "&kind=", ""], ["?photoIDs=", "&kind=", ""]), - _templateObject66 = _taggedTemplateLiteral(["\n\t\t\t

\n\t\t\t\t", "\n\t\t\t\t
\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\t", "\n\t\t\t\t\n\t\t\t

\n\t\t"], ["\n\t\t\t

\n\t\t\t\t", "\n\t\t\t\t
\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\t", "\n\t\t\t\t\n\t\t\t

\n\t\t"]), - _templateObject67 = _taggedTemplateLiteral(["\n\t\t\n\t"]), - _templateObject69 = _taggedTemplateLiteral(["

", "

"], ["

", "

"]), - _templateObject70 = _taggedTemplateLiteral([", "], [", "]), - _templateObject71 = _taggedTemplateLiteral(["$", ""], ["$", ""]), - _templateObject72 = _taggedTemplateLiteral(["$", ""], ["$", ""]), - _templateObject73 = _taggedTemplateLiteral(["\n\t\t\t\t\t\t \n\t\t\t\t\t\t\t ", "\n\t\t\t\t\t\t\t ", "\n\t\t\t\t\t\t \n\t\t\t\t\t\t "], ["\n\t\t\t\t\t\t \n\t\t\t\t\t\t\t ", "\n\t\t\t\t\t\t\t ", "\n\t\t\t\t\t\t \n\t\t\t\t\t\t "]), - _templateObject74 = _taggedTemplateLiteral(["\n\t\t\t\t \n\t\t\t\t
\n\t\t\t\t\t
", "
\n\t\t\t\t\t ", "\n\t\t\t\t
\n\t\t\t\t "], ["\n\t\t\t\t \n\t\t\t\t
\n\t\t\t\t\t
", "
\n\t\t\t\t\t ", "\n\t\t\t\t
\n\t\t\t\t "]), - _templateObject75 = _taggedTemplateLiteral(["

", "

"], ["

", "

"]), - _templateObject76 = _taggedTemplateLiteral(["

"], ["

"]), - _templateObject77 = _taggedTemplateLiteral(["\n\t\t\t

\n\t\t\t\t", "\n\t\t\t\t\n\t\t\t

\n\t\t"], ["\n\t\t\t

\n\t\t\t\t", "\n\t\t\t\t\n\t\t\t

\n\t\t"]), - _templateObject78 = _taggedTemplateLiteral(["\n\t\t\t
\n\t\t\t\t\n\t\t\t\t

\n\t\t\t\t\t", "\n\t\t\t\t

\n\t\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t

\n\t\t\t\t\t", "\n\t\t\t\t

\n\t\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t

\n\t\t\t\t\t", "\n\t\t\t\t

\n\t\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t

\n\t\t\t\t\t", "\n\t\t\t\t

\n\t\t\t
\n\t\t"], ["\n\t\t\t
\n\t\t\t\t\n\t\t\t\t

\n\t\t\t\t\t", "\n\t\t\t\t

\n\t\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t

\n\t\t\t\t\t", "\n\t\t\t\t

\n\t\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t

\n\t\t\t\t\t", "\n\t\t\t\t

\n\t\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t

\n\t\t\t\t\t", "\n\t\t\t\t

\n\t\t\t
\n\t\t"]), - _templateObject79 = _taggedTemplateLiteral(["url(\"", "\")"], ["url(\"", "\")"]), - _templateObject80 = _taggedTemplateLiteral(["linear-gradient(to bottom, rgba(0, 0, 0, .4), rgba(0, 0, 0, .4)), url(\"", "\")"], ["linear-gradient(to bottom, rgba(0, 0, 0, .4), rgba(0, 0, 0, .4)), url(\"", "\")"]), - _templateObject81 = _taggedTemplateLiteral(["\n\t\t\t
\n\t\t\t

$", "\n\t\t\t\t \n\t\t\t\t \n\t\t\t

\n\t\t\t

$", "\n\t\t\t\t \n\t\t\t\t \n\t\t\t\t \n\t\t\t

\n\t\t\t
\n\t\t\t\t\n\t\t\t\t$", "\n\t\t\t
\n\t\t\t
"], ["\n\t\t\t
\n\t\t\t

$", "\n\t\t\t\t \n\t\t\t\t \n\t\t\t

\n\t\t\t

$", "\n\t\t\t\t \n\t\t\t\t \n\t\t\t\t \n\t\t\t

\n\t\t\t
\n\t\t\t\t\n\t\t\t\t$", "\n\t\t\t
\n\t\t\t
"]), - _templateObject82 = _taggedTemplateLiteral(["\n\t\t\t\t
\n\t\t\t\t\t

\n\t\t\t\t\t\t$", "\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t$", "\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t$", "\n\t\t\t\t\t

\n\t\t\t\t\t

\n\t\t\t\t\t\t$", "\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t \t\t\n\t\t\t\t\t\t$", "\n\t\t\t\t \t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t$", "\n\t\t\t\t\t

\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t$", "\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t"], ["\n\t\t\t\t
\n\t\t\t\t\t

\n\t\t\t\t\t\t$", "\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t$", "\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t$", "\n\t\t\t\t\t

\n\t\t\t\t\t

\n\t\t\t\t\t\t$", "\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t \t\t\n\t\t\t\t\t\t$", "\n\t\t\t\t \t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t$", "\n\t\t\t\t\t

\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t$", "\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t"]), - _templateObject83 = _taggedTemplateLiteral(["\n\t\t\t
\n\t\t\t\t", "\n\t\t\t
\n\t\t\t"], ["\n\t\t\t
\n\t\t\t\t", "\n\t\t\t
\n\t\t\t"]), - _templateObject84 = _taggedTemplateLiteral(["\n\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t

\n\t\t\t\t", "\n\t\t\t\t

\n\t\t\t\t
\n\t\t\t\t"], ["\n\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t

\n\t\t\t\t", "\n\t\t\t\t

\n\t\t\t\t
\n\t\t\t\t"]), - _templateObject85 = _taggedTemplateLiteral(["\n\t\t\t\t\t\t
\n\t\t\t\t\t\t

\n\t\t\t\t\t\t$", "\n\t\t\t\t\t\t

\n\t\t\t\t\t\t
"], ["\n\t\t\t\t\t\t
\n\t\t\t\t\t\t

\n\t\t\t\t\t\t$", "\n\t\t\t\t\t\t

\n\t\t\t\t\t\t
"]), - _templateObject86 = _taggedTemplateLiteral(["\n\t\t\t
\n\t\t\t\t

\n\t\t\t\t$", "\n\t\t\t\t\n\t\t\t\t

\n\t\t\t
\n\t\t"], ["\n\t\t\t
\n\t\t\t\t

\n\t\t\t\t$", "\n\t\t\t\t\n\t\t\t\t

\n\t\t\t
\n\t\t"]), - _templateObject87 = _taggedTemplateLiteral(["\n\t\t\t", "\n\t\t
\n\t\t\t"], ["\n\t\t\t", "\n\t\t
\n\t\t\t"]), - _templateObject88 = _taggedTemplateLiteral([""], [""]), - _templateObject89 = _taggedTemplateLiteral(["", ""], ["", ""]); - -function _taggedTemplateLiteral(strings, raw) { return Object.freeze(Object.defineProperties(strings, { raw: { value: Object.freeze(raw) } })); } - -function gup(b) { - b = b.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]"); - - var a = "[\\?&]" + b + "=([^&#]*)"; - var d = new RegExp(a); - var c = d.exec(window.location.href); - - if (c === null) return "";else return c[1]; -} - -/** - * @description This module communicates with Lychee's API - */ - -var api = { - onError: null -}; - -api.isTimeout = function (errorThrown, jqXHR) { - if (errorThrown && (errorThrown === "Bad Request" && jqXHR && jqXHR.responseJSON && jqXHR.responseJSON.error && jqXHR.responseJSON.error === "Session timed out" || errorThrown === "unknown status" && jqXHR && jqXHR.status && jqXHR.status === 419 && jqXHR.responseJSON && jqXHR.responseJSON.message && jqXHR.responseJSON.message === "CSRF token mismatch.")) { - return true; - } - - return false; -}; - -api.post = function (fn, params, callback) { - var responseProgressCB = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null; - - loadingBar.show(); - - params = $.extend({ function: fn }, params); - - var api_url = "api/" + fn; - - var success = function success(data) { - setTimeout(loadingBar.hide, 100); - - // Catch errors - if (typeof data === "string" && data.substring(0, 7) === "Error: ") { - api.onError(data.substring(7, data.length), params, data); - return false; - } - - callback(data); - }; - - var error = function error(jqXHR, textStatus, errorThrown) { - api.onError(api.isTimeout(errorThrown, jqXHR) ? "Session timed out." : "Server error or API not found.", params, errorThrown); - }; - - var ajaxParams = { - type: "POST", - url: api_url, - data: params, - dataType: "json", - success: success, - error: error - }; - - if (responseProgressCB !== null) { - ajaxParams.xhrFields = { - onprogress: responseProgressCB - }; - } - - $.ajax(ajaxParams); -}; - -api.get = function (url, callback) { - loadingBar.show(); - - var success = function success(data) { - setTimeout(loadingBar.hide, 100); - - // Catch errors - if (typeof data === "string" && data.substring(0, 7) === "Error: ") { - api.onError(data.substring(7, data.length), params, data); - return false; - } - - callback(data); - }; - - var error = function error(jqXHR, textStatus, errorThrown) { - api.onError(api.isTimeout(errorThrown, jqXHR) ? "Session timed out." : "Server error or API not found.", {}, errorThrown); - }; - - $.ajax({ - type: "GET", - url: url, - data: {}, - dataType: "text", - success: success, - error: error - }); -}; - -api.post_raw = function (fn, params, callback) { - loadingBar.show(); - - params = $.extend({ function: fn }, params); - - var api_url = "api/" + fn; - - var success = function success(data) { - setTimeout(loadingBar.hide, 100); - - // Catch errors - if (typeof data === "string" && data.substring(0, 7) === "Error: ") { - api.onError(data.substring(7, data.length), params, data); - return false; - } - - callback(data); - }; - - var error = function error(jqXHR, textStatus, errorThrown) { - api.onError(api.isTimeout(errorThrown, jqXHR) ? "Session timed out." : "Server error or API not found.", params, errorThrown); - }; - - $.ajax({ - type: "POST", - url: api_url, - data: params, - dataType: "text", - success: success, - error: error - }); -}; - -var csrf = {}; - -csrf.addLaravelCSRF = function (event, jqxhr, settings) { - if (settings.url !== lychee.updatePath) { - jqxhr.setRequestHeader("X-XSRF-TOKEN", csrf.getCookie("XSRF-TOKEN")); - } -}; - -csrf.escape = function (s) { - return s.replace(/([.*+?\^${}()|\[\]\/\\])/g, "\\$1"); -}; - -csrf.getCookie = function (name) { - // we stop the selection at = (default json) but also at % to prevent any %3D at the end of the string - var match = document.cookie.match(RegExp("(?:^|;\\s*)" + csrf.escape(name) + "=([^;^%]*)")); - return match ? match[1] : null; -}; - -csrf.bind = function () { - $(document).on("ajaxSend", csrf.addLaravelCSRF); -}; - -(function ($) { - var Swipe = function Swipe(el) { - var self = this; - - this.el = $(el); - this.pos = { start: { x: 0, y: 0 }, end: { x: 0, y: 0 } }; - this.startTime = null; - - el.on("touchstart", function (e) { - self.touchStart(e); - }); - el.on("touchmove", function (e) { - self.touchMove(e); - }); - el.on("touchend", function () { - self.swipeEnd(); - }); - el.on("mousedown", function (e) { - self.mouseDown(e); - }); - }; - - Swipe.prototype = { - touchStart: function touchStart(e) { - var touch = e.originalEvent.touches[0]; - - this.swipeStart(e, touch.pageX, touch.pageY); - }, - - touchMove: function touchMove(e) { - var touch = e.originalEvent.touches[0]; - - this.swipeMove(e, touch.pageX, touch.pageY); - }, - - mouseDown: function mouseDown(e) { - var self = this; - - this.swipeStart(e, e.pageX, e.pageY); - - this.el.on("mousemove", function (_e) { - self.mouseMove(_e); - }); - this.el.on("mouseup", function () { - self.mouseUp(); - }); - }, - - mouseMove: function mouseMove(e) { - this.swipeMove(e, e.pageX, e.pageY); - }, - - mouseUp: function mouseUp(e) { - this.swipeEnd(e); - - this.el.off("mousemove"); - this.el.off("mouseup"); - }, - - swipeStart: function swipeStart(e, x, y) { - this.pos.start.x = x; - this.pos.start.y = y; - this.pos.end.x = x; - this.pos.end.y = y; - - this.startTime = new Date().getTime(); - - this.trigger("swipeStart", e); - }, - - swipeMove: function swipeMove(e, x, y) { - this.pos.end.x = x; - this.pos.end.y = y; - - this.trigger("swipeMove", e); - }, - - swipeEnd: function swipeEnd(e) { - this.trigger("swipeEnd", e); - }, - - trigger: function trigger(e, originalEvent) { - var self = this; - - var event = $.Event(e), - x = self.pos.start.x - self.pos.end.x, - y = self.pos.end.y - self.pos.start.y, - radians = Math.atan2(y, x), - direction = "up", - distance = Math.round(Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2))), - angle = Math.round(radians * 180 / Math.PI), - speed = Math.round(distance / (new Date().getTime() - self.startTime) * 1000); - - if (angle < 0) { - angle = 360 - Math.abs(angle); - } - - if (angle <= 45 && angle >= 0 || angle <= 360 && angle >= 315) { - direction = "left"; - } else if (angle >= 135 && angle <= 225) { - direction = "right"; - } else if (angle > 45 && angle < 135) { - direction = "down"; - } - - event.originalEvent = originalEvent; - - event.swipe = { - x: x, - y: y, - direction: direction, - distance: distance, - angle: angle, - speed: speed - }; - - $(self.el).trigger(event); - } - }; - - $.fn.swipe = function () { - // let swipe = new Swipe(this); - new Swipe(this); - - return this; - }; -})(jQuery); - -/** - * @description Takes care of every action an album can handle and execute. - */ - -var album = { - json: null -}; - -album.isSmartID = function (id) { - return id === "unsorted" || id === "starred" || id === "public" || id === "recent"; -}; - -album.getParent = function () { - if (album.json == null || album.isSmartID(album.json.id) === true || !album.json.parent_id || album.json.parent_id === 0) { - return ""; - } - return album.json.parent_id; -}; - -album.getID = function () { - var id = null; - - // this is a Lambda - var isID = function isID(_id) { - if (album.isSmartID(_id)) { - return true; - } - return $.isNumeric(_id); - }; - - if (_photo.json) id = _photo.json.album;else if (album.json) id = album.json.id;else if (mapview.albumID) id = mapview.albumID; - - // Search - if (isID(id) === false) id = $(".album:hover, .album.active").attr("data-id"); - if (isID(id) === false) id = $(".photo:hover, .photo.active").attr("data-album-id"); - - if (isID(id) === true) return id;else return false; -}; - -album.isTagAlbum = function () { - return album.json && album.json.tag_album && album.json.tag_album === "1"; -}; - -album.getByID = function (photoID) { - // Function returns the JSON of a photo - - if (photoID == null || !album.json || !album.json.photos) { - lychee.error("Error: Album json not found !"); - return undefined; - } - - var i = 0; - while (i < album.json.photos.length) { - if (parseInt(album.json.photos[i].id) === parseInt(photoID)) { - return album.json.photos[i]; - } - i++; - } - - lychee.error("Error: photo " + photoID + " not found !"); - return undefined; -}; - -album.getSubByID = function (albumID) { - // Function returns the JSON of a subalbum - - if (albumID == null || !album.json || !album.json.albums) { - lychee.error("Error: Album json not found!"); - return undefined; - } - - var i = 0; - while (i < album.json.albums.length) { - if (parseInt(album.json.albums[i].id) === parseInt(albumID)) { - return album.json.albums[i]; - } - i++; - } - - lychee.error("Error: album " + albumID + " not found!"); - return undefined; -}; - -// noinspection DuplicatedCode -album.deleteByID = function (photoID) { - if (photoID == null || !album.json || !album.json.photos) { - lychee.error("Error: Album json not found !"); - return false; - } - - var deleted = false; - - $.each(album.json.photos, function (i) { - if (parseInt(album.json.photos[i].id) === parseInt(photoID)) { - album.json.photos.splice(i, 1); - deleted = true; - return false; - } - }); - - return deleted; -}; - -// noinspection DuplicatedCode -album.deleteSubByID = function (albumID) { - if (albumID == null || !album.json || !album.json.albums) { - lychee.error("Error: Album json not found !"); - return false; - } - - var deleted = false; - - $.each(album.json.albums, function (i) { - if (parseInt(album.json.albums[i].id) === parseInt(albumID)) { - album.json.albums.splice(i, 1); - deleted = true; - return false; - } - }); - - return deleted; -}; - -album.load = function (albumID) { - var refresh = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; - - var params = { - albumID: albumID, - password: "" - }; - - var processData = function processData(data) { - if (data === "Warning: Wrong password!") { - // User hit Cancel at the password prompt - return false; - } - - if (data === "Warning: Album private!") { - if (document.location.hash.replace("#", "").split("/")[1] !== undefined) { - // Display photo only - lychee.setMode("view"); - lychee.footer_hide(); - } else { - // Album not public - lychee.content.show(); - lychee.footer_show(); - if (!visible.albums() && !visible.album()) lychee.goto(); - } - return false; - } - - album.json = data; - - if (refresh === false) { - lychee.animate(".content", "contentZoomOut"); - } - var waitTime = 300; - - // Skip delay when refresh is true - // Skip delay when opening a blank Lychee - if (refresh === true) waitTime = 0; - if (!visible.albums() && !visible.photo() && !visible.album()) waitTime = 0; - - setTimeout(function () { - view.album.init(); - - if (refresh === false) { - lychee.animate(lychee.content, "contentZoomIn"); - header.setMode("album"); - } - - tabindex.makeFocusable(lychee.content); - if (lychee.active_focus_on_page_load) { - // Put focus on first element - either album or photo - var _first_album = $(".album:first"); - if (_first_album.length !== 0) { - _first_album.focus(); - } else { - first_photo = $(".photo:first"); - if (first_photo.length !== 0) { - first_photo.focus(); - } - } - } - }, waitTime); - }; - - api.post("Album::get", params, function (data) { - if (data === "Warning: Wrong password!") { - password.getDialog(albumID, function () { - params.password = password.value; - - api.post("Album::get", params, function (_data) { - albums.refresh(); - processData(_data); - }); - }); - } else { - processData(data); - - tabindex.makeFocusable(lychee.content); - - if (lychee.active_focus_on_page_load) { - // Put focus on first element - either album or photo - first_album = $(".album:first"); - if (first_album.length !== 0) { - first_album.focus(); - } else { - first_photo = $(".photo:first"); - if (first_photo.length !== 0) { - first_photo.focus(); - } - } - } - } - }); -}; - -album.parse = function () { - if (!album.json.title) album.json.title = lychee.locale["UNTITLED"]; -}; - -album.add = function () { - var IDs = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; - var callback = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; - - var action = function action(data) { - // let title = data.title; - - var isNumber = function isNumber(n) { - return !isNaN(parseInt(n, 10)) && isFinite(n); - }; - - if (!data.title.trim()) { - basicModal.error("title"); - return; - } - - basicModal.close(); - - var params = { - title: data.title, - parent_id: 0 - }; - - if (visible.albums() || album.isSmartID(album.json.id)) { - params.parent_id = 0; - } else if (visible.album()) { - params.parent_id = album.json.id; - } else if (visible.photo()) { - params.parent_id = _photo.json.album; - } - - api.post("Album::add", params, function (_data) { - if (_data !== false && isNumber(_data)) { - if (IDs != null && callback != null) { - callback(IDs, _data, false); // we do not confirm - } else { - albums.refresh(); - lychee.goto(_data); - } - } else { - lychee.error(null, params, _data); - } - }); - }; - - basicModal.show({ - body: lychee.html(_templateObject, lychee.locale["TITLE_NEW_ALBUM"]), - buttons: { - action: { - title: lychee.locale["CREATE_ALBUM"], - fn: action - }, - cancel: { - title: lychee.locale["CANCEL"], - fn: basicModal.close - } - } - }); -}; - -album.addByTags = function () { - var action = function action(data) { - if (!data.title.trim()) { - basicModal.error("title"); - return; - } - if (!data.tags.trim()) { - basicModal.error("tags"); - return; - } - - basicModal.close(); - - var params = { - title: data.title, - tags: data.tags - }; - - api.post("Album::addByTags", params, function (_data) { - var isNumber = function isNumber(n) { - return !isNaN(parseInt(n, 10)) && isFinite(n); - }; - if (_data !== false && isNumber(_data)) { - albums.refresh(); - lychee.goto(_data); - } else { - lychee.error(null, params, _data); - } - }); - }; - - basicModal.show({ - body: lychee.html(_templateObject2, lychee.locale["TITLE_NEW_ALBUM"]), - buttons: { - action: { - title: lychee.locale["CREATE_TAG_ALBUM"], - fn: action - }, - cancel: { - title: lychee.locale["CANCEL"], - fn: basicModal.close - } - } - }); -}; - -album.setShowTags = function (albumID) { - var oldShowTags = album.json.show_tags; - - var action = function action(data) { - if (!data.show_tags.trim()) { - basicModal.error("show_tags"); - return; - } - - var show_tags = data.show_tags; - basicModal.close(); - - if (visible.album()) { - album.json.show_tags = show_tags; - view.album.show_tags(); - } - var params = { - albumID: albumID, - show_tags: show_tags - }; - - api.post("Album::setShowTags", params, function (_data) { - if (_data !== true) { - lychee.error(null, params, _data); - } else { - album.reload(); - } - }); - }; - - basicModal.show({ - body: lychee.html(_templateObject3, lychee.locale["ALBUM_NEW_SHOWTAGS"], oldShowTags), - buttons: { - action: { - title: lychee.locale["ALBUM_SET_SHOWTAGS"], - fn: action - }, - cancel: { - title: lychee.locale["CANCEL"], - fn: basicModal.close - } - } - }); -}; - -album.setTitle = function (albumIDs) { - var oldTitle = ""; - var msg = ""; - - if (!albumIDs) return false; - if (!(albumIDs instanceof Array)) { - albumIDs = [albumIDs]; - } - - if (albumIDs.length === 1) { - // Get old title if only one album is selected - if (album.json) { - if (parseInt(album.getID()) === parseInt(albumIDs[0])) { - oldTitle = album.json.title; - } else oldTitle = album.getSubByID(albumIDs[0]).title; - } - if (!oldTitle && albums.json) oldTitle = albums.getByID(albumIDs[0]).title; - } - - var action = function action(data) { - if (!data.title.trim()) { - basicModal.error("title"); - return; - } - - basicModal.close(); - - var newTitle = data.title; - - if (visible.album()) { - if (albumIDs.length === 1 && parseInt(album.getID()) === parseInt(albumIDs[0])) { - // Rename only one album - - album.json.title = newTitle; - view.album.title(); - - if (albums.json) albums.getByID(albumIDs[0]).title = newTitle; - } else { - albumIDs.forEach(function (id) { - album.getSubByID(id).title = newTitle; - view.album.content.titleSub(id); - - if (albums.json) albums.getByID(id).title = newTitle; - }); - } - } else if (visible.albums()) { - // Rename all albums - - albumIDs.forEach(function (id) { - albums.getByID(id).title = newTitle; - view.albums.content.title(id); - }); - } - - var params = { - albumIDs: albumIDs.join(), - title: newTitle - }; - - api.post("Album::setTitle", params, function (_data) { - if (_data !== true) { - lychee.error(null, params, _data); - } - }); - }; - - var input = lychee.html(_templateObject4, lychee.locale["ALBUM_TITLE"], oldTitle); - - if (albumIDs.length === 1) msg = lychee.html(_templateObject5, lychee.locale["ALBUM_NEW_TITLE"], input);else msg = lychee.html(_templateObject6, lychee.locale["ALBUMS_NEW_TITLE_1"], albumIDs.length, lychee.locale["ALBUMS_NEW_TITLE_2"], input); - - basicModal.show({ - body: msg, - buttons: { - action: { - title: lychee.locale["ALBUM_SET_TITLE"], - fn: action - }, - cancel: { - title: lychee.locale["CANCEL"], - fn: basicModal.close - } - } - }); -}; - -album.setDescription = function (albumID) { - var oldDescription = album.json.description; - - var action = function action(data) { - var description = data.description; - - basicModal.close(); - - if (visible.album()) { - album.json.description = description; - view.album.description(); - } - - var params = { - albumID: albumID, - description: description - }; - - api.post("Album::setDescription", params, function (_data) { - if (_data !== true) { - lychee.error(null, params, _data); - } - }); - }; - - basicModal.show({ - body: lychee.html(_templateObject7, lychee.locale["ALBUM_NEW_DESCRIPTION"], lychee.locale["ALBUM_DESCRIPTION"], oldDescription), - buttons: { - action: { - title: lychee.locale["ALBUM_SET_DESCRIPTION"], - fn: action - }, - cancel: { - title: lychee.locale["CANCEL"], - fn: basicModal.close - } - } - }); -}; - -album.toggleCover = function (photoID) { - if (!photoID) return false; - - album.json.cover_id = album.json.cover_id === photoID ? "" : photoID; - - var params = { - albumID: album.json.id, - photoID: album.json.cover_id - }; - - api.post("Album::setCover", params, function (data) { - if (data !== true) { - lychee.error(null, params, data); - } else { - view.album.content.cover(photoID); - if (!album.getParent()) { - albums.refresh(); - } - } - }); -}; - -album.setLicense = function (albumID) { - var callback = function callback() { - $("select#license").val(album.json.license === "" ? "none" : album.json.license); - return false; - }; - - var action = function action(data) { - var license = data.license; - - basicModal.close(); - - var params = { - albumID: albumID, - license: license - }; - - api.post("Album::setLicense", params, function (_data) { - if (_data !== true) { - lychee.error(null, params, _data); - } else { - if (visible.album()) { - album.json.license = params.license; - view.album.license(); - } - } - }); - }; - - var msg = lychee.html(_templateObject8, lychee.locale["ALBUM_LICENSE"], lychee.locale["ALBUM_LICENSE_NONE"], lychee.locale["ALBUM_RESERVED"], lychee.locale["ALBUM_LICENSE_HELP"]); - - basicModal.show({ - body: msg, - callback: callback, - buttons: { - action: { - title: lychee.locale["ALBUM_SET_LICENSE"], - fn: action - }, - cancel: { - title: lychee.locale["CANCEL"], - fn: basicModal.close - } - } - }); -}; - -album.setSorting = function (albumID) { - var callback = function callback() { - $("select#sortingCol").val(album.json.sorting_col); - $("select#sortingOrder").val(album.json.sorting_order); - return false; - }; - - var action = function action(data) { - var typePhotos = data.sortingCol; - var orderPhotos = data.sortingOrder; - - basicModal.close(); - - var params = { - albumID: albumID, - typePhotos: typePhotos, - orderPhotos: orderPhotos - }; - - api.post("Album::setSorting", params, function (_data) { - if (_data !== true) { - lychee.error(null, params, _data); - } else { - if (visible.album()) { - album.reload(); - } - } - }); - }; - - var msg = lychee.html(_templateObject9) + lychee.locale["SORT_PHOTO_BY_1"] + "\n\t\t\n\t\t\t\n\t\t\n\t\t" + lychee.locale["SORT_PHOTO_BY_2"] + "\n\t\t\n\t\t\t\n\t\t\n\t\t" + lychee.locale["SORT_PHOTO_BY_3"] + "\n\t\t

\n\t
"; - - basicModal.show({ - body: msg, - callback: callback, - buttons: { - action: { - title: lychee.locale["ALBUM_SET_ORDER"], - fn: action - }, - cancel: { - title: lychee.locale["CANCEL"], - fn: basicModal.close - } - } - }); -}; - -album.setPublic = function (albumID, e) { - var password = ""; - - if (!basicModal.visible()) { - var _msg = lychee.html(_templateObject10, lychee.locale["ALBUM_PUBLIC"], lychee.locale["ALBUM_PUBLIC_EXPL"], build.iconic("check"), lychee.locale["ALBUM_FULL"], lychee.locale["ALBUM_FULL_EXPL"], build.iconic("check"), lychee.locale["ALBUM_HIDDEN"], lychee.locale["ALBUM_HIDDEN_EXPL"], build.iconic("check"), lychee.locale["ALBUM_DOWNLOADABLE"], lychee.locale["ALBUM_DOWNLOADABLE_EXPL"], build.iconic("check"), lychee.locale["ALBUM_SHARE_BUTTON_VISIBLE"], lychee.locale["ALBUM_SHARE_BUTTON_VISIBLE_EXPL"], build.iconic("check"), lychee.locale["ALBUM_PASSWORD_PROT"], lychee.locale["ALBUM_PASSWORD_PROT_EXPL"], lychee.locale["PASSWORD"], lychee.locale["ALBUM_NSFW"], lychee.locale["ALBUM_NSFW_EXPL"]); - - basicModal.show({ - body: _msg, - buttons: { - action: { - title: lychee.locale["ALBUM_SHARING_CONFIRM"], - // Call setPublic function without showing the modal - fn: function fn() { - return album.setPublic(albumID, e); - } - }, - cancel: { - title: lychee.locale["CANCEL"], - fn: basicModal.close - } - } - }); - - $('.basicModal .switch input[name="public"]').on("click", function () { - if ($(this).prop("checked") === true) { - $(".basicModal .choice input").attr("disabled", false); - - if (album.json.public === "1") { - // Initialize options based on album settings. - if (album.json.full_photo !== null && album.json.full_photo === "1") $('.basicModal .choice input[name="full_photo"]').prop("checked", true); - if (album.json.visible === "0") $('.basicModal .choice input[name="hidden"]').prop("checked", true); - if (album.json.downloadable === "1") $('.basicModal .choice input[name="downloadable"]').prop("checked", true); - if (album.json.share_button_visible === "1") $('.basicModal .choice input[name="share_button_visible"]').prop("checked", true); - if (album.json.password === "1") { - $('.basicModal .choice input[name="password"]').prop("checked", true); - $('.basicModal .choice input[name="passwordtext"]').show(); - } - } else { - // Initialize options based on global settings. - if (lychee.full_photo) { - $('.basicModal .choice input[name="full_photo"]').prop("checked", true); - } - if (lychee.downloadable) { - $('.basicModal .choice input[name="downloadable"]').prop("checked", true); - } - if (lychee.share_button_visible) { - $('.basicModal .choice input[name="share_button_visible"]').prop("checked", true); - } - } - } else { - $(".basicModal .choice input").prop("checked", false).attr("disabled", true); - $('.basicModal .choice input[name="passwordtext"]').hide(); - } - }); - - if (album.json.nsfw === "1") { - $('.basicModal .switch input[name="nsfw"]').prop("checked", true); - } else { - $('.basicModal .switch input[name="nsfw"]').prop("checked", false); - } - - if (album.json.public === "1") { - $('.basicModal .switch input[name="public"]').click(); - } else { - $(".basicModal .choice input").attr("disabled", true); - } - - $('.basicModal .choice input[name="password"]').on("change", function () { - if ($(this).prop("checked") === true) $('.basicModal .choice input[name="passwordtext"]').show().focus();else $('.basicModal .choice input[name="passwordtext"]').hide(); - }); - - return true; - } - - albums.refresh(); - - // Set public - if ($('.basicModal .switch input[name="nsfw"]:checked').length === 1) { - album.json.nsfw = "1"; - } else { - album.json.nsfw = "0"; - } - - // Set public - if ($('.basicModal .switch input[name="public"]:checked').length === 1) { - album.json.public = "1"; - } else { - album.json.public = "0"; - } - - // Set full photo - if ($('.basicModal .choice input[name="full_photo"]:checked').length === 1) { - album.json.full_photo = "1"; - } else { - album.json.full_photo = "0"; - } - - // Set visible - if ($('.basicModal .choice input[name="hidden"]:checked').length === 1) { - album.json.visible = "0"; - } else { - album.json.visible = "1"; - } - - // Set downloadable - if ($('.basicModal .choice input[name="downloadable"]:checked').length === 1) { - album.json.downloadable = "1"; - } else { - album.json.downloadable = "0"; - } - - // Set share_button_visible - if ($('.basicModal .choice input[name="share_button_visible"]:checked').length === 1) { - album.json.share_button_visible = "1"; - } else { - album.json.share_button_visible = "0"; - } - - // Set password - var oldPassword = album.json.password; - if ($('.basicModal .choice input[name="password"]:checked').length === 1) { - password = $('.basicModal .choice input[name="passwordtext"]').val(); - album.json.password = "1"; - } else { - password = ""; - album.json.password = "0"; - } - - // Modal input has been processed, now it can be closed - basicModal.close(); - - // Set data and refresh view - if (visible.album()) { - view.album.nsfw(); - view.album.public(); - view.album.hidden(); - view.album.downloadable(); - view.album.shareButtonVisible(); - view.album.password(); - } - - var params = { - albumID: albumID, - full_photo: album.json.full_photo, - public: album.json.public, - nsfw: album.json.nsfw, - visible: album.json.visible, - downloadable: album.json.downloadable, - share_button_visible: album.json.share_button_visible - }; - if (oldPassword !== album.json.password || password.length > 0) { - // We send the password only if there's been a change; that way the - // server will keep the current password if it wasn't changed. - params.password = password; - } - - api.post("Album::setPublic", params, function (data) { - if (data !== true) lychee.error(null, params, data); - }); -}; - -album.shareUsers = function (albumID, e) { - if (!basicModal.visible()) { - var _msg2 = "
\n\t\t\t

" + lychee.locale["WAIT_FETCH_DATA"] + "

\n\t\t
"; - - api.post("Sharing::List", {}, function (data) { - var sharingForm = $("#sharing_people_form"); - sharingForm.empty(); - if (data !== undefined) { - if (data.users !== undefined) { - sharingForm.append("

" + lychee.locale["SHARING_ALBUM_USERS_LONG_MESSAGE"] + "

"); - // Fill with the list of users - data.users.forEach(function (user) { - sharingForm.append(lychee.html(_templateObject11, user.id, build.iconic("check"), user.username)); - }); - var sharingOfAlbum = data.shared !== undefined ? data.shared.filter(function (val) { - return val.album_id === albumID; - }) : []; - sharingOfAlbum.forEach(function (sharing) { - // Check all the shares who already exists, and store their sharing id on the element - var elem = $(".basicModal .choice input[name=\"" + sharing.user_id + "\"]"); - elem.prop("checked", true); - elem.data("sharingId", sharing.id); - }); - } else { - sharingForm.append("

" + lychee.locale["SHARING_ALBUM_USERS_NO_USERS"] + "

"); - } - } - }); - - basicModal.show({ - body: _msg2, - buttons: { - action: { - title: lychee.locale["ALBUM_SHARING_CONFIRM"], - fn: function fn(data) { - album.shareUsers(albumID, e); - } - }, - cancel: { - title: lychee.locale["CANCEL"], - fn: basicModal.close - } - } - }); - return true; - } - - basicModal.close(); - - var sharingToAdd = []; - var sharingToDelete = []; - $(".basicModal .choice input").each(function (_, input) { - var $input = $(input); - if ($input.is(":checked")) { - if ($input.data("sharingId") === undefined) { - // Input is checked but has no sharing id => new share to create - sharingToAdd.push(input.name); - } - } else { - var sharingId = $input.data("sharingId"); - if (sharingId !== undefined) { - // Input is not checked but has a sharing id => existing share to remove - sharingToDelete.push(sharingId); - } - } - }); - - if (sharingToDelete.length > 0) { - var params = { ShareIDs: sharingToDelete.join(",") }; - api.post("Sharing::Delete", params, function (data) { - if (data !== true) { - loadingBar.show("error", data.description); - lychee.error(null, params, data); - } - }); - } - if (sharingToAdd.length > 0) { - var params = { - albumIDs: albumID, - UserIDs: sharingToAdd.join(",") - }; - api.post("Sharing::Add", params, function (data) { - if (data !== true) { - loadingBar.show("error", data.description); - lychee.error(null, params, data); - } else { - loadingBar.show("success", "Sharing updated!"); - } - }); - } - - return true; -}; - -album.setNSFW = function (albumID, e) { - album.json.nsfw = album.json.nsfw === "0" ? "1" : "0"; - - view.album.nsfw(); - - var params = { - albumID: albumID - }; - - api.post("Album::setNSFW", params, function (data) { - if (data !== true) { - lychee.error(null, params, data); - } else { - albums.refresh(); - } - }); -}; - -album.share = function (service) { - if (album.json.hasOwnProperty("share_button_visible") && album.json.share_button_visible !== "1") { - return; - } - - var url = location.href; - - switch (service) { - case "twitter": - window.open("https://twitter.com/share?url=" + encodeURI(url)); - break; - case "facebook": - window.open("https://www.facebook.com/sharer.php?u=" + encodeURI(url) + "&t=" + encodeURI(album.json.title)); - break; - case "mail": - location.href = "mailto:?subject=" + encodeURI(album.json.title) + "&body=" + encodeURI(url); - break; - } -}; - -album.getArchive = function (albumIDs) { - location.href = "api/Album::getArchive" + lychee.html(_templateObject12, albumIDs.join()); -}; - -album.buildMessage = function (albumIDs, albumID, op1, op2, ops) { - var title = ""; - var sTitle = ""; - var msg = ""; - - if (!albumIDs) return false; - if (albumIDs instanceof Array === false) albumIDs = [albumIDs]; - - // Get title of first album - if (parseInt(albumID, 10) === 0) { - title = lychee.locale["ROOT"]; - } else if (albums.json) { - album1 = albums.getByID(albumID); - if (album1) { - title = album1.title; - } - } - - // Fallback for first album without a title - if (title === "") title = lychee.locale["UNTITLED"]; - - if (albumIDs.length === 1) { - // Get title of second album - if (albums.json) { - album2 = albums.getByID(albumIDs[0]); - if (album2) { - sTitle = album2.title; - } - } - - // Fallback for second album without a title - if (sTitle === "") sTitle = lychee.locale["UNTITLED"]; - - msg = lychee.html(_templateObject13, lychee.locale[op1], sTitle, lychee.locale[op2], title); - } else { - msg = lychee.html(_templateObject14, lychee.locale[ops], title); - } - - return msg; -}; - -album.delete = function (albumIDs) { - var action = {}; - var cancel = {}; - var msg = ""; - - if (!albumIDs) return false; - if (albumIDs instanceof Array === false) albumIDs = [albumIDs]; - - action.fn = function () { - basicModal.close(); - - var params = { - albumIDs: albumIDs.join() - }; - - api.post("Album::delete", params, function (data) { - if (visible.albums()) { - albumIDs.forEach(function (id) { - view.albums.content.delete(id); - albums.deleteByID(id); - }); - } else if (visible.album()) { - albums.refresh(); - if (albumIDs.length === 1 && album.getID() == albumIDs[0]) { - lychee.goto(album.getParent()); - } else { - albumIDs.forEach(function (id) { - album.deleteSubByID(id); - view.album.content.deleteSub(id); - }); - } - } - - if (data !== true) lychee.error(null, params, data); - }); - }; - - if (albumIDs.toString() === "unsorted") { - action.title = lychee.locale["CLEAR_UNSORTED"]; - cancel.title = lychee.locale["KEEP_UNSORTED"]; - - msg = "

" + lychee.locale["DELETE_UNSORTED_CONFIRM"] + "

"; - } else if (albumIDs.length === 1) { - var albumTitle = ""; - - action.title = lychee.locale["DELETE_ALBUM_QUESTION"]; - cancel.title = lychee.locale["KEEP_ALBUM"]; - - // Get title - if (album.json) { - if (parseInt(album.getID()) === parseInt(albumIDs[0])) { - albumTitle = album.json.title; - } else albumTitle = album.getSubByID(albumIDs[0]).title; - } - if (!albumTitle && albums.json) albumTitle = albums.getByID(albumIDs).title; - - // Fallback for album without a title - if (albumTitle === "") albumTitle = lychee.locale["UNTITLED"]; - - msg = lychee.html(_templateObject15, lychee.locale["DELETE_ALBUM_CONFIRMATION_1"], albumTitle, lychee.locale["DELETE_ALBUM_CONFIRMATION_2"]); - } else { - action.title = lychee.locale["DELETE_ALBUMS_QUESTION"]; - cancel.title = lychee.locale["KEEP_ALBUMS"]; - - msg = lychee.html(_templateObject16, lychee.locale["DELETE_ALBUMS_CONFIRMATION_1"], albumIDs.length, lychee.locale["DELETE_ALBUMS_CONFIRMATION_2"]); - } - - basicModal.show({ - body: msg, - buttons: { - action: { - title: action.title, - fn: action.fn, - class: "red" - }, - cancel: { - title: cancel.title, - fn: basicModal.close - } - } - }); -}; - -album.merge = function (albumIDs, albumID) { - var confirm = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true; - - var action = function action() { - basicModal.close(); - albumIDs.unshift(albumID); - - var params = { - albumIDs: albumIDs.join() - }; - - api.post("Album::merge", params, function (data) { - if (data !== true) { - lychee.error(null, params, data); - } else { - album.reload(); - } - }); - }; - - if (confirm) { - basicModal.show({ - body: album.buildMessage(albumIDs, albumID, "ALBUM_MERGE_1", "ALBUM_MERGE_2", "ALBUMS_MERGE"), - buttons: { - action: { - title: lychee.locale["MERGE_ALBUM"], - fn: action, - class: "red" - }, - cancel: { - title: lychee.locale["DONT_MERGE"], - fn: basicModal.close - } - } - }); - } else { - action(); - } -}; - -album.setAlbum = function (albumIDs, albumID) { - var confirm = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true; - - var action = function action() { - basicModal.close(); - albumIDs.unshift(albumID); - - var params = { - albumIDs: albumIDs.join() - }; - - api.post("Album::move", params, function (data) { - if (data !== true) { - lychee.error(null, params, data); - } else { - album.reload(); - } - }); - }; - - if (confirm) { - basicModal.show({ - body: album.buildMessage(albumIDs, albumID, "ALBUM_MOVE_1", "ALBUM_MOVE_2", "ALBUMS_MOVE"), - buttons: { - action: { - title: lychee.locale["MOVE_ALBUMS"], - fn: action, - class: "red" - }, - cancel: { - title: lychee.locale["NOT_MOVE_ALBUMS"], - fn: basicModal.close - } - } - }); - } else { - action(); - } -}; - -album.apply_nsfw_filter = function () { - if (lychee.nsfw_visible) { - $('.album[data-nsfw="1"]').show(); - } else { - $('.album[data-nsfw="1"]').hide(); - } -}; - -album.toggle_nsfw_filter = function () { - lychee.nsfw_visible = !lychee.nsfw_visible; - album.apply_nsfw_filter(); - return false; -}; - -album.isUploadable = function () { - if (lychee.admin) { - return true; - } - if (lychee.publicMode || !lychee.upload) { - return false; - } - - // For special cases of no album / smart album / etc. we return true. - // It's only for regular non-matching albums that we return false. - if (album.json === null || !album.json.owner) { - return true; - } - - return album.json.owner === lychee.username; -}; - -album.updatePhoto = function (data) { - var deepCopySizeVariant = function deepCopySizeVariant(src) { - if (src === undefined || src === null) return null; - var result = {}; - result.url = src.url; - result.width = src.width; - result.height = src.height; - return result; - }; - - if (album.json) { - $.each(album.json.photos, function () { - if (this.id === data.id) { - this.width = data.width; - this.height = data.height; - this.url = data.url; - this.filesize = data.filesize; - // Deep copy size variants - this.sizeVariants = { - thumb: null, - thumb2x: null, - small: null, - small2x: null, - medium: null, - medium2x: null - }; - if (data.sizeVariants !== undefined && data.sizeVariants !== null) { - this.sizeVariants.thumb = deepCopySizeVariant(data.sizeVariants.thumb); - this.sizeVariants.thumb2x = deepCopySizeVariant(data.sizeVariants.thumb2x); - this.sizeVariants.small = deepCopySizeVariant(data.sizeVariants.small); - this.sizeVariants.small2x = deepCopySizeVariant(data.sizeVariants.small2x); - this.sizeVariants.medium = deepCopySizeVariant(data.sizeVariants.medium); - this.sizeVariants.medium2x = deepCopySizeVariant(data.sizeVariants.medium2x); - } - view.album.content.updatePhoto(this); - albums.refresh(); - return false; - } - return true; - }); - } -}; - -album.reload = function () { - var albumID = album.getID(); - - album.refresh(); - albums.refresh(); - - if (visible.album()) lychee.goto(albumID);else lychee.goto(); -}; - -album.refresh = function () { - album.json = null; -}; - -/** - * @description Takes care of every action albums can handle and execute. - */ - -var albums = { - json: null -}; - -albums.load = function () { - var startTime = new Date().getTime(); - - lychee.animate(".content", "contentZoomOut"); - - if (albums.json === null) { - api.post("Albums::get", {}, function (data) { - var waitTime = void 0; - - // Smart Albums - if (data.smartalbums != null) albums._createSmartAlbums(data.smartalbums); - - albums.json = data; - - // Calculate delay - var durationTime = new Date().getTime() - startTime; - if (durationTime > 300) waitTime = 0;else waitTime = 300 - durationTime; - - // Skip delay when opening a blank Lychee - if (!visible.albums() && !visible.photo() && !visible.album()) waitTime = 0; - if (visible.album() && lychee.content.html() === "") waitTime = 0; - - setTimeout(function () { - header.setMode("albums"); - view.albums.init(); - lychee.animate(lychee.content, "contentZoomIn"); - - tabindex.makeFocusable(lychee.content); - - if (lychee.active_focus_on_page_load) { - // Put focus on first element - either album or photo - var _first_album2 = $(".album:first"); - if (_first_album2.length !== 0) { - _first_album2.focus(); - } else { - var _first_photo = $(".photo:first"); - if (_first_photo.length !== 0) { - _first_photo.focus(); - } - } - } - - setTimeout(function () { - lychee.footer_show(); - }, 300); - }, waitTime); - }); - } else { - setTimeout(function () { - header.setMode("albums"); - view.albums.init(); - lychee.animate(lychee.content, "contentZoomIn"); - - tabindex.makeFocusable(lychee.content); - - if (lychee.active_focus_on_page_load) { - // Put focus on first element - either album or photo - first_album = $(".album:first"); - if (first_album.length !== 0) { - first_album.focus(); - } else { - first_photo = $(".photo:first"); - if (first_photo.length !== 0) { - first_photo.focus(); - } - } - } - }, 300); - } -}; - -albums.parse = function (album) { - if (!album.thumb) { - album.thumb = {}; - album.thumb.id = ""; - album.thumb.thumb = album.password === "1" ? "img/password.svg" : "img/no_images.svg"; - album.thumb.type = ""; - album.thumb.thumb2x = ""; - } -}; - -// TODO: REFACTOR THIS -albums._createSmartAlbums = function (data) { - if (data.unsorted) { - data.unsorted = { - id: "unsorted", - title: lychee.locale["UNSORTED"], - created_at: null, - unsorted: "1", - thumb: data.unsorted.thumb - }; - } - - if (data.starred) { - data.starred = { - id: "starred", - title: lychee.locale["STARRED"], - created_at: null, - star: "1", - thumb: data.starred.thumb - }; - } - - if (data.public) { - data.public = { - id: "public", - title: lychee.locale["PUBLIC"], - created_at: null, - public: "1", - visible: "0", - thumb: data.public.thumb - }; - } - - if (data.recent) { - data.recent = { - id: "recent", - title: lychee.locale["RECENT"], - created_at: null, - recent: "1", - thumb: data.recent.thumb - }; - } -}; - -albums.isShared = function (albumID) { - if (albumID == null) return false; - if (!albums.json) return false; - if (!albums.json.albums) return false; - - var found = false; - - var func = function func() { - if (parseInt(this.id, 10) === parseInt(albumID, 10)) { - found = true; - return false; // stop the loop - } - if (this.albums) { - $.each(this.albums, func); - } - }; - - if (albums.json.shared_albums !== null) $.each(albums.json.shared_albums, func); - - return found; -}; - -albums.getByID = function (albumID) { - // Function returns the JSON of an album - - if (albumID == null) return undefined; - if (!albums.json) return undefined; - if (!albums.json.albums) return undefined; - - var json = undefined; - - var func = function func() { - if (parseInt(this.id, 10) === parseInt(albumID, 10)) { - json = this; - return false; // stop the loop - } - if (this.albums) { - $.each(this.albums, func); - } - }; - - $.each(albums.json.albums, func); - - if (json === undefined && albums.json.shared_albums !== null) $.each(albums.json.shared_albums, func); - - if (json === undefined && albums.json.smartalbums !== null) $.each(albums.json.smartalbums, func); - - return json; -}; - -albums.deleteByID = function (albumID) { - // Function returns the JSON of an album - // This function is only ever invoked for top-level albums so it - // doesn't need to descend down the albums tree. - - if (albumID == null) return false; - if (!albums.json) return false; - if (!albums.json.albums) return false; - - var deleted = false; - - $.each(albums.json.albums, function (i) { - if (parseInt(albums.json.albums[i].id) === parseInt(albumID)) { - albums.json.albums.splice(i, 1); - deleted = true; - return false; // stop the loop - } - }); - - if (deleted === false) { - if (!albums.json.shared_albums) return undefined; - $.each(albums.json.shared_albums, function (i) { - if (parseInt(albums.json.shared_albums[i].id) === parseInt(albumID)) { - albums.json.shared_albums.splice(i, 1); - deleted = true; - return false; // stop the loop - } - }); - } - - if (deleted === false) { - if (!albums.json.smartalbums) return undefined; - $.each(albums.json.smartalbums, function (i) { - if (parseInt(albums.json.smartalbums[i].id) === parseInt(albumID)) { - delete albums.json.smartalbums[i]; - deleted = true; - return false; // stop the loop - } - }); - } - - return deleted; -}; - -albums.refresh = function () { - albums.json = null; -}; - -/** - * @description This module is used to generate HTML-Code. - */ - -var build = {}; - -build.iconic = function (icon) { - var classes = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ""; - - var html = ""; - - html += lychee.html(_templateObject17, classes, icon); - - return html; -}; - -build.divider = function (title) { - var html = ""; - - html += lychee.html(_templateObject18, title); - - return html; -}; - -build.editIcon = function (id) { - var html = ""; - - html += lychee.html(_templateObject19, id, build.iconic("pencil")); - - return html; -}; - -build.multiselect = function (top, left) { - return lychee.html(_templateObject20, top, left); -}; - -// two additional images that are barely visible seems a bit overkill - use same image 3 times -// if this simplification comes to pass data.types, data.thumbs and data.thumbs2x no longer need to be arrays -build.getAlbumThumb = function (data) { - var isVideo = void 0; - var isRaw = void 0; - var thumb = void 0; - - isVideo = data.thumb.type && data.thumb.type.indexOf("video") > -1; - isRaw = data.thumb.type && data.thumb.type.indexOf("raw") > -1; - thumb = data.thumb.thumb; - var thumb2x = ""; - - if (thumb === "uploads/thumb/" && isVideo) { - return "Photo thumbnail"; - } - if (thumb === "uploads/thumb/" && isRaw) { - return "Photo thumbnail"; - } - - thumb2x = data.thumb.thumb2x; - - return "Photo thumbnail"; -}; - -build.album = function (data) { - var disabled = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; - - var formattedCreationTs = lychee.locale.printMonthYear(data.created_at); - var formattedMinTs = lychee.locale.printMonthYear(data.min_taken_at); - var formattedMaxTs = lychee.locale.printMonthYear(data.max_taken_at); - var subtitle = formattedCreationTs; - - // check setting album_subtitle_type: - // takedate: date range (min/max_takedate from EXIF; if missing defaults to creation) - // creation: creation date of album - // description: album description - // default: any other type defaults to old style setting subtitles based of album sorting - switch (lychee.album_subtitle_type) { - case "description": - subtitle = data.description ? data.description : ""; - break; - case "takedate": - if (formattedMinTs !== "" || formattedMaxTs !== "") { - // either min_taken_at or max_taken_at is set - subtitle = formattedMinTs === formattedMaxTs ? formattedMaxTs : formattedMinTs + " - " + formattedMaxTs; - subtitle = "" + build.iconic("camera-slr") + "" + subtitle; - break; - } - // fall through - case "creation": - break; - case "oldstyle": - default: - if (lychee.sortingAlbums !== "" && data.min_taken_at && data.max_taken_at) { - var sortingAlbums = lychee.sortingAlbums.replace("ORDER BY ", "").split(" "); - if (sortingAlbums[0] === "max_taken_at" || sortingAlbums[0] === "min_taken_at") { - if (formattedMinTs !== "" && formattedMaxTs !== "") { - subtitle = formattedMinTs === formattedMaxTs ? formattedMaxTs : formattedMinTs + " - " + formattedMaxTs; - } else if (formattedMinTs !== "" && sortingAlbums[0] === "min_taken_at") { - subtitle = formattedMinTs; - } else if (formattedMaxTs !== "" && sortingAlbums[0] === "max_taken_at") { - subtitle = formattedMaxTs; - } - } - } - } - - var html = lychee.html(_templateObject21, disabled ? "disabled" : "", data.nsfw && data.nsfw === "1" && lychee.nsfw_blur ? "blurred" : "", data.id, data.nsfw && data.nsfw === "1" ? "1" : "0", tabindex.get_next_tab_index(), build.getAlbumThumb(data), build.getAlbumThumb(data), build.getAlbumThumb(data), data.title, data.title, subtitle); - - if (album.isUploadable() && !disabled) { - var isCover = album.json && album.json.cover_id && data.thumb.id === album.json.cover_id; - html += lychee.html(_templateObject22, data.nsfw === "1" ? "badge--nsfw" : "", build.iconic("warning"), data.star === "1" ? "badge--star" : "", build.iconic("star"), data.recent === "1" ? "badge--visible badge--list" : "", build.iconic("clock"), data.public === "1" ? "badge--visible" : "", data.visible === "1" ? "badge--not--hidden" : "badge--hidden", build.iconic("eye"), data.unsorted === "1" ? "badge--visible" : "", build.iconic("list"), data.password === "1" ? "badge--visible" : "", build.iconic("lock-locked"), data.tag_album === "1" ? "badge--tag" : "", build.iconic("tag"), isCover ? "badge--cover" : "", build.iconic("folder-cover")); - } - - if (data.albums && data.albums.length > 0 || data.hasOwnProperty("has_albums") && data.has_albums === "1") { - html += lychee.html(_templateObject23, build.iconic("layers")); - } - - html += "
"; - - return html; -}; - -build.photo = function (data) { - var disabled = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; - - var html = ""; - var thumbnail = ""; - var thumb2x = ""; - var isCover = data.id === album.json.cover_id; - - var isVideo = data.type && data.type.indexOf("video") > -1; - var isRaw = data.type && data.type.indexOf("raw") > -1; - var isLivePhoto = data.livePhotoUrl !== "" && data.livePhotoUrl !== null; - - if (data.sizeVariants.thumb === null) { - if (isLivePhoto) { - thumbnail = "Photo thumbnail"; - } - if (isVideo) { - thumbnail = "Photo thumbnail"; - } else if (isRaw) { - thumbnail = "Photo thumbnail"; - } - } else if (lychee.layout === "0") { - if (data.sizeVariants.thumb2x !== null) { - thumb2x = data.sizeVariants.thumb2x.url; - } - - if (thumb2x !== "") { - thumb2x = "data-srcset='" + thumb2x + " 2x'"; - } - - thumbnail = ""; - thumbnail += "Photo thumbnail"; - thumbnail += ""; - } else { - if (data.sizeVariants.small !== null) { - if (data.sizeVariants.small2x !== null) { - thumb2x = "data-srcset='" + data.sizeVariants.small.url + " " + data.sizeVariants.small.width + "w, " + data.sizeVariants.small2x.url + " " + data.sizeVariants.small2x.width + "w'"; - } - - thumbnail = ""; - thumbnail += "Photo thumbnail"; - thumbnail += ""; - } else if (data.sizeVariants.medium !== null) { - if (data.sizeVariants.medium2x !== null) { - thumb2x = "data-srcset='" + data.sizeVariants.medium.url + " " + data.sizeVariants.medium.width + "w, " + data.sizeVariants.medium2x.url + " " + data.sizeVariants.medium2x.width + "w'"; - } - - thumbnail = ""; - thumbnail += "Photo thumbnail"; - thumbnail += ""; - } else if (!isVideo) { - // Fallback for images with no small or medium. - thumbnail = ""; - thumbnail += "Photo thumbnail"; - thumbnail += ""; - } else { - // Fallback for videos with no small (the case of no thumb is - // handled at the top of this function). - - if (data.sizeVariants.thumb2x !== null) { - thumb2x = data.sizeVariants.thumb2x.url; - } - - if (thumb2x !== "") { - thumb2x = "data-srcset='" + data.sizeVariants.thumb.url + " " + data.sizeVariants.thumb.width + "w, " + thumb2x + " " + data.sizeVariants.thumb2x.width + "w'"; - } - - thumbnail = ""; - thumbnail += "Photo thumbnail"; - thumbnail += ""; - } - } - - html += lychee.html(_templateObject24, disabled ? "disabled" : "", data.album, data.id, tabindex.get_next_tab_index(), thumbnail, data.title, data.title); - - if (data.taken_at !== null) html += lychee.html(_templateObject25, build.iconic("camera-slr"), lychee.locale.printDateTime(data.taken_at));else html += lychee.html(_templateObject26, lychee.locale.printDateTime(data.created_at)); - - html += "
"; - - if (album.isUploadable()) { - html += lychee.html(_templateObject27, data.star === "1" ? "badge--star" : "", build.iconic("star"), data.public === "1" && album.json.public !== "1" ? "badge--visible badge--hidden" : "", build.iconic("eye"), isCover ? "badge--cover" : "", build.iconic("folder-cover")); - } - - html += "
"; - - return html; -}; - -build.check_overlay_type = function (data, overlay_type) { - var next = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; - - var types = ["desc", "date", "exif", "none"]; - var idx = types.indexOf(overlay_type); - if (idx < 0) return "none"; - if (next) idx++; - var exifHash = data.make + data.model + data.shutter + data.iso + (data.type.indexOf("video") !== 0 ? data.aperture + data.focal : ""); - - for (var i = 0; i < types.length; i++) { - var type = types[(idx + i) % types.length]; - if (type === "date" || type === "none") return type; - if (type === "desc" && data.description && data.description !== "") return type; - if (type === "exif" && exifHash !== "") return type; - } -}; - -build.overlay_image = function (data) { - var overlay = ""; - switch (build.check_overlay_type(data, lychee.image_overlay_type)) { - case "desc": - overlay = data.description; - break; - case "date": - if (data.taken_at != null) overlay = "" + build.iconic("camera-slr") + "" + lychee.locale.printDateTime(data.taken_at) + "";else overlay = lychee.locale.printDateTime(data.created_at); - break; - case "exif": - var exifHash = data.make + data.model + data.shutter + data.aperture + data.focal + data.iso; - if (exifHash !== "") { - if (data.shutter && data.shutter !== "") overlay = data.shutter.replace("s", "sec"); - if (data.aperture && data.aperture !== "") { - if (overlay !== "") overlay += " at "; - overlay += data.aperture.replace("f/", "ƒ / "); - } - if (data.iso && data.iso !== "") { - if (overlay !== "") overlay += ", "; - overlay += lychee.locale["PHOTO_ISO"] + " " + data.iso; - } - if (data.focal && data.focal !== "") { - if (overlay !== "") overlay += "
"; - overlay += data.focal + (data.lens && data.lens !== "" ? " (" + data.lens + ")" : ""); - } - } - break; - case "none": - default: - return ""; - } - - return lychee.html(_templateObject28, data.title) + (overlay !== "" ? "

" + overlay + "

" : "") + "\n\t\t
\n\t\t"; -}; - -build.imageview = function (data, visibleControls, autoplay) { - var html = ""; - var thumb = ""; - - if (data.type.indexOf("video") > -1) { - html += lychee.html(_templateObject29, visibleControls === true ? "" : "full", autoplay ? "autoplay" : "", tabindex.get_next_tab_index(), data.url); - } else if (data.type.indexOf("raw") > -1 && data.sizeVariants.medium === null) { - html += lychee.html(_templateObject30, visibleControls === true ? "" : "full", tabindex.get_next_tab_index()); - } else { - var img = ""; - - if (data.livePhotoUrl === "" || data.livePhotoUrl === null) { - // It's normal photo - - // See if we have the thumbnail loaded... - $(".photo").each(function () { - if ($(this).attr("data-id") && $(this).attr("data-id") == data.id) { - var thumbimg = $(this).find("img"); - if (thumbimg.length > 0) { - thumb = thumbimg[0].currentSrc ? thumbimg[0].currentSrc : thumbimg[0].src; - return false; - } - } - }); - - if (data.sizeVariants.medium !== null) { - var medium = ""; - - if (data.sizeVariants.medium2x !== null) { - medium = "srcset='" + data.sizeVariants.medium.url + " " + data.sizeVariants.medium.width + "w, " + data.sizeVariants.medium2x.url + " " + data.sizeVariants.medium2x.width + "w'"; - } - img = "medium"); - } else { - img = "big"; - } - } else { - if (data.sizeVariants.medium !== null) { - var medium_width = data.sizeVariants.medium.width; - var medium_height = data.sizeVariants.medium.height; - // It's a live photo - img = "
"; - } else { - // It's a live photo - img = "
"; - } - } - - html += lychee.html(_templateObject31, img); - } - - html += build.overlay_image(data) + ("\n\t\t\t\n\t\t\t\n\t\t\t"); - - return { html: html, thumb: thumb }; -}; - -build.no_content = function (typ) { - var html = ""; - - html += lychee.html(_templateObject32, build.iconic(typ)); - - switch (typ) { - case "magnifying-glass": - html += lychee.html(_templateObject33, lychee.locale["VIEW_NO_RESULT"]); - break; - case "eye": - html += lychee.html(_templateObject33, lychee.locale["VIEW_NO_PUBLIC_ALBUMS"]); - break; - case "cog": - html += lychee.html(_templateObject33, lychee.locale["VIEW_NO_CONFIGURATION"]); - break; - case "question-mark": - html += lychee.html(_templateObject33, lychee.locale["VIEW_PHOTO_NOT_FOUND"]); - break; - } - - html += "
"; - - return html; -}; - -build.uploadModal = function (title, files) { - var html = ""; - - html += lychee.html(_templateObject34, title); - - var i = 0; - - while (i < files.length) { - var file = files[i]; - - if (file.name.length > 40) file.name = file.name.substr(0, 17) + "..." + file.name.substr(file.name.length - 20, 20); - - html += lychee.html(_templateObject35, file.name); - - i++; - } - - html += "
"; - - return html; -}; - -build.uploadNewFile = function (name) { - if (name.length > 40) { - name = name.substr(0, 17) + "..." + name.substr(name.length - 20, 20); - } - - return lychee.html(_templateObject36, name); -}; - -build.tags = function (tags) { - var html = ""; - var editable = typeof album !== "undefined" ? album.isUploadable() : false; - - // Search is enabled if logged in (not publicMode) or public seach is enabled - var searchable = lychee.publicMode === false || lychee.public_search === true; - - // build class_string for tag - var a_class = "tag"; - if (searchable) { - a_class = a_class + " search"; - } - - if (tags !== "") { - tags = tags.split(","); - - tags.forEach(function (tag, index) { - if (editable) { - html += lychee.html(_templateObject37, a_class, tag, index, build.iconic("x")); - } else { - html += lychee.html(_templateObject38, a_class, tag); - } - }); - } else { - html = lychee.html(_templateObject39, lychee.locale["NO_TAGS"]); - } - - return html; -}; - -build.user = function (user) { - var html = lychee.html(_templateObject40, user.id, user.id, user.username, user.id, user.id); - - return html; -}; - -build.u2f = function (credential) { - return lychee.html(_templateObject41, credential.id, credential.id, credential.id.slice(0, 30), credential.id); -}; - -/** - * @description This module is used for the context menu. - */ - -var contextMenu = {}; - -contextMenu.add = function (e) { - var items = [{ title: build.iconic("image") + lychee.locale["UPLOAD_PHOTO"], fn: function fn() { - return $("#upload_files").click(); - } }, {}, { title: build.iconic("link-intact") + lychee.locale["IMPORT_LINK"], fn: upload.start.url }, { title: build.iconic("dropbox", "ionicons") + lychee.locale["IMPORT_DROPBOX"], fn: upload.start.dropbox }, { title: build.iconic("terminal") + lychee.locale["IMPORT_SERVER"], fn: upload.start.server }, {}, { title: build.iconic("folder") + lychee.locale["NEW_ALBUM"], fn: album.add }]; - - if (visible.albums()) { - items.push({ title: build.iconic("tags") + lychee.locale["NEW_TAG_ALBUM"], fn: album.addByTags }); - } - - if (!lychee.admin) { - // remove import from dropbox and server if not admin - items.splice(3, 2); - } else if (!lychee.dropboxKey || lychee.dropboxKey === "") { - // remove import from dropbox if dropboxKey not set - items.splice(3, 1); - } - - // prepend further buttons if menu bar is reduced on small screens - var button_visibility_album = $("#button_visibility_album"); - if (button_visibility_album && button_visibility_album.css("display") === "none") { - items.unshift({ - title: build.iconic("eye") + lychee.locale["VISIBILITY_ALBUM"], - visible: lychee.enable_button_visibility, - fn: function fn(event) { - return album.setPublic(album.getID(), event); - } - }); - } - var button_trash_album = $("#button_trash_album"); - if (button_trash_album && button_trash_album.css("display") === "none") { - items.unshift({ - title: build.iconic("trash") + lychee.locale["DELETE_ALBUM"], - visible: lychee.enable_button_trash, - fn: function fn() { - return album.delete([album.getID()]); - } - }); - } - var button_move_album = $("#button_move_album"); - if (button_move_album && button_move_album.css("display") === "none") { - items.unshift({ - title: build.iconic("folder") + lychee.locale["MOVE_ALBUM"], - visible: lychee.enable_button_move, - fn: function fn(event) { - return contextMenu.move([album.getID()], event, album.setAlbum, "ROOT", album.getParent() !== ""); - } - }); - } - var button_nsfw_album = $("#button_nsfw_album"); - if (button_nsfw_album && button_nsfw_album.css("display") === "none") { - items.unshift({ - title: build.iconic("warning") + lychee.locale["ALBUM_MARK_NSFW"], - visible: true, - fn: function fn() { - return album.setNSFW(album.getID()); - } - }); - } - - basicContext.show(items, e.originalEvent); - - upload.notify(); -}; - -contextMenu.album = function (albumID, e) { - // Notice for 'Merge': - // fn must call basicContext.close() first, - // in order to keep the selection - - if (album.isSmartID(albumID)) return false; - - // Show merge-item when there's more than one album - // Commented out because it doesn't consider subalbums or shared albums. - // let showMerge = (albums.json && albums.json.albums && Object.keys(albums.json.albums).length>1); - var showMerge = true; - - var items = [{ title: build.iconic("pencil") + lychee.locale["RENAME"], fn: function fn() { - return album.setTitle([albumID]); - } }, { - title: build.iconic("collapse-left") + lychee.locale["MERGE"], - visible: showMerge, - fn: function fn() { - basicContext.close(); - contextMenu.move([albumID], e, album.merge, "ROOT", false); - } - }, { - title: build.iconic("folder") + lychee.locale["MOVE"], - visible: lychee.sub_albums, - fn: function fn() { - basicContext.close(); - contextMenu.move([albumID], e, album.setAlbum, "ROOT"); - } - }, { title: build.iconic("trash") + lychee.locale["DELETE"], fn: function fn() { - return album.delete([albumID]); - } }, { title: build.iconic("cloud-download") + lychee.locale["DOWNLOAD"], fn: function fn() { - return album.getArchive([albumID]); - } }]; - - if (visible.album()) { - // not top level - var myalbum = album.getSubByID(albumID); - if (myalbum.thumb.id) { - var coverActive = myalbum.thumb.id === album.json.cover_id; - // prepend context menu item - items.unshift({ - title: build.iconic("folder-cover", coverActive ? "active" : "") + lychee.locale[coverActive ? "REMOVE_COVER" : "SET_COVER"], - fn: function fn() { - return album.toggleCover(myalbum.thumb.id); - } - }); - } - } - - $('.album[data-id="' + albumID + '"]').addClass("active"); - - basicContext.show(items, e.originalEvent, contextMenu.close); -}; - -contextMenu.albumMulti = function (albumIDs, e) { - multiselect.stopResize(); - - // Automatically merge selected albums when albumIDs contains more than one album - // Show list of albums otherwise - var autoMerge = albumIDs.length > 1; - - // Show merge-item when there's more than one album - // Commented out because it doesn't consider subalbums or shared albums. - // let showMerge = (albums.json && albums.json.albums && Object.keys(albums.json.albums).length>1); - var showMerge = true; - - var items = [{ title: build.iconic("pencil") + lychee.locale["RENAME_ALL"], fn: function fn() { - return album.setTitle(albumIDs); - } }, { - title: build.iconic("collapse-left") + lychee.locale["MERGE_ALL"], - visible: showMerge && autoMerge, - fn: function fn() { - var albumID = albumIDs.shift(); - album.merge(albumIDs, albumID); - } - }, { - title: build.iconic("collapse-left") + lychee.locale["MERGE"], - visible: showMerge && !autoMerge, - fn: function fn() { - basicContext.close(); - contextMenu.move(albumIDs, e, album.merge, "ROOT", false); - } - }, { - title: build.iconic("folder") + lychee.locale["MOVE_ALL"], - visible: lychee.sub_albums, - fn: function fn() { - basicContext.close(); - contextMenu.move(albumIDs, e, album.setAlbum, "ROOT"); - } - }, { title: build.iconic("trash") + lychee.locale["DELETE_ALL"], fn: function fn() { - return album.delete(albumIDs); - } }, { title: build.iconic("cloud-download") + lychee.locale["DOWNLOAD_ALL"], fn: function fn() { - return album.getArchive(albumIDs); - } }]; - - basicContext.show(items, e.originalEvent, contextMenu.close); -}; - -contextMenu.buildList = function (lists, exclude, action) { - var parent = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 0; - var layer = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 0; - - var find = function find(excl, id) { - for (var _i = 0; _i < excl.length; _i++) { - if (parseInt(excl[_i], 10) === parseInt(id, 10)) return true; - } - return false; - }; - - var items = []; - - var i = 0; - while (i < lists.length) { - if (layer === 0 && !lists[i].parent_id || lists[i].parent_id === parent) { - (function () { - var item = lists[i]; - - var thumb = "img/no_cover.svg"; - if (item.thumb && item.thumb.thumb) { - if (item.thumb.thumb === "uploads/thumb/") { - if (item.thumb.type && item.thumb.type.indexOf("video") > -1) { - thumb = "img/play-icon.png"; - } - } else { - thumb = item.thumb.thumb; - } - } else if (item.sizeVariants) { - if (item.sizeVariants.thumb === null) { - if (item.type && item.type.indexOf("video") > -1) { - thumb = "img/play-icon.png"; - } - } else { - thumb = item.sizeVariants.thumb.url; - } - } - - if (item.title === "") item.title = lychee.locale["UNTITLED"]; - - var prefix = layer > 0 ? "  ".repeat(layer - 1) + "└ " : ""; - - var html = lychee.html(_templateObject42, prefix, thumb, item.title); - - items.push({ - title: html, - disabled: find(exclude, item.id), - fn: function fn() { - return action(item); - } - }); - - if (item.albums && item.albums.length > 0) { - items = items.concat(contextMenu.buildList(item.albums, exclude, action, item.id, layer + 1)); - } else { - // Fallback for flat tree representation. Should not be - // needed anymore but shouldn't hurt either. - items = items.concat(contextMenu.buildList(lists, exclude, action, item.id, layer + 1)); - } - })(); - } - - i++; - } - - return items; -}; - -contextMenu.albumTitle = function (albumID, e) { - api.post("Albums::tree", {}, function (data) { - var items = []; - - items = items.concat({ title: lychee.locale["ROOT"], disabled: albumID === false, fn: function fn() { - return lychee.goto(); - } }); - - if (data.albums && data.albums.length > 0) { - items = items.concat({}); - items = items.concat(contextMenu.buildList(data.albums, albumID !== false ? [parseInt(albumID, 10)] : [], function (a) { - return lychee.goto(a.id); - })); - } - - if (data.shared_albums && data.shared_albums.length > 0) { - items = items.concat({}); - items = items.concat(contextMenu.buildList(data.shared_albums, albumID !== false ? [parseInt(albumID, 10)] : [], function (a) { - return lychee.goto(a.id); - })); - } - - if (albumID !== false && !album.isSmartID(albumID) && album.isUploadable()) { - if (items.length > 0) { - items.unshift({}); - } - - items.unshift({ title: build.iconic("pencil") + lychee.locale["RENAME"], fn: function fn() { - return album.setTitle([albumID]); - } }); - } - - basicContext.show(items, e.originalEvent, contextMenu.close); - }); -}; - -contextMenu.photo = function (photoID, e) { - var coverActive = photoID === album.json.cover_id; - - var items = [{ title: build.iconic("star") + lychee.locale["STAR"], fn: function fn() { - return _photo.setStar([photoID]); - } }, { title: build.iconic("tag") + lychee.locale["TAGS"], fn: function fn() { - return _photo.editTags([photoID]); - } }, - // for future work, use a list of all the ancestors. - { - title: build.iconic("folder-cover", coverActive ? "active" : "") + lychee.locale[coverActive ? "REMOVE_COVER" : "SET_COVER"], - fn: function fn() { - return album.toggleCover(photoID); - } - }, {}, { title: build.iconic("pencil") + lychee.locale["RENAME"], fn: function fn() { - return _photo.setTitle([photoID]); - } }, { - title: build.iconic("layers") + lychee.locale["COPY_TO"], - fn: function fn() { - basicContext.close(); - contextMenu.move([photoID], e, _photo.copyTo, "UNSORTED"); - } - }, - // Notice for 'Move': - // fn must call basicContext.close() first, - // in order to keep the selection - { - title: build.iconic("folder") + lychee.locale["MOVE"], - fn: function fn() { - basicContext.close(); - contextMenu.move([photoID], e, _photo.setAlbum, "UNSORTED"); - } - }, { title: build.iconic("trash") + lychee.locale["DELETE"], fn: function fn() { - return _photo.delete([photoID]); - } }, { title: build.iconic("cloud-download") + lychee.locale["DOWNLOAD"], fn: function fn() { - return _photo.getArchive([photoID]); - } }]; - - $('.photo[data-id="' + photoID + '"]').addClass("active"); - - basicContext.show(items, e.originalEvent, contextMenu.close); -}; - -contextMenu.countSubAlbums = function (photoIDs) { - var count = 0; - - var i = void 0, - j = void 0; - - if (album.albums) { - for (i = 0; i < photoIDs.length; i++) { - for (j = 0; j < album.albums.length; j++) { - if (album.albums[j].id === photoIDs[i]) { - count++; - break; - } - } - } - } - - return count; -}; - -contextMenu.photoMulti = function (photoIDs, e) { - // Notice for 'Move All': - // fn must call basicContext.close() first, - // in order to keep the selection and multiselect - var subcount = contextMenu.countSubAlbums(photoIDs); - var photocount = photoIDs.length - subcount; - - if (subcount && photocount) { - multiselect.deselect(".photo.active, .album.active"); - multiselect.close(); - lychee.error("Please select either albums or photos!"); - return; - } - if (subcount) { - contextMenu.albumMulti(photoIDs, e); - return; - } - - multiselect.stopResize(); - - var items = [{ title: build.iconic("star") + lychee.locale["STAR_ALL"], fn: function fn() { - return _photo.setStar(photoIDs); - } }, { title: build.iconic("tag") + lychee.locale["TAGS_ALL"], fn: function fn() { - return _photo.editTags(photoIDs); - } }, {}, { title: build.iconic("pencil") + lychee.locale["RENAME_ALL"], fn: function fn() { - return _photo.setTitle(photoIDs); - } }, { - title: build.iconic("layers") + lychee.locale["COPY_ALL_TO"], - fn: function fn() { - basicContext.close(); - contextMenu.move(photoIDs, e, _photo.copyTo, "UNSORTED"); - } - }, { - title: build.iconic("folder") + lychee.locale["MOVE_ALL"], - fn: function fn() { - basicContext.close(); - contextMenu.move(photoIDs, e, _photo.setAlbum, "UNSORTED"); - } - }, { title: build.iconic("trash") + lychee.locale["DELETE_ALL"], fn: function fn() { - return _photo.delete(photoIDs); - } }, { title: build.iconic("cloud-download") + lychee.locale["DOWNLOAD_ALL"], fn: function fn() { - return _photo.getArchive(photoIDs, "FULL"); - } }]; - - basicContext.show(items, e.originalEvent, contextMenu.close); -}; - -contextMenu.photoTitle = function (albumID, photoID, e) { - var items = [{ title: build.iconic("pencil") + lychee.locale["RENAME"], fn: function fn() { - return _photo.setTitle([photoID]); - } }]; - - var data = album.json; - - if (data.photos !== false && data.photos.length > 0) { - items.push({}); - - items = items.concat(contextMenu.buildList(data.photos, [photoID], function (a) { - return lychee.goto(albumID + "/" + a.id); - })); - } - - if (!album.isUploadable()) { - // Remove Rename and the spacer. - items.splice(0, 2); - } - - basicContext.show(items, e.originalEvent, contextMenu.close); -}; - -contextMenu.photoMore = function (photoID, e) { - // Show download-item when - // a) We are allowed to upload to the album - // b) the photo is explicitly marked as downloadable (v4-only) - // c) or, the album is explicitly marked as downloadable - var showDownload = album.isUploadable() || (_photo.json.hasOwnProperty("downloadable") ? _photo.json.downloadable === "1" : album.json && album.json.downloadable && album.json.downloadable === "1"); - var showFull = _photo.json.url && _photo.json.url !== ""; - - var items = [{ title: build.iconic("fullscreen-enter") + lychee.locale["FULL_PHOTO"], visible: !!showFull, fn: function fn() { - return window.open(_photo.getDirectLink()); - } }, { title: build.iconic("cloud-download") + lychee.locale["DOWNLOAD"], visible: !!showDownload, fn: function fn() { - return _photo.getArchive([photoID]); - } }]; - // prepend further buttons if menu bar is reduced on small screens - var button_visibility = $("#button_visibility"); - if (button_visibility && button_visibility.css("display") === "none") { - items.unshift({ - title: build.iconic("eye") + lychee.locale["VISIBILITY_PHOTO"], - visible: lychee.enable_button_visibility, - fn: function fn(event) { - return _photo.setPublic(_photo.getID(), event); - } - }); - } - var button_trash = $("#button_trash"); - if (button_trash && button_trash.css("display") === "none") { - items.unshift({ - title: build.iconic("trash") + lychee.locale["DELETE"], - visible: lychee.enable_button_trash, - fn: function fn() { - return _photo.delete([_photo.getID()]); - } - }); - } - var button_move = $("#button_move"); - if (button_move && button_move.css("display") === "none") { - items.unshift({ - title: build.iconic("folder") + lychee.locale["MOVE"], - visible: lychee.enable_button_move, - fn: function fn(event) { - return contextMenu.move([_photo.getID()], event, _photo.setAlbum); - } - }); - } - var button_rotate_cwise = $("#button_rotate_cwise"); - if (button_rotate_cwise && button_rotate_cwise.css("display") === "none") { - items.unshift({ - title: build.iconic("clockwise") + lychee.locale["PHOTO_EDIT_ROTATECWISE"], - visible: lychee.enable_button_move, - fn: function fn() { - return photoeditor.rotate(_photo.getID(), 1); - } - }); - } - var button_rotate_ccwise = $("#button_rotate_ccwise"); - if (button_rotate_ccwise && button_rotate_ccwise.css("display") === "none") { - items.unshift({ - title: build.iconic("counterclockwise") + lychee.locale["PHOTO_EDIT_ROTATECCWISE"], - visible: lychee.enable_button_move, - fn: function fn() { - return photoeditor.rotate(_photo.getID(), -1); - } - }); - } - - basicContext.show(items, e.originalEvent); -}; - -contextMenu.getSubIDs = function (albums, albumID) { - var ids = [parseInt(albumID, 10)]; - var a = void 0; - - for (a = 0; a < albums.length; a++) { - if (parseInt(albums[a].parent_id, 10) === parseInt(albumID, 10)) { - ids = ids.concat(contextMenu.getSubIDs(albums, albums[a].id)); - } - - if (albums[a].albums && albums[a].albums.length > 0) { - ids = ids.concat(contextMenu.getSubIDs(albums[a].albums, albumID)); - } - } - - return ids; -}; - -contextMenu.move = function (IDs, e, callback) { - var kind = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : "UNSORTED"; - var display_root = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : true; - - var items = []; - - api.post("Albums::tree", {}, function (data) { - var addItems = function addItems(albums) { - // Disable all children - // It's not possible to move us into them - var i = void 0, - s = void 0; - var exclude = []; - for (i = 0; i < IDs.length; i++) { - var sub = contextMenu.getSubIDs(albums, IDs[i]); - for (s = 0; s < sub.length; s++) { - exclude.push(sub[s]); - } - } - if (visible.album()) { - // For merging, don't exclude the parent. - // For photo copy, don't exclude the current album. - if (callback !== album.merge && callback !== _photo.copyTo) { - exclude.push(album.getID().toString()); - } - if (IDs.length === 1 && IDs[0] === album.getID() && album.getParent() && callback === album.setAlbum) { - // If moving the current album, exclude its parent. - exclude.push(album.getParent().toString()); - } - } else if (visible.photo()) { - exclude.push(_photo.json.album.toString()); - } - items = items.concat(contextMenu.buildList(albums, exclude.concat(IDs), function (a) { - return callback(IDs, a.id); - })); - }; - - if (data.albums && data.albums.length > 0) { - // items = items.concat(contextMenu.buildList(data.albums, [ album.getID() ], (a) => callback(IDs, a.id))); //photo.setAlbum - - addItems(data.albums); - } - - if (data.shared_albums && data.shared_albums.length > 0 && lychee.admin) { - items = items.concat({}); - addItems(data.shared_albums); - } - - // Show Unsorted when unsorted is not the current album - if (display_root && album.getID() !== "0" && !visible.albums()) { - items.unshift({}); - items.unshift({ title: lychee.locale[kind], fn: function fn() { - return callback(IDs, 0); - } }); - } - - // Don't allow to move the current album to a newly created subalbum - // (creating a cycle). - if (IDs.length !== 1 || IDs[0] !== (album.json ? album.json.id : null) || callback !== album.setAlbum) { - items.unshift({}); - items.unshift({ title: lychee.locale["NEW_ALBUM"], fn: function fn() { - return album.add(IDs, callback); - } }); - } - - basicContext.show(items, e.originalEvent, contextMenu.close); - }); -}; - -contextMenu.sharePhoto = function (photoID, e) { - // v4+ only - if (_photo.json.hasOwnProperty("share_button_visible") && _photo.json.share_button_visible !== "1") { - return; - } - - var iconClass = "ionicons"; - - var items = [{ title: build.iconic("twitter", iconClass) + "Twitter", fn: function fn() { - return _photo.share(photoID, "twitter"); - } }, { title: build.iconic("facebook", iconClass) + "Facebook", fn: function fn() { - return _photo.share(photoID, "facebook"); - } }, { title: build.iconic("envelope-closed") + "Mail", fn: function fn() { - return _photo.share(photoID, "mail"); - } }, { title: build.iconic("dropbox", iconClass) + "Dropbox", visible: lychee.admin === true, fn: function fn() { - return _photo.share(photoID, "dropbox"); - } }, { title: build.iconic("link-intact") + lychee.locale["DIRECT_LINKS"], fn: function fn() { - return _photo.showDirectLinks(photoID); - } }]; - - basicContext.show(items, e.originalEvent); -}; - -contextMenu.shareAlbum = function (albumID, e) { - // v4+ only - if (album.json.hasOwnProperty("share_button_visible") && album.json.share_button_visible !== "1") { - return; - } - - var iconClass = "ionicons"; - - var items = [{ title: build.iconic("twitter", iconClass) + "Twitter", fn: function fn() { - return album.share("twitter"); - } }, { title: build.iconic("facebook", iconClass) + "Facebook", fn: function fn() { - return album.share("facebook"); - } }, { title: build.iconic("envelope-closed") + "Mail", fn: function fn() { - return album.share("mail"); - } }, { - title: build.iconic("link-intact") + lychee.locale["DIRECT_LINK"], - fn: function fn() { - var url = lychee.getBaseUrl() + "r/" + albumID; - if (album.json.password === "1") { - // Copy the url with prefilled password param - url += "?password="; - } - if (lychee.clipboardCopy(url)) { - loadingBar.show("success", lychee.locale["URL_COPIED_TO_CLIPBOARD"]); - } - } - }]; - - basicContext.show(items, e.originalEvent); -}; - -contextMenu.close = function () { - if (!visible.contextMenu()) return false; - - basicContext.close(); - - multiselect.clearSelection(); - if (visible.multiselect()) { - multiselect.close(); - } -}; - -contextMenu.config = function (e) { - var items = [{ title: build.iconic("cog") + lychee.locale["SETTINGS"], fn: settings.open }]; - if (lychee.admin) { - items.push({ title: build.iconic("person") + lychee.locale["USERS"], fn: users.list }); - } - items.push({ title: build.iconic("key") + lychee.locale["U2F"], fn: u2f.list }); - items.push({ title: build.iconic("cloud") + lychee.locale["SHARING"], fn: sharing.list }); - if (lychee.admin) { - items.push({ - title: build.iconic("align-left") + lychee.locale["LOGS"], - fn: function fn() { - view.logs.init(); - } - }); - items.push({ - title: build.iconic("wrench") + lychee.locale["DIAGNOSTICS"], - fn: function fn() { - view.diagnostics.init(); - } - }); - if (lychee.update_available) { - items.push({ title: build.iconic("timer") + lychee.locale["UPDATE_AVAILABLE"], fn: view.update.init }); - } - } - items.push({ title: build.iconic("info") + lychee.locale["ABOUT_LYCHEE"], fn: lychee.aboutDialog }); - items.push({ title: build.iconic("account-logout") + lychee.locale["SIGN_OUT"], fn: lychee.logout }); - - basicContext.show(items, e.originalEvent); -}; - -/** - * @description This module takes care of the header. - */ - -var header = { - _dom: $(".header") -}; - -header.dom = function (selector) { - if (selector == null || selector === "") return header._dom; - return header._dom.find(selector); -}; - -header.bind = function () { - // Event Name - var eventName = lychee.getEventName(); - - header.dom(".header__title").on(eventName, function (e) { - if ($(this).hasClass("header__title--editable") === false) return false; - - if (lychee.enable_contextmenu_header === false) return false; - - if (visible.photo()) contextMenu.photoTitle(album.getID(), _photo.getID(), e);else contextMenu.albumTitle(album.getID(), e); - }); - - header.dom("#button_visibility").on(eventName, function (e) { - _photo.setPublic(_photo.getID(), e); - }); - header.dom("#button_share").on(eventName, function (e) { - contextMenu.sharePhoto(_photo.getID(), e); - }); - - header.dom("#button_visibility_album").on(eventName, function (e) { - album.setPublic(album.getID(), e); - }); - - header.dom("#button_sharing_album_users").on(eventName, function (e) { - album.shareUsers(album.getID(), e); - }); - - header.dom("#button_share_album").on(eventName, function (e) { - contextMenu.shareAlbum(album.getID(), e); - }); - - header.dom("#button_signin").on(eventName, lychee.loginDialog); - header.dom("#button_settings").on(eventName, function (e) { - if ($(".leftMenu").css("display") === "none") { - // left menu disabled on small screens - contextMenu.config(e); - } else { - // standard left menu - leftMenu.open(); - } - }); - header.dom("#button_close_config").on(eventName, function () { - tabindex.makeFocusable(header.dom()); - tabindex.makeFocusable(lychee.content); - tabindex.makeUnfocusable(leftMenu._dom); - multiselect.bind(); - lychee.load(); - }); - header.dom("#button_info_album").on(eventName, _sidebar.toggle); - header.dom("#button_info").on(eventName, _sidebar.toggle); - header.dom(".button--map-albums").on(eventName, function () { - lychee.gotoMap(); - }); - header.dom("#button_map_album").on(eventName, function () { - lychee.gotoMap(album.getID()); - }); - header.dom("#button_map").on(eventName, function () { - lychee.gotoMap(album.getID()); - }); - header.dom(".button_add").on(eventName, contextMenu.add); - header.dom("#button_more").on(eventName, function (e) { - contextMenu.photoMore(_photo.getID(), e); - }); - header.dom("#button_move_album").on(eventName, function (e) { - contextMenu.move([album.getID()], e, album.setAlbum, "ROOT", album.getParent() != ""); - }); - header.dom("#button_nsfw_album").on(eventName, function (e) { - album.setNSFW(album.getID()); - }); - header.dom("#button_move").on(eventName, function (e) { - contextMenu.move([_photo.getID()], e, _photo.setAlbum); - }); - header.dom(".header__hostedwith").on(eventName, function () { - window.open(lychee.website); - }); - header.dom("#button_trash_album").on(eventName, function () { - album.delete([album.getID()]); - }); - header.dom("#button_trash").on(eventName, function () { - _photo.delete([_photo.getID()]); - }); - header.dom("#button_archive").on(eventName, function () { - album.getArchive([album.getID()]); - }); - header.dom("#button_star").on(eventName, function () { - _photo.setStar([_photo.getID()]); - }); - header.dom("#button_rotate_ccwise").on(eventName, function () { - photoeditor.rotate(_photo.getID(), -1); - }); - header.dom("#button_rotate_cwise").on(eventName, function () { - photoeditor.rotate(_photo.getID(), 1); - }); - header.dom("#button_back_home").on(eventName, function () { - if (!album.json.parent_id) { - lychee.goto(); - } else { - lychee.goto(album.getParent()); - } - }); - header.dom("#button_back").on(eventName, function () { - lychee.goto(album.getID()); - }); - header.dom("#button_back_map").on(eventName, function () { - lychee.goto(album.getID() || ""); - }); - header.dom("#button_fs_album_enter,#button_fs_enter").on(eventName, lychee.fullscreenEnter); - header.dom("#button_fs_album_exit,#button_fs_exit").on(eventName, lychee.fullscreenExit).hide(); - - header.dom(".header__search").on("keyup click", function () { - if ($(this).val().length > 0) { - lychee.goto("search/" + encodeURIComponent($(this).val())); - } else if (search.hash !== null) { - search.reset(); - } - }); - header.dom(".header__clear").on(eventName, function () { - search.reset(); - }); - - header.bind_back(); - - return true; -}; - -header.bind_back = function () { - // Event Name - var eventName = lychee.getEventName(); - - header.dom(".header__title").on(eventName, function () { - if (lychee.landing_page_enable && visible.albums()) { - window.location.href = "."; - } else { - return false; - } - }); -}; - -header.show = function () { - lychee.imageview.removeClass("full"); - header.dom().removeClass("header--hidden"); - - tabindex.restoreSettings(header.dom()); - - _photo.updateSizeLivePhotoDuringAnimation(); - - return true; -}; - -header.hideIfLivePhotoNotPlaying = function () { - // Hides the header, if current live photo is not playing - if (_photo.isLivePhotoPlaying() == true) return false; - return header.hide(); -}; - -header.hide = function () { - if (visible.photo() && !visible.sidebar() && !visible.contextMenu() && basicModal.visible() === false) { - tabindex.saveSettings(header.dom()); - tabindex.makeUnfocusable(header.dom()); - - lychee.imageview.addClass("full"); - header.dom().addClass("header--hidden"); - - _photo.updateSizeLivePhotoDuringAnimation(); - - return true; - } - - return false; -}; - -header.setTitle = function () { - var title = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : "Untitled"; - - var $title = header.dom(".header__title"); - var html = lychee.html(_templateObject43, title, build.iconic("caret-bottom")); - - $title.html(html); - - return true; -}; - -header.setMode = function (mode) { - if (mode === "albums" && lychee.publicMode === true) mode = "public"; - - switch (mode) { - case "public": - header.dom().removeClass("header--view"); - header.dom(".header__toolbar--albums, .header__toolbar--album, .header__toolbar--photo, .header__toolbar--map, .header__toolbar--config").removeClass("header__toolbar--visible"); - header.dom(".header__toolbar--public").addClass("header__toolbar--visible"); - tabindex.makeFocusable(header.dom(".header__toolbar--public")); - tabindex.makeUnfocusable(header.dom(".header__toolbar--albums, .header__toolbar--album, .header__toolbar--photo, .header__toolbar--map, .header__toolbar--config")); - - if (lychee.public_search) { - var e = $(".header__search, .header__clear", ".header__toolbar--public"); - e.show(); - tabindex.makeFocusable(e); - } else { - var _e2 = $(".header__search, .header__clear", ".header__toolbar--public"); - _e2.hide(); - tabindex.makeUnfocusable(_e2); - } - - // Set icon in Public mode - if (lychee.map_display_public) { - var _e3 = $(".button--map-albums", ".header__toolbar--public"); - _e3.show(); - tabindex.makeFocusable(_e3); - } else { - var _e4 = $(".button--map-albums", ".header__toolbar--public"); - _e4.hide(); - tabindex.makeUnfocusable(_e4); - } - - // Set focus on login button - if (lychee.active_focus_on_page_load) { - $("#button_signin").focus(); - } - return true; - - case "albums": - header.dom().removeClass("header--view"); - header.dom(".header__toolbar--public, .header__toolbar--album, .header__toolbar--photo, .header__toolbar--map, .header__toolbar--config").removeClass("header__toolbar--visible"); - header.dom(".header__toolbar--albums").addClass("header__toolbar--visible"); - - tabindex.makeFocusable(header.dom(".header__toolbar--albums")); - tabindex.makeUnfocusable(header.dom(".header__toolbar--public, .header__toolbar--album, .header__toolbar--photo, .header__toolbar--map, .header__toolbar--config")); - - // If map is disabled, we should hide the icon - if (lychee.map_display) { - var _e5 = $(".button--map-albums", ".header__toolbar--albums"); - _e5.show(); - tabindex.makeFocusable(_e5); - } else { - var _e6 = $(".button--map-albums", ".header__toolbar--albums"); - _e6.hide(); - tabindex.makeUnfocusable(_e6); - } - - if (lychee.enable_button_add) { - var _e7 = $(".button_add", ".header__toolbar--albums"); - _e7.show(); - tabindex.makeFocusable(_e7); - } else { - var _e8 = $(".button_add", ".header__toolbar--albums"); - _e8.remove(); - } - - return true; - - case "album": - var albumID = album.getID(); - - header.dom().removeClass("header--view"); - header.dom(".header__toolbar--public, .header__toolbar--albums, .header__toolbar--photo, .header__toolbar--map, .header__toolbar--config").removeClass("header__toolbar--visible"); - header.dom(".header__toolbar--album").addClass("header__toolbar--visible"); - - tabindex.makeFocusable(header.dom(".header__toolbar--album")); - tabindex.makeUnfocusable(header.dom(".header__toolbar--public, .header__toolbar--albums, .header__toolbar--photo, .header__toolbar--map, .header__toolbar--config")); - - // Hide download button when album empty or we are not allowed to - // upload to it and it's not explicitly marked as downloadable. - if (!album.json || album.json.photos === false && album.json.albums && album.json.albums.length === 0 || !album.isUploadable() && album.json.downloadable === "0") { - var _e9 = $("#button_archive"); - _e9.hide(); - tabindex.makeUnfocusable(_e9); - } else { - var _e10 = $("#button_archive"); - _e10.show(); - tabindex.makeFocusable(_e10); - } - - if (album.json && album.json.hasOwnProperty("share_button_visible") && album.json.share_button_visible !== "1") { - var _e11 = $("#button_share_album"); - _e11.hide(); - tabindex.makeUnfocusable(_e11); - } else { - var _e12 = $("#button_share_album"); - _e12.show(); - tabindex.makeFocusable(_e12); - } - - // If map is disabled, we should hide the icon - if (lychee.publicMode === true ? lychee.map_display_public : lychee.map_display) { - var _e13 = $("#button_map_album"); - _e13.show(); - tabindex.makeFocusable(_e13); - } else { - var _e14 = $("#button_map_album"); - _e14.hide(); - tabindex.makeUnfocusable(_e14); - } - - if (albumID === "starred" || albumID === "public" || albumID === "recent") { - $("#button_nsfw_album, #button_info_album, #button_trash_album, #button_visibility_album, #button_sharing_album_users, #button_move_album").hide(); - if (album.isUploadable()) { - $(".button_add, .header__divider", ".header__toolbar--album").show(); - tabindex.makeFocusable($(".button_add, .header__divider", ".header__toolbar--album")); - } else { - $(".button_add, .header__divider", ".header__toolbar--album").hide(); - tabindex.makeUnfocusable($(".button_add, .header__divider", ".header__toolbar--album")); - } - tabindex.makeUnfocusable($("#button_nsfw_album, #button_info_album, #button_trash_album, #button_visibility_album, #button_sharing_album_users, #button_move_album")); - } else if (albumID === "unsorted") { - $("#button_nsfw_album, #button_info_album, #button_visibility_album, #button_sharing_album_users, #button_move_album").hide(); - $("#button_trash_album, .button_add, .header__divider", ".header__toolbar--album").show(); - tabindex.makeFocusable($("#button_trash_album, .button_add, .header__divider", ".header__toolbar--album")); - tabindex.makeUnfocusable($("#button_nsfw_album, #button_info_album, #button_visibility_album, #button_sharing_album_users, #button_move_album")); - } else if (album.isTagAlbum()) { - $("#button_info_album").show(); - $("#button_nsfw_album, #button_move_album").hide(); - $(".button_add, .header__divider", ".header__toolbar--album").hide(); - tabindex.makeFocusable($("#button_info_album")); - tabindex.makeUnfocusable($("#button_nsfw_album, #button_move_album")); - tabindex.makeUnfocusable($(".button_add, .header__divider", ".header__toolbar--album")); - if (album.isUploadable()) { - $("#button_visibility_album, #button_sharing_album_users, #button_trash_album").show(); - tabindex.makeFocusable($("#button_visibility_album, #button_sharing_album_users, #button_trash_album")); - } else { - $("#button_visibility_album, #button_sharing_album_users, #button_trash_album").hide(); - tabindex.makeUnfocusable($("#button_visibility_album, #button_sharing_album_users, #button_trash_album")); - } - } else { - $("#button_info_album").show(); - tabindex.makeFocusable($("#button_info_album")); - if (album.isUploadable()) { - $("#button_nsfw_album, #button_trash_album, #button_move_album, #button_visibility_album, #button_sharing_album_users, .button_add, .header__divider", ".header__toolbar--album").show(); - tabindex.makeFocusable($("#button_nsfw_album, #button_trash_album, #button_move_album, #button_visibility_album, #button_sharing_album_users, .button_add, .header__divider", ".header__toolbar--album")); - } else { - $("#button_nsfw_album, #button_trash_album, #button_move_album, #button_visibility_album, #button_sharing_album_users, .button_add, .header__divider", ".header__toolbar--album").hide(); - tabindex.makeUnfocusable($("#button_nsfw_album, #button_trash_album, #button_move_album, #button_visibility_album, #button_sharing_album_users, .button_add, .header__divider", ".header__toolbar--album")); - } - } - - // Remove buttons if needed - if (!lychee.enable_button_visibility) { - var _e15 = $("#button_visibility_album", "#button_sharing_album_users", ".header__toolbar--album"); - _e15.remove(); - } - if (!lychee.enable_button_share) { - var _e16 = $("#button_share_album", ".header__toolbar--album"); - _e16.remove(); - } - if (!lychee.enable_button_archive) { - var _e17 = $("#button_archive", ".header__toolbar--album"); - _e17.remove(); - } - if (!lychee.enable_button_move) { - var _e18 = $("#button_move_album", ".header__toolbar--album"); - _e18.remove(); - } - if (!lychee.enable_button_trash) { - var _e19 = $("#button_trash_album", ".header__toolbar--album"); - _e19.remove(); - } - if (!lychee.enable_button_fullscreen || !lychee.fullscreenAvailable()) { - var _e20 = $("#button_fs_album_enter", ".header__toolbar--album"); - _e20.remove(); - } - if (!lychee.enable_button_add) { - var _e21 = $(".button_add", ".header__toolbar--album"); - _e21.remove(); - } - - return true; - - case "photo": - header.dom().addClass("header--view"); - header.dom(".header__toolbar--public, .header__toolbar--albums, .header__toolbar--album, .header__toolbar--map, .header__toolbar--config").removeClass("header__toolbar--visible"); - header.dom(".header__toolbar--photo").addClass("header__toolbar--visible"); - - tabindex.makeFocusable(header.dom(".header__toolbar--photo")); - tabindex.makeUnfocusable(header.dom(".header__toolbar--public, .header__toolbar--albums, .header__toolbar--album, .header__toolbar--map, .header__toolbar--config")); - // If map is disabled, we should hide the icon - if (lychee.publicMode === true ? lychee.map_display_public : lychee.map_display) { - var _e22 = $("#button_map"); - _e22.show(); - tabindex.makeFocusable(_e22); - } else { - var _e23 = $("#button_map"); - _e23.hide(); - tabindex.makeUnfocusable(_e23); - } - - if (album.isUploadable()) { - var _e24 = $("#button_trash, #button_move, #button_visibility, #button_star"); - _e24.show(); - tabindex.makeFocusable(_e24); - } else { - var _e25 = $("#button_trash, #button_move, #button_visibility, #button_star"); - _e25.hide(); - tabindex.makeUnfocusable(_e25); - } - - if (_photo.json && _photo.json.hasOwnProperty("share_button_visible") && _photo.json.share_button_visible !== "1") { - var _e26 = $("#button_share"); - _e26.hide(); - tabindex.makeUnfocusable(_e26); - } else { - var _e27 = $("#button_share"); - _e27.show(); - tabindex.makeFocusable(_e27); - } - - // Hide More menu if empty (see contextMenu.photoMore) - $("#button_more").show(); - tabindex.makeFocusable($("#button_more")); - if (!(album.isUploadable() || (_photo.json.hasOwnProperty("downloadable") ? _photo.json.downloadable === "1" : album.json && album.json.downloadable && album.json.downloadable === "1")) && !(_photo.json.url && _photo.json.url !== "")) { - var _e28 = $("#button_more"); - _e28.hide(); - tabindex.makeUnfocusable(_e28); - } - - // Remove buttons if needed - if (!lychee.enable_button_visibility) { - var _e29 = $("#button_visibility", ".header__toolbar--photo"); - _e29.remove(); - } - if (!lychee.enable_button_share) { - var _e30 = $("#button_share", ".header__toolbar--photo"); - _e30.remove(); - } - if (!lychee.enable_button_move) { - var _e31 = $("#button_move", ".header__toolbar--photo"); - _e31.remove(); - } - if (!lychee.enable_button_trash) { - var _e32 = $("#button_trash", ".header__toolbar--photo"); - _e32.remove(); - } - if (!lychee.enable_button_fullscreen || !lychee.fullscreenAvailable()) { - var _e33 = $("#button_fs_enter", ".header__toolbar--photo"); - _e33.remove(); - } - if (!lychee.enable_button_more) { - var _e34 = $("#button_more", ".header__toolbar--photo"); - _e34.remove(); - } - if (!lychee.enable_button_rotate) { - var _e35 = $("#button_rotate_cwise", ".header__toolbar--photo"); - _e35.remove(); - - _e35 = $("#button_rotate_ccwise", ".header__toolbar--photo"); - _e35.remove(); - } - return true; - case "map": - header.dom().removeClass("header--view"); - header.dom(".header__toolbar--public, .header__toolbar--album, .header__toolbar--albums, .header__toolbar--photo, .header__toolbar--config").removeClass("header__toolbar--visible"); - header.dom(".header__toolbar--map").addClass("header__toolbar--visible"); - - tabindex.makeFocusable(header.dom(".header__toolbar--map")); - tabindex.makeUnfocusable(header.dom(".header__toolbar--public, .header__toolbar--album, .header__toolbar--albums, .header__toolbar--photo, .header__toolbar--config")); - return true; - case "config": - header.dom().addClass("header--view"); - header.dom(".header__toolbar--public, .header__toolbar--albums, .header__toolbar--album, .header__toolbar--photo, .header__toolbar--map").removeClass("header__toolbar--visible"); - header.dom(".header__toolbar--config").addClass("header__toolbar--visible"); - return true; - } - - return false; -}; - -// Note that the pull-down menu is now enabled not only for editable -// items but for all of public/albums/album/photo views, so 'editable' is a -// bit of a misnomer at this point... -header.setEditable = function (editable) { - var $title = header.dom(".header__title"); - - if (editable) $title.addClass("header__title--editable");else $title.removeClass("header__title--editable"); - - return true; -}; - -/** - * @description This module is used for bindings. - */ - -$(document).ready(function () { - $("#sensitive_warning").hide(); - - // Event Name - var eventName = lychee.getEventName(); - - // set CSRF protection (Laravel) - csrf.bind(); - - // Set API error handler - api.onError = lychee.error; - - $("html").css("visibility", "visible"); - - // Multiselect - multiselect.bind(); - - // Header - header.bind(); - - // Image View - lychee.imageview.on(eventName, ".arrow_wrapper--previous", _photo.previous).on(eventName, ".arrow_wrapper--next", _photo.next).on(eventName, "img, #livephoto", _photo.cycle_display_overlay); - - // Keyboard - Mousetrap.addKeycodes({ - 18: "ContextMenu", - 179: "play_pause", - 227: "rewind", - 228: "forward" - }); - - Mousetrap.bind(["l"], function () { - lychee.loginDialog(); - return false; - }).bind(["k"], function () { - u2f.login(); - return false; - }).bind(["left"], function () { - if (visible.photo() && (!visible.header() || $("img#image").is(":focus") || $("img#livephoto").is(":focus") || $(":focus").length === 0)) { - $("#imageview a#previous").click(); - return false; - } - return true; - }).bind(["right"], function () { - if (visible.photo() && (!visible.header() || $("img#image").is(":focus") || $("img#livephoto").is(":focus") || $(":focus").length === 0)) { - $("#imageview a#next").click(); - return false; - } - return true; - }).bind(["u"], function () { - if (!visible.photo() && album.isUploadable()) { - $("#upload_files").click(); - return false; - } - }).bind(["n"], function () { - if (!visible.photo() && album.isUploadable()) { - album.add(); - return false; - } - }).bind(["s"], function () { - if (visible.photo() && album.isUploadable()) { - header.dom("#button_star").click(); - return false; - } else if (visible.albums()) { - header.dom(".header__search").focus(); - return false; - } - }).bind(["r"], function () { - if (album.isUploadable()) { - if (visible.album()) { - album.setTitle(album.getID()); - return false; - } else if (visible.photo()) { - _photo.setTitle([_photo.getID()]); - return false; - } - } - }).bind(["h"], album.toggle_nsfw_filter).bind(["d"], function () { - if (album.isUploadable()) { - if (visible.photo()) { - _photo.setDescription(_photo.getID()); - return false; - } else if (visible.album()) { - album.setDescription(album.getID()); - return false; - } - } - }).bind(["t"], function () { - if (visible.photo() && album.isUploadable()) { - _photo.editTags([_photo.getID()]); - return false; - } - }).bind(["i", "ContextMenu"], function () { - if (!visible.multiselect()) { - _sidebar.toggle(); - return false; - } - }).bind(["command+backspace", "ctrl+backspace"], function () { - if (album.isUploadable()) { - if (visible.photo() && basicModal.visible() === false) { - _photo.delete([_photo.getID()]); - return false; - } else if (visible.album() && basicModal.visible() === false) { - album.delete([album.getID()]); - return false; - } - } - }).bind(["command+a", "ctrl+a"], function () { - if (visible.album() && basicModal.visible() === false) { - multiselect.selectAll(); - return false; - } else if (visible.albums() && basicModal.visible() === false) { - multiselect.selectAll(); - return false; - } - }).bind(["o"], function () { - if (visible.photo()) { - _photo.cycle_display_overlay(); - return false; - } - }).bind(["f"], function () { - if (visible.album() || visible.photo()) { - lychee.fullscreenToggle(); - return false; - } - }); - - Mousetrap.bind(["play_pause"], function () { - // If it's a video, we toggle play/pause - var video = $("video"); - - if (video.length !== 0) { - if (video[0].paused) { - video[0].play(); - } else { - video[0].pause(); - } - } - }); - - Mousetrap.bindGlobal("enter", function () { - if (basicModal.visible() === true) { - // check if any of the input fields is focussed - // apply action, other do nothing - if ($(".basicModal__content input").is(":focus")) { - basicModal.action(); - return false; - } - } else if (visible.photo() && !lychee.header_auto_hide && ($("img#image").is(":focus") || $("img#livephoto").is(":focus") || $(":focus").length === 0)) { - if (visible.header()) { - header.hide(); - } else { - header.show(); - } - return false; - } - var clicked = false; - $(":focus").each(function () { - if (!$(this).is("input")) { - $(this).click(); - clicked = true; - } - }); - if (clicked) { - return false; - } - }); - - // Prevent 'esc keyup' event to trigger 'go back in history' - // and 'alt keyup' to show a webapp context menu for Fire TV - Mousetrap.bindGlobal(["esc", "ContextMenu"], function () { - return false; - }, "keyup"); - - Mousetrap.bindGlobal(["esc", "command+up"], function () { - if (basicModal.visible() === true) basicModal.cancel();else if (visible.config() || visible.leftMenu()) leftMenu.close();else if (visible.contextMenu()) contextMenu.close();else if (visible.photo()) lychee.goto(album.getID());else if (visible.album() && !album.json.parent_id) lychee.goto();else if (visible.album()) lychee.goto(album.getParent());else if (visible.albums() && search.hash !== null) search.reset();else if (visible.mapview()) mapview.close();else if (visible.albums() && lychee.enable_close_tab_on_esc) { - window.open("", "_self").close(); - } - return false; - }); - - $(document) - // Fullscreen on mobile - .on("touchend", "#imageview #image", function (e) { - // prevent triggering event 'mousemove' - // why? this also prevents 'click' from firing which results in unexpected behaviour - // unable to reproduce problems arising from 'mousemove' on iOS devices - // e.preventDefault(); - - if (typeof swipe.obj === "undefined" || Math.abs(swipe.offsetX) <= 5 && Math.abs(swipe.offsetY) <= 5) { - // Toggle header only if we're not moving to next/previous photo; - // In this case, swipe.preventNextHeaderToggle is set to true - if (typeof swipe.preventNextHeaderToggle === "undefined" || !swipe.preventNextHeaderToggle) { - if (visible.header()) { - header.hide(e); - } else { - header.show(); - } - } - - // For next 'touchend', behave again as normal and toggle header - swipe.preventNextHeaderToggle = false; - } - }); - $("#imageview") - // Swipe on mobile - .swipe().on("swipeStart", function () { - if (visible.photo()) swipe.start($("#imageview #image, #imageview #livephoto")); - }).swipe().on("swipeMove", function (e) { - if (visible.photo()) swipe.move(e.swipe); - }).swipe().on("swipeEnd", function (e) { - if (visible.photo()) swipe.stop(e.swipe, _photo.previous, _photo.next); - }); - - // Document - $(document) - // Navigation - .on("click", ".album", function (e) { - multiselect.albumClick(e, $(this)); - }).on("click", ".photo", function (e) { - multiselect.photoClick(e, $(this)); - }) - // Context Menu - .on("contextmenu", ".photo", function (e) { - multiselect.photoContextMenu(e, $(this)); - }).on("contextmenu", ".album", function (e) { - multiselect.albumContextMenu(e, $(this)); - }) - // Upload - .on("change", "#upload_files", function () { - basicModal.close(); - upload.start.local(this.files); - }) - // Drag and Drop upload - .on("dragover", function () { - return false; - }, false).on("drop", function (e) { - if (!album.isUploadable() || visible.contextMenu() || basicModal.visible() || visible.leftMenu() || visible.config() || !(visible.album() || visible.albums())) { - return false; - } - - // Detect if dropped item is a file or a link - if (e.originalEvent.dataTransfer.files.length > 0) upload.start.local(e.originalEvent.dataTransfer.files);else if (e.originalEvent.dataTransfer.getData("Text").length > 3) upload.start.url(e.originalEvent.dataTransfer.getData("Text")); - - return false; - }) - // click on thumbnail on map - .on("click", ".image-leaflet-popup", function (e) { - mapview.goto($(this)); - }) - // Paste upload - .on("paste", function (e) { - if (e.originalEvent.clipboardData.items) { - var items = e.originalEvent.clipboardData.items; - var filesToUpload = []; - - // Search clipboard items for an image - for (var i = 0; i < items.length; i++) { - if (items[i].type.indexOf("image") !== -1 || items[i].type.indexOf("video") !== -1) { - filesToUpload.push(items[i].getAsFile()); - } - } - - if (filesToUpload.length > 0) { - // We perform the check so deep because we don't want to - // prevent the paste from working in text input fields, etc. - if (album.isUploadable() && !visible.contextMenu() && !basicModal.visible() && !visible.leftMenu() && !visible.config() && (visible.album() || visible.albums())) { - upload.start.local(filesToUpload); - } - - return false; - } - } - }); - // Fullscreen - if (lychee.fullscreenAvailable()) $(document).on("fullscreenchange mozfullscreenchange webkitfullscreenchange msfullscreenchange", lychee.fullscreenUpdate); - - $("#sensitive_warning").on("click", view.album.nsfw_warning.next); - - var rememberScrollPage = function rememberScrollPage(scrollPos) { - if (visible.albums() && !visible.search() || visible.album()) { - var urls = JSON.parse(localStorage.getItem("scroll")); - if (urls == null || urls.length < 1) { - urls = {}; - } - - var urlWindow = window.location.href; - var urlScroll = scrollPos; - - urls[urlWindow] = urlScroll; - - if (urlScroll < 1) { - delete urls[urlWindow]; - } - - localStorage.setItem("scroll", JSON.stringify(urls)); - } - }; - - $(window) - // resize - .on("resize", function () { - if (visible.album() || visible.search()) view.album.content.justify(); - if (visible.photo()) view.photo.onresize(); - }) - // remember scroll positions - .on("scroll", function () { - var topScroll = $(window).scrollTop(); - rememberScrollPage(topScroll); - }); - - // Init - lychee.init(); -}); - -/** - * @description This module is used for the context menu. - */ - -var leftMenu = { - _dom: $(".leftMenu") -}; - -leftMenu.dom = function (selector) { - if (selector == null || selector === "") return leftMenu._dom; - return leftMenu._dom.find(selector); -}; - -leftMenu.build = function () { - var html = lychee.html(_templateObject44, lychee.locale["CLOSE"], lychee.locale["SETTINGS"]); - if (lychee.new_photos_notification) { - html += lychee.html(_templateObject45, build.iconic("bell"), lychee.locale["NOTIFICATIONS"]); - } - html += lychee.html(_templateObject46, build.iconic("person"), lychee.locale["USERS"], build.iconic("key"), lychee.locale["U2F"], build.iconic("cloud"), lychee.locale["SHARING"]); - html += lychee.html(_templateObject47, build.iconic("align-left"), lychee.locale["LOGS"], build.iconic("wrench"), lychee.locale["DIAGNOSTICS"], build.iconic("info"), lychee.locale["ABOUT_LYCHEE"], build.iconic("account-logout"), lychee.locale["SIGN_OUT"]); - if (lychee.update_available) { - html += lychee.html(_templateObject48, build.iconic("timer"), lychee.locale["UPDATE_AVAILABLE"]); - } - leftMenu._dom.html(html); -}; - -/* Set the width of the side navigation to 250px and the left margin of the page content to 250px */ -leftMenu.open = function () { - leftMenu._dom.addClass("leftMenu__visible"); - lychee.content.addClass("leftMenu__open"); - lychee.footer.addClass("leftMenu__open"); - header.dom(".header__title").addClass("leftMenu__open"); - loadingBar.dom().addClass("leftMenu__open"); - - // Make background unfocusable - tabindex.makeUnfocusable(header.dom()); - tabindex.makeUnfocusable(lychee.content); - tabindex.makeFocusable(leftMenu._dom); - $("#button_signout").focus(); - - multiselect.unbind(); -}; - -/* Set the width of the side navigation to 0 and the left margin of the page content to 0 */ -leftMenu.close = function () { - leftMenu._dom.removeClass("leftMenu__visible"); - lychee.content.removeClass("leftMenu__open"); - lychee.footer.removeClass("leftMenu__open"); - $(".content").removeClass("leftMenu__open"); - header.dom(".header__title").removeClass("leftMenu__open"); - loadingBar.dom().removeClass("leftMenu__open"); - - tabindex.makeFocusable(header.dom()); - tabindex.makeFocusable(lychee.content); - tabindex.makeUnfocusable(leftMenu._dom); - - multiselect.bind(); - lychee.load(); -}; - -leftMenu.bind = function () { - // Event Name - var eventName = lychee.getEventName(); - - leftMenu.dom("#button_settings_close").on(eventName, leftMenu.close); - leftMenu.dom("#text_settings_close").on(eventName, leftMenu.close); - leftMenu.dom("#button_settings_open").on(eventName, settings.open); - leftMenu.dom("#button_signout").on(eventName, lychee.logout); - leftMenu.dom("#button_logs").on(eventName, leftMenu.Logs); - leftMenu.dom("#button_diagnostics").on(eventName, leftMenu.Diagnostics); - leftMenu.dom("#button_about").on(eventName, lychee.aboutDialog); - leftMenu.dom("#button_notifications").on(eventName, leftMenu.Notifications); - leftMenu.dom("#button_users").on(eventName, leftMenu.Users); - leftMenu.dom("#button_u2f").on(eventName, leftMenu.u2f); - leftMenu.dom("#button_sharing").on(eventName, leftMenu.Sharing); - leftMenu.dom("#button_update").on(eventName, leftMenu.Update); - - return true; -}; - -leftMenu.Logs = function () { - view.logs.init(); -}; - -leftMenu.Diagnostics = function () { - view.diagnostics.init(); -}; - -leftMenu.Update = function () { - view.update.init(); -}; - -leftMenu.Notifications = function () { - notifications.load(); -}; - -leftMenu.Users = function () { - users.list(); -}; - -leftMenu.u2f = function () { - u2f.list(); -}; - -leftMenu.Sharing = function () { - sharing.list(); -}; - -/** - * @description This module is used to show and hide the loading bar. - */ - -var loadingBar = { - status: null, - _dom: $("#loading") -}; - -loadingBar.dom = function (selector) { - if (selector == null || selector === "") return loadingBar._dom; - return loadingBar._dom.find(selector); -}; - -loadingBar.show = function (status, errorText) { - if (status === "error") { - // Set status - loadingBar.status = "error"; - - // Parse text - if (errorText) errorText = errorText.replace("
", ""); - if (!errorText) errorText = lychee.locale["ERROR_TEXT"]; - - // Move header down - if (visible.header()) header.dom().addClass("header--error"); - - // Also move down the dark background - if (basicModal.visible()) { - $(".basicModalContainer").addClass("basicModalContainer--error"); - $(".basicModal").addClass("basicModal--error"); - } - - // Modify loading - loadingBar.dom().removeClass("loading uploading error success").html("

" + lychee.locale["ERROR"] + (": " + errorText + "

")).addClass(status).show(); - - // Set timeout - clearTimeout(loadingBar._timeout); - loadingBar._timeout = setTimeout(function () { - return loadingBar.hide(true); - }, 3000); - - return true; - } - - if (status === "success") { - // Set status - loadingBar.status = "success"; - - // Parse text - if (errorText) errorText = errorText.replace("
", ""); - if (!errorText) errorText = lychee.locale["ERROR_TEXT"]; - - // Move header down - if (visible.header()) header.dom().addClass("header--error"); - - // Also move down the dark background - if (basicModal.visible()) { - $(".basicModalContainer").addClass("basicModalContainer--error"); - $(".basicModal").addClass("basicModal--error"); - } - - // Modify loading - loadingBar.dom().removeClass("loading uploading error success").html("

" + lychee.locale["SUCCESS"] + (": " + errorText + "

")).addClass(status).show(); - - // Set timeout - clearTimeout(loadingBar._timeout); - loadingBar._timeout = setTimeout(function () { - return loadingBar.hide(true); - }, 2000); - - return true; - } - - if (loadingBar.status === null) { - // Set status - loadingBar.status = lychee.locale["LOADING"]; - - // Set timeout - clearTimeout(loadingBar._timeout); - loadingBar._timeout = setTimeout(function () { - // Move header down - if (visible.header()) header.dom().addClass("header--loading"); - - // Modify loading - loadingBar.dom().removeClass("loading uploading error").html("").addClass("loading").show(); - }, 1000); - - return true; - } -}; - -loadingBar.hide = function (force) { - if (loadingBar.status !== "error" && loadingBar.status !== "success" && loadingBar.status != null || force) { - // Remove status - loadingBar.status = null; - - // Move header up - header.dom().removeClass("header--error header--loading"); - // Also move up the dark background - $(".basicModalContainer").removeClass("basicModalContainer--error"); - $(".basicModal").removeClass("basicModal--error"); - - // Set timeout - clearTimeout(loadingBar._timeout); - setTimeout(function () { - return loadingBar.dom().hide(); - }, 300); - } -}; - -/** - * @description This module provides the basic functions of Lychee. - */ - -var lychee = { - title: document.title, - version: "", - versionCode: "", // not really needed anymore - - updatePath: "https://LycheeOrg.github.io/update.json", - updateURL: "https://github.com/LycheeOrg/Lychee/releases", - website: "https://LycheeOrg.github.io", - - publicMode: false, - viewMode: false, - full_photo: true, - downloadable: false, - public_photos_hidden: true, - share_button_visible: false, // enable only v4+ - api_V2: false, // enable api_V2 - sub_albums: false, // enable sub_albums features - admin: false, // enable admin mode (multi-user) - upload: false, // enable possibility to upload (multi-user) - lock: false, // locked user (multi-user) - username: null, - layout: "1", // 0: Use default, "square" layout. 1: Use Flickr-like "justified" layout. 2: Use Google-like "unjustified" layout - public_search: false, // display Search in publicMode - image_overlay_type: "exif", // current Overlay display type - image_overlay_type_default: "exif", // image overlay type default type - map_display: false, // display photo coordinates on map - map_display_public: false, // display photos of public album on map (user not logged in) - map_display_direction: true, // use the GPS direction data on displayed maps - map_provider: "Wikimedia", // Provider of OSM Tiles - map_include_subalbums: false, // include photos of subalbums on map - location_decoding: false, // retrieve location name from GPS data - location_decoding_caching_type: "Harddisk", // caching mode for GPS data decoding - location_show: false, // show location name - location_show_public: false, // show location name for public albums - swipe_tolerance_x: 150, // tolerance for navigating when swiping images to the left and right on mobile - swipe_tolerance_y: 250, // tolerance for navigating when swiping images up and down - - landing_page_enabled: false, // is landing page enabled ? - delete_imported: false, - import_via_symlink: false, - skip_duplicates: false, - - nsfw_visible: true, - nsfw_visible_saved: true, - nsfw_blur: false, - nsfw_warning: false, - - album_subtitle_type: "oldstyle", - - upload_processing_limit: 4, - - // this is device specific config, in this case default is Desktop. - header_auto_hide: true, - active_focus_on_page_load: false, - enable_button_visibility: true, - enable_button_share: true, - enable_button_archive: true, - enable_button_move: true, - enable_button_trash: true, - enable_button_fullscreen: true, - enable_button_download: true, - enable_button_add: true, - enable_button_more: true, - enable_button_rotate: true, - enable_close_tab_on_esc: false, - enable_tabindex: false, - enable_contextmenu_header: true, - hide_content_during_imageview: false, - device_type: "desktop", - - checkForUpdates: "1", - update_json: 0, - update_available: false, - new_photos_notification: false, - sortingPhotos: "", - sortingAlbums: "", - location: "", - - lang: "", - lang_available: {}, - - dropbox: false, - dropboxKey: "", - - content: $(".content"), - imageview: $("#imageview"), - footer: $("#footer"), - - locale: {}, - - nsfw_unlocked_albums: [] -}; - -lychee.diagnostics = function () { - return "/Diagnostics"; -}; - -lychee.logs = function () { - return "/Logs"; -}; - -lychee.aboutDialog = function () { - var msg = lychee.html(_templateObject49, lychee.version, lychee.updateURL, lychee.locale["UPDATE_AVAILABLE"], lychee.locale["ABOUT_SUBTITLE"], lychee.website, lychee.locale["ABOUT_DESCRIPTION"]); - - basicModal.show({ - body: msg, - buttons: { - cancel: { - title: lychee.locale["CLOSE"], - fn: basicModal.close - } - } - }); - - if (lychee.checkForUpdates === "1") lychee.getUpdate(); -}; - -lychee.init = function () { - var exitview = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; - - lychee.adjustContentHeight(); - - api.post("Session::init", {}, function (data) { - if (data.status === 0) { - // No configuration - - lychee.setMode("public"); - - header.dom().hide(); - lychee.content.hide(); - $("body").append(build.no_content("cog")); - settings.createConfig(); - - return true; - } - - lychee.sub_albums = data.sub_albums || false; - lychee.update_json = data.update_json; - lychee.update_available = data.update_available; - lychee.landing_page_enable = data.config.landing_page_enable && data.config.landing_page_enable === "1" || false; - lychee.new_photos_notification = false; - - lychee.versionCode = data.config.version; - if (lychee.versionCode !== "") { - var digits = lychee.versionCode.match(/.{1,2}/g); - lychee.version = parseInt(digits[0]).toString() + "." + parseInt(digits[1]).toString() + "." + parseInt(digits[2]).toString(); - } - - // we copy the locale that exists only. - // This ensure forward and backward compatibility. - // e.g. if the front localization is unfished in a language - // or if we need to change some locale string - for (var key in data.locale) { - lychee.locale[key] = data.locale[key]; - } - - var validatedSwipeToleranceX = data.config.swipe_tolerance_x && !isNaN(parseInt(data.config.swipe_tolerance_x)) && parseInt(data.config.swipe_tolerance_x) || 150; - var validatedSwipeToleranceY = data.config.swipe_tolerance_y && !isNaN(parseInt(data.config.swipe_tolerance_y)) && parseInt(data.config.swipe_tolerance_y) || 250; - - // Check status - // 0 = No configuration - // 1 = Logged out - // 2 = Logged in - if (data.status === 2) { - // Logged in - - lychee.sortingPhotos = data.config.sorting_Photos || data.config.sortingPhotos || ""; - lychee.sortingAlbums = data.config.sorting_Albums || data.config.sortingAlbums || ""; - lychee.album_subtitle_type = data.config.album_subtitle_type || "oldstyle"; - lychee.dropboxKey = data.config.dropbox_key || data.config.dropboxKey || ""; - lychee.location = data.config.location || ""; - lychee.checkForUpdates = data.config.check_for_updates || data.config.checkForUpdates || "1"; - lychee.lang = data.config.lang || ""; - lychee.lang_available = data.config.lang_available || {}; - lychee.layout = data.config.layout || "1"; - lychee.public_search = data.config.public_search && data.config.public_search === "1" || false; - lychee.image_overlay_type = !data.config.image_overlay_type ? "exif" : data.config.image_overlay_type; - lychee.image_overlay_type_default = lychee.image_overlay_type; - lychee.map_display = data.config.map_display && data.config.map_display === "1" || false; - lychee.map_display_public = data.config.map_display_public && data.config.map_display_public === "1" || false; - lychee.map_display_direction = data.config.map_display_direction && data.config.map_display_direction === "1" || false; - lychee.map_provider = !data.config.map_provider ? "Wikimedia" : data.config.map_provider; - lychee.map_include_subalbums = data.config.map_include_subalbums && data.config.map_include_subalbums === "1" || false; - lychee.location_decoding = data.config.location_decoding && data.config.location_decoding === "1" || false; - lychee.location_decoding_caching_type = !data.config.location_decoding_caching_type ? "Harddisk" : data.config.location_decoding_caching_type; - lychee.location_show = data.config.location_show && data.config.location_show === "1" || false; - lychee.location_show_public = data.config.location_show_public && data.config.location_show_public === "1" || false; - lychee.swipe_tolerance_x = validatedSwipeToleranceX; - lychee.swipe_tolerance_y = validatedSwipeToleranceY; - - lychee.default_license = data.config.default_license || "none"; - lychee.css = data.config.css || ""; - lychee.full_photo = data.config.full_photo == null || data.config.full_photo === "1"; - lychee.downloadable = data.config.downloadable && data.config.downloadable === "1" || false; - lychee.public_photos_hidden = data.config.public_photos_hidden == null || data.config.public_photos_hidden === "1"; - lychee.share_button_visible = data.config.share_button_visible && data.config.share_button_visible === "1" || false; - lychee.delete_imported = data.config.delete_imported && data.config.delete_imported === "1"; - lychee.import_via_symlink = data.config.import_via_symlink && data.config.import_via_symlink === "1"; - lychee.skip_duplicates = data.config.skip_duplicates && data.config.skip_duplicates === "1"; - lychee.nsfw_visible = data.config.nsfw_visible && data.config.nsfw_visible === "1" || false; - lychee.nsfw_blur = data.config.nsfw_blur && data.config.nsfw_blur === "1" || false; - lychee.nsfw_warning = data.config.nsfw_warning_admin && data.config.nsfw_warning_admin === "1" || false; - - lychee.header_auto_hide = data.config_device.header_auto_hide; - lychee.active_focus_on_page_load = data.config_device.active_focus_on_page_load; - lychee.enable_button_visibility = data.config_device.enable_button_visibility; - lychee.enable_button_share = data.config_device.enable_button_share; - lychee.enable_button_archive = data.config_device.enable_button_archive; - lychee.enable_button_move = data.config_device.enable_button_move; - lychee.enable_button_trash = data.config_device.enable_button_trash; - lychee.enable_button_fullscreen = data.config_device.enable_button_fullscreen; - lychee.enable_button_download = data.config_device.enable_button_download; - lychee.enable_button_add = data.config_device.enable_button_add; - lychee.enable_button_more = data.config_device.enable_button_more; - lychee.enable_button_rotate = data.config_device.enable_button_rotate; - lychee.enable_close_tab_on_esc = data.config_device.enable_close_tab_on_esc; - lychee.enable_tabindex = data.config_device.enable_tabindex; - lychee.enable_contextmenu_header = data.config_device.enable_contextmenu_header; - lychee.hide_content_during_imgview = data.config_device.hide_content_during_imgview; - lychee.device_type = data.config_device.device_type || "desktop"; // we set default as Desktop - - lychee.editor_enabled = data.config.editor_enabled && data.config.editor_enabled === "1" || false; - - lychee.nsfw_visible_saved = lychee.nsfw_visible; - - lychee.new_photos_notification = data.config.new_photos_notification && data.config.new_photos_notification === "1" || false; - - lychee.upload_processing_limit = parseInt(data.config.upload_processing_limit); - // when null or any non stringified numeric value is sent from the server we get NaN. - // we fix this. - if (isNaN(lychee.upload_processing_limit)) lychee.upload_processing_limit = 4; - - // leftMenu - leftMenu.build(); - leftMenu.bind(); - - lychee.upload = data.admin || data.upload; - lychee.admin = data.admin; - lychee.lock = data.lock; - lychee.username = data.username; - lychee.setMode("logged_in"); - - // Show dialog when there is no username and password - if (data.config.login === false) settings.createLogin(); - } else if (data.status === 1) { - // Logged out - - // TODO remove sortingPhoto once the v4 is out - lychee.sortingPhotos = data.config.sorting_Photos || data.config.sortingPhotos || ""; - lychee.sortingAlbums = data.config.sorting_Albums || data.config.sortingAlbums || ""; - lychee.album_subtitle_type = data.config.album_subtitle_type || "oldstyle"; - lychee.checkForUpdates = data.config.check_for_updates || data.config.checkForUpdates || "1"; - lychee.layout = data.config.layout || "1"; - lychee.public_search = data.config.public_search && data.config.public_search === "1" || false; - lychee.image_overlay_type = !data.config.image_overlay_type ? "exif" : data.config.image_overlay_type; - lychee.image_overlay_type_default = lychee.image_overlay_type; - lychee.map_display = data.config.map_display && data.config.map_display === "1" || false; - lychee.map_display_public = data.config.map_display_public && data.config.map_display_public === "1" || false; - lychee.map_display_direction = data.config.map_display_direction && data.config.map_display_direction === "1" || false; - lychee.map_provider = !data.config.map_provider ? "Wikimedia" : data.config.map_provider; - lychee.map_include_subalbums = data.config.map_include_subalbums && data.config.map_include_subalbums === "1" || false; - lychee.location_show = data.config.location_show && data.config.location_show === "1" || false; - lychee.location_show_public = data.config.location_show_public && data.config.location_show_public === "1" || false; - lychee.swipe_tolerance_x = validatedSwipeToleranceX; - lychee.swipe_tolerance_y = validatedSwipeToleranceY; - - lychee.nsfw_visible = data.config.nsfw_visible && data.config.nsfw_visible === "1" || false; - lychee.nsfw_blur = data.config.nsfw_blur && data.config.nsfw_blur === "1" || false; - lychee.nsfw_warning = data.config.nsfw_warning && data.config.nsfw_warning === "1" || false; - - lychee.header_auto_hide = data.config_device.header_auto_hide; - lychee.active_focus_on_page_load = data.config_device.active_focus_on_page_load; - lychee.enable_button_visibility = data.config_device.enable_button_visibility; - lychee.enable_button_share = data.config_device.enable_button_share; - lychee.enable_button_archive = data.config_device.enable_button_archive; - lychee.enable_button_move = data.config_device.enable_button_move; - lychee.enable_button_trash = data.config_device.enable_button_trash; - lychee.enable_button_fullscreen = data.config_device.enable_button_fullscreen; - lychee.enable_button_download = data.config_device.enable_button_download; - lychee.enable_button_add = data.config_device.enable_button_add; - lychee.enable_button_more = data.config_device.enable_button_more; - lychee.enable_button_rotate = data.config_device.enable_button_rotate; - lychee.enable_close_tab_on_esc = data.config_device.enable_close_tab_on_esc; - lychee.enable_tabindex = data.config_device.enable_tabindex; - lychee.enable_contextmenu_header = data.config_device.enable_contextmenu_header; - lychee.hide_content_during_imgview = data.config_device.hide_content_during_imgview; - lychee.device_type = data.config_device.device_type || "desktop"; // we set default as Desktop - lychee.nsfw_visible_saved = lychee.nsfw_visible; - - // console.log(lychee.full_photo); - lychee.setMode("public"); - } else { - // should not happen. - } - - if (exitview) { - $(window).bind("popstate", lychee.load); - lychee.load(); - } - }); -}; - -lychee.login = function (data) { - var username = data.username; - var password = data.password; - - if (!username.trim()) { - basicModal.error("username"); - return; - } - if (!password.trim()) { - basicModal.error("password"); - return; - } - - var params = { - username: username, - password: password - }; - - api.post("Session::login", params, function (_data) { - if (_data === true) { - window.location.reload(); - } else { - // Show error and reactive button - basicModal.error("password"); - } - }); -}; - -lychee.loginDialog = function () { - // Make background make unfocusable - tabindex.makeUnfocusable(header.dom()); - tabindex.makeUnfocusable(lychee.content); - tabindex.makeUnfocusable(lychee.imageview); - - var msg = lychee.html(_templateObject50, build.iconic("key"), lychee.locale["USERNAME"], tabindex.get_next_tab_index(), lychee.locale["PASSWORD"], tabindex.get_next_tab_index(), lychee.version, lychee.updateURL, lychee.locale["UPDATE_AVAILABLE"]); - - basicModal.show({ - body: msg, - buttons: { - action: { - title: lychee.locale["SIGN_IN"], - fn: lychee.login, - attributes: [["data-tabindex", tabindex.get_next_tab_index()]] - }, - cancel: { - title: lychee.locale["CANCEL"], - fn: basicModal.close, - attributes: [["data-tabindex", tabindex.get_next_tab_index()]] - } - } - }); - $("#signInKeyLess").on("click", u2f.login); - - if (lychee.checkForUpdates === "1") lychee.getUpdate(); - - tabindex.makeFocusable(basicModal.dom()); -}; - -lychee.logout = function () { - api.post("Session::logout", {}, function () { - window.location.reload(); - }); -}; - -lychee.goto = function () { - var url = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ""; - var autoplay = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; - - url = "#" + url; - - history.pushState(null, null, url); - lychee.load(autoplay); -}; - -lychee.gotoMap = function () { - var albumID = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ""; - var autoplay = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; - - // If map functionality is disabled -> go to album - if (!lychee.map_display) { - loadingBar.show("error", lychee.locale["ERROR_MAP_DEACTIVATED"]); - return; - } - lychee.goto("map/" + albumID, autoplay); -}; - -lychee.load = function () { - var autoplay = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; - - var albumID = ""; - var photoID = ""; - var hash = document.location.hash.replace("#", "").split("/"); - - contextMenu.close(); - multiselect.close(); - tabindex.reset(); - - if (hash[0] != null) albumID = hash[0]; - if (hash[1] != null) photoID = hash[1]; - - if (albumID && photoID) { - if (albumID == "map") { - // If map functionality is disabled -> do nothing - if (!lychee.map_display) { - loadingBar.show("error", lychee.locale["ERROR_MAP_DEACTIVATED"]); - return; - } - $(".no_content").remove(); - // show map - // albumID has been stored in photoID due to URL format #map/albumID - albumID = photoID; - - // Trash data - _photo.json = null; - - // Show Album -> it's below the map - if (visible.photo()) view.photo.hide(); - if (visible.sidebar()) _sidebar.toggle(); - if (album.json && albumID === album.json.id) { - view.album.title(); - } - mapview.open(albumID); - lychee.footer_hide(); - } else if (albumID == "search") { - // Search has been triggered - var search_string = decodeURIComponent(photoID); - - if (search_string.trim() === "") { - // do nothing on "only space" search strings - return; - } - // If public search is diabled -> do nothing - if (lychee.publicMode === true && !lychee.public_search) { - loadingBar.show("error", lychee.locale["ERROR_SEARCH_DEACTIVATED"]); - return; - } - - header.dom(".header__search").val(search_string); - search.find(search_string); - - lychee.footer_show(); - } else { - $(".no_content").remove(); - // Show photo - - // Trash data - _photo.json = null; - - // Show Photo - if (lychee.content.html() === "" || album.json == null || header.dom(".header__search").length && header.dom(".header__search").val().length !== 0) { - lychee.content.hide(); - album.load(albumID, true); - } - _photo.load(photoID, albumID, autoplay); - - // Make imageview focussable - tabindex.makeFocusable(lychee.imageview); - - // Make thumbnails unfocusable and store which element had focus - tabindex.makeUnfocusable(lychee.content, true); - - // hide contentview if requested - if (lychee.hide_content_during_imgview) lychee.content.hide(); - - lychee.footer_hide(); - } - } else if (albumID) { - if (albumID == "map") { - $(".no_content").remove(); - // Show map of all albums - // If map functionality is disabled -> do nothing - if (!lychee.map_display) { - loadingBar.show("error", lychee.locale["ERROR_MAP_DEACTIVATED"]); - return; - } - - // Trash data - _photo.json = null; - - // Show Album -> it's below the map - if (visible.photo()) view.photo.hide(); - if (visible.sidebar()) _sidebar.toggle(); - mapview.open(); - lychee.footer_hide(); - } else if (albumID == "search") { - // search string is empty -> do nothing - } else { - $(".no_content").remove(); - // Trash data - _photo.json = null; - - // Show Album - if (visible.photo()) { - view.photo.hide(); - tabindex.makeUnfocusable(lychee.imageview); - } - if (visible.mapview()) mapview.close(); - if (visible.sidebar() && album.isSmartID(albumID)) _sidebar.toggle(); - $("#sensitive_warning").hide(); - if (album.json && albumID === album.json.id) { - view.album.title(); - lychee.content.show(); - tabindex.makeFocusable(lychee.content, true); - } else { - album.load(albumID); - } - lychee.footer_show(); - } - } else { - $(".no_content").remove(); - // Trash albums.json when filled with search results - if (search.hash != null) { - albums.json = null; - search.hash = null; - } - - // Trash data - album.json = null; - _photo.json = null; - - // Hide sidebar - if (visible.sidebar()) _sidebar.toggle(); - - // Show Albums - if (visible.photo()) { - view.photo.hide(); - tabindex.makeUnfocusable(lychee.imageview); - } - if (visible.mapview()) mapview.close(); - $("#sensitive_warning").hide(); - lychee.content.show(); - lychee.footer_show(); - albums.load(); - } -}; - -lychee.getUpdate = function () { - // console.log(lychee.update_available); - // console.log(lychee.update_json); - - if (lychee.update_json !== 0) { - if (lychee.update_available) { - $(".version span").show(); - } - } else { - var success = function success(data) { - if (data.lychee.version > parseInt(lychee.versionCode)) $(".version span").show(); - }; - - $.ajax({ - url: lychee.updatePath, - success: success - }); - } -}; - -lychee.setTitle = function (title, editable) { - if (lychee.title === title) { - document.title = lychee.title + " - " + lychee.locale["ALBUMS"]; - } else { - document.title = lychee.title + " - " + title; - } - - header.setEditable(editable); - header.setTitle(title); -}; - -lychee.setMode = function (mode) { - if (lychee.lock) { - $("#button_settings_open").remove(); - } - if (!lychee.upload) { - $("#button_sharing").remove(); - - $(document).off("click", ".header__title--editable").off("touchend", ".header__title--editable").off("contextmenu", ".photo").off("contextmenu", ".album").off("drop"); - - Mousetrap.unbind(["u"]).unbind(["s"]).unbind(["n"]).unbind(["r"]).unbind(["d"]).unbind(["t"]).unbind(["command+backspace", "ctrl+backspace"]).unbind(["command+a", "ctrl+a"]); - } - if (!lychee.admin) { - $("#button_users, #button_logs, #button_diagnostics").remove(); - } - - if (mode === "logged_in") { - // we are logged in, we do not need that short cut anymore. :) - Mousetrap.unbind(["l"]).unbind(["k"]); - - // The code searches by class, so remove the other instance. - $(".header__search, .header__clear", ".header__toolbar--public").remove(); - - if (!lychee.editor_enabled) { - $("#button_rotate_cwise").remove(); - $("#button_rotate_ccwise").remove(); - } - return; - } else { - $(".header__search, .header__clear", ".header__toolbar--albums").remove(); - $("#button_rotate_cwise").remove(); - $("#button_rotate_ccwise").remove(); - } - - $("#button_settings, .header__divider, .leftMenu").remove(); - - if (mode === "public") { - lychee.publicMode = true; - } else if (mode === "view") { - Mousetrap.unbind(["esc", "command+up"]); - - $("#button_back, a#next, a#previous").remove(); - $(".no_content").remove(); - - lychee.publicMode = true; - lychee.viewMode = true; - } - - // just mak - header.bind_back(); -}; - -lychee.animate = function (obj, animation) { - var animations = [["fadeIn", "fadeOut"], ["contentZoomIn", "contentZoomOut"]]; - - if (!obj.jQuery) obj = $(obj); - - for (var i = 0; i < animations.length; i++) { - for (var x = 0; x < animations[i].length; x++) { - if (animations[i][x] == animation) { - obj.removeClass(animations[i][0] + " " + animations[i][1]).addClass(animation); - return true; - } - } - } - - return false; -}; - -lychee.retinize = function () { - var path = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ""; - - var extention = path.split(".").pop(); - var isPhoto = extention !== "svg"; - - if (isPhoto === true) { - path = path.replace(/\.[^/.]+$/, ""); - path = path + "@2x" + "." + extention; - } - - return { - path: path, - isPhoto: isPhoto - }; -}; - -lychee.loadDropbox = function (callback) { - if (lychee.dropbox === false && lychee.dropboxKey != null && lychee.dropboxKey !== "") { - loadingBar.show(); - - var g = document.createElement("script"); - var s = document.getElementsByTagName("script")[0]; - - g.src = "https://www.dropbox.com/static/api/1/dropins.js"; - g.id = "dropboxjs"; - g.type = "text/javascript"; - g.async = "true"; - g.setAttribute("data-app-key", lychee.dropboxKey); - g.onload = g.onreadystatechange = function () { - var rs = this.readyState; - if (rs && rs !== "complete" && rs !== "loaded") return; - lychee.dropbox = true; - loadingBar.hide(); - callback(); - }; - s.parentNode.insertBefore(g, s); - } else if (lychee.dropbox === true && lychee.dropboxKey != null && lychee.dropboxKey !== "") { - callback(); - } else { - settings.setDropboxKey(callback); - } -}; - -lychee.getEventName = function () { - if (lychee.device_type === "mobile") { - return "touchend"; - } - return "click"; -}; - -lychee.escapeHTML = function () { - var html = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ""; - - // Ensure that html is a string - html += ""; - - // Escape all critical characters - html = html.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """).replace(/'/g, "'").replace(/`/g, "`"); - - return html; -}; - -lychee.html = function (literalSections) { - // Use raw literal sections: we don’t want - // backslashes (\n etc.) to be interpreted - var raw = literalSections.raw; - var result = ""; - - for (var _len = arguments.length, substs = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { - substs[_key - 1] = arguments[_key]; - } - - substs.forEach(function (subst, i) { - // Retrieve the literal section preceding - // the current substitution - var lit = raw[i]; - - // If the substitution is preceded by a dollar sign, - // we escape special characters in it - if (lit.slice(-1) === "$") { - subst = lychee.escapeHTML(subst); - lit = lit.slice(0, -1); - } - - result += lit; - result += subst; - }); - - // Take care of last literal section - // (Never fails, because an empty template string - // produces one literal section, an empty string) - result += raw[raw.length - 1]; - - return result; -}; - -lychee.error = function (errorThrown) { - var params = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ""; - var data = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : ""; - - loadingBar.show("error", errorThrown); - - if (errorThrown === "Session timed out.") { - setTimeout(function () { - lychee.goto(); - window.location.reload(); - }, 3000); - } else { - console.error({ - description: errorThrown, - params: params, - response: data - }); - } -}; - -lychee.fullscreenEnter = function () { - var elem = document.documentElement; - if (elem.requestFullscreen) { - elem.requestFullscreen(); - } else if (elem.mozRequestFullScreen) { - /* Firefox */ - elem.mozRequestFullScreen(); - } else if (elem.webkitRequestFullscreen) { - /* Chrome, Safari and Opera */ - elem.webkitRequestFullscreen(); - } else if (elem.msRequestFullscreen) { - /* IE/Edge */ - elem.msRequestFullscreen(); - } -}; - -lychee.fullscreenExit = function () { - if (document.exitFullscreen) { - document.exitFullscreen(); - } else if (document.mozCancelFullScreen) { - /* Firefox */ - document.mozCancelFullScreen(); - } else if (document.webkitExitFullscreen) { - /* Chrome, Safari and Opera */ - document.webkitExitFullscreen(); - } else if (document.msExitFullscreen) { - /* IE/Edge */ - document.msExitFullscreen(); - } -}; - -lychee.fullscreenToggle = function () { - if (lychee.fullscreenStatus()) { - lychee.fullscreenExit(); - } else { - lychee.fullscreenEnter(); - } -}; - -lychee.fullscreenStatus = function () { - var elem = document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.msFullscreenElement; - return elem ? true : false; -}; - -lychee.fullscreenAvailable = function () { - return document.fullscreenEnabled || document.mozFullscreenEnabled || document.webkitFullscreenEnabled || document.msFullscreenEnabled; -}; - -lychee.fullscreenUpdate = function () { - if (lychee.fullscreenStatus()) { - $("#button_fs_album_enter,#button_fs_enter").hide(); - $("#button_fs_album_exit,#button_fs_exit").show(); - } else { - $("#button_fs_album_enter,#button_fs_enter").show(); - $("#button_fs_album_exit,#button_fs_exit").hide(); - } -}; - -lychee.footer_show = function () { - setTimeout(function () { - lychee.footer.removeClass("hide_footer"); - }, 200); -}; - -lychee.footer_hide = function () { - lychee.footer.addClass("hide_footer"); -}; - -// Because the height of the footer can vary, we need to set some -// dimensions dynamically, at startup. -lychee.adjustContentHeight = function () { - if (lychee.footer.length > 0) { - lychee.content.css("min-height", "calc(100vh - " + lychee.content.css("padding-top") + " - " + lychee.content.css("padding-bottom") + " - " + lychee.footer.outerHeight() + "px)"); - $("#container").css("padding-bottom", lychee.footer.outerHeight()); - } else { - lychee.content.css("min-height", "calc(100vh - " + lychee.content.css("padding-top") + " - " + lychee.content.css("padding-bottom") + ")"); - } -}; - -lychee.getBaseUrl = function () { - if (location.href.includes("index.html")) { - return location.href.replace("index.html" + location.hash, ""); - } else if (location.href.includes("gallery#")) { - return location.href.replace("gallery" + location.hash, ""); - } else { - return location.href.replace(location.hash, ""); - } -}; - -// Copied from https://github.com/feross/clipboard-copy/blob/9eba597c774feed48301fef689099599d612387c/index.js -lychee.clipboardCopy = function (text) { - // Use the Async Clipboard API when available. Requires a secure browsing - // context (i.e. HTTPS) - if (navigator.clipboard) { - return navigator.clipboard.writeText(text).catch(function (err) { - throw err !== undefined ? err : new DOMException("The request is not allowed", "NotAllowedError"); - }); - } - - // ...Otherwise, use document.execCommand() fallback - - // Put the text to copy into a - var span = document.createElement("span"); - span.textContent = text; - - // Preserve consecutive spaces and newlines - span.style.whiteSpace = "pre"; - - // Add the to the page - document.body.appendChild(span); - - // Make a selection object representing the range of text selected by the user - var selection = window.getSelection(); - var range = window.document.createRange(); - selection.removeAllRanges(); - range.selectNode(span); - selection.addRange(range); - - // Copy text to the clipboard - var success = false; - - try { - success = window.document.execCommand("copy"); - } catch (err) { - console.log("error", err); - } - - // Cleanup - selection.removeAllRanges(); - window.document.body.removeChild(span); - - return success; - // ? Promise.resolve() - // : Promise.reject(new DOMException('The request is not allowed', 'NotAllowedError')) -}; - -lychee.locale = { - USERNAME: "username", - PASSWORD: "password", - ENTER: "Enter", - CANCEL: "Cancel", - SIGN_IN: "Sign In", - CLOSE: "Close", - - SETTINGS: "Settings", - USERS: "Users", - U2F: "U2F", - NOTIFICATIONS: "Notifications", - SHARING: "Sharing", - CHANGE_LOGIN: "Change Login", - CHANGE_SORTING: "Change Sorting", - SET_DROPBOX: "Set Dropbox", - ABOUT_LYCHEE: "About Lychee", - DIAGNOSTICS: "Diagnostics", - DIAGNOSTICS_GET_SIZE: "Request space usage", - LOGS: "Show Logs", - CLEAN_LOGS: "Clean Noise", - SIGN_OUT: "Sign Out", - UPDATE_AVAILABLE: "Update available!", - MIGRATION_AVAILABLE: "Migration available!", - CHECK_FOR_UPDATE: "Check for updates", - DEFAULT_LICENSE: "Default License for new uploads:", - SET_LICENSE: "Set License", - SET_OVERLAY_TYPE: "Set Overlay", - SET_MAP_PROVIDER: "Set OpenStreetMap tiles provider", - SAVE_RISK: "Save my modifications, I accept the Risk!", - MORE: "More", - DEFAULT: "Default", - - SMART_ALBUMS: "Smart albums", - SHARED_ALBUMS: "Shared albums", - ALBUMS: "Albums", - PHOTOS: "Pictures", - SEARCH_RESULTS: "Search results", - - RENAME: "Rename", - RENAME_ALL: "Rename All", - MERGE: "Merge", - MERGE_ALL: "Merge All", - MAKE_PUBLIC: "Make Public", - SHARE_ALBUM: "Share Album", - SHARE_PHOTO: "Share Photo", - SHARE_WITH: "Share with...", - DOWNLOAD_ALBUM: "Download Album", - ABOUT_ALBUM: "About Album", - DELETE_ALBUM: "Delete Album", - FULLSCREEN_ENTER: "Enter Fullscreen", - FULLSCREEN_EXIT: "Exit Fullscreen", - - SHARING_ALBUM_USERS: "Share this album with users", - SHARING_ALBUM_USERS_LONG_MESSAGE: "Select the users to share this album with", - WAIT_FETCH_DATA: "Please wait while we get the data...", - SHARING_ALBUM_USERS_NO_USERS: "There are no users to share the album with", - - DELETE_ALBUM_QUESTION: "Delete Album and Photos", - KEEP_ALBUM: "Keep Album", - DELETE_ALBUM_CONFIRMATION_1: "Are you sure you want to delete the album", - DELETE_ALBUM_CONFIRMATION_2: "and all of the photos it contains? This action can't be undone!", - - DELETE_ALBUMS_QUESTION: "Delete Albums and Photos", - KEEP_ALBUMS: "Keep Albums", - DELETE_ALBUMS_CONFIRMATION_1: "Are you sure you want to delete all", - DELETE_ALBUMS_CONFIRMATION_2: "selected albums and all of the photos they contain? This action can't be undone!", - - DELETE_UNSORTED_CONFIRM: "Are you sure you want to delete all photos from 'Unsorted'?
This action can't be undone!", - CLEAR_UNSORTED: "Clear Unsorted", - KEEP_UNSORTED: "Keep Unsorted", - - EDIT_SHARING: "Edit Sharing", - MAKE_PRIVATE: "Make Private", - - CLOSE_ALBUM: "Close Album", - CLOSE_PHOTO: "Close Photo", - CLOSE_MAP: "Close Map", - - ADD: "Add", - MOVE: "Move", - MOVE_ALL: "Move All", - DUPLICATE: "Duplicate", - DUPLICATE_ALL: "Duplicate All", - COPY_TO: "Copy to...", - COPY_ALL_TO: "Copy All to...", - DELETE: "Delete", - DELETE_ALL: "Delete All", - DOWNLOAD: "Download", - DOWNLOAD_MEDIUM: "Download medium size", - DOWNLOAD_SMALL: "Download small size", - UPLOAD_PHOTO: "Upload Photo", - IMPORT_LINK: "Import from Link", - IMPORT_DROPBOX: "Import from Dropbox", - IMPORT_SERVER: "Import from Server", - NEW_ALBUM: "New Album", - NEW_TAG_ALBUM: "New Tag Album", - - TITLE_NEW_ALBUM: "Enter a title for the new album:", - UNTITLED: "Untilted", - UNSORTED: "Unsorted", - STARRED: "Starred", - RECENT: "Recent", - PUBLIC: "Public", - NUM_PHOTOS: "Photos", - - CREATE_ALBUM: "Create Album", - CREATE_TAG_ALBUM: "Create Tag Album", - - STAR_PHOTO: "Star Photo", - STAR: "Star", - STAR_ALL: "Star All", - TAGS: "Tags", - TAGS_ALL: "Tags All", - UNSTAR_PHOTO: "Unstar Photo", - - FULL_PHOTO: "Full Photo", - ABOUT_PHOTO: "About Photo", - DISPLAY_FULL_MAP: "Map", - DIRECT_LINK: "Direct Link", - DIRECT_LINKS: "Direct Links", - - ALBUM_ABOUT: "About", - ALBUM_BASICS: "Basics", - ALBUM_TITLE: "Title", - ALBUM_NEW_TITLE: "Enter a new title for this album:", - ALBUMS_NEW_TITLE_1: "Enter a title for all", - ALBUMS_NEW_TITLE_2: "selected albums:", - ALBUM_SET_TITLE: "Set Title", - ALBUM_DESCRIPTION: "Description", - ALBUM_SHOW_TAGS: "Tags to show", - ALBUM_NEW_DESCRIPTION: "Enter a new description for this album:", - ALBUM_SET_DESCRIPTION: "Set Description", - ALBUM_NEW_SHOWTAGS: "Enter tags of photos that will be visible in this album:", - ALBUM_SET_SHOWTAGS: "Set tags to show", - ALBUM_ALBUM: "Album", - ALBUM_CREATED: "Created", - ALBUM_IMAGES: "Images", - ALBUM_VIDEOS: "Videos", - ALBUM_SHARING: "Share", - ALBUM_OWNER: "Owner", - ALBUM_SHR_YES: "YES", - ALBUM_SHR_NO: "No", - ALBUM_PUBLIC: "Public", - ALBUM_PUBLIC_EXPL: "Album can be viewed by others, subject to the restrictions below.", - ALBUM_FULL: "Full size (v4 only)", - ALBUM_FULL_EXPL: "Full size pictures are available", - ALBUM_HIDDEN: "Hidden", - ALBUM_HIDDEN_EXPL: "Only people with the direct link can view this album.", - ALBUM_MARK_NSFW: "Mark album as sensitive", - ALBUM_UNMARK_NSFW: "Unmark album as sensitive", - ALBUM_NSFW: "Sensitive", - ALBUM_NSFW_EXPL: "Album contains sensitive content.", - ALBUM_DOWNLOADABLE: "Downloadable", - ALBUM_DOWNLOADABLE_EXPL: "Visitors of your Lychee can download this album.", - ALBUM_SHARE_BUTTON_VISIBLE: "Share button is visible", - ALBUM_SHARE_BUTTON_VISIBLE_EXPL: "Display social media sharing links.", - ALBUM_PASSWORD: "Password", - ALBUM_PASSWORD_PROT: "Password protected", - ALBUM_PASSWORD_PROT_EXPL: "Album only accessible with a valid password.", - ALBUM_PASSWORD_REQUIRED: "This album is protected by a password. Enter the password below to view the photos of this album:", - ALBUM_MERGE_1: "Are you sure you want to merge the album", - ALBUM_MERGE_2: "into the album", - ALBUMS_MERGE: "Are you sure you want to merge all selected albums into the album", - MERGE_ALBUM: "Merge Albums", - DONT_MERGE: "Don't Merge", - ALBUM_MOVE_1: "Are you sure you want to move the album", - ALBUM_MOVE_2: "into the album", - ALBUMS_MOVE: "Are you sure you want to move all selected albums into the album", - MOVE_ALBUMS: "Move Albums", - NOT_MOVE_ALBUMS: "Don't Move", - ROOT: "Root", - ALBUM_REUSE: "Reuse", - ALBUM_LICENSE: "License", - ALBUM_SET_LICENSE: "Set License", - ALBUM_LICENSE_HELP: "Need help choosing?", - ALBUM_LICENSE_NONE: "None", - ALBUM_RESERVED: "All Rights Reserved", - ALBUM_SET_ORDER: "Set Order", - ALBUM_ORDERING: "Order by", - - PHOTO_ABOUT: "About", - PHOTO_BASICS: "Basics", - PHOTO_TITLE: "Title", - PHOTO_NEW_TITLE: "Enter a new title for this photo:", - PHOTO_SET_TITLE: "Set Title", - PHOTO_UPLOADED: "Uploaded", - PHOTO_DESCRIPTION: "Description", - PHOTO_NEW_DESCRIPTION: "Enter a new description for this photo:", - PHOTO_SET_DESCRIPTION: "Set Description", - PHOTO_NEW_LICENSE: "Add a License", - PHOTO_SET_LICENSE: "Set License", - PHOTO_REUSE: "Reuse", - PHOTO_LICENSE: "License", - PHOTO_LICENSE_HELP: "Need help choosing?", - PHOTO_LICENSE_NONE: "None", - PHOTO_RESERVED: "All Rights Reserved", - PHOTO_IMAGE: "Image", - PHOTO_VIDEO: "Video", - PHOTO_SIZE: "Size", - PHOTO_FORMAT: "Format", - PHOTO_RESOLUTION: "Resolution", - PHOTO_DURATION: "Duration", - PHOTO_FPS: "Frame rate", - PHOTO_TAGS: "Tags", - PHOTO_NOTAGS: "No Tags", - PHOTO_NEW_TAGS: "Enter your tags for this photo. You can add multiple tags by separating them with a comma:", - PHOTO_NEW_TAGS_1: "Enter your tags for all", - PHOTO_NEW_TAGS_2: "selected photos. Existing tags will be overwritten. You can add multiple tags by separating them with a comma:", - PHOTO_SET_TAGS: "Set Tags", - PHOTO_CAMERA: "Camera", - PHOTO_CAPTURED: "Captured", - PHOTO_MAKE: "Make", - PHOTO_TYPE: "Type/Model", - PHOTO_LENS: "Lens", - PHOTO_SHUTTER: "Shutter Speed", - PHOTO_APERTURE: "Aperture", - PHOTO_FOCAL: "Focal Length", - PHOTO_ISO: "ISO", - PHOTO_SHARING: "Sharing", - PHOTO_SHR_PLUBLIC: "Public", - PHOTO_SHR_ALB: "Yes (Album)", - PHOTO_SHR_PHT: "Yes (Photo)", - PHOTO_SHR_NO: "No", - PHOTO_DELETE: "Delete Photo", - PHOTO_KEEP: "Keep Photo", - PHOTO_DELETE_1: "Are you sure you want to delete the photo", - PHOTO_DELETE_2: "? This action can't be undone!", - PHOTO_DELETE_ALL_1: "Are you sure you want to delete all", - PHOTO_DELETE_ALL_2: "selected photo? This action can't be undone!", - PHOTOS_NEW_TITLE_1: "Enter a title for all", - PHOTOS_NEW_TITLE_2: "selected photos:", - PHOTO_MAKE_PRIVATE_ALBUM: "This photo is located in a public album. To make this photo private or public, edit the visibility of the associated album.", - PHOTO_SHOW_ALBUM: "Show Album", - PHOTO_PUBLIC: "Public", - PHOTO_PUBLIC_EXPL: "Photo can be viewed by others, subject to the restrictions below.", - PHOTO_FULL: "Original", - PHOTO_FULL_EXPL: "Full-resolution picture is available.", - PHOTO_HIDDEN: "Hidden", - PHOTO_HIDDEN_EXPL: "Only people with the direct link can view this photo.", - PHOTO_DOWNLOADABLE: "Downloadable", - PHOTO_DOWNLOADABLE_EXPL: "Visitors of your gallery can download this photo.", - PHOTO_SHARE_BUTTON_VISIBLE: "Share button is visible", - PHOTO_SHARE_BUTTON_VISIBLE_EXPL: "Display social media sharing links.", - PHOTO_PASSWORD_PROT: "Password protected", - PHOTO_PASSWORD_PROT_EXPL: "Photo only accessible with a valid password.", - PHOTO_EDIT_SHARING_TEXT: "The sharing properties of this photo will be changed to the following:", - PHOTO_NO_EDIT_SHARING_TEXT: "Because this photo is located in a public album, it inherits that album's visibility settings. Its current visibility is shown below for informational purposes only.", - PHOTO_EDIT_GLOBAL_SHARING_TEXT: "The visibility of this photo can be fine-tuned using global Lychee settings. Its current visibility is shown below for informational purposes only.", - PHOTO_SHARING_CONFIRM: "Save", - PHOTO_LOCATION: "Location", - PHOTO_LATITUDE: "Latitude", - PHOTO_LONGITUDE: "Longitude", - PHOTO_ALTITUDE: "Altitude", - PHOTO_IMGDIRECTION: "Direction", - - LOADING: "Loading", - ERROR: "Error", - ERROR_TEXT: "Whoops, it looks like something went wrong. Please reload the site and try again!", - ERROR_DB_1: "Unable to connect to host database because access was denied. Double-check your host, username and password and ensure that access from your current location is permitted.", - ERROR_DB_2: "Unable to create the database. Double-check your host, username and password and ensure that the specified user has the rights to modify and add content to the database.", - ERROR_CONFIG_FILE: "Unable to save this configuration. Permission denied in 'data/'. Please set the read, write and execute rights for others in 'data/' and 'uploads/'. Take a look at the readme for more information.", - ERROR_UNKNOWN: "Something unexpected happened. Please try again and check your installation and server. Take a look at the readme for more information.", - ERROR_LOGIN: "Unable to save login. Please try again with another username and password!", - ERROR_MAP_DEACTIVATED: "Map functionality has been deactivated under settings.", - ERROR_SEARCH_DEACTIVATED: "Search functionality has been deactivated under settings.", - SUCCESS: "OK", - RETRY: "Retry", - - SETTINGS_WARNING: "Changing these advanced settings can be harmful to the stability, security and performance of this application. You should only modify them if you are sure of what you are doing.", - SETTINGS_SUCCESS_LOGIN: "Login Info updated.", - SETTINGS_SUCCESS_SORT: "Sorting order updated.", - SETTINGS_SUCCESS_DROPBOX: "Dropbox Key updated.", - SETTINGS_SUCCESS_LANG: "Language updated", - SETTINGS_SUCCESS_LAYOUT: "Layout updated", - SETTINGS_SUCCESS_IMAGE_OVERLAY: "EXIF Overlay setting updated", - SETTINGS_SUCCESS_PUBLIC_SEARCH: "Public search updated", - SETTINGS_SUCCESS_LICENSE: "Default license updated", - SETTINGS_SUCCESS_MAP_DISPLAY: "Map display settings updated", - SETTINGS_SUCCESS_MAP_DISPLAY_PUBLIC: "Map display settings for public albums updated", - SETTINGS_SUCCESS_MAP_PROVIDER: "Map provider settings updated", - - U2F_NOT_SUPPORTED: "U2F not supported. Sorry.", - U2F_NOT_SECURE: "Environment not secured. U2F not available.", - U2F_REGISTER_KEY: "Register new device.", - U2F_REGISTRATION_SUCCESS: "Registration successful!", - U2F_AUTHENTIFICATION_SUCCESS: "Authentication successful!", - U2F_CREDENTIALS: "Credentials", - U2F_CREDENTIALS_DELETED: "Credentials deleted!", - - NEW_PHOTOS_NOTIFICATION: "Send new photos notification emails.", - SETTINGS_SUCCESS_NEW_PHOTOS_NOTIFICATION: "New photos notification updated", - USER_EMAIL_INSTRUCTION: "Add your email below to enable receiving email notifications.
To stop receiving emails, simply remove your email below.", - - SETTINGS_SUCCESS_CSS: "CSS updated", - SETTINGS_SUCCESS_UPDATE: "Settings updated with success", - - DB_INFO_TITLE: "Enter your database connection details below:", - DB_INFO_HOST: "Database Host (optional)", - DB_INFO_USER: "Database Username", - DB_INFO_PASSWORD: "Database Password", - DB_INFO_TEXT: "Lychee will create its own database. If required, you can enter the name of an existing database instead:", - DB_NAME: "Database Name (optional)", - DB_PREFIX: "Table prefix (optional)", - DB_CONNECT: "Connect", - - LOGIN_TITLE: "Enter a username and password for your installation:", - LOGIN_USERNAME: "New Username", - LOGIN_PASSWORD: "New Password", - LOGIN_PASSWORD_CONFIRM: "Confirm Password", - LOGIN_CREATE: "Create Login", - - PASSWORD_TITLE: "Enter your current username and password:", - USERNAME_CURRENT: "Current Username", - PASSWORD_CURRENT: "Current Password", - PASSWORD_TEXT: "Your username and password will be changed to the following:", - PASSWORD_CHANGE: "Change Login", - - EDIT_SHARING_TITLE: "Edit Sharing", - EDIT_SHARING_TEXT: "The sharing-properties of this album will be changed to the following:", - SHARE_ALBUM_TEXT: "This album will be shared with the following properties:", - ALBUM_SHARING_CONFIRM: "Save", - - SORT_ALBUM_BY_1: "Sort albums by", - SORT_ALBUM_BY_2: "in an", - SORT_ALBUM_BY_3: "order.", - - SORT_ALBUM_SELECT_1: "Creation Time", - SORT_ALBUM_SELECT_2: "Title", - SORT_ALBUM_SELECT_3: "Description", - SORT_ALBUM_SELECT_4: "Public", - SORT_ALBUM_SELECT_5: "Latest Take Date", - SORT_ALBUM_SELECT_6: "Oldest Take Date", - - SORT_PHOTO_BY_1: "Sort photos by", - SORT_PHOTO_BY_2: "in an", - SORT_PHOTO_BY_3: "order.", - - SORT_PHOTO_SELECT_1: "Upload Time", - SORT_PHOTO_SELECT_2: "Take Date", - SORT_PHOTO_SELECT_3: "Title", - SORT_PHOTO_SELECT_4: "Description", - SORT_PHOTO_SELECT_5: "Public", - SORT_PHOTO_SELECT_6: "Star", - SORT_PHOTO_SELECT_7: "Photo Format", - - SORT_ASCENDING: "Ascending", - SORT_DESCENDING: "Descending", - SORT_CHANGE: "Change Sorting", - - DROPBOX_TITLE: "Set Dropbox Key", - DROPBOX_TEXT: "In order to import photos from your Dropbox, you need a valid drop-ins app key from their website. Generate yourself a personal key and enter it below:", - - LANG_TEXT: "Change Lychee language for:", - LANG_TITLE: "Change Language", - - CSS_TEXT: "Personalize your CSS:", - CSS_TITLE: "Change CSS", - - LAYOUT_TYPE: "Layout of photos:", - LAYOUT_SQUARES: "Square thumbnails", - LAYOUT_JUSTIFIED: "With aspect, justified", - LAYOUT_UNJUSTIFIED: "With aspect, unjustified", - SET_LAYOUT: "Change layout", - PUBLIC_SEARCH_TEXT: "Public search allowed:", - - IMAGE_OVERLAY_TEXT: "Display image overlay by default:", - - OVERLAY_TYPE: "Photo overlay:", - OVERLAY_NONE: "None", - OVERLAY_EXIF: "EXIF data", - OVERLAY_DESCRIPTION: "Description", - OVERLAY_DATE: "Date taken", - - MAP_PROVIDER: "Provider of OpenStreetMap tiles:", - MAP_PROVIDER_WIKIMEDIA: "Wikimedia", - MAP_PROVIDER_OSM_ORG: "OpenStreetMap.org (no retina)", - MAP_PROVIDER_OSM_DE: "OpenStreetMap.de (no retina)", - MAP_PROVIDER_OSM_FR: "OpenStreetMap.fr (no retina)", - MAP_PROVIDER_RRZE: "University of Erlangen, Germany (only retina)", - - MAP_DISPLAY_TEXT: "Enable maps (provided by OpenStreetMap):", - MAP_DISPLAY_PUBLIC_TEXT: "Enable maps for public albums (provided by OpenStreetMap):", - MAP_INCLUDE_SUBALBUMS_TEXT: "Include photos of subalbums on map:", - LOCATION_DECODING: "Decode GPS data into location name", - LOCATION_SHOW: "Show location name", - LOCATION_SHOW_PUBLIC: "Show location name for public mode", - - NSFW_VISIBLE_TEXT_1: "Make Sensitive albums visible by default.", - NSFW_VISIBLE_TEXT_2: "If the album is public, it is still accessible, just hidden from the view and can be revealed by pressing H.", - SETTINGS_SUCCESS_NSFW_VISIBLE: "Default sensitive album visibility updated with success.", - - VIEW_NO_RESULT: "No results", - VIEW_NO_PUBLIC_ALBUMS: "No public albums", - VIEW_NO_CONFIGURATION: "No configuration", - VIEW_PHOTO_NOT_FOUND: "Photo not found", - - NO_TAGS: "No Tags", - - UPLOAD_MANAGE_NEW_PHOTOS: "You can now manage your new photo(s).", - UPLOAD_COMPLETE: "Upload complete", - UPLOAD_COMPLETE_FAILED: "Failed to upload one or more photos.", - UPLOAD_IMPORTING: "Importing", - UPLOAD_IMPORTING_URL: "Importing URL", - UPLOAD_UPLOADING: "Uploading", - UPLOAD_FINISHED: "Finished", - UPLOAD_PROCESSING: "Processing", - UPLOAD_FAILED: "Failed", - UPLOAD_FAILED_ERROR: "Upload failed. Server returned an error!", - UPLOAD_FAILED_WARNING: "Upload failed. Server returned a warning!", - UPLOAD_SKIPPED: "Skipped", - UPLOAD_UPDATED: "Updated", - UPLOAD_IMPORT_SKIPPED_DUPLICATE: "This photo has been skipped because it's already in your library.", - UPLOAD_IMPORT_RESYNCED_DUPLICATE: "This photo has been skipped because it's already in your library, but its metadata has been updated.", - UPLOAD_ERROR_CONSOLE: "Please take a look at the console of your browser for further details.", - UPLOAD_UNKNOWN: "Server returned an unknown response. Please take a look at the console of your browser for further details.", - UPLOAD_ERROR_UNKNOWN: "Upload failed. Server returned an unkown error!", - UPLOAD_ERROR_POSTSIZE: "Upload failed. The PHP post_max_size limit is too small!", - UPLOAD_ERROR_FILESIZE: "Upload failed. The PHP upload_max_filesize limit is too small!", - UPLOAD_IN_PROGRESS: "Lychee is currently uploading!", - UPLOAD_IMPORT_WARN_ERR: "The import has been finished, but returned warnings or errors. Please take a look at the log (Settings -> Show Log) for further details.", - UPLOAD_IMPORT_COMPLETE: "Import complete", - UPLOAD_IMPORT_INSTR: "Please enter the direct link to a photo to import it:", - UPLOAD_IMPORT: "Import", - UPLOAD_IMPORT_SERVER: "Importing from server", - UPLOAD_IMPORT_SERVER_FOLD: "Folder empty or no readable files to process. Please take a look at the log (Settings -> Show Log) for further details.", - UPLOAD_IMPORT_SERVER_INSTR: "This action will import all photos, folders and sub-folders which are located in the following directory. The original files will be deleted after the import when possible.", - UPLOAD_ABSOLUTE_PATH: "Absolute path to directory", - UPLOAD_IMPORT_SERVER_EMPT: "Could not start import because the folder was empty!", - - ABOUT_SUBTITLE: "Self-hosted photo-management done right", - ABOUT_DESCRIPTION: "is a free photo-management tool, which runs on your server or web-space. Installing is a matter of seconds. Upload, manage and share photos like from a native application. Lychee comes with everything you need and all your photos are stored securely.", - - URL_COPY_TO_CLIPBOARD: "Copy to clipboard", - URL_COPIED_TO_CLIPBOARD: "Copied URL to clipboard!", - PHOTO_DIRECT_LINKS_TO_IMAGES: "Direct links to image files:", - PHOTO_MEDIUM: "Medium", - PHOTO_MEDIUM_HIDPI: "Medium HiDPI", - PHOTO_SMALL: "Thumb", - PHOTO_SMALL_HIDPI: "Thumb HiDPI", - PHOTO_THUMB: "Square thumb", - PHOTO_THUMB_HIDPI: "Square thumb HiDPI", - PHOTO_LIVE_VIDEO: "Video part of live-photo", - PHOTO_VIEW: "Lychee Photo View:", - - /** - * Formats a number representing a filesize in bytes as a localized string - * @param {!number} filesize - * @return {string} A formatted and localized string - */ - printFilesizeLocalized: function printFilesizeLocalized(filesize) { - console.assert(Number.isInteger(filesize), "printFilesizeLocalized: expected integer, got %s", typeof filesize === "undefined" ? "undefined" : _typeof(filesize)); - var suffix = [" B", " kB", " MB", " GB"]; - var i = 0; - // Sic! We check if the number is larger than 1000 but divide by 1024 by intention - // We aim at a number which has at most 3 non-decimal digits, i.e. the result shall be in the interval - // [1000/1024, 1000) = [0.977, 1000) (lower bound included, upper bound excluded) - while (filesize >= 1000.0 && i < suffix.length) { - filesize = filesize / 1024.0; - i++; - } - - // The number of decimal digits is anti-proportional to the number of non-decimal digits - // In total, there shall always be three digits - if (filesize >= 100.0) { - filesize = Math.round(filesize); - } else if (filesize >= 10.0) { - filesize = Math.round(filesize * 10.0) / 10.0; - } else { - filesize = Math.round(filesize * 100.0) / 100.0; - } - - return Number(filesize).toLocaleString() + suffix[i]; - }, - - /** - * Converts a JSON encoded date/time into a localized string relative to - * the original timezone - * - * The localized string uses the JS "medium" verbosity. - * The precise definition of "medium verbosity" depends on the current locale, but for Western languages this - * means that the date portion is fully printed with digits (e.g. something like 03/30/2021 for English, - * 30/03/2021 for French and 30.03.2021 for German), and that the time portion is printed with a resolution of - * seconds with two digits for all parts either in 24h or 12h scheme (e.g. something like 02:24:13pm for English - * and 14:24:13 for French/German). - * - * @param {?string} jsonDateTime - * @return {string} A formatted and localized time - */ - printDateTime: function printDateTime(jsonDateTime) { - if (typeof jsonDateTime !== "string" || jsonDateTime === "") return ""; - - // Unfortunately, the built-in JS Date object is rather dumb. - // It is only required to support the timezone of the runtime - // environment and UTC. - // Moreover, the method `toLocalString` may or may not convert - // the represented time to the timezone of the runtime environment - // before formatting it as a string. - // However, we want to keep the printed time in the original timezone, - // because this facilitates human interaction with a photo. - // To this end we apply a "dirty" trick here. - // We first cut off any explicit timezone indication from the JSON - // string and only pass a date/time of the form `YYYYMMDDThhmmss` to - // `Date`. - // `Date` is required to interpret those time values according to the - // local timezone (see [MDN Web Docs - Date Time String Format](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse#date_time_string_format)). - // Most likely, the resulting `Date` object will represent the - // wrong instant in time (given in seconds since epoch), but we only - // want to call `toLocalString` which is fine and don't do any time - // arithmetics. - // Then we add the original timezone to the string manually. - var splitDateTime = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})([-Z+])(\d{2}:\d{2})?$/.exec(jsonDateTime); - console.assert(splitDateTime.length === 4, "'jsonDateTime' is not formatted acc. to ISO 8601; passed string was: " + jsonDateTime); - var locale = "default"; // use the user's browser settings - var format = { dateStyle: "medium", timeStyle: "medium" }; - var result = new Date(splitDateTime[1]).toLocaleString(locale, format); - if (splitDateTime[2] === "Z" || splitDateTime[3] === "00:00") { - result += " UTC"; - } else { - result += " UTC" + splitDateTime[2] + splitDateTime[3]; - } - return result; - }, - - /** - * Converts a JSON encoded date/time into a localized string which only displays month and year. - * - * The month is printed as a shortened word with 3/4 letters, the year is printed with 4 digits (e.g. something like - * "Aug 2020" in English or "Août 2020" in French). - * - * @param {?string} jsonDateTime - * @return {string} A formatted and localized month and year - */ - printMonthYear: function printMonthYear(jsonDateTime) { - if (typeof jsonDateTime !== "string" || jsonDateTime === "") return ""; - var locale = "default"; // use the user's browser settings - var format = { month: "short", year: "numeric" }; - return new Date(jsonDateTime).toLocaleDateString(locale, format); - } -}; - -/** - * @description This module takes care of the map view of a full album and its sub-albums. - */ - -var map_provider_layer_attribution = { - Wikimedia: { - layer: "https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}{r}.png", - attribution: 'Wikimedia' - }, - "OpenStreetMap.org": { - layer: "https://{s}.tile.osm.org/{z}/{x}/{y}.png", - attribution: '© OpenStreetMap contributors' - }, - "OpenStreetMap.de": { - layer: "https://{s}.tile.openstreetmap.de/{z}/{x}/{y}.png ", - attribution: '© OpenStreetMap contributors' - }, - "OpenStreetMap.fr": { - layer: "https://{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png ", - attribution: '© OpenStreetMap contributors' - }, - RRZE: { - layer: "https://{s}.osm.rrze.fau.de/osmhd/{z}/{x}/{y}.png", - attribution: '© OpenStreetMap contributors' - } -}; - -var mapview = { - map: null, - photoLayer: null, - min_lat: null, - min_lng: null, - max_lat: null, - max_lng: null, - albumID: null, - map_provider: null -}; - -mapview.isInitialized = function () { - if (mapview.map === null || mapview.photoLayer === null) { - return false; - } - return true; -}; - -mapview.title = function (_albumID, _albumTitle) { - switch (_albumID) { - case "f": - lychee.setTitle(lychee.locale["STARRED"], false); - break; - case "s": - lychee.setTitle(lychee.locale["PUBLIC"], false); - break; - case "r": - lychee.setTitle(lychee.locale["RECENT"], false); - break; - case "0": - lychee.setTitle(lychee.locale["UNSORTED"], false); - break; - case null: - lychee.setTitle(lychee.locale["ALBUMS"], false); - break; - default: - lychee.setTitle(_albumTitle, false); - break; - } -}; - -// Open the map view -mapview.open = function () { - var albumID = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; - - // If map functionality is disabled -> do nothing - if (!lychee.map_display || lychee.publicMode === true && !lychee.map_display_public) { - loadingBar.show("error", lychee.locale["ERROR_MAP_DEACTIVATED"]); - return; - } - - lychee.animate($("#mapview"), "fadeIn"); - $("#mapview").show(); - header.setMode("map"); - - mapview.albumID = albumID; - - // initialize container only once - if (mapview.isInitialized() == false) { - // Leaflet seaches for icon in same directoy as js file -> paths needs - // to be overwritten - delete L.Icon.Default.prototype._getIconUrl; - L.Icon.Default.mergeOptions({ - iconRetinaUrl: "img/marker-icon-2x.png", - iconUrl: "img/marker-icon.png", - shadowUrl: "img/marker-shadow.png" - }); - - // Set initial view to (0,0) - mapview.map = L.map("leaflet_map_full").setView([0.0, 0.0], 13); - - L.tileLayer(map_provider_layer_attribution[lychee.map_provider].layer, { - attribution: map_provider_layer_attribution[lychee.map_provider].attribution - }).addTo(mapview.map); - - mapview.map_provider = lychee.map_provider; - } else { - if (mapview.map_provider !== lychee.map_provider) { - // removew all layers - mapview.map.eachLayer(function (layer) { - mapview.map.removeLayer(layer); - }); - - L.tileLayer(map_provider_layer_attribution[lychee.map_provider].layer, { - attribution: map_provider_layer_attribution[lychee.map_provider].attribution - }).addTo(mapview.map); - - mapview.map_provider = lychee.map_provider; - } else { - // Mapview has already shown data -> remove only photoLayer showing photos - mapview.photoLayer.clear(); - } - - // Reset min/max lat/lgn Values - mapview.min_lat = null; - mapview.max_lat = null; - mapview.min_lng = null; - mapview.max_lng = null; - } - - // Define how the photos on the map should look like - mapview.photoLayer = L.photo.cluster().on("click", function (e) { - var photo = { - photoID: e.layer.photo.photoID, - albumID: e.layer.photo.albumID, - name: e.layer.photo.name, - url: e.layer.photo.url, - url2x: e.layer.photo.url2x, - taken_at: lychee.locale.printDateTime(e.layer.photo.taken_at) - }; - var template = ""; - - // Retina version if available - if (photo.url2x !== "") { - template = template.concat('

{name}

', build.iconic("camera-slr"), "

{taken_at}

"); - } else { - template = template.concat('

{name}

', build.iconic("camera-slr"), "

{taken_at}

"); - } - - e.layer.bindPopup(L.Util.template(template, photo), { - minWidth: 400 - }).openPopup(); - }); - - // Adjusts zoom and position of map to show all images - var updateZoom = function updateZoom() { - if (mapview.min_lat && mapview.min_lng && mapview.max_lat && mapview.max_lng) { - var dist_lat = mapview.max_lat - mapview.min_lat; - var dist_lng = mapview.max_lng - mapview.min_lng; - mapview.map.fitBounds([[mapview.min_lat - 0.1 * dist_lat, mapview.min_lng - 0.1 * dist_lng], [mapview.max_lat + 0.1 * dist_lat, mapview.max_lng + 0.1 * dist_lng]]); - } else { - mapview.map.fitWorld(); - } - }; - - // Adds photos to the map - var addPhotosToMap = function addPhotosToMap(album) { - // check if empty - if (!album.photos) return; - - var photos = []; - - album.photos.forEach(function (element, index) { - if (element.latitude || element.longitude) { - photos.push({ - lat: parseFloat(element.latitude), - lng: parseFloat(element.longitude), - thumbnail: element.sizeVariants.thumb !== null ? element.sizeVariants.thumb.url : "img/placeholder.png", - thumbnail2x: element.sizeVariants.thumb2x !== null ? element.sizeVariants.thumb2x.url : null, - url: element.sizeVariants.small !== null ? element.sizeVariants.small.url : element.url, - url2x: element.sizeVariants.small2x !== null ? element.sizeVariants.small2x.url : null, - name: element.title, - taken_at: element.taken_at, - albumID: element.album, - photoID: element.id - }); - - // Update min/max lat/lng - if (mapview.min_lat === null || mapview.min_lat > element.latitude) { - mapview.min_lat = parseFloat(element.latitude); - } - if (mapview.min_lng === null || mapview.min_lng > element.longitude) { - mapview.min_lng = parseFloat(element.longitude); - } - if (mapview.max_lat === null || mapview.max_lat < element.latitude) { - mapview.max_lat = parseFloat(element.latitude); - } - if (mapview.max_lng === null || mapview.max_lng < element.longitude) { - mapview.max_lng = parseFloat(element.longitude); - } - } - }); - - // Add Photos to map - mapview.photoLayer.add(photos).addTo(mapview.map); - - // Update Zoom and Position - updateZoom(); - }; - - // Call backend, retrieve information of photos and display them - // This function is called recursively to retrieve data for sub-albums - // Possible enhancement could be to only have a single ajax call - var getAlbumData = function getAlbumData(_albumID) { - var _includeSubAlbums = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; - - if (_albumID !== "" && _albumID !== null) { - // _ablumID has been to a specific album - var _params = { - albumID: _albumID, - includeSubAlbums: _includeSubAlbums, - password: "" - }; - - api.post("Album::getPositionData", _params, function (data) { - if (data === "Warning: Wrong password!") { - password.getDialog(_albumID, function () { - _params.password = password.value; - - api.post("Album::getPositionData", _params, function (_data) { - addPhotosToMap(_data); - mapview.title(_albumID, _data.title); - }); - }); - } else { - addPhotosToMap(data); - mapview.title(_albumID, data.title); - } - }); - } else { - // AlbumID is empty -> fetch all photos of all albums - // _ablumID has been to a specific album - var _params2 = { - includeSubAlbums: _includeSubAlbums, - password: "" - }; - - api.post("Albums::getPositionData", _params2, function (data) { - if (data === "Warning: Wrong password!") { - password.getDialog(_albumID, function () { - _params2.password = password.value; - - api.post("Albums::getPositionData", _params2, function (_data) { - addPhotosToMap(_data); - mapview.title(_albumID, _data.title); - }); - }); - } else { - addPhotosToMap(data); - mapview.title(_albumID, data.title); - } - }); - } - }; - - // If subalbums not being included and album.json already has all data - // -> we can reuse it - if (lychee.map_include_subalbums === false && album.json !== null && album.json.photos !== null) { - addPhotosToMap(album.json); - } else { - // Not all needed data has been preloaded - we need to load everything - getAlbumData(albumID, lychee.map_include_subalbums); - } - - // Update Zoom and Position once more (for empty map) - updateZoom(); -}; - -mapview.close = function () { - // If map functionality is disabled -> do nothing - if (!lychee.map_display) return; - - lychee.animate($("#mapview"), "fadeOut"); - $("#mapview").hide(); - header.setMode("album"); - - // Make album focussable - tabindex.makeFocusable(lychee.content); -}; - -mapview.goto = function (elem) { - // If map functionality is disabled -> do nothing - if (!lychee.map_display) return; - - var photoID = elem.attr("data-id"); - var albumID = elem.attr("data-album-id"); - - if (albumID == "null") albumID = 0; - - if (album.json == null || albumID !== album.json.id) { - album.refresh(); - } - - lychee.goto(albumID + "/" + photoID); -}; - -/** - * @description Select multiple albums or photos. - */ - -var isSelectKeyPressed = function isSelectKeyPressed(e) { - return e.metaKey || e.ctrlKey; -}; - -var multiselect = { - ids: [], - albumsSelected: 0, - photosSelected: 0, - lastClicked: null -}; - -multiselect.position = { - top: null, - right: null, - bottom: null, - left: null -}; - -multiselect.bind = function () { - $(".content").on("mousedown", function (e) { - if (e.which === 1) multiselect.show(e); - }); - - return true; -}; - -multiselect.unbind = function () { - $(".content").off("mousedown"); -}; - -multiselect.isSelected = function (id) { - var pos = $.inArray(id, multiselect.ids); - - return { - selected: pos !== -1, - position: pos - }; -}; - -multiselect.toggleItem = function (object, id) { - if (album.isSmartID(id)) return; - - var selected = multiselect.isSelected(id).selected; - - if (selected === false) multiselect.addItem(object, id);else multiselect.removeItem(object, id); -}; - -multiselect.addItem = function (object, id) { - if (album.isSmartID(id)) return; - if (!lychee.admin && albums.isShared(id)) return; - if (multiselect.isSelected(id).selected === true) return; - - var isAlbum = object.hasClass("album"); - - if (isAlbum && multiselect.photosSelected > 0 || !isAlbum && multiselect.albumsSelected > 0) { - lychee.error("Please select either albums or photos!"); - return; - } - - multiselect.ids.push(id); - multiselect.select(object); - - if (isAlbum) { - multiselect.albumsSelected++; - } else { - multiselect.photosSelected++; - } - - multiselect.lastClicked = object; -}; - -multiselect.removeItem = function (object, id) { - var _multiselect$isSelect = multiselect.isSelected(id), - selected = _multiselect$isSelect.selected, - position = _multiselect$isSelect.position; - - if (selected === false) return; - - multiselect.ids.splice(position, 1); - multiselect.deselect(object); - - var isAlbum = object.hasClass("album"); - - if (isAlbum) { - multiselect.albumsSelected--; - } else { - multiselect.photosSelected--; - } - - multiselect.lastClicked = object; -}; - -multiselect.albumClick = function (e, albumObj) { - var id = albumObj.attr("data-id"); - - if ((isSelectKeyPressed(e) || e.shiftKey) && album.isUploadable()) { - if (albumObj.hasClass("disabled")) return; - - if (isSelectKeyPressed(e)) { - multiselect.toggleItem(albumObj, id); - } else { - if (multiselect.albumsSelected > 0) { - // Click with Shift. Select all elements between the current - // element and the last clicked-on one. - - if (albumObj.prevAll(".album").toArray().includes(multiselect.lastClicked[0])) { - albumObj.prevUntil(multiselect.lastClicked, ".album").each(function () { - multiselect.addItem($(this), $(this).attr("data-id")); - }); - } else if (albumObj.nextAll(".album").toArray().includes(multiselect.lastClicked[0])) { - albumObj.nextUntil(multiselect.lastClicked, ".album").each(function () { - multiselect.addItem($(this), $(this).attr("data-id")); - }); - } - } - - multiselect.addItem(albumObj, id); - } - } else { - lychee.goto(id); - } -}; - -multiselect.photoClick = function (e, photoObj) { - var id = photoObj.attr("data-id"); - - if ((isSelectKeyPressed(e) || e.shiftKey) && album.isUploadable()) { - if (photoObj.hasClass("disabled")) return; - - if (isSelectKeyPressed(e)) { - multiselect.toggleItem(photoObj, id); - } else { - if (multiselect.photosSelected > 0) { - // Click with Shift. Select all elements between the current - // element and the last clicked-on one. - - if (photoObj.prevAll(".photo").toArray().includes(multiselect.lastClicked[0])) { - photoObj.prevUntil(multiselect.lastClicked, ".photo").each(function () { - multiselect.addItem($(this), $(this).attr("data-id")); - }); - } else if (photoObj.nextAll(".photo").toArray().includes(multiselect.lastClicked[0])) { - photoObj.nextUntil(multiselect.lastClicked, ".photo").each(function () { - multiselect.addItem($(this), $(this).attr("data-id")); - }); - } - } - - multiselect.addItem(photoObj, id); - } - } else { - lychee.goto(album.getID() + "/" + id); - } -}; - -multiselect.albumContextMenu = function (e, albumObj) { - var id = albumObj.attr("data-id"); - var selected = multiselect.isSelected(id).selected; - - if (albumObj.hasClass("disabled")) return; - - if (selected !== false && multiselect.ids.length > 1) { - contextMenu.albumMulti(multiselect.ids, e); - } else { - contextMenu.album(id, e); - } -}; - -multiselect.photoContextMenu = function (e, photoObj) { - var id = photoObj.attr("data-id"); - var selected = multiselect.isSelected(id).selected; - - if (photoObj.hasClass("disabled")) return; - - if (selected !== false && multiselect.ids.length > 1) { - contextMenu.photoMulti(multiselect.ids, e); - } else if (visible.album() || visible.search()) { - contextMenu.photo(id, e); - } else if (visible.photo()) { - // should not happen... but you never know... - contextMenu.photo(_photo.getID(), e); - } else { - lychee.error("Could not find what you want."); - } -}; - -multiselect.clearSelection = function () { - multiselect.deselect(".photo.active, .album.active"); - multiselect.ids = []; - multiselect.albumsSelected = 0; - multiselect.photosSelected = 0; - multiselect.lastClicked = null; -}; - -multiselect.show = function (e) { - if (!album.isUploadable()) return false; - if (!visible.albums() && !visible.album()) return false; - if ($(".album:hover, .photo:hover").length !== 0) return false; - if (visible.search()) return false; - if (visible.multiselect()) $("#multiselect").remove(); - - _sidebar.setSelectable(false); - - if (!isSelectKeyPressed(e) && !e.shiftKey) { - multiselect.clearSelection(); - } - - multiselect.position.top = e.pageY; - multiselect.position.right = $(document).width() - e.pageX; - multiselect.position.bottom = $(document).height() - e.pageY; - multiselect.position.left = e.pageX; - - $("body").append(build.multiselect(multiselect.position.top, multiselect.position.left)); - - $(document).on("mousemove", multiselect.resize).on("mouseup", function (_e) { - if (_e.which === 1) { - multiselect.getSelection(_e); - } - }); -}; - -multiselect.resize = function (e) { - if (multiselect.position.top === null || multiselect.position.right === null || multiselect.position.bottom === null || multiselect.position.left === null) return false; - - // Default CSS - var newCSS = { - top: null, - bottom: null, - height: null, - left: null, - right: null, - width: null - }; - - if (e.pageY >= multiselect.position.top) { - newCSS.top = multiselect.position.top; - newCSS.bottom = "inherit"; - newCSS.height = Math.min(e.pageY, $(document).height() - 3) - multiselect.position.top; - } else { - newCSS.top = "inherit"; - newCSS.bottom = multiselect.position.bottom; - newCSS.height = multiselect.position.top - Math.max(e.pageY, 2); - } - - if (e.pageX >= multiselect.position.left) { - newCSS.right = "inherit"; - newCSS.left = multiselect.position.left; - newCSS.width = Math.min(e.pageX, $(document).width() - 3) - multiselect.position.left; - } else { - newCSS.right = multiselect.position.right; - newCSS.left = "inherit"; - newCSS.width = multiselect.position.left - Math.max(e.pageX, 2); - } - - // Updated all CSS properties at once - $("#multiselect").css(newCSS); -}; - -multiselect.stopResize = function () { - if (multiselect.position.top !== null) $(document).off("mousemove mouseup"); -}; - -multiselect.getSize = function () { - if (!visible.multiselect()) return false; - - var $elem = $("#multiselect"); - var offset = $elem.offset(); - - return { - top: offset.top, - left: offset.left, - width: parseFloat($elem.css("width"), 10), - height: parseFloat($elem.css("height"), 10) - }; -}; - -multiselect.getSelection = function (e) { - var size = multiselect.getSize(); - - if (visible.contextMenu()) return false; - if (!visible.multiselect()) return false; - - $(".photo, .album").each(function () { - // We select if there's even a slightest overlap. Overlap between - // an object and the selection occurs if the left edge of the - // object is to the left of the right edge of the selection *and* - // the right edge of the object is to the right of the left edge of - // the selection; analogous for top/bottom. - if ($(this).offset().left < size.left + size.width && $(this).offset().left + $(this).width() > size.left && $(this).offset().top < size.top + size.height && $(this).offset().top + $(this).height() > size.top) { - var id = $(this).attr("data-id"); - - if (isSelectKeyPressed(e)) { - multiselect.toggleItem($(this), id); - } else { - multiselect.addItem($(this), id); - } - } - }); - - multiselect.hide(); -}; - -multiselect.select = function (id) { - var el = $(id); - - el.addClass("selected"); - el.addClass("active"); -}; - -multiselect.deselect = function (id) { - var el = $(id); - - el.removeClass("selected"); - el.removeClass("active"); -}; - -multiselect.hide = function () { - _sidebar.setSelectable(true); - - multiselect.stopResize(); - - multiselect.position.top = null; - multiselect.position.right = null; - multiselect.position.bottom = null; - multiselect.position.left = null; - - lychee.animate("#multiselect", "fadeOut"); - setTimeout(function () { - return $("#multiselect").remove(); - }, 300); -}; - -multiselect.close = function () { - _sidebar.setSelectable(true); - - multiselect.stopResize(); - - multiselect.position.top = null; - multiselect.position.right = null; - multiselect.position.bottom = null; - multiselect.position.left = null; - - lychee.animate("#multiselect", "fadeOut"); - setTimeout(function () { - return $("#multiselect").remove(); - }, 300); -}; - -multiselect.selectAll = function () { - if (!album.isUploadable()) return false; - if (visible.search()) return false; - if (!visible.albums() && !visible.album) return false; - if (visible.multiselect()) $("#multiselect").remove(); - - _sidebar.setSelectable(false); - - multiselect.clearSelection(); - - $(".photo").each(function () { - multiselect.addItem($(this), $(this).attr("data-id")); - }); - - if (multiselect.photosSelected === 0) { - // There are no pictures. Try albums then. - $(".album").each(function () { - multiselect.addItem($(this), $(this).attr("data-id")); - }); - } -}; - -var notifications = { - json: "" -}; - -notifications.update = function (params) { - if (params.email.length > 1) { - var regexp = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; - - if (!regexp.test(String(params.email).toLowerCase())) { - loadingBar.show("error", "Not a valid email address."); - return false; - } - } - - api.post("User::UpdateEmail", params, function (data) { - if (data !== true) { - loadingBar.show("error", data.description); - lychee.error(null, params, data); - } else { - loadingBar.show("success", "Email updated!"); - } - }); -}; - -notifications.load = function () { - api.post("User::GetEmail", {}, function (data) { - notifications.json = data; - view.notifications.init(); - }); -}; - -/** - * @description Controls the access to password-protected albums and photos. - */ - -var password = { - value: "" -}; - -password.getDialog = function (albumID, callback) { - var action = function action(data) { - var passwd = data.password; - - var params = { - albumID: albumID, - password: passwd - }; - - api.post("Album::getPublic", params, function (_data) { - if (_data === true) { - basicModal.close(); - password.value = passwd; - callback(); - } else { - basicModal.error("password"); - } - }); - }; - - var cancel = function cancel() { - basicModal.close(); - if (!visible.albums() && !visible.album()) lychee.goto(); - }; - - var msg = "\n\t\t\t

\n\t\t\t\t " + lychee.locale["ALBUM_PASSWORD_REQUIRED"] + "\n\t\t\t\t \n\t\t\t

\n\t\t\t "; - - basicModal.show({ - body: msg, - buttons: { - action: { - title: lychee.locale["ENTER"], - fn: action - }, - cancel: { - title: lychee.locale["CANCEL"], - fn: cancel - } - } - }); -}; - -/** - * @description Takes care of every action a photo can handle and execute. - */ - -var _photo = { - json: null, - cache: null, - supportsPrefetch: null, - LivePhotosObject: null -}; - -_photo.getID = function () { - var id = null; - - if (_photo.json) id = _photo.json.id;else id = $(".photo:hover, .photo.active").attr("data-id"); - - if ($.isNumeric(id) === true) return id;else return false; -}; - -_photo.load = function (photoID, albumID, autoplay) { - var checkContent = function checkContent() { - if (album.json != null && album.json.photos) _photo.load(photoID, albumID, autoplay);else setTimeout(checkContent, 100); - }; - - var checkPasswd = function checkPasswd() { - if (password.value !== "") _photo.load(photoID, albumID, autoplay);else setTimeout(checkPasswd, 200); - }; - - // we need to check the album.json.photos because otherwise the script is too fast and this raise an error. - if (album.json == null || album.json.photos == null) { - checkContent(); - return false; - } - - var params = { - photoID: photoID, - password: password.value - }; - - api.post("Photo::get", params, function (data) { - if (data === "Warning: Photo private!") { - lychee.content.show(); - lychee.goto(); - return false; - } - - if (data === "Warning: Wrong password!") { - checkPasswd(); - return false; - } - - _photo.json = data; - _photo.json.original_album = _photo.json.album; - _photo.json.album = albumID; - - if (!visible.photo()) view.photo.show(); - view.photo.init(autoplay); - lychee.imageview.show(); - - if (!lychee.hide_content_during_imgview) { - setTimeout(function () { - lychee.content.show(); - tabindex.makeUnfocusable(lychee.content); - }, 300); - } - }); -}; - -_photo.hasExif = function () { - var exifHash = _photo.json.make + _photo.json.model + _photo.json.shutter + _photo.json.aperture + _photo.json.focal + _photo.json.iso; - - return exifHash !== ""; -}; - -_photo.hasTakestamp = function () { - return _photo.json.taken_at !== null; -}; - -_photo.hasDesc = function () { - return _photo.json.description && _photo.json.description !== ""; -}; - -_photo.isLivePhoto = function () { - if (!_photo.json) return false; // In case it's called, but not initialized - return _photo.json.livePhotoUrl && _photo.json.livePhotoUrl !== ""; -}; - -_photo.isLivePhotoInitizalized = function () { - return _photo.LivePhotosObject !== null; -}; - -_photo.isLivePhotoPlaying = function () { - if (_photo.isLivePhotoInitizalized() === false) return false; - return _photo.LivePhotosObject.isPlaying; -}; - -_photo.cycle_display_overlay = function () { - var oldtype = build.check_overlay_type(_photo.json, lychee.image_overlay_type); - var newtype = build.check_overlay_type(_photo.json, oldtype, true); - if (oldtype !== newtype) { - lychee.image_overlay_type = newtype; - $("#image_overlay").remove(); - var newoverlay = build.overlay_image(_photo.json); - if (newoverlay !== "") lychee.imageview.append(newoverlay); - } -}; - -// Preload the next and previous photos for better response time -_photo.preloadNextPrev = function (photoID) { - if (album.json && album.json.photos && album.getByID(photoID)) { - var previousPhotoID = album.getByID(photoID).previousPhoto; - var nextPhotoID = album.getByID(photoID).nextPhoto; - var imgs = $("img#image"); - var isUsing2xCurrently = imgs.length > 0 && imgs[0].currentSrc !== null && imgs[0].currentSrc.includes("@2x."); - - $("head [data-prefetch]").remove(); - - var preload = function preload(preloadID) { - var preloadPhoto = album.getByID(preloadID); - var href = ""; - - if (preloadPhoto.sizeVariants.medium != null) { - href = preloadPhoto.sizeVariants.medium.url; - if (preloadPhoto.sizeVariants.medium2x != null && isUsing2xCurrently) { - // If the currently displayed image uses the 2x variant, - // chances are that so will the next one. - href = preloadPhoto.sizeVariants.medium2x.url; - } - } else if (preloadPhoto.type && preloadPhoto.type.indexOf("video") === -1) { - // Preload the original size, but only if it's not a video - href = preloadPhoto.url; - } - - if (href !== "") { - if (_photo.supportsPrefetch === null) { - // Copied from https://www.smashingmagazine.com/2016/02/preload-what-is-it-good-for/ - var DOMTokenListSupports = function DOMTokenListSupports(tokenList, token) { - if (!tokenList || !tokenList.supports) { - return null; - } - try { - return tokenList.supports(token); - } catch (e) { - if (e instanceof TypeError) { - console.log("The DOMTokenList doesn't have a supported tokens list"); - } else { - console.error("That shouldn't have happened"); - } - } - }; - _photo.supportsPrefetch = DOMTokenListSupports(document.createElement("link").relList, "prefetch"); - } - - if (_photo.supportsPrefetch) { - $("head").append(lychee.html(_templateObject51, href)); - } else { - // According to https://caniuse.com/#feat=link-rel-prefetch, - // as of mid-2019 it's mainly Safari (both on desktop and mobile) - new Image().src = href; - } - } - }; - - if (nextPhotoID && nextPhotoID !== "") { - preload(nextPhotoID); - } - if (previousPhotoID && previousPhotoID !== "") { - preload(previousPhotoID); - } - } -}; - -_photo.parse = function () { - if (!_photo.json.title) _photo.json.title = lychee.locale["UNTITLED"]; -}; - -_photo.updateSizeLivePhotoDuringAnimation = function () { - var animationDuraction = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 300; - var pauseBetweenUpdated = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 10; - - // For the LivePhotoKit, we need to call the updateSize manually - // during CSS animations - // - var interval = setInterval(function () { - if (_photo.isLivePhotoInitizalized()) { - _photo.LivePhotosObject.updateSize(); - } - }, pauseBetweenUpdated); - - setTimeout(function () { - clearInterval(interval); - }, animationDuraction); -}; - -_photo.previous = function (animate) { - if (_photo.getID() !== false && album.json && album.getByID(_photo.getID()) && album.getByID(_photo.getID()).previousPhoto !== "") { - var delay = 0; - - if (animate === true) { - delay = 200; - - $("#imageview #image").css({ - WebkitTransform: "translateX(100%)", - MozTransform: "translateX(100%)", - transform: "translateX(100%)", - opacity: 0 - }); - } - - setTimeout(function () { - if (_photo.getID() === false) return false; - _photo.LivePhotosObject = null; - lychee.goto(album.getID() + "/" + album.getByID(_photo.getID()).previousPhoto, false); - }, delay); - } -}; - -_photo.next = function (animate) { - if (_photo.getID() !== false && album.json && album.getByID(_photo.getID()) && album.getByID(_photo.getID()).nextPhoto !== "") { - var delay = 0; - - if (animate === true) { - delay = 200; - - $("#imageview #image").css({ - WebkitTransform: "translateX(-100%)", - MozTransform: "translateX(-100%)", - transform: "translateX(-100%)", - opacity: 0 - }); - } - - setTimeout(function () { - if (_photo.getID() === false) return false; - _photo.LivePhotosObject = null; - lychee.goto(album.getID() + "/" + album.getByID(_photo.getID()).nextPhoto, false); - }, delay); - } -}; - -_photo.delete = function (photoIDs) { - var action = {}; - var cancel = {}; - var msg = ""; - var photoTitle = ""; - - if (!photoIDs) return false; - if (photoIDs instanceof Array === false) photoIDs = [photoIDs]; - - if (photoIDs.length === 1) { - // Get title if only one photo is selected - if (visible.photo()) photoTitle = _photo.json.title;else photoTitle = album.getByID(photoIDs).title; - - // Fallback for photos without a title - if (photoTitle === "") photoTitle = lychee.locale["UNTITLED"]; - } - - action.fn = function () { - var nextPhoto = ""; - var previousPhoto = ""; - - basicModal.close(); - - photoIDs.forEach(function (id, index) { - // Change reference for the next and previous photo - if (album.getByID(id).nextPhoto !== "" || album.getByID(id).previousPhoto !== "") { - nextPhoto = album.getByID(id).nextPhoto; - previousPhoto = album.getByID(id).previousPhoto; - - if (previousPhoto !== "") { - album.getByID(previousPhoto).nextPhoto = nextPhoto; - } - if (nextPhoto !== "") { - album.getByID(nextPhoto).previousPhoto = previousPhoto; - } - } - - album.deleteByID(id); - view.album.content.delete(id, index === photoIDs.length - 1); - }); - - albums.refresh(); - - // Go to next photo if there is a next photo and - // next photo is not the current one. Also try the previous one. - // Show album otherwise. - if (visible.photo()) { - if (nextPhoto !== "" && nextPhoto !== _photo.getID()) { - lychee.goto(album.getID() + "/" + nextPhoto); - } else if (previousPhoto !== "" && previousPhoto !== _photo.getID()) { - lychee.goto(album.getID() + "/" + previousPhoto); - } else { - lychee.goto(album.getID()); - } - } else if (!visible.albums()) { - lychee.goto(album.getID()); - } - - var params = { - photoIDs: photoIDs.join() - }; - - api.post("Photo::delete", params, function (data) { - if (data !== true) lychee.error(null, params, data); - }); - }; - - if (photoIDs.length === 1) { - action.title = lychee.locale["PHOTO_DELETE"]; - cancel.title = lychee.locale["PHOTO_KEEP"]; - - msg = lychee.html(_templateObject52, lychee.locale["PHOTO_DELETE_1"], photoTitle, lychee.locale["PHOTO_DELETE_2"]); - } else { - action.title = lychee.locale["PHOTO_DELETE"]; - cancel.title = lychee.locale["PHOTO_KEEP"]; - - msg = lychee.html(_templateObject53, lychee.locale["PHOTO_DELETE_ALL_1"], photoIDs.length, lychee.locale["PHOTO_DELETE_ALL_2"]); - } - - basicModal.show({ - body: msg, - buttons: { - action: { - title: action.title, - fn: action.fn, - class: "red" - }, - cancel: { - title: cancel.title, - fn: basicModal.close - } - } - }); -}; - -_photo.setTitle = function (photoIDs) { - var oldTitle = ""; - var msg = ""; - - if (!photoIDs) return false; - if (photoIDs instanceof Array === false) photoIDs = [photoIDs]; - - if (photoIDs.length === 1) { - // Get old title if only one photo is selected - if (_photo.json) oldTitle = _photo.json.title;else if (album.json) oldTitle = album.getByID(photoIDs).title; - } - - var action = function action(data) { - if (!data.title.trim()) { - basicModal.error("title"); - return; - } - - basicModal.close(); - - var newTitle = data.title; - - if (visible.photo()) { - _photo.json.title = newTitle === "" ? "Untitled" : newTitle; - view.photo.title(); - } - - photoIDs.forEach(function (id) { - album.getByID(id).title = newTitle; - view.album.content.title(id); - }); - - var params = { - photoIDs: photoIDs.join(), - title: newTitle - }; - - api.post("Photo::setTitle", params, function (_data) { - if (_data !== true) { - lychee.error(null, params, _data); - } - }); - }; - - var input = lychee.html(_templateObject54, oldTitle); - - if (photoIDs.length === 1) msg = lychee.html(_templateObject5, lychee.locale["PHOTO_NEW_TITLE"], input);else msg = lychee.html(_templateObject55, lychee.locale["PHOTOS_NEW_TITLE_1"], photoIDs.length, lychee.locale["PHOTOS_NEW_TITLE_2"], input); - - basicModal.show({ - body: msg, - buttons: { - action: { - title: lychee.locale["PHOTO_SET_TITLE"], - fn: action - }, - cancel: { - title: lychee.locale["CANCEL"], - fn: basicModal.close - } - } - }); -}; - -_photo.copyTo = function (photoIDs, albumID) { - if (!photoIDs) return false; - if (photoIDs instanceof Array === false) photoIDs = [photoIDs]; - - var params = { - photoIDs: photoIDs.join(), - albumID: albumID - }; - - api.post("Photo::duplicate", params, function (data) { - if (data !== true) { - lychee.error(null, params, data); - } else { - album.reload(); - } - }); -}; - -_photo.setAlbum = function (photoIDs, albumID) { - var nextPhoto = ""; - var previousPhoto = ""; - - if (!photoIDs) return false; - if (photoIDs instanceof Array === false) photoIDs = [photoIDs]; - - photoIDs.forEach(function (id, index) { - // Change reference for the next and previous photo - if (album.getByID(id).nextPhoto !== "" || album.getByID(id).previousPhoto !== "") { - nextPhoto = album.getByID(id).nextPhoto; - previousPhoto = album.getByID(id).previousPhoto; - - if (previousPhoto !== "") { - album.getByID(previousPhoto).nextPhoto = nextPhoto; - } - if (nextPhoto !== "") { - album.getByID(nextPhoto).previousPhoto = previousPhoto; - } - } - - album.deleteByID(id); - view.album.content.delete(id, index === photoIDs.length - 1); - }); - - albums.refresh(); - - // Go to next photo if there is a next photo and - // next photo is not the current one. Also try the previous one. - // Show album otherwise. - if (visible.photo()) { - if (nextPhoto !== "" && nextPhoto !== _photo.getID()) { - lychee.goto(album.getID() + "/" + nextPhoto); - } else if (previousPhoto !== "" && previousPhoto !== _photo.getID()) { - lychee.goto(album.getID() + "/" + previousPhoto); - } else { - lychee.goto(album.getID()); - } - } - - var params = { - photoIDs: photoIDs.join(), - albumID: albumID - }; - - api.post("Photo::setAlbum", params, function (data) { - if (data !== true) { - lychee.error(null, params, data); - } else { - // We only really need to do anything here if the destination - // is a (possibly nested) subalbum of the current album; but - // since we have no way of figuring it out (albums.json is - // null), we need to reload. - if (visible.album()) { - album.reload(); - } - } - }); -}; - -_photo.setStar = function (photoIDs) { - if (!photoIDs) return false; - - if (visible.photo()) { - _photo.json.star = _photo.json.star === "0" ? "1" : "0"; - view.photo.star(); - } - - photoIDs.forEach(function (id) { - album.getByID(id).star = album.getByID(id).star === "0" ? "1" : "0"; - view.album.content.star(id); - }); - - albums.refresh(); - - var params = { - photoIDs: photoIDs.join() - }; - - api.post("Photo::setStar", params, function (data) { - if (data !== true) lychee.error(null, params, data); - }); -}; - -_photo.setPublic = function (photoID, e) { - var msg_switch = lychee.html(_templateObject56, lychee.locale["PHOTO_PUBLIC"], lychee.locale["PHOTO_PUBLIC_EXPL"]); - - var msg_choices = lychee.html(_templateObject57, build.iconic("check"), lychee.locale["PHOTO_FULL"], lychee.locale["PHOTO_FULL_EXPL"], build.iconic("check"), lychee.locale["PHOTO_HIDDEN"], lychee.locale["PHOTO_HIDDEN_EXPL"], build.iconic("check"), lychee.locale["PHOTO_DOWNLOADABLE"], lychee.locale["PHOTO_DOWNLOADABLE_EXPL"], build.iconic("check"), lychee.locale["PHOTO_SHARE_BUTTON_VISIBLE"], lychee.locale["PHOTO_SHARE_BUTTON_VISIBLE_EXPL"], build.iconic("check"), lychee.locale["PHOTO_PASSWORD_PROT"], lychee.locale["PHOTO_PASSWORD_PROT_EXPL"]); - - if (_photo.json.public === "2") { - // Public album. We can't actually change anything but we will - // display the current settings. - - var _msg3 = lychee.html(_templateObject58, lychee.locale["PHOTO_NO_EDIT_SHARING_TEXT"], msg_switch, msg_choices); - - basicModal.show({ - body: _msg3, - buttons: { - cancel: { - title: lychee.locale["CLOSE"], - fn: basicModal.close - } - } - }); - - $('.basicModal .switch input[name="public"]').prop("checked", true); - if (album.json) { - if (album.json.full_photo !== null && album.json.full_photo === "1") { - $('.basicModal .choice input[name="full_photo"]').prop("checked", true); - } - // Photos in public albums are never hidden as such. It's the - // album that's hidden. Or is that distinction irrelevant to end - // users? - if (album.json.downloadable === "1") { - $('.basicModal .choice input[name="downloadable"]').prop("checked", true); - } - if (album.json.password === "1") { - $('.basicModal .choice input[name="password"]').prop("checked", true); - } - } - - $(".basicModal .switch input").attr("disabled", true); - $(".basicModal .switch .label").addClass("label--disabled"); - } else { - // Private album -- each photo can be shared individually. - - var _msg4 = lychee.html(_templateObject59, msg_switch, lychee.locale["PHOTO_EDIT_GLOBAL_SHARING_TEXT"], msg_choices); - - var action = function action() { - var newPublic = $('.basicModal .switch input[name="public"]:checked').length === 1 ? "1" : "0"; - - if (newPublic !== _photo.json.public) { - if (visible.photo()) { - _photo.json.public = newPublic; - view.photo.public(); - } - - album.getByID(photoID).public = newPublic; - view.album.content.public(photoID); - - albums.refresh(); - - // Photo::setPublic simply flips the current state. - // Ugly API but effective... - api.post("Photo::setPublic", { photoID: photoID }, function (data) { - if (data !== true) lychee.error(null, params, data); - }); - } - - basicModal.close(); - }; - - basicModal.show({ - body: _msg4, - buttons: { - action: { - title: lychee.locale["PHOTO_SHARING_CONFIRM"], - fn: action - }, - cancel: { - title: lychee.locale["CANCEL"], - fn: basicModal.close - } - } - }); - - $('.basicModal .switch input[name="public"]').on("click", function () { - if ($(this).prop("checked") === true) { - if (lychee.full_photo) { - $('.basicModal .choice input[name="full_photo"]').prop("checked", true); - } - if (lychee.public_photos_hidden) { - $('.basicModal .choice input[name="hidden"]').prop("checked", true); - } - if (lychee.downloadable) { - $('.basicModal .choice input[name="downloadable"]').prop("checked", true); - } - if (lychee.share_button_visible) { - $('.basicModal .choice input[name="share_button_visible"]').prop("checked", true); - } - // Photos shared individually can't be password-protected. - } else { - $(".basicModal .choice input").prop("checked", false); - } - }); - - if (_photo.json.public === "1") { - $('.basicModal .switch input[name="public"]').click(); - } - } - - return true; -}; - -_photo.setDescription = function (photoID) { - var oldDescription = _photo.json.description; - - var action = function action(data) { - basicModal.close(); - - var description = data.description; - - if (visible.photo()) { - _photo.json.description = description; - view.photo.description(); - } - - var params = { - photoID: photoID, - description: description - }; - - api.post("Photo::setDescription", params, function (_data) { - if (_data !== true) { - lychee.error(null, params, _data); - } - }); - }; - - basicModal.show({ - body: lychee.html(_templateObject60, lychee.locale["PHOTO_NEW_DESCRIPTION"], lychee.locale["PHOTO_DESCRIPTION"], oldDescription), - buttons: { - action: { - title: lychee.locale["PHOTO_SET_DESCRIPTION"], - fn: action - }, - cancel: { - title: lychee.locale["CANCEL"], - fn: basicModal.close - } - } - }); -}; - -_photo.editTags = function (photoIDs) { - var oldTags = ""; - var msg = ""; - - if (!photoIDs) return false; - if (photoIDs instanceof Array === false) photoIDs = [photoIDs]; - - // Get tags - if (visible.photo()) oldTags = _photo.json.tags;else if (visible.album() && photoIDs.length === 1) oldTags = album.getByID(photoIDs).tags;else if (visible.search() && photoIDs.length === 1) oldTags = album.getByID(photoIDs).tags;else if (visible.album() && photoIDs.length > 1) { - var same = true; - photoIDs.forEach(function (id) { - same = album.getByID(id).tags === album.getByID(photoIDs[0]).tags && same === true; - }); - if (same === true) oldTags = album.getByID(photoIDs[0]).tags; - } - - // Improve tags - oldTags = oldTags.replace(/,/g, ", "); - - var action = function action(data) { - basicModal.close(); - _photo.setTags(photoIDs, data.tags); - }; - - var input = lychee.html(_templateObject61, oldTags); - - if (photoIDs.length === 1) msg = lychee.html(_templateObject5, lychee.locale["PHOTO_NEW_TAGS"], input);else msg = lychee.html(_templateObject55, lychee.locale["PHOTO_NEW_TAGS_1"], photoIDs.length, lychee.locale["PHOTO_NEW_TAGS_2"], input); - - basicModal.show({ - body: msg, - buttons: { - action: { - title: lychee.locale["PHOTO_SET_TAGS"], - fn: action - }, - cancel: { - title: lychee.locale["CANCEL"], - fn: basicModal.close - } - } - }); -}; - -_photo.setTags = function (photoIDs, tags) { - if (!photoIDs) return false; - if (photoIDs instanceof Array === false) photoIDs = [photoIDs]; - - // Parse tags - tags = tags.replace(/(\ ,\ )|(\ ,)|(,\ )|(,{1,}\ {0,})|(,$|^,)/g, ","); - tags = tags.replace(/,$|^,|(\ ){0,}$/g, ""); - - if (visible.photo()) { - _photo.json.tags = tags; - view.photo.tags(); - } - - photoIDs.forEach(function (id, index, array) { - album.getByID(id).tags = tags; - }); - - var params = { - photoIDs: photoIDs.join(), - tags: tags - }; - - api.post("Photo::setTags", params, function (data) { - if (data !== true) { - lychee.error(null, params, data); - } else if (albums.json && albums.json.smartalbums) { - $.each(Object.entries(albums.json.smartalbums), function () { - if (this.length == 2 && this[1]["tag_album"] === "1") { - // If we have any tag albums, force a refresh. - albums.refresh(); - return false; - } - }); - } - }); -}; - -_photo.deleteTag = function (photoID, index) { - var tags = void 0; - - // Remove - tags = _photo.json.tags.split(","); - tags.splice(index, 1); - - // Save - _photo.json.tags = tags.toString(); - _photo.setTags([photoID], _photo.json.tags); -}; - -_photo.share = function (photoID, service) { - if (_photo.json.hasOwnProperty("share_button_visible") && _photo.json.share_button_visible !== "1") { - return; - } - - var url = _photo.getViewLink(photoID); - - switch (service) { - case "twitter": - window.open("https://twitter.com/share?url=" + encodeURI(url)); - break; - case "facebook": - window.open("https://www.facebook.com/sharer.php?u=" + encodeURI(url) + "&t=" + encodeURI(_photo.json.title)); - break; - case "mail": - location.href = "mailto:?subject=" + encodeURI(_photo.json.title) + "&body=" + encodeURI(url); - break; - case "dropbox": - lychee.loadDropbox(function () { - var filename = _photo.json.title + "." + _photo.getDirectLink().split(".").pop(); - Dropbox.save(_photo.getDirectLink(), filename); - }); - break; - } -}; - -_photo.setLicense = function (photoID) { - var callback = function callback() { - $("select#license").val(_photo.json.license === "" ? "none" : _photo.json.license); - return false; - }; - - var action = function action(data) { - basicModal.close(); - var license = data.license; - - var params = { - photoID: photoID, - license: license - }; - - api.post("Photo::setLicense", params, function (_data) { - if (_data !== true) { - lychee.error(null, params, _data); - } else { - // update the photo JSON and reload the license in the sidebar - _photo.json.license = params.license; - view.photo.license(); - } - }); - }; - - var msg = lychee.html(_templateObject8, lychee.locale["PHOTO_LICENSE"], lychee.locale["PHOTO_LICENSE_NONE"], lychee.locale["PHOTO_RESERVED"], lychee.locale["PHOTO_LICENSE_HELP"]); - - basicModal.show({ - body: msg, - callback: callback, - buttons: { - action: { - title: lychee.locale["PHOTO_SET_LICENSE"], - fn: action - }, - cancel: { - title: lychee.locale["CANCEL"], - fn: basicModal.close - } - } - }); -}; - -_photo.getArchive = function (photoIDs) { - var kind = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; - - if (photoIDs.length === 1 && kind === null) { - // For a single photo, allow to pick the kind via a dialog box. - - var myPhoto = void 0; - - if (_photo.json && _photo.json.id === photoIDs[0]) { - myPhoto = _photo.json; - } else { - myPhoto = album.getByID(photoIDs[0]); - } - - var buildButton = function buildButton(id, label) { - return lychee.html(_templateObject62, id, lychee.locale["DOWNLOAD"], build.iconic("cloud-download"), label); - }; - - var _msg5 = lychee.html(_templateObject63); - - if (myPhoto.url) { - _msg5 += buildButton("FULL", lychee.locale["PHOTO_FULL"] + " (" + myPhoto.width + "x" + myPhoto.height + ", " + lychee.locale.printFilesizeLocalized(myPhoto.filesize) + ")"); - } - if (myPhoto.livePhotoUrl !== null) { - _msg5 += buildButton("LIVEPHOTOVIDEO", "" + lychee.locale["PHOTO_LIVE_VIDEO"]); - } - if (myPhoto.sizeVariants.medium2x !== null) { - _msg5 += buildButton("MEDIUM2X", lychee.locale["PHOTO_MEDIUM_HIDPI"] + " (" + myPhoto.sizeVariants.medium2x.width + "x" + myPhoto.sizeVariants.medium2x.height + ")"); - } - if (myPhoto.sizeVariants.medium !== null) { - _msg5 += buildButton("MEDIUM", lychee.locale["PHOTO_MEDIUM"] + " (" + myPhoto.sizeVariants.medium.width + "x" + myPhoto.sizeVariants.medium.height + ")"); - } - if (myPhoto.sizeVariants.small2x !== null) { - _msg5 += buildButton("SMALL2X", lychee.locale["PHOTO_SMALL_HIDPI"] + " (" + myPhoto.sizeVariants.small2x.width + "x" + myPhoto.sizeVariants.small2x.height + ")"); - } - if (myPhoto.sizeVariants.small !== null) { - _msg5 += buildButton("SMALL", lychee.locale["PHOTO_SMALL"] + " (" + myPhoto.sizeVariants.small.width + "x" + myPhoto.sizeVariants.small.height + ")"); - } - if (myPhoto.sizeVariants.thumb2x !== null) { - _msg5 += buildButton("THUMB2X", lychee.locale["PHOTO_THUMB_HIDPI"] + " (" + myPhoto.sizeVariants.thumb2x.width + "x" + myPhoto.sizeVariants.thumb2x.height + ")"); - } - if (myPhoto.sizeVariants.thumb !== null) { - _msg5 += buildButton("THUMB", lychee.locale["PHOTO_THUMB"] + " (" + myPhoto.sizeVariants.thumb.width + "x" + myPhoto.sizeVariants.thumb.height + ")"); - } - - _msg5 += lychee.html(_templateObject64); - - basicModal.show({ - body: _msg5, - buttons: { - cancel: { - title: lychee.locale["CLOSE"], - fn: basicModal.close - } - } - }); - - $(".downloads .basicModal__button").on(lychee.getEventName(), function () { - kind = this.id; - basicModal.close(); - _photo.getArchive(photoIDs, kind); - }); - - return true; - } - - location.href = "api/Photo::getArchive" + lychee.html(_templateObject65, photoIDs.join(), kind); -}; - -_photo.getDirectLink = function () { - var url = ""; - - if (_photo.json && _photo.json.url && _photo.json.url !== "") url = _photo.json.url; - - return url; -}; - -_photo.getViewLink = function (photoID) { - var url = "view?p=" + photoID; - - return lychee.getBaseUrl() + url; -}; - -_photo.showDirectLinks = function (photoID) { - if (!_photo.json || _photo.json.id != photoID) { - return; - } - - var buildLine = function buildLine(label, url) { - return lychee.html(_templateObject66, label, url, lychee.locale["URL_COPY_TO_CLIPBOARD"], build.iconic("copy", "ionicons")); - }; - - var msg = lychee.html(_templateObject67, buildLine(lychee.locale["PHOTO_VIEW"], _photo.getViewLink(photoID)), lychee.locale["PHOTO_DIRECT_LINKS_TO_IMAGES"]); - - if (_photo.json.url) { - msg += buildLine(lychee.locale["PHOTO_FULL"] + " (" + _photo.json.width + "x" + _photo.json.height + ")", lychee.getBaseUrl() + _photo.json.url); - } - if (_photo.json.sizeVariants.medium2x !== null) { - msg += buildLine(lychee.locale["PHOTO_MEDIUM_HIDPI"] + " (" + _photo.json.sizeVariants.medium2x.width + "x" + _photo.json.sizeVariants.medium2x.height + ")", lychee.getBaseUrl() + _photo.json.sizeVariants.medium2x.url); - } - if (_photo.json.sizeVariants.medium !== null) { - msg += buildLine(lychee.locale["PHOTO_MEDIUM"] + " (" + _photo.json.sizeVariants.medium.width + "x" + _photo.json.sizeVariants.medium.height + ")", lychee.getBaseUrl() + _photo.json.sizeVariants.medium.url); - } - if (_photo.json.sizeVariants.small2x !== null) { - msg += buildLine(lychee.locale["PHOTO_SMALL_HIDPI"] + " (" + _photo.json.sizeVariants.small2x.width + "x" + _photo.json.sizeVariants.small2x.height + ")", lychee.getBaseUrl() + _photo.json.sizeVariants.small2x.url); - } - if (_photo.json.sizeVariants.small !== null) { - msg += buildLine(lychee.locale["PHOTO_SMALL"] + " (" + _photo.json.sizeVariants.small.width + "x" + _photo.json.sizeVariants.small.height + ")", lychee.getBaseUrl() + _photo.json.sizeVariants.small.url); - } - if (_photo.json.sizeVariants.thumb2x !== null) { - msg += buildLine(lychee.locale["PHOTO_THUMB_HIDPI"] + " (" + _photo.json.sizeVariants.thumb2x.width + "x" + _photo.json.sizeVariants.thumb2x.height + ")", lychee.getBaseUrl() + _photo.json.sizeVariants.thumb2x.url); - } - if (_photo.json.sizeVariants.thumb !== null) { - msg += buildLine(lychee.locale["PHOTO_THUMB"] + " (" + _photo.json.sizeVariants.thumb.width + "x" + _photo.json.sizeVariants.thumb.height + ")", lychee.getBaseUrl() + _photo.json.sizeVariants.thumb.url); - } - if (_photo.json.livePhotoUrl !== "") { - msg += buildLine(" " + lychee.locale["PHOTO_LIVE_VIDEO"] + " ", lychee.getBaseUrl() + _photo.json.livePhotoUrl); - } - - msg += lychee.html(_templateObject68); - - basicModal.show({ - body: msg, - buttons: { - cancel: { - title: lychee.locale["CLOSE"], - fn: basicModal.close - } - } - }); - - // Ensure that no input line is selected on opening. - $(".basicModal input:focus").blur(); - - $(".directLinks .basicModal__button").on(lychee.getEventName(), function () { - if (lychee.clipboardCopy($(this).prev().val())) { - loadingBar.show("success", lychee.locale["URL_COPIED_TO_CLIPBOARD"]); - } - }); -}; - -/** - * @description Takes care of every action a photoeditor can handle and execute. - */ - -photoeditor = {}; - -photoeditor.rotate = function (photoID, direction) { - if (!photoID) return false; - if (!direction) return false; - - var params = { - photoID: photoID, - direction: direction - }; - - api.post("PhotoEditor::rotate", params, function (data) { - if (data === false) { - lychee.error(null, params, data); - } else { - _photo.json = data; - _photo.json.original_album = _photo.json.album; - if (album.json) { - _photo.json.album = album.json.id; - } - - var image = $("img#image"); - if (_photo.json.sizeVariants.medium2x !== null) { - image.prop("srcset", _photo.json.sizeVariants.medium.url + " " + _photo.json.sizeVariants.medium.width + "w, " + _photo.json.sizeVariants.medium2x.url + " " + _photo.json.sizeVariants.medium2x.width + "w"); - } else { - image.prop("srcset", ""); - } - image.prop("src", _photo.json.sizeVariants.medium !== null ? _photo.json.sizeVariants.medium.url : _photo.json.url); - view.photo.onresize(); - view.photo.sidebar(); - - album.updatePhoto(data); - } - }); -}; - -/** - * @description Searches through your photos and albums. - */ - -var search = { - hash: null -}; - -search.find = function (term) { - if (term.trim() === "") return false; - - clearTimeout($(window).data("timeout")); - - $(window).data("timeout", setTimeout(function () { - if (header.dom(".header__search").val().length !== 0) { - api.post("search", { term: term }, function (data) { - var html = ""; - var albumsData = ""; - var photosData = ""; - - // Build albums - if (data && data.albums) { - albums.json = { albums: data.albums }; - $.each(albums.json.albums, function () { - albums.parse(this); - albumsData += build.album(this); - }); - } - - // Build photos - if (data && data.photos) { - album.json = { photos: data.photos }; - $.each(album.json.photos, function () { - photosData += build.photo(this); - }); - } - - var albums_divider = lychee.locale["ALBUMS"]; - var photos_divider = lychee.locale["PHOTOS"]; - - if (albumsData !== "") albums_divider += " (" + data.albums.length + ")"; - if (photosData !== "") { - photos_divider += " (" + data.photos.length + ")"; - if (lychee.layout === "1") { - photosData = '
' + photosData + "
"; - } else if (lychee.layout === "2") { - photosData = '
' + photosData + "
"; - } - } - - // 1. No albums and photos - // 2. Only photos - // 3. Only albums - // 4. Albums and photos - if (albumsData === "" && photosData === "") html = "error";else if (albumsData === "") html = build.divider(photos_divider) + photosData;else if (photosData === "") html = build.divider(albums_divider) + albumsData;else html = build.divider(albums_divider) + albumsData + build.divider(photos_divider) + photosData; - - // Only refresh view when search results are different - if (search.hash !== data.hash) { - $(".no_content").remove(); - - lychee.animate(".content", "contentZoomOut"); - - search.hash = data.hash; - - setTimeout(function () { - if (visible.photo()) view.photo.hide(); - if (visible.sidebar()) _sidebar.toggle(); - if (visible.mapview()) mapview.close(); - - header.setMode("albums"); - - if (html === "error") { - lychee.content.html(""); - $("body").append(build.no_content("magnifying-glass")); - } else { - lychee.content.html(html); - view.album.content.justify(); - lychee.animate(lychee.content, "contentZoomIn"); - } - lychee.setTitle(lychee.locale["SEARCH_RESULTS"], false); - - $(window).scrollTop(0); - }, 300); - } - }); - } else search.reset(); - }, 250)); -}; - -search.reset = function () { - header.dom(".header__search").val(""); - $(".no_content").remove(); - - if (search.hash != null) { - // Trash data - albums.json = null; - album.json = null; - _photo.json = null; - search.hash = null; - - lychee.animate(".divider", "fadeOut"); - lychee.goto(); - } -}; - -/** - * @description Lets you change settings. - */ - -var settings = {}; - -settings.open = function () { - view.settings.init(); -}; - -settings.createConfig = function () { - var action = function action(data) { - var dbName = data.dbName || ""; - var dbUser = data.dbUser || ""; - var dbPassword = data.dbPassword || ""; - var dbHost = data.dbHost || ""; - var dbTablePrefix = data.dbTablePrefix || ""; - - if (dbUser.length < 1) { - basicModal.error("dbUser"); - return false; - } - - if (dbHost.length < 1) dbHost = "localhost"; - if (dbName.length < 1) dbName = "lychee"; - - var params = { - dbName: dbName, - dbUser: dbUser, - dbPassword: dbPassword, - dbHost: dbHost, - dbTablePrefix: dbTablePrefix - }; - - api.post("Config::create", params, function (_data) { - if (_data !== true) { - // Connection failed - if (_data === "Warning: Connection failed!") { - basicModal.show({ - body: "

" + lychee.locale["ERROR_DB_1"] + "

", - buttons: { - action: { - title: lychee.locale["RETRY"], - fn: settings.createConfig - } - } - }); - - return false; - } - - // Creation failed - if (_data === "Warning: Creation failed!") { - basicModal.show({ - body: "

" + lychee.locale["ERROR_DB_2"] + "

", - buttons: { - action: { - title: lychee.locale["RETRY"], - fn: settings.createConfig - } - } - }); - - return false; - } - - // Could not create file - if (_data === "Warning: Could not create file!") { - basicModal.show({ - body: "

" + lychee.locale["ERROR_CONFIG_FILE"] + "

", - buttons: { - action: { - title: lychee.locale["RETRY"], - fn: settings.createConfig - } - } - }); - - return false; - } - - // Something went wrong - basicModal.show({ - body: "

" + lychee.locale["ERROR_UNKNOWN"] + "

", - buttons: { - action: { - title: lychee.locale["RETRY"], - fn: settings.createConfig - } - } - }); - - return false; - } else { - // Configuration successful - window.location.reload(); - - return false; - } - }); - }; - - var msg = "\n\t\t\t

\n\t\t\t\t " + lychee.locale["DB_INFO_TITLE"] + "\n\t\t\t\t \n\t\t\t\t \n\t\t\t\t \n\t\t\t

\n\t\t\t

\n\t\t\t\t " + lychee.locale["DB_INFO_TEXT"] + "\n\t\t\t\t \n\t\t\t\t \n\t\t\t

\n\t\t\t "; - - basicModal.show({ - body: msg, - buttons: { - action: { - title: lychee.locale["DB_CONNECT"], - fn: action - } - } - }); -}; - -settings.createLogin = function () { - var action = function action(data) { - var username = data.username; - var password = data.password; - var confirm = data.confirm; - - if (!username.trim()) { - basicModal.error("username"); - return false; - } - - if (!password.trim()) { - basicModal.error("password"); - return false; - } - - if (password !== confirm) { - basicModal.error("confirm"); - return false; - } - - basicModal.close(); - - var params = { - username: username, - password: password - }; - - api.post("Settings::setLogin", params, function (_data) { - if (_data !== true) { - basicModal.show({ - body: "

" + lychee.locale["ERROR_LOGIN"] + "

", - buttons: { - action: { - title: lychee.locale["RETRY"], - fn: settings.createLogin - } - } - }); - } - // else - // { - // window.location.reload() - // } - }); - }; - - var msg = "\n\t\t\t

\n\t\t\t\t " + lychee.locale["LOGIN_TITLE"] + "\n\t\t\t\t \n\t\t\t\t \n\t\t\t\t \n\t\t\t

\n\t\t\t "; - - basicModal.show({ - body: msg, - buttons: { - action: { - title: lychee.locale["LOGIN_CREATE"], - fn: action - } - } - }); -}; - -// from https://github.com/electerious/basicModal/blob/master/src/scripts/main.js -settings.getValues = function (form_name) { - var values = {}; - var inputs_select = $(form_name + " input[name], " + form_name + " select[name]"); - - // Get value from all inputs - $(inputs_select).each(function () { - var name = $(this).attr("name"); - // Store name and value of input - values[name] = $(this).val(); - }); - return Object.keys(values).length === 0 ? null : values; -}; - -// from https://github.com/electerious/basicModal/blob/master/src/scripts/main.js -settings.bind = function (item, name, fn) { - // if ($(item).length) - // { - // console.log('found'); - // } - // else - // { - // console.log('not found: ' + item); - // } - // Action-button - $(item).on("click", function () { - fn(settings.getValues(name)); - }); -}; - -settings.changeLogin = function (params) { - if (params.username.length < 1) { - loadingBar.show("error", "new username cannot be empty."); - $("input[name=username]").addClass("error"); - return false; - } else { - $("input[name=username]").removeClass("error"); - } - - if (params.password.length < 1) { - loadingBar.show("error", "new password cannot be empty."); - $("input[name=password]").addClass("error"); - return false; - } else { - $("input[name=password]").removeClass("error"); - } - - if (params.password !== params.confirm) { - loadingBar.show("error", "new password does not match."); - $("input[name=confirm]").addClass("error"); - return false; - } else { - $("input[name=confirm]").removeClass("error"); - } - - api.post("Settings::setLogin", params, function (data) { - if (data !== true) { - loadingBar.show("error", data.description); - lychee.error(null, datas, data); - } else { - $("input[name]").removeClass("error"); - loadingBar.show("success", lychee.locale["SETTINGS_SUCCESS_LOGIN"]); - view.settings.content.clearLogin(); - } - }); -}; - -settings.changeSorting = function (params) { - api.post("Settings::setSorting", params, function (data) { - if (data === true) { - lychee.sortingAlbums = "ORDER BY " + params["typeAlbums"] + " " + params["orderAlbums"]; - lychee.sortingPhotos = "ORDER BY " + params["typePhotos"] + " " + params["orderPhotos"]; - albums.refresh(); - loadingBar.show("success", lychee.locale["SETTINGS_SUCCESS_SORT"]); - } else lychee.error(null, params, data); - }); -}; - -settings.changeDropboxKey = function (params) { - // if params.key == "" key is cleared - api.post("Settings::setDropboxKey", params, function (data) { - if (data === true) { - lychee.dropboxKey = params.key; - // if (callback) lychee.loadDropbox(callback) - loadingBar.show("success", lychee.locale["SETTINGS_SUCCESS_DROPBOX"]); - } else lychee.error(null, params, data); - }); -}; - -settings.changeLang = function (params) { - api.post("Settings::setLang", params, function (data) { - if (data === true) { - loadingBar.show("success", lychee.locale["SETTINGS_SUCCESS_LANG"]); - lychee.init(); - } else lychee.error(null, params, data); - }); -}; - -settings.setDefaultLicense = function (params) { - api.post("Settings::setDefaultLicense", params, function (data) { - if (data === true) { - lychee.default_license = params.license; - loadingBar.show("success", lychee.locale["SETTINGS_SUCCESS_LICENSE"]); - } else lychee.error(null, params, data); - }); -}; - -settings.setLayout = function (params) { - api.post("Settings::setLayout", params, function (data) { - if (data === true) { - lychee.layout = params.layout; - loadingBar.show("success", lychee.locale["SETTINGS_SUCCESS_LAYOUT"]); - } else lychee.error(null, params, data); - }); -}; - -settings.changePublicSearch = function () { - var params = {}; - if ($("#PublicSearch:checked").length === 1) { - params.public_search = "1"; - } else { - params.public_search = "0"; - } - api.post("Settings::setPublicSearch", params, function (data) { - if (data === true) { - loadingBar.show("success", lychee.locale["SETTINGS_SUCCESS_PUBLIC_SEARCH"]); - lychee.public_search = params.public_search === "1"; - } else lychee.error(null, params, data); - }); -}; - -settings.setOverlayType = function () { - // validate the input - var params = {}; - var check = $("#ImageOverlay:checked") ? true : false; - var type = $("#ImgOverlayType").val(); - if (check && type === "exif") { - params.image_overlay_type = "exif"; - } else if (check && type === "desc") { - params.image_overlay_type = "desc"; - } else if (check && type === "date") { - params.image_overlay_type = "date"; - } else if (check && type === "none") { - params.image_overlay_type = "none"; - } else { - params.image_overlay_type = "exif"; - console.log("Error - default used"); - } - - api.post("Settings::setOverlayType", params, function (data) { - if (data === true) { - loadingBar.show("success", lychee.locale["SETTINGS_SUCCESS_IMAGE_OVERLAY"]); - lychee.image_overlay_type = params.image_overlay_type; - lychee.image_overlay_type_default = params.image_overlay_type; - } else lychee.error(null, params, data); - }); -}; - -settings.changeMapDisplay = function () { - var params = {}; - if ($("#MapDisplay:checked").length === 1) { - params.map_display = "1"; - } else { - params.map_display = "0"; - } - api.post("Settings::setMapDisplay", params, function (data) { - if (data === true) { - loadingBar.show("success", lychee.locale["SETTINGS_SUCCESS_MAP_DISPLAY"]); - lychee.map_display = params.map_display === "1"; - } else lychee.error(null, params, data); - }); - // Map functionality is disabled - // -> map for public albums also needs to be disabled - if (lychee.map_display_public === true) { - $("#MapDisplayPublic").click(); - } -}; - -settings.changeMapDisplayPublic = function () { - var params = {}; - if ($("#MapDisplayPublic:checked").length === 1) { - params.map_display_public = "1"; - - // If public map functionality is enabled, but map in general is disabled - // General map functionality needs to be enabled - if (lychee.map_display === false) { - $("#MapDisplay").click(); - } - } else { - params.map_display_public = "0"; - } - api.post("Settings::setMapDisplayPublic", params, function (data) { - if (data === true) { - loadingBar.show("success", lychee.locale["SETTINGS_SUCCESS_MAP_DISPLAY_PUBLIC"]); - lychee.map_display_public = params.map_display_public === "1"; - } else lychee.error(null, params, data); - }); -}; - -settings.setMapProvider = function () { - // validate the input - var params = {}; - params.map_provider = $("#MapProvider").val(); - - api.post("Settings::setMapProvider", params, function (data) { - if (data === true) { - loadingBar.show("success", lychee.locale["SETTINGS_SUCCESS_MAP_PROVIDER"]); - lychee.map_provider = params.map_provider; - } else lychee.error(null, params, data); - }); -}; - -settings.changeMapIncludeSubalbums = function () { - var params = {}; - if ($("#MapIncludeSubalbums:checked").length === 1) { - params.map_include_subalbums = "1"; - } else { - params.map_include_subalbums = "0"; - } - api.post("Settings::setMapIncludeSubalbums", params, function (data) { - if (data === true) { - loadingBar.show("success", lychee.locale["SETTINGS_SUCCESS_MAP_DISPLAY"]); - lychee.map_include_subalbums = params.map_include_subalbums === "1"; - } else lychee.error(null, params, data); - }); -}; - -settings.changeLocationDecoding = function () { - var params = {}; - if ($("#LocationDecoding:checked").length === 1) { - params.location_decoding = "1"; - } else { - params.location_decoding = "0"; - } - api.post("Settings::setLocationDecoding", params, function (data) { - if (data === true) { - loadingBar.show("success", lychee.locale["SETTINGS_SUCCESS_MAP_DISPLAY"]); - lychee.location_decoding = params.location_decoding === "1"; - } else lychee.error(null, params, data); - }); -}; - -settings.changeNSFWVisible = function () { - var params = {}; - if ($("#NSFWVisible:checked").length === 1) { - params.nsfw_visible = "1"; - } else { - params.nsfw_visible = "0"; - } - api.post("Settings::setNSFWVisible", params, function (data) { - if (data === true) { - loadingBar.show("success", lychee.locale["SETTINGS_SUCCESS_NSFW_VISIBLE"]); - lychee.nsfw_visible = params.nsfw_visible === "1"; - lychee.nsfw_visible_saved = lychee.nsfw_visible; - } else { - lychee.error(null, params, data); - } - }); -}; - -//TODO : later -// lychee.nsfw_blur = (data.config.nsfw_blur && data.config.nsfw_blur === '1') || false; -// lychee.nsfw_warning = (data.config.nsfw_warning && data.config.nsfw_warning === '1') || false; -// lychee.nsfw_warning_text = data.config.nsfw_warning_text || 'Sensitive content

This album contains sensitive content which some people may find offensive or disturbing.

'; - -settings.changeLocationShow = function () { - var params = {}; - if ($("#LocationShow:checked").length === 1) { - params.location_show = "1"; - } else { - params.location_show = "0"; - // Don't show location - // -> location for public albums also needs to be disabled - if (lychee.location_show_public === true) { - $("#LocationShowPublic").click(); - } - } - api.post("Settings::setLocationShow", params, function (data) { - if (data === true) { - loadingBar.show("success", lychee.locale["SETTINGS_SUCCESS_MAP_DISPLAY"]); - lychee.location_show = params.location_show === "1"; - } else lychee.error(null, params, data); - }); -}; - -settings.changeLocationShowPublic = function () { - var params = {}; - if ($("#LocationShowPublic:checked").length === 1) { - params.location_show_public = "1"; - // If public map functionality is enabled, but map in general is disabled - // General map functionality needs to be enabled - if (lychee.location_show === false) { - $("#LocationShow").click(); - } - } else { - params.location_show_public = "0"; - } - api.post("Settings::setLocationShowPublic", params, function (data) { - if (data === true) { - loadingBar.show("success", lychee.locale["SETTINGS_SUCCESS_MAP_DISPLAY"]); - lychee.location_show_public = params.location_show_public === "1"; - } else lychee.error(null, params, data); - }); -}; - -settings.changeNewPhotosNotification = function () { - var params = {}; - if ($("#NewPhotosNotification:checked").length === 1) { - params.new_photos_notification = "1"; - } else { - params.new_photos_notification = "0"; - } - api.post("Settings::setNewPhotosNotification", params, function (data) { - if (data === true) { - loadingBar.show("success", lychee.locale["SETTINGS_SUCCESS_NEW_PHOTOS_NOTIFICATION"]); - lychee.new_photos_notification = params.new_photos_notification === "1"; - } else { - lychee.error(null, params, data); - } - }); -}; - -settings.changeCSS = function () { - var params = {}; - params.css = $("#css").val(); - - api.post("Settings::setCSS", params, function (data) { - if (data === true) { - lychee.css = params.css; - loadingBar.show("success", lychee.locale["SETTINGS_SUCCESS_CSS"]); - } else lychee.error(null, params, data); - }); -}; - -settings.save = function (params) { - var exitview = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; - - api.post("Settings::saveAll", params, function (data) { - if (data === true) { - loadingBar.show("success", lychee.locale["SETTINGS_SUCCESS_UPDATE"]); - view.full_settings.init(); - // re-read settings - lychee.init(exitview); - } else lychee.error("Check the Logs", params, data); - }); -}; - -settings.save_enter = function (e) { - if (e.which === 13) { - // show confirmation box - $(":focus").blur(); - - var action = {}; - var cancel = {}; - - action.title = lychee.locale["ENTER"]; - action.msg = lychee.html(_templateObject69, lychee.locale["SAVE_RISK"]); - - cancel.title = lychee.locale["CANCEL"]; - - action.fn = function () { - settings.save(settings.getValues("#fullSettings"), false); - basicModal.close(); - }; - - basicModal.show({ - body: action.msg, - buttons: { - action: { - title: action.title, - fn: action.fn, - class: "red" - }, - cancel: { - title: cancel.title, - fn: basicModal.close - } - } - }); - } -}; - -var sharing = { - json: null -}; - -sharing.add = function () { - var params = { - albumIDs: "", - UserIDs: "" - }; - - $("#albums_list_to option").each(function () { - if (params.albumIDs !== "") params.albumIDs += ","; - params.albumIDs += this.value; - }); - - $("#user_list_to option").each(function () { - if (params.UserIDs !== "") params.UserIDs += ","; - params.UserIDs += this.value; - }); - - if (params.albumIDs === "") { - loadingBar.show("error", "Select an album to share!"); - return false; - } - if (params.UserIDs === "") { - loadingBar.show("error", "Select a user to share with!"); - return false; - } - - api.post("Sharing::Add", params, function (data) { - if (data !== true) { - loadingBar.show("error", data.description); - lychee.error(null, params, data); - } else { - loadingBar.show("success", "Sharing updated!"); - sharing.list(); // reload user list - } - }); -}; - -sharing.delete = function () { - var params = { - ShareIDs: "" - }; - - $('input[name="remove_id"]:checked').each(function () { - if (params.ShareIDs !== "") params.ShareIDs += ","; - params.ShareIDs += this.value; - }); - - if (params.ShareIDs === "") { - loadingBar.show("error", "Select a sharing to remove!"); - return false; - } - api.post("Sharing::Delete", params, function (data) { - if (data !== true) { - loadingBar.show("error", data.description); - lychee.error(null, params, data); - } else { - loadingBar.show("success", "Sharing removed!"); - sharing.list(); // reload user list - } - }); -}; - -sharing.list = function () { - api.post("Sharing::List", {}, function (data) { - sharing.json = data; - view.sharing.init(); - }); -}; - -/** - * @description This module takes care of the sidebar. - */ - -var _sidebar = { - _dom: $(".sidebar"), - types: { - DEFAULT: 0, - TAGS: 1 - }, - createStructure: {} -}; - -_sidebar.dom = function (selector) { - if (selector == null || selector === "") return _sidebar._dom; - - return _sidebar._dom.find(selector); -}; - -_sidebar.bind = function () { - // This function should be called after building and appending - // the sidebars content to the DOM. - // This function can be called multiple times, therefore - // event handlers should be removed before binding a new one. - - // Event Name - var eventName = lychee.getEventName(); - - _sidebar.dom("#edit_title").off(eventName).on(eventName, function () { - if (visible.photo()) _photo.setTitle([_photo.getID()]);else if (visible.album()) album.setTitle([album.getID()]); - }); - - _sidebar.dom("#edit_description").off(eventName).on(eventName, function () { - if (visible.photo()) _photo.setDescription(_photo.getID());else if (visible.album()) album.setDescription(album.getID()); - }); - - _sidebar.dom("#edit_showtags").off(eventName).on(eventName, function () { - album.setShowTags(album.getID()); - }); - - _sidebar.dom("#edit_tags").off(eventName).on(eventName, function () { - _photo.editTags([_photo.getID()]); - }); - - _sidebar.dom("#tags .tag").off(eventName).on(eventName, function () { - _sidebar.triggerSearch($(this).text()); - }); - - _sidebar.dom("#tags .tag span").off(eventName).on(eventName, function () { - _photo.deleteTag(_photo.getID(), $(this).data("index")); - }); - - _sidebar.dom("#edit_license").off(eventName).on(eventName, function () { - if (visible.photo()) _photo.setLicense(_photo.getID());else if (visible.album()) album.setLicense(album.getID()); - }); - - _sidebar.dom("#edit_sorting").off(eventName).on(eventName, function () { - album.setSorting(album.getID()); - }); - - _sidebar.dom(".attr_location").off(eventName).on(eventName, function () { - _sidebar.triggerSearch($(this).text()); - }); - - return true; -}; - -_sidebar.triggerSearch = function (search_string) { - // If public search is diabled -> do nothing - if (lychee.publicMode === true && !lychee.public_search) { - // Do not display an error -> just do nothing to not confuse the user - return; - } - - search.hash = null; - // We're either logged in or public search is allowed - lychee.goto("search/" + encodeURIComponent(search_string)); -}; - -_sidebar.toggle = function () { - if (visible.sidebar() || visible.sidebarbutton()) { - header.dom(".button--info").toggleClass("active"); - lychee.content.toggleClass("content--sidebar"); - lychee.imageview.toggleClass("image--sidebar"); - if (typeof view !== "undefined") view.album.content.justify(); - _sidebar.dom().toggleClass("active"); - _photo.updateSizeLivePhotoDuringAnimation(); - - return true; - } - - return false; -}; - -_sidebar.setSelectable = function () { - var selectable = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; - - // Attributes/Values inside the sidebar are selectable by default. - // Selection needs to be deactivated to prevent an unwanted selection - // while using multiselect. - - if (selectable === true) _sidebar.dom().removeClass("notSelectable");else _sidebar.dom().addClass("notSelectable"); -}; - -_sidebar.changeAttr = function (attr) { - var value = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "-"; - var dangerouslySetInnerHTML = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; - - if (attr == null || attr === "") return false; - - // Set a default for the value - if (value == null || value === "") value = "-"; - - // Escape value - if (dangerouslySetInnerHTML === false) value = lychee.escapeHTML(value); - - // Set new value - _sidebar.dom(".attr_" + attr).html(value); - - return true; -}; - -_sidebar.hideAttr = function (attr) { - _sidebar.dom(".attr_" + attr).closest("tr").hide(); -}; - -_sidebar.secondsToHMS = function (d) { - d = Number(d); - var h = Math.floor(d / 3600); - var m = Math.floor(d % 3600 / 60); - var s = Math.floor(d % 60); - - return (h > 0 ? h.toString() + "h" : "") + (m > 0 ? m.toString() + "m" : "") + (s > 0 || h == 0 && m == 0 ? s.toString() + "s" : ""); -}; - -_sidebar.createStructure.photo = function (data) { - if (data == null || data === "") return false; - - var editable = typeof album !== "undefined" ? album.isUploadable() : false; - var exifHash = data.taken_at + data.make + data.model + data.shutter + data.aperture + data.focal + data.iso; - var locationHash = data.longitude + data.latitude + data.altitude; - var structure = {}; - var _public = ""; - var isVideo = data.type && data.type.indexOf("video") > -1; - var license = void 0; - - // Set the license string for a photo - switch (data.license) { - // if the photo doesn't have a license - case "none": - license = ""; - break; - // Localize All Rights Reserved - case "reserved": - license = lychee.locale["PHOTO_RESERVED"]; - break; - // Display anything else that's set - default: - license = data.license; - break; - } - - // Set value for public - switch (data.public) { - case "0": - _public = lychee.locale["PHOTO_SHR_NO"]; - break; - case "1": - _public = lychee.locale["PHOTO_SHR_PHT"]; - break; - case "2": - _public = lychee.locale["PHOTO_SHR_ALB"]; - break; - default: - _public = "-"; - break; - } - - structure.basics = { - title: lychee.locale["PHOTO_BASICS"], - type: _sidebar.types.DEFAULT, - rows: [{ title: lychee.locale["PHOTO_TITLE"], kind: "title", value: data.title, editable: editable }, { title: lychee.locale["PHOTO_UPLOADED"], kind: "uploaded", value: lychee.locale.printDateTime(data.created_at) }, { title: lychee.locale["PHOTO_DESCRIPTION"], kind: "description", value: data.description, editable: editable }] - }; - - structure.image = { - title: lychee.locale[isVideo ? "PHOTO_VIDEO" : "PHOTO_IMAGE"], - type: _sidebar.types.DEFAULT, - rows: [{ title: lychee.locale["PHOTO_SIZE"], kind: "size", value: lychee.locale.printFilesizeLocalized(data.filesize) }, { title: lychee.locale["PHOTO_FORMAT"], kind: "type", value: data.type }, { title: lychee.locale["PHOTO_RESOLUTION"], kind: "resolution", value: data.width + " x " + data.height }] - }; - - if (isVideo) { - if (data.width === 0 || data.height === 0) { - // Remove the "Resolution" line if we don't have the data. - structure.image.rows.splice(-1, 1); - } - - // We overload the database, storing duration (in full seconds) in - // "aperture" and frame rate (floating point with three digits after - // the decimal point) in "focal". - if (data.aperture != "") { - structure.image.rows.push({ title: lychee.locale["PHOTO_DURATION"], kind: "duration", value: _sidebar.secondsToHMS(data.aperture) }); - } - if (data.focal != "") { - structure.image.rows.push({ title: lychee.locale["PHOTO_FPS"], kind: "fps", value: data.focal + " fps" }); - } - } - - // Always create tags section - behaviour for editing - //tags handled when contructing the html code for tags - - structure.tags = { - title: lychee.locale["PHOTO_TAGS"], - type: _sidebar.types.TAGS, - value: build.tags(data.tags), - editable: editable - }; - - // Only create EXIF section when EXIF data available - if (exifHash !== "") { - structure.exif = { - title: lychee.locale["PHOTO_CAMERA"], - type: _sidebar.types.DEFAULT, - rows: isVideo ? [{ title: lychee.locale["PHOTO_CAPTURED"], kind: "takedate", value: lychee.locale.printDateTime(data.taken_at) }, { title: lychee.locale["PHOTO_MAKE"], kind: "make", value: data.make }, { title: lychee.locale["PHOTO_TYPE"], kind: "model", value: data.model }] : [{ title: lychee.locale["PHOTO_CAPTURED"], kind: "takedate", value: lychee.locale.printDateTime(data.taken_at) }, { title: lychee.locale["PHOTO_MAKE"], kind: "make", value: data.make }, { title: lychee.locale["PHOTO_TYPE"], kind: "model", value: data.model }, { title: lychee.locale["PHOTO_LENS"], kind: "lens", value: data.lens }, { title: lychee.locale["PHOTO_SHUTTER"], kind: "shutter", value: data.shutter }, { title: lychee.locale["PHOTO_APERTURE"], kind: "aperture", value: data.aperture }, { title: lychee.locale["PHOTO_FOCAL"], kind: "focal", value: data.focal }, { title: lychee.locale["PHOTO_ISO"], kind: "iso", value: data.iso }] - }; - } else { - structure.exif = {}; - } - - structure.sharing = { - title: lychee.locale["PHOTO_SHARING"], - type: _sidebar.types.DEFAULT, - rows: [{ title: lychee.locale["PHOTO_SHR_PLUBLIC"], kind: "public", value: _public }] - }; - - structure.license = { - title: lychee.locale["PHOTO_REUSE"], - type: _sidebar.types.DEFAULT, - rows: [{ title: lychee.locale["PHOTO_LICENSE"], kind: "license", value: license, editable: editable }] - }; - - if (locationHash !== "" && locationHash !== 0) { - structure.location = { - title: lychee.locale["PHOTO_LOCATION"], - type: _sidebar.types.DEFAULT, - rows: [{ - title: lychee.locale["PHOTO_LATITUDE"], - kind: "latitude", - value: data.latitude ? DecimalToDegreeMinutesSeconds(data.latitude, true) : "" - }, { - title: lychee.locale["PHOTO_LONGITUDE"], - kind: "longitude", - value: data.longitude ? DecimalToDegreeMinutesSeconds(data.longitude, false) : "" - }, - // No point in displaying sub-mm precision; 10cm is more than enough. - { - title: lychee.locale["PHOTO_ALTITUDE"], - kind: "altitude", - value: data.altitude ? (Math.round(parseFloat(data.altitude) * 10) / 10).toString() + "m" : "" - }, { title: lychee.locale["PHOTO_LOCATION"], kind: "location", value: data.location ? data.location : "" }] - }; - if (data.imgDirection) { - // No point in display sub-degree precision. - structure.location.rows.push({ - title: lychee.locale["PHOTO_IMGDIRECTION"], - kind: "imgDirection", - value: Math.round(data.imgDirection).toString() + "°" - }); - } - } else { - structure.location = {}; - } - - // Construct all parts of the structure - var structure_ret = [structure.basics, structure.image, structure.tags, structure.exif, structure.location, structure.license]; - - if (!lychee.publicMode) { - structure_ret.push(structure.sharing); - } - - return structure_ret; -}; - -_sidebar.createStructure.album = function (album) { - var data = album.json; - - if (data == null || data === "") return false; - - var editable = album.isUploadable(); - var structure = {}; - var _public = ""; - var hidden = ""; - var downloadable = ""; - var share_button_visible = ""; - var password = ""; - var license = ""; - var sorting = ""; - - // Set value for public - switch (data.public) { - case "0": - _public = lychee.locale["ALBUM_SHR_NO"]; - break; - case "1": - _public = lychee.locale["ALBUM_SHR_YES"]; - break; - default: - _public = "-"; - break; - } - - // Set value for hidden - switch (data.visible) { - case "0": - hidden = lychee.locale["ALBUM_SHR_YES"]; - break; - case "1": - hidden = lychee.locale["ALBUM_SHR_NO"]; - break; - default: - hidden = "-"; - break; - } - - // Set value for downloadable - switch (data.downloadable) { - case "0": - downloadable = lychee.locale["ALBUM_SHR_NO"]; - break; - case "1": - downloadable = lychee.locale["ALBUM_SHR_YES"]; - break; - default: - downloadable = "-"; - break; - } - - // Set value for share_button_visible - switch (data.share_button_visible) { - case "0": - share_button_visible = lychee.locale["ALBUM_SHR_NO"]; - break; - case "1": - share_button_visible = lychee.locale["ALBUM_SHR_YES"]; - break; - default: - share_button_visible = "-"; - break; - } - - // Set value for password - switch (data.password) { - case "0": - password = lychee.locale["ALBUM_SHR_NO"]; - break; - case "1": - password = lychee.locale["ALBUM_SHR_YES"]; - break; - default: - password = "-"; - break; - } - - // Set license string - switch (data.license) { - case "none": - license = ""; // consistency - break; - case "reserved": - license = lychee.locale["ALBUM_RESERVED"]; - break; - default: - license = data.license; - break; - } - - if (data.sorting_col === "") { - sorting = lychee.locale["DEFAULT"]; - } else { - sorting = data.sorting_col + " " + data.sorting_order; - } - - structure.basics = { - title: lychee.locale["ALBUM_BASICS"], - type: _sidebar.types.DEFAULT, - rows: [{ title: lychee.locale["ALBUM_TITLE"], kind: "title", value: data.title, editable: editable }, { title: lychee.locale["ALBUM_DESCRIPTION"], kind: "description", value: data.description, editable: editable }] - }; - - if (album.isTagAlbum()) { - structure.basics.rows.push({ title: lychee.locale["ALBUM_SHOW_TAGS"], kind: "showtags", value: data.show_tags, editable: editable }); - } - - var videoCount = 0; - $.each(data.photos, function () { - if (this.type && this.type.indexOf("video") > -1) { - videoCount++; - } - }); - structure.album = { - title: lychee.locale["ALBUM_ALBUM"], - type: _sidebar.types.DEFAULT, - rows: [{ title: lychee.locale["ALBUM_CREATED"], kind: "created", value: lychee.locale.printDateTime(data.created_at) }] - }; - if (data.albums && data.albums.length > 0) { - structure.album.rows.push({ title: lychee.locale["ALBUM_SUBALBUMS"], kind: "subalbums", value: data.albums.length }); - } - if (data.photos) { - if (data.photos.length - videoCount > 0) { - structure.album.rows.push({ title: lychee.locale["ALBUM_IMAGES"], kind: "images", value: data.photos.length - videoCount }); - } - } - if (videoCount > 0) { - structure.album.rows.push({ title: lychee.locale["ALBUM_VIDEOS"], kind: "videos", value: videoCount }); - } - - if (data.photos) { - structure.album.rows.push({ title: lychee.locale["ALBUM_ORDERING"], kind: "sorting", value: sorting, editable: editable }); - } - - structure.share = { - title: lychee.locale["ALBUM_SHARING"], - type: _sidebar.types.DEFAULT, - rows: [{ title: lychee.locale["ALBUM_PUBLIC"], kind: "public", value: _public }, { title: lychee.locale["ALBUM_HIDDEN"], kind: "hidden", value: hidden }, { title: lychee.locale["ALBUM_DOWNLOADABLE"], kind: "downloadable", value: downloadable }, { title: lychee.locale["ALBUM_SHARE_BUTTON_VISIBLE"], kind: "share_button_visible", value: share_button_visible }, { title: lychee.locale["ALBUM_PASSWORD"], kind: "password", value: password }] - }; - - if (data.owner != null) { - structure.share.rows.push({ title: lychee.locale["ALBUM_OWNER"], kind: "owner", value: data.owner }); - } - - structure.license = { - title: lychee.locale["ALBUM_REUSE"], - type: _sidebar.types.DEFAULT, - rows: [{ title: lychee.locale["ALBUM_LICENSE"], kind: "license", value: license, editable: editable }] - }; - - // Construct all parts of the structure - var structure_ret = [structure.basics, structure.album, structure.license]; - if (!lychee.publicMode) { - structure_ret.push(structure.share); - } - - return structure_ret; -}; - -_sidebar.has_location = function (structure) { - if (structure == null || structure === "" || structure === false) return false; - - var _has_location = false; - - structure.forEach(function (section) { - if (section.title == lychee.locale["PHOTO_LOCATION"]) { - _has_location = true; - } - }); - - return _has_location; -}; - -_sidebar.render = function (structure) { - if (structure == null || structure === "" || structure === false) return false; - - var html = ""; - - var renderDefault = function renderDefault(section) { - var _html = ""; - - _html += "\n\t\t\t\t \n\t\t\t\t \n\t\t\t\t "; - - if (section.title == lychee.locale["PHOTO_LOCATION"]) { - var _has_latitude = false; - var _has_longitude = false; - - section.rows.forEach(function (row, index, object) { - if (row.kind == "latitude" && row.value !== "") { - _has_latitude = true; - } - - if (row.kind == "longitude" && row.value !== "") { - _has_longitude = true; - } - - // Do not show location is not enabled - if (row.kind == "location" && (lychee.publicMode === true && !lychee.location_show_public || !lychee.location_show)) { - object.splice(index, 1); - } else { - // Explode location string into an array to keep street, city etc separate - if (!(row.value === "" || row.value == null)) { - section.rows[index].value = row.value.split(",").map(function (item) { - return item.trim(); - }); - } - } - }); - - if (_has_latitude && _has_longitude && lychee.map_display) { - _html += "\n\t\t\t\t\t\t
\n\t\t\t\t\t\t "; - } - } - - section.rows.forEach(function (row) { - var value = row.value; - - // show only Exif rows which have a value or if its editable - if (!(value === "" || value == null) || row.editable === true) { - // Wrap span-element around value for easier selecting on change - if (Array.isArray(row.value)) { - value = ""; - row.value.forEach(function (v) { - if (v === "" || v == null) { - return; - } - // Add separator if needed - if (value !== "") { - value += lychee.html(_templateObject70, row.kind); - } - value += lychee.html(_templateObject71, row.kind, v); - }); - } else { - value = lychee.html(_templateObject72, row.kind, value); - } - - // Add edit-icon to the value when editable - if (row.editable === true) value += " " + build.editIcon("edit_" + row.kind); - - _html += lychee.html(_templateObject73, row.title, value); - } - }); - - _html += "\n\t\t\t\t
\n\t\t\t\t "; - - return _html; - }; - - var renderTags = function renderTags(section) { - var _html = ""; - var editable = ""; - - // Add edit-icon to the value when editable - if (section.editable === true) editable = build.editIcon("edit_tags"); - - _html += lychee.html(_templateObject74, section.title, section.title.toLowerCase(), section.value, editable); - - return _html; - }; - - structure.forEach(function (section) { - if (section.type === _sidebar.types.DEFAULT) html += renderDefault(section);else if (section.type === _sidebar.types.TAGS) html += renderTags(section); - }); - - return html; -}; - -function DecimalToDegreeMinutesSeconds(decimal, type) { - var degrees = 0; - var minutes = 0; - var seconds = 0; - var direction = void 0; - - //decimal must be integer or float no larger than 180; - //type must be Boolean - if (Math.abs(decimal) > 180 || typeof type !== "boolean") { - return false; - } - - //inputs OK, proceed - //type is latitude when true, longitude when false - - //set direction; north assumed - if (type && decimal < 0) { - direction = "S"; - } else if (!type && decimal < 0) { - direction = "W"; - } else if (!type) { - direction = "E"; - } else { - direction = "N"; - } - - //get absolute value of decimal - var d = Math.abs(decimal); - - //get degrees - degrees = Math.floor(d); - - //get seconds - seconds = (d - degrees) * 3600; - - //get minutes - minutes = Math.floor(seconds / 60); - - //reset seconds - seconds = Math.floor(seconds - minutes * 60); - - return degrees + "° " + minutes + "' " + seconds + '" ' + direction; -} - -/** - * @description Swipes and moves an object. - */ - -var swipe = { - obj: null, - offsetX: 0, - offsetY: 0, - preventNextHeaderToggle: false -}; - -swipe.start = function (obj) { - if (obj) swipe.obj = obj; - return true; -}; - -swipe.move = function (e) { - if (swipe.obj === null) { - return false; - } - - if (Math.abs(e.x) > Math.abs(e.y)) { - swipe.offsetX = -1 * e.x; - swipe.offsetY = 0.0; - } else { - swipe.offsetX = 0.0; - swipe.offsetY = +1 * e.y; - } - - var value = "translate(" + swipe.offsetX + "px, " + swipe.offsetY + "px)"; - swipe.obj.css({ - WebkitTransform: value, - MozTransform: value, - transform: value - }); - return; -}; - -swipe.stop = function (e, left, right) { - // Only execute once - if (swipe.obj == null) { - return false; - } - - if (e.y <= -lychee.swipe_tolerance_y) { - lychee.goto(album.getID()); - } else if (e.y >= lychee.swipe_tolerance_y) { - lychee.goto(album.getID()); - } else if (e.x <= -lychee.swipe_tolerance_x) { - left(true); - - // 'touchend' will be called after 'swipeEnd' - // in case of moving to next image, we want to skip - // the toggling of the header - swipe.preventNextHeaderToggle = true; - } else if (e.x >= lychee.swipe_tolerance_x) { - right(true); - - // 'touchend' will be called after 'swipeEnd' - // in case of moving to next image, we want to skip - // the toggling of the header - swipe.preventNextHeaderToggle = true; - } else { - var value = "translate(0px, 0px)"; - swipe.obj.css({ - WebkitTransform: value, - MozTransform: value, - transform: value - }); - } - - swipe.obj = null; - swipe.offsetX = 0; - swipe.offsetY = 0; - - return; -}; - -/** - * @description Helper class to manage tabindex - */ - -var tabindex = { - offset_for_header: 100, - next_tab_index: 100 -}; - -tabindex.saveSettings = function (elem) { - if (!lychee.enable_tabindex) return; - - // Todo: Make shorter notation - // Get all elements which have a tabindex - var tmp = $(elem).find("[tabindex]"); - - // iterate over all elements and set tabindex to stored value (i.e. make is not focussable) - tmp.each(function (i, e) { - // TODO: shorter notation - a = $(e).attr("tabindex"); - $(this).data("tabindex-saved", a); - }); -}; - -tabindex.restoreSettings = function (elem) { - if (!lychee.enable_tabindex) return; - - // Todo: Make shorter noation - // Get all elements which have a tabindex - var tmp = $(elem).find("[tabindex]"); - - // iterate over all elements and set tabindex to stored value (i.e. make is not focussable) - tmp.each(function (i, e) { - // TODO: shorter notation - a = $(e).data("tabindex-saved"); - $(e).attr("tabindex", a); - }); -}; - -tabindex.makeUnfocusable = function (elem) { - var saveFocusElement = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; - - if (!lychee.enable_tabindex) return; - - // Todo: Make shorter noation - // Get all elements which have a tabindex - var tmp = $(elem).find("[tabindex]"); - - // iterate over all elements and set tabindex to -1 (i.e. make is not focussable) - tmp.each(function (i, e) { - $(e).attr("tabindex", "-1"); - // Save which element had focus before we make it unfocusable - if (saveFocusElement && $(e).is(":focus")) { - $(e).data("tabindex-focus", true); - // Remove focus - $(e).blur(); - } - }); - - // Disable input fields - $(elem).find("input").attr("disabled", "disabled"); -}; - -tabindex.makeFocusable = function (elem) { - var restoreFocusElement = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; - - if (!lychee.enable_tabindex) return; - - // Todo: Make shorter noation - // Get all elements which have a tabindex - var tmp = $(elem).find("[data-tabindex]"); - - // iterate over all elements and set tabindex to stored value (i.e. make is not focussable) - tmp.each(function (i, e) { - $(e).attr("tabindex", $(e).data("tabindex")); - // restore focus elemente if wanted - if (restoreFocusElement) { - if ($(e).data("tabindex-focus") && lychee.active_focus_on_page_load) { - $(e).focus(); - $(e).removeData("tabindex-focus"); - } - } - }); - - // Enable input fields - $(elem).find("input").removeAttr("disabled"); -}; - -tabindex.get_next_tab_index = function () { - tabindex.next_tab_index = tabindex.next_tab_index + 1; - - return tabindex.next_tab_index - 1; -}; - -tabindex.reset = function () { - tabindex.next_tab_index = tabindex.offset_for_header; -}; - -var u2f = { - json: null -}; - -u2f.is_available = function () { - if (!window.isSecureContext && window.location.hostname !== "localhost" && window.location.hostname !== "127.0.0.1") { - var _msg6 = lychee.html(_templateObject75, lychee.locale["U2F_NOT_SECURE"]); - - basicModal.show({ - body: _msg6, - buttons: { - cancel: { - title: lychee.locale["CLOSE"], - fn: basicModal.close - } - } - }); - - return false; - } - return true; -}; - -u2f.login = function () { - if (!u2f.is_available()) { - return; - } - - new Larapass({ - login: "/api/webauthn::login", - loginOptions: "/api/webauthn::login/gen" - }).login({ - user_id: 0 // for now it is only available to Admin user via a secret key shortcut. - }).then(function (data) { - loadingBar.show("success", lychee.locale["U2F_AUTHENTIFICATION_SUCCESS"]); - window.location.reload(); - }).catch(function (error) { - return loadingBar.show("error", "Something went wrong!"); - }); -}; - -u2f.register = function () { - if (!u2f.is_available()) { - return; - } - - var larapass = new Larapass({ - register: "/api/webauthn::register", - registerOptions: "/api/webauthn::register/gen" - }); - if (Larapass.supportsWebAuthn()) { - larapass.register().then(function (response) { - loadingBar.show("success", lychee.locale["U2F_REGISTRATION_SUCCESS"]); - u2f.list(); // reload credential list - }).catch(function (response) { - return loadingBar.show("error", "Something went wrong!"); - }); - } else { - loadingBar.show("error", lychee.locale["U2F_NOT_SUPPORTED"]); - } -}; - -u2f.delete = function (params) { - api.post("webauthn::delete", params, function (data) { - console.log(data); - if (!data) { - loadingBar.show("error", data.description); - lychee.error(null, params, data); - } else { - loadingBar.show("success", lychee.locale["U2F_CREDENTIALS_DELETED"]); - u2f.list(); // reload credential list - } - }); -}; - -u2f.list = function () { - api.post("webauthn::list", {}, function (data) { - u2f.json = data; - view.u2f.init(); - }); -}; - -/** - * @description Takes care of every action an album can handle and execute. - */ - -var upload = {}; - -var choiceDeleteSelector = '.basicModal .choice input[name="delete"]'; -var choiceSymlinkSelector = '.basicModal .choice input[name="symlinks"]'; -var choiceDuplicateSelector = '.basicModal .choice input[name="skipduplicates"]'; -var choiceResyncSelector = '.basicModal .choice input[name="resyncmetadata"]'; -var actionSelector = ".basicModal #basicModal__action"; -var cancelSelector = ".basicModal #basicModal__cancel"; -var lastRowSelector = ".basicModal .rows .row:last-child"; -var prelastRowSelector = ".basicModal .rows .row:nth-last-child(2)"; - -var nRowStatusSelector = function nRowStatusSelector(row) { - return ".basicModal .rows .row:nth-child(" + row + ") .status"; -}; - -var showCloseButton = function showCloseButton() { - $(actionSelector).show(); - // re-activate cancel button to close modal panel if needed - $(cancelSelector).removeClass("basicModal__button--active").hide(); -}; - -upload.show = function (title, files, run_callback) { - var cancel_callback = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null; - - basicModal.show({ - body: build.uploadModal(title, files), - buttons: { - action: { - title: lychee.locale["CLOSE"], - class: "hidden", - fn: function fn() { - if ($(actionSelector).is(":visible")) basicModal.close(); - } - }, - cancel: { - title: lychee.locale["CANCEL"], - class: "red hidden", - fn: function fn() { - // close modal if close button is displayed - if ($(actionSelector).is(":visible")) basicModal.close(); - if (cancel_callback) { - $(cancelSelector).addClass("busy"); - cancel_callback(); - } - } - } - }, - callback: run_callback - }); -}; - -upload.notify = function (title, text) { - if (text == null || text === "") text = lychee.locale["UPLOAD_MANAGE_NEW_PHOTOS"]; - - if (!window.webkitNotifications) return false; - - if (window.webkitNotifications.checkPermission() !== 0) window.webkitNotifications.requestPermission(); - - if (window.webkitNotifications.checkPermission() === 0 && title) { - var popup = window.webkitNotifications.createNotification("", title, text); - popup.show(); - } -}; - -upload.start = { - local: function local(files) { - var albumID = album.getID(); - var error = false; - var warning = false; - var processing_count = 0; - var next_upload = 0; - var currently_uploading = false; - var cancelUpload = false; - - var process = function process(file_num) { - var formData = new FormData(); - var xhr = new XMLHttpRequest(); - var pre_progress = 0; - var progress = 0; - - if (file_num === 0) { - $(cancelSelector).show(); - } - - var finish = function finish() { - window.onbeforeunload = null; - - $("#upload_files").val(""); - - if (error === false && warning === false) { - // Success - basicModal.close(); - upload.notify(lychee.locale["UPLOAD_COMPLETE"]); - } else if (error === false && warning === true) { - // Warning - showCloseButton(); - upload.notify(lychee.locale["UPLOAD_COMPLETE"]); - } else { - // Error - showCloseButton(); - upload.notify(lychee.locale["UPLOAD_COMPLETE"], lychee.locale["UPLOAD_COMPLETE_FAILED"]); - } - - albums.refresh(); - - if (album.getID() === false) lychee.goto("unsorted");else album.load(albumID); - }; - - formData.append("function", "Photo::add"); - formData.append("albumID", albumID); - formData.append(0, files[file_num]); - - var api_url = "api/" + "Photo::add"; - - xhr.open("POST", api_url); - - xhr.onload = function () { - var data = null; - var errorText = ""; - - var isNumber = function isNumber(n) { - return !isNaN(parseFloat(n)) && isFinite(n); - }; - - data = xhr.responseText; - - if (typeof data === "string" && data.search("phpdebugbar") !== -1) { - // get rid of phpdebugbar thingy - var debug_bar_n = data.search(" 0) { - data = data.slice(0, debug_bar_n); - } - } - - try { - data = JSON.parse(data); - } catch (e) { - data = ""; - } - - // Set status - if (xhr.status === 200 && isNumber(data)) { - // Success - $(nRowStatusSelector(file_num + 1)).html(lychee.locale["UPLOAD_FINISHED"]).addClass("success"); - } else { - if (xhr.status === 413 || data.substr(0, 6) === "Error:") { - if (xhr.status === 413) { - errorText = lychee.locale["UPLOAD_ERROR_POSTSIZE"]; - } else { - errorText = data.substr(6); - if (errorText === " validation failed") { - errorText = lychee.locale["UPLOAD_ERROR_FILESIZE"]; - } else { - errorText += " " + lychee.locale["UPLOAD_ERROR_CONSOLE"]; - } - } - error = true; - - // Error Status - $(nRowStatusSelector(file_num + 1)).html(lychee.locale["UPLOAD_FAILED"]).addClass("error"); - - // Throw error - lychee.error(lychee.locale["UPLOAD_FAILED_ERROR"], xhr, data); - } else if (data.substr(0, 8) === "Warning:") { - errorText = data.substr(8); - warning = true; - - // Warning Status - $(nRowStatusSelector(file_num + 1)).html(lychee.locale["UPLOAD_SKIPPED"]).addClass("warning"); - - // Throw error - lychee.error(lychee.locale["UPLOAD_FAILED_WARNING"], xhr, data); - } else { - errorText = lychee.locale["UPLOAD_UNKNOWN"]; - error = true; - - // Error Status - $(nRowStatusSelector(file_num + 1)).html(lychee.locale["UPLOAD_FAILED"]).addClass("error"); - - // Throw error - lychee.error(lychee.locale["UPLOAD_ERROR_UNKNOWN"], xhr, data); - } - - $(".basicModal .rows .row:nth-child(" + (file_num + 1) + ") p.notice").html(errorText).show(); - } - - processing_count--; - - // Upload next file - if (!currently_uploading && !cancelUpload && (processing_count < lychee.upload_processing_limit || lychee.upload_processing_limit === 0) && next_upload < files.length) { - process(next_upload); - } - - // Finish upload when all files are finished - if (!currently_uploading && processing_count === 0) { - finish(); - } - }; - - xhr.upload.onprogress = function (e) { - if (e.lengthComputable !== true) return false; - - // Calculate progress - progress = e.loaded / e.total * 100 | 0; - - // Set progress when progress has changed - if (progress > pre_progress) { - $(nRowStatusSelector(file_num + 1)).html(progress + "%"); - pre_progress = progress; - } - - if (progress >= 100) { - // Scroll to the uploading file - var scrollPos = 0; - if (file_num + 1 > 4) scrollPos = (file_num + 1 - 4) * 40; - $(".basicModal .rows").scrollTop(scrollPos); - - // Set status to processing - $(nRowStatusSelector(file_num + 1)).html(lychee.locale["UPLOAD_PROCESSING"]); - processing_count++; - currently_uploading = false; - - // Upload next file - if (!cancelUpload && (processing_count < lychee.upload_processing_limit || lychee.upload_processing_limit === 0) && next_upload < files.length) { - process(next_upload); - } - } - }; - - currently_uploading = true; - next_upload++; - - xhr.setRequestHeader("X-XSRF-TOKEN", csrf.getCookie("XSRF-TOKEN")); - xhr.send(formData); - }; - - if (files.length <= 0) return false; - if (albumID === false || visible.albums() === true) albumID = 0; - - window.onbeforeunload = function () { - return lychee.locale["UPLOAD_IN_PROGRESS"]; - }; - - upload.show(lychee.locale["UPLOAD_UPLOADING"], files, function () { - // Upload first file - process(next_upload); - }, function () { - cancelUpload = true; - error = true; - }); - }, - - url: function url() { - var _url = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ""; - - var albumID = album.getID(); - - _url = typeof _url === "string" ? _url : ""; - - if (albumID === false) albumID = 0; - - var action = function action(data) { - var files = []; - - if (data.link && data.link.trim().length > 3) { - basicModal.close(); - - files[0] = { - name: data.link - }; - - upload.show(lychee.locale["UPLOAD_IMPORTING_URL"], files, function () { - $(".basicModal .rows .row .status").html(lychee.locale["UPLOAD_IMPORTING"]); - - var params = { - url: data.link, - albumID: albumID - }; - - api.post("Import::url", params, function (_data) { - // Same code as in import.dropbox() - - if (_data !== true) { - $(".basicModal .rows .row p.notice").html(lychee.locale["UPLOAD_IMPORT_WARN_ERR"]).show(); - - $(".basicModal .rows .row .status").html(lychee.locale["UPLOAD_FINISHED"]).addClass("warning"); - - // Show close button - $(".basicModal #basicModal__action.hidden").show(); - - // Log error - lychee.error(null, params, _data); - } else { - basicModal.close(); - } - - upload.notify(lychee.locale["UPLOAD_IMPORT_COMPLETE"]); - - albums.refresh(); - - if (album.getID() === false) lychee.goto("0");else album.load(albumID); - }); - }); - } else basicModal.error("link"); - }; - - basicModal.show({ - body: lychee.html(_templateObject76) + lychee.locale["UPLOAD_IMPORT_INSTR"] + ("

"), - buttons: { - action: { - title: lychee.locale["UPLOAD_IMPORT"], - fn: action - }, - cancel: { - title: lychee.locale["CANCEL"], - fn: basicModal.close - } - } - }); - }, - - server: function server() { - var albumID = album.getID(); - if (albumID === false) albumID = 0; - - var action = function action(data) { - if (!data.path.trim()) { - basicModal.error("path"); - return; - } - - var files = []; - - files[0] = { - name: data.path - }; - - var delete_imported = $(choiceDeleteSelector).prop("checked") ? "1" : "0"; - var import_via_symlink = $(choiceSymlinkSelector).prop("checked") ? "1" : "0"; - var skip_duplicates = $(choiceDuplicateSelector).prop("checked") ? "1" : "0"; - var resync_metadata = $(choiceResyncSelector).prop("checked") ? "1" : "0"; - var cancelUpload = false; - - upload.show(lychee.locale["UPLOAD_IMPORT_SERVER"], files, function () { - $(cancelSelector).show(); - $(".basicModal .rows .row .status").html(lychee.locale["UPLOAD_IMPORTING"]); - - var params = { - albumID: albumID, - path: data.path, - delete_imported: delete_imported, - import_via_symlink: import_via_symlink, - skip_duplicates: skip_duplicates, - resync_metadata: resync_metadata - }; - - // Variables holding state across the invocations of - // processIncremental(). - var lastReadIdx = 0; - var currentDir = data.path; - var encounteredProblems = false; - var topSkip = 0; - - // Worker function invoked from both the response progress - // callback and the completion callback. - var processIncremental = function processIncremental(jsonResponse) { - // Skip the part that we've already processed during - // the previous invocation(s). - var newResponse = jsonResponse.substring(lastReadIdx); - // Because of all the potential buffering along the way, - // we can't be sure if the last line is complete. For - // that reason, our custom protocol terminates every - // line with the newline character, including the last - // line. - var lastNewline = newResponse.lastIndexOf("\n"); - if (lastNewline === -1) { - // No valid input data to process. - return; - } - if (lastNewline !== newResponse.length - 1) { - // Last line is not newline-terminated, so it - // must be incomplete. Strip it; it will be - // handled during the next invocation. - newResponse = newResponse.substring(0, lastNewline + 1); - } - // Advance the counter past the last valid character. - lastReadIdx += newResponse.length; - newResponse.split("\n").forEach(function (resp) { - var matches = resp.match(/^Status: (.*): (\d+)$/); - if (matches !== null) { - if (matches[2] !== "100") { - if (currentDir !== matches[1]) { - // New directory. Add a new line to - // the dialog box. - currentDir = matches[1]; - $(".basicModal .rows").append(build.uploadNewFile(currentDir)); - topSkip += $(lastRowSelector).outerHeight(); - } - $(lastRowSelector + " .status").html(matches[2] + "%"); - } else { - // Final status report for this directory. - $(lastRowSelector + " .status").html(lychee.locale["UPLOAD_FINISHED"]).addClass("success"); - } - } else if ((matches = resp.match(/^Problem: (.*): ([^:]*)$/)) !== null) { - var rowSelector = void 0; - if (currentDir !== matches[1]) { - $(lastRowSelector).before(build.uploadNewFile(matches[1])); - rowSelector = prelastRowSelector; - } else { - // The problem is with the directory - // itself, so alter its existing line. - rowSelector = lastRowSelector; - topSkip -= $(rowSelector).outerHeight(); - } - if (matches[2] === "Given path is not a directory" || matches[2] === "Given path is reserved") { - $(rowSelector + " .status").html(lychee.locale["UPLOAD_FAILED"]).addClass("error"); - } else if (matches[2] === "Skipped duplicate (resynced metadata)") { - $(rowSelector + " .status").html(lychee.locale["UPLOAD_UPDATED"]).addClass("warning"); - } else if (matches[2] === "Import cancelled") { - $(rowSelector + " .status").html(lychee.locale["UPLOAD_CANCELLED"]).addClass("error"); - } else { - $(rowSelector + " .status").html(lychee.locale["UPLOAD_SKIPPED"]).addClass("warning"); - } - var translations = { - "Given path is not a directory": "UPLOAD_IMPORT_NOT_A_DIRECTORY", - "Given path is reserved": "UPLOAD_IMPORT_PATH_RESERVED", - "Could not read file": "UPLOAD_IMPORT_UNREADABLE", - "Could not import file": "UPLOAD_IMPORT_FAILED", - "Unsupported file type": "UPLOAD_IMPORT_UNSUPPORTED", - "Could not create album": "UPLOAD_IMPORT_ALBUM_FAILED", - "Skipped duplicate": "UPLOAD_IMPORT_SKIPPED_DUPLICATE", - "Skipped duplicate (resynced metadata)": "UPLOAD_IMPORT_RESYNCED_DUPLICATE", - "Import cancelled": "UPLOAD_IMPORT_CANCELLED" - }; - $(rowSelector + " .notice").html(matches[2] in translations ? lychee.locale[translations[matches[2]]] : matches[2]).show(); - topSkip += $(rowSelector).outerHeight(); - encounteredProblems = true; - } else if (resp === "Warning: Approaching memory limit") { - $(lastRowSelector).before(build.uploadNewFile(lychee.locale["UPLOAD_IMPORT_LOW_MEMORY"])); - topSkip += $(prelastRowSelector).outerHeight(); - $(prelastRowSelector + " .status").html(lychee.locale["UPLOAD_WARNING"]).addClass("warning"); - $(prelastRowSelector + " .notice").html(lychee.locale["UPLOAD_IMPORT_LOW_MEMORY_EXPL"]).show(); - } - $(".basicModal .rows").scrollTop(topSkip); - }); // forEach (resp) - }; // processIncremental - - api.post("Import::server", params, function (_data) { - // _data is already JSON-parsed. - processIncremental(_data); - - albums.refresh(); - - upload.notify(lychee.locale["UPLOAD_IMPORT_COMPLETE"], encounteredProblems ? lychee.locale["UPLOAD_COMPLETE_FAILED"] : null); - - if (album.getID() === false) lychee.goto("0");else album.load(albumID); - - if (encounteredProblems) showCloseButton();else basicModal.close(); - }, function (event) { - // We received a possibly partial response. - // We need to begin by terminating the data with a - // '"' so that it can be JSON-parsed. - var response = this.response; - if (response.length > 0) { - if (response.substring(this.response.length - 1) === '"') { - // This might be either a terminating '"' - // or it may come from, say, a filename, in - // which case it would be escaped. - if (response.length > 1) { - if (response.substring(this.response.length - 2) === '"') { - response += '"'; - } - // else it's a complete response, - // requiring no termination from us. - } else { - // The response is just '"'. - response += '"'; - } - } else { - // This should be the most common case for - // partial responses. - response += '"'; - } - } - // Parse the response as JSON. This will remove - // the surrounding '"' characters, unescape any '"' - // from the middle, and translate '\n' sequences into - // newlines. - var jsonResponse = void 0; - try { - jsonResponse = JSON.parse(response); - } catch (e) { - // Most likely a SyntaxError due to something - // that went wrong on the server side. - $(lastRowSelector + " .status").html(lychee.locale["UPLOAD_FAILED"]).addClass("error"); - - albums.refresh(); - upload.notify(lychee.locale["UPLOAD_COMPLETE"], lychee.locale["UPLOAD_COMPLETE_FAILED"]); - - if (album.getID() === false) lychee.goto("0");else album.load(albumID); - - showCloseButton(); - - return; - } - // The rest of the work is the same as for the full - // response. - processIncremental(jsonResponse); - }); // api.post - }, function () { - if (!cancelUpload) { - api.post("Import::serverCancel", {}, function (resp) { - if (resp === "true") cancelUpload = true; - }); - } - }); // upload.show - }; // action - - var msg = lychee.html(_templateObject77, lychee.locale["UPLOAD_IMPORT_SERVER_INSTR"], lychee.locale["UPLOAD_ABSOLUTE_PATH"], lychee.location); - msg += lychee.html(_templateObject78, build.iconic("check"), lychee.locale["UPLOAD_IMPORT_DELETE_ORIGINALS"], lychee.locale["UPLOAD_IMPORT_DELETE_ORIGINALS_EXPL"], build.iconic("check"), lychee.locale["UPLOAD_IMPORT_VIA_SYMLINK"], lychee.locale["UPLOAD_IMPORT_VIA_SYMLINK_EXPL"], build.iconic("check"), lychee.locale["UPLOAD_IMPORT_SKIP_DUPLICATES"], lychee.locale["UPLOAD_IMPORT_SKIP_DUPLICATES_EXPL"], build.iconic("check"), lychee.locale["UPLOAD_IMPORT_RESYNC_METADATA"], lychee.locale["UPLOAD_IMPORT_RESYNC_METADATA_EXPL"]); - - basicModal.show({ - body: msg, - buttons: { - action: { - title: lychee.locale["UPLOAD_IMPORT"], - fn: action - }, - cancel: { - title: lychee.locale["CANCEL"], - fn: basicModal.close - } - } - }); - - var $delete = $(choiceDeleteSelector); - var $symlinks = $(choiceSymlinkSelector); - var $duplicates = $(choiceDuplicateSelector); - var $resync = $(choiceResyncSelector); - - if (lychee.delete_imported) { - $delete.prop("checked", true); - $symlinks.prop("checked", false).prop("disabled", true); - } else { - if (lychee.import_via_symlink) { - $symlinks.prop("checked", true); - $delete.prop("checked", false).prop("disabled", true); - } - } - if (lychee.skip_duplicates) { - $duplicates.prop("checked", true); - if (lychee.resync_metadata) $resync.prop("checked", true); - } else { - $resync.prop("disabled", true); - } - }, - - dropbox: function dropbox() { - var albumID = album.getID(); - if (albumID === false) albumID = 0; - - var success = function success(files) { - var links = ""; - - for (var i = 0; i < files.length; i++) { - links += files[i].link + ","; - - files[i] = { - name: files[i].link - }; - } - - // Remove last comma - links = links.substr(0, links.length - 1); - - upload.show("Importing from Dropbox", files, function () { - $(".basicModal .rows .row .status").html(lychee.locale["UPLOAD_IMPORTING"]); - - var params = { - url: links, - albumID: albumID - }; - - api.post("Import::url", params, function (data) { - // Same code as in import.url() - - if (data !== true) { - $(".basicModal .rows .row p.notice").html(lychee.locale["UPLOAD_IMPORT_WARN_ERR"]).show(); - - $(".basicModal .rows .row .status").html(lychee.locale["UPLOAD_FINISHED"]).addClass("warning"); - - // Show close button - $(".basicModal #basicModal__action.hidden").show(); - - // Log error - lychee.error(null, params, data); - } else { - basicModal.close(); - } - - upload.notify(lychee.locale["UPLOAD_IMPORT_COMPLETE"]); - - albums.refresh(); - - if (album.getID() === false) lychee.goto("0");else album.load(albumID); - }); - }); - }; - - lychee.loadDropbox(function () { - Dropbox.choose({ - linkType: "direct", - multiselect: true, - success: success - }); - }); - } -}; - -upload.check = function () { - var $delete = $(choiceDeleteSelector); - var $symlinks = $(choiceSymlinkSelector); - - if ($delete.prop("checked")) { - $symlinks.prop("checked", false).prop("disabled", true); - } else { - $symlinks.prop("disabled", false); - if ($symlinks.prop("checked")) { - $delete.prop("checked", false).prop("disabled", true); - } else { - $delete.prop("disabled", false); - } - } - - var $duplicates = $(choiceDuplicateSelector); - var $resync = $(choiceResyncSelector); - - if ($duplicates.prop("checked")) { - $resync.prop("disabled", false); - } else { - $resync.prop("checked", false).prop("disabled", true); - } -}; - -var users = { - json: null -}; - -users.update = function (params) { - if (params.username.length < 1) { - loadingBar.show("error", "new username cannot be empty."); - return false; - } - - if ($("#UserData" + params.id + ' .choice input[name="upload"]:checked').length === 1) { - params.upload = "1"; - } else { - params.upload = "0"; - } - if ($("#UserData" + params.id + ' .choice input[name="lock"]:checked').length === 1) { - params.lock = "1"; - } else { - params.lock = "0"; - } - - api.post("User::Save", params, function (data) { - if (data !== true) { - loadingBar.show("error", data.description); - lychee.error(null, params, data); - } else { - loadingBar.show("success", "User updated!"); - users.list(); // reload user list - } - }); -}; - -users.create = function (params) { - if (params.username.length < 1) { - loadingBar.show("error", "new username cannot be empty."); - return false; - } - if (params.password.length < 1) { - loadingBar.show("error", "new password cannot be empty."); - return false; - } - - if ($('#UserCreate .choice input[name="upload"]:checked').length === 1) { - params.upload = "1"; - } else { - params.upload = "0"; - } - if ($('#UserCreate .choice input[name="lock"]:checked').length === 1) { - params.lock = "1"; - } else { - params.lock = "0"; - } - - api.post("User::Create", params, function (data) { - if (data !== true) { - loadingBar.show("error", data.description); - lychee.error(null, params, data); - } else { - loadingBar.show("success", "User created!"); - users.list(); // reload user list - } - }); -}; - -users.delete = function (params) { - api.post("User::Delete", params, function (data) { - if (data !== true) { - loadingBar.show("error", data.description); - lychee.error(null, params, data); - } else { - loadingBar.show("success", "User deleted!"); - users.list(); // reload user list - } - }); -}; - -users.list = function () { - api.post("User::List", {}, function (data) { - users.json = data; - view.users.init(); - }); -}; - -/** - * @description Responsible to reflect data changes to the UI. - */ - -var view = {}; - -view.albums = { - init: function init() { - multiselect.clearSelection(); - - view.albums.title(); - view.albums.content.init(); - }, - - title: function title() { - if (lychee.landing_page_enable) { - if (lychee.title !== "Lychee v4") { - lychee.setTitle(lychee.title, false); - } else { - lychee.setTitle(lychee.locale["ALBUMS"], false); - } - } else { - lychee.setTitle(lychee.locale["ALBUMS"], false); - } - }, - - content: { - init: function init() { - var smartData = ""; - var albumsData = ""; - var sharedData = ""; - - // Smart Albums - if (albums.json.smartalbums != null) { - if (lychee.publicMode === false) { - smartData = build.divider(lychee.locale["SMART_ALBUMS"]); - } - if (albums.json.smartalbums.unsorted) { - albums.parse(albums.json.smartalbums.unsorted); - smartData += build.album(albums.json.smartalbums.unsorted); - } - if (albums.json.smartalbums.public) { - albums.parse(albums.json.smartalbums.public); - smartData += build.album(albums.json.smartalbums.public); - } - if (albums.json.smartalbums.starred) { - albums.parse(albums.json.smartalbums.starred); - smartData += build.album(albums.json.smartalbums.starred); - } - if (albums.json.smartalbums.recent) { - albums.parse(albums.json.smartalbums.recent); - smartData += build.album(albums.json.smartalbums.recent); - } - - Object.entries(albums.json.smartalbums).forEach(function (_ref) { - var _ref2 = _slicedToArray(_ref, 2), - albumName = _ref2[0], - albumData = _ref2[1]; - - if (albumData["tag_album"] === "1") { - albums.parse(albumData); - smartData += build.album(albumData); - } - }); - } - - // Albums - if (albums.json.albums && albums.json.albums.length !== 0) { - $.each(albums.json.albums, function () { - if (!this.parent_id || this.parent_id === 0) { - albums.parse(this); - albumsData += build.album(this); - } - }); - - // Add divider - if (lychee.publicMode === false) albumsData = build.divider(lychee.locale["ALBUMS"]) + albumsData; - } - - var current_owner = ""; - var i = void 0; - // Shared - if (albums.json.shared_albums && albums.json.shared_albums.length !== 0) { - for (i = 0; i < albums.json.shared_albums.length; ++i) { - var alb = albums.json.shared_albums[i]; - if (!alb.parent_id || alb.parent_id === 0) { - albums.parse(alb); - if (current_owner !== alb.owner && lychee.publicMode === false) { - sharedData += build.divider(alb.owner); - current_owner = alb.owner; - } - sharedData += build.album(alb, !lychee.admin); - } - } - } - - if (smartData === "" && albumsData === "" && sharedData === "") { - lychee.content.html(""); - $("body").append(build.no_content("eye")); - } else { - lychee.content.html(smartData + albumsData + sharedData); - } - - album.apply_nsfw_filter(); - - // Restore scroll position - var urls = JSON.parse(localStorage.getItem("scroll")); - var urlWindow = window.location.href; - $(window).scrollTop(urls != null && urls[urlWindow] ? urls[urlWindow] : 0); - }, - - title: function title(albumID) { - var title = albums.getByID(albumID).title; - - title = lychee.escapeHTML(title); - - $('.album[data-id="' + albumID + '"] .overlay h1').html(title).attr("title", title); - }, - - delete: function _delete(albumID) { - $('.album[data-id="' + albumID + '"]').css("opacity", 0).animate({ - width: 0, - marginLeft: 0 - }, 300, function () { - $(this).remove(); - if (albums.json.albums.length <= 0) lychee.content.find(".divider:last-child").remove(); - }); - } - } -}; - -view.album = { - init: function init() { - multiselect.clearSelection(); - - album.parse(); - - view.album.sidebar(); - view.album.title(); - view.album.public(); - view.album.nsfw(); - view.album.nsfw_warning.init(); - view.album.content.init(); - - album.json.init = 1; - }, - - title: function title() { - if ((visible.album() || !album.json.init) && !visible.photo()) { - switch (album.getID()) { - case "starred": - lychee.setTitle(lychee.locale["STARRED"], true); - break; - case "public": - lychee.setTitle(lychee.locale["PUBLIC"], true); - break; - case "recent": - lychee.setTitle(lychee.locale["RECENT"], true); - break; - case "unsorted": - lychee.setTitle(lychee.locale["UNSORTED"], true); - break; - default: - if (album.json.init) _sidebar.changeAttr("title", album.json.title); - lychee.setTitle(album.json.title, true); - break; - } - } - }, - - nsfw_warning: { - init: function init() { - if (!lychee.nsfw_warning) { - $("#sensitive_warning").hide(); - return; - } - - if (album.json.nsfw && album.json.nsfw === "1" && !lychee.nsfw_unlocked_albums.includes(album.json.id)) { - $("#sensitive_warning").show(); - } else { - $("#sensitive_warning").hide(); - } - }, - - next: function next() { - lychee.nsfw_unlocked_albums.push(album.json.id); - $("#sensitive_warning").hide(); - } - }, - - content: { - init: function init() { - var photosData = ""; - var albumsData = ""; - var html = ""; - - if (album.json.albums && album.json.albums !== false) { - $.each(album.json.albums, function () { - albums.parse(this); - albumsData += build.album(this, !album.isUploadable()); - }); - } - if (album.json.photos && album.json.photos !== false) { - // Build photos - $.each(album.json.photos, function () { - photosData += build.photo(this, !album.isUploadable()); - }); - } - - if (photosData !== "") { - if (lychee.layout === "1") { - photosData = '
' + photosData + "
"; - } else if (lychee.layout === "2") { - photosData = '
' + photosData + "
"; - } - } - - if (albumsData !== "" && photosData !== "") { - html = build.divider(lychee.locale["ALBUMS"]); - } - html += albumsData; - if (albumsData !== "" && photosData !== "") { - html += build.divider(lychee.locale["PHOTOS"]); - } - html += photosData; - - // Add photos to view - lychee.content.html(html); - album.apply_nsfw_filter(); - - view.album.content.justify(); - - // Restore scroll position - var urls = JSON.parse(localStorage.getItem("scroll")); - var urlWindow = window.location.href; - $(window).scrollTop(urls != null && urls[urlWindow] ? urls[urlWindow] : 0); - }, - - title: function title(photoID) { - var title = album.getByID(photoID).title; - - title = lychee.escapeHTML(title); - - $('.photo[data-id="' + photoID + '"] .overlay h1').html(title).attr("title", title); - }, - - titleSub: function titleSub(albumID) { - var title = album.getSubByID(albumID).title; - - title = lychee.escapeHTML(title); - - $('.album[data-id="' + albumID + '"] .overlay h1').html(title).attr("title", title); - }, - - star: function star(photoID) { - var $badge = $('.photo[data-id="' + photoID + '"] .icn-star'); - - if (album.getByID(photoID).star === "1") $badge.addClass("badge--star");else $badge.removeClass("badge--star"); - }, - - public: function _public(photoID) { - var $badge = $('.photo[data-id="' + photoID + '"] .icn-share'); - - if (album.getByID(photoID).public === "1") $badge.addClass("badge--visible badge--hidden");else $badge.removeClass("badge--visible badge--hidden"); - }, - - cover: function cover(photoID) { - $(".album .icn-cover").removeClass("badge--cover"); - $(".photo .icn-cover").removeClass("badge--cover"); - - if (album.json.cover_id === photoID) { - var badge = $('.photo[data-id="' + photoID + '"] .icn-cover'); - if (badge.length > 0) { - badge.addClass("badge--cover"); - } else { - $.each(album.json.albums, function () { - if (this.thumb.id === photoID) { - $('.album[data-id="' + this.id + '"] .icn-cover').addClass("badge--cover"); - return false; - } - }); - } - } - }, - - updatePhoto: function updatePhoto(data) { - var src = void 0, - srcset = ""; - - // This mimicks the structure of build.photo - if (lychee.layout === "0") { - src = data.sizeVariants.thumb.url; - if (data.sizeVariants.thumb2x !== null) { - srcset = data.sizeVariants.thumb2x.url + " 2x"; - } - } else { - if (data.sizeVariants.small !== null) { - src = data.sizeVariants.small.url; - if (data.sizeVariants.small2x !== null) { - srcset = data.sizeVariants.small.url + " " + data.sizeVariants.small.width + "w, " + data.sizeVariants.small2x.url + " " + data.sizeVariants.small2x.width + "w"; - } - } else if (data.sizeVariants.medium !== null) { - src = data.sizeVariants.medium.url; - if (data.sizeVariants.medium2x !== null) { - srcset = data.sizeVariants.medium.url + " " + data.sizeVariants.medium.width + "w, " + data.sizeVariants.medium2x.url + " " + data.sizeVariants.medium2x.width + "w"; - } - } else if (!data.type || data.type.indexOf("video") !== 0) { - src = data.url; - } else { - src = data.sizeVariants.thumb.url; - if (data.sizeVariants.thumb2x !== null) { - srcset = data.sizeVariants.thumb.url + " " + data.sizeVariants.thumb.width + "w, " + data.sizeVariants.thumb2x.url + " " + data.sizeVariants.thumb2x.width + "w"; - } - } - } - - $('.photo[data-id="' + data.id + '"] > span.thumbimg > img').attr("data-src", src).attr("data-srcset", srcset).addClass("lazyload"); - - view.album.content.justify(); - }, - - delete: function _delete(photoID) { - var justify = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; - - $('.photo[data-id="' + photoID + '"]').css("opacity", 0).animate({ - width: 0, - marginLeft: 0 - }, 300, function () { - $(this).remove(); - // Only when search is not active - if (album.json) { - if (visible.sidebar()) { - var videoCount = 0; - $.each(album.json.photos, function () { - if (this.type && this.type.indexOf("video") > -1) { - videoCount++; - } - }); - if (album.json.photos.length - videoCount > 0) { - _sidebar.changeAttr("images", album.json.photos.length - videoCount); - } else { - _sidebar.hideAttr("images"); - } - if (videoCount > 0) { - _sidebar.changeAttr("videos", videoCount); - } else { - _sidebar.hideAttr("videos"); - } - } - if (album.json.photos.length <= 0) { - lychee.content.find(".divider").remove(); - } - if (justify) { - view.album.content.justify(); - } - } - }); - }, - - deleteSub: function deleteSub(albumID) { - $('.album[data-id="' + albumID + '"]').css("opacity", 0).animate({ - width: 0, - marginLeft: 0 - }, 300, function () { - $(this).remove(); - if (album.json) { - if (album.json.albums.length <= 0) { - lychee.content.find(".divider").remove(); - } - if (visible.sidebar()) { - if (album.json.albums.length > 0) { - _sidebar.changeAttr("subalbums", album.json.albums.length); - } else { - _sidebar.hideAttr("subalbums"); - } - } - } - }); - }, - - justify: function justify() { - if (!album.json || !album.json.photos || album.json.photos === false) return; - if (lychee.layout === "1") { - var containerWidth = parseFloat($(".justified-layout").width(), 10); - if (containerWidth == 0) { - // Triggered on Reload in photo view. - containerWidth = $(window).width() - parseFloat($(".justified-layout").css("margin-left"), 10) - parseFloat($(".justified-layout").css("margin-right"), 10) - parseFloat($(".content").css("padding-right"), 10); - } - var ratio = []; - $.each(album.json.photos, function (i) { - ratio[i] = this.height > 0 ? this.width / this.height : 1; - if (this.type && this.type.indexOf("video") > -1) { - // Video. If there's no small and medium, we have - // to fall back to the square thumb. - if (this.small === "" && this.medium === "") { - ratio[i] = 1; - } - } - }); - var layoutGeometry = require("justified-layout")(ratio, { - containerWidth: containerWidth, - containerPadding: 0, - // boxSpacing: { - // horizontal: 42, - // vertical: 150 - // }, - targetRowHeight: parseFloat($(".photo").css("--lychee-default-height"), 10) - }); - // if (lychee.admin) console.log(layoutGeometry); - $(".justified-layout").css("height", layoutGeometry.containerHeight + "px"); - $(".justified-layout > div").each(function (i) { - if (!layoutGeometry.boxes[i]) { - // Race condition in search.find -- window content - // and album.json can get out of sync as search - // query is being modified. - return false; - } - $(this).css("top", layoutGeometry.boxes[i].top); - $(this).css("width", layoutGeometry.boxes[i].width); - $(this).css("height", layoutGeometry.boxes[i].height); - $(this).css("left", layoutGeometry.boxes[i].left); - - var imgs = $(this).find(".thumbimg > img"); - if (imgs.length > 0 && imgs[0].getAttribute("data-srcset")) { - imgs[0].setAttribute("sizes", layoutGeometry.boxes[i].width + "px"); - } - }); - } else if (lychee.layout === "2") { - var _containerWidth = parseFloat($(".unjustified-layout").width(), 10); - if (_containerWidth == 0) { - // Triggered on Reload in photo view. - _containerWidth = $(window).width() - parseFloat($(".unjustified-layout").css("margin-left"), 10) - parseFloat($(".unjustified-layout").css("margin-right"), 10) - parseFloat($(".content").css("padding-right"), 10); - } - // For whatever reason, the calculation of margin is - // super-slow in Firefox (tested with 68), so we make sure to - // do it just once, outside the loop. Height doesn't seem to - // be affected, but we do it the same way for consistency. - var margin = parseFloat($(".photo").css("margin-right"), 10); - var origHeight = parseFloat($(".photo").css("max-height"), 10); - $(".unjustified-layout > div").each(function (i) { - if (!album.json.photos[i]) { - // Race condition in search.find -- window content - // and album.json can get out of sync as search - // query is being modified. - return false; - } - var ratio = album.json.photos[i].height > 0 ? album.json.photos[i].width / album.json.photos[i].height : 1; - if (album.json.photos[i].type && album.json.photos[i].type.indexOf("video") > -1) { - // Video. If there's no small and medium, we have - // to fall back to the square thumb. - if (album.json.photos[i].small === "" && album.json.photos[i].medium === "") { - ratio = 1; - } - } - - var height = origHeight; - var width = height * ratio; - var imgs = $(this).find(".thumbimg > img"); - - if (width > _containerWidth - margin) { - width = _containerWidth - margin; - height = width / ratio; - } - - $(this).css("width", width + "px"); - $(this).css("height", height + "px"); - if (imgs.length > 0 && imgs[0].getAttribute("data-srcset")) { - imgs[0].setAttribute("sizes", width + "px"); - } - }); - } - } - }, - - description: function description() { - _sidebar.changeAttr("description", album.json.description); - }, - - show_tags: function show_tags() { - _sidebar.changeAttr("show_tags", album.json.show_tags); - }, - - license: function license() { - var license = void 0; - switch (album.json.license) { - case "none": - license = ""; // none is displayed as - thus is empty. - break; - case "reserved": - license = lychee.locale["ALBUM_RESERVED"]; - break; - default: - license = album.json.license; - // console.log('default'); - break; - } - - _sidebar.changeAttr("license", license); - }, - - public: function _public() { - $("#button_visibility_album, #button_sharing_album_users").removeClass("active--not-hidden active--hidden"); - - if (album.json.public === "1") { - if (album.json.visible === "0") { - $("#button_visibility_album, #button_sharing_album_users").addClass("active--hidden"); - } else { - $("#button_visibility_album, #button_sharing_album_users").addClass("active--not-hidden"); - } - - $(".photo .iconic-share").remove(); - - if (album.json.init) _sidebar.changeAttr("public", lychee.locale["ALBUM_SHR_YES"]); - } else { - if (album.json.init) _sidebar.changeAttr("public", lychee.locale["ALBUM_SHR_NO"]); - } - }, - - hidden: function hidden() { - if (album.json.visible === "1") _sidebar.changeAttr("hidden", lychee.locale["ALBUM_SHR_NO"]);else _sidebar.changeAttr("hidden", lychee.locale["ALBUM_SHR_YES"]); - }, - - nsfw: function nsfw() { - if (album.json.nsfw === "1") { - // Sensitive - $("#button_nsfw_album").addClass("active").attr("title", lychee.locale["ALBUM_UNMARK_NSFW"]); - } else { - // Not Sensitive - $("#button_nsfw_album").removeClass("active").attr("title", lychee.locale["ALBUM_MARK_NSFW"]); - } - }, - - downloadable: function downloadable() { - if (album.json.downloadable === "1") _sidebar.changeAttr("downloadable", lychee.locale["ALBUM_SHR_YES"]);else _sidebar.changeAttr("downloadable", lychee.locale["ALBUM_SHR_NO"]); - }, - - shareButtonVisible: function shareButtonVisible() { - if (album.json.share_button_visible === "1") _sidebar.changeAttr("share_button_visible", lychee.locale["ALBUM_SHR_YES"]);else _sidebar.changeAttr("share_button_visible", lychee.locale["ALBUM_SHR_NO"]); - }, - - password: function password() { - if (album.json.password === "1") _sidebar.changeAttr("password", lychee.locale["ALBUM_SHR_YES"]);else _sidebar.changeAttr("password", lychee.locale["ALBUM_SHR_NO"]); - }, - - sidebar: function sidebar() { - if ((visible.album() || album.json && album.json.init) && !visible.photo()) { - var structure = _sidebar.createStructure.album(album); - var html = _sidebar.render(structure); - - _sidebar.dom(".sidebar__wrapper").html(html); - _sidebar.bind(); - } - } -}; - -view.photo = { - init: function init(autoplay) { - multiselect.clearSelection(); - - _photo.parse(); - - view.photo.sidebar(); - view.photo.title(); - view.photo.star(); - view.photo.public(); - view.photo.header(); - view.photo.photo(autoplay); - - _photo.json.init = 1; - }, - - show: function show() { - // Change header - lychee.content.addClass("view"); - header.setMode("photo"); - - // Make body not scrollable - // use bodyScrollLock package to enable locking on iOS - // Simple overflow: hidden not working on iOS Safari - // Only the info pane needs scrolling - // Touch event for swiping of photo still work - - scrollLock.disablePageScroll($(".sidebar__wrapper").get()); - - // Fullscreen - var timeout = null; - $(document).bind("mousemove", function () { - clearTimeout(timeout); - // For live Photos: header animtion only if LivePhoto is not playing - if (!_photo.isLivePhotoPlaying() && lychee.header_auto_hide) { - header.show(); - timeout = setTimeout(header.hideIfLivePhotoNotPlaying, 2500); - } - }); - - // we also put this timeout to enable it by default when you directly click on a picture. - if (lychee.header_auto_hide) { - setTimeout(header.hideIfLivePhotoNotPlaying, 2500); - } - - lychee.animate(lychee.imageview, "fadeIn"); - }, - - hide: function hide() { - header.show(); - - lychee.content.removeClass("view"); - header.setMode("album"); - - // Make body scrollable - scrollLock.enablePageScroll($(".sidebar__wrapper").get()); - - // Disable Fullscreen - $(document).unbind("mousemove"); - if ($("video").length) { - $("video")[$("video").length - 1].pause(); - } - - // Hide Photo - lychee.animate(lychee.imageview, "fadeOut"); - setTimeout(function () { - lychee.imageview.hide(); - view.album.sidebar(); - }, 300); - }, - - title: function title() { - if (_photo.json.init) _sidebar.changeAttr("title", _photo.json.title); - lychee.setTitle(_photo.json.title, true); - }, - - description: function description() { - if (_photo.json.init) _sidebar.changeAttr("description", _photo.json.description); - }, - - license: function license() { - var license = void 0; - - // Process key to display correct string - switch (_photo.json.license) { - case "none": - license = ""; // none is displayed as - thus is empty (uniformity of the display). - break; - case "reserved": - license = lychee.locale["PHOTO_RESERVED"]; - break; - default: - license = _photo.json.license; - break; - } - - // Update the sidebar if the photo is visible - if (_photo.json.init) _sidebar.changeAttr("license", license); - }, - - star: function star() { - if (_photo.json.star === "1") { - // Starred - $("#button_star").addClass("active").attr("title", lychee.locale["UNSTAR_PHOTO"]); - } else { - // Unstarred - $("#button_star").removeClass("active").attr("title", lychee.locale["STAR_PHOTO"]); - } - }, - - public: function _public() { - $("#button_visibility").removeClass("active--hidden active--not-hidden"); - - if (_photo.json.public === "1" || _photo.json.public === "2") { - // Photo public - if (_photo.json.public === "1") { - $("#button_visibility").addClass("active--hidden"); - } else { - $("#button_visibility").addClass("active--not-hidden"); - } - - if (_photo.json.init) _sidebar.changeAttr("public", lychee.locale["PHOTO_SHR_YES"]); - } else { - // Photo private - if (_photo.json.init) _sidebar.changeAttr("public", "No"); - } - }, - - tags: function tags() { - _sidebar.changeAttr("tags", build.tags(_photo.json.tags), true); - _sidebar.bind(); - }, - - photo: function photo(autoplay) { - var ret = build.imageview(_photo.json, visible.header(), autoplay); - lychee.imageview.html(ret.html); - tabindex.makeFocusable(lychee.imageview); - - // Init Live Photo if needed - if (_photo.isLivePhoto()) { - // Package gives warning that function will be remove and - // shoud be replaced by LivePhotosKit.augementElementAsPlayer - // But, LivePhotosKit.augementElementAsPlayer is not yet available - _photo.LivePhotosObject = LivePhotosKit.Player(document.getElementById("livephoto")); - } - - view.photo.onresize(); - - var $nextArrow = lychee.imageview.find("a#next"); - var $previousArrow = lychee.imageview.find("a#previous"); - var photoID = _photo.getID(); - var hasNext = album.json && album.json.photos && album.getByID(photoID) && album.getByID(photoID).nextPhoto != null && album.getByID(photoID).nextPhoto !== ""; - var hasPrevious = album.json && album.json.photos && album.getByID(photoID) && album.getByID(photoID).previousPhoto != null && album.getByID(photoID).previousPhoto !== ""; - - var img = $("img#image"); - if (img.length > 0) { - if (!img[0].complete || img[0].currentSrc !== null && img[0].currentSrc === "") { - // Image is still loading. Display the thumb version in the - // background. - if (ret.thumb !== "") { - img.css("background-image", lychee.html(_templateObject79, ret.thumb)); - } - - // Don't preload next/prev until the requested image is - // fully loaded. - img.on("load", function () { - _photo.preloadNextPrev(_photo.getID()); - }); - } else { - _photo.preloadNextPrev(_photo.getID()); - } - } - - if (hasNext === false || lychee.viewMode === true) { - $nextArrow.hide(); - } else { - var nextPhotoID = album.getByID(photoID).nextPhoto; - var nextPhoto = album.getByID(nextPhotoID); - - // Check if thumbUrl exists (for videos w/o ffmpeg, we add a play-icon) - var thumbUrl = "img/placeholder.png"; - if (nextPhoto.sizeVariants.thumb !== null) { - thumbUrl = nextPhoto.sizeVariants.thumb.url; - } else if (nextPhoto.type.indexOf("video") > -1) { - thumbUrl = "img/play-icon.png"; - } - $nextArrow.css("background-image", lychee.html(_templateObject80, thumbUrl)); - } - - if (hasPrevious === false || lychee.viewMode === true) { - $previousArrow.hide(); - } else { - var previousPhotoID = album.getByID(photoID).previousPhoto; - var previousPhoto = album.getByID(previousPhotoID); - - // Check if thumbUrl exists (for videos w/o ffmpeg, we add a play-icon) - var _thumbUrl = "img/placeholder.png"; - if (previousPhoto.sizeVariants.thumb !== null) { - _thumbUrl = previousPhoto.sizeVariants.thumb.url; - } else if (previousPhoto.type.indexOf("video") > -1) { - _thumbUrl = "img/play-icon.png"; - } - $previousArrow.css("background-image", lychee.html(_templateObject80, _thumbUrl)); - } - }, - - sidebar: function sidebar() { - var structure = _sidebar.createStructure.photo(_photo.json); - var html = _sidebar.render(structure); - var has_location = _photo.json.latitude && _photo.json.longitude ? true : false; - - _sidebar.dom(".sidebar__wrapper").html(html); - _sidebar.bind(); - - if (has_location && lychee.map_display) { - // Leaflet seaches for icon in same directoy as js file -> paths needs - // to be overwritten - delete L.Icon.Default.prototype._getIconUrl; - L.Icon.Default.mergeOptions({ - iconRetinaUrl: "img/marker-icon-2x.png", - iconUrl: "img/marker-icon.png", - shadowUrl: "img/marker-shadow.png" - }); - - var mymap = L.map("leaflet_map_single_photo").setView([_photo.json.latitude, _photo.json.longitude], 13); - - L.tileLayer(map_provider_layer_attribution[lychee.map_provider].layer, { - attribution: map_provider_layer_attribution[lychee.map_provider].attribution - }).addTo(mymap); - - if (!lychee.map_display_direction || !_photo.json.imgDirection || _photo.json.imgDirection === "") { - // Add Marker to map, direction is not set - L.marker([_photo.json.latitude, _photo.json.longitude]).addTo(mymap); - } else { - // Add Marker, direction has been set - var viewDirectionIcon = L.icon({ - iconUrl: "img/view-angle-icon.png", - iconRetinaUrl: "img/view-angle-icon-2x.png", - iconSize: [100, 58], // size of the icon - iconAnchor: [50, 49] // point of the icon which will correspond to marker's location - }); - var marker = L.marker([_photo.json.latitude, _photo.json.longitude], { icon: viewDirectionIcon }).addTo(mymap); - marker.setRotationAngle(_photo.json.imgDirection); - } - } - }, - - header: function header() { - if (_photo.json.type && (_photo.json.type.indexOf("video") === 0 || _photo.json.type === "raw") || _photo.json.livePhotoUrl !== "" && _photo.json.livePhotoUrl !== null) { - $("#button_rotate_cwise, #button_rotate_ccwise").hide(); - } else { - $("#button_rotate_cwise, #button_rotate_ccwise").show(); - } - }, - - onresize: function onresize() { - if (!_photo.json || _photo.json.sizeVariants.medium === null || _photo.json.sizeVariants.medium2x === null) return; - - // Calculate the width of the image in the current window without - // borders and set 'sizes' to it. - var imgWidth = _photo.json.sizeVariants.medium.width; - var imgHeight = _photo.json.sizeVariants.medium.height; - var containerWidth = $(window).outerWidth(); - var containerHeight = $(window).outerHeight(); - - // Image can be no larger than its natural size, but it can be - // smaller depending on the size of the window. - var width = imgWidth < containerWidth ? imgWidth : containerWidth; - var height = width * imgHeight / imgWidth; - if (height > containerHeight) { - width = containerHeight * imgWidth / imgHeight; - } - - $("img#image").attr("sizes", width + "px"); - } -}; - -view.settings = { - init: function init() { - multiselect.clearSelection(); - - view.photo.hide(); - view.settings.title(); - header.setMode("config"); - view.settings.content.init(); - }, - - title: function title() { - lychee.setTitle(lychee.locale["SETTINGS"], false); - }, - - clearContent: function clearContent() { - lychee.content.html('
'); - }, - - content: { - init: function init() { - view.settings.clearContent(); - view.settings.content.setLogin(); - if (lychee.admin) { - view.settings.content.setSorting(); - view.settings.content.setDropboxKey(); - view.settings.content.setLang(); - view.settings.content.setDefaultLicense(); - view.settings.content.setLayout(); - view.settings.content.setPublicSearch(); - view.settings.content.setOverlayType(); - view.settings.content.setMapDisplay(); - view.settings.content.setNSFWVisible(); - view.settings.content.setNotification(); - view.settings.content.setCSS(); - view.settings.content.moreButton(); - } - }, - - setLogin: function setLogin() { - var msg = lychee.html(_templateObject81, lychee.locale["PASSWORD_TITLE"], lychee.locale["USERNAME_CURRENT"], lychee.locale["PASSWORD_CURRENT"], lychee.locale["PASSWORD_TEXT"], lychee.locale["LOGIN_USERNAME"], lychee.locale["LOGIN_PASSWORD"], lychee.locale["LOGIN_PASSWORD_CONFIRM"], lychee.locale["PASSWORD_CHANGE"]); - - $(".settings_view").append(msg); - - settings.bind("#basicModal__action_password_change", ".setLogin", settings.changeLogin); - }, - - clearLogin: function clearLogin() { - $("input[name=oldUsername], input[name=oldPassword], input[name=username], input[name=password], input[name=confirm]").val(""); - }, - - setSorting: function setSorting() { - var sortingPhotos = []; - var sortingAlbums = []; - - var msg = lychee.html(_templateObject82, lychee.locale["SORT_ALBUM_BY_1"], lychee.locale["SORT_ALBUM_SELECT_1"], lychee.locale["SORT_ALBUM_SELECT_2"], lychee.locale["SORT_ALBUM_SELECT_3"], lychee.locale["SORT_ALBUM_SELECT_4"], lychee.locale["SORT_ALBUM_SELECT_5"], lychee.locale["SORT_ALBUM_SELECT_6"], lychee.locale["SORT_ALBUM_BY_2"], lychee.locale["SORT_ASCENDING"], lychee.locale["SORT_DESCENDING"], lychee.locale["SORT_ALBUM_BY_3"], lychee.locale["SORT_PHOTO_BY_1"], lychee.locale["SORT_PHOTO_SELECT_1"], lychee.locale["SORT_PHOTO_SELECT_2"], lychee.locale["SORT_PHOTO_SELECT_3"], lychee.locale["SORT_PHOTO_SELECT_4"], lychee.locale["SORT_PHOTO_SELECT_5"], lychee.locale["SORT_PHOTO_SELECT_6"], lychee.locale["SORT_PHOTO_SELECT_7"], lychee.locale["SORT_PHOTO_BY_2"], lychee.locale["SORT_ASCENDING"], lychee.locale["SORT_DESCENDING"], lychee.locale["SORT_PHOTO_BY_3"], lychee.locale["SORT_CHANGE"]); - - $(".settings_view").append(msg); - - if (lychee.sortingAlbums !== "") { - sortingAlbums = lychee.sortingAlbums.replace("ORDER BY ", "").split(" "); - - $(".setSorting select#settings_albums_type").val(sortingAlbums[0]); - $(".setSorting select#settings_albums_order").val(sortingAlbums[1]); - } - - if (lychee.sortingPhotos !== "") { - sortingPhotos = lychee.sortingPhotos.replace("ORDER BY ", "").split(" "); - - $(".setSorting select#settings_photos_type").val(sortingPhotos[0]); - $(".setSorting select#settings_photos_order").val(sortingPhotos[1]); - } - - settings.bind("#basicModal__action_sorting_change", ".setSorting", settings.changeSorting); - }, - - setDropboxKey: function setDropboxKey() { - var msg = "\n\t\t\t
\n\t\t\t

" + lychee.locale["DROPBOX_TEXT"] + "\n\t\t\t \n\t\t\t

\n\t\t\t\t\n\t\t\t
\n\t\t\t "; - - $(".settings_view").append(msg); - settings.bind("#basicModal__action_dropbox_change", ".setDropBox", settings.changeDropboxKey); - }, - - setLang: function setLang() { - var msg = "\n\t\t\t
\n\t\t\t

" + lychee.locale["LANG_TEXT"] + "\n\t\t\t \n\t\t\t\t \n\t\t\t \n\t\t\t

\n\t\t\t\n\t\t\t
"; - - $(".settings_view").append(msg); - settings.bind("#basicModal__action_set_lang", ".setLang", settings.changeLang); - }, - - setDefaultLicense: function setDefaultLicense() { - var msg = "\n\t\t\t
\n\t\t\t

" + lychee.locale["DEFAULT_LICENSE"] + "\n\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t
\n\t\t\t" + lychee.locale["PHOTO_LICENSE_HELP"] + "\n\t\t\t

\n\t\t\t\n\t\t\t
\n\t\t\t"; - $(".settings_view").append(msg); - $("select#license").val(lychee.default_license === "" ? "none" : lychee.default_license); - settings.bind("#basicModal__action_set_license", ".setDefaultLicense", settings.setDefaultLicense); - }, - - setLayout: function setLayout() { - var msg = "\n\t\t\t
\n\t\t\t

" + lychee.locale["LAYOUT_TYPE"] + "\n\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t

\n\t\t\t\n\t\t\t
\n\t\t\t"; - $(".settings_view").append(msg); - $("select#layout").val(lychee.layout); - settings.bind("#basicModal__action_set_layout", ".setLayout", settings.setLayout); - }, - - setPublicSearch: function setPublicSearch() { - var msg = "\n\t\t\t
\n\t\t\t

" + lychee.locale["PUBLIC_SEARCH_TEXT"] + "\n\t\t\t\n\t\t\t

\n\t\t\t
\n\t\t\t"; - - $(".settings_view").append(msg); - if (lychee.public_search) $("#PublicSearch").click(); - - settings.bind("#PublicSearch", ".setPublicSearch", settings.changePublicSearch); - }, - - setNSFWVisible: function setNSFWVisible() { - var msg = "\n\t\t\t
\n\t\t\t

" + lychee.locale["NSFW_VISIBLE_TEXT_1"] + "\n\t\t\t

\n\t\t\t

" + lychee.locale["NSFW_VISIBLE_TEXT_2"] + "\n\t\t\t

\n\t\t\t
\n\t\t\t"; - - $(".settings_view").append(msg); - if (lychee.nsfw_visible_saved) { - $("#NSFWVisible").click(); - } - - settings.bind("#NSFWVisible", ".setNSFWVisible", settings.changeNSFWVisible); - }, - // TODO: extend to the other settings. - - setOverlayType: function setOverlayType() { - var msg = "\n\t\t\t
\n\t\t\t

" + lychee.locale["OVERLAY_TYPE"] + "\n\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t

\n\t\t\t
\n\t\t\t"; - - $(".settings_view").append(msg); - - $("select#ImgOverlayType").val(!lychee.image_overlay_type_default ? "exif" : lychee.image_overlay_type_default); - settings.bind("#basicModal__action_set_overlay_type", ".setOverlayType", settings.setOverlayType); - }, - - setMapDisplay: function setMapDisplay() { - var msg = "\n\t\t\t
\n\t\t\t

" + lychee.locale["MAP_DISPLAY_TEXT"] + "\n\t\t\t\n\t\t\t

\n\t\t\t
\n\t\t\t"; - - $(".settings_view").append(msg); - if (lychee.map_display) $("#MapDisplay").click(); - - settings.bind("#MapDisplay", ".setMapDisplay", settings.changeMapDisplay); - - msg = "\n\t\t\t
\n\t\t\t

" + lychee.locale["MAP_DISPLAY_PUBLIC_TEXT"] + "\n\t\t\t\n\t\t\t

\n\t\t\t
\n\t\t\t"; - - $(".settings_view").append(msg); - if (lychee.map_display_public) $("#MapDisplayPublic").click(); - - settings.bind("#MapDisplayPublic", ".setMapDisplayPublic", settings.changeMapDisplayPublic); - - msg = "\n\t\t\t
\n\t\t\t

" + lychee.locale["MAP_PROVIDER"] + "\n\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t

\n\t\t\t
\n\t\t\t"; - - $(".settings_view").append(msg); - - $("select#MapProvider").val(!lychee.map_provider ? "Wikimedia" : lychee.map_provider); - settings.bind("#basicModal__action_set_map_provider", ".setMapProvider", settings.setMapProvider); - - msg = "\n\t\t\t
\n\t\t\t

" + lychee.locale["MAP_INCLUDE_SUBALBUMS_TEXT"] + "\n\t\t\t\n\t\t\t

\n\t\t\t
\n\t\t\t"; - - $(".settings_view").append(msg); - if (lychee.map_include_subalbums) $("#MapIncludeSubalbums").click(); - - settings.bind("#MapIncludeSubalbums", ".setMapIncludeSubalbums", settings.changeMapIncludeSubalbums); - - msg = "\n\t\t\t
\n\t\t\t

" + lychee.locale["LOCATION_DECODING"] + "\n\t\t\t\n\t\t\t

\n\t\t\t
\n\t\t\t"; - - $(".settings_view").append(msg); - if (lychee.location_decoding) $("#LocationDecoding").click(); - - settings.bind("#LocationDecoding", ".setLocationDecoding", settings.changeLocationDecoding); - - msg = "\n\t\t\t
\n\t\t\t

" + lychee.locale["LOCATION_SHOW"] + "\n\t\t\t\n\t\t\t

\n\t\t\t
\n\t\t\t"; - - $(".settings_view").append(msg); - if (lychee.location_show) $("#LocationShow").click(); - - settings.bind("#LocationShow", ".setLocationShow", settings.changeLocationShow); - - msg = "\n\t\t\t
\n\t\t\t

" + lychee.locale["LOCATION_SHOW_PUBLIC"] + "\n\t\t\t\n\t\t\t

\n\t\t\t
\n\t\t\t"; - - $(".settings_view").append(msg); - if (lychee.location_show_public) $("#LocationShowPublic").click(); - - settings.bind("#LocationShowPublic", ".setLocationShowPublic", settings.changeLocationShowPublic); - }, - - setNotification: function setNotification() { - msg = "\n\t\t\t
\n\t\t\t

" + lychee.locale["NEW_PHOTOS_NOTIFICATION"] + "\n\t\t\t\n\t\t\t

\n\t\t\t
\n\t\t\t"; - - $(".settings_view").append(msg); - if (lychee.new_photos_notification) $("#NewPhotosNotification").click(); - - settings.bind("#NewPhotosNotification", ".setNewPhotosNotification", settings.changeNewPhotosNotification); - }, - - setCSS: function setCSS() { - var msg = "\n\t\t\t
\n\t\t\t

" + lychee.locale["CSS_TEXT"] + "

\n\t\t\t\n\t\t\t\n\t\t\t
"; - - $(".settings_view").append(msg); - - var css_addr = $($("link")[1]).attr("href"); - - api.get(css_addr, function (data) { - $("#css").html(data); - }); - - settings.bind("#basicModal__action_set_css", ".setCSS", settings.changeCSS); - }, - - moreButton: function moreButton() { - var msg = lychee.html(_templateObject83, lychee.locale["MORE"]); - - $(".settings_view").append(msg); - - $("#basicModal__action_more").on("click", view.full_settings.init); - } - } -}; - -view.full_settings = { - init: function init() { - multiselect.clearSelection(); - - view.full_settings.title(); - view.full_settings.content.init(); - }, - - title: function title() { - lychee.setTitle("Full Settings", false); - }, - - clearContent: function clearContent() { - lychee.content.html('
'); - }, - - content: { - init: function init() { - view.full_settings.clearContent(); - - api.post("Settings::getAll", {}, function (data) { - var msg = lychee.html(_templateObject84, lychee.locale["SETTINGS_WARNING"]); - - var prev = ""; - $.each(data, function () { - if (this.cat && prev !== this.cat) { - msg += lychee.html(_templateObject85, this.cat); - prev = this.cat; - } - // prevent 'null' string for empty values - var val = this.value ? this.value : ""; - msg += lychee.html(_templateObject86, this.key, this.key, val); - }); - - msg += lychee.html(_templateObject87, lychee.locale["SAVE_RISK"]); - $(".settings_view").append(msg); - - settings.bind("#FullSettingsSave_button", "#fullSettings", settings.save); - - $("#fullSettings").on("keypress", function (e) { - settings.save_enter(e); - }); - }); - } - } -}; - -view.notifications = { - init: function init() { - multiselect.clearSelection(); - - view.notifications.title(); - view.notifications.content.init(); - }, - - title: function title() { - lychee.setTitle("Notifications", false); - }, - - clearContent: function clearContent() { - lychee.content.html('
'); - }, - - content: { - init: function init() { - view.notifications.clearContent(); - - $(".settings_view").append('

' + ("" + lychee.locale["USER_EMAIL_INSTRUCTION"]) + "

"); - - var html = ""; - - html += '

' + "Enter your email address:" + '' + '

' + 'Save' + "
"; - - $(".settings_view").append(html); - settings.bind("#UserUpdate_button", "#UserUpdate", notifications.update); - } - } -}; - -view.users = { - init: function init() { - multiselect.clearSelection(); - - view.photo.hide(); - view.users.title(); - header.setMode("config"); - view.users.content.init(); - }, - - title: function title() { - lychee.setTitle("Users", false); - }, - - clearContent: function clearContent() { - lychee.content.html('
'); - }, - - content: { - init: function init() { - view.users.clearContent(); - - if (users.json.length === 0) { - $(".users_view").append('

User list is empty!

'); - } - - var html = ""; - - html += '
' + "

" + 'username' + 'new password' + '' + build.iconic("data-transfer-upload") + "" + '' + build.iconic("lock-locked") + "" + "

" + "
"; - - $(".users_view").append(html); - - $.each(users.json, function () { - $(".users_view").append(build.user(this)); - settings.bind("#UserUpdate" + this.id, "#UserData" + this.id, users.update); - settings.bind("#UserDelete" + this.id, "#UserData" + this.id, users.delete); - if (this.upload === 1) { - $("#UserData" + this.id + ' .choice input[name="upload"]').click(); - } - if (this.lock === 1) { - $("#UserData" + this.id + ' .choice input[name="lock"]').click(); - } - }); - - html = '
' + ' ' + ' ' + '' + "" + " " + '' + "" + "" + "

" + 'Create' + "
"; - $(".users_view").append(html); - settings.bind("#UserCreate_button", "#UserCreate", users.create); - } - } -}; - -view.sharing = { - init: function init() { - multiselect.clearSelection(); - - view.photo.hide(); - view.sharing.title(); - header.setMode("config"); - view.sharing.content.init(); - }, - - title: function title() { - lychee.setTitle("Sharing", false); - }, - - clearContent: function clearContent() { - lychee.content.html(''); - }, - - content: { - init: function init() { - view.sharing.clearContent(); - - if (sharing.json.shared.length === 0) { - $(".sharing_view").append(''); - } - - var html = ""; - - html += "\n\t\t\t

Share

\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t
\n\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t
\n\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t
"; - - html += "\n\t\t\t

with

\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t
\n\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t
\n\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t
"; - html += ""; - html += '"; - if (sharing.json.shared.length !== 0) { - html += ""; - } - - $(".sharing_view").append(html); - - $("#albums_list").multiselect(); - $("#user_list").multiselect(); - $("#Share_button").on("click", sharing.add).on("mouseenter", function () { - $("#albums_list_to, #user_list_to").addClass("borderBlue"); - }).on("mouseleave", function () { - $("#albums_list_to, #user_list_to").removeClass("borderBlue"); - }); - - $("#Remove_button").on("click", sharing.delete); - } - } -}; - -view.logs = { - init: function init() { - multiselect.clearSelection(); - - view.photo.hide(); - view.logs.title(); - header.setMode("config"); - view.logs.content.init(); - }, - - title: function title() { - lychee.setTitle("Logs", false); - }, - - clearContent: function clearContent() { - var html = ""; - html += lychee.html(_templateObject88, lychee.locale["CLEAN_LOGS"]); - html += '
';
-		lychee.content.html(html);
-
-		$("#Clean_Noise").on("click", function () {
-			api.post_raw("Logs::clearNoise", {}, function () {
-				view.logs.init();
-			});
-		});
-	},
-
-	content: {
-		init: function init() {
-			view.logs.clearContent();
-			api.post_raw("Logs", {}, function (data) {
-				$(".logs_diagnostics_view").html(data);
-			});
-		}
-	}
-};
-
-view.diagnostics = {
-	init: function init() {
-		multiselect.clearSelection();
-
-		view.photo.hide();
-		view.diagnostics.title("Diagnostics");
-		header.setMode("config");
-		view.diagnostics.content.init();
-	},
-
-	title: function title() {
-		lychee.setTitle("Diagnostics", false);
-	},
-
-	clearContent: function clearContent(update) {
-		var html = "";
-
-		if (update === 2) {
-			html += view.diagnostics.button("", lychee.locale["UPDATE_AVAILABLE"]);
-		} else if (update === 3) {
-			html += view.diagnostics.button("", lychee.locale["MIGRATION_AVAILABLE"]);
-		} else if (update > 0) {
-			html += view.diagnostics.button("Check_", lychee.locale["CHECK_FOR_UPDATE"]);
-		}
-
-		html += '
';
-		lychee.content.html(html);
-	},
-
-	button: function button(type, locale) {
-		var html = "";
-		html += '
'; - html += lychee.html(_templateObject89, type, locale); - html += "
"; - - return html; - }, - - bind: function bind() { - $("#Update_Lychee").on("click", view.diagnostics.call_apply_update); - $("#Check_Update_Lychee").on("click", view.diagnostics.call_check_update); - }, - - content: { - init: function init() { - view.diagnostics.clearContent(false); - - view.diagnostics.content.v_2(); - }, - - v_2: function v_2() { - api.post("Diagnostics", {}, function (data) { - view.diagnostics.clearContent(data.update); - var html = ""; - - html += view.diagnostics.content.block("error", "Diagnostics", data.errors); - html += view.diagnostics.content.block("sys", "System Information", data.infos); - html += ''; - html += ''; - html += lychee.html(_templateObject31, lychee.locale["DIAGNOSTICS_GET_SIZE"]); - html += ""; - html += view.diagnostics.content.block("conf", "Config Information", data.configs); - - $(".logs_diagnostics_view").html(html); - - view.diagnostics.bind(); - - $("#Get_Size_Lychee").on("click", view.diagnostics.call_get_size); - }); - }, - - print_array: function print_array(arr) { - var html = ""; - var i = void 0; - - for (i = 0; i < arr.length; i++) { - html += " " + arr[i] + "\n"; - } - return html; - }, - - block: function block(id, title, arr) { - var html = ""; - html += '
\n\n\n\n';
-			html += "    " + title + "\n";
-			html += "    ".padEnd(title.length, "-") + "\n";
-			html += view.diagnostics.content.print_array(arr);
-			html += "
\n"; - return html; - } - }, - - call_check_update: function call_check_update() { - api.post("Update::Check", [], function (data) { - loadingBar.show("success", data); - $("#Check_Update_Lychee").remove(); - }); - }, - - call_apply_update: function call_apply_update() { - api.post("Update::Apply", [], function (data) { - var html = view.preify(data, ""); - $("#Update_Lychee").remove(); - $(html).prependTo(".logs_diagnostics_view"); - }); - }, - - call_get_size: function call_get_size() { - api.post("Diagnostics::getSize", [], function (data) { - var html = view.preify(data, ""); - $("#Get_Size_Lychee").remove(); - $(html).appendTo("#content_diag_sys"); - }); - } -}; - -view.update = { - init: function init() { - multiselect.clearSelection(); - - view.photo.hide(); - view.update.title(); - header.setMode("config"); - view.update.content.init(); - }, - - title: function title() { - lychee.setTitle("Update", false); - }, - - clearContent: function clearContent() { - var html = ""; - html += '
';
-		lychee.content.html(html);
-	},
-
-	content: {
-		init: function init() {
-			view.update.clearContent();
-
-			// code duplicate
-			api.post("Update::Apply", [], function (data) {
-				var html = view.preify(data, "logs_diagnostics_view");
-				lychee.content.html(html);
-			});
-		}
-	}
-};
-
-view.preify = function (data, css) {
-	var html = '
';
-	if (Array.isArray(data)) {
-		for (var i = 0; i < data.length; i++) {
-			html += "    " + data[i] + "\n";
-		}
-	} else {
-		html += "    " + data;
-	}
-	html += "
"; - - return html; -}; - -view.u2f = { - init: function init() { - multiselect.clearSelection(); - - view.photo.hide(); - view.u2f.title(); - header.setMode("config"); - view.u2f.content.init(); - }, - - title: function title() { - lychee.setTitle(lychee.locale["U2F"], false); - }, - - clearContent: function clearContent() { - lychee.content.html('
'); - }, - - content: { - init: function init() { - view.u2f.clearContent(); - - var html = ""; - - if (u2f.json.length === 0) { - $(".u2f_view").append('

Credentials list is empty!

'); - } else { - html += '
' + "

" + '' + lychee.locale["U2F_CREDENTIALS"] + "" + - // '' + build.iconic('data-transfer-upload') + '' + - // '' + build.iconic('lock-locked') + '' + - "

" + "
"; - - $(".u2f_view").append(html); - - $.each(u2f.json, function () { - $(".u2f_view").append(build.u2f(this)); - settings.bind("#CredentialDelete" + this.id, "#CredentialData" + this.id, u2f.delete); - // if (this.upload === 1) { - // $('#UserData' + this.id + ' .choice input[name="upload"]').click(); - // } - // if (this.lock === 1) { - // $('#UserData' + this.id + ' .choice input[name="lock"]').click(); - // } - }); - } - - html = '
' + lychee.locale["U2F_REGISTER_KEY"] + "" + "
"; - $(".u2f_view").append(html); - $("#RegisterU2FButton").on("click", u2f.register); - } - } -}; - -/** - * @description This module is used to check if elements are visible or not. - */ - -var visible = {}; - -visible.albums = function () { - if (header.dom(".header__toolbar--public").hasClass("header__toolbar--visible")) return true; - if (header.dom(".header__toolbar--albums").hasClass("header__toolbar--visible")) return true; - return false; -}; - -visible.album = function () { - if (header.dom(".header__toolbar--album").hasClass("header__toolbar--visible")) return true; - return false; -}; - -visible.photo = function () { - if ($("#imageview.fadeIn").length > 0) return true; - return false; -}; - -visible.mapview = function () { - if ($("#mapview.fadeIn").length > 0) return true; - return false; -}; - -visible.config = function () { - if (header.dom(".header__toolbar--config").hasClass("header__toolbar--visible")) return true; - return false; -}; - -visible.search = function () { - if (search.hash != null) return true; - return false; -}; - -visible.sidebar = function () { - if (_sidebar.dom().hasClass("active") === true) return true; - return false; -}; - -visible.sidebarbutton = function () { - if (visible.photo()) return true; - if (visible.album() && $("#button_info_album:visible").length > 0) return true; - return false; -}; - -visible.header = function () { - if (header.dom().hasClass("header--hidden") === true) return false; - return true; -}; - -visible.contextMenu = function () { - return basicContext.visible(); -}; - -visible.multiselect = function () { - if ($("#multiselect").length > 0) return true; - return false; -}; - -visible.leftMenu = function () { - if (leftMenu.dom().hasClass("leftMenu__visible")) return true; - return false; -}; - -(function (window, factory) { - var basicContext = factory(window, window.document); - window.basicContext = basicContext; - if ((typeof module === "undefined" ? "undefined" : _typeof(module)) == "object" && module.exports) { - module.exports = basicContext; - } -})(window, function l(window, document) { - var ITEM = "item", - SEPARATOR = "separator"; - - var dom = function dom() { - var elem = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ""; - - return document.querySelector(".basicContext " + elem); - }; - - var valid = function valid() { - var item = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - - var emptyItem = Object.keys(item).length === 0 ? true : false; - - if (emptyItem === true) item.type = SEPARATOR; - if (item.type == null) item.type = ITEM; - if (item.class == null) item.class = ""; - if (item.visible !== false) item.visible = true; - if (item.icon == null) item.icon = null; - if (item.title == null) item.title = "Undefined"; - - // Add disabled class when item disabled - if (item.disabled !== true) item.disabled = false; - if (item.disabled === true) item.class += " basicContext__item--disabled"; - - // Item requires a function when - // it's not a separator and not disabled - if (item.fn == null && item.type !== SEPARATOR && item.disabled === false) { - console.warn("Missing fn for item '" + item.title + "'"); - return false; - } - - return true; - }; - - var buildItem = function buildItem(item, num) { - var html = "", - span = ""; - - // Parse and validate item - if (valid(item) === false) return ""; - - // Skip when invisible - if (item.visible === false) return ""; - - // Give item a unique number - item.num = num; - - // Generate span/icon-element - if (item.icon !== null) span = ""; - - // Generate item - if (item.type === ITEM) { - html = "\n\t\t \n\t\t " + span + item.title + "\n\t\t \n\t\t "; - } else if (item.type === SEPARATOR) { - html = "\n\t\t \n\t\t "; - } - - return html; - }; - - var build = function build(items) { - var html = ""; - - html += "\n\t
\n\t
\n\t \n\t \n\t "; - - items.forEach(function (item, i) { - return html += buildItem(item, i); - }); - - html += "\n\t \n\t
\n\t
\n\t
\n\t "; - - return html; - }; - - var getNormalizedEvent = function getNormalizedEvent() { - var e = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - - var pos = { - x: e.clientX, - y: e.clientY - }; - - if (e.type === "touchend" && (pos.x == null || pos.y == null)) { - // We need to capture clientX and clientY from original event - // when the event 'touchend' does not return the touch position - - var touches = e.changedTouches; - - if (touches != null && touches.length > 0) { - pos.x = touches[0].clientX; - pos.y = touches[0].clientY; - } - } - - // Position unknown - if (pos.x == null || pos.x < 0) pos.x = 0; - if (pos.y == null || pos.y < 0) pos.y = 0; - - return pos; - }; - - var getPosition = function getPosition(e, context) { - // Get the click position - var normalizedEvent = getNormalizedEvent(e); - - // Set the initial position - var x = normalizedEvent.x, - y = normalizedEvent.y; - - var container = document.querySelector(".basicContextContainer"); - - // Get size of browser - var browserSize = { - width: container.offsetWidth, - height: container.offsetHeight - }; - - // Get size of context - var contextSize = { - width: context.offsetWidth, - height: context.offsetHeight - }; - - // Fix position based on context and browser size - if (x + contextSize.width > browserSize.width) x = x - (x + contextSize.width - browserSize.width); - if (y + contextSize.height > browserSize.height) y = y - (y + contextSize.height - browserSize.height); - - // Make context scrollable and start at the top of the browser - // when context is higher than the browser - if (contextSize.height > browserSize.height) { - y = 0; - context.classList.add("basicContext--scrollable"); - } - - // Calculate the relative position of the mouse to the context - var rx = normalizedEvent.x - x, - ry = normalizedEvent.y - y; - - return { x: x, y: y, rx: rx, ry: ry }; - }; - - var bind = function bind() { - var item = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - - if (item.fn == null) return false; - if (item.visible === false) return false; - if (item.disabled === true) return false; - - dom("td[data-num='" + item.num + "']").onclick = item.fn; - dom("td[data-num='" + item.num + "']").oncontextmenu = item.fn; - - return true; - }; - - var show = function show(items, e, fnClose, fnCallback) { - // Build context - var html = build(items); - - // Add context to the body - document.body.insertAdjacentHTML("beforeend", html); - - // Cache the context - var context = dom(); - - // Calculate position - var position = getPosition(e, context); - - // Set position - context.style.left = position.x + "px"; - context.style.top = position.y + "px"; - context.style.transformOrigin = position.rx + "px " + position.ry + "px"; - context.style.opacity = 1; - - // Close fn fallback - if (fnClose == null) fnClose = close; - - // Bind click on background - context.parentElement.onclick = fnClose; - context.parentElement.oncontextmenu = fnClose; - - // Bind click on items - items.forEach(bind); - - // Do not trigger default event or further propagation - if (typeof e.preventDefault === "function") e.preventDefault(); - if (typeof e.stopPropagation === "function") e.stopPropagation(); - - // Call callback when a function - if (typeof fnCallback === "function") fnCallback(); - - return true; - }; - - var visible = function visible() { - var elem = dom(); - - return !(elem == null || elem.length === 0); - }; - - var close = function close() { - if (visible() === false) return false; - - var container = document.querySelector(".basicContextContainer"); - - container.parentElement.removeChild(container); - - return true; - }; - - return { - ITEM: ITEM, - SEPARATOR: SEPARATOR, - show: show, - visible: visible, - close: close - }; -}); \ No newline at end of file diff --git a/public/dist/page.css b/public/dist/page.css deleted file mode 100644 index 22dd14b9a80..00000000000 --- a/public/dist/page.css +++ /dev/null @@ -1,3045 +0,0 @@ -@import url("https://fonts.googleapis.com/css?family=Roboto:300,400,700,900"); -html, -body, -div, -span, -applet, -object, -iframe, -h1, -h2, -h3, -h4, -h5, -h6, -p, -blockquote, -pre, -a, -abbr, -acronym, -address, -big, -cite, -code, -del, -dfn, -em, -img, -ins, -kbd, -q, -s, -samp, -small, -strike, -strong, -sub, -sup, -tt, -var, -b, -u, -i, -center, -dl, -dt, -dd, -ol, -ul, -li, -fieldset, -form, -label, -legend, -table, -caption, -tbody, -tfoot, -thead, -tr, -th, -td, -article, -aside, -canvas, -details, -embed, -figure, -figcaption, -footer, -header, -hgroup, -menu, -nav, -output, -ruby, -section, -summary, -time, -mark, -audio, -video { - margin: 0; - padding: 0; - border: 0; - font: inherit; - font-size: 100%; - vertical-align: baseline; } - -article, -aside, -details, -figcaption, -figure, -footer, -header, -hgroup, -menu, -nav, -section { - display: block; } - -body { - line-height: 1; } - -ol, -ul { - list-style: none; } - -blockquote, -q { - quotes: none; } - -blockquote:before, -blockquote:after, -q:before, -q:after { - content: ""; - content: none; } - -table { - border-collapse: collapse; - border-spacing: 0; } - -em, -i { - font-style: italic; } - -strong, -b { - font-weight: bold; } - -* { - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - -webkit-transition: color 0.3s, opacity 0.3s ease-out, -webkit-transform 0.3s ease-out, -webkit-box-shadow 0.3s; - transition: color 0.3s, opacity 0.3s ease-out, -webkit-transform 0.3s ease-out, -webkit-box-shadow 0.3s; - -o-transition: color 0.3s, opacity 0.3s ease-out, transform 0.3s ease-out, box-shadow 0.3s; - transition: color 0.3s, opacity 0.3s ease-out, transform 0.3s ease-out, box-shadow 0.3s; - transition: color 0.3s, opacity 0.3s ease-out, transform 0.3s ease-out, box-shadow 0.3s, -webkit-transform 0.3s ease-out, -webkit-box-shadow 0.3s; } - -html, -body { - font-family: "Roboto", sans-serif; - background: #ffffff; } - -ol, -ul { - list-style: none; } - -a { - text-decoration: none; } - -@font-face { - font-family: "socials"; - src: url("fonts/socials.eot?egvu10"); - src: url("fonts/socials.eot?egvu10#iefix") format("embedded-opentype"), url("fonts/socials.ttf?egvu10") format("truetype"), url("fonts/socials.woff?egvu10") format("woff"), url("fonts/socials.svg?egvu10#socials") format("svg"); - font-weight: normal; - font-style: normal; } - -[class^="icon-"], -[class*=" icon-"] { - /* use !important to prevent issues with browser extensions that change fonts */ - font-family: "socials" !important; - speak: none; - font-style: normal; - font-weight: normal; - font-variant: normal; - text-transform: none; - line-height: 1; - /* Better Font Rendering =========== */ - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; } - -.icon-facebook2:before { - content: "\ea91"; } - -.icon-instagram:before { - content: "\ea92"; } - -.icon-twitter:before { - content: "\ea96"; } - -.icon-youtube:before { - content: "\ea9d"; } - -.icon-flickr2:before { - content: "\eaa4"; } - -@font-face { - font-family: "icomoon"; - src: url("fonts/icomoon.eot?mqsjq9"); - src: url("fonts/icomoon.eot?mqsjq9#iefix") format("embedded-opentype"), url("fonts/icomoon.ttf?mqsjq9") format("truetype"), url("fonts/icomoon.woff?mqsjq9") format("woff"), url("fonts/icomoon.svg?mqsjq9#icomoon") format("svg"); - font-weight: normal; - font-style: normal; } - -[class^="icon-"], -[class*=" icon-"] { - /* use !important to prevent issues with browser extensions that change fonts */ - font-family: "icomoon" !important; - speak: none; - font-style: normal; - font-weight: normal; - font-variant: normal; - text-transform: none; - line-height: 1; - /* Better Font Rendering =========== */ - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; } - -.icon-3d_rotation:before { - content: "\e84d"; } - -.icon-ac_unit:before { - content: "\eb3b"; } - -.icon-alarm:before { - content: "\e855"; } - -.icon-access_alarms:before { - content: "\e191"; } - -.icon-schedule:before { - content: "\e8b5"; } - -.icon-accessibility:before { - content: "\e84e"; } - -.icon-accessible:before { - content: "\e914"; } - -.icon-account_balance:before { - content: "\e84f"; } - -.icon-account_balance_wallet:before { - content: "\e850"; } - -.icon-account_box:before { - content: "\e851"; } - -.icon-account_circle:before { - content: "\e853"; } - -.icon-adb:before { - content: "\e60e"; } - -.icon-add:before { - content: "\e145"; } - -.icon-add_a_photo:before { - content: "\e439"; } - -.icon-alarm_add:before { - content: "\e856"; } - -.icon-add_alert:before { - content: "\e003"; } - -.icon-add_box:before { - content: "\e146"; } - -.icon-add_circle:before { - content: "\e147"; } - -.icon-control_point:before { - content: "\e3ba"; } - -.icon-add_location:before { - content: "\e567"; } - -.icon-add_shopping_cart:before { - content: "\e854"; } - -.icon-queue:before { - content: "\e03c"; } - -.icon-add_to_queue:before { - content: "\e05c"; } - -.icon-adjust:before { - content: "\e39e"; } - -.icon-airline_seat_flat:before { - content: "\e630"; } - -.icon-airline_seat_flat_angled:before { - content: "\e631"; } - -.icon-airline_seat_individual_suite:before { - content: "\e632"; } - -.icon-airline_seat_legroom_extra:before { - content: "\e633"; } - -.icon-airline_seat_legroom_normal:before { - content: "\e634"; } - -.icon-airline_seat_legroom_reduced:before { - content: "\e635"; } - -.icon-airline_seat_recline_extra:before { - content: "\e636"; } - -.icon-airline_seat_recline_normal:before { - content: "\e637"; } - -.icon-flight:before { - content: "\e539"; } - -.icon-airplanemode_inactive:before { - content: "\e194"; } - -.icon-airplay:before { - content: "\e055"; } - -.icon-airport_shuttle:before { - content: "\eb3c"; } - -.icon-alarm_off:before { - content: "\e857"; } - -.icon-alarm_on:before { - content: "\e858"; } - -.icon-album:before { - content: "\e019"; } - -.icon-all_inclusive:before { - content: "\eb3d"; } - -.icon-all_out:before { - content: "\e90b"; } - -.icon-android:before { - content: "\e859"; } - -.icon-announcement:before { - content: "\e85a"; } - -.icon-apps:before { - content: "\e5c3"; } - -.icon-archive:before { - content: "\e149"; } - -.icon-arrow_back:before { - content: "\e5c4"; } - -.icon-arrow_downward:before { - content: "\e5db"; } - -.icon-arrow_drop_down:before { - content: "\e5c5"; } - -.icon-arrow_drop_down_circle:before { - content: "\e5c6"; } - -.icon-arrow_drop_up:before { - content: "\e5c7"; } - -.icon-arrow_forward:before { - content: "\e5c8"; } - -.icon-arrow_upward:before { - content: "\e5d8"; } - -.icon-art_track:before { - content: "\e060"; } - -.icon-aspect_ratio:before { - content: "\e85b"; } - -.icon-poll:before { - content: "\e801"; } - -.icon-assignment:before { - content: "\e85d"; } - -.icon-assignment_ind:before { - content: "\e85e"; } - -.icon-assignment_late:before { - content: "\e85f"; } - -.icon-assignment_return:before { - content: "\e860"; } - -.icon-assignment_returned:before { - content: "\e861"; } - -.icon-assignment_turned_in:before { - content: "\e862"; } - -.icon-assistant:before { - content: "\e39f"; } - -.icon-flag:before { - content: "\e153"; } - -.icon-attach_file:before { - content: "\e226"; } - -.icon-attach_money:before { - content: "\e227"; } - -.icon-attachment:before { - content: "\e2bc"; } - -.icon-audiotrack:before { - content: "\e3a1"; } - -.icon-autorenew:before { - content: "\e863"; } - -.icon-av_timer:before { - content: "\e01b"; } - -.icon-backspace:before { - content: "\e14a"; } - -.icon-cloud_upload:before { - content: "\e2c3"; } - -.icon-battery_alert:before { - content: "\e19c"; } - -.icon-battery_charging_full:before { - content: "\e1a3"; } - -.icon-battery_std:before { - content: "\e1a5"; } - -.icon-battery_unknown:before { - content: "\e1a6"; } - -.icon-beach_access:before { - content: "\eb3e"; } - -.icon-beenhere:before { - content: "\e52d"; } - -.icon-block:before { - content: "\e14b"; } - -.icon-bluetooth:before { - content: "\e1a7"; } - -.icon-bluetooth_searching:before { - content: "\e1aa"; } - -.icon-bluetooth_connected:before { - content: "\e1a8"; } - -.icon-bluetooth_disabled:before { - content: "\e1a9"; } - -.icon-blur_circular:before { - content: "\e3a2"; } - -.icon-blur_linear:before { - content: "\e3a3"; } - -.icon-blur_off:before { - content: "\e3a4"; } - -.icon-blur_on:before { - content: "\e3a5"; } - -.icon-class:before { - content: "\e86e"; } - -.icon-turned_in:before { - content: "\e8e6"; } - -.icon-turned_in_not:before { - content: "\e8e7"; } - -.icon-border_all:before { - content: "\e228"; } - -.icon-border_bottom:before { - content: "\e229"; } - -.icon-border_clear:before { - content: "\e22a"; } - -.icon-border_color:before { - content: "\e22b"; } - -.icon-border_horizontal:before { - content: "\e22c"; } - -.icon-border_inner:before { - content: "\e22d"; } - -.icon-border_left:before { - content: "\e22e"; } - -.icon-border_outer:before { - content: "\e22f"; } - -.icon-border_right:before { - content: "\e230"; } - -.icon-border_style:before { - content: "\e231"; } - -.icon-border_top:before { - content: "\e232"; } - -.icon-border_vertical:before { - content: "\e233"; } - -.icon-branding_watermark:before { - content: "\e06b"; } - -.icon-brightness_1:before { - content: "\e3a6"; } - -.icon-brightness_2:before { - content: "\e3a7"; } - -.icon-brightness_3:before { - content: "\e3a8"; } - -.icon-brightness_4:before { - content: "\e3a9"; } - -.icon-brightness_low:before { - content: "\e1ad"; } - -.icon-brightness_medium:before { - content: "\e1ae"; } - -.icon-brightness_high:before { - content: "\e1ac"; } - -.icon-brightness_auto:before { - content: "\e1ab"; } - -.icon-broken_image:before { - content: "\e3ad"; } - -.icon-brush:before { - content: "\e3ae"; } - -.icon-bubble_chart:before { - content: "\e6dd"; } - -.icon-bug_report:before { - content: "\e868"; } - -.icon-build:before { - content: "\e869"; } - -.icon-burst_mode:before { - content: "\e43c"; } - -.icon-domain:before { - content: "\e7ee"; } - -.icon-business_center:before { - content: "\eb3f"; } - -.icon-cached:before { - content: "\e86a"; } - -.icon-cake:before { - content: "\e7e9"; } - -.icon-phone:before { - content: "\e0cd"; } - -.icon-call_end:before { - content: "\e0b1"; } - -.icon-call_made:before { - content: "\e0b2"; } - -.icon-merge_type:before { - content: "\e252"; } - -.icon-call_missed:before { - content: "\e0b4"; } - -.icon-call_missed_outgoing:before { - content: "\e0e4"; } - -.icon-call_received:before { - content: "\e0b5"; } - -.icon-call_split:before { - content: "\e0b6"; } - -.icon-call_to_action:before { - content: "\e06c"; } - -.icon-camera:before { - content: "\e3af"; } - -.icon-photo_camera:before { - content: "\e412"; } - -.icon-camera_enhance:before { - content: "\e8fc"; } - -.icon-camera_front:before { - content: "\e3b1"; } - -.icon-camera_rear:before { - content: "\e3b2"; } - -.icon-camera_roll:before { - content: "\e3b3"; } - -.icon-cancel:before { - content: "\e5c9"; } - -.icon-redeem:before { - content: "\e8b1"; } - -.icon-card_membership:before { - content: "\e8f7"; } - -.icon-card_travel:before { - content: "\e8f8"; } - -.icon-casino:before { - content: "\eb40"; } - -.icon-cast:before { - content: "\e307"; } - -.icon-cast_connected:before { - content: "\e308"; } - -.icon-center_focus_strong:before { - content: "\e3b4"; } - -.icon-center_focus_weak:before { - content: "\e3b5"; } - -.icon-change_history:before { - content: "\e86b"; } - -.icon-chat:before { - content: "\e0b7"; } - -.icon-chat_bubble:before { - content: "\e0ca"; } - -.icon-chat_bubble_outline:before { - content: "\e0cb"; } - -.icon-check:before { - content: "\e5ca"; } - -.icon-check_box:before { - content: "\e834"; } - -.icon-check_box_outline_blank:before { - content: "\e835"; } - -.icon-check_circle:before { - content: "\e86c"; } - -.icon-navigate_before:before { - content: "\e408"; } - -.icon-navigate_next:before { - content: "\e409"; } - -.icon-child_care:before { - content: "\eb41"; } - -.icon-child_friendly:before { - content: "\eb42"; } - -.icon-chrome_reader_mode:before { - content: "\e86d"; } - -.icon-close:before { - content: "\e5cd"; } - -.icon-clear_all:before { - content: "\e0b8"; } - -.icon-closed_caption:before { - content: "\e01c"; } - -.icon-wb_cloudy:before { - content: "\e42d"; } - -.icon-cloud_circle:before { - content: "\e2be"; } - -.icon-cloud_done:before { - content: "\e2bf"; } - -.icon-cloud_download:before { - content: "\e2c0"; } - -.icon-cloud_off:before { - content: "\e2c1"; } - -.icon-cloud_queue:before { - content: "\e2c2"; } - -.icon-code:before { - content: "\e86f"; } - -.icon-photo_library:before { - content: "\e413"; } - -.icon-collections_bookmark:before { - content: "\e431"; } - -.icon-palette:before { - content: "\e40a"; } - -.icon-colorize:before { - content: "\e3b8"; } - -.icon-comment:before { - content: "\e0b9"; } - -.icon-compare:before { - content: "\e3b9"; } - -.icon-compare_arrows:before { - content: "\e915"; } - -.icon-laptop:before { - content: "\e31e"; } - -.icon-confirmation_number:before { - content: "\e638"; } - -.icon-contact_mail:before { - content: "\e0d0"; } - -.icon-contact_phone:before { - content: "\e0cf"; } - -.icon-contacts:before { - content: "\e0ba"; } - -.icon-content_copy:before { - content: "\e14d"; } - -.icon-content_cut:before { - content: "\e14e"; } - -.icon-content_paste:before { - content: "\e14f"; } - -.icon-control_point_duplicate:before { - content: "\e3bb"; } - -.icon-copyright:before { - content: "\e90c"; } - -.icon-mode_edit:before { - content: "\e254"; } - -.icon-create_new_folder:before { - content: "\e2cc"; } - -.icon-payment:before { - content: "\e8a1"; } - -.icon-crop:before { - content: "\e3be"; } - -.icon-crop_16_9:before { - content: "\e3bc"; } - -.icon-crop_3_2:before { - content: "\e3bd"; } - -.icon-crop_landscape:before { - content: "\e3c3"; } - -.icon-crop_7_5:before { - content: "\e3c0"; } - -.icon-crop_din:before { - content: "\e3c1"; } - -.icon-crop_free:before { - content: "\e3c2"; } - -.icon-crop_original:before { - content: "\e3c4"; } - -.icon-crop_portrait:before { - content: "\e3c5"; } - -.icon-crop_rotate:before { - content: "\e437"; } - -.icon-crop_square:before { - content: "\e3c6"; } - -.icon-dashboard:before { - content: "\e871"; } - -.icon-data_usage:before { - content: "\e1af"; } - -.icon-date_range:before { - content: "\e916"; } - -.icon-dehaze:before { - content: "\e3c7"; } - -.icon-delete:before { - content: "\e872"; } - -.icon-delete_forever:before { - content: "\e92b"; } - -.icon-delete_sweep:before { - content: "\e16c"; } - -.icon-description:before { - content: "\e873"; } - -.icon-desktop_mac:before { - content: "\e30b"; } - -.icon-desktop_windows:before { - content: "\e30c"; } - -.icon-details:before { - content: "\e3c8"; } - -.icon-developer_board:before { - content: "\e30d"; } - -.icon-developer_mode:before { - content: "\e1b0"; } - -.icon-device_hub:before { - content: "\e335"; } - -.icon-phonelink:before { - content: "\e326"; } - -.icon-devices_other:before { - content: "\e337"; } - -.icon-dialer_sip:before { - content: "\e0bb"; } - -.icon-dialpad:before { - content: "\e0bc"; } - -.icon-directions:before { - content: "\e52e"; } - -.icon-directions_bike:before { - content: "\e52f"; } - -.icon-directions_boat:before { - content: "\e532"; } - -.icon-directions_bus:before { - content: "\e530"; } - -.icon-directions_car:before { - content: "\e531"; } - -.icon-directions_railway:before { - content: "\e534"; } - -.icon-directions_run:before { - content: "\e566"; } - -.icon-directions_transit:before { - content: "\e535"; } - -.icon-directions_walk:before { - content: "\e536"; } - -.icon-disc_full:before { - content: "\e610"; } - -.icon-dns:before { - content: "\e875"; } - -.icon-not_interested:before { - content: "\e033"; } - -.icon-do_not_disturb_alt:before { - content: "\e611"; } - -.icon-do_not_disturb_off:before { - content: "\e643"; } - -.icon-remove_circle:before { - content: "\e15c"; } - -.icon-dock:before { - content: "\e30e"; } - -.icon-done:before { - content: "\e876"; } - -.icon-done_all:before { - content: "\e877"; } - -.icon-donut_large:before { - content: "\e917"; } - -.icon-donut_small:before { - content: "\e918"; } - -.icon-drafts:before { - content: "\e151"; } - -.icon-drag_handle:before { - content: "\e25d"; } - -.icon-time_to_leave:before { - content: "\e62c"; } - -.icon-dvr:before { - content: "\e1b2"; } - -.icon-edit_location:before { - content: "\e568"; } - -.icon-eject:before { - content: "\e8fb"; } - -.icon-markunread:before { - content: "\e159"; } - -.icon-enhanced_encryption:before { - content: "\e63f"; } - -.icon-equalizer:before { - content: "\e01d"; } - -.icon-error:before { - content: "\e000"; } - -.icon-error_outline:before { - content: "\e001"; } - -.icon-euro_symbol:before { - content: "\e926"; } - -.icon-ev_station:before { - content: "\e56d"; } - -.icon-insert_invitation:before { - content: "\e24f"; } - -.icon-event_available:before { - content: "\e614"; } - -.icon-event_busy:before { - content: "\e615"; } - -.icon-event_note:before { - content: "\e616"; } - -.icon-event_seat:before { - content: "\e903"; } - -.icon-exit_to_app:before { - content: "\e879"; } - -.icon-expand_less:before { - content: "\e5ce"; } - -.icon-expand_more:before { - content: "\e5cf"; } - -.icon-explicit:before { - content: "\e01e"; } - -.icon-explore:before { - content: "\e87a"; } - -.icon-exposure:before { - content: "\e3ca"; } - -.icon-exposure_neg_1:before { - content: "\e3cb"; } - -.icon-exposure_neg_2:before { - content: "\e3cc"; } - -.icon-exposure_plus_1:before { - content: "\e3cd"; } - -.icon-exposure_plus_2:before { - content: "\e3ce"; } - -.icon-exposure_zero:before { - content: "\e3cf"; } - -.icon-extension:before { - content: "\e87b"; } - -.icon-face:before { - content: "\e87c"; } - -.icon-fast_forward:before { - content: "\e01f"; } - -.icon-fast_rewind:before { - content: "\e020"; } - -.icon-favorite:before { - content: "\e87d"; } - -.icon-favorite_border:before { - content: "\e87e"; } - -.icon-featured_play_list:before { - content: "\e06d"; } - -.icon-featured_video:before { - content: "\e06e"; } - -.icon-sms_failed:before { - content: "\e626"; } - -.icon-fiber_dvr:before { - content: "\e05d"; } - -.icon-fiber_manual_record:before { - content: "\e061"; } - -.icon-fiber_new:before { - content: "\e05e"; } - -.icon-fiber_pin:before { - content: "\e06a"; } - -.icon-fiber_smart_record:before { - content: "\e062"; } - -.icon-get_app:before { - content: "\e884"; } - -.icon-file_upload:before { - content: "\e2c6"; } - -.icon-filter:before { - content: "\e3d3"; } - -.icon-filter_1:before { - content: "\e3d0"; } - -.icon-filter_2:before { - content: "\e3d1"; } - -.icon-filter_3:before { - content: "\e3d2"; } - -.icon-filter_4:before { - content: "\e3d4"; } - -.icon-filter_5:before { - content: "\e3d5"; } - -.icon-filter_6:before { - content: "\e3d6"; } - -.icon-filter_7:before { - content: "\e3d7"; } - -.icon-filter_8:before { - content: "\e3d8"; } - -.icon-filter_9:before { - content: "\e3d9"; } - -.icon-filter_9_plus:before { - content: "\e3da"; } - -.icon-filter_b_and_w:before { - content: "\e3db"; } - -.icon-filter_center_focus:before { - content: "\e3dc"; } - -.icon-filter_drama:before { - content: "\e3dd"; } - -.icon-filter_frames:before { - content: "\e3de"; } - -.icon-terrain:before { - content: "\e564"; } - -.icon-filter_list:before { - content: "\e152"; } - -.icon-filter_none:before { - content: "\e3e0"; } - -.icon-filter_tilt_shift:before { - content: "\e3e2"; } - -.icon-filter_vintage:before { - content: "\e3e3"; } - -.icon-find_in_page:before { - content: "\e880"; } - -.icon-find_replace:before { - content: "\e881"; } - -.icon-fingerprint:before { - content: "\e90d"; } - -.icon-first_page:before { - content: "\e5dc"; } - -.icon-fitness_center:before { - content: "\eb43"; } - -.icon-flare:before { - content: "\e3e4"; } - -.icon-flash_auto:before { - content: "\e3e5"; } - -.icon-flash_off:before { - content: "\e3e6"; } - -.icon-flash_on:before { - content: "\e3e7"; } - -.icon-flight_land:before { - content: "\e904"; } - -.icon-flight_takeoff:before { - content: "\e905"; } - -.icon-flip:before { - content: "\e3e8"; } - -.icon-flip_to_back:before { - content: "\e882"; } - -.icon-flip_to_front:before { - content: "\e883"; } - -.icon-folder:before { - content: "\e2c7"; } - -.icon-folder_open:before { - content: "\e2c8"; } - -.icon-folder_shared:before { - content: "\e2c9"; } - -.icon-folder_special:before { - content: "\e617"; } - -.icon-font_download:before { - content: "\e167"; } - -.icon-format_align_center:before { - content: "\e234"; } - -.icon-format_align_justify:before { - content: "\e235"; } - -.icon-format_align_left:before { - content: "\e236"; } - -.icon-format_align_right:before { - content: "\e237"; } - -.icon-format_bold:before { - content: "\e238"; } - -.icon-format_clear:before { - content: "\e239"; } - -.icon-format_color_fill:before { - content: "\e23a"; } - -.icon-format_color_reset:before { - content: "\e23b"; } - -.icon-format_color_text:before { - content: "\e23c"; } - -.icon-format_indent_decrease:before { - content: "\e23d"; } - -.icon-format_indent_increase:before { - content: "\e23e"; } - -.icon-format_italic:before { - content: "\e23f"; } - -.icon-format_line_spacing:before { - content: "\e240"; } - -.icon-format_list_bulleted:before { - content: "\e241"; } - -.icon-format_list_numbered:before { - content: "\e242"; } - -.icon-format_paint:before { - content: "\e243"; } - -.icon-format_quote:before { - content: "\e244"; } - -.icon-format_shapes:before { - content: "\e25e"; } - -.icon-format_size:before { - content: "\e245"; } - -.icon-format_strikethrough:before { - content: "\e246"; } - -.icon-format_textdirection_l_to_r:before { - content: "\e247"; } - -.icon-format_textdirection_r_to_l:before { - content: "\e248"; } - -.icon-format_underlined:before { - content: "\e249"; } - -.icon-question_answer:before { - content: "\e8af"; } - -.icon-forward:before { - content: "\e154"; } - -.icon-forward_10:before { - content: "\e056"; } - -.icon-forward_30:before { - content: "\e057"; } - -.icon-forward_5:before { - content: "\e058"; } - -.icon-free_breakfast:before { - content: "\eb44"; } - -.icon-fullscreen:before { - content: "\e5d0"; } - -.icon-fullscreen_exit:before { - content: "\e5d1"; } - -.icon-functions:before { - content: "\e24a"; } - -.icon-g_translate:before { - content: "\e927"; } - -.icon-games:before { - content: "\e021"; } - -.icon-gavel:before { - content: "\e90e"; } - -.icon-gesture:before { - content: "\e155"; } - -.icon-gif:before { - content: "\e908"; } - -.icon-goat:before { - content: "\e900"; } - -.icon-golf_course:before { - content: "\eb45"; } - -.icon-my_location:before { - content: "\e55c"; } - -.icon-location_searching:before { - content: "\e1b7"; } - -.icon-location_disabled:before { - content: "\e1b6"; } - -.icon-star:before { - content: "\e838"; } - -.icon-gradient:before { - content: "\e3e9"; } - -.icon-grain:before { - content: "\e3ea"; } - -.icon-graphic_eq:before { - content: "\e1b8"; } - -.icon-grid_off:before { - content: "\e3eb"; } - -.icon-grid_on:before { - content: "\e3ec"; } - -.icon-people:before { - content: "\e7fb"; } - -.icon-group_add:before { - content: "\e7f0"; } - -.icon-group_work:before { - content: "\e886"; } - -.icon-hd:before { - content: "\e052"; } - -.icon-hdr_off:before { - content: "\e3ed"; } - -.icon-hdr_on:before { - content: "\e3ee"; } - -.icon-hdr_strong:before { - content: "\e3f1"; } - -.icon-hdr_weak:before { - content: "\e3f2"; } - -.icon-headset:before { - content: "\e310"; } - -.icon-headset_mic:before { - content: "\e311"; } - -.icon-healing:before { - content: "\e3f3"; } - -.icon-hearing:before { - content: "\e023"; } - -.icon-help:before { - content: "\e887"; } - -.icon-help_outline:before { - content: "\e8fd"; } - -.icon-high_quality:before { - content: "\e024"; } - -.icon-highlight:before { - content: "\e25f"; } - -.icon-highlight_off:before { - content: "\e888"; } - -.icon-restore:before { - content: "\e8b3"; } - -.icon-home:before { - content: "\e88a"; } - -.icon-hot_tub:before { - content: "\eb46"; } - -.icon-local_hotel:before { - content: "\e549"; } - -.icon-hourglass_empty:before { - content: "\e88b"; } - -.icon-hourglass_full:before { - content: "\e88c"; } - -.icon-http:before { - content: "\e902"; } - -.icon-lock:before { - content: "\e897"; } - -.icon-photo:before { - content: "\e410"; } - -.icon-image_aspect_ratio:before { - content: "\e3f5"; } - -.icon-import_contacts:before { - content: "\e0e0"; } - -.icon-import_export:before { - content: "\e0c3"; } - -.icon-important_devices:before { - content: "\e912"; } - -.icon-inbox:before { - content: "\e156"; } - -.icon-indeterminate_check_box:before { - content: "\e909"; } - -.icon-info:before { - content: "\e88e"; } - -.icon-info_outline:before { - content: "\e88f"; } - -.icon-input:before { - content: "\e890"; } - -.icon-insert_comment:before { - content: "\e24c"; } - -.icon-insert_drive_file:before { - content: "\e24d"; } - -.icon-tag_faces:before { - content: "\e420"; } - -.icon-link:before { - content: "\e157"; } - -.icon-invert_colors:before { - content: "\e891"; } - -.icon-invert_colors_off:before { - content: "\e0c4"; } - -.icon-iso:before { - content: "\e3f6"; } - -.icon-keyboard:before { - content: "\e312"; } - -.icon-keyboard_arrow_down:before { - content: "\e313"; } - -.icon-keyboard_arrow_left:before { - content: "\e314"; } - -.icon-keyboard_arrow_right:before { - content: "\e315"; } - -.icon-keyboard_arrow_up:before { - content: "\e316"; } - -.icon-keyboard_backspace:before { - content: "\e317"; } - -.icon-keyboard_capslock:before { - content: "\e318"; } - -.icon-keyboard_hide:before { - content: "\e31a"; } - -.icon-keyboard_return:before { - content: "\e31b"; } - -.icon-keyboard_tab:before { - content: "\e31c"; } - -.icon-keyboard_voice:before { - content: "\e31d"; } - -.icon-kitchen:before { - content: "\eb47"; } - -.icon-label:before { - content: "\e892"; } - -.icon-label_outline:before { - content: "\e893"; } - -.icon-language:before { - content: "\e894"; } - -.icon-laptop_chromebook:before { - content: "\e31f"; } - -.icon-laptop_mac:before { - content: "\e320"; } - -.icon-laptop_windows:before { - content: "\e321"; } - -.icon-last_page:before { - content: "\e5dd"; } - -.icon-open_in_new:before { - content: "\e89e"; } - -.icon-layers:before { - content: "\e53b"; } - -.icon-layers_clear:before { - content: "\e53c"; } - -.icon-leak_add:before { - content: "\e3f8"; } - -.icon-leak_remove:before { - content: "\e3f9"; } - -.icon-lens:before { - content: "\e3fa"; } - -.icon-library_books:before { - content: "\e02f"; } - -.icon-library_music:before { - content: "\e030"; } - -.icon-lightbulb_outline:before { - content: "\e90f"; } - -.icon-line_style:before { - content: "\e919"; } - -.icon-line_weight:before { - content: "\e91a"; } - -.icon-linear_scale:before { - content: "\e260"; } - -.icon-linked_camera:before { - content: "\e438"; } - -.icon-list:before { - content: "\e896"; } - -.icon-live_help:before { - content: "\e0c6"; } - -.icon-live_tv:before { - content: "\e639"; } - -.icon-local_play:before { - content: "\e553"; } - -.icon-local_airport:before { - content: "\e53d"; } - -.icon-local_atm:before { - content: "\e53e"; } - -.icon-local_bar:before { - content: "\e540"; } - -.icon-local_cafe:before { - content: "\e541"; } - -.icon-local_car_wash:before { - content: "\e542"; } - -.icon-local_convenience_store:before { - content: "\e543"; } - -.icon-restaurant_menu:before { - content: "\e561"; } - -.icon-local_drink:before { - content: "\e544"; } - -.icon-local_florist:before { - content: "\e545"; } - -.icon-local_gas_station:before { - content: "\e546"; } - -.icon-shopping_cart:before { - content: "\e8cc"; } - -.icon-local_hospital:before { - content: "\e548"; } - -.icon-local_laundry_service:before { - content: "\e54a"; } - -.icon-local_library:before { - content: "\e54b"; } - -.icon-local_mall:before { - content: "\e54c"; } - -.icon-theaters:before { - content: "\e8da"; } - -.icon-local_offer:before { - content: "\e54e"; } - -.icon-local_parking:before { - content: "\e54f"; } - -.icon-local_pharmacy:before { - content: "\e550"; } - -.icon-local_pizza:before { - content: "\e552"; } - -.icon-print:before { - content: "\e8ad"; } - -.icon-local_shipping:before { - content: "\e558"; } - -.icon-local_taxi:before { - content: "\e559"; } - -.icon-location_city:before { - content: "\e7f1"; } - -.icon-location_off:before { - content: "\e0c7"; } - -.icon-room:before { - content: "\e8b4"; } - -.icon-lock_open:before { - content: "\e898"; } - -.icon-lock_outline:before { - content: "\e899"; } - -.icon-looks:before { - content: "\e3fc"; } - -.icon-looks_3:before { - content: "\e3fb"; } - -.icon-looks_4:before { - content: "\e3fd"; } - -.icon-looks_5:before { - content: "\e3fe"; } - -.icon-looks_6:before { - content: "\e3ff"; } - -.icon-looks_one:before { - content: "\e400"; } - -.icon-looks_two:before { - content: "\e401"; } - -.icon-sync:before { - content: "\e627"; } - -.icon-loupe:before { - content: "\e402"; } - -.icon-low_priority:before { - content: "\e16d"; } - -.icon-loyalty:before { - content: "\e89a"; } - -.icon-mail_outline:before { - content: "\e0e1"; } - -.icon-map:before { - content: "\e55b"; } - -.icon-markunread_mailbox:before { - content: "\e89b"; } - -.icon-memory:before { - content: "\e322"; } - -.icon-menu:before { - content: "\e5d2"; } - -.icon-message:before { - content: "\e0c9"; } - -.icon-mic:before { - content: "\e029"; } - -.icon-mic_none:before { - content: "\e02a"; } - -.icon-mic_off:before { - content: "\e02b"; } - -.icon-mms:before { - content: "\e618"; } - -.icon-mode_comment:before { - content: "\e253"; } - -.icon-monetization_on:before { - content: "\e263"; } - -.icon-money_off:before { - content: "\e25c"; } - -.icon-monochrome_photos:before { - content: "\e403"; } - -.icon-mood_bad:before { - content: "\e7f3"; } - -.icon-more:before { - content: "\e619"; } - -.icon-more_horiz:before { - content: "\e5d3"; } - -.icon-more_vert:before { - content: "\e5d4"; } - -.icon-motorcycle:before { - content: "\e91b"; } - -.icon-mouse:before { - content: "\e323"; } - -.icon-move_to_inbox:before { - content: "\e168"; } - -.icon-movie_creation:before { - content: "\e404"; } - -.icon-movie_filter:before { - content: "\e43a"; } - -.icon-multiline_chart:before { - content: "\e6df"; } - -.icon-music_note:before { - content: "\e405"; } - -.icon-music_video:before { - content: "\e063"; } - -.icon-nature:before { - content: "\e406"; } - -.icon-nature_people:before { - content: "\e407"; } - -.icon-navigation:before { - content: "\e55d"; } - -.icon-near_me:before { - content: "\e569"; } - -.icon-network_cell:before { - content: "\e1b9"; } - -.icon-network_check:before { - content: "\e640"; } - -.icon-network_locked:before { - content: "\e61a"; } - -.icon-network_wifi:before { - content: "\e1ba"; } - -.icon-new_releases:before { - content: "\e031"; } - -.icon-next_week:before { - content: "\e16a"; } - -.icon-nfc:before { - content: "\e1bb"; } - -.icon-no_encryption:before { - content: "\e641"; } - -.icon-signal_cellular_no_sim:before { - content: "\e1ce"; } - -.icon-note:before { - content: "\e06f"; } - -.icon-note_add:before { - content: "\e89c"; } - -.icon-notifications:before { - content: "\e7f4"; } - -.icon-notifications_active:before { - content: "\e7f7"; } - -.icon-notifications_none:before { - content: "\e7f5"; } - -.icon-notifications_off:before { - content: "\e7f6"; } - -.icon-notifications_paused:before { - content: "\e7f8"; } - -.icon-offline_pin:before { - content: "\e90a"; } - -.icon-ondemand_video:before { - content: "\e63a"; } - -.icon-opacity:before { - content: "\e91c"; } - -.icon-open_in_browser:before { - content: "\e89d"; } - -.icon-open_with:before { - content: "\e89f"; } - -.icon-pages:before { - content: "\e7f9"; } - -.icon-pageview:before { - content: "\e8a0"; } - -.icon-pan_tool:before { - content: "\e925"; } - -.icon-panorama:before { - content: "\e40b"; } - -.icon-radio_button_unchecked:before { - content: "\e836"; } - -.icon-panorama_horizontal:before { - content: "\e40d"; } - -.icon-panorama_vertical:before { - content: "\e40e"; } - -.icon-panorama_wide_angle:before { - content: "\e40f"; } - -.icon-party_mode:before { - content: "\e7fa"; } - -.icon-pause:before { - content: "\e034"; } - -.icon-pause_circle_filled:before { - content: "\e035"; } - -.icon-pause_circle_outline:before { - content: "\e036"; } - -.icon-people_outline:before { - content: "\e7fc"; } - -.icon-perm_camera_mic:before { - content: "\e8a2"; } - -.icon-perm_contact_calendar:before { - content: "\e8a3"; } - -.icon-perm_data_setting:before { - content: "\e8a4"; } - -.icon-perm_device_information:before { - content: "\e8a5"; } - -.icon-person_outline:before { - content: "\e7ff"; } - -.icon-perm_media:before { - content: "\e8a7"; } - -.icon-perm_phone_msg:before { - content: "\e8a8"; } - -.icon-perm_scan_wifi:before { - content: "\e8a9"; } - -.icon-person:before { - content: "\e7fd"; } - -.icon-person_add:before { - content: "\e7fe"; } - -.icon-person_pin:before { - content: "\e55a"; } - -.icon-person_pin_circle:before { - content: "\e56a"; } - -.icon-personal_video:before { - content: "\e63b"; } - -.icon-pets:before { - content: "\e91d"; } - -.icon-phone_android:before { - content: "\e324"; } - -.icon-phone_bluetooth_speaker:before { - content: "\e61b"; } - -.icon-phone_forwarded:before { - content: "\e61c"; } - -.icon-phone_in_talk:before { - content: "\e61d"; } - -.icon-phone_iphone:before { - content: "\e325"; } - -.icon-phone_locked:before { - content: "\e61e"; } - -.icon-phone_missed:before { - content: "\e61f"; } - -.icon-phone_paused:before { - content: "\e620"; } - -.icon-phonelink_erase:before { - content: "\e0db"; } - -.icon-phonelink_lock:before { - content: "\e0dc"; } - -.icon-phonelink_off:before { - content: "\e327"; } - -.icon-phonelink_ring:before { - content: "\e0dd"; } - -.icon-phonelink_setup:before { - content: "\e0de"; } - -.icon-photo_album:before { - content: "\e411"; } - -.icon-photo_filter:before { - content: "\e43b"; } - -.icon-photo_size_select_actual:before { - content: "\e432"; } - -.icon-photo_size_select_large:before { - content: "\e433"; } - -.icon-photo_size_select_small:before { - content: "\e434"; } - -.icon-picture_as_pdf:before { - content: "\e415"; } - -.icon-picture_in_picture:before { - content: "\e8aa"; } - -.icon-picture_in_picture_alt:before { - content: "\e911"; } - -.icon-pie_chart:before { - content: "\e6c4"; } - -.icon-pie_chart_outlined:before { - content: "\e6c5"; } - -.icon-pin_drop:before { - content: "\e55e"; } - -.icon-play_arrow:before { - content: "\e037"; } - -.icon-play_circle_filled:before { - content: "\e038"; } - -.icon-play_circle_outline:before { - content: "\e039"; } - -.icon-play_for_work:before { - content: "\e906"; } - -.icon-playlist_add:before { - content: "\e03b"; } - -.icon-playlist_add_check:before { - content: "\e065"; } - -.icon-playlist_play:before { - content: "\e05f"; } - -.icon-plus_one:before { - content: "\e800"; } - -.icon-polymer:before { - content: "\e8ab"; } - -.icon-pool:before { - content: "\eb48"; } - -.icon-portable_wifi_off:before { - content: "\e0ce"; } - -.icon-portrait:before { - content: "\e416"; } - -.icon-power:before { - content: "\e63c"; } - -.icon-power_input:before { - content: "\e336"; } - -.icon-power_settings_new:before { - content: "\e8ac"; } - -.icon-pregnant_woman:before { - content: "\e91e"; } - -.icon-present_to_all:before { - content: "\e0df"; } - -.icon-priority_high:before { - content: "\e645"; } - -.icon-public:before { - content: "\e80b"; } - -.icon-publish:before { - content: "\e255"; } - -.icon-queue_music:before { - content: "\e03d"; } - -.icon-queue_play_next:before { - content: "\e066"; } - -.icon-radio:before { - content: "\e03e"; } - -.icon-radio_button_checked:before { - content: "\e837"; } - -.icon-rate_review:before { - content: "\e560"; } - -.icon-receipt:before { - content: "\e8b0"; } - -.icon-recent_actors:before { - content: "\e03f"; } - -.icon-record_voice_over:before { - content: "\e91f"; } - -.icon-redo:before { - content: "\e15a"; } - -.icon-refresh:before { - content: "\e5d5"; } - -.icon-remove:before { - content: "\e15b"; } - -.icon-remove_circle_outline:before { - content: "\e15d"; } - -.icon-remove_from_queue:before { - content: "\e067"; } - -.icon-visibility:before { - content: "\e8f4"; } - -.icon-remove_shopping_cart:before { - content: "\e928"; } - -.icon-reorder:before { - content: "\e8fe"; } - -.icon-repeat:before { - content: "\e040"; } - -.icon-repeat_one:before { - content: "\e041"; } - -.icon-replay:before { - content: "\e042"; } - -.icon-replay_10:before { - content: "\e059"; } - -.icon-replay_30:before { - content: "\e05a"; } - -.icon-replay_5:before { - content: "\e05b"; } - -.icon-reply:before { - content: "\e15e"; } - -.icon-reply_all:before { - content: "\e15f"; } - -.icon-report:before { - content: "\e160"; } - -.icon-warning:before { - content: "\e002"; } - -.icon-restaurant:before { - content: "\e56c"; } - -.icon-restore_page:before { - content: "\e929"; } - -.icon-ring_volume:before { - content: "\e0d1"; } - -.icon-room_service:before { - content: "\eb49"; } - -.icon-rotate_90_degrees_ccw:before { - content: "\e418"; } - -.icon-rotate_left:before { - content: "\e419"; } - -.icon-rotate_right:before { - content: "\e41a"; } - -.icon-rounded_corner:before { - content: "\e920"; } - -.icon-router:before { - content: "\e328"; } - -.icon-rowing:before { - content: "\e921"; } - -.icon-rss_feed:before { - content: "\e0e5"; } - -.icon-rv_hookup:before { - content: "\e642"; } - -.icon-satellite:before { - content: "\e562"; } - -.icon-save:before { - content: "\e161"; } - -.icon-scanner:before { - content: "\e329"; } - -.icon-school:before { - content: "\e80c"; } - -.icon-screen_lock_landscape:before { - content: "\e1be"; } - -.icon-screen_lock_portrait:before { - content: "\e1bf"; } - -.icon-screen_lock_rotation:before { - content: "\e1c0"; } - -.icon-screen_rotation:before { - content: "\e1c1"; } - -.icon-screen_share:before { - content: "\e0e2"; } - -.icon-sd_storage:before { - content: "\e1c2"; } - -.icon-search:before { - content: "\e8b6"; } - -.icon-security:before { - content: "\e32a"; } - -.icon-select_all:before { - content: "\e162"; } - -.icon-send:before { - content: "\e163"; } - -.icon-sentiment_dissatisfied:before { - content: "\e811"; } - -.icon-sentiment_neutral:before { - content: "\e812"; } - -.icon-sentiment_satisfied:before { - content: "\e813"; } - -.icon-sentiment_very_dissatisfied:before { - content: "\e814"; } - -.icon-sentiment_very_satisfied:before { - content: "\e815"; } - -.icon-settings:before { - content: "\e8b8"; } - -.icon-settings_applications:before { - content: "\e8b9"; } - -.icon-settings_backup_restore:before { - content: "\e8ba"; } - -.icon-settings_bluetooth:before { - content: "\e8bb"; } - -.icon-settings_brightness:before { - content: "\e8bd"; } - -.icon-settings_cell:before { - content: "\e8bc"; } - -.icon-settings_ethernet:before { - content: "\e8be"; } - -.icon-settings_input_antenna:before { - content: "\e8bf"; } - -.icon-settings_input_composite:before { - content: "\e8c1"; } - -.icon-settings_input_hdmi:before { - content: "\e8c2"; } - -.icon-settings_input_svideo:before { - content: "\e8c3"; } - -.icon-settings_overscan:before { - content: "\e8c4"; } - -.icon-settings_phone:before { - content: "\e8c5"; } - -.icon-settings_power:before { - content: "\e8c6"; } - -.icon-settings_remote:before { - content: "\e8c7"; } - -.icon-settings_system_daydream:before { - content: "\e1c3"; } - -.icon-settings_voice:before { - content: "\e8c8"; } - -.icon-share:before { - content: "\e80d"; } - -.icon-shop:before { - content: "\e8c9"; } - -.icon-shop_two:before { - content: "\e8ca"; } - -.icon-shopping_basket:before { - content: "\e8cb"; } - -.icon-short_text:before { - content: "\e261"; } - -.icon-show_chart:before { - content: "\e6e1"; } - -.icon-shuffle:before { - content: "\e043"; } - -.icon-signal_cellular_4_bar:before { - content: "\e1c8"; } - -.icon-signal_cellular_connected_no_internet_4_bar:before { - content: "\e1cd"; } - -.icon-signal_cellular_null:before { - content: "\e1cf"; } - -.icon-signal_cellular_off:before { - content: "\e1d0"; } - -.icon-signal_wifi_4_bar:before { - content: "\e1d8"; } - -.icon-signal_wifi_4_bar_lock:before { - content: "\e1d9"; } - -.icon-signal_wifi_off:before { - content: "\e1da"; } - -.icon-sim_card:before { - content: "\e32b"; } - -.icon-sim_card_alert:before { - content: "\e624"; } - -.icon-skip_next:before { - content: "\e044"; } - -.icon-skip_previous:before { - content: "\e045"; } - -.icon-slideshow:before { - content: "\e41b"; } - -.icon-slow_motion_video:before { - content: "\e068"; } - -.icon-stay_primary_portrait:before { - content: "\e0d6"; } - -.icon-smoke_free:before { - content: "\eb4a"; } - -.icon-smoking_rooms:before { - content: "\eb4b"; } - -.icon-textsms:before { - content: "\e0d8"; } - -.icon-snooze:before { - content: "\e046"; } - -.icon-sort:before { - content: "\e164"; } - -.icon-sort_by_alpha:before { - content: "\e053"; } - -.icon-spa:before { - content: "\eb4c"; } - -.icon-space_bar:before { - content: "\e256"; } - -.icon-speaker:before { - content: "\e32d"; } - -.icon-speaker_group:before { - content: "\e32e"; } - -.icon-speaker_notes:before { - content: "\e8cd"; } - -.icon-speaker_notes_off:before { - content: "\e92a"; } - -.icon-speaker_phone:before { - content: "\e0d2"; } - -.icon-spellcheck:before { - content: "\e8ce"; } - -.icon-star_border:before { - content: "\e83a"; } - -.icon-star_half:before { - content: "\e839"; } - -.icon-stars:before { - content: "\e8d0"; } - -.icon-stay_primary_landscape:before { - content: "\e0d5"; } - -.icon-stop:before { - content: "\e047"; } - -.icon-stop_screen_share:before { - content: "\e0e3"; } - -.icon-storage:before { - content: "\e1db"; } - -.icon-store_mall_directory:before { - content: "\e563"; } - -.icon-straighten:before { - content: "\e41c"; } - -.icon-streetview:before { - content: "\e56e"; } - -.icon-strikethrough_s:before { - content: "\e257"; } - -.icon-style:before { - content: "\e41d"; } - -.icon-subdirectory_arrow_left:before { - content: "\e5d9"; } - -.icon-subdirectory_arrow_right:before { - content: "\e5da"; } - -.icon-subject:before { - content: "\e8d2"; } - -.icon-subscriptions:before { - content: "\e064"; } - -.icon-subtitles:before { - content: "\e048"; } - -.icon-subway:before { - content: "\e56f"; } - -.icon-supervisor_account:before { - content: "\e8d3"; } - -.icon-surround_sound:before { - content: "\e049"; } - -.icon-swap_calls:before { - content: "\e0d7"; } - -.icon-swap_horiz:before { - content: "\e8d4"; } - -.icon-swap_vert:before { - content: "\e8d5"; } - -.icon-swap_vertical_circle:before { - content: "\e8d6"; } - -.icon-switch_camera:before { - content: "\e41e"; } - -.icon-switch_video:before { - content: "\e41f"; } - -.icon-sync_disabled:before { - content: "\e628"; } - -.icon-sync_problem:before { - content: "\e629"; } - -.icon-system_update:before { - content: "\e62a"; } - -.icon-system_update_alt:before { - content: "\e8d7"; } - -.icon-tab:before { - content: "\e8d8"; } - -.icon-tab_unselected:before { - content: "\e8d9"; } - -.icon-tablet:before { - content: "\e32f"; } - -.icon-tablet_android:before { - content: "\e330"; } - -.icon-tablet_mac:before { - content: "\e331"; } - -.icon-tap_and_play:before { - content: "\e62b"; } - -.icon-text_fields:before { - content: "\e262"; } - -.icon-text_format:before { - content: "\e165"; } - -.icon-texture:before { - content: "\e421"; } - -.icon-thumb_down:before { - content: "\e8db"; } - -.icon-thumb_up:before { - content: "\e8dc"; } - -.icon-thumbs_up_down:before { - content: "\e8dd"; } - -.icon-timelapse:before { - content: "\e422"; } - -.icon-timeline:before { - content: "\e922"; } - -.icon-timer:before { - content: "\e425"; } - -.icon-timer_10:before { - content: "\e423"; } - -.icon-timer_3:before { - content: "\e424"; } - -.icon-timer_off:before { - content: "\e426"; } - -.icon-title:before { - content: "\e264"; } - -.icon-toc:before { - content: "\e8de"; } - -.icon-today:before { - content: "\e8df"; } - -.icon-toll:before { - content: "\e8e0"; } - -.icon-tonality:before { - content: "\e427"; } - -.icon-touch_app:before { - content: "\e913"; } - -.icon-toys:before { - content: "\e332"; } - -.icon-track_changes:before { - content: "\e8e1"; } - -.icon-traffic:before { - content: "\e565"; } - -.icon-train:before { - content: "\e570"; } - -.icon-tram:before { - content: "\e571"; } - -.icon-transfer_within_a_station:before { - content: "\e572"; } - -.icon-transform:before { - content: "\e428"; } - -.icon-translate:before { - content: "\e8e2"; } - -.icon-trending_down:before { - content: "\e8e3"; } - -.icon-trending_flat:before { - content: "\e8e4"; } - -.icon-trending_up:before { - content: "\e8e5"; } - -.icon-tune:before { - content: "\e429"; } - -.icon-tv:before { - content: "\e333"; } - -.icon-unarchive:before { - content: "\e169"; } - -.icon-undo:before { - content: "\e166"; } - -.icon-unfold_less:before { - content: "\e5d6"; } - -.icon-unfold_more:before { - content: "\e5d7"; } - -.icon-update:before { - content: "\e923"; } - -.icon-usb:before { - content: "\e1e0"; } - -.icon-verified_user:before { - content: "\e8e8"; } - -.icon-vertical_align_bottom:before { - content: "\e258"; } - -.icon-vertical_align_center:before { - content: "\e259"; } - -.icon-vertical_align_top:before { - content: "\e25a"; } - -.icon-vibration:before { - content: "\e62d"; } - -.icon-video_call:before { - content: "\e070"; } - -.icon-video_label:before { - content: "\e071"; } - -.icon-video_library:before { - content: "\e04a"; } - -.icon-videocam:before { - content: "\e04b"; } - -.icon-videocam_off:before { - content: "\e04c"; } - -.icon-videogame_asset:before { - content: "\e338"; } - -.icon-view_agenda:before { - content: "\e8e9"; } - -.icon-view_array:before { - content: "\e8ea"; } - -.icon-view_carousel:before { - content: "\e8eb"; } - -.icon-view_column:before { - content: "\e8ec"; } - -.icon-view_comfy:before { - content: "\e42a"; } - -.icon-view_compact:before { - content: "\e42b"; } - -.icon-view_day:before { - content: "\e8ed"; } - -.icon-view_headline:before { - content: "\e8ee"; } - -.icon-view_list:before { - content: "\e8ef"; } - -.icon-view_module:before { - content: "\e8f0"; } - -.icon-view_quilt:before { - content: "\e8f1"; } - -.icon-view_stream:before { - content: "\e8f2"; } - -.icon-view_week:before { - content: "\e8f3"; } - -.icon-vignette:before { - content: "\e435"; } - -.icon-visibility_off:before { - content: "\e8f5"; } - -.icon-voice_chat:before { - content: "\e62e"; } - -.icon-voicemail:before { - content: "\e0d9"; } - -.icon-volume_down:before { - content: "\e04d"; } - -.icon-volume_mute:before { - content: "\e04e"; } - -.icon-volume_off:before { - content: "\e04f"; } - -.icon-volume_up:before { - content: "\e050"; } - -.icon-vpn_key:before { - content: "\e0da"; } - -.icon-vpn_lock:before { - content: "\e62f"; } - -.icon-wallpaper:before { - content: "\e1bc"; } - -.icon-watch:before { - content: "\e334"; } - -.icon-watch_later:before { - content: "\e924"; } - -.icon-wb_auto:before { - content: "\e42c"; } - -.icon-wb_incandescent:before { - content: "\e42e"; } - -.icon-wb_iridescent:before { - content: "\e436"; } - -.icon-wb_sunny:before { - content: "\e430"; } - -.icon-wc:before { - content: "\e63d"; } - -.icon-web:before { - content: "\e051"; } - -.icon-web_asset:before { - content: "\e069"; } - -.icon-weekend:before { - content: "\e16b"; } - -.icon-whatshot:before { - content: "\e80e"; } - -.icon-widgets:before { - content: "\e1bd"; } - -.icon-wifi:before { - content: "\e63e"; } - -.icon-wifi_lock:before { - content: "\e1e1"; } - -.icon-wifi_tethering:before { - content: "\e1e2"; } - -.icon-work:before { - content: "\e8f9"; } - -.icon-wrap_text:before { - content: "\e25b"; } - -.icon-youtube_searched_for:before { - content: "\e8fa"; } - -.icon-zoom_in:before { - content: "\e8ff"; } - -.icon-zoom_out:before { - content: "\e901"; } - -.icon-zoom_out_map:before { - content: "\e56b"; } - -.pop-in.toggled, -.pop-out.toggled, -.pop-in-last.toggled { - opacity: 1; - -ms-transform: scale(1); - transform: scale(1); - -webkit-transform: scale(1); - -moz-transform: scale(1); - -o-transform: scale(1); } - -.pop-in, -.pop-in-last { - opacity: 0; - -ms-transform: scale(1.1); - transform: scale(1.1); - -webkit-transform: scale(1.1); - -moz-transform: scale(1.1); - -o-transform: scale(1.1); } - -.animate_slower { - transition: all 2s ease-in-out !important; - -webkit-transition: all 2s ease-in-out !important; - -moz-transition: all 2s ease-in-out !important; - -o-transition: all 2s ease-in-out !important; } - -.animate-up.toggled, -.animate-down.toggled { - opacity: 1; - -ms-transform: translateY(0px); - transform: translateY(0px); - -webkit-transform: translateY(0px); - -moz-transform: translateY(0px); - -o-transform: translateY(0px); } - -.animate-down { - opacity: 0; - -ms-transform: translateY(-300px); - transform: translateY(-300px); - -webkit-transform: translateY(-300px); - -moz-transform: translateY(-300px); - -o-transform: translateY(-300px); } - -.animate-up { - opacity: 0; - -ms-transform: translateY(300px); - transform: translateY(300px); - -webkit-transform: translateY(300px); - -moz-transform: translateY(300px); - -o-transform: translateY(300px); } - -.animate { - transition: all 1s ease-in-out; - -webkit-transition: all 1s ease-in-out; - -moz-transition: all 1s ease-in-out; - -o-transition: all 1s ease-in-out; } - -#header { - position: fixed; - left: 0; - top: 0; - right: 0; - z-index: 98; - background: #f7f7f7; } - -#logo { - float: left; - padding: 15px; } - #logo h1 { - color: #000000; - font-size: 1em; - text-transform: uppercase; - font-weight: 700; - text-align: center; } - #logo h1 span { - font-family: "Roboto", sans-serif; - font-size: 0.6em; - display: block; - font-weight: 300; - letter-spacing: 1px; - padding: 0 0 0 0; } - -#menu { - width: 100%; } - #menu li { - position: relative; - display: block; - float: right; - padding: 22px 1.5% 20px 1.5%; } - #menu a { - display: block; - font-size: 0.8em; - color: #666666; - text-transform: uppercase; - font-weight: 400; - transition: all 0.3s; - -webkit-transition: all 0.3s; - -moz-transition: all 0.3s; - -o-transition: all 0.3s; } - #menu .current-menu-item a { - color: #b5b5b5 !important; } - -#menu_wrap { - position: fixed; - right: 0; - top: 0; - z-index: 98; - width: 80%; } - -@media (hover: hover) { - #menu a:hover { - color: #b5b5b5 !important; } } - -#home_socials { - position: fixed; - bottom: 30px; - left: 0; - right: 0; - text-align: center; - z-index: 2; } - #home_socials .socialicons { - display: inline-block; - font-size: 1.4em; - margin: 15px 20px 15px 20px; } - -#socials { - position: fixed; - left: 0; - top: 37%; - background: rgba(0, 0, 0, 0.8); - z-index: 2; } - -.socialicons { - display: block; - font-size: 23px; - font-family: "socials" !important; - speak: none; - font-style: normal; - font-weight: normal; - font-variant: normal; - text-transform: none; - line-height: 1; - color: #ffffff; - text-decoration: none; - margin: 15px; - transition: all 0.3s; - -webkit-transition: all 0.3s; - -moz-transition: all 0.3s; - -o-transition: all 0.3s; } - -#twitter:before { - content: "\ea96"; } - -#instagram:before { - content: "\ea92"; } - -#youtube:before { - content: "\ea9d"; } - -#flickr:before { - content: "\eaa4"; } - -#facebook:before { - content: "\ea91"; } - -@media (hover: hover) { - .socialicons:hover { - color: #b5b5b5; - -ms-transform: scale(1.3); - transform: scale(1.3); - -webkit-transform: scale(1.3); } } - -#content { - margin-top: 70px; - max-width: 70%; - font-family: "Roboto", sans-serif; - margin-left: auto; - margin-right: auto; } - #content h1 { - display: block; - font-size: 2em; - margin-bottom: 0.67em; - margin-left: 0; - margin-right: 0; - font-weight: bold; } - #content h2 { - display: block; - font-size: 1.5em; - margin-bottom: 0.83em; - margin-left: 0; - margin-right: 0; - font-weight: bold; } - #content h3 { - display: block; - font-size: 1.17em; - margin-bottom: 1em; - margin-left: 0; - margin-right: 0; - font-weight: bold; } - #content h4 { - display: block; - font-size: 1em; - margin-bottom: 1.33em; - margin-left: 0; - margin-right: 0; - font-weight: bold; } - #content h5 { - display: block; - font-size: 0.83em; - margin-bottom: 1.67em; - margin-left: 0; - margin-right: 0; - font-weight: bold; } - #content h6 { - display: block; - font-size: 0.67em; - margin-bottom: 2.33em; - margin-left: 0; - margin-right: 0; - font-weight: bold; } - #content p { - color: #707070; - margin-bottom: 1.5em; - line-height: 1.2em; - text-align: justify; } - #content div.left_30 { - width: 30%; - padding-bottom: 50px; - margin-top: 3px; - float: left; } - #content div.left_50 { - width: 50%; - margin-top: 3px; - padding-bottom: 50px; - float: left; } - #content div.right_70 { - width: 70%; - padding-bottom: 50px; - float: right; } - #content div.right_50 { - width: 50%; - padding-bottom: 50px; - float: right; } - #content div.left_50 img { - width: 80%; - margin-left: auto; - margin-right: auto; - border: 2px solid #707070; - box-sizing: border-box; - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; } - #content .clearfix::after { - content: ""; - clear: both; - display: table; } - -@media screen and (max-width: 700px) { - div.left_30, - div.left_50, - div.right_50, - div.right_70 { - width: 100% !important; - float: none !important; - margin: 0 0 30px 0; } - div.left_50 img { - width: 100% !important; } } - -#footer { - z-index: 3; - left: 0; - right: 0; - bottom: 0; - text-align: center; - padding: 5px 0 5px 0; - -webkit-transition: color 0.3s, opacity 0.3s ease-out, margin-left 0.5s, -webkit-transform 0.3s ease-out, -webkit-box-shadow 0.3s; - transition: color 0.3s, opacity 0.3s ease-out, margin-left 0.5s, -webkit-transform 0.3s ease-out, -webkit-box-shadow 0.3s; - -o-transition: color 0.3s, opacity 0.3s ease-out, transform 0.3s ease-out, box-shadow 0.3s, margin-left 0.5s; - transition: color 0.3s, opacity 0.3s ease-out, transform 0.3s ease-out, box-shadow 0.3s, margin-left 0.5s; - transition: color 0.3s, opacity 0.3s ease-out, transform 0.3s ease-out, box-shadow 0.3s, margin-left 0.5s, -webkit-transform 0.3s ease-out, -webkit-box-shadow 0.3s; } - #footer p { - color: #cccccc; - font-size: 0.5em; - font-weight: 400; } - #footer p a { - color: #ccc; } - #footer p a:visited { - color: #ccc; } - #footer p.hosted_by, - #footer p.home_copyright { - text-transform: uppercase; } - -#footer { - position: fixed; - background: #ffffff; - color: #707070; - padding: 20px 0 20px 0; } - #footer p { - color: #707070; - font-size: 0.75em; } diff --git a/public/dist/view.js b/public/dist/view.js deleted file mode 100644 index 5119605dc18..00000000000 --- a/public/dist/view.js +++ /dev/null @@ -1,3157 +0,0 @@ -/*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */ -!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="
",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 049?function(){o(t,{timeout:n});if(n!==H.ricTimeout){n=H.ricTimeout}}:te(function(){I(t)},true);return function(e){var t;if(e=e===true){n=33}if(a){return}a=true;t=r-(f.now()-i);if(t<0){t=0}if(e||t<9){s()}else{I(s,t)}}},ie=function(e){var t,a;var i=99;var r=function(){t=null;e()};var n=function(){var e=f.now()-a;if(e0;if(r&&Z(i,"overflow")!="visible"){a=i.getBoundingClientRect();r=C>a.left&&pa.top-1&&g500&&O.clientWidth>500?500:370:H.expand;k._defEx=u;f=u*H.expFactor;c=H.hFac;A=null;if(w2&&h>2&&!D.hidden){w=f;N=0}else if(h>1&&N>1&&M<6){w=u}else{w=_}}if(l!==n){y=innerWidth+n*c;z=innerHeight+n;s=n*-1;l=n}a=d[t].getBoundingClientRect();if((b=a.bottom)>=s&&(g=a.top)<=z&&(C=a.right)>=s*c&&(p=a.left)<=y&&(b||C||p||g)&&(H.loadHidden||x(d[t]))&&(m&&M<3&&!o&&(h<3||N<4)||W(d[t],n))){R(d[t]);r=true;if(M>9){break}}else if(!r&&m&&!i&&M<4&&N<4&&h>2&&(v[0]||H.preloadAfterLoad)&&(v[0]||!o&&(b||C||p||g||d[t][$](H.sizesAttr)!="auto"))){i=v[0]||d[t]}}if(i&&!r){R(i)}}};var a=ae(t);var S=function(e){var t=e.target;if(t._lazyCache){delete t._lazyCache;return}L(e);K(t,H.loadedClass);Q(t,H.loadingClass);V(t,B);X(t,"lazyloaded")};var i=te(S);var B=function(e){i({target:e.target})};var T=function(e,t){var a=e.getAttribute("data-load-mode")||H.iframeLoadMode;if(a==0){e.contentWindow.location.replace(t)}else if(a==1){e.src=t}};var F=function(e){var t;var a=e[$](H.srcsetAttr);if(t=H.customMedia[e[$]("data-media")||e[$]("media")]){e.setAttribute("media",t)}if(a){e.setAttribute("srcset",a)}};var s=te(function(t,e,a,i,r){var n,s,o,l,u,f;if(!(u=X(t,"lazybeforeunveil",e)).defaultPrevented){if(i){if(a){K(t,H.autosizesClass)}else{t.setAttribute("sizes",i)}}s=t[$](H.srcsetAttr);n=t[$](H.srcAttr);if(r){o=t.parentNode;l=o&&j.test(o.nodeName||"")}f=e.firesLoad||"src"in t&&(s||n||l);u={target:t};K(t,H.loadingClass);if(f){clearTimeout(c);c=I(L,2500);V(t,B,true)}if(l){G.call(o.getElementsByTagName("source"),F)}if(s){t.setAttribute("srcset",s)}else if(n&&!l){if(d.test(t.nodeName)){T(t,n)}else{t.src=n}}if(r&&(s||l)){Y(t,{src:n})}}if(t._lazyRace){delete t._lazyRace}Q(t,H.lazyClass);ee(function(){var e=t.complete&&t.naturalWidth>1;if(!f||e){if(e){K(t,H.fastLoadedClass)}S(u);t._lazyCache=true;I(function(){if("_lazyCache"in t){delete t._lazyCache}},9)}if(t.loading=="lazy"){M--}},true)});var R=function(e){if(e._lazyRace){return}var t;var a=n.test(e.nodeName);var i=a&&(e[$](H.sizesAttr)||e[$]("sizes"));var r=i=="auto";if((r||!m)&&a&&(e[$]("src")||e.srcset)&&!e.complete&&!J(e,H.errorClass)&&J(e,H.lazyClass)){return}t=X(e,"lazyunveilread").detail;if(r){re.updateElem(e,true,e.offsetWidth)}e._lazyRace=true;M++;s(e,t,r,i,a)};var r=ie(function(){H.loadMode=3;a()});var o=function(){if(H.loadMode==3){H.loadMode=2}r()};var l=function(){if(m){return}if(f.now()-e<999){I(l,999);return}m=true;H.loadMode=3;a();q("scroll",o,true)};return{_:function(){e=f.now();k.elements=D.getElementsByClassName(H.lazyClass);v=D.getElementsByClassName(H.lazyClass+" "+H.preloadClass);q("scroll",a,true);q("resize",a,true);q("pageshow",function(e){if(e.persisted){var t=D.querySelectorAll("."+H.loadingClass);if(t.length&&t.forEach){U(function(){t.forEach(function(e){if(e.complete){R(e)}})})}}});if(u.MutationObserver){new MutationObserver(a).observe(O,{childList:true,subtree:true,attributes:true})}else{O[P]("DOMNodeInserted",a,true);O[P]("DOMAttrModified",a,true);setInterval(a,999)}q("hashchange",a,true);["focus","mouseover","click","load","transitionend","animationend"].forEach(function(e){D[P](e,a,true)});if(/d$|^c/.test(D.readyState)){l()}else{q("load",l);D[P]("DOMContentLoaded",a);I(l,2e4)}if(k.elements.length){t();ee._lsFlush()}else{a()}},checkElems:a,unveil:R,_aLSL:o}}(),re=function(){var a;var n=te(function(e,t,a,i){var r,n,s;e._lazysizesWidth=i;i+="px";e.setAttribute("sizes",i);if(j.test(t.nodeName||"")){r=t.getElementsByTagName("source");for(n=0,s=r.length;n"], [""]), - _templateObject2 = _taggedTemplateLiteral(["

", "

"], ["

", "

"]), - _templateObject3 = _taggedTemplateLiteral(["
", "
"], ["
", "
"]), - _templateObject4 = _taggedTemplateLiteral(["
"], ["
"]), - _templateObject5 = _taggedTemplateLiteral(["\n\t\t\t
\n\t\t\t\t ", "\n\t\t\t\t ", "\n\t\t\t\t ", "\n\t\t\t\t
\n\t\t\t\t\t

$", "

\n\t\t\t\t\t", "\n\t\t\t\t
\n\t\t\t"], ["\n\t\t\t
\n\t\t\t\t ", "\n\t\t\t\t ", "\n\t\t\t\t ", "\n\t\t\t\t
\n\t\t\t\t\t

$", "

\n\t\t\t\t\t", "\n\t\t\t\t
\n\t\t\t"]), - _templateObject6 = _taggedTemplateLiteral(["\n\t\t\t\t
\n\t\t\t\t\t", "\n\t\t\t\t\t", "\n\t\t\t\t\t", "\n\t\t\t\t\t", "\n\t\t\t\t\t", "\n\t\t\t\t\t", "\n\t\t\t\t\t", "\n\t\t\t\t\t", "\n\t\t\t\t
\n\t\t\t\t"], ["\n\t\t\t\t
\n\t\t\t\t\t", "\n\t\t\t\t\t", "\n\t\t\t\t\t", "\n\t\t\t\t\t", "\n\t\t\t\t\t", "\n\t\t\t\t\t", "\n\t\t\t\t\t", "\n\t\t\t\t\t", "\n\t\t\t\t
\n\t\t\t\t"]), - _templateObject7 = _taggedTemplateLiteral(["\n\t\t\t\t
\n\t\t\t\t\t", "\n\t\t\t\t
"], ["\n\t\t\t\t
\n\t\t\t\t\t", "\n\t\t\t\t
"]), - _templateObject8 = _taggedTemplateLiteral(["\n\t\t\t
\n\t\t\t\t", "\n\t\t\t\t
\n\t\t\t\t\t

$", "

\n\t\t\t"], ["\n\t\t\t
\n\t\t\t\t", "\n\t\t\t\t
\n\t\t\t\t\t

$", "

\n\t\t\t"]), - _templateObject9 = _taggedTemplateLiteral(["", "", ""], ["", "", ""]), - _templateObject10 = _taggedTemplateLiteral(["", ""], ["", ""]), - _templateObject11 = _taggedTemplateLiteral(["\n\t\t\t\t
\n\t\t\t\t", "\n\t\t\t\t", "\n\t\t\t\t", "\n\t\t\t\t
\n\t\t\t\t"], ["\n\t\t\t\t
\n\t\t\t\t", "\n\t\t\t\t", "\n\t\t\t\t", "\n\t\t\t\t
\n\t\t\t\t"]), - _templateObject12 = _taggedTemplateLiteral(["\n\t\t
\n\t\t

$", "

\n\t\t"], ["\n\t\t
\n\t\t

$", "

\n\t\t"]), - _templateObject13 = _taggedTemplateLiteral([""], [""]), - _templateObject14 = _taggedTemplateLiteral(["big"], ["big"]), - _templateObject15 = _taggedTemplateLiteral(["", ""], ["", ""]), - _templateObject16 = _taggedTemplateLiteral(["
", ""], ["
", ""]), - _templateObject17 = _taggedTemplateLiteral(["

", "

"], ["

", "

"]), - _templateObject18 = _taggedTemplateLiteral(["\n\t\t\t

$", "

\n\t\t\t
\n\t\t\t"], ["\n\t\t\t

$", "

\n\t\t\t
\n\t\t\t"]), - _templateObject19 = _taggedTemplateLiteral(["\n\t\t\t\t
\n\t\t\t\t\t", "\n\t\t\t\t\t\n\t\t\t\t\t

\n\t\t\t\t
\n\t\t\t\t"], ["\n\t\t\t\t
\n\t\t\t\t\t", "\n\t\t\t\t\t\n\t\t\t\t\t

\n\t\t\t\t
\n\t\t\t\t"]), - _templateObject20 = _taggedTemplateLiteral(["\n\t\t
\n\t\t\t", "\n\t\t\t\n\t\t\t

\n\t\t
\n\t\t"], ["\n\t\t
\n\t\t\t", "\n\t\t\t\n\t\t\t

\n\t\t
\n\t\t"]), - _templateObject21 = _taggedTemplateLiteral(["$", "", ""], ["$", "", ""]), - _templateObject22 = _taggedTemplateLiteral(["$", ""], ["$", ""]), - _templateObject23 = _taggedTemplateLiteral(["
", "
"], ["
", "
"]), - _templateObject24 = _taggedTemplateLiteral(["
\n\t\t\t

\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t

\n\t\t\tSave\n\t\t\tDelete\n\t\t
\n\t\t"], ["
\n\t\t\t

\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t

\n\t\t\tSave\n\t\t\tDelete\n\t\t
\n\t\t"]), - _templateObject25 = _taggedTemplateLiteral(["
\n\t\t\t

\n\t\t\t\n\t\t\t", "\n\t\t\t\n\t\t\t

\n\t\t\tDelete\n\t\t
\n\t\t"], ["
\n\t\t\t

\n\t\t\t\n\t\t\t", "\n\t\t\t\n\t\t\t

\n\t\t\tDelete\n\t\t
\n\t\t"]), - _templateObject26 = _taggedTemplateLiteral(["$", "", ""], ["$", "", ""]), - _templateObject27 = _taggedTemplateLiteral([", "], [", "]), - _templateObject28 = _taggedTemplateLiteral(["$", ""], ["$", ""]), - _templateObject29 = _taggedTemplateLiteral(["$", ""], ["$", ""]), - _templateObject30 = _taggedTemplateLiteral(["\n\t\t\t\t\t\t \n\t\t\t\t\t\t\t ", "\n\t\t\t\t\t\t\t ", "\n\t\t\t\t\t\t \n\t\t\t\t\t\t "], ["\n\t\t\t\t\t\t \n\t\t\t\t\t\t\t ", "\n\t\t\t\t\t\t\t ", "\n\t\t\t\t\t\t \n\t\t\t\t\t\t "]), - _templateObject31 = _taggedTemplateLiteral(["\n\t\t\t\t \n\t\t\t\t
\n\t\t\t\t\t
", "
\n\t\t\t\t\t ", "\n\t\t\t\t
\n\t\t\t\t "], ["\n\t\t\t\t \n\t\t\t\t
\n\t\t\t\t\t
", "
\n\t\t\t\t\t ", "\n\t\t\t\t
\n\t\t\t\t "]); - -function _taggedTemplateLiteral(strings, raw) { return Object.freeze(Object.defineProperties(strings, { raw: { value: Object.freeze(raw) } })); } - -function gup(b) { - b = b.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]"); - - var a = "[\\?&]" + b + "=([^&#]*)"; - var d = new RegExp(a); - var c = d.exec(window.location.href); - - if (c === null) return "";else return c[1]; -} - -/** - * @description This module communicates with Lychee's API - */ - -var api = { - onError: null -}; - -api.isTimeout = function (errorThrown, jqXHR) { - if (errorThrown && (errorThrown === "Bad Request" && jqXHR && jqXHR.responseJSON && jqXHR.responseJSON.error && jqXHR.responseJSON.error === "Session timed out" || errorThrown === "unknown status" && jqXHR && jqXHR.status && jqXHR.status === 419 && jqXHR.responseJSON && jqXHR.responseJSON.message && jqXHR.responseJSON.message === "CSRF token mismatch.")) { - return true; - } - - return false; -}; - -api.post = function (fn, params, callback) { - var responseProgressCB = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null; - - loadingBar.show(); - - params = $.extend({ function: fn }, params); - - var api_url = "api/" + fn; - - var success = function success(data) { - setTimeout(loadingBar.hide, 100); - - // Catch errors - if (typeof data === "string" && data.substring(0, 7) === "Error: ") { - api.onError(data.substring(7, data.length), params, data); - return false; - } - - callback(data); - }; - - var error = function error(jqXHR, textStatus, errorThrown) { - api.onError(api.isTimeout(errorThrown, jqXHR) ? "Session timed out." : "Server error or API not found.", params, errorThrown); - }; - - var ajaxParams = { - type: "POST", - url: api_url, - data: params, - dataType: "json", - success: success, - error: error - }; - - if (responseProgressCB !== null) { - ajaxParams.xhrFields = { - onprogress: responseProgressCB - }; - } - - $.ajax(ajaxParams); -}; - -api.get = function (url, callback) { - loadingBar.show(); - - var success = function success(data) { - setTimeout(loadingBar.hide, 100); - - // Catch errors - if (typeof data === "string" && data.substring(0, 7) === "Error: ") { - api.onError(data.substring(7, data.length), params, data); - return false; - } - - callback(data); - }; - - var error = function error(jqXHR, textStatus, errorThrown) { - api.onError(api.isTimeout(errorThrown, jqXHR) ? "Session timed out." : "Server error or API not found.", {}, errorThrown); - }; - - $.ajax({ - type: "GET", - url: url, - data: {}, - dataType: "text", - success: success, - error: error - }); -}; - -api.post_raw = function (fn, params, callback) { - loadingBar.show(); - - params = $.extend({ function: fn }, params); - - var api_url = "api/" + fn; - - var success = function success(data) { - setTimeout(loadingBar.hide, 100); - - // Catch errors - if (typeof data === "string" && data.substring(0, 7) === "Error: ") { - api.onError(data.substring(7, data.length), params, data); - return false; - } - - callback(data); - }; - - var error = function error(jqXHR, textStatus, errorThrown) { - api.onError(api.isTimeout(errorThrown, jqXHR) ? "Session timed out." : "Server error or API not found.", params, errorThrown); - }; - - $.ajax({ - type: "POST", - url: api_url, - data: params, - dataType: "text", - success: success, - error: error - }); -}; - -var csrf = {}; - -csrf.addLaravelCSRF = function (event, jqxhr, settings) { - if (settings.url !== lychee.updatePath) { - jqxhr.setRequestHeader("X-XSRF-TOKEN", csrf.getCookie("XSRF-TOKEN")); - } -}; - -csrf.escape = function (s) { - return s.replace(/([.*+?\^${}()|\[\]\/\\])/g, "\\$1"); -}; - -csrf.getCookie = function (name) { - // we stop the selection at = (default json) but also at % to prevent any %3D at the end of the string - var match = document.cookie.match(RegExp("(?:^|;\\s*)" + csrf.escape(name) + "=([^;^%]*)")); - return match ? match[1] : null; -}; - -csrf.bind = function () { - $(document).on("ajaxSend", csrf.addLaravelCSRF); -}; - -/** - * @description Used to view single photos with view.php - */ - -// Sub-implementation of lychee -------------------------------------------------------------- // - -var lychee = {}; - -lychee.content = $(".content"); -lychee.imageview = $("#imageview"); -lychee.mapview = $("#mapview"); - -lychee.escapeHTML = function () { - var html = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ""; - - // Ensure that html is a string - html += ""; - - // Escape all critical characters - html = html.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """).replace(/'/g, "'").replace(/`/g, "`"); - - return html; -}; - -lychee.html = function (literalSections) { - // Use raw literal sections: we don’t want - // backslashes (\n etc.) to be interpreted - var raw = literalSections.raw; - var result = ""; - - for (var _len = arguments.length, substs = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { - substs[_key - 1] = arguments[_key]; - } - - substs.forEach(function (subst, i) { - // Retrieve the literal section preceding - // the current substitution - var lit = raw[i]; - - // If the substitution is preceded by a dollar sign, - // we escape special characters in it - if (lit.slice(-1) === "$") { - subst = lychee.escapeHTML(subst); - lit = lit.slice(0, -1); - } - - result += lit; - result += subst; - }); - - // Take care of last literal section - // (Never fails, because an empty template string - // produces one literal section, an empty string) - result += raw[raw.length - 1]; - - return result; -}; - -lychee.getEventName = function () { - var touchendSupport = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent || navigator.vendor || window.opera) && "ontouchend" in document.documentElement; - return touchendSupport === true ? "touchend" : "click"; -}; - -// Sub-implementation of photo -------------------------------------------------------------- // - -var photo = { - json: null -}; - -photo.share = function (photoID, service) { - var url = location.toString(); - - switch (service) { - case "twitter": - window.open("https://twitter.com/share?url=" + encodeURI(url)); - break; - case "facebook": - window.open("https://www.facebook.com/sharer.php?u=" + encodeURI(url)); - break; - case "mail": - location.href = "mailto:?subject=&body=" + encodeURI(url); - break; - } -}; - -photo.getDirectLink = function () { - return $("#imageview img").attr("src").replace(/"/g, "").replace(/url\(|\)$/gi, ""); -}; - -photo.show = function () { - $("#imageview").removeClass("full"); - header.dom().removeClass("header--hidden"); - - return true; -}; - -photo.hide = function () { - if (visible.photo() && !visible.sidebar() && !visible.contextMenu()) { - $("#imageview").addClass("full"); - header.dom().addClass("header--hidden"); - - return true; - } - - return false; -}; - -photo.onresize = function () { - // Copy of view.photo.onresize - if (photo.json.sizeVariants.medium === null || photo.json.sizeVariants.medium2x === null) return; - - var imgWidth = photo.json.sizeVariants.medium.width; - var imgHeight = photo.json.sizeVariants.medium.height; - var containerWidth = parseFloat($("#imageview").width(), 10); - var containerHeight = parseFloat($("#imageview").height(), 10); - - var width = imgWidth < containerWidth ? imgWidth : containerWidth; - var height = width * imgHeight / imgWidth; - if (height > containerHeight) { - width = containerHeight * imgWidth / imgHeight; - } - - $("img#image").attr("sizes", width + "px"); -}; - -// Sub-implementation of contextMenu -------------------------------------------------------------- // - -var contextMenu = {}; - -contextMenu.sharePhoto = function (photoID, e) { - var iconClass = "ionicons"; - - var items = [{ title: build.iconic("twitter", iconClass) + "Twitter", fn: function fn() { - return photo.share(photoID, "twitter"); - } }, { title: build.iconic("facebook", iconClass) + "Facebook", fn: function fn() { - return photo.share(photoID, "facebook"); - } }, { title: build.iconic("envelope-closed") + "Mail", fn: function fn() { - return photo.share(photoID, "mail"); - } }, { title: build.iconic("link-intact") + "Direct Link", fn: function fn() { - return window.open(photo.getDirectLink(), "_newtab"); - } }]; - - basicContext.show(items, e.originalEvent); -}; - -// Main -------------------------------------------------------------- // - -var loadingBar = { - show: function show() {}, - hide: function hide() {} -}; - -var imageview = $("#imageview"); - -$(document).ready(function () { - // set CSRF protection (Laravel) - csrf.bind(); - - // Image View - $(window).on("resize", photo.onresize); - - // Save ID of photo - var photoID = gup("p"); - - // Set API error handler - api.onError = error; - - // Share - header.dom("#button_share").on("click", function (e) { - contextMenu.sharePhoto(photoID, e); - }); - - // Infobox - header.dom("#button_info").on("click", sidebar.toggle); - - // Load photo - loadPhotoInfo(photoID); -}); - -var loadPhotoInfo = function loadPhotoInfo(photoID) { - var params = { - photoID: photoID, - password: "" - }; - - api.post("Photo::get", params, function (data) { - if (data === "Warning: Photo private!" || data === "Warning: Wrong password!") { - $("body").append(build.no_content("question-mark")).removeClass("view"); - header.dom().remove(); - return false; - } - - photo.json = data; - - // Set title - if (!data.title) data.title = "Untitled"; - document.title = "Lychee - " + data.title; - header.dom(".header__title").html(lychee.escapeHTML(data.title)); - - // Render HTML - imageview.html(build.imageview(data, true).html); - imageview.find(".arrow_wrapper").remove(); - imageview.addClass("fadeIn").show(); - photo.onresize(); - - // Render Sidebar - var structure = sidebar.createStructure.photo(data); - var html = sidebar.render(structure); - - // Fullscreen - var timeout = null; - - $(document).bind("mousemove", function () { - clearTimeout(timeout); - photo.show(); - timeout = setTimeout(photo.hide, 2500); - }); - timeout = setTimeout(photo.hide, 2500); - - sidebar.dom(".sidebar__wrapper").html(html); - sidebar.bind(); - }); -}; - -var error = function error(errorThrown, params, data) { - console.error({ - description: errorThrown, - params: params, - response: data - }); - - loadingBar.show("error", errorThrown); -}; - -/** - * @description This module is used to generate HTML-Code. - */ - -var build = {}; - -build.iconic = function (icon) { - var classes = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ""; - - var html = ""; - - html += lychee.html(_templateObject, classes, icon); - - return html; -}; - -build.divider = function (title) { - var html = ""; - - html += lychee.html(_templateObject2, title); - - return html; -}; - -build.editIcon = function (id) { - var html = ""; - - html += lychee.html(_templateObject3, id, build.iconic("pencil")); - - return html; -}; - -build.multiselect = function (top, left) { - return lychee.html(_templateObject4, top, left); -}; - -// two additional images that are barely visible seems a bit overkill - use same image 3 times -// if this simplification comes to pass data.types, data.thumbs and data.thumbs2x no longer need to be arrays -build.getAlbumThumb = function (data) { - var isVideo = void 0; - var isRaw = void 0; - var thumb = void 0; - - isVideo = data.thumb.type && data.thumb.type.indexOf("video") > -1; - isRaw = data.thumb.type && data.thumb.type.indexOf("raw") > -1; - thumb = data.thumb.thumb; - var thumb2x = ""; - - if (thumb === "uploads/thumb/" && isVideo) { - return "Photo thumbnail"; - } - if (thumb === "uploads/thumb/" && isRaw) { - return "Photo thumbnail"; - } - - thumb2x = data.thumb.thumb2x; - - return "Photo thumbnail"; -}; - -build.album = function (data) { - var disabled = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; - - var formattedCreationTs = lychee.locale.printMonthYear(data.created_at); - var formattedMinTs = lychee.locale.printMonthYear(data.min_taken_at); - var formattedMaxTs = lychee.locale.printMonthYear(data.max_taken_at); - var subtitle = formattedCreationTs; - - // check setting album_subtitle_type: - // takedate: date range (min/max_takedate from EXIF; if missing defaults to creation) - // creation: creation date of album - // description: album description - // default: any other type defaults to old style setting subtitles based of album sorting - switch (lychee.album_subtitle_type) { - case "description": - subtitle = data.description ? data.description : ""; - break; - case "takedate": - if (formattedMinTs !== "" || formattedMaxTs !== "") { - // either min_taken_at or max_taken_at is set - subtitle = formattedMinTs === formattedMaxTs ? formattedMaxTs : formattedMinTs + " - " + formattedMaxTs; - subtitle = "" + build.iconic("camera-slr") + "" + subtitle; - break; - } - // fall through - case "creation": - break; - case "oldstyle": - default: - if (lychee.sortingAlbums !== "" && data.min_taken_at && data.max_taken_at) { - var sortingAlbums = lychee.sortingAlbums.replace("ORDER BY ", "").split(" "); - if (sortingAlbums[0] === "max_taken_at" || sortingAlbums[0] === "min_taken_at") { - if (formattedMinTs !== "" && formattedMaxTs !== "") { - subtitle = formattedMinTs === formattedMaxTs ? formattedMaxTs : formattedMinTs + " - " + formattedMaxTs; - } else if (formattedMinTs !== "" && sortingAlbums[0] === "min_taken_at") { - subtitle = formattedMinTs; - } else if (formattedMaxTs !== "" && sortingAlbums[0] === "max_taken_at") { - subtitle = formattedMaxTs; - } - } - } - } - - var html = lychee.html(_templateObject5, disabled ? "disabled" : "", data.nsfw && data.nsfw === "1" && lychee.nsfw_blur ? "blurred" : "", data.id, data.nsfw && data.nsfw === "1" ? "1" : "0", tabindex.get_next_tab_index(), build.getAlbumThumb(data), build.getAlbumThumb(data), build.getAlbumThumb(data), data.title, data.title, subtitle); - - if (album.isUploadable() && !disabled) { - var isCover = album.json && album.json.cover_id && data.thumb.id === album.json.cover_id; - html += lychee.html(_templateObject6, data.nsfw === "1" ? "badge--nsfw" : "", build.iconic("warning"), data.star === "1" ? "badge--star" : "", build.iconic("star"), data.recent === "1" ? "badge--visible badge--list" : "", build.iconic("clock"), data.public === "1" ? "badge--visible" : "", data.visible === "1" ? "badge--not--hidden" : "badge--hidden", build.iconic("eye"), data.unsorted === "1" ? "badge--visible" : "", build.iconic("list"), data.password === "1" ? "badge--visible" : "", build.iconic("lock-locked"), data.tag_album === "1" ? "badge--tag" : "", build.iconic("tag"), isCover ? "badge--cover" : "", build.iconic("folder-cover")); - } - - if (data.albums && data.albums.length > 0 || data.hasOwnProperty("has_albums") && data.has_albums === "1") { - html += lychee.html(_templateObject7, build.iconic("layers")); - } - - html += "
"; - - return html; -}; - -build.photo = function (data) { - var disabled = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; - - var html = ""; - var thumbnail = ""; - var thumb2x = ""; - var isCover = data.id === album.json.cover_id; - - var isVideo = data.type && data.type.indexOf("video") > -1; - var isRaw = data.type && data.type.indexOf("raw") > -1; - var isLivePhoto = data.livePhotoUrl !== "" && data.livePhotoUrl !== null; - - if (data.sizeVariants.thumb === null) { - if (isLivePhoto) { - thumbnail = "Photo thumbnail"; - } - if (isVideo) { - thumbnail = "Photo thumbnail"; - } else if (isRaw) { - thumbnail = "Photo thumbnail"; - } - } else if (lychee.layout === "0") { - if (data.sizeVariants.thumb2x !== null) { - thumb2x = data.sizeVariants.thumb2x.url; - } - - if (thumb2x !== "") { - thumb2x = "data-srcset='" + thumb2x + " 2x'"; - } - - thumbnail = ""; - thumbnail += "Photo thumbnail"; - thumbnail += ""; - } else { - if (data.sizeVariants.small !== null) { - if (data.sizeVariants.small2x !== null) { - thumb2x = "data-srcset='" + data.sizeVariants.small.url + " " + data.sizeVariants.small.width + "w, " + data.sizeVariants.small2x.url + " " + data.sizeVariants.small2x.width + "w'"; - } - - thumbnail = ""; - thumbnail += "Photo thumbnail"; - thumbnail += ""; - } else if (data.sizeVariants.medium !== null) { - if (data.sizeVariants.medium2x !== null) { - thumb2x = "data-srcset='" + data.sizeVariants.medium.url + " " + data.sizeVariants.medium.width + "w, " + data.sizeVariants.medium2x.url + " " + data.sizeVariants.medium2x.width + "w'"; - } - - thumbnail = ""; - thumbnail += "Photo thumbnail"; - thumbnail += ""; - } else if (!isVideo) { - // Fallback for images with no small or medium. - thumbnail = ""; - thumbnail += "Photo thumbnail"; - thumbnail += ""; - } else { - // Fallback for videos with no small (the case of no thumb is - // handled at the top of this function). - - if (data.sizeVariants.thumb2x !== null) { - thumb2x = data.sizeVariants.thumb2x.url; - } - - if (thumb2x !== "") { - thumb2x = "data-srcset='" + data.sizeVariants.thumb.url + " " + data.sizeVariants.thumb.width + "w, " + thumb2x + " " + data.sizeVariants.thumb2x.width + "w'"; - } - - thumbnail = ""; - thumbnail += "Photo thumbnail"; - thumbnail += ""; - } - } - - html += lychee.html(_templateObject8, disabled ? "disabled" : "", data.album, data.id, tabindex.get_next_tab_index(), thumbnail, data.title, data.title); - - if (data.taken_at !== null) html += lychee.html(_templateObject9, build.iconic("camera-slr"), lychee.locale.printDateTime(data.taken_at));else html += lychee.html(_templateObject10, lychee.locale.printDateTime(data.created_at)); - - html += "
"; - - if (album.isUploadable()) { - html += lychee.html(_templateObject11, data.star === "1" ? "badge--star" : "", build.iconic("star"), data.public === "1" && album.json.public !== "1" ? "badge--visible badge--hidden" : "", build.iconic("eye"), isCover ? "badge--cover" : "", build.iconic("folder-cover")); - } - - html += "
"; - - return html; -}; - -build.check_overlay_type = function (data, overlay_type) { - var next = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; - - var types = ["desc", "date", "exif", "none"]; - var idx = types.indexOf(overlay_type); - if (idx < 0) return "none"; - if (next) idx++; - var exifHash = data.make + data.model + data.shutter + data.iso + (data.type.indexOf("video") !== 0 ? data.aperture + data.focal : ""); - - for (var i = 0; i < types.length; i++) { - var type = types[(idx + i) % types.length]; - if (type === "date" || type === "none") return type; - if (type === "desc" && data.description && data.description !== "") return type; - if (type === "exif" && exifHash !== "") return type; - } -}; - -build.overlay_image = function (data) { - var overlay = ""; - switch (build.check_overlay_type(data, lychee.image_overlay_type)) { - case "desc": - overlay = data.description; - break; - case "date": - if (data.taken_at != null) overlay = "" + build.iconic("camera-slr") + "" + lychee.locale.printDateTime(data.taken_at) + "";else overlay = lychee.locale.printDateTime(data.created_at); - break; - case "exif": - var exifHash = data.make + data.model + data.shutter + data.aperture + data.focal + data.iso; - if (exifHash !== "") { - if (data.shutter && data.shutter !== "") overlay = data.shutter.replace("s", "sec"); - if (data.aperture && data.aperture !== "") { - if (overlay !== "") overlay += " at "; - overlay += data.aperture.replace("f/", "ƒ / "); - } - if (data.iso && data.iso !== "") { - if (overlay !== "") overlay += ", "; - overlay += lychee.locale["PHOTO_ISO"] + " " + data.iso; - } - if (data.focal && data.focal !== "") { - if (overlay !== "") overlay += "
"; - overlay += data.focal + (data.lens && data.lens !== "" ? " (" + data.lens + ")" : ""); - } - } - break; - case "none": - default: - return ""; - } - - return lychee.html(_templateObject12, data.title) + (overlay !== "" ? "

" + overlay + "

" : "") + "\n\t\t
\n\t\t"; -}; - -build.imageview = function (data, visibleControls, autoplay) { - var html = ""; - var thumb = ""; - - if (data.type.indexOf("video") > -1) { - html += lychee.html(_templateObject13, visibleControls === true ? "" : "full", autoplay ? "autoplay" : "", tabindex.get_next_tab_index(), data.url); - } else if (data.type.indexOf("raw") > -1 && data.sizeVariants.medium === null) { - html += lychee.html(_templateObject14, visibleControls === true ? "" : "full", tabindex.get_next_tab_index()); - } else { - var img = ""; - - if (data.livePhotoUrl === "" || data.livePhotoUrl === null) { - // It's normal photo - - // See if we have the thumbnail loaded... - $(".photo").each(function () { - if ($(this).attr("data-id") && $(this).attr("data-id") == data.id) { - var thumbimg = $(this).find("img"); - if (thumbimg.length > 0) { - thumb = thumbimg[0].currentSrc ? thumbimg[0].currentSrc : thumbimg[0].src; - return false; - } - } - }); - - if (data.sizeVariants.medium !== null) { - var medium = ""; - - if (data.sizeVariants.medium2x !== null) { - medium = "srcset='" + data.sizeVariants.medium.url + " " + data.sizeVariants.medium.width + "w, " + data.sizeVariants.medium2x.url + " " + data.sizeVariants.medium2x.width + "w'"; - } - img = "medium"); - } else { - img = "big"; - } - } else { - if (data.sizeVariants.medium !== null) { - var medium_width = data.sizeVariants.medium.width; - var medium_height = data.sizeVariants.medium.height; - // It's a live photo - img = "
"; - } else { - // It's a live photo - img = "
"; - } - } - - html += lychee.html(_templateObject15, img); - } - - html += build.overlay_image(data) + ("\n\t\t\t\n\t\t\t\n\t\t\t"); - - return { html: html, thumb: thumb }; -}; - -build.no_content = function (typ) { - var html = ""; - - html += lychee.html(_templateObject16, build.iconic(typ)); - - switch (typ) { - case "magnifying-glass": - html += lychee.html(_templateObject17, lychee.locale["VIEW_NO_RESULT"]); - break; - case "eye": - html += lychee.html(_templateObject17, lychee.locale["VIEW_NO_PUBLIC_ALBUMS"]); - break; - case "cog": - html += lychee.html(_templateObject17, lychee.locale["VIEW_NO_CONFIGURATION"]); - break; - case "question-mark": - html += lychee.html(_templateObject17, lychee.locale["VIEW_PHOTO_NOT_FOUND"]); - break; - } - - html += "
"; - - return html; -}; - -build.uploadModal = function (title, files) { - var html = ""; - - html += lychee.html(_templateObject18, title); - - var i = 0; - - while (i < files.length) { - var file = files[i]; - - if (file.name.length > 40) file.name = file.name.substr(0, 17) + "..." + file.name.substr(file.name.length - 20, 20); - - html += lychee.html(_templateObject19, file.name); - - i++; - } - - html += "
"; - - return html; -}; - -build.uploadNewFile = function (name) { - if (name.length > 40) { - name = name.substr(0, 17) + "..." + name.substr(name.length - 20, 20); - } - - return lychee.html(_templateObject20, name); -}; - -build.tags = function (tags) { - var html = ""; - var editable = typeof album !== "undefined" ? album.isUploadable() : false; - - // Search is enabled if logged in (not publicMode) or public seach is enabled - var searchable = lychee.publicMode === false || lychee.public_search === true; - - // build class_string for tag - var a_class = "tag"; - if (searchable) { - a_class = a_class + " search"; - } - - if (tags !== "") { - tags = tags.split(","); - - tags.forEach(function (tag, index) { - if (editable) { - html += lychee.html(_templateObject21, a_class, tag, index, build.iconic("x")); - } else { - html += lychee.html(_templateObject22, a_class, tag); - } - }); - } else { - html = lychee.html(_templateObject23, lychee.locale["NO_TAGS"]); - } - - return html; -}; - -build.user = function (user) { - var html = lychee.html(_templateObject24, user.id, user.id, user.username, user.id, user.id); - - return html; -}; - -build.u2f = function (credential) { - return lychee.html(_templateObject25, credential.id, credential.id, credential.id.slice(0, 30), credential.id); -}; - -/** - * @description This module takes care of the header. - */ - -var header = { - _dom: $(".header") -}; - -header.dom = function (selector) { - if (selector == null || selector === "") return header._dom; - return header._dom.find(selector); -}; - -header.bind = function () { - // Event Name - var eventName = lychee.getEventName(); - - header.dom(".header__title").on(eventName, function (e) { - if ($(this).hasClass("header__title--editable") === false) return false; - - if (lychee.enable_contextmenu_header === false) return false; - - if (visible.photo()) contextMenu.photoTitle(album.getID(), photo.getID(), e);else contextMenu.albumTitle(album.getID(), e); - }); - - header.dom("#button_visibility").on(eventName, function (e) { - photo.setPublic(photo.getID(), e); - }); - header.dom("#button_share").on(eventName, function (e) { - contextMenu.sharePhoto(photo.getID(), e); - }); - - header.dom("#button_visibility_album").on(eventName, function (e) { - album.setPublic(album.getID(), e); - }); - - header.dom("#button_sharing_album_users").on(eventName, function (e) { - album.shareUsers(album.getID(), e); - }); - - header.dom("#button_share_album").on(eventName, function (e) { - contextMenu.shareAlbum(album.getID(), e); - }); - - header.dom("#button_signin").on(eventName, lychee.loginDialog); - header.dom("#button_settings").on(eventName, function (e) { - if ($(".leftMenu").css("display") === "none") { - // left menu disabled on small screens - contextMenu.config(e); - } else { - // standard left menu - leftMenu.open(); - } - }); - header.dom("#button_close_config").on(eventName, function () { - tabindex.makeFocusable(header.dom()); - tabindex.makeFocusable(lychee.content); - tabindex.makeUnfocusable(leftMenu._dom); - multiselect.bind(); - lychee.load(); - }); - header.dom("#button_info_album").on(eventName, sidebar.toggle); - header.dom("#button_info").on(eventName, sidebar.toggle); - header.dom(".button--map-albums").on(eventName, function () { - lychee.gotoMap(); - }); - header.dom("#button_map_album").on(eventName, function () { - lychee.gotoMap(album.getID()); - }); - header.dom("#button_map").on(eventName, function () { - lychee.gotoMap(album.getID()); - }); - header.dom(".button_add").on(eventName, contextMenu.add); - header.dom("#button_more").on(eventName, function (e) { - contextMenu.photoMore(photo.getID(), e); - }); - header.dom("#button_move_album").on(eventName, function (e) { - contextMenu.move([album.getID()], e, album.setAlbum, "ROOT", album.getParent() != ""); - }); - header.dom("#button_nsfw_album").on(eventName, function (e) { - album.setNSFW(album.getID()); - }); - header.dom("#button_move").on(eventName, function (e) { - contextMenu.move([photo.getID()], e, photo.setAlbum); - }); - header.dom(".header__hostedwith").on(eventName, function () { - window.open(lychee.website); - }); - header.dom("#button_trash_album").on(eventName, function () { - album.delete([album.getID()]); - }); - header.dom("#button_trash").on(eventName, function () { - photo.delete([photo.getID()]); - }); - header.dom("#button_archive").on(eventName, function () { - album.getArchive([album.getID()]); - }); - header.dom("#button_star").on(eventName, function () { - photo.setStar([photo.getID()]); - }); - header.dom("#button_rotate_ccwise").on(eventName, function () { - photoeditor.rotate(photo.getID(), -1); - }); - header.dom("#button_rotate_cwise").on(eventName, function () { - photoeditor.rotate(photo.getID(), 1); - }); - header.dom("#button_back_home").on(eventName, function () { - if (!album.json.parent_id) { - lychee.goto(); - } else { - lychee.goto(album.getParent()); - } - }); - header.dom("#button_back").on(eventName, function () { - lychee.goto(album.getID()); - }); - header.dom("#button_back_map").on(eventName, function () { - lychee.goto(album.getID() || ""); - }); - header.dom("#button_fs_album_enter,#button_fs_enter").on(eventName, lychee.fullscreenEnter); - header.dom("#button_fs_album_exit,#button_fs_exit").on(eventName, lychee.fullscreenExit).hide(); - - header.dom(".header__search").on("keyup click", function () { - if ($(this).val().length > 0) { - lychee.goto("search/" + encodeURIComponent($(this).val())); - } else if (search.hash !== null) { - search.reset(); - } - }); - header.dom(".header__clear").on(eventName, function () { - search.reset(); - }); - - header.bind_back(); - - return true; -}; - -header.bind_back = function () { - // Event Name - var eventName = lychee.getEventName(); - - header.dom(".header__title").on(eventName, function () { - if (lychee.landing_page_enable && visible.albums()) { - window.location.href = "."; - } else { - return false; - } - }); -}; - -header.show = function () { - lychee.imageview.removeClass("full"); - header.dom().removeClass("header--hidden"); - - tabindex.restoreSettings(header.dom()); - - photo.updateSizeLivePhotoDuringAnimation(); - - return true; -}; - -header.hideIfLivePhotoNotPlaying = function () { - // Hides the header, if current live photo is not playing - if (photo.isLivePhotoPlaying() == true) return false; - return header.hide(); -}; - -header.hide = function () { - if (visible.photo() && !visible.sidebar() && !visible.contextMenu() && basicModal.visible() === false) { - tabindex.saveSettings(header.dom()); - tabindex.makeUnfocusable(header.dom()); - - lychee.imageview.addClass("full"); - header.dom().addClass("header--hidden"); - - photo.updateSizeLivePhotoDuringAnimation(); - - return true; - } - - return false; -}; - -header.setTitle = function () { - var title = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : "Untitled"; - - var $title = header.dom(".header__title"); - var html = lychee.html(_templateObject26, title, build.iconic("caret-bottom")); - - $title.html(html); - - return true; -}; - -header.setMode = function (mode) { - if (mode === "albums" && lychee.publicMode === true) mode = "public"; - - switch (mode) { - case "public": - header.dom().removeClass("header--view"); - header.dom(".header__toolbar--albums, .header__toolbar--album, .header__toolbar--photo, .header__toolbar--map, .header__toolbar--config").removeClass("header__toolbar--visible"); - header.dom(".header__toolbar--public").addClass("header__toolbar--visible"); - tabindex.makeFocusable(header.dom(".header__toolbar--public")); - tabindex.makeUnfocusable(header.dom(".header__toolbar--albums, .header__toolbar--album, .header__toolbar--photo, .header__toolbar--map, .header__toolbar--config")); - - if (lychee.public_search) { - var e = $(".header__search, .header__clear", ".header__toolbar--public"); - e.show(); - tabindex.makeFocusable(e); - } else { - var _e = $(".header__search, .header__clear", ".header__toolbar--public"); - _e.hide(); - tabindex.makeUnfocusable(_e); - } - - // Set icon in Public mode - if (lychee.map_display_public) { - var _e2 = $(".button--map-albums", ".header__toolbar--public"); - _e2.show(); - tabindex.makeFocusable(_e2); - } else { - var _e3 = $(".button--map-albums", ".header__toolbar--public"); - _e3.hide(); - tabindex.makeUnfocusable(_e3); - } - - // Set focus on login button - if (lychee.active_focus_on_page_load) { - $("#button_signin").focus(); - } - return true; - - case "albums": - header.dom().removeClass("header--view"); - header.dom(".header__toolbar--public, .header__toolbar--album, .header__toolbar--photo, .header__toolbar--map, .header__toolbar--config").removeClass("header__toolbar--visible"); - header.dom(".header__toolbar--albums").addClass("header__toolbar--visible"); - - tabindex.makeFocusable(header.dom(".header__toolbar--albums")); - tabindex.makeUnfocusable(header.dom(".header__toolbar--public, .header__toolbar--album, .header__toolbar--photo, .header__toolbar--map, .header__toolbar--config")); - - // If map is disabled, we should hide the icon - if (lychee.map_display) { - var _e4 = $(".button--map-albums", ".header__toolbar--albums"); - _e4.show(); - tabindex.makeFocusable(_e4); - } else { - var _e5 = $(".button--map-albums", ".header__toolbar--albums"); - _e5.hide(); - tabindex.makeUnfocusable(_e5); - } - - if (lychee.enable_button_add) { - var _e6 = $(".button_add", ".header__toolbar--albums"); - _e6.show(); - tabindex.makeFocusable(_e6); - } else { - var _e7 = $(".button_add", ".header__toolbar--albums"); - _e7.remove(); - } - - return true; - - case "album": - var albumID = album.getID(); - - header.dom().removeClass("header--view"); - header.dom(".header__toolbar--public, .header__toolbar--albums, .header__toolbar--photo, .header__toolbar--map, .header__toolbar--config").removeClass("header__toolbar--visible"); - header.dom(".header__toolbar--album").addClass("header__toolbar--visible"); - - tabindex.makeFocusable(header.dom(".header__toolbar--album")); - tabindex.makeUnfocusable(header.dom(".header__toolbar--public, .header__toolbar--albums, .header__toolbar--photo, .header__toolbar--map, .header__toolbar--config")); - - // Hide download button when album empty or we are not allowed to - // upload to it and it's not explicitly marked as downloadable. - if (!album.json || album.json.photos === false && album.json.albums && album.json.albums.length === 0 || !album.isUploadable() && album.json.downloadable === "0") { - var _e8 = $("#button_archive"); - _e8.hide(); - tabindex.makeUnfocusable(_e8); - } else { - var _e9 = $("#button_archive"); - _e9.show(); - tabindex.makeFocusable(_e9); - } - - if (album.json && album.json.hasOwnProperty("share_button_visible") && album.json.share_button_visible !== "1") { - var _e10 = $("#button_share_album"); - _e10.hide(); - tabindex.makeUnfocusable(_e10); - } else { - var _e11 = $("#button_share_album"); - _e11.show(); - tabindex.makeFocusable(_e11); - } - - // If map is disabled, we should hide the icon - if (lychee.publicMode === true ? lychee.map_display_public : lychee.map_display) { - var _e12 = $("#button_map_album"); - _e12.show(); - tabindex.makeFocusable(_e12); - } else { - var _e13 = $("#button_map_album"); - _e13.hide(); - tabindex.makeUnfocusable(_e13); - } - - if (albumID === "starred" || albumID === "public" || albumID === "recent") { - $("#button_nsfw_album, #button_info_album, #button_trash_album, #button_visibility_album, #button_sharing_album_users, #button_move_album").hide(); - if (album.isUploadable()) { - $(".button_add, .header__divider", ".header__toolbar--album").show(); - tabindex.makeFocusable($(".button_add, .header__divider", ".header__toolbar--album")); - } else { - $(".button_add, .header__divider", ".header__toolbar--album").hide(); - tabindex.makeUnfocusable($(".button_add, .header__divider", ".header__toolbar--album")); - } - tabindex.makeUnfocusable($("#button_nsfw_album, #button_info_album, #button_trash_album, #button_visibility_album, #button_sharing_album_users, #button_move_album")); - } else if (albumID === "unsorted") { - $("#button_nsfw_album, #button_info_album, #button_visibility_album, #button_sharing_album_users, #button_move_album").hide(); - $("#button_trash_album, .button_add, .header__divider", ".header__toolbar--album").show(); - tabindex.makeFocusable($("#button_trash_album, .button_add, .header__divider", ".header__toolbar--album")); - tabindex.makeUnfocusable($("#button_nsfw_album, #button_info_album, #button_visibility_album, #button_sharing_album_users, #button_move_album")); - } else if (album.isTagAlbum()) { - $("#button_info_album").show(); - $("#button_nsfw_album, #button_move_album").hide(); - $(".button_add, .header__divider", ".header__toolbar--album").hide(); - tabindex.makeFocusable($("#button_info_album")); - tabindex.makeUnfocusable($("#button_nsfw_album, #button_move_album")); - tabindex.makeUnfocusable($(".button_add, .header__divider", ".header__toolbar--album")); - if (album.isUploadable()) { - $("#button_visibility_album, #button_sharing_album_users, #button_trash_album").show(); - tabindex.makeFocusable($("#button_visibility_album, #button_sharing_album_users, #button_trash_album")); - } else { - $("#button_visibility_album, #button_sharing_album_users, #button_trash_album").hide(); - tabindex.makeUnfocusable($("#button_visibility_album, #button_sharing_album_users, #button_trash_album")); - } - } else { - $("#button_info_album").show(); - tabindex.makeFocusable($("#button_info_album")); - if (album.isUploadable()) { - $("#button_nsfw_album, #button_trash_album, #button_move_album, #button_visibility_album, #button_sharing_album_users, .button_add, .header__divider", ".header__toolbar--album").show(); - tabindex.makeFocusable($("#button_nsfw_album, #button_trash_album, #button_move_album, #button_visibility_album, #button_sharing_album_users, .button_add, .header__divider", ".header__toolbar--album")); - } else { - $("#button_nsfw_album, #button_trash_album, #button_move_album, #button_visibility_album, #button_sharing_album_users, .button_add, .header__divider", ".header__toolbar--album").hide(); - tabindex.makeUnfocusable($("#button_nsfw_album, #button_trash_album, #button_move_album, #button_visibility_album, #button_sharing_album_users, .button_add, .header__divider", ".header__toolbar--album")); - } - } - - // Remove buttons if needed - if (!lychee.enable_button_visibility) { - var _e14 = $("#button_visibility_album", "#button_sharing_album_users", ".header__toolbar--album"); - _e14.remove(); - } - if (!lychee.enable_button_share) { - var _e15 = $("#button_share_album", ".header__toolbar--album"); - _e15.remove(); - } - if (!lychee.enable_button_archive) { - var _e16 = $("#button_archive", ".header__toolbar--album"); - _e16.remove(); - } - if (!lychee.enable_button_move) { - var _e17 = $("#button_move_album", ".header__toolbar--album"); - _e17.remove(); - } - if (!lychee.enable_button_trash) { - var _e18 = $("#button_trash_album", ".header__toolbar--album"); - _e18.remove(); - } - if (!lychee.enable_button_fullscreen || !lychee.fullscreenAvailable()) { - var _e19 = $("#button_fs_album_enter", ".header__toolbar--album"); - _e19.remove(); - } - if (!lychee.enable_button_add) { - var _e20 = $(".button_add", ".header__toolbar--album"); - _e20.remove(); - } - - return true; - - case "photo": - header.dom().addClass("header--view"); - header.dom(".header__toolbar--public, .header__toolbar--albums, .header__toolbar--album, .header__toolbar--map, .header__toolbar--config").removeClass("header__toolbar--visible"); - header.dom(".header__toolbar--photo").addClass("header__toolbar--visible"); - - tabindex.makeFocusable(header.dom(".header__toolbar--photo")); - tabindex.makeUnfocusable(header.dom(".header__toolbar--public, .header__toolbar--albums, .header__toolbar--album, .header__toolbar--map, .header__toolbar--config")); - // If map is disabled, we should hide the icon - if (lychee.publicMode === true ? lychee.map_display_public : lychee.map_display) { - var _e21 = $("#button_map"); - _e21.show(); - tabindex.makeFocusable(_e21); - } else { - var _e22 = $("#button_map"); - _e22.hide(); - tabindex.makeUnfocusable(_e22); - } - - if (album.isUploadable()) { - var _e23 = $("#button_trash, #button_move, #button_visibility, #button_star"); - _e23.show(); - tabindex.makeFocusable(_e23); - } else { - var _e24 = $("#button_trash, #button_move, #button_visibility, #button_star"); - _e24.hide(); - tabindex.makeUnfocusable(_e24); - } - - if (photo.json && photo.json.hasOwnProperty("share_button_visible") && photo.json.share_button_visible !== "1") { - var _e25 = $("#button_share"); - _e25.hide(); - tabindex.makeUnfocusable(_e25); - } else { - var _e26 = $("#button_share"); - _e26.show(); - tabindex.makeFocusable(_e26); - } - - // Hide More menu if empty (see contextMenu.photoMore) - $("#button_more").show(); - tabindex.makeFocusable($("#button_more")); - if (!(album.isUploadable() || (photo.json.hasOwnProperty("downloadable") ? photo.json.downloadable === "1" : album.json && album.json.downloadable && album.json.downloadable === "1")) && !(photo.json.url && photo.json.url !== "")) { - var _e27 = $("#button_more"); - _e27.hide(); - tabindex.makeUnfocusable(_e27); - } - - // Remove buttons if needed - if (!lychee.enable_button_visibility) { - var _e28 = $("#button_visibility", ".header__toolbar--photo"); - _e28.remove(); - } - if (!lychee.enable_button_share) { - var _e29 = $("#button_share", ".header__toolbar--photo"); - _e29.remove(); - } - if (!lychee.enable_button_move) { - var _e30 = $("#button_move", ".header__toolbar--photo"); - _e30.remove(); - } - if (!lychee.enable_button_trash) { - var _e31 = $("#button_trash", ".header__toolbar--photo"); - _e31.remove(); - } - if (!lychee.enable_button_fullscreen || !lychee.fullscreenAvailable()) { - var _e32 = $("#button_fs_enter", ".header__toolbar--photo"); - _e32.remove(); - } - if (!lychee.enable_button_more) { - var _e33 = $("#button_more", ".header__toolbar--photo"); - _e33.remove(); - } - if (!lychee.enable_button_rotate) { - var _e34 = $("#button_rotate_cwise", ".header__toolbar--photo"); - _e34.remove(); - - _e34 = $("#button_rotate_ccwise", ".header__toolbar--photo"); - _e34.remove(); - } - return true; - case "map": - header.dom().removeClass("header--view"); - header.dom(".header__toolbar--public, .header__toolbar--album, .header__toolbar--albums, .header__toolbar--photo, .header__toolbar--config").removeClass("header__toolbar--visible"); - header.dom(".header__toolbar--map").addClass("header__toolbar--visible"); - - tabindex.makeFocusable(header.dom(".header__toolbar--map")); - tabindex.makeUnfocusable(header.dom(".header__toolbar--public, .header__toolbar--album, .header__toolbar--albums, .header__toolbar--photo, .header__toolbar--config")); - return true; - case "config": - header.dom().addClass("header--view"); - header.dom(".header__toolbar--public, .header__toolbar--albums, .header__toolbar--album, .header__toolbar--photo, .header__toolbar--map").removeClass("header__toolbar--visible"); - header.dom(".header__toolbar--config").addClass("header__toolbar--visible"); - return true; - } - - return false; -}; - -// Note that the pull-down menu is now enabled not only for editable -// items but for all of public/albums/album/photo views, so 'editable' is a -// bit of a misnomer at this point... -header.setEditable = function (editable) { - var $title = header.dom(".header__title"); - - if (editable) $title.addClass("header__title--editable");else $title.removeClass("header__title--editable"); - - return true; -}; - -/** - * @description This module is used to check if elements are visible or not. - */ - -var visible = {}; - -visible.albums = function () { - if (header.dom(".header__toolbar--public").hasClass("header__toolbar--visible")) return true; - if (header.dom(".header__toolbar--albums").hasClass("header__toolbar--visible")) return true; - return false; -}; - -visible.album = function () { - if (header.dom(".header__toolbar--album").hasClass("header__toolbar--visible")) return true; - return false; -}; - -visible.photo = function () { - if ($("#imageview.fadeIn").length > 0) return true; - return false; -}; - -visible.mapview = function () { - if ($("#mapview.fadeIn").length > 0) return true; - return false; -}; - -visible.config = function () { - if (header.dom(".header__toolbar--config").hasClass("header__toolbar--visible")) return true; - return false; -}; - -visible.search = function () { - if (search.hash != null) return true; - return false; -}; - -visible.sidebar = function () { - if (sidebar.dom().hasClass("active") === true) return true; - return false; -}; - -visible.sidebarbutton = function () { - if (visible.photo()) return true; - if (visible.album() && $("#button_info_album:visible").length > 0) return true; - return false; -}; - -visible.header = function () { - if (header.dom().hasClass("header--hidden") === true) return false; - return true; -}; - -visible.contextMenu = function () { - return basicContext.visible(); -}; - -visible.multiselect = function () { - if ($("#multiselect").length > 0) return true; - return false; -}; - -visible.leftMenu = function () { - if (leftMenu.dom().hasClass("leftMenu__visible")) return true; - return false; -}; - -/** - * @description This module takes care of the sidebar. - */ - -var sidebar = { - _dom: $(".sidebar"), - types: { - DEFAULT: 0, - TAGS: 1 - }, - createStructure: {} -}; - -sidebar.dom = function (selector) { - if (selector == null || selector === "") return sidebar._dom; - - return sidebar._dom.find(selector); -}; - -sidebar.bind = function () { - // This function should be called after building and appending - // the sidebars content to the DOM. - // This function can be called multiple times, therefore - // event handlers should be removed before binding a new one. - - // Event Name - var eventName = lychee.getEventName(); - - sidebar.dom("#edit_title").off(eventName).on(eventName, function () { - if (visible.photo()) photo.setTitle([photo.getID()]);else if (visible.album()) album.setTitle([album.getID()]); - }); - - sidebar.dom("#edit_description").off(eventName).on(eventName, function () { - if (visible.photo()) photo.setDescription(photo.getID());else if (visible.album()) album.setDescription(album.getID()); - }); - - sidebar.dom("#edit_showtags").off(eventName).on(eventName, function () { - album.setShowTags(album.getID()); - }); - - sidebar.dom("#edit_tags").off(eventName).on(eventName, function () { - photo.editTags([photo.getID()]); - }); - - sidebar.dom("#tags .tag").off(eventName).on(eventName, function () { - sidebar.triggerSearch($(this).text()); - }); - - sidebar.dom("#tags .tag span").off(eventName).on(eventName, function () { - photo.deleteTag(photo.getID(), $(this).data("index")); - }); - - sidebar.dom("#edit_license").off(eventName).on(eventName, function () { - if (visible.photo()) photo.setLicense(photo.getID());else if (visible.album()) album.setLicense(album.getID()); - }); - - sidebar.dom("#edit_sorting").off(eventName).on(eventName, function () { - album.setSorting(album.getID()); - }); - - sidebar.dom(".attr_location").off(eventName).on(eventName, function () { - sidebar.triggerSearch($(this).text()); - }); - - return true; -}; - -sidebar.triggerSearch = function (search_string) { - // If public search is diabled -> do nothing - if (lychee.publicMode === true && !lychee.public_search) { - // Do not display an error -> just do nothing to not confuse the user - return; - } - - search.hash = null; - // We're either logged in or public search is allowed - lychee.goto("search/" + encodeURIComponent(search_string)); -}; - -sidebar.toggle = function () { - if (visible.sidebar() || visible.sidebarbutton()) { - header.dom(".button--info").toggleClass("active"); - lychee.content.toggleClass("content--sidebar"); - lychee.imageview.toggleClass("image--sidebar"); - if (typeof view !== "undefined") view.album.content.justify(); - sidebar.dom().toggleClass("active"); - photo.updateSizeLivePhotoDuringAnimation(); - - return true; - } - - return false; -}; - -sidebar.setSelectable = function () { - var selectable = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; - - // Attributes/Values inside the sidebar are selectable by default. - // Selection needs to be deactivated to prevent an unwanted selection - // while using multiselect. - - if (selectable === true) sidebar.dom().removeClass("notSelectable");else sidebar.dom().addClass("notSelectable"); -}; - -sidebar.changeAttr = function (attr) { - var value = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "-"; - var dangerouslySetInnerHTML = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; - - if (attr == null || attr === "") return false; - - // Set a default for the value - if (value == null || value === "") value = "-"; - - // Escape value - if (dangerouslySetInnerHTML === false) value = lychee.escapeHTML(value); - - // Set new value - sidebar.dom(".attr_" + attr).html(value); - - return true; -}; - -sidebar.hideAttr = function (attr) { - sidebar.dom(".attr_" + attr).closest("tr").hide(); -}; - -sidebar.secondsToHMS = function (d) { - d = Number(d); - var h = Math.floor(d / 3600); - var m = Math.floor(d % 3600 / 60); - var s = Math.floor(d % 60); - - return (h > 0 ? h.toString() + "h" : "") + (m > 0 ? m.toString() + "m" : "") + (s > 0 || h == 0 && m == 0 ? s.toString() + "s" : ""); -}; - -sidebar.createStructure.photo = function (data) { - if (data == null || data === "") return false; - - var editable = typeof album !== "undefined" ? album.isUploadable() : false; - var exifHash = data.taken_at + data.make + data.model + data.shutter + data.aperture + data.focal + data.iso; - var locationHash = data.longitude + data.latitude + data.altitude; - var structure = {}; - var _public = ""; - var isVideo = data.type && data.type.indexOf("video") > -1; - var license = void 0; - - // Set the license string for a photo - switch (data.license) { - // if the photo doesn't have a license - case "none": - license = ""; - break; - // Localize All Rights Reserved - case "reserved": - license = lychee.locale["PHOTO_RESERVED"]; - break; - // Display anything else that's set - default: - license = data.license; - break; - } - - // Set value for public - switch (data.public) { - case "0": - _public = lychee.locale["PHOTO_SHR_NO"]; - break; - case "1": - _public = lychee.locale["PHOTO_SHR_PHT"]; - break; - case "2": - _public = lychee.locale["PHOTO_SHR_ALB"]; - break; - default: - _public = "-"; - break; - } - - structure.basics = { - title: lychee.locale["PHOTO_BASICS"], - type: sidebar.types.DEFAULT, - rows: [{ title: lychee.locale["PHOTO_TITLE"], kind: "title", value: data.title, editable: editable }, { title: lychee.locale["PHOTO_UPLOADED"], kind: "uploaded", value: lychee.locale.printDateTime(data.created_at) }, { title: lychee.locale["PHOTO_DESCRIPTION"], kind: "description", value: data.description, editable: editable }] - }; - - structure.image = { - title: lychee.locale[isVideo ? "PHOTO_VIDEO" : "PHOTO_IMAGE"], - type: sidebar.types.DEFAULT, - rows: [{ title: lychee.locale["PHOTO_SIZE"], kind: "size", value: lychee.locale.printFilesizeLocalized(data.filesize) }, { title: lychee.locale["PHOTO_FORMAT"], kind: "type", value: data.type }, { title: lychee.locale["PHOTO_RESOLUTION"], kind: "resolution", value: data.width + " x " + data.height }] - }; - - if (isVideo) { - if (data.width === 0 || data.height === 0) { - // Remove the "Resolution" line if we don't have the data. - structure.image.rows.splice(-1, 1); - } - - // We overload the database, storing duration (in full seconds) in - // "aperture" and frame rate (floating point with three digits after - // the decimal point) in "focal". - if (data.aperture != "") { - structure.image.rows.push({ title: lychee.locale["PHOTO_DURATION"], kind: "duration", value: sidebar.secondsToHMS(data.aperture) }); - } - if (data.focal != "") { - structure.image.rows.push({ title: lychee.locale["PHOTO_FPS"], kind: "fps", value: data.focal + " fps" }); - } - } - - // Always create tags section - behaviour for editing - //tags handled when contructing the html code for tags - - structure.tags = { - title: lychee.locale["PHOTO_TAGS"], - type: sidebar.types.TAGS, - value: build.tags(data.tags), - editable: editable - }; - - // Only create EXIF section when EXIF data available - if (exifHash !== "") { - structure.exif = { - title: lychee.locale["PHOTO_CAMERA"], - type: sidebar.types.DEFAULT, - rows: isVideo ? [{ title: lychee.locale["PHOTO_CAPTURED"], kind: "takedate", value: lychee.locale.printDateTime(data.taken_at) }, { title: lychee.locale["PHOTO_MAKE"], kind: "make", value: data.make }, { title: lychee.locale["PHOTO_TYPE"], kind: "model", value: data.model }] : [{ title: lychee.locale["PHOTO_CAPTURED"], kind: "takedate", value: lychee.locale.printDateTime(data.taken_at) }, { title: lychee.locale["PHOTO_MAKE"], kind: "make", value: data.make }, { title: lychee.locale["PHOTO_TYPE"], kind: "model", value: data.model }, { title: lychee.locale["PHOTO_LENS"], kind: "lens", value: data.lens }, { title: lychee.locale["PHOTO_SHUTTER"], kind: "shutter", value: data.shutter }, { title: lychee.locale["PHOTO_APERTURE"], kind: "aperture", value: data.aperture }, { title: lychee.locale["PHOTO_FOCAL"], kind: "focal", value: data.focal }, { title: lychee.locale["PHOTO_ISO"], kind: "iso", value: data.iso }] - }; - } else { - structure.exif = {}; - } - - structure.sharing = { - title: lychee.locale["PHOTO_SHARING"], - type: sidebar.types.DEFAULT, - rows: [{ title: lychee.locale["PHOTO_SHR_PLUBLIC"], kind: "public", value: _public }] - }; - - structure.license = { - title: lychee.locale["PHOTO_REUSE"], - type: sidebar.types.DEFAULT, - rows: [{ title: lychee.locale["PHOTO_LICENSE"], kind: "license", value: license, editable: editable }] - }; - - if (locationHash !== "" && locationHash !== 0) { - structure.location = { - title: lychee.locale["PHOTO_LOCATION"], - type: sidebar.types.DEFAULT, - rows: [{ - title: lychee.locale["PHOTO_LATITUDE"], - kind: "latitude", - value: data.latitude ? DecimalToDegreeMinutesSeconds(data.latitude, true) : "" - }, { - title: lychee.locale["PHOTO_LONGITUDE"], - kind: "longitude", - value: data.longitude ? DecimalToDegreeMinutesSeconds(data.longitude, false) : "" - }, - // No point in displaying sub-mm precision; 10cm is more than enough. - { - title: lychee.locale["PHOTO_ALTITUDE"], - kind: "altitude", - value: data.altitude ? (Math.round(parseFloat(data.altitude) * 10) / 10).toString() + "m" : "" - }, { title: lychee.locale["PHOTO_LOCATION"], kind: "location", value: data.location ? data.location : "" }] - }; - if (data.imgDirection) { - // No point in display sub-degree precision. - structure.location.rows.push({ - title: lychee.locale["PHOTO_IMGDIRECTION"], - kind: "imgDirection", - value: Math.round(data.imgDirection).toString() + "°" - }); - } - } else { - structure.location = {}; - } - - // Construct all parts of the structure - var structure_ret = [structure.basics, structure.image, structure.tags, structure.exif, structure.location, structure.license]; - - if (!lychee.publicMode) { - structure_ret.push(structure.sharing); - } - - return structure_ret; -}; - -sidebar.createStructure.album = function (album) { - var data = album.json; - - if (data == null || data === "") return false; - - var editable = album.isUploadable(); - var structure = {}; - var _public = ""; - var hidden = ""; - var downloadable = ""; - var share_button_visible = ""; - var password = ""; - var license = ""; - var sorting = ""; - - // Set value for public - switch (data.public) { - case "0": - _public = lychee.locale["ALBUM_SHR_NO"]; - break; - case "1": - _public = lychee.locale["ALBUM_SHR_YES"]; - break; - default: - _public = "-"; - break; - } - - // Set value for hidden - switch (data.visible) { - case "0": - hidden = lychee.locale["ALBUM_SHR_YES"]; - break; - case "1": - hidden = lychee.locale["ALBUM_SHR_NO"]; - break; - default: - hidden = "-"; - break; - } - - // Set value for downloadable - switch (data.downloadable) { - case "0": - downloadable = lychee.locale["ALBUM_SHR_NO"]; - break; - case "1": - downloadable = lychee.locale["ALBUM_SHR_YES"]; - break; - default: - downloadable = "-"; - break; - } - - // Set value for share_button_visible - switch (data.share_button_visible) { - case "0": - share_button_visible = lychee.locale["ALBUM_SHR_NO"]; - break; - case "1": - share_button_visible = lychee.locale["ALBUM_SHR_YES"]; - break; - default: - share_button_visible = "-"; - break; - } - - // Set value for password - switch (data.password) { - case "0": - password = lychee.locale["ALBUM_SHR_NO"]; - break; - case "1": - password = lychee.locale["ALBUM_SHR_YES"]; - break; - default: - password = "-"; - break; - } - - // Set license string - switch (data.license) { - case "none": - license = ""; // consistency - break; - case "reserved": - license = lychee.locale["ALBUM_RESERVED"]; - break; - default: - license = data.license; - break; - } - - if (data.sorting_col === "") { - sorting = lychee.locale["DEFAULT"]; - } else { - sorting = data.sorting_col + " " + data.sorting_order; - } - - structure.basics = { - title: lychee.locale["ALBUM_BASICS"], - type: sidebar.types.DEFAULT, - rows: [{ title: lychee.locale["ALBUM_TITLE"], kind: "title", value: data.title, editable: editable }, { title: lychee.locale["ALBUM_DESCRIPTION"], kind: "description", value: data.description, editable: editable }] - }; - - if (album.isTagAlbum()) { - structure.basics.rows.push({ title: lychee.locale["ALBUM_SHOW_TAGS"], kind: "showtags", value: data.show_tags, editable: editable }); - } - - var videoCount = 0; - $.each(data.photos, function () { - if (this.type && this.type.indexOf("video") > -1) { - videoCount++; - } - }); - structure.album = { - title: lychee.locale["ALBUM_ALBUM"], - type: sidebar.types.DEFAULT, - rows: [{ title: lychee.locale["ALBUM_CREATED"], kind: "created", value: lychee.locale.printDateTime(data.created_at) }] - }; - if (data.albums && data.albums.length > 0) { - structure.album.rows.push({ title: lychee.locale["ALBUM_SUBALBUMS"], kind: "subalbums", value: data.albums.length }); - } - if (data.photos) { - if (data.photos.length - videoCount > 0) { - structure.album.rows.push({ title: lychee.locale["ALBUM_IMAGES"], kind: "images", value: data.photos.length - videoCount }); - } - } - if (videoCount > 0) { - structure.album.rows.push({ title: lychee.locale["ALBUM_VIDEOS"], kind: "videos", value: videoCount }); - } - - if (data.photos) { - structure.album.rows.push({ title: lychee.locale["ALBUM_ORDERING"], kind: "sorting", value: sorting, editable: editable }); - } - - structure.share = { - title: lychee.locale["ALBUM_SHARING"], - type: sidebar.types.DEFAULT, - rows: [{ title: lychee.locale["ALBUM_PUBLIC"], kind: "public", value: _public }, { title: lychee.locale["ALBUM_HIDDEN"], kind: "hidden", value: hidden }, { title: lychee.locale["ALBUM_DOWNLOADABLE"], kind: "downloadable", value: downloadable }, { title: lychee.locale["ALBUM_SHARE_BUTTON_VISIBLE"], kind: "share_button_visible", value: share_button_visible }, { title: lychee.locale["ALBUM_PASSWORD"], kind: "password", value: password }] - }; - - if (data.owner != null) { - structure.share.rows.push({ title: lychee.locale["ALBUM_OWNER"], kind: "owner", value: data.owner }); - } - - structure.license = { - title: lychee.locale["ALBUM_REUSE"], - type: sidebar.types.DEFAULT, - rows: [{ title: lychee.locale["ALBUM_LICENSE"], kind: "license", value: license, editable: editable }] - }; - - // Construct all parts of the structure - var structure_ret = [structure.basics, structure.album, structure.license]; - if (!lychee.publicMode) { - structure_ret.push(structure.share); - } - - return structure_ret; -}; - -sidebar.has_location = function (structure) { - if (structure == null || structure === "" || structure === false) return false; - - var _has_location = false; - - structure.forEach(function (section) { - if (section.title == lychee.locale["PHOTO_LOCATION"]) { - _has_location = true; - } - }); - - return _has_location; -}; - -sidebar.render = function (structure) { - if (structure == null || structure === "" || structure === false) return false; - - var html = ""; - - var renderDefault = function renderDefault(section) { - var _html = ""; - - _html += "\n\t\t\t\t \n\t\t\t\t \n\t\t\t\t "; - - if (section.title == lychee.locale["PHOTO_LOCATION"]) { - var _has_latitude = false; - var _has_longitude = false; - - section.rows.forEach(function (row, index, object) { - if (row.kind == "latitude" && row.value !== "") { - _has_latitude = true; - } - - if (row.kind == "longitude" && row.value !== "") { - _has_longitude = true; - } - - // Do not show location is not enabled - if (row.kind == "location" && (lychee.publicMode === true && !lychee.location_show_public || !lychee.location_show)) { - object.splice(index, 1); - } else { - // Explode location string into an array to keep street, city etc separate - if (!(row.value === "" || row.value == null)) { - section.rows[index].value = row.value.split(",").map(function (item) { - return item.trim(); - }); - } - } - }); - - if (_has_latitude && _has_longitude && lychee.map_display) { - _html += "\n\t\t\t\t\t\t
\n\t\t\t\t\t\t "; - } - } - - section.rows.forEach(function (row) { - var value = row.value; - - // show only Exif rows which have a value or if its editable - if (!(value === "" || value == null) || row.editable === true) { - // Wrap span-element around value for easier selecting on change - if (Array.isArray(row.value)) { - value = ""; - row.value.forEach(function (v) { - if (v === "" || v == null) { - return; - } - // Add separator if needed - if (value !== "") { - value += lychee.html(_templateObject27, row.kind); - } - value += lychee.html(_templateObject28, row.kind, v); - }); - } else { - value = lychee.html(_templateObject29, row.kind, value); - } - - // Add edit-icon to the value when editable - if (row.editable === true) value += " " + build.editIcon("edit_" + row.kind); - - _html += lychee.html(_templateObject30, row.title, value); - } - }); - - _html += "\n\t\t\t\t
\n\t\t\t\t "; - - return _html; - }; - - var renderTags = function renderTags(section) { - var _html = ""; - var editable = ""; - - // Add edit-icon to the value when editable - if (section.editable === true) editable = build.editIcon("edit_tags"); - - _html += lychee.html(_templateObject31, section.title, section.title.toLowerCase(), section.value, editable); - - return _html; - }; - - structure.forEach(function (section) { - if (section.type === sidebar.types.DEFAULT) html += renderDefault(section);else if (section.type === sidebar.types.TAGS) html += renderTags(section); - }); - - return html; -}; - -function DecimalToDegreeMinutesSeconds(decimal, type) { - var degrees = 0; - var minutes = 0; - var seconds = 0; - var direction = void 0; - - //decimal must be integer or float no larger than 180; - //type must be Boolean - if (Math.abs(decimal) > 180 || typeof type !== "boolean") { - return false; - } - - //inputs OK, proceed - //type is latitude when true, longitude when false - - //set direction; north assumed - if (type && decimal < 0) { - direction = "S"; - } else if (!type && decimal < 0) { - direction = "W"; - } else if (!type) { - direction = "E"; - } else { - direction = "N"; - } - - //get absolute value of decimal - var d = Math.abs(decimal); - - //get degrees - degrees = Math.floor(d); - - //get seconds - seconds = (d - degrees) * 3600; - - //get minutes - minutes = Math.floor(seconds / 60); - - //reset seconds - seconds = Math.floor(seconds - minutes * 60); - - return degrees + "° " + minutes + "' " + seconds + '" ' + direction; -} - -/** - * @description This module takes care of the map view of a full album and its sub-albums. - */ - -var map_provider_layer_attribution = { - Wikimedia: { - layer: "https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}{r}.png", - attribution: 'Wikimedia' - }, - "OpenStreetMap.org": { - layer: "https://{s}.tile.osm.org/{z}/{x}/{y}.png", - attribution: '© OpenStreetMap contributors' - }, - "OpenStreetMap.de": { - layer: "https://{s}.tile.openstreetmap.de/{z}/{x}/{y}.png ", - attribution: '© OpenStreetMap contributors' - }, - "OpenStreetMap.fr": { - layer: "https://{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png ", - attribution: '© OpenStreetMap contributors' - }, - RRZE: { - layer: "https://{s}.osm.rrze.fau.de/osmhd/{z}/{x}/{y}.png", - attribution: '© OpenStreetMap contributors' - } -}; - -var mapview = { - map: null, - photoLayer: null, - min_lat: null, - min_lng: null, - max_lat: null, - max_lng: null, - albumID: null, - map_provider: null -}; - -mapview.isInitialized = function () { - if (mapview.map === null || mapview.photoLayer === null) { - return false; - } - return true; -}; - -mapview.title = function (_albumID, _albumTitle) { - switch (_albumID) { - case "f": - lychee.setTitle(lychee.locale["STARRED"], false); - break; - case "s": - lychee.setTitle(lychee.locale["PUBLIC"], false); - break; - case "r": - lychee.setTitle(lychee.locale["RECENT"], false); - break; - case "0": - lychee.setTitle(lychee.locale["UNSORTED"], false); - break; - case null: - lychee.setTitle(lychee.locale["ALBUMS"], false); - break; - default: - lychee.setTitle(_albumTitle, false); - break; - } -}; - -// Open the map view -mapview.open = function () { - var albumID = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; - - // If map functionality is disabled -> do nothing - if (!lychee.map_display || lychee.publicMode === true && !lychee.map_display_public) { - loadingBar.show("error", lychee.locale["ERROR_MAP_DEACTIVATED"]); - return; - } - - lychee.animate($("#mapview"), "fadeIn"); - $("#mapview").show(); - header.setMode("map"); - - mapview.albumID = albumID; - - // initialize container only once - if (mapview.isInitialized() == false) { - // Leaflet seaches for icon in same directoy as js file -> paths needs - // to be overwritten - delete L.Icon.Default.prototype._getIconUrl; - L.Icon.Default.mergeOptions({ - iconRetinaUrl: "img/marker-icon-2x.png", - iconUrl: "img/marker-icon.png", - shadowUrl: "img/marker-shadow.png" - }); - - // Set initial view to (0,0) - mapview.map = L.map("leaflet_map_full").setView([0.0, 0.0], 13); - - L.tileLayer(map_provider_layer_attribution[lychee.map_provider].layer, { - attribution: map_provider_layer_attribution[lychee.map_provider].attribution - }).addTo(mapview.map); - - mapview.map_provider = lychee.map_provider; - } else { - if (mapview.map_provider !== lychee.map_provider) { - // removew all layers - mapview.map.eachLayer(function (layer) { - mapview.map.removeLayer(layer); - }); - - L.tileLayer(map_provider_layer_attribution[lychee.map_provider].layer, { - attribution: map_provider_layer_attribution[lychee.map_provider].attribution - }).addTo(mapview.map); - - mapview.map_provider = lychee.map_provider; - } else { - // Mapview has already shown data -> remove only photoLayer showing photos - mapview.photoLayer.clear(); - } - - // Reset min/max lat/lgn Values - mapview.min_lat = null; - mapview.max_lat = null; - mapview.min_lng = null; - mapview.max_lng = null; - } - - // Define how the photos on the map should look like - mapview.photoLayer = L.photo.cluster().on("click", function (e) { - var photo = { - photoID: e.layer.photo.photoID, - albumID: e.layer.photo.albumID, - name: e.layer.photo.name, - url: e.layer.photo.url, - url2x: e.layer.photo.url2x, - taken_at: lychee.locale.printDateTime(e.layer.photo.taken_at) - }; - var template = ""; - - // Retina version if available - if (photo.url2x !== "") { - template = template.concat('

{name}

', build.iconic("camera-slr"), "

{taken_at}

"); - } else { - template = template.concat('

{name}

', build.iconic("camera-slr"), "

{taken_at}

"); - } - - e.layer.bindPopup(L.Util.template(template, photo), { - minWidth: 400 - }).openPopup(); - }); - - // Adjusts zoom and position of map to show all images - var updateZoom = function updateZoom() { - if (mapview.min_lat && mapview.min_lng && mapview.max_lat && mapview.max_lng) { - var dist_lat = mapview.max_lat - mapview.min_lat; - var dist_lng = mapview.max_lng - mapview.min_lng; - mapview.map.fitBounds([[mapview.min_lat - 0.1 * dist_lat, mapview.min_lng - 0.1 * dist_lng], [mapview.max_lat + 0.1 * dist_lat, mapview.max_lng + 0.1 * dist_lng]]); - } else { - mapview.map.fitWorld(); - } - }; - - // Adds photos to the map - var addPhotosToMap = function addPhotosToMap(album) { - // check if empty - if (!album.photos) return; - - var photos = []; - - album.photos.forEach(function (element, index) { - if (element.latitude || element.longitude) { - photos.push({ - lat: parseFloat(element.latitude), - lng: parseFloat(element.longitude), - thumbnail: element.sizeVariants.thumb !== null ? element.sizeVariants.thumb.url : "img/placeholder.png", - thumbnail2x: element.sizeVariants.thumb2x !== null ? element.sizeVariants.thumb2x.url : null, - url: element.sizeVariants.small !== null ? element.sizeVariants.small.url : element.url, - url2x: element.sizeVariants.small2x !== null ? element.sizeVariants.small2x.url : null, - name: element.title, - taken_at: element.taken_at, - albumID: element.album, - photoID: element.id - }); - - // Update min/max lat/lng - if (mapview.min_lat === null || mapview.min_lat > element.latitude) { - mapview.min_lat = parseFloat(element.latitude); - } - if (mapview.min_lng === null || mapview.min_lng > element.longitude) { - mapview.min_lng = parseFloat(element.longitude); - } - if (mapview.max_lat === null || mapview.max_lat < element.latitude) { - mapview.max_lat = parseFloat(element.latitude); - } - if (mapview.max_lng === null || mapview.max_lng < element.longitude) { - mapview.max_lng = parseFloat(element.longitude); - } - } - }); - - // Add Photos to map - mapview.photoLayer.add(photos).addTo(mapview.map); - - // Update Zoom and Position - updateZoom(); - }; - - // Call backend, retrieve information of photos and display them - // This function is called recursively to retrieve data for sub-albums - // Possible enhancement could be to only have a single ajax call - var getAlbumData = function getAlbumData(_albumID) { - var _includeSubAlbums = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; - - if (_albumID !== "" && _albumID !== null) { - // _ablumID has been to a specific album - var _params = { - albumID: _albumID, - includeSubAlbums: _includeSubAlbums, - password: "" - }; - - api.post("Album::getPositionData", _params, function (data) { - if (data === "Warning: Wrong password!") { - password.getDialog(_albumID, function () { - _params.password = password.value; - - api.post("Album::getPositionData", _params, function (_data) { - addPhotosToMap(_data); - mapview.title(_albumID, _data.title); - }); - }); - } else { - addPhotosToMap(data); - mapview.title(_albumID, data.title); - } - }); - } else { - // AlbumID is empty -> fetch all photos of all albums - // _ablumID has been to a specific album - var _params2 = { - includeSubAlbums: _includeSubAlbums, - password: "" - }; - - api.post("Albums::getPositionData", _params2, function (data) { - if (data === "Warning: Wrong password!") { - password.getDialog(_albumID, function () { - _params2.password = password.value; - - api.post("Albums::getPositionData", _params2, function (_data) { - addPhotosToMap(_data); - mapview.title(_albumID, _data.title); - }); - }); - } else { - addPhotosToMap(data); - mapview.title(_albumID, data.title); - } - }); - } - }; - - // If subalbums not being included and album.json already has all data - // -> we can reuse it - if (lychee.map_include_subalbums === false && album.json !== null && album.json.photos !== null) { - addPhotosToMap(album.json); - } else { - // Not all needed data has been preloaded - we need to load everything - getAlbumData(albumID, lychee.map_include_subalbums); - } - - // Update Zoom and Position once more (for empty map) - updateZoom(); -}; - -mapview.close = function () { - // If map functionality is disabled -> do nothing - if (!lychee.map_display) return; - - lychee.animate($("#mapview"), "fadeOut"); - $("#mapview").hide(); - header.setMode("album"); - - // Make album focussable - tabindex.makeFocusable(lychee.content); -}; - -mapview.goto = function (elem) { - // If map functionality is disabled -> do nothing - if (!lychee.map_display) return; - - var photoID = elem.attr("data-id"); - var albumID = elem.attr("data-album-id"); - - if (albumID == "null") albumID = 0; - - if (album.json == null || albumID !== album.json.id) { - album.refresh(); - } - - lychee.goto(albumID + "/" + photoID); -}; - -lychee.locale = { - USERNAME: "username", - PASSWORD: "password", - ENTER: "Enter", - CANCEL: "Cancel", - SIGN_IN: "Sign In", - CLOSE: "Close", - - SETTINGS: "Settings", - USERS: "Users", - U2F: "U2F", - NOTIFICATIONS: "Notifications", - SHARING: "Sharing", - CHANGE_LOGIN: "Change Login", - CHANGE_SORTING: "Change Sorting", - SET_DROPBOX: "Set Dropbox", - ABOUT_LYCHEE: "About Lychee", - DIAGNOSTICS: "Diagnostics", - DIAGNOSTICS_GET_SIZE: "Request space usage", - LOGS: "Show Logs", - CLEAN_LOGS: "Clean Noise", - SIGN_OUT: "Sign Out", - UPDATE_AVAILABLE: "Update available!", - MIGRATION_AVAILABLE: "Migration available!", - CHECK_FOR_UPDATE: "Check for updates", - DEFAULT_LICENSE: "Default License for new uploads:", - SET_LICENSE: "Set License", - SET_OVERLAY_TYPE: "Set Overlay", - SET_MAP_PROVIDER: "Set OpenStreetMap tiles provider", - SAVE_RISK: "Save my modifications, I accept the Risk!", - MORE: "More", - DEFAULT: "Default", - - SMART_ALBUMS: "Smart albums", - SHARED_ALBUMS: "Shared albums", - ALBUMS: "Albums", - PHOTOS: "Pictures", - SEARCH_RESULTS: "Search results", - - RENAME: "Rename", - RENAME_ALL: "Rename All", - MERGE: "Merge", - MERGE_ALL: "Merge All", - MAKE_PUBLIC: "Make Public", - SHARE_ALBUM: "Share Album", - SHARE_PHOTO: "Share Photo", - SHARE_WITH: "Share with...", - DOWNLOAD_ALBUM: "Download Album", - ABOUT_ALBUM: "About Album", - DELETE_ALBUM: "Delete Album", - FULLSCREEN_ENTER: "Enter Fullscreen", - FULLSCREEN_EXIT: "Exit Fullscreen", - - SHARING_ALBUM_USERS: "Share this album with users", - SHARING_ALBUM_USERS_LONG_MESSAGE: "Select the users to share this album with", - WAIT_FETCH_DATA: "Please wait while we get the data...", - SHARING_ALBUM_USERS_NO_USERS: "There are no users to share the album with", - - DELETE_ALBUM_QUESTION: "Delete Album and Photos", - KEEP_ALBUM: "Keep Album", - DELETE_ALBUM_CONFIRMATION_1: "Are you sure you want to delete the album", - DELETE_ALBUM_CONFIRMATION_2: "and all of the photos it contains? This action can't be undone!", - - DELETE_ALBUMS_QUESTION: "Delete Albums and Photos", - KEEP_ALBUMS: "Keep Albums", - DELETE_ALBUMS_CONFIRMATION_1: "Are you sure you want to delete all", - DELETE_ALBUMS_CONFIRMATION_2: "selected albums and all of the photos they contain? This action can't be undone!", - - DELETE_UNSORTED_CONFIRM: "Are you sure you want to delete all photos from 'Unsorted'?
This action can't be undone!", - CLEAR_UNSORTED: "Clear Unsorted", - KEEP_UNSORTED: "Keep Unsorted", - - EDIT_SHARING: "Edit Sharing", - MAKE_PRIVATE: "Make Private", - - CLOSE_ALBUM: "Close Album", - CLOSE_PHOTO: "Close Photo", - CLOSE_MAP: "Close Map", - - ADD: "Add", - MOVE: "Move", - MOVE_ALL: "Move All", - DUPLICATE: "Duplicate", - DUPLICATE_ALL: "Duplicate All", - COPY_TO: "Copy to...", - COPY_ALL_TO: "Copy All to...", - DELETE: "Delete", - DELETE_ALL: "Delete All", - DOWNLOAD: "Download", - DOWNLOAD_MEDIUM: "Download medium size", - DOWNLOAD_SMALL: "Download small size", - UPLOAD_PHOTO: "Upload Photo", - IMPORT_LINK: "Import from Link", - IMPORT_DROPBOX: "Import from Dropbox", - IMPORT_SERVER: "Import from Server", - NEW_ALBUM: "New Album", - NEW_TAG_ALBUM: "New Tag Album", - - TITLE_NEW_ALBUM: "Enter a title for the new album:", - UNTITLED: "Untilted", - UNSORTED: "Unsorted", - STARRED: "Starred", - RECENT: "Recent", - PUBLIC: "Public", - NUM_PHOTOS: "Photos", - - CREATE_ALBUM: "Create Album", - CREATE_TAG_ALBUM: "Create Tag Album", - - STAR_PHOTO: "Star Photo", - STAR: "Star", - STAR_ALL: "Star All", - TAGS: "Tags", - TAGS_ALL: "Tags All", - UNSTAR_PHOTO: "Unstar Photo", - - FULL_PHOTO: "Full Photo", - ABOUT_PHOTO: "About Photo", - DISPLAY_FULL_MAP: "Map", - DIRECT_LINK: "Direct Link", - DIRECT_LINKS: "Direct Links", - - ALBUM_ABOUT: "About", - ALBUM_BASICS: "Basics", - ALBUM_TITLE: "Title", - ALBUM_NEW_TITLE: "Enter a new title for this album:", - ALBUMS_NEW_TITLE_1: "Enter a title for all", - ALBUMS_NEW_TITLE_2: "selected albums:", - ALBUM_SET_TITLE: "Set Title", - ALBUM_DESCRIPTION: "Description", - ALBUM_SHOW_TAGS: "Tags to show", - ALBUM_NEW_DESCRIPTION: "Enter a new description for this album:", - ALBUM_SET_DESCRIPTION: "Set Description", - ALBUM_NEW_SHOWTAGS: "Enter tags of photos that will be visible in this album:", - ALBUM_SET_SHOWTAGS: "Set tags to show", - ALBUM_ALBUM: "Album", - ALBUM_CREATED: "Created", - ALBUM_IMAGES: "Images", - ALBUM_VIDEOS: "Videos", - ALBUM_SHARING: "Share", - ALBUM_OWNER: "Owner", - ALBUM_SHR_YES: "YES", - ALBUM_SHR_NO: "No", - ALBUM_PUBLIC: "Public", - ALBUM_PUBLIC_EXPL: "Album can be viewed by others, subject to the restrictions below.", - ALBUM_FULL: "Full size (v4 only)", - ALBUM_FULL_EXPL: "Full size pictures are available", - ALBUM_HIDDEN: "Hidden", - ALBUM_HIDDEN_EXPL: "Only people with the direct link can view this album.", - ALBUM_MARK_NSFW: "Mark album as sensitive", - ALBUM_UNMARK_NSFW: "Unmark album as sensitive", - ALBUM_NSFW: "Sensitive", - ALBUM_NSFW_EXPL: "Album contains sensitive content.", - ALBUM_DOWNLOADABLE: "Downloadable", - ALBUM_DOWNLOADABLE_EXPL: "Visitors of your Lychee can download this album.", - ALBUM_SHARE_BUTTON_VISIBLE: "Share button is visible", - ALBUM_SHARE_BUTTON_VISIBLE_EXPL: "Display social media sharing links.", - ALBUM_PASSWORD: "Password", - ALBUM_PASSWORD_PROT: "Password protected", - ALBUM_PASSWORD_PROT_EXPL: "Album only accessible with a valid password.", - ALBUM_PASSWORD_REQUIRED: "This album is protected by a password. Enter the password below to view the photos of this album:", - ALBUM_MERGE_1: "Are you sure you want to merge the album", - ALBUM_MERGE_2: "into the album", - ALBUMS_MERGE: "Are you sure you want to merge all selected albums into the album", - MERGE_ALBUM: "Merge Albums", - DONT_MERGE: "Don't Merge", - ALBUM_MOVE_1: "Are you sure you want to move the album", - ALBUM_MOVE_2: "into the album", - ALBUMS_MOVE: "Are you sure you want to move all selected albums into the album", - MOVE_ALBUMS: "Move Albums", - NOT_MOVE_ALBUMS: "Don't Move", - ROOT: "Root", - ALBUM_REUSE: "Reuse", - ALBUM_LICENSE: "License", - ALBUM_SET_LICENSE: "Set License", - ALBUM_LICENSE_HELP: "Need help choosing?", - ALBUM_LICENSE_NONE: "None", - ALBUM_RESERVED: "All Rights Reserved", - ALBUM_SET_ORDER: "Set Order", - ALBUM_ORDERING: "Order by", - - PHOTO_ABOUT: "About", - PHOTO_BASICS: "Basics", - PHOTO_TITLE: "Title", - PHOTO_NEW_TITLE: "Enter a new title for this photo:", - PHOTO_SET_TITLE: "Set Title", - PHOTO_UPLOADED: "Uploaded", - PHOTO_DESCRIPTION: "Description", - PHOTO_NEW_DESCRIPTION: "Enter a new description for this photo:", - PHOTO_SET_DESCRIPTION: "Set Description", - PHOTO_NEW_LICENSE: "Add a License", - PHOTO_SET_LICENSE: "Set License", - PHOTO_REUSE: "Reuse", - PHOTO_LICENSE: "License", - PHOTO_LICENSE_HELP: "Need help choosing?", - PHOTO_LICENSE_NONE: "None", - PHOTO_RESERVED: "All Rights Reserved", - PHOTO_IMAGE: "Image", - PHOTO_VIDEO: "Video", - PHOTO_SIZE: "Size", - PHOTO_FORMAT: "Format", - PHOTO_RESOLUTION: "Resolution", - PHOTO_DURATION: "Duration", - PHOTO_FPS: "Frame rate", - PHOTO_TAGS: "Tags", - PHOTO_NOTAGS: "No Tags", - PHOTO_NEW_TAGS: "Enter your tags for this photo. You can add multiple tags by separating them with a comma:", - PHOTO_NEW_TAGS_1: "Enter your tags for all", - PHOTO_NEW_TAGS_2: "selected photos. Existing tags will be overwritten. You can add multiple tags by separating them with a comma:", - PHOTO_SET_TAGS: "Set Tags", - PHOTO_CAMERA: "Camera", - PHOTO_CAPTURED: "Captured", - PHOTO_MAKE: "Make", - PHOTO_TYPE: "Type/Model", - PHOTO_LENS: "Lens", - PHOTO_SHUTTER: "Shutter Speed", - PHOTO_APERTURE: "Aperture", - PHOTO_FOCAL: "Focal Length", - PHOTO_ISO: "ISO", - PHOTO_SHARING: "Sharing", - PHOTO_SHR_PLUBLIC: "Public", - PHOTO_SHR_ALB: "Yes (Album)", - PHOTO_SHR_PHT: "Yes (Photo)", - PHOTO_SHR_NO: "No", - PHOTO_DELETE: "Delete Photo", - PHOTO_KEEP: "Keep Photo", - PHOTO_DELETE_1: "Are you sure you want to delete the photo", - PHOTO_DELETE_2: "? This action can't be undone!", - PHOTO_DELETE_ALL_1: "Are you sure you want to delete all", - PHOTO_DELETE_ALL_2: "selected photo? This action can't be undone!", - PHOTOS_NEW_TITLE_1: "Enter a title for all", - PHOTOS_NEW_TITLE_2: "selected photos:", - PHOTO_MAKE_PRIVATE_ALBUM: "This photo is located in a public album. To make this photo private or public, edit the visibility of the associated album.", - PHOTO_SHOW_ALBUM: "Show Album", - PHOTO_PUBLIC: "Public", - PHOTO_PUBLIC_EXPL: "Photo can be viewed by others, subject to the restrictions below.", - PHOTO_FULL: "Original", - PHOTO_FULL_EXPL: "Full-resolution picture is available.", - PHOTO_HIDDEN: "Hidden", - PHOTO_HIDDEN_EXPL: "Only people with the direct link can view this photo.", - PHOTO_DOWNLOADABLE: "Downloadable", - PHOTO_DOWNLOADABLE_EXPL: "Visitors of your gallery can download this photo.", - PHOTO_SHARE_BUTTON_VISIBLE: "Share button is visible", - PHOTO_SHARE_BUTTON_VISIBLE_EXPL: "Display social media sharing links.", - PHOTO_PASSWORD_PROT: "Password protected", - PHOTO_PASSWORD_PROT_EXPL: "Photo only accessible with a valid password.", - PHOTO_EDIT_SHARING_TEXT: "The sharing properties of this photo will be changed to the following:", - PHOTO_NO_EDIT_SHARING_TEXT: "Because this photo is located in a public album, it inherits that album's visibility settings. Its current visibility is shown below for informational purposes only.", - PHOTO_EDIT_GLOBAL_SHARING_TEXT: "The visibility of this photo can be fine-tuned using global Lychee settings. Its current visibility is shown below for informational purposes only.", - PHOTO_SHARING_CONFIRM: "Save", - PHOTO_LOCATION: "Location", - PHOTO_LATITUDE: "Latitude", - PHOTO_LONGITUDE: "Longitude", - PHOTO_ALTITUDE: "Altitude", - PHOTO_IMGDIRECTION: "Direction", - - LOADING: "Loading", - ERROR: "Error", - ERROR_TEXT: "Whoops, it looks like something went wrong. Please reload the site and try again!", - ERROR_DB_1: "Unable to connect to host database because access was denied. Double-check your host, username and password and ensure that access from your current location is permitted.", - ERROR_DB_2: "Unable to create the database. Double-check your host, username and password and ensure that the specified user has the rights to modify and add content to the database.", - ERROR_CONFIG_FILE: "Unable to save this configuration. Permission denied in 'data/'. Please set the read, write and execute rights for others in 'data/' and 'uploads/'. Take a look at the readme for more information.", - ERROR_UNKNOWN: "Something unexpected happened. Please try again and check your installation and server. Take a look at the readme for more information.", - ERROR_LOGIN: "Unable to save login. Please try again with another username and password!", - ERROR_MAP_DEACTIVATED: "Map functionality has been deactivated under settings.", - ERROR_SEARCH_DEACTIVATED: "Search functionality has been deactivated under settings.", - SUCCESS: "OK", - RETRY: "Retry", - - SETTINGS_WARNING: "Changing these advanced settings can be harmful to the stability, security and performance of this application. You should only modify them if you are sure of what you are doing.", - SETTINGS_SUCCESS_LOGIN: "Login Info updated.", - SETTINGS_SUCCESS_SORT: "Sorting order updated.", - SETTINGS_SUCCESS_DROPBOX: "Dropbox Key updated.", - SETTINGS_SUCCESS_LANG: "Language updated", - SETTINGS_SUCCESS_LAYOUT: "Layout updated", - SETTINGS_SUCCESS_IMAGE_OVERLAY: "EXIF Overlay setting updated", - SETTINGS_SUCCESS_PUBLIC_SEARCH: "Public search updated", - SETTINGS_SUCCESS_LICENSE: "Default license updated", - SETTINGS_SUCCESS_MAP_DISPLAY: "Map display settings updated", - SETTINGS_SUCCESS_MAP_DISPLAY_PUBLIC: "Map display settings for public albums updated", - SETTINGS_SUCCESS_MAP_PROVIDER: "Map provider settings updated", - - U2F_NOT_SUPPORTED: "U2F not supported. Sorry.", - U2F_NOT_SECURE: "Environment not secured. U2F not available.", - U2F_REGISTER_KEY: "Register new device.", - U2F_REGISTRATION_SUCCESS: "Registration successful!", - U2F_AUTHENTIFICATION_SUCCESS: "Authentication successful!", - U2F_CREDENTIALS: "Credentials", - U2F_CREDENTIALS_DELETED: "Credentials deleted!", - - NEW_PHOTOS_NOTIFICATION: "Send new photos notification emails.", - SETTINGS_SUCCESS_NEW_PHOTOS_NOTIFICATION: "New photos notification updated", - USER_EMAIL_INSTRUCTION: "Add your email below to enable receiving email notifications.
To stop receiving emails, simply remove your email below.", - - SETTINGS_SUCCESS_CSS: "CSS updated", - SETTINGS_SUCCESS_UPDATE: "Settings updated with success", - - DB_INFO_TITLE: "Enter your database connection details below:", - DB_INFO_HOST: "Database Host (optional)", - DB_INFO_USER: "Database Username", - DB_INFO_PASSWORD: "Database Password", - DB_INFO_TEXT: "Lychee will create its own database. If required, you can enter the name of an existing database instead:", - DB_NAME: "Database Name (optional)", - DB_PREFIX: "Table prefix (optional)", - DB_CONNECT: "Connect", - - LOGIN_TITLE: "Enter a username and password for your installation:", - LOGIN_USERNAME: "New Username", - LOGIN_PASSWORD: "New Password", - LOGIN_PASSWORD_CONFIRM: "Confirm Password", - LOGIN_CREATE: "Create Login", - - PASSWORD_TITLE: "Enter your current username and password:", - USERNAME_CURRENT: "Current Username", - PASSWORD_CURRENT: "Current Password", - PASSWORD_TEXT: "Your username and password will be changed to the following:", - PASSWORD_CHANGE: "Change Login", - - EDIT_SHARING_TITLE: "Edit Sharing", - EDIT_SHARING_TEXT: "The sharing-properties of this album will be changed to the following:", - SHARE_ALBUM_TEXT: "This album will be shared with the following properties:", - ALBUM_SHARING_CONFIRM: "Save", - - SORT_ALBUM_BY_1: "Sort albums by", - SORT_ALBUM_BY_2: "in an", - SORT_ALBUM_BY_3: "order.", - - SORT_ALBUM_SELECT_1: "Creation Time", - SORT_ALBUM_SELECT_2: "Title", - SORT_ALBUM_SELECT_3: "Description", - SORT_ALBUM_SELECT_4: "Public", - SORT_ALBUM_SELECT_5: "Latest Take Date", - SORT_ALBUM_SELECT_6: "Oldest Take Date", - - SORT_PHOTO_BY_1: "Sort photos by", - SORT_PHOTO_BY_2: "in an", - SORT_PHOTO_BY_3: "order.", - - SORT_PHOTO_SELECT_1: "Upload Time", - SORT_PHOTO_SELECT_2: "Take Date", - SORT_PHOTO_SELECT_3: "Title", - SORT_PHOTO_SELECT_4: "Description", - SORT_PHOTO_SELECT_5: "Public", - SORT_PHOTO_SELECT_6: "Star", - SORT_PHOTO_SELECT_7: "Photo Format", - - SORT_ASCENDING: "Ascending", - SORT_DESCENDING: "Descending", - SORT_CHANGE: "Change Sorting", - - DROPBOX_TITLE: "Set Dropbox Key", - DROPBOX_TEXT: "In order to import photos from your Dropbox, you need a valid drop-ins app key from their website. Generate yourself a personal key and enter it below:", - - LANG_TEXT: "Change Lychee language for:", - LANG_TITLE: "Change Language", - - CSS_TEXT: "Personalize your CSS:", - CSS_TITLE: "Change CSS", - - LAYOUT_TYPE: "Layout of photos:", - LAYOUT_SQUARES: "Square thumbnails", - LAYOUT_JUSTIFIED: "With aspect, justified", - LAYOUT_UNJUSTIFIED: "With aspect, unjustified", - SET_LAYOUT: "Change layout", - PUBLIC_SEARCH_TEXT: "Public search allowed:", - - IMAGE_OVERLAY_TEXT: "Display image overlay by default:", - - OVERLAY_TYPE: "Photo overlay:", - OVERLAY_NONE: "None", - OVERLAY_EXIF: "EXIF data", - OVERLAY_DESCRIPTION: "Description", - OVERLAY_DATE: "Date taken", - - MAP_PROVIDER: "Provider of OpenStreetMap tiles:", - MAP_PROVIDER_WIKIMEDIA: "Wikimedia", - MAP_PROVIDER_OSM_ORG: "OpenStreetMap.org (no retina)", - MAP_PROVIDER_OSM_DE: "OpenStreetMap.de (no retina)", - MAP_PROVIDER_OSM_FR: "OpenStreetMap.fr (no retina)", - MAP_PROVIDER_RRZE: "University of Erlangen, Germany (only retina)", - - MAP_DISPLAY_TEXT: "Enable maps (provided by OpenStreetMap):", - MAP_DISPLAY_PUBLIC_TEXT: "Enable maps for public albums (provided by OpenStreetMap):", - MAP_INCLUDE_SUBALBUMS_TEXT: "Include photos of subalbums on map:", - LOCATION_DECODING: "Decode GPS data into location name", - LOCATION_SHOW: "Show location name", - LOCATION_SHOW_PUBLIC: "Show location name for public mode", - - NSFW_VISIBLE_TEXT_1: "Make Sensitive albums visible by default.", - NSFW_VISIBLE_TEXT_2: "If the album is public, it is still accessible, just hidden from the view and can be revealed by pressing H.", - SETTINGS_SUCCESS_NSFW_VISIBLE: "Default sensitive album visibility updated with success.", - - VIEW_NO_RESULT: "No results", - VIEW_NO_PUBLIC_ALBUMS: "No public albums", - VIEW_NO_CONFIGURATION: "No configuration", - VIEW_PHOTO_NOT_FOUND: "Photo not found", - - NO_TAGS: "No Tags", - - UPLOAD_MANAGE_NEW_PHOTOS: "You can now manage your new photo(s).", - UPLOAD_COMPLETE: "Upload complete", - UPLOAD_COMPLETE_FAILED: "Failed to upload one or more photos.", - UPLOAD_IMPORTING: "Importing", - UPLOAD_IMPORTING_URL: "Importing URL", - UPLOAD_UPLOADING: "Uploading", - UPLOAD_FINISHED: "Finished", - UPLOAD_PROCESSING: "Processing", - UPLOAD_FAILED: "Failed", - UPLOAD_FAILED_ERROR: "Upload failed. Server returned an error!", - UPLOAD_FAILED_WARNING: "Upload failed. Server returned a warning!", - UPLOAD_SKIPPED: "Skipped", - UPLOAD_UPDATED: "Updated", - UPLOAD_IMPORT_SKIPPED_DUPLICATE: "This photo has been skipped because it's already in your library.", - UPLOAD_IMPORT_RESYNCED_DUPLICATE: "This photo has been skipped because it's already in your library, but its metadata has been updated.", - UPLOAD_ERROR_CONSOLE: "Please take a look at the console of your browser for further details.", - UPLOAD_UNKNOWN: "Server returned an unknown response. Please take a look at the console of your browser for further details.", - UPLOAD_ERROR_UNKNOWN: "Upload failed. Server returned an unkown error!", - UPLOAD_ERROR_POSTSIZE: "Upload failed. The PHP post_max_size limit is too small!", - UPLOAD_ERROR_FILESIZE: "Upload failed. The PHP upload_max_filesize limit is too small!", - UPLOAD_IN_PROGRESS: "Lychee is currently uploading!", - UPLOAD_IMPORT_WARN_ERR: "The import has been finished, but returned warnings or errors. Please take a look at the log (Settings -> Show Log) for further details.", - UPLOAD_IMPORT_COMPLETE: "Import complete", - UPLOAD_IMPORT_INSTR: "Please enter the direct link to a photo to import it:", - UPLOAD_IMPORT: "Import", - UPLOAD_IMPORT_SERVER: "Importing from server", - UPLOAD_IMPORT_SERVER_FOLD: "Folder empty or no readable files to process. Please take a look at the log (Settings -> Show Log) for further details.", - UPLOAD_IMPORT_SERVER_INSTR: "This action will import all photos, folders and sub-folders which are located in the following directory. The original files will be deleted after the import when possible.", - UPLOAD_ABSOLUTE_PATH: "Absolute path to directory", - UPLOAD_IMPORT_SERVER_EMPT: "Could not start import because the folder was empty!", - - ABOUT_SUBTITLE: "Self-hosted photo-management done right", - ABOUT_DESCRIPTION: "is a free photo-management tool, which runs on your server or web-space. Installing is a matter of seconds. Upload, manage and share photos like from a native application. Lychee comes with everything you need and all your photos are stored securely.", - - URL_COPY_TO_CLIPBOARD: "Copy to clipboard", - URL_COPIED_TO_CLIPBOARD: "Copied URL to clipboard!", - PHOTO_DIRECT_LINKS_TO_IMAGES: "Direct links to image files:", - PHOTO_MEDIUM: "Medium", - PHOTO_MEDIUM_HIDPI: "Medium HiDPI", - PHOTO_SMALL: "Thumb", - PHOTO_SMALL_HIDPI: "Thumb HiDPI", - PHOTO_THUMB: "Square thumb", - PHOTO_THUMB_HIDPI: "Square thumb HiDPI", - PHOTO_LIVE_VIDEO: "Video part of live-photo", - PHOTO_VIEW: "Lychee Photo View:", - - /** - * Formats a number representing a filesize in bytes as a localized string - * @param {!number} filesize - * @return {string} A formatted and localized string - */ - printFilesizeLocalized: function printFilesizeLocalized(filesize) { - console.assert(Number.isInteger(filesize), "printFilesizeLocalized: expected integer, got %s", typeof filesize === "undefined" ? "undefined" : _typeof(filesize)); - var suffix = [" B", " kB", " MB", " GB"]; - var i = 0; - // Sic! We check if the number is larger than 1000 but divide by 1024 by intention - // We aim at a number which has at most 3 non-decimal digits, i.e. the result shall be in the interval - // [1000/1024, 1000) = [0.977, 1000) (lower bound included, upper bound excluded) - while (filesize >= 1000.0 && i < suffix.length) { - filesize = filesize / 1024.0; - i++; - } - - // The number of decimal digits is anti-proportional to the number of non-decimal digits - // In total, there shall always be three digits - if (filesize >= 100.0) { - filesize = Math.round(filesize); - } else if (filesize >= 10.0) { - filesize = Math.round(filesize * 10.0) / 10.0; - } else { - filesize = Math.round(filesize * 100.0) / 100.0; - } - - return Number(filesize).toLocaleString() + suffix[i]; - }, - - /** - * Converts a JSON encoded date/time into a localized string relative to - * the original timezone - * - * The localized string uses the JS "medium" verbosity. - * The precise definition of "medium verbosity" depends on the current locale, but for Western languages this - * means that the date portion is fully printed with digits (e.g. something like 03/30/2021 for English, - * 30/03/2021 for French and 30.03.2021 for German), and that the time portion is printed with a resolution of - * seconds with two digits for all parts either in 24h or 12h scheme (e.g. something like 02:24:13pm for English - * and 14:24:13 for French/German). - * - * @param {?string} jsonDateTime - * @return {string} A formatted and localized time - */ - printDateTime: function printDateTime(jsonDateTime) { - if (typeof jsonDateTime !== "string" || jsonDateTime === "") return ""; - - // Unfortunately, the built-in JS Date object is rather dumb. - // It is only required to support the timezone of the runtime - // environment and UTC. - // Moreover, the method `toLocalString` may or may not convert - // the represented time to the timezone of the runtime environment - // before formatting it as a string. - // However, we want to keep the printed time in the original timezone, - // because this facilitates human interaction with a photo. - // To this end we apply a "dirty" trick here. - // We first cut off any explicit timezone indication from the JSON - // string and only pass a date/time of the form `YYYYMMDDThhmmss` to - // `Date`. - // `Date` is required to interpret those time values according to the - // local timezone (see [MDN Web Docs - Date Time String Format](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse#date_time_string_format)). - // Most likely, the resulting `Date` object will represent the - // wrong instant in time (given in seconds since epoch), but we only - // want to call `toLocalString` which is fine and don't do any time - // arithmetics. - // Then we add the original timezone to the string manually. - var splitDateTime = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})([-Z+])(\d{2}:\d{2})?$/.exec(jsonDateTime); - console.assert(splitDateTime.length === 4, "'jsonDateTime' is not formatted acc. to ISO 8601; passed string was: " + jsonDateTime); - var locale = "default"; // use the user's browser settings - var format = { dateStyle: "medium", timeStyle: "medium" }; - var result = new Date(splitDateTime[1]).toLocaleString(locale, format); - if (splitDateTime[2] === "Z" || splitDateTime[3] === "00:00") { - result += " UTC"; - } else { - result += " UTC" + splitDateTime[2] + splitDateTime[3]; - } - return result; - }, - - /** - * Converts a JSON encoded date/time into a localized string which only displays month and year. - * - * The month is printed as a shortened word with 3/4 letters, the year is printed with 4 digits (e.g. something like - * "Aug 2020" in English or "Août 2020" in French). - * - * @param {?string} jsonDateTime - * @return {string} A formatted and localized month and year - */ - printMonthYear: function printMonthYear(jsonDateTime) { - if (typeof jsonDateTime !== "string" || jsonDateTime === "") return ""; - var locale = "default"; // use the user's browser settings - var format = { month: "short", year: "numeric" }; - return new Date(jsonDateTime).toLocaleDateString(locale, format); - } -}; - -/** - * @description Helper class to manage tabindex - */ - -var tabindex = { - offset_for_header: 100, - next_tab_index: 100 -}; - -tabindex.saveSettings = function (elem) { - if (!lychee.enable_tabindex) return; - - // Todo: Make shorter notation - // Get all elements which have a tabindex - var tmp = $(elem).find("[tabindex]"); - - // iterate over all elements and set tabindex to stored value (i.e. make is not focussable) - tmp.each(function (i, e) { - // TODO: shorter notation - a = $(e).attr("tabindex"); - $(this).data("tabindex-saved", a); - }); -}; - -tabindex.restoreSettings = function (elem) { - if (!lychee.enable_tabindex) return; - - // Todo: Make shorter noation - // Get all elements which have a tabindex - var tmp = $(elem).find("[tabindex]"); - - // iterate over all elements and set tabindex to stored value (i.e. make is not focussable) - tmp.each(function (i, e) { - // TODO: shorter notation - a = $(e).data("tabindex-saved"); - $(e).attr("tabindex", a); - }); -}; - -tabindex.makeUnfocusable = function (elem) { - var saveFocusElement = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; - - if (!lychee.enable_tabindex) return; - - // Todo: Make shorter noation - // Get all elements which have a tabindex - var tmp = $(elem).find("[tabindex]"); - - // iterate over all elements and set tabindex to -1 (i.e. make is not focussable) - tmp.each(function (i, e) { - $(e).attr("tabindex", "-1"); - // Save which element had focus before we make it unfocusable - if (saveFocusElement && $(e).is(":focus")) { - $(e).data("tabindex-focus", true); - // Remove focus - $(e).blur(); - } - }); - - // Disable input fields - $(elem).find("input").attr("disabled", "disabled"); -}; - -tabindex.makeFocusable = function (elem) { - var restoreFocusElement = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; - - if (!lychee.enable_tabindex) return; - - // Todo: Make shorter noation - // Get all elements which have a tabindex - var tmp = $(elem).find("[data-tabindex]"); - - // iterate over all elements and set tabindex to stored value (i.e. make is not focussable) - tmp.each(function (i, e) { - $(e).attr("tabindex", $(e).data("tabindex")); - // restore focus elemente if wanted - if (restoreFocusElement) { - if ($(e).data("tabindex-focus") && lychee.active_focus_on_page_load) { - $(e).focus(); - $(e).removeData("tabindex-focus"); - } - } - }); - - // Enable input fields - $(elem).find("input").removeAttr("disabled"); -}; - -tabindex.get_next_tab_index = function () { - tabindex.next_tab_index = tabindex.next_tab_index + 1; - - return tabindex.next_tab_index - 1; -}; - -tabindex.reset = function () { - tabindex.next_tab_index = tabindex.offset_for_header; -}; - -(function (window, factory) { - var basicContext = factory(window, window.document); - window.basicContext = basicContext; - if ((typeof module === "undefined" ? "undefined" : _typeof(module)) == "object" && module.exports) { - module.exports = basicContext; - } -})(window, function l(window, document) { - var ITEM = "item", - SEPARATOR = "separator"; - - var dom = function dom() { - var elem = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ""; - - return document.querySelector(".basicContext " + elem); - }; - - var valid = function valid() { - var item = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - - var emptyItem = Object.keys(item).length === 0 ? true : false; - - if (emptyItem === true) item.type = SEPARATOR; - if (item.type == null) item.type = ITEM; - if (item.class == null) item.class = ""; - if (item.visible !== false) item.visible = true; - if (item.icon == null) item.icon = null; - if (item.title == null) item.title = "Undefined"; - - // Add disabled class when item disabled - if (item.disabled !== true) item.disabled = false; - if (item.disabled === true) item.class += " basicContext__item--disabled"; - - // Item requires a function when - // it's not a separator and not disabled - if (item.fn == null && item.type !== SEPARATOR && item.disabled === false) { - console.warn("Missing fn for item '" + item.title + "'"); - return false; - } - - return true; - }; - - var buildItem = function buildItem(item, num) { - var html = "", - span = ""; - - // Parse and validate item - if (valid(item) === false) return ""; - - // Skip when invisible - if (item.visible === false) return ""; - - // Give item a unique number - item.num = num; - - // Generate span/icon-element - if (item.icon !== null) span = ""; - - // Generate item - if (item.type === ITEM) { - html = "\n\t\t \n\t\t " + span + item.title + "\n\t\t \n\t\t "; - } else if (item.type === SEPARATOR) { - html = "\n\t\t \n\t\t "; - } - - return html; - }; - - var build = function build(items) { - var html = ""; - - html += "\n\t
\n\t
\n\t \n\t \n\t "; - - items.forEach(function (item, i) { - return html += buildItem(item, i); - }); - - html += "\n\t \n\t
\n\t
\n\t
\n\t "; - - return html; - }; - - var getNormalizedEvent = function getNormalizedEvent() { - var e = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - - var pos = { - x: e.clientX, - y: e.clientY - }; - - if (e.type === "touchend" && (pos.x == null || pos.y == null)) { - // We need to capture clientX and clientY from original event - // when the event 'touchend' does not return the touch position - - var touches = e.changedTouches; - - if (touches != null && touches.length > 0) { - pos.x = touches[0].clientX; - pos.y = touches[0].clientY; - } - } - - // Position unknown - if (pos.x == null || pos.x < 0) pos.x = 0; - if (pos.y == null || pos.y < 0) pos.y = 0; - - return pos; - }; - - var getPosition = function getPosition(e, context) { - // Get the click position - var normalizedEvent = getNormalizedEvent(e); - - // Set the initial position - var x = normalizedEvent.x, - y = normalizedEvent.y; - - var container = document.querySelector(".basicContextContainer"); - - // Get size of browser - var browserSize = { - width: container.offsetWidth, - height: container.offsetHeight - }; - - // Get size of context - var contextSize = { - width: context.offsetWidth, - height: context.offsetHeight - }; - - // Fix position based on context and browser size - if (x + contextSize.width > browserSize.width) x = x - (x + contextSize.width - browserSize.width); - if (y + contextSize.height > browserSize.height) y = y - (y + contextSize.height - browserSize.height); - - // Make context scrollable and start at the top of the browser - // when context is higher than the browser - if (contextSize.height > browserSize.height) { - y = 0; - context.classList.add("basicContext--scrollable"); - } - - // Calculate the relative position of the mouse to the context - var rx = normalizedEvent.x - x, - ry = normalizedEvent.y - y; - - return { x: x, y: y, rx: rx, ry: ry }; - }; - - var bind = function bind() { - var item = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - - if (item.fn == null) return false; - if (item.visible === false) return false; - if (item.disabled === true) return false; - - dom("td[data-num='" + item.num + "']").onclick = item.fn; - dom("td[data-num='" + item.num + "']").oncontextmenu = item.fn; - - return true; - }; - - var show = function show(items, e, fnClose, fnCallback) { - // Build context - var html = build(items); - - // Add context to the body - document.body.insertAdjacentHTML("beforeend", html); - - // Cache the context - var context = dom(); - - // Calculate position - var position = getPosition(e, context); - - // Set position - context.style.left = position.x + "px"; - context.style.top = position.y + "px"; - context.style.transformOrigin = position.rx + "px " + position.ry + "px"; - context.style.opacity = 1; - - // Close fn fallback - if (fnClose == null) fnClose = close; - - // Bind click on background - context.parentElement.onclick = fnClose; - context.parentElement.oncontextmenu = fnClose; - - // Bind click on items - items.forEach(bind); - - // Do not trigger default event or further propagation - if (typeof e.preventDefault === "function") e.preventDefault(); - if (typeof e.stopPropagation === "function") e.stopPropagation(); - - // Call callback when a function - if (typeof fnCallback === "function") fnCallback(); - - return true; - }; - - var visible = function visible() { - var elem = dom(); - - return !(elem == null || elem.length === 0); - }; - - var close = function close() { - if (visible() === false) return false; - - var container = document.querySelector(".basicContextContainer"); - - container.parentElement.removeChild(container); - - return true; - }; - - return { - ITEM: ITEM, - SEPARATOR: SEPARATOR, - show: show, - visible: visible, - close: close - }; -}); \ No newline at end of file diff --git a/public/fonts/roboto-v29-300.woff2 b/public/fonts/roboto-v29-300.woff2 new file mode 100644 index 00000000000..ccadbb0b76f Binary files /dev/null and b/public/fonts/roboto-v29-300.woff2 differ diff --git a/public/fonts/roboto-v29-400.woff2 b/public/fonts/roboto-v29-400.woff2 new file mode 100644 index 00000000000..55affe52e58 Binary files /dev/null and b/public/fonts/roboto-v29-400.woff2 differ diff --git a/public/fonts/roboto-v29-700.woff2 b/public/fonts/roboto-v29-700.woff2 new file mode 100644 index 00000000000..31abf9f17d5 Binary files /dev/null and b/public/fonts/roboto-v29-700.woff2 differ diff --git a/public/installer/assets/css/style.css b/public/installer/assets/css/style.css old mode 100755 new mode 100644 index 6df02d9da8f..c6019ed57e8 --- a/public/installer/assets/css/style.css +++ b/public/installer/assets/css/style.css @@ -5,12 +5,10 @@ */ /* FONT PATH * -------------------------- */ -@import url("https://fonts.googleapis.com/css?family=Roboto:400,300,500,700,900"); - @font-face { font-family: 'FontAwesome'; - src: url("../fonts/fontawesome-webfont.eot?v=4.7.0"); - src: url("../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0") format("embedded-opentype"), url("../fonts/fontawesome-webfont.woff2?v=4.7.0") format("woff2"), url("../fonts/fontawesome-webfont.woff?v=4.7.0") format("woff"), url("../fonts/fontawesome-webfont.ttf?v=4.7.0") format("truetype"), url("../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular") format("svg"); + src: url("../fonts/fontawesome-webfont.eot"); + src: url("../fonts/fontawesome-webfont.eot") format("embedded-opentype"), url("../fonts/fontawesome-webfont.woff2") format("woff2"), url("../fonts/fontawesome-webfont.woff") format("woff"), url("../fonts/fontawesome-webfont.ttf") format("truetype"), url("../fonts/fontawesome-webfont.svg") format("svg"); font-weight: normal; font-style: normal; } @@ -3061,7 +3059,7 @@ button, input, optgroup, select, textarea { html { -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; - font-family: Roboto, Helvetica Neue, Helvetica, Arial, sans-serif; + font-family: Helvetica Neue, Helvetica, Arial, sans-serif; font-weight: 300; color: #666; font-size: 12px; @@ -3218,7 +3216,7 @@ h1 { line-height: 1.130880986943067em; margin: 0; padding: 0; - font-family: Roboto, Helvetica Neue, Helvetica, Arial, sans-serif; + font-family: Helvetica Neue, Helvetica, Arial, sans-serif; font-weight: 500; color: #111; clear: both; @@ -3572,7 +3570,7 @@ form { h2 { padding: 0; - font-family: Roboto, Helvetica Neue, Helvetica, Arial, sans-serif; + font-family: Helvetica Neue, Helvetica, Arial, sans-serif; font-weight: 500; color: #111; clear: both; @@ -3583,7 +3581,7 @@ h2 { h3 { padding: 0; - font-family: Roboto, Helvetica Neue, Helvetica, Arial, sans-serif; + font-family: Helvetica Neue, Helvetica, Arial, sans-serif; font-weight: 500; color: #111; clear: both; @@ -3594,7 +3592,7 @@ h3 { h4 { padding: 0; - font-family: Roboto, Helvetica Neue, Helvetica, Arial, sans-serif; + font-family: Helvetica Neue, Helvetica, Arial, sans-serif; font-weight: 500; color: #111; clear: both; @@ -3605,7 +3603,7 @@ h4 { h5 { padding: 0; - font-family: Roboto, Helvetica Neue, Helvetica, Arial, sans-serif; + font-family: Helvetica Neue, Helvetica, Arial, sans-serif; font-weight: 500; color: #111; clear: both; @@ -3616,7 +3614,7 @@ h5 { h6 { padding: 0; - font-family: Roboto, Helvetica Neue, Helvetica, Arial, sans-serif; + font-family: Helvetica Neue, Helvetica, Arial, sans-serif; font-weight: 500; color: #111; clear: both; @@ -3945,31 +3943,6 @@ input[type=text] { color: #fff; } -.step__icon.welcome:before { - content: '\f144'; - font-family: Ionicons; -} - -.step__icon.requirements:before { - content: '\f127'; - font-family: Ionicons; -} - -.step__icon.permissions:before { - content: '\f296'; - font-family: Ionicons; -} - -.step__icon.database:before { - content: '\f454'; - font-family: Ionicons; -} - -.step__icon.update:before { - content: '\f2bf'; - font-family: Ionicons; -} - .main { margin-top: -20px; background-color: #fff; @@ -3982,6 +3955,10 @@ input[type=text] { font-size: 16px; } +.ml-4 { + margin-left: 1.5rem; +} + .buttons { text-align: center; } @@ -4005,9 +3982,11 @@ input[type=text] { text-decoration: none; outline: none; border: none; + border-top: 1px solid #eee; -webkit-transition: background-color .2s ease, -webkit-box-shadow .2s ease; transition: box-shadow .2s ease, background-color .2s ease, -webkit-box-shadow .2s ease; cursor: pointer; + font-size: 12px; } .button:hover { @@ -4018,7 +3997,6 @@ input[type=text] { } .button--light { - padding: 3px 16px; font-size: 16px; border-top: 1px solid #eee; color: #222; diff --git a/public/installer/assets/fonts/FontAwesome.otf b/public/installer/assets/fonts/FontAwesome.otf deleted file mode 100644 index 401ec0f36e4..00000000000 Binary files a/public/installer/assets/fonts/FontAwesome.otf and /dev/null differ diff --git a/public/installer/assets/fonts/ionicons.eot b/public/installer/assets/fonts/ionicons.eot deleted file mode 100644 index 92a3f20a392..00000000000 Binary files a/public/installer/assets/fonts/ionicons.eot and /dev/null differ diff --git a/public/installer/assets/fonts/ionicons.svg b/public/installer/assets/fonts/ionicons.svg deleted file mode 100644 index 49fc8f36740..00000000000 --- a/public/installer/assets/fonts/ionicons.svg +++ /dev/null @@ -1,2230 +0,0 @@ - - - - - -Created by FontForge 20120731 at Thu Dec 4 09:51:48 2014 - By Adam Bradley -Created by Adam Bradley with FontForge 2.0 (http://fontforge.sf.net) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/installer/assets/fonts/ionicons.ttf b/public/installer/assets/fonts/ionicons.ttf deleted file mode 100644 index c4e4632486d..00000000000 Binary files a/public/installer/assets/fonts/ionicons.ttf and /dev/null differ diff --git a/public/installer/assets/fonts/ionicons.woff b/public/installer/assets/fonts/ionicons.woff deleted file mode 100644 index 5f3a14e0a5c..00000000000 Binary files a/public/installer/assets/fonts/ionicons.woff and /dev/null differ diff --git a/public/installer/assets/img/background.png b/public/installer/assets/img/background.png old mode 100755 new mode 100644 diff --git a/public/installer/assets/img/favicon/favicon-16x16.png b/public/installer/assets/img/favicon/favicon-16x16.png old mode 100755 new mode 100644 diff --git a/public/installer/assets/img/favicon/favicon-32x32.png b/public/installer/assets/img/favicon/favicon-32x32.png old mode 100755 new mode 100644 diff --git a/public/installer/assets/img/favicon/favicon-96x96.png b/public/installer/assets/img/favicon/favicon-96x96.png old mode 100755 new mode 100644 diff --git a/public/installer/assets/img/pattern.png b/public/installer/assets/img/pattern.png deleted file mode 100755 index 8b3a8258174..00000000000 Binary files a/public/installer/assets/img/pattern.png and /dev/null differ diff --git a/public/js/app.js b/public/js/app.js deleted file mode 100644 index f3cce93c751..00000000000 --- a/public/js/app.js +++ /dev/null @@ -1,166 +0,0 @@ -/******/ (() => { // webpackBootstrap -/******/ var __webpack_modules__ = ({ - -/***/ "./resources/assets/js/app.js": -/*!************************************!*\ - !*** ./resources/assets/js/app.js ***! - \************************************/ -/***/ (() => { - - - -/***/ }), - -/***/ "./resources/assets/scss/app.scss": -/*!****************************************!*\ - !*** ./resources/assets/scss/app.scss ***! - \****************************************/ -/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { - -"use strict"; -__webpack_require__.r(__webpack_exports__); -// extracted by mini-css-extract-plugin - - -/***/ }) - -/******/ }); -/************************************************************************/ -/******/ // The module cache -/******/ var __webpack_module_cache__ = {}; -/******/ -/******/ // The require function -/******/ function __webpack_require__(moduleId) { -/******/ // Check if module is in cache -/******/ var cachedModule = __webpack_module_cache__[moduleId]; -/******/ if (cachedModule !== undefined) { -/******/ return cachedModule.exports; -/******/ } -/******/ // Create a new module (and put it into the cache) -/******/ var module = __webpack_module_cache__[moduleId] = { -/******/ // no module.id needed -/******/ // no module.loaded needed -/******/ exports: {} -/******/ }; -/******/ -/******/ // Execute the module function -/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); -/******/ -/******/ // Return the exports of the module -/******/ return module.exports; -/******/ } -/******/ -/******/ // expose the modules object (__webpack_modules__) -/******/ __webpack_require__.m = __webpack_modules__; -/******/ -/************************************************************************/ -/******/ /* webpack/runtime/chunk loaded */ -/******/ (() => { -/******/ var deferred = []; -/******/ __webpack_require__.O = (result, chunkIds, fn, priority) => { -/******/ if(chunkIds) { -/******/ priority = priority || 0; -/******/ for(var i = deferred.length; i > 0 && deferred[i - 1][2] > priority; i--) deferred[i] = deferred[i - 1]; -/******/ deferred[i] = [chunkIds, fn, priority]; -/******/ return; -/******/ } -/******/ var notFulfilled = Infinity; -/******/ for (var i = 0; i < deferred.length; i++) { -/******/ var [chunkIds, fn, priority] = deferred[i]; -/******/ var fulfilled = true; -/******/ for (var j = 0; j < chunkIds.length; j++) { -/******/ if ((priority & 1 === 0 || notFulfilled >= priority) && Object.keys(__webpack_require__.O).every((key) => (__webpack_require__.O[key](chunkIds[j])))) { -/******/ chunkIds.splice(j--, 1); -/******/ } else { -/******/ fulfilled = false; -/******/ if(priority < notFulfilled) notFulfilled = priority; -/******/ } -/******/ } -/******/ if(fulfilled) { -/******/ deferred.splice(i--, 1) -/******/ result = fn(); -/******/ } -/******/ } -/******/ return result; -/******/ }; -/******/ })(); -/******/ -/******/ /* webpack/runtime/hasOwnProperty shorthand */ -/******/ (() => { -/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) -/******/ })(); -/******/ -/******/ /* webpack/runtime/make namespace object */ -/******/ (() => { -/******/ // define __esModule on exports -/******/ __webpack_require__.r = (exports) => { -/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { -/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); -/******/ } -/******/ Object.defineProperty(exports, '__esModule', { value: true }); -/******/ }; -/******/ })(); -/******/ -/******/ /* webpack/runtime/jsonp chunk loading */ -/******/ (() => { -/******/ // no baseURI -/******/ -/******/ // object to store loaded and loading chunks -/******/ // undefined = chunk not loaded, null = chunk preloaded/prefetched -/******/ // [resolve, reject, Promise] = chunk loading, 0 = chunk loaded -/******/ var installedChunks = { -/******/ "/js/app": 0, -/******/ "css/app": 0 -/******/ }; -/******/ -/******/ // no chunk on demand loading -/******/ -/******/ // no prefetching -/******/ -/******/ // no preloaded -/******/ -/******/ // no HMR -/******/ -/******/ // no HMR manifest -/******/ -/******/ __webpack_require__.O.j = (chunkId) => (installedChunks[chunkId] === 0); -/******/ -/******/ // install a JSONP callback for chunk loading -/******/ var webpackJsonpCallback = (parentChunkLoadingFunction, data) => { -/******/ var [chunkIds, moreModules, runtime] = data; -/******/ // add "moreModules" to the modules object, -/******/ // then flag all "chunkIds" as loaded and fire callback -/******/ var moduleId, chunkId, i = 0; -/******/ for(moduleId in moreModules) { -/******/ if(__webpack_require__.o(moreModules, moduleId)) { -/******/ __webpack_require__.m[moduleId] = moreModules[moduleId]; -/******/ } -/******/ } -/******/ if(runtime) var result = runtime(__webpack_require__); -/******/ if(parentChunkLoadingFunction) parentChunkLoadingFunction(data); -/******/ for(;i < chunkIds.length; i++) { -/******/ chunkId = chunkIds[i]; -/******/ if(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) { -/******/ installedChunks[chunkId][0](); -/******/ } -/******/ installedChunks[chunkIds[i]] = 0; -/******/ } -/******/ return __webpack_require__.O(result); -/******/ } -/******/ -/******/ var chunkLoadingGlobal = self["webpackChunk"] = self["webpackChunk"] || []; -/******/ chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0)); -/******/ chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal)); -/******/ })(); -/******/ -/************************************************************************/ -/******/ -/******/ // startup -/******/ // Load entry module and return exports -/******/ // This entry module depends on other loaded chunks and execution need to be delayed -/******/ __webpack_require__.O(undefined, ["css/app"], () => (__webpack_require__("./resources/assets/js/app.js"))) -/******/ var __webpack_exports__ = __webpack_require__.O(undefined, ["css/app"], () => (__webpack_require__("./resources/assets/scss/app.scss"))) -/******/ __webpack_exports__ = __webpack_require__.O(__webpack_exports__); -/******/ -/******/ })() -; \ No newline at end of file diff --git a/public/mix-manifest.json b/public/mix-manifest.json deleted file mode 100644 index 2d60117130c..00000000000 --- a/public/mix-manifest.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "/js/app.js": "/js/app.js", - "/css/app.css": "/css/app.css" -} diff --git a/public/robots.txt b/public/robots.txt index 3b0e8822be7..811bc072d30 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -11,6 +11,5 @@ Disallow: /docs/ Disallow: /img/ Disallow: /php/ Disallow: /plugins/ -Disallow: /src/ Disallow: /sym/ Disallow: /uploads/ \ No newline at end of file diff --git a/public/src/.gitignore b/public/src/.gitignore deleted file mode 100644 index 9937604b018..00000000000 --- a/public/src/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -# Ignore everything in this directory, it's a placeholder for js source files needes for js.map files -# of leaflet.Markercluster plugin -* -# Except this file -!.gitignore diff --git a/public/storage b/public/storage new file mode 120000 index 00000000000..7051e94fa64 --- /dev/null +++ b/public/storage @@ -0,0 +1 @@ +../storage/app/public \ No newline at end of file diff --git a/public/uploads/big/index.html b/public/uploads/medium2x/index.html similarity index 100% rename from public/uploads/big/index.html rename to public/uploads/medium2x/index.html diff --git a/public/uploads/raw/index.html b/public/uploads/original/index.html similarity index 100% rename from public/uploads/raw/index.html rename to public/uploads/original/index.html diff --git a/resources/assets/js/app.js b/public/uploads/small2x/index.html similarity index 100% rename from resources/assets/js/app.js rename to public/uploads/small2x/index.html diff --git a/public/uploads/thumb2x/index.html b/public/uploads/thumb2x/index.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/public/uploads/tracks/index.html b/public/uploads/tracks/index.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/public/vendor/log-viewer/app.css b/public/vendor/log-viewer/app.css new file mode 100644 index 00000000000..ec5b3c4f5af --- /dev/null +++ b/public/vendor/log-viewer/app.css @@ -0,0 +1 @@ +/*! tailwindcss v3.3.5 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid #e4e4e7;box-sizing:border-box}:after,:before{--tw-content:""}html{-webkit-text-size-adjust:100%;font-feature-settings:normal;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-variation-settings:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{font-feature-settings:inherit;color:inherit;font-family:inherit;font-size:100%;font-variation-settings:inherit;font-weight:inherit;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#a1a1aa;opacity:1}input::placeholder,textarea::placeholder{color:#a1a1aa;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]{display:none}*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.\!container{width:100%!important}.container{width:100%}@media (min-width:640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media (min-width:768px){.\!container{max-width:768px!important}.container{max-width:768px}}@media (min-width:1024px){.\!container{max-width:1024px!important}.container{max-width:1024px}}@media (min-width:1280px){.\!container{max-width:1280px!important}.container{max-width:1280px}}@media (min-width:1536px){.\!container{max-width:1536px!important}.container{max-width:1536px}}.sr-only{clip:rect(0,0,0,0);border-width:0;height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;white-space:nowrap;width:1px}.pointer-events-none{pointer-events:none}.pointer-events-auto{pointer-events:auto}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.inset-y-0{bottom:0;top:0}.-left-\[200\%\]{left:-200%}.bottom-0{bottom:0}.bottom-10{bottom:2.5rem}.bottom-4{bottom:1rem}.left-0{left:0}.left-3{left:.75rem}.right-0{right:0}.right-4{right:1rem}.right-7{right:1.75rem}.right-\[200\%\]{right:200%}.top-0{top:0}.top-9{top:2.25rem}.z-10{z-index:10}.z-20{z-index:20}.m-1{margin:.25rem}.-my-1{margin-bottom:-.25rem;margin-top:-.25rem}.mx-1{margin-left:.25rem;margin-right:.25rem}.mx-1\.5{margin-left:.375rem;margin-right:.375rem}.mx-2{margin-left:.5rem;margin-right:.5rem}.mx-3{margin-left:.75rem;margin-right:.75rem}.my-1{margin-bottom:.25rem;margin-top:.25rem}.my-auto{margin-bottom:auto;margin-top:auto}.-mb-0{margin-bottom:0}.-mb-0\.5{margin-bottom:-.125rem}.-mb-px{margin-bottom:-1px}.-mr-2{margin-right:-.5rem}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mb-8{margin-bottom:2rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-3{margin-left:.75rem}.ml-5{margin-left:1.25rem}.mr-1{margin-right:.25rem}.mr-1\.5{margin-right:.375rem}.mr-2{margin-right:.5rem}.mr-2\.5{margin-right:.625rem}.mr-4{margin-right:1rem}.mr-5{margin-right:1.25rem}.mt-0{margin-top:0}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-6{margin-top:1.5rem}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.contents{display:contents}.hidden{display:none}.h-0{height:0}.h-14{height:3.5rem}.h-3{height:.75rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-7{height:1.75rem}.h-\[18px\]{height:18px}.h-full{height:100%}.max-h-60{max-height:15rem}.max-h-screen{max-height:100vh}.min-h-\[38px\]{min-height:38px}.min-h-screen{min-height:100vh}.w-14{width:3.5rem}.w-3{width:.75rem}.w-4{width:1rem}.w-48{width:12rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-\[18px\]{width:18px}.w-full{width:100%}.w-screen{width:100vw}.min-w-\[240px\]{min-width:240px}.min-w-full{min-width:100%}.max-w-\[1px\]{max-width:1px}.max-w-full{max-width:100%}.max-w-md{max-width:28rem}.flex-1{flex:1 1 0%}.shrink{flex-shrink:1}.table-fixed{table-layout:fixed}.border-separate{border-collapse:separate}.translate-x-0{--tw-translate-x:0px}.translate-x-0,.translate-x-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-full{--tw-translate-x:100%}.rotate-90{--tw-rotate:90deg}.rotate-90,.scale-100{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-100{--tw-scale-x:1;--tw-scale-y:1}.scale-90{--tw-scale-x:.9;--tw-scale-y:.9}.scale-90,.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.animate-spin{animation:spin 1s linear infinite}.cursor-default{cursor:default}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.grid-flow-col{grid-auto-flow:column}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.items-start{align-items:flex-start}.items-center{align-items:center}.items-baseline{align-items:baseline}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.space-x-6>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(1.5rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(1.5rem*var(--tw-space-x-reverse))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-visible{overflow:visible}.overflow-y-auto{overflow-y:auto}.overflow-y-scroll{overflow-y:scroll}.truncate{overflow:hidden;text-overflow:ellipsis}.truncate,.whitespace-nowrap{white-space:nowrap}.rounded{border-radius:.25rem}.rounded-md{border-radius:.375rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-l{border-left-width:1px}.border-t{border-top-width:1px}.border-t-2{border-top-width:2px}.border-brand-500{--tw-border-opacity:1;border-color:rgb(14 165 233/var(--tw-border-opacity))}.border-gray-200{--tw-border-opacity:1;border-color:rgb(228 228 231/var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgb(212 212 216/var(--tw-border-opacity))}.border-transparent{border-color:transparent}.border-yellow-300{--tw-border-opacity:1;border-color:rgb(253 224 71/var(--tw-border-opacity))}.bg-brand-600{--tw-bg-opacity:1;background-color:rgb(2 132 199/var(--tw-bg-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(244 244 245/var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(250 250 250/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-yellow-100{--tw-bg-opacity:1;background-color:rgb(254 249 195/var(--tw-bg-opacity))}.bg-opacity-75{--tw-bg-opacity:0.75}.bg-gradient-to-t{background-image:linear-gradient(to top,var(--tw-gradient-stops))}.from-gray-100{--tw-gradient-from:#f4f4f5 var(--tw-gradient-from-position);--tw-gradient-to:hsla(240,5%,96%,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.to-transparent{--tw-gradient-to:transparent var(--tw-gradient-to-position)}.p-12{padding:3rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-8{padding-left:2rem;padding-right:2rem}.py-1{padding-bottom:.25rem;padding-top:.25rem}.py-2{padding-bottom:.5rem;padding-top:.5rem}.py-4{padding-bottom:1rem;padding-top:1rem}.py-5{padding-bottom:1.25rem;padding-top:1.25rem}.py-6{padding-bottom:1.5rem;padding-top:1.5rem}.pb-1{padding-bottom:.25rem}.pb-16{padding-bottom:4rem}.pl-10{padding-left:2.5rem}.pl-3{padding-left:.75rem}.pl-4{padding-left:1rem}.pr-10{padding-right:2.5rem}.pr-2{padding-right:.5rem}.pr-4{padding-right:1rem}.pr-9{padding-right:2.25rem}.pt-2{padding-top:.5rem}.pt-3{padding-top:.75rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-middle{vertical-align:middle}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-base{font-size:1rem;line-height:1.5rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.italic{font-style:italic}.leading-5{line-height:1.25rem}.leading-6{line-height:1.5rem}.text-blue-600{--tw-text-opacity:1;color:rgb(37 99 235/var(--tw-text-opacity))}.text-brand-500{--tw-text-opacity:1;color:rgb(14 165 233/var(--tw-text-opacity))}.text-brand-600{--tw-text-opacity:1;color:rgb(2 132 199/var(--tw-text-opacity))}.text-brand-700{--tw-text-opacity:1;color:rgb(3 105 161/var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(113 113 122/var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity:1;color:rgb(82 82 91/var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity:1;color:rgb(63 63 70/var(--tw-text-opacity))}.text-gray-800{--tw-text-opacity:1;color:rgb(39 39 42/var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgb(24 24 27/var(--tw-text-opacity))}.text-green-600{--tw-text-opacity:1;color:rgb(22 163 74/var(--tw-text-opacity))}.text-red-600{--tw-text-opacity:1;color:rgb(220 38 38/var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.text-yellow-700{--tw-text-opacity:1;color:rgb(161 98 7/var(--tw-text-opacity))}.opacity-0{opacity:0}.opacity-100{opacity:1}.opacity-25{opacity:.25}.opacity-60{opacity:.6}.opacity-75{opacity:.75}.opacity-90{opacity:.9}.shadow-md{--tw-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1);--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.shadow-md,.shadow-xl{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1);--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color)}.outline-brand-500{outline-color:#0ea5e9}.ring-1{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-opacity-5{--tw-ring-opacity:0.05}.blur{--tw-blur:blur(8px)}.blur,.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1)}.duration-100{transition-duration:.1s}.duration-200{transition-duration:.2s}.ease-in{transition-timing-function:cubic-bezier(.4,0,1,1)}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.spin{-webkit-animation-duration:1.5s;-moz-animation-duration:1.5s;-ms-animation-duration:1.5s;animation-duration:1.5s;-webkit-animation-iteration-count:infinite;-moz-animation-iteration-count:infinite;-ms-animation-iteration-count:infinite;animation-iteration-count:infinite;-webkit-animation-name:spin;-moz-animation-name:spin;-ms-animation-name:spin;animation-name:spin;-webkit-animation-timing-function:linear;-moz-animation-timing-function:linear;-ms-animation-timing-function:linear;animation-timing-function:linear}@keyframes spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}html.dark{color-scheme:dark}#bmc-wbtn{height:48px!important;width:48px!important}#bmc-wbtn>img{height:32px!important;width:32px!important}.log-levels-selector .dropdown-toggle{white-space:nowrap}.log-levels-selector .dropdown-toggle:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);--tw-ring-opacity:1;--tw-ring-color:rgb(14 165 233/var(--tw-ring-opacity));box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);outline:2px solid transparent;outline-offset:2px}:is(.dark .log-levels-selector .dropdown-toggle:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(3 105 161/var(--tw-ring-opacity))}.log-levels-selector .dropdown-toggle>svg{height:1rem;margin-left:.25rem;opacity:.75;width:1rem}.log-levels-selector .dropdown .log-level{font-weight:600}.log-levels-selector .dropdown .log-level.notice,.log-levels-selector .dropdown .log-level.success{--tw-text-opacity:1;color:rgb(4 120 87/var(--tw-text-opacity))}:is(.dark .log-levels-selector .dropdown .log-level.notice),:is(.dark .log-levels-selector .dropdown .log-level.success){--tw-text-opacity:1;color:rgb(16 185 129/var(--tw-text-opacity))}.log-levels-selector .dropdown .log-level.info{--tw-text-opacity:1;color:rgb(3 105 161/var(--tw-text-opacity))}:is(.dark .log-levels-selector .dropdown .log-level.info){--tw-text-opacity:1;color:rgb(14 165 233/var(--tw-text-opacity))}.log-levels-selector .dropdown .log-level.warning{--tw-text-opacity:1;color:rgb(180 83 9/var(--tw-text-opacity))}:is(.dark .log-levels-selector .dropdown .log-level.warning){--tw-text-opacity:1;color:rgb(251 191 36/var(--tw-text-opacity))}.log-levels-selector .dropdown .log-level.danger{--tw-text-opacity:1;color:rgb(190 18 60/var(--tw-text-opacity))}:is(.dark .log-levels-selector .dropdown .log-level.danger){--tw-text-opacity:1;color:rgb(251 113 133/var(--tw-text-opacity))}.log-levels-selector .dropdown .log-level.none{--tw-text-opacity:1;color:rgb(63 63 70/var(--tw-text-opacity))}:is(.dark .log-levels-selector .dropdown .log-level.none){--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity))}.log-levels-selector .dropdown .log-count{--tw-text-opacity:1;color:rgb(113 113 122/var(--tw-text-opacity));margin-left:2rem;white-space:nowrap}:is(.dark .log-levels-selector .dropdown .log-count){--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity))}.log-levels-selector .dropdown button.active .log-level.notice,.log-levels-selector .dropdown button.active .log-level.success{--tw-text-opacity:1;color:rgb(209 250 229/var(--tw-text-opacity))}.log-levels-selector .dropdown button.active .log-level.info{--tw-text-opacity:1;color:rgb(224 242 254/var(--tw-text-opacity))}.log-levels-selector .dropdown button.active .log-level.warning{--tw-text-opacity:1;color:rgb(254 243 199/var(--tw-text-opacity))}.log-levels-selector .dropdown button.active .log-level.danger{--tw-text-opacity:1;color:rgb(255 228 230/var(--tw-text-opacity))}.log-levels-selector .dropdown button.active .log-level.none{--tw-text-opacity:1;color:rgb(244 244 245/var(--tw-text-opacity))}.log-levels-selector .dropdown button.active .log-count{--tw-text-opacity:1;color:rgb(228 228 231/var(--tw-text-opacity))}:is(.dark .log-levels-selector .dropdown button.active .log-count){--tw-text-opacity:1;color:rgb(212 212 216/var(--tw-text-opacity))}.log-levels-selector .dropdown .no-results{--tw-text-opacity:1;color:rgb(113 113 122/var(--tw-text-opacity));font-size:.75rem;line-height:1rem;padding:.25rem 1rem;text-align:center}:is(.dark .log-levels-selector .dropdown .no-results){--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity))}.log-item{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity));cursor:pointer;transition-duration:.2s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1)}:is(.dark .log-item){--tw-bg-opacity:1;background-color:rgb(39 39 42/var(--tw-bg-opacity))}.log-item.notice.active>td,.log-item.notice:focus-within>td,.log-item.notice:hover>td,.log-item.success.active>td,.log-item.success:focus-within>td,.log-item.success:hover>td{--tw-bg-opacity:1;background-color:rgb(236 253 245/var(--tw-bg-opacity))}:is(.dark .log-item.notice.active>td),:is(.dark .log-item.notice:focus-within>td),:is(.dark .log-item.notice:hover>td),:is(.dark .log-item.success.active>td),:is(.dark .log-item.success:focus-within>td),:is(.dark .log-item.success:hover>td){--tw-bg-opacity:0.4;background-color:rgb(6 95 70/var(--tw-bg-opacity))}.log-item.notice .log-level-indicator,.log-item.success .log-level-indicator{--tw-bg-opacity:1;background-color:rgb(4 120 87/var(--tw-bg-opacity))}:is(.dark .log-item.notice .log-level-indicator),:is(.dark .log-item.success .log-level-indicator){--tw-bg-opacity:1;background-color:rgb(16 185 129/var(--tw-bg-opacity))}.log-item.notice .log-level,.log-item.success .log-level{--tw-text-opacity:1;color:rgb(4 120 87/var(--tw-text-opacity))}:is(.dark .log-item.notice .log-level),:is(.dark .log-item.success .log-level){--tw-text-opacity:1;color:rgb(16 185 129/var(--tw-text-opacity))}.log-item.info.active>td,.log-item.info:focus-within>td,.log-item.info:hover>td{--tw-bg-opacity:1;background-color:rgb(240 249 255/var(--tw-bg-opacity))}:is(.dark .log-item.info.active>td),:is(.dark .log-item.info:focus-within>td),:is(.dark .log-item.info:hover>td){--tw-bg-opacity:0.4;background-color:rgb(7 89 133/var(--tw-bg-opacity))}.log-item.info .log-level-indicator{--tw-bg-opacity:1;background-color:rgb(3 105 161/var(--tw-bg-opacity))}:is(.dark .log-item.info .log-level-indicator){--tw-bg-opacity:1;background-color:rgb(14 165 233/var(--tw-bg-opacity))}.log-item.info .log-level{--tw-text-opacity:1;color:rgb(3 105 161/var(--tw-text-opacity))}:is(.dark .log-item.info .log-level){--tw-text-opacity:1;color:rgb(14 165 233/var(--tw-text-opacity))}.log-item.warning.active>td,.log-item.warning:focus-within>td,.log-item.warning:hover>td{--tw-bg-opacity:1;background-color:rgb(255 251 235/var(--tw-bg-opacity))}:is(.dark .log-item.warning.active>td),:is(.dark .log-item.warning:focus-within>td),:is(.dark .log-item.warning:hover>td){--tw-bg-opacity:0.4;background-color:rgb(146 64 14/var(--tw-bg-opacity))}.log-item.warning .log-level-indicator{--tw-bg-opacity:1;background-color:rgb(180 83 9/var(--tw-bg-opacity))}:is(.dark .log-item.warning .log-level-indicator){--tw-bg-opacity:1;background-color:rgb(251 191 36/var(--tw-bg-opacity))}.log-item.warning .log-level{--tw-text-opacity:1;color:rgb(180 83 9/var(--tw-text-opacity))}:is(.dark .log-item.warning .log-level){--tw-text-opacity:1;color:rgb(251 191 36/var(--tw-text-opacity))}.log-item.danger.active>td,.log-item.danger:focus-within>td,.log-item.danger:hover>td{--tw-bg-opacity:1;background-color:rgb(255 241 242/var(--tw-bg-opacity))}:is(.dark .log-item.danger.active>td),:is(.dark .log-item.danger:focus-within>td),:is(.dark .log-item.danger:hover>td){--tw-bg-opacity:0.4;background-color:rgb(159 18 57/var(--tw-bg-opacity))}.log-item.danger .log-level-indicator{--tw-bg-opacity:1;background-color:rgb(190 18 60/var(--tw-bg-opacity))}:is(.dark .log-item.danger .log-level-indicator){--tw-bg-opacity:1;background-color:rgb(251 113 133/var(--tw-bg-opacity))}.log-item.danger .log-level{--tw-text-opacity:1;color:rgb(190 18 60/var(--tw-text-opacity))}:is(.dark .log-item.danger .log-level){--tw-text-opacity:1;color:rgb(251 113 133/var(--tw-text-opacity))}.log-item.none.active>td,.log-item.none:focus-within>td,.log-item.none:hover>td{--tw-bg-opacity:1;background-color:rgb(250 250 250/var(--tw-bg-opacity))}:is(.dark .log-item.none.active>td),:is(.dark .log-item.none:focus-within>td),:is(.dark .log-item.none:hover>td){--tw-bg-opacity:0.4;background-color:rgb(39 39 42/var(--tw-bg-opacity))}.log-item.none .log-level-indicator{--tw-bg-opacity:1;background-color:rgb(63 63 70/var(--tw-bg-opacity))}:is(.dark .log-item.none .log-level-indicator){--tw-bg-opacity:1;background-color:rgb(161 161 170/var(--tw-bg-opacity))}.log-item.none .log-level{--tw-text-opacity:1;color:rgb(63 63 70/var(--tw-text-opacity))}:is(.dark .log-item.none .log-level){--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity))}.log-item:hover .log-level-icon{opacity:1}.badge{align-items:center;border-radius:.375rem;cursor:pointer;display:inline-flex;font-size:.875rem;line-height:1.25rem;margin-right:.5rem;margin-top:.25rem;padding:.25rem .75rem;transition-duration:.2s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1)}.badge.notice,.badge.success{--tw-border-opacity:1;--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(236 253 245/var(--tw-bg-opacity));border-color:rgb(167 243 208/var(--tw-border-opacity));border-width:1px;color:rgb(82 82 91/var(--tw-text-opacity))}:is(.dark .badge.notice),:is(.dark .badge.success){--tw-border-opacity:1;--tw-bg-opacity:0.4;--tw-text-opacity:1;background-color:rgb(6 78 59/var(--tw-bg-opacity));border-color:rgb(6 95 70/var(--tw-border-opacity));color:rgb(161 161 170/var(--tw-text-opacity))}.badge.notice:hover,.badge.success:hover{--tw-bg-opacity:1;background-color:rgb(209 250 229/var(--tw-bg-opacity))}:is(.dark .badge.notice:hover),:is(.dark .badge.success:hover){--tw-bg-opacity:0.75;background-color:rgb(6 78 59/var(--tw-bg-opacity))}.badge.notice .checkmark,.badge.success .checkmark{--tw-border-opacity:1;border-color:rgb(167 243 208/var(--tw-border-opacity))}:is(.dark .badge.notice .checkmark),:is(.dark .badge.success .checkmark){--tw-border-opacity:1;border-color:rgb(6 95 70/var(--tw-border-opacity))}.badge.notice.active,.badge.success.active{--tw-border-opacity:1;--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(5 150 105/var(--tw-bg-opacity));border-color:rgb(4 120 87/var(--tw-border-opacity));color:rgb(255 255 255/var(--tw-text-opacity))}:is(.dark .badge.notice.active),:is(.dark .badge.success.active){--tw-border-opacity:1;--tw-bg-opacity:1;background-color:rgb(4 120 87/var(--tw-bg-opacity));border-color:rgb(5 150 105/var(--tw-border-opacity))}.badge.notice.active:hover,.badge.success.active:hover{--tw-bg-opacity:1;background-color:rgb(16 185 129/var(--tw-bg-opacity))}:is(.dark .badge.notice.active:hover),:is(.dark .badge.success.active:hover){--tw-bg-opacity:1;background-color:rgb(6 95 70/var(--tw-bg-opacity))}.badge.notice.active .checkmark,.badge.success.active .checkmark{--tw-border-opacity:1;border-color:rgb(5 150 105/var(--tw-border-opacity))}:is(.dark .badge.notice.active .checkmark),:is(.dark .badge.success.active .checkmark){--tw-border-opacity:1;--tw-bg-opacity:1;background-color:rgb(209 250 229/var(--tw-bg-opacity));border-color:rgb(4 120 87/var(--tw-border-opacity))}.badge.notice.active .checkmark>svg,.badge.success.active .checkmark>svg{--tw-text-opacity:1;color:rgb(5 150 105/var(--tw-text-opacity))}:is(.dark .badge.notice.active .checkmark>svg),:is(.dark .badge.success.active .checkmark>svg){--tw-text-opacity:1;color:rgb(4 120 87/var(--tw-text-opacity))}.badge.info{--tw-border-opacity:1;--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(240 249 255/var(--tw-bg-opacity));border-color:rgb(186 230 253/var(--tw-border-opacity));border-width:1px;color:rgb(82 82 91/var(--tw-text-opacity))}:is(.dark .badge.info){--tw-border-opacity:1;--tw-bg-opacity:0.4;--tw-text-opacity:1;background-color:rgb(12 74 110/var(--tw-bg-opacity));border-color:rgb(7 89 133/var(--tw-border-opacity));color:rgb(161 161 170/var(--tw-text-opacity))}.badge.info:hover{--tw-bg-opacity:1;background-color:rgb(224 242 254/var(--tw-bg-opacity))}:is(.dark .badge.info:hover){--tw-bg-opacity:0.75;background-color:rgb(12 74 110/var(--tw-bg-opacity))}.badge.info .checkmark{--tw-border-opacity:1;border-color:rgb(186 230 253/var(--tw-border-opacity))}:is(.dark .badge.info .checkmark){--tw-border-opacity:1;border-color:rgb(7 89 133/var(--tw-border-opacity))}.badge.info.active{--tw-border-opacity:1;--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(2 132 199/var(--tw-bg-opacity));border-color:rgb(3 105 161/var(--tw-border-opacity));color:rgb(255 255 255/var(--tw-text-opacity))}:is(.dark .badge.info.active){--tw-border-opacity:1;--tw-bg-opacity:1;background-color:rgb(3 105 161/var(--tw-bg-opacity));border-color:rgb(2 132 199/var(--tw-border-opacity))}.badge.info.active:hover{--tw-bg-opacity:1;background-color:rgb(14 165 233/var(--tw-bg-opacity))}:is(.dark .badge.info.active:hover){--tw-bg-opacity:1;background-color:rgb(7 89 133/var(--tw-bg-opacity))}.badge.info.active .checkmark{--tw-border-opacity:1;border-color:rgb(2 132 199/var(--tw-border-opacity))}:is(.dark .badge.info.active .checkmark){--tw-border-opacity:1;--tw-bg-opacity:1;background-color:rgb(224 242 254/var(--tw-bg-opacity));border-color:rgb(3 105 161/var(--tw-border-opacity))}.badge.info.active .checkmark>svg{--tw-text-opacity:1;color:rgb(2 132 199/var(--tw-text-opacity))}:is(.dark .badge.info.active .checkmark>svg){--tw-text-opacity:1;color:rgb(3 105 161/var(--tw-text-opacity))}.badge.warning{--tw-border-opacity:1;--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(255 251 235/var(--tw-bg-opacity));border-color:rgb(253 230 138/var(--tw-border-opacity));border-width:1px;color:rgb(82 82 91/var(--tw-text-opacity))}:is(.dark .badge.warning){--tw-border-opacity:1;--tw-bg-opacity:0.4;--tw-text-opacity:1;background-color:rgb(120 53 15/var(--tw-bg-opacity));border-color:rgb(146 64 14/var(--tw-border-opacity));color:rgb(161 161 170/var(--tw-text-opacity))}.badge.warning:hover{--tw-bg-opacity:1;background-color:rgb(254 243 199/var(--tw-bg-opacity))}:is(.dark .badge.warning:hover){--tw-bg-opacity:0.75;background-color:rgb(120 53 15/var(--tw-bg-opacity))}.badge.warning .checkmark{--tw-border-opacity:1;border-color:rgb(253 230 138/var(--tw-border-opacity))}:is(.dark .badge.warning .checkmark){--tw-border-opacity:1;border-color:rgb(146 64 14/var(--tw-border-opacity))}.badge.warning.active{--tw-border-opacity:1;--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(217 119 6/var(--tw-bg-opacity));border-color:rgb(180 83 9/var(--tw-border-opacity));color:rgb(255 255 255/var(--tw-text-opacity))}:is(.dark .badge.warning.active){--tw-border-opacity:1;--tw-bg-opacity:1;background-color:rgb(180 83 9/var(--tw-bg-opacity));border-color:rgb(217 119 6/var(--tw-border-opacity))}.badge.warning.active:hover{--tw-bg-opacity:1;background-color:rgb(245 158 11/var(--tw-bg-opacity))}:is(.dark .badge.warning.active:hover){--tw-bg-opacity:1;background-color:rgb(146 64 14/var(--tw-bg-opacity))}.badge.warning.active .checkmark{--tw-border-opacity:1;border-color:rgb(217 119 6/var(--tw-border-opacity))}:is(.dark .badge.warning.active .checkmark){--tw-border-opacity:1;--tw-bg-opacity:1;background-color:rgb(254 243 199/var(--tw-bg-opacity));border-color:rgb(180 83 9/var(--tw-border-opacity))}.badge.warning.active .checkmark>svg{--tw-text-opacity:1;color:rgb(217 119 6/var(--tw-text-opacity))}:is(.dark .badge.warning.active .checkmark>svg){--tw-text-opacity:1;color:rgb(180 83 9/var(--tw-text-opacity))}.badge.danger{--tw-border-opacity:1;--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(255 241 242/var(--tw-bg-opacity));border-color:rgb(254 205 211/var(--tw-border-opacity));border-width:1px;color:rgb(82 82 91/var(--tw-text-opacity))}:is(.dark .badge.danger){--tw-border-opacity:1;--tw-bg-opacity:0.4;--tw-text-opacity:1;background-color:rgb(136 19 55/var(--tw-bg-opacity));border-color:rgb(159 18 57/var(--tw-border-opacity));color:rgb(161 161 170/var(--tw-text-opacity))}.badge.danger:hover{--tw-bg-opacity:1;background-color:rgb(255 228 230/var(--tw-bg-opacity))}:is(.dark .badge.danger:hover){--tw-bg-opacity:0.75;background-color:rgb(136 19 55/var(--tw-bg-opacity))}.badge.danger .checkmark{--tw-border-opacity:1;border-color:rgb(254 205 211/var(--tw-border-opacity))}:is(.dark .badge.danger .checkmark){--tw-border-opacity:1;border-color:rgb(159 18 57/var(--tw-border-opacity))}.badge.danger.active{--tw-border-opacity:1;--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(225 29 72/var(--tw-bg-opacity));border-color:rgb(190 18 60/var(--tw-border-opacity));color:rgb(255 255 255/var(--tw-text-opacity))}:is(.dark .badge.danger.active){--tw-border-opacity:1;--tw-bg-opacity:1;background-color:rgb(190 18 60/var(--tw-bg-opacity));border-color:rgb(225 29 72/var(--tw-border-opacity))}.badge.danger.active:hover{--tw-bg-opacity:1;background-color:rgb(244 63 94/var(--tw-bg-opacity))}:is(.dark .badge.danger.active:hover){--tw-bg-opacity:1;background-color:rgb(159 18 57/var(--tw-bg-opacity))}.badge.danger.active .checkmark{--tw-border-opacity:1;border-color:rgb(225 29 72/var(--tw-border-opacity))}:is(.dark .badge.danger.active .checkmark){--tw-border-opacity:1;--tw-bg-opacity:1;background-color:rgb(255 228 230/var(--tw-bg-opacity));border-color:rgb(190 18 60/var(--tw-border-opacity))}.badge.danger.active .checkmark>svg{--tw-text-opacity:1;color:rgb(225 29 72/var(--tw-text-opacity))}:is(.dark .badge.danger.active .checkmark>svg){--tw-text-opacity:1;color:rgb(190 18 60/var(--tw-text-opacity))}.badge.none{--tw-border-opacity:1;--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(244 244 245/var(--tw-bg-opacity));border-color:rgb(228 228 231/var(--tw-border-opacity));border-width:1px;color:rgb(82 82 91/var(--tw-text-opacity))}:is(.dark .badge.none){--tw-border-opacity:1;--tw-bg-opacity:0.4;--tw-text-opacity:1;background-color:rgb(24 24 27/var(--tw-bg-opacity));border-color:rgb(39 39 42/var(--tw-border-opacity));color:rgb(161 161 170/var(--tw-text-opacity))}.badge.none:hover{--tw-bg-opacity:1;background-color:rgb(250 250 250/var(--tw-bg-opacity))}:is(.dark .badge.none:hover){--tw-bg-opacity:0.75;background-color:rgb(24 24 27/var(--tw-bg-opacity))}.badge.none .checkmark{--tw-border-opacity:1;border-color:rgb(228 228 231/var(--tw-border-opacity))}:is(.dark .badge.none .checkmark){--tw-border-opacity:1;border-color:rgb(39 39 42/var(--tw-border-opacity))}.badge.none.active{--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity));color:rgb(39 39 42/var(--tw-text-opacity))}:is(.dark .badge.none.active){--tw-border-opacity:1;--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(39 39 42/var(--tw-bg-opacity));border-color:rgb(82 82 91/var(--tw-border-opacity));color:rgb(244 244 245/var(--tw-text-opacity))}.badge.none.active:hover{--tw-bg-opacity:1;background-color:rgb(250 250 250/var(--tw-bg-opacity))}:is(.dark .badge.none.active:hover){--tw-bg-opacity:1;background-color:rgb(24 24 27/var(--tw-bg-opacity))}.badge.none.active .checkmark{--tw-border-opacity:1;border-color:rgb(82 82 91/var(--tw-border-opacity))}:is(.dark .badge.none.active .checkmark){--tw-border-opacity:1;--tw-bg-opacity:1;background-color:rgb(244 244 245/var(--tw-bg-opacity));border-color:rgb(63 63 70/var(--tw-border-opacity))}.badge.none.active .checkmark>svg{--tw-text-opacity:1;color:rgb(82 82 91/var(--tw-text-opacity))}:is(.dark .badge.none.active .checkmark>svg){--tw-text-opacity:1;color:rgb(63 63 70/var(--tw-text-opacity))}.log-list table>thead th{--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(244 244 245/var(--tw-bg-opacity));color:rgb(113 113 122/var(--tw-text-opacity));font-size:.75rem;font-weight:600;line-height:1rem;padding:.5rem .25rem;position:sticky;text-align:left;top:0;z-index:10}.file-list .folder-container .folder-item-container.log-list table>thead th{position:sticky}:is(.dark .log-list table>thead th){--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(24 24 27/var(--tw-bg-opacity));color:rgb(161 161 170/var(--tw-text-opacity))}@media (min-width:1024px){.log-list table>thead th{font-size:.875rem;line-height:1.25rem;padding-left:.5rem;padding-right:.5rem}}.log-list .log-group{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity));position:relative}:is(.dark .log-list .log-group){--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(39 39 42/var(--tw-bg-opacity));color:rgb(228 228 231/var(--tw-text-opacity))}.log-list .log-group .log-item>td{--tw-border-opacity:1;border-color:rgb(228 228 231/var(--tw-border-opacity));border-top-width:1px;font-size:.75rem;line-height:1rem;padding:.375rem .25rem}:is(.dark .log-list .log-group .log-item>td){--tw-border-opacity:1;border-color:rgb(63 63 70/var(--tw-border-opacity))}@media (min-width:1024px){.log-list .log-group .log-item>td{font-size:.875rem;line-height:1.25rem;padding:.5rem}}.log-list .log-group.first .log-item>td{border-top-color:transparent}.log-list .log-group .mail-preview-attributes{--tw-border-opacity:1;background-color:rgba(240,249,255,.3);border-color:rgb(224 242 254/var(--tw-border-opacity));border-radius:.25rem;border-width:1px;font-size:.75rem;line-height:1rem;margin-bottom:1rem;overflow-x:auto;width:100%}:is(.dark .log-list .log-group .mail-preview-attributes){--tw-border-opacity:1;background-color:rgba(12,74,110,.2);border-color:rgb(7 89 133/var(--tw-border-opacity))}@media (min-width:1024px){.log-list .log-group .mail-preview-attributes{font-size:.875rem;line-height:1.25rem;margin-bottom:1.5rem;overflow:hidden}}.log-list .log-group .mail-preview-attributes table{width:100%}.log-list .log-group .mail-preview-attributes td{padding:.25rem .5rem}@media (min-width:1024px){.log-list .log-group .mail-preview-attributes td{padding:.5rem 1.5rem}}.log-list .log-group .mail-preview-attributes td:not(:first-child){overflow-wrap:anywhere}.log-list .log-group .mail-preview-attributes tr:first-child td{padding-top:.375rem}@media (min-width:1024px){.log-list .log-group .mail-preview-attributes tr:first-child td{padding-top:.75rem}}.log-list .log-group .mail-preview-attributes tr:last-child td{padding-bottom:.375rem}@media (min-width:1024px){.log-list .log-group .mail-preview-attributes tr:last-child td{padding-bottom:.75rem}}.log-list .log-group .mail-preview-attributes tr:not(:last-child) td{--tw-border-opacity:1;border-bottom-width:1px;border-color:rgb(224 242 254/var(--tw-border-opacity))}:is(.dark .log-list .log-group .mail-preview-attributes tr:not(:last-child) td){--tw-border-opacity:1;border-color:rgb(12 74 110/var(--tw-border-opacity))}.log-list .log-group .mail-preview-html{--tw-border-opacity:1;--tw-bg-opacity:1;background-color:rgb(250 250 250/var(--tw-bg-opacity));border-color:rgb(228 228 231/var(--tw-border-opacity));border-radius:.25rem;border-width:1px;margin-bottom:1rem;overflow:auto;width:100%}:is(.dark .log-list .log-group .mail-preview-html){--tw-border-opacity:1;--tw-bg-opacity:1;background-color:rgb(24 24 27/var(--tw-bg-opacity));border-color:rgb(63 63 70/var(--tw-border-opacity))}@media (min-width:1024px){.log-list .log-group .mail-preview-html{margin-bottom:1.5rem}}.log-list .log-group .mail-preview-text{--tw-border-opacity:1;--tw-bg-opacity:1;background-color:rgb(250 250 250/var(--tw-bg-opacity));border-color:rgb(228 228 231/var(--tw-border-opacity));border-radius:.25rem;border-width:1px;font-size:.875rem;line-height:1.25rem;margin-bottom:1rem;padding:1rem;white-space:pre-wrap;width:100%}:is(.dark .log-list .log-group .mail-preview-text){--tw-border-opacity:1;--tw-bg-opacity:1;background-color:rgb(24 24 27/var(--tw-bg-opacity));border-color:rgb(63 63 70/var(--tw-border-opacity))}@media (min-width:1024px){.log-list .log-group .mail-preview-text{margin-bottom:1.5rem}}.log-list .log-group .mail-attachment-button{--tw-bg-opacity:1;align-items:center;background-color:rgb(255 255 255/var(--tw-bg-opacity));border-radius:.25rem;border-width:1px;display:flex;justify-content:space-between;padding:.25rem .5rem}:is(.dark .log-list .log-group .mail-attachment-button){--tw-border-opacity:1;--tw-bg-opacity:1;background-color:rgb(39 39 42/var(--tw-bg-opacity));border-color:rgb(63 63 70/var(--tw-border-opacity))}@media (min-width:1024px){.log-list .log-group .mail-attachment-button{padding:.5rem 1rem}}.log-list .log-group .mail-attachment-button{max-width:460px}.log-list .log-group .mail-attachment-button:not(:last-child){margin-bottom:.5rem}.log-list .log-group .mail-attachment-button a:focus{outline-color:#0ea5e9}.log-list .log-group .tabs-container{font-size:.75rem;line-height:1rem}@media (min-width:1024px){.log-list .log-group .tabs-container{font-size:.875rem;line-height:1.25rem}}.log-list .log-group .log-stack,.log-list .log-group .mail-preview,.log-list .log-group .tabs-container{padding:.25rem .5rem}@media (min-width:1024px){.log-list .log-group .log-stack,.log-list .log-group .mail-preview,.log-list .log-group .tabs-container{padding:.5rem 2rem}}.log-list .log-group .log-stack{--tw-border-opacity:1;border-color:rgb(228 228 231/var(--tw-border-opacity));font-size:10px;line-height:.75rem;white-space:pre-wrap;word-break:break-all}:is(.dark .log-list .log-group .log-stack){--tw-border-opacity:1;border-color:rgb(63 63 70/var(--tw-border-opacity))}@media (min-width:1024px){.log-list .log-group .log-stack{font-size:.75rem;line-height:1rem}}.log-list .log-group .log-link{align-items:center;border-radius:.25rem;display:flex;justify-content:flex-end;margin-bottom:-.125rem;margin-top:-.125rem;padding-bottom:.125rem;padding-left:.25rem;padding-top:.125rem;width:100%}@media (min-width:640px){.log-list .log-group .log-link{min-width:64px}}.log-list .log-group .log-link>svg{height:1rem;margin-left:.25rem;transition-duration:.2s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);width:1rem}.log-list .log-group .log-link:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);--tw-ring-opacity:1;--tw-ring-color:rgb(14 165 233/var(--tw-ring-opacity));box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);outline:2px solid transparent;outline-offset:2px}:is(.dark .log-list .log-group .log-link:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(56 189 248/var(--tw-ring-opacity))}.log-list .log-group code,.log-list .log-group mark{--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(253 230 138/var(--tw-bg-opacity));border-radius:.25rem;color:rgb(24 24 27/var(--tw-text-opacity));padding:.125rem .25rem}:is(.dark .log-list .log-group code),:is(.dark .log-list .log-group mark){--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(133 77 14/var(--tw-bg-opacity));color:rgb(255 255 255/var(--tw-text-opacity))}.pagination{align-items:center;display:flex;justify-content:center;width:100%}@media (min-width:640px){.pagination{margin-top:.5rem;padding-left:1rem;padding-right:1rem}}@media (min-width:1024px){.pagination{padding-left:0;padding-right:0}}.pagination .previous{display:flex;flex:1 1 0%;justify-content:flex-start;margin-top:-1px;width:0}@media (min-width:768px){.pagination .previous{justify-content:flex-end}}.pagination .previous button{--tw-text-opacity:1;align-items:center;border-color:transparent;border-top-width:2px;color:rgb(113 113 122/var(--tw-text-opacity));display:inline-flex;font-size:.875rem;font-weight:500;line-height:1.25rem;padding-right:.25rem;padding-top:.75rem}:is(.dark .pagination .previous button){--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity))}.pagination .previous button:hover{--tw-border-opacity:1;--tw-text-opacity:1;border-color:rgb(161 161 170/var(--tw-border-opacity));color:rgb(63 63 70/var(--tw-text-opacity))}:is(.dark .pagination .previous button:hover){--tw-text-opacity:1;color:rgb(212 212 216/var(--tw-text-opacity))}.pagination .previous button:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);--tw-ring-opacity:1;--tw-ring-color:rgb(14 165 233/var(--tw-ring-opacity));border-radius:.375rem;box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);outline:2px solid transparent;outline-offset:2px}:is(.dark .pagination .previous button:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(56 189 248/var(--tw-ring-opacity))}.pagination .previous button svg{color:currentColor;height:1.25rem;margin-left:.75rem;margin-right:.75rem;width:1.25rem}.pagination .next{display:flex;flex:1 1 0%;justify-content:flex-end;margin-top:-1px;width:0}@media (min-width:768px){.pagination .next{justify-content:flex-start}}.pagination .next button{--tw-text-opacity:1;align-items:center;border-color:transparent;border-top-width:2px;color:rgb(113 113 122/var(--tw-text-opacity));display:inline-flex;font-size:.875rem;font-weight:500;line-height:1.25rem;padding-left:.25rem;padding-top:.75rem}:is(.dark .pagination .next button){--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity))}.pagination .next button:hover{--tw-border-opacity:1;--tw-text-opacity:1;border-color:rgb(161 161 170/var(--tw-border-opacity));color:rgb(63 63 70/var(--tw-text-opacity))}:is(.dark .pagination .next button:hover){--tw-text-opacity:1;color:rgb(212 212 216/var(--tw-text-opacity))}.pagination .next button:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);--tw-ring-opacity:1;--tw-ring-color:rgb(14 165 233/var(--tw-ring-opacity));border-radius:.375rem;box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);outline:2px solid transparent;outline-offset:2px}:is(.dark .pagination .next button:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(56 189 248/var(--tw-ring-opacity))}.pagination .next button svg{color:currentColor;height:1.25rem;margin-left:.75rem;margin-right:.75rem;width:1.25rem}.pagination .pages{display:none}@media (min-width:640px){.pagination .pages{display:flex;margin-top:-1px}}.pagination .pages span{--tw-text-opacity:1;align-items:center;border-color:transparent;border-top-width:2px;color:rgb(113 113 122/var(--tw-text-opacity));display:inline-flex;font-size:.875rem;font-weight:500;line-height:1.25rem;padding-left:1rem;padding-right:1rem;padding-top:.75rem}:is(.dark .pagination .pages span){--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity))}.pagination .pages button{align-items:center;border-top-width:2px;display:inline-flex;font-size:.875rem;font-weight:500;line-height:1.25rem;padding-left:1rem;padding-right:1rem;padding-top:.75rem}.pagination .pages button:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);--tw-ring-opacity:1;--tw-ring-color:rgb(14 165 233/var(--tw-ring-opacity));border-radius:.375rem;box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);outline:2px solid transparent;outline-offset:2px}:is(.dark .pagination .pages button:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(56 189 248/var(--tw-ring-opacity))}.search{--tw-border-opacity:1;--tw-bg-opacity:1;align-items:center;background-color:rgb(255 255 255/var(--tw-bg-opacity));border-color:rgb(212 212 216/var(--tw-border-opacity));border-radius:.375rem;border-width:1px;display:flex;font-size:.875rem;line-height:1.25rem;position:relative;transition-duration:.2s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);width:100%}:is(.dark .search){--tw-border-opacity:1;--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(39 39 42/var(--tw-bg-opacity));border-color:rgb(82 82 91/var(--tw-border-opacity));color:rgb(244 244 245/var(--tw-text-opacity))}.search .prefix-icon{--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity));margin-left:.75rem;margin-right:.25rem}:is(.dark .search .prefix-icon){--tw-text-opacity:1;color:rgb(113 113 122/var(--tw-text-opacity))}.search input{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);--tw-ring-color:transparent;background-color:inherit;border-radius:.25rem;box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);flex:1 1 0%;padding:.25rem;transition-duration:.2s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);width:100%}.search input:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(14 165 233/var(--tw-ring-opacity));border-color:transparent;outline:2px solid transparent;outline-offset:2px}:is(.dark .search input:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(3 105 161/var(--tw-ring-opacity))}.search.has-error{--tw-border-opacity:1;border-color:rgb(220 38 38/var(--tw-border-opacity))}.search .submit-search button{--tw-bg-opacity:1;--tw-text-opacity:1;align-items:center;background-color:rgb(244 244 245/var(--tw-bg-opacity));border-bottom-right-radius:.25rem;border-top-right-radius:.25rem;color:rgb(82 82 91/var(--tw-text-opacity));display:flex;padding:.5rem;transition-duration:.2s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1)}:is(.dark .search .submit-search button){--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(63 63 70/var(--tw-bg-opacity));color:rgb(212 212 216/var(--tw-text-opacity))}.search .submit-search button:hover{--tw-bg-opacity:1;background-color:rgb(228 228 231/var(--tw-bg-opacity))}:is(.dark .search .submit-search button:hover){--tw-bg-opacity:1;background-color:rgb(82 82 91/var(--tw-bg-opacity))}.search .submit-search button:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);--tw-ring-opacity:1;--tw-ring-color:rgb(14 165 233/var(--tw-ring-opacity));box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);outline:2px solid transparent;outline-offset:2px}:is(.dark .search .submit-search button:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(3 105 161/var(--tw-ring-opacity))}.search .submit-search button>svg{height:1.25rem;margin-left:.25rem;opacity:.75;width:1.25rem}.search .clear-search{position:absolute;right:0;top:0}.search .clear-search button{--tw-text-opacity:1;border-radius:.25rem;color:rgb(161 161 170/var(--tw-text-opacity));padding:.25rem;transition-duration:.2s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1)}:is(.dark .search .clear-search button){--tw-text-opacity:1;color:rgb(113 113 122/var(--tw-text-opacity))}.search .clear-search button:hover{--tw-text-opacity:1;color:rgb(82 82 91/var(--tw-text-opacity))}:is(.dark .search .clear-search button:hover){--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity))}.search .clear-search button:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);--tw-ring-opacity:1;--tw-ring-color:rgb(14 165 233/var(--tw-ring-opacity));box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);outline:2px solid transparent;outline-offset:2px}:is(.dark .search .clear-search button:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(3 105 161/var(--tw-ring-opacity))}.search .clear-search button>svg{height:1.25rem;width:1.25rem}.search-progress-bar{--tw-bg-opacity:1;background-color:rgb(14 165 233/var(--tw-bg-opacity));border-radius:.25rem;height:.125rem;position:absolute;top:.25rem;transition-duration:.3s;transition-property:width;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-timing-function:linear}:is(.dark .search-progress-bar){--tw-bg-opacity:1;background-color:rgb(2 132 199/var(--tw-bg-opacity))}.dropdown{--tw-border-opacity:1;--tw-bg-opacity:1;--tw-text-opacity:1;--tw-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1);--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color);background-color:rgb(255 255 255/var(--tw-bg-opacity));border-color:rgb(228 228 231/var(--tw-border-opacity));border-radius:.375rem;border-width:1px;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);color:rgb(24 24 27/var(--tw-text-opacity));margin-top:-.25rem;overflow:hidden;position:absolute;right:.25rem;top:100%;z-index:40}:is(.dark .dropdown){--tw-border-opacity:1;--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(39 39 42/var(--tw-bg-opacity));border-color:rgb(63 63 70/var(--tw-border-opacity));color:rgb(228 228 231/var(--tw-text-opacity))}.dropdown{transform-origin:top right!important}.dropdown:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);--tw-ring-color:rgb(14 165 233/var(--tw-ring-opacity));--tw-ring-opacity:0.5;box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);outline:2px solid transparent;outline-offset:2px}:is(.dark .dropdown:focus){--tw-ring-color:rgb(3 105 161/var(--tw-ring-opacity));--tw-ring-opacity:0.5}.dropdown.up{bottom:100%;margin-bottom:-.25rem;margin-top:0;top:auto;transform-origin:bottom right!important}.dropdown.left{left:.25rem;right:auto;transform-origin:top left!important}.dropdown.left.up{transform-origin:bottom left!important}.dropdown a:not(.inline-link),.dropdown button:not(.inline-link){align-items:center;display:block;display:flex;font-size:.875rem;line-height:1.25rem;outline-color:#0ea5e9;padding:.5rem 1rem;text-align:left;width:100%}:is(.dark .dropdown a:not(.inline-link)),:is(.dark .dropdown button:not(.inline-link)){outline-color:#075985}.dropdown a:not(.inline-link)>svg,.dropdown button:not(.inline-link)>svg{--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity));height:1rem;margin-right:.75rem;width:1rem}.dropdown a:not(.inline-link)>svg.spin,.dropdown button:not(.inline-link)>svg.spin{--tw-text-opacity:1;color:rgb(82 82 91/var(--tw-text-opacity))}.dropdown a.active,.dropdown a:hover,.dropdown button.active,.dropdown button:hover{--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(2 132 199/var(--tw-bg-opacity));color:rgb(255 255 255/var(--tw-text-opacity))}.dropdown a.active>.checkmark,.dropdown a:hover>.checkmark,.dropdown button.active>.checkmark,.dropdown button:hover>.checkmark{--tw-bg-opacity:1;background-color:rgb(2 132 199/var(--tw-bg-opacity))}:is(.dark .dropdown a.active>.checkmark),:is(.dark .dropdown a:hover>.checkmark),:is(.dark .dropdown button.active>.checkmark),:is(.dark .dropdown button:hover>.checkmark){--tw-border-opacity:1;border-color:rgb(212 212 216/var(--tw-border-opacity))}.dropdown a.active>svg,.dropdown a:hover>svg,.dropdown button.active>svg,.dropdown button:hover>svg{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.dropdown .divider{border-top-width:1px;margin-bottom:.5rem;margin-top:.5rem;width:100%}:is(.dark .dropdown .divider){--tw-border-opacity:1;border-top-color:rgb(63 63 70/var(--tw-border-opacity))}.dropdown .label{--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity));font-size:.75rem;font-weight:600;line-height:1rem;margin:.25rem 1rem}.file-list{height:100%;overflow-y:auto;padding-bottom:1rem;padding-left:.75rem;padding-right:.75rem;position:relative}@media (min-width:768px){.file-list{padding-left:0;padding-right:0}}.file-list .file-item-container,.file-list .folder-item-container{--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity));border-radius:.375rem;color:rgb(39 39 42/var(--tw-text-opacity));margin-top:.5rem;position:relative;top:0}:is(.dark .file-list .file-item-container),:is(.dark .file-list .folder-item-container){--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(39 39 42/var(--tw-bg-opacity));color:rgb(228 228 231/var(--tw-text-opacity))}.file-list .file-item-container .file-item,.file-list .folder-item-container .file-item{border-color:transparent;border-radius:.375rem;border-width:1px;cursor:pointer;position:relative;transition-duration:.1s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.file-list .file-item-container .file-item,.file-list .file-item-container .file-item .file-item-info,.file-list .folder-item-container .file-item,.file-list .folder-item-container .file-item .file-item-info{align-items:center;display:flex;justify-content:space-between;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter}.file-list .file-item-container .file-item .file-item-info,.file-list .folder-item-container .file-item .file-item-info{border-bottom-left-radius:.375rem;border-top-left-radius:.375rem;flex:1 1 0%;outline-color:#0ea5e9;padding:.5rem .75rem .5rem 1rem;text-align:left;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1)}:is(.dark .file-list .file-item-container .file-item .file-item-info),:is(.dark .file-list .folder-item-container .file-item .file-item-info){outline-color:#0369a1}.file-list .file-item-container .file-item .file-item-info:hover,.file-list .folder-item-container .file-item .file-item-info:hover{--tw-bg-opacity:1;background-color:rgb(240 249 255/var(--tw-bg-opacity))}:is(.dark .file-list .file-item-container .file-item .file-item-info:hover),:is(.dark .file-list .folder-item-container .file-item .file-item-info:hover){--tw-bg-opacity:1;background-color:rgb(12 74 110/var(--tw-bg-opacity))}.file-list .file-item-container .file-item .file-icon,.file-list .folder-item-container .file-item .file-icon{--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity));margin-right:.5rem}:is(.dark .file-list .file-item-container .file-item .file-icon),:is(.dark .file-list .folder-item-container .file-item .file-icon){--tw-text-opacity:1;color:rgb(113 113 122/var(--tw-text-opacity))}.file-list .file-item-container .file-item .file-icon>svg,.file-list .folder-item-container .file-item .file-icon>svg{height:1rem;width:1rem}.file-list .file-item-container .file-item .file-name,.file-list .folder-item-container .file-item .file-name{font-size:.875rem;line-height:1.25rem;margin-right:.75rem;width:100%;word-break:break-word}.file-list .file-item-container .file-item .file-size,.file-list .folder-item-container .file-item .file-size{--tw-text-opacity:1;color:rgb(113 113 122/var(--tw-text-opacity));font-size:.75rem;line-height:1rem;white-space:nowrap}:is(.dark .file-list .file-item-container .file-item .file-size),:is(.dark .file-list .folder-item-container .file-item .file-size){--tw-text-opacity:1;color:rgb(212 212 216/var(--tw-text-opacity));opacity:.9}.file-list .file-item-container.active .file-item,.file-list .folder-item-container.active .file-item{--tw-border-opacity:1;--tw-bg-opacity:1;background-color:rgb(240 249 255/var(--tw-bg-opacity));border-color:rgb(14 165 233/var(--tw-border-opacity))}:is(.dark .file-list .file-item-container.active .file-item),:is(.dark .file-list .folder-item-container.active .file-item){--tw-border-opacity:1;--tw-bg-opacity:0.4;background-color:rgb(12 74 110/var(--tw-bg-opacity));border-color:rgb(12 74 110/var(--tw-border-opacity))}.file-list .file-item-container.active-folder .file-item,.file-list .folder-item-container.active-folder .file-item{--tw-border-opacity:1;border-color:rgb(212 212 216/var(--tw-border-opacity))}:is(.dark .file-list .file-item-container.active-folder .file-item),:is(.dark .file-list .folder-item-container.active-folder .file-item){--tw-border-opacity:1;border-color:rgb(63 63 70/var(--tw-border-opacity))}.file-list .file-item-container:hover .file-item,.file-list .folder-item-container:hover .file-item{--tw-border-opacity:1;border-color:rgb(2 132 199/var(--tw-border-opacity))}:is(.dark .file-list .file-item-container:hover .file-item),:is(.dark .file-list .folder-item-container:hover .file-item){--tw-border-opacity:1;border-color:rgb(7 89 133/var(--tw-border-opacity))}.file-list .file-item-container .file-dropdown-toggle,.file-list .folder-item-container .file-dropdown-toggle{--tw-text-opacity:1;align-items:center;align-self:stretch;border-bottom-right-radius:.375rem;border-color:transparent;border-left-width:1px;border-top-right-radius:.375rem;color:rgb(113 113 122/var(--tw-text-opacity));display:flex;justify-content:center;outline-color:#0ea5e9;transition-duration:.2s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);width:2rem}:is(.dark .file-list .file-item-container .file-dropdown-toggle),:is(.dark .file-list .folder-item-container .file-dropdown-toggle){--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity));outline-color:#0369a1}.file-list .file-item-container .file-dropdown-toggle:hover,.file-list .folder-item-container .file-dropdown-toggle:hover{--tw-border-opacity:1;--tw-bg-opacity:1;background-color:rgb(240 249 255/var(--tw-bg-opacity));border-color:rgb(2 132 199/var(--tw-border-opacity))}:is(.dark .file-list .file-item-container .file-dropdown-toggle:hover),:is(.dark .file-list .folder-item-container .file-dropdown-toggle:hover){--tw-border-opacity:1;--tw-bg-opacity:1;background-color:rgb(12 74 110/var(--tw-bg-opacity));border-color:rgb(7 89 133/var(--tw-border-opacity))}.file-list .folder-container .folder-item-container.sticky{position:sticky}.file-list .folder-container:first-child>.folder-item-container{margin-top:0}.menu-button{--tw-text-opacity:1;border-radius:.375rem;color:rgb(161 161 170/var(--tw-text-opacity));cursor:pointer;outline-color:#0ea5e9;padding:.5rem;position:relative;transition-duration:.2s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1)}.menu-button:hover{--tw-text-opacity:1;color:rgb(113 113 122/var(--tw-text-opacity))}:is(.dark .menu-button:hover){--tw-text-opacity:1;color:rgb(212 212 216/var(--tw-text-opacity))}.menu-button:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);--tw-ring-opacity:1;--tw-ring-color:rgb(14 165 233/var(--tw-ring-opacity));box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);outline:2px solid transparent;outline-offset:2px}:is(.dark .menu-button:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(3 105 161/var(--tw-ring-opacity))}a.button,button.button{--tw-text-opacity:1;align-items:center;border-radius:.375rem;color:rgb(24 24 27/var(--tw-text-opacity));display:block;display:flex;font-size:.875rem;line-height:1.25rem;outline-color:#0ea5e9;padding:.5rem 1rem;text-align:left;width:100%}:is(.dark a.button),:is(.dark button.button){--tw-text-opacity:1;color:rgb(228 228 231/var(--tw-text-opacity));outline-color:#075985}a.button>svg,button.button>svg{--tw-text-opacity:1;color:rgb(82 82 91/var(--tw-text-opacity));height:1rem;width:1rem}:is(.dark a.button>svg),:is(.dark button.button>svg){--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity))}a.button>svg.spin,button.button>svg.spin{--tw-text-opacity:1;color:rgb(82 82 91/var(--tw-text-opacity))}a.button:hover,button.button:hover{--tw-bg-opacity:1;background-color:rgb(250 250 250/var(--tw-bg-opacity))}:is(.dark a.button:hover),:is(.dark button.button:hover){--tw-bg-opacity:1;background-color:rgb(63 63 70/var(--tw-bg-opacity))}.select{--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(244 244 245/var(--tw-bg-opacity));border-radius:.25rem;color:rgb(63 63 70/var(--tw-text-opacity));font-weight:400;margin-bottom:-.125rem;margin-top:-.125rem;outline:2px solid transparent;outline-offset:2px;padding:.125rem .25rem}:is(.dark .select){--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(24 24 27/var(--tw-bg-opacity));color:rgb(212 212 216/var(--tw-text-opacity))}.select:hover{--tw-bg-opacity:1;background-color:rgb(228 228 231/var(--tw-bg-opacity))}:is(.dark .select:hover){--tw-bg-opacity:1;background-color:rgb(39 39 42/var(--tw-bg-opacity))}.select:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);--tw-ring-opacity:1;--tw-ring-color:rgb(14 165 233/var(--tw-ring-opacity));box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}:is(.dark .select:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(3 105 161/var(--tw-ring-opacity))}.keyboard-shortcut{--tw-text-opacity:1;align-items:center;color:rgb(82 82 91/var(--tw-text-opacity));display:flex;font-size:.875rem;justify-content:flex-start;line-height:1.25rem;margin-bottom:.75rem;width:100%}:is(.dark .keyboard-shortcut){--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity))}.keyboard-shortcut .shortcut{--tw-border-opacity:1;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);--tw-ring-opacity:1;--tw-ring-color:rgb(244 244 245/var(--tw-ring-opacity));align-items:center;border-color:rgb(161 161 170/var(--tw-border-opacity));border-radius:.25rem;border-width:1px;box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);display:inline-flex;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1rem;height:1.5rem;justify-content:center;line-height:1.5rem;margin-right:.5rem;width:1.5rem}:is(.dark .keyboard-shortcut .shortcut){--tw-border-opacity:1;--tw-ring-opacity:1;--tw-ring-color:rgb(24 24 27/var(--tw-ring-opacity));border-color:rgb(82 82 91/var(--tw-border-opacity))}.hover\:border-brand-600:hover{--tw-border-opacity:1;border-color:rgb(2 132 199/var(--tw-border-opacity))}.hover\:border-gray-300:hover{--tw-border-opacity:1;border-color:rgb(212 212 216/var(--tw-border-opacity))}.hover\:bg-gray-50:hover{--tw-bg-opacity:1;background-color:rgb(250 250 250/var(--tw-bg-opacity))}.hover\:text-blue-700:hover{--tw-text-opacity:1;color:rgb(29 78 216/var(--tw-text-opacity))}.hover\:text-brand-800:hover{--tw-text-opacity:1;color:rgb(7 89 133/var(--tw-text-opacity))}.hover\:text-gray-500:hover{--tw-text-opacity:1;color:rgb(113 113 122/var(--tw-text-opacity))}.hover\:text-gray-700:hover{--tw-text-opacity:1;color:rgb(63 63 70/var(--tw-text-opacity))}.focus\:border-brand-500:focus{--tw-border-opacity:1;border-color:rgb(14 165 233/var(--tw-border-opacity))}.focus\:opacity-100:focus{opacity:1}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:outline-brand-500:focus{outline-color:#0ea5e9}.focus\:ring-1:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring-1:focus,.focus\:ring-2:focus{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring-brand-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(14 165 233/var(--tw-ring-opacity))}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px}.group:hover .group-hover\:inline-block{display:inline-block}.group:hover .group-hover\:hidden{display:none}.group:hover .group-hover\:border-brand-600{--tw-border-opacity:1;border-color:rgb(2 132 199/var(--tw-border-opacity))}.group:hover .group-hover\:underline{text-decoration-line:underline}.group:hover .group-hover\:opacity-100{opacity:1}.group:focus .group-focus\:inline-block{display:inline-block}.group:focus .group-focus\:hidden{display:none}:is(.dark .dark\:border-brand-400){--tw-border-opacity:1;border-color:rgb(56 189 248/var(--tw-border-opacity))}:is(.dark .dark\:border-brand-600){--tw-border-opacity:1;border-color:rgb(2 132 199/var(--tw-border-opacity))}:is(.dark .dark\:border-gray-600){--tw-border-opacity:1;border-color:rgb(82 82 91/var(--tw-border-opacity))}:is(.dark .dark\:border-gray-700){--tw-border-opacity:1;border-color:rgb(63 63 70/var(--tw-border-opacity))}:is(.dark .dark\:border-gray-800){--tw-border-opacity:1;border-color:rgb(39 39 42/var(--tw-border-opacity))}:is(.dark .dark\:border-yellow-800){--tw-border-opacity:1;border-color:rgb(133 77 14/var(--tw-border-opacity))}:is(.dark .dark\:bg-gray-700){--tw-bg-opacity:1;background-color:rgb(63 63 70/var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-800){--tw-bg-opacity:1;background-color:rgb(39 39 42/var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-900){--tw-bg-opacity:1;background-color:rgb(24 24 27/var(--tw-bg-opacity))}:is(.dark .dark\:bg-yellow-900){--tw-bg-opacity:1;background-color:rgb(113 63 18/var(--tw-bg-opacity))}:is(.dark .dark\:bg-opacity-40){--tw-bg-opacity:0.4}:is(.dark .dark\:from-gray-900){--tw-gradient-from:#18181b var(--tw-gradient-from-position);--tw-gradient-to:rgba(24,24,27,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}:is(.dark .dark\:text-blue-500){--tw-text-opacity:1;color:rgb(59 130 246/var(--tw-text-opacity))}:is(.dark .dark\:text-brand-500){--tw-text-opacity:1;color:rgb(14 165 233/var(--tw-text-opacity))}:is(.dark .dark\:text-brand-600){--tw-text-opacity:1;color:rgb(2 132 199/var(--tw-text-opacity))}:is(.dark .dark\:text-gray-200){--tw-text-opacity:1;color:rgb(228 228 231/var(--tw-text-opacity))}:is(.dark .dark\:text-gray-300){--tw-text-opacity:1;color:rgb(212 212 216/var(--tw-text-opacity))}:is(.dark .dark\:text-gray-400){--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity))}:is(.dark .dark\:text-green-500){--tw-text-opacity:1;color:rgb(34 197 94/var(--tw-text-opacity))}:is(.dark .dark\:text-yellow-400){--tw-text-opacity:1;color:rgb(250 204 21/var(--tw-text-opacity))}:is(.dark .dark\:opacity-90){opacity:.9}:is(.dark .dark\:outline-brand-800){outline-color:#075985}:is(.dark .dark\:hover\:border-brand-700:hover){--tw-border-opacity:1;border-color:rgb(3 105 161/var(--tw-border-opacity))}:is(.dark .dark\:hover\:border-gray-400:hover){--tw-border-opacity:1;border-color:rgb(161 161 170/var(--tw-border-opacity))}:is(.dark .hover\:dark\:border-brand-800):hover{--tw-border-opacity:1;border-color:rgb(7 89 133/var(--tw-border-opacity))}:is(.dark .dark\:hover\:bg-gray-600:hover){--tw-bg-opacity:1;background-color:rgb(82 82 91/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:text-blue-400:hover){--tw-text-opacity:1;color:rgb(96 165 250/var(--tw-text-opacity))}:is(.dark .dark\:hover\:text-brand-600:hover){--tw-text-opacity:1;color:rgb(2 132 199/var(--tw-text-opacity))}:is(.dark .dark\:hover\:text-gray-200:hover){--tw-text-opacity:1;color:rgb(228 228 231/var(--tw-text-opacity))}:is(.dark .dark\:hover\:text-gray-300:hover){--tw-text-opacity:1;color:rgb(212 212 216/var(--tw-text-opacity))}:is(.dark .dark\:focus\:ring-brand-700:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(3 105 161/var(--tw-ring-opacity))}.group:hover :is(.dark .group-hover\:dark\:border-brand-800){--tw-border-opacity:1;border-color:rgb(7 89 133/var(--tw-border-opacity))}@media (min-width:640px){.sm\:flex{display:flex}.sm\:hidden{display:none}.sm\:flex-col-reverse{flex-direction:column-reverse}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:duration-300{transition-duration:.3s}}@media (min-width:768px){.md\:fixed{position:fixed}.md\:inset-y-0{bottom:0;top:0}.md\:left-0{left:0}.md\:left-auto{left:auto}.md\:right-auto{right:auto}.md\:mx-0{margin-left:0;margin-right:0}.md\:mx-3{margin-left:.75rem;margin-right:.75rem}.md\:mt-0{margin-top:0}.md\:block{display:block}.md\:inline{display:inline}.md\:flex{display:flex}.md\:hidden{display:none}.md\:w-88{width:22rem}.md\:flex-col{flex-direction:column}.md\:px-4{padding-left:1rem;padding-right:1rem}.md\:pb-12{padding-bottom:3rem}.md\:pl-88{padding-left:22rem}.md\:opacity-75{opacity:.75}}@media (min-width:1024px){.lg\:absolute{position:absolute}.lg\:left-0{left:0}.lg\:right-0{right:0}.lg\:right-6{right:1.5rem}.lg\:top-2{top:.5rem}.lg\:mx-0{margin-left:0;margin-right:0}.lg\:mx-8{margin-left:2rem;margin-right:2rem}.lg\:mb-0{margin-bottom:0}.lg\:mt-0{margin-top:0}.lg\:block{display:block}.lg\:inline{display:inline}.lg\:table-cell{display:table-cell}.lg\:hidden{display:none}.lg\:w-auto{width:auto}.lg\:flex-row{flex-direction:row}.lg\:px-5{padding-left:1.25rem;padding-right:1.25rem}.lg\:pl-2{padding-left:.5rem}.lg\:text-sm{font-size:.875rem;line-height:1.25rem}}@media (min-width:1280px){.xl\:inline{display:inline}} diff --git a/public/vendor/log-viewer/app.js b/public/vendor/log-viewer/app.js new file mode 100644 index 00000000000..a546e24d1a8 --- /dev/null +++ b/public/vendor/log-viewer/app.js @@ -0,0 +1,2 @@ +/*! For license information please see app.js.LICENSE.txt */ +(()=>{var e,t={930:(e,t,n)=>{"use strict";var r={};n.r(r),n.d(r,{BaseTransition:()=>Er,BaseTransitionPropsValidators:()=>Or,Comment:()=>Li,EffectScope:()=>de,Fragment:()=>Ai,KeepAlive:()=>Br,ReactiveEffect:()=>Pe,Static:()=>Ri,Suspense:()=>rr,Teleport:()=>Pi,Text:()=>ji,Transition:()=>Ys,TransitionGroup:()=>Ul,VueElement:()=>Al,assertNumber:()=>sn,callWithAsyncErrorHandling:()=>an,callWithErrorHandling:()=>ln,camelize:()=>F,capitalize:()=>D,cloneVNode:()=>es,compatUtils:()=>Vs,computed:()=>Ls,createApp:()=>ma,createBlock:()=>Hi,createCommentVNode:()=>rs,createElementBlock:()=>Vi,createElementVNode:()=>Ji,createHydrationRenderer:()=>yi,createPropsRestProxy:()=>Ao,createRenderer:()=>mi,createSSRApp:()=>ya,createSlots:()=>so,createStaticVNode:()=>ns,createTextVNode:()=>ts,createVNode:()=>Qi,customRef:()=>Qt,defineAsyncComponent:()=>Fr,defineComponent:()=>Rr,defineCustomElement:()=>Cl,defineEmits:()=>mo,defineExpose:()=>yo,defineModel:()=>_o,defineOptions:()=>bo,defineProps:()=>go,defineSSRCustomElement:()=>Pl,defineSlots:()=>wo,devtools:()=>Pn,effect:()=>Ae,effectScope:()=>he,getCurrentInstance:()=>ds,getCurrentScope:()=>ge,getTransitionRawChildren:()=>Lr,guardReactiveProps:()=>Xi,h:()=>Rs,handleError:()=>un,hasInjectionContext:()=>Jo,hydrate:()=>ga,initCustomFormatter:()=>Ns,initDirectivesForSSR:()=>_a,inject:()=>Yo,isMemoSame:()=>Ds,isProxy:()=>Rt,isReactive:()=>At,isReadonly:()=>jt,isRef:()=>Ut,isRuntimeOnly:()=>Es,isShallow:()=>Lt,isVNode:()=>zi,markRaw:()=>Ft,mergeDefaults:()=>Po,mergeModels:()=>To,mergeProps:()=>ls,nextTick:()=>bn,normalizeClass:()=>ee,normalizeProps:()=>te,normalizeStyle:()=>Y,onActivated:()=>$r,onBeforeMount:()=>Yr,onBeforeUnmount:()=>Xr,onBeforeUpdate:()=>Qr,onDeactivated:()=>Vr,onErrorCaptured:()=>oo,onMounted:()=>Jr,onRenderTracked:()=>ro,onRenderTriggered:()=>no,onScopeDispose:()=>me,onServerPrefetch:()=>to,onUnmounted:()=>eo,onUpdated:()=>Zr,openBlock:()=>Ni,popScopeId:()=>Bn,provide:()=>Go,proxyRefs:()=>Yt,pushScopeId:()=>Dn,queuePostFlushCb:()=>xn,reactive:()=>kt,readonly:()=>Ct,ref:()=>$t,registerRuntimeCompiler:()=>ks,render:()=>va,renderList:()=>io,renderSlot:()=>lo,resolveComponent:()=>Jn,resolveDirective:()=>Xn,resolveDynamicComponent:()=>Zn,resolveFilter:()=>$s,resolveTransitionHooks:()=>Pr,setBlockTracking:()=>Ui,setDevtoolsHook:()=>jn,setTransitionHooks:()=>jr,shallowReactive:()=>Et,shallowReadonly:()=>Pt,shallowRef:()=>Vt,ssrContextKey:()=>Is,ssrUtils:()=>Us,stop:()=>je,toDisplayString:()=>ce,toHandlerKey:()=>B,toHandlers:()=>uo,toRaw:()=>It,toRef:()=>tn,toRefs:()=>Zt,toValue:()=>Kt,transformVNodeArgs:()=>Wi,triggerRef:()=>qt,unref:()=>Wt,useAttrs:()=>Oo,useCssModule:()=>jl,useCssVars:()=>Ll,useModel:()=>ko,useSSRContext:()=>Fs,useSlots:()=>So,useTransitionState:()=>xr,vModelCheckbox:()=>Yl,vModelDynamic:()=>na,vModelRadio:()=>Ql,vModelSelect:()=>Zl,vModelText:()=>Gl,vShow:()=>pl,version:()=>Bs,warn:()=>on,watch:()=>dr,watchEffect:()=>ur,watchPostEffect:()=>cr,watchSyncEffect:()=>fr,withAsyncContext:()=>jo,withCtx:()=>$n,withDefaults:()=>xo,withDirectives:()=>yr,withKeys:()=>ua,withMemo:()=>Ms,withModifiers:()=>la,withScopeId:()=>Un});var o={};function i(e,t){const n=Object.create(null),r=e.split(",");for(let e=0;e!!n[e.toLowerCase()]:e=>!!n[e]}n.r(o),n.d(o,{hasBrowserEnv:()=>dh,hasStandardBrowserEnv:()=>hh,hasStandardBrowserWebWorkerEnv:()=>gh});const s={},l=[],a=()=>{},u=()=>!1,c=/^on[^a-z]/,f=e=>c.test(e),p=e=>e.startsWith("onUpdate:"),d=Object.assign,h=(e,t)=>{const n=e.indexOf(t);n>-1&&e.splice(n,1)},v=Object.prototype.hasOwnProperty,g=(e,t)=>v.call(e,t),m=Array.isArray,y=e=>"[object Map]"===C(e),b=e=>"[object Set]"===C(e),w=e=>"[object Date]"===C(e),_=e=>"function"==typeof e,x=e=>"string"==typeof e,S=e=>"symbol"==typeof e,O=e=>null!==e&&"object"==typeof e,k=e=>(O(e)||_(e))&&_(e.then)&&_(e.catch),E=Object.prototype.toString,C=e=>E.call(e),P=e=>C(e).slice(8,-1),T=e=>"[object Object]"===C(e),A=e=>x(e)&&"NaN"!==e&&"-"!==e[0]&&""+parseInt(e,10)===e,j=i(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),L=i("bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text,memo"),R=e=>{const t=Object.create(null);return n=>t[n]||(t[n]=e(n))},I=/-(\w)/g,F=R((e=>e.replace(I,((e,t)=>t?t.toUpperCase():"")))),N=/\B([A-Z])/g,M=R((e=>e.replace(N,"-$1").toLowerCase())),D=R((e=>e.charAt(0).toUpperCase()+e.slice(1))),B=R((e=>e?`on${D(e)}`:"")),U=(e,t)=>!Object.is(e,t),$=(e,t)=>{for(let n=0;n{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,value:n})},H=e=>{const t=parseFloat(e);return isNaN(t)?e:t},z=e=>{const t=x(e)?Number(e):NaN;return isNaN(t)?e:t};let q;const W=()=>q||(q="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:void 0!==n.g?n.g:{});const K={1:"TEXT",2:"CLASS",4:"STYLE",8:"PROPS",16:"FULL_PROPS",32:"HYDRATE_EVENTS",64:"STABLE_FRAGMENT",128:"KEYED_FRAGMENT",256:"UNKEYED_FRAGMENT",512:"NEED_PATCH",1024:"DYNAMIC_SLOTS",2048:"DEV_ROOT_FRAGMENT",[-1]:"HOISTED",[-2]:"BAIL"},G=i("Infinity,undefined,NaN,isFinite,isNaN,parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt,console");function Y(e){if(m(e)){const t={};for(let n=0;n{if(e){const n=e.split(Q);n.length>1&&(t[n[0].trim()]=n[1].trim())}})),t}function ee(e){let t="";if(x(e))t=e;else if(m(e))for(let n=0;nae(e,t)))}const ce=e=>x(e)?e:null==e?"":m(e)||O(e)&&(e.toString===E||!_(e.toString))?JSON.stringify(e,fe,2):String(e),fe=(e,t)=>t&&t.__v_isRef?fe(e,t.value):y(t)?{[`Map(${t.size})`]:[...t.entries()].reduce(((e,[t,n])=>(e[`${t} =>`]=n,e)),{})}:b(t)?{[`Set(${t.size})`]:[...t.values()]}:!O(t)||m(t)||T(t)?t:String(t);let pe;class de{constructor(e=!1){this.detached=e,this._active=!0,this.effects=[],this.cleanups=[],this.parent=pe,!e&&pe&&(this.index=(pe.scopes||(pe.scopes=[])).push(this)-1)}get active(){return this._active}run(e){if(this._active){const t=pe;try{return pe=this,e()}finally{pe=t}}else 0}on(){pe=this}off(){pe=this.parent}stop(e){if(this._active){let t,n;for(t=0,n=this.effects.length;t{const t=new Set(e);return t.w=0,t.n=0,t},be=e=>(e.w&Se)>0,we=e=>(e.n&Se)>0,_e=new WeakMap;let xe=0,Se=1;const Oe=30;let ke;const Ee=Symbol(""),Ce=Symbol("");class Pe{constructor(e,t=null,n){this.fn=e,this.scheduler=t,this.active=!0,this.deps=[],this.parent=void 0,ve(this,n)}run(){if(!this.active)return this.fn();let e=ke,t=Le;for(;e;){if(e===this)return;e=e.parent}try{return this.parent=ke,ke=this,Le=!0,Se=1<<++xe,xe<=Oe?(({deps:e})=>{if(e.length)for(let t=0;t{const{deps:t}=e;if(t.length){let n=0;for(let r=0;r{("length"===n||!S(n)&&n>=e)&&l.push(t)}))}else switch(void 0!==n&&l.push(s.get(n)),t){case"add":m(e)?A(n)&&l.push(s.get("length")):(l.push(s.get(Ee)),y(e)&&l.push(s.get(Ce)));break;case"delete":m(e)||(l.push(s.get(Ee)),y(e)&&l.push(s.get(Ce)));break;case"set":y(e)&&l.push(s.get(Ee))}if(1===l.length)l[0]&&Be(l[0]);else{const e=[];for(const t of l)t&&e.push(...t);Be(ye(e))}}function Be(e,t){const n=m(e)?e:[...e];for(const e of n)e.computed&&Ue(e,t);for(const e of n)e.computed||Ue(e,t)}function Ue(e,t){(e!==ke||e.allowRecurse)&&(e.scheduler?e.scheduler():e.run())}const $e=i("__proto__,__v_isRef,__isVue"),Ve=new Set(Object.getOwnPropertyNames(Symbol).filter((e=>"arguments"!==e&&"caller"!==e)).map((e=>Symbol[e])).filter(S)),He=ze();function ze(){const e={};return["includes","indexOf","lastIndexOf"].forEach((t=>{e[t]=function(...e){const n=It(this);for(let e=0,t=this.length;e{e[t]=function(...e){Ie();const n=It(this)[t].apply(this,e);return Fe(),n}})),e}function qe(e){const t=It(this);return Ne(t,0,e),t.hasOwnProperty(e)}class We{constructor(e=!1,t=!1){this._isReadonly=e,this._shallow=t}get(e,t,n){const r=this._isReadonly,o=this._shallow;if("__v_isReactive"===t)return!r;if("__v_isReadonly"===t)return r;if("__v_isShallow"===t)return o;if("__v_raw"===t&&n===(r?o?Ot:St:o?xt:_t).get(e))return e;const i=m(e);if(!r){if(i&&g(He,t))return Reflect.get(He,t,n);if("hasOwnProperty"===t)return qe}const s=Reflect.get(e,t,n);return(S(t)?Ve.has(t):$e(t))?s:(r||Ne(e,0,t),o?s:Ut(s)?i&&A(t)?s:s.value:O(s)?r?Ct(s):kt(s):s)}}class Ke extends We{constructor(e=!1){super(!1,e)}set(e,t,n,r){let o=e[t];if(jt(o)&&Ut(o)&&!Ut(n))return!1;if(!this._shallow&&(Lt(n)||jt(n)||(o=It(o),n=It(n)),!m(e)&&Ut(o)&&!Ut(n)))return o.value=n,!0;const i=m(e)&&A(t)?Number(t)e,et=e=>Reflect.getPrototypeOf(e);function tt(e,t,n=!1,r=!1){const o=It(e=e.__v_raw),i=It(t);n||(U(t,i)&&Ne(o,0,t),Ne(o,0,i));const{has:s}=et(o),l=r?Xe:n?Mt:Nt;return s.call(o,t)?l(e.get(t)):s.call(o,i)?l(e.get(i)):void(e!==o&&e.get(t))}function nt(e,t=!1){const n=this.__v_raw,r=It(n),o=It(e);return t||(U(e,o)&&Ne(r,0,e),Ne(r,0,o)),e===o?n.has(e):n.has(e)||n.has(o)}function rt(e,t=!1){return e=e.__v_raw,!t&&Ne(It(e),0,Ee),Reflect.get(e,"size",e)}function ot(e){e=It(e);const t=It(this);return et(t).has.call(t,e)||(t.add(e),De(t,"add",e,e)),this}function it(e,t){t=It(t);const n=It(this),{has:r,get:o}=et(n);let i=r.call(n,e);i||(e=It(e),i=r.call(n,e));const s=o.call(n,e);return n.set(e,t),i?U(t,s)&&De(n,"set",e,t):De(n,"add",e,t),this}function st(e){const t=It(this),{has:n,get:r}=et(t);let o=n.call(t,e);o||(e=It(e),o=n.call(t,e));r&&r.call(t,e);const i=t.delete(e);return o&&De(t,"delete",e,void 0),i}function lt(){const e=It(this),t=0!==e.size,n=e.clear();return t&&De(e,"clear",void 0,void 0),n}function at(e,t){return function(n,r){const o=this,i=o.__v_raw,s=It(i),l=t?Xe:e?Mt:Nt;return!e&&Ne(s,0,Ee),i.forEach(((e,t)=>n.call(r,l(e),l(t),o)))}}function ut(e,t,n){return function(...r){const o=this.__v_raw,i=It(o),s=y(i),l="entries"===e||e===Symbol.iterator&&s,a="keys"===e&&s,u=o[e](...r),c=n?Xe:t?Mt:Nt;return!t&&Ne(i,0,a?Ce:Ee),{next(){const{value:e,done:t}=u.next();return t?{value:e,done:t}:{value:l?[c(e[0]),c(e[1])]:c(e),done:t}},[Symbol.iterator](){return this}}}}function ct(e){return function(...t){return"delete"!==e&&this}}function ft(){const e={get(e){return tt(this,e)},get size(){return rt(this)},has:nt,add:ot,set:it,delete:st,clear:lt,forEach:at(!1,!1)},t={get(e){return tt(this,e,!1,!0)},get size(){return rt(this)},has:nt,add:ot,set:it,delete:st,clear:lt,forEach:at(!1,!0)},n={get(e){return tt(this,e,!0)},get size(){return rt(this,!0)},has(e){return nt.call(this,e,!0)},add:ct("add"),set:ct("set"),delete:ct("delete"),clear:ct("clear"),forEach:at(!0,!1)},r={get(e){return tt(this,e,!0,!0)},get size(){return rt(this,!0)},has(e){return nt.call(this,e,!0)},add:ct("add"),set:ct("set"),delete:ct("delete"),clear:ct("clear"),forEach:at(!0,!0)};return["keys","values","entries",Symbol.iterator].forEach((o=>{e[o]=ut(o,!1,!1),n[o]=ut(o,!0,!1),t[o]=ut(o,!1,!0),r[o]=ut(o,!0,!0)})),[e,n,t,r]}const[pt,dt,ht,vt]=ft();function gt(e,t){const n=t?e?vt:ht:e?dt:pt;return(t,r,o)=>"__v_isReactive"===r?!e:"__v_isReadonly"===r?e:"__v_raw"===r?t:Reflect.get(g(n,r)&&r in t?n:t,r,o)}const mt={get:gt(!1,!1)},yt={get:gt(!1,!0)},bt={get:gt(!0,!1)},wt={get:gt(!0,!0)};const _t=new WeakMap,xt=new WeakMap,St=new WeakMap,Ot=new WeakMap;function kt(e){return jt(e)?e:Tt(e,!1,Ye,mt,_t)}function Et(e){return Tt(e,!1,Qe,yt,xt)}function Ct(e){return Tt(e,!0,Je,bt,St)}function Pt(e){return Tt(e,!0,Ze,wt,Ot)}function Tt(e,t,n,r,o){if(!O(e))return e;if(e.__v_raw&&(!t||!e.__v_isReactive))return e;const i=o.get(e);if(i)return i;const s=function(e){return e.__v_skip||!Object.isExtensible(e)?0:function(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}(P(e))}(e);if(0===s)return e;const l=new Proxy(e,2===s?r:n);return o.set(e,l),l}function At(e){return jt(e)?At(e.__v_raw):!(!e||!e.__v_isReactive)}function jt(e){return!(!e||!e.__v_isReadonly)}function Lt(e){return!(!e||!e.__v_isShallow)}function Rt(e){return At(e)||jt(e)}function It(e){const t=e&&e.__v_raw;return t?It(t):e}function Ft(e){return V(e,"__v_skip",!0),e}const Nt=e=>O(e)?kt(e):e,Mt=e=>O(e)?Ct(e):e;function Dt(e){Le&&ke&&Me((e=It(e)).dep||(e.dep=ye()))}function Bt(e,t){const n=(e=It(e)).dep;n&&Be(n)}function Ut(e){return!(!e||!0!==e.__v_isRef)}function $t(e){return Ht(e,!1)}function Vt(e){return Ht(e,!0)}function Ht(e,t){return Ut(e)?e:new zt(e,t)}class zt{constructor(e,t){this.__v_isShallow=t,this.dep=void 0,this.__v_isRef=!0,this._rawValue=t?e:It(e),this._value=t?e:Nt(e)}get value(){return Dt(this),this._value}set value(e){const t=this.__v_isShallow||Lt(e)||jt(e);e=t?e:It(e),U(e,this._rawValue)&&(this._rawValue=e,this._value=t?e:Nt(e),Bt(this))}}function qt(e){Bt(e)}function Wt(e){return Ut(e)?e.value:e}function Kt(e){return _(e)?e():Wt(e)}const Gt={get:(e,t,n)=>Wt(Reflect.get(e,t,n)),set:(e,t,n,r)=>{const o=e[t];return Ut(o)&&!Ut(n)?(o.value=n,!0):Reflect.set(e,t,n,r)}};function Yt(e){return At(e)?e:new Proxy(e,Gt)}class Jt{constructor(e){this.dep=void 0,this.__v_isRef=!0;const{get:t,set:n}=e((()=>Dt(this)),(()=>Bt(this)));this._get=t,this._set=n}get value(){return this._get()}set value(e){this._set(e)}}function Qt(e){return new Jt(e)}function Zt(e){const t=m(e)?new Array(e.length):{};for(const n in e)t[n]=nn(e,n);return t}class Xt{constructor(e,t,n){this._object=e,this._key=t,this._defaultValue=n,this.__v_isRef=!0}get value(){const e=this._object[this._key];return void 0===e?this._defaultValue:e}set value(e){this._object[this._key]=e}get dep(){return function(e,t){var n;return null==(n=_e.get(e))?void 0:n.get(t)}(It(this._object),this._key)}}class en{constructor(e){this._getter=e,this.__v_isRef=!0,this.__v_isReadonly=!0}get value(){return this._getter()}}function tn(e,t,n){return Ut(e)?e:_(e)?new en(e):O(e)&&arguments.length>1?nn(e,t,n):$t(e)}function nn(e,t,n){const r=e[t];return Ut(r)?r:new Xt(e,t,n)}class rn{constructor(e,t,n,r){this._setter=t,this.dep=void 0,this.__v_isRef=!0,this.__v_isReadonly=!1,this._dirty=!0,this.effect=new Pe(e,(()=>{this._dirty||(this._dirty=!0,Bt(this))})),this.effect.computed=this,this.effect.active=this._cacheable=!r,this.__v_isReadonly=n}get value(){const e=It(this);return Dt(e),!e._dirty&&e._cacheable||(e._dirty=!1,e._value=e.effect.run()),e._value}set value(e){this._setter(e)}}function on(e,...t){}function sn(e,t){}function ln(e,t,n,r){let o;try{o=r?e(...r):e()}catch(e){un(e,t,n)}return o}function an(e,t,n,r){if(_(e)){const o=ln(e,t,n,r);return o&&k(o)&&o.catch((e=>{un(e,t,n)})),o}const o=[];for(let i=0;i>>1,o=pn[r],i=kn(o);ikn(e)-kn(t))),gn=0;gnnull==e.id?1/0:e.id,En=(e,t)=>{const n=kn(e)-kn(t);if(0===n){if(e.pre&&!t.pre)return-1;if(t.pre&&!e.pre)return 1}return n};function Cn(e){fn=!1,cn=!0,pn.sort(En);try{for(dn=0;dnPn.emit(e,...t))),Tn=[];else if("undefined"!=typeof window&&window.HTMLElement&&!(null==(r=null==(n=window.navigator)?void 0:n.userAgent)?void 0:r.includes("jsdom"))){(t.__VUE_DEVTOOLS_HOOK_REPLAY__=t.__VUE_DEVTOOLS_HOOK_REPLAY__||[]).push((e=>{jn(e,t)})),setTimeout((()=>{Pn||(t.__VUE_DEVTOOLS_HOOK_REPLAY__=null,An=!0,Tn=[])}),3e3)}else An=!0,Tn=[]}function Ln(e,t,...n){if(e.isUnmounted)return;const r=e.vnode.props||s;let o=n;const i=t.startsWith("update:"),l=i&&t.slice(7);if(l&&l in r){const e=`${"modelValue"===l?"model":l}Modifiers`,{number:t,trim:i}=r[e]||s;i&&(o=n.map((e=>x(e)?e.trim():e))),t&&(o=n.map(H))}let a;let u=r[a=B(t)]||r[a=B(F(t))];!u&&i&&(u=r[a=B(M(t))]),u&&an(u,e,6,o);const c=r[a+"Once"];if(c){if(e.emitted){if(e.emitted[a])return}else e.emitted={};e.emitted[a]=!0,an(c,e,6,o)}}function Rn(e,t,n=!1){const r=t.emitsCache,o=r.get(e);if(void 0!==o)return o;const i=e.emits;let s={},l=!1;if(!_(e)){const r=e=>{const n=Rn(e,t,!0);n&&(l=!0,d(s,n))};!n&&t.mixins.length&&t.mixins.forEach(r),e.extends&&r(e.extends),e.mixins&&e.mixins.forEach(r)}return i||l?(m(i)?i.forEach((e=>s[e]=null)):d(s,i),O(e)&&r.set(e,s),s):(O(e)&&r.set(e,null),null)}function In(e,t){return!(!e||!f(t))&&(t=t.slice(2).replace(/Once$/,""),g(e,t[0].toLowerCase()+t.slice(1))||g(e,M(t))||g(e,t))}let Fn=null,Nn=null;function Mn(e){const t=Fn;return Fn=e,Nn=e&&e.type.__scopeId||null,t}function Dn(e){Nn=e}function Bn(){Nn=null}const Un=e=>$n;function $n(e,t=Fn,n){if(!t)return e;if(e._n)return e;const r=(...n)=>{r._d&&Ui(-1);const o=Mn(t);let i;try{i=e(...n)}finally{Mn(o),r._d&&Ui(1)}return i};return r._n=!0,r._c=!0,r._d=!0,r}function Vn(e){const{type:t,vnode:n,proxy:r,withProxy:o,props:i,propsOptions:[s],slots:l,attrs:a,emit:u,render:c,renderCache:f,data:d,setupState:h,ctx:v,inheritAttrs:g}=e;let m,y;const b=Mn(e);try{if(4&n.shapeFlag){const e=o||r;m=os(c.call(e,e,f,i,h,d,v)),y=a}else{const e=t;0,m=os(e.length>1?e(i,{attrs:a,slots:l,emit:u}):e(i,null)),y=t.props?a:zn(a)}}catch(t){Ii.length=0,un(t,e,1),m=Qi(Li)}let w=m;if(y&&!1!==g){const e=Object.keys(y),{shapeFlag:t}=w;e.length&&7&t&&(s&&e.some(p)&&(y=qn(y,s)),w=es(w,y))}return n.dirs&&(w=es(w),w.dirs=w.dirs?w.dirs.concat(n.dirs):n.dirs),n.transition&&(w.transition=n.transition),m=w,Mn(b),m}function Hn(e){let t;for(let n=0;n{let t;for(const n in e)("class"===n||"style"===n||f(n))&&((t||(t={}))[n]=e[n]);return t},qn=(e,t)=>{const n={};for(const r in e)p(r)&&r.slice(9)in t||(n[r]=e[r]);return n};function Wn(e,t,n){const r=Object.keys(t);if(r.length!==Object.keys(e).length)return!0;for(let o=0;oe.__isSuspense,rr={name:"Suspense",__isSuspense:!0,process(e,t,n,r,o,i,s,l,a,u){null==e?function(e,t,n,r,o,i,s,l,a){const{p:u,o:{createElement:c}}=a,f=c("div"),p=e.suspense=ir(e,o,r,t,f,n,i,s,l,a);u(null,p.pendingBranch=e.ssContent,f,null,r,p,i,s),p.deps>0?(or(e,"onPending"),or(e,"onFallback"),u(null,e.ssFallback,t,n,r,null,i,s),ar(p,e.ssFallback)):p.resolve(!1,!0)}(t,n,r,o,i,s,l,a,u):function(e,t,n,r,o,i,s,l,{p:a,um:u,o:{createElement:c}}){const f=t.suspense=e.suspense;f.vnode=t,t.el=e.el;const p=t.ssContent,d=t.ssFallback,{activeBranch:h,pendingBranch:v,isInFallback:g,isHydrating:m}=f;if(v)f.pendingBranch=p,qi(p,v)?(a(v,p,f.hiddenContainer,null,o,f,i,s,l),f.deps<=0?f.resolve():g&&(a(h,d,n,r,o,null,i,s,l),ar(f,d))):(f.pendingId++,m?(f.isHydrating=!1,f.activeBranch=v):u(v,o,f),f.deps=0,f.effects.length=0,f.hiddenContainer=c("div"),g?(a(null,p,f.hiddenContainer,null,o,f,i,s,l),f.deps<=0?f.resolve():(a(h,d,n,r,o,null,i,s,l),ar(f,d))):h&&qi(p,h)?(a(h,p,n,r,o,f,i,s,l),f.resolve(!0)):(a(null,p,f.hiddenContainer,null,o,f,i,s,l),f.deps<=0&&f.resolve()));else if(h&&qi(p,h))a(h,p,n,r,o,f,i,s,l),ar(f,p);else if(or(t,"onPending"),f.pendingBranch=p,f.pendingId++,a(null,p,f.hiddenContainer,null,o,f,i,s,l),f.deps<=0)f.resolve();else{const{timeout:e,pendingId:t}=f;e>0?setTimeout((()=>{f.pendingId===t&&f.fallback(d)}),e):0===e&&f.fallback(d)}}(e,t,n,r,o,s,l,a,u)},hydrate:function(e,t,n,r,o,i,s,l,a){const u=t.suspense=ir(t,r,n,e.parentNode,document.createElement("div"),null,o,i,s,l,!0),c=a(e,u.pendingBranch=t.ssContent,n,u,i,s);0===u.deps&&u.resolve(!1,!0);return c},create:ir,normalize:function(e){const{shapeFlag:t,children:n}=e,r=32&t;e.ssContent=sr(r?n.default:n),e.ssFallback=r?sr(n.fallback):Qi(Li)}};function or(e,t){const n=e.props&&e.props[t];_(n)&&n()}function ir(e,t,n,r,o,i,s,l,a,u,c=!1){const{p:f,m:p,um:d,n:h,o:{parentNode:v,remove:g}}=u;let m;const y=function(e){var t;return null!=(null==(t=e.props)?void 0:t.suspensible)&&!1!==e.props.suspensible}(e);y&&(null==t?void 0:t.pendingBranch)&&(m=t.pendingId,t.deps++);const b=e.props?z(e.props.timeout):void 0;const w={vnode:e,parent:t,parentComponent:n,isSVG:s,container:r,hiddenContainer:o,anchor:i,deps:0,pendingId:0,timeout:"number"==typeof b?b:-1,activeBranch:null,pendingBranch:null,isInFallback:!0,isHydrating:c,isUnmounted:!1,effects:[],resolve(e=!1,n=!1){const{vnode:r,activeBranch:o,pendingBranch:i,pendingId:s,effects:l,parentComponent:a,container:u}=w;let c=!1;if(w.isHydrating)w.isHydrating=!1;else if(!e){c=o&&i.transition&&"out-in"===i.transition.mode,c&&(o.transition.afterLeave=()=>{s===w.pendingId&&(p(i,u,e,0),xn(l))});let{anchor:e}=w;o&&(e=h(o),d(o,a,w,!0)),c||p(i,u,e,0)}ar(w,i),w.pendingBranch=null,w.isInFallback=!1;let f=w.parent,v=!1;for(;f;){if(f.pendingBranch){f.effects.push(...l),v=!0;break}f=f.parent}v||c||xn(l),w.effects=[],y&&t&&t.pendingBranch&&m===t.pendingId&&(t.deps--,0!==t.deps||n||t.resolve()),or(r,"onResolve")},fallback(e){if(!w.pendingBranch)return;const{vnode:t,activeBranch:n,parentComponent:r,container:o,isSVG:i}=w;or(t,"onFallback");const s=h(n),u=()=>{w.isInFallback&&(f(null,e,o,s,r,null,i,l,a),ar(w,e))},c=e.transition&&"out-in"===e.transition.mode;c&&(n.transition.afterLeave=u),w.isInFallback=!0,d(n,r,null,!0),c||u()},move(e,t,n){w.activeBranch&&p(w.activeBranch,e,t,n),w.container=e},next:()=>w.activeBranch&&h(w.activeBranch),registerDep(e,t){const n=!!w.pendingBranch;n&&w.deps++;const r=e.vnode.el;e.asyncDep.catch((t=>{un(t,e,0)})).then((o=>{if(e.isUnmounted||w.isUnmounted||w.pendingId!==e.suspenseId)return;e.asyncResolved=!0;const{vnode:i}=e;Os(e,o,!1),r&&(i.el=r);const l=!r&&e.subTree.el;t(e,i,v(r||e.subTree.el),r?null:h(e.subTree),w,s,a),l&&g(l),Kn(e,i.el),n&&0==--w.deps&&w.resolve()}))},unmount(e,t){w.isUnmounted=!0,w.activeBranch&&d(w.activeBranch,n,e,t),w.pendingBranch&&d(w.pendingBranch,n,e,t)}};return w}function sr(e){let t;if(_(e)){const n=Bi&&e._c;n&&(e._d=!1,Ni()),e=e(),n&&(e._d=!0,t=Fi,Mi())}if(m(e)){const t=Hn(e);0,e=t}return e=os(e),t&&!e.dynamicChildren&&(e.dynamicChildren=t.filter((t=>t!==e))),e}function lr(e,t){t&&t.pendingBranch?m(e)?t.effects.push(...e):t.effects.push(e):xn(e)}function ar(e,t){e.activeBranch=t;const{vnode:n,parentComponent:r}=e,o=n.el=t.el;r&&r.subTree===n&&(r.vnode.el=o,Kn(r,o))}function ur(e,t){return hr(e,null,t)}function cr(e,t){return hr(e,null,{flush:"post"})}function fr(e,t){return hr(e,null,{flush:"sync"})}const pr={};function dr(e,t,n){return hr(e,t,n)}function hr(e,t,{immediate:n,deep:r,flush:o,onTrack:i,onTrigger:l}=s){var u;const c=ge()===(null==(u=ps)?void 0:u.scope)?ps:null;let f,p,d=!1,v=!1;if(Ut(e)?(f=()=>e.value,d=Lt(e)):At(e)?(f=()=>e,r=!0):m(e)?(v=!0,d=e.some((e=>At(e)||Lt(e))),f=()=>e.map((e=>Ut(e)?e.value:At(e)?mr(e):_(e)?ln(e,c,2):void 0))):f=_(e)?t?()=>ln(e,c,2):()=>{if(!c||!c.isUnmounted)return p&&p(),an(e,c,3,[y])}:a,t&&r){const e=f;f=()=>mr(e())}let g,y=e=>{p=S.onStop=()=>{ln(e,c,4)}};if(xs){if(y=a,t?n&&an(t,c,3,[f(),v?[]:void 0,y]):f(),"sync"!==o)return a;{const e=Fs();g=e.__watcherHandles||(e.__watcherHandles=[])}}let b=v?new Array(e.length).fill(pr):pr;const w=()=>{if(S.active)if(t){const e=S.run();(r||d||(v?e.some(((e,t)=>U(e,b[t]))):U(e,b)))&&(p&&p(),an(t,c,3,[e,b===pr?void 0:v&&b[0]===pr?[]:b,y]),b=e)}else S.run()};let x;w.allowRecurse=!!t,"sync"===o?x=w:"post"===o?x=()=>gi(w,c&&c.suspense):(w.pre=!0,c&&(w.id=c.uid),x=()=>wn(w));const S=new Pe(f,x);t?n?w():b=S.run():"post"===o?gi(S.run.bind(S),c&&c.suspense):S.run();const O=()=>{S.stop(),c&&c.scope&&h(c.scope.effects,S)};return g&&g.push(O),O}function vr(e,t,n){const r=this.proxy,o=x(e)?e.includes(".")?gr(r,e):()=>r[e]:e.bind(r,r);let i;_(t)?i=t:(i=t.handler,n=t);const s=ps;ms(this);const l=hr(o,i.bind(r),n);return s?ms(s):ys(),l}function gr(e,t){const n=t.split(".");return()=>{let t=e;for(let e=0;e{mr(e,t)}));else if(T(e))for(const n in e)mr(e[n],t);return e}function yr(e,t){const n=Fn;if(null===n)return e;const r=Ts(n)||n.proxy,o=e.dirs||(e.dirs=[]);for(let e=0;e{e.isMounted=!0})),Xr((()=>{e.isUnmounting=!0})),e}const Sr=[Function,Array],Or={mode:String,appear:Boolean,persisted:Boolean,onBeforeEnter:Sr,onEnter:Sr,onAfterEnter:Sr,onEnterCancelled:Sr,onBeforeLeave:Sr,onLeave:Sr,onAfterLeave:Sr,onLeaveCancelled:Sr,onBeforeAppear:Sr,onAppear:Sr,onAfterAppear:Sr,onAppearCancelled:Sr},kr={name:"BaseTransition",props:Or,setup(e,{slots:t}){const n=ds(),r=xr();let o;return()=>{const i=t.default&&Lr(t.default(),!0);if(!i||!i.length)return;let s=i[0];if(i.length>1){let e=!1;for(const t of i)if(t.type!==Li){0,s=t,e=!0;break}}const l=It(e),{mode:a}=l;if(r.isLeaving)return Tr(s);const u=Ar(s);if(!u)return Tr(s);const c=Pr(u,l,r,n);jr(u,c);const f=n.subTree,p=f&&Ar(f);let d=!1;const{getTransitionKey:h}=u.type;if(h){const e=h();void 0===o?o=e:e!==o&&(o=e,d=!0)}if(p&&p.type!==Li&&(!qi(u,p)||d)){const e=Pr(p,l,r,n);if(jr(p,e),"out-in"===a)return r.isLeaving=!0,e.afterLeave=()=>{r.isLeaving=!1,!1!==n.update.active&&n.update()},Tr(s);"in-out"===a&&u.type!==Li&&(e.delayLeave=(e,t,n)=>{Cr(r,p)[String(p.key)]=p,e[wr]=()=>{t(),e[wr]=void 0,delete c.delayedLeave},c.delayedLeave=n})}return s}}},Er=kr;function Cr(e,t){const{leavingVNodes:n}=e;let r=n.get(t.type);return r||(r=Object.create(null),n.set(t.type,r)),r}function Pr(e,t,n,r){const{appear:o,mode:i,persisted:s=!1,onBeforeEnter:l,onEnter:a,onAfterEnter:u,onEnterCancelled:c,onBeforeLeave:f,onLeave:p,onAfterLeave:d,onLeaveCancelled:h,onBeforeAppear:v,onAppear:g,onAfterAppear:y,onAppearCancelled:b}=t,w=String(e.key),_=Cr(n,e),x=(e,t)=>{e&&an(e,r,9,t)},S=(e,t)=>{const n=t[1];x(e,t),m(e)?e.every((e=>e.length<=1))&&n():e.length<=1&&n()},O={mode:i,persisted:s,beforeEnter(t){let r=l;if(!n.isMounted){if(!o)return;r=v||l}t[wr]&&t[wr](!0);const i=_[w];i&&qi(e,i)&&i.el[wr]&&i.el[wr](),x(r,[t])},enter(e){let t=a,r=u,i=c;if(!n.isMounted){if(!o)return;t=g||a,r=y||u,i=b||c}let s=!1;const l=e[_r]=t=>{s||(s=!0,x(t?i:r,[e]),O.delayedLeave&&O.delayedLeave(),e[_r]=void 0)};t?S(t,[e,l]):l()},leave(t,r){const o=String(e.key);if(t[_r]&&t[_r](!0),n.isUnmounting)return r();x(f,[t]);let i=!1;const s=t[wr]=n=>{i||(i=!0,r(),x(n?h:d,[t]),t[wr]=void 0,_[o]===e&&delete _[o])};_[o]=e,p?S(p,[t,s]):s()},clone:e=>Pr(e,t,n,r)};return O}function Tr(e){if(Mr(e))return(e=es(e)).children=null,e}function Ar(e){return Mr(e)?e.children?e.children[0]:void 0:e}function jr(e,t){6&e.shapeFlag&&e.component?jr(e.component.subTree,t):128&e.shapeFlag?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function Lr(e,t=!1,n){let r=[],o=0;for(let i=0;i1)for(let e=0;ed({name:e.name},t,{setup:e}))():e}const Ir=e=>!!e.type.__asyncLoader;function Fr(e){_(e)&&(e={loader:e});const{loader:t,loadingComponent:n,errorComponent:r,delay:o=200,timeout:i,suspensible:s=!0,onError:l}=e;let a,u=null,c=0;const f=()=>{let e;return u||(e=u=t().catch((e=>{if(e=e instanceof Error?e:new Error(String(e)),l)return new Promise(((t,n)=>{l(e,(()=>t((c++,u=null,f()))),(()=>n(e)),c+1)}));throw e})).then((t=>e!==u&&u?u:(t&&(t.__esModule||"Module"===t[Symbol.toStringTag])&&(t=t.default),a=t,t))))};return Rr({name:"AsyncComponentWrapper",__asyncLoader:f,get __asyncResolved(){return a},setup(){const e=ps;if(a)return()=>Nr(a,e);const t=t=>{u=null,un(t,e,13,!r)};if(s&&e.suspense||xs)return f().then((t=>()=>Nr(t,e))).catch((e=>(t(e),()=>r?Qi(r,{error:e}):null)));const l=$t(!1),c=$t(),p=$t(!!o);return o&&setTimeout((()=>{p.value=!1}),o),null!=i&&setTimeout((()=>{if(!l.value&&!c.value){const e=new Error(`Async component timed out after ${i}ms.`);t(e),c.value=e}}),i),f().then((()=>{l.value=!0,e.parent&&Mr(e.parent.vnode)&&wn(e.parent.update)})).catch((e=>{t(e),c.value=e})),()=>l.value&&a?Nr(a,e):c.value&&r?Qi(r,{error:c.value}):n&&!p.value?Qi(n):void 0}})}function Nr(e,t){const{ref:n,props:r,children:o,ce:i}=t.vnode,s=Qi(e,r,o);return s.ref=n,s.ce=i,delete t.vnode.ce,s}const Mr=e=>e.type.__isKeepAlive,Dr={name:"KeepAlive",__isKeepAlive:!0,props:{include:[String,RegExp,Array],exclude:[String,RegExp,Array],max:[String,Number]},setup(e,{slots:t}){const n=ds(),r=n.ctx;if(!r.renderer)return()=>{const e=t.default&&t.default();return e&&1===e.length?e[0]:e};const o=new Map,i=new Set;let s=null;const l=n.suspense,{renderer:{p:a,m:u,um:c,o:{createElement:f}}}=r,p=f("div");function d(e){qr(e),c(e,n,l,!0)}function h(e){o.forEach(((t,n)=>{const r=As(t.type);!r||e&&e(r)||v(n)}))}function v(e){const t=o.get(e);s&&qi(t,s)?s&&qr(s):d(t),o.delete(e),i.delete(e)}r.activate=(e,t,n,r,o)=>{const i=e.component;u(e,t,n,0,l),a(i.vnode,e,t,n,i,l,r,e.slotScopeIds,o),gi((()=>{i.isDeactivated=!1,i.a&&$(i.a);const t=e.props&&e.props.onVnodeMounted;t&&as(t,i.parent,e)}),l)},r.deactivate=e=>{const t=e.component;u(e,p,null,1,l),gi((()=>{t.da&&$(t.da);const n=e.props&&e.props.onVnodeUnmounted;n&&as(n,t.parent,e),t.isDeactivated=!0}),l)},dr((()=>[e.include,e.exclude]),(([e,t])=>{e&&h((t=>Ur(e,t))),t&&h((e=>!Ur(t,e)))}),{flush:"post",deep:!0});let g=null;const m=()=>{null!=g&&o.set(g,Wr(n.subTree))};return Jr(m),Zr(m),Xr((()=>{o.forEach((e=>{const{subTree:t,suspense:r}=n,o=Wr(t);if(e.type!==o.type||e.key!==o.key)d(e);else{qr(o);const e=o.component.da;e&&gi(e,r)}}))})),()=>{if(g=null,!t.default)return null;const n=t.default(),r=n[0];if(n.length>1)return s=null,n;if(!(zi(r)&&(4&r.shapeFlag||128&r.shapeFlag)))return s=null,r;let l=Wr(r);const a=l.type,u=As(Ir(l)?l.type.__asyncResolved||{}:a),{include:c,exclude:f,max:p}=e;if(c&&(!u||!Ur(c,u))||f&&u&&Ur(f,u))return s=l,r;const d=null==l.key?a:l.key,h=o.get(d);return l.el&&(l=es(l),128&r.shapeFlag&&(r.ssContent=l)),g=d,h?(l.el=h.el,l.component=h.component,l.transition&&jr(l,l.transition),l.shapeFlag|=512,i.delete(d),i.add(d)):(i.add(d),p&&i.size>parseInt(p,10)&&v(i.values().next().value)),l.shapeFlag|=256,s=l,nr(r.type)?r:l}}},Br=Dr;function Ur(e,t){return m(e)?e.some((e=>Ur(e,t))):x(e)?e.split(",").includes(t):"[object RegExp]"===C(e)&&e.test(t)}function $r(e,t){Hr(e,"a",t)}function Vr(e,t){Hr(e,"da",t)}function Hr(e,t,n=ps){const r=e.__wdc||(e.__wdc=()=>{let t=n;for(;t;){if(t.isDeactivated)return;t=t.parent}return e()});if(Kr(t,r,n),n){let e=n.parent;for(;e&&e.parent;)Mr(e.parent.vnode)&&zr(r,t,n,e),e=e.parent}}function zr(e,t,n,r){const o=Kr(t,e,r,!0);eo((()=>{h(r[t],o)}),n)}function qr(e){e.shapeFlag&=-257,e.shapeFlag&=-513}function Wr(e){return 128&e.shapeFlag?e.ssContent:e}function Kr(e,t,n=ps,r=!1){if(n){const o=n[e]||(n[e]=[]),i=t.__weh||(t.__weh=(...r)=>{if(n.isUnmounted)return;Ie(),ms(n);const o=an(t,n,e,r);return ys(),Fe(),o});return r?o.unshift(i):o.push(i),i}}const Gr=e=>(t,n=ps)=>(!xs||"sp"===e)&&Kr(e,((...e)=>t(...e)),n),Yr=Gr("bm"),Jr=Gr("m"),Qr=Gr("bu"),Zr=Gr("u"),Xr=Gr("bum"),eo=Gr("um"),to=Gr("sp"),no=Gr("rtg"),ro=Gr("rtc");function oo(e,t=ps){Kr("ec",e,t)}function io(e,t,n,r){let o;const i=n&&n[r];if(m(e)||x(e)){o=new Array(e.length);for(let n=0,r=e.length;nt(e,n,void 0,i&&i[n])));else{const n=Object.keys(e);o=new Array(n.length);for(let r=0,s=n.length;r{const t=r.fn(...e);return t&&(t.key=r.key),t}:r.fn)}return e}function lo(e,t,n={},r,o){if(Fn.isCE||Fn.parent&&Ir(Fn.parent)&&Fn.parent.isCE)return"default"!==t&&(n.name=t),Qi("slot",n,r&&r());let i=e[t];i&&i._c&&(i._d=!1),Ni();const s=i&&ao(i(n)),l=Hi(Ai,{key:n.key||s&&s.key||`_${t}`},s||(r?r():[]),s&&1===e._?64:-2);return!o&&l.scopeId&&(l.slotScopeIds=[l.scopeId+"-s"]),i&&i._c&&(i._d=!0),l}function ao(e){return e.some((e=>!zi(e)||e.type!==Li&&!(e.type===Ai&&!ao(e.children))))?e:null}function uo(e,t){const n={};for(const r in e)n[t&&/[A-Z]/.test(r)?`on:${r}`:B(r)]=e[r];return n}const co=e=>e?bs(e)?Ts(e)||e.proxy:co(e.parent):null,fo=d(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>co(e.parent),$root:e=>co(e.root),$emit:e=>e.emit,$options:e=>No(e),$forceUpdate:e=>e.f||(e.f=()=>wn(e.update)),$nextTick:e=>e.n||(e.n=bn.bind(e.proxy)),$watch:e=>vr.bind(e)}),po=(e,t)=>e!==s&&!e.__isScriptSetup&&g(e,t),ho={get({_:e},t){const{ctx:n,setupState:r,data:o,props:i,accessCache:l,type:a,appContext:u}=e;let c;if("$"!==t[0]){const a=l[t];if(void 0!==a)switch(a){case 1:return r[t];case 2:return o[t];case 4:return n[t];case 3:return i[t]}else{if(po(r,t))return l[t]=1,r[t];if(o!==s&&g(o,t))return l[t]=2,o[t];if((c=e.propsOptions[0])&&g(c,t))return l[t]=3,i[t];if(n!==s&&g(n,t))return l[t]=4,n[t];Lo&&(l[t]=0)}}const f=fo[t];let p,d;return f?("$attrs"===t&&Ne(e,0,t),f(e)):(p=a.__cssModules)&&(p=p[t])?p:n!==s&&g(n,t)?(l[t]=4,n[t]):(d=u.config.globalProperties,g(d,t)?d[t]:void 0)},set({_:e},t,n){const{data:r,setupState:o,ctx:i}=e;return po(o,t)?(o[t]=n,!0):r!==s&&g(r,t)?(r[t]=n,!0):!g(e.props,t)&&(("$"!==t[0]||!(t.slice(1)in e))&&(i[t]=n,!0))},has({_:{data:e,setupState:t,accessCache:n,ctx:r,appContext:o,propsOptions:i}},l){let a;return!!n[l]||e!==s&&g(e,l)||po(t,l)||(a=i[0])&&g(a,l)||g(r,l)||g(fo,l)||g(o.config.globalProperties,l)},defineProperty(e,t,n){return null!=n.get?e._.accessCache[t]=0:g(n,"value")&&this.set(e,t,n.value,null),Reflect.defineProperty(e,t,n)}};const vo=d({},ho,{get(e,t){if(t!==Symbol.unscopables)return ho.get(e,t,e)},has:(e,t)=>"_"!==t[0]&&!G(t)});function go(){return null}function mo(){return null}function yo(e){0}function bo(e){0}function wo(){return null}function _o(){0}function xo(e,t){return null}function So(){return Eo().slots}function Oo(){return Eo().attrs}function ko(e,t,n){const r=ds();if(n&&n.local){const n=$t(e[t]);return dr((()=>e[t]),(e=>n.value=e)),dr(n,(n=>{n!==e[t]&&r.emit(`update:${t}`,n)})),n}return{__v_isRef:!0,get value(){return e[t]},set value(e){r.emit(`update:${t}`,e)}}}function Eo(){const e=ds();return e.setupContext||(e.setupContext=Ps(e))}function Co(e){return m(e)?e.reduce(((e,t)=>(e[t]=null,e)),{}):e}function Po(e,t){const n=Co(e);for(const e in t){if(e.startsWith("__skip"))continue;let r=n[e];r?m(r)||_(r)?r=n[e]={type:r,default:t[e]}:r.default=t[e]:null===r&&(r=n[e]={default:t[e]}),r&&t[`__skip_${e}`]&&(r.skipFactory=!0)}return n}function To(e,t){return e&&t?m(e)&&m(t)?e.concat(t):d({},Co(e),Co(t)):e||t}function Ao(e,t){const n={};for(const r in e)t.includes(r)||Object.defineProperty(n,r,{enumerable:!0,get:()=>e[r]});return n}function jo(e){const t=ds();let n=e();return ys(),k(n)&&(n=n.catch((e=>{throw ms(t),e}))),[n,()=>ms(t)]}let Lo=!0;function Ro(e){const t=No(e),n=e.proxy,r=e.ctx;Lo=!1,t.beforeCreate&&Io(t.beforeCreate,e,"bc");const{data:o,computed:i,methods:s,watch:l,provide:u,inject:c,created:f,beforeMount:p,mounted:d,beforeUpdate:h,updated:v,activated:g,deactivated:y,beforeDestroy:b,beforeUnmount:w,destroyed:x,unmounted:S,render:k,renderTracked:E,renderTriggered:C,errorCaptured:P,serverPrefetch:T,expose:A,inheritAttrs:j,components:L,directives:R,filters:I}=t;if(c&&function(e,t,n=a){m(e)&&(e=Uo(e));for(const n in e){const r=e[n];let o;o=O(r)?"default"in r?Yo(r.from||n,r.default,!0):Yo(r.from||n):Yo(r),Ut(o)?Object.defineProperty(t,n,{enumerable:!0,configurable:!0,get:()=>o.value,set:e=>o.value=e}):t[n]=o}}(c,r,null),s)for(const e in s){const t=s[e];_(t)&&(r[e]=t.bind(n))}if(o){0;const t=o.call(n,n);0,O(t)&&(e.data=kt(t))}if(Lo=!0,i)for(const e in i){const t=i[e],o=_(t)?t.bind(n,n):_(t.get)?t.get.bind(n,n):a;0;const s=!_(t)&&_(t.set)?t.set.bind(n):a,l=Ls({get:o,set:s});Object.defineProperty(r,e,{enumerable:!0,configurable:!0,get:()=>l.value,set:e=>l.value=e})}if(l)for(const e in l)Fo(l[e],r,n,e);if(u){const e=_(u)?u.call(n):u;Reflect.ownKeys(e).forEach((t=>{Go(t,e[t])}))}function F(e,t){m(t)?t.forEach((t=>e(t.bind(n)))):t&&e(t.bind(n))}if(f&&Io(f,e,"c"),F(Yr,p),F(Jr,d),F(Qr,h),F(Zr,v),F($r,g),F(Vr,y),F(oo,P),F(ro,E),F(no,C),F(Xr,w),F(eo,S),F(to,T),m(A))if(A.length){const t=e.exposed||(e.exposed={});A.forEach((e=>{Object.defineProperty(t,e,{get:()=>n[e],set:t=>n[e]=t})}))}else e.exposed||(e.exposed={});k&&e.render===a&&(e.render=k),null!=j&&(e.inheritAttrs=j),L&&(e.components=L),R&&(e.directives=R)}function Io(e,t,n){an(m(e)?e.map((e=>e.bind(t.proxy))):e.bind(t.proxy),t,n)}function Fo(e,t,n,r){const o=r.includes(".")?gr(n,r):()=>n[r];if(x(e)){const n=t[e];_(n)&&dr(o,n)}else if(_(e))dr(o,e.bind(n));else if(O(e))if(m(e))e.forEach((e=>Fo(e,t,n,r)));else{const r=_(e.handler)?e.handler.bind(n):t[e.handler];_(r)&&dr(o,r,e)}else 0}function No(e){const t=e.type,{mixins:n,extends:r}=t,{mixins:o,optionsCache:i,config:{optionMergeStrategies:s}}=e.appContext,l=i.get(t);let a;return l?a=l:o.length||n||r?(a={},o.length&&o.forEach((e=>Mo(a,e,s,!0))),Mo(a,t,s)):a=t,O(t)&&i.set(t,a),a}function Mo(e,t,n,r=!1){const{mixins:o,extends:i}=t;i&&Mo(e,i,n,!0),o&&o.forEach((t=>Mo(e,t,n,!0)));for(const o in t)if(r&&"expose"===o);else{const r=Do[o]||n&&n[o];e[o]=r?r(e[o],t[o]):t[o]}return e}const Do={data:Bo,props:Ho,emits:Ho,methods:Vo,computed:Vo,beforeCreate:$o,created:$o,beforeMount:$o,mounted:$o,beforeUpdate:$o,updated:$o,beforeDestroy:$o,beforeUnmount:$o,destroyed:$o,unmounted:$o,activated:$o,deactivated:$o,errorCaptured:$o,serverPrefetch:$o,components:Vo,directives:Vo,watch:function(e,t){if(!e)return t;if(!t)return e;const n=d(Object.create(null),e);for(const r in t)n[r]=$o(e[r],t[r]);return n},provide:Bo,inject:function(e,t){return Vo(Uo(e),Uo(t))}};function Bo(e,t){return t?e?function(){return d(_(e)?e.call(this,this):e,_(t)?t.call(this,this):t)}:t:e}function Uo(e){if(m(e)){const t={};for(let n=0;n(i.has(e)||(e&&_(e.install)?(i.add(e),e.install(l,...t)):_(e)&&(i.add(e),e(l,...t))),l),mixin:e=>(o.mixins.includes(e)||o.mixins.push(e),l),component:(e,t)=>t?(o.components[e]=t,l):o.components[e],directive:(e,t)=>t?(o.directives[e]=t,l):o.directives[e],mount(i,a,u){if(!s){0;const c=Qi(n,r);return c.appContext=o,a&&t?t(c,i):e(c,i,u),s=!0,l._container=i,i.__vue_app__=l,Ts(c.component)||c.component.proxy}},unmount(){s&&(e(null,l._container),delete l._container.__vue_app__)},provide:(e,t)=>(o.provides[e]=t,l),runWithContext(e){Ko=l;try{return e()}finally{Ko=null}}};return l}}let Ko=null;function Go(e,t){if(ps){let n=ps.provides;const r=ps.parent&&ps.parent.provides;r===n&&(n=ps.provides=Object.create(r)),n[e]=t}else 0}function Yo(e,t,n=!1){const r=ps||Fn;if(r||Ko){const o=r?null==r.parent?r.vnode.appContext&&r.vnode.appContext.provides:r.parent.provides:Ko._context.provides;if(o&&e in o)return o[e];if(arguments.length>1)return n&&_(t)?t.call(r&&r.proxy):t}else 0}function Jo(){return!!(ps||Fn||Ko)}function Qo(e,t,n,r){const[o,i]=e.propsOptions;let l,a=!1;if(t)for(let s in t){if(j(s))continue;const u=t[s];let c;o&&g(o,c=F(s))?i&&i.includes(c)?(l||(l={}))[c]=u:n[c]=u:In(e.emitsOptions,s)||s in r&&u===r[s]||(r[s]=u,a=!0)}if(i){const t=It(n),r=l||s;for(let s=0;s{c=!0;const[n,r]=Xo(e,t,!0);d(a,n),r&&u.push(...r)};!n&&t.mixins.length&&t.mixins.forEach(r),e.extends&&r(e.extends),e.mixins&&e.mixins.forEach(r)}if(!i&&!c)return O(e)&&r.set(e,l),l;if(m(i))for(let e=0;e-1,r[1]=n<0||e-1||g(r,"default"))&&u.push(t)}}}}const f=[a,u];return O(e)&&r.set(e,f),f}function ei(e){return"$"!==e[0]}function ti(e){const t=e&&e.toString().match(/^\s*(function|class) (\w+)/);return t?t[2]:null===e?"null":""}function ni(e,t){return ti(e)===ti(t)}function ri(e,t){return m(t)?t.findIndex((t=>ni(t,e))):_(t)&&ni(t,e)?0:-1}const oi=e=>"_"===e[0]||"$stable"===e,ii=e=>m(e)?e.map(os):[os(e)],si=(e,t,n)=>{if(t._n)return t;const r=$n(((...e)=>ii(t(...e))),n);return r._c=!1,r},li=(e,t,n)=>{const r=e._ctx;for(const n in e){if(oi(n))continue;const o=e[n];if(_(o))t[n]=si(0,o,r);else if(null!=o){0;const e=ii(o);t[n]=()=>e}}},ai=(e,t)=>{const n=ii(t);e.slots.default=()=>n},ui=(e,t)=>{if(32&e.vnode.shapeFlag){const n=t._;n?(e.slots=It(t),V(t,"_",n)):li(t,e.slots={})}else e.slots={},t&&ai(e,t);V(e.slots,Ki,1)},ci=(e,t,n)=>{const{vnode:r,slots:o}=e;let i=!0,l=s;if(32&r.shapeFlag){const e=t._;e?n&&1===e?i=!1:(d(o,t),n||1!==e||delete o._):(i=!t.$stable,li(t,o)),l=t}else t&&(ai(e,t),l={default:1});if(i)for(const e in o)oi(e)||null!=l[e]||delete o[e]};function fi(e,t,n,r,o=!1){if(m(e))return void e.forEach(((e,i)=>fi(e,t&&(m(t)?t[i]:t),n,r,o)));if(Ir(r)&&!o)return;const i=4&r.shapeFlag?Ts(r.component)||r.component.proxy:r.el,l=o?null:i,{i:a,r:u}=e;const c=t&&t.r,f=a.refs===s?a.refs={}:a.refs,p=a.setupState;if(null!=c&&c!==u&&(x(c)?(f[c]=null,g(p,c)&&(p[c]=null)):Ut(c)&&(c.value=null)),_(u))ln(u,a,12,[l,f]);else{const t=x(u),r=Ut(u);if(t||r){const s=()=>{if(e.f){const n=t?g(p,u)?p[u]:f[u]:u.value;o?m(n)&&h(n,i):m(n)?n.includes(i)||n.push(i):t?(f[u]=[i],g(p,u)&&(p[u]=f[u])):(u.value=[i],e.k&&(f[e.k]=u.value))}else t?(f[u]=l,g(p,u)&&(p[u]=l)):r&&(u.value=l,e.k&&(f[e.k]=l))};l?(s.id=-1,gi(s,n)):s()}else 0}}let pi=!1;const di=e=>/svg/.test(e.namespaceURI)&&"foreignObject"!==e.tagName,hi=e=>8===e.nodeType;function vi(e){const{mt:t,p:n,o:{patchProp:r,createText:o,nextSibling:i,parentNode:s,remove:l,insert:a,createComment:u}}=e,c=(n,r,l,u,f,b=!1)=>{const w=hi(n)&&"["===n.data,_=()=>v(n,r,l,u,f,w),{type:x,ref:S,shapeFlag:O,patchFlag:k}=r;let E=n.nodeType;r.el=n,-2===k&&(b=!1,r.dynamicChildren=null);let C=null;switch(x){case ji:3!==E?""===r.children?(a(r.el=o(""),s(n),n),C=n):C=_():(n.data!==r.children&&(pi=!0,n.data=r.children),C=i(n));break;case Li:y(n)?(C=i(n),m(r.el=n.content.firstChild,n,l)):C=8!==E||w?_():i(n);break;case Ri:if(w&&(E=(n=i(n)).nodeType),1===E||3===E){C=n;const e=!r.children.length;for(let t=0;t{s=s||!!t.dynamicChildren;const{type:a,props:u,patchFlag:c,shapeFlag:p,dirs:h,transition:v}=t,g="input"===a&&h||"option"===a;if(g||-1!==c){if(h&&br(t,null,n,"created"),u)if(g||!s||48&c)for(const t in u)(g&&t.endsWith("value")||f(t)&&!j(t))&&r(e,t,null,u[t],!1,void 0,n);else u.onClick&&r(e,"onClick",null,u.onClick,!1,void 0,n);let a;(a=u&&u.onVnodeBeforeMount)&&as(a,n,t);let b=!1;if(y(e)){b=_i(o,v)&&n&&n.vnode.props&&n.vnode.props.appear;const r=e.content.firstChild;b&&v.beforeEnter(r),m(r,e,n),t.el=e=r}if(h&&br(t,null,n,"beforeMount"),((a=u&&u.onVnodeMounted)||h||b)&&lr((()=>{a&&as(a,n,t),b&&v.enter(e),h&&br(t,null,n,"mounted")}),o),16&p&&(!u||!u.innerHTML&&!u.textContent)){let r=d(e.firstChild,t,e,n,o,i,s);for(;r;){pi=!0;const e=r;r=r.nextSibling,l(e)}}else 8&p&&e.textContent!==t.children&&(pi=!0,e.textContent=t.children)}return e.nextSibling},d=(e,t,r,o,i,s,l)=>{l=l||!!t.dynamicChildren;const a=t.children,u=a.length;for(let t=0;t{const{slotScopeIds:c}=t;c&&(o=o?o.concat(c):c);const f=s(e),p=d(i(e),t,f,n,r,o,l);return p&&hi(p)&&"]"===p.data?i(t.anchor=p):(pi=!0,a(t.anchor=u("]"),f,p),p)},v=(e,t,r,o,a,u)=>{if(pi=!0,t.el=null,u){const t=g(e);for(;;){const n=i(e);if(!n||n===t)break;l(n)}}const c=i(e),f=s(e);return l(e),n(null,t,f,c,r,o,di(f),a),c},g=(e,t="[",n="]")=>{let r=0;for(;e;)if((e=i(e))&&hi(e)&&(e.data===t&&r++,e.data===n)){if(0===r)return i(e);r--}return e},m=(e,t,n)=>{const r=t.parentNode;r&&r.replaceChild(e,t);let o=n;for(;o;)o.vnode.el===t&&(o.vnode.el=o.subTree.el=e),o=o.parent},y=e=>1===e.nodeType&&"template"===e.tagName.toLowerCase();return[(e,t)=>{if(!t.hasChildNodes())return n(null,e,t),On(),void(t._vnode=e);pi=!1,c(t.firstChild,e,null,null,null),On(),t._vnode=e},c]}const gi=lr;function mi(e){return bi(e)}function yi(e){return bi(e,vi)}function bi(e,t){W().__VUE__=!0;const{insert:n,remove:r,patchProp:o,createElement:i,createText:u,createComment:c,setText:f,setElementText:p,parentNode:d,nextSibling:h,setScopeId:v=a,insertStaticContent:m}=e,y=(e,t,n,r=null,o=null,i=null,s=!1,l=null,a=!!t.dynamicChildren)=>{if(e===t)return;e&&!qi(e,t)&&(r=J(e),z(e,o,i,!0),e=null),-2===t.patchFlag&&(a=!1,t.dynamicChildren=null);const{type:u,ref:c,shapeFlag:f}=t;switch(u){case ji:b(e,t,n,r);break;case Li:w(e,t,n,r);break;case Ri:null==e&&_(t,n,r,s);break;case Ai:A(e,t,n,r,o,i,s,l,a);break;default:1&f?S(e,t,n,r,o,i,s,l,a):6&f?L(e,t,n,r,o,i,s,l,a):(64&f||128&f)&&u.process(e,t,n,r,o,i,s,l,a,Z)}null!=c&&o&&fi(c,e&&e.ref,i,t||e,!t)},b=(e,t,r,o)=>{if(null==e)n(t.el=u(t.children),r,o);else{const n=t.el=e.el;t.children!==e.children&&f(n,t.children)}},w=(e,t,r,o)=>{null==e?n(t.el=c(t.children||""),r,o):t.el=e.el},_=(e,t,n,r)=>{[e.el,e.anchor]=m(e.children,t,n,r,e.el,e.anchor)},x=({el:e,anchor:t})=>{let n;for(;e&&e!==t;)n=h(e),r(e),e=n;r(t)},S=(e,t,n,r,o,i,s,l,a)=>{s=s||"svg"===t.type,null==e?O(t,n,r,o,i,s,l,a):C(e,t,o,i,s,l,a)},O=(e,t,r,s,l,a,u,c)=>{let f,d;const{type:h,props:v,shapeFlag:g,transition:m,dirs:y}=e;if(f=e.el=i(e.type,a,v&&v.is,v),8&g?p(f,e.children):16&g&&E(e.children,f,null,s,l,a&&"foreignObject"!==h,u,c),y&&br(e,null,s,"created"),k(f,e,e.scopeId,u,s),v){for(const t in v)"value"===t||j(t)||o(f,t,null,v[t],a,e.children,s,l,Y);"value"in v&&o(f,"value",null,v.value),(d=v.onVnodeBeforeMount)&&as(d,s,e)}y&&br(e,null,s,"beforeMount");const b=_i(l,m);b&&m.beforeEnter(f),n(f,t,r),((d=v&&v.onVnodeMounted)||b||y)&&gi((()=>{d&&as(d,s,e),b&&m.enter(f),y&&br(e,null,s,"mounted")}),l)},k=(e,t,n,r,o)=>{if(n&&v(e,n),r)for(let t=0;t{for(let u=a;u{const u=t.el=e.el;let{patchFlag:c,dynamicChildren:f,dirs:d}=t;c|=16&e.patchFlag;const h=e.props||s,v=t.props||s;let g;n&&wi(n,!1),(g=v.onVnodeBeforeUpdate)&&as(g,n,t,e),d&&br(t,e,n,"beforeUpdate"),n&&wi(n,!0);const m=i&&"foreignObject"!==t.type;if(f?P(e.dynamicChildren,f,u,n,r,m,l):a||B(e,t,u,null,n,r,m,l,!1),c>0){if(16&c)T(u,t,h,v,n,r,i);else if(2&c&&h.class!==v.class&&o(u,"class",null,v.class,i),4&c&&o(u,"style",h.style,v.style,i),8&c){const s=t.dynamicProps;for(let t=0;t{g&&as(g,n,t,e),d&&br(t,e,n,"updated")}),r)},P=(e,t,n,r,o,i,s)=>{for(let l=0;l{if(n!==r){if(n!==s)for(const s in n)j(s)||s in r||o(e,s,n[s],null,a,t.children,i,l,Y);for(const s in r){if(j(s))continue;const u=r[s],c=n[s];u!==c&&"value"!==s&&o(e,s,c,u,a,t.children,i,l,Y)}"value"in r&&o(e,"value",n.value,r.value)}},A=(e,t,r,o,i,s,l,a,c)=>{const f=t.el=e?e.el:u(""),p=t.anchor=e?e.anchor:u("");let{patchFlag:d,dynamicChildren:h,slotScopeIds:v}=t;v&&(a=a?a.concat(v):v),null==e?(n(f,r,o),n(p,r,o),E(t.children,r,p,i,s,l,a,c)):d>0&&64&d&&h&&e.dynamicChildren?(P(e.dynamicChildren,h,r,i,s,l,a),(null!=t.key||i&&t===i.subTree)&&xi(e,t,!0)):B(e,t,r,p,i,s,l,a,c)},L=(e,t,n,r,o,i,s,l,a)=>{t.slotScopeIds=l,null==e?512&t.shapeFlag?o.ctx.activate(t,n,r,s,a):R(t,n,r,o,i,s,a):I(e,t,a)},R=(e,t,n,r,o,i,s)=>{const l=e.component=fs(e,r,o);if(Mr(e)&&(l.ctx.renderer=Z),Ss(l),l.asyncDep){if(o&&o.registerDep(l,N),!e.el){const e=l.subTree=Qi(Li);w(null,e,t,n)}}else N(l,e,t,n,o,i,s)},I=(e,t,n)=>{const r=t.component=e.component;if(function(e,t,n){const{props:r,children:o,component:i}=e,{props:s,children:l,patchFlag:a}=t,u=i.emitsOptions;if(t.dirs||t.transition)return!0;if(!(n&&a>=0))return!(!o&&!l||l&&l.$stable)||r!==s&&(r?!s||Wn(r,s,u):!!s);if(1024&a)return!0;if(16&a)return r?Wn(r,s,u):!!s;if(8&a){const e=t.dynamicProps;for(let t=0;tdn&&pn.splice(t,1)}(r.update),r.update()}else t.el=e.el,r.vnode=t},N=(e,t,n,r,o,i,s)=>{const l=e.effect=new Pe((()=>{if(e.isMounted){let t,{next:n,bu:r,u:l,parent:a,vnode:u}=e,c=n;0,wi(e,!1),n?(n.el=u.el,D(e,n,s)):n=u,r&&$(r),(t=n.props&&n.props.onVnodeBeforeUpdate)&&as(t,a,n,u),wi(e,!0);const f=Vn(e);0;const p=e.subTree;e.subTree=f,y(p,f,d(p.el),J(p),e,o,i),n.el=f.el,null===c&&Kn(e,f.el),l&&gi(l,o),(t=n.props&&n.props.onVnodeUpdated)&&gi((()=>as(t,a,n,u)),o)}else{let s;const{el:l,props:a}=t,{bm:u,m:c,parent:f}=e,p=Ir(t);if(wi(e,!1),u&&$(u),!p&&(s=a&&a.onVnodeBeforeMount)&&as(s,f,t),wi(e,!0),l&&ee){const n=()=>{e.subTree=Vn(e),ee(l,e.subTree,e,o,null)};p?t.type.__asyncLoader().then((()=>!e.isUnmounted&&n())):n()}else{0;const s=e.subTree=Vn(e);0,y(null,s,n,r,e,o,i),t.el=s.el}if(c&&gi(c,o),!p&&(s=a&&a.onVnodeMounted)){const e=t;gi((()=>as(s,f,e)),o)}(256&t.shapeFlag||f&&Ir(f.vnode)&&256&f.vnode.shapeFlag)&&e.a&&gi(e.a,o),e.isMounted=!0,t=n=r=null}}),(()=>wn(a)),e.scope),a=e.update=()=>l.run();a.id=e.uid,wi(e,!0),a()},D=(e,t,n)=>{t.component=e;const r=e.vnode.props;e.vnode=t,e.next=null,function(e,t,n,r){const{props:o,attrs:i,vnode:{patchFlag:s}}=e,l=It(o),[a]=e.propsOptions;let u=!1;if(!(r||s>0)||16&s){let r;Qo(e,t,o,i)&&(u=!0);for(const i in l)t&&(g(t,i)||(r=M(i))!==i&&g(t,r))||(a?!n||void 0===n[i]&&void 0===n[r]||(o[i]=Zo(a,l,i,void 0,e,!0)):delete o[i]);if(i!==l)for(const e in i)t&&g(t,e)||(delete i[e],u=!0)}else if(8&s){const n=e.vnode.dynamicProps;for(let r=0;r{const u=e&&e.children,c=e?e.shapeFlag:0,f=t.children,{patchFlag:d,shapeFlag:h}=t;if(d>0){if(128&d)return void V(u,f,n,r,o,i,s,l,a);if(256&d)return void U(u,f,n,r,o,i,s,l,a)}8&h?(16&c&&Y(u,o,i),f!==u&&p(n,f)):16&c?16&h?V(u,f,n,r,o,i,s,l,a):Y(u,o,i,!0):(8&c&&p(n,""),16&h&&E(f,n,r,o,i,s,l,a))},U=(e,t,n,r,o,i,s,a,u)=>{t=t||l;const c=(e=e||l).length,f=t.length,p=Math.min(c,f);let d;for(d=0;df?Y(e,o,i,!0,!1,p):E(t,n,r,o,i,s,a,u,p)},V=(e,t,n,r,o,i,s,a,u)=>{let c=0;const f=t.length;let p=e.length-1,d=f-1;for(;c<=p&&c<=d;){const r=e[c],l=t[c]=u?is(t[c]):os(t[c]);if(!qi(r,l))break;y(r,l,n,null,o,i,s,a,u),c++}for(;c<=p&&c<=d;){const r=e[p],l=t[d]=u?is(t[d]):os(t[d]);if(!qi(r,l))break;y(r,l,n,null,o,i,s,a,u),p--,d--}if(c>p){if(c<=d){const e=d+1,l=ed)for(;c<=p;)z(e[c],o,i,!0),c++;else{const h=c,v=c,g=new Map;for(c=v;c<=d;c++){const e=t[c]=u?is(t[c]):os(t[c]);null!=e.key&&g.set(e.key,c)}let m,b=0;const w=d-v+1;let _=!1,x=0;const S=new Array(w);for(c=0;c=w){z(r,o,i,!0);continue}let l;if(null!=r.key)l=g.get(r.key);else for(m=v;m<=d;m++)if(0===S[m-v]&&qi(r,t[m])){l=m;break}void 0===l?z(r,o,i,!0):(S[l-v]=c+1,l>=x?x=l:_=!0,y(r,t[l],n,null,o,i,s,a,u),b++)}const O=_?function(e){const t=e.slice(),n=[0];let r,o,i,s,l;const a=e.length;for(r=0;r>1,e[n[l]]0&&(t[r]=n[i-1]),n[i]=r)}}i=n.length,s=n[i-1];for(;i-- >0;)n[i]=s,s=t[s];return n}(S):l;for(m=O.length-1,c=w-1;c>=0;c--){const e=v+c,l=t[e],p=e+1{const{el:s,type:l,transition:a,children:u,shapeFlag:c}=e;if(6&c)return void H(e.component.subTree,t,r,o);if(128&c)return void e.suspense.move(t,r,o);if(64&c)return void l.move(e,t,r,Z);if(l===Ai){n(s,t,r);for(let e=0;e{let i;for(;e&&e!==t;)i=h(e),n(e,r,o),e=i;n(t,r,o)})(e,t,r);if(2!==o&&1&c&&a)if(0===o)a.beforeEnter(s),n(s,t,r),gi((()=>a.enter(s)),i);else{const{leave:e,delayLeave:o,afterLeave:i}=a,l=()=>n(s,t,r),u=()=>{e(s,(()=>{l(),i&&i()}))};o?o(s,l,u):u()}else n(s,t,r)},z=(e,t,n,r=!1,o=!1)=>{const{type:i,props:s,ref:l,children:a,dynamicChildren:u,shapeFlag:c,patchFlag:f,dirs:p}=e;if(null!=l&&fi(l,null,n,e,!0),256&c)return void t.ctx.deactivate(e);const d=1&c&&p,h=!Ir(e);let v;if(h&&(v=s&&s.onVnodeBeforeUnmount)&&as(v,t,e),6&c)G(e.component,n,r);else{if(128&c)return void e.suspense.unmount(n,r);d&&br(e,null,t,"beforeUnmount"),64&c?e.type.remove(e,t,n,o,Z,r):u&&(i!==Ai||f>0&&64&f)?Y(u,t,n,!1,!0):(i===Ai&&384&f||!o&&16&c)&&Y(a,t,n),r&&q(e)}(h&&(v=s&&s.onVnodeUnmounted)||d)&&gi((()=>{v&&as(v,t,e),d&&br(e,null,t,"unmounted")}),n)},q=e=>{const{type:t,el:n,anchor:o,transition:i}=e;if(t===Ai)return void K(n,o);if(t===Ri)return void x(e);const s=()=>{r(n),i&&!i.persisted&&i.afterLeave&&i.afterLeave()};if(1&e.shapeFlag&&i&&!i.persisted){const{leave:t,delayLeave:r}=i,o=()=>t(n,s);r?r(e.el,s,o):o()}else s()},K=(e,t)=>{let n;for(;e!==t;)n=h(e),r(e),e=n;r(t)},G=(e,t,n)=>{const{bum:r,scope:o,update:i,subTree:s,um:l}=e;r&&$(r),o.stop(),i&&(i.active=!1,z(s,e,t,n)),l&&gi(l,t),gi((()=>{e.isUnmounted=!0}),t),t&&t.pendingBranch&&!t.isUnmounted&&e.asyncDep&&!e.asyncResolved&&e.suspenseId===t.pendingId&&(t.deps--,0===t.deps&&t.resolve())},Y=(e,t,n,r=!1,o=!1,i=0)=>{for(let s=i;s6&e.shapeFlag?J(e.component.subTree):128&e.shapeFlag?e.suspense.next():h(e.anchor||e.el),Q=(e,t,n)=>{null==e?t._vnode&&z(t._vnode,null,null,!0):y(t._vnode||null,e,t,null,null,null,n),Sn(),On(),t._vnode=e},Z={p:y,um:z,m:H,r:q,mt:R,mc:E,pc:B,pbc:P,n:J,o:e};let X,ee;return t&&([X,ee]=t(Z)),{render:Q,hydrate:X,createApp:Wo(Q,X)}}function wi({effect:e,update:t},n){e.allowRecurse=t.allowRecurse=n}function _i(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function xi(e,t,n=!1){const r=e.children,o=t.children;if(m(r)&&m(o))for(let e=0;ee&&(e.disabled||""===e.disabled),Oi=e=>"undefined"!=typeof SVGElement&&e instanceof SVGElement,ki=(e,t)=>{const n=e&&e.to;if(x(n)){if(t){const e=t(n);return e}return null}return n},Ei={__isTeleport:!0,process(e,t,n,r,o,i,s,l,a,u){const{mc:c,pc:f,pbc:p,o:{insert:d,querySelector:h,createText:v,createComment:g}}=u,m=Si(t.props);let{shapeFlag:y,children:b,dynamicChildren:w}=t;if(null==e){const e=t.el=v(""),u=t.anchor=v("");d(e,n,r),d(u,n,r);const f=t.target=ki(t.props,h),p=t.targetAnchor=v("");f&&(d(p,f),s=s||Oi(f));const g=(e,t)=>{16&y&&c(b,e,t,o,i,s,l,a)};m?g(n,u):f&&g(f,p)}else{t.el=e.el;const r=t.anchor=e.anchor,c=t.target=e.target,d=t.targetAnchor=e.targetAnchor,v=Si(e.props),g=v?n:c,y=v?r:d;if(s=s||Oi(c),w?(p(e.dynamicChildren,w,g,o,i,s,l),xi(e,t,!0)):a||f(e,t,g,y,o,i,s,l,!1),m)v?t.props&&e.props&&t.props.to!==e.props.to&&(t.props.to=e.props.to):Ci(t,n,r,u,1);else if((t.props&&t.props.to)!==(e.props&&e.props.to)){const e=t.target=ki(t.props,h);e&&Ci(t,e,null,u,0)}else v&&Ci(t,c,d,u,1)}Ti(t)},remove(e,t,n,r,{um:o,o:{remove:i}},s){const{shapeFlag:l,children:a,anchor:u,targetAnchor:c,target:f,props:p}=e;if(f&&i(c),s&&i(u),16&l){const e=s||!Si(p);for(let r=0;r0?Fi||l:null,Mi(),Bi>0&&Fi&&Fi.push(e),e}function Vi(e,t,n,r,o,i){return $i(Ji(e,t,n,r,o,i,!0))}function Hi(e,t,n,r,o){return $i(Qi(e,t,n,r,o,!0))}function zi(e){return!!e&&!0===e.__v_isVNode}function qi(e,t){return e.type===t.type&&e.key===t.key}function Wi(e){Di=e}const Ki="__vInternal",Gi=({key:e})=>null!=e?e:null,Yi=({ref:e,ref_key:t,ref_for:n})=>("number"==typeof e&&(e=""+e),null!=e?x(e)||Ut(e)||_(e)?{i:Fn,r:e,k:t,f:!!n}:e:null);function Ji(e,t=null,n=null,r=0,o=null,i=(e===Ai?0:1),s=!1,l=!1){const a={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&Gi(t),ref:t&&Yi(t),scopeId:Nn,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetAnchor:null,staticCount:0,shapeFlag:i,patchFlag:r,dynamicProps:o,dynamicChildren:null,appContext:null,ctx:Fn};return l?(ss(a,n),128&i&&e.normalize(a)):n&&(a.shapeFlag|=x(n)?8:16),Bi>0&&!s&&Fi&&(a.patchFlag>0||6&i)&&32!==a.patchFlag&&Fi.push(a),a}const Qi=Zi;function Zi(e,t=null,n=null,r=0,o=null,i=!1){if(e&&e!==Qn||(e=Li),zi(e)){const r=es(e,t,!0);return n&&ss(r,n),Bi>0&&!i&&Fi&&(6&r.shapeFlag?Fi[Fi.indexOf(e)]=r:Fi.push(r)),r.patchFlag|=-2,r}if(js(e)&&(e=e.__vccOpts),t){t=Xi(t);let{class:e,style:n}=t;e&&!x(e)&&(t.class=ee(e)),O(n)&&(Rt(n)&&!m(n)&&(n=d({},n)),t.style=Y(n))}return Ji(e,t,n,r,o,x(e)?1:nr(e)?128:(e=>e.__isTeleport)(e)?64:O(e)?4:_(e)?2:0,i,!0)}function Xi(e){return e?Rt(e)||Ki in e?d({},e):e:null}function es(e,t,n=!1){const{props:r,ref:o,patchFlag:i,children:s}=e,l=t?ls(r||{},t):r;return{__v_isVNode:!0,__v_skip:!0,type:e.type,props:l,key:l&&Gi(l),ref:t&&t.ref?n&&o?m(o)?o.concat(Yi(t)):[o,Yi(t)]:Yi(t):o,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:s,target:e.target,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==Ai?-1===i?16:16|i:i,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:e.transition,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&es(e.ssContent),ssFallback:e.ssFallback&&es(e.ssFallback),el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce}}function ts(e=" ",t=0){return Qi(ji,null,e,t)}function ns(e,t){const n=Qi(Ri,null,e);return n.staticCount=t,n}function rs(e="",t=!1){return t?(Ni(),Hi(Li,null,e)):Qi(Li,null,e)}function os(e){return null==e||"boolean"==typeof e?Qi(Li):m(e)?Qi(Ai,null,e.slice()):"object"==typeof e?is(e):Qi(ji,null,String(e))}function is(e){return null===e.el&&-1!==e.patchFlag||e.memo?e:es(e)}function ss(e,t){let n=0;const{shapeFlag:r}=e;if(null==t)t=null;else if(m(t))n=16;else if("object"==typeof t){if(65&r){const n=t.default;return void(n&&(n._c&&(n._d=!1),ss(e,n()),n._c&&(n._d=!0)))}{n=32;const r=t._;r||Ki in t?3===r&&Fn&&(1===Fn.slots._?t._=1:(t._=2,e.patchFlag|=1024)):t._ctx=Fn}}else _(t)?(t={default:t,_ctx:Fn},n=32):(t=String(t),64&r?(n=16,t=[ts(t)]):n=8);e.children=t,e.shapeFlag|=n}function ls(...e){const t={};for(let n=0;nps||Fn;let hs,vs,gs="__VUE_INSTANCE_SETTERS__";(vs=W()[gs])||(vs=W()[gs]=[]),vs.push((e=>ps=e)),hs=e=>{vs.length>1?vs.forEach((t=>t(e))):vs[0](e)};const ms=e=>{hs(e),e.scope.on()},ys=()=>{ps&&ps.scope.off(),hs(null)};function bs(e){return 4&e.vnode.shapeFlag}let ws,_s,xs=!1;function Ss(e,t=!1){xs=t;const{props:n,children:r}=e.vnode,o=bs(e);!function(e,t,n,r=!1){const o={},i={};V(i,Ki,1),e.propsDefaults=Object.create(null),Qo(e,t,o,i);for(const t in e.propsOptions[0])t in o||(o[t]=void 0);n?e.props=r?o:Et(o):e.type.props?e.props=o:e.props=i,e.attrs=i}(e,n,o,t),ui(e,r);const i=o?function(e,t){const n=e.type;0;e.accessCache=Object.create(null),e.proxy=Ft(new Proxy(e.ctx,ho)),!1;const{setup:r}=n;if(r){const n=e.setupContext=r.length>1?Ps(e):null;ms(e),Ie();const o=ln(r,e,0,[e.props,n]);if(Fe(),ys(),k(o)){if(o.then(ys,ys),t)return o.then((n=>{Os(e,n,t)})).catch((t=>{un(t,e,0)}));e.asyncDep=o}else Os(e,o,t)}else Cs(e,t)}(e,t):void 0;return xs=!1,i}function Os(e,t,n){_(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:O(t)&&(e.setupState=Yt(t)),Cs(e,n)}function ks(e){ws=e,_s=e=>{e.render._rc&&(e.withProxy=new Proxy(e.ctx,vo))}}const Es=()=>!ws;function Cs(e,t,n){const r=e.type;if(!e.render){if(!t&&ws&&!r.render){const t=r.template||No(e).template;if(t){0;const{isCustomElement:n,compilerOptions:o}=e.appContext.config,{delimiters:i,compilerOptions:s}=r,l=d(d({isCustomElement:n,delimiters:i},o),s);r.render=ws(t,l)}}e.render=r.render||a,_s&&_s(e)}ms(e),Ie();try{Ro(e)}finally{Fe(),ys()}}function Ps(e){const t=t=>{e.exposed=t||{}};return{get attrs(){return function(e){return e.attrsProxy||(e.attrsProxy=new Proxy(e.attrs,{get:(t,n)=>(Ne(e,0,"$attrs"),t[n])}))}(e)},slots:e.slots,emit:e.emit,expose:t}}function Ts(e){if(e.exposed)return e.exposeProxy||(e.exposeProxy=new Proxy(Yt(Ft(e.exposed)),{get:(t,n)=>n in t?t[n]:n in fo?fo[n](e):void 0,has:(e,t)=>t in e||t in fo}))}function As(e,t=!0){return _(e)?e.displayName||e.name:e.name||t&&e.__name}function js(e){return _(e)&&"__vccOpts"in e}const Ls=(e,t)=>function(e,t,n=!1){let r,o;const i=_(e);return i?(r=e,o=a):(r=e.get,o=e.set),new rn(r,o,i||!o,n)}(e,0,xs);function Rs(e,t,n){const r=arguments.length;return 2===r?O(t)&&!m(t)?zi(t)?Qi(e,null,[t]):Qi(e,t):Qi(e,null,t):(r>3?n=Array.prototype.slice.call(arguments,2):3===r&&zi(n)&&(n=[n]),Qi(e,t,n))}const Is=Symbol.for("v-scx"),Fs=()=>{{const e=Yo(Is);return e}};function Ns(){return void 0}function Ms(e,t,n,r){const o=n[r];if(o&&Ds(o,e))return o;const i=t();return i.memo=e.slice(),n[r]=i}function Ds(e,t){const n=e.memo;if(n.length!=t.length)return!1;for(let e=0;e0&&Fi&&Fi.push(e),!0}const Bs="3.3.8",Us={createComponentInstance:fs,setupComponent:Ss,renderComponentRoot:Vn,setCurrentRenderingInstance:Mn,isVNode:zi,normalizeVNode:os},$s=null,Vs=null,Hs="undefined"!=typeof document?document:null,zs=Hs&&Hs.createElement("template"),qs={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,r)=>{const o=t?Hs.createElementNS("http://www.w3.org/2000/svg",e):Hs.createElement(e,n?{is:n}:void 0);return"select"===e&&r&&null!=r.multiple&&o.setAttribute("multiple",r.multiple),o},createText:e=>Hs.createTextNode(e),createComment:e=>Hs.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>Hs.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,n,r,o,i){const s=n?n.previousSibling:t.lastChild;if(o&&(o===i||o.nextSibling))for(;t.insertBefore(o.cloneNode(!0),n),o!==i&&(o=o.nextSibling););else{zs.innerHTML=r?`${e}`:e;const o=zs.content;if(r){const e=o.firstChild;for(;e.firstChild;)o.appendChild(e.firstChild);o.removeChild(e)}t.insertBefore(o,n)}return[s?s.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}},Ws="transition",Ks="animation",Gs=Symbol("_vtc"),Ys=(e,{slots:t})=>Rs(Er,el(e),t);Ys.displayName="Transition";const Js={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String},Qs=Ys.props=d({},Or,Js),Zs=(e,t=[])=>{m(e)?e.forEach((e=>e(...t))):e&&e(...t)},Xs=e=>!!e&&(m(e)?e.some((e=>e.length>1)):e.length>1);function el(e){const t={};for(const n in e)n in Js||(t[n]=e[n]);if(!1===e.css)return t;const{name:n="v",type:r,duration:o,enterFromClass:i=`${n}-enter-from`,enterActiveClass:s=`${n}-enter-active`,enterToClass:l=`${n}-enter-to`,appearFromClass:a=i,appearActiveClass:u=s,appearToClass:c=l,leaveFromClass:f=`${n}-leave-from`,leaveActiveClass:p=`${n}-leave-active`,leaveToClass:h=`${n}-leave-to`}=e,v=function(e){if(null==e)return null;if(O(e))return[tl(e.enter),tl(e.leave)];{const t=tl(e);return[t,t]}}(o),g=v&&v[0],m=v&&v[1],{onBeforeEnter:y,onEnter:b,onEnterCancelled:w,onLeave:_,onLeaveCancelled:x,onBeforeAppear:S=y,onAppear:k=b,onAppearCancelled:E=w}=t,C=(e,t,n)=>{rl(e,t?c:l),rl(e,t?u:s),n&&n()},P=(e,t)=>{e._isLeaving=!1,rl(e,f),rl(e,h),rl(e,p),t&&t()},T=e=>(t,n)=>{const o=e?k:b,s=()=>C(t,e,n);Zs(o,[t,s]),ol((()=>{rl(t,e?a:i),nl(t,e?c:l),Xs(o)||sl(t,r,g,s)}))};return d(t,{onBeforeEnter(e){Zs(y,[e]),nl(e,i),nl(e,s)},onBeforeAppear(e){Zs(S,[e]),nl(e,a),nl(e,u)},onEnter:T(!1),onAppear:T(!0),onLeave(e,t){e._isLeaving=!0;const n=()=>P(e,t);nl(e,f),cl(),nl(e,p),ol((()=>{e._isLeaving&&(rl(e,f),nl(e,h),Xs(_)||sl(e,r,m,n))})),Zs(_,[e,n])},onEnterCancelled(e){C(e,!1),Zs(w,[e])},onAppearCancelled(e){C(e,!0),Zs(E,[e])},onLeaveCancelled(e){P(e),Zs(x,[e])}})}function tl(e){return z(e)}function nl(e,t){t.split(/\s+/).forEach((t=>t&&e.classList.add(t))),(e[Gs]||(e[Gs]=new Set)).add(t)}function rl(e,t){t.split(/\s+/).forEach((t=>t&&e.classList.remove(t)));const n=e[Gs];n&&(n.delete(t),n.size||(e[Gs]=void 0))}function ol(e){requestAnimationFrame((()=>{requestAnimationFrame(e)}))}let il=0;function sl(e,t,n,r){const o=e._endId=++il,i=()=>{o===e._endId&&r()};if(n)return setTimeout(i,n);const{type:s,timeout:l,propCount:a}=ll(e,t);if(!s)return r();const u=s+"end";let c=0;const f=()=>{e.removeEventListener(u,p),i()},p=t=>{t.target===e&&++c>=a&&f()};setTimeout((()=>{c(n[e]||"").split(", "),o=r(`${Ws}Delay`),i=r(`${Ws}Duration`),s=al(o,i),l=r(`${Ks}Delay`),a=r(`${Ks}Duration`),u=al(l,a);let c=null,f=0,p=0;t===Ws?s>0&&(c=Ws,f=s,p=i.length):t===Ks?u>0&&(c=Ks,f=u,p=a.length):(f=Math.max(s,u),c=f>0?s>u?Ws:Ks:null,p=c?c===Ws?i.length:a.length:0);return{type:c,timeout:f,propCount:p,hasTransform:c===Ws&&/\b(transform|all)(,|$)/.test(r(`${Ws}Property`).toString())}}function al(e,t){for(;e.lengthul(t)+ul(e[n]))))}function ul(e){return"auto"===e?0:1e3*Number(e.slice(0,-1).replace(",","."))}function cl(){return document.body.offsetHeight}const fl=Symbol("_vod"),pl={beforeMount(e,{value:t},{transition:n}){e[fl]="none"===e.style.display?"":e.style.display,n&&t?n.beforeEnter(e):dl(e,t)},mounted(e,{value:t},{transition:n}){n&&t&&n.enter(e)},updated(e,{value:t,oldValue:n},{transition:r}){!t!=!n&&(r?t?(r.beforeEnter(e),dl(e,!0),r.enter(e)):r.leave(e,(()=>{dl(e,!1)})):dl(e,t))},beforeUnmount(e,{value:t}){dl(e,t)}};function dl(e,t){e.style.display=t?e[fl]:"none"}const hl=/\s*!important$/;function vl(e,t,n){if(m(n))n.forEach((n=>vl(e,t,n)));else if(null==n&&(n=""),t.startsWith("--"))e.setProperty(t,n);else{const r=function(e,t){const n=ml[t];if(n)return n;let r=F(t);if("filter"!==r&&r in e)return ml[t]=r;r=D(r);for(let n=0;n{if(e._vts){if(e._vts<=n.attached)return}else e._vts=Date.now();an(function(e,t){if(m(t)){const n=e.stopImmediatePropagation;return e.stopImmediatePropagation=()=>{n.call(e),e._stopped=!0},t.map((e=>t=>!t._stopped&&e&&e(t)))}return t}(e,n.value),t,5,[e])};return n.value=e,n.attached=kl(),n}(r,o);bl(e,n,s,l)}else s&&(!function(e,t,n,r){e.removeEventListener(t,n,r)}(e,n,s,l),i[t]=void 0)}}const xl=/(?:Once|Passive|Capture)$/;let Sl=0;const Ol=Promise.resolve(),kl=()=>Sl||(Ol.then((()=>Sl=0)),Sl=Date.now());const El=/^on[a-z]/;function Cl(e,t){const n=Rr(e);class r extends Al{constructor(e){super(n,e,t)}}return r.def=n,r}const Pl=e=>Cl(e,ga),Tl="undefined"!=typeof HTMLElement?HTMLElement:class{};class Al extends Tl{constructor(e,t={},n){super(),this._def=e,this._props=t,this._instance=null,this._connected=!1,this._resolved=!1,this._numberProps=null,this._ob=null,this.shadowRoot&&n?n(this._createVNode(),this.shadowRoot):(this.attachShadow({mode:"open"}),this._def.__asyncLoader||this._resolveProps(this._def))}connectedCallback(){this._connected=!0,this._instance||(this._resolved?this._update():this._resolveDef())}disconnectedCallback(){this._connected=!1,this._ob&&(this._ob.disconnect(),this._ob=null),bn((()=>{this._connected||(va(null,this.shadowRoot),this._instance=null)}))}_resolveDef(){this._resolved=!0;for(let e=0;e{for(const t of e)this._setAttr(t.attributeName)})),this._ob.observe(this,{attributes:!0});const e=(e,t=!1)=>{const{props:n,styles:r}=e;let o;if(n&&!m(n))for(const e in n){const t=n[e];(t===Number||t&&t.type===Number)&&(e in this._props&&(this._props[e]=z(this._props[e])),(o||(o=Object.create(null)))[F(e)]=!0)}this._numberProps=o,t&&this._resolveProps(e),this._applyStyles(r),this._update()},t=this._def.__asyncLoader;t?t().then((t=>e(t,!0))):e(this._def)}_resolveProps(e){const{props:t}=e,n=m(t)?t:Object.keys(t||{});for(const e of Object.keys(this))"_"!==e[0]&&n.includes(e)&&this._setProp(e,this[e],!0,!1);for(const e of n.map(F))Object.defineProperty(this,e,{get(){return this._getProp(e)},set(t){this._setProp(e,t)}})}_setAttr(e){let t=this.getAttribute(e);const n=F(e);this._numberProps&&this._numberProps[n]&&(t=z(t)),this._setProp(n,t,!1)}_getProp(e){return this._props[e]}_setProp(e,t,n=!0,r=!0){t!==this._props[e]&&(this._props[e]=t,r&&this._instance&&this._update(),n&&(!0===t?this.setAttribute(M(e),""):"string"==typeof t||"number"==typeof t?this.setAttribute(M(e),t+""):t||this.removeAttribute(M(e))))}_update(){va(this._createVNode(),this.shadowRoot)}_createVNode(){const e=Qi(this._def,d({},this._props));return this._instance||(e.ce=e=>{this._instance=e,e.isCE=!0;const t=(e,t)=>{this.dispatchEvent(new CustomEvent(e,{detail:t}))};e.emit=(e,...n)=>{t(e,n),M(e)!==e&&t(M(e),n)};let n=this;for(;n=n&&(n.parentNode||n.host);)if(n instanceof Al){e.parent=n._instance,e.provides=n._instance.provides;break}}),e}_applyStyles(e){e&&e.forEach((e=>{const t=document.createElement("style");t.textContent=e,this.shadowRoot.appendChild(t)}))}}function jl(e="$style"){{const t=ds();if(!t)return s;const n=t.type.__cssModules;if(!n)return s;const r=n[e];return r||s}}function Ll(e){const t=ds();if(!t)return;const n=t.ut=(n=e(t.proxy))=>{Array.from(document.querySelectorAll(`[data-v-owner="${t.uid}"]`)).forEach((e=>Il(e,n)))},r=()=>{const r=e(t.proxy);Rl(t.subTree,r),n(r)};cr(r),Jr((()=>{const e=new MutationObserver(r);e.observe(t.subTree.el.parentNode,{childList:!0}),eo((()=>e.disconnect()))}))}function Rl(e,t){if(128&e.shapeFlag){const n=e.suspense;e=n.activeBranch,n.pendingBranch&&!n.isHydrating&&n.effects.push((()=>{Rl(n.activeBranch,t)}))}for(;e.component;)e=e.component.subTree;if(1&e.shapeFlag&&e.el)Il(e.el,t);else if(e.type===Ai)e.children.forEach((e=>Rl(e,t)));else if(e.type===Ri){let{el:n,anchor:r}=e;for(;n&&(Il(n,t),n!==r);)n=n.nextSibling}}function Il(e,t){if(1===e.nodeType){const n=e.style;for(const e in t)n.setProperty(`--${e}`,t[e])}}const Fl=new WeakMap,Nl=new WeakMap,Ml=Symbol("_moveCb"),Dl=Symbol("_enterCb"),Bl={name:"TransitionGroup",props:d({},Qs,{tag:String,moveClass:String}),setup(e,{slots:t}){const n=ds(),r=xr();let o,i;return Zr((()=>{if(!o.length)return;const t=e.moveClass||`${e.name||"v"}-move`;if(!function(e,t,n){const r=e.cloneNode(),o=e[Gs];o&&o.forEach((e=>{e.split(/\s+/).forEach((e=>e&&r.classList.remove(e)))}));n.split(/\s+/).forEach((e=>e&&r.classList.add(e))),r.style.display="none";const i=1===t.nodeType?t:t.parentNode;i.appendChild(r);const{hasTransform:s}=ll(r);return i.removeChild(r),s}(o[0].el,n.vnode.el,t))return;o.forEach($l),o.forEach(Vl);const r=o.filter(Hl);cl(),r.forEach((e=>{const n=e.el,r=n.style;nl(n,t),r.transform=r.webkitTransform=r.transitionDuration="";const o=n[Ml]=e=>{e&&e.target!==n||e&&!/transform$/.test(e.propertyName)||(n.removeEventListener("transitionend",o),n[Ml]=null,rl(n,t))};n.addEventListener("transitionend",o)}))})),()=>{const s=It(e),l=el(s);let a=s.tag||Ai;o=i,i=t.default?Lr(t.default()):[];for(let e=0;e{const t=e.props["onUpdate:modelValue"]||!1;return m(t)?e=>$(t,e):t};function ql(e){e.target.composing=!0}function Wl(e){const t=e.target;t.composing&&(t.composing=!1,t.dispatchEvent(new Event("input")))}const Kl=Symbol("_assign"),Gl={created(e,{modifiers:{lazy:t,trim:n,number:r}},o){e[Kl]=zl(o);const i=r||o.props&&"number"===o.props.type;bl(e,t?"change":"input",(t=>{if(t.target.composing)return;let r=e.value;n&&(r=r.trim()),i&&(r=H(r)),e[Kl](r)})),n&&bl(e,"change",(()=>{e.value=e.value.trim()})),t||(bl(e,"compositionstart",ql),bl(e,"compositionend",Wl),bl(e,"change",Wl))},mounted(e,{value:t}){e.value=null==t?"":t},beforeUpdate(e,{value:t,modifiers:{lazy:n,trim:r,number:o}},i){if(e[Kl]=zl(i),e.composing)return;if(document.activeElement===e&&"range"!==e.type){if(n)return;if(r&&e.value.trim()===t)return;if((o||"number"===e.type)&&H(e.value)===t)return}const s=null==t?"":t;e.value!==s&&(e.value=s)}},Yl={deep:!0,created(e,t,n){e[Kl]=zl(n),bl(e,"change",(()=>{const t=e._modelValue,n=ea(e),r=e.checked,o=e[Kl];if(m(t)){const e=ue(t,n),i=-1!==e;if(r&&!i)o(t.concat(n));else if(!r&&i){const n=[...t];n.splice(e,1),o(n)}}else if(b(t)){const e=new Set(t);r?e.add(n):e.delete(n),o(e)}else o(ta(e,r))}))},mounted:Jl,beforeUpdate(e,t,n){e[Kl]=zl(n),Jl(e,t,n)}};function Jl(e,{value:t,oldValue:n},r){e._modelValue=t,m(t)?e.checked=ue(t,r.props.value)>-1:b(t)?e.checked=t.has(r.props.value):t!==n&&(e.checked=ae(t,ta(e,!0)))}const Ql={created(e,{value:t},n){e.checked=ae(t,n.props.value),e[Kl]=zl(n),bl(e,"change",(()=>{e[Kl](ea(e))}))},beforeUpdate(e,{value:t,oldValue:n},r){e[Kl]=zl(r),t!==n&&(e.checked=ae(t,r.props.value))}},Zl={deep:!0,created(e,{value:t,modifiers:{number:n}},r){const o=b(t);bl(e,"change",(()=>{const t=Array.prototype.filter.call(e.options,(e=>e.selected)).map((e=>n?H(ea(e)):ea(e)));e[Kl](e.multiple?o?new Set(t):t:t[0])})),e[Kl]=zl(r)},mounted(e,{value:t}){Xl(e,t)},beforeUpdate(e,t,n){e[Kl]=zl(n)},updated(e,{value:t}){Xl(e,t)}};function Xl(e,t){const n=e.multiple;if(!n||m(t)||b(t)){for(let r=0,o=e.options.length;r-1:o.selected=t.has(i);else if(ae(ea(o),t))return void(e.selectedIndex!==r&&(e.selectedIndex=r))}n||-1===e.selectedIndex||(e.selectedIndex=-1)}}function ea(e){return"_value"in e?e._value:e.value}function ta(e,t){const n=t?"_trueValue":"_falseValue";return n in e?e[n]:t}const na={created(e,t,n){oa(e,t,n,null,"created")},mounted(e,t,n){oa(e,t,n,null,"mounted")},beforeUpdate(e,t,n,r){oa(e,t,n,r,"beforeUpdate")},updated(e,t,n,r){oa(e,t,n,r,"updated")}};function ra(e,t){switch(e){case"SELECT":return Zl;case"TEXTAREA":return Gl;default:switch(t){case"checkbox":return Yl;case"radio":return Ql;default:return Gl}}}function oa(e,t,n,r,o){const i=ra(e.tagName,n.props&&n.props.type)[o];i&&i(e,t,n,r)}const ia=["ctrl","shift","alt","meta"],sa={stop:e=>e.stopPropagation(),prevent:e=>e.preventDefault(),self:e=>e.target!==e.currentTarget,ctrl:e=>!e.ctrlKey,shift:e=>!e.shiftKey,alt:e=>!e.altKey,meta:e=>!e.metaKey,left:e=>"button"in e&&0!==e.button,middle:e=>"button"in e&&1!==e.button,right:e=>"button"in e&&2!==e.button,exact:(e,t)=>ia.some((n=>e[`${n}Key`]&&!t.includes(n)))},la=(e,t)=>(n,...r)=>{for(let e=0;en=>{if(!("key"in n))return;const r=M(n.key);return t.some((e=>e===r||aa[e]===r))?e(n):void 0},ca=d({patchProp:(e,t,n,r,o=!1,i,s,l,a)=>{"class"===t?function(e,t,n){const r=e[Gs];r&&(t=(t?[t,...r]:[...r]).join(" ")),null==t?e.removeAttribute("class"):n?e.setAttribute("class",t):e.className=t}(e,r,o):"style"===t?function(e,t,n){const r=e.style,o=x(n);if(n&&!o){if(t&&!x(t))for(const e in t)null==n[e]&&vl(r,e,"");for(const e in n)vl(r,e,n[e])}else{const i=r.display;o?t!==n&&(r.cssText=n):t&&e.removeAttribute("style"),fl in e&&(r.display=i)}}(e,n,r):f(t)?p(t)||_l(e,t,0,r,s):("."===t[0]?(t=t.slice(1),1):"^"===t[0]?(t=t.slice(1),0):function(e,t,n,r){if(r)return"innerHTML"===t||"textContent"===t||!!(t in e&&El.test(t)&&_(n));if("spellcheck"===t||"draggable"===t||"translate"===t)return!1;if("form"===t)return!1;if("list"===t&&"INPUT"===e.tagName)return!1;if("type"===t&&"TEXTAREA"===e.tagName)return!1;if(El.test(t)&&x(n))return!1;return t in e}(e,t,r,o))?function(e,t,n,r,o,i,s){if("innerHTML"===t||"textContent"===t)return r&&s(r,o,i),void(e[t]=null==n?"":n);const l=e.tagName;if("value"===t&&"PROGRESS"!==l&&!l.includes("-")){e._value=n;const r=null==n?"":n;return("OPTION"===l?e.getAttribute("value"):e.value)!==r&&(e.value=r),void(null==n&&e.removeAttribute(t))}let a=!1;if(""===n||null==n){const r=typeof e[t];"boolean"===r?n=le(n):null==n&&"string"===r?(n="",a=!0):"number"===r&&(n=0,a=!0)}try{e[t]=n}catch(e){}a&&e.removeAttribute(t)}(e,t,r,i,s,l,a):("true-value"===t?e._trueValue=r:"false-value"===t&&(e._falseValue=r),function(e,t,n,r,o){if(r&&t.startsWith("xlink:"))null==n?e.removeAttributeNS(yl,t.slice(6,t.length)):e.setAttributeNS(yl,t,n);else{const r=se(t);null==n||r&&!le(n)?e.removeAttribute(t):e.setAttribute(t,r?"":n)}}(e,t,r,o))}},qs);let fa,pa=!1;function da(){return fa||(fa=mi(ca))}function ha(){return fa=pa?fa:yi(ca),pa=!0,fa}const va=(...e)=>{da().render(...e)},ga=(...e)=>{ha().hydrate(...e)},ma=(...e)=>{const t=da().createApp(...e);const{mount:n}=t;return t.mount=e=>{const r=ba(e);if(!r)return;const o=t._component;_(o)||o.render||o.template||(o.template=r.innerHTML),r.innerHTML="";const i=n(r,!1,r instanceof SVGElement);return r instanceof Element&&(r.removeAttribute("v-cloak"),r.setAttribute("data-v-app","")),i},t},ya=(...e)=>{const t=ha().createApp(...e);const{mount:n}=t;return t.mount=e=>{const t=ba(e);if(t)return n(t,!0,t instanceof SVGElement)},t};function ba(e){if(x(e)){return document.querySelector(e)}return e}let wa=!1;const _a=()=>{wa||(wa=!0,Gl.getSSRProps=({value:e})=>({value:e}),Ql.getSSRProps=({value:e},t)=>{if(t.props&&ae(t.props.value,e))return{checked:!0}},Yl.getSSRProps=({value:e},t)=>{if(m(e)){if(t.props&&ue(e,t.props.value)>-1)return{checked:!0}}else if(b(e)){if(t.props&&e.has(t.props.value))return{checked:!0}}else if(e)return{checked:!0}},na.getSSRProps=(e,t)=>{if("string"!=typeof t.type)return;const n=ra(t.type.toUpperCase(),t.props&&t.props.type);return n.getSSRProps?n.getSSRProps(e,t):void 0},pl.getSSRProps=({value:e})=>{if(!e)return{style:{display:"none"}}})};function xa(e){throw e}function Sa(e){}function Oa(e,t,n,r){const o=new SyntaxError(String(e));return o.code=e,o.loc=t,o}const ka=Symbol(""),Ea=Symbol(""),Ca=Symbol(""),Pa=Symbol(""),Ta=Symbol(""),Aa=Symbol(""),ja=Symbol(""),La=Symbol(""),Ra=Symbol(""),Ia=Symbol(""),Fa=Symbol(""),Na=Symbol(""),Ma=Symbol(""),Da=Symbol(""),Ba=Symbol(""),Ua=Symbol(""),$a=Symbol(""),Va=Symbol(""),Ha=Symbol(""),za=Symbol(""),qa=Symbol(""),Wa=Symbol(""),Ka=Symbol(""),Ga=Symbol(""),Ya=Symbol(""),Ja=Symbol(""),Qa=Symbol(""),Za=Symbol(""),Xa=Symbol(""),eu=Symbol(""),tu=Symbol(""),nu=Symbol(""),ru=Symbol(""),ou=Symbol(""),iu=Symbol(""),su=Symbol(""),lu=Symbol(""),au=Symbol(""),uu=Symbol(""),cu={[ka]:"Fragment",[Ea]:"Teleport",[Ca]:"Suspense",[Pa]:"KeepAlive",[Ta]:"BaseTransition",[Aa]:"openBlock",[ja]:"createBlock",[La]:"createElementBlock",[Ra]:"createVNode",[Ia]:"createElementVNode",[Fa]:"createCommentVNode",[Na]:"createTextVNode",[Ma]:"createStaticVNode",[Da]:"resolveComponent",[Ba]:"resolveDynamicComponent",[Ua]:"resolveDirective",[$a]:"resolveFilter",[Va]:"withDirectives",[Ha]:"renderList",[za]:"renderSlot",[qa]:"createSlots",[Wa]:"toDisplayString",[Ka]:"mergeProps",[Ga]:"normalizeClass",[Ya]:"normalizeStyle",[Ja]:"normalizeProps",[Qa]:"guardReactiveProps",[Za]:"toHandlers",[Xa]:"camelize",[eu]:"capitalize",[tu]:"toHandlerKey",[nu]:"setBlockTracking",[ru]:"pushScopeId",[ou]:"popScopeId",[iu]:"withCtx",[su]:"unref",[lu]:"isRef",[au]:"withMemo",[uu]:"isMemoSame"};const fu={source:"",start:{line:1,column:1,offset:0},end:{line:1,column:1,offset:0}};function pu(e,t,n,r,o,i,s,l=!1,a=!1,u=!1,c=fu){return e&&(l?(e.helper(Aa),e.helper(xu(e.inSSR,u))):e.helper(_u(e.inSSR,u)),s&&e.helper(Va)),{type:13,tag:t,props:n,children:r,patchFlag:o,dynamicProps:i,directives:s,isBlock:l,disableTracking:a,isComponent:u,loc:c}}function du(e,t=fu){return{type:17,loc:t,elements:e}}function hu(e,t=fu){return{type:15,loc:t,properties:e}}function vu(e,t){return{type:16,loc:fu,key:x(e)?gu(e,!0):e,value:t}}function gu(e,t=!1,n=fu,r=0){return{type:4,loc:n,content:e,isStatic:t,constType:t?3:r}}function mu(e,t=fu){return{type:8,loc:t,children:e}}function yu(e,t=[],n=fu){return{type:14,loc:n,callee:e,arguments:t}}function bu(e,t=void 0,n=!1,r=!1,o=fu){return{type:18,params:e,returns:t,newline:n,isSlot:r,loc:o}}function wu(e,t,n,r=!0){return{type:19,test:e,consequent:t,alternate:n,newline:r,loc:fu}}function _u(e,t){return e||t?Ra:Ia}function xu(e,t){return e||t?ja:La}function Su(e,{helper:t,removeHelper:n,inSSR:r}){e.isBlock||(e.isBlock=!0,n(_u(r,e.isComponent)),t(Aa),t(xu(r,e.isComponent)))}const Ou=e=>4===e.type&&e.isStatic,ku=(e,t)=>e===t||e===M(t);function Eu(e){return ku(e,"Teleport")?Ea:ku(e,"Suspense")?Ca:ku(e,"KeepAlive")?Pa:ku(e,"BaseTransition")?Ta:void 0}const Cu=/^\d|[^\$\w]/,Pu=e=>!Cu.test(e),Tu=/[A-Za-z_$\xA0-\uFFFF]/,Au=/[\.\?\w$\xA0-\uFFFF]/,ju=/\s+[.[]\s*|\s*[.[]\s+/g,Lu=e=>{e=e.trim().replace(ju,(e=>e.trim()));let t=0,n=[],r=0,o=0,i=null;for(let s=0;s4===e.key.type&&e.key.content===r))}return n}function Ku(e,t){return`_${t}_${e.replace(/[^\w]/g,((t,n)=>"-"===t?"_":e.charCodeAt(n).toString()))}`}function Gu(e,t){const n=t.options?t.options.compatConfig:t.compatConfig,r=n&&n[e];return"MODE"===e?r||3:r}function Yu(e,t){const n=Gu("MODE",t),r=Gu(e,t);return 3===n?!0===r:!1!==r}function Ju(e,t,n,...r){return Yu(e,t)}const Qu=/&(gt|lt|amp|apos|quot);/g,Zu={gt:">",lt:"<",amp:"&",apos:"'",quot:'"'},Xu={delimiters:["{{","}}"],getNamespace:()=>0,getTextMode:()=>0,isVoidTag:u,isPreTag:u,isCustomElement:u,decodeEntities:e=>e.replace(Qu,((e,t)=>Zu[t])),onError:xa,onWarn:Sa,comments:!1};function ec(e,t={}){const n=function(e,t){const n=d({},Xu);let r;for(r in t)n[r]=void 0===t[r]?Xu[r]:t[r];return{options:n,column:1,line:1,offset:0,originalSource:e,source:e,inPre:!1,inVPre:!1,onWarn:n.onWarn}}(e,t),r=hc(n);return function(e,t=fu){return{type:0,children:e,helpers:new Set,components:[],directives:[],hoists:[],imports:[],cached:0,temps:0,codegenNode:void 0,loc:t}}(tc(n,0,[]),vc(n,r))}function tc(e,t,n){const r=gc(n),o=r?r.ns:0,i=[];for(;!xc(e,t,n);){const s=e.source;let l;if(0===t||1===t)if(!e.inVPre&&mc(s,e.options.delimiters[0]))l=fc(e,t);else if(0===t&&"<"===s[0])if(1===s.length)_c(e,5,1);else if("!"===s[1])mc(s,"\x3c!--")?l=oc(e):mc(s,""===s[2]){_c(e,14,2),yc(e,3);continue}if(/[a-z]/i.test(s[2])){_c(e,23),ac(e,1,r);continue}_c(e,12,2),l=ic(e)}else/[a-z]/i.test(s[1])?(l=sc(e,n),Yu("COMPILER_NATIVE_TEMPLATE",e)&&l&&"template"===l.tag&&!l.props.some((e=>7===e.type&&lc(e.name)))&&(l=l.children)):"?"===s[1]?(_c(e,21,1),l=ic(e)):_c(e,12,1);if(l||(l=pc(e,t)),m(l))for(let e=0;e/.exec(e.source);if(r){r.index<=3&&_c(e,0),r[1]&&_c(e,10),n=e.source.slice(4,r.index);const t=e.source.slice(0,r.index);let o=1,i=0;for(;-1!==(i=t.indexOf("\x3c!--",o));)yc(e,i-o+1),i+4");return-1===o?(r=e.source.slice(n),yc(e,e.source.length)):(r=e.source.slice(n,o),yc(e,o+1)),{type:3,content:r,loc:vc(e,t)}}function sc(e,t){const n=e.inPre,r=e.inVPre,o=gc(t),i=ac(e,0,o),s=e.inPre&&!n,l=e.inVPre&&!r;if(i.isSelfClosing||e.options.isVoidTag(i.tag))return s&&(e.inPre=!1),l&&(e.inVPre=!1),i;t.push(i);const a=e.options.getTextMode(i,o),u=tc(e,a,t);t.pop();{const t=i.props.find((e=>6===e.type&&"inline-template"===e.name));if(t&&Ju("COMPILER_INLINE_TEMPLATE",e,t.loc)){const n=vc(e,i.loc.end);t.value={type:2,content:n.source,loc:n}}}if(i.children=u,Sc(e.source,i.tag))ac(e,1,o);else if(_c(e,24,0,i.loc.start),0===e.source.length&&"script"===i.tag.toLowerCase()){const t=u[0];t&&mc(t.loc.source,"\x3c!--")&&_c(e,8)}return i.loc=vc(e,i.loc.start),s&&(e.inPre=!1),l&&(e.inVPre=!1),i}const lc=i("if,else,else-if,for,slot");function ac(e,t,n){const r=hc(e),o=/^<\/?([a-z][^\t\r\n\f />]*)/i.exec(e.source),i=o[1],s=e.options.getNamespace(i,n);yc(e,o[0].length),bc(e);const l=hc(e),a=e.source;e.options.isPreTag(i)&&(e.inPre=!0);let u=uc(e,t);0===t&&!e.inVPre&&u.some((e=>7===e.type&&"pre"===e.name))&&(e.inVPre=!0,d(e,l),e.source=a,u=uc(e,t).filter((e=>"v-pre"!==e.name)));let c=!1;if(0===e.source.length?_c(e,9):(c=mc(e.source,"/>"),1===t&&c&&_c(e,4),yc(e,c?2:1)),1===t)return;let f=0;return e.inVPre||("slot"===i?f=2:"template"===i?u.some((e=>7===e.type&&lc(e.name)))&&(f=3):function(e,t,n){const r=n.options;if(r.isCustomElement(e))return!1;if("component"===e||/^[A-Z]/.test(e)||Eu(e)||r.isBuiltInComponent&&r.isBuiltInComponent(e)||r.isNativeTag&&!r.isNativeTag(e))return!0;for(let e=0;e0&&!mc(e.source,">")&&!mc(e.source,"/>");){if(mc(e.source,"/")){_c(e,22),yc(e,1),bc(e);continue}1===t&&_c(e,3);const o=cc(e,r);6===o.type&&o.value&&"class"===o.name&&(o.value.content=o.value.content.replace(/\s+/g," ").trim()),0===t&&n.push(o),/^[^\t\r\n\f />]/.test(e.source)&&_c(e,15),bc(e)}return n}function cc(e,t){var n;const r=hc(e),o=/^[^\t\r\n\f />][^\t\r\n\f />=]*/.exec(e.source)[0];t.has(o)&&_c(e,2),t.add(o),"="===o[0]&&_c(e,19);{const t=/["'<]/g;let n;for(;n=t.exec(o);)_c(e,17,n.index)}let i;yc(e,o.length),/^[\t\r\n\f ]*=/.test(e.source)&&(bc(e),yc(e,1),bc(e),i=function(e){const t=hc(e);let n;const r=e.source[0],o='"'===r||"'"===r;if(o){yc(e,1);const t=e.source.indexOf(r);-1===t?n=dc(e,e.source.length,4):(n=dc(e,t,4),yc(e,1))}else{const t=/^[^\t\r\n\f >]+/.exec(e.source);if(!t)return;const r=/["'<=`]/g;let o;for(;o=r.exec(t[0]);)_c(e,18,o.index);n=dc(e,t[0].length,4)}return{content:n,isQuoted:o,loc:vc(e,t)}}(e),i||_c(e,13));const s=vc(e,r);if(!e.inVPre&&/^(v-[A-Za-z0-9-]|:|\.|@|#)/.test(o)){const t=/(?:^v-([a-z0-9-]+))?(?:(?::|^\.|^@|^#)(\[[^\]]+\]|[^\.]+))?(.+)?$/i.exec(o);let l,a=mc(o,"."),u=t[1]||(a||mc(o,":")?"bind":mc(o,"@")?"on":"slot");if(t[2]){const i="slot"===u,s=o.lastIndexOf(t[2],o.length-((null==(n=t[3])?void 0:n.length)||0)),a=vc(e,wc(e,r,s),wc(e,r,s+t[2].length+(i&&t[3]||"").length));let c=t[2],f=!0;c.startsWith("[")?(f=!1,c.endsWith("]")?c=c.slice(1,c.length-1):(_c(e,27),c=c.slice(1))):i&&(c+=t[3]||""),l={type:4,content:c,isStatic:f,constType:f?3:0,loc:a}}if(i&&i.isQuoted){const e=i.loc;e.start.offset++,e.start.column++,e.end=Iu(e.start,i.content),e.source=e.source.slice(1,-1)}const c=t[3]?t[3].slice(1).split("."):[];return a&&c.push("prop"),"bind"===u&&l&&c.includes("sync")&&Ju("COMPILER_V_BIND_SYNC",e,0,l.loc.source)&&(u="model",c.splice(c.indexOf("sync"),1)),{type:7,name:u,exp:i&&{type:4,content:i.content,isStatic:!1,constType:0,loc:i.loc},arg:l,modifiers:c,loc:s}}return!e.inVPre&&mc(o,"v-")&&_c(e,26),{type:6,name:o,value:i&&{type:2,content:i.content,loc:i.loc},loc:s}}function fc(e,t){const[n,r]=e.options.delimiters,o=e.source.indexOf(r,n.length);if(-1===o)return void _c(e,25);const i=hc(e);yc(e,n.length);const s=hc(e),l=hc(e),a=o-n.length,u=e.source.slice(0,a),c=dc(e,a,t),f=c.trim(),p=c.indexOf(f);p>0&&Fu(s,u,p);return Fu(l,u,a-(c.length-f.length-p)),yc(e,r.length),{type:5,content:{type:4,isStatic:!1,constType:0,content:f,loc:vc(e,s,l)},loc:vc(e,i)}}function pc(e,t){const n=3===t?["]]>"]:["<",e.options.delimiters[0]];let r=e.source.length;for(let t=0;to&&(r=o)}const o=hc(e);return{type:2,content:dc(e,r,t),loc:vc(e,o)}}function dc(e,t,n){const r=e.source.slice(0,t);return yc(e,t),2!==n&&3!==n&&r.includes("&")?e.options.decodeEntities(r,4===n):r}function hc(e){const{column:t,line:n,offset:r}=e;return{column:t,line:n,offset:r}}function vc(e,t,n){return{start:t,end:n=n||hc(e),source:e.originalSource.slice(t.offset,n.offset)}}function gc(e){return e[e.length-1]}function mc(e,t){return e.startsWith(t)}function yc(e,t){const{source:n}=e;Fu(e,n,t),e.source=n.slice(t)}function bc(e){const t=/^[\t\r\n\f ]+/.exec(e.source);t&&yc(e,t[0].length)}function wc(e,t,n){return Iu(t,e.originalSource.slice(t.offset,n),n)}function _c(e,t,n,r=hc(e)){n&&(r.offset+=n,r.column+=n),e.options.onError(Oa(t,{start:r,end:r,source:""}))}function xc(e,t,n){const r=e.source;switch(t){case 0:if(mc(r,"=0;--e)if(Sc(r,n[e].tag))return!0;break;case 1:case 2:{const e=gc(n);if(e&&Sc(r,e.tag))return!0;break}case 3:if(mc(r,"]]>"))return!0}return!r}function Sc(e,t){return mc(e,"]/.test(e[2+t.length]||">")}function Oc(e,t){Ec(e,t,kc(e,e.children[0]))}function kc(e,t){const{children:n}=e;return 1===n.length&&1===t.type&&!Vu(t)}function Ec(e,t,n=!1){const{children:r}=e,o=r.length;let i=0;for(let e=0;e0){if(e>=2){o.codegenNode.patchFlag="-1",o.codegenNode=t.hoist(o.codegenNode),i++;continue}}else{const e=o.codegenNode;if(13===e.type){const n=Lc(e);if((!n||512===n||1===n)&&Ac(o,t)>=2){const n=jc(o);n&&(e.props=t.hoist(n))}e.dynamicProps&&(e.dynamicProps=t.hoist(e.dynamicProps))}}}if(1===o.type){const e=1===o.tagType;e&&t.scopes.vSlot++,Ec(o,t),e&&t.scopes.vSlot--}else if(11===o.type)Ec(o,t,1===o.children.length);else if(9===o.type)for(let e=0;e1)for(let o=0;o`_${cu[C.helper(e)]}`,replaceNode(e){C.parent.children[C.childIndex]=C.currentNode=e},removeNode(e){const t=C.parent.children,n=e?t.indexOf(e):C.currentNode?C.childIndex:-1;e&&e!==C.currentNode?C.childIndex>n&&(C.childIndex--,C.onNodeRemoved()):(C.currentNode=null,C.onNodeRemoved()),C.parent.children.splice(n,1)},onNodeRemoved:()=>{},addIdentifiers(e){},removeIdentifiers(e){},hoist(e){x(e)&&(e=gu(e)),C.hoists.push(e);const t=gu(`_hoisted_${C.hoists.length}`,!1,e.loc,2);return t.hoisted=e,t},cache:(e,t=!1)=>function(e,t,n=!1){return{type:20,index:e,value:t,isVNode:n,loc:fu}}(C.cached++,e,t)};return C.filters=new Set,C}function Ic(e,t){const n=Rc(e,t);Fc(e,n),t.hoistStatic&&Oc(e,n),t.ssr||function(e,t){const{helper:n}=t,{children:r}=e;if(1===r.length){const n=r[0];if(kc(e,n)&&n.codegenNode){const r=n.codegenNode;13===r.type&&Su(r,t),e.codegenNode=r}else e.codegenNode=n}else if(r.length>1){let r=64;K[64];0,e.codegenNode=pu(t,n(ka),void 0,e.children,r+"",void 0,void 0,!0,void 0,!1)}}(e,n),e.helpers=new Set([...n.helpers.keys()]),e.components=[...n.components],e.directives=[...n.directives],e.imports=n.imports,e.hoists=n.hoists,e.temps=n.temps,e.cached=n.cached,e.filters=[...n.filters]}function Fc(e,t){t.currentNode=e;const{nodeTransforms:n}=t,r=[];for(let o=0;o{n--};for(;nt===e:t=>e.test(t);return(e,r)=>{if(1===e.type){const{props:o}=e;if(3===e.tagType&&o.some(Uu))return;const i=[];for(let s=0;s`${cu[e]}: _${cu[e]}`;function Bc(e,{mode:t="function",prefixIdentifiers:n="module"===t,sourceMap:r=!1,filename:o="template.vue.html",scopeId:i=null,optimizeImports:s=!1,runtimeGlobalName:l="Vue",runtimeModuleName:a="vue",ssrRuntimeModuleName:u="vue/server-renderer",ssr:c=!1,isTS:f=!1,inSSR:p=!1}){const d={mode:t,prefixIdentifiers:n,sourceMap:r,filename:o,scopeId:i,optimizeImports:s,runtimeGlobalName:l,runtimeModuleName:a,ssrRuntimeModuleName:u,ssr:c,isTS:f,inSSR:p,source:e.loc.source,code:"",column:1,line:1,offset:0,indentLevel:0,pure:!1,map:void 0,helper:e=>`_${cu[e]}`,push(e,t){d.code+=e},indent(){h(++d.indentLevel)},deindent(e=!1){e?--d.indentLevel:h(--d.indentLevel)},newline(){h(d.indentLevel)}};function h(e){d.push("\n"+" ".repeat(e))}return d}function Uc(e,t={}){const n=Bc(e,t);t.onContextCreated&&t.onContextCreated(n);const{mode:r,push:o,prefixIdentifiers:i,indent:s,deindent:l,newline:a,scopeId:u,ssr:c}=n,f=Array.from(e.helpers),p=f.length>0,d=!i&&"module"!==r,h=n;!function(e,t){const{ssr:n,prefixIdentifiers:r,push:o,newline:i,runtimeModuleName:s,runtimeGlobalName:l,ssrRuntimeModuleName:a}=t,u=l,c=Array.from(e.helpers);if(c.length>0&&(o(`const _Vue = ${u}\n`),e.hoists.length)){o(`const { ${[Ra,Ia,Fa,Na,Ma].filter((e=>c.includes(e))).map(Dc).join(", ")} } = _Vue\n`)}(function(e,t){if(!e.length)return;t.pure=!0;const{push:n,newline:r,helper:o,scopeId:i,mode:s}=t;r();for(let o=0;o0)&&a()),e.directives.length&&($c(e.directives,"directive",n),e.temps>0&&a()),e.filters&&e.filters.length&&(a(),$c(e.filters,"filter",n),a()),e.temps>0){o("let ");for(let t=0;t0?", ":""}_temp${t}`)}return(e.components.length||e.directives.length||e.temps)&&(o("\n"),a()),c||o("return "),e.codegenNode?zc(e.codegenNode,n):o("null"),d&&(l(),o("}")),l(),o("}"),{ast:e,code:n.code,preamble:"",map:n.map?n.map.toJSON():void 0}}function $c(e,t,{helper:n,push:r,newline:o,isTS:i}){const s=n("filter"===t?$a:"component"===t?Da:Ua);for(let n=0;n3||!1;t.push("["),n&&t.indent(),Hc(e,t,n),n&&t.deindent(),t.push("]")}function Hc(e,t,n=!1,r=!0){const{push:o,newline:i}=t;for(let s=0;se||"null"))}([i,s,l,a,u]),t),n(")"),f&&n(")");c&&(n(", "),zc(c,t),n(")"))}(e,t);break;case 14:!function(e,t){const{push:n,helper:r,pure:o}=t,i=x(e.callee)?e.callee:r(e.callee);o&&n(Mc);n(i+"(",e),Hc(e.arguments,t),n(")")}(e,t);break;case 15:!function(e,t){const{push:n,indent:r,deindent:o,newline:i}=t,{properties:s}=e;if(!s.length)return void n("{}",e);const l=s.length>1||!1;n(l?"{":"{ "),l&&r();for(let e=0;e "),(a||l)&&(n("{"),r());s?(a&&n("return "),m(s)?Vc(s,t):zc(s,t)):l&&zc(l,t);(a||l)&&(o(),n("}"));u&&(e.isNonScopedSlot&&n(", undefined, true"),n(")"))}(e,t);break;case 19:!function(e,t){const{test:n,consequent:r,alternate:o,newline:i}=e,{push:s,indent:l,deindent:a,newline:u}=t;if(4===n.type){const e=!Pu(n.content);e&&s("("),qc(n,t),e&&s(")")}else s("("),zc(n,t),s(")");i&&l(),t.indentLevel++,i||s(" "),s("? "),zc(r,t),t.indentLevel--,i&&u(),i||s(" "),s(": ");const c=19===o.type;c||t.indentLevel++;zc(o,t),c||t.indentLevel--;i&&a(!0)}(e,t);break;case 20:!function(e,t){const{push:n,helper:r,indent:o,deindent:i,newline:s}=t;n(`_cache[${e.index}] || (`),e.isVNode&&(o(),n(`${r(nu)}(-1),`),s());n(`_cache[${e.index}] = `),zc(e.value,t),e.isVNode&&(n(","),s(),n(`${r(nu)}(1),`),s(),n(`_cache[${e.index}]`),i());n(")")}(e,t);break;case 21:Hc(e.body,t,!0,!1)}}function qc(e,t){const{content:n,isStatic:r}=e;t.push(r?JSON.stringify(n):n,e)}function Wc(e,t){for(let n=0;nfunction(e,t,n,r){if(!("else"===t.name||t.exp&&t.exp.content.trim())){const r=t.exp?t.exp.loc:e.loc;n.onError(Oa(28,t.loc)),t.exp=gu("true",!1,r)}0;if("if"===t.name){const o=Yc(e,t),i={type:9,loc:e.loc,branches:[o]};if(n.replaceNode(i),r)return r(i,o,!0)}else{const o=n.parent.children;let i=o.indexOf(e);for(;i-- >=-1;){const s=o[i];if(s&&3===s.type)n.removeNode(s);else{if(!s||2!==s.type||s.content.trim().length){if(s&&9===s.type){"else-if"===t.name&&void 0===s.branches[s.branches.length-1].condition&&n.onError(Oa(30,e.loc)),n.removeNode();const o=Yc(e,t);0,s.branches.push(o);const i=r&&r(s,o,!1);Fc(o,n),i&&i(),n.currentNode=null}else n.onError(Oa(30,e.loc));break}n.removeNode(s)}}}}(e,t,n,((e,t,r)=>{const o=n.parent.children;let i=o.indexOf(e),s=0;for(;i-- >=0;){const e=o[i];e&&9===e.type&&(s+=e.branches.length)}return()=>{if(r)e.codegenNode=Jc(t,s,n);else{const r=function(e){for(;;)if(19===e.type){if(19!==e.alternate.type)return e;e=e.alternate}else 20===e.type&&(e=e.value)}(e.codegenNode);r.alternate=Jc(t,s+e.branches.length-1,n)}}}))));function Yc(e,t){const n=3===e.tagType;return{type:10,loc:e.loc,condition:"else"===t.name?void 0:t.exp,children:n&&!Nu(e,"for")?e.children:[e],userKey:Mu(e,"key"),isTemplateIf:n}}function Jc(e,t,n){return e.condition?wu(e.condition,Qc(e,t,n),yu(n.helper(Fa),['""',"true"])):Qc(e,t,n)}function Qc(e,t,n){const{helper:r}=n,o=vu("key",gu(`${t}`,!1,fu,2)),{children:i}=e,s=i[0];if(1!==i.length||1!==s.type){if(1===i.length&&11===s.type){const e=s.codegenNode;return qu(e,o,n),e}{let t=64;K[64];return pu(n,r(ka),hu([o]),i,t+"",void 0,void 0,!0,!1,!1,e.loc)}}{const e=s.codegenNode,t=14===(l=e).type&&l.callee===au?l.arguments[1].returns:l;return 13===t.type&&Su(t,n),qu(t,o,n),e}var l}const Zc=Nc("for",((e,t,n)=>{const{helper:r,removeHelper:o}=n;return function(e,t,n,r){if(!t.exp)return void n.onError(Oa(31,t.loc));const o=nf(t.exp,n);if(!o)return void n.onError(Oa(32,t.loc));const{addIdentifiers:i,removeIdentifiers:s,scopes:l}=n,{source:a,value:u,key:c,index:f}=o,p={type:11,loc:t.loc,source:a,valueAlias:u,keyAlias:c,objectIndexAlias:f,parseResult:o,children:$u(e)?e.children:[e]};n.replaceNode(p),l.vFor++;const d=r&&r(p);return()=>{l.vFor--,d&&d()}}(e,t,n,(t=>{const i=yu(r(Ha),[t.source]),s=$u(e),l=Nu(e,"memo"),a=Mu(e,"key"),u=a&&(6===a.type?gu(a.value.content,!0):a.exp),c=a?vu("key",u):null,f=4===t.source.type&&t.source.constType>0,p=f?64:a?128:256;return t.codegenNode=pu(n,r(ka),void 0,i,p+"",void 0,void 0,!0,!f,!1,e.loc),()=>{let a;const{children:p}=t;const d=1!==p.length||1!==p[0].type,h=Vu(e)?e:s&&1===e.children.length&&Vu(e.children[0])?e.children[0]:null;if(h?(a=h.codegenNode,s&&c&&qu(a,c,n)):d?a=pu(n,r(ka),c?hu([c]):void 0,e.children,"64",void 0,void 0,!0,void 0,!1):(a=p[0].codegenNode,s&&c&&qu(a,c,n),a.isBlock!==!f&&(a.isBlock?(o(Aa),o(xu(n.inSSR,a.isComponent))):o(_u(n.inSSR,a.isComponent))),a.isBlock=!f,a.isBlock?(r(Aa),r(xu(n.inSSR,a.isComponent))):r(_u(n.inSSR,a.isComponent))),l){const e=bu(of(t.parseResult,[gu("_cached")]));e.body={type:21,body:[mu(["const _memo = (",l.exp,")"]),mu(["if (_cached",...u?[" && _cached.key === ",u]:[],` && ${n.helperString(uu)}(_cached, _memo)) return _cached`]),mu(["const _item = ",a]),gu("_item.memo = _memo"),gu("return _item")],loc:fu},i.arguments.push(e,gu("_cache"),gu(String(n.cached++)))}else i.arguments.push(bu(of(t.parseResult),a,!0))}}))}));const Xc=/([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/,ef=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/,tf=/^\(|\)$/g;function nf(e,t){const n=e.loc,r=e.content,o=r.match(Xc);if(!o)return;const[,i,s]=o,l={source:rf(n,s.trim(),r.indexOf(s,i.length)),value:void 0,key:void 0,index:void 0};let a=i.trim().replace(tf,"").trim();const u=i.indexOf(a),c=a.match(ef);if(c){a=a.replace(ef,"").trim();const e=c[1].trim();let t;if(e&&(t=r.indexOf(e,u+a.length),l.key=rf(n,e,t)),c[2]){const o=c[2].trim();o&&(l.index=rf(n,o,r.indexOf(o,l.key?t+e.length:u+a.length)))}}return a&&(l.value=rf(n,a,u)),l}function rf(e,t,n){return gu(t,!1,Ru(e,n,t.length))}function of({value:e,key:t,index:n},r=[]){return function(e){let t=e.length;for(;t--&&!e[t];);return e.slice(0,t+1).map(((e,t)=>e||gu("_".repeat(t+1),!1)))}([e,t,n,...r])}const sf=gu("undefined",!1),lf=(e,t)=>{if(1===e.type&&(1===e.tagType||3===e.tagType)){const n=Nu(e,"slot");if(n)return n.exp,t.scopes.vSlot++,()=>{t.scopes.vSlot--}}},af=(e,t,n,r)=>bu(e,n,!1,!0,n.length?n[0].loc:r);function uf(e,t,n=af){t.helper(iu);const{children:r,loc:o}=e,i=[],s=[];let l=t.scopes.vSlot>0||t.scopes.vFor>0;const a=Nu(e,"slot",!0);if(a){const{arg:e,exp:t}=a;e&&!Ou(e)&&(l=!0),i.push(vu(e||gu("default",!0),n(t,void 0,r,o)))}let u=!1,c=!1;const f=[],p=new Set;let d=0;for(let e=0;e{const i=n(e,void 0,r,o);return t.compatConfig&&(i.isNonScopedSlot=!0),vu("default",i)};u?f.length&&f.some((e=>pf(e)))&&(c?t.onError(Oa(39,f[0].loc)):i.push(e(void 0,f))):i.push(e(void 0,r))}const h=l?2:ff(e.children)?3:1;let v=hu(i.concat(vu("_",gu(h+"",!1))),o);return s.length&&(v=yu(t.helper(qa),[v,du(s)])),{slots:v,hasDynamicSlots:l}}function cf(e,t,n){const r=[vu("name",e),vu("fn",t)];return null!=n&&r.push(vu("key",gu(String(n),!0))),hu(r)}function ff(e){for(let t=0;tfunction(){if(1!==(e=t.currentNode).type||0!==e.tagType&&1!==e.tagType)return;const{tag:n,props:r}=e,o=1===e.tagType;let i=o?function(e,t,n=!1){let{tag:r}=e;const o=yf(r),i=Mu(e,"is");if(i)if(o||Yu("COMPILER_IS_ON_ELEMENT",t)){const e=6===i.type?i.value&&gu(i.value.content,!0):i.exp;if(e)return yu(t.helper(Ba),[e])}else 6===i.type&&i.value.content.startsWith("vue:")&&(r=i.value.content.slice(4));const s=!o&&Nu(e,"is");if(s&&s.exp)return yu(t.helper(Ba),[s.exp]);const l=Eu(r)||t.isBuiltInComponent(r);if(l)return n||t.helper(l),l;return t.helper(Da),t.components.add(r),Ku(r,"component")}(e,t):`"${n}"`;const s=O(i)&&i.callee===Ba;let l,a,u,c,f,p,d=0,h=s||i===Ea||i===Ca||!o&&("svg"===n||"foreignObject"===n);if(r.length>0){const n=vf(e,t,void 0,o,s);l=n.props,d=n.patchFlag,f=n.dynamicPropNames;const r=n.directives;p=r&&r.length?du(r.map((e=>function(e,t){const n=[],r=df.get(e);r?n.push(t.helperString(r)):(t.helper(Ua),t.directives.add(e.name),n.push(Ku(e.name,"directive")));const{loc:o}=e;e.exp&&n.push(e.exp);e.arg&&(e.exp||n.push("void 0"),n.push(e.arg));if(Object.keys(e.modifiers).length){e.arg||(e.exp||n.push("void 0"),n.push("void 0"));const t=gu("true",!1,o);n.push(hu(e.modifiers.map((e=>vu(e,t))),o))}return du(n,e.loc)}(e,t)))):void 0,n.shouldUseBlock&&(h=!0)}if(e.children.length>0){i===Pa&&(h=!0,d|=1024);if(o&&i!==Ea&&i!==Pa){const{slots:n,hasDynamicSlots:r}=uf(e,t);a=n,r&&(d|=1024)}else if(1===e.children.length&&i!==Ea){const n=e.children[0],r=n.type,o=5===r||8===r;o&&0===Cc(n,t)&&(d|=1),a=o||2===r?n:e.children}else a=e.children}0!==d&&(u=String(d),f&&f.length&&(c=function(e){let t="[";for(let n=0,r=e.length;n0;let h=!1,v=0,g=!1,m=!1,y=!1,b=!1,w=!1,_=!1;const x=[],O=e=>{u.length&&(c.push(hu(gf(u),l)),u=[]),e&&c.push(e)},k=({key:e,value:n})=>{if(Ou(e)){const i=e.content,s=f(i);if(!s||r&&!o||"onclick"===i.toLowerCase()||"onUpdate:modelValue"===i||j(i)||(b=!0),s&&j(i)&&(_=!0),20===n.type||(4===n.type||8===n.type)&&Cc(n,t)>0)return;"ref"===i?g=!0:"class"===i?m=!0:"style"===i?y=!0:"key"===i||x.includes(i)||x.push(i),!r||"class"!==i&&"style"!==i||x.includes(i)||x.push(i)}else w=!0};for(let o=0;o0&&u.push(vu(gu("ref_for",!0),gu("true")))),"is"===n&&(yf(s)||r&&r.content.startsWith("vue:")||Yu("COMPILER_IS_ON_ELEMENT",t)))continue;u.push(vu(gu(n,!0,Ru(e,0,n.length)),gu(r?r.content:"",o,r?r.loc:e)))}else{const{name:n,arg:o,exp:f,loc:v}=a,g="bind"===n,m="on"===n;if("slot"===n){r||t.onError(Oa(40,v));continue}if("once"===n||"memo"===n)continue;if("is"===n||g&&Du(o,"is")&&(yf(s)||Yu("COMPILER_IS_ON_ELEMENT",t)))continue;if(m&&i)continue;if((g&&Du(o,"key")||m&&d&&Du(o,"vue:before-update"))&&(h=!0),g&&Du(o,"ref")&&t.scopes.vFor>0&&u.push(vu(gu("ref_for",!0),gu("true"))),!o&&(g||m)){if(w=!0,f)if(g){if(O(),Yu("COMPILER_V_BIND_OBJECT_ORDER",t)){c.unshift(f);continue}c.push(f)}else O({type:14,loc:v,callee:t.helper(Za),arguments:r?[f]:[f,"true"]});else t.onError(Oa(g?34:35,v));continue}const y=t.directiveTransforms[n];if(y){const{props:n,needRuntime:r}=y(a,e,t);!i&&n.forEach(k),m&&o&&!Ou(o)?O(hu(n,l)):u.push(...n),r&&(p.push(a),S(r)&&df.set(a,r))}else L(n)||(p.push(a),d&&(h=!0))}}let E;if(c.length?(O(),E=c.length>1?yu(t.helper(Ka),c,l):c[0]):u.length&&(E=hu(gf(u),l)),w?v|=16:(m&&!r&&(v|=2),y&&!r&&(v|=4),x.length&&(v|=8),b&&(v|=32)),h||0!==v&&32!==v||!(g||_||p.length>0)||(v|=512),!t.inSSR&&E)switch(E.type){case 15:let e=-1,n=-1,r=!1;for(let t=0;t{if(Vu(e)){const{children:n,loc:r}=e,{slotName:o,slotProps:i}=function(e,t){let n,r='"default"';const o=[];for(let t=0;t0){const{props:r,directives:i}=vf(e,t,o,!1,!1);n=r,i.length&&t.onError(Oa(36,i[0].loc))}return{slotName:r,slotProps:n}}(e,t),s=[t.prefixIdentifiers?"_ctx.$slots":"$slots",o,"{}","undefined","true"];let l=2;i&&(s[2]=i,l=3),n.length&&(s[3]=bu([],n,!1,!1,r),l=4),t.scopeId&&!t.slotted&&(l=5),s.splice(l),e.codegenNode=yu(t.helper(za),s,r)}};const wf=/^\s*([\w$_]+|(async\s*)?\([^)]*?\))\s*(:[^=]+)?=>|^\s*(async\s+)?function(?:\s+[\w$]+)?\s*\(/,_f=(e,t,n,r)=>{const{loc:o,modifiers:i,arg:s}=e;let l;if(e.exp||i.length||n.onError(Oa(35,o)),4===s.type)if(s.isStatic){let e=s.content;0,e.startsWith("vue:")&&(e=`vnode-${e.slice(4)}`);l=gu(0!==t.tagType||e.startsWith("vnode")||!/[A-Z]/.test(e)?B(F(e)):`on:${e}`,!0,s.loc)}else l=mu([`${n.helperString(tu)}(`,s,")"]);else l=s,l.children.unshift(`${n.helperString(tu)}(`),l.children.push(")");let a=e.exp;a&&!a.content.trim()&&(a=void 0);let u=n.cacheHandlers&&!a&&!n.inVOnce;if(a){const e=Lu(a.content),t=!(e||wf.test(a.content)),n=a.content.includes(";");0,(t||u&&e)&&(a=mu([`${t?"$event":"(...args)"} => ${n?"{":"("}`,a,n?"}":")"]))}let c={props:[vu(l,a||gu("() => {}",!1,o))]};return r&&(c=r(c)),u&&(c.props[0].value=n.cache(c.props[0].value)),c.props.forEach((e=>e.key.isHandlerKey=!0)),c},xf=(e,t,n)=>{const{exp:r,modifiers:o,loc:i}=e,s=e.arg;return 4!==s.type?(s.children.unshift("("),s.children.push(') || ""')):s.isStatic||(s.content=`${s.content} || ""`),o.includes("camel")&&(4===s.type?s.isStatic?s.content=F(s.content):s.content=`${n.helperString(Xa)}(${s.content})`:(s.children.unshift(`${n.helperString(Xa)}(`),s.children.push(")"))),n.inSSR||(o.includes("prop")&&Sf(s,"."),o.includes("attr")&&Sf(s,"^")),!r||4===r.type&&!r.content.trim()?(n.onError(Oa(34,i)),{props:[vu(s,gu("",!0,i))]}):{props:[vu(s,r)]}},Sf=(e,t)=>{4===e.type?e.isStatic?e.content=t+e.content:e.content=`\`${t}\${${e.content}}\``:(e.children.unshift(`'${t}' + (`),e.children.push(")"))},Of=(e,t)=>{if(0===e.type||1===e.type||11===e.type||10===e.type)return()=>{const n=e.children;let r,o=!1;for(let e=0;e7===e.type&&!t.directiveTransforms[e.name]))||"template"===e.tag)))for(let e=0;e{if(1===e.type&&Nu(e,"once",!0)){if(kf.has(e)||t.inVOnce||t.inSSR)return;return kf.add(e),t.inVOnce=!0,t.helper(nu),()=>{t.inVOnce=!1;const e=t.currentNode;e.codegenNode&&(e.codegenNode=t.cache(e.codegenNode,!0))}}},Cf=(e,t,n)=>{const{exp:r,arg:o}=e;if(!r)return n.onError(Oa(41,e.loc)),Pf();const i=r.loc.source,s=4===r.type?r.content:i,l=n.bindingMetadata[i];if("props"===l||"props-aliased"===l)return n.onError(Oa(44,r.loc)),Pf();if(!s.trim()||!Lu(s))return n.onError(Oa(42,r.loc)),Pf();const a=o||gu("modelValue",!0),u=o?Ou(o)?`onUpdate:${F(o.content)}`:mu(['"onUpdate:" + ',o]):"onUpdate:modelValue";let c;c=mu([`${n.isTS?"($event: any)":"$event"} => ((`,r,") = $event)"]);const f=[vu(a,e.exp),vu(u,c)];if(e.modifiers.length&&1===t.tagType){const t=e.modifiers.map((e=>(Pu(e)?e:JSON.stringify(e))+": true")).join(", "),n=o?Ou(o)?`${o.content}Modifiers`:mu([o,' + "Modifiers"']):"modelModifiers";f.push(vu(n,gu(`{ ${t} }`,!1,e.loc,2)))}return Pf(f)};function Pf(e=[]){return{props:e}}const Tf=/[\w).+\-_$\]]/,Af=(e,t)=>{Yu("COMPILER_FILTER",t)&&(5===e.type&&jf(e.content,t),1===e.type&&e.props.forEach((e=>{7===e.type&&"for"!==e.name&&e.exp&&jf(e.exp,t)})))};function jf(e,t){if(4===e.type)Lf(e,t);else for(let n=0;n=0&&(e=n.charAt(t)," "===e);t--);e&&Tf.test(e)||(c=!0)}}else void 0===s?(h=i+1,s=n.slice(0,i).trim()):g();function g(){v.push(n.slice(h,i).trim()),h=i+1}if(void 0===s?s=n.slice(0,i).trim():0!==h&&g(),v.length){for(i=0;i{if(1===e.type){const n=Nu(e,"memo");if(!n||If.has(e))return;return If.add(e),()=>{const r=e.codegenNode||t.currentNode.codegenNode;r&&13===r.type&&(1!==e.tagType&&Su(r,t),e.codegenNode=yu(t.helper(au),[n.exp,bu(void 0,r),"_cache",String(t.cached++)]))}}};function Nf(e,t={}){const n=t.onError||xa,r="module"===t.mode;!0===t.prefixIdentifiers?n(Oa(47)):r&&n(Oa(48));t.cacheHandlers&&n(Oa(49)),t.scopeId&&!r&&n(Oa(50));const o=x(e)?ec(e,t):e,[i,s]=[[Ef,Gc,Ff,Zc,Af,bf,hf,lf,Of],{on:_f,bind:xf,model:Cf}];return Ic(o,d({},t,{prefixIdentifiers:false,nodeTransforms:[...i,...t.nodeTransforms||[]],directiveTransforms:d({},s,t.directiveTransforms||{})})),Uc(o,d({},t,{prefixIdentifiers:false}))}const Mf=Symbol(""),Df=Symbol(""),Bf=Symbol(""),Uf=Symbol(""),$f=Symbol(""),Vf=Symbol(""),Hf=Symbol(""),zf=Symbol(""),qf=Symbol(""),Wf=Symbol("");var Kf;let Gf;Kf={[Mf]:"vModelRadio",[Df]:"vModelCheckbox",[Bf]:"vModelText",[Uf]:"vModelSelect",[$f]:"vModelDynamic",[Vf]:"withModifiers",[Hf]:"withKeys",[zf]:"vShow",[qf]:"Transition",[Wf]:"TransitionGroup"},Object.getOwnPropertySymbols(Kf).forEach((e=>{cu[e]=Kf[e]}));const Yf=i("style,iframe,script,noscript",!0),Jf={isVoidTag:oe,isNativeTag:e=>ne(e)||re(e),isPreTag:e=>"pre"===e,decodeEntities:function(e,t=!1){return Gf||(Gf=document.createElement("div")),t?(Gf.innerHTML=`
`,Gf.children[0].getAttribute("foo")):(Gf.innerHTML=e,Gf.textContent)},isBuiltInComponent:e=>ku(e,"Transition")?qf:ku(e,"TransitionGroup")?Wf:void 0,getNamespace(e,t){let n=t?t.ns:0;if(t&&2===n)if("annotation-xml"===t.tag){if("svg"===e)return 1;t.props.some((e=>6===e.type&&"encoding"===e.name&&null!=e.value&&("text/html"===e.value.content||"application/xhtml+xml"===e.value.content)))&&(n=0)}else/^m(?:[ions]|text)$/.test(t.tag)&&"mglyph"!==e&&"malignmark"!==e&&(n=0);else t&&1===n&&("foreignObject"!==t.tag&&"desc"!==t.tag&&"title"!==t.tag||(n=0));if(0===n){if("svg"===e)return 1;if("math"===e)return 2}return n},getTextMode({tag:e,ns:t}){if(0===t){if("textarea"===e||"title"===e)return 1;if(Yf(e))return 2}return 0}},Qf=(e,t)=>{const n=X(e);return gu(JSON.stringify(n),!1,t,3)};function Zf(e,t){return Oa(e,t)}const Xf=i("passive,once,capture"),ep=i("stop,prevent,self,ctrl,shift,alt,meta,exact,middle"),tp=i("left,right"),np=i("onkeyup,onkeydown,onkeypress",!0),rp=(e,t)=>Ou(e)&&"onclick"===e.content.toLowerCase()?gu(t,!0):4!==e.type?mu(["(",e,`) === "onClick" ? "${t}" : (`,e,")"]):e;const op=(e,t)=>{1!==e.type||0!==e.tagType||"script"!==e.tag&&"style"!==e.tag||t.removeNode()},ip=[e=>{1===e.type&&e.props.forEach(((t,n)=>{6===t.type&&"style"===t.name&&t.value&&(e.props[n]={type:7,name:"bind",arg:gu("style",!0,t.loc),exp:Qf(t.value.content,t.loc),modifiers:[],loc:t.loc})}))}],sp={cloak:()=>({props:[]}),html:(e,t,n)=>{const{exp:r,loc:o}=e;return r||n.onError(Zf(53,o)),t.children.length&&(n.onError(Zf(54,o)),t.children.length=0),{props:[vu(gu("innerHTML",!0,o),r||gu("",!0))]}},text:(e,t,n)=>{const{exp:r,loc:o}=e;return r||n.onError(Zf(55,o)),t.children.length&&(n.onError(Zf(56,o)),t.children.length=0),{props:[vu(gu("textContent",!0),r?Cc(r,n)>0?r:yu(n.helperString(Wa),[r],o):gu("",!0))]}},model:(e,t,n)=>{const r=Cf(e,t,n);if(!r.props.length||1===t.tagType)return r;e.arg&&n.onError(Zf(58,e.arg.loc));const{tag:o}=t,i=n.isCustomElement(o);if("input"===o||"textarea"===o||"select"===o||i){let s=Bf,l=!1;if("input"===o||i){const r=Mu(t,"type");if(r){if(7===r.type)s=$f;else if(r.value)switch(r.value.content){case"radio":s=Mf;break;case"checkbox":s=Df;break;case"file":l=!0,n.onError(Zf(59,e.loc))}}else(function(e){return e.props.some((e=>!(7!==e.type||"bind"!==e.name||e.arg&&4===e.arg.type&&e.arg.isStatic)))})(t)&&(s=$f)}else"select"===o&&(s=Uf);l||(r.needRuntime=n.helper(s))}else n.onError(Zf(57,e.loc));return r.props=r.props.filter((e=>!(4===e.key.type&&"modelValue"===e.key.content))),r},on:(e,t,n)=>_f(e,t,n,(t=>{const{modifiers:r}=e;if(!r.length)return t;let{key:o,value:i}=t.props[0];const{keyModifiers:s,nonKeyModifiers:l,eventOptionModifiers:a}=((e,t,n,r)=>{const o=[],i=[],s=[];for(let r=0;r{const{exp:r,loc:o}=e;return r||n.onError(Zf(61,o)),{props:[],needRuntime:n.helper(zf)}}};const lp=Object.create(null);ks((function(e,t){if(!x(e)){if(!e.nodeType)return a;e=e.innerHTML}const n=e,o=lp[n];if(o)return o;if("#"===e[0]){const t=document.querySelector(e);0,e=t?t.innerHTML:""}const i=d({hoistStatic:!0,onError:void 0,onWarn:a},t);i.isCustomElement||"undefined"==typeof customElements||(i.isCustomElement=e=>!!customElements.get(e));const{code:s}=function(e,t={}){return Nf(e,d({},Jf,t,{nodeTransforms:[op,...ip,...t.nodeTransforms||[]],directiveTransforms:d({},sp,t.directiveTransforms||{}),transformHoist:null}))}(e,i),l=new Function("Vue",s)(r);return l._rc=!0,lp[n]=l}));var ap=!1;function up(e,t,n){return Array.isArray(e)?(e.length=Math.max(e.length,t),e.splice(t,1,n),n):(e[t]=n,n)}function cp(){return"undefined"!=typeof navigator&&"undefined"!=typeof window?window:void 0!==n.g?n.g:{}}const fp="function"==typeof Proxy,pp="devtools-plugin:setup";let dp,hp,vp;function gp(){return function(){var e;return void 0!==dp||("undefined"!=typeof window&&window.performance?(dp=!0,hp=window.performance):void 0!==n.g&&(null===(e=n.g.perf_hooks)||void 0===e?void 0:e.performance)?(dp=!0,hp=n.g.perf_hooks.performance):dp=!1),dp}()?hp.now():Date.now()}class mp{constructor(e,t){this.target=null,this.targetQueue=[],this.onQueue=[],this.plugin=e,this.hook=t;const n={};if(e.settings)for(const t in e.settings){const r=e.settings[t];n[t]=r.defaultValue}const r=`__vue-devtools-plugin-settings__${e.id}`;let o=Object.assign({},n);try{const e=localStorage.getItem(r),t=JSON.parse(e);Object.assign(o,t)}catch(e){}this.fallbacks={getSettings:()=>o,setSettings(e){try{localStorage.setItem(r,JSON.stringify(e))}catch(e){}o=e},now:()=>gp()},t&&t.on("plugin:settings:set",((e,t)=>{e===this.plugin.id&&this.fallbacks.setSettings(t)})),this.proxiedOn=new Proxy({},{get:(e,t)=>this.target?this.target.on[t]:(...e)=>{this.onQueue.push({method:t,args:e})}}),this.proxiedTarget=new Proxy({},{get:(e,t)=>this.target?this.target[t]:"on"===t?this.proxiedOn:Object.keys(this.fallbacks).includes(t)?(...e)=>(this.targetQueue.push({method:t,args:e,resolve:()=>{}}),this.fallbacks[t](...e)):(...e)=>new Promise((n=>{this.targetQueue.push({method:t,args:e,resolve:n})}))})}async setRealTarget(e){this.target=e;for(const e of this.onQueue)this.target.on[e.method](...e.args);for(const e of this.targetQueue)e.resolve(await this.target[e.method](...e.args))}}function yp(e,t){const n=e,r=cp(),o=cp().__VUE_DEVTOOLS_GLOBAL_HOOK__,i=fp&&n.enableEarlyProxy;if(!o||!r.__VUE_DEVTOOLS_PLUGIN_API_AVAILABLE__&&i){const e=i?new mp(n,o):null;(r.__VUE_DEVTOOLS_PLUGINS__=r.__VUE_DEVTOOLS_PLUGINS__||[]).push({pluginDescriptor:n,setupFn:t,proxy:e}),e&&t(e.proxiedTarget)}else o.emit(pp,e,t)}const bp=e=>vp=e,wp=Symbol();function _p(e){return e&&"object"==typeof e&&"[object Object]"===Object.prototype.toString.call(e)&&"function"!=typeof e.toJSON}var xp;!function(e){e.direct="direct",e.patchObject="patch object",e.patchFunction="patch function"}(xp||(xp={}));const Sp="undefined"!=typeof window,Op=!1,kp=(()=>"object"==typeof window&&window.window===window?window:"object"==typeof self&&self.self===self?self:"object"==typeof global&&global.global===global?global:"object"==typeof globalThis?globalThis:{HTMLElement:null})();function Ep(e,t,n){const r=new XMLHttpRequest;r.open("GET",e),r.responseType="blob",r.onload=function(){jp(r.response,t,n)},r.onerror=function(){},r.send()}function Cp(e){const t=new XMLHttpRequest;t.open("HEAD",e,!1);try{t.send()}catch(e){}return t.status>=200&&t.status<=299}function Pp(e){try{e.dispatchEvent(new MouseEvent("click"))}catch(t){const n=document.createEvent("MouseEvents");n.initMouseEvent("click",!0,!0,window,0,0,0,80,20,!1,!1,!1,!1,0,null),e.dispatchEvent(n)}}const Tp="object"==typeof navigator?navigator:{userAgent:""},Ap=(()=>/Macintosh/.test(Tp.userAgent)&&/AppleWebKit/.test(Tp.userAgent)&&!/Safari/.test(Tp.userAgent))(),jp=Sp?"undefined"!=typeof HTMLAnchorElement&&"download"in HTMLAnchorElement.prototype&&!Ap?function(e,t="download",n){const r=document.createElement("a");r.download=t,r.rel="noopener","string"==typeof e?(r.href=e,r.origin!==location.origin?Cp(r.href)?Ep(e,t,n):(r.target="_blank",Pp(r)):Pp(r)):(r.href=URL.createObjectURL(e),setTimeout((function(){URL.revokeObjectURL(r.href)}),4e4),setTimeout((function(){Pp(r)}),0))}:"msSaveOrOpenBlob"in Tp?function(e,t="download",n){if("string"==typeof e)if(Cp(e))Ep(e,t,n);else{const t=document.createElement("a");t.href=e,t.target="_blank",setTimeout((function(){Pp(t)}))}else navigator.msSaveOrOpenBlob(function(e,{autoBom:t=!1}={}){return t&&/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(e.type)?new Blob([String.fromCharCode(65279),e],{type:e.type}):e}(e,n),t)}:function(e,t,n,r){(r=r||open("","_blank"))&&(r.document.title=r.document.body.innerText="downloading...");if("string"==typeof e)return Ep(e,t,n);const o="application/octet-stream"===e.type,i=/constructor/i.test(String(kp.HTMLElement))||"safari"in kp,s=/CriOS\/[\d]+/.test(navigator.userAgent);if((s||o&&i||Ap)&&"undefined"!=typeof FileReader){const t=new FileReader;t.onloadend=function(){let e=t.result;if("string"!=typeof e)throw r=null,new Error("Wrong reader.result type");e=s?e:e.replace(/^data:[^;]*;/,"data:attachment/file;"),r?r.location.href=e:location.assign(e),r=null},t.readAsDataURL(e)}else{const t=URL.createObjectURL(e);r?r.location.assign(t):location.href=t,r=null,setTimeout((function(){URL.revokeObjectURL(t)}),4e4)}}:()=>{};function Lp(e,t){"function"==typeof __VUE_DEVTOOLS_TOAST__&&__VUE_DEVTOOLS_TOAST__("🍍 "+e,t)}function Rp(e){return"_a"in e&&"install"in e}function Ip(){if(!("clipboard"in navigator))return Lp("Your browser doesn't support the Clipboard API","error"),!0}function Fp(e){return!!(e instanceof Error&&e.message.toLowerCase().includes("document is not focused"))&&(Lp('You need to activate the "Emulate a focused page" setting in the "Rendering" panel of devtools.',"warn"),!0)}let Np;async function Mp(e){try{const t=(Np||(Np=document.createElement("input"),Np.type="file",Np.accept=".json"),function(){return new Promise(((e,t)=>{Np.onchange=async()=>{const t=Np.files;if(!t)return e(null);const n=t.item(0);return e(n?{text:await n.text(),file:n}:null)},Np.oncancel=()=>e(null),Np.onerror=t,Np.click()}))}),n=await t();if(!n)return;const{text:r,file:o}=n;Dp(e,JSON.parse(r)),Lp(`Global state imported from "${o.name}".`)}catch(e){Lp("Failed to import the state from JSON. Check the console for more details.","error")}}function Dp(e,t){for(const n in t){const r=e.state.value[n];r?Object.assign(r,t[n]):e.state.value[n]=t[n]}}function Bp(e){return{_custom:{display:e}}}const Up="🍍 Pinia (root)",$p="_root";function Vp(e){return Rp(e)?{id:$p,label:Up}:{id:e.$id,label:e.$id}}function Hp(e){return e?Array.isArray(e)?e.reduce(((e,t)=>(e.keys.push(t.key),e.operations.push(t.type),e.oldValue[t.key]=t.oldValue,e.newValue[t.key]=t.newValue,e)),{oldValue:{},keys:[],operations:[],newValue:{}}):{operation:Bp(e.type),key:Bp(e.key),oldValue:e.oldValue,newValue:e.newValue}:{}}function zp(e){switch(e){case xp.direct:return"mutation";case xp.patchFunction:case xp.patchObject:return"$patch";default:return"unknown"}}let qp=!0;const Wp=[],Kp="pinia:mutations",Gp="pinia",{assign:Yp}=Object,Jp=e=>"🍍 "+e;function Qp(e,t){yp({id:"dev.esm.pinia",label:"Pinia 🍍",logo:"https://pinia.vuejs.org/logo.svg",packageName:"pinia",homepage:"https://pinia.vuejs.org",componentStateTypes:Wp,app:e},(n=>{"function"!=typeof n.now&&Lp("You seem to be using an outdated version of Vue Devtools. Are you still using the Beta release instead of the stable one? You can find the links at https://devtools.vuejs.org/guide/installation.html."),n.addTimelineLayer({id:Kp,label:"Pinia 🍍",color:15064968}),n.addInspector({id:Gp,label:"Pinia 🍍",icon:"storage",treeFilterPlaceholder:"Search stores",actions:[{icon:"content_copy",action:()=>{!async function(e){if(!Ip())try{await navigator.clipboard.writeText(JSON.stringify(e.state.value)),Lp("Global state copied to clipboard.")}catch(e){if(Fp(e))return;Lp("Failed to serialize the state. Check the console for more details.","error")}}(t)},tooltip:"Serialize and copy the state"},{icon:"content_paste",action:async()=>{await async function(e){if(!Ip())try{Dp(e,JSON.parse(await navigator.clipboard.readText())),Lp("Global state pasted from clipboard.")}catch(e){if(Fp(e))return;Lp("Failed to deserialize the state from clipboard. Check the console for more details.","error")}}(t),n.sendInspectorTree(Gp),n.sendInspectorState(Gp)},tooltip:"Replace the state with the content of your clipboard"},{icon:"save",action:()=>{!async function(e){try{jp(new Blob([JSON.stringify(e.state.value)],{type:"text/plain;charset=utf-8"}),"pinia-state.json")}catch(e){Lp("Failed to export the state as JSON. Check the console for more details.","error")}}(t)},tooltip:"Save the state as a JSON file"},{icon:"folder_open",action:async()=>{await Mp(t),n.sendInspectorTree(Gp),n.sendInspectorState(Gp)},tooltip:"Import the state from a JSON file"}],nodeActions:[{icon:"restore",tooltip:'Reset the state (with "$reset")',action:e=>{const n=t._s.get(e);n?"function"!=typeof n.$reset?Lp(`Cannot reset "${e}" store because it doesn't have a "$reset" method implemented.`,"warn"):(n.$reset(),Lp(`Store "${e}" reset.`)):Lp(`Cannot reset "${e}" store because it wasn't found.`,"warn")}}]}),n.on.inspectComponent(((e,t)=>{const n=e.componentInstance&&e.componentInstance.proxy;if(n&&n._pStores){const t=e.componentInstance.proxy._pStores;Object.values(t).forEach((t=>{e.instanceData.state.push({type:Jp(t.$id),key:"state",editable:!0,value:t._isOptionsAPI?{_custom:{value:It(t.$state),actions:[{icon:"restore",tooltip:"Reset the state of this store",action:()=>t.$reset()}]}}:Object.keys(t.$state).reduce(((e,n)=>(e[n]=t.$state[n],e)),{})}),t._getters&&t._getters.length&&e.instanceData.state.push({type:Jp(t.$id),key:"getters",editable:!1,value:t._getters.reduce(((e,n)=>{try{e[n]=t[n]}catch(t){e[n]=t}return e}),{})})}))}})),n.on.getInspectorTree((n=>{if(n.app===e&&n.inspectorId===Gp){let e=[t];e=e.concat(Array.from(t._s.values())),n.rootNodes=(n.filter?e.filter((e=>"$id"in e?e.$id.toLowerCase().includes(n.filter.toLowerCase()):Up.toLowerCase().includes(n.filter.toLowerCase()))):e).map(Vp)}})),n.on.getInspectorState((n=>{if(n.app===e&&n.inspectorId===Gp){const e=n.nodeId===$p?t:t._s.get(n.nodeId);if(!e)return;e&&(n.state=function(e){if(Rp(e)){const t=Array.from(e._s.keys()),n=e._s,r={state:t.map((t=>({editable:!0,key:t,value:e.state.value[t]}))),getters:t.filter((e=>n.get(e)._getters)).map((e=>{const t=n.get(e);return{editable:!1,key:e,value:t._getters.reduce(((e,n)=>(e[n]=t[n],e)),{})}}))};return r}const t={state:Object.keys(e.$state).map((t=>({editable:!0,key:t,value:e.$state[t]})))};return e._getters&&e._getters.length&&(t.getters=e._getters.map((t=>({editable:!1,key:t,value:e[t]})))),e._customProperties.size&&(t.customProperties=Array.from(e._customProperties).map((t=>({editable:!0,key:t,value:e[t]})))),t}(e))}})),n.on.editInspectorState(((n,r)=>{if(n.app===e&&n.inspectorId===Gp){const e=n.nodeId===$p?t:t._s.get(n.nodeId);if(!e)return Lp(`store "${n.nodeId}" not found`,"error");const{path:r}=n;Rp(e)?r.unshift("state"):1===r.length&&e._customProperties.has(r[0])&&!(r[0]in e.$state)||r.unshift("$state"),qp=!1,n.set(e,r,n.state.value),qp=!0}})),n.on.editComponentState((e=>{if(e.type.startsWith("🍍")){const n=e.type.replace(/^🍍\s*/,""),r=t._s.get(n);if(!r)return Lp(`store "${n}" not found`,"error");const{path:o}=e;if("state"!==o[0])return Lp(`Invalid path for store "${n}":\n${o}\nOnly state can be modified.`);o[0]="$state",qp=!1,e.set(r,o,e.state.value),qp=!0}}))}))}let Zp,Xp=0;function ed(e,t,n){const r=t.reduce(((t,n)=>(t[n]=It(e)[n],t)),{});for(const t in r)e[t]=function(){const o=Xp,i=n?new Proxy(e,{get:(...e)=>(Zp=o,Reflect.get(...e)),set:(...e)=>(Zp=o,Reflect.set(...e))}):e;Zp=o;const s=r[t].apply(i,arguments);return Zp=void 0,s}}function td({app:e,store:t,options:n}){if(t.$id.startsWith("__hot:"))return;t._isOptionsAPI=!!n.state,ed(t,Object.keys(n.actions),t._isOptionsAPI);const r=t._hotUpdate;It(t)._hotUpdate=function(e){r.apply(this,arguments),ed(t,Object.keys(e._hmrPayload.actions),!!t._isOptionsAPI)},function(e,t){Wp.includes(Jp(t.$id))||Wp.push(Jp(t.$id)),yp({id:"dev.esm.pinia",label:"Pinia 🍍",logo:"https://pinia.vuejs.org/logo.svg",packageName:"pinia",homepage:"https://pinia.vuejs.org",componentStateTypes:Wp,app:e,settings:{logStoreChanges:{label:"Notify about new/deleted stores",type:"boolean",defaultValue:!0}}},(e=>{const n="function"==typeof e.now?e.now.bind(e):Date.now;t.$onAction((({after:r,onError:o,name:i,args:s})=>{const l=Xp++;e.addTimelineEvent({layerId:Kp,event:{time:n(),title:"🛫 "+i,subtitle:"start",data:{store:Bp(t.$id),action:Bp(i),args:s},groupId:l}}),r((r=>{Zp=void 0,e.addTimelineEvent({layerId:Kp,event:{time:n(),title:"🛬 "+i,subtitle:"end",data:{store:Bp(t.$id),action:Bp(i),args:s,result:r},groupId:l}})})),o((r=>{Zp=void 0,e.addTimelineEvent({layerId:Kp,event:{time:n(),logType:"error",title:"💥 "+i,subtitle:"end",data:{store:Bp(t.$id),action:Bp(i),args:s,error:r},groupId:l}})}))}),!0),t._customProperties.forEach((r=>{dr((()=>Wt(t[r])),((t,o)=>{e.notifyComponentUpdate(),e.sendInspectorState(Gp),qp&&e.addTimelineEvent({layerId:Kp,event:{time:n(),title:"Change",subtitle:r,data:{newValue:t,oldValue:o},groupId:Zp}})}),{deep:!0})})),t.$subscribe((({events:r,type:o},i)=>{if(e.notifyComponentUpdate(),e.sendInspectorState(Gp),!qp)return;const s={time:n(),title:zp(o),data:Yp({store:Bp(t.$id)},Hp(r)),groupId:Zp};o===xp.patchFunction?s.subtitle="⤵️":o===xp.patchObject?s.subtitle="🧩":r&&!Array.isArray(r)&&(s.subtitle=r.type),r&&(s.data["rawEvent(s)"]={_custom:{display:"DebuggerEvent",type:"object",tooltip:"raw DebuggerEvent[]",value:r}}),e.addTimelineEvent({layerId:Kp,event:s})}),{detached:!0,flush:"sync"});const r=t._hotUpdate;t._hotUpdate=Ft((o=>{r(o),e.addTimelineEvent({layerId:Kp,event:{time:n(),title:"🔥 "+t.$id,subtitle:"HMR update",data:{store:Bp(t.$id),info:Bp("HMR update")}}}),e.notifyComponentUpdate(),e.sendInspectorTree(Gp),e.sendInspectorState(Gp)}));const{$dispose:o}=t;t.$dispose=()=>{o(),e.notifyComponentUpdate(),e.sendInspectorTree(Gp),e.sendInspectorState(Gp),e.getSettings().logStoreChanges&&Lp(`Disposed "${t.$id}" store 🗑`)},e.notifyComponentUpdate(),e.sendInspectorTree(Gp),e.sendInspectorState(Gp),e.getSettings().logStoreChanges&&Lp(`"${t.$id}" store installed 🆕`)}))}(e,t)}const nd=()=>{};function rd(e,t,n,r=nd){e.push(t);const o=()=>{const n=e.indexOf(t);n>-1&&(e.splice(n,1),r())};return!n&&ge()&&me(o),o}function od(e,...t){e.slice().forEach((e=>{e(...t)}))}const id=e=>e();function sd(e,t){e instanceof Map&&t instanceof Map&&t.forEach(((t,n)=>e.set(n,t))),e instanceof Set&&t instanceof Set&&t.forEach(e.add,e);for(const n in t){if(!t.hasOwnProperty(n))continue;const r=t[n],o=e[n];_p(o)&&_p(r)&&e.hasOwnProperty(n)&&!Ut(r)&&!At(r)?e[n]=sd(o,r):e[n]=r}return e}const ld=Symbol(),ad=new WeakMap;const{assign:ud}=Object;function cd(e){return!(!Ut(e)||!e.effect)}function fd(e,t,n={},r,o,i){let s;const l=ud({actions:{}},n);const a={deep:!0};let u,c;let f,p=[],d=[];const h=r.state.value[e];i||h||(ap?up(r.state.value,e,{}):r.state.value[e]={});const v=$t({});let g;function m(t){let n;u=c=!1,"function"==typeof t?(t(r.state.value[e]),n={type:xp.patchFunction,storeId:e,events:f}):(sd(r.state.value[e],t),n={type:xp.patchObject,payload:t,storeId:e,events:f});const o=g=Symbol();bn().then((()=>{g===o&&(u=!0)})),c=!0,od(p,n,r.state.value[e])}const y=i?function(){const{state:e}=n,t=e?e():{};this.$patch((e=>{ud(e,t)}))}:nd;function b(t,n){return function(){bp(r);const o=Array.from(arguments),i=[],s=[];let l;od(d,{args:o,name:t,store:x,after:function(e){i.push(e)},onError:function(e){s.push(e)}});try{l=n.apply(this&&this.$id===e?this:x,o)}catch(e){throw od(s,e),e}return l instanceof Promise?l.then((e=>(od(i,e),e))).catch((e=>(od(s,e),Promise.reject(e)))):(od(i,l),l)}}const w=Ft({actions:{},getters:{},state:[],hotState:v}),_={_p:r,$id:e,$onAction:rd.bind(null,d),$patch:m,$reset:y,$subscribe(t,n={}){const o=rd(p,t,n.detached,(()=>i())),i=s.run((()=>dr((()=>r.state.value[e]),(r=>{("sync"===n.flush?c:u)&&t({storeId:e,type:xp.direct,events:f},r)}),ud({},a,n))));return o},$dispose:function(){s.stop(),p=[],d=[],r._s.delete(e)}};ap&&(_._r=!1);const x=kt(Op?ud({_hmrPayload:w,_customProperties:Ft(new Set)},_):_);r._s.set(e,x);const S=(r._a&&r._a.runWithContext||id)((()=>r._e.run((()=>(s=he()).run(t)))));for(const t in S){const n=S[t];if(Ut(n)&&!cd(n)||At(n))i||(!h||(O=n,ap?ad.has(O):_p(O)&&O.hasOwnProperty(ld))||(Ut(n)?n.value=h[t]:sd(n,h[t])),ap?up(r.state.value[e],t,n):r.state.value[e][t]=n);else if("function"==typeof n){const e=b(t,n);ap?up(S,t,e):S[t]=e,l.actions[t]=n}else 0}var O;if(ap?Object.keys(S).forEach((e=>{up(x,e,S[e])})):(ud(x,S),ud(It(x),S)),Object.defineProperty(x,"$state",{get:()=>r.state.value[e],set:e=>{m((t=>{ud(t,e)}))}}),Op){const e={writable:!0,configurable:!0,enumerable:!1};["_p","_hmrPayload","_getters","_customProperties"].forEach((t=>{Object.defineProperty(x,t,ud({value:x[t]},e))}))}return ap&&(x._r=!0),r._p.forEach((e=>{if(Op){const t=s.run((()=>e({store:x,app:r._a,pinia:r,options:l})));Object.keys(t||{}).forEach((e=>x._customProperties.add(e))),ud(x,t)}else ud(x,s.run((()=>e({store:x,app:r._a,pinia:r,options:l}))))})),h&&i&&n.hydrate&&n.hydrate(x.$state,h),u=!0,c=!0,x}function pd(e,t,n){let r,o;const i="function"==typeof t;function s(e,n){const s=Jo();(e=e||(s?Yo(wp,null):null))&&bp(e),(e=vp)._s.has(r)||(i?fd(r,t,o,e):function(e,t,n,r){const{state:o,actions:i,getters:s}=t,l=n.state.value[e];let a;a=fd(e,(function(){l||(ap?up(n.state.value,e,o?o():{}):n.state.value[e]=o?o():{});const t=Zt(n.state.value[e]);return ud(t,i,Object.keys(s||{}).reduce(((t,r)=>(t[r]=Ft(Ls((()=>{bp(n);const t=n._s.get(e);if(!ap||t._r)return s[r].call(t,t)}))),t)),{}))}),t,n,0,!0)}(r,o,e));return e._s.get(r)}return"string"==typeof e?(r=e,o=i?n:t):(o=e,r=e.id),s.$id=r,s}function dd(e,t){return function(){return e.apply(t,arguments)}}const{toString:hd}=Object.prototype,{getPrototypeOf:vd}=Object,gd=(md=Object.create(null),e=>{const t=hd.call(e);return md[t]||(md[t]=t.slice(8,-1).toLowerCase())});var md;const yd=e=>(e=e.toLowerCase(),t=>gd(t)===e),bd=e=>t=>typeof t===e,{isArray:wd}=Array,_d=bd("undefined");const xd=yd("ArrayBuffer");const Sd=bd("string"),Od=bd("function"),kd=bd("number"),Ed=e=>null!==e&&"object"==typeof e,Cd=e=>{if("object"!==gd(e))return!1;const t=vd(e);return!(null!==t&&t!==Object.prototype&&null!==Object.getPrototypeOf(t)||Symbol.toStringTag in e||Symbol.iterator in e)},Pd=yd("Date"),Td=yd("File"),Ad=yd("Blob"),jd=yd("FileList"),Ld=yd("URLSearchParams");function Rd(e,t,{allOwnKeys:n=!1}={}){if(null==e)return;let r,o;if("object"!=typeof e&&(e=[e]),wd(e))for(r=0,o=e.length;r0;)if(r=n[o],t===r.toLowerCase())return r;return null}const Fd="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:global,Nd=e=>!_d(e)&&e!==Fd;const Md=(Dd="undefined"!=typeof Uint8Array&&vd(Uint8Array),e=>Dd&&e instanceof Dd);var Dd;const Bd=yd("HTMLFormElement"),Ud=(({hasOwnProperty:e})=>(t,n)=>e.call(t,n))(Object.prototype),$d=yd("RegExp"),Vd=(e,t)=>{const n=Object.getOwnPropertyDescriptors(e),r={};Rd(n,((n,o)=>{let i;!1!==(i=t(n,o,e))&&(r[o]=i||n)})),Object.defineProperties(e,r)},Hd="abcdefghijklmnopqrstuvwxyz",zd="0123456789",qd={DIGIT:zd,ALPHA:Hd,ALPHA_DIGIT:Hd+Hd.toUpperCase()+zd};const Wd=yd("AsyncFunction"),Kd={isArray:wd,isArrayBuffer:xd,isBuffer:function(e){return null!==e&&!_d(e)&&null!==e.constructor&&!_d(e.constructor)&&Od(e.constructor.isBuffer)&&e.constructor.isBuffer(e)},isFormData:e=>{let t;return e&&("function"==typeof FormData&&e instanceof FormData||Od(e.append)&&("formdata"===(t=gd(e))||"object"===t&&Od(e.toString)&&"[object FormData]"===e.toString()))},isArrayBufferView:function(e){let t;return t="undefined"!=typeof ArrayBuffer&&ArrayBuffer.isView?ArrayBuffer.isView(e):e&&e.buffer&&xd(e.buffer),t},isString:Sd,isNumber:kd,isBoolean:e=>!0===e||!1===e,isObject:Ed,isPlainObject:Cd,isUndefined:_d,isDate:Pd,isFile:Td,isBlob:Ad,isRegExp:$d,isFunction:Od,isStream:e=>Ed(e)&&Od(e.pipe),isURLSearchParams:Ld,isTypedArray:Md,isFileList:jd,forEach:Rd,merge:function e(){const{caseless:t}=Nd(this)&&this||{},n={},r=(r,o)=>{const i=t&&Id(n,o)||o;Cd(n[i])&&Cd(r)?n[i]=e(n[i],r):Cd(r)?n[i]=e({},r):wd(r)?n[i]=r.slice():n[i]=r};for(let e=0,t=arguments.length;e(Rd(t,((t,r)=>{n&&Od(t)?e[r]=dd(t,n):e[r]=t}),{allOwnKeys:r}),e),trim:e=>e.trim?e.trim():e.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,""),stripBOM:e=>(65279===e.charCodeAt(0)&&(e=e.slice(1)),e),inherits:(e,t,n,r)=>{e.prototype=Object.create(t.prototype,r),e.prototype.constructor=e,Object.defineProperty(e,"super",{value:t.prototype}),n&&Object.assign(e.prototype,n)},toFlatObject:(e,t,n,r)=>{let o,i,s;const l={};if(t=t||{},null==e)return t;do{for(o=Object.getOwnPropertyNames(e),i=o.length;i-- >0;)s=o[i],r&&!r(s,e,t)||l[s]||(t[s]=e[s],l[s]=!0);e=!1!==n&&vd(e)}while(e&&(!n||n(e,t))&&e!==Object.prototype);return t},kindOf:gd,kindOfTest:yd,endsWith:(e,t,n)=>{e=String(e),(void 0===n||n>e.length)&&(n=e.length),n-=t.length;const r=e.indexOf(t,n);return-1!==r&&r===n},toArray:e=>{if(!e)return null;if(wd(e))return e;let t=e.length;if(!kd(t))return null;const n=new Array(t);for(;t-- >0;)n[t]=e[t];return n},forEachEntry:(e,t)=>{const n=(e&&e[Symbol.iterator]).call(e);let r;for(;(r=n.next())&&!r.done;){const n=r.value;t.call(e,n[0],n[1])}},matchAll:(e,t)=>{let n;const r=[];for(;null!==(n=e.exec(t));)r.push(n);return r},isHTMLForm:Bd,hasOwnProperty:Ud,hasOwnProp:Ud,reduceDescriptors:Vd,freezeMethods:e=>{Vd(e,((t,n)=>{if(Od(e)&&-1!==["arguments","caller","callee"].indexOf(n))return!1;const r=e[n];Od(r)&&(t.enumerable=!1,"writable"in t?t.writable=!1:t.set||(t.set=()=>{throw Error("Can not rewrite read-only method '"+n+"'")}))}))},toObjectSet:(e,t)=>{const n={},r=e=>{e.forEach((e=>{n[e]=!0}))};return wd(e)?r(e):r(String(e).split(t)),n},toCamelCase:e=>e.toLowerCase().replace(/[-_\s]([a-z\d])(\w*)/g,(function(e,t,n){return t.toUpperCase()+n})),noop:()=>{},toFiniteNumber:(e,t)=>(e=+e,Number.isFinite(e)?e:t),findKey:Id,global:Fd,isContextDefined:Nd,ALPHABET:qd,generateString:(e=16,t=qd.ALPHA_DIGIT)=>{let n="";const{length:r}=t;for(;e--;)n+=t[Math.random()*r|0];return n},isSpecCompliantForm:function(e){return!!(e&&Od(e.append)&&"FormData"===e[Symbol.toStringTag]&&e[Symbol.iterator])},toJSONObject:e=>{const t=new Array(10),n=(e,r)=>{if(Ed(e)){if(t.indexOf(e)>=0)return;if(!("toJSON"in e)){t[r]=e;const o=wd(e)?[]:{};return Rd(e,((e,t)=>{const i=n(e,r+1);!_d(i)&&(o[t]=i)})),t[r]=void 0,o}}return e};return n(e,0)},isAsyncFn:Wd,isThenable:e=>e&&(Ed(e)||Od(e))&&Od(e.then)&&Od(e.catch)};function Gd(e,t,n,r,o){Error.call(this),Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=(new Error).stack,this.message=e,this.name="AxiosError",t&&(this.code=t),n&&(this.config=n),r&&(this.request=r),o&&(this.response=o)}Kd.inherits(Gd,Error,{toJSON:function(){return{message:this.message,name:this.name,description:this.description,number:this.number,fileName:this.fileName,lineNumber:this.lineNumber,columnNumber:this.columnNumber,stack:this.stack,config:Kd.toJSONObject(this.config),code:this.code,status:this.response&&this.response.status?this.response.status:null}}});const Yd=Gd.prototype,Jd={};["ERR_BAD_OPTION_VALUE","ERR_BAD_OPTION","ECONNABORTED","ETIMEDOUT","ERR_NETWORK","ERR_FR_TOO_MANY_REDIRECTS","ERR_DEPRECATED","ERR_BAD_RESPONSE","ERR_BAD_REQUEST","ERR_CANCELED","ERR_NOT_SUPPORT","ERR_INVALID_URL"].forEach((e=>{Jd[e]={value:e}})),Object.defineProperties(Gd,Jd),Object.defineProperty(Yd,"isAxiosError",{value:!0}),Gd.from=(e,t,n,r,o,i)=>{const s=Object.create(Yd);return Kd.toFlatObject(e,s,(function(e){return e!==Error.prototype}),(e=>"isAxiosError"!==e)),Gd.call(s,e.message,t,n,r,o),s.cause=e,s.name=e.name,i&&Object.assign(s,i),s};const Qd=Gd;var Zd=n(764).lW;function Xd(e){return Kd.isPlainObject(e)||Kd.isArray(e)}function eh(e){return Kd.endsWith(e,"[]")?e.slice(0,-2):e}function th(e,t,n){return e?e.concat(t).map((function(e,t){return e=eh(e),!n&&t?"["+e+"]":e})).join(n?".":""):t}const nh=Kd.toFlatObject(Kd,{},null,(function(e){return/^is[A-Z]/.test(e)}));const rh=function(e,t,n){if(!Kd.isObject(e))throw new TypeError("target must be an object");t=t||new FormData;const r=(n=Kd.toFlatObject(n,{metaTokens:!0,dots:!1,indexes:!1},!1,(function(e,t){return!Kd.isUndefined(t[e])}))).metaTokens,o=n.visitor||u,i=n.dots,s=n.indexes,l=(n.Blob||"undefined"!=typeof Blob&&Blob)&&Kd.isSpecCompliantForm(t);if(!Kd.isFunction(o))throw new TypeError("visitor must be a function");function a(e){if(null===e)return"";if(Kd.isDate(e))return e.toISOString();if(!l&&Kd.isBlob(e))throw new Qd("Blob is not supported. Use a Buffer instead.");return Kd.isArrayBuffer(e)||Kd.isTypedArray(e)?l&&"function"==typeof Blob?new Blob([e]):Zd.from(e):e}function u(e,n,o){let l=e;if(e&&!o&&"object"==typeof e)if(Kd.endsWith(n,"{}"))n=r?n:n.slice(0,-2),e=JSON.stringify(e);else if(Kd.isArray(e)&&function(e){return Kd.isArray(e)&&!e.some(Xd)}(e)||(Kd.isFileList(e)||Kd.endsWith(n,"[]"))&&(l=Kd.toArray(e)))return n=eh(n),l.forEach((function(e,r){!Kd.isUndefined(e)&&null!==e&&t.append(!0===s?th([n],r,i):null===s?n:n+"[]",a(e))})),!1;return!!Xd(e)||(t.append(th(o,n,i),a(e)),!1)}const c=[],f=Object.assign(nh,{defaultVisitor:u,convertValue:a,isVisitable:Xd});if(!Kd.isObject(e))throw new TypeError("data must be an object");return function e(n,r){if(!Kd.isUndefined(n)){if(-1!==c.indexOf(n))throw Error("Circular reference detected in "+r.join("."));c.push(n),Kd.forEach(n,(function(n,i){!0===(!(Kd.isUndefined(n)||null===n)&&o.call(t,n,Kd.isString(i)?i.trim():i,r,f))&&e(n,r?r.concat(i):[i])})),c.pop()}}(e),t};function oh(e){const t={"!":"%21","'":"%27","(":"%28",")":"%29","~":"%7E","%20":"+","%00":"\0"};return encodeURIComponent(e).replace(/[!'()~]|%20|%00/g,(function(e){return t[e]}))}function ih(e,t){this._pairs=[],e&&rh(e,this,t)}const sh=ih.prototype;sh.append=function(e,t){this._pairs.push([e,t])},sh.toString=function(e){const t=e?function(t){return e.call(this,t,oh)}:oh;return this._pairs.map((function(e){return t(e[0])+"="+t(e[1])}),"").join("&")};const lh=ih;function ah(e){return encodeURIComponent(e).replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,"+").replace(/%5B/gi,"[").replace(/%5D/gi,"]")}function uh(e,t,n){if(!t)return e;const r=n&&n.encode||ah,o=n&&n.serialize;let i;if(i=o?o(t,n):Kd.isURLSearchParams(t)?t.toString():new lh(t,n).toString(r),i){const t=e.indexOf("#");-1!==t&&(e=e.slice(0,t)),e+=(-1===e.indexOf("?")?"?":"&")+i}return e}const ch=class{constructor(){this.handlers=[]}use(e,t,n){return this.handlers.push({fulfilled:e,rejected:t,synchronous:!!n&&n.synchronous,runWhen:n?n.runWhen:null}),this.handlers.length-1}eject(e){this.handlers[e]&&(this.handlers[e]=null)}clear(){this.handlers&&(this.handlers=[])}forEach(e){Kd.forEach(this.handlers,(function(t){null!==t&&e(t)}))}},fh={silentJSONParsing:!0,forcedJSONParsing:!0,clarifyTimeoutError:!1},ph={isBrowser:!0,classes:{URLSearchParams:"undefined"!=typeof URLSearchParams?URLSearchParams:lh,FormData:"undefined"!=typeof FormData?FormData:null,Blob:"undefined"!=typeof Blob?Blob:null},protocols:["http","https","file","blob","url","data"]},dh="undefined"!=typeof window&&"undefined"!=typeof document,hh=(vh="undefined"!=typeof navigator&&navigator.product,dh&&["ReactNative","NativeScript","NS"].indexOf(vh)<0);var vh;const gh="undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope&&"function"==typeof self.importScripts,mh={...o,...ph};const yh=function(e){function t(e,n,r,o){let i=e[o++];const s=Number.isFinite(+i),l=o>=e.length;if(i=!i&&Kd.isArray(r)?r.length:i,l)return Kd.hasOwnProp(r,i)?r[i]=[r[i],n]:r[i]=n,!s;r[i]&&Kd.isObject(r[i])||(r[i]=[]);return t(e,n,r[i],o)&&Kd.isArray(r[i])&&(r[i]=function(e){const t={},n=Object.keys(e);let r;const o=n.length;let i;for(r=0;r{t(function(e){return Kd.matchAll(/\w+|\[(\w*)]/g,e).map((e=>"[]"===e[0]?"":e[1]||e[0]))}(e),r,n,0)})),n}return null};const bh={transitional:fh,adapter:["xhr","http"],transformRequest:[function(e,t){const n=t.getContentType()||"",r=n.indexOf("application/json")>-1,o=Kd.isObject(e);o&&Kd.isHTMLForm(e)&&(e=new FormData(e));if(Kd.isFormData(e))return r&&r?JSON.stringify(yh(e)):e;if(Kd.isArrayBuffer(e)||Kd.isBuffer(e)||Kd.isStream(e)||Kd.isFile(e)||Kd.isBlob(e))return e;if(Kd.isArrayBufferView(e))return e.buffer;if(Kd.isURLSearchParams(e))return t.setContentType("application/x-www-form-urlencoded;charset=utf-8",!1),e.toString();let i;if(o){if(n.indexOf("application/x-www-form-urlencoded")>-1)return function(e,t){return rh(e,new mh.classes.URLSearchParams,Object.assign({visitor:function(e,t,n,r){return mh.isNode&&Kd.isBuffer(e)?(this.append(t,e.toString("base64")),!1):r.defaultVisitor.apply(this,arguments)}},t))}(e,this.formSerializer).toString();if((i=Kd.isFileList(e))||n.indexOf("multipart/form-data")>-1){const t=this.env&&this.env.FormData;return rh(i?{"files[]":e}:e,t&&new t,this.formSerializer)}}return o||r?(t.setContentType("application/json",!1),function(e,t,n){if(Kd.isString(e))try{return(t||JSON.parse)(e),Kd.trim(e)}catch(e){if("SyntaxError"!==e.name)throw e}return(n||JSON.stringify)(e)}(e)):e}],transformResponse:[function(e){const t=this.transitional||bh.transitional,n=t&&t.forcedJSONParsing,r="json"===this.responseType;if(e&&Kd.isString(e)&&(n&&!this.responseType||r)){const n=!(t&&t.silentJSONParsing)&&r;try{return JSON.parse(e)}catch(e){if(n){if("SyntaxError"===e.name)throw Qd.from(e,Qd.ERR_BAD_RESPONSE,this,null,this.response);throw e}}}return e}],timeout:0,xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN",maxContentLength:-1,maxBodyLength:-1,env:{FormData:mh.classes.FormData,Blob:mh.classes.Blob},validateStatus:function(e){return e>=200&&e<300},headers:{common:{Accept:"application/json, text/plain, */*","Content-Type":void 0}}};Kd.forEach(["delete","get","head","post","put","patch"],(e=>{bh.headers[e]={}}));const wh=bh,_h=Kd.toObjectSet(["age","authorization","content-length","content-type","etag","expires","from","host","if-modified-since","if-unmodified-since","last-modified","location","max-forwards","proxy-authorization","referer","retry-after","user-agent"]),xh=Symbol("internals");function Sh(e){return e&&String(e).trim().toLowerCase()}function Oh(e){return!1===e||null==e?e:Kd.isArray(e)?e.map(Oh):String(e)}function kh(e,t,n,r,o){return Kd.isFunction(r)?r.call(this,t,n):(o&&(t=n),Kd.isString(t)?Kd.isString(r)?-1!==t.indexOf(r):Kd.isRegExp(r)?r.test(t):void 0:void 0)}class Eh{constructor(e){e&&this.set(e)}set(e,t,n){const r=this;function o(e,t,n){const o=Sh(t);if(!o)throw new Error("header name must be a non-empty string");const i=Kd.findKey(r,o);(!i||void 0===r[i]||!0===n||void 0===n&&!1!==r[i])&&(r[i||t]=Oh(e))}const i=(e,t)=>Kd.forEach(e,((e,n)=>o(e,n,t)));return Kd.isPlainObject(e)||e instanceof this.constructor?i(e,t):Kd.isString(e)&&(e=e.trim())&&!/^[-_a-zA-Z0-9^`|~,!#$%&'*+.]+$/.test(e.trim())?i((e=>{const t={};let n,r,o;return e&&e.split("\n").forEach((function(e){o=e.indexOf(":"),n=e.substring(0,o).trim().toLowerCase(),r=e.substring(o+1).trim(),!n||t[n]&&_h[n]||("set-cookie"===n?t[n]?t[n].push(r):t[n]=[r]:t[n]=t[n]?t[n]+", "+r:r)})),t})(e),t):null!=e&&o(t,e,n),this}get(e,t){if(e=Sh(e)){const n=Kd.findKey(this,e);if(n){const e=this[n];if(!t)return e;if(!0===t)return function(e){const t=Object.create(null),n=/([^\s,;=]+)\s*(?:=\s*([^,;]+))?/g;let r;for(;r=n.exec(e);)t[r[1]]=r[2];return t}(e);if(Kd.isFunction(t))return t.call(this,e,n);if(Kd.isRegExp(t))return t.exec(e);throw new TypeError("parser must be boolean|regexp|function")}}}has(e,t){if(e=Sh(e)){const n=Kd.findKey(this,e);return!(!n||void 0===this[n]||t&&!kh(0,this[n],n,t))}return!1}delete(e,t){const n=this;let r=!1;function o(e){if(e=Sh(e)){const o=Kd.findKey(n,e);!o||t&&!kh(0,n[o],o,t)||(delete n[o],r=!0)}}return Kd.isArray(e)?e.forEach(o):o(e),r}clear(e){const t=Object.keys(this);let n=t.length,r=!1;for(;n--;){const o=t[n];e&&!kh(0,this[o],o,e,!0)||(delete this[o],r=!0)}return r}normalize(e){const t=this,n={};return Kd.forEach(this,((r,o)=>{const i=Kd.findKey(n,o);if(i)return t[i]=Oh(r),void delete t[o];const s=e?function(e){return e.trim().toLowerCase().replace(/([a-z\d])(\w*)/g,((e,t,n)=>t.toUpperCase()+n))}(o):String(o).trim();s!==o&&delete t[o],t[s]=Oh(r),n[s]=!0})),this}concat(...e){return this.constructor.concat(this,...e)}toJSON(e){const t=Object.create(null);return Kd.forEach(this,((n,r)=>{null!=n&&!1!==n&&(t[r]=e&&Kd.isArray(n)?n.join(", "):n)})),t}[Symbol.iterator](){return Object.entries(this.toJSON())[Symbol.iterator]()}toString(){return Object.entries(this.toJSON()).map((([e,t])=>e+": "+t)).join("\n")}get[Symbol.toStringTag](){return"AxiosHeaders"}static from(e){return e instanceof this?e:new this(e)}static concat(e,...t){const n=new this(e);return t.forEach((e=>n.set(e))),n}static accessor(e){const t=(this[xh]=this[xh]={accessors:{}}).accessors,n=this.prototype;function r(e){const r=Sh(e);t[r]||(!function(e,t){const n=Kd.toCamelCase(" "+t);["get","set","has"].forEach((r=>{Object.defineProperty(e,r+n,{value:function(e,n,o){return this[r].call(this,t,e,n,o)},configurable:!0})}))}(n,e),t[r]=!0)}return Kd.isArray(e)?e.forEach(r):r(e),this}}Eh.accessor(["Content-Type","Content-Length","Accept","Accept-Encoding","User-Agent","Authorization"]),Kd.reduceDescriptors(Eh.prototype,(({value:e},t)=>{let n=t[0].toUpperCase()+t.slice(1);return{get:()=>e,set(e){this[n]=e}}})),Kd.freezeMethods(Eh);const Ch=Eh;function Ph(e,t){const n=this||wh,r=t||n,o=Ch.from(r.headers);let i=r.data;return Kd.forEach(e,(function(e){i=e.call(n,i,o.normalize(),t?t.status:void 0)})),o.normalize(),i}function Th(e){return!(!e||!e.__CANCEL__)}function Ah(e,t,n){Qd.call(this,null==e?"canceled":e,Qd.ERR_CANCELED,t,n),this.name="CanceledError"}Kd.inherits(Ah,Qd,{__CANCEL__:!0});const jh=Ah;const Lh=mh.hasStandardBrowserEnv?{write(e,t,n,r,o,i){const s=[e+"="+encodeURIComponent(t)];Kd.isNumber(n)&&s.push("expires="+new Date(n).toGMTString()),Kd.isString(r)&&s.push("path="+r),Kd.isString(o)&&s.push("domain="+o),!0===i&&s.push("secure"),document.cookie=s.join("; ")},read(e){const t=document.cookie.match(new RegExp("(^|;\\s*)("+e+")=([^;]*)"));return t?decodeURIComponent(t[3]):null},remove(e){this.write(e,"",Date.now()-864e5)}}:{write(){},read:()=>null,remove(){}};function Rh(e,t){return e&&!/^([a-z][a-z\d+\-.]*:)?\/\//i.test(t)?function(e,t){return t?e.replace(/\/+$/,"")+"/"+t.replace(/^\/+/,""):e}(e,t):t}const Ih=mh.hasStandardBrowserEnv?function(){const e=/(msie|trident)/i.test(navigator.userAgent),t=document.createElement("a");let n;function r(n){let r=n;return e&&(t.setAttribute("href",r),r=t.href),t.setAttribute("href",r),{href:t.href,protocol:t.protocol?t.protocol.replace(/:$/,""):"",host:t.host,search:t.search?t.search.replace(/^\?/,""):"",hash:t.hash?t.hash.replace(/^#/,""):"",hostname:t.hostname,port:t.port,pathname:"/"===t.pathname.charAt(0)?t.pathname:"/"+t.pathname}}return n=r(window.location.href),function(e){const t=Kd.isString(e)?r(e):e;return t.protocol===n.protocol&&t.host===n.host}}():function(){return!0};const Fh=function(e,t){e=e||10;const n=new Array(e),r=new Array(e);let o,i=0,s=0;return t=void 0!==t?t:1e3,function(l){const a=Date.now(),u=r[s];o||(o=a),n[i]=l,r[i]=a;let c=s,f=0;for(;c!==i;)f+=n[c++],c%=e;if(i=(i+1)%e,i===s&&(s=(s+1)%e),a-o{const i=o.loaded,s=o.lengthComputable?o.total:void 0,l=i-n,a=r(l);n=i;const u={loaded:i,total:s,progress:s?i/s:void 0,bytes:l,rate:a||void 0,estimated:a&&s&&i<=s?(s-i)/a:void 0,event:o};u[t?"download":"upload"]=!0,e(u)}}const Mh="undefined"!=typeof XMLHttpRequest&&function(e){return new Promise((function(t,n){let r=e.data;const o=Ch.from(e.headers).normalize();let i,s,{responseType:l,withXSRFToken:a}=e;function u(){e.cancelToken&&e.cancelToken.unsubscribe(i),e.signal&&e.signal.removeEventListener("abort",i)}if(Kd.isFormData(r))if(mh.hasStandardBrowserEnv||mh.hasStandardBrowserWebWorkerEnv)o.setContentType(!1);else if(!1!==(s=o.getContentType())){const[e,...t]=s?s.split(";").map((e=>e.trim())).filter(Boolean):[];o.setContentType([e||"multipart/form-data",...t].join("; "))}let c=new XMLHttpRequest;if(e.auth){const t=e.auth.username||"",n=e.auth.password?unescape(encodeURIComponent(e.auth.password)):"";o.set("Authorization","Basic "+btoa(t+":"+n))}const f=Rh(e.baseURL,e.url);function p(){if(!c)return;const r=Ch.from("getAllResponseHeaders"in c&&c.getAllResponseHeaders());!function(e,t,n){const r=n.config.validateStatus;n.status&&r&&!r(n.status)?t(new Qd("Request failed with status code "+n.status,[Qd.ERR_BAD_REQUEST,Qd.ERR_BAD_RESPONSE][Math.floor(n.status/100)-4],n.config,n.request,n)):e(n)}((function(e){t(e),u()}),(function(e){n(e),u()}),{data:l&&"text"!==l&&"json"!==l?c.response:c.responseText,status:c.status,statusText:c.statusText,headers:r,config:e,request:c}),c=null}if(c.open(e.method.toUpperCase(),uh(f,e.params,e.paramsSerializer),!0),c.timeout=e.timeout,"onloadend"in c?c.onloadend=p:c.onreadystatechange=function(){c&&4===c.readyState&&(0!==c.status||c.responseURL&&0===c.responseURL.indexOf("file:"))&&setTimeout(p)},c.onabort=function(){c&&(n(new Qd("Request aborted",Qd.ECONNABORTED,e,c)),c=null)},c.onerror=function(){n(new Qd("Network Error",Qd.ERR_NETWORK,e,c)),c=null},c.ontimeout=function(){let t=e.timeout?"timeout of "+e.timeout+"ms exceeded":"timeout exceeded";const r=e.transitional||fh;e.timeoutErrorMessage&&(t=e.timeoutErrorMessage),n(new Qd(t,r.clarifyTimeoutError?Qd.ETIMEDOUT:Qd.ECONNABORTED,e,c)),c=null},mh.hasStandardBrowserEnv&&(a&&Kd.isFunction(a)&&(a=a(e)),a||!1!==a&&Ih(f))){const t=e.xsrfHeaderName&&e.xsrfCookieName&&Lh.read(e.xsrfCookieName);t&&o.set(e.xsrfHeaderName,t)}void 0===r&&o.setContentType(null),"setRequestHeader"in c&&Kd.forEach(o.toJSON(),(function(e,t){c.setRequestHeader(t,e)})),Kd.isUndefined(e.withCredentials)||(c.withCredentials=!!e.withCredentials),l&&"json"!==l&&(c.responseType=e.responseType),"function"==typeof e.onDownloadProgress&&c.addEventListener("progress",Nh(e.onDownloadProgress,!0)),"function"==typeof e.onUploadProgress&&c.upload&&c.upload.addEventListener("progress",Nh(e.onUploadProgress)),(e.cancelToken||e.signal)&&(i=t=>{c&&(n(!t||t.type?new jh(null,e,c):t),c.abort(),c=null)},e.cancelToken&&e.cancelToken.subscribe(i),e.signal&&(e.signal.aborted?i():e.signal.addEventListener("abort",i)));const d=function(e){const t=/^([-+\w]{1,25})(:?\/\/|:)/.exec(e);return t&&t[1]||""}(f);d&&-1===mh.protocols.indexOf(d)?n(new Qd("Unsupported protocol "+d+":",Qd.ERR_BAD_REQUEST,e)):c.send(r||null)}))},Dh={http:null,xhr:Mh};Kd.forEach(Dh,((e,t)=>{if(e){try{Object.defineProperty(e,"name",{value:t})}catch(e){}Object.defineProperty(e,"adapterName",{value:t})}}));const Bh=e=>`- ${e}`,Uh=e=>Kd.isFunction(e)||null===e||!1===e,$h=e=>{e=Kd.isArray(e)?e:[e];const{length:t}=e;let n,r;const o={};for(let i=0;i`adapter ${e} `+(!1===t?"is not supported by the environment":"is not available in the build")));let n=t?e.length>1?"since :\n"+e.map(Bh).join("\n"):" "+Bh(e[0]):"as no adapter specified";throw new Qd("There is no suitable adapter to dispatch the request "+n,"ERR_NOT_SUPPORT")}return r};function Vh(e){if(e.cancelToken&&e.cancelToken.throwIfRequested(),e.signal&&e.signal.aborted)throw new jh(null,e)}function Hh(e){Vh(e),e.headers=Ch.from(e.headers),e.data=Ph.call(e,e.transformRequest),-1!==["post","put","patch"].indexOf(e.method)&&e.headers.setContentType("application/x-www-form-urlencoded",!1);return $h(e.adapter||wh.adapter)(e).then((function(t){return Vh(e),t.data=Ph.call(e,e.transformResponse,t),t.headers=Ch.from(t.headers),t}),(function(t){return Th(t)||(Vh(e),t&&t.response&&(t.response.data=Ph.call(e,e.transformResponse,t.response),t.response.headers=Ch.from(t.response.headers))),Promise.reject(t)}))}const zh=e=>e instanceof Ch?e.toJSON():e;function qh(e,t){t=t||{};const n={};function r(e,t,n){return Kd.isPlainObject(e)&&Kd.isPlainObject(t)?Kd.merge.call({caseless:n},e,t):Kd.isPlainObject(t)?Kd.merge({},t):Kd.isArray(t)?t.slice():t}function o(e,t,n){return Kd.isUndefined(t)?Kd.isUndefined(e)?void 0:r(void 0,e,n):r(e,t,n)}function i(e,t){if(!Kd.isUndefined(t))return r(void 0,t)}function s(e,t){return Kd.isUndefined(t)?Kd.isUndefined(e)?void 0:r(void 0,e):r(void 0,t)}function l(n,o,i){return i in t?r(n,o):i in e?r(void 0,n):void 0}const a={url:i,method:i,data:i,baseURL:s,transformRequest:s,transformResponse:s,paramsSerializer:s,timeout:s,timeoutMessage:s,withCredentials:s,withXSRFToken:s,adapter:s,responseType:s,xsrfCookieName:s,xsrfHeaderName:s,onUploadProgress:s,onDownloadProgress:s,decompress:s,maxContentLength:s,maxBodyLength:s,beforeRedirect:s,transport:s,httpAgent:s,httpsAgent:s,cancelToken:s,socketPath:s,responseEncoding:s,validateStatus:l,headers:(e,t)=>o(zh(e),zh(t),!0)};return Kd.forEach(Object.keys(Object.assign({},e,t)),(function(r){const i=a[r]||o,s=i(e[r],t[r],r);Kd.isUndefined(s)&&i!==l||(n[r]=s)})),n}const Wh="1.6.2",Kh={};["object","boolean","number","function","string","symbol"].forEach(((e,t)=>{Kh[e]=function(n){return typeof n===e||"a"+(t<1?"n ":" ")+e}}));const Gh={};Kh.transitional=function(e,t,n){return(r,o,i)=>{if(!1===e)throw new Qd(function(e,t){return"[Axios v1.6.2] Transitional option '"+e+"'"+t+(n?". "+n:"")}(o," has been removed"+(t?" in "+t:"")),Qd.ERR_DEPRECATED);return t&&!Gh[o]&&(Gh[o]=!0),!e||e(r,o,i)}};const Yh={assertOptions:function(e,t,n){if("object"!=typeof e)throw new Qd("options must be an object",Qd.ERR_BAD_OPTION_VALUE);const r=Object.keys(e);let o=r.length;for(;o-- >0;){const i=r[o],s=t[i];if(s){const t=e[i],n=void 0===t||s(t,i,e);if(!0!==n)throw new Qd("option "+i+" must be "+n,Qd.ERR_BAD_OPTION_VALUE)}else if(!0!==n)throw new Qd("Unknown option "+i,Qd.ERR_BAD_OPTION)}},validators:Kh},Jh=Yh.validators;class Qh{constructor(e){this.defaults=e,this.interceptors={request:new ch,response:new ch}}request(e,t){"string"==typeof e?(t=t||{}).url=e:t=e||{},t=qh(this.defaults,t);const{transitional:n,paramsSerializer:r,headers:o}=t;void 0!==n&&Yh.assertOptions(n,{silentJSONParsing:Jh.transitional(Jh.boolean),forcedJSONParsing:Jh.transitional(Jh.boolean),clarifyTimeoutError:Jh.transitional(Jh.boolean)},!1),null!=r&&(Kd.isFunction(r)?t.paramsSerializer={serialize:r}:Yh.assertOptions(r,{encode:Jh.function,serialize:Jh.function},!0)),t.method=(t.method||this.defaults.method||"get").toLowerCase();let i=o&&Kd.merge(o.common,o[t.method]);o&&Kd.forEach(["delete","get","head","post","put","patch","common"],(e=>{delete o[e]})),t.headers=Ch.concat(i,o);const s=[];let l=!0;this.interceptors.request.forEach((function(e){"function"==typeof e.runWhen&&!1===e.runWhen(t)||(l=l&&e.synchronous,s.unshift(e.fulfilled,e.rejected))}));const a=[];let u;this.interceptors.response.forEach((function(e){a.push(e.fulfilled,e.rejected)}));let c,f=0;if(!l){const e=[Hh.bind(this),void 0];for(e.unshift.apply(e,s),e.push.apply(e,a),c=e.length,u=Promise.resolve(t);f{if(!n._listeners)return;let t=n._listeners.length;for(;t-- >0;)n._listeners[t](e);n._listeners=null})),this.promise.then=e=>{let t;const r=new Promise((e=>{n.subscribe(e),t=e})).then(e);return r.cancel=function(){n.unsubscribe(t)},r},e((function(e,r,o){n.reason||(n.reason=new jh(e,r,o),t(n.reason))}))}throwIfRequested(){if(this.reason)throw this.reason}subscribe(e){this.reason?e(this.reason):this._listeners?this._listeners.push(e):this._listeners=[e]}unsubscribe(e){if(!this._listeners)return;const t=this._listeners.indexOf(e);-1!==t&&this._listeners.splice(t,1)}static source(){let e;const t=new Xh((function(t){e=t}));return{token:t,cancel:e}}}const ev=Xh;const tv={Continue:100,SwitchingProtocols:101,Processing:102,EarlyHints:103,Ok:200,Created:201,Accepted:202,NonAuthoritativeInformation:203,NoContent:204,ResetContent:205,PartialContent:206,MultiStatus:207,AlreadyReported:208,ImUsed:226,MultipleChoices:300,MovedPermanently:301,Found:302,SeeOther:303,NotModified:304,UseProxy:305,Unused:306,TemporaryRedirect:307,PermanentRedirect:308,BadRequest:400,Unauthorized:401,PaymentRequired:402,Forbidden:403,NotFound:404,MethodNotAllowed:405,NotAcceptable:406,ProxyAuthenticationRequired:407,RequestTimeout:408,Conflict:409,Gone:410,LengthRequired:411,PreconditionFailed:412,PayloadTooLarge:413,UriTooLong:414,UnsupportedMediaType:415,RangeNotSatisfiable:416,ExpectationFailed:417,ImATeapot:418,MisdirectedRequest:421,UnprocessableEntity:422,Locked:423,FailedDependency:424,TooEarly:425,UpgradeRequired:426,PreconditionRequired:428,TooManyRequests:429,RequestHeaderFieldsTooLarge:431,UnavailableForLegalReasons:451,InternalServerError:500,NotImplemented:501,BadGateway:502,ServiceUnavailable:503,GatewayTimeout:504,HttpVersionNotSupported:505,VariantAlsoNegotiates:506,InsufficientStorage:507,LoopDetected:508,NotExtended:510,NetworkAuthenticationRequired:511};Object.entries(tv).forEach((([e,t])=>{tv[t]=e}));const nv=tv;const rv=function e(t){const n=new Zh(t),r=dd(Zh.prototype.request,n);return Kd.extend(r,Zh.prototype,n,{allOwnKeys:!0}),Kd.extend(r,n,null,{allOwnKeys:!0}),r.create=function(n){return e(qh(t,n))},r}(wh);rv.Axios=Zh,rv.CanceledError=jh,rv.CancelToken=ev,rv.isCancel=Th,rv.VERSION=Wh,rv.toFormData=rh,rv.AxiosError=Qd,rv.Cancel=rv.CanceledError,rv.all=function(e){return Promise.all(e)},rv.spread=function(e){return function(t){return e.apply(null,t)}},rv.isAxiosError=function(e){return Kd.isObject(e)&&!0===e.isAxiosError},rv.mergeConfig=qh,rv.AxiosHeaders=Ch,rv.formToJSON=e=>yh(Kd.isHTMLForm(e)?new FormData(e):e),rv.getAdapter=$h,rv.HttpStatusCode=nv,rv.default=rv;const ov=rv,iv="undefined"!=typeof window;function sv(e){return e.__esModule||"Module"===e[Symbol.toStringTag]}const lv=Object.assign;function av(e,t){const n={};for(const r in t){const o=t[r];n[r]=cv(o)?o.map(e):e(o)}return n}const uv=()=>{},cv=Array.isArray;const fv=/\/$/,pv=e=>e.replace(fv,"");function dv(e,t,n="/"){let r,o={},i="",s="";const l=t.indexOf("#");let a=t.indexOf("?");return l=0&&(a=-1),a>-1&&(r=t.slice(0,a),i=t.slice(a+1,l>-1?l:t.length),o=e(i)),l>-1&&(r=r||t.slice(0,l),s=t.slice(l,t.length)),r=function(e,t){if(e.startsWith("/"))return e;0;if(!e)return t;const n=t.split("/"),r=e.split("/"),o=r[r.length-1];".."!==o&&"."!==o||r.push("");let i,s,l=n.length-1;for(i=0;i1&&l--}return n.slice(0,l).join("/")+"/"+r.slice(i-(i===r.length?1:0)).join("/")}(null!=r?r:t,n),{fullPath:r+(i&&"?")+i+s,path:r,query:o,hash:s}}function hv(e,t){return t&&e.toLowerCase().startsWith(t.toLowerCase())?e.slice(t.length)||"/":e}function vv(e,t){return(e.aliasOf||e)===(t.aliasOf||t)}function gv(e,t){if(Object.keys(e).length!==Object.keys(t).length)return!1;for(const n in e)if(!mv(e[n],t[n]))return!1;return!0}function mv(e,t){return cv(e)?yv(e,t):cv(t)?yv(t,e):e===t}function yv(e,t){return cv(t)?e.length===t.length&&e.every(((e,n)=>e===t[n])):1===e.length&&e[0]===t}var bv,wv;!function(e){e.pop="pop",e.push="push"}(bv||(bv={})),function(e){e.back="back",e.forward="forward",e.unknown=""}(wv||(wv={}));function _v(e){if(!e)if(iv){const t=document.querySelector("base");e=(e=t&&t.getAttribute("href")||"/").replace(/^\w+:\/\/[^\/]+/,"")}else e="/";return"/"!==e[0]&&"#"!==e[0]&&(e="/"+e),pv(e)}const xv=/^[^#]+#/;function Sv(e,t){return e.replace(xv,"#")+t}const Ov=()=>({left:window.pageXOffset,top:window.pageYOffset});function kv(e){let t;if("el"in e){const n=e.el,r="string"==typeof n&&n.startsWith("#");0;const o="string"==typeof n?r?document.getElementById(n.slice(1)):document.querySelector(n):n;if(!o)return;t=function(e,t){const n=document.documentElement.getBoundingClientRect(),r=e.getBoundingClientRect();return{behavior:t.behavior,left:r.left-n.left-(t.left||0),top:r.top-n.top-(t.top||0)}}(o,e)}else t=e;"scrollBehavior"in document.documentElement.style?window.scrollTo(t):window.scrollTo(null!=t.left?t.left:window.pageXOffset,null!=t.top?t.top:window.pageYOffset)}function Ev(e,t){return(history.state?history.state.position-t:-1)+e}const Cv=new Map;let Pv=()=>location.protocol+"//"+location.host;function Tv(e,t){const{pathname:n,search:r,hash:o}=t,i=e.indexOf("#");if(i>-1){let t=o.includes(e.slice(i))?e.slice(i).length:1,n=o.slice(t);return"/"!==n[0]&&(n="/"+n),hv(n,"")}return hv(n,e)+r+o}function Av(e,t,n,r=!1,o=!1){return{back:e,current:t,forward:n,replaced:r,position:window.history.length,scroll:o?Ov():null}}function jv(e){const t=function(e){const{history:t,location:n}=window,r={value:Tv(e,n)},o={value:t.state};function i(r,i,s){const l=e.indexOf("#"),a=l>-1?(n.host&&document.querySelector("base")?e:e.slice(l))+r:Pv()+e+r;try{t[s?"replaceState":"pushState"](i,"",a),o.value=i}catch(e){n[s?"replace":"assign"](a)}}return o.value||i(r.value,{back:null,current:r.value,forward:null,position:t.length-1,replaced:!0,scroll:null},!0),{location:r,state:o,push:function(e,n){const s=lv({},o.value,t.state,{forward:e,scroll:Ov()});i(s.current,s,!0),i(e,lv({},Av(r.value,e,null),{position:s.position+1},n),!1),r.value=e},replace:function(e,n){i(e,lv({},t.state,Av(o.value.back,e,o.value.forward,!0),n,{position:o.value.position}),!0),r.value=e}}}(e=_v(e)),n=function(e,t,n,r){let o=[],i=[],s=null;const l=({state:i})=>{const l=Tv(e,location),a=n.value,u=t.value;let c=0;if(i){if(n.value=l,t.value=i,s&&s===a)return void(s=null);c=u?i.position-u.position:0}else r(l);o.forEach((e=>{e(n.value,a,{delta:c,type:bv.pop,direction:c?c>0?wv.forward:wv.back:wv.unknown})}))};function a(){const{history:e}=window;e.state&&e.replaceState(lv({},e.state,{scroll:Ov()}),"")}return window.addEventListener("popstate",l),window.addEventListener("beforeunload",a,{passive:!0}),{pauseListeners:function(){s=n.value},listen:function(e){o.push(e);const t=()=>{const t=o.indexOf(e);t>-1&&o.splice(t,1)};return i.push(t),t},destroy:function(){for(const e of i)e();i=[],window.removeEventListener("popstate",l),window.removeEventListener("beforeunload",a)}}}(e,t.state,t.location,t.replace);const r=lv({location:"",base:e,go:function(e,t=!0){t||n.pauseListeners(),history.go(e)},createHref:Sv.bind(null,e)},t,n);return Object.defineProperty(r,"location",{enumerable:!0,get:()=>t.location.value}),Object.defineProperty(r,"state",{enumerable:!0,get:()=>t.state.value}),r}function Lv(e){return"string"==typeof e||"symbol"==typeof e}const Rv={path:"/",name:void 0,params:{},query:{},hash:"",fullPath:"/",matched:[],meta:{},redirectedFrom:void 0},Iv=Symbol("");var Fv;!function(e){e[e.aborted=4]="aborted",e[e.cancelled=8]="cancelled",e[e.duplicated=16]="duplicated"}(Fv||(Fv={}));function Nv(e,t){return lv(new Error,{type:e,[Iv]:!0},t)}function Mv(e,t){return e instanceof Error&&Iv in e&&(null==t||!!(e.type&t))}const Dv="[^/]+?",Bv={sensitive:!1,strict:!1,start:!0,end:!0},Uv=/[.+*?^${}()[\]/\\]/g;function $v(e,t){let n=0;for(;nt.length?1===t.length&&80===t[0]?1:-1:0}function Vv(e,t){let n=0;const r=e.score,o=t.score;for(;n0&&t[t.length-1]<0}const zv={type:0,value:""},qv=/[a-zA-Z0-9_]/;function Wv(e,t,n){const r=function(e,t){const n=lv({},Bv,t),r=[];let o=n.start?"^":"";const i=[];for(const t of e){const e=t.length?[]:[90];n.strict&&!t.length&&(o+="/");for(let r=0;r1&&("*"===l||"+"===l)&&t(`A repeatable param (${u}) must be alone in its segment. eg: '/:ids+.`),i.push({type:1,value:u,regexp:c,repeatable:"*"===l||"+"===l,optional:"*"===l||"?"===l})):t("Invalid state to consume buffer"),u="")}function p(){u+=l}for(;a{i(p)}:uv}function i(e){if(Lv(e)){const t=r.get(e);t&&(r.delete(e),n.splice(n.indexOf(t),1),t.children.forEach(i),t.alias.forEach(i))}else{const t=n.indexOf(e);t>-1&&(n.splice(t,1),e.record.name&&r.delete(e.record.name),e.children.forEach(i),e.alias.forEach(i))}}function s(e){let t=0;for(;t=0&&(e.record.path!==n[t].record.path||!Xv(e,n[t]));)t++;n.splice(t,0,e),e.record.name&&!Jv(e)&&r.set(e.record.name,e)}return t=Zv({strict:!1,end:!0,sensitive:!1},t),e.forEach((e=>o(e))),{addRoute:o,resolve:function(e,t){let o,i,s,l={};if("name"in e&&e.name){if(o=r.get(e.name),!o)throw Nv(1,{location:e});0,s=o.record.name,l=lv(Gv(t.params,o.keys.filter((e=>!e.optional)).map((e=>e.name))),e.params&&Gv(e.params,o.keys.map((e=>e.name)))),i=o.stringify(l)}else if("path"in e)i=e.path,o=n.find((e=>e.re.test(i))),o&&(l=o.parse(i),s=o.record.name);else{if(o=t.name?r.get(t.name):n.find((e=>e.re.test(t.path))),!o)throw Nv(1,{location:e,currentLocation:t});s=o.record.name,l=lv({},t.params,e.params),i=o.stringify(l)}const a=[];let u=o;for(;u;)a.unshift(u.record),u=u.parent;return{name:s,path:i,params:l,matched:a,meta:Qv(a)}},removeRoute:i,getRoutes:function(){return n},getRecordMatcher:function(e){return r.get(e)}}}function Gv(e,t){const n={};for(const r of t)r in e&&(n[r]=e[r]);return n}function Yv(e){const t={},n=e.props||!1;if("component"in e)t.default=n;else for(const r in e.components)t[r]="object"==typeof n?n[r]:n;return t}function Jv(e){for(;e;){if(e.record.aliasOf)return!0;e=e.parent}return!1}function Qv(e){return e.reduce(((e,t)=>lv(e,t.meta)),{})}function Zv(e,t){const n={};for(const r in e)n[r]=r in t?t[r]:e[r];return n}function Xv(e,t){return t.children.some((t=>t===e||Xv(e,t)))}const eg=/#/g,tg=/&/g,ng=/\//g,rg=/=/g,og=/\?/g,ig=/\+/g,sg=/%5B/g,lg=/%5D/g,ag=/%5E/g,ug=/%60/g,cg=/%7B/g,fg=/%7C/g,pg=/%7D/g,dg=/%20/g;function hg(e){return encodeURI(""+e).replace(fg,"|").replace(sg,"[").replace(lg,"]")}function vg(e){return hg(e).replace(ig,"%2B").replace(dg,"+").replace(eg,"%23").replace(tg,"%26").replace(ug,"`").replace(cg,"{").replace(pg,"}").replace(ag,"^")}function gg(e){return null==e?"":function(e){return hg(e).replace(eg,"%23").replace(og,"%3F")}(e).replace(ng,"%2F")}function mg(e){try{return decodeURIComponent(""+e)}catch(e){}return""+e}function yg(e){const t={};if(""===e||"?"===e)return t;const n=("?"===e[0]?e.slice(1):e).split("&");for(let e=0;ee&&vg(e))):[r&&vg(r)];o.forEach((e=>{void 0!==e&&(t+=(t.length?"&":"")+n,null!=e&&(t+="="+e))}))}return t}function wg(e){const t={};for(const n in e){const r=e[n];void 0!==r&&(t[n]=cv(r)?r.map((e=>null==e?null:""+e)):null==r?r:""+r)}return t}const _g=Symbol(""),xg=Symbol(""),Sg=Symbol(""),Og=Symbol(""),kg=Symbol("");function Eg(){let e=[];return{add:function(t){return e.push(t),()=>{const n=e.indexOf(t);n>-1&&e.splice(n,1)}},list:()=>e.slice(),reset:function(){e=[]}}}function Cg(e,t,n,r,o){const i=r&&(r.enterCallbacks[o]=r.enterCallbacks[o]||[]);return()=>new Promise(((s,l)=>{const a=e=>{var a;!1===e?l(Nv(4,{from:n,to:t})):e instanceof Error?l(e):"string"==typeof(a=e)||a&&"object"==typeof a?l(Nv(2,{from:t,to:e})):(i&&r.enterCallbacks[o]===i&&"function"==typeof e&&i.push(e),s())},u=e.call(r&&r.instances[o],t,n,a);let c=Promise.resolve(u);e.length<3&&(c=c.then(a)),c.catch((e=>l(e)))}))}function Pg(e,t,n,r){const o=[];for(const s of e){0;for(const e in s.components){let l=s.components[e];if("beforeRouteEnter"===t||s.instances[e])if("object"==typeof(i=l)||"displayName"in i||"props"in i||"__vccOpts"in i){const i=(l.__vccOpts||l)[t];i&&o.push(Cg(i,n,r,s,e))}else{let i=l();0,o.push((()=>i.then((o=>{if(!o)return Promise.reject(new Error(`Couldn't resolve component "${e}" at "${s.path}"`));const i=sv(o)?o.default:o;s.components[e]=i;const l=(i.__vccOpts||i)[t];return l&&Cg(l,n,r,s,e)()}))))}}}var i;return o}function Tg(e){const t=Yo(Sg),n=Yo(Og),r=Ls((()=>t.resolve(Wt(e.to)))),o=Ls((()=>{const{matched:e}=r.value,{length:t}=e,o=e[t-1],i=n.matched;if(!o||!i.length)return-1;const s=i.findIndex(vv.bind(null,o));if(s>-1)return s;const l=jg(e[t-2]);return t>1&&jg(o)===l&&i[i.length-1].path!==l?i.findIndex(vv.bind(null,e[t-2])):s})),i=Ls((()=>o.value>-1&&function(e,t){for(const n in t){const r=t[n],o=e[n];if("string"==typeof r){if(r!==o)return!1}else if(!cv(o)||o.length!==r.length||r.some(((e,t)=>e!==o[t])))return!1}return!0}(n.params,r.value.params))),s=Ls((()=>o.value>-1&&o.value===n.matched.length-1&&gv(n.params,r.value.params)));return{route:r,href:Ls((()=>r.value.href)),isActive:i,isExactActive:s,navigate:function(n={}){return function(e){if(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)return;if(e.defaultPrevented)return;if(void 0!==e.button&&0!==e.button)return;if(e.currentTarget&&e.currentTarget.getAttribute){const t=e.currentTarget.getAttribute("target");if(/\b_blank\b/i.test(t))return}e.preventDefault&&e.preventDefault();return!0}(n)?t[Wt(e.replace)?"replace":"push"](Wt(e.to)).catch(uv):Promise.resolve()}}}const Ag=Rr({name:"RouterLink",compatConfig:{MODE:3},props:{to:{type:[String,Object],required:!0},replace:Boolean,activeClass:String,exactActiveClass:String,custom:Boolean,ariaCurrentValue:{type:String,default:"page"}},useLink:Tg,setup(e,{slots:t}){const n=kt(Tg(e)),{options:r}=Yo(Sg),o=Ls((()=>({[Lg(e.activeClass,r.linkActiveClass,"router-link-active")]:n.isActive,[Lg(e.exactActiveClass,r.linkExactActiveClass,"router-link-exact-active")]:n.isExactActive})));return()=>{const r=t.default&&t.default(n);return e.custom?r:Rs("a",{"aria-current":n.isExactActive?e.ariaCurrentValue:null,href:n.href,onClick:n.navigate,class:o.value},r)}}});function jg(e){return e?e.aliasOf?e.aliasOf.path:e.path:""}const Lg=(e,t,n)=>null!=e?e:null!=t?t:n;function Rg(e,t){if(!e)return null;const n=e(t);return 1===n.length?n[0]:n}const Ig=Rr({name:"RouterView",inheritAttrs:!1,props:{name:{type:String,default:"default"},route:Object},compatConfig:{MODE:3},setup(e,{attrs:t,slots:n}){const r=Yo(kg),o=Ls((()=>e.route||r.value)),i=Yo(xg,0),s=Ls((()=>{let e=Wt(i);const{matched:t}=o.value;let n;for(;(n=t[e])&&!n.components;)e++;return e})),l=Ls((()=>o.value.matched[s.value]));Go(xg,Ls((()=>s.value+1))),Go(_g,l),Go(kg,o);const a=$t();return dr((()=>[a.value,l.value,e.name]),(([e,t,n],[r,o,i])=>{t&&(t.instances[n]=e,o&&o!==t&&e&&e===r&&(t.leaveGuards.size||(t.leaveGuards=o.leaveGuards),t.updateGuards.size||(t.updateGuards=o.updateGuards))),!e||!t||o&&vv(t,o)&&r||(t.enterCallbacks[n]||[]).forEach((t=>t(e)))}),{flush:"post"}),()=>{const r=o.value,i=e.name,s=l.value,u=s&&s.components[i];if(!u)return Rg(n.default,{Component:u,route:r});const c=s.props[i],f=c?!0===c?r.params:"function"==typeof c?c(r):c:null,p=Rs(u,lv({},f,t,{onVnodeUnmounted:e=>{e.component.isUnmounted&&(s.instances[i]=null)},ref:a}));return Rg(n.default,{Component:p,route:r})||p}}});function Fg(){return Yo(Sg)}function Ng(){return Yo(Og)}function Mg(e,t,...n){if(e in t){let r=t[e];return"function"==typeof r?r(...n):r}let r=new Error(`Tried to handle "${e}" but there is no handler defined. Only defined handlers are: ${Object.keys(t).map((e=>`"${e}"`)).join(", ")}.`);throw Error.captureStackTrace&&Error.captureStackTrace(r,Mg),r}var Dg=(e=>(e[e.None=0]="None",e[e.RenderStrategy=1]="RenderStrategy",e[e.Static=2]="Static",e))(Dg||{}),Bg=(e=>(e[e.Unmount=0]="Unmount",e[e.Hidden=1]="Hidden",e))(Bg||{});function Ug({visible:e=!0,features:t=0,ourProps:n,theirProps:r,...o}){var i;let s=Hg(r,n),l=Object.assign(o,{props:s});if(e||2&t&&s.static)return $g(l);if(1&t){return Mg(null==(i=s.unmount)||i?0:1,{0:()=>null,1:()=>$g({...o,props:{...s,hidden:!0,style:{display:"none"}}})})}return $g(l)}function $g({props:e,attrs:t,slots:n,slot:r,name:o}){var i,s;let{as:l,...a}=zg(e,["unmount","static"]),u=null==(i=n.default)?void 0:i.call(n,r),c={};if(r){let e=!1,t=[];for(let[n,o]of Object.entries(r))"boolean"==typeof o&&(e=!0),!0===o&&t.push(n);e&&(c["data-headlessui-state"]=t.join(" "))}if("template"===l){if(u=Vg(null!=u?u:[]),Object.keys(a).length>0||Object.keys(t).length>0){let[e,...n]=null!=u?u:[];if(!function(e){return null!=e&&("string"==typeof e.type||"object"==typeof e.type||"function"==typeof e.type)}(e)||n.length>0)throw new Error(['Passing props on "template"!',"",`The current component <${o} /> is rendering a "template".`,"However we need to passthrough the following props:",Object.keys(a).concat(Object.keys(t)).map((e=>e.trim())).filter(((e,t,n)=>n.indexOf(e)===t)).sort(((e,t)=>e.localeCompare(t))).map((e=>` - ${e}`)).join("\n"),"","You can apply a few solutions:",['Add an `as="..."` prop, to ensure that we render an actual element instead of a "template".',"Render a single element as the child so that we can forward the props onto that element."].map((e=>` - ${e}`)).join("\n")].join("\n"));let r=Hg(null!=(s=e.props)?s:{},a),i=es(e,r);for(let e in r)e.startsWith("on")&&(i.props||(i.props={}),i.props[e]=r[e]);return i}return Array.isArray(u)&&1===u.length?u[0]:u}return Rs(l,Object.assign({},a,c),{default:()=>u})}function Vg(e){return e.flatMap((e=>e.type===Ai?Vg(e.children):[e]))}function Hg(...e){if(0===e.length)return{};if(1===e.length)return e[0];let t={},n={};for(let r of e)for(let e in r)e.startsWith("on")&&"function"==typeof r[e]?(null!=n[e]||(n[e]=[]),n[e].push(r[e])):t[e]=r[e];if(t.disabled||t["aria-disabled"])return Object.assign(t,Object.fromEntries(Object.keys(n).map((e=>[e,void 0]))));for(let e in n)Object.assign(t,{[e](t,...r){let o=n[e];for(let e of o){if(t instanceof Event&&t.defaultPrevented)return;e(t,...r)}}});return t}function zg(e,t=[]){let n=Object.assign({},e);for(let e of t)e in n&&delete n[e];return n}let qg=0;function Wg(){return++qg}var Kg=(e=>(e.Space=" ",e.Enter="Enter",e.Escape="Escape",e.Backspace="Backspace",e.Delete="Delete",e.ArrowLeft="ArrowLeft",e.ArrowUp="ArrowUp",e.ArrowRight="ArrowRight",e.ArrowDown="ArrowDown",e.Home="Home",e.End="End",e.PageUp="PageUp",e.PageDown="PageDown",e.Tab="Tab",e))(Kg||{});var Gg=(e=>(e[e.First=0]="First",e[e.Previous=1]="Previous",e[e.Next=2]="Next",e[e.Last=3]="Last",e[e.Specific=4]="Specific",e[e.Nothing=5]="Nothing",e))(Gg||{});function Yg(e,t){let n=t.resolveItems();if(n.length<=0)return null;let r=t.resolveActiveIndex(),o=null!=r?r:-1,i=(()=>{switch(e.focus){case 0:return n.findIndex((e=>!t.resolveDisabled(e)));case 1:{let e=n.slice().reverse().findIndex(((e,n,r)=>!(-1!==o&&r.length-n-1>=o)&&!t.resolveDisabled(e)));return-1===e?e:n.length-1-e}case 2:return n.findIndex(((e,n)=>!(n<=o)&&!t.resolveDisabled(e)));case 3:{let e=n.slice().reverse().findIndex((e=>!t.resolveDisabled(e)));return-1===e?e:n.length-1-e}case 4:return n.findIndex((n=>t.resolveId(n)===e.id));case 5:return null;default:!function(e){throw new Error("Unexpected object: "+e)}(e)}})();return-1===i?r:i}function Jg(e){var t;return null==e||null==e.value?null:null!=(t=e.value.$el)?t:e.value}var Qg=Object.defineProperty,Zg=(e,t,n)=>(((e,t,n)=>{t in e?Qg(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n})(e,"symbol"!=typeof t?t+"":t,n),n);let Xg=new class{constructor(){Zg(this,"current",this.detect()),Zg(this,"currentId",0)}set(e){this.current!==e&&(this.currentId=0,this.current=e)}reset(){this.set(this.detect())}nextId(){return++this.currentId}get isServer(){return"server"===this.current}get isClient(){return"client"===this.current}detect(){return"undefined"==typeof window||"undefined"==typeof document?"server":"client"}};function em(e){if(Xg.isServer)return null;if(e instanceof Node)return e.ownerDocument;if(null!=e&&e.hasOwnProperty("value")){let t=Jg(e);if(t)return t.ownerDocument}return document}let tm=Symbol("Context");var nm=(e=>(e[e.Open=1]="Open",e[e.Closed=2]="Closed",e[e.Closing=4]="Closing",e[e.Opening=8]="Opening",e))(nm||{});function rm(){return Yo(tm,null)}function om(e){Go(tm,e)}function im(e,t){if(e)return e;let n=null!=t?t:"button";return"string"==typeof n&&"button"===n.toLowerCase()?"button":void 0}function sm(e,t){let n=$t(im(e.value.type,e.value.as));return Jr((()=>{n.value=im(e.value.type,e.value.as)})),ur((()=>{var e;n.value||Jg(t)&&Jg(t)instanceof HTMLButtonElement&&(null==(e=Jg(t))||!e.hasAttribute("type"))&&(n.value="button")})),n}let lm=["[contentEditable=true]","[tabindex]","a[href]","area[href]","button:not([disabled])","iframe","input:not([disabled])","select:not([disabled])","textarea:not([disabled])"].map((e=>`${e}:not([tabindex='-1'])`)).join(",");var am=(e=>(e[e.First=1]="First",e[e.Previous=2]="Previous",e[e.Next=4]="Next",e[e.Last=8]="Last",e[e.WrapAround=16]="WrapAround",e[e.NoScroll=32]="NoScroll",e))(am||{}),um=(e=>(e[e.Error=0]="Error",e[e.Overflow=1]="Overflow",e[e.Success=2]="Success",e[e.Underflow=3]="Underflow",e))(um||{}),cm=(e=>(e[e.Previous=-1]="Previous",e[e.Next=1]="Next",e))(cm||{});function fm(e=document.body){return null==e?[]:Array.from(e.querySelectorAll(lm)).sort(((e,t)=>Math.sign((e.tabIndex||Number.MAX_SAFE_INTEGER)-(t.tabIndex||Number.MAX_SAFE_INTEGER))))}var pm=(e=>(e[e.Strict=0]="Strict",e[e.Loose=1]="Loose",e))(pm||{});function dm(e,t=0){var n;return e!==(null==(n=em(e))?void 0:n.body)&&Mg(t,{0:()=>e.matches(lm),1(){let t=e;for(;null!==t;){if(t.matches(lm))return!0;t=t.parentElement}return!1}})}function hm(e){let t=em(e);bn((()=>{t&&!dm(t.activeElement,0)&&gm(e)}))}var vm=(e=>(e[e.Keyboard=0]="Keyboard",e[e.Mouse=1]="Mouse",e))(vm||{});function gm(e){null==e||e.focus({preventScroll:!0})}"undefined"!=typeof window&&"undefined"!=typeof document&&(document.addEventListener("keydown",(e=>{e.metaKey||e.altKey||e.ctrlKey||(document.documentElement.dataset.headlessuiFocusVisible="")}),!0),document.addEventListener("click",(e=>{1===e.detail?delete document.documentElement.dataset.headlessuiFocusVisible:0===e.detail&&(document.documentElement.dataset.headlessuiFocusVisible="")}),!0));let mm=["textarea","input"].join(",");function ym(e,t=(e=>e)){return e.slice().sort(((e,n)=>{let r=t(e),o=t(n);if(null===r||null===o)return 0;let i=r.compareDocumentPosition(o);return i&Node.DOCUMENT_POSITION_FOLLOWING?-1:i&Node.DOCUMENT_POSITION_PRECEDING?1:0}))}function bm(e,t,{sorted:n=!0,relativeTo:r=null,skipElements:o=[]}={}){var i;let s=null!=(i=Array.isArray(e)?e.length>0?e[0].ownerDocument:document:null==e?void 0:e.ownerDocument)?i:document,l=Array.isArray(e)?n?ym(e):e:fm(e);o.length>0&&l.length>1&&(l=l.filter((e=>!o.includes(e)))),r=null!=r?r:s.activeElement;let a,u=(()=>{if(5&t)return 1;if(10&t)return-1;throw new Error("Missing Focus.First, Focus.Previous, Focus.Next or Focus.Last")})(),c=(()=>{if(1&t)return 0;if(2&t)return Math.max(0,l.indexOf(r))-1;if(4&t)return Math.max(0,l.indexOf(r))+1;if(8&t)return l.length-1;throw new Error("Missing Focus.First, Focus.Previous, Focus.Next or Focus.Last")})(),f=32&t?{preventScroll:!0}:{},p=0,d=l.length;do{if(p>=d||p+d<=0)return 0;let e=c+p;if(16&t)e=(e+d)%d;else{if(e<0)return 3;if(e>=d)return 1}a=l[e],null==a||a.focus(f),p+=u}while(a!==s.activeElement);return 6&t&&function(e){var t,n;return null!=(n=null==(t=null==e?void 0:e.matches)?void 0:t.call(e,mm))&&n}(a)&&a.select(),2}function wm(e,t,n){Xg.isServer||ur((r=>{document.addEventListener(e,t,n),r((()=>document.removeEventListener(e,t,n)))}))}function _m(e,t,n){Xg.isServer||ur((r=>{window.addEventListener(e,t,n),r((()=>window.removeEventListener(e,t,n)))}))}function xm(e,t,n=Ls((()=>!0))){function r(r,o){if(!n.value||r.defaultPrevented)return;let i=o(r);if(null===i||!i.getRootNode().contains(i))return;let s=function e(t){return"function"==typeof t?e(t()):Array.isArray(t)||t instanceof Set?t:[t]}(e);for(let e of s){if(null===e)continue;let t=e instanceof HTMLElement?e:Jg(e);if(null!=t&&t.contains(i)||r.composed&&r.composedPath().includes(t))return}return!dm(i,pm.Loose)&&-1!==i.tabIndex&&r.preventDefault(),t(r,i)}let o=$t(null);wm("pointerdown",(e=>{var t,r;n.value&&(o.value=(null==(r=null==(t=e.composedPath)?void 0:t.call(e))?void 0:r[0])||e.target)}),!0),wm("mousedown",(e=>{var t,r;n.value&&(o.value=(null==(r=null==(t=e.composedPath)?void 0:t.call(e))?void 0:r[0])||e.target)}),!0),wm("click",(e=>{o.value&&(r(e,(()=>o.value)),o.value=null)}),!0),wm("touchend",(e=>r(e,(()=>e.target instanceof HTMLElement?e.target:null))),!0),_m("blur",(e=>r(e,(()=>window.document.activeElement instanceof HTMLIFrameElement?window.document.activeElement:null))),!0)}function Sm(e){return[e.screenX,e.screenY]}function Om(){let e=$t([-1,-1]);return{wasMoved(t){let n=Sm(t);return(e.value[0]!==n[0]||e.value[1]!==n[1])&&(e.value=n,!0)},update(t){e.value=Sm(t)}}}let km=/([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g;function Em(e){var t,n;let r=null!=(t=e.innerText)?t:"",o=e.cloneNode(!0);if(!(o instanceof HTMLElement))return r;let i=!1;for(let e of o.querySelectorAll('[hidden],[aria-hidden],[role="img"]'))e.remove(),i=!0;let s=i?null!=(n=o.innerText)?n:"":r;return km.test(s)&&(s=s.replace(km,"")),s}function Cm(e){let t=$t(""),n=$t("");return()=>{let r=Jg(e);if(!r)return"";let o=r.innerText;if(t.value===o)return n.value;let i=function(e){let t=e.getAttribute("aria-label");if("string"==typeof t)return t.trim();let n=e.getAttribute("aria-labelledby");if(n){let e=n.split(" ").map((e=>{let t=document.getElementById(e);if(t){let e=t.getAttribute("aria-label");return"string"==typeof e?e.trim():Em(t).trim()}return null})).filter(Boolean);if(e.length>0)return e.join(", ")}return Em(e).trim()}(r).trim().toLowerCase();return t.value=o,n.value=i,i}}var Pm=(e=>(e[e.Open=0]="Open",e[e.Closed=1]="Closed",e))(Pm||{}),Tm=(e=>(e[e.Pointer=0]="Pointer",e[e.Other=1]="Other",e))(Tm||{});let Am=Symbol("MenuContext");function jm(e){let t=Yo(Am,null);if(null===t){let t=new Error(`<${e} /> is missing a parent component.`);throw Error.captureStackTrace&&Error.captureStackTrace(t,jm),t}return t}let Lm=Rr({name:"Menu",props:{as:{type:[Object,String],default:"template"}},setup(e,{slots:t,attrs:n}){let r=$t(1),o=$t(null),i=$t(null),s=$t([]),l=$t(""),a=$t(null),u=$t(1);function c(e=(e=>e)){let t=null!==a.value?s.value[a.value]:null,n=ym(e(s.value.slice()),(e=>Jg(e.dataRef.domRef))),r=t?n.indexOf(t):null;return-1===r&&(r=null),{items:n,activeItemIndex:r}}let f={menuState:r,buttonRef:o,itemsRef:i,items:s,searchQuery:l,activeItemIndex:a,activationTrigger:u,closeMenu:()=>{r.value=1,a.value=null},openMenu:()=>r.value=0,goToItem(e,t,n){let r=c(),o=Yg(e===Gg.Specific?{focus:Gg.Specific,id:t}:{focus:e},{resolveItems:()=>r.items,resolveActiveIndex:()=>r.activeItemIndex,resolveId:e=>e.id,resolveDisabled:e=>e.dataRef.disabled});l.value="",a.value=o,u.value=null!=n?n:1,s.value=r.items},search(e){let t=""!==l.value?0:1;l.value+=e.toLowerCase();let n=(null!==a.value?s.value.slice(a.value+t).concat(s.value.slice(0,a.value+t)):s.value).find((e=>e.dataRef.textValue.startsWith(l.value)&&!e.dataRef.disabled)),r=n?s.value.indexOf(n):-1;-1===r||r===a.value||(a.value=r,u.value=1)},clearSearch(){l.value=""},registerItem(e,t){let n=c((n=>[...n,{id:e,dataRef:t}]));s.value=n.items,a.value=n.activeItemIndex,u.value=1},unregisterItem(e){let t=c((t=>{let n=t.findIndex((t=>t.id===e));return-1!==n&&t.splice(n,1),t}));s.value=t.items,a.value=t.activeItemIndex,u.value=1}};return xm([o,i],((e,t)=>{var n;f.closeMenu(),dm(t,pm.Loose)||(e.preventDefault(),null==(n=Jg(o))||n.focus())}),Ls((()=>0===r.value))),Go(Am,f),om(Ls((()=>Mg(r.value,{0:nm.Open,1:nm.Closed})))),()=>{let o={open:0===r.value,close:f.closeMenu};return Ug({ourProps:{},theirProps:e,slot:o,slots:t,attrs:n,name:"Menu"})}}}),Rm=Rr({name:"MenuButton",props:{disabled:{type:Boolean,default:!1},as:{type:[Object,String],default:"button"},id:{type:String,default:()=>`headlessui-menu-button-${Wg()}`}},setup(e,{attrs:t,slots:n,expose:r}){let o=jm("MenuButton");function i(e){switch(e.key){case Kg.Space:case Kg.Enter:case Kg.ArrowDown:e.preventDefault(),e.stopPropagation(),o.openMenu(),bn((()=>{var e;null==(e=Jg(o.itemsRef))||e.focus({preventScroll:!0}),o.goToItem(Gg.First)}));break;case Kg.ArrowUp:e.preventDefault(),e.stopPropagation(),o.openMenu(),bn((()=>{var e;null==(e=Jg(o.itemsRef))||e.focus({preventScroll:!0}),o.goToItem(Gg.Last)}))}}function s(e){if(e.key===Kg.Space)e.preventDefault()}function l(t){e.disabled||(0===o.menuState.value?(o.closeMenu(),bn((()=>{var e;return null==(e=Jg(o.buttonRef))?void 0:e.focus({preventScroll:!0})}))):(t.preventDefault(),o.openMenu(),function(e){requestAnimationFrame((()=>requestAnimationFrame(e)))}((()=>{var e;return null==(e=Jg(o.itemsRef))?void 0:e.focus({preventScroll:!0})}))))}r({el:o.buttonRef,$el:o.buttonRef});let a=sm(Ls((()=>({as:e.as,type:t.type}))),o.buttonRef);return()=>{var r;let u={open:0===o.menuState.value},{id:c,...f}=e;return Ug({ourProps:{ref:o.buttonRef,id:c,type:a.value,"aria-haspopup":"menu","aria-controls":null==(r=Jg(o.itemsRef))?void 0:r.id,"aria-expanded":0===o.menuState.value,onKeydown:i,onKeyup:s,onClick:l},theirProps:f,slot:u,attrs:t,slots:n,name:"MenuButton"})}}}),Im=Rr({name:"MenuItems",props:{as:{type:[Object,String],default:"div"},static:{type:Boolean,default:!1},unmount:{type:Boolean,default:!0},id:{type:String,default:()=>`headlessui-menu-items-${Wg()}`}},setup(e,{attrs:t,slots:n,expose:r}){let o=jm("MenuItems"),i=$t(null);function s(e){var t;switch(i.value&&clearTimeout(i.value),e.key){case Kg.Space:if(""!==o.searchQuery.value)return e.preventDefault(),e.stopPropagation(),o.search(e.key);case Kg.Enter:if(e.preventDefault(),e.stopPropagation(),null!==o.activeItemIndex.value){null==(t=Jg(o.items.value[o.activeItemIndex.value].dataRef.domRef))||t.click()}o.closeMenu(),hm(Jg(o.buttonRef));break;case Kg.ArrowDown:return e.preventDefault(),e.stopPropagation(),o.goToItem(Gg.Next);case Kg.ArrowUp:return e.preventDefault(),e.stopPropagation(),o.goToItem(Gg.Previous);case Kg.Home:case Kg.PageUp:return e.preventDefault(),e.stopPropagation(),o.goToItem(Gg.First);case Kg.End:case Kg.PageDown:return e.preventDefault(),e.stopPropagation(),o.goToItem(Gg.Last);case Kg.Escape:e.preventDefault(),e.stopPropagation(),o.closeMenu(),bn((()=>{var e;return null==(e=Jg(o.buttonRef))?void 0:e.focus({preventScroll:!0})}));break;case Kg.Tab:e.preventDefault(),e.stopPropagation(),o.closeMenu(),bn((()=>function(e,t){return bm(fm(),t,{relativeTo:e})}(Jg(o.buttonRef),e.shiftKey?am.Previous:am.Next)));break;default:1===e.key.length&&(o.search(e.key),i.value=setTimeout((()=>o.clearSearch()),350))}}function l(e){if(e.key===Kg.Space)e.preventDefault()}r({el:o.itemsRef,$el:o.itemsRef}),function({container:e,accept:t,walk:n,enabled:r}){ur((()=>{let o=e.value;if(!o||void 0!==r&&!r.value)return;let i=em(e);if(!i)return;let s=Object.assign((e=>t(e)),{acceptNode:t}),l=i.createTreeWalker(o,NodeFilter.SHOW_ELEMENT,s,!1);for(;l.nextNode();)n(l.currentNode)}))}({container:Ls((()=>Jg(o.itemsRef))),enabled:Ls((()=>0===o.menuState.value)),accept:e=>"menuitem"===e.getAttribute("role")?NodeFilter.FILTER_REJECT:e.hasAttribute("role")?NodeFilter.FILTER_SKIP:NodeFilter.FILTER_ACCEPT,walk(e){e.setAttribute("role","none")}});let a=rm(),u=Ls((()=>null!==a?(a.value&nm.Open)===nm.Open:0===o.menuState.value));return()=>{var r,i;let a={open:0===o.menuState.value},{id:c,...f}=e;return Ug({ourProps:{"aria-activedescendant":null===o.activeItemIndex.value||null==(r=o.items.value[o.activeItemIndex.value])?void 0:r.id,"aria-labelledby":null==(i=Jg(o.buttonRef))?void 0:i.id,id:c,onKeydown:s,onKeyup:l,role:"menu",tabIndex:0,ref:o.itemsRef},theirProps:f,slot:a,attrs:t,slots:n,features:Dg.RenderStrategy|Dg.Static,visible:u.value,name:"MenuItems"})}}}),Fm=Rr({name:"MenuItem",inheritAttrs:!1,props:{as:{type:[Object,String],default:"template"},disabled:{type:Boolean,default:!1},id:{type:String,default:()=>`headlessui-menu-item-${Wg()}`}},setup(e,{slots:t,attrs:n,expose:r}){let o=jm("MenuItem"),i=$t(null);r({el:i,$el:i});let s=Ls((()=>null!==o.activeItemIndex.value&&o.items.value[o.activeItemIndex.value].id===e.id)),l=Cm(i),a=Ls((()=>({disabled:e.disabled,get textValue(){return l()},domRef:i})));function u(t){if(e.disabled)return t.preventDefault();o.closeMenu(),hm(Jg(o.buttonRef))}function c(){if(e.disabled)return o.goToItem(Gg.Nothing);o.goToItem(Gg.Specific,e.id)}Jr((()=>o.registerItem(e.id,a))),eo((()=>o.unregisterItem(e.id))),ur((()=>{0===o.menuState.value&&s.value&&0!==o.activationTrigger.value&&bn((()=>{var e,t;return null==(t=null==(e=Jg(i))?void 0:e.scrollIntoView)?void 0:t.call(e,{block:"nearest"})}))}));let f=Om();function p(e){f.update(e)}function d(t){f.wasMoved(t)&&(e.disabled||s.value||o.goToItem(Gg.Specific,e.id,0))}function h(t){f.wasMoved(t)&&(e.disabled||s.value&&o.goToItem(Gg.Nothing))}return()=>{let{disabled:r}=e,l={active:s.value,disabled:r,close:o.closeMenu},{id:a,...f}=e;return Ug({ourProps:{id:a,ref:i,role:"menuitem",tabIndex:!0===r?void 0:-1,"aria-disabled":!0===r||void 0,disabled:void 0,onClick:u,onFocus:c,onPointerenter:p,onMouseenter:p,onPointermove:d,onMousemove:d,onPointerleave:h,onMouseleave:h},theirProps:{...n,...f},slot:l,attrs:n,slots:t,name:"MenuItem"})}}});function Nm(e,t){return Ni(),Vi("svg",{xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24","stroke-width":"1.5",stroke:"currentColor","aria-hidden":"true"},[Ji("path",{"stroke-linecap":"round","stroke-linejoin":"round",d:"M6 18L18 6M6 6l12 12"})])}function Mm(e,t){return Ni(),Vi("svg",{xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24","stroke-width":"1.5",stroke:"currentColor","aria-hidden":"true"},[Ji("path",{"stroke-linecap":"round","stroke-linejoin":"round",d:"M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18"})])}function Dm(e,t){return Ni(),Vi("svg",{xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24","stroke-width":"1.5",stroke:"currentColor","aria-hidden":"true"},[Ji("path",{"stroke-linecap":"round","stroke-linejoin":"round",d:"M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"})])}function Bm(e,t){return Ni(),Vi("svg",{xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24","stroke-width":"1.5",stroke:"currentColor","aria-hidden":"true"},[Ji("path",{"stroke-linecap":"round","stroke-linejoin":"round",d:"M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"})])}function Um(e,t){return Ni(),Vi("svg",{xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24","stroke-width":"1.5",stroke:"currentColor","aria-hidden":"true"},[Ji("path",{"stroke-linecap":"round","stroke-linejoin":"round",d:"M2.25 12.75V12A2.25 2.25 0 014.5 9.75h15A2.25 2.25 0 0121.75 12v.75m-8.69-6.44l-2.12-2.12a1.5 1.5 0 00-1.061-.44H4.5A2.25 2.25 0 002.25 6v12a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9a2.25 2.25 0 00-2.25-2.25h-5.379a1.5 1.5 0 01-1.06-.44z"})])}function $m(e,t){return Ni(),Vi("svg",{xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24","stroke-width":"1.5",stroke:"currentColor","aria-hidden":"true"},[Ji("path",{"stroke-linecap":"round","stroke-linejoin":"round",d:"M3.75 9.776c.112-.017.227-.026.344-.026h15.812c.117 0 .232.009.344.026m-16.5 0a2.25 2.25 0 00-1.883 2.542l.857 6a2.25 2.25 0 002.227 1.932H19.05a2.25 2.25 0 002.227-1.932l.857-6a2.25 2.25 0 00-1.883-2.542m-16.5 0V6A2.25 2.25 0 016 3.75h3.879a1.5 1.5 0 011.06.44l2.122 2.12a1.5 1.5 0 001.06.44H18A2.25 2.25 0 0120.25 9v.776"})])}function Vm(e,t){return Ni(),Vi("svg",{xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24","stroke-width":"1.5",stroke:"currentColor","aria-hidden":"true"},[Ji("path",{"stroke-linecap":"round","stroke-linejoin":"round",d:"M8.25 4.5l7.5 7.5-7.5 7.5"})])}function Hm(e,t){return Ni(),Vi("svg",{xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24","stroke-width":"1.5",stroke:"currentColor","aria-hidden":"true"},[Ji("path",{"stroke-linecap":"round","stroke-linejoin":"round",d:"M12 6.75a.75.75 0 110-1.5.75.75 0 010 1.5zM12 12.75a.75.75 0 110-1.5.75.75 0 010 1.5zM12 18.75a.75.75 0 110-1.5.75.75 0 010 1.5z"})])}function zm(e,t){return Ni(),Vi("svg",{xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24","stroke-width":"1.5",stroke:"currentColor","aria-hidden":"true"},[Ji("path",{"stroke-linecap":"round","stroke-linejoin":"round",d:"M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125"})])}function qm(e,t){return Ni(),Vi("svg",{xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24","stroke-width":"1.5",stroke:"currentColor","aria-hidden":"true"},[Ji("path",{"stroke-linecap":"round","stroke-linejoin":"round",d:"M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"})])}var Wm=pd({id:"hosts",state:function(){return{selectedHostIdentifier:null}},getters:{supportsHosts:function(){return LogViewer.supports_hosts},hosts:function(){return LogViewer.hosts||[]},hasRemoteHosts:function(){return this.hosts.some((function(e){return e.is_remote}))},selectedHost:function(){var e=this;return this.hosts.find((function(t){return t.identifier===e.selectedHostIdentifier}))},localHost:function(){return this.hosts.find((function(e){return!e.is_remote}))},hostQueryParam:function(){return this.selectedHost&&this.selectedHost.is_remote?this.selectedHost.identifier:void 0}},actions:{selectHost:function(e){var t;this.supportsHosts||(e=null),"string"==typeof e&&(e=this.hosts.find((function(t){return t.identifier===e}))),e||(e=this.hosts.find((function(e){return!e.is_remote}))),this.selectedHostIdentifier=(null===(t=e)||void 0===t?void 0:t.identifier)||null}}});var Km;Object.defineProperty,Object.defineProperties,Object.getOwnPropertyDescriptors,Object.getOwnPropertySymbols,Object.prototype.hasOwnProperty,Object.prototype.propertyIsEnumerable;const Gm="undefined"!=typeof window,Ym=(Object.prototype.toString,e=>"function"==typeof e),Jm=e=>"string"==typeof e,Qm=()=>{};Gm&&(null==(Km=null==window?void 0:window.navigator)?void 0:Km.userAgent)&&/iP(ad|hone|od)/.test(window.navigator.userAgent);function Zm(e){return"function"==typeof e?e():Wt(e)}function Xm(e,t){return function(...n){return new Promise(((r,o)=>{Promise.resolve(e((()=>t.apply(this,n)),{fn:t,thisArg:this,args:n})).then(r).catch(o)}))}}const ey=e=>e();function ty(e){return!!ge()&&(me(e),!0)}Object.defineProperty,Object.getOwnPropertySymbols,Object.prototype.hasOwnProperty,Object.prototype.propertyIsEnumerable;Object.defineProperty,Object.defineProperties,Object.getOwnPropertyDescriptors,Object.getOwnPropertySymbols,Object.prototype.hasOwnProperty,Object.prototype.propertyIsEnumerable;Object.defineProperty,Object.getOwnPropertySymbols,Object.prototype.hasOwnProperty,Object.prototype.propertyIsEnumerable;Object.defineProperty,Object.getOwnPropertySymbols,Object.prototype.hasOwnProperty,Object.prototype.propertyIsEnumerable;var ny=Object.getOwnPropertySymbols,ry=Object.prototype.hasOwnProperty,oy=Object.prototype.propertyIsEnumerable,iy=(e,t)=>{var n={};for(var r in e)ry.call(e,r)&&t.indexOf(r)<0&&(n[r]=e[r]);if(null!=e&&ny)for(var r of ny(e))t.indexOf(r)<0&&oy.call(e,r)&&(n[r]=e[r]);return n};function sy(e,t,n={}){const r=n,{eventFilter:o=ey}=r,i=iy(r,["eventFilter"]);return dr(e,Xm(o,t),i)}Object.getOwnPropertySymbols,Object.prototype.hasOwnProperty,Object.prototype.propertyIsEnumerable;Object.defineProperty,Object.defineProperties,Object.getOwnPropertyDescriptors,Object.getOwnPropertySymbols,Object.prototype.hasOwnProperty,Object.prototype.propertyIsEnumerable;Object.defineProperty,Object.defineProperties,Object.getOwnPropertyDescriptors,Object.getOwnPropertySymbols,Object.prototype.hasOwnProperty,Object.prototype.propertyIsEnumerable;var ly=Object.defineProperty,ay=Object.defineProperties,uy=Object.getOwnPropertyDescriptors,cy=Object.getOwnPropertySymbols,fy=Object.prototype.hasOwnProperty,py=Object.prototype.propertyIsEnumerable,dy=(e,t,n)=>t in e?ly(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,hy=(e,t)=>{for(var n in t||(t={}))fy.call(t,n)&&dy(e,n,t[n]);if(cy)for(var n of cy(t))py.call(t,n)&&dy(e,n,t[n]);return e},vy=(e,t)=>ay(e,uy(t)),gy=(e,t)=>{var n={};for(var r in e)fy.call(e,r)&&t.indexOf(r)<0&&(n[r]=e[r]);if(null!=e&&cy)for(var r of cy(e))t.indexOf(r)<0&&py.call(e,r)&&(n[r]=e[r]);return n};function my(e,t,n={}){const r=n,{eventFilter:o}=r,i=gy(r,["eventFilter"]),{eventFilter:s,pause:l,resume:a,isActive:u}=function(e=ey){const t=$t(!0);return{isActive:Ct(t),pause:function(){t.value=!1},resume:function(){t.value=!0},eventFilter:(...n)=>{t.value&&e(...n)}}}(o);return{stop:sy(e,t,vy(hy({},i),{eventFilter:s})),pause:l,resume:a,isActive:u}}Object.defineProperty,Object.defineProperties,Object.getOwnPropertyDescriptors,Object.getOwnPropertySymbols,Object.prototype.hasOwnProperty,Object.prototype.propertyIsEnumerable;Object.defineProperty,Object.defineProperties,Object.getOwnPropertyDescriptors,Object.getOwnPropertySymbols,Object.prototype.hasOwnProperty,Object.prototype.propertyIsEnumerable;function yy(e){var t;const n=Zm(e);return null!=(t=null==n?void 0:n.$el)?t:n}const by=Gm?window:void 0;Gm&&window.document,Gm&&window.navigator,Gm&&window.location;function wy(...e){let t,n,r,o;if(Jm(e[0])||Array.isArray(e[0])?([n,r,o]=e,t=by):[t,n,r,o]=e,!t)return Qm;Array.isArray(n)||(n=[n]),Array.isArray(r)||(r=[r]);const i=[],s=()=>{i.forEach((e=>e())),i.length=0},l=dr((()=>[yy(t),Zm(o)]),(([e,t])=>{s(),e&&i.push(...n.flatMap((n=>r.map((r=>((e,t,n,r)=>(e.addEventListener(t,n,r),()=>e.removeEventListener(t,n,r)))(e,n,r,t))))))}),{immediate:!0,flush:"post"}),a=()=>{l(),s()};return ty(a),a}Object.defineProperty,Object.defineProperties,Object.getOwnPropertyDescriptors,Object.getOwnPropertySymbols,Object.prototype.hasOwnProperty,Object.prototype.propertyIsEnumerable;Object.defineProperty,Object.getOwnPropertySymbols,Object.prototype.hasOwnProperty,Object.prototype.propertyIsEnumerable;Object.defineProperty,Object.defineProperties,Object.getOwnPropertyDescriptors,Object.getOwnPropertySymbols,Object.prototype.hasOwnProperty,Object.prototype.propertyIsEnumerable;const _y="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},xy="__vueuse_ssr_handlers__";_y[xy]=_y[xy]||{};const Sy=_y[xy];function Oy(e,t){return Sy[e]||t}function ky(e){return null==e?"any":e instanceof Set?"set":e instanceof Map?"map":e instanceof Date?"date":"boolean"==typeof e?"boolean":"string"==typeof e?"string":"object"==typeof e?"object":Number.isNaN(e)?"any":"number"}var Ey=Object.defineProperty,Cy=Object.getOwnPropertySymbols,Py=Object.prototype.hasOwnProperty,Ty=Object.prototype.propertyIsEnumerable,Ay=(e,t,n)=>t in e?Ey(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,jy=(e,t)=>{for(var n in t||(t={}))Py.call(t,n)&&Ay(e,n,t[n]);if(Cy)for(var n of Cy(t))Ty.call(t,n)&&Ay(e,n,t[n]);return e};const Ly={boolean:{read:e=>"true"===e,write:e=>String(e)},object:{read:e=>JSON.parse(e),write:e=>JSON.stringify(e)},number:{read:e=>Number.parseFloat(e),write:e=>String(e)},any:{read:e=>e,write:e=>String(e)},string:{read:e=>e,write:e=>String(e)},map:{read:e=>new Map(JSON.parse(e)),write:e=>JSON.stringify(Array.from(e.entries()))},set:{read:e=>new Set(JSON.parse(e)),write:e=>JSON.stringify(Array.from(e))},date:{read:e=>new Date(e),write:e=>e.toISOString()}},Ry="vueuse-storage";function Iy(e,t,n,r={}){var o;const{flush:i="pre",deep:s=!0,listenToStorageChanges:l=!0,writeDefaults:a=!0,mergeDefaults:u=!1,shallow:c,window:f=by,eventFilter:p,onError:d=(e=>{})}=r,h=(c?Vt:$t)(t);if(!n)try{n=Oy("getDefaultStorage",(()=>{var e;return null==(e=by)?void 0:e.localStorage}))()}catch(e){d(e)}if(!n)return h;const v=Zm(t),g=ky(v),m=null!=(o=r.serializer)?o:Ly[g],{pause:y,resume:b}=my(h,(()=>function(t){try{if(null==t)n.removeItem(e);else{const r=m.write(t),o=n.getItem(e);o!==r&&(n.setItem(e,r),f&&f.dispatchEvent(new CustomEvent(Ry,{detail:{key:e,oldValue:o,newValue:r,storageArea:n}})))}}catch(e){d(e)}}(h.value)),{flush:i,deep:s,eventFilter:p});return f&&l&&(wy(f,"storage",w),wy(f,Ry,(function(e){w(e.detail)}))),w(),h;function w(t){if(!t||t.storageArea===n)if(t&&null==t.key)h.value=v;else if(!t||t.key===e){y();try{h.value=function(t){const r=t?t.newValue:n.getItem(e);if(null==r)return a&&null!==v&&n.setItem(e,m.write(v)),v;if(!t&&u){const e=m.read(r);return Ym(u)?u(e,v):"object"!==g||Array.isArray(e)?e:jy(jy({},v),e)}return"string"!=typeof r?r:m.read(r)}(t)}catch(e){d(e)}finally{t?bn(b):b()}}}}Object.defineProperty,Object.getOwnPropertySymbols,Object.prototype.hasOwnProperty,Object.prototype.propertyIsEnumerable;Object.defineProperty,Object.defineProperties,Object.getOwnPropertyDescriptors,Object.getOwnPropertySymbols,Object.prototype.hasOwnProperty,Object.prototype.propertyIsEnumerable;Object.defineProperty,Object.defineProperties,Object.getOwnPropertyDescriptors,Object.getOwnPropertySymbols,Object.prototype.hasOwnProperty,Object.prototype.propertyIsEnumerable;Object.defineProperty,Object.defineProperties,Object.getOwnPropertyDescriptors,Object.getOwnPropertySymbols,Object.prototype.hasOwnProperty,Object.prototype.propertyIsEnumerable;Object.defineProperty,Object.defineProperties,Object.getOwnPropertyDescriptors,Object.getOwnPropertySymbols,Object.prototype.hasOwnProperty,Object.prototype.propertyIsEnumerable;Object.getOwnPropertySymbols,Object.prototype.hasOwnProperty,Object.prototype.propertyIsEnumerable;Object.defineProperty,Object.getOwnPropertySymbols,Object.prototype.hasOwnProperty,Object.prototype.propertyIsEnumerable;new Map;Object.defineProperty,Object.defineProperties,Object.getOwnPropertyDescriptors,Object.getOwnPropertySymbols,Object.prototype.hasOwnProperty,Object.prototype.propertyIsEnumerable;Object.defineProperty,Object.getOwnPropertySymbols,Object.prototype.hasOwnProperty,Object.prototype.propertyIsEnumerable;Object.defineProperty,Object.getOwnPropertySymbols,Object.prototype.hasOwnProperty,Object.prototype.propertyIsEnumerable;Object.defineProperty,Object.getOwnPropertySymbols,Object.prototype.hasOwnProperty,Object.prototype.propertyIsEnumerable;Object.defineProperty,Object.defineProperties,Object.getOwnPropertyDescriptors,Object.getOwnPropertySymbols,Object.prototype.hasOwnProperty,Object.prototype.propertyIsEnumerable;function Fy(e,t,n={}){const{window:r=by}=n;return Iy(e,t,null==r?void 0:r.localStorage,n)}Object.defineProperty,Object.getOwnPropertySymbols,Object.prototype.hasOwnProperty,Object.prototype.propertyIsEnumerable;Object.getOwnPropertySymbols,Object.prototype.hasOwnProperty,Object.prototype.propertyIsEnumerable;Object.defineProperty,Object.getOwnPropertySymbols,Object.prototype.hasOwnProperty,Object.prototype.propertyIsEnumerable;Object.defineProperty,Object.defineProperties,Object.getOwnPropertyDescriptors,Object.getOwnPropertySymbols,Object.prototype.hasOwnProperty,Object.prototype.propertyIsEnumerable;var Ny,My;(My=Ny||(Ny={})).UP="UP",My.RIGHT="RIGHT",My.DOWN="DOWN",My.LEFT="LEFT",My.NONE="NONE";Object.defineProperty,Object.getOwnPropertySymbols,Object.prototype.hasOwnProperty,Object.prototype.propertyIsEnumerable;Object.defineProperty,Object.getOwnPropertySymbols,Object.prototype.hasOwnProperty,Object.prototype.propertyIsEnumerable;Object.defineProperty,Object.defineProperties,Object.getOwnPropertyDescriptors,Object.getOwnPropertySymbols,Object.prototype.hasOwnProperty,Object.prototype.propertyIsEnumerable;Object.defineProperty,Object.getOwnPropertySymbols,Object.prototype.hasOwnProperty,Object.prototype.propertyIsEnumerable;Object.defineProperty,Object.getOwnPropertySymbols,Object.prototype.hasOwnProperty,Object.prototype.propertyIsEnumerable;var Dy=Object.defineProperty,By=Object.getOwnPropertySymbols,Uy=Object.prototype.hasOwnProperty,$y=Object.prototype.propertyIsEnumerable,Vy=(e,t,n)=>t in e?Dy(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n;((e,t)=>{for(var n in t||(t={}))Uy.call(t,n)&&Vy(e,n,t[n]);if(By)for(var n of By(t))$y.call(t,n)&&Vy(e,n,t[n])})({linear:function(e){return e}},{easeInSine:[.12,0,.39,0],easeOutSine:[.61,1,.88,1],easeInOutSine:[.37,0,.63,1],easeInQuad:[.11,0,.5,0],easeOutQuad:[.5,1,.89,1],easeInOutQuad:[.45,0,.55,1],easeInCubic:[.32,0,.67,0],easeOutCubic:[.33,1,.68,1],easeInOutCubic:[.65,0,.35,1],easeInQuart:[.5,0,.75,0],easeOutQuart:[.25,1,.5,1],easeInOutQuart:[.76,0,.24,1],easeInQuint:[.64,0,.78,0],easeOutQuint:[.22,1,.36,1],easeInOutQuint:[.83,0,.17,1],easeInExpo:[.7,0,.84,0],easeOutExpo:[.16,1,.3,1],easeInOutExpo:[.87,0,.13,1],easeInCirc:[.55,0,1,.45],easeOutCirc:[0,.55,.45,1],easeInOutCirc:[.85,0,.15,1],easeInBack:[.36,0,.66,-.56],easeOutBack:[.34,1.56,.64,1],easeInOutBack:[.68,-.6,.32,1.6]});var Hy=pd({id:"search",state:function(){return{query:"",searchMoreRoute:null,searching:!1,percentScanned:0,error:null}},getters:{hasQuery:function(e){return""!==String(e.query).trim()}},actions:{init:function(){this.checkSearchProgress()},setQuery:function(e){this.query=e},update:function(e,t,n){var r=arguments.length>3&&void 0!==arguments[3]&&arguments[3],o=arguments.length>4&&void 0!==arguments[4]?arguments[4]:0;this.query=e,this.error=t&&""!==t?t:null,this.searchMoreRoute=n,this.searching=r,this.percentScanned=o,this.searching&&this.checkSearchProgress()},checkSearchProgress:function(){var e=this,t=this.query;if(""!==t){var n="?"+new URLSearchParams({query:t});ov.get(this.searchMoreRoute+n).then((function(n){var r=n.data;if(e.query===t){var o=e.searching;e.searching=r.hasMoreResults,e.percentScanned=r.percentScanned,e.searching?e.checkSearchProgress():o&&!e.searching&&window.dispatchEvent(new CustomEvent("reload-results"))}}))}}}}),zy=pd({id:"pagination",state:function(){return{page:1,pagination:{}}},getters:{currentPage:function(e){return 1!==e.page?Number(e.page):null},links:function(e){var t;return((null===(t=e.pagination)||void 0===t?void 0:t.links)||[]).slice(1,-1)},linksShort:function(e){var t;return((null===(t=e.pagination)||void 0===t?void 0:t.links_short)||[]).slice(1,-1)},hasPages:function(e){var t;return(null===(t=e.pagination)||void 0===t?void 0:t.last_page)>1},hasMorePages:function(e){var t;return null!==(null===(t=e.pagination)||void 0===t?void 0:t.next_page_url)}},actions:{setPagination:function(e){var t,n;(this.pagination=e,(null===(t=this.pagination)||void 0===t?void 0:t.last_page)0}))},totalResults:function(){return this.levelsFound.reduce((function(e,t){return e+t.count}),0)},levelsSelected:function(){return this.levelsFound.filter((function(e){return e.selected}))},totalResultsSelected:function(){return this.levelsSelected.reduce((function(e,t){return e+t.count}),0)}},actions:{setLevelCounts:function(e){e.hasOwnProperty("length")?this.levelCounts=e:this.levelCounts=Object.values(e),this.allLevels=e.map((function(e){return e.level}))},selectAllLevels:function(){this.excludedLevels=[],this.levelCounts.forEach((function(e){return e.selected=!0}))},deselectAllLevels:function(){this.excludedLevels=this.allLevels,this.levelCounts.forEach((function(e){return e.selected=!1}))},toggleLevel:function(e){var t=this.levelCounts.find((function(t){return t.level===e}))||{};this.excludedLevels.includes(e)?(this.excludedLevels=this.excludedLevels.filter((function(t){return t!==e})),t.selected=!0):(this.excludedLevels.push(e),t.selected=!1)}}}),Wy=n(486),Ky={System:"System",Light:"Light",Dark:"Dark"},Gy=[{label:"Datetime",data_key:"datetime"},{label:"Severity",data_key:"level"},{label:"Message",data_key:"message"}],Yy=pd({id:"logViewer",state:function(){return{theme:Fy("logViewerTheme",Ky.System),shorterStackTraces:Fy("logViewerShorterStackTraces",!1),direction:Fy("logViewerDirection","desc"),resultsPerPage:Fy("logViewerResultsPerPage",25),helpSlideOverOpen:!1,loading:!1,error:null,logs:[],columns:Gy,levelCounts:[],performance:{},hasMoreResults:!1,percentScanned:100,abortController:null,viewportWidth:window.innerWidth,viewportHeight:window.innerHeight,stacksOpen:[],stacksInView:[],stackTops:{},containerTop:0,showLevelsDropdown:!0}},getters:{selectedFile:function(){return nb().selectedFile},isOpen:function(e){return function(t){return e.stacksOpen.includes(t)}},isMobile:function(e){return e.viewportWidth<=1023},tableRowHeight:function(){return this.isMobile?29:36},headerHeight:function(){return this.isMobile?0:36},shouldBeSticky:function(e){var t=this;return function(n){return t.isOpen(n)&&e.stacksInView.includes(n)}},stickTopPosition:function(){var e=this;return function(t){var n=e.pixelsAboveFold(t);return n<0?Math.max(e.headerHeight-e.tableRowHeight,e.headerHeight+n)+"px":e.headerHeight+"px"}},pixelsAboveFold:function(e){var t=this;return function(n){var r=document.getElementById("tbody-"+n);if(!r)return!1;var o=r.getClientRects()[0];return o.top+o.height-t.tableRowHeight-t.headerHeight-e.containerTop}},isInViewport:function(){var e=this;return function(t){return e.pixelsAboveFold(t)>-e.tableRowHeight}}},actions:{setViewportDimensions:function(e,t){this.viewportWidth=e,this.viewportHeight=t;var n=document.querySelector(".log-item-container");n&&(this.containerTop=n.getBoundingClientRect().top)},toggleTheme:function(){switch(this.theme){case Ky.System:this.theme=Ky.Light;break;case Ky.Light:this.theme=Ky.Dark;break;default:this.theme=Ky.System}this.syncTheme()},syncTheme:function(){var e=this.theme;e===Ky.Dark||e===Ky.System&&window.matchMedia("(prefers-color-scheme: dark)").matches?document.documentElement.classList.add("dark"):document.documentElement.classList.remove("dark")},toggle:function(e){this.isOpen(e)?this.stacksOpen=this.stacksOpen.filter((function(t){return t!==e})):this.stacksOpen.push(e),this.onScroll()},onScroll:function(){var e=this;this.stacksOpen.forEach((function(t){e.isInViewport(t)?(e.stacksInView.includes(t)||e.stacksInView.push(t),e.stackTops[t]=e.stickTopPosition(t)):(e.stacksInView=e.stacksInView.filter((function(e){return e!==t})),delete e.stackTops[t])}))},reset:function(){this.stacksOpen=[],this.stacksInView=[],this.stackTops={};var e=document.querySelector(".log-item-container");e&&(this.containerTop=e.getBoundingClientRect().top,e.scrollTo(0,0))},loadLogs:(0,Wy.debounce)((function(){var e,t=this,n=(arguments.length>0&&void 0!==arguments[0]?arguments[0]:{}).silently,r=void 0!==n&&n,o=Wm(),i=nb(),s=Hy(),l=zy(),a=qy();if(0!==i.folders.length&&(this.abortController&&this.abortController.abort(),this.selectedFile||s.hasQuery)){this.abortController=new AbortController;var u={host:o.hostQueryParam,file:null===(e=this.selectedFile)||void 0===e?void 0:e.identifier,direction:this.direction,query:s.query,page:l.currentPage,per_page:this.resultsPerPage,exclude_levels:It(a.excludedLevels),exclude_file_types:It(i.fileTypesExcluded),shorter_stack_traces:this.shorterStackTraces};r||(this.loading=!0),ov.get("".concat(LogViewer.basePath,"/api/logs"),{params:u,signal:this.abortController.signal}).then((function(e){var n=e.data;t.logs=u.host?n.logs.map((function(e){var t={host:u.host,file:e.file_identifier,query:"log-index:".concat(e.index)};return e.url="".concat(window.location.host).concat(LogViewer.basePath,"?").concat(new URLSearchParams(t)),e})):n.logs,t.columns=n.columns||Gy,t.hasMoreResults=n.hasMoreResults,t.percentScanned=n.percentScanned,t.error=n.error||null,t.performance=n.performance||{},a.setLevelCounts(n.levelCounts),l.setPagination(n.pagination),t.loading=!1,r?document.dispatchEvent(new Event("logsPageLoadedSilently")):bn((function(){document.dispatchEvent(new Event("logsPageLoaded")),t.reset(),n.expandAutomatically&&t.stacksOpen.push(0)})),t.hasMoreResults&&t.loadLogs({silently:!0})})).catch((function(e){var n;if("ERR_CANCELED"===e.code)return t.hasMoreResults=!1,void(t.percentScanned=100);t.loading=!1,t.error=e.message,null!==(n=e.response)&&void 0!==n&&null!==(n=n.data)&&void 0!==n&&n.message&&(t.error+=": "+e.response.data.message)}))}}),10)}});function Jy(e){return Jy="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},Jy(e)}function Qy(e){return function(e){if(Array.isArray(e))return Zy(e)}(e)||function(e){if("undefined"!=typeof Symbol&&null!=e[Symbol.iterator]||null!=e["@@iterator"])return Array.from(e)}(e)||function(e,t){if(!e)return;if("string"==typeof e)return Zy(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);"Object"===n&&e.constructor&&(n=e.constructor.name);if("Map"===n||"Set"===n)return Array.from(e);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return Zy(e,t)}(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function Zy(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n0}))},files:function(e){return e.folders.flatMap((function(e){return e.files}))},selectedFile:function(e){return e.files.find((function(t){return t.identifier===e.selectedFileIdentifier}))},foldersOpen:function(e){return e.openFolderIdentifiers.map((function(t){return e.folders.find((function(e){return e.identifier===t}))}))},isOpen:function(){var e=this;return function(t){return e.foldersOpen.map((function(e){return e.identifier})).includes(t.identifier)}},isChecked:function(e){return function(t){return e.filesChecked.includes("string"==typeof t?t:t.identifier)}},shouldBeSticky:function(e){var t=this;return function(n){return t.isOpen(n)&&e.foldersInView.map((function(e){return e.identifier})).includes(n.identifier)}},isInViewport:function(){var e=this;return function(t){return e.pixelsAboveFold(t)>-36}},pixelsAboveFold:function(e){return function(t){var n=document.getElementById("folder-"+t);if(!n)return!1;var r=n.getClientRects()[0];return r.top+r.height-e.containerTop}},hasFilesChecked:function(e){return e.filesChecked.length>0},fileTypesSelected:function(e){return e.fileTypesAvailable.filter((function(t){return e.selectedFileTypes.includes(t.identifier)}))},fileTypesExcluded:function(e){return e.fileTypesAvailable.filter((function(t){return!e.selectedFileTypes.includes(t.identifier)})).map((function(e){return e.identifier}))},selectedFileTypesString:function(){var e=this.fileTypesSelected.map((function(e){return e.name}));return 0===e.length?"Please select at least one file type":1===e.length?e[0]:2===e.length?e.join(" and "):3===e.length?e.slice(0,-1).join(", ")+" and "+e.slice(-1):e.slice(0,3).join(", ")+" and "+(e.length-3)+" more"}},actions:{setDirection:function(e){this.direction=e},selectFile:function(e){this.selectedFileIdentifier!==e&&(this.selectedFileIdentifier=e,this.openFolderForActiveFile(),this.sidebarOpen=!1)},openFolderForActiveFile:function(){var e=this;if(this.selectedFile){var t=this.folders.find((function(t){return t.files.some((function(t){return t.identifier===e.selectedFile.identifier}))}));t&&!this.isOpen(t)&&this.toggle(t)}},openRootFolderIfNoneOpen:function(){var e=this.folders.find((function(e){return e.is_root}));e&&0===this.openFolderIdentifiers.length&&this.openFolderIdentifiers.push(e.identifier)},loadFolders:function(){var e=this;return this.abortController&&this.abortController.abort(),this.selectedHost?(this.abortController=new AbortController,this.loading=!0,ov.get("".concat(LogViewer.basePath,"/api/folders"),{params:{host:this.hostQueryParam,direction:this.direction},signal:this.abortController.signal}).then((function(t){var n=t.data;e.folders=n,e.error=n.error||null,e.loading=!1,0===e.openFolderIdentifiers.length&&(e.openFolderForActiveFile(),e.openRootFolderIfNoneOpen()),e.setAvailableFileTypes(n),e.onScroll()})).catch((function(t){var n;"ERR_CANCELED"!==t.code&&(e.loading=!1,e.error=t.message,null!==(n=t.response)&&void 0!==n&&null!==(n=n.data)&&void 0!==n&&n.message&&(e.error+=": "+t.response.data.message))}))):(this.folders=[],this.error=null,void(this.loading=!1))},setAvailableFileTypes:function(e){var t=e.flatMap((function(e){return e.files.map((function(e){return e.type}))})),n=Qy(new Set(t.map((function(e){return e.value}))));this.fileTypesAvailable=n.map((function(e){return{identifier:e,name:t.find((function(t){return t.value===e})).name,count:t.filter((function(t){return t.value===e})).length}})),this.selectedFileTypes&&0!==this.selectedFileTypes.length||(this.selectedFileTypes=n)},toggle:function(e){this.isOpen(e)?this.openFolderIdentifiers=this.openFolderIdentifiers.filter((function(t){return t!==e.identifier})):this.openFolderIdentifiers.push(e.identifier),this.onScroll()},onScroll:function(){var e=this;this.foldersOpen.forEach((function(t){e.isInViewport(t)?e.foldersInView.includes(t)||e.foldersInView.push(t):e.foldersInView=e.foldersInView.filter((function(e){return e!==t}))}))},reset:function(){this.openFolderIdentifiers=[],this.foldersInView=[];var e=document.getElementById("file-list-container");e&&(this.containerTop=e.getBoundingClientRect().top,e.scrollTo(0,0))},toggleSidebar:function(){this.sidebarOpen=!this.sidebarOpen},checkBoxToggle:function(e){this.isChecked(e)?this.filesChecked=this.filesChecked.filter((function(t){return t!==e})):this.filesChecked.push(e)},toggleCheckboxVisibility:function(){this.checkBoxesVisibility=!this.checkBoxesVisibility},resetChecks:function(){this.filesChecked=[],this.checkBoxesVisibility=!1},clearCacheForFile:function(e){var t=this;return this.clearingCache[e.identifier]=!0,ov.post("".concat(LogViewer.basePath,"/api/files/").concat(e.identifier,"/clear-cache"),{},{params:{host:this.hostQueryParam}}).then((function(){e.identifier===t.selectedFileIdentifier&&Yy().loadLogs(),t.cacheRecentlyCleared[e.identifier]=!0,setTimeout((function(){return t.cacheRecentlyCleared[e.identifier]=!1}),2e3)})).catch((function(e){})).finally((function(){return t.clearingCache[e.identifier]=!1}))},deleteFile:function(e){var t=this;return ov.delete("".concat(LogViewer.basePath,"/api/files/").concat(e.identifier),{params:{host:this.hostQueryParam}}).then((function(){return t.loadFolders()}))},clearCacheForFolder:function(e){var t=this;return this.clearingCache[e.identifier]=!0,ov.post("".concat(LogViewer.basePath,"/api/folders/").concat(e.identifier,"/clear-cache"),{},{params:{host:this.hostQueryParam}}).then((function(){e.files.some((function(e){return e.identifier===t.selectedFileIdentifier}))&&Yy().loadLogs(),t.cacheRecentlyCleared[e.identifier]=!0,setTimeout((function(){return t.cacheRecentlyCleared[e.identifier]=!1}),2e3)})).catch((function(e){})).finally((function(){t.clearingCache[e.identifier]=!1}))},deleteFolder:function(e){var t=this;return this.deleting[e.identifier]=!0,ov.delete("".concat(LogViewer.basePath,"/api/folders/").concat(e.identifier),{params:{host:this.hostQueryParam}}).then((function(){return t.loadFolders()})).catch((function(e){})).finally((function(){t.deleting[e.identifier]=!1}))},deleteSelectedFiles:function(){return ov.post("".concat(LogViewer.basePath,"/api/delete-multiple-files"),{files:this.filesChecked},{params:{host:this.hostQueryParam}})},clearCacheForAllFiles:function(){var e=this;this.clearingCache["*"]=!0,ov.post("".concat(LogViewer.basePath,"/api/clear-cache-all"),{},{params:{host:this.hostQueryParam}}).then((function(){e.cacheRecentlyCleared["*"]=!0,setTimeout((function(){return e.cacheRecentlyCleared["*"]=!1}),2e3),Yy().loadLogs()})).catch((function(e){})).finally((function(){return e.clearingCache["*"]=!1}))}}}),rb=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;if(e=e||"",t)try{e=e.replace(new RegExp(t,"gi"),"$&")}catch(e){}return ob(e).replace(/<mark>/g,"").replace(/<\/mark>/g,"").replace(/<br\/>/g,"
")},ob=function(e){var t={"&":"&","<":"<",">":">",'"':""","'":"'"};return e.replace(/[&<>"']/g,(function(e){return t[e]}))},ib=function(e){var t=document.createElement("textarea");t.value=e,t.setAttribute("readonly",""),t.style.position="absolute",t.style.left="-9999px",document.body.appendChild(t);var n=document.getSelection().rangeCount>0&&document.getSelection().getRangeAt(0);t.select(),document.execCommand("copy"),document.body.removeChild(t),n&&(document.getSelection().removeAllRanges(),document.getSelection().addRange(n))},sb=function(e,t,n){var r=e.currentRoute.value,o={host:r.query.host||void 0,file:r.query.file||void 0,query:r.query.query||void 0,page:r.query.page||void 0};"host"===t?(o.file=void 0,o.page=void 0):"file"===t&&void 0!==o.page&&(o.page=void 0),o[t]=n?String(n):void 0,e.push({name:"home",query:o})},lb=function(){var e=$t({});return{dropdownDirections:e,calculateDropdownDirection:function(t){e.value[t.dataset.toggleId]=function(e){window.innerWidth||document.documentElement.clientWidth;var t=window.innerHeight||document.documentElement.clientHeight;return e.getBoundingClientRect().bottom+1900&&e[0].focus()},yb=function(){var e=Array.from(document.querySelectorAll(".".concat(hb)));e.length>0&&e[e.length-1].focus()},bb=function(e){"true"===e.getAttribute("aria-expanded")||e.click()},wb=function(e){"true"===e.getAttribute("aria-expanded")&&e.click()},_b=function(){var e=document.activeElement,t=Eb(e,hb);if(!t){return document.addEventListener("logsPageLoaded",(function e(){setTimeout((function(){mb(),bb(document.activeElement)}),50),document.removeEventListener("logsPageLoaded",e)})),void document.dispatchEvent(new Event("goToNextPage"))}wb(e),t.focus(),bb(t)},xb=function(){var e=document.activeElement,t=kb(e,hb);if(!t){return document.addEventListener("logsPageLoaded",(function e(){setTimeout((function(){yb(),bb(document.activeElement)}),50),document.removeEventListener("logsPageLoaded",e)})),void document.dispatchEvent(new Event("goToPreviousPage"))}wb(e),t.focus(),bb(t)},Sb=function(){var e=Eb(document.activeElement,db);e&&e.focus()},Ob=function(){var e=kb(document.activeElement,db);e&&e.focus()},kb=function(e,t){for(var n=Array.from(document.querySelectorAll(".".concat(t))),r=n.findIndex((function(t){return t===e}))-1;r>=0&&null===n[r].offsetParent;)r--;return n[r]?n[r]:null},Eb=function(e,t){for(var n=Array.from(document.querySelectorAll(".".concat(t))),r=n.findIndex((function(t){return t===e}))+1;rt&&(e.preventDefault(),n[t].focus())}else if("ArrowUp"===e.key){var r=kb(document.activeElement,hb);r&&(e.preventDefault(),r.focus())}else if("ArrowDown"===e.key){var o=Eb(document.activeElement,hb);o&&(e.preventDefault(),o.focus())}},Ab=function(e){if("ArrowLeft"===e.key){var t=Cb(document.activeElement,vb),n=Array.from(document.querySelectorAll(".".concat(hb)));n.length>t&&(e.preventDefault(),n[t].focus())}else if("ArrowUp"===e.key){var r=kb(document.activeElement,vb);r&&(e.preventDefault(),r.focus())}else if("ArrowDown"===e.key){var o=Eb(document.activeElement,vb);o&&(e.preventDefault(),o.focus())}else if("Enter"===e.key||" "===e.key){e.preventDefault();var i=document.activeElement;i.click(),i.focus()}},jb=function(e){"ArrowUp"===e.key?(e.preventDefault(),Ob()):"ArrowDown"===e.key?(e.preventDefault(),Sb()):"ArrowRight"===e.key&&(e.preventDefault(),document.activeElement.nextElementSibling.focus())},Lb=function(e){if("ArrowLeft"===e.key)e.preventDefault(),document.activeElement.previousElementSibling.focus();else if("ArrowRight"===e.key){e.preventDefault();var t=Array.from(document.querySelectorAll(".".concat(hb)));t.length>0&&t[0].focus()}};function Rb(e,t){return Ni(),Vi("svg",{xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24","stroke-width":"1.5",stroke:"currentColor","aria-hidden":"true"},[Ji("path",{"stroke-linecap":"round","stroke-linejoin":"round",d:"M12 9.75v6.75m0 0l-3-3m3 3l3-3m-8.25 6a4.5 4.5 0 01-1.41-8.775 5.25 5.25 0 0110.233-2.33 3 3 0 013.758 3.848A3.752 3.752 0 0118 19.5H6.75z"})])}const Ib={__name:"DownloadLink",props:["url"],setup:function(e){var t=e,n=function(){ov.get("".concat(t.url,"/request")).then((function(e){r(e.data.url)})).catch((function(e){e.response&&e.response.data&&alert("".concat(e.message,": ").concat(e.response.data.message,". Check developer console for more info."))}))},r=function(e){var t=document.createElement("a");t.href=e,t.setAttribute("download",""),document.body.appendChild(t),t.click(),document.body.removeChild(t)};return function(e,t){return Ni(),Vi("button",{onClick:n},[lo(e.$slots,"default",{},(function(){return[Qi(Wt(Rb),{class:"w-4 h-4 mr-2"}),ts(" Download ")]}))])}}};function Fb(e){return Fb="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},Fb(e)}function Nb(){Nb=function(){return t};var e,t={},n=Object.prototype,r=n.hasOwnProperty,o=Object.defineProperty||function(e,t,n){e[t]=n.value},i="function"==typeof Symbol?Symbol:{},s=i.iterator||"@@iterator",l=i.asyncIterator||"@@asyncIterator",a=i.toStringTag||"@@toStringTag";function u(e,t,n){return Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}),e[t]}try{u({},"")}catch(e){u=function(e,t,n){return e[t]=n}}function c(e,t,n,r){var i=t&&t.prototype instanceof m?t:m,s=Object.create(i.prototype),l=new A(r||[]);return o(s,"_invoke",{value:E(e,n,l)}),s}function f(e,t,n){try{return{type:"normal",arg:e.call(t,n)}}catch(e){return{type:"throw",arg:e}}}t.wrap=c;var p="suspendedStart",d="suspendedYield",h="executing",v="completed",g={};function m(){}function y(){}function b(){}var w={};u(w,s,(function(){return this}));var _=Object.getPrototypeOf,x=_&&_(_(j([])));x&&x!==n&&r.call(x,s)&&(w=x);var S=b.prototype=m.prototype=Object.create(w);function O(e){["next","throw","return"].forEach((function(t){u(e,t,(function(e){return this._invoke(t,e)}))}))}function k(e,t){function n(o,i,s,l){var a=f(e[o],e,i);if("throw"!==a.type){var u=a.arg,c=u.value;return c&&"object"==Fb(c)&&r.call(c,"__await")?t.resolve(c.__await).then((function(e){n("next",e,s,l)}),(function(e){n("throw",e,s,l)})):t.resolve(c).then((function(e){u.value=e,s(u)}),(function(e){return n("throw",e,s,l)}))}l(a.arg)}var i;o(this,"_invoke",{value:function(e,r){function o(){return new t((function(t,o){n(e,r,t,o)}))}return i=i?i.then(o,o):o()}})}function E(t,n,r){var o=p;return function(i,s){if(o===h)throw new Error("Generator is already running");if(o===v){if("throw"===i)throw s;return{value:e,done:!0}}for(r.method=i,r.arg=s;;){var l=r.delegate;if(l){var a=C(l,r);if(a){if(a===g)continue;return a}}if("next"===r.method)r.sent=r._sent=r.arg;else if("throw"===r.method){if(o===p)throw o=v,r.arg;r.dispatchException(r.arg)}else"return"===r.method&&r.abrupt("return",r.arg);o=h;var u=f(t,n,r);if("normal"===u.type){if(o=r.done?v:d,u.arg===g)continue;return{value:u.arg,done:r.done}}"throw"===u.type&&(o=v,r.method="throw",r.arg=u.arg)}}}function C(t,n){var r=n.method,o=t.iterator[r];if(o===e)return n.delegate=null,"throw"===r&&t.iterator.return&&(n.method="return",n.arg=e,C(t,n),"throw"===n.method)||"return"!==r&&(n.method="throw",n.arg=new TypeError("The iterator does not provide a '"+r+"' method")),g;var i=f(o,t.iterator,n.arg);if("throw"===i.type)return n.method="throw",n.arg=i.arg,n.delegate=null,g;var s=i.arg;return s?s.done?(n[t.resultName]=s.value,n.next=t.nextLoc,"return"!==n.method&&(n.method="next",n.arg=e),n.delegate=null,g):s:(n.method="throw",n.arg=new TypeError("iterator result is not an object"),n.delegate=null,g)}function P(e){var t={tryLoc:e[0]};1 in e&&(t.catchLoc=e[1]),2 in e&&(t.finallyLoc=e[2],t.afterLoc=e[3]),this.tryEntries.push(t)}function T(e){var t=e.completion||{};t.type="normal",delete t.arg,e.completion=t}function A(e){this.tryEntries=[{tryLoc:"root"}],e.forEach(P,this),this.reset(!0)}function j(t){if(t||""===t){var n=t[s];if(n)return n.call(t);if("function"==typeof t.next)return t;if(!isNaN(t.length)){var o=-1,i=function n(){for(;++o=0;--i){var s=this.tryEntries[i],l=s.completion;if("root"===s.tryLoc)return o("end");if(s.tryLoc<=this.prev){var a=r.call(s,"catchLoc"),u=r.call(s,"finallyLoc");if(a&&u){if(this.prev=0;--n){var o=this.tryEntries[n];if(o.tryLoc<=this.prev&&r.call(o,"finallyLoc")&&this.prev=0;--t){var n=this.tryEntries[t];if(n.finallyLoc===e)return this.complete(n.completion,n.afterLoc),T(n),g}},catch:function(e){for(var t=this.tryEntries.length-1;t>=0;--t){var n=this.tryEntries[t];if(n.tryLoc===e){var r=n.completion;if("throw"===r.type){var o=r.arg;T(n)}return o}}throw new Error("illegal catch attempt")},delegateYield:function(t,n,r){return this.delegate={iterator:j(t),resultName:n,nextLoc:r},"next"===this.method&&(this.arg=e),g}},t}function Mb(e,t,n,r,o,i,s){try{var l=e[i](s),a=l.value}catch(e){return void n(e)}l.done?t(a):Promise.resolve(a).then(r,o)}var Db={class:"file-item group"},Bb={key:0,class:"sr-only"},Ub={key:1,class:"sr-only"},$b={key:2,class:"my-auto mr-2"},Vb=["onClick","checked","value"],Hb={class:"file-name"},zb=Ji("span",{class:"sr-only"},"Name:",-1),qb={class:"file-size"},Wb=Ji("span",{class:"sr-only"},"Size:",-1),Kb={class:"py-2"},Gb={class:"text-brand-500"},Yb=Ji("div",{class:"divider"},null,-1);const Jb={__name:"FileListItem",props:{logFile:{type:Object,required:!0},showSelectToggle:{type:Boolean,default:!1}},emits:["selectForDeletion"],setup:function(e,t){t.emit;var n=e,r=nb(),o=Fg(),i=lb(),s=i.dropdownDirections,l=i.calculateDropdownDirection,a=Ls((function(){return r.selectedFile&&r.selectedFile.identifier===n.logFile.identifier})),u=function(){var e,t=(e=Nb().mark((function e(){return Nb().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:if(!confirm("Are you sure you want to delete the log file '".concat(n.logFile.name,"'? THIS ACTION CANNOT BE UNDONE."))){e.next=6;break}return e.next=3,r.deleteFile(n.logFile);case 3:return n.logFile.identifier===r.selectedFileIdentifier&&sb(o,"file",null),e.next=6,r.loadFolders();case 6:case"end":return e.stop()}}),e)})),function(){var t=this,n=arguments;return new Promise((function(r,o){var i=e.apply(t,n);function s(e){Mb(i,r,o,s,l,"next",e)}function l(e){Mb(i,r,o,s,l,"throw",e)}s(void 0)}))});return function(){return t.apply(this,arguments)}}(),c=function(){r.checkBoxToggle(n.logFile.identifier)},f=function(){r.toggleCheckboxVisibility(),c()};return function(t,n){return Ni(),Vi("div",{class:ee(["file-item-container",[a.value?"active":""]])},[Qi(Wt(Lm),null,{default:$n((function(){return[Ji("div",Db,[Ji("button",{class:"file-item-info",onKeydown:n[0]||(n[0]=function(){return Wt(jb)&&Wt(jb).apply(void 0,arguments)})},[a.value?rs("",!0):(Ni(),Vi("span",Bb,"Select log file")),a.value?(Ni(),Vi("span",Ub,"Deselect log file")):rs("",!0),e.logFile.can_delete?yr((Ni(),Vi("span",$b,[Ji("input",{type:"checkbox",onClick:la(c,["stop"]),checked:Wt(r).isChecked(e.logFile),value:Wt(r).isChecked(e.logFile)},null,8,Vb)],512)),[[pl,Wt(r).checkBoxesVisibility]]):rs("",!0),Ji("span",Hb,[zb,ts(ce(e.logFile.name),1)]),Ji("span",qb,[Wb,ts(ce(e.logFile.size_formatted),1)])],32),Qi(Wt(Rm),{as:"button",class:"file-dropdown-toggle group-hover:border-brand-600 group-hover:dark:border-brand-800","data-toggle-id":e.logFile.identifier,onKeydown:Wt(Lb),onClick:n[1]||(n[1]=la((function(e){return Wt(l)(e.target)}),["stop"]))},{default:$n((function(){return[Qi(Wt(Hm),{class:"w-4 h-4 pointer-events-none"})]})),_:1},8,["data-toggle-id","onKeydown"])]),Qi(Ys,{"leave-active-class":"transition ease-in duration-100","leave-from-class":"opacity-100 scale-100","leave-to-class":"opacity-0 scale-90","enter-active-class":"transition ease-out duration-100","enter-from-class":"opacity-0 scale-90","enter-to-class":"opacity-100 scale-100"},{default:$n((function(){return[Qi(Wt(Im),{as:"div",class:ee(["dropdown w-48",[Wt(s)[e.logFile.identifier]]])},{default:$n((function(){return[Ji("div",Kb,[Qi(Wt(Fm),{onClick:n[2]||(n[2]=la((function(t){return Wt(r).clearCacheForFile(e.logFile)}),["stop","prevent"]))},{default:$n((function(t){return[Ji("button",{class:ee([t.active?"active":""])},[yr(Qi(Wt(zm),{class:"h-4 w-4 mr-2"},null,512),[[pl,!Wt(r).clearingCache[e.logFile.identifier]]]),yr(Qi(pb,null,null,512),[[pl,Wt(r).clearingCache[e.logFile.identifier]]]),yr(Ji("span",null,"Clear index",512),[[pl,!Wt(r).cacheRecentlyCleared[e.logFile.identifier]&&!Wt(r).clearingCache[e.logFile.identifier]]]),yr(Ji("span",null,"Clearing...",512),[[pl,!Wt(r).cacheRecentlyCleared[e.logFile.identifier]&&Wt(r).clearingCache[e.logFile.identifier]]]),yr(Ji("span",Gb,"Index cleared",512),[[pl,Wt(r).cacheRecentlyCleared[e.logFile.identifier]]])],2)]})),_:1}),e.logFile.can_download?(Ni(),Hi(Wt(Fm),{key:0,onClick:n[3]||(n[3]=la((function(){}),["stop"]))},{default:$n((function(t){var n=t.active;return[Qi(Ib,{url:e.logFile.download_url,class:ee([n?"active":""])},null,8,["url","class"])]})),_:1})):rs("",!0),e.logFile.can_delete?(Ni(),Vi(Ai,{key:1},[Yb,Qi(Wt(Fm),{onClick:la(u,["stop","prevent"])},{default:$n((function(e){return[Ji("button",{class:ee([e.active?"active":""])},[Qi(Wt(Bm),{class:"w-4 h-4 mr-2"}),ts(" Delete ")],2)]})),_:1},8,["onClick"]),Qi(Wt(Fm),{onClick:la(f,["stop"])},{default:$n((function(e){return[Ji("button",{class:ee([e.active?"active":""])},[Qi(Wt(Bm),{class:"w-4 h-4 mr-2"}),ts(" Delete Multiple ")],2)]})),_:1},8,["onClick"])],64)):rs("",!0)])]})),_:1},8,["class"])]})),_:1})]})),_:1})],2)}}},Qb=Jb;function Zb(e,t){return Ni(),Vi("svg",{xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24","stroke-width":"1.5",stroke:"currentColor","aria-hidden":"true"},[Ji("path",{"stroke-linecap":"round","stroke-linejoin":"round",d:"M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z"}),Ji("path",{"stroke-linecap":"round","stroke-linejoin":"round",d:"M15 12a3 3 0 11-6 0 3 3 0 016 0z"})])}function Xb(e,t){return Ni(),Vi("svg",{xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24","stroke-width":"1.5",stroke:"currentColor","aria-hidden":"true"},[Ji("path",{"stroke-linecap":"round","stroke-linejoin":"round",d:"M7.217 10.907a2.25 2.25 0 100 2.186m0-2.186c.18.324.283.696.283 1.093s-.103.77-.283 1.093m0-2.186l9.566-5.314m-9.566 7.5l9.566 5.314m0 0a2.25 2.25 0 103.935 2.186 2.25 2.25 0 00-3.935-2.186zm0-12.814a2.25 2.25 0 103.933-2.185 2.25 2.25 0 00-3.933 2.185z"})])}function ew(e,t){return Ni(),Vi("svg",{xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24","stroke-width":"1.5",stroke:"currentColor","aria-hidden":"true"},[Ji("path",{"stroke-linecap":"round","stroke-linejoin":"round",d:"M9 17.25v1.007a3 3 0 01-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0115 18.257V17.25m6-12V15a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 15V5.25m18 0A2.25 2.25 0 0018.75 3H5.25A2.25 2.25 0 003 5.25m18 0V12a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 12V5.25"})])}function tw(e,t){return Ni(),Vi("svg",{xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24","stroke-width":"1.5",stroke:"currentColor","aria-hidden":"true"},[Ji("path",{"stroke-linecap":"round","stroke-linejoin":"round",d:"M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z"})])}function nw(e,t){return Ni(),Vi("svg",{xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24","stroke-width":"1.5",stroke:"currentColor","aria-hidden":"true"},[Ji("path",{"stroke-linecap":"round","stroke-linejoin":"round",d:"M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z"})])}function rw(e,t){return Ni(),Vi("svg",{xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24","stroke-width":"1.5",stroke:"currentColor","aria-hidden":"true"},[Ji("path",{"stroke-linecap":"round","stroke-linejoin":"round",d:"M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z"})])}function ow(e,t){return Ni(),Vi("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 20 20",fill:"currentColor","aria-hidden":"true"},[Ji("path",{"fill-rule":"evenodd",d:"M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z","clip-rule":"evenodd"})])}var iw={class:"checkmark w-[18px] h-[18px] bg-gray-50 dark:bg-gray-800 rounded border dark:border-gray-600 inline-flex items-center justify-center"};const sw={__name:"Checkmark",props:{checked:{type:Boolean,required:!0}},setup:function(e){return function(t,n){return Ni(),Vi("div",iw,[e.checked?(Ni(),Hi(Wt(ow),{key:0,width:"18",height:"18",class:"w-full h-full"})):rs("",!0)])}}};var lw=Ji("span",{class:"sr-only"},"Settings dropdown",-1),aw={class:"py-2"},uw=Ji("div",{class:"label"},"Settings",-1),cw=Ji("span",{class:"ml-3"},"Shorter stack traces",-1),fw=Ji("div",{class:"divider"},null,-1),pw=Ji("div",{class:"label"},"Actions",-1),dw={class:"text-brand-500"},hw={class:"text-brand-500"},vw=Ji("div",{class:"divider"},null,-1),gw=["innerHTML"];const mw={__name:"SiteSettingsDropdown",setup:function(e){var t=Yy(),n=nb(),r=$t(!1),o=function(){ib(window.location.href),r.value=!0,setTimeout((function(){return r.value=!1}),2e3)};return dr((function(){return t.shorterStackTraces}),(function(){return t.loadLogs()})),function(e,i){return Ni(),Hi(Wt(Lm),{as:"div",class:"relative"},{default:$n((function(){return[Qi(Wt(Rm),{as:"button",class:"menu-button"},{default:$n((function(){return[lw,Qi(Wt(Zb),{class:"w-5 h-5"})]})),_:1}),Qi(Ys,{"leave-active-class":"transition ease-in duration-100","leave-from-class":"opacity-100 scale-100","leave-to-class":"opacity-0 scale-90","enter-active-class":"transition ease-out duration-100","enter-from-class":"opacity-0 scale-90","enter-to-class":"opacity-100 scale-100"},{default:$n((function(){return[Qi(Wt(Im),{as:"div",style:{"min-width":"250px"},class:"dropdown"},{default:$n((function(){return[Ji("div",aw,[uw,Qi(Wt(Fm),null,{default:$n((function(e){return[Ji("button",{class:ee([e.active?"active":""]),onClick:i[0]||(i[0]=la((function(e){return Wt(t).shorterStackTraces=!Wt(t).shorterStackTraces}),["stop","prevent"]))},[Qi(sw,{checked:Wt(t).shorterStackTraces},null,8,["checked"]),cw],2)]})),_:1}),fw,pw,Qi(Wt(Fm),{onClick:la(Wt(n).clearCacheForAllFiles,["stop","prevent"])},{default:$n((function(e){return[Ji("button",{class:ee([e.active?"active":""])},[yr(Qi(Wt(zm),{class:"w-4 h-4 mr-1.5"},null,512),[[pl,!Wt(n).clearingCache["*"]]]),yr(Qi(pb,{class:"w-4 h-4 mr-1.5"},null,512),[[pl,Wt(n).clearingCache["*"]]]),yr(Ji("span",null,"Clear indices for all files",512),[[pl,!Wt(n).cacheRecentlyCleared["*"]&&!Wt(n).clearingCache["*"]]]),yr(Ji("span",null,"Please wait...",512),[[pl,!Wt(n).cacheRecentlyCleared["*"]&&Wt(n).clearingCache["*"]]]),yr(Ji("span",dw,"File indices cleared",512),[[pl,Wt(n).cacheRecentlyCleared["*"]]])],2)]})),_:1},8,["onClick"]),Qi(Wt(Fm),{onClick:la(o,["stop","prevent"])},{default:$n((function(e){return[Ji("button",{class:ee([e.active?"active":""])},[Qi(Wt(Xb),{class:"w-4 h-4"}),yr(Ji("span",null,"Share this page",512),[[pl,!r.value]]),yr(Ji("span",hw,"Link copied!",512),[[pl,r.value]])],2)]})),_:1},8,["onClick"]),vw,Qi(Wt(Fm),{onClick:i[1]||(i[1]=la((function(e){return Wt(t).toggleTheme()}),["stop","prevent"]))},{default:$n((function(e){return[Ji("button",{class:ee([e.active?"active":""])},[yr(Qi(Wt(ew),{class:"w-4 h-4"},null,512),[[pl,Wt(t).theme===Wt(Ky).System]]),yr(Qi(Wt(tw),{class:"w-4 h-4"},null,512),[[pl,Wt(t).theme===Wt(Ky).Light]]),yr(Qi(Wt(nw),{class:"w-4 h-4"},null,512),[[pl,Wt(t).theme===Wt(Ky).Dark]]),Ji("span",null,[ts("Theme: "),Ji("span",{innerHTML:Wt(t).theme,class:"font-semibold"},null,8,gw)])],2)]})),_:1}),Qi(Wt(Fm),null,{default:$n((function(e){var n=e.active;return[Ji("button",{onClick:i[2]||(i[2]=function(e){return Wt(t).helpSlideOverOpen=!0}),class:ee([n?"active":""])},[Qi(Wt(rw),{class:"w-4 h-4"}),ts(" Keyboard Shortcuts ")],2)]})),_:1})])]})),_:1})]})),_:1})]})),_:1})}}};var yw=(e=>(e[e.None=1]="None",e[e.Focusable=2]="Focusable",e[e.Hidden=4]="Hidden",e))(yw||{});let bw=Rr({name:"Hidden",props:{as:{type:[Object,String],default:"div"},features:{type:Number,default:1}},setup:(e,{slots:t,attrs:n})=>()=>{let{features:r,...o}=e;return Ug({ourProps:{"aria-hidden":2==(2&r)||void 0,style:{position:"fixed",top:1,left:1,width:1,height:0,padding:0,margin:-1,overflow:"hidden",clip:"rect(0, 0, 0, 0)",whiteSpace:"nowrap",borderWidth:"0",...4==(4&r)&&2!=(2&r)&&{display:"none"}}},theirProps:o,slot:{},attrs:n,slots:t,name:"Hidden"})}});function ww(e={},t=null,n=[]){for(let[r,o]of Object.entries(e))xw(n,_w(t,r),o);return n}function _w(e,t){return e?e+"["+t+"]":t}function xw(e,t,n){if(Array.isArray(n))for(let[r,o]of n.entries())xw(e,_w(t,r.toString()),o);else n instanceof Date?e.push([t,n.toISOString()]):"boolean"==typeof n?e.push([t,n?"1":"0"]):"string"==typeof n?e.push([t,n]):"number"==typeof n?e.push([t,`${n}`]):null==n?e.push([t,""]):ww(n,t,e)}function Sw(e,t){return e===t}var Ow=(e=>(e[e.Open=0]="Open",e[e.Closed=1]="Closed",e))(Ow||{}),kw=(e=>(e[e.Single=0]="Single",e[e.Multi=1]="Multi",e))(kw||{}),Ew=(e=>(e[e.Pointer=0]="Pointer",e[e.Other=1]="Other",e))(Ew||{});let Cw=Symbol("ListboxContext");function Pw(e){let t=Yo(Cw,null);if(null===t){let t=new Error(`<${e} /> is missing a parent component.`);throw Error.captureStackTrace&&Error.captureStackTrace(t,Pw),t}return t}let Tw=Rr({name:"Listbox",emits:{"update:modelValue":e=>!0},props:{as:{type:[Object,String],default:"template"},disabled:{type:[Boolean],default:!1},by:{type:[String,Function],default:()=>Sw},horizontal:{type:[Boolean],default:!1},modelValue:{type:[Object,String,Number,Boolean],default:void 0},defaultValue:{type:[Object,String,Number,Boolean],default:void 0},form:{type:String,optional:!0},name:{type:String,optional:!0},multiple:{type:[Boolean],default:!1}},inheritAttrs:!1,setup(e,{slots:t,attrs:n,emit:r}){let o=$t(1),i=$t(null),s=$t(null),l=$t(null),a=$t([]),u=$t(""),c=$t(null),f=$t(1);function p(e=(e=>e)){let t=null!==c.value?a.value[c.value]:null,n=ym(e(a.value.slice()),(e=>Jg(e.dataRef.domRef))),r=t?n.indexOf(t):null;return-1===r&&(r=null),{options:n,activeOptionIndex:r}}let d=Ls((()=>e.multiple?1:0)),[h,v]=function(e,t,n){let r=$t(null==n?void 0:n.value),o=Ls((()=>void 0!==e.value));return[Ls((()=>o.value?e.value:r.value)),function(e){return o.value||(r.value=e),null==t?void 0:t(e)}]}(Ls((()=>e.modelValue)),(e=>r("update:modelValue",e)),Ls((()=>e.defaultValue))),g=Ls((()=>void 0===h.value?Mg(d.value,{1:[],0:void 0}):h.value)),m={listboxState:o,value:g,mode:d,compare(t,n){if("string"==typeof e.by){let r=e.by;return(null==t?void 0:t[r])===(null==n?void 0:n[r])}return e.by(t,n)},orientation:Ls((()=>e.horizontal?"horizontal":"vertical")),labelRef:i,buttonRef:s,optionsRef:l,disabled:Ls((()=>e.disabled)),options:a,searchQuery:u,activeOptionIndex:c,activationTrigger:f,closeListbox(){e.disabled||1!==o.value&&(o.value=1,c.value=null)},openListbox(){e.disabled||0!==o.value&&(o.value=0)},goToOption(t,n,r){if(e.disabled||1===o.value)return;let i=p(),s=Yg(t===Gg.Specific?{focus:Gg.Specific,id:n}:{focus:t},{resolveItems:()=>i.options,resolveActiveIndex:()=>i.activeOptionIndex,resolveId:e=>e.id,resolveDisabled:e=>e.dataRef.disabled});u.value="",c.value=s,f.value=null!=r?r:1,a.value=i.options},search(t){if(e.disabled||1===o.value)return;let n=""!==u.value?0:1;u.value+=t.toLowerCase();let r=(null!==c.value?a.value.slice(c.value+n).concat(a.value.slice(0,c.value+n)):a.value).find((e=>e.dataRef.textValue.startsWith(u.value)&&!e.dataRef.disabled)),i=r?a.value.indexOf(r):-1;-1===i||i===c.value||(c.value=i,f.value=1)},clearSearch(){e.disabled||1!==o.value&&""!==u.value&&(u.value="")},registerOption(e,t){let n=p((n=>[...n,{id:e,dataRef:t}]));a.value=n.options,c.value=n.activeOptionIndex},unregisterOption(e){let t=p((t=>{let n=t.findIndex((t=>t.id===e));return-1!==n&&t.splice(n,1),t}));a.value=t.options,c.value=t.activeOptionIndex,f.value=1},theirOnChange(t){e.disabled||v(t)},select(t){e.disabled||v(Mg(d.value,{0:()=>t,1:()=>{let e=It(m.value.value).slice(),n=It(t),r=e.findIndex((e=>m.compare(n,It(e))));return-1===r?e.push(n):e.splice(r,1),e}}))}};xm([s,l],((e,t)=>{var n;m.closeListbox(),dm(t,pm.Loose)||(e.preventDefault(),null==(n=Jg(s))||n.focus())}),Ls((()=>0===o.value))),Go(Cw,m),om(Ls((()=>Mg(o.value,{0:nm.Open,1:nm.Closed}))));let y=Ls((()=>{var e;return null==(e=Jg(s))?void 0:e.closest("form")}));return Jr((()=>{dr([y],(()=>{if(y.value&&void 0!==e.defaultValue)return y.value.addEventListener("reset",t),()=>{var e;null==(e=y.value)||e.removeEventListener("reset",t)};function t(){m.theirOnChange(e.defaultValue)}}),{immediate:!0})})),()=>{let{name:r,modelValue:i,disabled:s,form:l,...a}=e,u={open:0===o.value,disabled:s,value:g.value};return Rs(Ai,[...null!=r&&null!=g.value?ww({[r]:g.value}).map((([e,t])=>Rs(bw,function(e){let t=Object.assign({},e);for(let e in t)void 0===t[e]&&delete t[e];return t}({features:yw.Hidden,key:e,as:"input",type:"hidden",hidden:!0,readOnly:!0,form:l,name:e,value:t})))):[],Ug({ourProps:{},theirProps:{...n,...zg(a,["defaultValue","onUpdate:modelValue","horizontal","multiple","by"])},slot:u,slots:t,attrs:n,name:"Listbox"})])}}}),Aw=Rr({name:"ListboxLabel",props:{as:{type:[Object,String],default:"label"},id:{type:String,default:()=>`headlessui-listbox-label-${Wg()}`}},setup(e,{attrs:t,slots:n}){let r=Pw("ListboxLabel");function o(){var e;null==(e=Jg(r.buttonRef))||e.focus({preventScroll:!0})}return()=>{let i={open:0===r.listboxState.value,disabled:r.disabled.value},{id:s,...l}=e;return Ug({ourProps:{id:s,ref:r.labelRef,onClick:o},theirProps:l,slot:i,attrs:t,slots:n,name:"ListboxLabel"})}}}),jw=Rr({name:"ListboxButton",props:{as:{type:[Object,String],default:"button"},id:{type:String,default:()=>`headlessui-listbox-button-${Wg()}`}},setup(e,{attrs:t,slots:n,expose:r}){let o=Pw("ListboxButton");function i(e){switch(e.key){case Kg.Space:case Kg.Enter:case Kg.ArrowDown:e.preventDefault(),o.openListbox(),bn((()=>{var e;null==(e=Jg(o.optionsRef))||e.focus({preventScroll:!0}),o.value.value||o.goToOption(Gg.First)}));break;case Kg.ArrowUp:e.preventDefault(),o.openListbox(),bn((()=>{var e;null==(e=Jg(o.optionsRef))||e.focus({preventScroll:!0}),o.value.value||o.goToOption(Gg.Last)}))}}function s(e){if(e.key===Kg.Space)e.preventDefault()}function l(e){o.disabled.value||(0===o.listboxState.value?(o.closeListbox(),bn((()=>{var e;return null==(e=Jg(o.buttonRef))?void 0:e.focus({preventScroll:!0})}))):(e.preventDefault(),o.openListbox(),function(e){requestAnimationFrame((()=>requestAnimationFrame(e)))}((()=>{var e;return null==(e=Jg(o.optionsRef))?void 0:e.focus({preventScroll:!0})}))))}r({el:o.buttonRef,$el:o.buttonRef});let a=sm(Ls((()=>({as:e.as,type:t.type}))),o.buttonRef);return()=>{var r,u;let c={open:0===o.listboxState.value,disabled:o.disabled.value,value:o.value.value},{id:f,...p}=e;return Ug({ourProps:{ref:o.buttonRef,id:f,type:a.value,"aria-haspopup":"listbox","aria-controls":null==(r=Jg(o.optionsRef))?void 0:r.id,"aria-expanded":0===o.listboxState.value,"aria-labelledby":o.labelRef.value?[null==(u=Jg(o.labelRef))?void 0:u.id,f].join(" "):void 0,disabled:!0===o.disabled.value||void 0,onKeydown:i,onKeyup:s,onClick:l},theirProps:p,slot:c,attrs:t,slots:n,name:"ListboxButton"})}}}),Lw=Rr({name:"ListboxOptions",props:{as:{type:[Object,String],default:"ul"},static:{type:Boolean,default:!1},unmount:{type:Boolean,default:!0},id:{type:String,default:()=>`headlessui-listbox-options-${Wg()}`}},setup(e,{attrs:t,slots:n,expose:r}){let o=Pw("ListboxOptions"),i=$t(null);function s(e){switch(i.value&&clearTimeout(i.value),e.key){case Kg.Space:if(""!==o.searchQuery.value)return e.preventDefault(),e.stopPropagation(),o.search(e.key);case Kg.Enter:if(e.preventDefault(),e.stopPropagation(),null!==o.activeOptionIndex.value){let e=o.options.value[o.activeOptionIndex.value];o.select(e.dataRef.value)}0===o.mode.value&&(o.closeListbox(),bn((()=>{var e;return null==(e=Jg(o.buttonRef))?void 0:e.focus({preventScroll:!0})})));break;case Mg(o.orientation.value,{vertical:Kg.ArrowDown,horizontal:Kg.ArrowRight}):return e.preventDefault(),e.stopPropagation(),o.goToOption(Gg.Next);case Mg(o.orientation.value,{vertical:Kg.ArrowUp,horizontal:Kg.ArrowLeft}):return e.preventDefault(),e.stopPropagation(),o.goToOption(Gg.Previous);case Kg.Home:case Kg.PageUp:return e.preventDefault(),e.stopPropagation(),o.goToOption(Gg.First);case Kg.End:case Kg.PageDown:return e.preventDefault(),e.stopPropagation(),o.goToOption(Gg.Last);case Kg.Escape:e.preventDefault(),e.stopPropagation(),o.closeListbox(),bn((()=>{var e;return null==(e=Jg(o.buttonRef))?void 0:e.focus({preventScroll:!0})}));break;case Kg.Tab:e.preventDefault(),e.stopPropagation();break;default:1===e.key.length&&(o.search(e.key),i.value=setTimeout((()=>o.clearSearch()),350))}}r({el:o.optionsRef,$el:o.optionsRef});let l=rm(),a=Ls((()=>null!==l?(l.value&nm.Open)===nm.Open:0===o.listboxState.value));return()=>{var r,i,l,u;let c={open:0===o.listboxState.value},{id:f,...p}=e;return Ug({ourProps:{"aria-activedescendant":null===o.activeOptionIndex.value||null==(r=o.options.value[o.activeOptionIndex.value])?void 0:r.id,"aria-multiselectable":1===o.mode.value||void 0,"aria-labelledby":null!=(u=null==(i=Jg(o.labelRef))?void 0:i.id)?u:null==(l=Jg(o.buttonRef))?void 0:l.id,"aria-orientation":o.orientation.value,id:f,onKeydown:s,role:"listbox",tabIndex:0,ref:o.optionsRef},theirProps:p,slot:c,attrs:t,slots:n,features:Dg.RenderStrategy|Dg.Static,visible:a.value,name:"ListboxOptions"})}}}),Rw=Rr({name:"ListboxOption",props:{as:{type:[Object,String],default:"li"},value:{type:[Object,String,Number,Boolean]},disabled:{type:Boolean,default:!1},id:{type:String,default:()=>`headlessui-listbox.option-${Wg()}`}},setup(e,{slots:t,attrs:n,expose:r}){let o=Pw("ListboxOption"),i=$t(null);r({el:i,$el:i});let s=Ls((()=>null!==o.activeOptionIndex.value&&o.options.value[o.activeOptionIndex.value].id===e.id)),l=Ls((()=>Mg(o.mode.value,{0:()=>o.compare(It(o.value.value),It(e.value)),1:()=>It(o.value.value).some((t=>o.compare(It(t),It(e.value))))}))),a=Ls((()=>Mg(o.mode.value,{1:()=>{var t;let n=It(o.value.value);return(null==(t=o.options.value.find((e=>n.some((t=>o.compare(It(t),It(e.dataRef.value)))))))?void 0:t.id)===e.id},0:()=>l.value}))),u=Cm(i),c=Ls((()=>({disabled:e.disabled,value:e.value,get textValue(){return u()},domRef:i})));function f(t){if(e.disabled)return t.preventDefault();o.select(e.value),0===o.mode.value&&(o.closeListbox(),bn((()=>{var e;return null==(e=Jg(o.buttonRef))?void 0:e.focus({preventScroll:!0})})))}function p(){if(e.disabled)return o.goToOption(Gg.Nothing);o.goToOption(Gg.Specific,e.id)}Jr((()=>o.registerOption(e.id,c))),eo((()=>o.unregisterOption(e.id))),Jr((()=>{dr([o.listboxState,l],(()=>{0===o.listboxState.value&&l.value&&Mg(o.mode.value,{1:()=>{a.value&&o.goToOption(Gg.Specific,e.id)},0:()=>{o.goToOption(Gg.Specific,e.id)}})}),{immediate:!0})})),ur((()=>{0===o.listboxState.value&&s.value&&0!==o.activationTrigger.value&&bn((()=>{var e,t;return null==(t=null==(e=Jg(i))?void 0:e.scrollIntoView)?void 0:t.call(e,{block:"nearest"})}))}));let d=Om();function h(e){d.update(e)}function v(t){d.wasMoved(t)&&(e.disabled||s.value||o.goToOption(Gg.Specific,e.id,0))}function g(t){d.wasMoved(t)&&(e.disabled||s.value&&o.goToOption(Gg.Nothing))}return()=>{let{disabled:r}=e,o={active:s.value,selected:l.value,disabled:r},{id:a,value:u,disabled:c,...d}=e;return Ug({ourProps:{id:a,ref:i,role:"option",tabIndex:!0===r?void 0:-1,"aria-disabled":!0===r||void 0,"aria-selected":l.value,disabled:void 0,onClick:f,onFocus:p,onPointerenter:h,onMouseenter:h,onPointermove:v,onMousemove:v,onPointerleave:g,onMouseleave:g},theirProps:d,slot:o,attrs:n,slots:t,name:"ListboxOption"})}}});function Iw(e,t){return Ni(),Vi("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 20 20",fill:"currentColor","aria-hidden":"true"},[Ji("path",{"fill-rule":"evenodd",d:"M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z","clip-rule":"evenodd"})])}var Fw={class:"relative mt-1"},Nw={class:"block truncate"},Mw={class:"pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"};const Dw={__name:"HostSelector",setup:function(e){var t=Fg(),n=Wm();return dr((function(){return n.selectedHost}),(function(e){sb(t,"host",null!=e&&e.is_remote?e.identifier:null)})),function(e,t){return Ni(),Hi(Wt(Tw),{as:"div",modelValue:Wt(n).selectedHostIdentifier,"onUpdate:modelValue":t[0]||(t[0]=function(e){return Wt(n).selectedHostIdentifier=e})},{default:$n((function(){return[Qi(Wt(Aw),{class:"ml-1 block text-sm text-gray-500 dark:text-gray-400"},{default:$n((function(){return[ts("Select host")]})),_:1}),Ji("div",Fw,[Qi(Wt(jw),{id:"hosts-toggle-button",class:"cursor-pointer relative text-gray-800 dark:text-gray-200 w-full cursor-default rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 py-2 pl-4 pr-10 text-left hover:border-brand-600 hover:dark:border-brand-800 focus:border-brand-500 focus:outline-none focus:ring-1 focus:ring-brand-500 text-sm"},{default:$n((function(){var e;return[Ji("span",Nw,ce((null===(e=Wt(n).selectedHost)||void 0===e?void 0:e.name)||"Please select a server"),1),Ji("span",Mw,[Qi(Wt(Iw),{class:"h-5 w-5 text-gray-400","aria-hidden":"true"})])]})),_:1}),Qi(Ys,{"leave-active-class":"transition ease-in duration-100","leave-from-class":"opacity-100","leave-to-class":"opacity-0"},{default:$n((function(){return[Qi(Wt(Lw),{class:"absolute z-20 mt-1 max-h-60 w-full overflow-auto rounded-md shadow-md bg-white dark:bg-gray-800 py-1 border border-gray-200 dark:border-gray-700 ring-1 ring-brand ring-opacity-5 focus:outline-none text-sm"},{default:$n((function(){return[(Ni(!0),Vi(Ai,null,io(Wt(n).hosts,(function(e){return Ni(),Hi(Wt(Rw),{as:"template",key:e.identifier,value:e.identifier},{default:$n((function(t){var n=t.active,r=t.selected;return[Ji("li",{class:ee([n?"text-white bg-brand-600":"text-gray-900 dark:text-gray-300","relative cursor-default select-none py-2 pl-3 pr-9"])},[Ji("span",{class:ee([r?"font-semibold":"font-normal","block truncate"])},ce(e.name),3),r?(Ni(),Vi("span",{key:0,class:ee([n?"text-white":"text-brand-600","absolute inset-y-0 right-0 flex items-center pr-4"])},[Qi(Wt(ow),{class:"h-5 w-5","aria-hidden":"true"})],2)):rs("",!0)],2)]})),_:2},1032,["value"])})),128))]})),_:1})]})),_:1})])]})),_:1},8,["modelValue"])}}},Bw=Dw;var Uw={class:"relative mt-1"},$w={class:"block truncate"},Vw={class:"pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"};const Hw={__name:"FileTypeSelector",setup:function(e){Fg();var t=nb();return function(e,n){return Ni(),Hi(Wt(Tw),{as:"div",modelValue:Wt(t).selectedFileTypes,"onUpdate:modelValue":n[0]||(n[0]=function(e){return Wt(t).selectedFileTypes=e}),multiple:""},{default:$n((function(){return[Qi(Wt(Aw),{class:"ml-1 block text-sm text-gray-500 dark:text-gray-400"},{default:$n((function(){return[ts("Selected file types")]})),_:1}),Ji("div",Uw,[Qi(Wt(jw),{id:"hosts-toggle-button",class:"cursor-pointer relative text-gray-800 dark:text-gray-200 w-full cursor-default rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 py-2 pl-4 pr-10 text-left hover:border-brand-600 hover:dark:border-brand-800 focus:border-brand-500 focus:outline-none focus:ring-1 focus:ring-brand-500 text-sm"},{default:$n((function(){return[Ji("span",$w,ce(Wt(t).selectedFileTypesString),1),Ji("span",Vw,[Qi(Wt(Iw),{class:"h-5 w-5 text-gray-400","aria-hidden":"true"})])]})),_:1}),Qi(Ys,{"leave-active-class":"transition ease-in duration-100","leave-from-class":"opacity-100","leave-to-class":"opacity-0"},{default:$n((function(){return[Qi(Wt(Lw),{class:"absolute z-20 mt-1 max-h-60 w-full overflow-auto rounded-md shadow-md bg-white dark:bg-gray-800 py-1 border border-gray-200 dark:border-gray-700 ring-1 ring-brand ring-opacity-5 focus:outline-none text-sm"},{default:$n((function(){return[(Ni(!0),Vi(Ai,null,io(Wt(t).fileTypesAvailable,(function(e){return Ni(),Hi(Wt(Rw),{as:"template",key:e.identifier,value:e.identifier},{default:$n((function(t){var n=t.active,r=t.selected;return[Ji("li",{class:ee([n?"text-white bg-brand-600":"text-gray-900 dark:text-gray-300","relative cursor-default select-none py-2 pl-3 pr-9"])},[Ji("span",{class:ee([r?"font-semibold":"font-normal","block truncate"])},ce(e.name),3),r?(Ni(),Vi("span",{key:0,class:ee([n?"text-white":"text-brand-600","absolute inset-y-0 right-0 flex items-center pr-4"])},[Qi(Wt(ow),{class:"h-5 w-5","aria-hidden":"true"})],2)):rs("",!0)],2)]})),_:2},1032,["value"])})),128))]})),_:1})]})),_:1})])]})),_:1},8,["modelValue"])}}};function zw(e){return zw="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},zw(e)}function qw(){qw=function(){return t};var e,t={},n=Object.prototype,r=n.hasOwnProperty,o=Object.defineProperty||function(e,t,n){e[t]=n.value},i="function"==typeof Symbol?Symbol:{},s=i.iterator||"@@iterator",l=i.asyncIterator||"@@asyncIterator",a=i.toStringTag||"@@toStringTag";function u(e,t,n){return Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}),e[t]}try{u({},"")}catch(e){u=function(e,t,n){return e[t]=n}}function c(e,t,n,r){var i=t&&t.prototype instanceof m?t:m,s=Object.create(i.prototype),l=new A(r||[]);return o(s,"_invoke",{value:E(e,n,l)}),s}function f(e,t,n){try{return{type:"normal",arg:e.call(t,n)}}catch(e){return{type:"throw",arg:e}}}t.wrap=c;var p="suspendedStart",d="suspendedYield",h="executing",v="completed",g={};function m(){}function y(){}function b(){}var w={};u(w,s,(function(){return this}));var _=Object.getPrototypeOf,x=_&&_(_(j([])));x&&x!==n&&r.call(x,s)&&(w=x);var S=b.prototype=m.prototype=Object.create(w);function O(e){["next","throw","return"].forEach((function(t){u(e,t,(function(e){return this._invoke(t,e)}))}))}function k(e,t){function n(o,i,s,l){var a=f(e[o],e,i);if("throw"!==a.type){var u=a.arg,c=u.value;return c&&"object"==zw(c)&&r.call(c,"__await")?t.resolve(c.__await).then((function(e){n("next",e,s,l)}),(function(e){n("throw",e,s,l)})):t.resolve(c).then((function(e){u.value=e,s(u)}),(function(e){return n("throw",e,s,l)}))}l(a.arg)}var i;o(this,"_invoke",{value:function(e,r){function o(){return new t((function(t,o){n(e,r,t,o)}))}return i=i?i.then(o,o):o()}})}function E(t,n,r){var o=p;return function(i,s){if(o===h)throw new Error("Generator is already running");if(o===v){if("throw"===i)throw s;return{value:e,done:!0}}for(r.method=i,r.arg=s;;){var l=r.delegate;if(l){var a=C(l,r);if(a){if(a===g)continue;return a}}if("next"===r.method)r.sent=r._sent=r.arg;else if("throw"===r.method){if(o===p)throw o=v,r.arg;r.dispatchException(r.arg)}else"return"===r.method&&r.abrupt("return",r.arg);o=h;var u=f(t,n,r);if("normal"===u.type){if(o=r.done?v:d,u.arg===g)continue;return{value:u.arg,done:r.done}}"throw"===u.type&&(o=v,r.method="throw",r.arg=u.arg)}}}function C(t,n){var r=n.method,o=t.iterator[r];if(o===e)return n.delegate=null,"throw"===r&&t.iterator.return&&(n.method="return",n.arg=e,C(t,n),"throw"===n.method)||"return"!==r&&(n.method="throw",n.arg=new TypeError("The iterator does not provide a '"+r+"' method")),g;var i=f(o,t.iterator,n.arg);if("throw"===i.type)return n.method="throw",n.arg=i.arg,n.delegate=null,g;var s=i.arg;return s?s.done?(n[t.resultName]=s.value,n.next=t.nextLoc,"return"!==n.method&&(n.method="next",n.arg=e),n.delegate=null,g):s:(n.method="throw",n.arg=new TypeError("iterator result is not an object"),n.delegate=null,g)}function P(e){var t={tryLoc:e[0]};1 in e&&(t.catchLoc=e[1]),2 in e&&(t.finallyLoc=e[2],t.afterLoc=e[3]),this.tryEntries.push(t)}function T(e){var t=e.completion||{};t.type="normal",delete t.arg,e.completion=t}function A(e){this.tryEntries=[{tryLoc:"root"}],e.forEach(P,this),this.reset(!0)}function j(t){if(t||""===t){var n=t[s];if(n)return n.call(t);if("function"==typeof t.next)return t;if(!isNaN(t.length)){var o=-1,i=function n(){for(;++o=0;--i){var s=this.tryEntries[i],l=s.completion;if("root"===s.tryLoc)return o("end");if(s.tryLoc<=this.prev){var a=r.call(s,"catchLoc"),u=r.call(s,"finallyLoc");if(a&&u){if(this.prev=0;--n){var o=this.tryEntries[n];if(o.tryLoc<=this.prev&&r.call(o,"finallyLoc")&&this.prev=0;--t){var n=this.tryEntries[t];if(n.finallyLoc===e)return this.complete(n.completion,n.afterLoc),T(n),g}},catch:function(e){for(var t=this.tryEntries.length-1;t>=0;--t){var n=this.tryEntries[t];if(n.tryLoc===e){var r=n.completion;if("throw"===r.type){var o=r.arg;T(n)}return o}}throw new Error("illegal catch attempt")},delegateYield:function(t,n,r){return this.delegate={iterator:j(t),resultName:n,nextLoc:r},"next"===this.method&&(this.arg=e),g}},t}function Ww(e,t,n,r,o,i,s){try{var l=e[i](s),a=l.value}catch(e){return void n(e)}l.done?t(a):Promise.resolve(a).then(r,o)}function Kw(e){return function(){var t=this,n=arguments;return new Promise((function(r,o){var i=e.apply(t,n);function s(e){Ww(i,r,o,s,l,"next",e)}function l(e){Ww(i,r,o,s,l,"throw",e)}s(void 0)}))}}var Gw={class:"flex flex-col h-full py-5"},Yw={class:"mx-3 md:mx-0 mb-1"},Jw={class:"sm:flex sm:flex-col-reverse"},Qw={class:"font-semibold text-brand-700 dark:text-brand-600 text-2xl flex items-center"},Zw={class:"md:hidden flex-1 flex justify-end"},Xw={type:"button",class:"menu-button"},e_={key:0},t_=["href"],n_={key:0,class:"bg-yellow-100 dark:bg-yellow-900 bg-opacity-75 dark:bg-opacity-40 border border-yellow-300 dark:border-yellow-800 rounded-md px-2 py-1 mt-2 text-xs leading-5 text-yellow-700 dark:text-yellow-400"},r_=Ji("code",{class:"font-mono px-2 py-1 bg-gray-100 dark:bg-gray-900 rounded"},"php artisan log-viewer:publish",-1),o_={key:3,class:"flex justify-between items-baseline mt-6"},i_={class:"ml-1 block text-sm text-gray-500 dark:text-gray-400 truncate"},s_={class:"text-sm text-gray-500 dark:text-gray-400"},l_=Ji("label",{for:"file-sort-direction",class:"sr-only"},"Sort direction",-1),a_=[Ji("option",{value:"desc"},"Newest first",-1),Ji("option",{value:"asc"},"Oldest first",-1)],u_={key:4,class:"mx-1 mt-1 text-red-600 text-xs"},c_=Ji("p",{class:"text-sm text-gray-600 dark:text-gray-400"},"Please select files to delete and confirm or cancel deletion.",-1),f_=["onClick"],p_={id:"file-list-container",class:"relative h-full overflow-hidden"},d_=["id"],h_=["onClick"],v_={class:"file-item group"},g_={key:0,class:"sr-only"},m_={key:1,class:"sr-only"},y_={class:"file-icon group-hover:hidden group-focus:hidden"},b_={class:"file-icon hidden group-hover:inline-block group-focus:inline-block"},w_={class:"file-name"},__={key:0},x_=Ji("span",{class:"text-gray-500 dark:text-gray-400"},"root",-1),S_={key:1},O_=Ji("span",{class:"sr-only"},"Open folder options",-1),k_={class:"py-2"},E_={class:"text-brand-500"},C_=Ji("div",{class:"divider"},null,-1),P_=["onClick","disabled"],T_={class:"folder-files pl-3 ml-1 border-l border-gray-200 dark:border-gray-800"},A_={key:0,class:"text-center text-sm text-gray-600 dark:text-gray-400"},j_=Ji("p",{class:"mb-5"},"No log files were found.",-1),L_={class:"flex items-center justify-center px-1"},R_=Ji("div",{class:"pointer-events-none absolute z-10 bottom-0 h-4 w-full bg-gradient-to-t from-gray-100 dark:from-gray-900 to-transparent"},null,-1),I_={class:"absolute inset-y-0 left-3 right-7 lg:left-0 lg:right-0 z-10"},F_={class:"rounded-md bg-white text-gray-800 dark:bg-gray-700 dark:text-gray-200 opacity-90 w-full h-full flex items-center justify-center"};const N_={__name:"FileList",setup:function(e){var t=Fg(),n=Ng(),r=Wm(),o=nb(),i=lb(),s=i.dropdownDirections,l=i.calculateDropdownDirection,a=function(){var e=Kw(qw().mark((function e(n){return qw().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:if(!confirm("Are you sure you want to delete the log folder '".concat(n.path,"'? THIS ACTION CANNOT BE UNDONE."))){e.next=4;break}return e.next=3,o.deleteFolder(n);case 3:n.files.some((function(e){return e.identifier===o.selectedFileIdentifier}))&&sb(t,"file",null);case 4:case"end":return e.stop()}}),e)})));return function(t){return e.apply(this,arguments)}}(),u=function(){var e=Kw(qw().mark((function e(){return qw().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:if(!confirm("Are you sure you want to delete selected log files? THIS ACTION CANNOT BE UNDONE.")){e.next=7;break}return e.next=3,o.deleteSelectedFiles();case 3:return o.filesChecked.includes(o.selectedFileIdentifier)&&sb(t,"file",null),o.resetChecks(),e.next=7,o.loadFolders();case 7:case"end":return e.stop()}}),e)})));return function(){return e.apply(this,arguments)}}();return Jr(Kw(qw().mark((function e(){return qw().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:r.selectHost(n.query.host||null);case 1:case"end":return e.stop()}}),e)})))),dr((function(){return o.direction}),(function(){return o.loadFolders()})),function(e,i){var c,f;return Ni(),Vi("nav",Gw,[Ji("div",Yw,[Ji("div",Jw,[Ji("h1",Qw,[ts(" Log Viewer "),Ji("span",Zw,[Qi(mw,{class:"ml-2"}),Ji("button",Xw,[Qi(Wt(Nm),{class:"w-5 h-5 ml-2",onClick:Wt(o).toggleSidebar},null,8,["onClick"])])])]),e.LogViewer.back_to_system_url?(Ni(),Vi("div",e_,[Ji("a",{href:e.LogViewer.back_to_system_url,class:"rounded shrink inline-flex items-center text-sm text-gray-500 dark:text-gray-400 hover:text-brand-800 dark:hover:text-brand-600 focus:outline-none focus:ring-2 focus:ring-brand-500 dark:focus:ring-brand-700 mt-0"},[Qi(Wt(Mm),{class:"h-3 w-3 mr-1.5"}),ts(" "+ce(e.LogViewer.back_to_system_label||"Back to ".concat(e.LogViewer.app_name)),1)],8,t_)])):rs("",!0)]),e.LogViewer.assets_outdated?(Ni(),Vi("div",n_,[Qi(Wt(Dm),{class:"h-4 w-4 mr-1 inline"}),ts(" Front-end assets are outdated. To update, please run "),r_])):rs("",!0),Wt(r).supportsHosts&&Wt(r).hasRemoteHosts?(Ni(),Hi(Bw,{key:1,class:"mb-8 mt-6"})):rs("",!0),Wt(o).fileTypesAvailable&&Wt(o).fileTypesAvailable.length>1?(Ni(),Hi(Hw,{key:2,class:"mb-8 mt-6"})):rs("",!0),(null===(c=Wt(o).filteredFolders)||void 0===c?void 0:c.length)>0?(Ni(),Vi("div",o_,[Ji("div",i_,"Log files on "+ce(null===(f=Wt(o).selectedHost)||void 0===f?void 0:f.name),1),Ji("div",s_,[l_,yr(Ji("select",{id:"file-sort-direction",class:"select","onUpdate:modelValue":i[0]||(i[0]=function(e){return Wt(o).direction=e})},a_,512),[[Zl,Wt(o).direction]])])])):rs("",!0),Wt(o).error?(Ni(),Vi("p",u_,ce(Wt(o).error),1)):rs("",!0)]),yr(Ji("div",null,[c_,Ji("div",{class:ee(["grid grid-flow-col pr-4 mt-2",[Wt(o).hasFilesChecked?"justify-between":"justify-end"]])},[yr(Ji("button",{onClick:la(u,["stop"]),class:"button inline-flex"},[Qi(Wt(Bm),{class:"w-5 mr-1"}),ts(" Delete selected files ")],8,f_),[[pl,Wt(o).hasFilesChecked]]),Ji("button",{class:"button inline-flex",onClick:i[1]||(i[1]=la((function(e){return Wt(o).resetChecks()}),["stop"]))},[ts(" Cancel "),Qi(Wt(Nm),{class:"w-5 ml-1"})])],2)],512),[[pl,Wt(o).checkBoxesVisibility]]),Ji("div",p_,[Ji("div",{class:"file-list",onScroll:i[6]||(i[6]=function(e){return Wt(o).onScroll(e)})},[(Ni(!0),Vi(Ai,null,io(Wt(o).filteredFolders,(function(e){return Ni(),Vi("div",{key:e.identifier,id:"folder-".concat(e.identifier),class:"relative folder-container"},[Qi(Wt(Lm),null,{default:$n((function(t){var n=t.open;return[Ji("div",{class:ee(["folder-item-container",[Wt(o).isOpen(e)?"active-folder":"",Wt(o).shouldBeSticky(e)?"sticky "+(n?"z-20":"z-10"):""]]),onClick:function(t){return Wt(o).toggle(e)}},[Ji("div",v_,[Ji("button",{class:"file-item-info group",onKeydown:i[2]||(i[2]=function(){return Wt(jb)&&Wt(jb).apply(void 0,arguments)})},[Wt(o).isOpen(e)?rs("",!0):(Ni(),Vi("span",g_,"Open folder")),Wt(o).isOpen(e)?(Ni(),Vi("span",m_,"Close folder")):rs("",!0),Ji("span",y_,[yr(Qi(Wt(Um),{class:"w-5 h-5"},null,512),[[pl,!Wt(o).isOpen(e)]]),yr(Qi(Wt($m),{class:"w-5 h-5"},null,512),[[pl,Wt(o).isOpen(e)]])]),Ji("span",b_,[Qi(Wt(Vm),{class:ee([Wt(o).isOpen(e)?"rotate-90":"","transition duration-100"])},null,8,["class"])]),Ji("span",w_,[String(e.clean_path||"").startsWith("root")?(Ni(),Vi("span",__,[x_,ts(ce(String(e.clean_path).substring(4)),1)])):(Ni(),Vi("span",S_,ce(e.clean_path),1))])],32),Qi(Wt(Rm),{as:"button",class:"file-dropdown-toggle group-hover:border-brand-600 group-hover:dark:border-brand-800","data-toggle-id":e.identifier,onKeydown:Wt(Lb),onClick:i[3]||(i[3]=la((function(e){return Wt(l)(e.target)}),["stop"]))},{default:$n((function(){return[O_,Qi(Wt(Hm),{class:"w-4 h-4 pointer-events-none"})]})),_:2},1032,["data-toggle-id","onKeydown"])]),Qi(Ys,{"leave-active-class":"transition ease-in duration-100","leave-from-class":"opacity-100 scale-100","leave-to-class":"opacity-0 scale-90","enter-active-class":"transition ease-out duration-100","enter-from-class":"opacity-0 scale-90","enter-to-class":"opacity-100 scale-100"},{default:$n((function(){return[yr(Qi(Wt(Im),{static:"",as:"div",class:ee(["dropdown w-48",[Wt(s)[e.identifier]]])},{default:$n((function(){return[Ji("div",k_,[Qi(Wt(Fm),{onClick:la((function(t){return Wt(o).clearCacheForFolder(e)}),["stop","prevent"])},{default:$n((function(t){return[Ji("button",{class:ee([t.active?"active":""])},[yr(Qi(Wt(zm),{class:"w-4 h-4 mr-2"},null,512),[[pl,!Wt(o).clearingCache[e.identifier]]]),yr(Qi(pb,{class:"w-4 h-4 mr-2"},null,512),[[pl,Wt(o).clearingCache[e.identifier]]]),yr(Ji("span",null,"Clear indices",512),[[pl,!Wt(o).cacheRecentlyCleared[e.identifier]&&!Wt(o).clearingCache[e.identifier]]]),yr(Ji("span",null,"Clearing...",512),[[pl,!Wt(o).cacheRecentlyCleared[e.identifier]&&Wt(o).clearingCache[e.identifier]]]),yr(Ji("span",E_,"Indices cleared",512),[[pl,Wt(o).cacheRecentlyCleared[e.identifier]]])],2)]})),_:2},1032,["onClick"]),e.can_download?(Ni(),Hi(Wt(Fm),{key:0},{default:$n((function(t){var n=t.active;return[Qi(Ib,{url:e.download_url,onClick:i[4]||(i[4]=la((function(){}),["stop"])),class:ee([n?"active":""])},null,8,["url","class"])]})),_:2},1024)):rs("",!0),e.can_delete?(Ni(),Vi(Ai,{key:1},[C_,Qi(Wt(Fm),null,{default:$n((function(t){var n=t.active;return[Ji("button",{onClick:la((function(t){return a(e)}),["stop"]),disabled:Wt(o).deleting[e.identifier],class:ee([n?"active":""])},[yr(Qi(Wt(Bm),{class:"w-4 h-4 mr-2"},null,512),[[pl,!Wt(o).deleting[e.identifier]]]),yr(Qi(pb,null,null,512),[[pl,Wt(o).deleting[e.identifier]]]),ts(" Delete ")],10,P_)]})),_:2},1024)],64)):rs("",!0)])]})),_:2},1032,["class"]),[[pl,n]])]})),_:2},1024)],10,h_)]})),_:2},1024),yr(Ji("div",T_,[(Ni(!0),Vi(Ai,null,io(e.files||[],(function(e){return Ni(),Hi(Qb,{key:e.identifier,"log-file":e,onClick:function(r){return o=e.identifier,void(n.query.file&&n.query.file===o?sb(t,"file",null):sb(t,"file",o));var o}},null,8,["log-file","onClick"])})),128))],512),[[pl,Wt(o).isOpen(e)]])],8,d_)})),128)),0===Wt(o).folders.length?(Ni(),Vi("div",A_,[j_,Ji("div",L_,[Ji("button",{onClick:i[5]||(i[5]=la((function(e){return Wt(o).loadFolders()}),["prevent"])),class:"inline-flex items-center px-4 py-2 text-left text-sm bg-white hover:bg-gray-50 outline-brand-500 dark:outline-brand-800 text-gray-900 dark:text-gray-200 rounded-md dark:bg-gray-700 dark:hover:bg-gray-600"},[Qi(Wt(qm),{class:"w-4 h-4 mr-1.5"}),ts(" Refresh file list ")])])])):rs("",!0)],32),R_,yr(Ji("div",I_,[Ji("div",F_,[Qi(pb,{class:"w-14 h-14"})])],512),[[pl,Wt(o).loading]])])])}}},M_=N_;function D_(e,t){return Ni(),Vi("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor","aria-hidden":"true"},[Ji("path",{"fill-rule":"evenodd",d:"M4.755 10.059a7.5 7.5 0 0112.548-3.364l1.903 1.903h-3.183a.75.75 0 100 1.5h4.992a.75.75 0 00.75-.75V4.356a.75.75 0 00-1.5 0v3.18l-1.9-1.9A9 9 0 003.306 9.67a.75.75 0 101.45.388zm15.408 3.352a.75.75 0 00-.919.53 7.5 7.5 0 01-12.548 3.364l-1.902-1.903h3.183a.75.75 0 000-1.5H2.984a.75.75 0 00-.75.75v4.992a.75.75 0 001.5 0v-3.18l1.9 1.9a9 9 0 0015.059-4.035.75.75 0 00-.53-.918z","clip-rule":"evenodd"})])}function B_(e,t){return Ni(),Vi("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor","aria-hidden":"true"},[Ji("path",{"fill-rule":"evenodd",d:"M3 6.75A.75.75 0 013.75 6h16.5a.75.75 0 010 1.5H3.75A.75.75 0 013 6.75zM3 12a.75.75 0 01.75-.75h16.5a.75.75 0 010 1.5H3.75A.75.75 0 013 12zm0 5.25a.75.75 0 01.75-.75h16.5a.75.75 0 010 1.5H3.75a.75.75 0 01-.75-.75z","clip-rule":"evenodd"})])}function U_(e,t){return Ni(),Vi("svg",{xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24","stroke-width":"1.5",stroke:"currentColor","aria-hidden":"true"},[Ji("path",{"stroke-linecap":"round","stroke-linejoin":"round",d:"M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"})])}var $_={class:"pagination"},V_={class:"previous"},H_=["disabled"],z_=Ji("span",{class:"sm:hidden"},"Previous page",-1),q_={class:"sm:hidden border-transparent text-gray-500 dark:text-gray-400 border-t-2 pt-3 px-4 inline-flex items-center text-sm font-medium"},W_={class:"pages"},K_={key:0,class:"border-brand-500 text-brand-600 dark:border-brand-600 dark:text-brand-500","aria-current":"page"},G_={key:1},Y_=["onClick"],J_={class:"next"},Q_=["disabled"],Z_=Ji("span",{class:"sm:hidden"},"Next page",-1);const X_={__name:"Pagination",props:{loading:{type:Boolean,required:!0},short:{type:Boolean,default:!1}},setup:function(e){var t=zy(),n=Fg(),r=Ng(),o=(Ls((function(){return Number(r.query.page)||1})),function(e){e<1&&(e=1),t.pagination&&e>t.pagination.last_page&&(e=t.pagination.last_page),sb(n,"page",e>1?Number(e):null)}),i=function(){return o(t.page+1)},s=function(){return o(t.page-1)};return Jr((function(){document.addEventListener("goToNextPage",i),document.addEventListener("goToPreviousPage",s)})),Xr((function(){document.removeEventListener("goToNextPage",i),document.removeEventListener("goToPreviousPage",s)})),function(n,r){return Ni(),Vi("nav",$_,[Ji("div",V_,[1!==Wt(t).page?(Ni(),Vi("button",{key:0,onClick:s,disabled:e.loading,rel:"prev"},[Qi(Wt(Mm),{class:"h-5 w-5"}),z_],8,H_)):rs("",!0)]),Ji("div",q_,[Ji("span",null,ce(Wt(t).page),1)]),Ji("div",W_,[(Ni(!0),Vi(Ai,null,io(e.short?Wt(t).linksShort:Wt(t).links,(function(e){return Ni(),Vi(Ai,null,[e.active?(Ni(),Vi("button",K_,ce(Number(e.label).toLocaleString()),1)):"..."===e.label?(Ni(),Vi("span",G_,ce(e.label),1)):(Ni(),Vi("button",{key:2,onClick:function(t){return o(Number(e.label))},class:"border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 hover:border-gray-300 dark:hover:text-gray-300 dark:hover:border-gray-400"},ce(Number(e.label).toLocaleString()),9,Y_))],64)})),256))]),Ji("div",J_,[Wt(t).hasMorePages?(Ni(),Vi("button",{key:0,onClick:i,disabled:e.loading,rel:"next"},[Z_,Qi(Wt(U_),{class:"h-5 w-5"})],8,Q_)):rs("",!0)])])}}},ex=X_;function tx(e,t){return Ni(),Vi("svg",{xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24","stroke-width":"1.5",stroke:"currentColor","aria-hidden":"true"},[Ji("path",{"stroke-linecap":"round","stroke-linejoin":"round",d:"M19.5 8.25l-7.5 7.5-7.5-7.5"})])}var nx={class:"flex items-center"},rx={class:"opacity-90 mr-1"},ox={class:"font-semibold"},ix={class:"opacity-90 mr-1"},sx={class:"font-semibold"},lx={key:2,class:"opacity-90"},ax={key:3,class:"opacity-90"},ux={class:"py-2"},cx={class:"label flex justify-between"},fx={key:0,class:"no-results"},px={class:"flex-1 inline-flex justify-between"},dx={class:"log-count"};const hx={__name:"LevelButtons",setup:function(e){var t=Yy(),n=qy();return dr((function(){return n.excludedLevels}),(function(){return t.loadLogs()})),function(e,r){return Ni(),Vi("div",nx,[Qi(Wt(Lm),{as:"div",class:"mr-5 relative log-levels-selector"},{default:$n((function(){return[Qi(Wt(Rm),{as:"button",id:"severity-dropdown-toggle",class:ee(["dropdown-toggle badge none",Wt(n).levelsSelected.length>0?"active":""])},{default:$n((function(){return[Wt(n).levelsSelected.length>2?(Ni(),Vi(Ai,{key:0},[Ji("span",rx,ce(Wt(n).totalResultsSelected.toLocaleString()+(Wt(t).hasMoreResults?"+":""))+" entries in",1),Ji("strong",ox,ce(Wt(n).levelsSelected[0].level_name)+" + "+ce(Wt(n).levelsSelected.length-1)+" more",1)],64)):Wt(n).levelsSelected.length>0?(Ni(),Vi(Ai,{key:1},[Ji("span",ix,ce(Wt(n).totalResultsSelected.toLocaleString()+(Wt(t).hasMoreResults?"+":""))+" entries in",1),Ji("strong",sx,ce(Wt(n).levelsSelected.map((function(e){return e.level_name})).join(", ")),1)],64)):Wt(n).levelsFound.length>0?(Ni(),Vi("span",lx,ce(Wt(n).totalResults.toLocaleString()+(Wt(t).hasMoreResults?"+":""))+" entries found. None selected",1)):(Ni(),Vi("span",ax,"No entries found")),Qi(Wt(tx),{class:"w-4 h-4"})]})),_:1},8,["class"]),Qi(Ys,{"leave-active-class":"transition ease-in duration-100","leave-from-class":"opacity-100 scale-100","leave-to-class":"opacity-0 scale-90","enter-active-class":"transition ease-out duration-100","enter-from-class":"opacity-0 scale-90","enter-to-class":"opacity-100 scale-100"},{default:$n((function(){return[Qi(Wt(Im),{as:"div",class:"dropdown down left min-w-[240px]"},{default:$n((function(){return[Ji("div",ux,[Ji("div",cx,[ts(" Severity "),Wt(n).levelsFound.length>0?(Ni(),Vi(Ai,{key:0},[Wt(n).levelsSelected.length===Wt(n).levelsFound.length?(Ni(),Hi(Wt(Fm),{key:0,onClick:la(Wt(n).deselectAllLevels,["stop"])},{default:$n((function(e){return[Ji("a",{class:ee(["inline-link px-2 -mr-2 py-1 -my-1 rounded-md cursor-pointer text-brand-700 dark:text-brand-500 font-normal",[e.active?"active":""]])}," Deselect all ",2)]})),_:1},8,["onClick"])):(Ni(),Hi(Wt(Fm),{key:1,onClick:la(Wt(n).selectAllLevels,["stop"])},{default:$n((function(e){return[Ji("a",{class:ee(["inline-link px-2 -mr-2 py-1 -my-1 rounded-md cursor-pointer text-brand-700 dark:text-brand-500 font-normal",[e.active?"active":""]])}," Select all ",2)]})),_:1},8,["onClick"]))],64)):rs("",!0)]),0===Wt(n).levelsFound.length?(Ni(),Vi("div",fx,"There are no severity filters to display because no entries have been found.")):(Ni(!0),Vi(Ai,{key:1},io(Wt(n).levelsFound,(function(e){return Ni(),Hi(Wt(Fm),{onClick:la((function(t){return Wt(n).toggleLevel(e.level)}),["stop","prevent"])},{default:$n((function(t){return[Ji("button",{class:ee([t.active?"active":""])},[Qi(sw,{class:"checkmark mr-2.5",checked:e.selected},null,8,["checked"]),Ji("span",px,[Ji("span",{class:ee(["log-level",e.level_class])},ce(e.level_name),3),Ji("span",dx,ce(Number(e.count).toLocaleString()),1)])],2)]})),_:2},1032,["onClick"])})),256))])]})),_:1})]})),_:1})]})),_:1})])}}};function vx(e,t){return Ni(),Vi("svg",{xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24","stroke-width":"1.5",stroke:"currentColor","aria-hidden":"true"},[Ji("path",{"stroke-linecap":"round","stroke-linejoin":"round",d:"M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z"})])}var gx={class:"flex-1"},mx={class:"prefix-icon"},yx=Ji("label",{for:"query",class:"sr-only"},"Search",-1),bx={class:"relative flex-1 m-1"},wx=["onKeydown"],_x={class:"clear-search"},xx={class:"submit-search"},Sx={key:0,disabled:"disabled"},Ox={class:"hidden xl:inline ml-1"},kx={class:"hidden xl:inline ml-1"},Ex={class:"relative h-0 w-full overflow-visible"},Cx=["innerHTML"];const Px={__name:"SearchInput",setup:function(e){var t=Hy(),n=Yy(),r=Fg(),o=Ng(),i=Ls((function(){return n.selectedFile})),s=$t(o.query.query||""),l=function(){var e;sb(r,"query",""===s.value?null:s.value),null===(e=document.getElementById("query-submit"))||void 0===e||e.focus()},a=function(){s.value="",l()};return dr((function(){return o.query.query}),(function(e){return s.value=e||""})),function(e,r){return Ni(),Vi("div",gx,[Ji("div",{class:ee(["search",{"has-error":Wt(n).error}])},[Ji("div",mx,[yx,yr(Qi(Wt(vx),{class:"h-4 w-4"},null,512),[[pl,!Wt(n).hasMoreResults]]),yr(Qi(pb,{class:"w-4 h-4"},null,512),[[pl,Wt(n).hasMoreResults]])]),Ji("div",bx,[yr(Ji("input",{"onUpdate:modelValue":r[0]||(r[0]=function(e){return s.value=e}),name:"query",id:"query",type:"text",onKeydown:[ua(l,["enter"]),r[1]||(r[1]=ua((function(e){return e.target.blur()}),["esc"]))]},null,40,wx),[[Gl,s.value]]),yr(Ji("div",_x,[Ji("button",{onClick:a},[Qi(Wt(Nm),{class:"h-4 w-4"})])],512),[[pl,Wt(t).hasQuery]])]),Ji("div",xx,[Wt(n).hasMoreResults?(Ni(),Vi("button",Sx,[Ji("span",null,[ts("Searching"),Ji("span",Ox,ce(i.value?i.value.name:"all files"),1),ts("...")])])):(Ni(),Vi("button",{key:1,onClick:l,id:"query-submit"},[Ji("span",null,[ts("Search"),Ji("span",kx,ce(i.value?'in "'+i.value.name+'"':"all files"),1)]),Qi(Wt(U_),{class:"h-4 w-4"})]))])],2),Ji("div",Ex,[yr(Ji("div",{class:"search-progress-bar",style:Y({width:Wt(n).percentScanned+"%"})},null,4),[[pl,Wt(n).hasMoreResults]])]),yr(Ji("p",{class:"mt-1 text-red-600 text-xs",innerHTML:Wt(n).error},null,8,Cx),[[pl,Wt(n).error]])])}}},Tx=Px;function Ax(e,t){return Ni(),Vi("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor","aria-hidden":"true"},[Ji("path",{"fill-rule":"evenodd",d:"M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z","clip-rule":"evenodd"})])}function jx(e,t){return Ni(),Vi("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor","aria-hidden":"true"},[Ji("path",{"fill-rule":"evenodd",d:"M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z","clip-rule":"evenodd"})])}function Lx(e,t){return Ni(),Vi("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor","aria-hidden":"true"},[Ji("path",{"fill-rule":"evenodd",d:"M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zm13.36-1.814a.75.75 0 10-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 00-1.06 1.06l2.25 2.25a.75.75 0 001.14-.094l3.75-5.25z","clip-rule":"evenodd"})])}function Rx(e,t){return Ni(),Vi("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor","aria-hidden":"true"},[Ji("path",{"fill-rule":"evenodd",d:"M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zm8.706-1.442c1.146-.573 2.437.463 2.126 1.706l-.709 2.836.042-.02a.75.75 0 01.67 1.34l-.04.022c-1.147.573-2.438-.463-2.127-1.706l.71-2.836-.042.02a.75.75 0 11-.671-1.34l.041-.022zM12 9a.75.75 0 100-1.5.75.75 0 000 1.5z","clip-rule":"evenodd"})])}function Ix(e,t){return Ni(),Vi("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor","aria-hidden":"true"},[Ji("path",{"fill-rule":"evenodd",d:"M16.28 11.47a.75.75 0 010 1.06l-7.5 7.5a.75.75 0 01-1.06-1.06L14.69 12 7.72 5.03a.75.75 0 011.06-1.06l7.5 7.5z","clip-rule":"evenodd"})])}function Fx(e,t){return Ni(),Vi("svg",{xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24","stroke-width":"1.5",stroke:"currentColor","aria-hidden":"true"},[Ji("path",{"stroke-linecap":"round","stroke-linejoin":"round",d:"M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244"})])}function Nx(e,t){return Ni(),Vi("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor","aria-hidden":"true"},[Ji("path",{d:"M7.493 18.75c-.425 0-.82-.236-.975-.632A7.48 7.48 0 016 15.375c0-1.75.599-3.358 1.602-4.634.151-.192.373-.309.6-.397.473-.183.89-.514 1.212-.924a9.042 9.042 0 012.861-2.4c.723-.384 1.35-.956 1.653-1.715a4.498 4.498 0 00.322-1.672V3a.75.75 0 01.75-.75 2.25 2.25 0 012.25 2.25c0 1.152-.26 2.243-.723 3.218-.266.558.107 1.282.725 1.282h3.126c1.026 0 1.945.694 2.054 1.715.045.422.068.85.068 1.285a11.95 11.95 0 01-2.649 7.521c-.388.482-.987.729-1.605.729H14.23c-.483 0-.964-.078-1.423-.23l-3.114-1.04a4.501 4.501 0 00-1.423-.23h-.777zM2.331 10.977a11.969 11.969 0 00-.831 4.398 12 12 0 00.52 3.507c.26.85 1.084 1.368 1.973 1.368H4.9c.445 0 .72-.498.523-.898a8.963 8.963 0 01-.924-3.977c0-1.708.476-3.305 1.302-4.666.245-.403-.028-.959-.5-.959H4.25c-.832 0-1.612.453-1.918 1.227z"})])}var Mx=["onClick"],Dx={class:"sr-only"},Bx={class:"text-green-600 dark:text-green-500 hidden md:inline"};const Ux={__name:"LogCopyButton",props:{log:{type:Object,required:!0}},setup:function(e){var t=e,n=$t(!1),r=function(){ib(t.log.url),n.value=!0,setTimeout((function(){return n.value=!1}),1e3)};return function(t,o){return Ni(),Vi("button",{class:"log-link group",onClick:la(r,["stop"]),onKeydown:o[0]||(o[0]=function(){return Wt(Ab)&&Wt(Ab).apply(void 0,arguments)}),title:"Copy link to this log entry"},[Ji("span",Dx,"Log index "+ce(e.log.index)+". Click the button to copy link to this log entry.",1),yr(Ji("span",{class:"hidden md:inline group-hover:underline"},ce(Number(e.log.index).toLocaleString()),513),[[pl,!n.value]]),yr(Qi(Wt(Fx),{class:"md:opacity-75 group-hover:opacity-100"},null,512),[[pl,!n.value]]),yr(Qi(Wt(Nx),{class:"text-green-600 dark:text-green-500 md:hidden"},null,512),[[pl,n.value]]),yr(Ji("span",Bx,"Copied!",512),[[pl,n.value]])],40,Mx)}}};var $x={key:0,class:"tabs-container"},Vx={class:"border-b border-gray-200 dark:border-gray-800"},Hx={class:"-mb-px flex space-x-6","aria-label":"Tabs"},zx=["onClick","aria-current"];const qx={__name:"TabContainer",props:{tabs:{type:Array,required:!0}},setup:function(e){var t=$t(e.tabs[0]);Go("currentTab",t);var n=function(e){return t.value&&t.value.value===e.value};return function(r,o){return Ni(),Vi("div",null,[e.tabs&&e.tabs.length>1?(Ni(),Vi("div",$x,[Ji("div",Vx,[Ji("nav",Hx,[(Ni(!0),Vi(Ai,null,io(e.tabs,(function(e){return Ni(),Vi("a",{key:e.name,href:"#",onClick:la((function(n){return t.value=e}),["prevent"]),class:ee([n(e)?"border-brand-500 dark:border-brand-400 text-brand-600 dark:text-brand-500":"border-transparent text-gray-500 dark:text-gray-400 hover:border-gray-300 hover:text-gray-700 dark:hover:text-gray-200","whitespace-nowrap border-b-2 py-2 px-1 text-sm font-medium focus:outline-brand-500"]),"aria-current":n(e)?"page":void 0},ce(e.name),11,zx)})),128))])])])):rs("",!0),lo(r.$slots,"default")])}}};var Wx={key:0};const Kx={__name:"TabContent",props:{tabValue:{type:String,required:!0}},setup:function(e){var t=e,n=Yo("currentTab"),r=Ls((function(){return n.value&&n.value.value===t.tabValue}));return function(e,t){return r.value?(Ni(),Vi("div",Wx,[lo(e.$slots,"default")])):rs("",!0)}}};function Gx(e,t){return Ni(),Vi("svg",{xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24","stroke-width":"1.5",stroke:"currentColor","aria-hidden":"true"},[Ji("path",{"stroke-linecap":"round","stroke-linejoin":"round",d:"M18.375 12.739l-7.693 7.693a4.5 4.5 0 01-6.364-6.364l10.94-10.94A3 3 0 1119.5 7.372L8.552 18.32m.009-.01l-.01.01m5.699-9.941l-7.81 7.81a1.5 1.5 0 002.112 2.13"})])}var Yx={class:"mail-preview-attributes"},Jx={key:0},Qx=Ji("td",{class:"font-semibold"},"From",-1),Zx={key:1},Xx=Ji("td",{class:"font-semibold"},"To",-1),eS={key:2},tS=Ji("td",{class:"font-semibold"},"Message ID",-1),nS={key:3},rS=Ji("td",{class:"font-semibold"},"Subject",-1),oS={key:4},iS=Ji("td",{class:"font-semibold"},"Attachments",-1),sS={class:"flex items-center"},lS={class:"opacity-60"},aS=["onClick"];const uS={__name:"MailPreviewAttributes",props:["mail"],setup:function(e){return function(t,n){return Ni(),Vi("div",Yx,[Ji("table",null,[e.mail.from?(Ni(),Vi("tr",Jx,[Qx,Ji("td",null,ce(e.mail.from),1)])):rs("",!0),e.mail.to?(Ni(),Vi("tr",Zx,[Xx,Ji("td",null,ce(e.mail.to),1)])):rs("",!0),e.mail.id?(Ni(),Vi("tr",eS,[tS,Ji("td",null,ce(e.mail.id),1)])):rs("",!0),e.mail.subject?(Ni(),Vi("tr",nS,[rS,Ji("td",null,ce(e.mail.subject),1)])):rs("",!0),e.mail.attachments&&e.mail.attachments.length>0?(Ni(),Vi("tr",oS,[iS,Ji("td",null,[(Ni(!0),Vi(Ai,null,io(e.mail.attachments,(function(t,n){return Ni(),Vi("div",{key:"mail-".concat(e.mail.id,"-attachment-").concat(n),class:"mail-attachment-button"},[Ji("div",sS,[Qi(Wt(Gx),{class:"h-4 w-4 text-gray-500 dark:text-gray-400 mr-1"}),Ji("span",null,[ts(ce(t.filename)+" ",1),Ji("span",lS,"("+ce(t.size_formatted)+")",1)])]),Ji("div",null,[Ji("a",{href:"#",onClick:la((function(e){return function(e){for(var t=atob(e.content),n=new Array(t.length),r=0;re.length)&&(t=e.length);for(var n=0,r=new Array(t);n