mirror of https://github.com/nextcloud/server
Extract colour from custom background
Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>
This commit is contained in:
parent
cedae7c6d7
commit
064fa10ecf
|
@ -0,0 +1,98 @@
|
|||
name: Cypress
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- stable*
|
||||
|
||||
env:
|
||||
APP_NAME: viewer
|
||||
BRANCH: ${{ github.base_ref }}
|
||||
TESTING: true
|
||||
|
||||
jobs:
|
||||
init:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout server
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Read package.json node and npm engines version
|
||||
uses: skjnldsv/read-package-engines-version-actions@v1.2
|
||||
id: versions
|
||||
with:
|
||||
fallbackNode: "^12"
|
||||
fallbackNpm: "^6"
|
||||
|
||||
- name: Set up node ${{ steps.versions.outputs.nodeVersion }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'npm'
|
||||
node-version: ${{ steps.versions.outputs.nodeVersion }}
|
||||
|
||||
- name: Set up npm ${{ steps.versions.outputs.npmVersion }}
|
||||
run: npm i -g npm@"${{ steps.versions.outputs.npmVersion }}"
|
||||
|
||||
- name: Install dependencies & build app
|
||||
run: |
|
||||
npm ci
|
||||
TESTING=true npm run build --if-present
|
||||
|
||||
- name: Save context
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
key: cypress-context-${{ github.run_id }}
|
||||
path: /home/runner/work/server
|
||||
|
||||
cypress:
|
||||
runs-on: ubuntu-latest
|
||||
needs: init
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# run multiple copies of the current job in parallel
|
||||
containers: [1]
|
||||
|
||||
name: runner ${{ matrix.containers }}
|
||||
|
||||
steps:
|
||||
- name: Restore context
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
key: cypress-context-${{ github.run_id }}
|
||||
path: /home/runner/work/server
|
||||
|
||||
- name: Run E2E cypress tests
|
||||
uses: cypress-io/github-action@v4
|
||||
with:
|
||||
record: true
|
||||
parallel: true
|
||||
# cypress env
|
||||
ci-build-id: ${{ github.sha }}-${{ github.run_number }}
|
||||
tag: ${{ github.event_name }}
|
||||
env:
|
||||
# Needs to be prefixed with CYPRESS_
|
||||
CYPRESS_BRANCH: ${{ env.BRANCH }}
|
||||
CYPRESS_GH: true
|
||||
# https://github.com/cypress-io/github-action/issues/124
|
||||
COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title }}
|
||||
# Needed for some specific code workarounds
|
||||
TESTING: true
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||
|
||||
summary:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [init, cypress]
|
||||
|
||||
if: always()
|
||||
|
||||
name: cypress-summary
|
||||
|
||||
steps:
|
||||
- name: Summary status
|
||||
run: if ${{ needs.init.result != 'success' || ( needs.cypress.result != 'success' && needs.cypress.result != 'skipped' ) }}; then exit 1; fi
|
|
@ -163,3 +163,7 @@ composer.phar
|
|||
|
||||
./.htaccess
|
||||
core/js/mimetypelist.js
|
||||
|
||||
# Tests - cypress
|
||||
cypress/snapshots
|
||||
cypress/videos
|
||||
|
|
|
@ -54,9 +54,6 @@
|
|||
--background-invert-if-dark: no;
|
||||
--background-invert-if-bright: invert(100%);
|
||||
--background-image-invert-if-bright: no;
|
||||
--image-background: url('/core/img/app-background.jpg');
|
||||
--image-background-default: url('/core/img/app-background.jpg');
|
||||
--color-background-plain: #0082c9;
|
||||
--primary-invert-if-bright: no;
|
||||
--color-primary: #006aa3;
|
||||
--color-primary-default: #0082c9;
|
||||
|
@ -75,4 +72,6 @@
|
|||
--color-primary-element-light-hover: #dbe5ea;
|
||||
--color-primary-element-text-dark: #ededed;
|
||||
--gradient-primary-background: linear-gradient(40deg, var(--color-primary) 0%, var(--color-primary-hover) 100%);
|
||||
--image-background-default: url('/apps/theming/img/background/kamil-porembinski-clouds.jpg');
|
||||
--color-background-plain: #0082c9;
|
||||
}
|
||||
|
|
|
@ -1,148 +0,0 @@
|
|||
#theming input {
|
||||
width: 230px;
|
||||
}
|
||||
#theming input:focus,
|
||||
#theming input:active {
|
||||
padding-right: 30px;
|
||||
}
|
||||
#theming .fileupload {
|
||||
display: none;
|
||||
}
|
||||
#theming div > label {
|
||||
position: relative;
|
||||
}
|
||||
#theming .theme-undo {
|
||||
position: absolute;
|
||||
top: -7px;
|
||||
right: 4px;
|
||||
cursor: pointer;
|
||||
opacity: 0.3;
|
||||
padding: 7px;
|
||||
vertical-align: top;
|
||||
display: inline-block;
|
||||
visibility: hidden;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
}
|
||||
#theming form.uploadButton {
|
||||
width: 411px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
#theming form .theme-undo,
|
||||
#theming .theme-remove-bg {
|
||||
cursor: pointer;
|
||||
opacity: 0.3;
|
||||
padding: 7px;
|
||||
vertical-align: top;
|
||||
display: inline-block;
|
||||
float: right;
|
||||
position: relative;
|
||||
top: 4px;
|
||||
right: 0px;
|
||||
visibility: visible;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
margin-left: auto;
|
||||
}
|
||||
#theming form .theme-undo:not([style*="display:"]) ~ .theme-remove-bg {
|
||||
margin-left: 0;
|
||||
}
|
||||
#theming input[type=text]:hover + .theme-undo,
|
||||
#theming input[type=text] + .theme-undo:hover,
|
||||
#theming input[type=text]:focus + .theme-undo,
|
||||
#theming input[type=text]:active + .theme-undo,
|
||||
#theming input[type=url]:hover + .theme-undo,
|
||||
#theming input[type=url] + .theme-undo:hover,
|
||||
#theming input[type=url]:focus + .theme-undo,
|
||||
#theming input[type=url]:active + .theme-undo {
|
||||
visibility: visible;
|
||||
}
|
||||
#theming label span {
|
||||
display: inline-block;
|
||||
min-width: 175px;
|
||||
max-width: 175px;
|
||||
white-space: wrap;
|
||||
padding: 8px 0px;
|
||||
vertical-align: top;
|
||||
}
|
||||
#theming .icon-upload,
|
||||
#theming .uploadButton .icon-loading-small {
|
||||
padding: 8px 20px;
|
||||
width: 20px;
|
||||
margin: 2px 0px;
|
||||
min-height: 32px;
|
||||
display: inline-block;
|
||||
}
|
||||
#theming #theming_settings_status {
|
||||
height: 26px;
|
||||
margin: 10px;
|
||||
}
|
||||
#theming #theming_settings_loading {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-right: 10px;
|
||||
}
|
||||
#theming #theming_settings_msg {
|
||||
vertical-align: middle;
|
||||
border-radius: 3px;
|
||||
}
|
||||
#theming #theming-preview {
|
||||
width: 230px;
|
||||
height: 140px;
|
||||
background-size: cover;
|
||||
background-position: center center;
|
||||
text-align: center;
|
||||
margin-left: 178px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 20px;
|
||||
cursor: pointer;
|
||||
background-color: var(--color-primary-default);
|
||||
background-image: var(--image-background-default, var(--image-background-plain, linear-gradient(40deg, #0082c9 0%, #30b6ff 100%)));
|
||||
}
|
||||
#theming #theming-preview #theming-preview-logo {
|
||||
cursor: pointer;
|
||||
width: 20%;
|
||||
height: 20%;
|
||||
margin-top: 20px;
|
||||
display: inline-block;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
background-image: var(--image-logo, url("../../../core/img/logo/logo.svg"));
|
||||
}
|
||||
#theming .theming-hints {
|
||||
margin-top: 20px;
|
||||
}
|
||||
#theming .image-preview {
|
||||
display: inline-block;
|
||||
width: 80px;
|
||||
height: 36px;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
}
|
||||
#theming #theming-preview-logoheader {
|
||||
background-image: var(--image-logoheader);
|
||||
}
|
||||
#theming #theming-preview-favicon {
|
||||
background-image: var(--image-favicon);
|
||||
}
|
||||
#theming #user-theming {
|
||||
margin-top: 44px;
|
||||
display: flex;
|
||||
}
|
||||
#theming #user-theming > div {
|
||||
max-width: 400px;
|
||||
margin-bottom: 44px;
|
||||
}
|
||||
|
||||
/* transition effects for theming value changes */
|
||||
#header {
|
||||
transition: background-color 500ms linear;
|
||||
}
|
||||
#header svg, #header img {
|
||||
transition: 500ms filter linear;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=settings-admin.css.map */
|
|
@ -1,168 +0,0 @@
|
|||
#theming {
|
||||
input {
|
||||
width: 230px;
|
||||
}
|
||||
|
||||
input:focus,
|
||||
input:active {
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
.fileupload {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div > label {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.theme-undo {
|
||||
position: absolute;
|
||||
top: -7px; // input padding
|
||||
right: 4px; // input right margin + border
|
||||
cursor: pointer;
|
||||
opacity: .3;
|
||||
padding: 7px;
|
||||
vertical-align: top;
|
||||
display: inline-block;
|
||||
visibility: hidden;
|
||||
height: 32px; // height of input
|
||||
width: 32px; // height of input
|
||||
}
|
||||
form.uploadButton {
|
||||
width: 411px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
form .theme-undo,
|
||||
.theme-remove-bg {
|
||||
cursor: pointer;
|
||||
opacity: .3;
|
||||
padding: 7px;
|
||||
vertical-align: top;
|
||||
display: inline-block;
|
||||
float: right;
|
||||
position: relative;
|
||||
top: 4px;
|
||||
right: 0px;
|
||||
visibility: visible;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
// right align
|
||||
margin-left: auto;
|
||||
}
|
||||
form .theme-undo:not([style*="display:"]) ~ .theme-remove-bg {
|
||||
// Only align the undo button if both are shown
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
input[type='text']:hover + .theme-undo,
|
||||
input[type='text'] + .theme-undo:hover,
|
||||
input[type='text']:focus + .theme-undo,
|
||||
input[type='text']:active + .theme-undo,
|
||||
input[type='url']:hover + .theme-undo,
|
||||
input[type='url'] + .theme-undo:hover,
|
||||
input[type='url']:focus + .theme-undo,
|
||||
input[type='url']:active + .theme-undo{
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
label span {
|
||||
display: inline-block;
|
||||
min-width: 175px;
|
||||
max-width: 175px;
|
||||
white-space: wrap;
|
||||
padding: 8px 0px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.icon-upload,
|
||||
.uploadButton .icon-loading-small {
|
||||
padding: 8px 20px;
|
||||
width: 20px;
|
||||
margin: 2px 0px;
|
||||
min-height: 32px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#theming_settings_status {
|
||||
height: 26px;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
#theming_settings_loading {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
#theming_settings_msg {
|
||||
vertical-align: middle;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
#theming-preview {
|
||||
width: 230px;
|
||||
height: 140px;
|
||||
background-size: cover;
|
||||
background-position: center center;
|
||||
text-align: center;
|
||||
margin-left: 178px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 20px;
|
||||
cursor: pointer;
|
||||
background-color: var(--color-primary-default);
|
||||
background-image: var(--image-background-default, var(--image-background-plain, linear-gradient(40deg, #0082c9 0%, #30b6ff 100%)));
|
||||
|
||||
#theming-preview-logo {
|
||||
cursor: pointer;
|
||||
width: 20%;
|
||||
height: 20%;
|
||||
margin-top: 20px;
|
||||
display: inline-block;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
background-image: var(--image-logo, url('../../../core/img/logo/logo.svg'));
|
||||
}
|
||||
}
|
||||
|
||||
.theming-hints {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
display: inline-block;
|
||||
width: 80px;
|
||||
height: 36px;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
#theming-preview-logoheader {
|
||||
// Only using --image-logoheader to show the custom value only
|
||||
background-image: var(--image-logoheader);
|
||||
}
|
||||
|
||||
#theming-preview-favicon {
|
||||
background-image: var(--image-favicon);
|
||||
}
|
||||
|
||||
#user-theming {
|
||||
margin-top: 44px;
|
||||
display: flex;
|
||||
& > div {
|
||||
max-width: 400px;
|
||||
margin-bottom: 44px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* transition effects for theming value changes */
|
||||
#header {
|
||||
transition: background-color 500ms linear;
|
||||
svg, img {
|
||||
transition: 500ms filter linear;
|
||||
}
|
||||
}
|
|
@ -168,9 +168,15 @@ class UserThemeController extends OCSController {
|
|||
/**
|
||||
* @NoAdminRequired
|
||||
*/
|
||||
public function setBackground(string $type = BackgroundService::BACKGROUND_DEFAULT, string $value = ''): JSONResponse {
|
||||
public function setBackground(string $type = BackgroundService::BACKGROUND_DEFAULT, string $value = '', string $color = null): JSONResponse {
|
||||
$currentVersion = (int)$this->config->getUserValue($this->userId, Application::APP_ID, 'userCacheBuster', '0');
|
||||
|
||||
// Set color if provided
|
||||
if ($color) {
|
||||
$this->backgroundService->setColorBackground($color);
|
||||
}
|
||||
|
||||
// Set background image if provided
|
||||
try {
|
||||
switch ($type) {
|
||||
case BackgroundService::BACKGROUND_SHIPPED:
|
||||
|
@ -179,14 +185,13 @@ class UserThemeController extends OCSController {
|
|||
case BackgroundService::BACKGROUND_CUSTOM:
|
||||
$this->backgroundService->setFileBackground($value);
|
||||
break;
|
||||
case 'color':
|
||||
$this->backgroundService->setColorBackground($value);
|
||||
break;
|
||||
case BackgroundService::BACKGROUND_DEFAULT:
|
||||
$this->backgroundService->setDefaultBackground();
|
||||
break;
|
||||
default:
|
||||
return new JSONResponse(['error' => 'Invalid type provided'], Http::STATUS_BAD_REQUEST);
|
||||
if (!$color) {
|
||||
return new JSONResponse(['error' => 'Invalid type provided'], Http::STATUS_BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
|
||||
|
|
|
@ -94,7 +94,7 @@ class ImageManager {
|
|||
case 'favicon':
|
||||
return $this->urlGenerator->imagePath('core', 'logo/logo.png') . '?v=' . $cacheBusterCounter;
|
||||
case 'background':
|
||||
return $this->urlGenerator->linkTo(Application::APP_ID, "img/background/" . BackgroundService::DEFAULT_BACKGROUND);
|
||||
return $this->urlGenerator->linkTo(Application::APP_ID, 'img/background/' . BackgroundService::DEFAULT_BACKGROUND);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ namespace OCA\Theming\Service;
|
|||
use InvalidArgumentException;
|
||||
use OC\User\NoUserException;
|
||||
use OCA\Theming\AppInfo\Application;
|
||||
use OCP\Files\AppData\IAppDataFactory;
|
||||
use OCA\Theming\ThemingDefaults;
|
||||
use OCP\Files\File;
|
||||
use OCP\Files\IAppData;
|
||||
use OCP\Files\IRootFolder;
|
||||
|
@ -140,13 +140,13 @@ class BackgroundService {
|
|||
private IAppData $appData;
|
||||
private IConfig $config;
|
||||
private string $userId;
|
||||
private IAppDataFactory $appDataFactory;
|
||||
private ThemingDefaults $themingDefaults;
|
||||
|
||||
public function __construct(IRootFolder $rootFolder,
|
||||
IAppData $appData,
|
||||
IConfig $config,
|
||||
?string $userId,
|
||||
IAppDataFactory $appDataFactory) {
|
||||
ThemingDefaults $themingDefaults) {
|
||||
if ($userId === null) {
|
||||
return;
|
||||
}
|
||||
|
@ -155,11 +155,12 @@ class BackgroundService {
|
|||
$this->config = $config;
|
||||
$this->userId = $userId;
|
||||
$this->appData = $appData;
|
||||
$this->appDataFactory = $appDataFactory;
|
||||
$this->themingDefaults = $themingDefaults;
|
||||
}
|
||||
|
||||
public function setDefaultBackground(): void {
|
||||
$this->config->deleteUserValue($this->userId, Application::APP_ID, 'background_image');
|
||||
$this->config->setUserValue($this->userId, Application::APP_ID, 'background_color', $this->themingDefaults->getDefaultColorPrimary());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -171,7 +172,7 @@ class BackgroundService {
|
|||
* @throws NoUserException
|
||||
*/
|
||||
public function setFileBackground($path): void {
|
||||
$this->config->setUserValue($this->userId, Application::APP_ID, 'background_image', self::BACKGROUND_DEFAULT);
|
||||
$this->config->setUserValue($this->userId, Application::APP_ID, 'background_image', self::BACKGROUND_CUSTOM);
|
||||
$userFolder = $this->rootFolder->getUserFolder($this->userId);
|
||||
|
||||
/** @var File $file */
|
||||
|
|
|
@ -97,7 +97,7 @@ trait CommonThemeTrait {
|
|||
if ($backgroundDeleted) {
|
||||
$variables['--color-background-plain'] = $this->themingDefaults->getColorPrimary();
|
||||
if ($this->themingDefaults->isUserThemingDisabled() || $user === null) {
|
||||
$variables['--image-background-plain'] = 'true';
|
||||
$variables['--image-background-plain'] = 'yes';
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -108,13 +108,12 @@ trait CommonThemeTrait {
|
|||
if ($image === 'background') {
|
||||
// If background deleted is set, ignoring variable
|
||||
if ($backgroundDeleted) {
|
||||
$variables['--image-background-default'] = 'no';
|
||||
continue;
|
||||
}
|
||||
$variables['--image-background-size'] = 'cover';
|
||||
$variables['--image-background-default'] = "url('" . $imageUrl . "')";
|
||||
}
|
||||
// --image-background is overriden by user theming
|
||||
// --image-background is overridden by user theming
|
||||
$variables["--image-$image"] = "url('" . $imageUrl . "')";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -247,7 +247,7 @@ class ThemingDefaults extends \OC_Defaults {
|
|||
* Return the default color primary
|
||||
*/
|
||||
public function getDefaultColorPrimary(): string {
|
||||
$color = $this->config->getAppValue(Application::APP_ID, 'color');
|
||||
$color = $this->config->getAppValue(Application::APP_ID, 'color', '');
|
||||
if (!preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $color)) {
|
||||
$color = '#0082c9';
|
||||
}
|
||||
|
|
|
@ -285,8 +285,15 @@ export default {
|
|||
background-position: center;
|
||||
text-align: center;
|
||||
margin-top: 10px;
|
||||
background-color: var(--color-primary-default);
|
||||
background-image: var(--image-background-default, var(--image-background-plain, url('../../../core/img/app-background.jpg'), linear-gradient(40deg, #0082c9 0%, #30b6ff 100%)));
|
||||
/* This is basically https://github.com/nextcloud/server/blob/master/core/css/guest.css
|
||||
But without the user variables. That way the admin can preview the render as guest*/
|
||||
/* As guest, there is no user color color-background-plain */
|
||||
background-color: var(--color-primary-default, #0082c9);
|
||||
/* As guest, there is no user background (--image-background)
|
||||
1. Empty background if defined
|
||||
2. Else default background
|
||||
3. Finally default gradient (should not happened, the background is always defined anyway) */
|
||||
background-image: var(--image-background-plain, var(--image-background-default, linear-gradient(40deg, #0082c9 0%, #30b6ff 100%)));
|
||||
|
||||
&-logo {
|
||||
width: 20%;
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<!--
|
||||
- @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net>
|
||||
- @copyright Copyright (c) 2022 Greta Doci <gretadoci@gmail.com>
|
||||
-
|
||||
- @author Julius Härtl <jus@bitgrid.net>
|
||||
- @author Greta Doci <gretadoci@gmail.com>
|
||||
- @author Christopher Ng <chrng8@gmail.com>
|
||||
- @author Greta Doci <gretadoci@gmail.com>
|
||||
- @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
- @author Julius Härtl <jus@bitgrid.net>
|
||||
-
|
||||
- @license GNU AGPL version 3 or any later version
|
||||
-
|
||||
|
@ -24,13 +24,16 @@
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div class="background-selector">
|
||||
<div class="background-selector" data-user-theming-background-settings>
|
||||
<!-- Custom background -->
|
||||
<button class="background background__filepicker"
|
||||
:class="{ 'background--active': backgroundImage === 'custom' }"
|
||||
:class="{ 'icon-loading': loading === 'custom', 'background--active': backgroundImage === 'custom' }"
|
||||
:data-color-bright="invertTextColor(Theming.color)"
|
||||
data-user-theming-background-custom
|
||||
tabindex="0"
|
||||
@click="pickFile">
|
||||
{{ t('theming', 'Custom background') }}
|
||||
<Check :size="44" />
|
||||
</button>
|
||||
|
||||
<!-- Default background -->
|
||||
|
@ -38,6 +41,7 @@
|
|||
:class="{ 'icon-loading': loading === 'default', 'background--active': backgroundImage === 'default' }"
|
||||
:data-color-bright="invertTextColor(Theming.defaultColor)"
|
||||
:style="{ '--border-color': Theming.defaultColor }"
|
||||
data-user-theming-background-default
|
||||
tabindex="0"
|
||||
@click="setDefault">
|
||||
{{ t('theming', 'Default background') }}
|
||||
|
@ -50,6 +54,7 @@
|
|||
:data-color="Theming.color"
|
||||
:data-color-bright="invertTextColor(Theming.color)"
|
||||
:style="{ backgroundColor: Theming.color, '--border-color': Theming.color}"
|
||||
data-user-theming-background-color
|
||||
tabindex="0">
|
||||
{{ t('theming', 'Change color') }}
|
||||
</button>
|
||||
|
@ -61,6 +66,7 @@
|
|||
v-tooltip="shippedBackground.details.attribution"
|
||||
:class="{ 'icon-loading': loading === shippedBackground.name, 'background--active': backgroundImage === shippedBackground.name }"
|
||||
:data-color-bright="shippedBackground.details.theming === 'dark'"
|
||||
:data-user-theming-background-shipped="shippedBackground.name"
|
||||
:style="{ backgroundImage: 'url(' + shippedBackground.preview + ')', '--border-color': shippedBackground.details.primary_color }"
|
||||
class="background background__shipped"
|
||||
tabindex="0"
|
||||
|
@ -70,16 +76,17 @@
|
|||
|
||||
<!-- Remove background -->
|
||||
<button class="background background__delete"
|
||||
data-user-theming-background-clear
|
||||
tabindex="0"
|
||||
@click="removeBackground">
|
||||
{{ t('theming', 'Remove background') }}
|
||||
<Close :size="24" />
|
||||
<Close :size="32" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { generateFilePath, generateUrl } from '@nextcloud/router'
|
||||
import { generateFilePath, generateRemoteUrl, generateUrl } from '@nextcloud/router'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import axios from '@nextcloud/axios'
|
||||
import Check from 'vue-material-design-icons/Check.vue'
|
||||
|
@ -87,6 +94,10 @@ import Close from 'vue-material-design-icons/Close.vue'
|
|||
import debounce from 'debounce'
|
||||
import NcColorPicker from '@nextcloud/vue/dist/Components/NcColorPicker'
|
||||
import Tooltip from '@nextcloud/vue/dist/Directives/Tooltip'
|
||||
import Vibrant from 'node-vibrant'
|
||||
import { Palette } from 'node-vibrant/lib/color'
|
||||
import { getFilePickerBuilder } from '@nextcloud/dialogs'
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
|
||||
const backgroundColor = loadState('theming', 'backgroundColor')
|
||||
const backgroundImage = loadState('theming', 'backgroundImage')
|
||||
|
@ -95,6 +106,12 @@ const themingDefaultBackground = loadState('theming', 'themingDefaultBackground'
|
|||
const defaultShippedBackground = loadState('theming', 'defaultShippedBackground')
|
||||
|
||||
const prefixWithBaseUrl = (url) => generateFilePath('theming', '', 'img/background/') + url
|
||||
const picker = getFilePickerBuilder(t('theming', 'Select a background from your files'))
|
||||
.setMultiSelect(false)
|
||||
.setModal(true)
|
||||
.setType(1)
|
||||
.setMimeTypeFilter(['image/png', 'image/gif', 'image/jpeg', 'image/svg+xml', 'image/svg'])
|
||||
.build()
|
||||
|
||||
export default {
|
||||
name: 'BackgroundSettings',
|
||||
|
@ -213,9 +230,9 @@ export default {
|
|||
this.update(result.data)
|
||||
},
|
||||
|
||||
async setFile(path) {
|
||||
async setFile(path, color = null) {
|
||||
this.loading = 'custom'
|
||||
const result = await axios.post(generateUrl('/apps/theming/background/custom'), { value: path })
|
||||
const result = await axios.post(generateUrl('/apps/theming/background/custom'), { value: path, color })
|
||||
this.update(result.data)
|
||||
},
|
||||
|
||||
|
@ -228,19 +245,55 @@ export default {
|
|||
async pickColor(event) {
|
||||
this.loading = 'color'
|
||||
const color = event?.target?.dataset?.color || this.Theming?.color || '#0082c9'
|
||||
const result = await axios.post(generateUrl('/apps/theming/background/color'), { value: color })
|
||||
const result = await axios.post(generateUrl('/apps/theming/background/color'), { color })
|
||||
this.update(result.data)
|
||||
},
|
||||
debouncePickColor: debounce(function() {
|
||||
this.pickColor(...arguments)
|
||||
}, 200),
|
||||
|
||||
pickFile() {
|
||||
window.OC.dialogs.filepicker(t('theming', 'Select a background from your files'), (path, type) => {
|
||||
if (type === OC.dialogs.FILEPICKER_TYPE_CHOOSE) {
|
||||
this.setFile(path)
|
||||
}
|
||||
}, false, ['image/png', 'image/gif', 'image/jpeg', 'image/svg'], true, OC.dialogs.FILEPICKER_TYPE_CHOOSE)
|
||||
async pickFile() {
|
||||
const path = await picker.pick()
|
||||
this.loading = 'custom'
|
||||
|
||||
// Extract primary color from image
|
||||
let response = null
|
||||
let color = null
|
||||
try {
|
||||
const fileUrl = generateRemoteUrl('dav/files/' + getCurrentUser().uid + path)
|
||||
response = await axios.get(fileUrl, { responseType: 'blob' })
|
||||
const blobUrl = URL.createObjectURL(response.data)
|
||||
const palette = await this.getColorPaletteFromBlob(blobUrl)
|
||||
|
||||
// DarkVibrant is accessible AND visually pleasing
|
||||
// Vibrant is not accessible enough and others are boring
|
||||
color = palette?.DarkVibrant?.hex
|
||||
this.setFile(path, color)
|
||||
|
||||
// Log data
|
||||
console.debug('Extracted colour', color, 'from custom image', path, palette)
|
||||
} catch (error) {
|
||||
this.setFile(path)
|
||||
console.error('Unable to extract colour from custom image', { error, path, response, color })
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Extract a Vibrant color palette from a blob URL
|
||||
*
|
||||
* @param {string} blobUrl the blob URL
|
||||
* @return {Promise<Palette>}
|
||||
*/
|
||||
getColorPaletteFromBlob(blobUrl) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const vibrant = new Vibrant(blobUrl)
|
||||
vibrant.getPalette((error, palette) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
}
|
||||
resolve(palette)
|
||||
})
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -263,6 +316,13 @@ export default {
|
|||
background-position: center center;
|
||||
background-size: cover;
|
||||
|
||||
&__filepicker {
|
||||
&.background--active {
|
||||
color: white;
|
||||
background-image: var(--image-background);
|
||||
}
|
||||
}
|
||||
|
||||
&__default {
|
||||
background-color: var(--color-primary-default);
|
||||
background-image: var(--image-background-default);
|
||||
|
@ -277,6 +337,12 @@ export default {
|
|||
background-color: var(--color-primary-default);
|
||||
}
|
||||
|
||||
// Over a background image
|
||||
&__default,
|
||||
&__shipped {
|
||||
color: white;
|
||||
}
|
||||
|
||||
// Text and svg icon dark on bright background
|
||||
&[data-color-bright] {
|
||||
color: black;
|
||||
|
@ -294,18 +360,14 @@ export default {
|
|||
margin: 4px;
|
||||
}
|
||||
|
||||
&__default,
|
||||
&__shipped {
|
||||
color: white;
|
||||
span {
|
||||
display: none;
|
||||
}
|
||||
&__filepicker span,
|
||||
&__default span,
|
||||
&__shipped span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&--active:not(.icon-loading) {
|
||||
span {
|
||||
display: block;
|
||||
}
|
||||
&--active:not(.icon-loading) span {
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -680,7 +680,7 @@ class ThemingControllerTest extends TestCase {
|
|||
|
||||
public function testGetLoginBackground() {
|
||||
$file = $this->createMock(ISimpleFile::class);
|
||||
$file->method('getName')->willReturn('app-background.jpg');
|
||||
$file->method('getName')->willReturn('background.png');
|
||||
$file->method('getMTime')->willReturn(42);
|
||||
$this->imageManager->expects($this->once())
|
||||
->method('getImage')
|
||||
|
|
|
@ -22,8 +22,10 @@
|
|||
*/
|
||||
namespace OCA\Theming\Tests\Service;
|
||||
|
||||
use OCA\Theming\AppInfo\Application;
|
||||
use OCA\Theming\ImageManager;
|
||||
use OCA\Theming\ITheme;
|
||||
use OCA\Theming\Service\BackgroundService;
|
||||
use OCA\Theming\Themes\DefaultTheme;
|
||||
use OCA\Theming\ThemingDefaults;
|
||||
use OCA\Theming\Util;
|
||||
|
@ -80,6 +82,11 @@ class DefaultThemeTest extends TestCase {
|
|||
->method('getDefaultColorPrimary')
|
||||
->willReturn('#0082c9');
|
||||
|
||||
$this->themingDefaults
|
||||
->expects($this->any())
|
||||
->method('getBackground')
|
||||
->willReturn('/apps/' . Application::APP_ID . '/img/background/' . BackgroundService::DEFAULT_BACKGROUND);
|
||||
|
||||
$this->l10n
|
||||
->expects($this->any())
|
||||
->method('t')
|
||||
|
|
|
@ -473,6 +473,7 @@ class ThemingDefaultsTest extends TestCase {
|
|||
public function testGetColorPrimaryWithCustomBackground() {
|
||||
$backgroundIndex = 2;
|
||||
$background = array_values(BackgroundService::SHIPPED_BACKGROUNDS)[$backgroundIndex];
|
||||
|
||||
$user = $this->createMock(IUser::class);
|
||||
$this->userSession->expects($this->any())
|
||||
->method('getUser')
|
||||
|
@ -484,14 +485,15 @@ class ThemingDefaultsTest extends TestCase {
|
|||
$this->config
|
||||
->expects($this->once())
|
||||
->method('getUserValue')
|
||||
->with('user', 'theming', 'background_image', '')
|
||||
->willReturn(array_keys(BackgroundService::SHIPPED_BACKGROUNDS)[$backgroundIndex]);
|
||||
->with('user', 'theming', 'background_color', '')
|
||||
->willReturn($background['primary_color']);
|
||||
|
||||
$this->config
|
||||
->expects($this->exactly(2))
|
||||
->method('getAppValue')
|
||||
->willReturnMap([
|
||||
['theming', 'disable-user-theming', 'no', 'no'],
|
||||
['theming', 'color', '', ''],
|
||||
['theming', 'disable-user-theming', 'no', 'no'],
|
||||
]);
|
||||
|
||||
$this->assertEquals($background['primary_color'], $this->template->getColorPrimary());
|
||||
|
@ -509,14 +511,14 @@ class ThemingDefaultsTest extends TestCase {
|
|||
$this->config
|
||||
->expects($this->once())
|
||||
->method('getUserValue')
|
||||
->with('user', 'theming', 'background_image', '')
|
||||
->with('user', 'theming', 'background_color', '')
|
||||
->willReturn('#fff');
|
||||
$this->config
|
||||
->expects($this->exactly(2))
|
||||
->method('getAppValue')
|
||||
->willReturnMap([
|
||||
['theming', 'disable-user-theming', 'no', 'no'],
|
||||
['theming', 'color', '', ''],
|
||||
['theming', 'disable-user-theming', 'no', 'no'],
|
||||
]);
|
||||
|
||||
$this->assertEquals('#fff', $this->template->getColorPrimary());
|
||||
|
@ -534,14 +536,14 @@ class ThemingDefaultsTest extends TestCase {
|
|||
$this->config
|
||||
->expects($this->once())
|
||||
->method('getUserValue')
|
||||
->with('user', 'theming', 'background_image', '')
|
||||
->with('user', 'theming', 'background_color', '')
|
||||
->willReturn('nextcloud');
|
||||
$this->config
|
||||
->expects($this->exactly(3))
|
||||
->method('getAppValue')
|
||||
->willReturnMap([
|
||||
['theming', 'disable-user-theming', 'no', 'no'],
|
||||
['theming', 'color', '', ''],
|
||||
['theming', 'disable-user-theming', 'no', 'no'],
|
||||
]);
|
||||
|
||||
$this->assertEquals($this->template->getDefaultColorPrimary(), $this->template->getColorPrimary());
|
||||
|
@ -650,16 +652,14 @@ class ThemingDefaultsTest extends TestCase {
|
|||
->method('deleteAppValue')
|
||||
->with('theming', 'color');
|
||||
$this->config
|
||||
->expects($this->exactly(3))
|
||||
->expects($this->exactly(2))
|
||||
->method('getAppValue')
|
||||
->withConsecutive(
|
||||
['theming', 'cachebuster', '0'],
|
||||
['theming', 'color', null],
|
||||
['theming', 'disable-user-theming', 'no'],
|
||||
)->willReturnOnConsecutiveCalls(
|
||||
'15',
|
||||
$this->defaults->getColorPrimary(),
|
||||
'no',
|
||||
);
|
||||
$this->config
|
||||
->expects($this->once())
|
||||
|
@ -778,10 +778,10 @@ class ThemingDefaultsTest extends TestCase {
|
|||
$this->imageManager->expects($this->exactly(4))
|
||||
->method('getImageUrl')
|
||||
->willReturnMap([
|
||||
['logo', true, 'custom-logo?v=0'],
|
||||
['logoheader', true, 'custom-logoheader?v=0'],
|
||||
['favicon', true, 'custom-favicon?v=0'],
|
||||
['background_image', true, 'custom-background?v=0'],
|
||||
['logo', 'custom-logo?v=0'],
|
||||
['logoheader', 'custom-logoheader?v=0'],
|
||||
['favicon', 'custom-favicon?v=0'],
|
||||
['background', 'custom-background?v=0'],
|
||||
]);
|
||||
|
||||
$expected = [
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -39,15 +39,15 @@ html {
|
|||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
// color-background-plain should always be defined. It is the primary user colour
|
||||
background-color: var(--color-background-plain, var(--color-main-background));
|
||||
background-image: var(--image-background);
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
body {
|
||||
// color-background-plain should always be defined. It is the primary user colour
|
||||
background-color: var(--color-background-plain, var(--color-main-background));
|
||||
background-image: var(--image-background-plain, var(--image-background, var(--image-background-default)));
|
||||
// color-background-plain should always be defined. It is the primary user colour
|
||||
background-image: var(--image-background, var(--image-background-default));
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
position: fixed;
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,85 @@
|
|||
/* eslint-disable node/no-unpublished-import */
|
||||
import { applyChangesToNextcloud, configureNextcloud, preppingNextcloud, startNextcloud, stopNextcloud, waitOnNextcloud } from './cypress/dockerNode'
|
||||
import { defineConfig } from 'cypress'
|
||||
|
||||
import browserify from '@cypress/browserify-preprocessor'
|
||||
|
||||
export default defineConfig({
|
||||
projectId: '37xpdh',
|
||||
|
||||
// 16/9 screen ratio
|
||||
viewportWidth: 1280,
|
||||
viewportHeight: 720,
|
||||
|
||||
// Tries again 2 more times on failure
|
||||
retries: {
|
||||
runMode: 2,
|
||||
// do not retry in `cypress open`
|
||||
openMode: 0,
|
||||
},
|
||||
|
||||
// Needed to trigger `after:run` events with cypress open
|
||||
experimentalInteractiveRunEvents: true,
|
||||
|
||||
// faster video processing
|
||||
videoCompression: false,
|
||||
|
||||
// Visual regression testing
|
||||
env: {
|
||||
failSilently: false,
|
||||
type: 'actual',
|
||||
},
|
||||
screenshotsFolder: 'cypress/snapshots/actual',
|
||||
trashAssetsBeforeRuns: true,
|
||||
|
||||
e2e: {
|
||||
// Enable session management and disable isolation
|
||||
experimentalSessionAndOrigin: true,
|
||||
testIsolation: 'off',
|
||||
|
||||
// We've imported your old cypress plugins here.
|
||||
// You may want to clean this up later by importing these.
|
||||
async setupNodeEvents(on, config) {
|
||||
// Fix browserslist extend https://github.com/cypress-io/cypress/issues/2983#issuecomment-570616682
|
||||
on('file:preprocessor', browserify({ typescript: require.resolve('typescript') }))
|
||||
|
||||
// Disable spell checking to prevent rendering differences
|
||||
on('before:browser:launch', (browser, launchOptions) => {
|
||||
if (browser.family === 'chromium' && browser.name !== 'electron') {
|
||||
launchOptions.preferences.default['browser.enable_spellchecking'] = false
|
||||
return launchOptions
|
||||
}
|
||||
|
||||
if (browser.family === 'firefox') {
|
||||
launchOptions.preferences['layout.spellcheckDefault'] = 0
|
||||
return launchOptions
|
||||
}
|
||||
|
||||
if (browser.name === 'electron') {
|
||||
launchOptions.preferences.spellcheck = false
|
||||
return launchOptions
|
||||
}
|
||||
})
|
||||
|
||||
// Remove container after run
|
||||
on('after:run', () => {
|
||||
stopNextcloud()
|
||||
})
|
||||
|
||||
// Before the browser launches
|
||||
// starting Nextcloud testing container
|
||||
return startNextcloud(process.env.BRANCH)
|
||||
.then((ip) => {
|
||||
// Setting container's IP as base Url
|
||||
config.baseUrl = `http://${ip}/index.php`
|
||||
return ip
|
||||
})
|
||||
.then(waitOnNextcloud)
|
||||
.then(configureNextcloud)
|
||||
.then(applyChangesToNextcloud)
|
||||
.then(() => {
|
||||
return config
|
||||
})
|
||||
},
|
||||
},
|
||||
})
|
|
@ -0,0 +1,243 @@
|
|||
/**
|
||||
* @copyright Copyright (c) 2022 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
/* eslint-disable no-console */
|
||||
/* eslint-disable node/no-unpublished-import */
|
||||
|
||||
import Docker from 'dockerode'
|
||||
import waitOn from 'wait-on'
|
||||
import tar from 'tar'
|
||||
|
||||
export const docker = new Docker()
|
||||
|
||||
const CONTAINER_NAME = 'nextcloud-cypress-tests-server'
|
||||
const SERVER_IMAGE = 'ghcr.io/nextcloud/continuous-integration-shallow-server'
|
||||
|
||||
/**
|
||||
* Start the testing container
|
||||
*
|
||||
* @param {string} branch the branch of your current work
|
||||
*/
|
||||
export const startNextcloud = async function(branch: string = 'master'): Promise<any> {
|
||||
|
||||
try {
|
||||
// Pulling images
|
||||
console.log('\nPulling images... ⏳')
|
||||
await new Promise((resolve, reject): any => docker.pull(SERVER_IMAGE, (err, stream) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
}
|
||||
// https://github.com/apocas/dockerode/issues/357
|
||||
docker.modem.followProgress(stream, onFinished)
|
||||
|
||||
function onFinished(err) {
|
||||
if (!err) {
|
||||
resolve(true)
|
||||
return
|
||||
}
|
||||
reject(err)
|
||||
}
|
||||
}))
|
||||
console.log('└─ Done')
|
||||
|
||||
// Remove old container if exists
|
||||
console.log('\nChecking running containers... 🔍')
|
||||
try {
|
||||
const oldContainer = docker.getContainer(CONTAINER_NAME)
|
||||
const oldContainerData = await oldContainer.inspect()
|
||||
if (oldContainerData) {
|
||||
console.log('├─ Existing running container found')
|
||||
console.log('├─ Removing... ⏳')
|
||||
// Forcing any remnants to be removed just in case
|
||||
await oldContainer.remove({ force: true })
|
||||
console.log('└─ Done')
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('└─ None found!')
|
||||
}
|
||||
|
||||
// Starting container
|
||||
console.log('\nStarting Nextcloud container... 🚀')
|
||||
console.log(`├─ Using branch '${branch}'`)
|
||||
const container = await docker.createContainer({
|
||||
Image: SERVER_IMAGE,
|
||||
name: CONTAINER_NAME,
|
||||
HostConfig: {
|
||||
Binds: [],
|
||||
},
|
||||
})
|
||||
await container.start()
|
||||
|
||||
// Get container's IP
|
||||
const ip = await getContainerIP(container)
|
||||
|
||||
console.log(`├─ Nextcloud container's IP is ${ip} 🌏`)
|
||||
return ip
|
||||
} catch (err) {
|
||||
console.log('└─ Unable to start the container 🛑')
|
||||
console.log(err)
|
||||
stopNextcloud()
|
||||
throw new Error('Unable to start the container')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure Nextcloud
|
||||
*/
|
||||
export const configureNextcloud = async function() {
|
||||
console.log('\nConfiguring nextcloud...')
|
||||
const container = docker.getContainer(CONTAINER_NAME)
|
||||
await runExec(container, ['php', 'occ', '--version'], true)
|
||||
|
||||
// Be consistent for screenshots
|
||||
await runExec(container, ['php', 'occ', 'config:system:set', 'default_language', '--value', 'en'], true)
|
||||
await runExec(container, ['php', 'occ', 'config:system:set', 'force_language', '--value', 'en'], true)
|
||||
await runExec(container, ['php', 'occ', 'config:system:set', 'default_locale', '--value', 'en_US'], true)
|
||||
await runExec(container, ['php', 'occ', 'config:system:set', 'force_locale', '--value', 'en_US'], true)
|
||||
await runExec(container, ['php', 'occ', 'config:system:set', 'enforce_theme', '--value', 'light'], true)
|
||||
|
||||
// Enable the app and give status
|
||||
await runExec(container, ['php', 'occ', 'app:enable', '--force', 'viewer'], true)
|
||||
// await runExec(container, ['php', 'occ', 'app:list'], true)
|
||||
|
||||
console.log('└─ Nextcloud is now ready to use 🎉')
|
||||
}
|
||||
|
||||
/**
|
||||
* Applying local changes to the container
|
||||
* Only triggered if we're not in CI. Otherwise the
|
||||
* continuous-integration-shallow-server image will
|
||||
* already fetch the proper branch.
|
||||
*/
|
||||
export const applyChangesToNextcloud = async function() {
|
||||
console.log('\nApply local changes to nextcloud...')
|
||||
const container = docker.getContainer(CONTAINER_NAME)
|
||||
|
||||
const htmlPath = '/var/www/html'
|
||||
const folderPaths = [
|
||||
'./apps',
|
||||
'./core',
|
||||
'./dist',
|
||||
'./lib',
|
||||
'./ocs',
|
||||
]
|
||||
|
||||
// Tar-streaming the above folder sinto the container
|
||||
const serverTar = tar.c({ gzip: false }, folderPaths)
|
||||
await container.putArchive(serverTar, {
|
||||
path: htmlPath,
|
||||
})
|
||||
|
||||
// Making sure we have the proper permissions
|
||||
await runExec(container, ['chown', '-R', 'www-data:www-data', htmlPath], false, 'root')
|
||||
|
||||
console.log('└─ Changes applied successfully 🎉')
|
||||
}
|
||||
|
||||
/**
|
||||
* Force stop the testing container
|
||||
*/
|
||||
export const stopNextcloud = async function() {
|
||||
try {
|
||||
const container = docker.getContainer(CONTAINER_NAME)
|
||||
console.log('Stopping Nextcloud container...')
|
||||
container.remove({ force: true })
|
||||
console.log('└─ Nextcloud container removed 🥀')
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the testing container's IP
|
||||
*
|
||||
* @param {Docker.Container} container the container to get the IP from
|
||||
*/
|
||||
export const getContainerIP = async function(
|
||||
container = docker.getContainer(CONTAINER_NAME)
|
||||
): Promise<string> {
|
||||
let ip = ''
|
||||
let tries = 0
|
||||
while (ip === '' && tries < 10) {
|
||||
tries++
|
||||
|
||||
await container.inspect(function(err, data) {
|
||||
if (err) {
|
||||
throw err
|
||||
}
|
||||
ip = data?.NetworkSettings?.IPAddress || ''
|
||||
})
|
||||
|
||||
if (ip !== '') {
|
||||
break
|
||||
}
|
||||
|
||||
await sleep(1000 * tries)
|
||||
}
|
||||
|
||||
return ip
|
||||
}
|
||||
|
||||
// Would be simpler to start the container from cypress.config.ts,
|
||||
// but when checking out different branches, it can take a few seconds
|
||||
// Until we can properly configure the baseUrl retry intervals,
|
||||
// We need to make sure the server is already running before cypress
|
||||
// https://github.com/cypress-io/cypress/issues/22676
|
||||
export const waitOnNextcloud = async function(ip: string) {
|
||||
console.log('├─ Waiting for Nextcloud to be ready... ⏳')
|
||||
await waitOn({ resources: [`http://${ip}/index.php`] })
|
||||
console.log('└─ Done')
|
||||
}
|
||||
|
||||
const runExec = async function(
|
||||
container: Docker.Container,
|
||||
command: string[],
|
||||
verbose = false,
|
||||
user = 'www-data'
|
||||
) {
|
||||
const exec = await container.exec({
|
||||
Cmd: command,
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
User: user,
|
||||
})
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
exec.start({}, (err, stream) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
}
|
||||
if (stream) {
|
||||
stream.setEncoding('utf-8')
|
||||
stream.on('data', str => {
|
||||
if (verbose && str.trim() !== '') {
|
||||
console.log(`├─ ${str.trim().replace(/\n/gi, '\n├─ ')}`)
|
||||
}
|
||||
})
|
||||
stream.on('end', resolve)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const sleep = function(milliseconds: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, milliseconds))
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
/**
|
||||
* @copyright Copyright (c) 2022 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
describe('Login with a new user and open the files app', function() {
|
||||
before(function() {
|
||||
cy.createRandomUser().then((user) => {
|
||||
cy.login(user)
|
||||
})
|
||||
})
|
||||
|
||||
after(function() {
|
||||
cy.logout()
|
||||
})
|
||||
|
||||
it('See the default file welcome.txt in the files list', function() {
|
||||
cy.visit('/apps/files')
|
||||
cy.get('.files-fileList tr').should('contain', 'welcome.txt')
|
||||
})
|
||||
})
|
|
@ -0,0 +1,164 @@
|
|||
/**
|
||||
* @copyright Copyright (c) 2022 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
import type { User } from '@nextcloud/cypress'
|
||||
|
||||
const defaultPrimary = '#006aa3'
|
||||
const defaultBackground = 'kamil-porembinski-clouds.jpg'
|
||||
|
||||
const validateThemingCss = function(expectedPrimary = '#0082c9', expectedBackground = 'kamil-porembinski-clouds.jpg', bright = false) {
|
||||
return cy.window().then((win) => {
|
||||
const primary = getComputedStyle(win.document.body).getPropertyValue('--color-primary')
|
||||
const background = getComputedStyle(win.document.body).getPropertyValue('--image-background')
|
||||
const invertIfBright = getComputedStyle(win.document.body).getPropertyValue('--background-image-invert-if-bright')
|
||||
|
||||
// Returning boolean for cy.waitUntil usage
|
||||
return primary === expectedPrimary
|
||||
&& background.includes(expectedBackground)
|
||||
&& invertIfBright === (bright ? 'invert(100%)' : 'no')
|
||||
})
|
||||
}
|
||||
|
||||
describe('User default background settings', function() {
|
||||
before(function() {
|
||||
cy.createRandomUser().then((user: User) => {
|
||||
cy.login(user)
|
||||
})
|
||||
})
|
||||
|
||||
it('See the user background settings', function() {
|
||||
cy.visit('/settings/user/theming')
|
||||
cy.get('[data-user-theming-background-settings]').scrollIntoView().should('be.visible')
|
||||
})
|
||||
|
||||
// Default cloud background is not rendered if admin theming background remains unchanged
|
||||
it('Default cloud background is not rendered', function() {
|
||||
cy.get(`[data-user-theming-background-shipped="${defaultBackground}"]`).should('not.exist')
|
||||
})
|
||||
|
||||
it('Default is selected on new users', function() {
|
||||
cy.get('[data-user-theming-background-default]').should('be.visible')
|
||||
cy.get('[data-user-theming-background-default]').should('have.class', 'background--active')
|
||||
})
|
||||
})
|
||||
|
||||
describe('User select shipped backgrounds', function() {
|
||||
before(function() {
|
||||
cy.createRandomUser().then((user: User) => {
|
||||
cy.login(user)
|
||||
})
|
||||
})
|
||||
|
||||
it('See the user background settings', function() {
|
||||
cy.visit('/settings/user/theming')
|
||||
cy.get('[data-user-theming-background-settings]').scrollIntoView().should('be.visible')
|
||||
})
|
||||
|
||||
it('Select a shipped background', function() {
|
||||
const background = 'anatoly-mikhaltsov-butterfly-wing-scale.jpg'
|
||||
cy.intercept('*/apps/theming/background/shipped').as('setBackground')
|
||||
|
||||
// Select background
|
||||
cy.get(`[data-user-theming-background-shipped="${background}"]`).click()
|
||||
|
||||
// Validate changed background and primary
|
||||
cy.wait('@setBackground')
|
||||
cy.waitUntil(() => validateThemingCss('#a53c17', background))
|
||||
})
|
||||
|
||||
it('Select a bright shipped background', function() {
|
||||
const background = 'bernie-cetonia-aurata-take-off-composition.jpg'
|
||||
cy.intercept('*/apps/theming/background/shipped').as('setBackground')
|
||||
|
||||
// Select background
|
||||
cy.get(`[data-user-theming-background-shipped="${background}"]`).click()
|
||||
|
||||
// Validate changed background and primary
|
||||
cy.wait('@setBackground')
|
||||
cy.waitUntil(() => validateThemingCss('#56633d', background, true))
|
||||
})
|
||||
|
||||
it('Remove background', function() {
|
||||
cy.intercept('*/apps/theming/background/custom').as('clearBackground')
|
||||
|
||||
// Clear background
|
||||
cy.get('[data-user-theming-background-clear]').click()
|
||||
|
||||
// Validate clear background
|
||||
cy.wait('@clearBackground')
|
||||
cy.waitUntil(() => validateThemingCss('#56633d', ''))
|
||||
})
|
||||
})
|
||||
|
||||
describe('User select a custom color', function() {
|
||||
before(function() {
|
||||
cy.createRandomUser().then((user: User) => {
|
||||
cy.login(user)
|
||||
})
|
||||
})
|
||||
|
||||
it('See the user background settings', function() {
|
||||
cy.visit('/settings/user/theming')
|
||||
cy.get('[data-user-theming-background-settings]').scrollIntoView().should('be.visible')
|
||||
})
|
||||
|
||||
it('Select a custom color', function() {
|
||||
cy.intercept('*/apps/theming/background/color').as('setColor')
|
||||
|
||||
cy.get('[data-user-theming-background-color]').click()
|
||||
cy.get('.color-picker__simple-color-circle:eq(3)').click()
|
||||
|
||||
// Validate clear background
|
||||
cy.wait('@setColor')
|
||||
cy.waitUntil(() => cy.window().then((win) => {
|
||||
const primary = getComputedStyle(win.document.body).getPropertyValue('--color-primary')
|
||||
return primary !== defaultPrimary
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
describe('User select a custom background', function() {
|
||||
const image = 'image.jpg'
|
||||
before(function() {
|
||||
cy.createRandomUser().then((user: User) => {
|
||||
cy.uploadFile(user, image, 'image/jpeg')
|
||||
cy.login(user)
|
||||
})
|
||||
})
|
||||
|
||||
it('See the user background settings', function() {
|
||||
cy.visit('/settings/user/theming')
|
||||
cy.get('[data-user-theming-background-settings]').scrollIntoView().should('be.visible')
|
||||
})
|
||||
|
||||
it('Select a custom background', function() {
|
||||
cy.intercept('*/apps/theming/background/custom').as('setBackground')
|
||||
|
||||
// Pick background
|
||||
cy.get('[data-user-theming-background-custom]').click()
|
||||
cy.get(`#picker-filestable tr[data-entryname="${image}"]`).click()
|
||||
cy.get('#oc-dialog-filepicker-content ~ .oc-dialog-buttonrow button.primary').click()
|
||||
|
||||
// Wait for background to be set
|
||||
cy.wait('@setBackground')
|
||||
cy.waitUntil(() => validateThemingCss('#4c0c04', 'apps/theming/background?v='))
|
||||
})
|
||||
})
|
Binary file not shown.
After Width: | Height: | Size: 1.5 MiB |
|
@ -0,0 +1,86 @@
|
|||
/**
|
||||
* @copyright Copyright (c) 2022 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
/* eslint-disable node/no-unpublished-import */
|
||||
import axios from '@nextcloud/axios'
|
||||
import { addCommands, type User} from '@nextcloud/cypress'
|
||||
import { basename } from 'path'
|
||||
|
||||
// Add custom commands
|
||||
import 'cypress-wait-until'
|
||||
addCommands()
|
||||
|
||||
// Register this file's custom commands types
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface Chainable<Subject = any> {
|
||||
uploadFile(user: User, fixture: string, mimeType: string, target ?: string): Cypress.Chainable<void>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const url = (Cypress.config('baseUrl') || '').replace(/\/index.php\/?$/g, '')
|
||||
Cypress.env('baseUrl', url)
|
||||
|
||||
/**
|
||||
* cy.uploadedFile - uploads a file from the fixtures folder
|
||||
* TODO: standardise in @nextcloud/cypress
|
||||
*
|
||||
* @param {User} user the owner of the file, e.g. admin
|
||||
* @param {string} fixture the fixture file name, e.g. image1.jpg
|
||||
* @param {string} mimeType e.g. image/png
|
||||
* @param {string} [target] the target of the file relative to the user root
|
||||
*/
|
||||
Cypress.Commands.add('uploadFile', (user, fixture, mimeType, target = `/${fixture}`) => {
|
||||
cy.clearCookies()
|
||||
const fileName = basename(target)
|
||||
|
||||
// get fixture
|
||||
return cy.fixture(fixture, 'base64').then(async file => {
|
||||
// convert the base64 string to a blob
|
||||
const blob = Cypress.Blob.base64StringToBlob(file, mimeType)
|
||||
|
||||
// Process paths
|
||||
const rootPath = `${Cypress.env('baseUrl')}/remote.php/dav/files/${encodeURIComponent(user.userId)}`
|
||||
const filePath = target.split('/').map(encodeURIComponent).join('/')
|
||||
try {
|
||||
const file = new File([blob], fileName, { type: mimeType })
|
||||
await axios({
|
||||
url: `${rootPath}${filePath}`,
|
||||
method: 'PUT',
|
||||
data: file,
|
||||
headers: {
|
||||
'Content-Type': mimeType,
|
||||
},
|
||||
auth: {
|
||||
username: user.userId,
|
||||
password: user.password,
|
||||
},
|
||||
}).then(response => {
|
||||
cy.log(`Uploaded ${fixture} as ${fileName}`, response)
|
||||
})
|
||||
} catch (error) {
|
||||
cy.log('error', error)
|
||||
throw new Error(`Unable to process fixture ${fixture}`)
|
||||
}
|
||||
})
|
||||
})
|
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
* @copyright Copyright (c) 2022 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
import './commands'
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"include": ["./**/*.ts"],
|
||||
"compilerOptions": {
|
||||
"types": ["cypress", "dockerode", "cypress-wait-until"],
|
||||
}
|
||||
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
20
package.json
20
package.json
|
@ -18,7 +18,10 @@
|
|||
"test:jsunit": "karma start tests/karma.config.js --single-run",
|
||||
"sass": "sass --load-path core/css core/css/ apps/*/css",
|
||||
"sass:watch": "sass --watch --load-path core/css core/css/ apps/*/css",
|
||||
"sass:icons": "babel-node core/src/icons.js"
|
||||
"sass:icons": "babel-node core/src/icons.js",
|
||||
"cypress": "npm run cypress:e2e",
|
||||
"cypress:e2e": "cypress run --e2e",
|
||||
"cypress:gui": "cypress open --e2e"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -78,6 +81,7 @@
|
|||
"moment": "^2.29.4",
|
||||
"moment-timezone": "^0.5.38",
|
||||
"nextcloud-vue-collections": "^0.10.0",
|
||||
"node-vibrant": "^3.1.6",
|
||||
"p-limit": "^4.0.0",
|
||||
"p-queue": "^7.3.0",
|
||||
"path": "^0.12.7",
|
||||
|
@ -107,18 +111,28 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@babel/node": "^7.20.0",
|
||||
"@cypress/browserify-preprocessor": "^3.0.2",
|
||||
"@nextcloud/babel-config": "^1.0.0",
|
||||
"@nextcloud/cypress": "^1.0.0-beta.1",
|
||||
"@nextcloud/eslint-config": "^8.0.0",
|
||||
"@nextcloud/stylelint-config": "^2.1.2",
|
||||
"@testing-library/jest-dom": "^5.16.4",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@testing-library/vue": "^5.8.3",
|
||||
"@types/dockerode": "^3.3.14",
|
||||
"@typescript-eslint/eslint-plugin": "^5.44.0",
|
||||
"@typescript-eslint/parser": "^5.44.0",
|
||||
"@vue/test-utils": "^1.3.0",
|
||||
"@vue/tsconfig": "^0.1.3",
|
||||
"@vue/vue2-jest": "^29.1.1",
|
||||
"babel-jest": "^29.0.3",
|
||||
"babel-loader": "^8.2.5",
|
||||
"babel-loader-exclude-node-modules-except": "^1.2.1",
|
||||
"css-loader": "^6.7.1",
|
||||
"cypress": "^11.2.0",
|
||||
"cypress-wait-until": "^1.7.2",
|
||||
"dockerode": "^3.3.4",
|
||||
"eslint-plugin-cypress": "^2.12.1",
|
||||
"eslint-plugin-es": "^4.1.0",
|
||||
"exports-loader": "^4.0.0",
|
||||
"file-loader": "^6.2.0",
|
||||
|
@ -143,8 +157,12 @@
|
|||
"sass-loader": "^12.6.0",
|
||||
"sinon": "<= 5.0.7",
|
||||
"style-loader": "^3.3.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^4.9.3",
|
||||
"vue-loader": "^15.9.8",
|
||||
"vue-template-compiler": "^2.7.13",
|
||||
"wait-on": "^6.0.1",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-cli": "^4.9.2",
|
||||
"webpack-merge": "^5.8.0"
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.json",
|
||||
"include": ["./**/*.ts"],
|
||||
"compilerOptions": {
|
||||
"types": ["node"],
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"moduleResolution": "node",
|
||||
"target": "ESNext",
|
||||
"module": "esnext",
|
||||
"declaration": true,
|
||||
"strict": true,
|
||||
"noImplicitAny": false,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"ts-node": {
|
||||
// these options are overrides used only by ts-node
|
||||
// same as our --compilerOptions flag and our TS_NODE_COMPILER_OPTIONS environment variable
|
||||
"compilerOptions": {
|
||||
"module": "commonjs"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -166,8 +166,9 @@ module.exports = {
|
|||
extensions: ['*', '.js', '.vue'],
|
||||
symlinks: true,
|
||||
fallback: {
|
||||
stream: require.resolve('stream-browserify'),
|
||||
buffer: require.resolve('buffer'),
|
||||
fs: false,
|
||||
stream: require.resolve('stream-browserify'),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue