Use appsidebar for apps

Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
This commit is contained in:
John Molakvoæ (skjnldsv) 2020-04-09 12:58:05 +02:00
parent 228a96508a
commit 8e7c95effb
No known key found for this signature in database
GPG Key ID: 60C25B8C072916CF
15 changed files with 165 additions and 130 deletions

View File

@ -663,8 +663,6 @@ span.version {
} }
.app-level { .app-level {
margin-top: 8px;
span { span {
color: var(--color-text-maxcontrast); color: var(--color-text-maxcontrast);
background-color: transparent; background-color: transparent;

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

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

View File

@ -21,76 +21,74 @@
--> -->
<template> <template>
<div id="app-details-view" style="padding: 20px;"> <div class="app-details">
<div class="actions"> <div class="app-details__actions">
<div class="actions-buttons"> <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="">
<Multiselect v-if="isLimitedToGroups(app)"
:options="groups"
:value="appGroups"
:options-limit="5"
:placeholder="t('settings', 'Limit app usage to groups')"
label="name"
track-by="id"
class="multiselect-vue"
:multiple="true"
:close-on-select="false"
:tag-width="60"
@select="addGroupLimitation"
@remove="removeGroupLimitation"
@search-change="asyncFindGroup">
<span slot="noResult">{{ t('settings', 'No results') }}</span>
</Multiselect>
</div>
<div class="app-details__actions-manage">
<input v-if="app.update" <input v-if="app.update"
class="update primary" class="update primary"
type="button" type="button"
:value="t('settings', 'Update to {version}', {version: app.update})" :value="t('settings', 'Update to {version}', { version: app.update })"
:disabled="installing || loading(app.id)" :disabled="installing || isLoading"
@click="update(app.id)"> @click="update(app.id)">
<input v-if="app.canUnInstall" <input v-if="app.canUnInstall"
class="uninstall" class="uninstall"
type="button" type="button"
:value="t('settings', 'Remove')" :value="t('settings', 'Remove')"
:disabled="installing || loading(app.id)" :disabled="installing || isLoading"
@click="remove(app.id)"> @click="remove(app.id)">
<input v-if="app.active" <input v-if="app.active"
class="enable" class="enable"
type="button" type="button"
:value="t('settings','Disable')" :value="t('settings','Disable')"
:disabled="installing || loading(app.id)" :disabled="installing || isLoading"
@click="disable(app.id)"> @click="disable(app.id)">
<input v-if="!app.active && (app.canInstall || app.isCompatible)" <input v-if="!app.active && (app.canInstall || app.isCompatible)"
v-tooltip.auto="enableButtonTooltip" v-tooltip.auto="enableButtonTooltip"
class="enable primary" class="enable primary"
type="button" type="button"
:value="enableButtonText" :value="enableButtonText"
:disabled="!app.canInstall || installing || loading(app.id)" :disabled="!app.canInstall || installing || isLoading"
@click="enable(app.id)"> @click="enable(app.id)">
<input v-else-if="!app.active" <input v-else-if="!app.active && !app.canInstall"
v-tooltip.auto="forceEnableButtonTooltip" v-tooltip.auto="forceEnableButtonTooltip"
class="enable force" class="enable force"
type="button" type="button"
:value="forceEnableButtonText" :value="forceEnableButtonText"
:disabled="installing || loading(app.id)" :disabled="installing || isLoading"
@click="forceEnable(app.id)"> @click="forceEnable(app.id)">
</div> </div>
<div class="app-groups">
<div v-if="app.active && canLimitToGroups(app)" class="groups-enable">
<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="">
<Multiselect v-if="isLimitedToGroups(app)"
:options="groups"
:value="appGroups"
:options-limit="5"
:placeholder="t('settings', 'Limit app usage to groups')"
label="name"
track-by="id"
class="multiselect-vue"
:multiple="true"
:close-on-select="false"
:tag-width="60"
@select="addGroupLimitation"
@remove="removeGroupLimitation"
@search-change="asyncFindGroup">
<span slot="noResult">{{ t('settings', 'No results') }}</span>
</Multiselect>
</div>
</div>
</div> </div>
<ul class="app-dependencies"> <ul class="app-details__dependencies">
<li v-if="app.missingMinOwnCloudVersion"> <li v-if="app.missingMinOwnCloudVersion">
{{ t('settings', 'This app has no minimum Nextcloud version assigned. This will be an error in the future.') }} {{ t('settings', 'This app has no minimum Nextcloud version assigned. This will be an error in the future.') }}
</li> </li>
@ -107,7 +105,7 @@
</li> </li>
</ul> </ul>
<p class="documentation"> <p class="app-details__documentation">
<a v-if="!app.internal" <a v-if="!app.internal"
class="appslink" class="appslink"
:href="appstoreUrl" :href="appstoreUrl"
@ -142,7 +140,7 @@
rel="noreferrer noopener">{{ t('settings', 'Developer documentation') }} </a> rel="noreferrer noopener">{{ t('settings', 'Developer documentation') }} </a>
</p> </p>
<div class="app-description" v-html="renderMarkdown" /> <div class="app-details__description" v-html="renderMarkdown" />
</div> </div>
</template> </template>
@ -151,7 +149,6 @@ import { Multiselect } from '@nextcloud/vue'
import marked from 'marked' import marked from 'marked'
import dompurify from 'dompurify' import dompurify from 'dompurify'
import AppScore from './AppList/AppScore'
import AppManagement from '../mixins/AppManagement' import AppManagement from '../mixins/AppManagement'
import PrefixMixin from './PrefixMixin' import PrefixMixin from './PrefixMixin'
@ -160,11 +157,15 @@ export default {
components: { components: {
Multiselect, Multiselect,
AppScore,
}, },
mixins: [AppManagement, PrefixMixin], mixins: [AppManagement, PrefixMixin],
props: ['category', 'app'], props: {
app: {
type: Object,
required: true,
},
},
data() { data() {
return { return {
@ -182,9 +183,6 @@ export default {
} }
return null return null
}, },
hasRating() {
return this.app.appstoreData && this.app.appstoreData.ratingNumOverall > 5
},
author() { author() {
if (typeof this.app.author === 'string') { if (typeof this.app.author === 'string') {
return [ return [
@ -275,16 +273,45 @@ export default {
} }
</script> </script>
<style scoped> <style scoped lang="scss">
.force { .app-details {
background: var(--color-main-background); padding: 20px;
border-color: var(--color-error);
color: var(--color-error); &__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;
}
}
} }
.force:hover, &__dependencies {
.force:active { opacity: .7;
background: var(--color-error);
border-color: var(--color-error) !important;
color: var(--color-main-background);
} }
&__documentation {
padding-top: 20px;
}
&__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> </style>

View File

@ -25,6 +25,7 @@
:class="{ 'with-app-sidebar': app}" :class="{ 'with-app-sidebar': app}"
:content-class="{ 'icon-loading': loadingList }" :content-class="{ 'icon-loading': loadingList }"
:navigation-class="{ 'icon-loading': loading }"> :navigation-class="{ 'icon-loading': loading }">
<!-- Categories & filters -->
<AppNavigation> <AppNavigation>
<template #list> <template #list>
<AppNavigationItem <AppNavigationItem
@ -86,9 +87,13 @@
:title="t('settings', 'Developer documentation') + ' ↗'" /> :title="t('settings', 'Developer documentation') + ' ↗'" />
</template> </template>
</AppNavigation> </AppNavigation>
<!-- Apps list -->
<AppContent class="app-settings-content" :class="{ 'icon-loading': loadingList }"> <AppContent class="app-settings-content" :class="{ 'icon-loading': loadingList }">
<AppList :category="category" :app="app" :search="searchQuery" /> <AppList :category="category" :app="app" :search="searchQuery" />
</AppContent> </AppContent>
<!-- Selected app details -->
<AppSidebar <AppSidebar
v-if="id && app" v-if="id && app"
v-bind="appSidebar" v-bind="appSidebar"
@ -97,7 +102,9 @@
<template v-if="!appSidebar.background" #header> <template v-if="!appSidebar.background" #header>
<div class="app-sidebar-header__figure--default-app-icon icon-settings-dark" /> <div class="app-sidebar-header__figure--default-app-icon icon-settings-dark" />
</template> </template>
<template #primary-actions> <template #primary-actions>
<!-- Featured/Supported badges -->
<div v-if="app.level === 300 || app.level === 200 || hasRating" class="app-level"> <div v-if="app.level === 300 || app.level === 200 || hasRating" class="app-level">
<span v-if="app.level === 300" <span v-if="app.level === 300"
v-tooltip.auto="t('settings', 'This app is supported via your current Nextcloud subscription.')" v-tooltip.auto="t('settings', 'This app is supported via your current Nextcloud subscription.')"
@ -110,47 +117,14 @@
<AppScore v-if="hasRating" :score="app.appstoreData.ratingOverall" /> <AppScore v-if="hasRating" :score="app.appstoreData.ratingOverall" />
</div> </div>
</template> </template>
<template #secondary-actions>
<ActionButton v-if="app.update" <!-- Tab content -->
:disabled="installing || isLoading" <AppDetails :app="app" />
icon="icon-download"
@click="update(app.id)">
{{ t('settings', 'Update to {version}', {version: app.update}) }}
</ActionButton>
<ActionButton v-if="app.canUnInstall"
:disabled="installing || isLoading"
icon="icon-delete"
@click="remove(app.id)">
{{ t('settings', 'Remove') }}
</ActionButton>
<ActionButton v-if="app.active"
:disabled="installing || isLoading"
icon="icon-close"
@click="disable(app.id)">
{{ t('settings','Disable') }}
</ActionButton>
<ActionButton v-if="!app.active && (app.canInstall || app.isCompatible)"
v-tooltip.auto="enableButtonTooltip"
:disabled="!app.canInstall || installing || isLoading"
icon="icon-checkmark"
@click="enable(app.id)">
{{ enableButtonText }}
</ActionButton>
<ActionButton v-else-if="!app.active"
v-tooltip.auto="forceEnableButtonTooltip"
:disabled="installing || isLoading"
icon="icon-checkmark"
@click="forceEnable(app.id)">
{{ forceEnableButtonText }}
</ActionButton>
</template>
<AppDetails :category="category" :app="app" />
</AppSidebar> </AppSidebar>
</Content> </Content>
</template> </template>
<script> <script>
import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
import AppContent from '@nextcloud/vue/dist/Components/AppContent' import AppContent from '@nextcloud/vue/dist/Components/AppContent'
import AppNavigation from '@nextcloud/vue/dist/Components/AppNavigation' import AppNavigation from '@nextcloud/vue/dist/Components/AppNavigation'
import AppNavigationCounter from '@nextcloud/vue/dist/Components/AppNavigationCounter' import AppNavigationCounter from '@nextcloud/vue/dist/Components/AppNavigationCounter'
@ -164,13 +138,14 @@ import VueLocalStorage from 'vue-localstorage'
import AppList from '../components/AppList' import AppList from '../components/AppList'
import AppDetails from '../components/AppDetails' import AppDetails from '../components/AppDetails'
import AppManagement from '../mixins/AppManagement' import AppManagement from '../mixins/AppManagement'
import AppScore from '../components/AppList/AppScore'
Vue.use(VueLocalStorage) Vue.use(VueLocalStorage)
export default { export default {
name: 'Apps', name: 'Apps',
components: { components: {
ActionButton,
AppContent, AppContent,
AppDetails, AppDetails,
AppList, AppList,
@ -178,10 +153,13 @@ export default {
AppNavigationCounter, AppNavigationCounter,
AppNavigationItem, AppNavigationItem,
AppNavigationSpacer, AppNavigationSpacer,
AppScore,
AppSidebar, AppSidebar,
Content, Content,
}, },
mixins: [AppManagement], mixins: [AppManagement],
props: { props: {
category: { category: {
type: String, type: String,
@ -192,11 +170,14 @@ export default {
default: '', default: '',
}, },
}, },
data() { data() {
return { return {
searchQuery: '', searchQuery: '',
screenshotLoaded: false,
} }
}, },
computed: { computed: {
loading() { loading() {
return this.$store.getters.loading('categories') return this.$store.getters.loading('categories')
@ -220,6 +201,10 @@ export default {
return this.$store.getters.getServerData return this.$store.getters.getServerData
}, },
hasRating() {
return this.app.appstoreData && this.app.appstoreData.ratingNumOverall > 5
},
// sidebar app binding // sidebar app binding
appSidebar() { appSidebar() {
const author = Array.isArray(this.app.author) const author = Array.isArray(this.app.author)
@ -235,20 +220,33 @@ export default {
return { return {
subtitle, subtitle,
background: this.app.screenshot background: this.app.screenshot && this.screenshotLoaded
? this.app.screenshot ? this.app.screenshot
: this.app.preview, : this.app.preview,
compact: !this.app.screenshot, compact: !(this.app.screenshot && this.screenshotLoaded),
title: this.app.name, title: this.app.name,
} }
}, },
}, },
watch: { watch: {
category(val, old) { category(val, old) {
this.setSearch('') this.setSearch('')
}, },
app() {
this.screenshotLoaded = false
if (this.app && this.app.screenshot) {
const image = new Image()
image.onload = (e) => {
this.screenshotLoaded = true
}
image.src = this.app.screenshot
}
},
}, },
beforeMount() { beforeMount() {
this.$store.dispatch('getCategories') this.$store.dispatch('getCategories')
this.$store.dispatch('getAllApps') this.$store.dispatch('getAllApps')
@ -261,6 +259,7 @@ export default {
*/ */
this.appSearch = new OCA.Search(this.setSearch, this.resetSearch) this.appSearch = new OCA.Search(this.setSearch, this.resetSearch)
}, },
methods: { methods: {
setSearch(query) { setSearch(query) {
this.searchQuery = query this.searchQuery = query
@ -279,17 +278,17 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.app-sidebar::v-deep {
#app-sidebar::v-deep {
&:not(.app-sidebar--without-background) { &:not(.app-sidebar--without-background) {
// with full screenshot, let's fill the figure // with full screenshot, let's fill the figure
:not(.app-sidebar-header--compact) .app-sidebar-header__figure { :not(.app-sidebar-header--compact) .app-sidebar-header__figure {
background-size: cover background-size: cover;
} }
// revert sidebar app icon so it is black // revert sidebar app icon so it is black
.app-sidebar-header--compact .app-sidebar-header__figure { .app-sidebar-header--compact .app-sidebar-header__figure {
filter: invert(1);
background-size: 32px; background-size: 32px;
filter: invert(1);
} }
} }
@ -300,19 +299,30 @@ export default {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
&--default-app-icon { &--default-app-icon {
height: 32px;
width: 32px; width: 32px;
height: 32px;
background-size: 32px; background-size: 32px;
} }
} }
} }
// allow multi line subtitle for the license // TODO: migrate to components
.app-sidebar-header__subtitle { .app-sidebar-header__desc {
white-space: pre-line !important; // allow multi line subtitle for the license
line-height: 16px; .app-sidebar-header__subtitle {
overflow: visible !important; overflow: visible !important;
height: 22px; height: auto;
white-space: normal !important;
line-height: 16px;
}
}
.app-sidebar-header__action {
// align with tab content
margin: 0 20px;
input {
margin: 3px;
}
} }
} }