diff --git a/apps/settings/lib/Controller/AppSettingsController.php b/apps/settings/lib/Controller/AppSettingsController.php index 95edce40a05..86d0f81aef8 100644 --- a/apps/settings/lib/Controller/AppSettingsController.php +++ b/apps/settings/lib/Controller/AppSettingsController.php @@ -90,7 +90,12 @@ class AppSettingsController extends Controller { $this->initialState->provideInitialState('appstoreDeveloperDocs', $this->urlGenerator->linkToDocs('developer-manual')); $this->initialState->provideInitialState('appstoreUpdateCount', count($this->getAppsWithUpdates())); - $this->provideAppApiState(); + if ($this->appManager->isInstalled('app_api')) { + try { + Server::get(\OCA\AppAPI\Service\ExAppsPageService::class)->provideAppApiState($this->initialState); + } catch (\Psr\Container\NotFoundExceptionInterface|\Psr\Container\ContainerExceptionInterface $e) { + } + } $policy = new ContentSecurityPolicy(); $policy->addAllowedImageDomain('https://usercontent.apps.nextcloud.com'); @@ -104,40 +109,6 @@ class AppSettingsController extends Controller { return $templateResponse; } - /** - * @psalm-suppress UndefinedClass - */ - private function provideAppApiState(): void { - $appApiEnabled = $this->appManager->isInstalled('app_api'); - $this->initialState->provideInitialState('appApiEnabled', $appApiEnabled); - $daemonConfigAccessible = false; - $defaultDaemonConfig = null; - - if ($appApiEnabled) { - $exAppFetcher = Server::get(\OCA\AppAPI\Fetcher\ExAppFetcher::class); - $this->initialState->provideInitialState('appstoreExAppUpdateCount', count($exAppFetcher->getExAppsWithUpdates())); - - $defaultDaemonConfigName = $this->config->getAppValue('app_api', 'default_daemon_config'); - if ($defaultDaemonConfigName !== '') { - $daemonConfigService = Server::get(\OCA\AppAPI\Service\DaemonConfigService::class); - $daemonConfig = $daemonConfigService->getDaemonConfigByName($defaultDaemonConfigName); - if ($daemonConfig !== null) { - $defaultDaemonConfig = $daemonConfig->jsonSerialize(); - unset($defaultDaemonConfig['deploy_config']['haproxy_password']); - $dockerActions = Server::get(\OCA\AppAPI\DeployActions\DockerActions::class); - $dockerActions->initGuzzleClient($daemonConfig); - $daemonConfigAccessible = $dockerActions->ping($dockerActions->buildDockerUrl($daemonConfig)); - if (!$daemonConfigAccessible) { - $this->logger->warning(sprintf('Deploy daemon "%s" is not accessible by Nextcloud. Please verify its configuration', $daemonConfig->getName())); - } - } - } - } - - $this->initialState->provideInitialState('defaultDaemonConfigAccessible', $daemonConfigAccessible); - $this->initialState->provideInitialState('defaultDaemonConfig', $defaultDaemonConfig); - } - /** * Get all active entries for the app discover section */ @@ -475,6 +446,7 @@ class AppSettingsController extends Controller { $formattedApps[] = [ 'id' => $app['id'], + 'app_api' => false, 'name' => $app['translations'][$currentLanguage]['name'] ?? $app['translations']['en']['name'], 'description' => $app['translations'][$currentLanguage]['description'] ?? $app['translations']['en']['description'], 'summary' => $app['translations'][$currentLanguage]['summary'] ?? $app['translations']['en']['summary'], diff --git a/apps/settings/src/app-types.ts b/apps/settings/src/app-types.ts index 3ce2aa1648c..da7a62e47cf 100644 --- a/apps/settings/src/app-types.ts +++ b/apps/settings/src/app-types.ts @@ -41,6 +41,7 @@ export interface IAppstoreApp { preview?: string screenshot?: string + app_api: boolean active: boolean internal: boolean removeable: boolean @@ -48,6 +49,8 @@ export interface IAppstoreApp { canInstall: boolean canUninstall: boolean isCompatible: boolean + needsDownload: boolean + update: string | null appstoreData: Record releases?: IAppstoreAppRelease[] @@ -75,18 +78,18 @@ export interface IDeployDaemon { } export interface IExAppStatus { - action: string, - deploy: number, - deploy_start_time: number, - error: string, - init: number, - init_start_time: number, - type: string, + action: string + deploy: number + deploy_start_time: number + error: string + init: number + init_start_time: number + type: string } export interface IAppstoreExApp extends IAppstoreApp { - daemon: IDeployDaemon, - status: IExAppStatus, - error: string, - app_api: boolean, + daemon: IDeployDaemon | null | undefined + status: IExAppStatus | Record + error: string + removable: boolean } diff --git a/apps/settings/src/components/AppList/AppItem.vue b/apps/settings/src/components/AppList/AppItem.vue index d7c4d1bdc71..ffb17dc958f 100644 --- a/apps/settings/src/components/AppList/AppItem.vue +++ b/apps/settings/src/components/AppList/AppItem.vue @@ -14,8 +14,8 @@ -
- + @@ -79,6 +79,7 @@ {{ t('settings', 'Update to {update}', {update:app.update}) }} @@ -182,6 +183,9 @@ export default { withSidebar() { return !!this.$route.params.id }, + shouldDisplayDefaultIcon() { + return this.listView && !this.app.preview || !this.listView && !this.screenshotLoaded + }, }, watch: { '$route.params.id'(id) { diff --git a/apps/settings/src/components/AppStoreSidebar/AppDeployDaemonTab.vue b/apps/settings/src/components/AppStoreSidebar/AppDeployDaemonTab.vue index 8eb8f943ea4..36087cdd617 100644 --- a/apps/settings/src/components/AppStoreSidebar/AppDeployDaemonTab.vue +++ b/apps/settings/src/components/AppStoreSidebar/AppDeployDaemonTab.vue @@ -12,11 +12,11 @@
-

{{ t('app_api', 'Deploy Daemon') }}

-

{{ t('app_api', 'Type') }}: {{ app?.daemon.accepts_deploy_id }}

-

{{ t('app_api', 'Name') }}: {{ app?.daemon.name }}

-

{{ t('app_api', 'Display Name') }}: {{ app?.daemon.display_name }}

-

{{ t('app_api', 'GPUs support') }}: {{ app?.daemon.deploy_config?.computeDevice?.id !== 'cpu' || 'false' }}

+

{{ t('settings', 'Deploy Daemon') }}

+

{{ t('settings', 'Type') }}: {{ app?.daemon.accepts_deploy_id }}

+

{{ t('settings', 'Name') }}: {{ app?.daemon.name }}

+

{{ t('settings', 'Display Name') }}: {{ app?.daemon.display_name }}

+

{{ t('settings', 'GPUs support') }}: {{ app?.daemon.deploy_config?.computeDevice?.id !== 'cpu' || 'false' }}

diff --git a/apps/settings/src/composables/useAppIcon.ts b/apps/settings/src/composables/useAppIcon.ts index 76efea267a8..60c3ee6e3ea 100644 --- a/apps/settings/src/composables/useAppIcon.ts +++ b/apps/settings/src/composables/useAppIcon.ts @@ -5,7 +5,7 @@ import type { Ref } from 'vue' import type { IAppstoreApp } from '../app-types.ts' -import { mdiCog } from '@mdi/js' +import { mdiCog, mdiCogOutline } from '@mdi/js' import { computed, ref, watchEffect } from 'vue' import AppstoreCategoryIcons from '../constants/AppstoreCategoryIcons.ts' import logger from '../logger.ts' @@ -23,11 +23,17 @@ export function useAppIcon(app: Ref) { * Fallback value if no app icon available */ const categoryIcon = computed(() => { - const path = [app.value?.category ?? []].flat() - .map((name) => AppstoreCategoryIcons[name]) - .filter((icon) => !!icon) - .at(0) - ?? mdiCog + let path: string + if (app.value?.app_api) { + // Use different default icon for ExApps (AppAPI) + path = mdiCogOutline + } else { + path = [app.value?.category ?? []].flat() + .map((name) => AppstoreCategoryIcons[name]) + .filter((icon) => !!icon) + .at(0) + ?? (!app.value?.app_api ? mdiCog : mdiCogOutline) + } return path ? `` : null }) diff --git a/apps/settings/src/mixins/AppManagement.js b/apps/settings/src/mixins/AppManagement.js index 4edfa741d07..6d4b97ea82d 100644 --- a/apps/settings/src/mixins/AppManagement.js +++ b/apps/settings/src/mixins/AppManagement.js @@ -42,26 +42,26 @@ export default { return false }, updateButtonText() { - if (this.app?.daemon?.accepts_deploy_id === 'manual-install') { - return t('app_api', 'manual-install apps cannot be updated') + if (this.app?.app_api && this.app?.daemon?.accepts_deploy_id === 'manual-install') { + return t('settings', 'manual-install apps cannot be updated') } - return '' + return t('settings', 'Update to {version}', { version: this.app?.update }) }, enableButtonText() { if (this.app?.app_api) { - if (this.app && Object.hasOwn(this.app?.status, 'action') && this.app.status.action === 'deploy') { - return t('app_api', '{progress}% Deploying', { progress: this.app.status?.deploy }) + if (this.app && this.app?.status?.action && this.app?.status?.action === 'deploy') { + return t('settings', '{progress}% Deploying …', { progress: this.app?.status?.deploy ?? 0 }) } - if (this.app && Object.hasOwn(this.app?.status, 'action') && this.app.status.action === 'init') { - return t('app_api', '{progress}% Initializing', { progress: this.app.status?.init }) + if (this.app && this.app?.status?.action && this.app?.status?.action === 'init') { + return t('settings', '{progress}% Initializing …', { progress: this.app?.status?.init ?? 0 }) } - if (this.app && Object.hasOwn(this.app?.status, 'action') && this.app.status.action === 'healthcheck') { - return t('app_api', 'Healthchecking') + if (this.app && this.app?.status?.action && this.app?.status?.action === 'healthcheck') { + return t('settings', 'Health checking') } if (this.app.needsDownload) { - return t('app_api', 'Deploy and Enable') + return t('settings', 'Deploy and Enable') } - return t('app_api', 'Enable') + return t('settings', 'Enable') } else { if (this.app.needsDownload) { return t('settings', 'Download and enable') @@ -71,17 +71,17 @@ export default { }, disableButtonText() { if (this.app?.app_api) { - if (this.app && Object.hasOwn(this.app?.status, 'action') && this.app.status.action === 'deploy') { - return t('app_api', '{progress}% Deploying', { progress: this.app.status?.deploy }) + if (this.app && this.app?.status?.action && this.app?.status?.action === 'deploy') { + return t('settings', '{progress}% Deploying …', { progress: this.app?.status?.deploy }) } - if (this.app && Object.hasOwn(this.app?.status, 'action') && this.app.status.action === 'init') { - return t('app_api', '{progress}% Initializing', { progress: this.app.status?.init }) + if (this.app && this.app?.status?.action && this.app?.status?.action === 'init') { + return t('settings', '{progress}% Initializing …', { progress: this.app?.status?.init }) } - if (this.app && Object.hasOwn(this.app?.status, 'action') && this.app.status.action === 'healthcheck') { - return t('app_api', 'Healthchecking') + if (this.app && this.app?.status?.action && this.app?.status?.action === 'healthcheck') { + return t('settings', 'Health checking') } } - return t('app_api', 'Disable') + return t('settings', 'Disable') }, forceEnableButtonText() { if (this.app.needsDownload) { diff --git a/apps/settings/src/store/app_api_apps.js b/apps/settings/src/store/app_api_apps.js index 390f1f4db1a..7470c8c44b1 100644 --- a/apps/settings/src/store/app_api_apps.js +++ b/apps/settings/src/store/app_api_apps.js @@ -25,7 +25,7 @@ const state = { const mutations = { APPS_API_FAILURE(state, error) { - showError(t('app_api', 'An error occurred during the request. Unable to proceed.') + '
' + error.error.response.data.data.message, { isHTML: true }) + showError(t('settings', 'An error occurred during the request. Unable to proceed.') + '
' + error.error.response.data.data.message, { isHTML: true }) console.error(state, error) }, @@ -227,7 +227,7 @@ const actions = { .catch(() => { context.commit('setError', { appId: [appId], - error: t('app_api', 'Error: This app cannot be enabled because it makes the server unstable'), + error: t('settings', 'Error: This app cannot be enabled because it makes the server unstable'), }) }) }) diff --git a/apps/settings/src/store/apps.js b/apps/settings/src/store/apps.js index 7364f96fae2..c58651a3cf5 100644 --- a/apps/settings/src/store/apps.js +++ b/apps/settings/src/store/apps.js @@ -16,7 +16,7 @@ const state = { updateCount: loadState('settings', 'appstoreUpdateCount', 0), loading: {}, gettingCategoriesPromise: null, - appApiEnabled: loadState('settings', 'appApiEnabled'), + appApiEnabled: loadState('settings', 'appApiEnabled', false), } const mutations = { diff --git a/apps/settings/src/views/AppStoreSidebar.vue b/apps/settings/src/views/AppStoreSidebar.vue index bb60e425308..c003174a1d2 100644 --- a/apps/settings/src/views/AppStoreSidebar.vue +++ b/apps/settings/src/views/AppStoreSidebar.vue @@ -84,7 +84,15 @@ const { appIcon } = useAppIcon(app) /** * The second text line shown on the sidebar */ -const licenseText = computed(() => app.value ? t('settings', 'Version {version}, {license}-licensed', { version: app.value.version, license: app.value.licence.toString().toUpperCase() }) : '') +const licenseText = computed(() => { + if (!app.value) { + return '' + } + if (app.value.license !== '') { + return t('settings', 'Version {version}, {license}-licensed', { version: app.value.version, license: app.value.licence.toString().toUpperCase() }) + } + return t('settings', 'Version {version}', { version: app.value.version }) +}) const activeTab = ref('details') watch([app], () => { activeTab.value = 'details' })