Compare commits
186 Commits
folderCont
...
v25.0.0rc4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
684bd274b8 | ||
|
|
7f498d0fa0 | ||
|
|
7c4a5b0dd1 | ||
|
|
08227ca28d | ||
|
|
1309459fee | ||
|
|
37cca49630 | ||
|
|
982534349a | ||
|
|
61d2937bf4 | ||
|
|
5c7613fd67 | ||
|
|
10cf013e19 | ||
|
|
df218bc4db | ||
|
|
f17f24ffad | ||
|
|
b3832bcd05 | ||
|
|
a7211e6ab0 | ||
|
|
faab6c8072 | ||
|
|
ae6a8d5cf5 | ||
|
|
6c47b26102 | ||
|
|
2a42dd4f4e | ||
|
|
1fb7567294 | ||
|
|
1360771be8 | ||
|
|
7da871437b | ||
|
|
1dd634c0e6 | ||
|
|
5e2c5e5fb6 | ||
|
|
c2510a481e | ||
|
|
39938297ee | ||
|
|
267f3896ae | ||
|
|
be005877ff | ||
|
|
c48b15fb70 | ||
|
|
ec230f9290 | ||
|
|
1db6d96e94 | ||
|
|
1d02efdd8b | ||
|
|
8e89c281a5 | ||
|
|
ad0405a18b | ||
|
|
775e4e023d | ||
|
|
be7b7f5d13 | ||
|
|
8caffb784f | ||
|
|
ce7bfc6b33 | ||
|
|
c98334fdac | ||
|
|
f477a22404 | ||
|
|
cecd778c2b | ||
|
|
b9b201cc9a | ||
|
|
b885e44dde | ||
|
|
4beffc4209 | ||
|
|
4cbfeec38e | ||
|
|
66b1cf3a19 | ||
|
|
873ba714ae | ||
|
|
9068bf5788 | ||
|
|
0644eab947 | ||
|
|
116897fc90 | ||
|
|
061833b83d | ||
|
|
e50f95eab6 | ||
|
|
8fcb1c1d76 | ||
|
|
263e3e829e | ||
|
|
1921685e4e | ||
|
|
402143d4e8 | ||
|
|
4807c23e09 | ||
|
|
0c6b5cde95 | ||
|
|
ab959249d3 | ||
|
|
c4a6f5c7b2 | ||
|
|
0612e008d4 | ||
|
|
47e950dbee | ||
|
|
36473a3f63 | ||
|
|
f4fc7daf73 | ||
|
|
31e5e102c1 | ||
|
|
511ade9497 | ||
|
|
d1dc38dc1f | ||
|
|
fad56d683c | ||
|
|
c765dac633 | ||
|
|
bbd972b2ca | ||
|
|
a4bfea7f37 | ||
|
|
7b841c060d | ||
|
|
0a2744a998 | ||
|
|
c26e17104c | ||
|
|
1f0245e68f | ||
|
|
a3febe2a40 | ||
|
|
7ed5679f2c | ||
|
|
380aaef96e | ||
|
|
1b4c82156d | ||
|
|
07ac47812b | ||
|
|
46180dd9f3 | ||
|
|
d847a89436 | ||
|
|
eafcc4e3a1 | ||
|
|
51c249d9ab | ||
|
|
b1617bca39 | ||
|
|
14a59c461b | ||
|
|
a2d8bd4c11 | ||
|
|
115058720a | ||
|
|
65144258d2 | ||
|
|
7b2f226ce6 | ||
|
|
ac5ec1af7d | ||
|
|
34ca126b37 | ||
|
|
02d07e14b3 | ||
|
|
61def5e276 | ||
|
|
ffecfc6441 | ||
|
|
94b86b22ce | ||
|
|
cfd01baf04 | ||
|
|
036af3556c | ||
|
|
a3b96282ed | ||
|
|
56abd79050 | ||
|
|
5267da34b1 | ||
|
|
446e495c59 | ||
|
|
cae61e00f6 | ||
|
|
5660589ca9 | ||
|
|
8aed08e058 | ||
|
|
f2d7d13579 | ||
|
|
4dea095a76 | ||
|
|
2ea723d1e1 | ||
|
|
019ffa2ec0 | ||
|
|
885b55b7af | ||
|
|
467a0eaee2 | ||
|
|
77da30a906 | ||
|
|
ce1dddcc64 | ||
|
|
b84eb26f72 | ||
|
|
b6fd615bf7 | ||
|
|
68284b87f2 | ||
|
|
904ac63310 | ||
|
|
1f27bd153b | ||
|
|
92a4ce9f3b | ||
|
|
729b78708f | ||
|
|
8b34878771 | ||
|
|
a5c5740e84 | ||
|
|
ab51e4ef24 | ||
|
|
d442a3c1be | ||
|
|
6ac50c26ef | ||
|
|
4aeb701b87 | ||
|
|
9c073fd760 | ||
|
|
413610ede4 | ||
|
|
c12f26867d | ||
|
|
847535ee88 | ||
|
|
d9c07af32a | ||
|
|
e85870afa7 | ||
|
|
7bdaa9fa04 | ||
|
|
9c21bda303 | ||
|
|
3bb4d01a7c | ||
|
|
99cb56cda2 | ||
|
|
a85d1cad6a | ||
|
|
6965d4b4af | ||
|
|
4441416245 | ||
|
|
96423a1172 | ||
|
|
2d86948c34 | ||
|
|
dbc421580e | ||
|
|
89f8179e64 | ||
|
|
bc9f3d3d4c | ||
|
|
9fe747f2c5 | ||
|
|
6f8b3916ad | ||
|
|
82e9af439e | ||
|
|
c471c8d6de | ||
|
|
42ca267d83 | ||
|
|
981da3d49f | ||
|
|
7083596dc1 | ||
|
|
ac2e94f121 | ||
|
|
de295ba8dd | ||
|
|
5d078383b6 | ||
|
|
e938bbec19 | ||
|
|
1f0f227b43 | ||
|
|
b2fa292ee8 | ||
|
|
cc6b12a166 | ||
|
|
b45d342654 | ||
|
|
e43814f74d | ||
|
|
611b920e3d | ||
|
|
c25d61c7b8 | ||
|
|
4be6d23d00 | ||
|
|
f2bd15229a | ||
|
|
a703818ee7 | ||
|
|
5ee61a5fc6 | ||
|
|
019e85d92c | ||
|
|
1fae3ae4ae | ||
|
|
c6c4328e2a | ||
|
|
aff9302638 | ||
|
|
1115199ae3 | ||
|
|
86b787d21e | ||
|
|
359a37fd10 | ||
|
|
079a8c0e0f | ||
|
|
42bc4a0b2a | ||
|
|
205760a3aa | ||
|
|
2948697257 | ||
|
|
3c47caf08b | ||
|
|
515e05cf16 | ||
|
|
060230eec7 | ||
|
|
b2a893abad | ||
|
|
95dcc610fc | ||
|
|
a5a8f4e9ef | ||
|
|
9366ec0fb8 | ||
|
|
bfaa31af61 | ||
|
|
063aac8ebc | ||
|
|
773826f9e1 |
@@ -1214,7 +1214,7 @@ steps:
|
||||
commands:
|
||||
# JavaScript files are not used in integration tests so it is not needed to
|
||||
# build them.
|
||||
- git clone --depth 1 https://github.com/nextcloud/spreed apps/spreed
|
||||
- git clone --depth 1 --branch stable25 https://github.com/nextcloud/spreed apps/spreed
|
||||
- name: integration-sharing-v1-video-verification
|
||||
image: ghcr.io/nextcloud/continuous-integration-integration-php7.4:latest
|
||||
commands:
|
||||
|
||||
2
3rdparty
@@ -88,7 +88,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { generateUrl, imagePath } from '@nextcloud/router'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import axios from '@nextcloud/axios'
|
||||
@@ -99,13 +99,11 @@ import Pencil from 'vue-material-design-icons/Pencil.vue'
|
||||
import Vue from 'vue'
|
||||
|
||||
import isMobile from './mixins/isMobile.js'
|
||||
import { getBackgroundUrl } from './helpers/getBackgroundUrl.js'
|
||||
|
||||
const panels = loadState('dashboard', 'panels')
|
||||
const firstRun = loadState('dashboard', 'firstRun')
|
||||
|
||||
const background = loadState('theming', 'background')
|
||||
const backgroundVersion = loadState('theming', 'backgroundVersion')
|
||||
const themingDefaultBackground = loadState('theming', 'themingDefaultBackground')
|
||||
const shippedBackgroundList = loadState('theming', 'shippedBackgrounds')
|
||||
|
||||
@@ -155,19 +153,6 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
backgroundImage() {
|
||||
return getBackgroundUrl(this.background, backgroundVersion, this.themingDefaultBackground)
|
||||
},
|
||||
backgroundStyle() {
|
||||
if ((this.background === 'default' && this.themingDefaultBackground === 'backgroundColor')
|
||||
|| this.background.match(/#[0-9A-Fa-f]{6}/g)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
backgroundImage: this.background === 'default' ? 'var(--image-main-background)' : `url('${this.backgroundImage}')`,
|
||||
}
|
||||
},
|
||||
greeting() {
|
||||
const time = this.timer.getHours()
|
||||
|
||||
@@ -286,17 +271,6 @@ export default {
|
||||
// document.body.removeAttribute('data-theme-light')
|
||||
// document.body.setAttribute('data-theme-dark', 'true')
|
||||
}
|
||||
|
||||
const themeElements = [document.documentElement, document.querySelector('#header'), document.querySelector('body')]
|
||||
for (const element of themeElements) {
|
||||
if (this.background === 'default') {
|
||||
element.style.setProperty('--image-main-background', `url('${imagePath('core', 'app-background.jpg')}')`)
|
||||
} else if (this.background.match(/#[0-9A-Fa-f]{6}/g)) {
|
||||
element.style.setProperty('--image-main-background', undefined)
|
||||
} else {
|
||||
element.style.setProperty('--image-main-background', this.backgroundStyle.backgroundImage)
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Method to register panels that will be called by the integrating apps
|
||||
@@ -441,7 +415,7 @@ export default {
|
||||
.panels {
|
||||
width: auto;
|
||||
margin: auto;
|
||||
max-width: 1500px;
|
||||
max-width: 1800px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: row;
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
/**
|
||||
* @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net>
|
||||
*
|
||||
* @author Avior <florian.bouillon@delta-wings.net>
|
||||
* @author Julien Veyssier <eneiluj@posteo.net>
|
||||
* @author Julius Härtl <jus@bitgrid.net>
|
||||
*
|
||||
* @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 { generateUrl } from '@nextcloud/router'
|
||||
import { prefixWithBaseUrl } from './prefixWithBaseUrl.js'
|
||||
|
||||
export const getBackgroundUrl = (background, time = 0, themingDefaultBackground = '') => {
|
||||
const enabledThemes = window.OCA?.Theming?.enabledThemes || []
|
||||
const isDarkTheme = (enabledThemes.length === 0 || enabledThemes[0] === 'default')
|
||||
? window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
: enabledThemes.join('').indexOf('dark') !== -1
|
||||
|
||||
if (background === 'default') {
|
||||
if (themingDefaultBackground && themingDefaultBackground !== 'backgroundColor') {
|
||||
return generateUrl('/apps/theming/image/background') + '?v=' + window.OCA.Theming.cacheBuster
|
||||
}
|
||||
|
||||
if (isDarkTheme) {
|
||||
return prefixWithBaseUrl('eduardo-neves-pedra-azul.jpg')
|
||||
}
|
||||
|
||||
return prefixWithBaseUrl('kamil-porembinski-clouds.jpg')
|
||||
} else if (background === 'custom') {
|
||||
return generateUrl('/apps/theming/background') + '?v=' + time
|
||||
}
|
||||
|
||||
return prefixWithBaseUrl(background)
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
/**
|
||||
* @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net>
|
||||
*
|
||||
* @author Julius Härtl <jus@bitgrid.net>
|
||||
*
|
||||
* @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 { generateFilePath } from '@nextcloud/router'
|
||||
|
||||
export const prefixWithBaseUrl = (url) => generateFilePath('theming', '', 'img/background/') + url
|
||||
@@ -1151,7 +1151,7 @@ class FileTest extends TestCase {
|
||||
|
||||
$info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [
|
||||
'permissions' => \OCP\Constants::PERMISSION_ALL,
|
||||
'type' => FileInfo::TYPE_FOLDER,
|
||||
'type' => FileInfo::TYPE_FILE,
|
||||
], null);
|
||||
|
||||
$file = new \OCA\DAV\Connector\Sabre\File($view, $info);
|
||||
@@ -1172,7 +1172,7 @@ class FileTest extends TestCase {
|
||||
|
||||
$info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [
|
||||
'permissions' => \OCP\Constants::PERMISSION_ALL,
|
||||
'type' => FileInfo::TYPE_FOLDER,
|
||||
'type' => FileInfo::TYPE_FILE,
|
||||
], null);
|
||||
|
||||
$file = new \OCA\DAV\Connector\Sabre\File($view, $info);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<name>Files</name>
|
||||
<summary>File Management</summary>
|
||||
<description>File Management</description>
|
||||
<version>1.20.0</version>
|
||||
<version>1.20.1</version>
|
||||
<licence>agpl</licence>
|
||||
<author>Robin Appelman</author>
|
||||
<author>Vincent Petry</author>
|
||||
@@ -26,6 +26,7 @@
|
||||
<job>OCA\Files\BackgroundJob\DeleteOrphanedItems</job>
|
||||
<job>OCA\Files\BackgroundJob\CleanupFileLocks</job>
|
||||
<job>OCA\Files\BackgroundJob\CleanupDirectEditingTokens</job>
|
||||
<job>OCA\Files\BackgroundJob\DeleteExpiredOpenLocalEditor</job>
|
||||
</background-jobs>
|
||||
|
||||
<commands>
|
||||
|
||||
@@ -37,6 +37,8 @@ declare(strict_types=1);
|
||||
*/
|
||||
namespace OCA\Files\AppInfo;
|
||||
|
||||
use OCA\Files\Controller\OpenLocalEditorController;
|
||||
|
||||
/** @var Application $application */
|
||||
$application = \OC::$server->query(Application::class);
|
||||
$application->registerRoutes(
|
||||
@@ -169,6 +171,18 @@ $application->registerRoutes(
|
||||
'url' => '/api/v1/transferownership/{id}',
|
||||
'verb' => 'DELETE',
|
||||
],
|
||||
[
|
||||
/** @see OpenLocalEditorController::create() */
|
||||
'name' => 'OpenLocalEditor#create',
|
||||
'url' => '/api/v1/openlocaleditor',
|
||||
'verb' => 'POST',
|
||||
],
|
||||
[
|
||||
/** @see OpenLocalEditorController::validate() */
|
||||
'name' => 'OpenLocalEditor#validate',
|
||||
'url' => '/api/v1/openlocaleditor/{token}',
|
||||
'verb' => 'POST',
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
@@ -20,6 +20,7 @@ return array(
|
||||
'OCA\\Files\\AppInfo\\Application' => $baseDir . '/../lib/AppInfo/Application.php',
|
||||
'OCA\\Files\\BackgroundJob\\CleanupDirectEditingTokens' => $baseDir . '/../lib/BackgroundJob/CleanupDirectEditingTokens.php',
|
||||
'OCA\\Files\\BackgroundJob\\CleanupFileLocks' => $baseDir . '/../lib/BackgroundJob/CleanupFileLocks.php',
|
||||
'OCA\\Files\\BackgroundJob\\DeleteExpiredOpenLocalEditor' => $baseDir . '/../lib/BackgroundJob/DeleteExpiredOpenLocalEditor.php',
|
||||
'OCA\\Files\\BackgroundJob\\DeleteOrphanedItems' => $baseDir . '/../lib/BackgroundJob/DeleteOrphanedItems.php',
|
||||
'OCA\\Files\\BackgroundJob\\ScanFiles' => $baseDir . '/../lib/BackgroundJob/ScanFiles.php',
|
||||
'OCA\\Files\\BackgroundJob\\TransferOwnership' => $baseDir . '/../lib/BackgroundJob/TransferOwnership.php',
|
||||
@@ -35,9 +36,12 @@ return array(
|
||||
'OCA\\Files\\Controller\\ApiController' => $baseDir . '/../lib/Controller/ApiController.php',
|
||||
'OCA\\Files\\Controller\\DirectEditingController' => $baseDir . '/../lib/Controller/DirectEditingController.php',
|
||||
'OCA\\Files\\Controller\\DirectEditingViewController' => $baseDir . '/../lib/Controller/DirectEditingViewController.php',
|
||||
'OCA\\Files\\Controller\\OpenLocalEditorController' => $baseDir . '/../lib/Controller/OpenLocalEditorController.php',
|
||||
'OCA\\Files\\Controller\\TemplateController' => $baseDir . '/../lib/Controller/TemplateController.php',
|
||||
'OCA\\Files\\Controller\\TransferOwnershipController' => $baseDir . '/../lib/Controller/TransferOwnershipController.php',
|
||||
'OCA\\Files\\Controller\\ViewController' => $baseDir . '/../lib/Controller/ViewController.php',
|
||||
'OCA\\Files\\Db\\OpenLocalEditor' => $baseDir . '/../lib/Db/OpenLocalEditor.php',
|
||||
'OCA\\Files\\Db\\OpenLocalEditorMapper' => $baseDir . '/../lib/Db/OpenLocalEditorMapper.php',
|
||||
'OCA\\Files\\Db\\TransferOwnership' => $baseDir . '/../lib/Db/TransferOwnership.php',
|
||||
'OCA\\Files\\Db\\TransferOwnershipMapper' => $baseDir . '/../lib/Db/TransferOwnershipMapper.php',
|
||||
'OCA\\Files\\DirectEditingCapabilities' => $baseDir . '/../lib/DirectEditingCapabilities.php',
|
||||
@@ -48,6 +52,7 @@ return array(
|
||||
'OCA\\Files\\Listener\\LegacyLoadAdditionalScriptsAdapter' => $baseDir . '/../lib/Listener/LegacyLoadAdditionalScriptsAdapter.php',
|
||||
'OCA\\Files\\Listener\\LoadSidebarListener' => $baseDir . '/../lib/Listener/LoadSidebarListener.php',
|
||||
'OCA\\Files\\Migration\\Version11301Date20191205150729' => $baseDir . '/../lib/Migration/Version11301Date20191205150729.php',
|
||||
'OCA\\Files\\Migration\\Version12101Date20221011153334' => $baseDir . '/../lib/Migration/Version12101Date20221011153334.php',
|
||||
'OCA\\Files\\Notification\\Notifier' => $baseDir . '/../lib/Notification/Notifier.php',
|
||||
'OCA\\Files\\Search\\FilesSearchProvider' => $baseDir . '/../lib/Search/FilesSearchProvider.php',
|
||||
'OCA\\Files\\Service\\DirectEditingService' => $baseDir . '/../lib/Service/DirectEditingService.php',
|
||||
|
||||
@@ -35,6 +35,7 @@ class ComposerStaticInitFiles
|
||||
'OCA\\Files\\AppInfo\\Application' => __DIR__ . '/..' . '/../lib/AppInfo/Application.php',
|
||||
'OCA\\Files\\BackgroundJob\\CleanupDirectEditingTokens' => __DIR__ . '/..' . '/../lib/BackgroundJob/CleanupDirectEditingTokens.php',
|
||||
'OCA\\Files\\BackgroundJob\\CleanupFileLocks' => __DIR__ . '/..' . '/../lib/BackgroundJob/CleanupFileLocks.php',
|
||||
'OCA\\Files\\BackgroundJob\\DeleteExpiredOpenLocalEditor' => __DIR__ . '/..' . '/../lib/BackgroundJob/DeleteExpiredOpenLocalEditor.php',
|
||||
'OCA\\Files\\BackgroundJob\\DeleteOrphanedItems' => __DIR__ . '/..' . '/../lib/BackgroundJob/DeleteOrphanedItems.php',
|
||||
'OCA\\Files\\BackgroundJob\\ScanFiles' => __DIR__ . '/..' . '/../lib/BackgroundJob/ScanFiles.php',
|
||||
'OCA\\Files\\BackgroundJob\\TransferOwnership' => __DIR__ . '/..' . '/../lib/BackgroundJob/TransferOwnership.php',
|
||||
@@ -50,9 +51,12 @@ class ComposerStaticInitFiles
|
||||
'OCA\\Files\\Controller\\ApiController' => __DIR__ . '/..' . '/../lib/Controller/ApiController.php',
|
||||
'OCA\\Files\\Controller\\DirectEditingController' => __DIR__ . '/..' . '/../lib/Controller/DirectEditingController.php',
|
||||
'OCA\\Files\\Controller\\DirectEditingViewController' => __DIR__ . '/..' . '/../lib/Controller/DirectEditingViewController.php',
|
||||
'OCA\\Files\\Controller\\OpenLocalEditorController' => __DIR__ . '/..' . '/../lib/Controller/OpenLocalEditorController.php',
|
||||
'OCA\\Files\\Controller\\TemplateController' => __DIR__ . '/..' . '/../lib/Controller/TemplateController.php',
|
||||
'OCA\\Files\\Controller\\TransferOwnershipController' => __DIR__ . '/..' . '/../lib/Controller/TransferOwnershipController.php',
|
||||
'OCA\\Files\\Controller\\ViewController' => __DIR__ . '/..' . '/../lib/Controller/ViewController.php',
|
||||
'OCA\\Files\\Db\\OpenLocalEditor' => __DIR__ . '/..' . '/../lib/Db/OpenLocalEditor.php',
|
||||
'OCA\\Files\\Db\\OpenLocalEditorMapper' => __DIR__ . '/..' . '/../lib/Db/OpenLocalEditorMapper.php',
|
||||
'OCA\\Files\\Db\\TransferOwnership' => __DIR__ . '/..' . '/../lib/Db/TransferOwnership.php',
|
||||
'OCA\\Files\\Db\\TransferOwnershipMapper' => __DIR__ . '/..' . '/../lib/Db/TransferOwnershipMapper.php',
|
||||
'OCA\\Files\\DirectEditingCapabilities' => __DIR__ . '/..' . '/../lib/DirectEditingCapabilities.php',
|
||||
@@ -63,6 +67,7 @@ class ComposerStaticInitFiles
|
||||
'OCA\\Files\\Listener\\LegacyLoadAdditionalScriptsAdapter' => __DIR__ . '/..' . '/../lib/Listener/LegacyLoadAdditionalScriptsAdapter.php',
|
||||
'OCA\\Files\\Listener\\LoadSidebarListener' => __DIR__ . '/..' . '/../lib/Listener/LoadSidebarListener.php',
|
||||
'OCA\\Files\\Migration\\Version11301Date20191205150729' => __DIR__ . '/..' . '/../lib/Migration/Version11301Date20191205150729.php',
|
||||
'OCA\\Files\\Migration\\Version12101Date20221011153334' => __DIR__ . '/..' . '/../lib/Migration/Version12101Date20221011153334.php',
|
||||
'OCA\\Files\\Notification\\Notifier' => __DIR__ . '/..' . '/../lib/Notification/Notifier.php',
|
||||
'OCA\\Files\\Search\\FilesSearchProvider' => __DIR__ . '/..' . '/../lib/Search/FilesSearchProvider.php',
|
||||
'OCA\\Files\\Service\\DirectEditingService' => __DIR__ . '/..' . '/../lib/Service/DirectEditingService.php',
|
||||
|
||||
@@ -973,8 +973,7 @@ table.dragshadow td.size {
|
||||
background-image: none;
|
||||
}
|
||||
.files-filestable .filename .favorite-mark .icon-starred {
|
||||
/* $dir is the app name, so we add this to the icon var to avoid conflicts between apps */
|
||||
background-image: var(--icon-star-dark-yellow);
|
||||
background-image: var(--icon-starred-yellow) !important;
|
||||
}
|
||||
|
||||
.files-filestable .filename .action .icon.hidden,
|
||||
|
||||
@@ -874,7 +874,7 @@ table.dragshadow td.size {
|
||||
background-image: none;
|
||||
}
|
||||
& .icon-starred {
|
||||
@include icon-color('star-dark', 'actions', variables.$color-yellow, 1, true);
|
||||
background-image: var(--icon-starred-yellow) !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -973,8 +973,7 @@ table.dragshadow td.size {
|
||||
background-image: none;
|
||||
}
|
||||
.files-filestable .filename .favorite-mark .icon-starred {
|
||||
/* $dir is the app name, so we add this to the icon var to avoid conflicts between apps */
|
||||
background-image: var(--icon-star-dark-yellow);
|
||||
background-image: var(--icon-starred-yellow) !important;
|
||||
}
|
||||
|
||||
.files-filestable .filename .action .icon.hidden,
|
||||
|
||||
@@ -114,13 +114,13 @@
|
||||
OCA.Files.FileList.MultiSelectMenuActions.ToggleSelectionModeAction,
|
||||
{
|
||||
name: 'delete',
|
||||
displayName: t('files', 'Delete'),
|
||||
displayName: t('files', 'Delete'),
|
||||
iconClass: 'icon-delete',
|
||||
order: 99,
|
||||
},
|
||||
{
|
||||
name: 'tags',
|
||||
displayName: 'Tags',
|
||||
displayName: t('files', 'Tags'),
|
||||
iconClass: 'icon-tag',
|
||||
order: 100,
|
||||
},
|
||||
|
||||
@@ -710,29 +710,31 @@
|
||||
}
|
||||
});
|
||||
|
||||
this.registerAction({
|
||||
name: 'EditLocally',
|
||||
displayName: function(context) {
|
||||
var locked = context.$file.data('locked');
|
||||
if (!locked) {
|
||||
return t('files', 'Edit locally');
|
||||
}
|
||||
},
|
||||
mime: 'all',
|
||||
order: -23,
|
||||
icon: function(filename, context) {
|
||||
var locked = context.$file.data('locked');
|
||||
if (!locked) {
|
||||
return OC.imagePath('files', 'computer.svg')
|
||||
}
|
||||
},
|
||||
permissions: OC.PERMISSION_UPDATE,
|
||||
actionHandler: function (filename, context) {
|
||||
var dir = context.dir || context.fileList.getCurrentDirectory();
|
||||
var path = dir === '/' ? dir + filename : dir + '/' + filename;
|
||||
context.fileList.openLocalClient(path);
|
||||
},
|
||||
});
|
||||
if (!/Android|iPhone|iPad|iPod/i.test(navigator.userAgent)) {
|
||||
this.registerAction({
|
||||
name: 'EditLocally',
|
||||
displayName: function(context) {
|
||||
var locked = context.$file.data('locked');
|
||||
if (!locked) {
|
||||
return t('files', 'Edit locally');
|
||||
}
|
||||
},
|
||||
mime: 'all',
|
||||
order: -23,
|
||||
icon: function(filename, context) {
|
||||
var locked = context.$file.data('locked');
|
||||
if (!locked) {
|
||||
return OC.imagePath('files', 'computer.svg')
|
||||
}
|
||||
},
|
||||
permissions: OC.PERMISSION_UPDATE,
|
||||
actionHandler: function (filename, context) {
|
||||
var dir = context.dir || context.fileList.getCurrentDirectory();
|
||||
var path = dir === '/' ? dir + filename : dir + '/' + filename;
|
||||
context.fileList.openLocalClient(path);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
this.registerAction({
|
||||
name: 'Open',
|
||||
|
||||
@@ -2808,12 +2808,23 @@
|
||||
},
|
||||
|
||||
openLocalClient: function(path) {
|
||||
var scheme = 'nc://';
|
||||
var command = 'open';
|
||||
var uid = OC.getCurrentUser().uid;
|
||||
var url = scheme + command + '/' + uid + '@' + window.location.host + OC.encodePath(path);
|
||||
var link = OC.linkToOCS('apps/files/api/v1', 2) + 'openlocaleditor?format=json';
|
||||
|
||||
window.location.href = url;
|
||||
$.post(link, {
|
||||
path
|
||||
})
|
||||
.success(function(result) {
|
||||
var scheme = 'nc://';
|
||||
var command = 'open';
|
||||
var uid = OC.getCurrentUser().uid;
|
||||
var url = scheme + command + '/' + uid + '@' + window.location.host + OC.encodePath(path);
|
||||
url += '?token=' + result.ocs.data.token;
|
||||
|
||||
window.location.href = url;
|
||||
})
|
||||
.fail(function() {
|
||||
OC.Notification.show(t('files', 'Failed to redirect to client'))
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
|
||||
*
|
||||
* @author Joas Schilling <coding@schilljs.com>
|
||||
*
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OCA\Files\BackgroundJob;
|
||||
|
||||
use OCA\Files\Controller\OpenLocalEditorController;
|
||||
use OCA\Files\Db\OpenLocalEditorMapper;
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
use OCP\BackgroundJob\IJob;
|
||||
use OCP\BackgroundJob\TimedJob;
|
||||
|
||||
/**
|
||||
* Delete all expired "Open local editor" token
|
||||
*/
|
||||
class DeleteExpiredOpenLocalEditor extends TimedJob {
|
||||
protected OpenLocalEditorMapper $mapper;
|
||||
|
||||
public function __construct(
|
||||
ITimeFactory $time,
|
||||
OpenLocalEditorMapper $mapper
|
||||
) {
|
||||
parent::__construct($time);
|
||||
$this->mapper = $mapper;
|
||||
|
||||
// Run every 12h
|
||||
$this->interval = 12 * 3600;
|
||||
$this->setTimeSensitivity(IJob::TIME_INSENSITIVE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the background job do its work
|
||||
*
|
||||
* @param array $argument unused argument
|
||||
*/
|
||||
public function run($argument): void {
|
||||
$this->mapper->deleteExpiredTokens($this->time->getTime());
|
||||
}
|
||||
}
|
||||
138
apps/files/lib/Controller/OpenLocalEditorController.php
Normal file
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
|
||||
*
|
||||
* @author Joas Schilling <coding@schilljs.com>
|
||||
*
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OCA\Files\Controller;
|
||||
|
||||
use OCA\Files\Db\OpenLocalEditor;
|
||||
use OCA\Files\Db\OpenLocalEditorMapper;
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\AppFramework\Http\DataResponse;
|
||||
use OCP\AppFramework\OCSController;
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
use OCP\DB\Exception;
|
||||
use OCP\IRequest;
|
||||
use OCP\Security\ISecureRandom;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class OpenLocalEditorController extends OCSController {
|
||||
public const TOKEN_LENGTH = 128;
|
||||
public const TOKEN_DURATION = 600; // 10 Minutes
|
||||
public const TOKEN_RETRIES = 50;
|
||||
|
||||
protected ITimeFactory $timeFactory;
|
||||
protected OpenLocalEditorMapper $mapper;
|
||||
protected ISecureRandom $secureRandom;
|
||||
protected LoggerInterface $logger;
|
||||
protected ?string $userId;
|
||||
|
||||
public function __construct(
|
||||
string $appName,
|
||||
IRequest $request,
|
||||
ITimeFactory $timeFactory,
|
||||
OpenLocalEditorMapper $mapper,
|
||||
ISecureRandom $secureRandom,
|
||||
LoggerInterface $logger,
|
||||
?string $userId
|
||||
) {
|
||||
parent::__construct($appName, $request);
|
||||
|
||||
$this->timeFactory = $timeFactory;
|
||||
$this->mapper = $mapper;
|
||||
$this->secureRandom = $secureRandom;
|
||||
$this->logger = $logger;
|
||||
$this->userId = $userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
* @UserRateThrottle(limit=10, period=120)
|
||||
*/
|
||||
public function create(string $path): DataResponse {
|
||||
$pathHash = sha1($path);
|
||||
|
||||
$entity = new OpenLocalEditor();
|
||||
$entity->setUserId($this->userId);
|
||||
$entity->setPathHash($pathHash);
|
||||
$entity->setExpirationTime($this->timeFactory->getTime() + self::TOKEN_DURATION); // Expire in 10 minutes
|
||||
|
||||
for ($i = 1; $i <= self::TOKEN_RETRIES; $i++) {
|
||||
$token = $this->secureRandom->generate(self::TOKEN_LENGTH, ISecureRandom::CHAR_ALPHANUMERIC);
|
||||
$entity->setToken($token);
|
||||
|
||||
try {
|
||||
$this->mapper->insert($entity);
|
||||
|
||||
return new DataResponse([
|
||||
'userId' => $this->userId,
|
||||
'pathHash' => $pathHash,
|
||||
'expirationTime' => $entity->getExpirationTime(),
|
||||
'token' => $entity->getToken(),
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
if ($e->getCode() !== Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
|
||||
// Only retry on unique constraint violation
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->logger->error('Giving up after ' . self::TOKEN_RETRIES . ' retries to generate a unique local editor token for path hash: ' . $pathHash);
|
||||
return new DataResponse([], Http::STATUS_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
* @BruteForceProtection(action=openLocalEditor)
|
||||
*/
|
||||
public function validate(string $path, string $token): DataResponse {
|
||||
$pathHash = sha1($path);
|
||||
|
||||
try {
|
||||
$entity = $this->mapper->verifyToken($this->userId, $pathHash, $token);
|
||||
} catch (DoesNotExistException $e) {
|
||||
$response = new DataResponse([], Http::STATUS_NOT_FOUND);
|
||||
$response->throttle(['userId' => $this->userId, 'pathHash' => $pathHash]);
|
||||
return $response;
|
||||
}
|
||||
|
||||
$this->mapper->delete($entity);
|
||||
|
||||
if ($entity->getExpirationTime() <= $this->timeFactory->getTime()) {
|
||||
$response = new DataResponse([], Http::STATUS_NOT_FOUND);
|
||||
$response->throttle(['userId' => $this->userId, 'pathHash' => $pathHash]);
|
||||
return $response;
|
||||
}
|
||||
|
||||
return new DataResponse([
|
||||
'userId' => $this->userId,
|
||||
'pathHash' => $pathHash,
|
||||
'expirationTime' => $entity->getExpirationTime(),
|
||||
'token' => $entity->getToken(),
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -266,7 +266,7 @@ class ViewController extends Controller {
|
||||
$nav->assign('quota', $storageInfo['quota']);
|
||||
$nav->assign('usage_relative', $storageInfo['relative']);
|
||||
|
||||
$nav->assign('webdav_url', \OCP\Util::linkToRemote('dav/files/' . $user));
|
||||
$nav->assign('webdav_url', \OCP\Util::linkToRemote('dav/files/' . rawurlencode($user)));
|
||||
|
||||
$contentItems = [];
|
||||
|
||||
|
||||
60
apps/files/lib/Db/OpenLocalEditor.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
|
||||
*
|
||||
* @author Joas Schilling <coding@schilljs.com>
|
||||
*
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OCA\Files\Db;
|
||||
|
||||
use OCP\AppFramework\Db\Entity;
|
||||
|
||||
/**
|
||||
* @method void setUserId(string $userId)
|
||||
* @method string getUserId()
|
||||
* @method void setPathHash(string $pathHash)
|
||||
* @method string getPathHash()
|
||||
* @method void setExpirationTime(int $expirationTime)
|
||||
* @method int getExpirationTime()
|
||||
* @method void setToken(string $token)
|
||||
* @method string getToken()
|
||||
*/
|
||||
class OpenLocalEditor extends Entity {
|
||||
/** @var string */
|
||||
protected $userId;
|
||||
|
||||
/** @var string */
|
||||
protected $pathHash;
|
||||
|
||||
/** @var int */
|
||||
protected $expirationTime;
|
||||
|
||||
/** @var string */
|
||||
protected $token;
|
||||
|
||||
public function __construct() {
|
||||
$this->addType('userId', 'string');
|
||||
$this->addType('pathHash', 'string');
|
||||
$this->addType('expirationTime', 'integer');
|
||||
$this->addType('token', 'string');
|
||||
}
|
||||
}
|
||||
65
apps/files/lib/Db/OpenLocalEditorMapper.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
|
||||
*
|
||||
* @author Joas Schilling <coding@schilljs.com>
|
||||
*
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OCA\Files\Db;
|
||||
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
|
||||
use OCP\AppFramework\Db\QBMapper;
|
||||
use OCP\DB\Exception;
|
||||
use OCP\IDBConnection;
|
||||
|
||||
class OpenLocalEditorMapper extends QBMapper {
|
||||
public function __construct(IDBConnection $db) {
|
||||
parent::__construct($db, 'open_local_editor', OpenLocalEditor::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws DoesNotExistException
|
||||
* @throws MultipleObjectsReturnedException
|
||||
* @throws Exception
|
||||
*/
|
||||
public function verifyToken(string $userId, string $pathHash, string $token): OpenLocalEditor {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
|
||||
$qb->select('*')
|
||||
->from($this->getTableName())
|
||||
->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId)))
|
||||
->andWhere($qb->expr()->eq('path_hash', $qb->createNamedParameter($pathHash)))
|
||||
->andWhere($qb->expr()->eq('token', $qb->createNamedParameter($token)));
|
||||
|
||||
return $this->findEntity($qb);
|
||||
}
|
||||
|
||||
public function deleteExpiredTokens(int $time): void {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
|
||||
$qb->delete($this->getTableName())
|
||||
->where($qb->expr()->lt('expiration_time', $qb->createNamedParameter($time)));
|
||||
|
||||
$qb->executeStatement();
|
||||
}
|
||||
}
|
||||
69
apps/files/lib/Migration/Version12101Date20221011153334.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
|
||||
*
|
||||
* @author Joas Schilling <coding@schilljs.com>
|
||||
*
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OCA\Files\Migration;
|
||||
|
||||
use Closure;
|
||||
use OCP\DB\ISchemaWrapper;
|
||||
use OCP\DB\Types;
|
||||
use OCP\Migration\IOutput;
|
||||
use OCP\Migration\SimpleMigrationStep;
|
||||
|
||||
class Version12101Date20221011153334 extends SimpleMigrationStep {
|
||||
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
|
||||
/** @var ISchemaWrapper $schema */
|
||||
$schema = $schemaClosure();
|
||||
|
||||
$table = $schema->createTable('open_local_editor');
|
||||
$table->addColumn('id',Types::BIGINT, [
|
||||
'autoincrement' => true,
|
||||
'notnull' => true,
|
||||
'length' => 20,
|
||||
'unsigned' => true,
|
||||
]);
|
||||
$table->addColumn('user_id', Types::STRING, [
|
||||
'notnull' => true,
|
||||
'length' => 64,
|
||||
]);
|
||||
$table->addColumn('path_hash', Types::STRING, [
|
||||
'notnull' => true,
|
||||
'length' => 64,
|
||||
]);
|
||||
$table->addColumn('expiration_time', Types::BIGINT, [
|
||||
'notnull' => true,
|
||||
'unsigned' => true,
|
||||
]);
|
||||
$table->addColumn('token', Types::STRING, [
|
||||
'notnull' => true,
|
||||
'length' => 128,
|
||||
]);
|
||||
|
||||
$table->setPrimaryKey(['id']);
|
||||
$table->addUniqueIndex(['user_id', 'path_hash', 'token'], 'openlocal_user_path_token');
|
||||
|
||||
return $schema;
|
||||
}
|
||||
}
|
||||
@@ -47,13 +47,17 @@
|
||||
}
|
||||
|
||||
#imgframe img {
|
||||
max-height: calc(100vh - var(--header-height) - 65px - 200px) !important;
|
||||
max-height: calc(100vh - var(--header-height) - 65px - 200px - 16px) !important;
|
||||
max-width: 100% !important;
|
||||
width: unset !important;
|
||||
}
|
||||
|
||||
#imgframe :not(#viewer) img {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
#imgframe video {
|
||||
max-height: calc(100vh - var(--header-height) - 65px - 200px);
|
||||
max-height: calc(100vh - var(--header-height) - 65px - 200px - 16px);
|
||||
}
|
||||
|
||||
#imgframe audio {
|
||||
@@ -94,8 +98,8 @@
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.app-files_sharing #app-content {
|
||||
max-height: calc(100vh - var(--header-height) - 65px);
|
||||
.app-files_sharing #app-content footer {
|
||||
position: sticky !important;
|
||||
}
|
||||
|
||||
/* fix multiselect bar offset on shared page */
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"sourceRoot":"","sources":["../../../core/css/variables.scss","public.scss"],"names":[],"mappings":";AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;ACKA;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;;;AAGD;EACC;EACA;EACA;;;AAGD;EAEI;;;AAGJ;EACI;;;AAGJ;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;;;AAGD;EACC;EACA;;;AAGD;EACC;;;AAID;EACI;;;AAGJ;AACA;EACC;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;AACA;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;;;AAGD;AACA;EACC;;;AAED;AAAA;AAAA;EAGC;EACA;;;AAED;AAAA;AAAA;AAGC;EACA;;;AAGD;EACC;;;AAIA;EACC;;;AAIF;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;;;AAGD;EACC;;;AAGD;AAAA;EAEC;EACA;EACA;;;AAGD;EACC;EACA;EACA;;AACA;EACC;EACA;;;AAIF;EACC;EACA;;;AAKD;EAII;IACC;;;AAQL;EAGG;IACC","file":"public.css"}
|
||||
{"version":3,"sourceRoot":"","sources":["../../../core/css/variables.scss","public.scss"],"names":[],"mappings":";AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;ACMA;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACI;;;AAGJ;EACC;;;AAGD;EACC;EACA;EACA;;;AAGD;EAEI;;;AAGJ;EACI;;;AAGJ;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;;;AAGD;EACC;EACA;;;AAGD;EACC;;;AAID;EACC;;;AAGD;AACA;EACC;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;AACA;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;;;AAGD;AACA;EACC;;;AAED;AAAA;AAAA;EAGC;EACA;;;AAED;AAAA;AAAA;AAGC;EACA;;;AAGD;EACC;;;AAIA;EACC;;;AAIF;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;;;AAGD;EACC;;;AAGD;AAAA;EAEC;EACA;EACA;;;AAGD;EACC;EACA;EACA;;AACA;EACC;EACA;;;AAIF;EACC;EACA;;;AAKD;EAII;IACC;;;AAQL;EAGG;IACC","file":"public.css"}
|
||||
@@ -1,6 +1,7 @@
|
||||
@use 'variables';
|
||||
|
||||
$footer-height: 65px;
|
||||
$footer-padding-height: 16px;
|
||||
$download-button-section-height: 200px;
|
||||
|
||||
#preview {
|
||||
@@ -30,13 +31,17 @@ $download-button-section-height: 200px;
|
||||
}
|
||||
|
||||
#imgframe img {
|
||||
max-height: calc(100vh - var(--header-height) - #{$footer-height} - #{$download-button-section-height}) !important;
|
||||
max-height: calc(100vh - var(--header-height) - #{$footer-height} - #{$download-button-section-height} - #{$footer-padding-height}) !important;
|
||||
max-width: 100% !important;
|
||||
width: unset !important;
|
||||
}
|
||||
|
||||
#imgframe :not(#viewer) img {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
#imgframe video {
|
||||
max-height: calc(100vh - var(--header-height) - #{$footer-height} - #{$download-button-section-height});
|
||||
max-height: calc(100vh - var(--header-height) - #{$footer-height} - #{$download-button-section-height} - #{$footer-padding-height});
|
||||
}
|
||||
|
||||
#imgframe audio {
|
||||
@@ -78,9 +83,9 @@ $download-button-section-height: 200px;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
// Fix footer overlapping with app-content
|
||||
.app-files_sharing #app-content {
|
||||
max-height: calc(100vh - var(--header-height) - #{$footer-height});
|
||||
|
||||
.app-files_sharing #app-content footer {
|
||||
position: sticky !important;
|
||||
}
|
||||
|
||||
/* fix multiselect bar offset on shared page */
|
||||
|
||||
@@ -47,13 +47,17 @@
|
||||
}
|
||||
|
||||
#imgframe img {
|
||||
max-height: calc(100vh - var(--header-height) - 65px - 200px) !important;
|
||||
max-height: calc(100vh - var(--header-height) - 65px - 200px - 16px) !important;
|
||||
max-width: 100% !important;
|
||||
width: unset !important;
|
||||
}
|
||||
|
||||
#imgframe :not(#viewer) img {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
#imgframe video {
|
||||
max-height: calc(100vh - var(--header-height) - 65px - 200px);
|
||||
max-height: calc(100vh - var(--header-height) - 65px - 200px - 16px);
|
||||
}
|
||||
|
||||
#imgframe audio {
|
||||
@@ -94,8 +98,8 @@
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.app-files_sharing #app-content {
|
||||
max-height: calc(100vh - var(--header-height) - 65px);
|
||||
.app-files_sharing #app-content footer {
|
||||
position: sticky !important;
|
||||
}
|
||||
|
||||
/* fix multiselect bar offset on shared page */
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"sourceRoot":"","sources":["../../../core/css/variables.scss","public.scss","mobile.scss"],"names":[],"mappings":";AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;ACKA;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;;;AAGD;EACC;EACA;EACA;;;AAGD;EAEI;;;AAGJ;EACI;;;AAGJ;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;;;AAGD;EACC;EACA;;;AAGD;EACC;;;AAID;EACI;;;AAGJ;AACA;EACC;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;AACA;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;;;AAGD;AACA;EACC;;;AAED;AAAA;AAAA;EAGC;EACA;;;AAED;AAAA;AAAA;AAGC;EACA;;;AAGD;EACC;;;AAIA;EACC;;;AAIF;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;;;AAGD;EACC;;;AAGD;AAAA;EAEC;EACA;EACA;;;AAGD;EACC;EACA;EACA;;AACA;EACC;EACA;;;AAIF;EACC;EACA;;;AAKD;EAII;IACC;;;AAQL;EAGG;IACC;;;ADpQJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AEEA;AAEA;EACA;IACC;;;AAGD;EACA;AAAA;AAAA;AAAA;IAIC;;;AAGD;EACA;IACC;;;AAGD;EACA;IACC;IACA;;;AAED;EACA;IACC;;;AAGD;EACA;IACC;;;AAED;EACA;IACC;;;AAGD;EACA;IACC;IACA;IACA;IACA;;;EAGD;IACI;IACA;;;EAEJ;IACC;;;EAGD;IACC","file":"publicView.css"}
|
||||
{"version":3,"sourceRoot":"","sources":["../../../core/css/variables.scss","public.scss","mobile.scss"],"names":[],"mappings":";AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;ACMA;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACI;;;AAGJ;EACC;;;AAGD;EACC;EACA;EACA;;;AAGD;EAEI;;;AAGJ;EACI;;;AAGJ;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;;;AAGD;EACC;EACA;;;AAGD;EACC;;;AAID;EACC;;;AAGD;AACA;EACC;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;AACA;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;;;AAGD;AACA;EACC;;;AAED;AAAA;AAAA;EAGC;EACA;;;AAED;AAAA;AAAA;AAGC;EACA;;;AAGD;EACC;;;AAIA;EACC;;;AAIF;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;;;AAGD;EACC;;;AAGD;AAAA;EAEC;EACA;EACA;;;AAGD;EACC;EACA;EACA;;AACA;EACC;EACA;;;AAIF;EACC;EACA;;;AAKD;EAII;IACC;;;AAQL;EAGG;IACC;;;ADzQJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AEEA;AAEA;EACA;IACC;;;AAGD;EACA;AAAA;AAAA;AAAA;IAIC;;;AAGD;EACA;IACC;;;AAGD;EACA;IACC;IACA;;;AAED;EACA;IACC;;;AAGD;EACA;IACC;;;AAED;EACA;IACC;;;AAGD;EACA;IACC;IACA;IACA;IACA;;;EAGD;IACI;IACA;;;EAEJ;IACC;;;EAGD;IACC","file":"publicView.css"}
|
||||
@@ -62,6 +62,11 @@ OCA.Sharing.PublicApp = {
|
||||
|
||||
// file list mode ?
|
||||
if ($el.find('.files-filestable').length) {
|
||||
// Toggle for grid view
|
||||
this.$showGridView = $('input#showgridview');
|
||||
this.$showGridView.on('change', _.bind(this._onGridviewChange, this));
|
||||
$('#view-toggle').tooltip({placement: 'bottom', trigger: 'hover'});
|
||||
|
||||
var filesClient = new OC.Files.Client({
|
||||
host: OC.getHost(),
|
||||
port: OC.getPort(),
|
||||
@@ -364,6 +369,26 @@ OCA.Sharing.PublicApp = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle showing gridview by default or not
|
||||
*
|
||||
* @returns {undefined}
|
||||
*/
|
||||
_onGridviewChange: function() {
|
||||
const isGridView = this.$showGridView.is(':checked');
|
||||
this.$showGridView.next('#view-toggle')
|
||||
.removeClass('icon-toggle-filelist icon-toggle-pictures')
|
||||
.addClass(isGridView ? 'icon-toggle-filelist' : 'icon-toggle-pictures')
|
||||
this.$showGridView.next('#view-toggle').attr(
|
||||
'data-original-title',
|
||||
isGridView ? t('files', 'Show list view') : t('files', 'Show grid view'),
|
||||
)
|
||||
|
||||
if (this.fileList) {
|
||||
this.fileList.setGridView(isGridView);
|
||||
}
|
||||
},
|
||||
|
||||
_onDirectoryChanged: function (e) {
|
||||
OC.Util.History.pushState({
|
||||
// arghhhh, why is this not called "dir" !?
|
||||
|
||||
@@ -179,11 +179,6 @@
|
||||
// storage info like free space / used space
|
||||
},
|
||||
|
||||
updateRow: function($tr, fileInfo, options) {
|
||||
// no-op, suppress re-rendering
|
||||
return $tr
|
||||
},
|
||||
|
||||
reload: function() {
|
||||
this.showMask()
|
||||
if (this._reloadCall) {
|
||||
|
||||
@@ -1555,7 +1555,7 @@ class ShareAPIController extends OCSController {
|
||||
*/
|
||||
private function parseDate(string $expireDate): \DateTime {
|
||||
try {
|
||||
$date = new \DateTime($expireDate);
|
||||
$date = new \DateTime(trim($expireDate, "\""));
|
||||
} catch (\Exception $e) {
|
||||
throw new \Exception('Invalid date. Format must be YYYY-MM-DD');
|
||||
}
|
||||
|
||||
@@ -95,20 +95,15 @@
|
||||
</NcActionCheckbox>
|
||||
<NcActionInput v-if="hasExpirationDate"
|
||||
ref="expireDate"
|
||||
v-tooltip.auto="{
|
||||
content: errors.expireDate,
|
||||
show: errors.expireDate,
|
||||
trigger: 'manual'
|
||||
}"
|
||||
:is-native-picker="true"
|
||||
:hide-label="true"
|
||||
:class="{ error: errors.expireDate}"
|
||||
:disabled="saving"
|
||||
:lang="lang"
|
||||
:value="share.expireDate"
|
||||
value-type="format"
|
||||
icon="icon-calendar-dark"
|
||||
type="date"
|
||||
:disabled-date="disabledDate"
|
||||
@update:value="onExpirationChange">
|
||||
:min="dateTomorrow"
|
||||
:max="dateMaxEnforced"
|
||||
@input="onExpirationChange">
|
||||
{{ t('files_sharing', 'Enter a date') }}
|
||||
</NcActionInput>
|
||||
|
||||
@@ -380,21 +375,20 @@ export default {
|
||||
},
|
||||
set(enabled) {
|
||||
this.share.expireDate = enabled
|
||||
? this.config.defaultInternalExpirationDateString !== ''
|
||||
? this.config.defaultInternalExpirationDateString
|
||||
: moment().format('YYYY-MM-DD')
|
||||
? this.config.defaultInternalExpirationDate !== ''
|
||||
? this.config.defaultInternalExpirationDate
|
||||
: new Date()
|
||||
: ''
|
||||
},
|
||||
},
|
||||
|
||||
dateMaxEnforced() {
|
||||
if (!this.isRemote) {
|
||||
return this.config.isDefaultInternalExpireDateEnforced
|
||||
&& moment().add(1 + this.config.defaultInternalExpireDate, 'days')
|
||||
} else {
|
||||
return this.config.isDefaultRemoteExpireDateEnforced
|
||||
&& moment().add(1 + this.config.defaultRemoteExpireDate, 'days')
|
||||
if (!this.isRemote && this.config.isDefaultInternalExpireDateEnforced) {
|
||||
return new Date(new Date().setDate(new Date().getDate() + 1 + this.config.defaultInternalExpireDate))
|
||||
} else if (this.config.isDefaultRemoteExpireDateEnforced) {
|
||||
return new Date(new Date().setDate(new Date().getDate() + 1 + this.config.defaultRemoteExpireDate))
|
||||
}
|
||||
return null
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
|
||||
<template>
|
||||
<ul>
|
||||
<SharingEntrySimple class="sharing-entry__internal"
|
||||
<SharingEntrySimple ref="shareEntrySimple"
|
||||
class="sharing-entry__internal"
|
||||
:title="t('files_sharing', 'Internal link')"
|
||||
:subtitle="internalLinkSubtitle">
|
||||
<template #avatar>
|
||||
<div class="avatar-external icon-external-white" />
|
||||
</template>
|
||||
|
||||
<NcActionLink ref="copyButton"
|
||||
:href="internalLink"
|
||||
<NcActionLink :href="internalLink"
|
||||
:aria-label="t('files_sharing', 'Copy internal link to clipboard')"
|
||||
target="_blank"
|
||||
:icon="copied && copySuccess ? 'icon-checkmark-color' : 'icon-clippy'"
|
||||
@@ -84,8 +84,8 @@ export default {
|
||||
async copyLink() {
|
||||
try {
|
||||
await this.$copyText(this.internalLink)
|
||||
// focus and show the tooltip
|
||||
this.$refs.copyButton.$el.focus()
|
||||
// focus and show the tooltip (note: cannot set ref on NcActionLink)
|
||||
this.$refs.shareEntrySimple.$refs.actionsComponent.$el.focus()
|
||||
this.copySuccess = true
|
||||
this.copied = true
|
||||
} catch (error) {
|
||||
|
||||
@@ -98,20 +98,13 @@
|
||||
</NcActionText>
|
||||
<NcActionInput v-if="pendingExpirationDate"
|
||||
v-model="share.expireDate"
|
||||
v-tooltip.auto="{
|
||||
content: errors.expireDate,
|
||||
show: errors.expireDate,
|
||||
trigger: 'manual',
|
||||
defaultContainer: '#app-sidebar'
|
||||
}"
|
||||
class="share-link-expire-date"
|
||||
:disabled="saving"
|
||||
|
||||
:lang="lang"
|
||||
icon=""
|
||||
:is-native-picker="true"
|
||||
:hide-label="true"
|
||||
type="date"
|
||||
value-type="format"
|
||||
:disabled-date="disabledDate">
|
||||
:min="dateTomorrow"
|
||||
:max="dateMaxEnforced">
|
||||
<!-- let's not submit when picked, the user
|
||||
might want to still edit or copy the password -->
|
||||
{{ t('files_sharing', 'Enter a date') }}
|
||||
@@ -220,22 +213,16 @@
|
||||
</NcActionCheckbox>
|
||||
<NcActionInput v-if="hasExpirationDate"
|
||||
ref="expireDate"
|
||||
v-tooltip.auto="{
|
||||
content: errors.expireDate,
|
||||
show: errors.expireDate,
|
||||
trigger: 'manual',
|
||||
defaultContainer: '#app-sidebar'
|
||||
}"
|
||||
:is-native-picker="true"
|
||||
:hide-label="true"
|
||||
class="share-link-expire-date"
|
||||
:class="{ error: errors.expireDate}"
|
||||
:disabled="saving"
|
||||
:lang="lang"
|
||||
:value="share.expireDate"
|
||||
value-type="format"
|
||||
icon="icon-calendar-dark"
|
||||
type="date"
|
||||
:disabled-date="disabledDate"
|
||||
@update:value="onExpirationChange">
|
||||
:min="dateTomorrow"
|
||||
:max="dateMaxEnforced"
|
||||
@input="onExpirationChange">
|
||||
{{ t('files_sharing', 'Enter a date') }}
|
||||
</NcActionInput>
|
||||
|
||||
@@ -435,20 +422,22 @@ export default {
|
||||
|| !!this.share.expireDate
|
||||
},
|
||||
set(enabled) {
|
||||
let dateString = moment(this.config.defaultExpirationDateString)
|
||||
if (!dateString.isValid()) {
|
||||
dateString = moment()
|
||||
let defaultExpirationDate = this.config.defaultExpirationDate
|
||||
if (!defaultExpirationDate) {
|
||||
defaultExpirationDate = new Date()
|
||||
}
|
||||
this.share.state.expiration = enabled
|
||||
? dateString.format('YYYY-MM-DD')
|
||||
? defaultExpirationDate
|
||||
: ''
|
||||
console.debug('Expiration date status', enabled, this.share.expireDate)
|
||||
},
|
||||
},
|
||||
|
||||
dateMaxEnforced() {
|
||||
return this.config.isDefaultExpireDateEnforced
|
||||
&& moment().add(1 + this.config.defaultExpireDate, 'days')
|
||||
if (this.config.isDefaultExpireDateEnforced) {
|
||||
return new Date(new Date().setDate(new Date().getDate() + 1 + this.config.defaultExpireDate))
|
||||
}
|
||||
return null
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -631,7 +620,7 @@ export default {
|
||||
if (this.config.isDefaultExpireDateEnforced) {
|
||||
// default is empty string if not set
|
||||
// expiration is the share object key, not expireDate
|
||||
shareDefaults.expiration = this.config.defaultExpirationDateString
|
||||
shareDefaults.expiration = this.config.defaultExpirationDate
|
||||
}
|
||||
if (this.config.enableLinkPasswordByDefault) {
|
||||
shareDefaults.password = await GeneratePassword()
|
||||
|
||||
@@ -29,7 +29,8 @@
|
||||
{{ subtitle }}
|
||||
</p>
|
||||
</div>
|
||||
<NcActions v-if="$slots['default']"
|
||||
<NcActions ref="actionsComponent"
|
||||
v-if="$slots['default']"
|
||||
class="sharing-entry__actions"
|
||||
menu-align="right"
|
||||
:aria-expanded="ariaExpandedValue">
|
||||
|
||||
@@ -97,7 +97,7 @@ export default {
|
||||
},
|
||||
|
||||
dateTomorrow() {
|
||||
return moment().add(1, 'days')
|
||||
return new Date(new Date().setDate(new Date().getDate() + 1))
|
||||
},
|
||||
|
||||
// Datepicker language
|
||||
@@ -142,7 +142,7 @@ export default {
|
||||
}
|
||||
}
|
||||
if (share.expirationDate) {
|
||||
const date = moment(share.expirationDate)
|
||||
const date = share.expirationDate
|
||||
if (!date.isValid()) {
|
||||
return false
|
||||
}
|
||||
@@ -151,16 +151,12 @@ export default {
|
||||
},
|
||||
|
||||
/**
|
||||
* ActionInput can be a little tricky to work with.
|
||||
* Since we expect a string and not a Date,
|
||||
* we need to process the value here
|
||||
* Save given value to expireDate and trigger queueUpdate
|
||||
*
|
||||
* @param {Date} date js date to be parsed by moment.js
|
||||
* @param {Date} date
|
||||
*/
|
||||
onExpirationChange(date) {
|
||||
// format to YYYY-MM-DD
|
||||
const value = moment(date).format('YYYY-MM-DD')
|
||||
this.share.expireDate = value
|
||||
this.share.expireDate = date
|
||||
this.queueUpdate('expireDate')
|
||||
},
|
||||
|
||||
@@ -318,17 +314,5 @@ export default {
|
||||
debounceQueueUpdate: debounce(function(property) {
|
||||
this.queueUpdate(property)
|
||||
}, 500),
|
||||
|
||||
/**
|
||||
* Returns which dates are disabled for the datepicker
|
||||
*
|
||||
* @param {Date} date date to check
|
||||
* @return {boolean}
|
||||
*/
|
||||
disabledDate(date) {
|
||||
const dateMoment = moment(date)
|
||||
return (this.dateTomorrow && dateMoment.isBefore(this.dateTomorrow, 'day'))
|
||||
|| (this.dateMaxEnforced && dateMoment.isSameOrAfter(this.dateMaxEnforced, 'day'))
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -248,9 +248,9 @@ export default class Share {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the expiration date as a string format
|
||||
* Get the expiration date
|
||||
*
|
||||
* @return {string}
|
||||
* @return {Date|null}
|
||||
* @readonly
|
||||
* @memberof Share
|
||||
*/
|
||||
@@ -259,10 +259,9 @@ export default class Share {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the expiration date as a string format
|
||||
* e.g. YYYY-MM-DD
|
||||
* Set the expiration date
|
||||
*
|
||||
* @param {string} date the share expiration date
|
||||
* @param {Date|null} date the share expiration date
|
||||
* @memberof Share
|
||||
*/
|
||||
set expireDate(date) {
|
||||
|
||||
@@ -60,57 +60,45 @@ export default class Config {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default link share expiration date as string
|
||||
* Get the default link share expiration date
|
||||
*
|
||||
* @return {string}
|
||||
* @return {Date|null}
|
||||
* @readonly
|
||||
* @memberof Config
|
||||
*/
|
||||
get defaultExpirationDateString() {
|
||||
let expireDateString = ''
|
||||
get defaultExpirationDate() {
|
||||
if (this.isDefaultExpireDateEnabled) {
|
||||
const date = window.moment.utc()
|
||||
const expireAfterDays = this.defaultExpireDate
|
||||
date.add(expireAfterDays, 'days')
|
||||
expireDateString = date.format('YYYY-MM-DD')
|
||||
return new Date(new Date().setDate(new Date().getDate() + this.defaultExpireDate))
|
||||
}
|
||||
return expireDateString
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default internal expiration date as string
|
||||
* Get the default internal expiration date
|
||||
*
|
||||
* @return {string}
|
||||
* @return {Date|null}
|
||||
* @readonly
|
||||
* @memberof Config
|
||||
*/
|
||||
get defaultInternalExpirationDateString() {
|
||||
let expireDateString = ''
|
||||
get defaultInternalExpirationDate() {
|
||||
if (this.isDefaultInternalExpireDateEnabled) {
|
||||
const date = window.moment.utc()
|
||||
const expireAfterDays = this.defaultInternalExpireDate
|
||||
date.add(expireAfterDays, 'days')
|
||||
expireDateString = date.format('YYYY-MM-DD')
|
||||
return new Date(new Date().setDate(new Date().getDate() + this.defaultInternalExpireDate))
|
||||
}
|
||||
return expireDateString
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default remote expiration date as string
|
||||
* Get the default remote expiration date
|
||||
*
|
||||
* @return {string}
|
||||
* @return {Date|null}
|
||||
* @readonly
|
||||
* @memberof Config
|
||||
*/
|
||||
get defaultRemoteExpirationDateString() {
|
||||
let expireDateString = ''
|
||||
if (this.isDefaultRemoteExpireDateEnabled) {
|
||||
const date = window.moment.utc()
|
||||
const expireAfterDays = this.defaultRemoteExpireDate
|
||||
date.add(expireAfterDays, 'days')
|
||||
expireDateString = date.format('YYYY-MM-DD')
|
||||
return new Date(new Date().setDate(new Date().getDate() + this.defaultRemoteExpireDate))
|
||||
}
|
||||
return expireDateString
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -190,6 +178,17 @@ export default class Config {
|
||||
return OC.appConfig.core.defaultInternalExpireDateEnabled === true
|
||||
}
|
||||
|
||||
/**
|
||||
* Is there a default expiration date for new remote shares ?
|
||||
*
|
||||
* @return {boolean}
|
||||
* @readonly
|
||||
* @memberof Config
|
||||
*/
|
||||
get isDefaultRemoteExpireDateEnabled() {
|
||||
return OC.appConfig.core.defaultRemoteExpireDateEnabled === true
|
||||
}
|
||||
|
||||
/**
|
||||
* Are users on this server allowed to send shares to other servers ?
|
||||
*
|
||||
|
||||
@@ -389,6 +389,9 @@ class UsersController extends AUserData {
|
||||
}
|
||||
|
||||
$generatePasswordResetToken = false;
|
||||
if (strlen($password) > 469) {
|
||||
throw new OCSException('Invalid password value', 101);
|
||||
}
|
||||
if ($password === '') {
|
||||
if ($email === '') {
|
||||
throw new OCSException('To send a password link to the user an email address is required.', 108);
|
||||
@@ -882,6 +885,9 @@ class UsersController extends AUserData {
|
||||
break;
|
||||
case self::USER_FIELD_PASSWORD:
|
||||
try {
|
||||
if (strlen($value) > 469) {
|
||||
throw new OCSException('Invalid password value', 102);
|
||||
}
|
||||
if (!$targetUser->canChangePassword()) {
|
||||
throw new OCSException('Setting the password is not supported by the users backend', 103);
|
||||
}
|
||||
|
||||
@@ -379,7 +379,7 @@ class CheckSetupController extends Controller {
|
||||
return true;
|
||||
}
|
||||
|
||||
// there are two different memcached modules for PHP
|
||||
// there are two different memcache modules for PHP
|
||||
// we only support memcached and not memcache
|
||||
// https://code.google.com/p/memcached/wiki/PHPClientComparison
|
||||
return !(!extension_loaded('memcached') && extension_loaded('memcache'));
|
||||
@@ -392,7 +392,7 @@ class CheckSetupController extends Controller {
|
||||
*/
|
||||
private function isSettimelimitAvailable() {
|
||||
if (function_exists('set_time_limit')
|
||||
&& strpos(@ini_get('disable_functions'), 'set_time_limit') === false) {
|
||||
&& strpos(ini_get('disable_functions'), 'set_time_limit') === false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -819,12 +819,12 @@ Raw output
|
||||
|
||||
$tempPath = sys_get_temp_dir();
|
||||
if (!is_dir($tempPath)) {
|
||||
$this->logger->error('Error while checking the temporary PHP path - it was not properly set to a directory. value: ' . $tempPath);
|
||||
$this->logger->error('Error while checking the temporary PHP path - it was not properly set to a directory. Returned value: ' . $tempPath);
|
||||
return false;
|
||||
}
|
||||
$freeSpaceInTemp = disk_free_space($tempPath);
|
||||
$freeSpaceInTemp = function_exists('disk_free_space') ? disk_free_space($tempPath) : false;
|
||||
if ($freeSpaceInTemp === false) {
|
||||
$this->logger->error('Error while checking the available disk space of temporary PHP path - no free disk space returned. temporary path: ' . $tempPath);
|
||||
$this->logger->error('Error while checking the available disk space of temporary PHP path or no free disk space returned. Temporary path: ' . $tempPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,10 +38,11 @@ class SecurityTxtHandler implements IHandler {
|
||||
}
|
||||
|
||||
$response = "Contact: https://hackerone.com/nextcloud
|
||||
Expires: 2021-12-31T23:00:00.000Z
|
||||
Expires: 2023-04-31T23:00:00.000Z
|
||||
Acknowledgments: https://hackerone.com/nextcloud/thanks
|
||||
Acknowledgments: https://github.com/nextcloud/security-advisories/security/advisories
|
||||
Policy: https://hackerone.com/nextcloud";
|
||||
Policy: https://hackerone.com/nextcloud
|
||||
Preferred-Languages: en";
|
||||
|
||||
return new GenericResponse(new TextPlainResponse($response, 200));
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
ref="newuserpassword"
|
||||
v-model="newUser.password"
|
||||
:minlength="minPasswordLength"
|
||||
:maxlength="469"
|
||||
:placeholder="t('settings', 'Password')"
|
||||
:required="newUser.mailAddress===''"
|
||||
autocapitalize="none"
|
||||
|
||||
@@ -31,6 +31,9 @@
|
||||
<pre-migration>
|
||||
<step>OCA\Theming\Migration\MigrateUserConfig</step>
|
||||
</pre-migration>
|
||||
<post-migration>
|
||||
<step>OCA\Theming\Migration\InitBackgroundImagesMigration</step>
|
||||
</post-migration>
|
||||
</repair-steps>
|
||||
|
||||
<commands>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
:root {
|
||||
--color-main-background: #ffffff;
|
||||
--color-main-background-not-plain: #0082c9;
|
||||
--color-main-background-rgb: 255,255,255;
|
||||
--color-main-background-translucent: rgba(var(--color-main-background-rgb), .97);
|
||||
--color-main-background-blur: rgba(var(--color-main-background-rgb), .8);
|
||||
@@ -10,23 +11,10 @@
|
||||
--color-background-darker: #dbdbdb;
|
||||
--color-placeholder-light: #e6e6e6;
|
||||
--color-placeholder-dark: #cccccc;
|
||||
--color-primary: #0082c9;
|
||||
--color-primary-text: #ffffff;
|
||||
--color-primary-hover: #329bd3;
|
||||
--color-primary-light: #e5f2f9;
|
||||
--color-primary-light-text: #0082c9;
|
||||
--color-primary-light-hover: #dbe7ee;
|
||||
--color-primary-text-dark: #ededed;
|
||||
--color-primary-element: #0082c9;
|
||||
--color-primary-element-text: #ffffff;
|
||||
--color-primary-element-hover: #329bd3;
|
||||
--color-primary-element-light: #e5f2f9;
|
||||
--color-primary-element-light-text: #0082c9;
|
||||
--color-primary-element-light-hover: #dbe7ee;
|
||||
--color-primary-element-text-dark: #ededed;
|
||||
--gradient-primary-background: linear-gradient(40deg, var(--color-primary) 0%, var(--color-primary-hover) 100%);
|
||||
--color-main-text: #222222;
|
||||
--color-text-maxcontrast: #767676;
|
||||
--color-text-maxcontrast-default: #767676;
|
||||
--color-text-maxcontrast-background-blur: #646464;
|
||||
--color-text-light: #222222;
|
||||
--color-text-lighter: #767676;
|
||||
--color-scrollbar: rgba(34,34,34, .15);
|
||||
@@ -68,4 +56,20 @@
|
||||
--background-invert-if-dark: no;
|
||||
--background-invert-if-bright: invert(100%);
|
||||
--image-main-background: url('/core/img/app-background.jpg');
|
||||
--color-primary: #00639a;
|
||||
--color-primary-default: #0082c9;
|
||||
--color-primary-text: #ffffff;
|
||||
--color-primary-hover: #3282ae;
|
||||
--color-primary-light: #e5eff4;
|
||||
--color-primary-light-text: #00273d;
|
||||
--color-primary-light-hover: #dbe4e9;
|
||||
--color-primary-text-dark: #ededed;
|
||||
--color-primary-element: #00639a;
|
||||
--color-primary-element-text: #ffffff;
|
||||
--color-primary-element-hover: #3282ae;
|
||||
--color-primary-element-light: #e5eff4;
|
||||
--color-primary-element-light-text: #00273d;
|
||||
--color-primary-element-light-hover: #dbe4e9;
|
||||
--color-primary-element-text-dark: #ededed;
|
||||
--gradient-primary-background: linear-gradient(40deg, var(--color-primary) 0%, var(--color-primary-hover) 100%);
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
margin-top: 10px;
|
||||
margin-bottom: 20px;
|
||||
cursor: pointer;
|
||||
background-color: var(--color-primary);
|
||||
background-color: var(--color-primary-default);
|
||||
background-image: var(--image-background, var(--image-background-plain, url("../../../core/img/app-background.jpg"), linear-gradient(40deg, #0082c9 0%, #30b6ff 100%)));
|
||||
}
|
||||
#theming #theming-preview #theming-preview-logo {
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
margin-top: 10px;
|
||||
margin-bottom: 20px;
|
||||
cursor: pointer;
|
||||
background-color: var(--color-primary);
|
||||
background-color: var(--color-primary-default);
|
||||
background-image: var(--image-background, var(--image-background-plain, url('../../../core/img/app-background.jpg'), linear-gradient(40deg, #0082c9 0%, #30b6ff 100%)));
|
||||
|
||||
#theming-preview-logo {
|
||||
@@ -145,4 +145,4 @@
|
||||
svg, img {
|
||||
transition: 500ms filter linear;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 422 KiB After Width: | Height: | Size: 419 KiB |
|
Before Width: | Height: | Size: 369 KiB After Width: | Height: | Size: 410 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 419 KiB |
BIN
apps/theming/img/light-highcontrast.jpg
Normal file
|
After Width: | Height: | Size: 443 KiB |
|
Before Width: | Height: | Size: 382 KiB After Width: | Height: | Size: 428 KiB |
@@ -34,6 +34,7 @@ use OCA\Theming\AppInfo\Application;
|
||||
use OCA\Theming\ITheme;
|
||||
use OCA\Theming\Service\BackgroundService;
|
||||
use OCA\Theming\Service\ThemesService;
|
||||
use OCA\Theming\ThemingDefaults;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\AppFramework\Http\DataResponse;
|
||||
use OCP\AppFramework\Http\FileDisplayResponse;
|
||||
@@ -53,6 +54,7 @@ class UserThemeController extends OCSController {
|
||||
private IConfig $config;
|
||||
private IUserSession $userSession;
|
||||
private ThemesService $themesService;
|
||||
private ThemingDefaults $themingDefaults;
|
||||
private BackgroundService $backgroundService;
|
||||
|
||||
/**
|
||||
@@ -63,11 +65,13 @@ class UserThemeController extends OCSController {
|
||||
IConfig $config,
|
||||
IUserSession $userSession,
|
||||
ThemesService $themesService,
|
||||
ThemingDefaults $themingDefaults,
|
||||
BackgroundService $backgroundService) {
|
||||
parent::__construct($appName, $request);
|
||||
$this->config = $config;
|
||||
$this->userSession = $userSession;
|
||||
$this->themesService = $themesService;
|
||||
$this->themingDefaults = $themingDefaults;
|
||||
$this->backgroundService = $backgroundService;
|
||||
$this->userId = $userSession->getUser()->getUID();
|
||||
}
|
||||
@@ -177,6 +181,8 @@ class UserThemeController extends OCSController {
|
||||
}
|
||||
$currentVersion++;
|
||||
$this->config->setUserValue($this->userId, Application::APP_ID, 'backgroundVersion', (string)$currentVersion);
|
||||
// FIXME replace with user-specific cachebuster increase https://github.com/nextcloud/server/issues/34472
|
||||
$this->themingDefaults->increaseCacheBuster();
|
||||
return new JSONResponse([
|
||||
'type' => $type,
|
||||
'value' => $value,
|
||||
|
||||
107
apps/theming/lib/Jobs/MigrateBackgroundImages.php
Normal file
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright Copyright (c) 2022 Arthur Schiwon <blizzz@arthur-schiwon.de>
|
||||
*
|
||||
* @author Arthur Schiwon <blizzz@arthur-schiwon.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 <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OCA\Theming\Jobs;
|
||||
|
||||
use OCA\Theming\AppInfo\Application;
|
||||
use OCP\App\IAppManager;
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
use OCP\BackgroundJob\IJobList;
|
||||
use OCP\BackgroundJob\QueuedJob;
|
||||
use OCP\Files\AppData\IAppDataFactory;
|
||||
use OCP\Files\NotFoundException;
|
||||
use OCP\Files\NotPermittedException;
|
||||
use OCP\IConfig;
|
||||
|
||||
class MigrateBackgroundImages extends QueuedJob {
|
||||
public const TIME_SENSITIVE = 0;
|
||||
|
||||
private IConfig $config;
|
||||
private IAppManager $appManager;
|
||||
private IAppDataFactory $appDataFactory;
|
||||
private IJobList $jobList;
|
||||
|
||||
public function __construct(ITimeFactory $time, IAppDataFactory $appDataFactory, IConfig $config, IAppManager $appManager, IJobList $jobList) {
|
||||
parent::__construct($time);
|
||||
$this->config = $config;
|
||||
$this->appManager = $appManager;
|
||||
$this->appDataFactory = $appDataFactory;
|
||||
$this->jobList = $jobList;
|
||||
}
|
||||
|
||||
protected function run($argument): void {
|
||||
if (!$this->appManager->isEnabledForUser('dashboard')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$themingData = $this->appDataFactory->get(Application::APP_ID);
|
||||
$dashboardData = $this->appDataFactory->get('dashboard');
|
||||
|
||||
$userIds = $this->config->getUsersForUserValue('theming', 'background', 'custom');
|
||||
|
||||
$notSoFastMode = \count($userIds) > 5000;
|
||||
$reTrigger = false;
|
||||
$processed = 0;
|
||||
|
||||
foreach ($userIds as $userId) {
|
||||
try {
|
||||
// precondition
|
||||
if ($notSoFastMode) {
|
||||
if ($this->config->getUserValue($userId, 'theming', 'background-migrated', '0') === '1') {
|
||||
// already migrated
|
||||
continue;
|
||||
}
|
||||
$reTrigger = true;
|
||||
}
|
||||
|
||||
// migration
|
||||
$file = $dashboardData->getFolder($userId)->getFile('background.jpg');
|
||||
try {
|
||||
$targetDir = $themingData->getFolder($userId);
|
||||
} catch (NotFoundException $e) {
|
||||
$targetDir = $themingData->newFolder($userId);
|
||||
}
|
||||
if (!$targetDir->fileExists('background.jpg')) {
|
||||
$targetDir->newFile('background.jpg', $file->getContent());
|
||||
}
|
||||
$file->delete();
|
||||
} catch (NotFoundException|NotPermittedException $e) {
|
||||
}
|
||||
// capture state
|
||||
if ($notSoFastMode) {
|
||||
$this->config->setUserValue($userId, 'theming', 'background-migrated', '1');
|
||||
$processed++;
|
||||
}
|
||||
if ($processed > 4999) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($reTrigger) {
|
||||
$this->jobList->add(self::class);
|
||||
}
|
||||
}
|
||||
}
|
||||
48
apps/theming/lib/Migration/InitBackgroundImagesMigration.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright Copyright (c) 2022 Arthur Schiwon <blizzz@arthur-schiwon.de>
|
||||
*
|
||||
* @author Arthur Schiwon <blizzz@arthur-schiwon.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 <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OCA\Theming\Migration;
|
||||
|
||||
use OCA\Theming\Jobs\MigrateBackgroundImages;
|
||||
use OCP\BackgroundJob\IJobList;
|
||||
use OCP\Migration\IOutput;
|
||||
|
||||
class InitBackgroundImagesMigration implements \OCP\Migration\IRepairStep {
|
||||
|
||||
private IJobList $jobList;
|
||||
|
||||
public function __construct(IJobList $jobList) {
|
||||
$this->jobList = $jobList;
|
||||
}
|
||||
|
||||
public function getName() {
|
||||
return 'Initialize migration of background images from dashboard to theming app';
|
||||
}
|
||||
|
||||
public function run(IOutput $output) {
|
||||
$this->jobList->add(MigrateBackgroundImages::class);
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ namespace OCA\Theming\Service;
|
||||
use InvalidArgumentException;
|
||||
use OC\User\NoUserException;
|
||||
use OCA\Theming\AppInfo\Application;
|
||||
use OCP\Files\AppData\IAppDataFactory;
|
||||
use OCP\Files\File;
|
||||
use OCP\Files\IAppData;
|
||||
use OCP\Files\IRootFolder;
|
||||
@@ -44,92 +45,111 @@ use OCP\PreConditionNotMetException;
|
||||
class BackgroundService {
|
||||
// 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 = '#00639a';
|
||||
|
||||
public const SHIPPED_BACKGROUNDS = [
|
||||
'anatoly-mikhaltsov-butterfly-wing-scale.jpg' => [
|
||||
'attribution' => 'Butterfly wing scale (Anatoly Mikhaltsov, CC BY-SA)',
|
||||
'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',
|
||||
'primary_color' => '#a53c17',
|
||||
],
|
||||
'bernie-cetonia-aurata-take-off-composition.jpg' => [
|
||||
'attribution' => 'Cetonia aurata take off composition (Bernie, Public Domain)',
|
||||
'attribution_url' => 'https://commons.wikimedia.org/wiki/File:Cetonia_aurata_take_off_composition_05172009.jpg',
|
||||
'theming' => self::THEMING_MODE_DARK,
|
||||
'primary_color' => '#56633d',
|
||||
],
|
||||
'dejan-krsmanovic-ribbed-red-metal.jpg' => [
|
||||
'attribution' => 'Ribbed red metal (Dejan Krsmanovic, CC BY)',
|
||||
'attribution_url' => 'https://www.flickr.com/photos/dejankrsmanovic/42971456774/',
|
||||
'primary_color' => '#9c4236',
|
||||
],
|
||||
'eduardo-neves-pedra-azul.jpg' => [
|
||||
'attribution' => 'Pedra azul milky way (Eduardo Neves, CC BY-SA)',
|
||||
'attribution_url' => 'https://commons.wikimedia.org/wiki/File:Pedra_Azul_Milky_Way.jpg',
|
||||
'primary_color' => '#4f6071',
|
||||
],
|
||||
'european-space-agency-barents-bloom.jpg' => [
|
||||
'attribution' => 'Barents bloom (European Space Agency, CC BY-SA)',
|
||||
'attribution_url' => 'https://www.esa.int/ESA_Multimedia/Images/2016/08/Barents_bloom',
|
||||
'primary_color' => '#396475',
|
||||
],
|
||||
'hannes-fritz-flippity-floppity.jpg' => [
|
||||
'attribution' => 'Flippity floppity (Hannes Fritz, CC BY-SA)',
|
||||
'attribution_url' => 'http://hannes.photos/flippity-floppity',
|
||||
'primary_color' => '#98415a',
|
||||
],
|
||||
'hannes-fritz-roulette.jpg' => [
|
||||
'attribution' => 'Roulette (Hannes Fritz, CC BY-SA)',
|
||||
'attribution_url' => 'http://hannes.photos/roulette',
|
||||
'primary_color' => '#845334',
|
||||
],
|
||||
'hannes-fritz-sea-spray.jpg' => [
|
||||
'attribution' => 'Sea spray (Hannes Fritz, CC BY-SA)',
|
||||
'attribution_url' => 'http://hannes.photos/sea-spray',
|
||||
'primary_color' => '#4f6071',
|
||||
],
|
||||
'kamil-porembinski-clouds.jpg' => [
|
||||
'attribution' => 'Clouds (Kamil Porembiński, CC BY-SA)',
|
||||
'attribution_url' => 'https://www.flickr.com/photos/paszczak000/8715851521/',
|
||||
'primary_color' => self::DEFAULT_COLOR,
|
||||
],
|
||||
'bernard-spragg-new-zealand-fern.jpg' => [
|
||||
'attribution' => 'New zealand fern (Bernard Spragg, CC0)',
|
||||
'attribution_url' => 'https://commons.wikimedia.org/wiki/File:NZ_Fern.(Blechnum_chambersii)_(11263534936).jpg',
|
||||
'primary_color' => '#316b26',
|
||||
],
|
||||
'rawpixel-pink-tapioca-bubbles.jpg' => [
|
||||
'attribution' => 'Pink tapioca bubbles (Rawpixel, CC BY)',
|
||||
'attribution_url' => 'https://www.flickr.com/photos/byrawpixel/27665140298/in/photostream/',
|
||||
'theming' => self::THEMING_MODE_DARK,
|
||||
'primary_color' => '#7b4e7e',
|
||||
],
|
||||
'nasa-waxing-crescent-moon.jpg' => [
|
||||
'attribution' => 'Waxing crescent moon (NASA, Public Domain)',
|
||||
'attribution_url' => 'https://www.nasa.gov/image-feature/a-waxing-crescent-moon',
|
||||
'primary_color' => '#005ac1',
|
||||
],
|
||||
'tommy-chau-already.jpg' => [
|
||||
'attribution' => 'Cityscape (Tommy Chau, CC BY)',
|
||||
'attribution_url' => 'https://www.flickr.com/photos/90975693@N05/16910999368',
|
||||
'primary_color' => '#6a2af4',
|
||||
],
|
||||
'tommy-chau-lion-rock-hill.jpg' => [
|
||||
'attribution' => 'Lion rock hill (Tommy Chau, CC BY)',
|
||||
'attribution_url' => 'https://www.flickr.com/photos/90975693@N05/17136440246',
|
||||
'theming' => self::THEMING_MODE_DARK,
|
||||
'primary_color' => '#7f4f70',
|
||||
],
|
||||
'lali-masriera-yellow-bricks.jpg' => [
|
||||
'attribution' => 'Yellow bricks (Lali Masriera, CC BY)',
|
||||
'attribution_url' => 'https://www.flickr.com/photos/visualpanic/3982464447',
|
||||
'theming' => self::THEMING_MODE_DARK,
|
||||
]
|
||||
'primary_color' => '#7f5700',
|
||||
],
|
||||
];
|
||||
|
||||
private IRootFolder $rootFolder;
|
||||
private IAppData $appData;
|
||||
private IConfig $config;
|
||||
private string $userId;
|
||||
private IAppDataFactory $appDataFactory;
|
||||
|
||||
public function __construct(
|
||||
IRootFolder $rootFolder,
|
||||
IAppData $appData,
|
||||
IConfig $config,
|
||||
?string $userId
|
||||
IRootFolder $rootFolder,
|
||||
IAppDataFactory $appDataFactory,
|
||||
IConfig $config,
|
||||
?string $userId
|
||||
) {
|
||||
if ($userId === null) {
|
||||
return;
|
||||
}
|
||||
$this->rootFolder = $rootFolder;
|
||||
$this->appData = $appData;
|
||||
$this->appData = $appDataFactory->get(Application::APP_ID);
|
||||
$this->config = $config;
|
||||
$this->userId = $userId;
|
||||
$this->appDataFactory = $appDataFactory;
|
||||
}
|
||||
|
||||
public function setDefaultBackground(): void {
|
||||
@@ -176,6 +196,11 @@ class BackgroundService {
|
||||
try {
|
||||
return $this->getAppDataFolder()->getFile('background.jpg');
|
||||
} catch (NotFoundException | NotPermittedException $e) {
|
||||
try {
|
||||
// Fallback can be removed in 26
|
||||
$dashboardFolder = $this->appDataFactory->get('dashboard');
|
||||
return $dashboardFolder->getFolder($this->userId)->getFile('background.jpg');
|
||||
} catch (\Throwable $t) {}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -55,6 +55,7 @@ class JSDataService implements \JsonSerializable {
|
||||
'url' => $this->themingDefaults->getBaseUrl(),
|
||||
'slogan' => $this->themingDefaults->getSlogan(),
|
||||
'color' => $this->themingDefaults->getColorPrimary(),
|
||||
'defaultColor' => $this->themingDefaults->getDefaultColorPrimary(),
|
||||
'imprintUrl' => $this->themingDefaults->getImprintUrl(),
|
||||
'privacyUrl' => $this->themingDefaults->getPrivacyUrl(),
|
||||
'inverted' => $this->util->invertTextColor($this->themingDefaults->getColorPrimary()),
|
||||
|
||||
@@ -87,9 +87,9 @@ class ThemesService {
|
||||
}
|
||||
|
||||
/** @var ITheme[] */
|
||||
$themes = array_map(function($themeId) {
|
||||
$themes = array_filter(array_map(function($themeId) {
|
||||
return $this->getThemes()[$themeId];
|
||||
}, $themesIds);
|
||||
}, $themesIds));
|
||||
|
||||
// Filtering all themes with the same type
|
||||
$filteredThemes = array_filter($themes, function(ITheme $t) use ($theme) {
|
||||
|
||||
@@ -75,7 +75,7 @@ class Admin implements IDelegatedSettings {
|
||||
'name' => $this->themingDefaults->getEntity(),
|
||||
'url' => $this->themingDefaults->getBaseUrl(),
|
||||
'slogan' => $this->themingDefaults->getSlogan(),
|
||||
'color' => $this->themingDefaults->getColorPrimary(),
|
||||
'color' => $this->themingDefaults->getDefaultColorPrimary(),
|
||||
'uploadLogoRoute' => $this->urlGenerator->linkToRoute('theming.Theming.uploadImage'),
|
||||
'canThemeIcons' => $this->imageManager->shouldReplaceIcons(),
|
||||
'iconDocs' => $this->urlGenerator->linkToDocs('admin-theming-icons'),
|
||||
|
||||
66
apps/theming/lib/Themes/CommonThemeTrait.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
|
||||
*
|
||||
* @author Joas Schilling <coding@schilljs.com>
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
namespace OCA\Theming\Themes;
|
||||
|
||||
use OCA\Theming\Util;
|
||||
|
||||
trait CommonThemeTrait {
|
||||
public Util $util;
|
||||
|
||||
/**
|
||||
* Generate primary-related variables
|
||||
* This is shared between multiple themes because colorMainBackground and colorMainText
|
||||
* will change in between.
|
||||
*/
|
||||
protected function generatePrimaryVariables(string $colorMainBackground, string $colorMainText): array {
|
||||
$colorPrimaryLight = $this->util->mix($this->primaryColor, $colorMainBackground, -80);
|
||||
$colorPrimaryElement = $this->util->elementColor($this->primaryColor);
|
||||
$colorPrimaryElementLight = $this->util->mix($colorPrimaryElement, $colorMainBackground, -80);
|
||||
|
||||
// primary related colours
|
||||
return [
|
||||
'--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,
|
||||
'--color-primary-light-text' => $this->util->mix($this->primaryColor, $this->util->invertTextColor($colorPrimaryLight) ? '#000000' : '#ffffff', -20),
|
||||
'--color-primary-light-hover' => $this->util->mix($colorPrimaryLight, $colorMainText, 90),
|
||||
'--color-primary-text-dark' => $this->util->darken($this->util->invertTextColor($this->primaryColor) ? '#000000' : '#ffffff', 7),
|
||||
|
||||
// used for buttons, inputs...
|
||||
'--color-primary-element' => $colorPrimaryElement,
|
||||
'--color-primary-element-text' => $this->util->invertTextColor($colorPrimaryElement) ? '#000000' : '#ffffff',
|
||||
'--color-primary-element-hover' => $this->util->mix($colorPrimaryElement, $colorMainBackground, 60),
|
||||
'--color-primary-element-light' => $colorPrimaryElementLight,
|
||||
'--color-primary-element-light-text' => $this->util->mix($colorPrimaryElement, $this->util->invertTextColor($colorPrimaryElementLight) ? '#000000' : '#ffffff', -20),
|
||||
'--color-primary-element-light-hover' => $this->util->mix($colorPrimaryElementLight, $colorMainText, 90),
|
||||
'--color-primary-element-text-dark' => $this->util->darken($this->util->invertTextColor($colorPrimaryElement) ? '#000000' : '#ffffff', 7),
|
||||
|
||||
// to use like this: background-image: var(--gradient-primary-background);
|
||||
'--gradient-primary-background' => 'linear-gradient(40deg, var(--color-primary) 0%, var(--color-primary-hover) 100%)',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -49,42 +49,51 @@ class DarkHighContrastTheme extends DarkTheme implements ITheme {
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to keep this consistent with HighContrastTheme
|
||||
* Keep this consistent with other HighContrast Themes
|
||||
*/
|
||||
public function getCSSVariables(): array {
|
||||
$variables = parent::getCSSVariables();
|
||||
$defaultVariables = parent::getCSSVariables();
|
||||
|
||||
$colorMainText = '#ffffff';
|
||||
$colorMainBackground = '#000000';
|
||||
$colorMainBackgroundRGB = join(',', $this->util->hexToRGB($colorMainBackground));
|
||||
|
||||
$variables['--color-main-background'] = $colorMainBackground;
|
||||
$variables['--color-main-background-translucent'] = 'rgba(var(--color-main-background-rgb), .1)';
|
||||
$variables['--color-main-text'] = $colorMainText;
|
||||
return array_merge(
|
||||
$defaultVariables,
|
||||
$this->generatePrimaryVariables($colorMainBackground, $colorMainText),
|
||||
[
|
||||
'--color-main-background' => $colorMainBackground,
|
||||
'--color-main-background-rgb' => $colorMainBackgroundRGB,
|
||||
'--color-main-background-translucent' => 'rgba(var(--color-main-background-rgb), 1)',
|
||||
'--color-main-text' => $colorMainText,
|
||||
|
||||
$variables['--color-background-dark'] = $this->util->lighten($colorMainBackground, 30);
|
||||
$variables['--color-background-darker'] = $this->util->lighten($colorMainBackground, 30);
|
||||
'--color-background-dark' => $this->util->lighten($colorMainBackground, 30),
|
||||
'--color-background-darker' => $this->util->lighten($colorMainBackground, 30),
|
||||
|
||||
$variables['--color-placeholder-light'] = $this->util->lighten($colorMainBackground, 30);
|
||||
$variables['--color-placeholder-dark'] = $this->util->lighten($colorMainBackground, 45);
|
||||
'--color-main-background-blur' => $colorMainBackground,
|
||||
'--filter-background-blur' => 'none',
|
||||
|
||||
$variables['--color-text-maxcontrast'] = $colorMainText;
|
||||
$variables['--color-text-light'] = $colorMainText;
|
||||
$variables['--color-text-lighter'] = $colorMainText;
|
||||
'--color-placeholder-light' => $this->util->lighten($colorMainBackground, 30),
|
||||
'--color-placeholder-dark' => $this->util->lighten($colorMainBackground, 45),
|
||||
|
||||
$variables['--color-scrollbar'] = $this->util->lighten($colorMainBackground, 35);
|
||||
'--color-text-maxcontrast' => $colorMainText,
|
||||
'--color-text-maxcontrast-background-blur' => $colorMainText,
|
||||
'--color-text-light' => $colorMainText,
|
||||
'--color-text-lighter' => $colorMainText,
|
||||
|
||||
// used for the icon loading animation
|
||||
$variables['--color-loading-light'] = '#000000';
|
||||
$variables['--color-loading-dark'] = '#dddddd';
|
||||
'--color-scrollbar' => $this->util->lighten($colorMainBackground, 35),
|
||||
|
||||
// used for the icon loading animation
|
||||
'--color-loading-light' => '#000000',
|
||||
'--color-loading-dark' => '#dddddd',
|
||||
|
||||
$variables['--color-box-shadow-rgb'] = 'var(--color-main-text)';
|
||||
$variables['--color-box-shadow'] = 'var(--color-main-text)';
|
||||
'--color-box-shadow-rgb' => $colorMainText,
|
||||
'--color-box-shadow' => $colorMainText,
|
||||
|
||||
|
||||
$variables['--color-border'] = $this->util->lighten($colorMainBackground, 50);
|
||||
$variables['--color-border-dark'] = $this->util->lighten($colorMainBackground, 50);
|
||||
|
||||
return $variables;
|
||||
'--color-border' => $this->util->lighten($colorMainBackground, 50),
|
||||
'--color-border-dark' => $this->util->lighten($colorMainBackground, 50),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function getCustomCss(): string {
|
||||
|
||||
@@ -54,45 +54,47 @@ class DarkTheme extends DefaultTheme implements ITheme {
|
||||
$colorMainText = '#D8D8D8';
|
||||
$colorMainBackground = '#171717';
|
||||
$colorMainBackgroundRGB = join(',', $this->util->hexToRGB($colorMainBackground));
|
||||
$colorTextMaxcontrast = $this->util->darken($colorMainText, 30);
|
||||
|
||||
$colorBoxShadow = $this->util->darken($colorMainBackground, 70);
|
||||
$colorBoxShadowRGB = join(',', $this->util->hexToRGB($colorBoxShadow));
|
||||
$colorPrimaryLight = $this->util->mix($this->primaryColor, $colorMainBackground, -80);
|
||||
|
||||
return array_merge($defaultVariables, [
|
||||
'--color-main-text' => $colorMainText,
|
||||
'--color-main-background' => $colorMainBackground,
|
||||
'--color-main-background-rgb' => $colorMainBackgroundRGB,
|
||||
return array_merge(
|
||||
$defaultVariables,
|
||||
$this->generatePrimaryVariables($colorMainBackground, $colorMainText),
|
||||
[
|
||||
'--color-main-text' => $colorMainText,
|
||||
'--color-main-background' => $colorMainBackground,
|
||||
'--color-main-background-rgb' => $colorMainBackgroundRGB,
|
||||
|
||||
'--color-scrollbar' => $this->util->lighten($colorMainBackground, 15),
|
||||
'--color-scrollbar' => $this->util->lighten($colorMainBackground, 15),
|
||||
|
||||
'--color-background-hover' => $this->util->lighten($colorMainBackground, 4),
|
||||
'--color-background-dark' => $this->util->lighten($colorMainBackground, 7),
|
||||
'--color-background-darker' => $this->util->lighten($colorMainBackground, 14),
|
||||
'--color-background-hover' => $this->util->lighten($colorMainBackground, 4),
|
||||
'--color-background-dark' => $this->util->lighten($colorMainBackground, 7),
|
||||
'--color-background-darker' => $this->util->lighten($colorMainBackground, 14),
|
||||
|
||||
'--color-placeholder-light' => $this->util->lighten($colorMainBackground, 10),
|
||||
'--color-placeholder-dark' => $this->util->lighten($colorMainBackground, 20),
|
||||
'--color-placeholder-light' => $this->util->lighten($colorMainBackground, 10),
|
||||
'--color-placeholder-dark' => $this->util->lighten($colorMainBackground, 20),
|
||||
|
||||
'--color-primary-hover' => $this->util->mix($this->primaryColor, $colorMainBackground, 60),
|
||||
'--color-primary-light' => $colorPrimaryLight,
|
||||
'--color-primary-light-hover' => $this->util->mix($colorPrimaryLight, $colorMainText, 90),
|
||||
'--color-primary-element' => $this->util->elementColor($this->primaryColor, false),
|
||||
'--color-primary-element-hover' => $this->util->mix($this->util->elementColor($this->primaryColor, false), $colorMainBackground, 80),
|
||||
'--color-primary-element-light' => $this->util->lighten($this->util->elementColor($this->primaryColor, false), 15),
|
||||
'--color-text-maxcontrast' => $colorTextMaxcontrast,
|
||||
'--color-text-maxcontrast-default' => $colorTextMaxcontrast,
|
||||
'--color-text-maxcontrast-background-blur' => $this->util->lighten($colorTextMaxcontrast, 2),
|
||||
'--color-text-light' => $this->util->darken($colorMainText, 10),
|
||||
'--color-text-lighter' => $this->util->darken($colorMainText, 20),
|
||||
|
||||
'--color-text-maxcontrast' => $this->util->darken($colorMainText, 30),
|
||||
'--color-text-light' => $this->util->darken($colorMainText, 10),
|
||||
'--color-text-lighter' => $this->util->darken($colorMainText, 20),
|
||||
// used for the icon loading animation
|
||||
'--color-loading-light' => '#777',
|
||||
'--color-loading-dark' => '#CCC',
|
||||
|
||||
'--color-loading-light' => '#777',
|
||||
'--color-loading-dark' => '#CCC',
|
||||
'--color-box-shadow' => $colorBoxShadow,
|
||||
'--color-box-shadow-rgb' => $colorBoxShadowRGB,
|
||||
|
||||
'--color-box-shadow-rgb' => $colorBoxShadowRGB,
|
||||
'--color-border' => $this->util->lighten($colorMainBackground, 7),
|
||||
'--color-border-dark' => $this->util->lighten($colorMainBackground, 14),
|
||||
|
||||
'--color-border' => $this->util->lighten($colorMainBackground, 7),
|
||||
'--color-border-dark' => $this->util->lighten($colorMainBackground, 14),
|
||||
|
||||
'--background-invert-if-dark' => 'invert(100%)',
|
||||
'--background-invert-if-bright' => 'no',
|
||||
]);
|
||||
'--background-invert-if-dark' => 'invert(100%)',
|
||||
'--background-invert-if-bright' => 'no',
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ namespace OCA\Theming\Themes;
|
||||
use OCA\Theming\AppInfo\Application;
|
||||
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;
|
||||
@@ -37,29 +38,41 @@ use OCP\IUserSession;
|
||||
use OCP\Server;
|
||||
|
||||
class DefaultTheme implements ITheme {
|
||||
use CommonThemeTrait;
|
||||
|
||||
public Util $util;
|
||||
public ThemingDefaults $themingDefaults;
|
||||
public IUserSession $userSession;
|
||||
public IURLGenerator $urlGenerator;
|
||||
public ImageManager $imageManager;
|
||||
public IConfig $config;
|
||||
public IL10N $l;
|
||||
|
||||
public string $defaultPrimaryColor;
|
||||
public string $primaryColor;
|
||||
|
||||
public function __construct(Util $util,
|
||||
ThemingDefaults $themingDefaults,
|
||||
IUserSession $userSession,
|
||||
IURLGenerator $urlGenerator,
|
||||
ImageManager $imageManager,
|
||||
IConfig $config,
|
||||
IL10N $l) {
|
||||
$this->util = $util;
|
||||
$this->themingDefaults = $themingDefaults;
|
||||
$this->userSession = $userSession;
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
$this->imageManager = $imageManager;
|
||||
$this->config = $config;
|
||||
$this->l = $l;
|
||||
|
||||
$this->defaultPrimaryColor = $this->themingDefaults->getDefaultColorPrimary();
|
||||
$this->primaryColor = $this->themingDefaults->getColorPrimary();
|
||||
|
||||
// Override default defaultPrimaryColor if set to improve accessibility
|
||||
if ($this->primaryColor === BackgroundService::DEFAULT_COLOR) {
|
||||
$this->primaryColor = BackgroundService::DEFAULT_ACCESSIBLE_COLOR;
|
||||
}
|
||||
}
|
||||
|
||||
public function getId(): string {
|
||||
@@ -89,20 +102,18 @@ class DefaultTheme implements ITheme {
|
||||
public function getCSSVariables(): array {
|
||||
$colorMainText = '#222222';
|
||||
$colorMainTextRgb = join(',', $this->util->hexToRGB($colorMainText));
|
||||
$colorTextMaxcontrast = $this->util->lighten($colorMainText, 33);
|
||||
$colorMainBackground = '#ffffff';
|
||||
$colorMainBackgroundRGB = join(',', $this->util->hexToRGB($colorMainBackground));
|
||||
$colorBoxShadow = $this->util->darken($colorMainBackground, 70);
|
||||
$colorBoxShadowRGB = join(',', $this->util->hexToRGB($colorBoxShadow));
|
||||
$colorPrimaryLight = $this->util->mix($this->primaryColor, $colorMainBackground, -80);
|
||||
|
||||
$colorPrimaryElement = $this->util->elementColor($this->primaryColor);
|
||||
$colorPrimaryElementLight = $this->util->mix($colorPrimaryElement, $colorMainBackground, -80);
|
||||
|
||||
$hasCustomLogoHeader = $this->imageManager->hasImage('logo') || $this->imageManager->hasImage('logoheader');
|
||||
$hasCustomPrimaryColour = !empty($this->config->getAppValue(Application::APP_ID, 'color'));
|
||||
|
||||
$variables = [
|
||||
'--color-main-background' => $colorMainBackground,
|
||||
'--color-main-background-not-plain' => $this->themingDefaults->getColorPrimary(),
|
||||
'--color-main-background-rgb' => $colorMainBackgroundRGB,
|
||||
'--color-main-background-translucent' => 'rgba(var(--color-main-background-rgb), .97)',
|
||||
'--color-main-background-blur' => 'rgba(var(--color-main-background-rgb), .8)',
|
||||
@@ -119,28 +130,11 @@ class DefaultTheme implements ITheme {
|
||||
'--color-placeholder-light' => $this->util->darken($colorMainBackground, 10),
|
||||
'--color-placeholder-dark' => $this->util->darken($colorMainBackground, 20),
|
||||
|
||||
// primary related colours
|
||||
'--color-primary' => $this->primaryColor,
|
||||
'--color-primary-text' => $this->util->invertTextColor($this->primaryColor) ? '#000000' : '#ffffff',
|
||||
'--color-primary-hover' => $this->util->mix($this->primaryColor, $colorMainBackground, 60),
|
||||
'--color-primary-light' => $colorPrimaryLight,
|
||||
'--color-primary-light-text' => $this->primaryColor,
|
||||
'--color-primary-light-hover' => $this->util->mix($colorPrimaryLight, $colorMainText, 90),
|
||||
'--color-primary-text-dark' => $this->util->darken($this->util->invertTextColor($this->primaryColor) ? '#000000' : '#ffffff', 7),
|
||||
// used for buttons, inputs...
|
||||
'--color-primary-element' => $colorPrimaryElement,
|
||||
'--color-primary-element-text' => $this->util->invertTextColor($colorPrimaryElement) ? '#000000' : '#ffffff',
|
||||
'--color-primary-element-hover' => $this->util->mix($colorPrimaryElement, $colorMainBackground, 60),
|
||||
'--color-primary-element-light' => $colorPrimaryElementLight,
|
||||
'--color-primary-element-light-text' => $colorPrimaryElement,
|
||||
'--color-primary-element-light-hover' => $this->util->mix($colorPrimaryElementLight, $colorMainText, 90),
|
||||
'--color-primary-element-text-dark' => $this->util->darken($this->util->invertTextColor($colorPrimaryElement) ? '#000000' : '#ffffff', 7),
|
||||
// to use like this: background-image: var(--gradient-primary-background);
|
||||
'--gradient-primary-background' => 'linear-gradient(40deg, var(--color-primary) 0%, var(--color-primary-hover) 100%)',
|
||||
|
||||
// max contrast for WCAG compliance
|
||||
'--color-main-text' => $colorMainText,
|
||||
'--color-text-maxcontrast' => $this->util->lighten($colorMainText, 33),
|
||||
'--color-text-maxcontrast' => $colorTextMaxcontrast,
|
||||
'--color-text-maxcontrast-default' => $colorTextMaxcontrast,
|
||||
'--color-text-maxcontrast-background-blur' => $this->util->darken($colorTextMaxcontrast, 7),
|
||||
'--color-text-light' => $colorMainText,
|
||||
'--color-text-lighter' => $this->util->lighten($colorMainText, 33),
|
||||
|
||||
@@ -211,6 +205,9 @@ class DefaultTheme implements ITheme {
|
||||
'--image-main-background' => "url('" . $this->urlGenerator->imagePath('core', 'app-background.jpg') . "')",
|
||||
];
|
||||
|
||||
// Primary variables
|
||||
$variables = array_merge($variables, $this->generatePrimaryVariables($colorMainBackground, $colorMainText));
|
||||
|
||||
$backgroundDeleted = $this->config->getAppValue(Application::APP_ID, 'backgroundMime', '') === 'backgroundColor';
|
||||
// If primary as background has been request or if we have a custom primary colour
|
||||
// let's not define the background image
|
||||
@@ -239,15 +236,17 @@ class DefaultTheme implements ITheme {
|
||||
}
|
||||
|
||||
$appManager = Server::get(IAppManager::class);
|
||||
$userSession = Server::get(IUserSession::class);
|
||||
$user = $userSession->getUser();
|
||||
$user = $this->userSession->getUser();
|
||||
if ($appManager->isEnabledForUser(Application::APP_ID) && $user !== null) {
|
||||
$themingBackground = $this->config->getUserValue($user->getUID(), Application::APP_ID, 'background', 'default');
|
||||
|
||||
if ($themingBackground === 'custom') {
|
||||
$variables['--image-main-background'] = "url('" . $this->urlGenerator->linkToRouteAbsolute('theming.theming.getBackground') . "')";
|
||||
} elseif ($themingBackground !== 'default' && substr($themingBackground, 0, 1) !== '#') {
|
||||
$variables['--image-main-background'] = "url('" . $this->urlGenerator->linkToRouteAbsolute('theming.userTheme.getBackground') . "')";
|
||||
} elseif (isset(BackgroundService::SHIPPED_BACKGROUNDS[$themingBackground])) {
|
||||
$variables['--image-main-background'] = "url('" . $this->urlGenerator->linkTo(Application::APP_ID, "/img/background/$themingBackground") . "')";
|
||||
} elseif (substr($themingBackground, 0, 1) === '#') {
|
||||
unset($variables['--image-main-background']);
|
||||
$variables['--color-main-background-plain'] = $this->themingDefaults->getColorPrimary();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ use OCA\Theming\ITheme;
|
||||
class HighContrastTheme extends DefaultTheme implements ITheme {
|
||||
|
||||
public function getId(): string {
|
||||
return 'highcontrast';
|
||||
return 'light-highcontrast';
|
||||
}
|
||||
|
||||
public function getMediaQuery(): string {
|
||||
@@ -48,41 +48,52 @@ class HighContrastTheme extends DefaultTheme implements ITheme {
|
||||
return $this->l->t('A high contrast mode to ease your navigation. Visual quality will be reduced but clarity will be increased.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep this consistent with other HighContrast Themes
|
||||
*/
|
||||
public function getCSSVariables(): array {
|
||||
$variables = parent::getCSSVariables();
|
||||
$defaultVariables = parent::getCSSVariables();
|
||||
|
||||
$colorMainText = '#000000';
|
||||
$colorMainBackground = '#ffffff';
|
||||
$colorMainBackgroundRGB = join(',', $this->util->hexToRGB($colorMainBackground));
|
||||
|
||||
$variables['--color-main-background'] = $colorMainBackground;
|
||||
$variables['--color-main-background-translucent'] = 'rgba(var(--color-main-background-rgb), .1)';
|
||||
$variables['--color-main-text'] = $colorMainText;
|
||||
return array_merge(
|
||||
$defaultVariables,
|
||||
$this->generatePrimaryVariables($colorMainBackground, $colorMainText),
|
||||
[
|
||||
'--color-main-background' => $colorMainBackground,
|
||||
'--color-main-background-rgb' => $colorMainBackgroundRGB,
|
||||
'--color-main-background-translucent' => 'rgba(var(--color-main-background-rgb), 1)',
|
||||
'--color-main-text' => $colorMainText,
|
||||
|
||||
$variables['--color-background-dark'] = $this->util->darken($colorMainBackground, 30);
|
||||
$variables['--color-background-darker'] = $this->util->darken($colorMainBackground, 30);
|
||||
'--color-background-dark' => $this->util->darken($colorMainBackground, 30),
|
||||
'--color-background-darker' => $this->util->darken($colorMainBackground, 30),
|
||||
|
||||
$variables['--color-main-background-blur'] = $colorMainBackground;
|
||||
$variables['--filter-background-blur'] = 'none';
|
||||
'--color-main-background-blur' => $colorMainBackground,
|
||||
'--filter-background-blur' => 'none',
|
||||
|
||||
$variables['--color-placeholder-light'] = $this->util->darken($colorMainBackground, 30);
|
||||
$variables['--color-placeholder-dark'] = $this->util->darken($colorMainBackground, 45);
|
||||
'--color-placeholder-light' => $this->util->darken($colorMainBackground, 30),
|
||||
'--color-placeholder-dark' => $this->util->darken($colorMainBackground, 45),
|
||||
|
||||
$variables['--color-text-maxcontrast'] = 'var(--color-main-text)';
|
||||
$variables['--color-text-light'] = 'var(--color-main-text)';
|
||||
$variables['--color-text-lighter'] = 'var(--color-main-text)';
|
||||
'--color-text-maxcontrast' => $colorMainText,
|
||||
'--color-text-maxcontrast-background-blur' => $colorMainText,
|
||||
'--color-text-light' => $colorMainText,
|
||||
'--color-text-lighter' => $colorMainText,
|
||||
|
||||
$variables['--color-scrollbar'] = $this->util->darken($colorMainBackground, 25);
|
||||
'--color-scrollbar' => $this->util->darken($colorMainBackground, 25),
|
||||
|
||||
// used for the icon loading animation
|
||||
$variables['--color-loading-light'] = '#dddddd';
|
||||
$variables['--color-loading-dark'] = '#000000';
|
||||
// used for the icon loading animation
|
||||
'--color-loading-light' => '#dddddd',
|
||||
'--color-loading-dark' => '#000000',
|
||||
|
||||
$variables['--color-box-shadow-rgb'] = 'var(--color-main-text)';
|
||||
$variables['--color-box-shadow'] = 'var(--color-main-text)';
|
||||
'--color-box-shadow-rgb' => $colorMainText,
|
||||
'--color-box-shadow' => $colorMainText,
|
||||
|
||||
$variables['--color-border'] = $this->util->darken($colorMainBackground, 50);
|
||||
$variables['--color-border-dark'] = $this->util->darken($colorMainBackground, 50);
|
||||
|
||||
return $variables;
|
||||
'--color-border' => $this->util->darken($colorMainBackground, 50),
|
||||
'--color-border-dark' => $this->util->darken($colorMainBackground, 50),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function getCustomCss(): string {
|
||||
|
||||
@@ -40,6 +40,8 @@
|
||||
*/
|
||||
namespace OCA\Theming;
|
||||
|
||||
use OCA\Theming\AppInfo\Application;
|
||||
use OCA\Theming\Service\BackgroundService;
|
||||
use OCP\App\AppPathNotFoundException;
|
||||
use OCP\App\IAppManager;
|
||||
use OCP\Files\NotFoundException;
|
||||
@@ -49,47 +51,31 @@ use OCP\IConfig;
|
||||
use OCP\IL10N;
|
||||
use OCP\INavigationManager;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\IUserSession;
|
||||
|
||||
class ThemingDefaults extends \OC_Defaults {
|
||||
|
||||
/** @var IConfig */
|
||||
private $config;
|
||||
/** @var IL10N */
|
||||
private $l;
|
||||
/** @var ImageManager */
|
||||
private $imageManager;
|
||||
/** @var IURLGenerator */
|
||||
private $urlGenerator;
|
||||
/** @var ICacheFactory */
|
||||
private $cacheFactory;
|
||||
/** @var Util */
|
||||
private $util;
|
||||
/** @var IAppManager */
|
||||
private $appManager;
|
||||
/** @var INavigationManager */
|
||||
private $navigationManager;
|
||||
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;
|
||||
|
||||
/** @var string */
|
||||
private $name;
|
||||
/** @var string */
|
||||
private $title;
|
||||
/** @var string */
|
||||
private $entity;
|
||||
/** @var string */
|
||||
private $productName;
|
||||
/** @var string */
|
||||
private $url;
|
||||
/** @var string */
|
||||
private $color;
|
||||
private string $name;
|
||||
private string $title;
|
||||
private string $entity;
|
||||
private string $productName;
|
||||
private string $url;
|
||||
private string $color;
|
||||
|
||||
/** @var string */
|
||||
private $iTunesAppId;
|
||||
/** @var string */
|
||||
private $iOSClientUrl;
|
||||
/** @var string */
|
||||
private $AndroidClientUrl;
|
||||
/** @var string */
|
||||
private $FDroidClientUrl;
|
||||
private string $iTunesAppId;
|
||||
private string $iOSClientUrl;
|
||||
private string $AndroidClientUrl;
|
||||
private string $FDroidClientUrl;
|
||||
|
||||
/**
|
||||
* ThemingDefaults constructor.
|
||||
@@ -97,6 +83,7 @@ class ThemingDefaults extends \OC_Defaults {
|
||||
* @param IConfig $config
|
||||
* @param IL10N $l
|
||||
* @param ImageManager $imageManager
|
||||
* @param IUserSession $userSession
|
||||
* @param IURLGenerator $urlGenerator
|
||||
* @param ICacheFactory $cacheFactory
|
||||
* @param Util $util
|
||||
@@ -104,6 +91,7 @@ class ThemingDefaults extends \OC_Defaults {
|
||||
*/
|
||||
public function __construct(IConfig $config,
|
||||
IL10N $l,
|
||||
IUserSession $userSession,
|
||||
IURLGenerator $urlGenerator,
|
||||
ICacheFactory $cacheFactory,
|
||||
Util $util,
|
||||
@@ -115,6 +103,7 @@ class ThemingDefaults extends \OC_Defaults {
|
||||
$this->config = $config;
|
||||
$this->l = $l;
|
||||
$this->imageManager = $imageManager;
|
||||
$this->userSession = $userSession;
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
$this->cacheFactory = $cacheFactory;
|
||||
$this->util = $util;
|
||||
@@ -225,11 +214,47 @@ class ThemingDefaults extends \OC_Defaults {
|
||||
|
||||
/**
|
||||
* Color that is used for the header as well as for mail headers
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getColorPrimary() {
|
||||
$color = $this->config->getAppValue('theming', 'color', $this->color);
|
||||
public function getColorPrimary(): string {
|
||||
$user = $this->userSession->getUser();
|
||||
|
||||
// admin-defined primary color
|
||||
$defaultColor = $this->getDefaultColorPrimary();
|
||||
|
||||
// user-defined primary color
|
||||
$themingBackground = '';
|
||||
if (!empty($user)) {
|
||||
$themingBackground = $this->config->getUserValue($user->getUID(), Application::APP_ID, 'background', '');
|
||||
// If the user selected the default background
|
||||
if ($themingBackground === '') {
|
||||
return BackgroundService::DEFAULT_COLOR;
|
||||
}
|
||||
|
||||
// If the user selected a specific colour
|
||||
if (preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $themingBackground)) {
|
||||
return $themingBackground;
|
||||
}
|
||||
|
||||
// if the user-selected background is a background reference
|
||||
if (isset(BackgroundService::SHIPPED_BACKGROUNDS[$themingBackground]['primary_color'])) {
|
||||
return BackgroundService::SHIPPED_BACKGROUNDS[$themingBackground]['primary_color'];
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the default color primary
|
||||
*/
|
||||
public function getDefaultColorPrimary(): string {
|
||||
$color = $this->config->getAppValue(Application::APP_ID, 'color');
|
||||
if (!preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $color)) {
|
||||
$color = '#0082c9';
|
||||
}
|
||||
@@ -408,7 +433,7 @@ class ThemingDefaults extends \OC_Defaults {
|
||||
/**
|
||||
* Increases the cache buster key
|
||||
*/
|
||||
private function increaseCacheBuster(): void {
|
||||
public function increaseCacheBuster(): void {
|
||||
$cacheBusterKey = (int)$this->config->getAppValue('theming', 'cachebuster', '0');
|
||||
$this->config->setAppValue('theming', 'cachebuster', (string)($cacheBusterKey + 1));
|
||||
$this->cacheFactory->createDistributed('theming-')->clear();
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { generateOcsUrl, imagePath } from '@nextcloud/router'
|
||||
import { generateOcsUrl } from '@nextcloud/router'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import axios from '@nextcloud/axios'
|
||||
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch'
|
||||
@@ -83,8 +83,6 @@ import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection'
|
||||
import BackgroundSettings from './components/BackgroundSettings.vue'
|
||||
import ItemPreview from './components/ItemPreview.vue'
|
||||
|
||||
import { getBackgroundUrl } from '../src/helpers/getBackgroundUrl.js'
|
||||
|
||||
const availableThemes = loadState('theming', 'themes', [])
|
||||
const enforceTheme = loadState('theming', 'enforceTheme', '')
|
||||
const shortcutsDisabled = loadState('theming', 'shortcutsDisabled', false)
|
||||
@@ -111,24 +109,12 @@ export default {
|
||||
enforceTheme,
|
||||
shortcutsDisabled,
|
||||
background,
|
||||
backgroundVersion,
|
||||
themingDefaultBackground,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
backgroundImage() {
|
||||
return getBackgroundUrl(this.background, backgroundVersion, this.themingDefaultBackground)
|
||||
},
|
||||
backgroundStyle() {
|
||||
if ((this.background === 'default' && this.themingDefaultBackground === 'backgroundColor')
|
||||
|| this.background.match(/#[0-9A-Fa-f]{6}/g)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
backgroundImage: this.background === 'default' ? 'var(--image-main-background)' : `url('${this.backgroundImage}')`,
|
||||
}
|
||||
},
|
||||
themes() {
|
||||
return this.availableThemes.filter(theme => theme.type === 1)
|
||||
},
|
||||
@@ -170,20 +156,22 @@ export default {
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.updateGlobalStyles()
|
||||
},
|
||||
|
||||
watch: {
|
||||
shortcutsDisabled(newState) {
|
||||
this.changeShortcutsDisabled(newState)
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.updateGlobalStyles()
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateBackground(data) {
|
||||
this.background = (data.type === 'custom' || data.type === 'default') ? data.type : data.value
|
||||
this.backgroundVersion = data.version
|
||||
this.updateGlobalStyles()
|
||||
this.$emit('update:background')
|
||||
},
|
||||
updateGlobalStyles() {
|
||||
// Override primary-invert-if-bright and color-primary-text if background is set
|
||||
@@ -199,17 +187,6 @@ export default {
|
||||
// document.body.removeAttribute('data-theme-light')
|
||||
// document.body.setAttribute('data-theme-dark', 'true')
|
||||
}
|
||||
|
||||
const themeElements = [document.documentElement, document.querySelector('#header'), document.querySelector('body')]
|
||||
for (const element of themeElements) {
|
||||
if (this.background === 'default') {
|
||||
element.style.setProperty('--image-main-background', `url('${imagePath('core', 'app-background.jpg')}')`)
|
||||
} else if (this.background.match(/#[0-9A-Fa-f]{6}/g)) {
|
||||
element.style.setProperty('--image-main-background', undefined)
|
||||
} else {
|
||||
element.style.setProperty('--image-main-background', this.backgroundStyle.backgroundImage)
|
||||
}
|
||||
}
|
||||
},
|
||||
changeTheme({ enabled, id }) {
|
||||
// Reset selected and select new one
|
||||
|
||||
@@ -25,42 +25,67 @@
|
||||
|
||||
<template>
|
||||
<div class="background-selector">
|
||||
<!-- Custom background -->
|
||||
<button class="background filepicker"
|
||||
:class="{ active: background === 'custom' }"
|
||||
tabindex="0"
|
||||
@click="pickFile">
|
||||
{{ t('theming', 'Pick from Files') }}
|
||||
</button>
|
||||
|
||||
<!-- Default background -->
|
||||
<button class="background default"
|
||||
tabindex="0"
|
||||
:class="{ 'icon-loading': loading === 'default', active: background === 'default' }"
|
||||
@click="setDefault">
|
||||
{{ t('theming', 'Default image') }}
|
||||
</button>
|
||||
|
||||
<!-- Custom color picker -->
|
||||
<NcColorPicker v-model="Theming.color" @input="debouncePickColor">
|
||||
<button class="background color"
|
||||
:class="{ active: background === Theming.color}"
|
||||
tabindex="0"
|
||||
:data-color="Theming.color"
|
||||
:data-color-bright="invertTextColor(Theming.color)"
|
||||
:style="{ backgroundColor: Theming.color, color: invertTextColor(Theming.color) ? '#000000' : '#ffffff'}">
|
||||
{{ t('theming', 'Custom color') }}
|
||||
</button>
|
||||
</NcColorPicker>
|
||||
|
||||
<!-- Default admin primary color -->
|
||||
<button class="background color"
|
||||
:class="{ active: background === 'custom' }"
|
||||
:class="{ active: background === Theming.defaultColor }"
|
||||
tabindex="0"
|
||||
@click="pickColor">
|
||||
:data-color="Theming.defaultColor"
|
||||
:data-color-bright="invertTextColor(Theming.defaultColor)"
|
||||
:style="{ color: invertTextColor(Theming.defaultColor) ? '#000000' : '#ffffff'}"
|
||||
@click="debouncePickColor">
|
||||
{{ t('theming', 'Plain background') }}
|
||||
</button>
|
||||
|
||||
<!-- Background set selection -->
|
||||
<button v-for="shippedBackground in shippedBackgrounds"
|
||||
:key="shippedBackground.name"
|
||||
v-tooltip="shippedBackground.details.attribution"
|
||||
:class="{ 'icon-loading': loading === shippedBackground.name, active: background === shippedBackground.name }"
|
||||
tabindex="0"
|
||||
class="background"
|
||||
:data-color-bright="shippedBackground.details.theming === 'dark'"
|
||||
:style="{ 'background-image': 'url(' + shippedBackground.preview + ')' }"
|
||||
@click="setShipped(shippedBackground.name)" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from '@nextcloud/axios'
|
||||
import Tooltip from '@nextcloud/vue/dist/Directives/Tooltip'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { getBackgroundUrl } from '../helpers/getBackgroundUrl.js'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { prefixWithBaseUrl } from '../helpers/prefixWithBaseUrl.js'
|
||||
import axios from '@nextcloud/axios'
|
||||
import debounce from 'debounce'
|
||||
import NcColorPicker from '@nextcloud/vue/dist/Components/NcColorPicker'
|
||||
import Tooltip from '@nextcloud/vue/dist/Directives/Tooltip'
|
||||
|
||||
const shippedBackgroundList = loadState('theming', 'shippedBackgrounds')
|
||||
|
||||
@@ -69,6 +94,11 @@ export default {
|
||||
directives: {
|
||||
Tooltip,
|
||||
},
|
||||
|
||||
components: {
|
||||
NcColorPicker,
|
||||
},
|
||||
|
||||
props: {
|
||||
background: {
|
||||
type: String,
|
||||
@@ -79,12 +109,15 @@ export default {
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
backgroundImage: generateUrl('/apps/theming/background') + '?v=' + Date.now(),
|
||||
loading: false,
|
||||
Theming: loadState('theming', 'data', {}),
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
shippedBackgrounds() {
|
||||
return Object.keys(shippedBackgroundList).map(fileName => {
|
||||
@@ -97,7 +130,39 @@ export default {
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Do we need to invert the text if color is too bright?
|
||||
*
|
||||
* @param {string} color the hex color
|
||||
*/
|
||||
invertTextColor(color) {
|
||||
return this.calculateLuma(color) > 0.6
|
||||
},
|
||||
|
||||
/**
|
||||
* Calculate luminance of provided hex color
|
||||
*
|
||||
* @param {string} color the hex color
|
||||
*/
|
||||
calculateLuma(color) {
|
||||
const [red, green, blue] = this.hexToRGB(color)
|
||||
return (0.2126 * red + 0.7152 * green + 0.0722 * blue) / 255
|
||||
},
|
||||
|
||||
/**
|
||||
* Convert hex color to RGB
|
||||
*
|
||||
* @param {string} hex the hex color
|
||||
*/
|
||||
hexToRGB(hex) {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
||||
return result
|
||||
? [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)]
|
||||
: null
|
||||
},
|
||||
|
||||
async update(data) {
|
||||
const background = data.type === 'custom' || data.type === 'default' ? data.type : data.value
|
||||
this.backgroundImage = getBackgroundUrl(background, data.version, this.themingDefaultBackground)
|
||||
@@ -113,27 +178,35 @@ export default {
|
||||
}
|
||||
image.src = this.backgroundImage
|
||||
},
|
||||
|
||||
async setDefault() {
|
||||
this.loading = 'default'
|
||||
const result = await axios.post(generateUrl('/apps/theming/background/default'))
|
||||
this.update(result.data)
|
||||
},
|
||||
|
||||
async setShipped(shipped) {
|
||||
this.loading = shipped
|
||||
const result = await axios.post(generateUrl('/apps/theming/background/shipped'), { value: shipped })
|
||||
this.update(result.data)
|
||||
},
|
||||
|
||||
async setFile(path) {
|
||||
this.loading = 'custom'
|
||||
const result = await axios.post(generateUrl('/apps/theming/background/custom'), { value: path })
|
||||
this.update(result.data)
|
||||
},
|
||||
async pickColor() {
|
||||
|
||||
debouncePickColor: debounce(function() {
|
||||
this.pickColor(...arguments)
|
||||
}, 200),
|
||||
async pickColor(event) {
|
||||
this.loading = 'color'
|
||||
const color = OCA && OCA.Theming ? OCA.Theming.color : '#0082c9'
|
||||
const color = event?.target?.dataset?.color || this.Theming?.color || '#0082c9'
|
||||
const result = await axios.post(generateUrl('/apps/theming/background/color'), { value: color })
|
||||
this.update(result.data)
|
||||
},
|
||||
|
||||
pickFile() {
|
||||
window.OC.dialogs.filepicker(t('theming', 'Insert from {productName}', { productName: OC.theme.name }), (path, type) => {
|
||||
if (type === OC.dialogs.FILEPICKER_TYPE_CHOOSE) {
|
||||
@@ -171,7 +244,7 @@ export default {
|
||||
}
|
||||
|
||||
&.color {
|
||||
background-color: var(--color-primary);
|
||||
background-color: var(--color-primary-default);
|
||||
color: var(--color-primary-text);
|
||||
}
|
||||
|
||||
@@ -181,14 +254,20 @@ export default {
|
||||
border: 2px solid var(--color-primary);
|
||||
}
|
||||
|
||||
&.active:not(.icon-loading):after {
|
||||
background-image: var(--icon-checkmark-white);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: 44px;
|
||||
content: '';
|
||||
display: block;
|
||||
height: 100%;
|
||||
&.active:not(.icon-loading) {
|
||||
&:after {
|
||||
background-image: var(--icon-checkmark-white);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: 44px;
|
||||
content: '';
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&[data-color-bright]:after {
|
||||
background-image: var(--icon-checkmark-dark);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,6 @@
|
||||
*
|
||||
*/
|
||||
|
||||
// FIXME hoist this into a package? The same logic is used in `apps/dashboard/src/helpers/getBackgroundUrl.js`
|
||||
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { prefixWithBaseUrl } from './prefixWithBaseUrl.js'
|
||||
|
||||
|
||||
@@ -20,8 +20,6 @@
|
||||
*
|
||||
*/
|
||||
|
||||
// FIXME hoist this into a package? The same logic is used in `apps/dashboard/src/helpers/prefixWithBaseUrl.js`
|
||||
|
||||
import { generateFilePath } from '@nextcloud/router'
|
||||
|
||||
export const prefixWithBaseUrl = (url) => generateFilePath('theming', '', 'img/background/') + url
|
||||
|
||||
@@ -30,3 +30,15 @@ Vue.prototype.t = t
|
||||
const View = Vue.extend(App)
|
||||
const theming = new View()
|
||||
theming.$mount('#theming')
|
||||
|
||||
theming.$on('update:background', () => {
|
||||
// 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()
|
||||
document.head.append(newTheme)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -33,6 +33,7 @@ use OCA\Theming\Themes\DyslexiaFont;
|
||||
use OCA\Theming\Themes\HighContrastTheme;
|
||||
use OCA\Theming\Service\ThemesService;
|
||||
use OCA\Theming\Themes\LightTheme;
|
||||
use OCA\Theming\ThemingDefaults;
|
||||
use OCP\AppFramework\Http\DataResponse;
|
||||
use OCP\AppFramework\OCS\OCSBadRequestException;
|
||||
use OCP\IConfig;
|
||||
@@ -54,6 +55,8 @@ class UserThemeControllerTest extends TestCase {
|
||||
private $userSession;
|
||||
/** @var ThemeService|MockObject */
|
||||
private $themesService;
|
||||
/** @var ThemingDefaults */
|
||||
private $themingDefaults;
|
||||
/** @var BackgroundService|MockObject */
|
||||
private $backgroundService;
|
||||
|
||||
@@ -66,13 +69,14 @@ class UserThemeControllerTest extends TestCase {
|
||||
$this->config = $this->createMock(IConfig::class);
|
||||
$this->userSession = $this->createMock(IUserSession::class);
|
||||
$this->themesService = $this->createMock(ThemesService::class);
|
||||
$this->themingDefaults = $this->createMock(ThemingDefaults::class);
|
||||
$this->backgroundService = $this->createMock(BackgroundService::class);
|
||||
|
||||
$this->themes = [
|
||||
'default' => $this->createMock(DefaultTheme::class),
|
||||
'light' => $this->createMock(LightTheme::class),
|
||||
'dark' => $this->createMock(DarkTheme::class),
|
||||
'highcontrast' => $this->createMock(HighContrastTheme::class),
|
||||
'light-highcontrast' => $this->createMock(HighContrastTheme::class),
|
||||
'dark-highcontrast' => $this->createMock(DarkHighContrastTheme::class),
|
||||
'opendyslexic' => $this->createMock(DyslexiaFont::class),
|
||||
];
|
||||
@@ -91,6 +95,7 @@ class UserThemeControllerTest extends TestCase {
|
||||
$this->config,
|
||||
$this->userSession,
|
||||
$this->themesService,
|
||||
$this->themingDefaults,
|
||||
$this->backgroundService,
|
||||
);
|
||||
|
||||
@@ -102,7 +107,7 @@ class UserThemeControllerTest extends TestCase {
|
||||
['default'],
|
||||
['light'],
|
||||
['dark'],
|
||||
['highcontrast'],
|
||||
['light-highcontrast'],
|
||||
['dark-highcontrast'],
|
||||
['opendyslexic'],
|
||||
['', OCSBadRequestException::class],
|
||||
|
||||
@@ -68,6 +68,10 @@ class ThemesServiceTest extends TestCase {
|
||||
->method('getColorPrimary')
|
||||
->willReturn('#0082c9');
|
||||
|
||||
$this->themingDefaults->expects($this->any())
|
||||
->method('getDefaultColorPrimary')
|
||||
->willReturn('#0082c9');
|
||||
|
||||
$this->initThemes();
|
||||
|
||||
$this->themesService = new ThemesService(
|
||||
@@ -84,7 +88,7 @@ class ThemesServiceTest extends TestCase {
|
||||
'default',
|
||||
'light',
|
||||
'dark',
|
||||
'highcontrast',
|
||||
'light-highcontrast',
|
||||
'dark-highcontrast',
|
||||
'opendyslexic',
|
||||
];
|
||||
@@ -98,7 +102,7 @@ class ThemesServiceTest extends TestCase {
|
||||
['dark', [], ['dark']],
|
||||
['dark', ['dark'], ['dark']],
|
||||
['opendyslexic', ['dark'], ['dark', 'opendyslexic']],
|
||||
['dark', ['highcontrast', 'opendyslexic'], ['opendyslexic', 'dark']],
|
||||
['dark', ['light-highcontrast', 'opendyslexic'], ['opendyslexic', 'dark']],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -132,7 +136,7 @@ class ThemesServiceTest extends TestCase {
|
||||
['dark', [], []],
|
||||
['dark', ['dark'], []],
|
||||
['opendyslexic', ['dark', 'opendyslexic'], ['dark'], ],
|
||||
['highcontrast', ['opendyslexic'], ['opendyslexic']],
|
||||
['light-highcontrast', ['opendyslexic'], ['opendyslexic']],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -156,7 +160,7 @@ class ThemesServiceTest extends TestCase {
|
||||
->method('getUserValue')
|
||||
->with('user', Application::APP_ID, 'enabled-themes', '[]')
|
||||
->willReturn(json_encode($enabledThemes));
|
||||
|
||||
|
||||
|
||||
$this->assertEquals($expectedEnabled, $this->themesService->disableTheme($this->themes[$toDisable]));
|
||||
}
|
||||
@@ -167,7 +171,7 @@ class ThemesServiceTest extends TestCase {
|
||||
['dark', [], false],
|
||||
['dark', ['dark'], true],
|
||||
['opendyslexic', ['dark', 'opendyslexic'], true],
|
||||
['highcontrast', ['opendyslexic'], false],
|
||||
['light-highcontrast', ['opendyslexic'], false],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -190,7 +194,7 @@ class ThemesServiceTest extends TestCase {
|
||||
->method('getUserValue')
|
||||
->with('user', Application::APP_ID, 'enabled-themes', '[]')
|
||||
->willReturn(json_encode($enabledThemes));
|
||||
|
||||
|
||||
|
||||
$this->assertEquals($expected, $this->themesService->isEnabled($this->themes[$themeId]));
|
||||
}
|
||||
@@ -281,6 +285,7 @@ class ThemesServiceTest extends TestCase {
|
||||
'default' => new DefaultTheme(
|
||||
$util,
|
||||
$this->themingDefaults,
|
||||
$this->userSession,
|
||||
$urlGenerator,
|
||||
$imageManager,
|
||||
$this->config,
|
||||
@@ -289,6 +294,7 @@ class ThemesServiceTest extends TestCase {
|
||||
'light' => new LightTheme(
|
||||
$util,
|
||||
$this->themingDefaults,
|
||||
$this->userSession,
|
||||
$urlGenerator,
|
||||
$imageManager,
|
||||
$this->config,
|
||||
@@ -297,14 +303,16 @@ class ThemesServiceTest extends TestCase {
|
||||
'dark' => new DarkTheme(
|
||||
$util,
|
||||
$this->themingDefaults,
|
||||
$this->userSession,
|
||||
$urlGenerator,
|
||||
$imageManager,
|
||||
$this->config,
|
||||
$l10n,
|
||||
),
|
||||
'highcontrast' => new HighContrastTheme(
|
||||
'light-highcontrast' => new HighContrastTheme(
|
||||
$util,
|
||||
$this->themingDefaults,
|
||||
$this->userSession,
|
||||
$urlGenerator,
|
||||
$imageManager,
|
||||
$this->config,
|
||||
@@ -313,6 +321,7 @@ class ThemesServiceTest extends TestCase {
|
||||
'dark-highcontrast' => new DarkHighContrastTheme(
|
||||
$util,
|
||||
$this->themingDefaults,
|
||||
$this->userSession,
|
||||
$urlGenerator,
|
||||
$imageManager,
|
||||
$this->config,
|
||||
@@ -321,6 +330,7 @@ class ThemesServiceTest extends TestCase {
|
||||
'opendyslexic' => new DyslexiaFont(
|
||||
$util,
|
||||
$this->themingDefaults,
|
||||
$this->userSession,
|
||||
$urlGenerator,
|
||||
$imageManager,
|
||||
$this->config,
|
||||
|
||||
@@ -97,7 +97,7 @@ class AdminTest extends TestCase {
|
||||
->willReturn('MySlogan');
|
||||
$this->themingDefaults
|
||||
->expects($this->once())
|
||||
->method('getColorPrimary')
|
||||
->method('getDefaultColorPrimary')
|
||||
->willReturn('#fff');
|
||||
$this->urlGenerator
|
||||
->expects($this->once())
|
||||
@@ -156,7 +156,7 @@ class AdminTest extends TestCase {
|
||||
->willReturn('MySlogan');
|
||||
$this->themingDefaults
|
||||
->expects($this->once())
|
||||
->method('getColorPrimary')
|
||||
->method('getDefaultColorPrimary')
|
||||
->willReturn('#fff');
|
||||
$this->urlGenerator
|
||||
->expects($this->once())
|
||||
|
||||
@@ -45,6 +45,7 @@ use OCP\AppFramework\Services\IInitialState;
|
||||
use OCP\IConfig;
|
||||
use OCP\IL10N;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\IUserSession;
|
||||
use Test\TestCase;
|
||||
|
||||
class PersonalTest extends TestCase {
|
||||
@@ -83,7 +84,7 @@ class PersonalTest extends TestCase {
|
||||
$this->formatThemeForm('default'),
|
||||
$this->formatThemeForm('light'),
|
||||
$this->formatThemeForm('dark'),
|
||||
$this->formatThemeForm('highcontrast'),
|
||||
$this->formatThemeForm('light-highcontrast'),
|
||||
$this->formatThemeForm('dark-highcontrast'),
|
||||
$this->formatThemeForm('opendyslexic'),
|
||||
]],
|
||||
@@ -128,6 +129,7 @@ class PersonalTest extends TestCase {
|
||||
private function initThemes() {
|
||||
$util = $this->createMock(Util::class);
|
||||
$themingDefaults = $this->createMock(ThemingDefaults::class);
|
||||
$userSession = $this->createMock(IUserSession::class);
|
||||
$urlGenerator = $this->createMock(IURLGenerator::class);
|
||||
$imageManager = $this->createMock(ImageManager::class);
|
||||
$config = $this->createMock(IConfig::class);
|
||||
@@ -136,11 +138,16 @@ class PersonalTest extends TestCase {
|
||||
$themingDefaults->expects($this->any())
|
||||
->method('getColorPrimary')
|
||||
->willReturn('#0082c9');
|
||||
|
||||
$themingDefaults->expects($this->any())
|
||||
->method('getDefaultColorPrimary')
|
||||
->willReturn('#0082c9');
|
||||
|
||||
$this->themes = [
|
||||
'default' => new DefaultTheme(
|
||||
$util,
|
||||
$themingDefaults,
|
||||
$userSession,
|
||||
$urlGenerator,
|
||||
$imageManager,
|
||||
$config,
|
||||
@@ -149,6 +156,7 @@ class PersonalTest extends TestCase {
|
||||
'light' => new LightTheme(
|
||||
$util,
|
||||
$themingDefaults,
|
||||
$userSession,
|
||||
$urlGenerator,
|
||||
$imageManager,
|
||||
$config,
|
||||
@@ -157,14 +165,16 @@ class PersonalTest extends TestCase {
|
||||
'dark' => new DarkTheme(
|
||||
$util,
|
||||
$themingDefaults,
|
||||
$userSession,
|
||||
$urlGenerator,
|
||||
$imageManager,
|
||||
$config,
|
||||
$l10n,
|
||||
),
|
||||
'highcontrast' => new HighContrastTheme(
|
||||
'light-highcontrast' => new HighContrastTheme(
|
||||
$util,
|
||||
$themingDefaults,
|
||||
$userSession,
|
||||
$urlGenerator,
|
||||
$imageManager,
|
||||
$config,
|
||||
@@ -173,6 +183,7 @@ class PersonalTest extends TestCase {
|
||||
'dark-highcontrast' => new DarkHighContrastTheme(
|
||||
$util,
|
||||
$themingDefaults,
|
||||
$userSession,
|
||||
$urlGenerator,
|
||||
$imageManager,
|
||||
$config,
|
||||
@@ -181,6 +192,7 @@ class PersonalTest extends TestCase {
|
||||
'opendyslexic' => new DyslexiaFont(
|
||||
$util,
|
||||
$themingDefaults,
|
||||
$userSession,
|
||||
$urlGenerator,
|
||||
$imageManager,
|
||||
$config,
|
||||
|
||||
@@ -32,6 +32,7 @@ use OCP\Files\IAppData;
|
||||
use OCP\IConfig;
|
||||
use OCP\IL10N;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\IUserSession;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Test\TestCase;
|
||||
|
||||
@@ -52,6 +53,7 @@ class DefaultThemeTest extends TestCase {
|
||||
|
||||
protected function setUp(): void {
|
||||
$this->themingDefaults = $this->createMock(ThemingDefaults::class);
|
||||
$this->userSession = $this->createMock(IUserSession::class);
|
||||
$this->urlGenerator = $this->createMock(IURLGenerator::class);
|
||||
$this->imageManager = $this->createMock(ImageManager::class);
|
||||
$this->config = $this->createMock(IConfig::class);
|
||||
@@ -68,6 +70,11 @@ class DefaultThemeTest extends TestCase {
|
||||
->method('getColorPrimary')
|
||||
->willReturn('#0082c9');
|
||||
|
||||
$this->themingDefaults
|
||||
->expects($this->any())
|
||||
->method('getDefaultColorPrimary')
|
||||
->willReturn('#0082c9');
|
||||
|
||||
$this->l10n
|
||||
->expects($this->any())
|
||||
->method('t')
|
||||
@@ -85,6 +92,7 @@ class DefaultThemeTest extends TestCase {
|
||||
$this->defaultTheme = new DefaultTheme(
|
||||
$util,
|
||||
$this->themingDefaults,
|
||||
$this->userSession,
|
||||
$this->urlGenerator,
|
||||
$this->imageManager,
|
||||
$this->config,
|
||||
|
||||
@@ -56,6 +56,7 @@ class DyslexiaFontTest extends TestCase {
|
||||
|
||||
protected function setUp(): void {
|
||||
$this->themingDefaults = $this->createMock(ThemingDefaults::class);
|
||||
$this->userSession = $this->createMock(IUserSession::class);
|
||||
$this->imageManager = $this->createMock(ImageManager::class);
|
||||
$this->config = $this->createMock(IConfig::class);
|
||||
$this->l10n = $this->createMock(IL10N::class);
|
||||
@@ -83,6 +84,11 @@ class DyslexiaFontTest extends TestCase {
|
||||
->method('getColorPrimary')
|
||||
->willReturn('#0082c9');
|
||||
|
||||
$this->themingDefaults
|
||||
->expects($this->any())
|
||||
->method('getDefaultColorPrimary')
|
||||
->willReturn('#0082c9');
|
||||
|
||||
$this->l10n
|
||||
->expects($this->any())
|
||||
->method('t')
|
||||
@@ -93,6 +99,7 @@ class DyslexiaFontTest extends TestCase {
|
||||
$this->dyslexiaFont = new DyslexiaFont(
|
||||
$util,
|
||||
$this->themingDefaults,
|
||||
$this->userSession,
|
||||
$this->urlGenerator,
|
||||
$this->imageManager,
|
||||
$this->config,
|
||||
@@ -142,7 +149,7 @@ class DyslexiaFontTest extends TestCase {
|
||||
|
||||
/**
|
||||
* @dataProvider dataTestGetCustomCss
|
||||
*
|
||||
*
|
||||
* Ensure the fonts are always loaded from the web root
|
||||
* despite having url rewriting enabled or not
|
||||
*
|
||||
@@ -155,7 +162,7 @@ class DyslexiaFontTest extends TestCase {
|
||||
->method('getSystemValue')
|
||||
->with('htaccess.IgnoreFrontController', false)
|
||||
->willReturn($prettyUrlsEnabled);
|
||||
|
||||
|
||||
$this->assertStringContainsString("'$webRoot/apps/theming/fonts/OpenDyslexic-Regular.woff'", $this->dyslexiaFont->getCustomCss());
|
||||
$this->assertStringNotContainsString('index.php', $this->dyslexiaFont->getCustomCss());
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
namespace OCA\Theming\Tests;
|
||||
|
||||
use OCA\Theming\ImageManager;
|
||||
use OCA\Theming\Service\BackgroundService;
|
||||
use OCA\Theming\ThemingDefaults;
|
||||
use OCA\Theming\Util;
|
||||
use OCP\App\IAppManager;
|
||||
@@ -46,6 +47,8 @@ use OCP\IConfig;
|
||||
use OCP\IL10N;
|
||||
use OCP\INavigationManager;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\IUser;
|
||||
use OCP\IUserSession;
|
||||
use Test\TestCase;
|
||||
|
||||
class ThemingDefaultsTest extends TestCase {
|
||||
@@ -53,6 +56,8 @@ class ThemingDefaultsTest extends TestCase {
|
||||
private $config;
|
||||
/** @var IL10N|\PHPUnit\Framework\MockObject\MockObject */
|
||||
private $l10n;
|
||||
/** @var IUserSession|\PHPUnit\Framework\MockObject\MockObject */
|
||||
private $userSession;
|
||||
/** @var IURLGenerator|\PHPUnit\Framework\MockObject\MockObject */
|
||||
private $urlGenerator;
|
||||
/** @var \OC_Defaults|\PHPUnit\Framework\MockObject\MockObject */
|
||||
@@ -78,6 +83,7 @@ class ThemingDefaultsTest extends TestCase {
|
||||
parent::setUp();
|
||||
$this->config = $this->createMock(IConfig::class);
|
||||
$this->l10n = $this->createMock(IL10N::class);
|
||||
$this->userSession = $this->createMock(IUserSession::class);
|
||||
$this->urlGenerator = $this->createMock(IURLGenerator::class);
|
||||
$this->cacheFactory = $this->createMock(ICacheFactory::class);
|
||||
$this->cache = $this->createMock(ICache::class);
|
||||
@@ -93,6 +99,7 @@ class ThemingDefaultsTest extends TestCase {
|
||||
$this->template = new ThemingDefaults(
|
||||
$this->config,
|
||||
$this->l10n,
|
||||
$this->userSession,
|
||||
$this->urlGenerator,
|
||||
$this->cacheFactory,
|
||||
$this->util,
|
||||
@@ -415,11 +422,11 @@ class ThemingDefaultsTest extends TestCase {
|
||||
$this->assertEquals('<a href="url" target="_blank" rel="noreferrer noopener" class="entity-name">Name</a> – Slogan', $this->template->getShortFooter());
|
||||
}
|
||||
|
||||
public function testgetColorPrimaryWithDefault() {
|
||||
public function testGetColorPrimaryWithDefault() {
|
||||
$this->config
|
||||
->expects($this->once())
|
||||
->method('getAppValue')
|
||||
->with('theming', 'color', $this->defaults->getColorPrimary())
|
||||
->with('theming', 'color', null)
|
||||
->willReturn($this->defaults->getColorPrimary());
|
||||
|
||||
$this->assertEquals($this->defaults->getColorPrimary(), $this->template->getColorPrimary());
|
||||
@@ -429,12 +436,80 @@ class ThemingDefaultsTest extends TestCase {
|
||||
$this->config
|
||||
->expects($this->once())
|
||||
->method('getAppValue')
|
||||
->with('theming', 'color', $this->defaults->getColorPrimary())
|
||||
->with('theming', 'color', null)
|
||||
->willReturn('#fff');
|
||||
|
||||
$this->assertEquals('#fff', $this->template->getColorPrimary());
|
||||
}
|
||||
|
||||
public function testGetColorPrimaryWithDefaultBackground() {
|
||||
$user = $this->createMock(IUser::class);
|
||||
$this->userSession->expects($this->any())
|
||||
->method('getUser')
|
||||
->willReturn($user);
|
||||
$user->expects($this->any())
|
||||
->method('getUID')
|
||||
->willReturn('user');
|
||||
|
||||
$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', '')
|
||||
->willReturn(array_keys(BackgroundService::SHIPPED_BACKGROUNDS)[$backgroundIndex]);
|
||||
|
||||
$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', '')
|
||||
->willReturn('#fff');
|
||||
|
||||
$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', '')
|
||||
->willReturn('nextcloud');
|
||||
|
||||
$this->assertEquals($this->template->getDefaultColorPrimary(), $this->template->getColorPrimary());
|
||||
}
|
||||
|
||||
public function testSet() {
|
||||
$this->config
|
||||
->expects($this->exactly(2))
|
||||
@@ -542,7 +617,7 @@ class ThemingDefaultsTest extends TestCase {
|
||||
->method('getAppValue')
|
||||
->withConsecutive(
|
||||
['theming', 'cachebuster', '0'],
|
||||
['theming', 'color', $this->defaults->getColorPrimary()],
|
||||
['theming', 'color', null],
|
||||
)->willReturnOnConsecutiveCalls(
|
||||
'15',
|
||||
$this->defaults->getColorPrimary(),
|
||||
|
||||
@@ -406,7 +406,7 @@ export default {
|
||||
const hiddenField = document.createElement('input')
|
||||
hiddenField.setAttribute('type', 'hidden')
|
||||
hiddenField.setAttribute('name', 'updater-secret-input')
|
||||
hiddenField.setAttribute('value', data.token)
|
||||
hiddenField.setAttribute('value', data)
|
||||
|
||||
form.appendChild(hiddenField)
|
||||
|
||||
|
||||
@@ -94,8 +94,8 @@
|
||||
}
|
||||
|
||||
.icon-user-status-invisible {
|
||||
/* $dir is the app name, so we add this to the icon var to avoid conflicts between apps */
|
||||
background-image: var(--icon-user-status-invisible-dark);
|
||||
background-image: url("../img/user-status-invisible.svg");
|
||||
filter: var(--background-invert-if-dark);
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=user-status-menu.css.map */
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"sourceRoot":"","sources":["../../../core/css/variables.scss","user-status-menu.scss","../../../core/css/functions.scss"],"names":[],"mappings":";AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;ACAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;ACAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAsBA;AAAA;AAAA;AA4BA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AD1BA;ACuCC;EAEA;;;ADrCD;EACC;;;AAGD;EACC;;;AAGD;EACC;;;AAID;ACsBC;EAEA","file":"user-status-menu.css"}
|
||||
{"version":3,"sourceRoot":"","sources":["../../../core/css/variables.scss","user-status-menu.scss","../../../core/css/functions.scss"],"names":[],"mappings":";AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;ACAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;ACAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAsBA;AAAA;AAAA;AA4BA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AD1BA;ACuCC;EAEA;;;ADrCD;EACC;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA","file":"user-status-menu.css"}
|
||||
@@ -38,7 +38,7 @@
|
||||
background-image: url('../img/user-status-dnd.svg');
|
||||
}
|
||||
|
||||
// TODO: debug why icon-black-white does not work here
|
||||
.icon-user-status-invisible {
|
||||
@include icon-color('user-status-invisible', 'user_status', variables.$color-black, 1);
|
||||
background-image: url('../img/user-status-invisible.svg');
|
||||
filter: var(--background-invert-if-dark);
|
||||
}
|
||||
|
||||
@@ -48,11 +48,17 @@ import NcEmojiPicker from '@nextcloud/vue/dist/Components/NcEmojiPicker.js'
|
||||
|
||||
export default {
|
||||
name: 'CustomMessageInput',
|
||||
|
||||
components: {
|
||||
NcButton,
|
||||
NcEmojiPicker,
|
||||
},
|
||||
|
||||
props: {
|
||||
icon: {
|
||||
type: String,
|
||||
default: '😀',
|
||||
},
|
||||
message: {
|
||||
type: String,
|
||||
required: true,
|
||||
@@ -63,11 +69,13 @@ export default {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
emits: [
|
||||
'change',
|
||||
'submit',
|
||||
'icon-selected',
|
||||
],
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Returns the user-set icon or a smiley in case no icon is set
|
||||
@@ -78,6 +86,7 @@ export default {
|
||||
return this.icon || '😀'
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
focus() {
|
||||
this.$refs.input.focus()
|
||||
@@ -96,8 +105,8 @@ export default {
|
||||
this.$emit('submit', event.target.value)
|
||||
},
|
||||
|
||||
setIcon(event) {
|
||||
this.$emit('icon-selected', event)
|
||||
setIcon(icon) {
|
||||
this.$emit('select-icon', icon)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -27,8 +27,9 @@
|
||||
type="radio"
|
||||
name="user-status-online"
|
||||
@change="onChange">
|
||||
<label :for="id" :class="icon" class="user-status-online-select__label">
|
||||
<label :for="id" class="user-status-online-select__label">
|
||||
{{ label }}
|
||||
<span :class="icon" role="img" />
|
||||
<em class="user-status-online-select__subline">{{ subline }}</em>
|
||||
</label>
|
||||
</div>
|
||||
@@ -76,6 +77,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use 'sass:math';
|
||||
$icon-size: 24px;
|
||||
$label-padding: 8px;
|
||||
|
||||
@@ -91,6 +93,7 @@ $label-padding: 8px;
|
||||
}
|
||||
|
||||
&__label {
|
||||
position: relative;
|
||||
display: block;
|
||||
margin: $label-padding;
|
||||
padding: $label-padding;
|
||||
@@ -105,6 +108,15 @@ $label-padding: 8px;
|
||||
& {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
span {
|
||||
position: absolute;
|
||||
top: calc(50% - math.div($icon-size, 2));
|
||||
left: $label-padding;
|
||||
display: block;
|
||||
width: $icon-size;
|
||||
height: $icon-size;
|
||||
}
|
||||
}
|
||||
|
||||
&__input:checked + &__label,
|
||||
|
||||
@@ -42,10 +42,11 @@
|
||||
</div>
|
||||
<div class="set-status-modal__custom-input">
|
||||
<CustomMessageInput ref="customMessageInput"
|
||||
:icon="icon"
|
||||
:message="message"
|
||||
@change="setMessage"
|
||||
@submit="saveStatus"
|
||||
@iconSelected="setIcon" />
|
||||
@select-icon="setIcon" />
|
||||
</div>
|
||||
<PredefinedStatusesList @select-status="selectPredefinedMessage" />
|
||||
<ClearAtSelect :clear-at="clearAt"
|
||||
@@ -74,12 +75,12 @@
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import NcModal from '@nextcloud/vue/dist/Components/NcModal'
|
||||
import NcButton from '@nextcloud/vue/dist/Components/NcButton'
|
||||
import { getAllStatusOptions } from '../services/statusOptionsService'
|
||||
import OnlineStatusMixin from '../mixins/OnlineStatusMixin'
|
||||
import PredefinedStatusesList from './PredefinedStatusesList'
|
||||
import CustomMessageInput from './CustomMessageInput'
|
||||
import ClearAtSelect from './ClearAtSelect'
|
||||
import OnlineStatusSelect from './OnlineStatusSelect'
|
||||
import { getAllStatusOptions } from '../services/statusOptionsService.js'
|
||||
import OnlineStatusMixin from '../mixins/OnlineStatusMixin.js'
|
||||
import PredefinedStatusesList from './PredefinedStatusesList.vue'
|
||||
import CustomMessageInput from './CustomMessageInput.vue'
|
||||
import ClearAtSelect from './ClearAtSelect.vue'
|
||||
import OnlineStatusSelect from './OnlineStatusSelect.vue'
|
||||
|
||||
export default {
|
||||
name: 'SetStatusModal',
|
||||
|
||||
@@ -1 +1 @@
|
||||
3650d-5e41fd9674803
|
||||
3707b-5eab9a4124dc3
|
||||
|
||||
@@ -4622,7 +4622,7 @@ function idn_to_ascii($domain, $options = 0, $variant = INTL_IDNA_VARIANT_2003,
|
||||
* @param int $variant [optional] <p>
|
||||
* Either INTL_IDNA_VARIANT_2003 for IDNA 2003 or INTL_IDNA_VARIANT_UTS46 for UTS #46.
|
||||
* </p>
|
||||
* @param int &$idna_info [optional] <p>
|
||||
* @param array &$idna_info [optional] <p>
|
||||
* This parameter can be used only if INTL_IDNA_VARIANT_UTS46 was used for variant.
|
||||
* In that case, it will be filled with an array with the keys 'result',
|
||||
* the possibly illegal result of the transformation, 'isTransitionalDifferent',
|
||||
@@ -4634,7 +4634,7 @@ function idn_to_ascii($domain, $options = 0, $variant = INTL_IDNA_VARIANT_2003,
|
||||
* RFC 3490 4.2 states though "ToUnicode never fails. If any step fails, then the original input
|
||||
* sequence is returned immediately in that step."
|
||||
*/
|
||||
function idn_to_utf8($domain, $options = 0, $variant = INTL_IDNA_VARIANT_2003, array &$idna_info) { }
|
||||
function idn_to_utf8($domain, $options = 0, $variant = INTL_IDNA_VARIANT_2003, array &$idna_info = null) { }
|
||||
|
||||
/**
|
||||
* (PHP 5 >=5.5.0 PECL intl >= 3.0.0a1)<br/>
|
||||
|
||||
@@ -2177,7 +2177,7 @@ class Redis
|
||||
*
|
||||
* @param string $pattern pattern, using '*' as a wildcard
|
||||
*
|
||||
* @return array string[] The keys that match a certain pattern.
|
||||
* @return string[]|false The keys that match a certain pattern.
|
||||
*
|
||||
* @link https://redis.io/commands/keys
|
||||
* @example
|
||||
|
||||
@@ -45,14 +45,14 @@ class BackgroundCleanupUpdaterBackupsJob extends QueuedJob {
|
||||
* This job cleans up all backups except the latest 3 from the updaters backup directory
|
||||
*/
|
||||
public function run($arguments) {
|
||||
$dataDir = $this->config->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data');
|
||||
$updateDir = $this->config->getSystemValue('updatedirectory', null) ?? $this->config->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data');
|
||||
$instanceId = $this->config->getSystemValue('instanceid', null);
|
||||
|
||||
if (!is_string($instanceId) || empty($instanceId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$updaterFolderPath = $dataDir . '/updater-' . $instanceId;
|
||||
$updaterFolderPath = $updateDir . '/updater-' . $instanceId;
|
||||
$backupFolderPath = $updaterFolderPath . '/backups';
|
||||
if (file_exists($backupFolderPath)) {
|
||||
$this->log->info("$backupFolderPath exists - start to clean it up");
|
||||
|
||||
@@ -57,6 +57,16 @@ class ReferenceApiController extends \OCP\AppFramework\OCSController {
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
*/
|
||||
public function resolveOne(string $reference): DataResponse {
|
||||
$resolvedReference = $this->referenceManager->resolveReference(trim($reference));
|
||||
|
||||
$response = new DataResponse(['references' => [ $reference => $resolvedReference ]]);
|
||||
$response->cacheFor(3600, false, true);
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
|
||||
@@ -59,9 +59,11 @@ class ReferenceController extends Controller {
|
||||
$appData = $this->appDataFactory->get('core');
|
||||
$folder = $appData->getFolder('opengraph');
|
||||
$file = $folder->getFile($referenceId);
|
||||
return new DataDownloadResponse($file->getContent(), $referenceId, $reference->getImageContentType());
|
||||
$response = new DataDownloadResponse($file->getContent(), $referenceId, $reference->getImageContentType());
|
||||
} catch (NotFoundException|NotPermittedException $e) {
|
||||
return new DataResponse('', Http::STATUS_NOT_FOUND);
|
||||
$response = new DataResponse('', Http::STATUS_NOT_FOUND);
|
||||
}
|
||||
$response->cacheFor(3600, false, true);
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
72
core/Migrations/Version25000Date20221007010957.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright 2022 Christopher Ng <chrng8@gmail.com>
|
||||
*
|
||||
* @author Christopher Ng <chrng8@gmail.com>
|
||||
*
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OC\Core\Migrations;
|
||||
|
||||
use Closure;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\Migration\IOutput;
|
||||
use OCP\Migration\SimpleMigrationStep;
|
||||
|
||||
/**
|
||||
* User background settings handling was moved from the
|
||||
* dashboard app to the theming app so we migrate the
|
||||
* respective preference values here
|
||||
*
|
||||
*/
|
||||
class Version25000Date20221007010957 extends SimpleMigrationStep {
|
||||
protected IDBConnection $connection;
|
||||
|
||||
public function __construct(IDBConnection $connection) {
|
||||
$this->connection = $connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param IOutput $output
|
||||
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
|
||||
* @param array $options
|
||||
*/
|
||||
public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
|
||||
$cleanUpQuery = $this->connection->getQueryBuilder();
|
||||
$cleanUpQuery->delete('preferences')
|
||||
->where($cleanUpQuery->expr()->eq('appid', $cleanUpQuery->createNamedParameter('theming')))
|
||||
->andWhere($cleanUpQuery->expr()->orX(
|
||||
$cleanUpQuery->expr()->eq('configkey', $cleanUpQuery->createNamedParameter('background')),
|
||||
$cleanUpQuery->expr()->eq('configkey', $cleanUpQuery->createNamedParameter('backgroundVersion')),
|
||||
));
|
||||
$cleanUpQuery->executeStatement();
|
||||
|
||||
$updateQuery = $this->connection->getQueryBuilder();
|
||||
$updateQuery->update('preferences')
|
||||
->set('appid', $updateQuery->createNamedParameter('theming'))
|
||||
->where($updateQuery->expr()->eq('appid', $updateQuery->createNamedParameter('dashboard')))
|
||||
->andWhere($updateQuery->expr()->orX(
|
||||
$updateQuery->expr()->eq('configkey', $updateQuery->createNamedParameter('background')),
|
||||
$updateQuery->expr()->eq('configkey', $updateQuery->createNamedParameter('backgroundVersion')),
|
||||
));
|
||||
$updateQuery->executeStatement();
|
||||
}
|
||||
}
|
||||
@@ -89,14 +89,14 @@ html {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
background-color: var(--color-primary);
|
||||
background-color: var(--color-main-background-plain, var(--color-main-background));
|
||||
background-image: var(--image-main-background);
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--color-primary);
|
||||
background-color: var(--color-main-background-plain, var(--color-main-background));
|
||||
background-image: var(--image-main-background);
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
@@ -814,7 +814,7 @@ kbd {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: var(--color-main-background);
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
border: 0;
|
||||
border-radius: calc(var(--default-clickable-area) / 2);
|
||||
@@ -958,6 +958,11 @@ kbd {
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.contact .popovermenu ul > li > a > img,
|
||||
.popover__menu > li > a > img {
|
||||
filter: var(--background-invert-if-dark);
|
||||
}
|
||||
|
||||
.bubble,
|
||||
.app-navigation-entry-menu,
|
||||
.popovermenu {
|
||||
|
||||