mirror of
https://github.com/nextcloud/server.git
synced 2026-07-16 05:13:10 -04:00
Merge pull request #44129 from nextcloud/feat/discover-apps-section
This commit is contained in:
commit
74f996bbcb
105 changed files with 1337 additions and 145 deletions
|
|
@ -39,6 +39,8 @@ return [
|
|||
['name' => 'MailSettings#storeCredentials', 'url' => '/settings/admin/mailsettings/credentials', 'verb' => 'POST' , 'root' => ''],
|
||||
['name' => 'MailSettings#sendTestMail', 'url' => '/settings/admin/mailtest', 'verb' => 'POST' , 'root' => ''],
|
||||
|
||||
['name' => 'AppSettings#getAppDiscoverJSON', 'url' => '/settings/api/apps/discover', 'verb' => 'GET', 'root' => ''],
|
||||
['name' => 'AppSettings#getAppDiscoverMedia', 'url' => '/settings/api/apps/media', 'verb' => 'GET', 'root' => ''],
|
||||
['name' => 'AppSettings#listCategories', 'url' => '/settings/apps/categories', 'verb' => 'GET' , 'root' => ''],
|
||||
['name' => 'AppSettings#viewApps', 'url' => '/settings/apps', 'verb' => 'GET' , 'root' => ''],
|
||||
['name' => 'AppSettings#listApps', 'url' => '/settings/apps/list', 'verb' => 'GET' , 'root' => ''],
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
* @author Roeland Jago Douma <roeland@famdouma.nl>
|
||||
* @author Thomas Müller <thomas.mueller@tmit.eu>
|
||||
* @author Kate Döen <kate.doeen@nextcloud.com>
|
||||
* @author Ferdinand Thiessen <opensource@fthiessen.de>
|
||||
*
|
||||
* @license AGPL-3.0
|
||||
*
|
||||
|
|
@ -33,6 +34,7 @@
|
|||
namespace OCA\Settings\Controller;
|
||||
|
||||
use OC\App\AppStore\Bundles\BundleFetcher;
|
||||
use OC\App\AppStore\Fetcher\AppDiscoverFetcher;
|
||||
use OC\App\AppStore\Fetcher\AppFetcher;
|
||||
use OC\App\AppStore\Fetcher\CategoryFetcher;
|
||||
use OC\App\AppStore\Version\VersionParser;
|
||||
|
|
@ -40,14 +42,25 @@ use OC\App\DependencyAnalyzer;
|
|||
use OC\App\Platform;
|
||||
use OC\Installer;
|
||||
use OC_App;
|
||||
use OCP\App\AppPathNotFoundException;
|
||||
use OCP\App\IAppManager;
|
||||
use OCP\AppFramework\Controller;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\AppFramework\Http\Attribute\OpenAPI;
|
||||
use OCP\AppFramework\Http\ContentSecurityPolicy;
|
||||
use OCP\AppFramework\Http\FileDisplayResponse;
|
||||
use OCP\AppFramework\Http\JSONResponse;
|
||||
use OCP\AppFramework\Http\NotFoundResponse;
|
||||
use OCP\AppFramework\Http\Response;
|
||||
use OCP\AppFramework\Http\TemplateResponse;
|
||||
use OCP\AppFramework\Services\IInitialState;
|
||||
use OCP\Files\AppData\IAppDataFactory;
|
||||
use OCP\Files\IAppData;
|
||||
use OCP\Files\NotFoundException;
|
||||
use OCP\Files\NotPermittedException;
|
||||
use OCP\Files\SimpleFS\ISimpleFile;
|
||||
use OCP\Files\SimpleFS\ISimpleFolder;
|
||||
use OCP\Http\Client\IClientService;
|
||||
use OCP\IConfig;
|
||||
use OCP\IL10N;
|
||||
use OCP\INavigationManager;
|
||||
|
|
@ -62,9 +75,12 @@ class AppSettingsController extends Controller {
|
|||
/** @var array */
|
||||
private $allApps = [];
|
||||
|
||||
private IAppData $appData;
|
||||
|
||||
public function __construct(
|
||||
string $appName,
|
||||
IRequest $request,
|
||||
IAppDataFactory $appDataFactory,
|
||||
private IL10N $l10n,
|
||||
private IConfig $config,
|
||||
private INavigationManager $navigationManager,
|
||||
|
|
@ -77,8 +93,11 @@ class AppSettingsController extends Controller {
|
|||
private IURLGenerator $urlGenerator,
|
||||
private LoggerInterface $logger,
|
||||
private IInitialState $initialState,
|
||||
private AppDiscoverFetcher $discoverFetcher,
|
||||
private IClientService $clientService,
|
||||
) {
|
||||
parent::__construct($appName, $request);
|
||||
$this->appData = $appDataFactory->get('appstore');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -106,6 +125,93 @@ class AppSettingsController extends Controller {
|
|||
return $templateResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all active entries for the app discover section
|
||||
*
|
||||
* @NoCSRFRequired
|
||||
*/
|
||||
public function getAppDiscoverJSON(): JSONResponse {
|
||||
$data = $this->discoverFetcher->get();
|
||||
return new JSONResponse($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @PublicPage
|
||||
* @NoCSRFRequired
|
||||
*
|
||||
* Get a image for the app discover section - this is proxied for privacy and CSP reasons
|
||||
*
|
||||
* @param string $image
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getAppDiscoverMedia(string $fileName): Response {
|
||||
$etag = $this->discoverFetcher->getETag() ?? date('Y-m');
|
||||
$folder = null;
|
||||
try {
|
||||
$folder = $this->appData->getFolder('app-discover-cache');
|
||||
$this->cleanUpImageCache($folder, $etag);
|
||||
} catch (\Throwable $e) {
|
||||
$folder = $this->appData->newFolder('app-discover-cache');
|
||||
}
|
||||
|
||||
// Get the current cache folder
|
||||
try {
|
||||
$folder = $folder->getFolder($etag);
|
||||
} catch (NotFoundException $e) {
|
||||
$folder = $folder->newFolder($etag);
|
||||
}
|
||||
|
||||
$info = pathinfo($fileName);
|
||||
$hashName = md5($fileName);
|
||||
$allFiles = $folder->getDirectoryListing();
|
||||
// Try to find the file
|
||||
$file = array_filter($allFiles, function (ISimpleFile $file) use ($hashName) {
|
||||
return str_starts_with($file->getName(), $hashName);
|
||||
});
|
||||
// Get the first entry
|
||||
$file = reset($file);
|
||||
// If not found request from Web
|
||||
if ($file === false) {
|
||||
try {
|
||||
$client = $this->clientService->newClient();
|
||||
$fileResponse = $client->get($fileName);
|
||||
$contentType = $fileResponse->getHeader('Content-Type');
|
||||
$extension = $info['extension'] ?? '';
|
||||
$file = $folder->newFile($hashName . '.' . base64_encode($contentType) . '.' . $extension, $fileResponse->getBody());
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->warning('Could not load media file for app discover section', ['media_src' => $fileName, 'exception' => $e]);
|
||||
return new NotFoundResponse();
|
||||
}
|
||||
} else {
|
||||
// File was found so we can get the content type from the file name
|
||||
$contentType = base64_decode(explode('.', $file->getName())[1] ?? '');
|
||||
}
|
||||
|
||||
$response = new FileDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => $contentType]);
|
||||
// cache for 7 days
|
||||
$response->cacheFor(604800, false, true);
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove orphaned folders from the image cache that do not match the current etag
|
||||
* @param ISimpleFolder $folder The folder to clear
|
||||
* @param string $etag The etag (directory name) to keep
|
||||
*/
|
||||
private function cleanUpImageCache(ISimpleFolder $folder, string $etag): void {
|
||||
// Cleanup old cache folders
|
||||
$allFiles = $folder->getDirectoryListing();
|
||||
foreach ($allFiles as $dir) {
|
||||
try {
|
||||
if ($dir->getName() !== $etag) {
|
||||
$dir->delete();
|
||||
}
|
||||
} catch (NotPermittedException $e) {
|
||||
// ignore folder for now
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function getAppsWithUpdates() {
|
||||
$appClass = new \OC_App();
|
||||
$apps = $appClass->listAllApps();
|
||||
|
|
@ -190,6 +296,7 @@ class AppSettingsController extends Controller {
|
|||
private function getAllApps() {
|
||||
return $this->allApps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available apps in a category
|
||||
*
|
||||
|
|
@ -291,7 +398,14 @@ class AppSettingsController extends Controller {
|
|||
$nextCloudVersionDependencies['nextcloud']['@attributes']['max-version'] = $nextCloudVersion->getMaximumVersion();
|
||||
}
|
||||
$phpVersion = $versionParser->getVersion($app['releases'][0]['rawPhpVersionSpec']);
|
||||
$existsLocally = \OC_App::getAppPath($app['id']) !== false;
|
||||
|
||||
try {
|
||||
$this->appManager->getAppPath($app['id']);
|
||||
$existsLocally = true;
|
||||
} catch (AppPathNotFoundException $e) {
|
||||
$existsLocally = false;
|
||||
}
|
||||
|
||||
$phpDependencies = [];
|
||||
if ($phpVersion->getMinimumVersion() !== '') {
|
||||
$phpDependencies['php']['@attributes']['min-version'] = $phpVersion->getMinimumVersion();
|
||||
|
|
@ -310,7 +424,7 @@ class AppSettingsController extends Controller {
|
|||
}
|
||||
}
|
||||
|
||||
$currentLanguage = substr(\OC::$server->getL10NFactory()->findLanguage(), 0, 2);
|
||||
$currentLanguage = substr($this->l10nFactory->findLanguage(), 0, 2);
|
||||
$enabledValue = $this->config->getAppValue($app['id'], 'enabled', 'no');
|
||||
$groups = null;
|
||||
if ($enabledValue !== 'no' && $enabledValue !== 'yes') {
|
||||
|
|
@ -397,7 +511,7 @@ class AppSettingsController extends Controller {
|
|||
|
||||
// Check if app is already downloaded
|
||||
/** @var Installer $installer */
|
||||
$installer = \OC::$server->query(Installer::class);
|
||||
$installer = \OC::$server->get(Installer::class);
|
||||
$isDownloaded = $installer->isDownloaded($appId);
|
||||
|
||||
if (!$isDownloaded) {
|
||||
|
|
@ -416,7 +530,7 @@ class AppSettingsController extends Controller {
|
|||
}
|
||||
}
|
||||
return new JSONResponse(['data' => ['update_required' => $updateRequired]]);
|
||||
} catch (\Exception $e) {
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->error('could not enable apps', ['exception' => $e]);
|
||||
return new JSONResponse(['data' => ['message' => $e->getMessage()]], Http::STATUS_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,101 @@
|
|||
<template>
|
||||
<div class="app-discover">
|
||||
<NcEmptyContent v-if="hasError"
|
||||
:name="t('settings', 'Nothing to show')"
|
||||
:description="t('settings', 'Could not load section content from app store.')">
|
||||
<template #icon>
|
||||
<NcIconSvgWrapper :path="mdiEyeOff" :size="64" />
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
<NcEmptyContent v-else-if="elements.length === 0"
|
||||
:name="t('settings', 'Loading')"
|
||||
:description="t('settings', 'Fetching the latest news…')">
|
||||
<template #icon>
|
||||
<NcLoadingIcon :size="64" />
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
<template v-else>
|
||||
<component :is="getComponent(entry.type)"
|
||||
v-for="entry, index in elements"
|
||||
:key="entry.id ?? index"
|
||||
v-bind="entry" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { IAppDiscoverElements } from '../../constants/AppDiscoverTypes.ts'
|
||||
|
||||
import { mdiEyeOff } from '@mdi/js'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { defineAsyncComponent, defineComponent, onBeforeMount, ref } from 'vue'
|
||||
|
||||
import axios from '@nextcloud/axios'
|
||||
import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
|
||||
import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
|
||||
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
|
||||
|
||||
import logger from '../../logger'
|
||||
import { apiTypeParser } from '../../utils/appDiscoverTypeParser.ts'
|
||||
|
||||
const PostType = defineAsyncComponent(() => import('./PostType.vue'))
|
||||
const CarouselType = defineAsyncComponent(() => import('./CarouselType.vue'))
|
||||
|
||||
const hasError = ref(false)
|
||||
const elements = ref<IAppDiscoverElements[]>([])
|
||||
|
||||
/**
|
||||
* Shuffle using the Fisher-Yates algorithm
|
||||
* @param array The array to shuffle (in place)
|
||||
*/
|
||||
const shuffleArray = (array) => {
|
||||
for (let i = array.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[array[i], array[j]] = [array[j], array[i]]
|
||||
}
|
||||
return array
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the app discover section information
|
||||
*/
|
||||
onBeforeMount(async () => {
|
||||
try {
|
||||
const { data } = await axios.get<Record<string, unknown>[]>(generateUrl('/settings/api/apps/discover'))
|
||||
const parsedData = data.map(apiTypeParser)
|
||||
elements.value = shuffleArray(parsedData)
|
||||
} catch (error) {
|
||||
hasError.value = true
|
||||
logger.error(error as Error)
|
||||
showError(t('settings', 'Could not load app discover section'))
|
||||
}
|
||||
})
|
||||
|
||||
const getComponent = (type) => {
|
||||
if (type === 'post') {
|
||||
return PostType
|
||||
} else if (type === 'carousel') {
|
||||
return CarouselType
|
||||
}
|
||||
return defineComponent({
|
||||
mounted: () => logger.error('Unknown component requested ', type),
|
||||
render: (h) => h('div', t('settings', 'Could not render element')),
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.app-discover {
|
||||
max-width: 1008px; /* 900px + 2x 54px padding for the carousel controls */
|
||||
margin-inline: auto;
|
||||
padding-inline: 54px;
|
||||
/* Padding required to make last element not bound to the bottom */
|
||||
padding-block-end: var(--default-clickable-area, 44px);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--default-clickable-area, 44px);
|
||||
}
|
||||
</style>
|
||||
202
apps/settings/src/components/AppStoreDiscover/CarouselType.vue
Normal file
202
apps/settings/src/components/AppStoreDiscover/CarouselType.vue
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
<template>
|
||||
<section :aria-roledescription="t('settings', 'Carousel')" :aria-labelledby="headingId ? `${headingId}` : undefined">
|
||||
<h3 v-if="headline" :id="headingId">
|
||||
{{ translatedHeadline }}
|
||||
</h3>
|
||||
<div class="app-discover-carousel__wrapper">
|
||||
<div class="app-discover-carousel__button-wrapper">
|
||||
<NcButton class="app-discover-carousel__button app-discover-carousel__button--previous"
|
||||
type="tertiary-no-background"
|
||||
:aria-label="t('settings', 'Previous slide')"
|
||||
:disabled="!hasPrevious"
|
||||
@click="currentIndex -= 1">
|
||||
<template #icon>
|
||||
<NcIconSvgWrapper :path="mdiChevronLeft" />
|
||||
</template>
|
||||
</NcButton>
|
||||
</div>
|
||||
|
||||
<Transition :name="transitionName" mode="out-in">
|
||||
<PostType v-bind="shownElement"
|
||||
:key="shownElement.id ?? currentIndex"
|
||||
:aria-labelledby="`${internalId}-tab-${currentIndex}`"
|
||||
:dom-id="`${internalId}-tabpanel-${currentIndex}`"
|
||||
inline
|
||||
role="tabpanel" />
|
||||
</Transition>
|
||||
|
||||
<div class="app-discover-carousel__button-wrapper">
|
||||
<NcButton class="app-discover-carousel__button app-discover-carousel__button--next"
|
||||
type="tertiary-no-background"
|
||||
:aria-label="t('settings', 'Next slide')"
|
||||
:disabled="!hasNext"
|
||||
@click="currentIndex += 1">
|
||||
<template #icon>
|
||||
<NcIconSvgWrapper :path="mdiChevronRight" />
|
||||
</template>
|
||||
</NcButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="app-discover-carousel__tabs" role="tablist" :aria-label="t('settings', 'Choose slide to display')">
|
||||
<NcButton v-for="index of content.length"
|
||||
:id="`${internalId}-tab-${index}`"
|
||||
:key="index"
|
||||
:aria-label="t('settings', '{index} of {total}', { index, total: content.length })"
|
||||
:aria-controls="`${internalId}-tabpanel-${index}`"
|
||||
:aria-selected="`${currentIndex === (index - 1)}`"
|
||||
role="tab"
|
||||
type="tertiary-no-background"
|
||||
@click="currentIndex = index - 1">
|
||||
<template #icon>
|
||||
<NcIconSvgWrapper :path="currentIndex === (index - 1) ? mdiCircleSlice8 : mdiCircleOutline" />
|
||||
</template>
|
||||
</NcButton>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import type { PropType } from 'vue'
|
||||
import type { IAppDiscoverCarousel } from '../../constants/AppDiscoverTypes.ts'
|
||||
|
||||
import { mdiChevronLeft, mdiChevronRight, mdiCircleOutline, mdiCircleSlice8 } from '@mdi/js'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { computed, defineComponent, nextTick, ref, watch } from 'vue'
|
||||
import { commonAppDiscoverProps } from './common.ts'
|
||||
import { useLocalizedValue } from '../../composables/useGetLocalizedValue.ts'
|
||||
|
||||
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
|
||||
import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
|
||||
import PostType from './PostType.vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CarouselType',
|
||||
|
||||
components: {
|
||||
NcButton,
|
||||
NcIconSvgWrapper,
|
||||
PostType,
|
||||
},
|
||||
|
||||
props: {
|
||||
...commonAppDiscoverProps,
|
||||
|
||||
/**
|
||||
* The content of the carousel
|
||||
*/
|
||||
content: {
|
||||
type: Array as PropType<IAppDiscoverCarousel['content']>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
const translatedHeadline = useLocalizedValue(computed(() => props.headline))
|
||||
|
||||
const currentIndex = ref(Math.min(1, props.content.length - 1))
|
||||
const shownElement = ref(props.content[currentIndex.value])
|
||||
const hasNext = computed(() => currentIndex.value < (props.content.length - 1))
|
||||
const hasPrevious = computed(() => currentIndex.value > 0)
|
||||
|
||||
const internalId = computed(() => props.id ?? (Math.random() + 1).toString(36).substring(7))
|
||||
const headingId = computed(() => `${internalId.value}-h`)
|
||||
|
||||
const transitionName = ref('slide-out')
|
||||
watch(() => currentIndex.value, (o, n) => {
|
||||
if (o < n) {
|
||||
transitionName.value = 'slide-out'
|
||||
} else {
|
||||
transitionName.value = 'slide-in'
|
||||
}
|
||||
|
||||
// Wait next tick
|
||||
nextTick(() => {
|
||||
shownElement.value = props.content[currentIndex.value]
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
t,
|
||||
internalId,
|
||||
headingId,
|
||||
|
||||
hasNext,
|
||||
hasPrevious,
|
||||
currentIndex,
|
||||
shownElement,
|
||||
|
||||
transitionName,
|
||||
|
||||
translatedHeadline,
|
||||
|
||||
mdiChevronLeft,
|
||||
mdiChevronRight,
|
||||
mdiCircleOutline,
|
||||
mdiCircleSlice8,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
h3 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin-block: 0 1em;
|
||||
}
|
||||
|
||||
.app-discover-carousel {
|
||||
&__wrapper {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&__button {
|
||||
color: var(--color-text-maxcontrast);
|
||||
position: absolute;
|
||||
top: calc(50% - 22px); // 50% minus half of button height
|
||||
|
||||
&-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
// See padding of discover section
|
||||
&--next {
|
||||
right: -54px;
|
||||
}
|
||||
&--previous {
|
||||
left: -54px;
|
||||
}
|
||||
}
|
||||
|
||||
&__tabs {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
|
||||
> * {
|
||||
color: var(--color-text-maxcontrast);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.slide-in-enter-active,
|
||||
.slide-in-leave-active,
|
||||
.slide-out-enter-active,
|
||||
.slide-out-leave-active {
|
||||
transition: all .4s ease-out;
|
||||
}
|
||||
|
||||
.slide-in-leave-to,
|
||||
.slide-out-enter {
|
||||
opacity: 0;
|
||||
transform: translateX(50%);
|
||||
}
|
||||
|
||||
.slide-in-enter,
|
||||
.slide-out-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
</style>
|
||||
285
apps/settings/src/components/AppStoreDiscover/PostType.vue
Normal file
285
apps/settings/src/components/AppStoreDiscover/PostType.vue
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
<!--
|
||||
- @copyright Copyright (c) 2024 Ferdinand Thiessen <opensource@fthiessen.de>
|
||||
-
|
||||
- @author Ferdinand Thiessen <opensource@fthiessen.de>
|
||||
-
|
||||
- @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/>.
|
||||
-
|
||||
-->
|
||||
<template>
|
||||
<article :id="domId"
|
||||
class="app-discover-post"
|
||||
:class="{ 'app-discover-post--reverse': media && media.alignment === 'start' }">
|
||||
<component :is="link ? 'a' : 'div'"
|
||||
v-if="headline || text"
|
||||
:href="link"
|
||||
:target="link ? '_blank' : undefined"
|
||||
class="app-discover-post__text">
|
||||
<component :is="inline ? 'h4' : 'h3'">{{ translatedHeadline }}</component>
|
||||
<p>{{ translatedText }}</p>
|
||||
</component>
|
||||
<component :is="mediaLink ? 'a' : 'div'"
|
||||
v-if="mediaSources"
|
||||
:href="mediaLink"
|
||||
:target="mediaLink ? '_blank' : undefined"
|
||||
class="app-discover-post__media"
|
||||
:class="{
|
||||
'app-discover-post__media--fullwidth': isFullWidth,
|
||||
'app-discover-post__media--start': media?.alignment === 'start',
|
||||
'app-discover-post__media--end': media?.alignment === 'end',
|
||||
}">
|
||||
<component :is="isImage ? 'picture' : 'video'"
|
||||
ref="mediaElement"
|
||||
class="app-discover-post__media-element"
|
||||
:muted="!isImage"
|
||||
:playsinline="!isImage"
|
||||
:preload="!isImage && 'auto'"
|
||||
@ended="hasPlaybackEnded = true">
|
||||
<source v-for="source of mediaSources"
|
||||
:key="source.src"
|
||||
:src="isImage ? undefined : generatePrivacyUrl(source.src)"
|
||||
:srcset="isImage ? generatePrivacyUrl(source.src) : undefined"
|
||||
:type="source.mime">
|
||||
<img v-if="isImage"
|
||||
:src="generatePrivacyUrl(mediaSources[0].src)"
|
||||
:alt="mediaAlt">
|
||||
</component>
|
||||
<div class="app-discover-post__play-icon-wrapper">
|
||||
<NcIconSvgWrapper v-if="!isImage && showPlayVideo"
|
||||
class="app-discover-post__play-icon"
|
||||
:path="mdiPlayCircleOutline"
|
||||
:size="92" />
|
||||
</div>
|
||||
</component>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import type { IAppDiscoverPost } from '../../constants/AppDiscoverTypes.ts'
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
import { mdiPlayCircleOutline } from '@mdi/js'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { useElementVisibility } from '@vueuse/core'
|
||||
import { computed, defineComponent, ref, watchEffect } from 'vue'
|
||||
import { commonAppDiscoverProps } from './common'
|
||||
import { useLocalizedValue } from '../../composables/useGetLocalizedValue'
|
||||
|
||||
import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
NcIconSvgWrapper,
|
||||
},
|
||||
|
||||
props: {
|
||||
...commonAppDiscoverProps,
|
||||
|
||||
text: {
|
||||
type: Object as PropType<IAppDiscoverPost['text']>,
|
||||
required: false,
|
||||
default: () => null,
|
||||
},
|
||||
|
||||
media: {
|
||||
type: Object as PropType<IAppDiscoverPost['media']>,
|
||||
required: false,
|
||||
default: () => null,
|
||||
},
|
||||
|
||||
inline: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
|
||||
domId: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
const translatedHeadline = useLocalizedValue(computed(() => props.headline))
|
||||
const translatedText = useLocalizedValue(computed(() => props.text))
|
||||
|
||||
const localizedMedia = useLocalizedValue(computed(() => props.media?.content))
|
||||
const mediaSources = computed(() => localizedMedia.value !== null ? [localizedMedia.value.src].flat() : undefined)
|
||||
const mediaAlt = computed(() => localizedMedia.value?.alt ?? '')
|
||||
|
||||
const isImage = computed(() => mediaSources?.value?.[0].mime.startsWith('image/') === true)
|
||||
/**
|
||||
* Is the media is shown full width
|
||||
*/
|
||||
const isFullWidth = computed(() => !translatedHeadline.value && !translatedText.value)
|
||||
|
||||
/**
|
||||
* Link on the media
|
||||
* Fallback to post link to prevent link inside link (which is invalid HTML)
|
||||
*/
|
||||
const mediaLink = computed(() => localizedMedia.value?.link ?? props.link)
|
||||
|
||||
const hasPlaybackEnded = ref(false)
|
||||
const showPlayVideo = computed(() => localizedMedia.value?.link && hasPlaybackEnded.value)
|
||||
|
||||
/**
|
||||
* Generate URL for cached media to prevent user can be tracked
|
||||
* @param url The URL to resolve
|
||||
*/
|
||||
const generatePrivacyUrl = (url: string) => url.startsWith('/') ? url : generateUrl('/settings/api/apps/media?fileName={fileName}', { fileName: url })
|
||||
|
||||
const mediaElement = ref<HTMLVideoElement|HTMLPictureElement>()
|
||||
const mediaIsVisible = useElementVisibility(mediaElement, { threshold: 0.3 })
|
||||
watchEffect(() => {
|
||||
// Only if media is video
|
||||
if (!isImage.value && mediaElement.value) {
|
||||
const video = mediaElement.value as HTMLVideoElement
|
||||
|
||||
if (mediaIsVisible.value) {
|
||||
// Ensure video is muted - otherwise .play() will be blocked by browsers
|
||||
video.muted = true
|
||||
// If visible start playback
|
||||
video.play()
|
||||
} else {
|
||||
// If not visible pause the playback
|
||||
video.pause()
|
||||
// If the animation has ended reset
|
||||
if (video.ended) {
|
||||
video.currentTime = 0
|
||||
hasPlaybackEnded.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
mdiPlayCircleOutline,
|
||||
|
||||
translatedText,
|
||||
translatedHeadline,
|
||||
mediaElement,
|
||||
mediaSources,
|
||||
mediaAlt,
|
||||
mediaLink,
|
||||
|
||||
hasPlaybackEnded,
|
||||
showPlayVideo,
|
||||
|
||||
isFullWidth,
|
||||
isImage,
|
||||
|
||||
generatePrivacyUrl,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.app-discover-post {
|
||||
width: 100%;
|
||||
background-color: var(--color-primary-element-light);
|
||||
border-radius: var(--border-radius-rounded);
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
&--reverse {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
h3, h4 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin-block: 0 1em;
|
||||
}
|
||||
|
||||
&__text {
|
||||
display: block;
|
||||
padding: var(--border-radius-rounded);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__media {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
|
||||
max-height: 300px;
|
||||
max-width: 450px;
|
||||
border-radius: var(--border-radius-rounded);
|
||||
|
||||
&--fullwidth {
|
||||
max-width: unset;
|
||||
max-height: unset;
|
||||
}
|
||||
|
||||
&--end {
|
||||
border-end-start-radius: 0;
|
||||
border-start-start-radius: 0;
|
||||
}
|
||||
|
||||
&--start {
|
||||
border-end-end-radius: 0;
|
||||
border-start-end-radius: 0;
|
||||
}
|
||||
|
||||
img, &-element {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
}
|
||||
|
||||
&__play-icon {
|
||||
&-wrapper {
|
||||
position: relative;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
}
|
||||
|
||||
position: absolute;
|
||||
top: -46px; // half of the icon height
|
||||
right: -46px; // half of the icon width
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure section works on mobile devices
|
||||
@media only screen and (max-width: 699px) {
|
||||
.app-discover-post {
|
||||
flex-direction: column;
|
||||
|
||||
&--reverse {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
&__media {
|
||||
min-width: 100%;
|
||||
|
||||
&--end {
|
||||
border-radius: var(--border-radius-rounded);
|
||||
border-start-end-radius: 0;
|
||||
border-start-start-radius: 0;
|
||||
}
|
||||
|
||||
&--start {
|
||||
border-radius: var(--border-radius-rounded);
|
||||
border-end-end-radius: 0;
|
||||
border-end-start-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
65
apps/settings/src/components/AppStoreDiscover/common.ts
Normal file
65
apps/settings/src/components/AppStoreDiscover/common.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
/**
|
||||
* @copyright Copyright (c) 2024 Ferdinand Thiessen <opensource@fthiessen.de>
|
||||
*
|
||||
* @author Ferdinand Thiessen <opensource@fthiessen.de>
|
||||
*
|
||||
* @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 { PropType } from 'vue'
|
||||
import type { IAppDiscoverElement } from '../../constants/AppDiscoverTypes.ts'
|
||||
|
||||
import { APP_DISCOVER_KNOWN_TYPES } from '../../constants/AppDiscoverTypes.ts'
|
||||
|
||||
/**
|
||||
* Common Props for all app discover types
|
||||
*/
|
||||
export const commonAppDiscoverProps = {
|
||||
type: {
|
||||
type: String as PropType<IAppDiscoverElement['type']>,
|
||||
required: true,
|
||||
validator: (v: unknown) => typeof v === 'string' && APP_DISCOVER_KNOWN_TYPES.includes(v as never),
|
||||
},
|
||||
|
||||
id: {
|
||||
type: String as PropType<IAppDiscoverElement['id']>,
|
||||
required: true,
|
||||
},
|
||||
|
||||
date: {
|
||||
type: Number as PropType<IAppDiscoverElement['date']>,
|
||||
required: false,
|
||||
default: undefined,
|
||||
},
|
||||
|
||||
expiryDate: {
|
||||
type: Number as PropType<IAppDiscoverElement['expiryDate']>,
|
||||
required: false,
|
||||
default: undefined,
|
||||
},
|
||||
|
||||
headline: {
|
||||
type: Object as PropType<IAppDiscoverElement['headline']>,
|
||||
required: false,
|
||||
default: () => null,
|
||||
},
|
||||
|
||||
link: {
|
||||
type: String as PropType<IAppDiscoverElement['link']>,
|
||||
required: false,
|
||||
default: () => null,
|
||||
},
|
||||
} as const
|
||||
47
apps/settings/src/composables/useGetLocalizedValue.ts
Normal file
47
apps/settings/src/composables/useGetLocalizedValue.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
/**
|
||||
* @copyright Copyright (c) 2024 Ferdinand Thiessen <opensource@fthiessen.de>
|
||||
*
|
||||
* @author Ferdinand Thiessen <opensource@fthiessen.de>
|
||||
*
|
||||
* @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 { ILocalizedValue } from '../constants/AppDiscoverTypes'
|
||||
|
||||
import { getLanguage } from '@nextcloud/l10n'
|
||||
import { computed, type Ref } from 'vue'
|
||||
|
||||
/**
|
||||
* Helper to get the localized value for the current users language
|
||||
* @param dict The dictionary to get the value from
|
||||
* @param language The language to use
|
||||
*/
|
||||
const getLocalizedValue = <T, >(dict: ILocalizedValue<T>, language: string) => dict[language] ?? dict[language.split('_')[0]] ?? dict.en ?? null
|
||||
|
||||
/**
|
||||
* Get the localized value of the dictionary provided
|
||||
* @param dict Dictionary
|
||||
* @return String or null if invalid dictionary
|
||||
*/
|
||||
export const useLocalizedValue = <T, >(dict: Ref<ILocalizedValue<T|undefined>|undefined|null>) => {
|
||||
/**
|
||||
* Language of the current user
|
||||
*/
|
||||
const language = getLanguage()
|
||||
|
||||
return computed(() => !dict?.value ? null : getLocalizedValue<T>(dict.value as ILocalizedValue<T>, language))
|
||||
}
|
||||
129
apps/settings/src/constants/AppDiscoverTypes.ts
Normal file
129
apps/settings/src/constants/AppDiscoverTypes.ts
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
/**
|
||||
* @copyright Copyright (c) 2024 Ferdinand Thiessen <opensource@fthiessen.de>
|
||||
*
|
||||
* @author Ferdinand Thiessen <opensource@fthiessen.de>
|
||||
*
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Currently known types of app discover section elements
|
||||
*/
|
||||
export const APP_DISCOVER_KNOWN_TYPES = ['post', 'showcase', 'carousel'] as const
|
||||
|
||||
/**
|
||||
* Helper for localized values
|
||||
*/
|
||||
export type ILocalizedValue<T> = Record<string, T | undefined> & { en: T }
|
||||
|
||||
export interface IAppDiscoverElement {
|
||||
/**
|
||||
* Type of the element
|
||||
*/
|
||||
type: typeof APP_DISCOVER_KNOWN_TYPES[number]
|
||||
|
||||
/**
|
||||
* Identifier for this element
|
||||
*/
|
||||
id: string,
|
||||
|
||||
/**
|
||||
* Optional, localized, headline for the element
|
||||
*/
|
||||
headline?: ILocalizedValue<string>
|
||||
|
||||
/**
|
||||
* Optional link target for the element
|
||||
*/
|
||||
link?: string
|
||||
|
||||
/**
|
||||
* Optional date when this element will get valid (only show since then)
|
||||
*/
|
||||
date?: Date|number
|
||||
|
||||
/**
|
||||
* Optional date when this element will be invalid (only show until then)
|
||||
*/
|
||||
expiryDate?: Date|number
|
||||
}
|
||||
|
||||
/** Wrapper for media source and MIME type */
|
||||
type MediaSource = { src: string, mime: string }
|
||||
|
||||
/**
|
||||
* Media content type for posts
|
||||
*/
|
||||
interface IAppDiscoverMediaContent {
|
||||
/**
|
||||
* The media source to show - either one or a list of sources with their MIME type for fallback options
|
||||
*/
|
||||
src: MediaSource | MediaSource[]
|
||||
|
||||
/**
|
||||
* Alternative text for the media
|
||||
*/
|
||||
alt: string
|
||||
|
||||
/**
|
||||
* Optional link target for the media (e.g. to the full video)
|
||||
*/
|
||||
link?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* An app element only used for the showcase type
|
||||
*/
|
||||
interface IAppDiscoverApp {
|
||||
/** The App ID */
|
||||
type: 'app'
|
||||
app: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for post media
|
||||
*/
|
||||
interface IAppDiscoverMedia {
|
||||
/**
|
||||
* The alignment of the media element
|
||||
*/
|
||||
alignment?: 'start' | 'end' | 'center'
|
||||
|
||||
/**
|
||||
* The (localized) content
|
||||
*/
|
||||
content: ILocalizedValue<IAppDiscoverMediaContent>
|
||||
}
|
||||
|
||||
export interface IAppDiscoverPost extends IAppDiscoverElement {
|
||||
type: 'post'
|
||||
text?: ILocalizedValue<string>
|
||||
media?: IAppDiscoverMedia
|
||||
}
|
||||
|
||||
export interface IAppDiscoverShowcase extends IAppDiscoverElement {
|
||||
type: 'showcase'
|
||||
content: (IAppDiscoverPost | IAppDiscoverApp)[]
|
||||
}
|
||||
|
||||
export interface IAppDiscoverCarousel extends IAppDiscoverElement {
|
||||
type: 'carousel'
|
||||
text?: ILocalizedValue<string>
|
||||
content: IAppDiscoverPost[]
|
||||
}
|
||||
|
||||
export type IAppDiscoverElements = IAppDiscoverPost | IAppDiscoverCarousel | IAppDiscoverShowcase
|
||||
|
|
@ -24,6 +24,7 @@ import { translate as t } from '@nextcloud/l10n'
|
|||
|
||||
/** Enum of verification constants, according to Apps */
|
||||
export const APPS_SECTION_ENUM = Object.freeze({
|
||||
discover: t('settings', 'Discover'),
|
||||
installed: t('settings', 'Your apps'),
|
||||
enabled: t('settings', 'Active apps'),
|
||||
disabled: t('settings', 'Disabled apps'),
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import {
|
|||
mdiOpenInApp,
|
||||
mdiSecurity,
|
||||
mdiStar,
|
||||
mdiStarCircleOutline,
|
||||
mdiStarShooting,
|
||||
mdiTools,
|
||||
mdiViewDashboard,
|
||||
|
|
@ -49,6 +50,7 @@ import {
|
|||
*/
|
||||
export default Object.freeze({
|
||||
// system special categories
|
||||
discover: mdiStarCircleOutline,
|
||||
installed: mdiAccount,
|
||||
enabled: mdiCheck,
|
||||
disabled: mdiClose,
|
||||
|
|
|
|||
47
apps/settings/src/utils/appDiscoverTypeParser.ts
Normal file
47
apps/settings/src/utils/appDiscoverTypeParser.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
/**
|
||||
* @copyright Copyright (c) 2024 Ferdinand Thiessen <opensource@fthiessen.de>
|
||||
*
|
||||
* @author Ferdinand Thiessen <opensource@fthiessen.de>
|
||||
*
|
||||
* @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 { IAppDiscoverCarousel, IAppDiscoverElements, IAppDiscoverPost, IAppDiscoverShowcase } from '../constants/AppDiscoverTypes.ts'
|
||||
|
||||
/**
|
||||
* Helper to transform the JSON API results to proper frontend objects (app discover section elements)
|
||||
*
|
||||
* @param element The JSON API element to transform
|
||||
*/
|
||||
export const apiTypeParser = (element: Record<string, unknown>): IAppDiscoverElements => {
|
||||
const appElement = { ...element }
|
||||
if (appElement.date) {
|
||||
appElement.date = Date.parse(appElement.date as string)
|
||||
}
|
||||
if (appElement.expiryDate) {
|
||||
appElement.expiryDate = Date.parse(appElement.expiryDate as string)
|
||||
}
|
||||
|
||||
if (appElement.type === 'post') {
|
||||
return appElement as unknown as IAppDiscoverPost
|
||||
} else if (appElement.type === 'showcase') {
|
||||
return appElement as unknown as IAppDiscoverShowcase
|
||||
} else if (appElement.type === 'carousel') {
|
||||
return appElement as unknown as IAppDiscoverCarousel
|
||||
}
|
||||
throw new Error(`Invalid argument, app discover element with type ${element.type ?? 'unknown'} is unknown`)
|
||||
}
|
||||
|
|
@ -24,8 +24,11 @@
|
|||
<template>
|
||||
<!-- Apps list -->
|
||||
<NcAppContent class="app-settings-content"
|
||||
:page-heading="pageHeading">
|
||||
<NcEmptyContent v-if="isLoading"
|
||||
:page-heading="appStoreLabel">
|
||||
<h2 class="app-settings-content__label" v-text="viewLabel" />
|
||||
|
||||
<AppStoreDiscoverSection v-if="currentCategory === 'discover'" />
|
||||
<NcEmptyContent v-else-if="isLoading"
|
||||
class="empty-content__loading"
|
||||
:name="t('settings', 'Loading app list')">
|
||||
<template #icon>
|
||||
|
|
@ -38,36 +41,31 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { computed, getCurrentInstance, onBeforeMount, watch } from 'vue'
|
||||
import { computed, getCurrentInstance, onBeforeMount, watchEffect } from 'vue'
|
||||
import { useRoute } from 'vue-router/composables'
|
||||
import { APPS_SECTION_ENUM } from '../constants/AppsConstants.js'
|
||||
|
||||
import { useAppsStore } from '../store/apps-store'
|
||||
import { APPS_SECTION_ENUM } from '../constants/AppsConstants'
|
||||
|
||||
import NcAppContent from '@nextcloud/vue/dist/Components/NcAppContent.js'
|
||||
import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
|
||||
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
|
||||
import AppList from '../components/AppList.vue'
|
||||
import AppStoreDiscoverSection from '../components/AppStoreDiscover/AppStoreDiscoverSection.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const store = useAppsStore()
|
||||
|
||||
/**
|
||||
* ID of the current active category, default is `installed`
|
||||
* ID of the current active category, default is `discover`
|
||||
*/
|
||||
const currentCategory = computed(() => route.params?.category ?? 'installed')
|
||||
const currentCategory = computed(() => route.params?.category ?? 'discover')
|
||||
|
||||
/**
|
||||
* The H1 to be used on the website
|
||||
*/
|
||||
const pageHeading = computed(() => {
|
||||
if (currentCategory.value in APPS_SECTION_ENUM) {
|
||||
return APPS_SECTION_ENUM[currentCategory.value]
|
||||
}
|
||||
const category = store.getCategoryById(currentCategory.value)
|
||||
return category?.displayName ?? t('settings', 'Apps')
|
||||
})
|
||||
watch([pageHeading], () => {
|
||||
window.document.title = `${pageHeading.value} - Apps - Nextcloud`
|
||||
const appStoreLabel = t('settings', 'App Store')
|
||||
const viewLabel = computed(() => APPS_SECTION_ENUM[currentCategory.value] ?? store.getCategoryById(currentCategory.value)?.displayName ?? appStoreLabel)
|
||||
|
||||
watchEffect(() => {
|
||||
window.document.title = `${viewLabel.value} - ${appStoreLabel} - Nextcloud`
|
||||
})
|
||||
|
||||
// TODO this part should be migrated to pinia
|
||||
|
|
@ -87,4 +85,12 @@ onBeforeMount(() => {
|
|||
.empty-content__loading {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.app-settings-content__label {
|
||||
margin-block-start: var(--app-navigation-padding);
|
||||
margin-inline-start: calc(var(--default-clickable-area) + var(--app-navigation-padding) * 2);
|
||||
min-height: var(--default-clickable-area);
|
||||
line-height: var(--default-clickable-area);
|
||||
vertical-align: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -2,9 +2,17 @@
|
|||
<!-- Categories & filters -->
|
||||
<NcAppNavigation :aria-label="t('settings', 'Apps')">
|
||||
<template #list>
|
||||
<NcAppNavigationItem id="app-category-your-apps"
|
||||
<NcAppNavigationItem id="app-category-discover"
|
||||
:to="{ name: 'apps' }"
|
||||
:exact="true"
|
||||
:name="APPS_SECTION_ENUM.discover">
|
||||
<template #icon>
|
||||
<NcIconSvgWrapper :path="APPSTORE_CATEGORY_ICONS.discover" />
|
||||
</template>
|
||||
</NcAppNavigationItem>
|
||||
<NcAppNavigationItem id="app-category-installed"
|
||||
:to="{ name: 'apps-category', params: { category: 'installed'} }"
|
||||
:exact="true"
|
||||
:name="APPS_SECTION_ENUM.installed">
|
||||
<template #icon>
|
||||
<NcIconSvgWrapper :path="APPSTORE_CATEGORY_ICONS.installed" />
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@
|
|||
namespace OCA\Settings\Tests\Controller;
|
||||
|
||||
use OC\App\AppStore\Bundles\BundleFetcher;
|
||||
use OC\App\AppStore\Fetcher\AppDiscoverFetcher;
|
||||
use OC\App\AppStore\Fetcher\AppFetcher;
|
||||
use OC\App\AppStore\Fetcher\CategoryFetcher;
|
||||
use OC\Installer;
|
||||
|
|
@ -38,6 +39,8 @@ use OCP\AppFramework\Http\ContentSecurityPolicy;
|
|||
use OCP\AppFramework\Http\JSONResponse;
|
||||
use OCP\AppFramework\Http\TemplateResponse;
|
||||
use OCP\AppFramework\Services\IInitialState;
|
||||
use OCP\Files\AppData\IAppDataFactory;
|
||||
use OCP\Http\Client\IClientService;
|
||||
use OCP\IConfig;
|
||||
use OCP\IL10N;
|
||||
use OCP\INavigationManager;
|
||||
|
|
@ -84,11 +87,18 @@ class AppSettingsControllerTest extends TestCase {
|
|||
private $logger;
|
||||
/** @var IInitialState|MockObject */
|
||||
private $initialState;
|
||||
/** @var IAppDataFactory|MockObject */
|
||||
private $appDataFactory;
|
||||
/** @var AppDiscoverFetcher|MockObject */
|
||||
private $discoverFetcher;
|
||||
/** @var IClientService|MockObject */
|
||||
private $clientService;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->request = $this->createMock(IRequest::class);
|
||||
$this->appDataFactory = $this->createMock(IAppDataFactory::class);
|
||||
$this->l10n = $this->createMock(IL10N::class);
|
||||
$this->l10n->expects($this->any())
|
||||
->method('t')
|
||||
|
|
@ -104,10 +114,13 @@ class AppSettingsControllerTest extends TestCase {
|
|||
$this->urlGenerator = $this->createMock(IURLGenerator::class);
|
||||
$this->logger = $this->createMock(LoggerInterface::class);
|
||||
$this->initialState = $this->createMock(IInitialState::class);
|
||||
$this->discoverFetcher = $this->createMock(AppDiscoverFetcher::class);
|
||||
$this->clientService = $this->createMock(IClientService::class);
|
||||
|
||||
$this->appSettingsController = new AppSettingsController(
|
||||
'settings',
|
||||
$this->request,
|
||||
$this->appDataFactory,
|
||||
$this->l10n,
|
||||
$this->config,
|
||||
$this->navigationManager,
|
||||
|
|
@ -120,6 +133,8 @@ class AppSettingsControllerTest extends TestCase {
|
|||
$this->urlGenerator,
|
||||
$this->logger,
|
||||
$this->initialState,
|
||||
$this->discoverFetcher,
|
||||
$this->clientService,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ describe('Settings: App management', { testIsolation: true }, () => {
|
|||
// I am logged in as the admin
|
||||
cy.login(admin)
|
||||
// I open the Apps management
|
||||
cy.visit('/settings/apps')
|
||||
cy.visit('/settings/apps/installed')
|
||||
})
|
||||
|
||||
it('Can enable an installed app', () => {
|
||||
|
|
|
|||
4
dist/1359-1359.js
vendored
4
dist/1359-1359.js
vendored
File diff suppressed because one or more lines are too long
2
dist/1359-1359.js.map
vendored
2
dist/1359-1359.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/1439-1439.js
vendored
4
dist/1439-1439.js
vendored
File diff suppressed because one or more lines are too long
2
dist/1439-1439.js.map
vendored
2
dist/1439-1439.js.map
vendored
File diff suppressed because one or more lines are too long
3
dist/8484-8484.js
vendored
Normal file
3
dist/8484-8484.js
vendored
Normal file
File diff suppressed because one or more lines are too long
21
dist/8484-8484.js.LICENSE.txt
vendored
Normal file
21
dist/8484-8484.js.LICENSE.txt
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/**
|
||||
* @copyright Copyright (c) 2024 Ferdinand Thiessen <opensource@fthiessen.de>
|
||||
*
|
||||
* @author Ferdinand Thiessen <opensource@fthiessen.de>
|
||||
*
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
1
dist/8484-8484.js.map
vendored
Normal file
1
dist/8484-8484.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
3
dist/8753-8753.js
vendored
Normal file
3
dist/8753-8753.js
vendored
Normal file
File diff suppressed because one or more lines are too long
21
dist/8753-8753.js.LICENSE.txt
vendored
Normal file
21
dist/8753-8753.js.LICENSE.txt
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/**
|
||||
* @copyright Copyright (c) 2024 Ferdinand Thiessen <opensource@fthiessen.de>
|
||||
*
|
||||
* @author Ferdinand Thiessen <opensource@fthiessen.de>
|
||||
*
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
1
dist/8753-8753.js.map
vendored
Normal file
1
dist/8753-8753.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
4
dist/comments-comments-app.js
vendored
4
dist/comments-comments-app.js
vendored
File diff suppressed because one or more lines are too long
2
dist/comments-comments-app.js.map
vendored
2
dist/comments-comments-app.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/comments-comments-tab.js
vendored
4
dist/comments-comments-tab.js
vendored
File diff suppressed because one or more lines are too long
2
dist/comments-comments-tab.js.map
vendored
2
dist/comments-comments-tab.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/core-legacy-unified-search.js
vendored
4
dist/core-legacy-unified-search.js
vendored
File diff suppressed because one or more lines are too long
2
dist/core-legacy-unified-search.js.map
vendored
2
dist/core-legacy-unified-search.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/core-login.js
vendored
4
dist/core-login.js
vendored
File diff suppressed because one or more lines are too long
2
dist/core-login.js.map
vendored
2
dist/core-login.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/core-main.js
vendored
4
dist/core-main.js
vendored
File diff suppressed because one or more lines are too long
2
dist/core-main.js.map
vendored
2
dist/core-main.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/core-profile.js
vendored
4
dist/core-profile.js
vendored
File diff suppressed because one or more lines are too long
2
dist/core-profile.js.map
vendored
2
dist/core-profile.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/dav-settings-personal-availability.js
vendored
4
dist/dav-settings-personal-availability.js
vendored
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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4
dist/files-init.js
vendored
4
dist/files-init.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files-init.js.map
vendored
2
dist/files-init.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/files-main.js
vendored
4
dist/files-main.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files-main.js.map
vendored
2
dist/files-main.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/files-personal-settings.js
vendored
4
dist/files-personal-settings.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files-personal-settings.js.map
vendored
2
dist/files-personal-settings.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/files-reference-files.js
vendored
4
dist/files-reference-files.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files-reference-files.js.map
vendored
2
dist/files-reference-files.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/files-search.js
vendored
4
dist/files-search.js
vendored
|
|
@ -1,3 +1,3 @@
|
|||
/*! For license information please see files-search.js.LICENSE.txt */
|
||||
(()=>{"use strict";var e,t,r,i={66747:(e,t,r)=>{var i=r(61338),a=r(85168),o=r(99498),n=r(53334);const l=(0,r(53529).YK)().setApp("files").detectUser().build();r(18205),document.addEventListener("DOMContentLoaded",(function(){const e=window.OCA;e.UnifiedSearch&&(l.info("Initializing unified search plugin: folder search from files app"),e.UnifiedSearch.registerFilterAction({id:"files",appId:"files",label:(0,n.Tl)("files","In folder"),icon:(0,o.d0)("files","app.svg"),callback:()=>{(0,a.a1)("Pick plain text files").addMimeTypeFilter("httpd/unix-directory").allowDirectories(!0).addButton({label:"Pick",callback:e=>{l.info("Folder picked",{folder:e[0]});const t=e[0];(0,i.Ic)("nextcloud:unified-search:add-filter",{id:"files",payload:t,filterUpdateText:(0,n.Tl)("files","Search in folder: {folder}",{folder:t.basename}),filterParams:{path:t.path}})}}).build().pick()}}))}))},63710:e=>{e.exports="data:image/svg+xml,%3csvg%20xmlns=%27http://www.w3.org/2000/svg%27%20height=%2716%27%20width=%2716%27%3e%3cpath%20d=%27M14%2012.3L12.3%2014%208%209.7%203.7%2014%202%2012.3%206.3%208%202%203.7%203.7%202%208%206.3%2012.3%202%2014%203.7%209.7%208z%27%20style=%27fill-opacity:1;fill:%23ffffff%27/%3e%3c/svg%3e"},57273:e=>{e.exports="data:image/svg+xml,%3csvg%20xmlns=%27http://www.w3.org/2000/svg%27%20height=%2716%27%20width=%2716%27%3e%3cpath%20d=%27M14%2012.3L12.3%2014%208%209.7%203.7%2014%202%2012.3%206.3%208%202%203.7%203.7%202%208%206.3%2012.3%202%2014%203.7%209.7%208z%27/%3e%3c/svg%3e"}},a={};function o(e){var t=a[e];if(void 0!==t)return t.exports;var r=a[e]={id:e,loaded:!1,exports:{}};return i[e].call(r.exports,r,r.exports,o),r.loaded=!0,r.exports}o.m=i,e=[],o.O=(t,r,i,a)=>{if(!r){var n=1/0;for(s=0;s<e.length;s++){r=e[s][0],i=e[s][1],a=e[s][2];for(var l=!0,d=0;d<r.length;d++)(!1&a||n>=a)&&Object.keys(o.O).every((e=>o.O[e](r[d])))?r.splice(d--,1):(l=!1,a<n&&(n=a));if(l){e.splice(s--,1);var c=i();void 0!==c&&(t=c)}}return t}a=a||0;for(var s=e.length;s>0&&e[s-1][2]>a;s--)e[s]=e[s-1];e[s]=[r,i,a]},o.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return o.d(t,{a:t}),t},o.d=(e,t)=>{for(var r in t)o.o(t,r)&&!o.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},o.f={},o.e=e=>Promise.all(Object.keys(o.f).reduce(((t,r)=>(o.f[r](e,t),t)),[])),o.u=e=>e+"-"+e+".js?v="+{1359:"79a120e5671b1b5ba537",8618:"1e8f15db3b14455fef8f"}[e],o.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),o.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),t={},r="nextcloud:",o.l=(e,i,a,n)=>{if(t[e])t[e].push(i);else{var l,d;if(void 0!==a)for(var c=document.getElementsByTagName("script"),s=0;s<c.length;s++){var f=c[s];if(f.getAttribute("src")==e||f.getAttribute("data-webpack")==r+a){l=f;break}}l||(d=!0,(l=document.createElement("script")).charset="utf-8",l.timeout=120,o.nc&&l.setAttribute("nonce",o.nc),l.setAttribute("data-webpack",r+a),l.src=e),t[e]=[i];var p=(r,i)=>{l.onerror=l.onload=null,clearTimeout(u);var a=t[e];if(delete t[e],l.parentNode&&l.parentNode.removeChild(l),a&&a.forEach((e=>e(i))),r)return r(i)},u=setTimeout(p.bind(null,void 0,{type:"timeout",target:l}),12e4);l.onerror=p.bind(null,l.onerror),l.onload=p.bind(null,l.onload),d&&document.head.appendChild(l)}},o.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},o.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),o.j=2277,(()=>{var e;o.g.importScripts&&(e=o.g.location+"");var t=o.g.document;if(!e&&t&&(t.currentScript&&(e=t.currentScript.src),!e)){var r=t.getElementsByTagName("script");if(r.length)for(var i=r.length-1;i>-1&&(!e||!/^http(s?):/.test(e));)e=r[i--].src}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),o.p=e})(),(()=>{o.b=document.baseURI||self.location.href;var e={2277:0};o.f.j=(t,r)=>{var i=o.o(e,t)?e[t]:void 0;if(0!==i)if(i)r.push(i[2]);else{var a=new Promise(((r,a)=>i=e[t]=[r,a]));r.push(i[2]=a);var n=o.p+o.u(t),l=new Error;o.l(n,(r=>{if(o.o(e,t)&&(0!==(i=e[t])&&(e[t]=void 0),i)){var a=r&&("load"===r.type?"missing":r.type),n=r&&r.target&&r.target.src;l.message="Loading chunk "+t+" failed.\n("+a+": "+n+")",l.name="ChunkLoadError",l.type=a,l.request=n,i[1](l)}}),"chunk-"+t,t)}},o.O.j=t=>0===e[t];var t=(t,r)=>{var i,a,n=r[0],l=r[1],d=r[2],c=0;if(n.some((t=>0!==e[t]))){for(i in l)o.o(l,i)&&(o.m[i]=l[i]);if(d)var s=d(o)}for(t&&t(r);c<n.length;c++)a=n[c],o.o(e,a)&&e[a]&&e[a][0](),e[a]=0;return o.O(s)},r=self.webpackChunknextcloud=self.webpackChunknextcloud||[];r.forEach(t.bind(null,0)),r.push=t.bind(null,r.push.bind(r))})(),o.nc=void 0;var n=o.O(void 0,[4208],(()=>o(66747)));n=o.O(n)})();
|
||||
//# sourceMappingURL=files-search.js.map?v=1938bf213ab3fe94a3eb
|
||||
(()=>{"use strict";var e,t,r,i={66747:(e,t,r)=>{var i=r(61338),o=r(85168),a=r(99498),n=r(53334);const l=(0,r(53529).YK)().setApp("files").detectUser().build();r(18205),document.addEventListener("DOMContentLoaded",(function(){const e=window.OCA;e.UnifiedSearch&&(l.info("Initializing unified search plugin: folder search from files app"),e.UnifiedSearch.registerFilterAction({id:"files",appId:"files",label:(0,n.Tl)("files","In folder"),icon:(0,a.d0)("files","app.svg"),callback:()=>{(0,o.a1)("Pick plain text files").addMimeTypeFilter("httpd/unix-directory").allowDirectories(!0).addButton({label:"Pick",callback:e=>{l.info("Folder picked",{folder:e[0]});const t=e[0];(0,i.Ic)("nextcloud:unified-search:add-filter",{id:"files",payload:t,filterUpdateText:(0,n.Tl)("files","Search in folder: {folder}",{folder:t.basename}),filterParams:{path:t.path}})}}).build().pick()}}))}))},63710:e=>{e.exports="data:image/svg+xml,%3csvg%20xmlns=%27http://www.w3.org/2000/svg%27%20height=%2716%27%20width=%2716%27%3e%3cpath%20d=%27M14%2012.3L12.3%2014%208%209.7%203.7%2014%202%2012.3%206.3%208%202%203.7%203.7%202%208%206.3%2012.3%202%2014%203.7%209.7%208z%27%20style=%27fill-opacity:1;fill:%23ffffff%27/%3e%3c/svg%3e"},57273:e=>{e.exports="data:image/svg+xml,%3csvg%20xmlns=%27http://www.w3.org/2000/svg%27%20height=%2716%27%20width=%2716%27%3e%3cpath%20d=%27M14%2012.3L12.3%2014%208%209.7%203.7%2014%202%2012.3%206.3%208%202%203.7%203.7%202%208%206.3%2012.3%202%2014%203.7%209.7%208z%27/%3e%3c/svg%3e"}},o={};function a(e){var t=o[e];if(void 0!==t)return t.exports;var r=o[e]={id:e,loaded:!1,exports:{}};return i[e].call(r.exports,r,r.exports,a),r.loaded=!0,r.exports}a.m=i,e=[],a.O=(t,r,i,o)=>{if(!r){var n=1/0;for(s=0;s<e.length;s++){r=e[s][0],i=e[s][1],o=e[s][2];for(var l=!0,d=0;d<r.length;d++)(!1&o||n>=o)&&Object.keys(a.O).every((e=>a.O[e](r[d])))?r.splice(d--,1):(l=!1,o<n&&(n=o));if(l){e.splice(s--,1);var c=i();void 0!==c&&(t=c)}}return t}o=o||0;for(var s=e.length;s>0&&e[s-1][2]>o;s--)e[s]=e[s-1];e[s]=[r,i,o]},a.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return a.d(t,{a:t}),t},a.d=(e,t)=>{for(var r in t)a.o(t,r)&&!a.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},a.f={},a.e=e=>Promise.all(Object.keys(a.f).reduce(((t,r)=>(a.f[r](e,t),t)),[])),a.u=e=>e+"-"+e+".js?v="+{1359:"93ce268d719fb104336a",8618:"1e8f15db3b14455fef8f"}[e],a.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),a.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),t={},r="nextcloud:",a.l=(e,i,o,n)=>{if(t[e])t[e].push(i);else{var l,d;if(void 0!==o)for(var c=document.getElementsByTagName("script"),s=0;s<c.length;s++){var f=c[s];if(f.getAttribute("src")==e||f.getAttribute("data-webpack")==r+o){l=f;break}}l||(d=!0,(l=document.createElement("script")).charset="utf-8",l.timeout=120,a.nc&&l.setAttribute("nonce",a.nc),l.setAttribute("data-webpack",r+o),l.src=e),t[e]=[i];var p=(r,i)=>{l.onerror=l.onload=null,clearTimeout(u);var o=t[e];if(delete t[e],l.parentNode&&l.parentNode.removeChild(l),o&&o.forEach((e=>e(i))),r)return r(i)},u=setTimeout(p.bind(null,void 0,{type:"timeout",target:l}),12e4);l.onerror=p.bind(null,l.onerror),l.onload=p.bind(null,l.onload),d&&document.head.appendChild(l)}},a.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),a.j=2277,(()=>{var e;a.g.importScripts&&(e=a.g.location+"");var t=a.g.document;if(!e&&t&&(t.currentScript&&(e=t.currentScript.src),!e)){var r=t.getElementsByTagName("script");if(r.length)for(var i=r.length-1;i>-1&&(!e||!/^http(s?):/.test(e));)e=r[i--].src}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),a.p=e})(),(()=>{a.b=document.baseURI||self.location.href;var e={2277:0};a.f.j=(t,r)=>{var i=a.o(e,t)?e[t]:void 0;if(0!==i)if(i)r.push(i[2]);else{var o=new Promise(((r,o)=>i=e[t]=[r,o]));r.push(i[2]=o);var n=a.p+a.u(t),l=new Error;a.l(n,(r=>{if(a.o(e,t)&&(0!==(i=e[t])&&(e[t]=void 0),i)){var o=r&&("load"===r.type?"missing":r.type),n=r&&r.target&&r.target.src;l.message="Loading chunk "+t+" failed.\n("+o+": "+n+")",l.name="ChunkLoadError",l.type=o,l.request=n,i[1](l)}}),"chunk-"+t,t)}},a.O.j=t=>0===e[t];var t=(t,r)=>{var i,o,n=r[0],l=r[1],d=r[2],c=0;if(n.some((t=>0!==e[t]))){for(i in l)a.o(l,i)&&(a.m[i]=l[i]);if(d)var s=d(a)}for(t&&t(r);c<n.length;c++)o=n[c],a.o(e,o)&&e[o]&&e[o][0](),e[o]=0;return a.O(s)},r=self.webpackChunknextcloud=self.webpackChunknextcloud||[];r.forEach(t.bind(null,0)),r.push=t.bind(null,r.push.bind(r))})(),a.nc=void 0;var n=a.O(void 0,[4208],(()=>a(66747)));n=a.O(n)})();
|
||||
//# sourceMappingURL=files-search.js.map?v=d346b086d2dda45c48a7
|
||||
2
dist/files-search.js.map
vendored
2
dist/files-search.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/files-sidebar.js
vendored
4
dist/files-sidebar.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files-sidebar.js.map
vendored
2
dist/files-sidebar.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/files_external-init.js
vendored
4
dist/files_external-init.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files_external-init.js.map
vendored
2
dist/files_external-init.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/files_reminders-init.js
vendored
4
dist/files_reminders-init.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files_reminders-init.js.map
vendored
2
dist/files_reminders-init.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/files_sharing-files_sharing_tab.js
vendored
4
dist/files_sharing-files_sharing_tab.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files_sharing-files_sharing_tab.js.map
vendored
2
dist/files_sharing-files_sharing_tab.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/files_sharing-personal-settings.js
vendored
4
dist/files_sharing-personal-settings.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files_sharing-personal-settings.js.map
vendored
2
dist/files_sharing-personal-settings.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/files_versions-files_versions.js
vendored
4
dist/files_versions-files_versions.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files_versions-files_versions.js.map
vendored
2
dist/files_versions-files_versions.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/settings-apps-view-4529.js
vendored
4
dist/settings-apps-view-4529.js
vendored
File diff suppressed because one or more lines are too long
2
dist/settings-apps-view-4529.js.map
vendored
2
dist/settings-apps-view-4529.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/settings-declarative-settings-forms.js
vendored
4
dist/settings-declarative-settings-forms.js
vendored
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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4
dist/settings-vue-settings-admin-security.js
vendored
4
dist/settings-vue-settings-admin-security.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4
dist/settings-vue-settings-admin-sharing.js
vendored
4
dist/settings-vue-settings-admin-sharing.js
vendored
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
4
dist/settings-vue-settings-personal-info.js
vendored
4
dist/settings-vue-settings-personal-info.js
vendored
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
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
4
dist/systemtags-admin.js
vendored
4
dist/systemtags-admin.js
vendored
File diff suppressed because one or more lines are too long
2
dist/systemtags-admin.js.map
vendored
2
dist/systemtags-admin.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/theming-admin-theming.js
vendored
4
dist/theming-admin-theming.js
vendored
File diff suppressed because one or more lines are too long
2
dist/theming-admin-theming.js.map
vendored
2
dist/theming-admin-theming.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/theming-personal-theming.js
vendored
4
dist/theming-personal-theming.js
vendored
File diff suppressed because one or more lines are too long
2
dist/theming-personal-theming.js.map
vendored
2
dist/theming-personal-theming.js.map
vendored
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
4
dist/user_status-menu.js
vendored
4
dist/user_status-menu.js
vendored
File diff suppressed because one or more lines are too long
2
dist/user_status-menu.js.map
vendored
2
dist/user_status-menu.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/weather_status-weather-status.js
vendored
4
dist/weather_status-weather-status.js
vendored
File diff suppressed because one or more lines are too long
2
dist/weather_status-weather-status.js.map
vendored
2
dist/weather_status-weather-status.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/workflowengine-workflowengine.js
vendored
4
dist/workflowengine-workflowengine.js
vendored
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue