aboutsummaryrefslogtreecommitdiffstats
path: root/apps
diff options
context:
space:
mode:
authorFerdinand Thiessen <opensource@fthiessen.de>2024-02-22 16:43:45 +0100
committerFerdinand Thiessen <opensource@fthiessen.de>2024-03-11 16:02:34 +0100
commit84cb04f7c068defef353db8823ca8666d85874af (patch)
treedbbe4e5faadf67ae6c9a008d5b96907a83510980 /apps
parent289e43a54858878b0f2b7e5dd0536787351fe52a (diff)
downloadnextcloud-server-84cb04f7c068defef353db8823ca8666d85874af.tar.gz
nextcloud-server-84cb04f7c068defef353db8823ca8666d85874af.zip
feat: Make appstore sidebar tabs standalone components
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
Diffstat (limited to 'apps')
-rw-r--r--apps/settings/src/components/AppDetails.vue262
-rw-r--r--apps/settings/src/components/AppStoreSidebar/AppDescriptionTab.vue55
-rw-r--r--apps/settings/src/components/AppStoreSidebar/AppDetailsTab.vue429
-rw-r--r--apps/settings/src/components/AppStoreSidebar/AppReleasesTab.vue10
-rw-r--r--apps/settings/src/views/AppStoreSidebar.vue146
5 files changed, 635 insertions, 267 deletions
diff --git a/apps/settings/src/components/AppDetails.vue b/apps/settings/src/components/AppDetails.vue
deleted file mode 100644
index c17adc6b230..00000000000
--- a/apps/settings/src/components/AppDetails.vue
+++ /dev/null
@@ -1,262 +0,0 @@
-<!--
- - @copyright Copyright (c) 2018 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="app-details">
- <div class="app-details__actions">
- <div v-if="app.active && canLimitToGroups(app)" class="app-details__actions-groups">
- <input :id="prefix('groups_enable', app.id)"
- v-model="groupCheckedAppsData"
- type="checkbox"
- :value="app.id"
- class="groups-enable__checkbox checkbox"
- @change="setGroupLimit">
- <label :for="prefix('groups_enable', app.id)">{{ t('settings', 'Limit to groups') }}</label>
- <input type="hidden"
- class="group_select"
- :title="t('settings', 'All')"
- value="">
- <br />
- <label for="limitToGroups">
- <span>{{ t('settings', 'Limit app usage to groups') }}</span>
- </label>
- <NcSelect v-if="isLimitedToGroups(app)"
- input-id="limitToGroups"
- :options="groups"
- :value="appGroups"
- :limit="5"
- label="name"
- :multiple="true"
- :close-on-select="false"
- @option:selected="addGroupLimitation"
- @option:deselected="removeGroupLimitation"
- @search="asyncFindGroup">
- <span slot="noResult">{{ t('settings', 'No results') }}</span>
- </NcSelect>
- </div>
- <div class="app-details__actions-manage">
- <input v-if="app.update"
- class="update primary"
- type="button"
- :value="t('settings', 'Update to {version}', { version: app.update })"
- :disabled="installing || isLoading"
- @click="update(app.id)">
- <input v-if="app.canUnInstall"
- class="uninstall"
- type="button"
- :value="t('settings', 'Remove')"
- :disabled="installing || isLoading"
- @click="remove(app.id)">
- <input v-if="app.active"
- class="enable"
- type="button"
- :value="t('settings','Disable')"
- :disabled="installing || isLoading"
- @click="disable(app.id)">
- <input v-if="!app.active && (app.canInstall || app.isCompatible)"
- :title="enableButtonTooltip"
- :aria-label="enableButtonTooltip"
- class="enable primary"
- type="button"
- :value="enableButtonText"
- :disabled="!app.canInstall || installing || isLoading"
- @click="enable(app.id)">
- <input v-else-if="!app.active && !app.canInstall"
- :title="forceEnableButtonTooltip"
- :aria-label="forceEnableButtonTooltip"
- class="enable force"
- type="button"
- :value="forceEnableButtonText"
- :disabled="installing || isLoading"
- @click="forceEnable(app.id)">
- </div>
- </div>
-
- <ul class="app-details__dependencies">
- <li v-if="app.missingMinOwnCloudVersion">
- {{ t('settings', 'This app has no minimum Nextcloud version assigned. This will be an error in the future.') }}
- </li>
- <li v-if="app.missingMaxOwnCloudVersion">
- {{ t('settings', 'This app has no maximum Nextcloud version assigned. This will be an error in the future.') }}
- </li>
- <li v-if="!app.canInstall">
- {{ t('settings', 'This app cannot be installed because the following dependencies are not fulfilled:') }}
- <ul class="missing-dependencies">
- <li v-for="(dep, index) in app.missingDependencies" :key="index">
- {{ dep }}
- </li>
- </ul>
- </li>
- </ul>
-
- <p class="app-details__documentation">
- <a v-if="!app.internal"
- class="appslink"
- :href="appstoreUrl"
- target="_blank"
- rel="noreferrer noopener">{{ t('settings', 'View in store') }} ↗</a>
-
- <a v-if="app.website"
- class="appslink"
- :href="app.website"
- target="_blank"
- rel="noreferrer noopener">{{ t('settings', 'Visit website') }} ↗</a>
- <a v-if="app.bugs"
- class="appslink"
- :href="app.bugs"
- target="_blank"
- rel="noreferrer noopener">{{ t('settings', 'Report a bug') }} ↗</a>
-
- <a v-if="app.documentation && app.documentation.user"
- class="appslink"
- :href="app.documentation.user"
- target="_blank"
- rel="noreferrer noopener">{{ t('settings', 'Usage documentation') }} ↗</a>
- <a v-if="app.documentation && app.documentation.admin"
- class="appslink"
- :href="app.documentation.admin"
- target="_blank"
- rel="noreferrer noopener">{{ t('settings', 'Admin documentation') }} ↗</a>
- <a v-if="app.documentation && app.documentation.developer"
- class="appslink"
- :href="app.documentation.developer"
- target="_blank"
- rel="noreferrer noopener">{{ t('settings', 'Developer documentation') }} ↗</a>
- </p>
- <Markdown class="app-details__description" :min-heading="2" :text="app.description" />
- </div>
-</template>
-
-<script>
-import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js'
-
-import AppManagement from '../mixins/AppManagement.js'
-import PrefixMixin from './PrefixMixin.vue'
-import Markdown from './Markdown.vue'
-
-export default {
- name: 'AppDetails',
-
- components: {
- NcSelect,
- Markdown,
- },
- mixins: [AppManagement, PrefixMixin],
-
- props: {
- app: {
- type: Object,
- required: true,
- },
- },
-
- data() {
- return {
- groupCheckedAppsData: false,
- }
- },
-
- computed: {
- appstoreUrl() {
- return `https://apps.nextcloud.com/apps/${this.app.id}`
- },
- licence() {
- if (this.app.licence) {
- return t('settings', '{license}-licensed', { license: ('' + this.app.licence).toUpperCase() })
- }
- return null
- },
- author() {
- if (typeof this.app.author === 'string') {
- return [
- {
- '@value': this.app.author,
- },
- ]
- }
- if (this.app.author['@value']) {
- return [this.app.author]
- }
- return this.app.author
- },
- appGroups() {
- return this.app.groups.map(group => { return { id: group, name: group } })
- },
- groups() {
- return this.$store.getters.getGroups
- .filter(group => group.id !== 'disabled')
- .sort((a, b) => a.name.localeCompare(b.name))
- },
- },
- mounted() {
- if (this.app.groups.length > 0) {
- this.groupCheckedAppsData = true
- }
- },
-}
-</script>
-
-<style scoped lang="scss">
-.app-details {
- padding: 20px;
-
- &__actions {
- // app management
- &-manage {
- // if too many, shrink them and ellipsis
- display: flex;
- input {
- flex: 0 1 auto;
- min-width: 0;
- text-overflow: ellipsis;
- white-space: nowrap;
- overflow: hidden;
- }
- }
- }
- &__dependencies {
- opacity: .7;
- }
- &__documentation {
- padding-top: 20px;
- a.appslink {
- display: block;
- }
- }
- &__description {
- padding-top: 20px;
- }
-}
-
-.force {
- color: var(--color-error);
- border-color: var(--color-error);
- background: var(--color-main-background);
-}
-.force:hover,
-.force:active {
- color: var(--color-main-background);
- border-color: var(--color-error) !important;
- background: var(--color-error);
-}
-
-</style>
diff --git a/apps/settings/src/components/AppStoreSidebar/AppDescriptionTab.vue b/apps/settings/src/components/AppStoreSidebar/AppDescriptionTab.vue
new file mode 100644
index 00000000000..0466ddf4f4c
--- /dev/null
+++ b/apps/settings/src/components/AppStoreSidebar/AppDescriptionTab.vue
@@ -0,0 +1,55 @@
+<!--
+ - @copyright Copyright (c) 2018 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>
+ <NcAppSidebarTab id="desc"
+ :name="t('settings', 'Description')"
+ :order="0">
+ <template #icon>
+ <NcIconSvgWrapper :path="mdiTextShort" />
+ </template>
+ <div class="app-description">
+ <Markdown :text="app.description" :min-heading="4" />
+ </div>
+ </NcAppSidebarTab>
+</template>
+
+<script setup lang="ts">
+import type { IAppstoreApp } from '../../app-types'
+
+import { mdiTextShort } from '@mdi/js'
+import { translate as t } from '@nextcloud/l10n'
+
+import NcAppSidebarTab from '@nextcloud/vue/dist/Components/NcAppSidebarTab.js'
+import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
+import Markdown from '../Markdown.vue'
+
+defineProps<{
+ app: IAppstoreApp,
+}>()
+</script>
+
+<style scoped lang="scss">
+.app-description {
+ padding: 12px;
+}
+</style>
diff --git a/apps/settings/src/components/AppStoreSidebar/AppDetailsTab.vue b/apps/settings/src/components/AppStoreSidebar/AppDetailsTab.vue
new file mode 100644
index 00000000000..b9fad458300
--- /dev/null
+++ b/apps/settings/src/components/AppStoreSidebar/AppDetailsTab.vue
@@ -0,0 +1,429 @@
+<!--
+ - @copyright Copyright (c) 2018 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>
+ <NcAppSidebarTab id="details"
+ :name="t('settings', 'Details')"
+ :order="1">
+ <template #icon>
+ <NcIconSvgWrapper :path="mdiTextBox" />
+ </template>
+ <div class="app-details">
+ <div class="app-details__actions">
+ <div v-if="app.active && canLimitToGroups(app)" class="app-details__actions-groups">
+ <input :id="`groups_enable_${app.id}`"
+ v-model="groupCheckedAppsData"
+ type="checkbox"
+ :value="app.id"
+ class="groups-enable__checkbox checkbox"
+ @change="setGroupLimit">
+ <label :for="`groups_enable_${app.id}`">{{ t('settings', 'Limit to groups') }}</label>
+ <input type="hidden"
+ class="group_select"
+ :title="t('settings', 'All')"
+ value="">
+ <br>
+ <label for="limitToGroups">
+ <span>{{ t('settings', 'Limit app usage to groups') }}</span>
+ </label>
+ <NcSelect v-if="isLimitedToGroups(app)"
+ input-id="limitToGroups"
+ :options="groups"
+ :value="appGroups"
+ :limit="5"
+ label="name"
+ :multiple="true"
+ :close-on-select="false"
+ @option:selected="addGroupLimitation"
+ @option:deselected="removeGroupLimitation"
+ @search="asyncFindGroup">
+ <span slot="noResult">{{ t('settings', 'No results') }}</span>
+ </NcSelect>
+ </div>
+ <div class="app-details__actions-manage">
+ <input v-if="app.update"
+ class="update primary"
+ type="button"
+ :value="t('settings', 'Update to {version}', { version: app.update })"
+ :disabled="installing || isLoading"
+ @click="update(app.id)">
+ <input v-if="app.canUnInstall"
+ class="uninstall"
+ type="button"
+ :value="t('settings', 'Remove')"
+ :disabled="installing || isLoading"
+ @click="remove(app.id)">
+ <input v-if="app.active"
+ class="enable"
+ type="button"
+ :value="t('settings','Disable')"
+ :disabled="installing || isLoading"
+ @click="disable(app.id)">
+ <input v-if="!app.active && (app.canInstall || app.isCompatible)"
+ :title="enableButtonTooltip"
+ :aria-label="enableButtonTooltip"
+ class="enable primary"
+ type="button"
+ :value="enableButtonText"
+ :disabled="!app.canInstall || installing || isLoading"
+ @click="enable(app.id)">
+ <input v-else-if="!app.active && !app.canInstall"
+ :title="forceEnableButtonTooltip"
+ :aria-label="forceEnableButtonTooltip"
+ class="enable force"
+ type="button"
+ :value="forceEnableButtonText"
+ :disabled="installing || isLoading"
+ @click="forceEnable(app.id)">
+ </div>
+ </div>
+
+ <ul class="app-details__dependencies">
+ <li v-if="app.missingMinOwnCloudVersion">
+ {{ t('settings', 'This app has no minimum Nextcloud version assigned. This will be an error in the future.') }}
+ </li>
+ <li v-if="app.missingMaxOwnCloudVersion">
+ {{ t('settings', 'This app has no maximum Nextcloud version assigned. This will be an error in the future.') }}
+ </li>
+ <li v-if="!app.canInstall">
+ {{ t('settings', 'This app cannot be installed because the following dependencies are not fulfilled:') }}
+ <ul class="missing-dependencies">
+ <li v-for="(dep, index) in app.missingDependencies" :key="index">
+ {{ dep }}
+ </li>
+ </ul>
+ </li>
+ </ul>
+
+ <div v-if="lastModified" class="app-details__section">
+ <h4>
+ {{ t('settings', 'Latest updated') }}
+ </h4>
+ <NcDateTime :timestamp="lastModified" />
+ </div>
+
+ <div class="app-details__section">
+ <h4>
+ {{ t('settings', 'Author') }}
+ </h4>
+ <p class="app-details__authors">
+ {{ appAuthors }}
+ </p>
+ </div>
+
+ <div class="app-details__section">
+ <h4>
+ {{ t('settings', 'Categories') }}
+ </h4>
+ <p>
+ {{ appCategories }}
+ </p>
+ </div>
+
+ <div v-if="externalResources.length > 0" class="app-details__section">
+ <h4>{{ t('settings', 'Resources') }}</h4>
+ <ul class="app-details__documentation" :aria-label="t('settings', 'Documentation')">
+ <li v-for="resource of externalResources" :key="resource.id">
+ <a class="appslink"
+ :href="resource.href"
+ target="_blank"
+ rel="noreferrer noopener">
+ {{ resource.label }} ↗
+ </a>
+ </li>
+ </ul>
+ </div>
+
+ <div class="app-details__section">
+ <h4>{{ t('settings', 'Interact') }}</h4>
+ <div class="app-details__interact">
+ <NcButton :disabled="!app.bugs"
+ :href="app.bugs ?? '#'"
+ :aria-label="t('settings', 'Report a bug')"
+ :title="t('settings', 'Report a bug')">
+ <template #icon>
+ <NcIconSvgWrapper :path="mdiBug" />
+ </template>
+ </NcButton>
+ <NcButton :disabled="!app.bugs"
+ :href="app.bugs ?? '#'"
+ :aria-label="t('settings', 'Request feature')"
+ :title="t('settings', 'Request feature')">
+ <template #icon>
+ <NcIconSvgWrapper :path="mdiFeatureSearch" />
+ </template>
+ </NcButton>
+ <NcButton v-if="app.appstoreData?.discussion"
+ :href="app.appstoreData.discussion"
+ :aria-label="t('settings', 'Ask questions or discuss')"
+ :title="t('settings', 'Ask questions or discuss')">
+ <template #icon>
+ <NcIconSvgWrapper :path="mdiTooltipQuestion" />
+ </template>
+ </NcButton>
+ <NcButton v-if="!app.internal"
+ :href="rateAppUrl"
+ :aria-label="t('settings', 'Rate the app')"
+ :title="t('settings', 'Rate')">
+ <template #icon>
+ <NcIconSvgWrapper :path="mdiStar" />
+ </template>
+ </NcButton>
+ </div>
+ </div>
+ </div>
+ </NcAppSidebarTab>
+</template>
+
+<script>
+import NcAppSidebarTab from '@nextcloud/vue/dist/Components/NcAppSidebarTab.js'
+import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
+import NcDateTime from '@nextcloud/vue/dist/Components/NcDateTime.js'
+import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
+import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js'
+
+import AppManagement from '../../mixins/AppManagement.js'
+import { mdiBug, mdiFeatureSearch, mdiStar, mdiTextBox, mdiTooltipQuestion } from '@mdi/js'
+import { useAppsStore } from '../../store/apps-store'
+
+export default {
+ name: 'AppDetailsTab',
+
+ components: {
+ NcAppSidebarTab,
+ NcButton,
+ NcDateTime,
+ NcIconSvgWrapper,
+ NcSelect,
+ },
+ mixins: [AppManagement],
+
+ props: {
+ app: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ setup() {
+ const store = useAppsStore()
+
+ return {
+ store,
+
+ mdiBug,
+ mdiFeatureSearch,
+ mdiStar,
+ mdiTextBox,
+ mdiTooltipQuestion,
+ }
+ },
+
+ data() {
+ return {
+ groupCheckedAppsData: false,
+ }
+ },
+
+ computed: {
+ lastModified() {
+ return (this.app.appstoreData?.releases ?? [])
+ .map(({ lastModified }) => Date.parse(lastModified))
+ .sort()
+ .at(0) ?? null
+ },
+ /**
+ * App authors as comma separated string
+ */
+ appAuthors() {
+ console.warn(this.app)
+ if (!this.app) {
+ return ''
+ }
+
+ const authorName = (xmlNode) => {
+ if (xmlNode['@value']) {
+ // Complex node (with email or homepage attribute)
+ return xmlNode['@value']
+ }
+ // Simple text node
+ return xmlNode
+ }
+
+ const authors = Array.isArray(this.app.author)
+ ? this.app.author.map(authorName)
+ : [authorName(this.app.author)]
+
+ return authors
+ .sort((a, b) => a.split(' ').at(-1).localeCompare(b.split(' ').at(-1)))
+ .join(', ')
+ },
+
+ appstoreUrl() {
+ return `https://apps.nextcloud.com/apps/${this.app.id}`
+ },
+
+ /**
+ * Further external resources (e.g. website)
+ */
+ externalResources() {
+ const resources = []
+ if (!this.app.internal) {
+ resources.push({
+ id: 'appstore',
+ href: this.appstoreUrl,
+ label: t('settings', 'View in store'),
+ })
+ }
+ if (this.app.website) {
+ resources.push({
+ id: 'website',
+ href: this.app.website,
+ label: t('settings', 'Visit website'),
+ })
+ }
+ if (this.app.documentation) {
+ if (this.app.documentation.user) {
+ resources.push({
+ id: 'doc-user',
+ href: this.app.documentation.user,
+ label: t('settings', 'Usage documentation'),
+ })
+ }
+ if (this.app.documentation.admin) {
+ resources.push({
+ id: 'doc-admin',
+ href: this.app.documentation.admin,
+ label: t('settings', 'Admin documentation'),
+ })
+ }
+ if (this.app.documentation.developer) {
+ resources.push({
+ id: 'doc-developer',
+ href: this.app.documentation.developer,
+ label: t('settings', 'Developer documentation'),
+ })
+ }
+ }
+ return resources
+ },
+
+ appCategories() {
+ return [this.app.category].flat()
+ .map((id) => this.store.getCategoryById(id)?.displayName ?? id)
+ .join(', ')
+ },
+
+ rateAppUrl() {
+ return `${this.appstoreUrl}#comments`
+ },
+ appGroups() {
+ return this.app.groups.map(group => { return { id: group, name: group } })
+ },
+ groups() {
+ return this.$store.getters.getGroups
+ .filter(group => group.id !== 'disabled')
+ .sort((a, b) => a.name.localeCompare(b.name))
+ },
+ },
+ mounted() {
+ if (this.app.groups.length > 0) {
+ this.groupCheckedAppsData = true
+ }
+ },
+}
+</script>
+
+<style scoped lang="scss">
+.app-details {
+ padding: 20px;
+
+ &__actions {
+ // app management
+ &-manage {
+ // if too many, shrink them and ellipsis
+ display: flex;
+ input {
+ flex: 0 1 auto;
+ min-width: 0;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+ }
+ }
+ }
+ &__authors {
+ color: var(--color-text-maxcontrast);
+ }
+
+ &__section {
+ margin-top: 15px;
+
+ h4 {
+ font-size: 16px;
+ font-weight: bold;
+ margin-block-end: 5px;
+ }
+ }
+
+ &__interact {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ gap: 12px;
+ }
+
+ &__documentation {
+ a {
+ text-decoration: underline;
+ }
+ li {
+ padding-inline-start: 20px;
+
+ &::before {
+ width: 5px;
+ height: 5px;
+ border-radius: 100%;
+ background-color: var(--color-main-text);
+ content: "";
+ float: inline-start;
+ margin-inline-start: -13px;
+ position: relative;
+ top: 10px;
+ }
+ }
+ }
+}
+
+.force {
+ color: var(--color-error);
+ border-color: var(--color-error);
+ background: var(--color-main-background);
+}
+.force:hover,
+.force:active {
+ color: var(--color-main-background);
+ border-color: var(--color-error) !important;
+ background: var(--color-error);
+}
+
+</style>
diff --git a/apps/settings/src/components/AppStoreSidebar/AppReleasesTab.vue b/apps/settings/src/components/AppStoreSidebar/AppReleasesTab.vue
index cd520220337..8fe89221170 100644
--- a/apps/settings/src/components/AppStoreSidebar/AppReleasesTab.vue
+++ b/apps/settings/src/components/AppStoreSidebar/AppReleasesTab.vue
@@ -22,15 +22,16 @@
-->
<template>
<NcAppSidebarTab v-if="hasChangelog"
- id="desca"
+ id="changelog"
:name="t('settings', 'Changelog')"
- :order="1">
+ :order="2">
<template #icon>
<NcIconSvgWrapper :path="mdiClockFast" :size="24" />
</template>
<div v-for="release in app.releases" :key="release.version" class="app-sidebar-tabs__release">
<h2>{{ release.version }}</h2>
- <Markdown class="app-sidebar-tabs__release-text" :text="createChangelogFromRelease(release)" />
+ <Markdown class="app-sidebar-tabs__release-text"
+ :text="createChangelogFromRelease(release)" />
</div>
</NcAppSidebarTab>
</template>
@@ -40,16 +41,15 @@ import type { IAppstoreApp, IAppstoreAppRelease } from '../../app-types.ts'
import { mdiClockFast } from '@mdi/js'
import { getLanguage, translate as t } from '@nextcloud/l10n'
+import { computed } from 'vue'
import NcAppSidebarTab from '@nextcloud/vue/dist/Components/NcAppSidebarTab.js'
import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
import Markdown from '../Markdown.vue'
-import { computed, watch } from 'vue'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const props = defineProps<{ app: IAppstoreApp }>()
-watch([props], () => console.warn(props.app.releases))
const hasChangelog = computed(() => Object.values(props.app.releases[0]?.translations ?? {}).some(({ changelog }) => !!changelog))
const createChangelogFromRelease = (release: IAppstoreAppRelease) => release.translations?.[getLanguage()]?.changelog ?? release.translations?.en?.changelog ?? ''
diff --git a/apps/settings/src/views/AppStoreSidebar.vue b/apps/settings/src/views/AppStoreSidebar.vue
new file mode 100644
index 00000000000..dda9afd21f8
--- /dev/null
+++ b/apps/settings/src/views/AppStoreSidebar.vue
@@ -0,0 +1,146 @@
+<!--
+ - @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
+ -
+ - @author Julius Härtl <jus@bitgrid.net>
+ - @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>
+ <!-- Selected app details -->
+ <NcAppSidebar v-if="showSidebar"
+ class="app-sidebar"
+ :class="{ 'app-sidebar--with-screenshot': hasScreenshot }"
+ :background="hasScreenshot ? app.screenshot : undefined"
+ :compact="!hasScreenshot"
+ :name="app.name"
+ :title="app.name"
+ :subname="licenseText"
+ :subtitle="licenseText"
+ @close="hideAppDetails">
+ <!-- Fallback icon incase no app icon is available -->
+ <template v-if="!hasScreenshot" #header>
+ <NcIconSvgWrapper class="app-sidebar__fallback-icon"
+ :svg="appIcon ?? ''"
+ :size="64" />
+ </template>
+
+ <template #description>
+ <!-- Featured/Supported badges -->
+ <div class="app-sidebar__badges">
+ <AppLevelBadge :level="app.level" />
+ <AppScore v-if="hasRating" :score="rating" />
+ </div>
+ </template>
+
+ <!-- Tab content -->
+ <AppDescriptionTab :app="app" />
+ <AppDetailsTab :app="app" />
+ <AppReleasesTab :app="app" />
+ </NcAppSidebar>
+</template>
+
+<script setup lang="ts">
+import { translate as t } from '@nextcloud/l10n'
+import { computed, onMounted, ref, watch } from 'vue'
+import { useRoute, useRouter } from 'vue-router/composables'
+import { useAppsStore } from '../store/apps-store'
+
+import NcAppSidebar from '@nextcloud/vue/dist/Components/NcAppSidebar.js'
+import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
+import AppScore from '../components/AppList/AppScore.vue'
+import AppDescriptionTab from '../components/AppStoreSidebar/AppDescriptionTab.vue'
+import AppDetailsTab from '../components/AppStoreSidebar/AppDetailsTab.vue'
+import AppReleasesTab from '../components/AppStoreSidebar/AppReleasesTab.vue'
+import AppLevelBadge from '../components/AppList/AppLevelBadge.vue'
+import { useAppIcon } from '../composables/useAppIcon.ts'
+
+const route = useRoute()
+const router = useRouter()
+const store = useAppsStore()
+
+const appId = computed(() => route.params.id ?? '')
+const app = computed(() => store.getAppById(appId.value)!)
+const hasRating = computed(() => app.value.appstoreData?.ratingNumOverall > 5)
+const rating = computed(() => app.value.appstoreData?.ratingNumRecent > 5
+ ? app.value.appstoreData.ratingRecent
+ : (app.value.appstoreData?.ratingOverall ?? 0.5))
+const showSidebar = computed(() => app.value)
+
+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() }) : '')
+
+/**
+ * Hide the details sidebar by pushing a new route
+ */
+const hideAppDetails = () => router.push({
+ name: 'apps-category',
+ params: { category: route.params.category },
+})
+
+/**
+ * Whether the app screenshot is loaded
+ */
+const screenshotLoaded = ref(false)
+const hasScreenshot = computed(() => app.value?.screenshot && screenshotLoaded.value)
+/**
+ * Preload the app screenshot
+ */
+const loadScreenshot = () => {
+ if (app.value?.releases && app.value?.screenshot) {
+ const image = new Image()
+ image.onload = () => {
+ screenshotLoaded.value = true
+ }
+ image.src = app.value.screenshot
+ }
+}
+// Watch app and set screenshot loaded when
+watch([app], loadScreenshot)
+onMounted(loadScreenshot)
+</script>
+
+<style scoped lang="scss">
+.app-sidebar {
+ // If a screenshot is available it should cover the whole figure
+ &--with-screenshot {
+ :deep(.app-sidebar-header__figure) {
+ background-size: cover;
+ }
+ }
+
+ &__fallback-icon {
+ // both 100% to center the icon
+ width: 100%;
+ height: 100%;
+ }
+
+ &__badges {
+ display: flex;
+ flex-direction: row;
+ gap: 12px;
+ }
+
+ &__version {
+ color: var(--color-text-maxcontrast);
+ }
+}
+</style>