Merge pull request #22214 from nextcloud/dashboard/design-fixes

Custom dashboard background
pull/13712/head
Morris Jobke 4 years ago committed by GitHub
commit d3a53d6f39
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -28,5 +28,7 @@ return [
'routes' => [
['name' => 'dashboard#index', 'url' => '/', 'verb' => 'GET'],
['name' => 'dashboard#updateLayout', 'url' => '/layout', 'verb' => 'POST'],
['name' => 'dashboard#getBackground', 'url' => '/background', 'verb' => 'GET'],
['name' => 'dashboard#setBackground', 'url' => '/background/{type}', 'verb' => 'POST'],
]
];

@ -0,0 +1,71 @@
// Show Dashboard background image beneath header
#body-user #header {
background-size: cover !important;
background-position: center 50% !important;
background-repeat: no-repeat !important;
background-attachment: fixed !important;
}
#content {
padding-top: 0 !important;
}
// Hide triangle indicators from navigation since they are out of place without the header bar
#appmenu li a.active::before,
#appmenu li:hover a::before,
#appmenu li:hover a.active::before,
#appmenu li a:focus::before {
display: none !important;
}
$has-custom-logo: variable_exists('theming-logo-mime') and $theming-logo-mime != '';
body.dashboard-inverted:not(.dashboard-dark) {
// Do not invert the default logo
@if ($has-custom-logo == false) {
$image-logo: url(icon-color-path('logo', 'logo', #ffffff, 1, true));
#header .logo {
background-image: $image-logo !important;
opacity: 1;
}
}
#app-dashboard > h2 {
color: #fff;
}
#appmenu li span {
color: #fff;
}
#appmenu svg image {
filter: invert(0);
}
#appmenu .icon-more-white,
.header-right > div:not(#settings) > *:first-child {
filter: invert(1) hue-rotate(180deg);
}
}
body.dashboard-dark:not(.dashboard-inverted) {
// invert the default logo
@if ($has-custom-logo == false) {
$image-logo: url(icon-color-path('logo', 'logo', #000000, 1, true));
#header .logo {
background-image: $image-logo !important;
opacity: 1;
}
}
#app-dashboard > h2 {
color: #000;
}
#appmenu li span {
color: #000;
}
#appmenu svg {
filter: invert(1) hue-rotate(180deg) !important;
}
#appmenu .icon-more-white,
.header-right > div:not(#settings) > *:first-child {
filter: invert(1) hue-rotate(180deg) !important;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 523 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 627 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 934 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 454 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -26,10 +26,14 @@ declare(strict_types=1);
namespace OCA\Dashboard\Controller;
use OCA\Dashboard\Service\BackgroundService;
use OCA\Files\Event\LoadSidebar;
use OCA\Viewer\Event\LoadViewer;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\FileDisplayResponse;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\NotFoundResponse;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\Dashboard\IManager;
use OCP\Dashboard\IWidget;
@ -51,6 +55,10 @@ class DashboardController extends Controller {
private $config;
/** @var string */
private $userId;
/**
* @var BackgroundService
*/
private $backgroundService;
public function __construct(
string $appName,
@ -59,6 +67,7 @@ class DashboardController extends Controller {
IEventDispatcher $eventDispatcher,
IManager $dashboardManager,
IConfig $config,
BackgroundService $backgroundService,
$userId
) {
parent::__construct($appName, $request);
@ -67,6 +76,7 @@ class DashboardController extends Controller {
$this->eventDispatcher = $eventDispatcher;
$this->dashboardManager = $dashboardManager;
$this->config = $config;
$this->backgroundService = $backgroundService;
$this->userId = $userId;
}
@ -76,6 +86,7 @@ class DashboardController extends Controller {
* @return TemplateResponse
*/
public function index(): TemplateResponse {
\OCP\Util::addStyle('dashboard', 'dashboard');
$this->eventDispatcher->dispatchTyped(new LoadSidebar());
if (class_exists(LoadViewer::class)) {
$this->eventDispatcher->dispatchTyped(new LoadViewer());
@ -95,6 +106,9 @@ class DashboardController extends Controller {
$this->inititalStateService->provideInitialState('dashboard', 'panels', $widgets);
$this->inititalStateService->provideInitialState('dashboard', 'layout', $userLayout);
$this->inititalStateService->provideInitialState('dashboard', 'firstRun', $this->config->getUserValue($this->userId, 'dashboard', 'firstRun', '1') === '1');
$this->inititalStateService->provideInitialState('dashboard', 'shippedBackgrounds', BackgroundService::SHIPPED_BACKGROUNDS);
$this->inititalStateService->provideInitialState('dashboard', 'background', $this->config->getUserValue($this->userId, 'dashboard', 'background', 'default'));
$this->inititalStateService->provideInitialState('dashboard', 'version', $this->config->getUserValue($this->userId, 'dashboard', 'backgroundVersion', 0));
$this->config->setUserValue($this->userId, 'dashboard', 'firstRun', '0');
return new TemplateResponse('dashboard', 'index');
@ -109,4 +123,54 @@ class DashboardController extends Controller {
$this->config->setUserValue($this->userId, 'dashboard', 'layout', $layout);
return new JSONResponse(['layout' => $layout]);
}
/**
* @NoAdminRequired
*/
public function setBackground(string $type, string $value): JSONResponse {
$currentVersion = (int)$this->config->getUserValue($this->userId, 'dashboard', 'backgroundVersion', 0);
try {
switch ($type) {
case 'shipped':
$this->backgroundService->setShippedBackground($value);
break;
case 'custom':
$this->backgroundService->setFileBackground($value);
break;
case 'color':
$this->backgroundService->setColorBackground($value);
break;
case 'default':
$this->backgroundService->setDefaultBackground();
break;
default:
return new JSONResponse(['error' => 'Invalid type provided'], Http::STATUS_BAD_REQUEST);
}
} catch (\InvalidArgumentException $e) {
return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
} catch (\Throwable $e) {
return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_INTERNAL_SERVER_ERROR);
}
$currentVersion++;
$this->config->setUserValue($this->userId, 'dashboard', 'backgroundVersion', (string)$currentVersion);
return new JSONResponse([
'type' => $type,
'value' => $value,
'version' => $this->config->getUserValue($this->userId, 'dashboard', 'backgroundVersion', $currentVersion)
]);
}
/**
* @NoAdminRequired
* @NoCSRFRequired
*/
public function getBackground() {
$file = $this->backgroundService->getBackground();
if ($file !== null) {
$response = new FileDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => $file->getMimeType()]);
$response->cacheFor(24 * 60 * 60);
return $response;
}
return new NotFoundResponse();
}
}

@ -0,0 +1,63 @@
<?php
/**
* @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
declare(strict_types=1);
namespace OCA\Dashboard\Controller;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\OCSController;
use OCP\IConfig;
use OCP\IRequest;
class LayoutApiController extends OCSController {
/** @var IConfig */
private $config;
/** @var string */
private $userId;
public function __construct(
string $appName,
IRequest $request,
IConfig $config,
$userId
) {
parent::__construct($appName, $request);
$this->config = $config;
$this->userId = $userId;
}
/**
* @NoAdminRequired
*
* @param string $layout
* @return JSONResponse
*/
public function create(string $layout): JSONResponse {
$this->config->setUserValue($this->userId, 'dashboard', 'layout', $layout);
return new JSONResponse(['layout' => $layout]);
}
}

@ -0,0 +1,173 @@
<?php
/**
* @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
declare(strict_types=1);
namespace OCA\Dashboard\Service;
use OCP\Files\IAppData;
use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException;
use OCP\Files\SimpleFS\ISimpleFile;
use OCP\IConfig;
class BackgroundService {
public const THEMING_MODE_DARK = 'dark';
public const SHIPPED_BACKGROUNDS = [
'anatoly-mikhaltsov-butterfly-wing-scale.jpg' => [
'attribution' => 'Butterfly wing scale (Anatoly Mikhaltsov, CC BY-SA)',
'attribution_url' => 'https://commons.wikimedia.org/wiki/File:%D0%A7%D0%B5%D1%88%D1%83%D0%B9%D0%BA%D0%B8_%D0%BA%D1%80%D1%8B%D0%BB%D0%B0_%D0%B1%D0%B0%D0%B1%D0%BE%D1%87%D0%BA%D0%B8.jpg',
],
'bernie-cetonia-aurata-take-off-composition.jpg' => [
'attribution' => 'Cetonia aurata take off composition (Bernie, Public Domain)',
'attribution_url' => 'https://commons.wikimedia.org/wiki/File:Cetonia_aurata_take_off_composition_05172009.jpg',
'theming' => self::THEMING_MODE_DARK,
],
'dejan-krsmanovic-ribbed-red-metal.jpg' => [
'attribution' => 'Ribbed red metal (Dejan Krsmanovic, CC BY)',
'attribution_url' => 'https://www.flickr.com/photos/dejankrsmanovic/42971456774/',
],
'eduardo-neves-pedra-azul.jpg' => [
'attribution' => 'Pedra azul milky way (Eduardo Neves, CC BY-SA)',
'attribution_url' => 'https://commons.wikimedia.org/wiki/File:Pedra_Azul_Milky_Way.jpg',
],
'european-space-agency-barents-bloom.jpg' => [
'attribution' => 'Barents bloom (European Space Agency, CC BY-SA)',
'attribution_url' => 'https://www.esa.int/ESA_Multimedia/Images/2016/08/Barents_bloom',
],
'hannes-fritz-flippity-floppity.jpg' => [
'attribution' => 'Flippity floppity (Hannes Fritz, CC BY-SA)',
'attribution_url' => 'http://hannes.photos/flippity-floppity',
],
'hannes-fritz-roulette.jpg' => [
'attribution' => 'Roulette (Hannes Fritz, CC BY-SA)',
'attribution_url' => 'http://hannes.photos/roulette',
],
'hannes-fritz-sea-spray.jpg' => [
'attribution' => 'Sea spray (Hannes Fritz, CC BY-SA)',
'attribution_url' => 'http://hannes.photos/sea-spray',
],
'kamil-porembinski-clouds.jpg' => [
'attribution' => 'Clouds (Kamil Porembiński, CC BY-SA)',
'attribution_url' => 'https://www.flickr.com/photos/paszczak000/8715851521/',
],
'bernard-spragg-new-zealand-fern.jpg' => [
'attribution' => 'New zealand fern (Bernard Spragg, CC0)',
'attribution_url' => 'https://commons.wikimedia.org/wiki/File:NZ_Fern.(Blechnum_chambersii)_(11263534936).jpg',
],
'rawpixel-pink-tapioca-bubbles.jpg' => [
'attribution' => 'Pink tapioca bubbles (Rawpixel, CC BY)',
'attribution_url' => 'https://www.flickr.com/photos/byrawpixel/27665140298/in/photostream/',
'theming' => self::THEMING_MODE_DARK,
],
'nasa-waxing-crescent-moon.jpg' => [
'attribution' => 'Waxing crescent moon (NASA, Public Domain)',
'attribution_url' => 'https://www.nasa.gov/image-feature/a-waxing-crescent-moon',
],
'tommy-chau-already.jpg' => [
'attribution' => 'Cityscape (Tommy Chau, CC BY)',
'attribution_url' => 'https://www.flickr.com/photos/90975693@N05/16910999368',
],
'tommy-chau-lion-rock-hill.jpg' => [
'attribution' => 'Lion rock hill (Tommy Chau, CC BY)',
'attribution_url' => 'https://www.flickr.com/photos/90975693@N05/17136440246',
'theming' => self::THEMING_MODE_DARK,
],
'lali-masriera-yellow-bricks.jpg' => [
'attribution' => 'Yellow bricks (Lali Masriera, CC BY)',
'attribution_url' => 'https://www.flickr.com/photos/visualpanic/3982464447',
'theming' => self::THEMING_MODE_DARK,
]
];
/**
* @var \OCP\Files\Folder
*/
private $userFolder;
/**
* @var \OCP\Files\SimpleFS\ISimpleFolder
*/
private $dashboardUserFolder;
/**
* @var IConfig
*/
private $config;
private $userId;
public function __construct(IRootFolder $rootFolder, IAppData $appData, IConfig $config, $userId) {
if ($userId === null) {
return;
}
$this->userFolder = $rootFolder->getUserFolder($userId);
try {
$this->dashboardUserFolder = $appData->getFolder($userId);
} catch (NotFoundException $e) {
$this->dashboardUserFolder = $appData->newFolder($userId);
}
$this->config = $config;
$this->userId = $userId;
}
public function setDefaultBackground(): void {
$this->config->deleteUserValue($this->userId, 'dashboard', 'background');
}
/**
* @param $path
* @throws NotFoundException
* @throws \OCP\Files\NotPermittedException
* @throws \OCP\PreConditionNotMetException
*/
public function setFileBackground($path): void {
$this->config->setUserValue($this->userId, 'dashboard', 'background', 'custom');
/** @var \OCP\Files\File $file */
$file = $this->userFolder->get($path);
$this->dashboardUserFolder->newFile('background.jpg', $file->fopen('r'));
}
public function setShippedBackground($fileName): void {
if (!array_key_exists($fileName, self::SHIPPED_BACKGROUNDS)) {
throw new \InvalidArgumentException('The given file name is invalid');
}
$this->config->setUserValue($this->userId, 'dashboard', 'background', $fileName);
}
public function setColorBackground(string $color): void {
if (!preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $color)) {
throw new \InvalidArgumentException('The given color is invalid');
}
$this->config->setUserValue($this->userId, 'dashboard', 'background', $color);
}
public function getBackground(): ?ISimpleFile {
$background = $this->config->getUserValue($this->userId, 'dashboard', 'background', 'default');
if ($background === 'custom') {
try {
return $this->dashboardUserFolder->getFile('background.jpg');
} catch (NotFoundException $e) {
}
}
return null;
}
}

@ -1,11 +1,12 @@
<template>
<div id="app-dashboard" :style="{ backgroundImage: `url(${backgroundImage})` }">
<div id="app-dashboard" :style="backgroundStyle">
<h2>{{ greeting.text }}</h2>
<div class="statuses">
<div v-for="status in registeredStatus"
:id="'status-' + status"
:key="status"
:ref="'status-' + status" />
:key="status">
<div :ref="'status-' + status" />
</div>
</div>
<Draggable v-model="layout"
@ -24,10 +25,15 @@
</div>
</Draggable>
<a v-tooltip="tooltip"
class="edit-panels icon-add"
:class="{ firstrun: firstRun }"
@click="showModal">{{ t('dashboard', 'Customize') }}</a>
<div class="footer"
:class="{ firstrun: firstRun }">
<a v-tooltip="tooltip"
class="edit-panels icon-rename"
tabindex="0"
@click="showModal"
@keyup.enter="showModal"
@keyup.space="showModal">{{ t('dashboard', 'Customize') }}</a>
</div>
<Modal v-if="modal" @close="closeModal">
<div class="modal__content">
@ -49,10 +55,10 @@
</li>
</Draggable>
<a :href="appStoreUrl" class="button">{{ t('dashboard', 'Get more widgets from the app store') }}</a>
<a v-if="isAdmin" :href="appStoreUrl" class="button">{{ t('dashboard', 'Get more widgets from the app store') }}</a>
<h3>{{ t('dashboard', 'Credits') }}</h3>
<p>{{ t('dashboard', 'Photos') }}: <a href="https://www.flickr.com/photos/paszczak000/8715851521/" target="_blank" rel="noopener">Clouds (Kamil Porembiński)</a>, <a href="https://www.flickr.com/photos/148302424@N05/36591009215/" target="_blank" rel="noopener">Un beau soir dété (Tanguy Domenge)</a>.</p>
<h3>{{ t('dashboard', 'Change background image') }}</h3>
<BackgroundSettings :background="background" @updateBackground="updateBackground" />
</div>
</Modal>
</div>
@ -65,23 +71,31 @@ import { getCurrentUser } from '@nextcloud/auth'
import { Modal } from '@nextcloud/vue'
import Draggable from 'vuedraggable'
import axios from '@nextcloud/axios'
import { generateUrl, generateFilePath } from '@nextcloud/router'
import { generateUrl } from '@nextcloud/router'
import isMobile from './mixins/isMobile'
import BackgroundSettings from './components/BackgroundSettings'
import getBackgroundUrl from './helpers/getBackgroundUrl'
import prefixWithBaseUrl from './helpers/prefixWithBaseUrl'
const panels = loadState('dashboard', 'panels')
const firstRun = loadState('dashboard', 'firstRun')
const background = loadState('dashboard', 'background')
const version = loadState('dashboard', 'version')
const shippedBackgroundList = loadState('dashboard', 'shippedBackgrounds')
export default {
name: 'App',
components: {
Modal,
Draggable,
BackgroundSettings,
},
mixins: [
isMobile,
],
data() {
return {
isAdmin: getCurrentUser().isAdmin,
timer: new Date(),
registeredStatus: [],
callbacks: {},
@ -94,15 +108,22 @@ export default {
modal: false,
appStoreUrl: generateUrl('/settings/apps/dashboard'),
statuses: {},
background,
version,
defaultBackground: window.OCA.Accessibility.theme === 'dark' ? prefixWithBaseUrl('flickr-148302424@N05-36591009215.jpg?v=1') : prefixWithBaseUrl('flickr-paszczak000-8715851521.jpg?v=1'),
}
},
computed: {
backgroundImage() {
const prefixWithBaseUrl = (url) => generateFilePath('dashboard', '', 'img/') + url
if (window.OCA.Accessibility.theme === 'dark') {
return !isMobile ? prefixWithBaseUrl('flickr-148302424@N05-36591009215.jpg?v=1') : prefixWithBaseUrl('flickr-148302424@N05-36591009215-mobile.jpg?v=1')
return getBackgroundUrl(this.background, this.version)
},
backgroundStyle() {
if (this.background.match(/#[0-9A-Fa-f]{6}/g)) {
return null
}
return {
backgroundImage: `url(${this.backgroundImage})`,
}
return !isMobile ? prefixWithBaseUrl('flickr-paszczak000-8715851521.jpg?v=1') : prefixWithBaseUrl('flickr-paszczak000-8715851521-mobile.jpg?v=1')
},
tooltip() {
if (!this.firstRun) {
@ -162,8 +183,17 @@ export default {
}
}
},
backgroundImage: {
immediate: true,
handler() {
const header = document.getElementById('header')
header.style.backgroundImage = `url(${this.backgroundImage})`
},
},
},
mounted() {
this.updateGlobalStyles()
setInterval(() => {
this.timer = new Date()
}, 30000)
@ -198,7 +228,9 @@ export default {
continue
}
if (element) {
this.callbacks[app](element[0])
this.callbacks[app](element[0], {
widget: this.panels[app],
})
Vue.set(this.panels[app], 'mounted', true)
} else {
console.error('Failed to register panel in the frontend as no backend data was provided for ' + app)
@ -235,45 +267,51 @@ export default {
this.firstRun = false
}, 1000)
},
updateBackground(data) {
this.background = data.type === 'custom' || data.type === 'default' ? data.type : data.value
this.version = data.version
this.updateGlobalStyles()
},
updateGlobalStyles() {
document.body.setAttribute('data-dashboard-background', this.background)
if (window.OCA.Theming.inverted) {
document.body.classList.add('dashboard-inverted')
}
const shippedBackgroundTheme = shippedBackgroundList[this.background] ? shippedBackgroundList[this.background].theming : 'light'
if (shippedBackgroundTheme === 'dark') {
document.body.classList.add('dashboard-dark')
} else {
document.body.classList.remove('dashboard-dark')
}
},
},
}
</script>
<style lang="scss">
// Show Dashboard background image beneath header
#body-user #header {
background: none;
}
#content {
padding-top: 0;
}
// Hide triangle indicators from navigation since they are out of place without the header bar
#appmenu li a.active::before,
#appmenu li:hover a::before,
#appmenu li:hover a.active::before,
#appmenu li a:focus::before {
display: none;
}
</style>
<style lang="scss" scoped>
#app-dashboard {
width: 100%;
padding-bottom: 100px;
background-size: cover;
background-position: center center;
background-repeat: no-repeat;
background-attachment: fixed;
background-color: var(--color-primary);
--color-background-translucent: rgba(255, 255, 255, 0.8);
--background-blur: blur(10px);
#body-user:not(.dark) & {
background-color: var(--color-primary);
#body-user.theme--dark & {
background-color: var(--color-main-background);
--color-background-translucent: rgba(24, 24, 24, 0.8);
}
#body-user.dark & {
#body-user.theme--highcontrast & {
background-color: var(--color-main-background);
--color-background-translucent: var(--color-main-background);
}
}
@ -285,17 +323,6 @@ export default {
padding: 120px 16px 0px;
}
.statuses {
::v-deep #user-status-menu-item__subheader>button {
backdrop-filter: blur(10px);
background-color: rgba(255, 255, 255, 0.8);
#body-user.dark & {
background-color: rgba(24, 24, 24, 0.8);
}
}
}
.panels {
width: auto;
margin: auto;
@ -311,12 +338,12 @@ export default {
width: 320px;
max-width: 100%;
margin: 16px;
background-color: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
background-color: var(--color-background-translucent);
backdrop-filter: var(--background-blur);
border-radius: var(--border-radius-large);
#body-user.dark & {
background-color: rgba(24, 24, 24, 0.8);
#body-user.theme--highcontrast & {
border: 2px solid var(--color-border);
}
&.sortable-ghost {
@ -367,55 +394,82 @@ export default {
}
}
.footer {
text-align: center;
transition: bottom var(--animation-slow) ease-in-out;
bottom: 0;
padding: 44px 0;
&.firstrun {
position: sticky;
bottom: 10px;
}
}
.edit-panels {
z-index: 99;
position: fixed;
bottom: 20px;
right: 20px;
padding: 10px 15px 10px 35px;
background-position: 10px center;
opacity: .7;
background-color: var(--color-main-background);
display: inline-block;
margin:auto;
background-position: 16px center;
padding: 12px 16px;
padding-left: 36px;
border-radius: var(--border-radius-pill);
transition: right var(--animation-slow) ease-in-out;
max-width: 200px;
opacity: 1;
text-align: center;
}
&:hover {
opacity: 1;
background-color: var(--color-background-hover);
}
.edit-panels,
.statuses ::v-deep #user-status-menu-item__subheader>button {
background-color: var(--color-background-translucent);
backdrop-filter: var(--background-blur);
&.firstrun {
right: 50%;
transform: translateX(50%);
max-width: 200px;
box-shadow: 0px 0px 3px var(--color-box-shadow);
opacity: 1;
text-align: center;
&:hover,
&:focus {
background-color: var(--color-background-hover);
}
}
.modal__content {
width: 30vw;
margin: 20px;
padding: 32px 16px;
max-height: 70vh;
text-align: center;
overflow: auto;
ol {
display: flex;
flex-direction: column;
flex-direction: row;
justify-content: center;
list-style-type: none;
padding-bottom: 16px;
}
li label {
padding: 10px;
display: block;
list-style-type: none;
background-size: 16px;
background-position: left center;
padding-left: 26px;
li {
label {
display: block;
padding: 48px 8px 16px 8px;
margin: 8px;
width: 160px;
background-color: var(--color-background-hover);
border: 2px solid var(--color-main-background);
border-radius: var(--border-radius-large);
background-size: 24px;
background-position: center 16px;
text-align: center;
&:hover {
border-color: var(--color-primary);
}
}
input:focus + label {
border-color: var(--color-primary);
}
}
h3 {
font-weight: bold;
&:not(:first-of-type) {
margin-top: 32px;
margin-top: 64px;
}
}
}
@ -432,6 +486,8 @@ export default {
& > div {
max-width: 200px;
margin-left: 10px;
margin-right: 10px;
}
}

@ -0,0 +1,194 @@
<!--
- @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net>
-
- @author Julius Härtl <jus@bitgrid.net>
-
- @license GNU AGPL version 3 or any later version
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-->
<template>
<div class="background-selector">
<a class="background filepicker"
:class="{ active: background === 'custom' }"
tabindex="0"
@click="pickFile"
@keyup.enter="pickFile"
@keyup.space="pickFile">
{{ t('dashboard', 'Pick from files') }}
</a>
<a class="background default"
tabindex="0"
:class="{ 'icon-loading': loading === 'default', active: background === 'default' }"
@click="setDefault"
@keyup.enter="setDefault"
@keyup.space="setDefault">
{{ t('dashboard', 'Default images') }}
</a>
<a class="background color"
:class="{ active: background === 'custom' }"
tabindex="0"
@click="pickColor"
@keyup.enter="pickColor"
@keyup.space="pickColor">
{{ t('dashboard', 'Plain background') }}
</a>
<a v-for="shippedBackground in shippedBackgrounds"
:key="shippedBackground.name"
v-tooltip="shippedBackground.details.attribution"
:class="{ 'icon-loading': loading === shippedBackground.name, active: background === shippedBackground.name }"
tabindex="0"
class="background"
:style="{ 'background-image': 'url(' + shippedBackground.preview + ')' }"
@click="setShipped(shippedBackground.name)"
@keyup.enter="setShipped(shippedBackground.name)"
@keyup.space="setShipped(shippedBackground.name)" />
</div>
</template>
<script>
import axios from '@nextcloud/axios'
import { generateUrl } from '@nextcloud/router'
import { loadState } from '@nextcloud/initial-state'
import getBackgroundUrl from './../helpers/getBackgroundUrl'
import prefixWithBaseUrl from './../helpers/prefixWithBaseUrl'
const shippedBackgroundList = loadState('dashboard', 'shippedBackgrounds')
export default {
name: 'BackgroundSettings',
props: {
background: {
type: String,
default: 'default',
},
},
data() {
return {
backgroundImage: generateUrl('/apps/dashboard/background') + '?v=' + Date.now(),
loading: false,
}
},
computed: {
shippedBackgrounds() {
return Object.keys(shippedBackgroundList).map((item) => {
return {
name: item,
url: prefixWithBaseUrl(item),
preview: prefixWithBaseUrl('previews/' + item),
details: shippedBackgroundList[item],
}
})
},
},
methods: {
async update(data) {
const background = data.type === 'custom' || data.type === 'default' ? data.type : data.value
this.backgroundImage = getBackgroundUrl(background, data.version)
if (data.type === 'color') {
this.$emit('updateBackground', data)
this.loading = false
return
}
const image = new Image()
image.onload = () => {
this.$emit('updateBackground', data)
this.loading = false
}
image.src = this.backgroundImage
},
async setDefault() {
this.loading = 'default'
const result = await axios.post(generateUrl('/apps/dashboard/background/default'))
this.update(result.data)
},
async setShipped(shipped) {
this.loading = shipped
const result = await axios.post(generateUrl('/apps/dashboard/background/shipped'), { value: shipped })
this.update(result.data)
},
async setFile(path) {
this.loading = 'custom'
const result = await axios.post(generateUrl('/apps/dashboard/background/custom'), { value: path })
this.update(result.data)
},
async pickColor() {
this.loading = 'color'
const color = OCA && OCA.Theming ? OCA.Theming.color : '#0082c9'
const result = await axios.post(generateUrl('/apps/dashboard/background/color'), { value: color })
this.update(result.data)
},
pickFile() {
window.OC.dialogs.filepicker(t('dashboard', 'Insert from {productName}', { productName: OC.theme.name }), (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)
},
},
}
</script>
<style scoped lang="scss">
.background-selector {
display: flex;
flex-wrap: wrap;
justify-content: center;
.background {
width: 176px;
height: 96px;
margin: 8px;
background-size: cover;
background-position: center center;
text-align: center;
border-radius: var(--border-radius-large);
border: 2px solid var(--color-main-background);
overflow: hidden;
&.current {
background-image: var(--color-background-dark);
}
&.filepicker, &.default, &.color {
border-color: var(--color-border);
line-height: 96px;
}
&.color {
background-color: var(--color-primary);
color: var(--color-primary-text);
}
&.active,
&:hover,
&:focus {
border: 2px solid var(--color-primary);
}
&.active:not(.icon-loading):after {
background-image: var(--icon-checkmark-fff);
background-repeat: no-repeat;
background-position: center;
background-size: 44px;
content: '';
display: block;
height: 100%;
}
}
}
</style>

@ -0,0 +1,36 @@
/*
* @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import { generateUrl } from '@nextcloud/router'
import prefixWithBaseUrl from './prefixWithBaseUrl'
export default (background, time = 0) => {
if (background === 'default') {
if (window.OCA.Accessibility.theme === 'dark') {
return prefixWithBaseUrl('eduardo-neves-pedra-azul.jpg')
}
return prefixWithBaseUrl('kamil-porembinski-clouds.jpg')
} else if (background === 'custom') {
return generateUrl('/apps/dashboard/background') + '?v=' + time
}
return prefixWithBaseUrl(background)
}

@ -0,0 +1,24 @@
/*
* @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import { generateFilePath } from '@nextcloud/router'
export default (url) => generateFilePath('dashboard', '', 'img/') + url
Loading…
Cancel
Save