Extract colour from custom background

Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>
pull/34696/head
John Molakvoæ 2 years ago
parent cedae7c6d7
commit 064fa10ecf
No known key found for this signature in database
GPG Key ID: 60C25B8C072916CF

@ -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

4
.gitignore vendored

@ -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 = [

@ -90,14 +90,11 @@ html {
height: 100%;
position: absolute;
background-color: var(--color-background-plain, var(--color-main-background));
background-image: var(--image-background);
background-size: cover;
background-position: center;
}
body {
background-color: var(--color-background-plain, var(--color-main-background));
background-image: var(--image-background-plain, var(--image-background, var(--image-background-default)));
background-image: var(--image-background, var(--image-background-default));
background-size: cover;
background-position: center;
position: fixed;

File diff suppressed because one or more lines are too long

@ -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;

@ -23,8 +23,14 @@ body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
color: var(--color-text);
text-align: center;
background-color: var(--color-main-background-not-plain, var(--color-primary));
background-image: var(--image-background, var(--image-background-plain, linear-gradient(40deg, #0082c9 0%, #30b6ff 100%)));
/* As guest, there is no color-background-plain */
background-color: var(--color-background-plain, var(--color-primary-default, #0082c9));
/* As guest, there is no user background (--image-background)
1. User background if logged in ('no' if removed, that way the variable is _defined_)
2. Empty background if enabled ('yes' is used, that way the variable is _defined_)
3. Else default background
4. Finally default gradient (should not happened, the background is always defined anyway) */
background-image: var(--image-background, var(--image-background-plain, var(--image-background-default, linear-gradient(40deg, #0082c9 0%, #30b6ff 100%))));
background-attachment: fixed;
min-height: 100%; /* fix sticky footer */
height: auto;

@ -2672,14 +2672,11 @@ html {
height: 100%;
position: absolute;
background-color: var(--color-background-plain, var(--color-main-background));
background-image: var(--image-background);
background-size: cover;
background-position: center;
}
body {
background-color: var(--color-background-plain, var(--color-main-background));
background-image: var(--image-background-plain, var(--image-background, var(--image-background-default)));
background-image: var(--image-background, var(--image-background-default));
background-size: cover;
background-position: center;
position: fixed;

File diff suppressed because one or more lines are too long

@ -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"],
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

7789
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -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…
Cancel
Save