Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 45da3915c0 |
+1
-6
@@ -2,9 +2,6 @@
|
||||
kind: pipeline
|
||||
name: litmus
|
||||
|
||||
# SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
steps:
|
||||
- name: submodules
|
||||
image: ghcr.io/nextcloud/continuous-integration-alpine-git:latest
|
||||
@@ -127,6 +124,4 @@ trigger:
|
||||
|
||||
---
|
||||
kind: signature
|
||||
hmac: 06ddea3f1885983230fcc996e805245357ac90e39599ed11a70161a7c09746d7
|
||||
|
||||
...
|
||||
hmac: 1c487e85d1dba3fec3151868f8f94fc46b4ecb0f821c35516c193700cdbc2a9c
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
# SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Ignoring folders for eslint
|
||||
node_modules/
|
||||
3rdparty/
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
module.exports = {
|
||||
globals: {
|
||||
__webpack_nonce__: true,
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
<!--
|
||||
- SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
## Submitting issues
|
||||
|
||||
If you have questions about how to install or use Nextcloud, please direct these to our [forum][forum]. We are also available on [IRC][irc].
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
# SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
custom: https://nextcloud.com/include/
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
# SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
name: "🐛 Bug report: Nextcloud Server"
|
||||
description: "Submit a report and help us improve Nextcloud Server"
|
||||
title: "[Bug]: "
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
@@ -1,5 +1,3 @@
|
||||
# SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
contact_links:
|
||||
- name: 🚨 Report a security or privacy issue
|
||||
url: https://hackerone.com/nextcloud
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
# SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
firstPRMergeComment: >
|
||||
Thanks for your first pull request and welcome to the community!
|
||||
Feel free to keep them coming! If you are looking for issues to tackle then have a look at this selection: https://github.com/nextcloud/server/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
# SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
version: 2
|
||||
updates:
|
||||
# Linting and coding style
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
@@ -53,11 +53,11 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
php-versions: ['8.1']
|
||||
# To keep the matrix smaller we ignore PostgreSQL '13', '14', and '15' as we already test 12 and 16 as lower and upper bound
|
||||
postgres-versions: ['12', '16']
|
||||
# To keep the matrix smaller we ignore PostgreSQL '11', '13', '14' as we already test 10 and 15 as lower and upper bound
|
||||
postgres-versions: ['10', '15', '16']
|
||||
include:
|
||||
- php-versions: '8.3'
|
||||
postgres-versions: '16'
|
||||
postgres-versions: '15'
|
||||
coverage: ${{ github.event_name != 'pull_request' }}
|
||||
|
||||
name: PostgreSQL ${{ matrix.postgres-versions }} (PHP ${{ matrix.php-versions }}) - database tests
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
# SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
|
||||
# SPDX-FileCopyrightText: 2011-2016 ownCloud contributors
|
||||
# SPDX-FileCopyrightText: 2010 ownCloud contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# the default generated dir + db file
|
||||
/data
|
||||
/config/config.php
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
require_once './vendor-bin/cs-fixer/vendor/autoload.php';
|
||||
|
||||
use Nextcloud\CodingStandard\Config;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# See https://pre-commit.com for more information
|
||||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
# SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.3.0
|
||||
|
||||
+2
-106
@@ -7,62 +7,6 @@ Files: lib/l10n/*.js lib/l10n/*.json core/l10n/*.js core/l10n/*.json apps/admin_
|
||||
Copyright: 2016 ownCloud, Inc., 2016-2024 Nextcloud translators
|
||||
License: AGPL-3.0-only OR AGPL-3.0-or-later
|
||||
|
||||
Files: tests/data/block-aligned-plus-one.txt tests/data/block-aligned.txt tests/data/data.tar.gz tests/data/data.zip tests/data/desktopapp.png tests/data/desktopapp.svg tests/data/certificates/badCertificate.crt tests/data/certificates/expiredCertificate.crt tests/data/certificates/goodCertificate.crt tests/data/integritycheck/app/AnotherFile.txt tests/data/integritycheck/app/subfolder/file.txt tests/data/integritycheck/appWithInvalidData/AnotherFile.txt tests/data/integritycheck/appWithInvalidData/UnecessaryFile
|
||||
Copyright: 2015 ownCloud, Inc.
|
||||
License: AGPL-3.0-only
|
||||
|
||||
Files: tests/data/integritycheck/htaccessUnmodified/.htaccess tests/data/integritycheck/htaccessUnmodified/subfolder/.htaccess tests/data/integritycheck/htaccessWithInvalidModifiedContent/.htaccess tests/data/integritycheck/htaccessWithValidModifiedContent/subfolder/.htaccess
|
||||
Copyright: 2016 ownCloud, Inc.
|
||||
License: AGPL-3.0-only
|
||||
|
||||
Files: tests/data/l10n/cs.json tests/data/l10n/de.json tests/data/l10n/ru.json
|
||||
Copyright: 2015 ownCloud, Inc., 2021 Nextcloud GmbH and Nextcloud contributors
|
||||
License: AGPL-3.0-only
|
||||
|
||||
Files: tests/data/lorem.txt
|
||||
Copyright: 2012 ownCloud, Inc.
|
||||
License: AGPL-3.0-only
|
||||
|
||||
Files: tests/data/testavatar.png tests/data/testimage.gif tests/data/testimage.jpg tests/data/testimage.png
|
||||
Copyright: 2013 ownCloud, Inc.
|
||||
License: AGPL-3.0-only
|
||||
|
||||
Files: tests/data/lorem-big.txt tests/data/strängé*filename*(duplicate*#2).txt tests/data/strõngÚ*filename*(duplicate*#2).txt
|
||||
Copyright: 2014 ownCloud, Inc.
|
||||
License: AGPL-3.0-only
|
||||
|
||||
Files: tests/data/testimagelarge.svg
|
||||
Copyright: 2015 ownCloud, Inc., 2016 Nextcloud GmbH and Nextcloud contributors
|
||||
License: AGPL-3.0-only
|
||||
|
||||
Files: tests/data/setUploadLimit/htaccess tests/data/setUploadLimit/user.ini
|
||||
Copyright: 2015 ownCloud, Inc., 2018 Nextcloud GmbH and Nextcloud contributors
|
||||
License: AGPL-3.0-only
|
||||
|
||||
Files: tests/data/testimage-wide.png tests/data/testimage.eps tests/data/testimage.mp3 tests/data/testimage.mp4 tests/data/testimage.odt
|
||||
Copyright: 2015 ownCloud, Inc.
|
||||
License: AGPL-3.0-only
|
||||
|
||||
Files: tests/data/testapp.zip tests/data/testapp2.zip tests/docker/mariadb/oc.cnf
|
||||
Copyright: 2016 ownCloud, Inc.
|
||||
License: AGPL-3.0-only
|
||||
|
||||
Files: tests/docker/mysqlmb4/mb4.cnf
|
||||
Copyright: 2016 ownCloud, Inc., 2017 Nextcloud GmbH and Nextcloud contributors
|
||||
License: AGPL-3.0-only
|
||||
|
||||
Files: tests/data/integritycheck/htaccessWithValidModifiedContent/.htaccess
|
||||
Copyright: 2016 ownCloud, Inc., 2019 Nextcloud GmbH and Nextcloud contributors
|
||||
License: AGPL-3.0-only
|
||||
|
||||
Files: core/img/favicon*.* core/img/logo/logo*.* tests/data/testimage.webp
|
||||
Copyright: 2016-2024 Nextcloud GmbH
|
||||
License: LicenseRef-NextcloudTrademarks
|
||||
|
||||
Files: core/css/*.css core/css/*.css.map
|
||||
Copyright: 2016-2024 Nextcloud GmbH and Nextcloud contributors
|
||||
License: AGPL-3.0-or-later
|
||||
|
||||
Files: build/psalm-baseline-ocp.xml build/psalm-baseline.xml build/stubs/xsl.php build/stubs/gd.php build/stubs/imagick.php build/stubs/intl.php build/stubs/IntlChar.php build/stubs/ldap.php build/stubs/memcached.php build/stubs/redis.php build/stubs/redis_cluster.php build/stubs/sftp.php build/stubs/ssh2.php build/stubs/apcu.php
|
||||
Copyright: 2020 Nextcloud GmbH and Nextcloud contributors
|
||||
License: AGPL-3.0-or-later
|
||||
@@ -71,35 +15,11 @@ Files: build/stubs/pcntl.php build/stubs/zip.php
|
||||
Copyright: 2022 JetBrains
|
||||
License: Apache-2.0
|
||||
|
||||
Files: core/js/mimetypelist.js core/js/core.json themes/example/core/img
|
||||
Copyright: 2016 ownCloud, Inc., 2016-2024 Nextcloud GmbH and Nextcloud contributors
|
||||
License: AGPL-3.0-only
|
||||
|
||||
Files: tests/data/testapp.0.8.tar.gz tests/data/testapp.tar.gz tests/data/testapp1.tar.gz
|
||||
Copyright: 2016 Nextcloud GmbH and Nextcloud contributors
|
||||
License: AGPL-3.0-or-later
|
||||
|
||||
Files: core/js/merged-template-prepend.json tests/data/certificates/goodCertificate.crt tests/data/emails/new-account-email-custom-text-alternative.txt tests/data/emails/new-account-email-custom.html tests/data/emails/new-account-email-custom.txt tests/data/emails/new-account-email-single-button.html tests/data/emails/new-account-email-single-button.txt tests/data/emails/new-account-email.html tests/data/emails/new-account-email.txt
|
||||
Copyright: 2017 Nextcloud GmbH and Nextcloud contributors
|
||||
License: AGPL-3.0-or-later
|
||||
|
||||
Files: core/src/systemtags/templates/result.handlebars core/src/systemtags/templates/result_form.handlebars core/src/systemtags/templates/selection.handlebars tests/data/testimage.heic
|
||||
Copyright: 2018 Nextcloud GmbH and Nextcloud contributors
|
||||
License: AGPL-3.0-or-later
|
||||
|
||||
Files: core/src/jquery/contactsmenu/jquery_entry.handlebars tests/data/integritycheck/mimetypeListModified/core/js/mimetypelist.js tests/data/integritycheck/mimetypeListModified/core/signature.json tests/data/guest_avatar_einstein_32.png tests/data/guest_avatar_einstein_32.svg tests/data/test.pdf
|
||||
Copyright: 2019 Nextcloud GmbH and Nextcloud contributors
|
||||
License: AGPL-3.0-or-later
|
||||
|
||||
Files: tests/data/testimage-badheader.jpg
|
||||
Copyright: 2021 Nextcloud GmbH and Nextcloud contributors
|
||||
License: AGPL-3.0-or-later
|
||||
|
||||
Files: build/stubs/ftp.php tests/data/integritycheck/mimetypeListModified/coresignature.json
|
||||
Files: build/stubs/ftp.php
|
||||
Copyright: 2022 Nextcloud GmbH and Nextcloud contributors
|
||||
License: AGPL-3.0-or-later
|
||||
|
||||
Files: build/stubs/app_api.php build/stubs/SensitiveParameter.phpstub build/stubs/psr_container.php tests/data/testimage-large.jpg
|
||||
Files: build/stubs/app_api.php build/stubs/SensitiveParameter.phpstub build/stubs/psr_container.php
|
||||
Copyright: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
License: AGPL-3.0-or-later
|
||||
|
||||
@@ -110,27 +30,3 @@ License: AGPL-3.0-or-later
|
||||
Files: .gitattributes composer.json composer.lock .github/CODEOWNERS __tests__/tsconfig.json tsconfig.json build/integration/composer.* build/integration/data/*.png
|
||||
Copyright: 2011-2016 ownCloud, Inc., 2016-2024 Nextcloud GmbH and Nextcloud contributors
|
||||
License: AGPL-3.0-only OR AGPL-3.0-or-later
|
||||
|
||||
Files: core/img/twitter.svg
|
||||
Copyright: X Corp.
|
||||
License: LicenseRef-XTrademarks
|
||||
|
||||
Files: core/img/facebook.svg
|
||||
Copyright: 2024 Meta
|
||||
License: LicenseRef-FacebookTrademarks
|
||||
|
||||
Files: core/img/mastodon.svg
|
||||
Copyright: 2024 Mastodon gGmbH
|
||||
License: LicenseRef-MastodonTrademarks
|
||||
|
||||
Files: core/img/googleplay.png
|
||||
Copyright: 2024 Google LLC.
|
||||
License: LicenseRef-GooglePlayBadge
|
||||
|
||||
Files: core/img/appstore.svg
|
||||
Copyright: 2024 Apple Inc.
|
||||
License: LicenseRef-AppleAppStoreBadge
|
||||
|
||||
Files: core/img/fdroid.svg
|
||||
Copyright: 2016 Andrew Nayenko <relan@airpost.net>
|
||||
License: CC-BY-SA-3.0 OR GPL-3.0-or-later
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
# SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
|
||||
# SPDX-FileCopyrightText: 2016 ownCloud, Inc.
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
before_commands:
|
||||
- 'git submodule update --init --recursive'
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
- Andreas Fischer <bantu@owncloud.com>
|
||||
- Andreas Pflug <dev@admin4.org>
|
||||
- Andrew Brown <andrew@casabrown.com>
|
||||
- Andrey Borysenko <andrey.borysenko@nextcloud.com>
|
||||
- Andy Xheli <axheli@axtsolutions.com>
|
||||
- Anna Larch <anna@nextcloud.com>
|
||||
- ArcticFall <23174635+ArcticFall@users.noreply.github.com>
|
||||
@@ -113,7 +112,7 @@
|
||||
- David <37280718+yeyulantu@users.noreply.github.com>
|
||||
- David Prévot <taffit@debian.org>
|
||||
- David Toledo <dtoledo@solidgear.es>
|
||||
- Denis MosolovDenis Mosolov <denismosolov@gmail.com>
|
||||
- Denis Mosolov <denismosolov@gmail.com>
|
||||
- Derek <derek.kelly27@gmail.com>
|
||||
- Dominik Schmidt <dev@dominik-schmidt.de>
|
||||
- Donquixote <marjunebatac@gmail.com>
|
||||
@@ -137,7 +136,6 @@
|
||||
- Florent <florent@coppint.com>
|
||||
- Florian Schunk <florian.schunk@rwth-aachen.de>
|
||||
- Florin Peter <github@florin-peter.de>
|
||||
- Fon E. Noel NFEBE <opensource@nfebe.com>
|
||||
- Frank Karlitschek <frank@karlitschek.de>
|
||||
- François Freitag <mail@franek.fr>
|
||||
- François Kubler <francois@kubler.org>
|
||||
@@ -203,7 +201,6 @@
|
||||
- Jos Poortvliet <jos@opensuse.org>
|
||||
- Jose Quinteiro <github@quinteiro.org>
|
||||
- Josh Richards <josh.t.richards@gmail.com>
|
||||
- Joshua Trees <me@jtrees.io>
|
||||
- Juan Pablo Villafañez <jvillafanez@solidgear.es>
|
||||
- Juan Pablo Villafáñez <jvillafanez@solidgear.es>
|
||||
- Julien Lutran <julien.lutran@corp.ovh.com>
|
||||
@@ -224,7 +221,6 @@
|
||||
- Klaas Freitag <freitag@owncloud.com>
|
||||
- Knut Ahlers <knut@ahlers.me>
|
||||
- Ko- <k.stoffelen@cs.ru.nl>
|
||||
- Konrad Abicht <hi@inspirito.de>
|
||||
- Konrad Bucheli <kb@open.ch>
|
||||
- Kristof Provost <github@sigsegv.be>
|
||||
- Kyle Fazzari <kyrofa@ubuntu.com>
|
||||
@@ -246,7 +242,6 @@
|
||||
- Louis <6653109+artonge@users.noreply.github.com>
|
||||
- Louis Chemineau <louis@chmn.me>
|
||||
- Loïc Hermann <loic.hermann@sciam.fr>
|
||||
- Lucas Azevedo <lhs_azevedo@hotmail.com>
|
||||
- Luka Trovic <luka@nextcloud.com>
|
||||
- Lukas Reschke <lukas@statuscode.ch>
|
||||
- Lukas Stabe <lukas@stabe.de>
|
||||
@@ -312,7 +307,6 @@
|
||||
- Mohammed Abdellatif <m.latief@gmail.com>
|
||||
- Morris Jobke <hey@morrisjobke.de>
|
||||
- Mátyás Jani <jzombi@gmail.com>
|
||||
- nacho <nacho@ownyourbits.com>
|
||||
- Naoto Kobayashi <naoto.kobayashi4c@gmail.com>
|
||||
- Nazar Mokrynskyi <nazar@mokrynskyi.com>
|
||||
- Nicolai Ehemann <en@enlightened.de>
|
||||
@@ -340,7 +334,6 @@
|
||||
- Peter Kubica <peter@kubica.ch>
|
||||
- Petre T <petre.tudor@dorkfarm.com>
|
||||
- Phil Davis <phil.davis@inf.org>
|
||||
- Philip Gatzka <philip.gatzka@mailbox.org>
|
||||
- Philipp Kapfer <philipp.kapfer@gmx.at>
|
||||
- Philipp Schaffrath <github@philipp.schaffrath.email>
|
||||
- Philipp Staiger <philipp@staiger.it>
|
||||
@@ -408,7 +401,6 @@
|
||||
- Sergey Shliakhov <husband.sergey@gmail.com>
|
||||
- Sergio Bertolin <sbertolin@solidgear.es>
|
||||
- Sergio Bertolín <sbertolin@solidgear.es>
|
||||
- Serhii Shliakhov <shlyakhov.up@gmail.com>
|
||||
- Sijmen Schoon <me@sijmenschoon.nl>
|
||||
- Simon Könnecke <simonkoennecke@gmail.com>
|
||||
- Simon L <szaimen@e.mail.de>
|
||||
|
||||
+1
-1
@@ -18,4 +18,4 @@ Licensing of components:
|
||||
All unmodified files from these and other sources retain their original copyright
|
||||
and license notices: see the relevant individual files.
|
||||
|
||||
Attribution information for Nextcloud is contained in the `AUTHORS` file.
|
||||
Attribution information for Nextcloud is contained in the `AUTHORS.md` file.
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
App Store Badges
|
||||
|
||||
Include App Store badges in all digital and printed marketing materials as a clear call to action to get your app. App Store badges are available in 40 localizations to help you reach a broader audience. Versions are available for the App Store for iPhone and iPad, the Mac App Store, and Apple TV.
|
||||
|
||||
|
||||
Preferred Badges
|
||||
|
||||
Use the preferred black badge in all marketing communications promoting your app. The gray border surrounding the black badge is part of the badge artwork and should not be modified. Whenever one or more badges for other app platforms appear in the layout, use the preferred black badge. Place the App Store badge first in the lineup of badges.
|
||||
|
||||
|
||||
Legal Requirements
|
||||
Trademark Symbols
|
||||
|
||||
In communications distributed only in the United States, the appropriate symbol (™, ℠, or ®) must follow each Apple trademark the first time it is mentioned in body copy. Do not use trademark symbols on products, product documentation, or other product communications that will be distributed outside the United States.
|
||||
|
||||
For example, use Apple Watch®, iPhone®, iPad®, iPod touch®, Apple TV®, App Store®, Mac App Store℠, Mac®, MacBook Pro®, MacBook Air®, and iMac®.
|
||||
|
||||
Don’t add symbols to headline copy or to the App Store badge artwork provided by Apple.
|
||||
|
||||
For the correct trademark symbols, refer to the Apple Trademark List.
|
||||
Credit Lines
|
||||
|
||||
Use the appropriate credit lines in all communications worldwide, listing all the Apple trademarks and products included in your communication and advertising. Include the credit lines only once in your communication or website, and place the credit lines wherever you provide legal notification. Follow standard practices for the placement of legal copy, such as creating additional screens or providing interactive links. When the App Store badge is used, credit both Apple and the Apple Logo.
|
||||
|
||||
Refer to the Apple Trademark List for the correct trademark symbol, spelling of the trademark, and generic term to use with the trademark. Generally, the symbol appears at the right shoulder of the trademark (except the Apple Logo, where the logo appears at the right foot).
|
||||
|
||||
Use the following formats for distribution within the United States only:
|
||||
|
||||
______ and ______ are registered trademarks of Apple Inc.
|
||||
|
||||
______ and ______ are trademarks of Apple Inc.
|
||||
|
||||
For distribution outside the United States, use one of the following international credit notices:
|
||||
|
||||
______ and______ are trademarks of Apple Inc., registered in the U.S. and other countries.
|
||||
|
||||
______ and______ are trademarks of Apple Inc.
|
||||
|
||||
A translation of the legal notice and credit lines (but not the trademarks) can be used in materials distributed outside the U.S.
|
||||
|
||||
For more information on using Apple trademarks, see Using Apple Trademarks and Copyrights.
|
||||
Association with Apple
|
||||
|
||||
Your app screen images, Mac, Apple Watch, iPhone, iPad, iPod touch, and Apple TV product images, or photographs thereof cannot be used in any manner that falsely suggests an association with Apple or is likely to reduce, diminish, or damage the goodwill, value, or reputation associated with the App Store, the Mac App Store, iPhone, iPad, iPod touch, Apple Watch, Apple TV, or Apple itself.
|
||||
|
||||
see https://developer.apple.com/app-store/marketing/guidelines/
|
||||
@@ -1,17 +0,0 @@
|
||||
Facebook brand resources and guidelines
|
||||
|
||||
The logo is our most important brand asset. We use it with consistency and intention to represent the world of social discovery to billions of people around the world.
|
||||
|
||||
Our Logo Evolution
|
||||
|
||||
Our logo has evolved over time, building on our past heritage, using an 'f' within a blue circle. The current logo has been refined in shape and color to make it more accessible and legible. Always ensure you are using the current version of our logo in any new designs or communications.
|
||||
|
||||
Legal
|
||||
|
||||
Meta dedicates substantial resources to the development and protection of its intellectual property. In addition to seeking registration of its trademarks and logos around the world, Meta enforces its rights against people who misuse its trademarks.
|
||||
|
||||
Meta's trademarks are owned by Meta and may only be used as provided in these guidelines or with Meta’s permission. A list of some of Meta’s trademarks can be found here (https://about.meta.com/brand/resources/meta/our-trademarks/). You may not use or register, or otherwise claim rights in any Meta trademark, including as or as part of any trademark, service mark, company name, trade name, username or domain registration. You should not use or claim rights in any trademark in a way that is confusingly similar to or dilutive of Meta’s trademarks, including as, or as any part of, a trademark. Do not use Meta's trademarks for anything that would be inconsistent with Meta’s Terms of Service (https://www.facebook.com/terms.php) or Community Standards (https://transparency.meta.com/policies/community-standards/).
|
||||
|
||||
We may revoke permission to use Meta’s trademarks at any time. Meta reserves the right to withhold approval of content that it considers inconsistent with the Meta brand.
|
||||
|
||||
A copy can be found at https://about.meta.com/brand/resources/facebook/logo/
|
||||
@@ -1,23 +0,0 @@
|
||||
badge guidelines
|
||||
Dos & Don'ts
|
||||
|
||||
Follow these guidelines whenever you are using a Google Play badge.
|
||||
|
||||
* Use the badges as provided. Never alter the badges.
|
||||
* There must be clear space surrounding the badge equal to one-quarter the height of the badge.
|
||||
* The badge must be large enough that all of the text is legible.
|
||||
* The Google Play badge should be the same size or larger than other application store badges.
|
||||
* Badges must be shown on a solid colored background or a simple background image that does not obscure the badge.
|
||||
* Match the badge language to the language of your marketing campaign whenever possible.
|
||||
* Any online use of the badge must link to the Google Play store. Use the generator below to get the HTML to include in your digital marketing.
|
||||
* The badge can only be used to promote content available on Google Play.
|
||||
* Include the appropriate Google Play legal attribution when there is space in the creative. See the badge generator for localized legal attributions.
|
||||
* Use of the Google Play badge must be reviewed and approved by the Google Play Partner Brand team if the badge will be in:
|
||||
* a TV commercial
|
||||
* an out-of-home marketing campaign
|
||||
* a marketing campaign that will receive over 1 million impressions
|
||||
* Use this form (https://partnermarketinghub.withgoogle.com/login/?next=/submit-assets/) to submit your marketing for approval.
|
||||
|
||||
see https://play.google.com/intl/en_us/badges/
|
||||
|
||||
Google Play and the Google Play logo are trademarks of Google LLC.
|
||||
@@ -1,46 +0,0 @@
|
||||
Trademark Policy
|
||||
|
||||
Last updated December 21, 2022
|
||||
|
||||
The Mastodon name and logos are trademarks of Mastodon gGmbH. As such, their use is restricted and protected by intellectual property law. While the software we create is available under a free and open source software license, the copyright license does not include an implied right or license to use our trademarks.
|
||||
|
||||
The role of trademarks is to prevent the exploitation of the good name and reputation of Mastodon by other people and organizations, and to provide assurance about the quality of the products and services associated with it.
|
||||
|
||||
To use our trademarks beyond what is considered "fair" or "nominative" use, you must follow these guidelines. By making use of our trademarks, you agree to abide by the following terms and conditions. You further agree that any dispute arising in connection with your use of our trademarks or under these terms and conditions shall be under the exclusive jurisdiction of the state and federal courts of New York in the United States of America and that the state and federal courts of New York shall have personal jurisdiction over you for the purposes of adjudicating any dispute concerning the use of our trademarks or these terms and conditions.
|
||||
|
||||
You agree to defend and indemnify Mastodon gGmbH from and against any and all claims and losses brought by a third party in connection with your use of the Mastodon trademarks.
|
||||
|
||||
To request the use of the Mastodon name and logos in a way not covered in these guidelines, or to report violations, please contact us at trademark@joinmastodon.org. In the event that we do not approve such use of the Mastodon name and logos within ten (10) business days, your request shall be deemed denied.
|
||||
General guidelines
|
||||
|
||||
In general:
|
||||
|
||||
* Only use the Mastodon marks to accurately identify those goods or services that are built using the Mastodon software.
|
||||
* Do not use the Mastodon marks in any way that could mistakenly imply that Mastodon GmbH has reviewed, approved, or guaranteed your goods or services.
|
||||
* Do not use or register, in whole or in part, the Mastodon marks as part of your own or any other trademark, service mark, domain name, company name, trade name, product name, or service name.
|
||||
* Do not use the Mastodon marks in a manner that disparages or defames the marks, Mastodon gGmbH, or Mastodon’s products.
|
||||
* Do not use the Mastodon marks in connection with any illegal activity.
|
||||
* You may use the Mastodon word mark in referential phrases such as "for", "for use with", or "compatible with".
|
||||
* You may use the Mastodon marks when embedding or otherwise displaying user generated content published using the Mastodon software.
|
||||
* Do not change or modify the Mastodon marks.
|
||||
* Any all use of the Mastodon marks, and any goodwill accrued as a result of that use, belongs entirely to, and shall inure for the benefit of, Mastodon gGmbH.
|
||||
|
||||
Server guidelines
|
||||
|
||||
If you run your own Mastodon server using the Mastodon software, including modified Mastodon software on the condition that the modifications are limited to switching on or off features already included in the software, minor tweaks in visual appearance, translations into other languages, and bug fixes:
|
||||
|
||||
* You may not use the Mastodon word mark, or any similar mark, in your domain name, unless you have written permission from Mastodon gGmbH.
|
||||
* As long as you abide by the terms and conditions of this agreement, you may use the Mastodon marks included in the Mastodon server software for the purposes of running the server.
|
||||
|
||||
Open source project guidelines
|
||||
|
||||
If you choose to build on or modify Mastodon's open-source code, beyond modifications limited to switching on or off features already included in the software, minor tweaks in visual appearance, translations into other languages, and bug fixes:
|
||||
|
||||
* You must choose your own branding, logos, and trademarks that denote your unique identity so as to clearly signal to users that there is no affiliation with or endorsement by Mastodon gGmbH.
|
||||
* You may use word marks, but not our logos, in truthful statements that describe the relationship between your software and ours, for example "this software is derived from the source code of the Mastodon software".
|
||||
|
||||
Social media guidelines
|
||||
|
||||
The name and handle of your social media account and any and all pages cannot begin with a Mastodon word mark, or a similar mark (e.g. "mastodoon", "mast0don", "mstdn"). In addition, Mastodon logos cannot be used in a way that might suggest affiliation with or endorsement by Mastodon.
|
||||
|
||||
A copy can be found at https://joinmastodon.org/de/trademark
|
||||
@@ -1,9 +0,0 @@
|
||||
The Nextcloud marks
|
||||
Nextcloud and the Nextcloud logo is a registered trademark of Nextcloud GmbH in Germany and/or other countries.
|
||||
These guidelines cover the following marks pertaining both to the product names and the logo: “Nextcloud”
|
||||
and the blue/white cloud logo with or without the word Nextcloud; the service “Nextcloud Enterprise”;
|
||||
and our products: “Nextcloud Files”; “Nextcloud Groupware” and “Nextcloud Talk”.
|
||||
This set of marks is collectively referred to as the “Nextcloud marks.”
|
||||
|
||||
Use of Nextcloud logos and other marks is only permitted under the guidelines provided by the Nextcloud GmbH.
|
||||
A copy can be found at https://discord.com/branding
|
||||
@@ -1,49 +0,0 @@
|
||||
Trademark policy
|
||||
April 2023
|
||||
|
||||
|
||||
You may not violate others’ intellectual property rights, including copyright and trademark.
|
||||
|
||||
A trademark is a word, logo, phrase, or device that distinguishes a trademark holder’s good or service in the marketplace. Trademark law may prevent others from using a trademark in an unauthorized or confusing manner.
|
||||
|
||||
|
||||
What is in violation of this policy?
|
||||
|
||||
Using another’s trademark in a way that may mislead or confuse people about your affiliation may be a violation of our trademark policy.
|
||||
|
||||
|
||||
What is not a violation of this policy?
|
||||
|
||||
Referencing another’s trademark is not automatically a violation of X's trademark policy. Examples of non-violations include:
|
||||
|
||||
* using a trademark in a way that is outside the scope of the trademark registration e.g., in a different territory, or a different class of goods or services than that identified in the registration; and
|
||||
* using a trademark in a nominative or other fair use manner. For more information, see our Misleading and deceptive identities policy (https://help.twitter.com/en/rules-and-policies/twitter-impersonation-and-deceptive-identities-policy.html).
|
||||
|
||||
|
||||
Who can report violations of this policy?
|
||||
|
||||
X only investigates requests that are submitted by the trademark holder or their authorized representative e.g., a legal representative or other representative for a brand.
|
||||
|
||||
|
||||
How can I report violations of this policy?
|
||||
|
||||
You can submit a trademark report through our trademark report form (https://help.twitter.com/forms/trademark). Please provide all the information requested in the form. If you submit an incomplete report, we’ll need to follow up about the missing information. Please note that this will result in a delay in processing your report.
|
||||
|
||||
Note: We may provide the account holder with your name and other information included in the copy of the report.
|
||||
|
||||
|
||||
What happens if you violate this policy?
|
||||
|
||||
If we determine that you violated our trademark policy, we may suspend your account. Depending on the type of violation, we may give you an opportunity to comply with our policies. In other instances, an account may be permanently suspended upon first review. If you believe that your account was suspended in error, you can submit an appeal (https://help.twitter.com/forms/general?subtopic=suspended).
|
||||
|
||||
|
||||
Additional resources
|
||||
|
||||
Learn more about our range of enforcement options (https://help.twitter.com/rules-and-policies/enforcement-options) and our approach to policy development and enforcement (https://help.twitter.com/rules-and-policies/enforcement-philosophy).
|
||||
|
||||
|
||||
Legal disclaimer
|
||||
|
||||
By using the X trademarks and resources on this site, you agree to follow the X Trademark Guidelines in our Brand Guidelines — as well as our Terms of Service and all other X rules and policies. If you have any questions, contact us at trademarks@x.com.
|
||||
|
||||
A copy can be found at https://about.x.com/en/who-we-are/brand-toolkit and https://help.twitter.com/en/rules-and-policies/x-trademark-policy
|
||||
@@ -1,6 +1,23 @@
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
export const getCurrentUser = function() {
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,23 @@
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
export default {
|
||||
get: async () => ({ status: 200, data: {} }),
|
||||
|
||||
@@ -1,6 +1,23 @@
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
import { jest } from '@jest/globals'
|
||||
|
||||
@@ -1,6 +1,23 @@
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
export const loadState = function(app: string, key: string, fallback?: any) {
|
||||
|
||||
+19
-2
@@ -1,5 +1,22 @@
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* @copyright Copyright (c) 2023 Lucas Azevedo <lhs_azevedo@hotmail.com>
|
||||
*
|
||||
* @author Lucas Azevedo <lhs_azevedo@hotmail.com>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
export default {}
|
||||
|
||||
+19
-2
@@ -1,5 +1,22 @@
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
export default '<svg>SvgMock</svg>'
|
||||
|
||||
+19
-2
@@ -1,6 +1,23 @@
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
export const createClient = () => {}
|
||||
export const getPatcher = () => {
|
||||
|
||||
@@ -471,8 +471,8 @@ export default {
|
||||
background-attachment: fixed;
|
||||
|
||||
> h2 {
|
||||
// this is shown directly on the background image / color
|
||||
color: var(--color-background-plain-text);
|
||||
// this is shown directly on the background which has `color-primary`, so we need `color-primary-text`
|
||||
color: var(--color-primary-text);
|
||||
text-align: center;
|
||||
font-size: 32px;
|
||||
line-height: 130%;
|
||||
|
||||
@@ -97,16 +97,19 @@ class CalendarObject extends \Sabre\CalDAV\CalendarObject {
|
||||
* @param Component\VCalendar $vObject
|
||||
* @return void
|
||||
*/
|
||||
private function createConfidentialObject(Component\VCalendar $vObject): void {
|
||||
private function createConfidentialObject(Component\VCalendar $vObject) {
|
||||
/** @var Component $vElement */
|
||||
$vElements = array_filter($vObject->getComponents(), static function ($vElement) {
|
||||
return $vElement instanceof Component\VEvent || $vElement instanceof Component\VJournal || $vElement instanceof Component\VTodo;
|
||||
});
|
||||
|
||||
foreach ($vElements as $vElement) {
|
||||
if (empty($vElement->select('SUMMARY'))) {
|
||||
$vElement->add('SUMMARY', $this->l10n->t('Busy')); // This is needed to mask "Untitled Event" events
|
||||
}
|
||||
$vElement = null;
|
||||
if (isset($vObject->VEVENT)) {
|
||||
$vElement = $vObject->VEVENT;
|
||||
}
|
||||
if (isset($vObject->VJOURNAL)) {
|
||||
$vElement = $vObject->VJOURNAL;
|
||||
}
|
||||
if (isset($vObject->VTODO)) {
|
||||
$vElement = $vObject->VTODO;
|
||||
}
|
||||
if (!is_null($vElement)) {
|
||||
foreach ($vElement->children() as &$property) {
|
||||
/** @var Property $property */
|
||||
switch ($property->name) {
|
||||
|
||||
@@ -578,10 +578,6 @@ class FederatedShareProvider implements IShareProvider {
|
||||
|
||||
|
||||
public function getSharesInFolder($userId, Folder $node, $reshares, $shallow = true) {
|
||||
if (!$shallow) {
|
||||
throw new \Exception("non-shallow getSharesInFolder is no longer supported");
|
||||
}
|
||||
|
||||
$qb = $this->dbConnection->getQueryBuilder();
|
||||
$qb->select('*')
|
||||
->from('share', 's')
|
||||
@@ -609,7 +605,12 @@ class FederatedShareProvider implements IShareProvider {
|
||||
|
||||
$qb->innerJoin('s', 'filecache', 'f', $qb->expr()->eq('s.file_source', 'f.fileid'));
|
||||
|
||||
$qb->andWhere($qb->expr()->eq('f.parent', $qb->createNamedParameter($node->getId())));
|
||||
$qb->andWhere($qb->expr()->eq('f.storage', $qb->createNamedParameter($node->getMountPoint()->getNumericStorageId(), IQueryBuilder::PARAM_INT)));
|
||||
if ($shallow) {
|
||||
$qb->andWhere($qb->expr()->eq('f.parent', $qb->createNamedParameter($node->getId(), IQueryBuilder::PARAM_INT)));
|
||||
} else {
|
||||
$qb->andWhere($qb->expr()->like('f.path', $qb->createNamedParameter($this->dbConnection->escapeLikeParameter($node->getInternalPath()) . '/%')));
|
||||
}
|
||||
|
||||
$qb->orderBy('id');
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -22,7 +22,7 @@
|
||||
import { emit } from '@nextcloud/event-bus'
|
||||
import { Permission, Node, View, FileAction, FileType } from '@nextcloud/files'
|
||||
import { showInfo } from '@nextcloud/dialogs'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { translate as t, translatePlural as n } from '@nextcloud/l10n'
|
||||
import axios from '@nextcloud/axios'
|
||||
|
||||
import CloseSvg from '@mdi/svg/svg/close.svg?raw'
|
||||
@@ -30,7 +30,6 @@ import NetworkOffSvg from '@mdi/svg/svg/network-off.svg?raw'
|
||||
import TrashCanSvg from '@mdi/svg/svg/trash-can.svg?raw'
|
||||
|
||||
import logger from '../logger.js'
|
||||
import PQueue from 'p-queue'
|
||||
|
||||
const canUnshareOnly = (nodes: Node[]) => {
|
||||
return nodes.every(node => node.attributes['is-mount-root'] === true
|
||||
@@ -120,8 +119,6 @@ const displayName = (nodes: Node[], view: View) => {
|
||||
return t('files', 'Delete')
|
||||
}
|
||||
|
||||
const queue = new PQueue({ concurrency: 1 })
|
||||
|
||||
export const action = new FileAction({
|
||||
id: 'delete',
|
||||
displayName,
|
||||
@@ -186,19 +183,7 @@ export const action = new FileAction({
|
||||
return Promise.all(nodes.map(() => false))
|
||||
}
|
||||
|
||||
// Map each node to a promise that resolves with the result of exec(node)
|
||||
const promises = nodes.map(node => {
|
||||
// Create a promise that resolves with the result of exec(node)
|
||||
const promise = new Promise<boolean>(resolve => {
|
||||
queue.add(async () => {
|
||||
const result = await this.exec(node, view, dir)
|
||||
resolve(result !== null ? result : false)
|
||||
})
|
||||
})
|
||||
return promise
|
||||
})
|
||||
|
||||
return Promise.all(promises)
|
||||
return Promise.all(nodes.map(node => this.exec(node, view, dir)))
|
||||
},
|
||||
|
||||
order: 100,
|
||||
|
||||
@@ -121,13 +121,14 @@ import type { Upload } from '@nextcloud/upload'
|
||||
import type { UserConfig } from '../types.ts'
|
||||
import type { View, ContentsWithRoot } from '@nextcloud/files'
|
||||
|
||||
import { getCapabilities } from '@nextcloud/capabilities'
|
||||
import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'
|
||||
import { Folder, Node, Permission } from '@nextcloud/files'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { getCapabilities } from '@nextcloud/capabilities'
|
||||
import { join, dirname } from 'path'
|
||||
import { orderBy } from 'natural-orderby'
|
||||
import { Parser } from 'xml2js'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import { translate, translatePlural } from '@nextcloud/l10n'
|
||||
import { Type } from '@nextcloud/sharing'
|
||||
import { UploadPicker } from '@nextcloud/upload'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
@@ -468,8 +469,6 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
methods: {
|
||||
t,
|
||||
|
||||
async fetchContent() {
|
||||
this.loading = true
|
||||
const dir = this.dir
|
||||
@@ -570,19 +569,17 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
// Else we try to parse the response error message
|
||||
if (typeof upload.response?.data === 'string') {
|
||||
try {
|
||||
const parser = new DOMParser()
|
||||
const doc = parser.parseFromString(upload.response.data, 'text/xml')
|
||||
const message = doc.getElementsByTagName('s:message')[0]?.textContent ?? ''
|
||||
if (message.trim() !== '') {
|
||||
// The server message is also translated
|
||||
showError(t('files', 'Error during upload: {message}', { message }))
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Could not parse message', { error })
|
||||
try {
|
||||
const parser = new Parser({ trim: true, explicitRoot: false })
|
||||
const response = await parser.parseStringPromise(upload.response?.data)
|
||||
const message = response['s:message'][0] as string
|
||||
if (typeof message === 'string' && message.trim() !== '') {
|
||||
// The server message is also translated
|
||||
showError(this.t('files', 'Error during upload: {message}', { message }))
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error while parsing', { error })
|
||||
}
|
||||
|
||||
// Finally, check the status code if we have one
|
||||
@@ -635,6 +632,9 @@ export default defineComponent({
|
||||
toggleGridView() {
|
||||
this.userConfigStore.update('grid_view', !this.userConfig.grid_view)
|
||||
},
|
||||
|
||||
t: translate,
|
||||
n: translatePlural,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -19,10 +19,7 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
import type { Node, View } from '@nextcloud/files'
|
||||
|
||||
import { FileAction } from '@nextcloud/files'
|
||||
import { FileAction, Node } from '@nextcloud/files'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import CalendarClockSvg from '@mdi/svg/svg/calendar-clock.svg?raw'
|
||||
|
||||
@@ -35,10 +32,7 @@ export const action = new FileAction({
|
||||
title: () => t('files_reminders', 'Set reminder at custom date & time'),
|
||||
iconSvgInline: () => CalendarClockSvg,
|
||||
|
||||
enabled: (_nodes: Node[], view: View) => {
|
||||
return view.id !== 'trashbin'
|
||||
},
|
||||
|
||||
enabled: () => true,
|
||||
parent: SET_REMINDER_MENU_ID,
|
||||
|
||||
async exec(file: Node) {
|
||||
|
||||
@@ -19,9 +19,6 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
import type { Node, View } from '@nextcloud/files'
|
||||
|
||||
import { FileAction } from '@nextcloud/files'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import AlarmSvg from '@mdi/svg/svg/alarm.svg?raw'
|
||||
@@ -33,9 +30,7 @@ export const action = new FileAction({
|
||||
displayName: () => t('files_reminders', 'Set reminder'),
|
||||
iconSvgInline: () => AlarmSvg,
|
||||
|
||||
enabled: (_nodes: Node[], view: View) => {
|
||||
return view.id !== 'trashbin'
|
||||
},
|
||||
enabled: () => true,
|
||||
|
||||
async exec() {
|
||||
return null
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
*/
|
||||
|
||||
import Vue from 'vue'
|
||||
import type { Node, View } from '@nextcloud/files'
|
||||
import type { Node } from '@nextcloud/files'
|
||||
|
||||
import { FileAction } from '@nextcloud/files'
|
||||
import { emit } from '@nextcloud/event-bus'
|
||||
@@ -91,13 +91,7 @@ const generateFileAction = (option: ReminderOption): FileAction|null => {
|
||||
// Empty svg to hide the icon
|
||||
iconSvgInline: () => '<svg></svg>',
|
||||
|
||||
enabled: (_nodes: Node[], view: View) => {
|
||||
if (view.id === 'trashbin') {
|
||||
return false
|
||||
}
|
||||
return Boolean(getDateTime(option.dateTimePreset))
|
||||
},
|
||||
|
||||
enabled: () => Boolean(getDateTime(option.dateTimePreset)),
|
||||
parent: SET_REMINDER_MENU_ID,
|
||||
|
||||
async exec(node: Node) {
|
||||
|
||||
@@ -26,11 +26,9 @@
|
||||
*/
|
||||
namespace OCA\Files_Sharing;
|
||||
|
||||
use OC\Files\Cache\FileAccess;
|
||||
use OC\Files\Mount\MountPoint;
|
||||
use OCP\Constants;
|
||||
use OCP\Files\Folder;
|
||||
use OCP\Server;
|
||||
use OCP\Share\IShare;
|
||||
|
||||
class Updater {
|
||||
@@ -60,40 +58,20 @@ class Updater {
|
||||
if ($userFolder === null) {
|
||||
return;
|
||||
}
|
||||
$user = $userFolder->getOwner();
|
||||
if (!$user) {
|
||||
throw new \Exception("user folder has no owner");
|
||||
}
|
||||
|
||||
$src = $userFolder->get($path);
|
||||
|
||||
$shareManager = \OC::$server->getShareManager();
|
||||
|
||||
// FIXME: should CIRCLES be included here ??
|
||||
$shares = $shareManager->getSharesBy($user->getUID(), IShare::TYPE_USER, $src, false, -1);
|
||||
$shares = array_merge($shares, $shareManager->getSharesBy($user->getUID(), IShare::TYPE_GROUP, $src, false, -1));
|
||||
$shares = array_merge($shares, $shareManager->getSharesBy($user->getUID(), IShare::TYPE_ROOM, $src, false, -1));
|
||||
$shares = $shareManager->getSharesBy($userFolder->getOwner()->getUID(), IShare::TYPE_USER, $src, false, -1);
|
||||
$shares = array_merge($shares, $shareManager->getSharesBy($userFolder->getOwner()->getUID(), IShare::TYPE_GROUP, $src, false, -1));
|
||||
$shares = array_merge($shares, $shareManager->getSharesBy($userFolder->getOwner()->getUID(), IShare::TYPE_ROOM, $src, false, -1));
|
||||
|
||||
if ($src instanceof Folder) {
|
||||
$cacheAccess = Server::get(FileAccess::class);
|
||||
|
||||
$sourceStorageId = $src->getStorage()->getCache()->getNumericStorageId();
|
||||
$sourceInternalPath = $src->getInternalPath();
|
||||
$subShares = array_merge(
|
||||
$shareManager->getSharesBy($user->getUID(), IShare::TYPE_USER),
|
||||
$shareManager->getSharesBy($user->getUID(), IShare::TYPE_GROUP),
|
||||
$shareManager->getSharesBy($user->getUID(), IShare::TYPE_ROOM),
|
||||
);
|
||||
$shareSourceIds = array_map(fn (IShare $share) => $share->getNodeId(), $subShares);
|
||||
$shareSources = $cacheAccess->getByFileIdsInStorage($shareSourceIds, $sourceStorageId);
|
||||
$subShares = $shareManager->getSharesInFolder($userFolder->getOwner()->getUID(), $src, false, false);
|
||||
foreach ($subShares as $subShare) {
|
||||
$shareCacheEntry = $shareSources[$subShare->getNodeId()] ?? null;
|
||||
if (
|
||||
$shareCacheEntry &&
|
||||
str_starts_with($shareCacheEntry->getPath(), $sourceInternalPath . '/')
|
||||
) {
|
||||
$shares[] = $subShare;
|
||||
}
|
||||
$shares = array_merge($shares, array_values($subShare));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -137,9 +137,6 @@ abstract class AUserData extends OCSController {
|
||||
$groups = $this->groupManager->getUserGroups($targetUserObject);
|
||||
$gids = [];
|
||||
foreach ($groups as $group) {
|
||||
if (!$this->groupManager->getSubAdmin()->isSubAdminOfGroup($currentLoggedInUser, $group)) {
|
||||
continue;
|
||||
};
|
||||
$gids[] = $group->getGID();
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"sourceRoot":"","sources":["settings.scss","../../../core/css/functions.scss"],"names":[],"mappings":"AAQC,0BACC,WAKF,OACC,WAID,4BC6BC,2CDzBD,mBCyBC,kDDrBD,qBCqBC,yCDjBD,0BCiBC,wCDbD,oECaC,2CDTD,oCACC,oBACA,0BACA,+BACA,mBAGD,4BACC,oBACA,kCAGD,yBACC,WAIA,wCACC,kBACA,yDACC,gBAIA,mOACC,WAKH,uCACC,aAGD,sCACC,WAED,uDACC,WAKD,gBACC,WAIF,mBACC,aACA,aACA,iBACA,uEACA,qBAEA,4BACC,kBACA,SAEA,+BACC,mBAIA,qCACC,iBAKH,kCACC,iBACA,mBACA,gBAGD,mGACC,4BACA,kBACA,WAMF,oBACC,kBACA,wCACC,0BAIF,aACC,qBACA,YACA,kBACA,8CACA,wCACA,wCACA,8CACA,6CAEA,qFAIC,6DACA,oDAGD,uBACC,kBACA,yBAIF,6BACC,oBACA,kCAEA,mCACC,WAIA,oCACC,kBACA,oBACA,iBACA,2BACA,WACA,mBACA,QAEA,0CACC,mBACA,uBACA,gBAKD,gIACC,kBACA,UACA,UACA,oBACA,YAKH,qCACC,kBACA,UACA,MACA,SAEA,yCACC,qBAIF,4CACC,eAGD,4CACC,sBACA,WACA,YACA,YAMF,qBACC,aACA,WACA,SACA,YAEA,uBACC,aAGD,uCACC,sBACA,cACA,yBAIF,iBACC,kBACA,eACA,WACA,YACA,aACA,SACA,gBACA,YAEA,8CAEC,+CACA,wCAEA,0FACC,WAIF,uCACC,kBACA,qBACA,gCACA,WACA,eAEA,wDACC,qBACA,sBACA,eAIF,sCACC,SAGC,4DAEC,iBACA,kBAEA,kFACC,YAGD,mEACC,oDAEA,kFACC,iBAIF,qEACC,WAEA,eAEA,uEACC,eAQN,gBACC,YAIA,2BACC,kCAGD,mBACC,YAIF,sCAEC,aAGD,eACC,WAGD,YACC,qBAIA,aACC,WACA,yBACA,YAGD,WACC,WACA,yBACA,YAMD,oBACC,iBAGD,iBACC,eAKD,iCACC,aACA,eACA,sBACA,SACA,gDACC,aACA,eACA,sBACA,sDACC,oBAIF,kGACC,cACA,YACA,gBAKA,iEACC,kBACA,UAED,+EACC,oBACA,eACA,wBACA,UAIF,wCACC,WAGD,iDACC,qBAGD,sDACC,kBACA,OACA,WACA,0BACA,eACA,gBACA,WAQF,oBACC,gBAGD,wBACC,iBAGD,oDACC,WACA,YACA,mBACA,wCAOD,oBACC,UACA,cACA,gBACA,uBAGD,2BACC,UAKD,oCAEC,cAKD,wEAEC,aAIF,gBACC,kBACA,QACA,QAEA,sBACC,YAGD,sBACC,iBAKF,WACC,WAEA,cACC,WACA,kBACA,4CACA,gBACA,mBAGD,cACC,4CACA,kBACA,gBACA,mBAKD,gBACC,kBACA,cACA,eACA,uBACA,gBAGD,wBACC,kBAEA,gCACC,kBAIF,sCACC,kBAGD,sDAEC,cACA,eACA,eAEA,0EACC,UACA,qBACA,uBACA,gBAIF,8BACC,eAGD,kCACC,mBACA,cAIF,2BACC,mBAMA,oBACC,mBACA,iBACA,WAGD,gCACC,kBAIA,gGACC,cAMH,SACC,gBAEA,0BACC,4CAID,YACC,mBAEA,uBACC,iBACA,2BACA,qBAMH,KACC,mBACA,mBAGD,SACC,aAGD,mBACC,mBAGD,eACC,gBAOA,+IACC,sBAEA,+KACC,aAGD,mKACC,WACA,YACA,kCACA,qBACA,kBAGD,mOACC,sCAGD,mNACC,sCAGD,mNACC,oCAMF,sBACC,aAGD,YACC,oBAGD,kBACC,kBAGD,yBACC,kBAGD,sBACC,kBAGD,oCACC,uBAIF,yCACC,kBAGD,wBACC,qBAGD,2BACC,wBAEA,gBACA,aACA,iBACA,sBAKD,WACC,kBACA,2BACA,WAGD,2DAGC,qBAIA,mCACC,qBACA,YACA,iBAGD,+EAEC,YAIF,yBACC,mCACC,YACA,gBACA,cACA,iDAIF,eACC,WAGD,SACC,iBAGD,QACC,qBACA,YACA,WACA,2BAEA,gBACC,kBAIF,qBACC,sBACA,qBACA,YACA,iBAGD,kBACC,qBACA,gBAIA,aACC,sCACA,mCAGD,WACC,oCAGD,mBACC,sCACA,oBAMF,8CACC,WACA,YAGD,wBACC,WACA,YACA,mBACA,kBACA,+DAIA,oBACC,iBACA,gBAEA,uBACC,cAGD,uBACC,kBAIF,0BACC,YACA,gCAGD,oDACC,yBAGD,wDACC,2BAGD,uBACC,cAKD,oBACC,0BAGD,oCACC,gBAIF,2BACC,aACA,eACA,mDAEA,8BACC,kBAGD,6BACC,WAIF,eACC,mBAEA,iBACC,qBACA,cAIF,SACC,UAGD,eACC,iBACA,mBACA,WAGD,UACI,+CAGJ,2BACE,GACE,YAGJ,mCACE,GACE","file":"settings.css"}
|
||||
{"version":3,"sourceRoot":"","sources":["settings.scss","../../../core/css/functions.scss"],"names":[],"mappings":"AAQC,0BACC,WAKF,OACC,WAID,4BC8CC,2CD1CD,mBC0CC,kDDtCD,qBCsCC,yCDlCD,0BCkCC,wCD9BD,oEC8BC,2CD1BD,oCACC,oBACA,0BACA,+BACA,mBAGD,4BACC,oBACA,kCAGD,yBACC,WAIA,wCACC,kBACA,yDACC,gBAIA,mOACC,WAKH,uCACC,aAGD,sCACC,WAED,uDACC,WAKD,gBACC,WAIF,mBACC,aACA,aACA,iBACA,uEACA,qBAEA,4BACC,kBACA,SAEA,+BACC,mBAIA,qCACC,iBAKH,kCACC,iBACA,mBACA,gBAGD,mGACC,4BACA,kBACA,WAMF,oBACC,kBACA,wCACC,0BAIF,aACC,qBACA,YACA,kBACA,8CACA,wCACA,wCACA,8CACA,6CAEA,qFAIC,6DACA,oDAGD,uBACC,kBACA,yBAIF,6BACC,oBACA,kCAEA,mCACC,WAIA,oCACC,kBACA,oBACA,iBACA,2BACA,WACA,mBACA,QAEA,0CACC,mBACA,uBACA,gBAKD,gIACC,kBACA,UACA,UACA,oBACA,YAKH,qCACC,kBACA,UACA,MACA,SAEA,yCACC,qBAIF,4CACC,eAGD,4CACC,sBACA,WACA,YACA,YAMF,qBACC,aACA,WACA,SACA,YAEA,uBACC,aAGD,uCACC,sBACA,cACA,yBAIF,iBACC,kBACA,eACA,WACA,YACA,aACA,SACA,gBACA,YAEA,8CAEC,+CACA,wCAEA,0FACC,WAIF,uCACC,kBACA,qBACA,gCACA,WACA,eAEA,wDACC,qBACA,sBACA,eAIF,sCACC,SAGC,4DAEC,iBACA,kBAEA,kFACC,YAGD,mEACC,oDAEA,kFACC,iBAIF,qEACC,WAEA,eAEA,uEACC,eAQN,gBACC,YAIA,2BACC,kCAGD,mBACC,YAIF,sCAEC,aAGD,eACC,WAGD,YACC,qBAIA,aACC,WACA,yBACA,YAGD,WACC,WACA,yBACA,YAMD,oBACC,iBAGD,iBACC,eAKD,iCACC,aACA,eACA,sBACA,SACA,gDACC,aACA,eACA,sBACA,sDACC,oBAIF,kGACC,cACA,YACA,gBAKA,iEACC,kBACA,UAED,+EACC,oBACA,eACA,wBACA,UAIF,wCACC,WAGD,iDACC,qBAGD,sDACC,kBACA,OACA,WACA,0BACA,eACA,gBACA,WAQF,oBACC,gBAGD,wBACC,iBAGD,oDACC,WACA,YACA,mBACA,wCAOD,oBACC,UACA,cACA,gBACA,uBAGD,2BACC,UAKD,oCAEC,cAKD,wEAEC,aAIF,gBACC,kBACA,QACA,QAEA,sBACC,YAGD,sBACC,iBAKF,WACC,WAEA,cACC,WACA,kBACA,4CACA,gBACA,mBAGD,cACC,4CACA,kBACA,gBACA,mBAKD,gBACC,kBACA,cACA,eACA,uBACA,gBAGD,wBACC,kBAEA,gCACC,kBAIF,sCACC,kBAGD,sDAEC,cACA,eACA,eAEA,0EACC,UACA,qBACA,uBACA,gBAIF,8BACC,eAGD,kCACC,mBACA,cAIF,2BACC,mBAMA,oBACC,mBACA,iBACA,WAGD,gCACC,kBAIA,gGACC,cAMH,SACC,gBAEA,0BACC,4CAID,YACC,mBAEA,uBACC,iBACA,2BACA,qBAMH,KACC,mBACA,mBAGD,SACC,aAGD,mBACC,mBAGD,eACC,gBAOA,+IACC,sBAEA,+KACC,aAGD,mKACC,WACA,YACA,kCACA,qBACA,kBAGD,mOACC,sCAGD,mNACC,sCAGD,mNACC,oCAMF,sBACC,aAGD,YACC,oBAGD,kBACC,kBAGD,yBACC,kBAGD,sBACC,kBAGD,oCACC,uBAIF,yCACC,kBAGD,wBACC,qBAGD,2BACC,wBAEA,gBACA,aACA,iBACA,sBAKD,WACC,kBACA,2BACA,WAGD,2DAGC,qBAIA,mCACC,qBACA,YACA,iBAGD,+EAEC,YAIF,yBACC,mCACC,YACA,gBACA,cACA,iDAIF,eACC,WAGD,SACC,iBAGD,QACC,qBACA,YACA,WACA,2BAEA,gBACC,kBAIF,qBACC,sBACA,qBACA,YACA,iBAGD,kBACC,qBACA,gBAIA,aACC,sCACA,mCAGD,WACC,oCAGD,mBACC,sCACA,oBAMF,8CACC,WACA,YAGD,wBACC,WACA,YACA,mBACA,kBACA,+DAIA,oBACC,iBACA,gBAEA,uBACC,cAGD,uBACC,kBAIF,0BACC,YACA,gCAGD,oDACC,yBAGD,wDACC,2BAGD,uBACC,cAKD,oBACC,0BAGD,oCACC,gBAIF,2BACC,aACA,eACA,mDAEA,8BACC,kBAGD,6BACC,WAIF,eACC,mBAEA,iBACC,qBACA,cAIF,SACC,UAGD,eACC,iBACA,mBACA,WAGD,UACI,+CAGJ,2BACE,GACE,YAGJ,mCACE,GACE","file":"settings.css"}
|
||||
@@ -47,7 +47,7 @@ trait CheckServerResponseTrait {
|
||||
* Common helper string in case a check could not fetch any results
|
||||
*/
|
||||
protected function serverConfigHelp(): string {
|
||||
return $this->l10n->t('To allow this check to run you have to make sure that your Web server can connect to itself. Therefore it must be able to resolve and connect to at least one of its `trusted_domains` or the `overwrite.cli.url`. This failure may be the result of a server-side DNS mismatch or outbound firewall rule.');
|
||||
return $this->l10n->t('To allow this check to run you have to make sure that your webserver can connect to itself. Therefor it must be able to resolve and connect to at least one its `trusted_domains` or the `overwrite.cli.url`.');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -31,7 +31,6 @@ use OC\DB\SchemaWrapper;
|
||||
use OCP\DB\Events\AddMissingIndicesEvent;
|
||||
use OCP\EventDispatcher\IEventDispatcher;
|
||||
use OCP\IL10N;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\SetupCheck\ISetupCheck;
|
||||
use OCP\SetupCheck\SetupResult;
|
||||
|
||||
@@ -40,7 +39,6 @@ class DatabaseHasMissingIndices implements ISetupCheck {
|
||||
private IL10N $l10n,
|
||||
private Connection $connection,
|
||||
private IEventDispatcher $dispatcher,
|
||||
private IURLGenerator $urlGenerator,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -92,18 +90,12 @@ class DatabaseHasMissingIndices implements ISetupCheck {
|
||||
if (empty($missingIndices)) {
|
||||
return SetupResult::success('None');
|
||||
} else {
|
||||
$processed = 0;
|
||||
$list = $this->l10n->t('Missing indices:');
|
||||
$list = '';
|
||||
foreach ($missingIndices as $missingIndex) {
|
||||
$processed++;
|
||||
$list .= "\n " . $this->l10n->t('"%s" in table "%s"', [$missingIndex['indexName'], $missingIndex['tableName']]);
|
||||
if (count($missingIndices) > $processed) {
|
||||
$list .= ", ";
|
||||
}
|
||||
$list .= "\n".$this->l10n->t('Missing optional index "%s" in table "%s".', [$missingIndex['indexName'], $missingIndex['tableName']]);
|
||||
}
|
||||
return SetupResult::warning(
|
||||
$this->l10n->t('Detected some missing optional indices. Occasionally new indices are added (by Nextcloud or installed applications) to improve database performance. Adding indices can sometimes take awhile and temporarily hurt performance so this is not done automatically during upgrades. Once the indices are added, queries to those tables should be faster. Use the command `occ db:add-missing-indices` to add them. ') . $list . '.',
|
||||
$this->urlGenerator->linkToDocs('admin-long-running-migration-steps')
|
||||
$this->l10n->t('The database is missing some indexes. Due to the fact that adding indexes on big tables could take some time they were not added automatically. By running "occ db:add-missing-indices" those missing indexes could be added manually while the instance keeps running. Once the indexes are added queries to those tables are usually much faster.').$list
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,19 +64,12 @@ class HttpsUrlGeneration implements ISetupCheck {
|
||||
}
|
||||
$generatedUrl = $this->urlGenerator->getAbsoluteURL('index.php');
|
||||
if (!str_starts_with($generatedUrl, 'https://')) {
|
||||
if (!\OC::$CLI) {
|
||||
return SetupResult::warning(
|
||||
$this->l10n->t('You are accessing your instance over a secure connection, however your instance is generating insecure URLs. This likely means that your instance is behind a reverse proxy and the Nextcloud `overwrite*` config values are not set correctly.'),
|
||||
$this->urlGenerator->linkToDocs('admin-reverse-proxy')
|
||||
);
|
||||
/* We were called from CLI so we can't be 100% sure which scenario is applicable */
|
||||
} else {
|
||||
return SetupResult::info(
|
||||
$this->l10n->t('Your instance is generating insecure URLs. If you access your instance over HTTPS, this likely means that your instance is behind a reverse proxy and the Nextcloud `overwrite*` config values are not set correctly.'),
|
||||
$this->urlGenerator->linkToDocs('admin-reverse-proxy')
|
||||
);
|
||||
}
|
||||
return SetupResult::warning(
|
||||
$this->l10n->t('You are accessing your instance over a secure connection, however your instance is generating insecure URLs. This most likely means that you are behind a reverse proxy and the overwrite config variables are not set correctly.'),
|
||||
$this->urlGenerator->linkToDocs('admin-reverse-proxy')
|
||||
);
|
||||
}
|
||||
|
||||
return SetupResult::success($this->l10n->t('You are accessing your instance over a secure connection, and your instance is generating secure URLs.'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ class JavaScriptModules implements ISetupCheck {
|
||||
}
|
||||
|
||||
if ($noResponse) {
|
||||
return SetupResult::warning($this->l10n->t('Unable to run check for JavaScript support. Please remedy or confirm manually if your webserver serves `.mjs` files using the JavaScript MIME type.') . "\n" . $this->serverConfigHelp());
|
||||
return SetupResult::warning($this->l10n->t('Could not check for JavaScript support via any of your `trusted_domains` nor `overwrite.cli.url`. This may be the result of a server-side DNS mismatch or outbound firewall rule. Please check manually if your webserver serves `.mjs` files using the JavaScript MIME type.') . "\n" . $this->serverConfigHelp());
|
||||
}
|
||||
return SetupResult::error($this->l10n->t('Your webserver does not serve `.mjs` files using the JavaScript MIME type. This will break some apps by preventing browsers from executing the JavaScript files. You should configure your webserver to serve `.mjs` files with either the `text/javascript` or `application/javascript` MIME type.'));
|
||||
|
||||
|
||||
@@ -53,16 +53,15 @@ class MemcacheConfigured implements ISetupCheck {
|
||||
$memcacheLocalClass = $this->config->getSystemValue('memcache.local', null);
|
||||
$caches = array_filter([$memcacheDistributedClass,$memcacheLockingClass,$memcacheLocalClass]);
|
||||
if (in_array(\OC\Memcache\Memcached::class, array_map(fn (string $class) => ltrim($class, '\\'), $caches))) {
|
||||
// wrong PHP module is installed
|
||||
if (extension_loaded('memcache') && !extension_loaded('memcached')) {
|
||||
if (extension_loaded('memcache')) {
|
||||
return SetupResult::warning(
|
||||
$this->l10n->t('Memcached is configured as distributed cache, but the wrong PHP module ("memcache") is installed. Please install the PHP module "memcached".')
|
||||
$this->l10n->t('Memcached is configured as distributed cache, but the wrong PHP module "memcache" is installed. \\OC\\Memcache\\Memcached only supports "memcached" and not "memcache".'),
|
||||
'https://code.google.com/p/memcached/wiki/PHPClientComparison'
|
||||
);
|
||||
}
|
||||
// required PHP module is missing
|
||||
if (!extension_loaded('memcached')) {
|
||||
return SetupResult::warning(
|
||||
$this->l10n->t('Memcached is configured as distributed cache, but the PHP module "memcached" is not installed. Please install the PHP module "memcached".')
|
||||
$this->l10n->t('Memcached is configured as distributed cache, but the PHP module "memcached" is not installed.')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ class PhpOutdated implements ISetupCheck {
|
||||
|
||||
public function run(): SetupResult {
|
||||
if (PHP_VERSION_ID < 80200) {
|
||||
return SetupResult::warning($this->l10n->t('You are currently running PHP %s. PHP 8.1 is now deprecated in Nextcloud 30. Nextcloud 31 may require at least PHP 8.2. Please upgrade to one of the officially supported PHP versions provided by the PHP Group as soon as possible.', [PHP_VERSION]), 'https://secure.php.net/supported-versions.php');
|
||||
return SetupResult::warning($this->l10n->t('PHP %s detected. PHP >= 8.2 and <= 8.3 is suggested for optimal performance, security, and stability with this version of Nextcloud. Additionally, the next major release of Nextcloud will likely require >= PHP 8.2.', [PHP_VERSION]), 'https://secure.php.net/supported-versions.php');
|
||||
}
|
||||
return SetupResult::success($this->l10n->t('You are currently running PHP %s.', [PHP_VERSION]));
|
||||
}
|
||||
|
||||
@@ -62,16 +62,14 @@ class SupportedDatabase implements ISetupCheck {
|
||||
$row = $result->fetch();
|
||||
$version = $row['Value'];
|
||||
$versionlc = strtolower($version);
|
||||
// we only care about X.Y not X.Y.Z differences
|
||||
[$major, $minor, ] = explode('.', $versionlc);
|
||||
$versionConcern = $major . '.' . $minor;
|
||||
|
||||
if (str_contains($versionlc, 'mariadb')) {
|
||||
if (version_compare($versionConcern, '10.3', '<') || version_compare($versionConcern, '10.11', '>')) {
|
||||
return SetupResult::warning($this->l10n->t('MariaDB version "%s" detected. MariaDB >=10.3 and <=10.11 is suggested for best performance, stability and functionality with this version of Nextcloud.', $version));
|
||||
if (version_compare($versionlc, '10.2', '<')) {
|
||||
return SetupResult::warning($this->l10n->t('MariaDB version "%s" is used. Nextcloud 21 and higher do not support this version and require MariaDB 10.2 or higher.', $version));
|
||||
}
|
||||
} else {
|
||||
if (version_compare($versionConcern, '8.0', '<') || version_compare($versionConcern, '8.3', '>')) {
|
||||
return SetupResult::warning($this->l10n->t('MySQL version "%s" detected. MySQL >=8.0 and <=8.3 is suggested for best performance, stability and functionality with this version of Nextcloud.', $version));
|
||||
if (version_compare($versionlc, '8', '<')) {
|
||||
return SetupResult::warning($this->l10n->t('MySQL version "%s" is used. Nextcloud 21 and higher do not support this version and require MySQL 8.0 or MariaDB 10.2 or higher.', $version));
|
||||
}
|
||||
}
|
||||
} elseif ($databasePlatform instanceof PostgreSQLPlatform) {
|
||||
@@ -79,12 +77,8 @@ class SupportedDatabase implements ISetupCheck {
|
||||
$result->execute();
|
||||
$row = $result->fetch();
|
||||
$version = $row['server_version'];
|
||||
$versionlc = strtolower($version);
|
||||
// we only care about X not X.Y or X.Y.Z differences
|
||||
[$major, ] = explode('.', $versionlc);
|
||||
$versionConcern = $major;
|
||||
if (version_compare($versionConcern, '12', '<') || version_compare($versionConcern, '16', '>')) {
|
||||
return SetupResult::warning($this->l10n->t('PostgreSQL version "%s" detected. PostgreSQL >=12 and <=16 is suggested for best performance, stability and functionality with this version of Nextcloud.', $version));
|
||||
if (version_compare(strtolower($version), '9.6', '<')) {
|
||||
return SetupResult::warning($this->l10n->t('PostgreSQL version "%s" is used. Nextcloud 21 and higher do not support this version and require PostgreSQL 9.6 or higher.', $version));
|
||||
}
|
||||
} elseif ($databasePlatform instanceof OraclePlatform) {
|
||||
$version = 'Oracle';
|
||||
|
||||
@@ -190,9 +190,9 @@ export default {
|
||||
|
||||
.draggable__number {
|
||||
border-radius: 20px;
|
||||
border: 2px solid var(--color-primary-element);
|
||||
color: var(--color-primary-element);
|
||||
padding: 0px 7px;
|
||||
border: 2px solid var(--color-primary-default);
|
||||
color: var(--color-primary-default);
|
||||
padding: 0px 7px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
|
||||
@@ -160,17 +160,17 @@
|
||||
<NcCheckboxRadioSwitch type="switch"
|
||||
aria-controls="settings-sharing-privacy-user-enumeration"
|
||||
:checked.sync="settings.allowShareDialogUserEnumeration">
|
||||
{{ t('settings', 'Allow account name autocompletion in share dialog and allow access to the system address book') }}
|
||||
{{ t('settings', 'Allow username autocompletion in share dialog and allow access to the system address book') }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
<fieldset v-show="settings.allowShareDialogUserEnumeration" id="settings-sharing-privacy-user-enumeration" class="sharing__sub-section">
|
||||
<em>
|
||||
{{ t('settings', 'If autocompletion "same group" and "phone number integration" are enabled a match in either is enough to show the user.') }}
|
||||
</em>
|
||||
<NcCheckboxRadioSwitch :checked.sync="settings.restrictUserEnumerationToGroup">
|
||||
{{ t('settings', 'Allow account name autocompletion to users within the same groups and limit system address books to users in the same groups') }}
|
||||
{{ t('settings', 'Allow username autocompletion to users within the same groups and limit system address books to users in the same groups') }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
<NcCheckboxRadioSwitch :checked.sync="settings.restrictUserEnumerationToPhone">
|
||||
{{ t('settings', 'Allow account name autocompletion to users based on phone number integration') }}
|
||||
{{ t('settings', 'Allow username autocompletion to users based on phone number integration') }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
</fieldset>
|
||||
|
||||
|
||||
@@ -220,9 +220,9 @@ export default {
|
||||
|
||||
usernameLabel() {
|
||||
if (this.settings.newUserGenerateUserID) {
|
||||
return t('settings', 'Account name will be autogenerated')
|
||||
return t('settings', 'Username will be autogenerated')
|
||||
}
|
||||
return t('settings', 'Account name (required)')
|
||||
return t('settings', 'Username (required)')
|
||||
},
|
||||
|
||||
minPasswordLength() {
|
||||
|
||||
@@ -35,12 +35,8 @@
|
||||
<strong>
|
||||
{{ t('settings', 'Display name') }}
|
||||
</strong>
|
||||
</th>
|
||||
<th class="header__cell header__cell--username"
|
||||
data-cy-user-list-header-username
|
||||
scope="col">
|
||||
<span>
|
||||
{{ t('settings', 'Account name') }}
|
||||
<span class="header__subtitle">
|
||||
{{ t('settings', 'Username') }}
|
||||
</span>
|
||||
</th>
|
||||
<th class="header__cell"
|
||||
@@ -81,7 +77,7 @@
|
||||
data-cy-user-list-header-storage-location
|
||||
scope="col">
|
||||
<span v-if="showConfig.showUserBackend">
|
||||
{{ t('settings', 'Account backend') }}
|
||||
{{ t('settings', 'User backend') }}
|
||||
</span>
|
||||
<span v-if="showConfig.showStoragePath"
|
||||
class="header__subtitle">
|
||||
|
||||
@@ -54,14 +54,13 @@
|
||||
spellcheck="false"
|
||||
@trailing-button-click="updateDisplayName" />
|
||||
</template>
|
||||
<strong v-else-if="!isObfuscated"
|
||||
:title="user.displayname?.length > 20 ? user.displayname : null">
|
||||
{{ user.displayname }}
|
||||
</strong>
|
||||
</td>
|
||||
|
||||
<td class="row__cell row__cell--username" data-cy-user-list-cell-username>
|
||||
<span class="row__subtitle">{{ user.id }}</span>
|
||||
<template v-else>
|
||||
<strong v-if="!isObfuscated"
|
||||
:title="user.displayname?.length > 20 ? user.displayname : null">
|
||||
{{ user.displayname }}
|
||||
</strong>
|
||||
<span class="row__subtitle">{{ user.id }}</span>
|
||||
</template>
|
||||
</td>
|
||||
|
||||
<td data-cy-user-list-cell-password
|
||||
|
||||
@@ -66,10 +66,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
&--username {
|
||||
padding-left: calc(var(--default-grid-baseline) * 3);
|
||||
}
|
||||
|
||||
&--avatar {
|
||||
min-width: var(--avatar-cell-width);
|
||||
width: var(--avatar-cell-width);
|
||||
|
||||
@@ -1069,7 +1069,12 @@ class ShareByMailProvider implements IShareProvider {
|
||||
|
||||
$qb->innerJoin('s', 'filecache', 'f', $qb->expr()->eq('s.file_source', 'f.fileid'));
|
||||
|
||||
$qb->andWhere($qb->expr()->eq('f.parent', $qb->createNamedParameter($node->getId())));
|
||||
$qb->andWhere($qb->expr()->eq('f.storage', $qb->createNamedParameter($node->getMountPoint()->getNumericStorageId(), IQueryBuilder::PARAM_INT)));
|
||||
if ($shallow) {
|
||||
$qb->andWhere($qb->expr()->eq('f.parent', $qb->createNamedParameter($node->getId())));
|
||||
} else {
|
||||
$qb->andWhere($qb->expr()->like('f.path', $qb->createNamedParameter($this->dbConnection->escapeLikeParameter($node->getInternalPath()) . '/%')));
|
||||
}
|
||||
|
||||
$qb->orderBy('id');
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
--primary-invert-if-bright: no;
|
||||
--primary-invert-if-dark: invert(100%);
|
||||
--color-primary: #00679e;
|
||||
--color-primary-default: #0082c9;
|
||||
--color-primary-text: #ffffff;
|
||||
--color-primary-hover: #3285b1;
|
||||
--color-primary-light: #e5eff5;
|
||||
@@ -84,7 +85,6 @@
|
||||
--color-primary-element-light-hover: #dbe4ea;
|
||||
--color-primary-element-light-text: #00293f;
|
||||
--gradient-primary-background: linear-gradient(40deg, var(--color-primary) 0%, var(--color-primary-hover) 100%);
|
||||
--image-background-default: url('/apps/theming/img/background/kamil-porembinski-clouds.jpg');
|
||||
--color-background-plain: #00679e;
|
||||
--color-background-plain-text: #ffffff;
|
||||
--image-background: url('/apps/theming/img/background/kamil-porembinski-clouds.jpg');
|
||||
}
|
||||
|
||||
@@ -85,7 +85,6 @@ class Capabilities implements IPublicCapability {
|
||||
* color-element-dark: string,
|
||||
* logo: string,
|
||||
* background: string,
|
||||
* background-text: string,
|
||||
* background-plain: bool,
|
||||
* background-default: bool,
|
||||
* logoheader: string,
|
||||
@@ -95,13 +94,15 @@ class Capabilities implements IPublicCapability {
|
||||
*/
|
||||
public function getCapabilities() {
|
||||
$color = $this->theming->getDefaultColorPrimary();
|
||||
// Same as in DefaultTheme
|
||||
if ($color === BackgroundService::DEFAULT_COLOR) {
|
||||
$color = BackgroundService::DEFAULT_ACCESSIBLE_COLOR;
|
||||
}
|
||||
$colorText = $this->util->invertTextColor($color) ? '#000000' : '#ffffff';
|
||||
|
||||
$backgroundLogo = $this->config->getAppValue('theming', 'backgroundMime', '');
|
||||
$backgroundColor = $this->theming->getColorBackground();
|
||||
$backgroundText = $this->theming->getTextColorBackground();
|
||||
$backgroundPlain = $backgroundLogo === 'backgroundColor' || ($backgroundLogo === '' && $backgroundColor !== BackgroundService::DEFAULT_COLOR);
|
||||
$background = $backgroundPlain ? $backgroundColor : $this->url->getAbsoluteURL($this->theming->getBackground());
|
||||
$backgroundPlain = $backgroundLogo === 'backgroundColor' || ($backgroundLogo === '' && $color !== '#0082c9');
|
||||
$background = $backgroundPlain ? $color : $this->url->getAbsoluteURL($this->theming->getBackground());
|
||||
|
||||
$user = $this->userSession->getUser();
|
||||
if ($user instanceof IUser) {
|
||||
@@ -111,7 +112,10 @@ class Capabilities implements IPublicCapability {
|
||||
* @see \OCA\Theming\Themes\CommonThemeTrait::generateUserBackgroundVariables()
|
||||
*/
|
||||
$color = $this->theming->getColorPrimary();
|
||||
$colorText = $this->theming->getTextColorPrimary();
|
||||
if ($color === BackgroundService::DEFAULT_COLOR) {
|
||||
$color = BackgroundService::DEFAULT_ACCESSIBLE_COLOR;
|
||||
}
|
||||
$colorText = $this->util->invertTextColor($color) ? '#000000' : '#ffffff';
|
||||
|
||||
$backgroundImage = $this->config->getUserValue($user->getUID(), Application::APP_ID, 'background_image', BackgroundService::BACKGROUND_DEFAULT);
|
||||
if ($backgroundImage === BackgroundService::BACKGROUND_CUSTOM) {
|
||||
@@ -122,7 +126,7 @@ class Capabilities implements IPublicCapability {
|
||||
$background = $this->url->linkTo(Application::APP_ID, "img/background/$backgroundImage");
|
||||
} elseif ($backgroundImage !== BackgroundService::BACKGROUND_DEFAULT) {
|
||||
$backgroundPlain = true;
|
||||
$background = $backgroundColor;
|
||||
$background = $color;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,7 +142,6 @@ class Capabilities implements IPublicCapability {
|
||||
'color-element-dark' => $this->util->elementColor($color, false),
|
||||
'logo' => $this->url->getAbsoluteURL($this->theming->getLogo()),
|
||||
'background' => $background,
|
||||
'background-text' => $backgroundText,
|
||||
'background-plain' => $backgroundPlain,
|
||||
'background-default' => !$this->util->isBackgroundThemed(),
|
||||
'logoheader' => $this->url->getAbsoluteURL($this->theming->getLogo()),
|
||||
|
||||
@@ -33,7 +33,7 @@ use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class UpdateConfig extends Command {
|
||||
public const SUPPORTED_KEYS = [
|
||||
'name', 'url', 'imprintUrl', 'privacyUrl', 'slogan', 'color', 'primary_color', 'disable-user-theming'
|
||||
'name', 'url', 'imprintUrl', 'privacyUrl', 'slogan', 'color', 'disable-user-theming'
|
||||
];
|
||||
|
||||
private $themingDefaults;
|
||||
@@ -128,13 +128,8 @@ class UpdateConfig extends Command {
|
||||
$value = $this->imageManager->updateImage($key, $value);
|
||||
$key = $key . 'Mime';
|
||||
}
|
||||
|
||||
if ($key === 'color') {
|
||||
$output->writeln('<warning>Using "color" is depreacted, use "primary_color" instead');
|
||||
$key = 'primary_color';
|
||||
}
|
||||
|
||||
if ($key === 'primary_color' && !preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $value)) {
|
||||
if ($key === 'color' && !preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $value)) {
|
||||
$output->writeln('<error>The given color is invalid: ' . $value . '</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -50,11 +50,13 @@ use OCP\AppFramework\Http\DataResponse;
|
||||
use OCP\AppFramework\Http\FileDisplayResponse;
|
||||
use OCP\AppFramework\Http\JSONResponse;
|
||||
use OCP\AppFramework\Http\NotFoundResponse;
|
||||
use OCP\Files\IAppData;
|
||||
use OCP\Files\NotFoundException;
|
||||
use OCP\Files\NotPermittedException;
|
||||
use OCP\IConfig;
|
||||
use OCP\IL10N;
|
||||
use OCP\IRequest;
|
||||
use OCP\ITempManager;
|
||||
use OCP\IURLGenerator;
|
||||
use ScssPhp\ScssPhp\Compiler;
|
||||
|
||||
@@ -71,6 +73,8 @@ class ThemingController extends Controller {
|
||||
private ThemingDefaults $themingDefaults;
|
||||
private IL10N $l10n;
|
||||
private IConfig $config;
|
||||
private ITempManager $tempManager;
|
||||
private IAppData $appData;
|
||||
private IURLGenerator $urlGenerator;
|
||||
private IAppManager $appManager;
|
||||
private ImageManager $imageManager;
|
||||
@@ -82,6 +86,8 @@ class ThemingController extends Controller {
|
||||
IConfig $config,
|
||||
ThemingDefaults $themingDefaults,
|
||||
IL10N $l,
|
||||
ITempManager $tempManager,
|
||||
IAppData $appData,
|
||||
IURLGenerator $urlGenerator,
|
||||
IAppManager $appManager,
|
||||
ImageManager $imageManager,
|
||||
@@ -92,6 +98,8 @@ class ThemingController extends Controller {
|
||||
$this->themingDefaults = $themingDefaults;
|
||||
$this->l10n = $l;
|
||||
$this->config = $config;
|
||||
$this->tempManager = $tempManager;
|
||||
$this->appData = $appData;
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
$this->appManager = $appManager;
|
||||
$this->imageManager = $imageManager;
|
||||
@@ -143,12 +151,7 @@ class ThemingController extends Controller {
|
||||
$error = $this->l10n->t('The given slogan is too long');
|
||||
}
|
||||
break;
|
||||
case 'primary_color':
|
||||
if (!preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $value)) {
|
||||
$error = $this->l10n->t('The given color is invalid');
|
||||
}
|
||||
break;
|
||||
case 'background_color':
|
||||
case 'color':
|
||||
if (!preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $value)) {
|
||||
$error = $this->l10n->t('The given color is invalid');
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ class UserThemeController extends OCSController {
|
||||
protected ?string $userId = null;
|
||||
|
||||
private IConfig $config;
|
||||
private IUserSession $userSession;
|
||||
private ThemesService $themesService;
|
||||
private ThemingDefaults $themingDefaults;
|
||||
private BackgroundService $backgroundService;
|
||||
@@ -71,6 +72,7 @@ class UserThemeController extends OCSController {
|
||||
BackgroundService $backgroundService) {
|
||||
parent::__construct($appName, $request);
|
||||
$this->config = $config;
|
||||
$this->userSession = $userSession;
|
||||
$this->themesService = $themesService;
|
||||
$this->themingDefaults = $themingDefaults;
|
||||
$this->backgroundService = $backgroundService;
|
||||
@@ -184,8 +186,7 @@ class UserThemeController extends OCSController {
|
||||
$this->backgroundService->deleteBackgroundImage();
|
||||
return new JSONResponse([
|
||||
'backgroundImage' => null,
|
||||
'backgroundColor' => $this->themingDefaults->getColorBackground(),
|
||||
'primaryColor' => $this->themingDefaults->getColorPrimary(),
|
||||
'backgroundColor' => $this->themingDefaults->getColorPrimary(),
|
||||
'version' => $currentVersion,
|
||||
]);
|
||||
}
|
||||
@@ -240,8 +241,7 @@ class UserThemeController extends OCSController {
|
||||
|
||||
return new JSONResponse([
|
||||
'backgroundImage' => $this->config->getUserValue($this->userId, Application::APP_ID, 'background_image', BackgroundService::BACKGROUND_DEFAULT),
|
||||
'backgroundColor' => $this->themingDefaults->getColorBackground(),
|
||||
'primaryColor' => $this->themingDefaults->getColorPrimary(),
|
||||
'backgroundColor' => $this->themingDefaults->getColorPrimary(),
|
||||
'version' => $currentVersion,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
*
|
||||
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
* @author Daniel Kesselberg <mail@danielkesselberg.de>
|
||||
* @author Ferdinand Thiessen <opensource@fthiessen.de>
|
||||
* @author Gary Kim <gary@garykim.dev>
|
||||
* @author Jacob Neplokh <me@jacobneplokh.com>
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
@@ -57,7 +56,6 @@ class ImageManager {
|
||||
private ICacheFactory $cacheFactory,
|
||||
private LoggerInterface $logger,
|
||||
private ITempManager $tempManager,
|
||||
private BackgroundService $backgroundService,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -79,11 +77,7 @@ class ImageManager {
|
||||
case 'favicon':
|
||||
return $this->urlGenerator->imagePath('core', 'logo/logo.png') . '?v=' . $cacheBusterCounter;
|
||||
case 'background':
|
||||
// Removing the background defines its mime as 'backgroundColor'
|
||||
$mimeSetting = $this->config->getAppValue('theming', 'backgroundMime', '');
|
||||
if ($mimeSetting !== 'backgroundColor') {
|
||||
return $this->urlGenerator->linkTo(Application::APP_ID, 'img/background/' . BackgroundService::DEFAULT_BACKGROUND_IMAGE);
|
||||
}
|
||||
return $this->urlGenerator->linkTo(Application::APP_ID, 'img/background/' . BackgroundService::DEFAULT_BACKGROUND_IMAGE);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
@@ -233,56 +227,47 @@ class ImageManager {
|
||||
throw new \Exception('Unsupported image type: ' . $detectedMimeType);
|
||||
}
|
||||
|
||||
if ($key === 'background') {
|
||||
if ($this->shouldOptimizeBackgroundImage($detectedMimeType, filesize($tmpFile))) {
|
||||
try {
|
||||
// Optimize the image since some people may upload images that will be
|
||||
// either to big or are not progressive rendering.
|
||||
$newImage = @imagecreatefromstring(file_get_contents($tmpFile));
|
||||
if ($newImage === false) {
|
||||
throw new \Exception('Could not read background image, possibly corrupted.');
|
||||
}
|
||||
|
||||
// Preserve transparency
|
||||
imagesavealpha($newImage, true);
|
||||
imagealphablending($newImage, true);
|
||||
|
||||
$imageWidth = imagesx($newImage);
|
||||
$imageHeight = imagesy($newImage);
|
||||
|
||||
/** @var int */
|
||||
$newWidth = min(4096, $imageWidth);
|
||||
$newHeight = intval($imageHeight / ($imageWidth / $newWidth));
|
||||
$outputImage = imagescale($newImage, $newWidth, $newHeight);
|
||||
if ($outputImage === false) {
|
||||
throw new \Exception('Could not scale uploaded background image.');
|
||||
}
|
||||
|
||||
$newTmpFile = $this->tempManager->getTemporaryFile();
|
||||
imageinterlace($outputImage, true);
|
||||
// Keep jpeg images encoded as jpeg
|
||||
if (str_contains($detectedMimeType, 'image/jpeg')) {
|
||||
if (!imagejpeg($outputImage, $newTmpFile, 90)) {
|
||||
throw new \Exception('Could not recompress background image as JPEG');
|
||||
}
|
||||
} else {
|
||||
if (!imagepng($outputImage, $newTmpFile, 8)) {
|
||||
throw new \Exception('Could not recompress background image as PNG');
|
||||
}
|
||||
}
|
||||
$tmpFile = $newTmpFile;
|
||||
imagedestroy($outputImage);
|
||||
} catch (\Exception $e) {
|
||||
if (isset($outputImage) && is_resource($outputImage) || $outputImage instanceof \GdImage) {
|
||||
imagedestroy($outputImage);
|
||||
}
|
||||
|
||||
$this->logger->debug($e->getMessage());
|
||||
if ($key === 'background' && $this->shouldOptimizeBackgroundImage($detectedMimeType, filesize($tmpFile))) {
|
||||
try {
|
||||
// Optimize the image since some people may upload images that will be
|
||||
// either to big or are not progressive rendering.
|
||||
$newImage = @imagecreatefromstring(file_get_contents($tmpFile));
|
||||
if ($newImage === false) {
|
||||
throw new \Exception('Could not read background image, possibly corrupted.');
|
||||
}
|
||||
}
|
||||
|
||||
// For background images we need to announce it
|
||||
$this->backgroundService->setGlobalBackground($tmpFile);
|
||||
// Preserve transparency
|
||||
imagesavealpha($newImage, true);
|
||||
imagealphablending($newImage, true);
|
||||
|
||||
$newWidth = (imagesx($newImage) < 4096 ? imagesx($newImage) : 4096);
|
||||
$newHeight = (int)(imagesy($newImage) / (imagesx($newImage) / $newWidth));
|
||||
$outputImage = imagescale($newImage, $newWidth, $newHeight);
|
||||
if ($outputImage === false) {
|
||||
throw new \Exception('Could not scale uploaded background image.');
|
||||
}
|
||||
|
||||
$newTmpFile = $this->tempManager->getTemporaryFile();
|
||||
imageinterlace($outputImage, true);
|
||||
// Keep jpeg images encoded as jpeg
|
||||
if (str_contains($detectedMimeType, 'image/jpeg')) {
|
||||
if (!imagejpeg($outputImage, $newTmpFile, 90)) {
|
||||
throw new \Exception('Could not recompress background image as JPEG');
|
||||
}
|
||||
} else {
|
||||
if (!imagepng($outputImage, $newTmpFile, 8)) {
|
||||
throw new \Exception('Could not recompress background image as PNG');
|
||||
}
|
||||
}
|
||||
$tmpFile = $newTmpFile;
|
||||
imagedestroy($outputImage);
|
||||
} catch (\Exception $e) {
|
||||
if (is_resource($outputImage) || $outputImage instanceof \GdImage) {
|
||||
imagedestroy($outputImage);
|
||||
}
|
||||
|
||||
$this->logger->debug($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$target->putContent(file_get_contents($tmpFile));
|
||||
|
||||
@@ -55,24 +55,14 @@ class BeforePreferenceListener implements IEventListener {
|
||||
}
|
||||
|
||||
private function handleThemingValues(BeforePreferenceSetEvent|BeforePreferenceDeletedEvent $event): void {
|
||||
$allowedKeys = ['shortcuts_disabled', 'primary_color'];
|
||||
|
||||
if (!in_array($event->getConfigKey(), $allowedKeys)) {
|
||||
if ($event->getConfigKey() !== 'shortcuts_disabled') {
|
||||
// Not allowed config key
|
||||
return;
|
||||
}
|
||||
|
||||
if ($event instanceof BeforePreferenceSetEvent) {
|
||||
switch ($event->getConfigKey()) {
|
||||
case 'shortcuts_disabled':
|
||||
$event->setValid($event->getConfigValue() === 'yes');
|
||||
break;
|
||||
case 'primary_color':
|
||||
$event->setValid(preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $event->getConfigValue()) === 1);
|
||||
break;
|
||||
default:
|
||||
$event->setValid(false);
|
||||
}
|
||||
$event->setValid($event->getConfigValue() === 'yes');
|
||||
return;
|
||||
}
|
||||
|
||||
$event->setValid(true);
|
||||
|
||||
@@ -26,6 +26,7 @@ declare(strict_types=1);
|
||||
namespace OCA\Theming\Listener;
|
||||
|
||||
use OCA\Theming\AppInfo\Application;
|
||||
use OCA\Theming\Service\BackgroundService;
|
||||
use OCA\Theming\Service\JSDataService;
|
||||
use OCA\Theming\Service\ThemeInjectionService;
|
||||
use OCP\AppFramework\Http\Events\BeforeLoginTemplateRenderedEvent;
|
||||
@@ -80,6 +81,43 @@ class BeforeTemplateRenderedListener implements IEventListener {
|
||||
|
||||
$this->themeInjectionService->injectHeaders();
|
||||
|
||||
$user = $this->userSession->getUser();
|
||||
|
||||
if (!empty($user)) {
|
||||
$userId = $user->getUID();
|
||||
|
||||
/** User background */
|
||||
$this->initialState->provideInitialState(
|
||||
'backgroundImage',
|
||||
$this->config->getUserValue($userId, Application::APP_ID, 'background_image', BackgroundService::BACKGROUND_DEFAULT),
|
||||
);
|
||||
|
||||
/** User color */
|
||||
$this->initialState->provideInitialState(
|
||||
'backgroundColor',
|
||||
$this->config->getUserValue($userId, Application::APP_ID, 'background_color', BackgroundService::DEFAULT_COLOR),
|
||||
);
|
||||
|
||||
/**
|
||||
* Admin background. `backgroundColor` if disabled,
|
||||
* mime type if defined and empty by default
|
||||
*/
|
||||
$this->initialState->provideInitialState(
|
||||
'themingDefaultBackground',
|
||||
$this->config->getAppValue('theming', 'backgroundMime', ''),
|
||||
);
|
||||
$this->initialState->provideInitialState(
|
||||
'defaultShippedBackground',
|
||||
BackgroundService::DEFAULT_BACKGROUND_IMAGE,
|
||||
);
|
||||
|
||||
/** List of all shipped backgrounds */
|
||||
$this->initialState->provideInitialState(
|
||||
'shippedBackgrounds',
|
||||
BackgroundService::SHIPPED_BACKGROUNDS,
|
||||
);
|
||||
}
|
||||
|
||||
// Making sure to inject just after core
|
||||
\OCP\Util::addScript('theming', 'theming', 'core');
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ namespace OCA\Theming;
|
||||
* @psalm-type ThemingBackground = array{
|
||||
* backgroundImage: ?string,
|
||||
* backgroundColor: string,
|
||||
* primaryColor: string,
|
||||
* version: int,
|
||||
* }
|
||||
*/
|
||||
|
||||
@@ -8,7 +8,6 @@ declare(strict_types=1);
|
||||
* @author Jan C. Borchardt <hey@jancborchardt.net>
|
||||
* @author Julius Härtl <jus@bitgrid.net>
|
||||
* @author Christopher Ng <chrng8@gmail.com>
|
||||
* @author Ferdinand Thiessen <opensource@fthiessen.de>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
@@ -31,6 +30,7 @@ namespace OCA\Theming\Service;
|
||||
use InvalidArgumentException;
|
||||
use OC\User\NoUserException;
|
||||
use OCA\Theming\AppInfo\Application;
|
||||
use OCA\Theming\ThemingDefaults;
|
||||
use OCP\Files\File;
|
||||
use OCP\Files\IAppData;
|
||||
use OCP\Files\IRootFolder;
|
||||
@@ -41,186 +41,167 @@ use OCP\Files\SimpleFS\ISimpleFolder;
|
||||
use OCP\IConfig;
|
||||
use OCP\Lock\LockedException;
|
||||
use OCP\PreConditionNotMetException;
|
||||
use RuntimeException;
|
||||
|
||||
class BackgroundService {
|
||||
public const DEFAULT_COLOR = '#00679e';
|
||||
public const DEFAULT_BACKGROUND_COLOR = '#00679e';
|
||||
// true when the background is bright and need dark icons
|
||||
public const THEMING_MODE_DARK = 'dark';
|
||||
public const DEFAULT_COLOR = '#0082c9';
|
||||
public const DEFAULT_ACCESSIBLE_COLOR = '#00679e';
|
||||
|
||||
/**
|
||||
* One of our shipped background images is used
|
||||
*/
|
||||
public const BACKGROUND_SHIPPED = 'shipped';
|
||||
/**
|
||||
* A custom background image is used
|
||||
*/
|
||||
public const BACKGROUND_CUSTOM = 'custom';
|
||||
/**
|
||||
* The default background image is used
|
||||
*/
|
||||
public const BACKGROUND_DEFAULT = 'default';
|
||||
/**
|
||||
* Just a background color is used
|
||||
*/
|
||||
public const BACKGROUND_COLOR = 'color';
|
||||
public const BACKGROUND_DISABLED = 'disabled';
|
||||
|
||||
public const DEFAULT_BACKGROUND_IMAGE = 'kamil-porembinski-clouds.jpg';
|
||||
|
||||
/**
|
||||
* 'attribution': Name, artist and license
|
||||
* 'description': Alternative text
|
||||
* 'attribution_url': URL for attribution
|
||||
* 'background_color': Cached mean color of the top part to calculate app menu colors and use as fallback
|
||||
* 'primary_color': Recommended primary color for this theme / image
|
||||
*/
|
||||
public const SHIPPED_BACKGROUNDS = [
|
||||
'hannah-maclean-soft-floral.jpg' => [
|
||||
'attribution' => 'Soft floral (Hannah MacLean, CC0)',
|
||||
'description' => 'Abstract background picture in yellow and white color whith a flower on it',
|
||||
'attribution_url' => 'https://stocksnap.io/photo/soft-floral-XOYWCCW5PA',
|
||||
'background_color' => '#e4d2c1',
|
||||
'primary_color' => '#9f652f',
|
||||
'theming' => self::THEMING_MODE_DARK,
|
||||
'primary_color' => '#D8A06C',
|
||||
],
|
||||
'ted-moravec-morning-fog.jpg' => [
|
||||
'attribution' => 'Morning fog (Ted Moravec, Public Domain)',
|
||||
'description' => 'Background picture of a forest shrouded in fog',
|
||||
'attribution_url' => 'https://flickr.com/photos/tmoravec/52392410261',
|
||||
'background_color' => '#f6f7f6',
|
||||
'primary_color' => '#114c3b',
|
||||
'theming' => self::THEMING_MODE_DARK,
|
||||
'primary_color' => '#38A084',
|
||||
],
|
||||
'stefanus-martanto-setyo-husodo-underwater-ocean.jpg' => [
|
||||
'attribution' => 'Underwater ocean (Stefanus Martanto Setyo Husodo, CC0)',
|
||||
'description' => 'Background picture of an underwater ocean',
|
||||
'attribution_url' => 'https://stocksnap.io/photo/underwater-ocean-TJA9LBH4WS',
|
||||
'background_color' => '#003351',
|
||||
'primary_color' => '#04577e',
|
||||
],
|
||||
'zoltan-voros-rhythm-and-blues.jpg' => [
|
||||
'attribution' => 'Rhythm and blues (Zoltán Vörös, CC BY)',
|
||||
'description' => 'Abstract background picture of sand dunes during night',
|
||||
'attribution_url' => 'https://flickr.com/photos/v923z/51634409289/',
|
||||
'background_color' => '#1c2437',
|
||||
'primary_color' => '#1c243c',
|
||||
],
|
||||
'anatoly-mikhaltsov-butterfly-wing-scale.jpg' => [
|
||||
'attribution' => 'Butterfly wing scale (Anatoly Mikhaltsov, CC BY-SA)',
|
||||
'description' => 'Background picture of a red-ish butterfly wing under microscope',
|
||||
'attribution_url' => 'https://commons.wikimedia.org/wiki/File:%D0%A7%D0%B5%D1%88%D1%83%D0%B9%D0%BA%D0%B8_%D0%BA%D1%80%D1%8B%D0%BB%D0%B0_%D0%B1%D0%B0%D0%B1%D0%BE%D1%87%D0%BA%D0%B8.jpg',
|
||||
'background_color' => '#652e11',
|
||||
'primary_color' => '#a53c17',
|
||||
],
|
||||
'bernie-cetonia-aurata-take-off-composition.jpg' => [
|
||||
'attribution' => 'Cetonia aurata take off composition (Bernie, Public Domain)',
|
||||
'description' => 'Montage of a cetonia aurata bug that takes off with white background',
|
||||
'attribution_url' => 'https://commons.wikimedia.org/wiki/File:Cetonia_aurata_take_off_composition_05172009.jpg',
|
||||
'background_color' => '#dee0d3',
|
||||
'primary_color' => '#56633d',
|
||||
'theming' => self::THEMING_MODE_DARK,
|
||||
'primary_color' => '#869171',
|
||||
],
|
||||
'dejan-krsmanovic-ribbed-red-metal.jpg' => [
|
||||
'attribution' => 'Ribbed red metal (Dejan Krsmanovic, CC BY)',
|
||||
'description' => 'Abstract background picture of red ribbed metal with two horizontal white elements on top of it',
|
||||
'attribution_url' => 'https://www.flickr.com/photos/dejankrsmanovic/42971456774/',
|
||||
'background_color' => '#9b171c',
|
||||
'primary_color' => '#9c4236',
|
||||
],
|
||||
'eduardo-neves-pedra-azul.jpg' => [
|
||||
'attribution' => 'Pedra azul milky way (Eduardo Neves, CC BY-SA)',
|
||||
'description' => 'Background picture of the milky way during night with a mountain in front of it',
|
||||
'attribution_url' => 'https://commons.wikimedia.org/wiki/File:Pedra_Azul_Milky_Way.jpg',
|
||||
'background_color' => '#1d242d',
|
||||
'primary_color' => '#4f6071',
|
||||
],
|
||||
'european-space-agency-barents-bloom.jpg' => [
|
||||
'attribution' => 'Barents bloom (European Space Agency, CC BY-SA)',
|
||||
'description' => 'Abstract background picture of blooming barents in blue and green colors',
|
||||
'attribution_url' => 'https://www.esa.int/ESA_Multimedia/Images/2016/08/Barents_bloom',
|
||||
'background_color' => '#1c383d',
|
||||
'primary_color' => '#396475',
|
||||
],
|
||||
'hannes-fritz-flippity-floppity.jpg' => [
|
||||
'attribution' => 'Flippity floppity (Hannes Fritz, CC BY-SA)',
|
||||
'description' => 'Abstract background picture of many pairs of flip flops hanging on a wall in multiple colors',
|
||||
'attribution_url' => 'http://hannes.photos/flippity-floppity',
|
||||
'background_color' => '#5b2d53',
|
||||
'primary_color' => '#98415a',
|
||||
],
|
||||
'hannes-fritz-roulette.jpg' => [
|
||||
'attribution' => 'Roulette (Hannes Fritz, CC BY-SA)',
|
||||
'description' => 'Background picture of a rotating giant wheel during night',
|
||||
'attribution_url' => 'http://hannes.photos/roulette',
|
||||
'background_color' => '#000000',
|
||||
'primary_color' => '#845334',
|
||||
],
|
||||
'hannes-fritz-sea-spray.jpg' => [
|
||||
'attribution' => 'Sea spray (Hannes Fritz, CC BY-SA)',
|
||||
'description' => 'Background picture of a stone coast with fog and sea behind it',
|
||||
'attribution_url' => 'http://hannes.photos/sea-spray',
|
||||
'background_color' => '#333f47',
|
||||
'primary_color' => '#4f6071',
|
||||
],
|
||||
'kamil-porembinski-clouds.jpg' => [
|
||||
'attribution' => 'Clouds (Kamil Porembiński, CC BY-SA)',
|
||||
'description' => 'Background picture of white clouds on in front of a blue sky',
|
||||
'attribution_url' => 'https://www.flickr.com/photos/paszczak000/8715851521/',
|
||||
'background_color' => self::DEFAULT_BACKGROUND_COLOR,
|
||||
'primary_color' => self::DEFAULT_COLOR,
|
||||
],
|
||||
'bernard-spragg-new-zealand-fern.jpg' => [
|
||||
'attribution' => 'New zealand fern (Bernard Spragg, CC0)',
|
||||
'description' => 'Abstract background picture of fern leafes',
|
||||
'attribution_url' => 'https://commons.wikimedia.org/wiki/File:NZ_Fern.(Blechnum_chambersii)_(11263534936).jpg',
|
||||
'background_color' => '#0c3c03',
|
||||
'primary_color' => '#316b26',
|
||||
],
|
||||
'rawpixel-pink-tapioca-bubbles.jpg' => [
|
||||
'attribution' => 'Pink tapioca bubbles (Rawpixel, CC BY)',
|
||||
'description' => 'Abstract background picture of pink tapioca bubbles',
|
||||
'attribution_url' => 'https://www.flickr.com/photos/byrawpixel/27665140298/in/photostream/',
|
||||
'background_color' => '#c56e95',
|
||||
'primary_color' => '#7b4e7e',
|
||||
'theming' => self::THEMING_MODE_DARK,
|
||||
'primary_color' => '#b17ab4',
|
||||
],
|
||||
'nasa-waxing-crescent-moon.jpg' => [
|
||||
'attribution' => 'Waxing crescent moon (NASA, Public Domain)',
|
||||
'description' => 'Background picture of glowing earth in foreground and moon in the background',
|
||||
'attribution_url' => 'https://www.nasa.gov/image-feature/a-waxing-crescent-moon',
|
||||
'background_color' => '#000002',
|
||||
'primary_color' => '#005ac1',
|
||||
],
|
||||
'tommy-chau-already.jpg' => [
|
||||
'attribution' => 'Cityscape (Tommy Chau, CC BY)',
|
||||
'description' => 'Background picture of a skyscraper city during night',
|
||||
'attribution_url' => 'https://www.flickr.com/photos/90975693@N05/16910999368',
|
||||
'background_color' => '#35229f',
|
||||
'primary_color' => '#6a2af4',
|
||||
],
|
||||
'tommy-chau-lion-rock-hill.jpg' => [
|
||||
'attribution' => 'Lion rock hill (Tommy Chau, CC BY)',
|
||||
'description' => 'Background picture of mountains during sunset or sunrise',
|
||||
'attribution_url' => 'https://www.flickr.com/photos/90975693@N05/17136440246',
|
||||
'background_color' => '#cb92b7',
|
||||
'primary_color' => '#7f4f70',
|
||||
'theming' => self::THEMING_MODE_DARK,
|
||||
'primary_color' => '#c074a9',
|
||||
],
|
||||
'lali-masriera-yellow-bricks.jpg' => [
|
||||
'attribution' => 'Yellow bricks (Lali Masriera, CC BY)',
|
||||
'description' => 'Background picture of yellow bricks with some yellow tubes',
|
||||
'attribution_url' => 'https://www.flickr.com/photos/visualpanic/3982464447',
|
||||
'background_color' => '#c78a19',
|
||||
'primary_color' => '#7f5700',
|
||||
'theming' => self::THEMING_MODE_DARK,
|
||||
'primary_color' => '#bc8210',
|
||||
],
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private IRootFolder $rootFolder,
|
||||
private IAppData $appData,
|
||||
private IConfig $config,
|
||||
private ?string $userId,
|
||||
) {
|
||||
private IRootFolder $rootFolder;
|
||||
private IAppData $appData;
|
||||
private IConfig $config;
|
||||
private string $userId;
|
||||
private ThemingDefaults $themingDefaults;
|
||||
|
||||
public function __construct(IRootFolder $rootFolder,
|
||||
IAppData $appData,
|
||||
IConfig $config,
|
||||
?string $userId,
|
||||
ThemingDefaults $themingDefaults) {
|
||||
if ($userId === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->rootFolder = $rootFolder;
|
||||
$this->config = $config;
|
||||
$this->userId = $userId;
|
||||
$this->appData = $appData;
|
||||
$this->themingDefaults = $themingDefaults;
|
||||
}
|
||||
|
||||
public function setDefaultBackground(): void {
|
||||
$this->config->deleteUserValue($this->userId, Application::APP_ID, 'background_image');
|
||||
$this->config->deleteUserValue($this->userId, Application::APP_ID, 'background_color');
|
||||
$this->config->deleteUserValue($this->userId, Application::APP_ID, 'primary_color');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -232,9 +213,7 @@ class BackgroundService {
|
||||
* @throws NoUserException
|
||||
*/
|
||||
public function setFileBackground($path): void {
|
||||
if ($this->userId === null) {
|
||||
throw new RuntimeException('No currently logged-in user');
|
||||
}
|
||||
$this->config->setUserValue($this->userId, Application::APP_ID, 'background_image', self::BACKGROUND_CUSTOM);
|
||||
$userFolder = $this->rootFolder->getUserFolder($this->userId);
|
||||
|
||||
/** @var File $file */
|
||||
@@ -245,46 +224,26 @@ class BackgroundService {
|
||||
throw new InvalidArgumentException('Invalid image file');
|
||||
}
|
||||
|
||||
$meanColor = $this->calculateMeanColor($image);
|
||||
if ($meanColor !== false) {
|
||||
$this->setColorBackground($meanColor);
|
||||
}
|
||||
|
||||
$this->getAppDataFolder()->newFile('background.jpg', $file->fopen('r'));
|
||||
$this->config->setUserValue($this->userId, Application::APP_ID, 'background_image', self::BACKGROUND_CUSTOM);
|
||||
}
|
||||
|
||||
public function setShippedBackground($fileName): void {
|
||||
if ($this->userId === null) {
|
||||
throw new RuntimeException('No currently logged-in user');
|
||||
}
|
||||
if (!array_key_exists($fileName, self::SHIPPED_BACKGROUNDS)) {
|
||||
throw new InvalidArgumentException('The given file name is invalid');
|
||||
}
|
||||
$this->setColorBackground(self::SHIPPED_BACKGROUNDS[$fileName]['background_color']);
|
||||
$this->config->setUserValue($this->userId, Application::APP_ID, 'background_image', $fileName);
|
||||
$this->config->setUserValue($this->userId, Application::APP_ID, 'primary_color', self::SHIPPED_BACKGROUNDS[$fileName]['primary_color']);
|
||||
$this->setColorBackground(self::SHIPPED_BACKGROUNDS[$fileName]['primary_color']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the background to color only
|
||||
*/
|
||||
public function setColorBackground(string $color): void {
|
||||
if ($this->userId === null) {
|
||||
throw new RuntimeException('No currently logged-in user');
|
||||
}
|
||||
if (!preg_match('/^#([0-9a-f]{3}|[0-9a-f]{6})$/i', $color)) {
|
||||
throw new InvalidArgumentException('The given color is invalid');
|
||||
}
|
||||
$this->config->setUserValue($this->userId, Application::APP_ID, 'background_color', $color);
|
||||
$this->config->setUserValue($this->userId, Application::APP_ID, 'background_image', self::BACKGROUND_COLOR);
|
||||
}
|
||||
|
||||
public function deleteBackgroundImage(): void {
|
||||
if ($this->userId === null) {
|
||||
throw new RuntimeException('No currently logged-in user');
|
||||
}
|
||||
$this->config->setUserValue($this->userId, Application::APP_ID, 'background_image', self::BACKGROUND_COLOR);
|
||||
$this->config->setUserValue($this->userId, Application::APP_ID, 'background_image', self::BACKGROUND_DISABLED);
|
||||
}
|
||||
|
||||
public function getBackground(): ?ISimpleFile {
|
||||
@@ -299,82 +258,6 @@ class BackgroundService {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a new global background (backgroundMime) is uploaded (admin setting)
|
||||
* This sets all necessary app config values
|
||||
* @param resource|string $path
|
||||
* @return string|null The fallback background color - if any
|
||||
*/
|
||||
public function setGlobalBackground($path): string|null {
|
||||
$image = new \OCP\Image();
|
||||
$handle = is_resource($path) ? $path : fopen($path, 'rb');
|
||||
|
||||
if ($handle && $image->loadFromFileHandle($handle) !== false) {
|
||||
$meanColor = $this->calculateMeanColor($image);
|
||||
if ($meanColor !== false) {
|
||||
$this->config->setAppValue(Application::APP_ID, 'background_color', $meanColor);
|
||||
return $meanColor;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate mean color of an given image
|
||||
* It only takes the upper part into account so that a matching text color can be derived for the app menu
|
||||
*/
|
||||
private function calculateMeanColor(\OCP\Image $image): false|string {
|
||||
/**
|
||||
* Small helper to ensure one channel is returned as 8byte hex
|
||||
*/
|
||||
function toHex(int $channel): string {
|
||||
$hex = dechex($channel);
|
||||
return match (strlen($hex)) {
|
||||
0 => '00',
|
||||
1 => '0'.$hex,
|
||||
2 => $hex,
|
||||
default => 'ff',
|
||||
};
|
||||
}
|
||||
|
||||
$tempImage = new \OCP\Image();
|
||||
|
||||
// Crop to only analyze top bar
|
||||
$resource = $image->cropNew(0, 0, $image->width(), min(max(50, (int)($image->height() * 0.125)), $image->height()));
|
||||
if ($resource === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$tempImage->setResource($resource);
|
||||
if (!$tempImage->preciseResize(100, 7)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$resource = $tempImage->resource();
|
||||
if ($resource === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$reds = [];
|
||||
$greens = [];
|
||||
$blues = [];
|
||||
for ($y = 0; $y < 7; $y++) {
|
||||
for ($x = 0; $x < 100; $x++) {
|
||||
$value = imagecolorat($resource, $x, $y);
|
||||
if ($value === false) {
|
||||
continue;
|
||||
}
|
||||
$reds[] = ($value >> 16) & 0xFF;
|
||||
$greens[] = ($value >> 8) & 0xFF;
|
||||
$blues[] = $value & 0xFF;
|
||||
}
|
||||
}
|
||||
$meanColor = '#' . toHex((int)(array_sum($reds) / count($reds)));
|
||||
$meanColor .= toHex((int)(array_sum($greens) / count($greens)));
|
||||
$meanColor .= toHex((int)(array_sum($blues) / count($blues)));
|
||||
return $meanColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Storing the data in appdata/theming/users/USERID
|
||||
*
|
||||
|
||||
@@ -28,40 +28,38 @@ namespace OCA\Theming\Service;
|
||||
|
||||
use OCA\Theming\ThemingDefaults;
|
||||
use OCA\Theming\Util;
|
||||
use OCP\IConfig;
|
||||
|
||||
class JSDataService implements \JsonSerializable {
|
||||
private ThemingDefaults $themingDefaults;
|
||||
private Util $util;
|
||||
private IConfig $appConfig;
|
||||
private ThemesService $themesService;
|
||||
|
||||
public function __construct(
|
||||
private ThemingDefaults $themingDefaults,
|
||||
private Util $util,
|
||||
private ThemesService $themesService,
|
||||
ThemingDefaults $themingDefaults,
|
||||
Util $util,
|
||||
IConfig $appConfig,
|
||||
ThemesService $themesService
|
||||
) {
|
||||
$this->themingDefaults = $themingDefaults;
|
||||
$this->util = $util;
|
||||
$this->appConfig = $appConfig;
|
||||
$this->themesService = $themesService;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array {
|
||||
return [
|
||||
'name' => $this->themingDefaults->getName(),
|
||||
'slogan' => $this->themingDefaults->getSlogan(),
|
||||
|
||||
'url' => $this->themingDefaults->getBaseUrl(),
|
||||
'slogan' => $this->themingDefaults->getSlogan(),
|
||||
'color' => $this->themingDefaults->getColorPrimary(),
|
||||
'defaultColor' => $this->themingDefaults->getDefaultColorPrimary(),
|
||||
'imprintUrl' => $this->themingDefaults->getImprintUrl(),
|
||||
'privacyUrl' => $this->themingDefaults->getPrivacyUrl(),
|
||||
|
||||
'primaryColor' => $this->themingDefaults->getColorPrimary(),
|
||||
'backgroundColor' => $this->themingDefaults->getColorBackground(),
|
||||
'defaultPrimaryColor' => $this->themingDefaults->getDefaultColorPrimary(),
|
||||
'defaultBackgroundColor' => $this->themingDefaults->getDefaultColorBackground(),
|
||||
'inverted' => $this->util->invertTextColor($this->themingDefaults->getColorPrimary()),
|
||||
|
||||
'cacheBuster' => $this->util->getCacheBuster(),
|
||||
'enabledThemes' => $this->themesService->getEnabledThemes(),
|
||||
|
||||
// deprecated use primaryColor
|
||||
'color' => $this->themingDefaults->getColorPrimary(),
|
||||
'' => 'color is deprecated since Nextcloud 29, use primaryColor instead'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ namespace OCA\Theming\Settings;
|
||||
use OCA\Theming\AppInfo\Application;
|
||||
use OCA\Theming\Controller\ThemingController;
|
||||
use OCA\Theming\ImageManager;
|
||||
use OCA\Theming\Service\BackgroundService;
|
||||
use OCA\Theming\ThemingDefaults;
|
||||
use OCP\AppFramework\Http\TemplateResponse;
|
||||
use OCP\AppFramework\Services\IInitialState;
|
||||
@@ -76,13 +75,9 @@ class Admin implements IDelegatedSettings {
|
||||
'name' => $this->themingDefaults->getEntity(),
|
||||
'url' => $this->themingDefaults->getBaseUrl(),
|
||||
'slogan' => $this->themingDefaults->getSlogan(),
|
||||
'primaryColor' => $this->themingDefaults->getDefaultColorPrimary(),
|
||||
'backgroundColor' => $this->themingDefaults->getDefaultColorBackground(),
|
||||
'color' => $this->themingDefaults->getDefaultColorPrimary(),
|
||||
'logoMime' => $this->config->getAppValue(Application::APP_ID, 'logoMime', ''),
|
||||
'allowedMimeTypes' => $allowedMimeTypes,
|
||||
'backgroundURL' => $this->imageManager->getImageUrl('background'),
|
||||
'defaultBackgroundURL' => $this->urlGenerator->linkTo(Application::APP_ID, 'img/background/' . BackgroundService::DEFAULT_BACKGROUND_IMAGE),
|
||||
'defaultBackgroundColor' => BackgroundService::DEFAULT_BACKGROUND_COLOR,
|
||||
'backgroundMime' => $this->config->getAppValue(Application::APP_ID, 'backgroundMime', ''),
|
||||
'logoheaderMime' => $this->config->getAppValue(Application::APP_ID, 'logoheaderMime', ''),
|
||||
'faviconMime' => $this->config->getAppValue(Application::APP_ID, 'faviconMime', ''),
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
namespace OCA\Theming\Settings;
|
||||
|
||||
use OCA\Theming\ITheme;
|
||||
use OCA\Theming\Service\BackgroundService;
|
||||
use OCA\Theming\Service\ThemesService;
|
||||
use OCA\Theming\ThemingDefaults;
|
||||
use OCP\App\IAppManager;
|
||||
@@ -72,26 +71,6 @@ class Personal implements ISettings {
|
||||
// Get the default app enforced by admin
|
||||
$forcedDefaultApp = $this->appManager->getDefaultAppForUser(null, false);
|
||||
|
||||
/** List of all shipped backgrounds */
|
||||
$this->initialStateService->provideInitialState('shippedBackgrounds', BackgroundService::SHIPPED_BACKGROUNDS);
|
||||
|
||||
/**
|
||||
* Admin theming
|
||||
*/
|
||||
$this->initialStateService->provideInitialState('themingDefaults', [
|
||||
/** URL of admin configured background image */
|
||||
'backgroundImage' => $this->themingDefaults->getBackground(),
|
||||
/** `backgroundColor` if disabled, mime type if defined and empty by default */
|
||||
'backgroundMime' => $this->config->getAppValue('theming', 'backgroundMime', ''),
|
||||
/** Admin configured background color */
|
||||
'backgroundColor' => $this->themingDefaults->getDefaultColorBackground(),
|
||||
/** Admin configured primary color */
|
||||
'primaryColor' => $this->themingDefaults->getDefaultColorPrimary(),
|
||||
/** Nextcloud default background image */
|
||||
'defaultShippedBackground' => BackgroundService::DEFAULT_BACKGROUND_IMAGE,
|
||||
]);
|
||||
|
||||
$this->initialStateService->provideInitialState('userBackgroundImage', $this->config->getUserValue($this->userId, 'theming', 'background_image', BackgroundService::BACKGROUND_DEFAULT));
|
||||
$this->initialStateService->provideInitialState('themes', array_values($themes));
|
||||
$this->initialStateService->provideInitialState('enforceTheme', $enforcedTheme);
|
||||
$this->initialStateService->provideInitialState('isUserThemingDisabled', $this->themingDefaults->isUserThemingDisabled());
|
||||
|
||||
@@ -6,7 +6,6 @@ declare(strict_types=1);
|
||||
*
|
||||
* @author Joas Schilling <coding@schilljs.com>
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
* @author Ferdinand Thiessen <opensource@fthiessen.de>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
@@ -29,12 +28,10 @@ namespace OCA\Theming\Themes;
|
||||
use OCA\Theming\AppInfo\Application;
|
||||
use OCA\Theming\ImageManager;
|
||||
use OCA\Theming\Service\BackgroundService;
|
||||
use OCA\Theming\ThemingDefaults;
|
||||
use OCA\Theming\Util;
|
||||
|
||||
trait CommonThemeTrait {
|
||||
public Util $util;
|
||||
public ThemingDefaults $themingDefaults;
|
||||
|
||||
/**
|
||||
* Generate primary-related variables
|
||||
@@ -61,6 +58,7 @@ trait CommonThemeTrait {
|
||||
'--primary-invert-if-dark' => $this->util->invertTextColor($colorPrimaryElement) ? 'no' : 'invert(100%)',
|
||||
|
||||
'--color-primary' => $this->primaryColor,
|
||||
'--color-primary-default' => $this->defaultPrimaryColor,
|
||||
'--color-primary-text' => $this->util->invertTextColor($this->primaryColor) ? '#000000' : '#ffffff',
|
||||
'--color-primary-hover' => $this->util->mix($this->primaryColor, $colorMainBackground, 60),
|
||||
'--color-primary-light' => $colorPrimaryLight,
|
||||
@@ -90,33 +88,34 @@ trait CommonThemeTrait {
|
||||
protected function generateGlobalBackgroundVariables(): array {
|
||||
$backgroundDeleted = $this->config->getAppValue(Application::APP_ID, 'backgroundMime', '') === 'backgroundColor';
|
||||
$hasCustomLogoHeader = $this->util->isLogoThemed();
|
||||
$backgroundColor = $this->themingDefaults->getColorBackground();
|
||||
$isPrimaryBright = $this->util->invertTextColor($this->primaryColor);
|
||||
|
||||
$variables = [];
|
||||
|
||||
// Default last fallback values
|
||||
$variables = [
|
||||
'--color-background-plain' => $backgroundColor,
|
||||
'--color-background-plain-text' => $this->util->invertTextColor($backgroundColor) ? '#000000' : '#ffffff',
|
||||
'--background-image-invert-if-bright' => $this->util->invertTextColor($backgroundColor) ? 'invert(100%)' : 'no',
|
||||
];
|
||||
$variables['--image-background-default'] = "url('" . $this->themingDefaults->getBackground() . "')";
|
||||
$variables['--color-background-plain'] = $this->primaryColor;
|
||||
|
||||
// Register image variables only if custom-defined
|
||||
foreach (ImageManager::SUPPORTED_IMAGE_KEYS as $image) {
|
||||
if ($this->imageManager->hasImage($image)) {
|
||||
$imageUrl = $this->imageManager->getImageUrl($image);
|
||||
// --image-background is overridden by user theming if logged in
|
||||
$variables["--image-$image"] = "url('" . $imageUrl . "')";
|
||||
} elseif ($image === 'background') {
|
||||
// Apply default background if nothing is configured
|
||||
$variables['--image-background'] = "url('" . $this->themingDefaults->getBackground() . "')";
|
||||
}
|
||||
}
|
||||
|
||||
// If a background has been requested let's not define the background image
|
||||
// If primary as background has been request or if we have a custom primary colour
|
||||
// let's not define the background image
|
||||
if ($backgroundDeleted) {
|
||||
$variables['--image-background'] = 'none';
|
||||
$variables['--color-background-plain'] = $this->primaryColor;
|
||||
$variables['--image-background-plain'] = 'yes';
|
||||
$variables['--image-background'] = 'no';
|
||||
// If no background image is set, we need to check against the shown primary colour
|
||||
$variables['--background-image-invert-if-bright'] = $isPrimaryBright ? 'invert(100%)' : 'no';
|
||||
}
|
||||
|
||||
if ($hasCustomLogoHeader) {
|
||||
// prevent inverting the logo on bright colors if customized
|
||||
$variables['--image-logoheader-custom'] = 'true';
|
||||
}
|
||||
|
||||
@@ -131,37 +130,48 @@ trait CommonThemeTrait {
|
||||
if ($user !== null
|
||||
&& !$this->themingDefaults->isUserThemingDisabled()
|
||||
&& $this->appManager->isEnabledForUser(Application::APP_ID)) {
|
||||
$adminBackgroundDeleted = $this->config->getAppValue(Application::APP_ID, 'backgroundMime', '') === 'backgroundColor';
|
||||
$backgroundImage = $this->config->getUserValue($user->getUID(), Application::APP_ID, 'background_image', BackgroundService::BACKGROUND_DEFAULT);
|
||||
$backgroundColor = $this->config->getUserValue($user->getUID(), Application::APP_ID, 'background_color', $this->themingDefaults->getColorBackground());
|
||||
|
||||
$currentVersion = (int)$this->config->getUserValue($user->getUID(), Application::APP_ID, 'userCacheBuster', '0');
|
||||
$isBackgroundBright = $this->util->invertTextColor($backgroundColor);
|
||||
$backgroundTextColor = $this->util->invertTextColor($backgroundColor) ? '#000000' : '#ffffff';
|
||||
$isPrimaryBright = $this->util->invertTextColor($this->primaryColor);
|
||||
|
||||
$variables = [
|
||||
'--color-background-plain' => $backgroundColor,
|
||||
'--color-background-plain-text' => $backgroundTextColor,
|
||||
'--background-image-invert-if-bright' => $isBackgroundBright ? 'invert(100%)' : 'no',
|
||||
];
|
||||
|
||||
// Only use a background color without an image
|
||||
if ($backgroundImage === BackgroundService::BACKGROUND_COLOR) {
|
||||
// Might be defined already by admin theming, needs to be overridden
|
||||
$variables['--image-background'] = 'none';
|
||||
// The user removed the background
|
||||
if ($backgroundImage === BackgroundService::BACKGROUND_DISABLED) {
|
||||
return [
|
||||
// Might be defined already by admin theming, needs to be overridden
|
||||
'--image-background' => 'none',
|
||||
'--color-background-plain' => $this->primaryColor,
|
||||
// If no background image is set, we need to check against the shown primary colour
|
||||
'--background-image-invert-if-bright' => $isPrimaryBright ? 'invert(100%)' : 'no',
|
||||
];
|
||||
}
|
||||
|
||||
// The user uploaded a custom background
|
||||
if ($backgroundImage === BackgroundService::BACKGROUND_CUSTOM) {
|
||||
$cacheBuster = substr(sha1($user->getUID() . '_' . $currentVersion), 0, 8);
|
||||
$variables['--image-background'] = "url('" . $this->urlGenerator->linkToRouteAbsolute('theming.userTheme.getBackground') . "?v=$cacheBuster')";
|
||||
return [
|
||||
'--image-background' => "url('" . $this->urlGenerator->linkToRouteAbsolute('theming.userTheme.getBackground') . "?v=$cacheBuster')",
|
||||
'--color-background-plain' => $this->primaryColor,
|
||||
];
|
||||
}
|
||||
|
||||
// The user is using the default background and admin removed the background image
|
||||
if ($backgroundImage === BackgroundService::BACKGROUND_DEFAULT && $adminBackgroundDeleted) {
|
||||
return [
|
||||
// --image-background is not defined in this case
|
||||
'--color-background-plain' => $this->primaryColor,
|
||||
'--background-image-invert-if-bright' => $isPrimaryBright ? 'invert(100%)' : 'no',
|
||||
];
|
||||
}
|
||||
|
||||
// The user picked a shipped background
|
||||
if (isset(BackgroundService::SHIPPED_BACKGROUNDS[$backgroundImage])) {
|
||||
$variables['--image-background'] = "url('" . $this->urlGenerator->linkTo(Application::APP_ID, "img/background/$backgroundImage") . "')";
|
||||
return [
|
||||
'--image-background' => "url('" . $this->urlGenerator->linkTo(Application::APP_ID, "img/background/$backgroundImage") . "')",
|
||||
'--color-background-plain' => $this->primaryColor,
|
||||
'--background-image-invert-if-bright' => BackgroundService::SHIPPED_BACKGROUNDS[$backgroundImage]['theming'] ?? null === BackgroundService::THEMING_MODE_DARK ? 'invert(100%)' : 'no',
|
||||
];
|
||||
}
|
||||
|
||||
return $variables;
|
||||
}
|
||||
|
||||
return [];
|
||||
|
||||
@@ -27,6 +27,7 @@ namespace OCA\Theming\Themes;
|
||||
|
||||
use OCA\Theming\ImageManager;
|
||||
use OCA\Theming\ITheme;
|
||||
use OCA\Theming\Service\BackgroundService;
|
||||
use OCA\Theming\ThemingDefaults;
|
||||
use OCA\Theming\Util;
|
||||
use OCP\App\IAppManager;
|
||||
@@ -69,6 +70,11 @@ class DefaultTheme implements ITheme {
|
||||
|
||||
$this->defaultPrimaryColor = $this->themingDefaults->getDefaultColorPrimary();
|
||||
$this->primaryColor = $this->themingDefaults->getColorPrimary();
|
||||
|
||||
// Override primary colors (if set) to improve accessibility
|
||||
if ($this->primaryColor === BackgroundService::DEFAULT_COLOR) {
|
||||
$this->primaryColor = BackgroundService::DEFAULT_ACCESSIBLE_COLOR;
|
||||
}
|
||||
}
|
||||
|
||||
public function getId(): string {
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
* @author Bjoern Schiessle <bjoern@schiessle.org>
|
||||
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
* @author Daniel Kesselberg <mail@danielkesselberg.de>
|
||||
* @author Ferdinand Thiessen <opensource@fthiessen.de>
|
||||
* @author Guillaume COMPAGNON <gcompagnon@outlook.com>
|
||||
* @author Jan-Christoph Borchardt <hey@jancborchardt.net>
|
||||
* @author Joachim Bauch <bauch@struktur.de>
|
||||
@@ -56,13 +55,22 @@ use OCP\IUserSession;
|
||||
|
||||
class ThemingDefaults extends \OC_Defaults {
|
||||
|
||||
private IConfig $config;
|
||||
private IL10N $l;
|
||||
private ImageManager $imageManager;
|
||||
private IUserSession $userSession;
|
||||
private IURLGenerator $urlGenerator;
|
||||
private ICacheFactory $cacheFactory;
|
||||
private Util $util;
|
||||
private IAppManager $appManager;
|
||||
private INavigationManager $navigationManager;
|
||||
|
||||
private string $name;
|
||||
private string $title;
|
||||
private string $entity;
|
||||
private string $productName;
|
||||
private string $url;
|
||||
private string $backgroundColor;
|
||||
private string $primaryColor;
|
||||
private string $color;
|
||||
private string $docBaseUrl;
|
||||
|
||||
private string $iTunesAppId;
|
||||
@@ -72,28 +80,43 @@ class ThemingDefaults extends \OC_Defaults {
|
||||
|
||||
/**
|
||||
* ThemingDefaults constructor.
|
||||
*
|
||||
* @param IConfig $config
|
||||
* @param IL10N $l
|
||||
* @param ImageManager $imageManager
|
||||
* @param IUserSession $userSession
|
||||
* @param IURLGenerator $urlGenerator
|
||||
* @param ICacheFactory $cacheFactory
|
||||
* @param Util $util
|
||||
* @param IAppManager $appManager
|
||||
*/
|
||||
public function __construct(
|
||||
private IConfig $config,
|
||||
private IL10N $l,
|
||||
private IUserSession $userSession,
|
||||
private IURLGenerator $urlGenerator,
|
||||
private ICacheFactory $cacheFactory,
|
||||
private Util $util,
|
||||
private ImageManager $imageManager,
|
||||
private IAppManager $appManager,
|
||||
private INavigationManager $navigationManager,
|
||||
private BackgroundService $backgroundService,
|
||||
public function __construct(IConfig $config,
|
||||
IL10N $l,
|
||||
IUserSession $userSession,
|
||||
IURLGenerator $urlGenerator,
|
||||
ICacheFactory $cacheFactory,
|
||||
Util $util,
|
||||
ImageManager $imageManager,
|
||||
IAppManager $appManager,
|
||||
INavigationManager $navigationManager
|
||||
) {
|
||||
parent::__construct();
|
||||
$this->config = $config;
|
||||
$this->l = $l;
|
||||
$this->imageManager = $imageManager;
|
||||
$this->userSession = $userSession;
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
$this->cacheFactory = $cacheFactory;
|
||||
$this->util = $util;
|
||||
$this->appManager = $appManager;
|
||||
$this->navigationManager = $navigationManager;
|
||||
|
||||
$this->name = parent::getName();
|
||||
$this->title = parent::getTitle();
|
||||
$this->entity = parent::getEntity();
|
||||
$this->productName = parent::getProductName();
|
||||
$this->url = parent::getBaseUrl();
|
||||
$this->primaryColor = parent::getColorPrimary();
|
||||
$this->backgroundColor = parent::getColorBackground();
|
||||
$this->color = parent::getColorPrimary();
|
||||
$this->iTunesAppId = parent::getiTunesAppId();
|
||||
$this->iOSClientUrl = parent::getiOSClientUrl();
|
||||
$this->AndroidClientUrl = parent::getAndroidClientUrl();
|
||||
@@ -201,8 +224,7 @@ class ThemingDefaults extends \OC_Defaults {
|
||||
}
|
||||
|
||||
/**
|
||||
* Color that is used for highlighting elements like important buttons
|
||||
* If user theming is enabled then the user defined value is returned
|
||||
* Color that is used for the header as well as for mail headers
|
||||
*/
|
||||
public function getColorPrimary(): string {
|
||||
$user = $this->userSession->getUser();
|
||||
@@ -216,66 +238,32 @@ class ThemingDefaults extends \OC_Defaults {
|
||||
|
||||
// user-defined primary color
|
||||
if (!empty($user)) {
|
||||
$userPrimaryColor = $this->config->getUserValue($user->getUID(), Application::APP_ID, 'primary_color', '');
|
||||
if (preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $userPrimaryColor)) {
|
||||
return $userPrimaryColor;
|
||||
$themingBackgroundColor = $this->config->getUserValue($user->getUID(), Application::APP_ID, 'background_color', '');
|
||||
// If the user selected a specific colour
|
||||
if (preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $themingBackgroundColor)) {
|
||||
return $themingBackgroundColor;
|
||||
}
|
||||
}
|
||||
|
||||
// If the default color is not valid, return the default background one
|
||||
if (!preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $defaultColor)) {
|
||||
return BackgroundService::DEFAULT_COLOR;
|
||||
}
|
||||
|
||||
// Finally, return the system global primary color
|
||||
return $defaultColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Color that is used for the page background (e.g. the header)
|
||||
* If user theming is enabled then the user defined value is returned
|
||||
*/
|
||||
public function getColorBackground(): string {
|
||||
$user = $this->userSession->getUser();
|
||||
|
||||
// admin-defined background color
|
||||
$defaultColor = $this->getDefaultColorBackground();
|
||||
|
||||
if ($this->isUserThemingDisabled()) {
|
||||
return $defaultColor;
|
||||
}
|
||||
|
||||
// user-defined background color
|
||||
if (!empty($user)) {
|
||||
$userPrimaryColor = $this->config->getUserValue($user->getUID(), Application::APP_ID, 'background_color', '');
|
||||
if (preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $userPrimaryColor)) {
|
||||
return $userPrimaryColor;
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, return the system global background color
|
||||
return $defaultColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the default primary color - only taking admin setting into account
|
||||
* Return the default color primary
|
||||
*/
|
||||
public function getDefaultColorPrimary(): string {
|
||||
// try admin color
|
||||
$defaultColor = $this->config->getAppValue(Application::APP_ID, 'primary_color', '');
|
||||
if (preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $defaultColor)) {
|
||||
return $defaultColor;
|
||||
$color = $this->config->getAppValue(Application::APP_ID, 'color', '');
|
||||
if (!preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $color)) {
|
||||
return BackgroundService::DEFAULT_COLOR;
|
||||
}
|
||||
|
||||
// fall back to default primary color
|
||||
return $this->primaryColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default background color only taking admin setting into account
|
||||
*/
|
||||
public function getDefaultColorBackground(): string {
|
||||
$defaultColor = $this->config->getAppValue(Application::APP_ID, 'background_color', '');
|
||||
if (preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $defaultColor)) {
|
||||
return $defaultColor;
|
||||
}
|
||||
|
||||
return $this->backgroundColor;
|
||||
return $color;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -356,7 +344,6 @@ class ThemingDefaults extends \OC_Defaults {
|
||||
|
||||
/**
|
||||
* @return array scss variables to overwrite
|
||||
* @deprecated since Nextcloud 22 - https://github.com/nextcloud/server/issues/9940
|
||||
*/
|
||||
public function getScssVariables() {
|
||||
$cacheBuster = $this->config->getAppValue('theming', 'cachebuster', '0');
|
||||
@@ -379,7 +366,7 @@ class ThemingDefaults extends \OC_Defaults {
|
||||
$variables['image-login-background'] = "url('".$this->imageManager->getImageUrl('background')."')";
|
||||
$variables['image-login-plain'] = 'false';
|
||||
|
||||
if ($this->config->getAppValue('theming', 'primary_color', '') !== '') {
|
||||
if ($this->config->getAppValue('theming', 'color', '') !== '') {
|
||||
$variables['color-primary'] = $this->getColorPrimary();
|
||||
$variables['color-primary-text'] = $this->getTextColorPrimary();
|
||||
$variables['color-primary-element'] = $this->util->elementColor($this->getColorPrimary());
|
||||
@@ -478,7 +465,7 @@ class ThemingDefaults extends \OC_Defaults {
|
||||
}
|
||||
|
||||
/**
|
||||
* Revert admin settings to the default value
|
||||
* Revert settings to the default value
|
||||
*
|
||||
* @param string $setting setting which should be reverted
|
||||
* @return string default value
|
||||
@@ -498,15 +485,8 @@ class ThemingDefaults extends \OC_Defaults {
|
||||
case 'slogan':
|
||||
$returnValue = $this->getSlogan();
|
||||
break;
|
||||
case 'primary_color':
|
||||
$returnValue = BackgroundService::DEFAULT_COLOR;
|
||||
break;
|
||||
case 'background_color':
|
||||
// If a background image is set we revert to the mean image color
|
||||
if ($this->imageManager->hasImage('background')) {
|
||||
$file = $this->imageManager->getImage('background');
|
||||
$returnValue = $this->backgroundService->setGlobalBackground($file->read()) ?? '';
|
||||
}
|
||||
case 'color':
|
||||
$returnValue = $this->getDefaultColorPrimary();
|
||||
break;
|
||||
case 'logo':
|
||||
case 'logoheader':
|
||||
@@ -521,16 +501,7 @@ class ThemingDefaults extends \OC_Defaults {
|
||||
}
|
||||
|
||||
/**
|
||||
* Color of text in the header menu
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTextColorBackground() {
|
||||
return $this->util->invertTextColor($this->getColorBackground()) ? '#000000' : '#ffffff';
|
||||
}
|
||||
|
||||
/**
|
||||
* Color of text on primary buttons and other elements
|
||||
* Color of text in the header and primary buttons
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
|
||||
@@ -93,7 +93,7 @@ class Util {
|
||||
$contrast = $this->colorContrast($color, $blurredBackground);
|
||||
|
||||
// Min. element contrast is 3:1 but we need to keep hover states in mind -> min 3.2:1
|
||||
$minContrast = $highContrast ? 5.6 : 3.2;
|
||||
$minContrast = $highContrast ? 5.5 : 3.2;
|
||||
|
||||
while ($contrast < $minContrast && $iteration++ < 100) {
|
||||
$hsl = Color::hexToHsl($color);
|
||||
|
||||
@@ -63,7 +63,6 @@
|
||||
"color-element-dark",
|
||||
"logo",
|
||||
"background",
|
||||
"background-text",
|
||||
"background-plain",
|
||||
"background-default",
|
||||
"logoheader",
|
||||
@@ -100,9 +99,6 @@
|
||||
"background": {
|
||||
"type": "string"
|
||||
},
|
||||
"background-text": {
|
||||
"type": "string"
|
||||
},
|
||||
"background-plain": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
@@ -44,50 +44,31 @@
|
||||
:placeholder="field.placeholder"
|
||||
:type="field.type"
|
||||
:value.sync="field.value"
|
||||
@update:theming="refreshStyles" />
|
||||
@update:theming="$emit('update:theming')" />
|
||||
|
||||
<!-- Primary color picker -->
|
||||
<ColorPickerField :name="primaryColorPickerField.name"
|
||||
:description="primaryColorPickerField.description"
|
||||
:default-value="primaryColorPickerField.defaultValue"
|
||||
:display-name="primaryColorPickerField.displayName"
|
||||
:value.sync="primaryColorPickerField.value"
|
||||
data-admin-theming-setting-primary-color
|
||||
@update:theming="refreshStyles" />
|
||||
|
||||
<!-- Background color picker -->
|
||||
<ColorPickerField name="background_color"
|
||||
:description="t('theming', 'Instead of a background image you can also configure a plain background color. If you use a background image changing this color will influence the color of the app menu icons.')"
|
||||
:default-value.sync="defaultBackgroundColor"
|
||||
:display-name="t('theming', 'Background color')"
|
||||
:value.sync="backgroundColor"
|
||||
data-admin-theming-setting-background-color
|
||||
@update:theming="refreshStyles" />
|
||||
<ColorPickerField :name="colorPickerField.name"
|
||||
:default-value="colorPickerField.defaultValue"
|
||||
:display-name="colorPickerField.displayName"
|
||||
:value.sync="colorPickerField.value"
|
||||
@update:theming="$emit('update:theming')" />
|
||||
|
||||
<!-- Default background picker -->
|
||||
<FileInputField :aria-label="t('theming', 'Upload new logo')"
|
||||
data-admin-theming-setting-file="logo"
|
||||
:display-name="t('theming', 'Logo')"
|
||||
mime-name="logoMime"
|
||||
:mime-value.sync="logoMime"
|
||||
name="logo"
|
||||
@update:theming="refreshStyles" />
|
||||
|
||||
<FileInputField :aria-label="t('theming', 'Upload new background and login image')"
|
||||
data-admin-theming-setting-file="background"
|
||||
:display-name="t('theming', 'Background and login image')"
|
||||
mime-name="backgroundMime"
|
||||
:mime-value.sync="backgroundMime"
|
||||
name="background"
|
||||
@uploaded="backgroundURL = $event"
|
||||
@update:theming="refreshStyles" />
|
||||
|
||||
<FileInputField v-for="field in fileInputFields"
|
||||
:key="field.name"
|
||||
:aria-label="field.ariaLabel"
|
||||
:data-admin-theming-setting-file="field.name"
|
||||
:default-mime-value="field.defaultMimeValue"
|
||||
:display-name="field.displayName"
|
||||
:mime-name="field.mimeName"
|
||||
:mime-value.sync="field.mimeValue"
|
||||
:name="field.name"
|
||||
@update:theming="$emit('update:theming')" />
|
||||
<div class="admin-theming__preview" data-admin-theming-preview>
|
||||
<div class="admin-theming__preview-logo" data-admin-theming-preview-logo />
|
||||
</div>
|
||||
</div>
|
||||
</NcSettingsSection>
|
||||
|
||||
<NcSettingsSection :name="t('theming', 'Advanced options')">
|
||||
<div class="admin-theming-advanced">
|
||||
<TextField v-for="field in advancedTextFields"
|
||||
@@ -99,7 +80,7 @@
|
||||
:display-name="field.displayName"
|
||||
:placeholder="field.placeholder"
|
||||
:maxlength="field.maxlength"
|
||||
@update:theming="refreshStyles" />
|
||||
@update:theming="$emit('update:theming')" />
|
||||
<FileInputField v-for="field in advancedFileInputFields"
|
||||
:key="field.name"
|
||||
:name="field.name"
|
||||
@@ -108,7 +89,7 @@
|
||||
:default-mime-value="field.defaultMimeValue"
|
||||
:display-name="field.displayName"
|
||||
:aria-label="field.ariaLabel"
|
||||
@update:theming="refreshStyles" />
|
||||
@update:theming="$emit('update:theming')" />
|
||||
<CheckboxField :name="userThemingField.name"
|
||||
:value="userThemingField.value"
|
||||
:default-value="userThemingField.defaultValue"
|
||||
@@ -116,7 +97,7 @@
|
||||
:label="userThemingField.label"
|
||||
:description="userThemingField.description"
|
||||
data-admin-theming-setting-disable-user-theming
|
||||
@update:theming="refreshStyles" />
|
||||
@update:theming="$emit('update:theming')" />
|
||||
<a v-if="!canThemeIcons"
|
||||
:href="docUrlIcons"
|
||||
rel="noreferrer noopener">
|
||||
@@ -130,7 +111,6 @@
|
||||
|
||||
<script>
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { refreshStyles } from './helpers/refreshStyles.js'
|
||||
|
||||
import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
|
||||
import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js'
|
||||
@@ -141,12 +121,9 @@ import TextField from './components/admin/TextField.vue'
|
||||
import AppMenuSection from './components/admin/AppMenuSection.vue'
|
||||
|
||||
const {
|
||||
defaultBackgroundURL,
|
||||
|
||||
backgroundMime,
|
||||
backgroundURL,
|
||||
backgroundColor,
|
||||
canThemeIcons,
|
||||
color,
|
||||
docUrl,
|
||||
docUrlIcons,
|
||||
faviconMime,
|
||||
@@ -156,7 +133,6 @@ const {
|
||||
logoMime,
|
||||
name,
|
||||
notThemableErrorMessage,
|
||||
primaryColor,
|
||||
privacyPolicyUrl,
|
||||
slogan,
|
||||
url,
|
||||
@@ -194,14 +170,32 @@ const textFields = [
|
||||
},
|
||||
]
|
||||
|
||||
const primaryColorPickerField = {
|
||||
name: 'primary_color',
|
||||
value: primaryColor,
|
||||
const colorPickerField = {
|
||||
name: 'color',
|
||||
value: color,
|
||||
defaultValue: '#0082c9',
|
||||
displayName: t('theming', 'Primary color'),
|
||||
description: t('theming', 'The primary color is used for highlighting elements like important buttons. It might get slightly adjusted depending on the current color schema.'),
|
||||
displayName: t('theming', 'Color'),
|
||||
}
|
||||
|
||||
const fileInputFields = [
|
||||
{
|
||||
name: 'logo',
|
||||
mimeName: 'logoMime',
|
||||
mimeValue: logoMime,
|
||||
defaultMimeValue: '',
|
||||
displayName: t('theming', 'Logo'),
|
||||
ariaLabel: t('theming', 'Upload new logo'),
|
||||
},
|
||||
{
|
||||
name: 'background',
|
||||
mimeName: 'backgroundMime',
|
||||
mimeValue: backgroundMime,
|
||||
defaultMimeValue: '',
|
||||
displayName: t('theming', 'Background and login image'),
|
||||
ariaLabel: t('theming', 'Upload new background and login image'),
|
||||
},
|
||||
]
|
||||
|
||||
const advancedTextFields = [
|
||||
{
|
||||
name: 'imprintUrl',
|
||||
@@ -264,17 +258,17 @@ export default {
|
||||
TextField,
|
||||
},
|
||||
|
||||
emits: [
|
||||
'update:theming',
|
||||
],
|
||||
|
||||
textFields,
|
||||
|
||||
data() {
|
||||
return {
|
||||
backgroundMime,
|
||||
backgroundURL,
|
||||
backgroundColor,
|
||||
defaultBackgroundColor: '#0069c3',
|
||||
|
||||
logoMime,
|
||||
|
||||
textFields,
|
||||
primaryColorPickerField,
|
||||
colorPickerField,
|
||||
fileInputFields,
|
||||
advancedTextFields,
|
||||
advancedFileInputFields,
|
||||
userThemingField,
|
||||
@@ -287,64 +281,6 @@ export default {
|
||||
notThemableErrorMessage,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
cssBackgroundImage() {
|
||||
if (this.backgroundURL) {
|
||||
return `url('${this.backgroundURL}')`
|
||||
}
|
||||
return 'unset'
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
backgroundMime() {
|
||||
if (this.backgroundMime === '') {
|
||||
// Reset URL to default value for preview
|
||||
this.backgroundURL = defaultBackgroundURL
|
||||
} else if (this.backgroundMime === 'backgroundColor') {
|
||||
// Reset URL to empty image when only color is configured
|
||||
this.backgroundURL = ''
|
||||
}
|
||||
},
|
||||
async backgroundURL() {
|
||||
// When the background is changed we need to emulate the background color change
|
||||
if (this.backgroundURL !== '') {
|
||||
const color = await this.calculateDefaultBackground()
|
||||
this.defaultBackgroundColor = color
|
||||
this.backgroundColor = color
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
if (this.backgroundURL) {
|
||||
this.defaultBackgroundColor = await this.calculateDefaultBackground()
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
refreshStyles,
|
||||
|
||||
/**
|
||||
* Same as on server - if a user uploads an image the mean color will be set as the background color
|
||||
*/
|
||||
calculateDefaultBackground() {
|
||||
const toHex = (num) => `00${num.toString(16)}`.slice(-2)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image()
|
||||
img.src = this.backgroundURL
|
||||
img.onload = () => {
|
||||
const context = document.createElement('canvas').getContext('2d')
|
||||
context.imageSmoothingEnabled = true
|
||||
context.drawImage(img, 0, 0, 1, 1)
|
||||
resolve('#' + [...context.getImageData(0, 0, 1, 1).data.slice(0, 3)].map(toHex).join(''))
|
||||
}
|
||||
img.onerror = reject
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -364,8 +300,15 @@ export default {
|
||||
background-position: center;
|
||||
text-align: center;
|
||||
margin-top: 10px;
|
||||
background-color: v-bind('backgroundColor');
|
||||
background-image: v-bind('cssBackgroundImage');
|
||||
/* This is basically https://github.com/nextcloud/server/blob/master/core/css/guest.css
|
||||
But without the user variables. That way the admin can preview the render as guest*/
|
||||
/* As guest, there is no user color color-background-plain */
|
||||
background-color: var(--color-primary-element-default);
|
||||
/* As guest, there is no user background (--image-background)
|
||||
1. Empty background if defined
|
||||
2. Else default background
|
||||
3. Finally default gradient (should not happened, the background is always defined anyway) */
|
||||
background-image: var(--image-background-plain, var(--image-background-default));
|
||||
|
||||
&-logo {
|
||||
width: 20%;
|
||||
|
||||
@@ -53,27 +53,20 @@
|
||||
</div>
|
||||
</NcSettingsSection>
|
||||
|
||||
<NcSettingsSection :name="t('theming', 'Primary color')"
|
||||
:description="isUserThemingDisabled
|
||||
? t('theming', 'Customization has been disabled by your administrator')
|
||||
: t('theming', 'Set a primary color to highlight important elements. The color used for elements such as primary buttons might differ a bit as it gets adjusted to fulfill accessibility requirements.')">
|
||||
<UserPrimaryColor v-if="!isUserThemingDisabled"
|
||||
ref="primaryColor"
|
||||
@refresh-styles="refreshGlobalStyles" />
|
||||
<NcSettingsSection :name="t('theming', 'Background and color')"
|
||||
class="background"
|
||||
data-user-theming-background-disabled>
|
||||
<template v-if="isUserThemingDisabled">
|
||||
<p>{{ t('theming', 'Customization has been disabled by your administrator') }}</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p>{{ t('theming', 'The background can be set to an image from the default set, a custom uploaded image, or a plain color. The primary color will automatically be adapted based on this and used for elements like folder icons, primary buttons and highlights.') }}</p>
|
||||
<BackgroundSettings class="background__grid" @update:background="refreshGlobalStyles" />
|
||||
</template>
|
||||
</NcSettingsSection>
|
||||
|
||||
<NcSettingsSection class="background"
|
||||
:name="t('theming', 'Background and color')"
|
||||
:description="isUserThemingDisabled
|
||||
? t('theming', 'Customization has been disabled by your administrator')
|
||||
: t('theming', 'The background can be set to an image from the default set, a custom uploaded image, or a plain color.')">
|
||||
<BackgroundSettings v-if="!isUserThemingDisabled"
|
||||
class="background__grid"
|
||||
@update:background="refreshGlobalStyles" />
|
||||
</NcSettingsSection>
|
||||
|
||||
<NcSettingsSection :name="t('theming', 'Keyboard shortcuts')"
|
||||
:description="t('theming', 'In some cases keyboard shortcuts can interfere with accessibility tools. In order to allow focusing on your tool correctly you can disable all keyboard shortcuts here. This will also disable all available shortcuts in apps.')">
|
||||
<NcSettingsSection :name="t('theming', 'Keyboard shortcuts')">
|
||||
<p>{{ t('theming', 'In some cases keyboard shortcuts can interfere with accessibility tools. In order to allow focusing on your tool correctly you can disable all keyboard shortcuts here. This will also disable all available shortcuts in apps.') }}</p>
|
||||
<NcCheckboxRadioSwitch class="theming__preview-toggle"
|
||||
:checked.sync="shortcutsDisabled"
|
||||
type="switch"
|
||||
@@ -89,17 +82,13 @@
|
||||
<script>
|
||||
import { generateOcsUrl } from '@nextcloud/router'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { refreshStyles } from './helpers/refreshStyles'
|
||||
|
||||
import axios from '@nextcloud/axios'
|
||||
|
||||
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
|
||||
import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js'
|
||||
|
||||
import BackgroundSettings from './components/BackgroundSettings.vue'
|
||||
import ItemPreview from './components/ItemPreview.vue'
|
||||
import UserAppMenuSection from './components/UserAppMenuSection.vue'
|
||||
import UserPrimaryColor from './components/UserPrimaryColor.vue'
|
||||
|
||||
const availableThemes = loadState('theming', 'themes', [])
|
||||
const enforceTheme = loadState('theming', 'enforceTheme', '')
|
||||
@@ -108,7 +97,7 @@ const shortcutsDisabled = loadState('theming', 'shortcutsDisabled', false)
|
||||
const isUserThemingDisabled = loadState('theming', 'isUserThemingDisabled')
|
||||
|
||||
export default {
|
||||
name: 'UserTheming',
|
||||
name: 'UserThemes',
|
||||
|
||||
components: {
|
||||
ItemPreview,
|
||||
@@ -116,7 +105,6 @@ export default {
|
||||
NcSettingsSection,
|
||||
BackgroundSettings,
|
||||
UserAppMenuSection,
|
||||
UserPrimaryColor,
|
||||
},
|
||||
|
||||
data() {
|
||||
@@ -185,9 +173,20 @@ export default {
|
||||
|
||||
methods: {
|
||||
// Refresh server-side generated theming CSS
|
||||
async refreshGlobalStyles() {
|
||||
await refreshStyles()
|
||||
this.$nextTick(() => this.$refs.primaryColor.reload())
|
||||
refreshGlobalStyles() {
|
||||
[...document.head.querySelectorAll('link.theme')].forEach(theme => {
|
||||
const url = new URL(theme.href)
|
||||
url.searchParams.set('v', Date.now())
|
||||
const newTheme = theme.cloneNode()
|
||||
newTheme.href = url.toString()
|
||||
newTheme.onload = () => theme.remove()
|
||||
document.head.append(newTheme)
|
||||
})
|
||||
},
|
||||
|
||||
updateBackground(data) {
|
||||
this.background = (data.type === 'custom' || data.type === 'default') ? data.type : data.value
|
||||
this.refreshGlobalStyles()
|
||||
},
|
||||
|
||||
changeTheme({ enabled, id }) {
|
||||
@@ -22,6 +22,7 @@
|
||||
import { getRequestToken } from '@nextcloud/auth'
|
||||
import Vue from 'vue'
|
||||
|
||||
import { refreshStyles } from './helpers/refreshStyles.js'
|
||||
import App from './AdminTheming.vue'
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
@@ -33,3 +34,4 @@ Vue.prototype.t = t
|
||||
const View = Vue.extend(App)
|
||||
const theming = new View()
|
||||
theming.$mount('#admin-theming')
|
||||
theming.$on('update:theming', refreshStyles)
|
||||
|
||||
@@ -32,34 +32,15 @@
|
||||
'background background__filepicker': true,
|
||||
'background--active': backgroundImage === 'custom'
|
||||
}"
|
||||
:data-color-bright="invertTextColor(Theming.color)"
|
||||
data-user-theming-background-custom
|
||||
tabindex="0"
|
||||
@click="pickFile">
|
||||
{{ t('theming', 'Custom background') }}
|
||||
<ImageEdit v-if="backgroundImage !== 'custom'" :size="20" />
|
||||
<ImageEdit v-if="backgroundImage !== 'custom'" :size="26" />
|
||||
<Check :size="44" />
|
||||
</button>
|
||||
|
||||
<!-- Custom color picker -->
|
||||
<NcColorPicker v-model="Theming.backgroundColor" @update:value="debouncePickColor">
|
||||
<button :class="{
|
||||
'icon-loading': loading === 'color',
|
||||
'background background__color': true,
|
||||
'background--active': backgroundImage === 'color'
|
||||
}"
|
||||
:aria-pressed="backgroundImage === 'color'"
|
||||
:data-color="Theming.backgroundColor"
|
||||
:data-color-bright="invertTextColor(Theming.backgroundColor)"
|
||||
:style="{ backgroundColor: Theming.backgroundColor, '--border-color': Theming.backgroundColor}"
|
||||
data-user-theming-background-color
|
||||
tabindex="0"
|
||||
@click="backgroundImage !== 'color' && debouncePickColor(Theming.backgroundColor)">
|
||||
{{ t('theming', 'Plain background') /* TRANSLATORS: Background using a single color */ }}
|
||||
<ColorPalette v-if="backgroundImage !== 'color'" :size="20" />
|
||||
<Check :size="44" />
|
||||
</button>
|
||||
</NcColorPicker>
|
||||
|
||||
<!-- Default background -->
|
||||
<button :aria-pressed="backgroundImage === 'default'"
|
||||
:class="{
|
||||
@@ -67,8 +48,8 @@
|
||||
'background background__default': true,
|
||||
'background--active': backgroundImage === 'default'
|
||||
}"
|
||||
:data-color-bright="invertTextColor(Theming.defaultBackgroundColor)"
|
||||
:style="{ '--border-color': Theming.defaultBackgroundColor }"
|
||||
:data-color-bright="invertTextColor(Theming.defaultColor)"
|
||||
:style="{ '--border-color': Theming.defaultColor }"
|
||||
data-user-theming-background-default
|
||||
tabindex="0"
|
||||
@click="setDefault">
|
||||
@@ -76,6 +57,31 @@
|
||||
<Check :size="44" />
|
||||
</button>
|
||||
|
||||
<!-- Custom color picker -->
|
||||
<div class="background-color"
|
||||
data-user-theming-background-color>
|
||||
<NcColorPicker v-model="Theming.color"
|
||||
@input="debouncePickColor">
|
||||
<NcButton type="ternary">
|
||||
{{ t('theming', 'Change color') }}
|
||||
</NcButton>
|
||||
</NcColorPicker>
|
||||
</div>
|
||||
|
||||
<!-- Remove background -->
|
||||
<button :aria-pressed="isBackgroundDisabled"
|
||||
:class="{
|
||||
'background background__delete': true,
|
||||
'background--active': isBackgroundDisabled
|
||||
}"
|
||||
data-user-theming-background-clear
|
||||
tabindex="0"
|
||||
@click="removeBackground">
|
||||
{{ t('theming', 'No background') }}
|
||||
<Close v-if="!isBackgroundDisabled" :size="32" />
|
||||
<Check :size="44" />
|
||||
</button>
|
||||
|
||||
<!-- Background set selection -->
|
||||
<button v-for="shippedBackground in shippedBackgrounds"
|
||||
:key="shippedBackground.name"
|
||||
@@ -87,7 +93,7 @@
|
||||
'icon-loading': loading === shippedBackground.name,
|
||||
'background--active': backgroundImage === shippedBackground.name
|
||||
}"
|
||||
:data-color-bright="invertTextColor(shippedBackground.details.background_color)"
|
||||
:data-color-bright="shippedBackground.details.theming === 'dark'"
|
||||
:data-user-theming-background-shipped="shippedBackground.name"
|
||||
:style="{ backgroundImage: 'url(' + shippedBackground.preview + ')', '--border-color': shippedBackground.details.primary_color }"
|
||||
tabindex="0"
|
||||
@@ -106,20 +112,17 @@ import { Palette } from 'node-vibrant/lib/color.js'
|
||||
import axios from '@nextcloud/axios'
|
||||
import debounce from 'debounce'
|
||||
import NcColorPicker from '@nextcloud/vue/dist/Components/NcColorPicker.js'
|
||||
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
|
||||
import Vibrant from 'node-vibrant'
|
||||
|
||||
import Check from 'vue-material-design-icons/Check.vue'
|
||||
import Close from 'vue-material-design-icons/Close.vue'
|
||||
import ImageEdit from 'vue-material-design-icons/ImageEdit.vue'
|
||||
import ColorPalette from 'vue-material-design-icons/Palette.vue'
|
||||
|
||||
const backgroundImage = loadState('theming', 'backgroundImage')
|
||||
const shippedBackgroundList = loadState('theming', 'shippedBackgrounds')
|
||||
const backgroundImage = loadState('theming', 'userBackgroundImage')
|
||||
const {
|
||||
backgroundImage: defaultBackgroundImage,
|
||||
backgroundColor: defaultBackgroundColor,
|
||||
backgroundMime: defaultBackgroundMime,
|
||||
defaultShippedBackground,
|
||||
} = loadState('theming', 'themingDefaults')
|
||||
const themingDefaultBackground = loadState('theming', 'themingDefaultBackground')
|
||||
const defaultShippedBackground = loadState('theming', 'defaultShippedBackground')
|
||||
|
||||
const prefixWithBaseUrl = (url) => generateFilePath('theming', '', 'img/background/') + url
|
||||
|
||||
@@ -128,8 +131,9 @@ export default {
|
||||
|
||||
components: {
|
||||
Check,
|
||||
ColorPalette,
|
||||
Close,
|
||||
ImageEdit,
|
||||
NcButton,
|
||||
NcColorPicker,
|
||||
},
|
||||
|
||||
@@ -146,12 +150,7 @@ export default {
|
||||
computed: {
|
||||
shippedBackgrounds() {
|
||||
return Object.keys(shippedBackgroundList)
|
||||
.filter((background) => {
|
||||
// If the admin did not changed the global background
|
||||
// let's hide the default background to not show it twice
|
||||
return background !== defaultShippedBackground || !this.isGlobalBackgroundDefault
|
||||
})
|
||||
.map((fileName) => {
|
||||
.map(fileName => {
|
||||
return {
|
||||
name: fileName,
|
||||
url: prefixWithBaseUrl(fileName),
|
||||
@@ -159,18 +158,27 @@ export default {
|
||||
details: shippedBackgroundList[fileName],
|
||||
}
|
||||
})
|
||||
.filter(background => {
|
||||
// If the admin did not changed the global background
|
||||
// let's hide the default background to not show it twice
|
||||
if (!this.isGlobalBackgroundDeleted && !this.isGlobalBackgroundDefault) {
|
||||
return background.name !== defaultShippedBackground
|
||||
}
|
||||
return true
|
||||
})
|
||||
},
|
||||
|
||||
isGlobalBackgroundDefault() {
|
||||
return defaultBackgroundMime === ''
|
||||
return !!themingDefaultBackground
|
||||
},
|
||||
|
||||
isGlobalBackgroundDeleted() {
|
||||
return defaultBackgroundMime === 'backgroundColor'
|
||||
return themingDefaultBackground === 'backgroundColor'
|
||||
},
|
||||
|
||||
cssDefaultBackgroundImage() {
|
||||
return `url('${defaultBackgroundImage}')`
|
||||
isBackgroundDisabled() {
|
||||
return this.backgroundImage === 'disabled'
|
||||
|| !this.backgroundImage
|
||||
},
|
||||
},
|
||||
|
||||
@@ -218,7 +226,7 @@ export default {
|
||||
async update(data) {
|
||||
// Update state
|
||||
this.backgroundImage = data.backgroundImage
|
||||
this.Theming.backgroundColor = data.backgroundColor
|
||||
this.Theming.color = data.backgroundColor
|
||||
|
||||
// Notify parent and reload style
|
||||
this.$emit('update:background')
|
||||
@@ -249,12 +257,12 @@ export default {
|
||||
this.update(result.data)
|
||||
},
|
||||
|
||||
async pickColor(color) {
|
||||
async pickColor(event) {
|
||||
this.loading = 'color'
|
||||
const { data } = await axios.post(generateUrl('/apps/theming/background/color'), { color: color || '#0082c9' })
|
||||
this.update(data)
|
||||
const color = event?.target?.dataset?.color || this.Theming?.color || '#0082c9'
|
||||
const result = await axios.post(generateUrl('/apps/theming/background/color'), { color })
|
||||
this.update(result.data)
|
||||
},
|
||||
|
||||
debouncePickColor: debounce(function(...args) {
|
||||
this.pickColor(...args)
|
||||
}, 200),
|
||||
@@ -351,26 +359,21 @@ export default {
|
||||
height: 96px;
|
||||
margin: 8px;
|
||||
text-align: center;
|
||||
word-wrap: break-word;
|
||||
hyphens: auto;
|
||||
border: 2px solid var(--color-main-background);
|
||||
border-radius: var(--border-radius-large);
|
||||
background-position: center center;
|
||||
background-size: cover;
|
||||
|
||||
&__filepicker {
|
||||
background-color: var(--color-main-text);
|
||||
background-color: var(--color-background-dark);
|
||||
|
||||
&.background--active {
|
||||
color: var(--color-background-plain-text);
|
||||
color: white;
|
||||
background-image: var(--image-background);
|
||||
}
|
||||
}
|
||||
|
||||
&__default {
|
||||
background-color: var(--color-background-plain);
|
||||
background-image: linear-gradient(to bottom, rgba(23, 23, 23, 0.5), rgba(23, 23, 23, 0.5)), v-bind(cssDefaultBackgroundImage);
|
||||
background-color: var(--color-primary-default);
|
||||
background-image: linear-gradient(to bottom, rgba(23, 23, 23, 0.5), rgba(23, 23, 23, 0.5)), var(--image-background-plain, var(--image-background-default));
|
||||
}
|
||||
|
||||
&__filepicker, &__default, &__color {
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
<template>
|
||||
<div class="primary-color__wrapper">
|
||||
<NcColorPicker v-model="primaryColor"
|
||||
data-user-theming-primary-color
|
||||
@update:value="debouncedOnUpdate">
|
||||
<button ref="trigger"
|
||||
class="color-container primary-color__trigger"
|
||||
:style="{ 'background-color': primaryColor }"
|
||||
data-user-theming-primary-color-trigger>
|
||||
{{ t('theming', 'Primary color') }}
|
||||
<NcLoadingIcon v-if="loading" />
|
||||
<IconColorPalette v-else :size="20" />
|
||||
</button>
|
||||
</NcColorPicker>
|
||||
<NcButton type="tertiary" :disabled="isdefaultPrimaryColor" @click="onReset">
|
||||
<template #icon>
|
||||
<IconUndo :size="20" />
|
||||
</template>
|
||||
{{ t('theming', 'Reset primary color') }}
|
||||
</NcButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { generateOcsUrl } from '@nextcloud/router'
|
||||
import { colord } from 'colord'
|
||||
import { debounce } from 'debounce'
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
import axios from '@nextcloud/axios'
|
||||
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
|
||||
import NcColorPicker from '@nextcloud/vue/dist/Components/NcColorPicker.js'
|
||||
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
|
||||
import IconColorPalette from 'vue-material-design-icons/Palette.vue'
|
||||
import IconUndo from 'vue-material-design-icons/UndoVariant.vue'
|
||||
|
||||
const { primaryColor, defaultPrimaryColor } = loadState('theming', 'data', { primaryColor: '#0082c9', defaultPrimaryColor: '#0082c9' })
|
||||
|
||||
export default defineComponent({
|
||||
name: 'UserPrimaryColor',
|
||||
|
||||
components: {
|
||||
IconColorPalette,
|
||||
IconUndo,
|
||||
NcButton,
|
||||
NcColorPicker,
|
||||
NcLoadingIcon,
|
||||
},
|
||||
|
||||
emits: ['refresh-styles'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
primaryColor,
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
isdefaultPrimaryColor() {
|
||||
return colord(this.primaryColor).isEqual(colord(defaultPrimaryColor))
|
||||
},
|
||||
|
||||
debouncedOnUpdate() {
|
||||
return debounce(this.onUpdate, 500)
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
t,
|
||||
|
||||
/**
|
||||
* Global styles are reloaded so we might need to update the current value
|
||||
*/
|
||||
reload() {
|
||||
const trigger = this.$refs.trigger as HTMLButtonElement
|
||||
const newColor = window.getComputedStyle(trigger).backgroundColor
|
||||
if (newColor.toLowerCase() !== this.primaryColor) {
|
||||
this.primaryColor = newColor
|
||||
}
|
||||
},
|
||||
|
||||
onReset() {
|
||||
this.primaryColor = defaultPrimaryColor
|
||||
this.onUpdate(null)
|
||||
},
|
||||
|
||||
async onUpdate(value: string | null) {
|
||||
this.loading = true
|
||||
const url = generateOcsUrl('apps/provisioning_api/api/v1/config/users/{appId}/{configKey}', {
|
||||
appId: 'theming',
|
||||
configKey: 'primary_color',
|
||||
})
|
||||
try {
|
||||
if (value) {
|
||||
await axios.post(url, {
|
||||
configValue: value,
|
||||
})
|
||||
} else {
|
||||
await axios.delete(url)
|
||||
}
|
||||
this.$emit('refresh-styles')
|
||||
} catch (e) {
|
||||
console.error('Could not update primary color', e)
|
||||
showError(t('theming', 'Could not set primary color'))
|
||||
}
|
||||
this.loading = false
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.primary-color {
|
||||
&__wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
&__trigger {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
|
||||
background-color: var(--color-primary);
|
||||
color: var(--color-primary-text);
|
||||
width: 350px;
|
||||
max-width: 100vw;
|
||||
height: 96px;
|
||||
|
||||
word-wrap: break-word;
|
||||
hyphens: auto;
|
||||
|
||||
border: 2px solid var(--color-main-background);
|
||||
border-radius: var(--border-radius-large);
|
||||
|
||||
&:active {
|
||||
background-color: var(--color-primary-hover) !important;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:focus-visible {
|
||||
border-color: var(--color-main-background) !important;
|
||||
outline: 2px solid var(--color-main-text) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -26,35 +26,27 @@
|
||||
<div class="field__row">
|
||||
<NcColorPicker :value.sync="localValue"
|
||||
:advanced-fields="true"
|
||||
data-admin-theming-setting-primary-color-picker
|
||||
@update:value="debounceSave">
|
||||
<NcButton :id="id"
|
||||
class="field__button"
|
||||
type="primary"
|
||||
:aria-label="t('theming', 'Select a custom color')"
|
||||
data-admin-theming-setting-color-picker>
|
||||
<NcButton type="secondary"
|
||||
:id="id">
|
||||
<template #icon>
|
||||
<NcLoadingIcon v-if="loading"
|
||||
:appearance="calculatedTextColor === '#ffffff' ? 'light' : 'dark'"
|
||||
:size="20" />
|
||||
<Palette v-else :size="20" />
|
||||
<Palette :size="20" />
|
||||
</template>
|
||||
{{ value }}
|
||||
{{ t('theming', 'Change color') }}
|
||||
</NcButton>
|
||||
</NcColorPicker>
|
||||
<div class="field__color-preview" data-admin-theming-setting-color />
|
||||
<div class="field__color-preview" data-admin-theming-setting-primary-color />
|
||||
<NcButton v-if="value !== defaultValue"
|
||||
type="tertiary"
|
||||
:aria-label="t('theming', 'Reset to default')"
|
||||
data-admin-theming-setting-color-reset
|
||||
data-admin-theming-setting-primary-color-reset
|
||||
@click="undo">
|
||||
<template #icon>
|
||||
<Undo :size="20" />
|
||||
</template>
|
||||
</NcButton>
|
||||
</div>
|
||||
<div v-if="description" class="description">
|
||||
{{ description }}
|
||||
</div>
|
||||
|
||||
<NcNoteCard v-if="errorMessage"
|
||||
type="error"
|
||||
@@ -66,10 +58,8 @@
|
||||
|
||||
<script>
|
||||
import { debounce } from 'debounce'
|
||||
import { colord } from 'colord'
|
||||
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
|
||||
import NcColorPicker from '@nextcloud/vue/dist/Components/NcColorPicker.js'
|
||||
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
|
||||
import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
|
||||
import Undo from 'vue-material-design-icons/UndoVariant.vue'
|
||||
import Palette from 'vue-material-design-icons/Palette.vue'
|
||||
@@ -82,7 +72,6 @@ export default {
|
||||
components: {
|
||||
NcButton,
|
||||
NcColorPicker,
|
||||
NcLoadingIcon,
|
||||
NcNoteCard,
|
||||
Undo,
|
||||
Palette,
|
||||
@@ -97,18 +86,10 @@ export default {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
textColor: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
defaultValue: {
|
||||
type: String,
|
||||
required: true,
|
||||
@@ -119,33 +100,9 @@ export default {
|
||||
},
|
||||
},
|
||||
|
||||
emits: ['update:theming'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
calculatedTextColor() {
|
||||
const color = colord(this.value)
|
||||
return color.isLight() ? '#000000' : '#ffffff'
|
||||
},
|
||||
usedTextColor() {
|
||||
if (this.textColor) {
|
||||
return this.textColor
|
||||
}
|
||||
return this.calculatedTextColor
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
debounceSave: debounce(async function() {
|
||||
this.loading = true
|
||||
await this.save()
|
||||
this.$emit('update:theming')
|
||||
this.loading = false
|
||||
}, 200),
|
||||
},
|
||||
}
|
||||
@@ -153,20 +110,12 @@ export default {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import './shared/field.scss';
|
||||
.description {
|
||||
color: var(--color-text-maxcontrast);
|
||||
}
|
||||
|
||||
.field {
|
||||
&__button {
|
||||
background-color: v-bind('value') !important;
|
||||
color: v-bind('usedTextColor') !important;
|
||||
}
|
||||
|
||||
&__color-preview {
|
||||
width: var(--default-clickable-area);
|
||||
border-radius: var(--border-radius-large);
|
||||
background-color: v-bind('value');
|
||||
background-color: var(--color-primary-default);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -126,7 +126,7 @@ export default {
|
||||
},
|
||||
defaultMimeValue: {
|
||||
type: String,
|
||||
default: '',
|
||||
required: true,
|
||||
},
|
||||
displayName: {
|
||||
type: String,
|
||||
@@ -182,10 +182,9 @@ export default {
|
||||
const url = generateUrl('/apps/theming/ajax/uploadImage')
|
||||
try {
|
||||
this.showLoading = true
|
||||
const { data } = await axios.post(url, formData)
|
||||
await axios.post(url, formData)
|
||||
this.showLoading = false
|
||||
this.$emit('update:mime-value', file.type)
|
||||
this.$emit('uploaded', data.data.url)
|
||||
this.handleSuccess()
|
||||
} catch (e) {
|
||||
this.showLoading = false
|
||||
|
||||
@@ -20,24 +20,14 @@
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Refresh server-side generated theming CSS
|
||||
* This resolves when all themes are reloaded
|
||||
*/
|
||||
export async function refreshStyles() {
|
||||
const themes = [...document.head.querySelectorAll('link.theme')]
|
||||
const promises = themes.map((theme) => new Promise((resolve) => {
|
||||
export const refreshStyles = () => {
|
||||
// Refresh server-side generated theming CSS
|
||||
[...document.head.querySelectorAll('link.theme')].forEach(theme => {
|
||||
const url = new URL(theme.href)
|
||||
url.searchParams.set('v', Date.now())
|
||||
const newTheme = theme.cloneNode()
|
||||
newTheme.href = url.toString()
|
||||
newTheme.onload = () => {
|
||||
theme.remove()
|
||||
resolve()
|
||||
}
|
||||
newTheme.onload = () => theme.remove()
|
||||
document.head.append(newTheme)
|
||||
}))
|
||||
|
||||
// Wait until all themes are loaded
|
||||
await Promise.allSettled(promises)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -64,14 +64,10 @@ export default {
|
||||
this.reset()
|
||||
const url = generateUrl('/apps/theming/ajax/undoChanges')
|
||||
try {
|
||||
const { data } = await axios.post(url, {
|
||||
await axios.post(url, {
|
||||
setting: this.name,
|
||||
})
|
||||
|
||||
if (data.data.value) {
|
||||
this.$emit('update:defaultValue', data.data.value)
|
||||
}
|
||||
this.$emit('update:value', data.data.value || this.defaultValue)
|
||||
this.$emit('update:value', this.defaultValue)
|
||||
this.handleSuccess()
|
||||
} catch (e) {
|
||||
this.errorMessage = e.response.data.data?.message
|
||||
|
||||
@@ -23,7 +23,7 @@ import { getRequestToken } from '@nextcloud/auth'
|
||||
import Vue from 'vue'
|
||||
|
||||
import { refreshStyles } from './helpers/refreshStyles.js'
|
||||
import App from './UserTheming.vue'
|
||||
import App from './UserThemes.vue'
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
__webpack_nonce__ = btoa(getRequestToken())
|
||||
|
||||
@@ -37,7 +37,6 @@ use OCP\Files\IAppData;
|
||||
use OCP\IConfig;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\IUserSession;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Test\TestCase;
|
||||
|
||||
/**
|
||||
@@ -46,16 +45,16 @@ use Test\TestCase;
|
||||
* @package OCA\Theming\Tests
|
||||
*/
|
||||
class CapabilitiesTest extends TestCase {
|
||||
/** @var ThemingDefaults|MockObject */
|
||||
/** @var ThemingDefaults|\PHPUnit\Framework\MockObject\MockObject */
|
||||
protected $theming;
|
||||
|
||||
/** @var IURLGenerator|MockObject */
|
||||
/** @var IURLGenerator|\PHPUnit\Framework\MockObject\MockObject */
|
||||
protected $url;
|
||||
|
||||
/** @var IConfig|MockObject */
|
||||
/** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */
|
||||
protected $config;
|
||||
|
||||
/** @var Util|MockObject */
|
||||
/** @var Util|\PHPUnit\Framework\MockObject\MockObject */
|
||||
protected $util;
|
||||
|
||||
protected IUserSession $userSession;
|
||||
@@ -67,22 +66,16 @@ class CapabilitiesTest extends TestCase {
|
||||
parent::setUp();
|
||||
|
||||
$this->theming = $this->createMock(ThemingDefaults::class);
|
||||
$this->url = $this->createMock(IURLGenerator::class);
|
||||
$this->url = $this->getMockBuilder(IURLGenerator::class)->getMock();
|
||||
$this->config = $this->createMock(IConfig::class);
|
||||
$this->util = $this->createMock(Util::class);
|
||||
$this->userSession = $this->createMock(IUserSession::class);
|
||||
$this->capabilities = new Capabilities(
|
||||
$this->theming,
|
||||
$this->util,
|
||||
$this->url,
|
||||
$this->config,
|
||||
$this->userSession,
|
||||
);
|
||||
$this->capabilities = new Capabilities($this->theming, $this->util, $this->url, $this->config, $this->userSession);
|
||||
}
|
||||
|
||||
public function dataGetCapabilities() {
|
||||
return [
|
||||
['name', 'url', 'slogan', '#FFFFFF', '#000000', 'logo', 'background', '#fff', '#000', 'http://absolute/', true, [
|
||||
['name', 'url', 'slogan', '#FFFFFF', '#000000', 'logo', 'background', 'http://absolute/', true, [
|
||||
'name' => 'name',
|
||||
'url' => 'url',
|
||||
'slogan' => 'slogan',
|
||||
@@ -93,13 +86,12 @@ class CapabilitiesTest extends TestCase {
|
||||
'color-element-dark' => '#FFFFFF',
|
||||
'logo' => 'http://absolute/logo',
|
||||
'background' => 'http://absolute/background',
|
||||
'background-text' => '#000',
|
||||
'background-plain' => false,
|
||||
'background-default' => false,
|
||||
'logoheader' => 'http://absolute/logo',
|
||||
'favicon' => 'http://absolute/logo',
|
||||
]],
|
||||
['name1', 'url2', 'slogan3', '#01e4a0', '#ffffff', 'logo5', 'background6', '#fff', '#000', 'http://localhost/', false, [
|
||||
['name1', 'url2', 'slogan3', '#01e4a0', '#ffffff', 'logo5', 'background6', 'http://localhost/', false, [
|
||||
'name' => 'name1',
|
||||
'url' => 'url2',
|
||||
'slogan' => 'slogan3',
|
||||
@@ -110,13 +102,12 @@ class CapabilitiesTest extends TestCase {
|
||||
'color-element-dark' => '#01e4a0',
|
||||
'logo' => 'http://localhost/logo5',
|
||||
'background' => 'http://localhost/background6',
|
||||
'background-text' => '#000',
|
||||
'background-plain' => false,
|
||||
'background-default' => true,
|
||||
'logoheader' => 'http://localhost/logo5',
|
||||
'favicon' => 'http://localhost/logo5',
|
||||
]],
|
||||
['name1', 'url2', 'slogan3', '#000000', '#ffffff', 'logo5', 'backgroundColor', '#000000', '#ffffff', 'http://localhost/', true, [
|
||||
['name1', 'url2', 'slogan3', '#000000', '#ffffff', 'logo5', 'backgroundColor', 'http://localhost/', true, [
|
||||
'name' => 'name1',
|
||||
'url' => 'url2',
|
||||
'slogan' => 'slogan3',
|
||||
@@ -127,13 +118,12 @@ class CapabilitiesTest extends TestCase {
|
||||
'color-element-dark' => '#4d4d4d',
|
||||
'logo' => 'http://localhost/logo5',
|
||||
'background' => '#000000',
|
||||
'background-text' => '#ffffff',
|
||||
'background-plain' => true,
|
||||
'background-default' => false,
|
||||
'logoheader' => 'http://localhost/logo5',
|
||||
'favicon' => 'http://localhost/logo5',
|
||||
]],
|
||||
['name1', 'url2', 'slogan3', '#000000', '#ffffff', 'logo5', 'backgroundColor', '#000000', '#ffffff', 'http://localhost/', false, [
|
||||
['name1', 'url2', 'slogan3', '#000000', '#ffffff', 'logo5', 'backgroundColor', 'http://localhost/', false, [
|
||||
'name' => 'name1',
|
||||
'url' => 'url2',
|
||||
'slogan' => 'slogan3',
|
||||
@@ -144,7 +134,6 @@ class CapabilitiesTest extends TestCase {
|
||||
'color-element-dark' => '#4d4d4d',
|
||||
'logo' => 'http://localhost/logo5',
|
||||
'background' => '#000000',
|
||||
'background-text' => '#ffffff',
|
||||
'background-plain' => true,
|
||||
'background-default' => true,
|
||||
'logoheader' => 'http://localhost/logo5',
|
||||
@@ -166,7 +155,7 @@ class CapabilitiesTest extends TestCase {
|
||||
* @param bool $backgroundThemed
|
||||
* @param string[] $expected
|
||||
*/
|
||||
public function testGetCapabilities($name, $url, $slogan, $color, $textColor, $logo, $background, $backgroundColor, $backgroundTextColor, $baseUrl, $backgroundThemed, array $expected) {
|
||||
public function testGetCapabilities($name, $url, $slogan, $color, $textColor, $logo, $background, $baseUrl, $backgroundThemed, array $expected) {
|
||||
$this->config->expects($this->once())
|
||||
->method('getAppValue')
|
||||
->willReturn($background);
|
||||
@@ -179,12 +168,6 @@ class CapabilitiesTest extends TestCase {
|
||||
$this->theming->expects($this->once())
|
||||
->method('getSlogan')
|
||||
->willReturn($slogan);
|
||||
$this->theming->expects($this->once())
|
||||
->method('getColorBackground')
|
||||
->willReturn($backgroundColor);
|
||||
$this->theming->expects($this->once())
|
||||
->method('getTextColorBackground')
|
||||
->willReturn($backgroundTextColor);
|
||||
$this->theming->expects($this->atLeast(1))
|
||||
->method('getDefaultColorPrimary')
|
||||
->willReturn($color);
|
||||
|
||||
@@ -42,11 +42,13 @@ use OCP\App\IAppManager;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\AppFramework\Http\DataResponse;
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
use OCP\Files\IAppData;
|
||||
use OCP\Files\NotFoundException;
|
||||
use OCP\Files\SimpleFS\ISimpleFile;
|
||||
use OCP\IConfig;
|
||||
use OCP\IL10N;
|
||||
use OCP\IRequest;
|
||||
use OCP\ITempManager;
|
||||
use OCP\IURLGenerator;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Test\TestCase;
|
||||
@@ -62,13 +64,17 @@ class ThemingControllerTest extends TestCase {
|
||||
private $l10n;
|
||||
/** @var ThemingController */
|
||||
private $themingController;
|
||||
/** @var ITempManager */
|
||||
private $tempManager;
|
||||
/** @var IAppManager|MockObject */
|
||||
private $appManager;
|
||||
/** @var IAppData|MockObject */
|
||||
private $appData;
|
||||
/** @var ImageManager|MockObject */
|
||||
private $imageManager;
|
||||
/** @var IURLGenerator|MockObject */
|
||||
private $urlGenerator;
|
||||
/** @var ThemesService|MockObject */
|
||||
/** @var ThemeService|MockObject */
|
||||
private $themesService;
|
||||
|
||||
protected function setUp(): void {
|
||||
@@ -76,7 +82,9 @@ class ThemingControllerTest extends TestCase {
|
||||
$this->config = $this->createMock(IConfig::class);
|
||||
$this->themingDefaults = $this->createMock(ThemingDefaults::class);
|
||||
$this->l10n = $this->createMock(L10N::class);
|
||||
$this->appData = $this->createMock(IAppData::class);
|
||||
$this->appManager = $this->createMock(IAppManager::class);
|
||||
$this->tempManager = \OC::$server->getTempManager();
|
||||
$this->urlGenerator = $this->createMock(IURLGenerator::class);
|
||||
$this->imageManager = $this->createMock(ImageManager::class);
|
||||
$this->themesService = $this->createMock(ThemesService::class);
|
||||
@@ -94,6 +102,8 @@ class ThemingControllerTest extends TestCase {
|
||||
$this->config,
|
||||
$this->themingDefaults,
|
||||
$this->l10n,
|
||||
$this->tempManager,
|
||||
$this->appData,
|
||||
$this->urlGenerator,
|
||||
$this->appManager,
|
||||
$this->imageManager,
|
||||
@@ -154,12 +164,9 @@ class ThemingControllerTest extends TestCase {
|
||||
['url', str_repeat('a', 501), 'The given web address is not a valid URL'],
|
||||
['url', 'javascript:alert(1)', 'The given web address is not a valid URL'],
|
||||
['slogan', str_repeat('a', 501), 'The given slogan is too long'],
|
||||
['primary_color', '0082C9', 'The given color is invalid'],
|
||||
['primary_color', '#0082Z9', 'The given color is invalid'],
|
||||
['primary_color', 'Nextcloud', 'The given color is invalid'],
|
||||
['background_color', '0082C9', 'The given color is invalid'],
|
||||
['background_color', '#0082Z9', 'The given color is invalid'],
|
||||
['background_color', 'Nextcloud', 'The given color is invalid'],
|
||||
['color', '0082C9', 'The given color is invalid'],
|
||||
['color', '#0082Z9', 'The given color is invalid'],
|
||||
['color', 'Nextcloud', 'The given color is invalid'],
|
||||
['imprintUrl', '0082C9', 'The given legal notice address is not a valid URL'],
|
||||
['imprintUrl', '0082C9', 'The given legal notice address is not a valid URL'],
|
||||
['imprintUrl', 'javascript:foo', 'The given legal notice address is not a valid URL'],
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
namespace OCA\Theming\Tests;
|
||||
|
||||
use OCA\Theming\ImageManager;
|
||||
use OCA\Theming\Service\BackgroundService;
|
||||
use OCP\Files\IAppData;
|
||||
use OCP\Files\NotFoundException;
|
||||
use OCP\Files\SimpleFS\ISimpleFile;
|
||||
@@ -58,8 +57,6 @@ class ImageManagerTest extends TestCase {
|
||||
private $tempManager;
|
||||
/** @var ISimpleFolder|MockObject */
|
||||
private $rootFolder;
|
||||
/** @var BackgroundService|MockObject */
|
||||
private $backgroundService;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
@@ -70,15 +67,13 @@ class ImageManagerTest extends TestCase {
|
||||
$this->logger = $this->createMock(LoggerInterface::class);
|
||||
$this->tempManager = $this->createMock(ITempManager::class);
|
||||
$this->rootFolder = $this->createMock(ISimpleFolder::class);
|
||||
$this->backgroundService = $this->createMock(BackgroundService::class);
|
||||
$this->imageManager = new ImageManager(
|
||||
$this->config,
|
||||
$this->appData,
|
||||
$this->urlGenerator,
|
||||
$this->cacheFactory,
|
||||
$this->logger,
|
||||
$this->tempManager,
|
||||
$this->backgroundService,
|
||||
$this->tempManager
|
||||
);
|
||||
$this->appData
|
||||
->expects($this->any())
|
||||
|
||||
@@ -30,7 +30,6 @@ namespace OCA\Theming\Tests\Settings;
|
||||
use OCA\Theming\AppInfo\Application;
|
||||
use OCA\Theming\ImageManager;
|
||||
use OCA\Theming\ITheme;
|
||||
use OCA\Theming\Service\BackgroundService;
|
||||
use OCA\Theming\Service\ThemesService;
|
||||
use OCA\Theming\Settings\Personal;
|
||||
use OCA\Theming\Themes\DarkHighContrastTheme;
|
||||
@@ -117,23 +116,18 @@ class PersonalTest extends TestCase {
|
||||
->with('enforce_theme', '')
|
||||
->willReturn($enforcedTheme);
|
||||
|
||||
$this->config->expects($this->any())
|
||||
$this->config->expects($this->once())
|
||||
->method('getUserValue')
|
||||
->willReturnMap([
|
||||
['admin', 'core', 'apporder', '[]', '[]'],
|
||||
['admin', 'theming', 'background_image', BackgroundService::BACKGROUND_DEFAULT],
|
||||
]);
|
||||
->with('admin', 'core', 'apporder')
|
||||
->willReturn('[]');
|
||||
|
||||
$this->appManager->expects($this->once())
|
||||
->method('getDefaultAppForUser')
|
||||
->willReturn('forcedapp');
|
||||
|
||||
$this->initialStateService->expects($this->exactly(7))
|
||||
$this->initialStateService->expects($this->exactly(4))
|
||||
->method('provideInitialState')
|
||||
->withConsecutive(
|
||||
['shippedBackgrounds', BackgroundService::SHIPPED_BACKGROUNDS],
|
||||
['themingDefaults'],
|
||||
['userBackgroundImage'],
|
||||
['themes', $themesState],
|
||||
['enforceTheme', $enforcedTheme],
|
||||
['isUserThemingDisabled', false],
|
||||
|
||||
@@ -81,14 +81,6 @@ class DarkHighContrastThemeTest extends AccessibleThemeTestCase {
|
||||
->expects($this->any())
|
||||
->method('getDefaultColorPrimary')
|
||||
->willReturn('#0082c9');
|
||||
$this->themingDefaults
|
||||
->expects($this->any())
|
||||
->method('getColorBackground')
|
||||
->willReturn('#0082c9');
|
||||
$this->themingDefaults
|
||||
->expects($this->any())
|
||||
->method('getDefaultColorBackground')
|
||||
->willReturn('#0082c9');
|
||||
|
||||
$this->themingDefaults
|
||||
->expects($this->any())
|
||||
|
||||
@@ -78,14 +78,6 @@ class DarkThemeTest extends AccessibleThemeTestCase {
|
||||
->expects($this->any())
|
||||
->method('getDefaultColorPrimary')
|
||||
->willReturn('#0082c9');
|
||||
$this->themingDefaults
|
||||
->expects($this->any())
|
||||
->method('getColorBackground')
|
||||
->willReturn('#0082c9');
|
||||
$this->themingDefaults
|
||||
->expects($this->any())
|
||||
->method('getDefaultColorBackground')
|
||||
->willReturn('#0082c9');
|
||||
|
||||
$this->themingDefaults
|
||||
->expects($this->any())
|
||||
|
||||
@@ -69,27 +69,15 @@ class DefaultThemeTest extends AccessibleThemeTestCase {
|
||||
$this->imageManager
|
||||
);
|
||||
|
||||
$defaultBackground = BackgroundService::SHIPPED_BACKGROUNDS[BackgroundService::DEFAULT_BACKGROUND_IMAGE];
|
||||
|
||||
$this->themingDefaults
|
||||
->expects($this->any())
|
||||
->method('getColorPrimary')
|
||||
->willReturn($defaultBackground['primary_color']);
|
||||
|
||||
$this->themingDefaults
|
||||
->expects($this->any())
|
||||
->method('getColorBackground')
|
||||
->willReturn($defaultBackground['background_color']);
|
||||
->willReturn('#0082c9');
|
||||
|
||||
$this->themingDefaults
|
||||
->expects($this->any())
|
||||
->method('getDefaultColorPrimary')
|
||||
->willReturn($defaultBackground['primary_color']);
|
||||
|
||||
$this->themingDefaults
|
||||
->expects($this->any())
|
||||
->method('getDefaultColorBackground')
|
||||
->willReturn($defaultBackground['background_color']);
|
||||
->willReturn('#0082c9');
|
||||
|
||||
$this->themingDefaults
|
||||
->expects($this->any())
|
||||
|
||||
@@ -94,16 +94,6 @@ class DyslexiaFontTest extends TestCase {
|
||||
->method('getDefaultColorPrimary')
|
||||
->willReturn('#0082c9');
|
||||
|
||||
$this->themingDefaults
|
||||
->expects($this->any())
|
||||
->method('getColorBackground')
|
||||
->willReturn('#0082c9');
|
||||
|
||||
$this->themingDefaults
|
||||
->expects($this->any())
|
||||
->method('getDefaultColorBackground')
|
||||
->willReturn('#0082c9');
|
||||
|
||||
$this->l10n
|
||||
->expects($this->any())
|
||||
->method('t')
|
||||
|
||||
@@ -81,14 +81,6 @@ class HighContrastThemeTest extends AccessibleThemeTestCase {
|
||||
->expects($this->any())
|
||||
->method('getDefaultColorPrimary')
|
||||
->willReturn('#0082c9');
|
||||
$this->themingDefaults
|
||||
->expects($this->any())
|
||||
->method('getColorBackground')
|
||||
->willReturn('#0082c9');
|
||||
$this->themingDefaults
|
||||
->expects($this->any())
|
||||
->method('getDefaultColorBackground')
|
||||
->willReturn('#0082c9');
|
||||
|
||||
$this->themingDefaults
|
||||
->expects($this->any())
|
||||
|
||||
@@ -78,8 +78,6 @@ class ThemingDefaultsTest extends TestCase {
|
||||
private $imageManager;
|
||||
/** @var INavigationManager|\PHPUnit\Framework\MockObject\MockObject */
|
||||
private $navigationManager;
|
||||
/** @var BackgroundService|\PHPUnit\Framework\MockObject\MockObject */
|
||||
private $backgroundService;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
@@ -93,7 +91,6 @@ class ThemingDefaultsTest extends TestCase {
|
||||
$this->imageManager = $this->createMock(ImageManager::class);
|
||||
$this->appManager = $this->createMock(IAppManager::class);
|
||||
$this->navigationManager = $this->createMock(INavigationManager::class);
|
||||
$this->backgroundService = $this->createMock(BackgroundService::class);
|
||||
$this->defaults = new \OC_Defaults();
|
||||
$this->urlGenerator
|
||||
->expects($this->any())
|
||||
@@ -108,8 +105,7 @@ class ThemingDefaultsTest extends TestCase {
|
||||
$this->util,
|
||||
$this->imageManager,
|
||||
$this->appManager,
|
||||
$this->navigationManager,
|
||||
$this->backgroundService,
|
||||
$this->navigationManager
|
||||
);
|
||||
}
|
||||
|
||||
@@ -432,7 +428,7 @@ class ThemingDefaultsTest extends TestCase {
|
||||
->method('getAppValue')
|
||||
->willReturnMap([
|
||||
['theming', 'disable-user-theming', 'no', 'no'],
|
||||
['theming', 'primary_color', '', $this->defaults->getColorPrimary()],
|
||||
['theming', 'color', '', $this->defaults->getColorPrimary()],
|
||||
]);
|
||||
|
||||
$this->assertEquals($this->defaults->getColorPrimary(), $this->template->getColorPrimary());
|
||||
@@ -444,57 +440,13 @@ class ThemingDefaultsTest extends TestCase {
|
||||
->method('getAppValue')
|
||||
->willReturnMap([
|
||||
['theming', 'disable-user-theming', 'no', 'no'],
|
||||
['theming', 'primary_color', '', '#fff'],
|
||||
['theming', 'color', '', '#fff'],
|
||||
]);
|
||||
|
||||
$this->assertEquals('#fff', $this->template->getColorPrimary());
|
||||
}
|
||||
|
||||
public function dataGetColorPrimary() {
|
||||
return [
|
||||
'with fallback default' => [
|
||||
'disableTheming' => 'no',
|
||||
'primaryColor' => '',
|
||||
'userPrimaryColor' => '',
|
||||
'expected' => BackgroundService::DEFAULT_COLOR,
|
||||
],
|
||||
'with custom admin primary' => [
|
||||
'disableTheming' => 'no',
|
||||
'primaryColor' => '#aaa',
|
||||
'userPrimaryColor' => '',
|
||||
'expected' => '#aaa',
|
||||
],
|
||||
'with custom invalid admin primary' => [
|
||||
'disableTheming' => 'no',
|
||||
'primaryColor' => 'invalid',
|
||||
'userPrimaryColor' => '',
|
||||
'expected' => BackgroundService::DEFAULT_COLOR,
|
||||
],
|
||||
'with custom invalid user primary' => [
|
||||
'disableTheming' => 'no',
|
||||
'primaryColor' => '',
|
||||
'userPrimaryColor' => 'invalid-name',
|
||||
'expected' => BackgroundService::DEFAULT_COLOR,
|
||||
],
|
||||
'with custom user primary' => [
|
||||
'disableTheming' => 'no',
|
||||
'primaryColor' => '',
|
||||
'userPrimaryColor' => '#bbb',
|
||||
'expected' => '#bbb',
|
||||
],
|
||||
'with disabled user theming primary' => [
|
||||
'disableTheming' => 'yes',
|
||||
'primaryColor' => '#aaa',
|
||||
'userPrimaryColor' => '#bbb',
|
||||
'expected' => '#aaa',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataGetColorPrimary
|
||||
*/
|
||||
public function testGetColorPrimary(string $disableTheming, string $primaryColor, string $userPrimaryColor, string $expected) {
|
||||
public function testGetColorPrimaryWithDefaultBackground() {
|
||||
$user = $this->createMock(IUser::class);
|
||||
$this->userSession->expects($this->any())
|
||||
->method('getUser')
|
||||
@@ -503,19 +455,98 @@ class ThemingDefaultsTest extends TestCase {
|
||||
->method('getUID')
|
||||
->willReturn('user');
|
||||
$this->config
|
||||
->expects($this->any())
|
||||
->expects($this->exactly(2))
|
||||
->method('getAppValue')
|
||||
->willReturnMap([
|
||||
['theming', 'disable-user-theming', 'no', $disableTheming],
|
||||
['theming', 'primary_color', '', $primaryColor],
|
||||
['theming', 'disable-user-theming', 'no', 'no'],
|
||||
['theming', 'color', '', ''],
|
||||
]);
|
||||
$this->config
|
||||
->expects($this->any())
|
||||
->expects($this->once())
|
||||
->method('getUserValue')
|
||||
->with('user', 'theming', 'primary_color', '')
|
||||
->willReturn($userPrimaryColor);
|
||||
->with('user', 'theming', 'background_color')
|
||||
->willReturn('');
|
||||
|
||||
$this->assertEquals($expected, $this->template->getColorPrimary());
|
||||
$this->assertEquals(BackgroundService::DEFAULT_COLOR, $this->template->getColorPrimary());
|
||||
}
|
||||
|
||||
public function testGetColorPrimaryWithCustomBackground() {
|
||||
$backgroundIndex = 2;
|
||||
$background = array_values(BackgroundService::SHIPPED_BACKGROUNDS)[$backgroundIndex];
|
||||
|
||||
$user = $this->createMock(IUser::class);
|
||||
$this->userSession->expects($this->any())
|
||||
->method('getUser')
|
||||
->willReturn($user);
|
||||
$user->expects($this->any())
|
||||
->method('getUID')
|
||||
->willReturn('user');
|
||||
|
||||
$this->config
|
||||
->expects($this->once())
|
||||
->method('getUserValue')
|
||||
->with('user', 'theming', 'background_color', '')
|
||||
->willReturn($background['primary_color']);
|
||||
|
||||
$this->config
|
||||
->expects($this->exactly(2))
|
||||
->method('getAppValue')
|
||||
->willReturnMap([
|
||||
['theming', 'color', '', ''],
|
||||
['theming', 'disable-user-theming', 'no', 'no'],
|
||||
]);
|
||||
|
||||
$this->assertEquals($background['primary_color'], $this->template->getColorPrimary());
|
||||
}
|
||||
|
||||
public function testGetColorPrimaryWithCustomBackgroundColor() {
|
||||
$user = $this->createMock(IUser::class);
|
||||
$this->userSession->expects($this->any())
|
||||
->method('getUser')
|
||||
->willReturn($user);
|
||||
$user->expects($this->any())
|
||||
->method('getUID')
|
||||
->willReturn('user');
|
||||
|
||||
$this->config
|
||||
->expects($this->once())
|
||||
->method('getUserValue')
|
||||
->with('user', 'theming', 'background_color', '')
|
||||
->willReturn('#fff');
|
||||
$this->config
|
||||
->expects($this->exactly(2))
|
||||
->method('getAppValue')
|
||||
->willReturnMap([
|
||||
['theming', 'color', '', ''],
|
||||
['theming', 'disable-user-theming', 'no', 'no'],
|
||||
]);
|
||||
|
||||
$this->assertEquals('#fff', $this->template->getColorPrimary());
|
||||
}
|
||||
|
||||
public function testGetColorPrimaryWithInvalidCustomBackgroundColor() {
|
||||
$user = $this->createMock(IUser::class);
|
||||
$this->userSession->expects($this->any())
|
||||
->method('getUser')
|
||||
->willReturn($user);
|
||||
$user->expects($this->any())
|
||||
->method('getUID')
|
||||
->willReturn('user');
|
||||
|
||||
$this->config
|
||||
->expects($this->once())
|
||||
->method('getUserValue')
|
||||
->with('user', 'theming', 'background_color', '')
|
||||
->willReturn('nextcloud');
|
||||
$this->config
|
||||
->expects($this->exactly(3))
|
||||
->method('getAppValue')
|
||||
->willReturnMap([
|
||||
['theming', 'color', '', ''],
|
||||
['theming', 'disable-user-theming', 'no', 'no'],
|
||||
]);
|
||||
|
||||
$this->assertEquals($this->template->getDefaultColorPrimary(), $this->template->getColorPrimary());
|
||||
}
|
||||
|
||||
public function testSet() {
|
||||
@@ -615,22 +646,27 @@ class ThemingDefaultsTest extends TestCase {
|
||||
$this->assertSame($this->defaults->getSlogan(), $this->template->undo('slogan'));
|
||||
}
|
||||
|
||||
public function testUndoPrimaryColor() {
|
||||
public function testUndoColor() {
|
||||
$this->config
|
||||
->expects($this->once())
|
||||
->method('deleteAppValue')
|
||||
->with('theming', 'primary_color');
|
||||
->with('theming', 'color');
|
||||
$this->config
|
||||
->expects($this->once())
|
||||
->expects($this->exactly(2))
|
||||
->method('getAppValue')
|
||||
->with('theming', 'cachebuster', '0')
|
||||
->willReturn('15');
|
||||
->withConsecutive(
|
||||
['theming', 'cachebuster', '0'],
|
||||
['theming', 'color', null],
|
||||
)->willReturnOnConsecutiveCalls(
|
||||
'15',
|
||||
$this->defaults->getColorPrimary(),
|
||||
);
|
||||
$this->config
|
||||
->expects($this->once())
|
||||
->method('setAppValue')
|
||||
->with('theming', 'cachebuster', 16);
|
||||
|
||||
$this->assertSame($this->defaults->getColorPrimary(), $this->template->undo('primary_color'));
|
||||
$this->assertSame($this->defaults->getColorPrimary(), $this->template->undo('color'));
|
||||
}
|
||||
|
||||
public function testUndoDefaultAction() {
|
||||
@@ -728,8 +764,8 @@ class ThemingDefaultsTest extends TestCase {
|
||||
['theming', 'backgroundMime', '', 'jpeg'],
|
||||
['theming', 'logoheaderMime', '', 'jpeg'],
|
||||
['theming', 'faviconMime', '', 'jpeg'],
|
||||
['theming', 'primary_color', '', $this->defaults->getColorPrimary()],
|
||||
['theming', 'primary_color', $this->defaults->getColorPrimary(), $this->defaults->getColorPrimary()],
|
||||
['theming', 'color', '', $this->defaults->getColorPrimary()],
|
||||
['theming', 'color', $this->defaults->getColorPrimary(), $this->defaults->getColorPrimary()],
|
||||
]);
|
||||
|
||||
$this->util->expects($this->any())->method('invertTextColor')->with($this->defaults->getColorPrimary())->willReturn(false);
|
||||
|
||||
@@ -319,7 +319,7 @@ class LDAP implements ILDAPWrapper {
|
||||
|
||||
private function preFunctionCall(string $functionName, array $args): void {
|
||||
$this->curArgs = $args;
|
||||
if(strcasecmp($functionName, 'ldap_bind') === 0 || strcasecmp($functionName, 'ldap_exop_passwd') === 0) {
|
||||
if(strcasecmp($functionName, 'ldap_bind') === 0) {
|
||||
// The arguments are not key value pairs
|
||||
// \OCA\User_LDAP\LDAP::bind passes 3 arguments, the 3rd being the pw
|
||||
// Remove it via direct array access for now, although a better solution could be found mebbe?
|
||||
|
||||
+17
-2
@@ -1,7 +1,22 @@
|
||||
<?php
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* @copyright Copyright (c) 2017 Morris Jobke <hey@morrisjobke.de>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
$expectedFiles = [
|
||||
|
||||
@@ -1,8 +1,22 @@
|
||||
<?php
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* @author Thomas Müller <thomas.mueller@tmit.eu>
|
||||
*
|
||||
* @copyright Copyright (c) 2016, ownCloud, Inc.
|
||||
* @license AGPL-3.0
|
||||
*
|
||||
* This code is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License, version 3,
|
||||
* as published by the Free Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License, version 3,
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
*/
|
||||
|
||||
if (!isset($argv[1])) {
|
||||
|
||||
@@ -1,7 +1,24 @@
|
||||
<?php
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch>
|
||||
*
|
||||
* @author Lukas Reschke <lukas@statuscode.ch>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user