feat: Make appstore sidebar tabs standalone components

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
pull/44092/head
Ferdinand Thiessen 3 months ago
parent 289e43a548
commit 84cb04f7c0
No known key found for this signature in database
GPG Key ID: 45FAE7268762B400

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

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

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

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

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

@ -152,7 +152,7 @@ describe('Settings: App management', { testIsolation: true }, () => {
cy.get('#app-sidebar-vue').contains('a', 'View in store').should('exist')
cy.get('#app-sidebar-vue').find('input[type="button"][value="Enable"]').should('be.visible')
cy.get('#app-sidebar-vue').find('input[type="button"][value="Remove"]').should('be.visible')
cy.get('#app-sidebar-vue .app-version').contains(/\d+\.\d+\.\d+/)
cy.get('#app-sidebar-vue').contains(/Version \d+\.\d+\.\d+/).should('be.visible')
})
/*

Loading…
Cancel
Save