diff --git a/apps/settings/css/settings.scss b/apps/settings/css/settings.scss index 7b90a261c9077adedbf6a2d6d86f8081aea5da73..dc9f7b5741604463383e3c4fac843af1a36944ae 100644 --- a/apps/settings/css/settings.scss +++ b/apps/settings/css/settings.scss @@ -663,8 +663,6 @@ span.version { } .app-level { - margin-top: 8px; - span { color: var(--color-text-maxcontrast); background-color: transparent; diff --git a/apps/settings/js/vue-0.js b/apps/settings/js/vue-0.js index 4e5d6cda8e46c2e39e2de6d8925924b77583fae6..499281f8b3bef0b45d50fc81426030bfa5b257ce 100644 Binary files a/apps/settings/js/vue-0.js and b/apps/settings/js/vue-0.js differ diff --git a/apps/settings/js/vue-0.js.map b/apps/settings/js/vue-0.js.map index 642324ff6af4f315310907013b67a4094eb54aca..887fcc153d97c16616d15bee8b2503627c3604d3 100644 Binary files a/apps/settings/js/vue-0.js.map and b/apps/settings/js/vue-0.js.map differ diff --git a/apps/settings/js/vue-5.js b/apps/settings/js/vue-5.js index bd529a1dab503b1548cbf868fc8e5ecd1493bf26..d575663433859cc60fa6643be0f3c20edad18df0 100644 Binary files a/apps/settings/js/vue-5.js and b/apps/settings/js/vue-5.js differ diff --git a/apps/settings/js/vue-5.js.map b/apps/settings/js/vue-5.js.map index 7887add594ffa94c88b93785093f7471bd3f905f..5a0c11b7c700ecd3e082a92956e9a229f59962bf 100644 Binary files a/apps/settings/js/vue-5.js.map and b/apps/settings/js/vue-5.js.map differ diff --git a/apps/settings/js/vue-6.js b/apps/settings/js/vue-6.js index caffe98d91f79400974a19148ebb7f4c85b096d4..6fe5a2dae81ce297bc7c16cb7d52fb5b8e979089 100644 Binary files a/apps/settings/js/vue-6.js and b/apps/settings/js/vue-6.js differ diff --git a/apps/settings/js/vue-6.js.map b/apps/settings/js/vue-6.js.map index a23ad4f22780ffbae6a786ec4d8efb4b52b20732..5fc6c45729727473233a7357a49cd4ba39d5585a 100644 Binary files a/apps/settings/js/vue-6.js.map and b/apps/settings/js/vue-6.js.map differ diff --git a/apps/settings/js/vue-7.js b/apps/settings/js/vue-7.js index c918e823b17f73abeda7611c7667333aed273cba..2bb630b6652aa5406d73836065f38db9f18d1575 100644 Binary files a/apps/settings/js/vue-7.js and b/apps/settings/js/vue-7.js differ diff --git a/apps/settings/js/vue-7.js.map b/apps/settings/js/vue-7.js.map index 4be042dc8ef26c6f58aa3f2c88360c4b6eb62f27..21ce72178afacefe23507df619542cd2330ed921 100644 Binary files a/apps/settings/js/vue-7.js.map and b/apps/settings/js/vue-7.js.map differ diff --git a/apps/settings/js/vue-8.js b/apps/settings/js/vue-8.js index 7c2e4c9b1e84922a268953a75f8ac22aad9772fd..22bf3803fcf9e3522576266f41200ead96ef9b66 100644 Binary files a/apps/settings/js/vue-8.js and b/apps/settings/js/vue-8.js differ diff --git a/apps/settings/js/vue-8.js.map b/apps/settings/js/vue-8.js.map index 8ab4dcc14c7ebfc9e40c4585844c583afd8b2cd1..08f21d56c180537b5b19cdec687a317a13df9179 100644 Binary files a/apps/settings/js/vue-8.js.map and b/apps/settings/js/vue-8.js.map differ diff --git a/apps/settings/js/vue-settings-apps-users-management.js b/apps/settings/js/vue-settings-apps-users-management.js index 54f76e26a40ec8638ed5a5167dec07a75d57e11f..56ac2fe80d3bb3975174f43e28f6b388f188ca6d 100644 Binary files a/apps/settings/js/vue-settings-apps-users-management.js and b/apps/settings/js/vue-settings-apps-users-management.js differ diff --git a/apps/settings/js/vue-settings-apps-users-management.js.map b/apps/settings/js/vue-settings-apps-users-management.js.map index ebed48babe12c0b762d59cc37233a8d998f806fa..fe0b4cddda07dc99ee148ef72f97cacc8e704c07 100644 Binary files a/apps/settings/js/vue-settings-apps-users-management.js.map and b/apps/settings/js/vue-settings-apps-users-management.js.map differ diff --git a/apps/settings/src/components/AppDetails.vue b/apps/settings/src/components/AppDetails.vue index 2c99e9c0c928074c8b91ef9c8a72dfd3b277da0d..55519bf9f8054df9fced182b1b632a2130567b6e 100644 --- a/apps/settings/src/components/AppDetails.vue +++ b/apps/settings/src/components/AppDetails.vue @@ -21,116 +21,74 @@ --> <template> - <div id="app-details-view" style="padding: 20px;"> - <h2> - <div v-if="!app.preview" class="icon-settings-dark" /> - <svg v-if="app.previewAsIcon && app.preview" - width="32" - height="32" - viewBox="0 0 32 32"> - <defs><filter :id="filterId"><feColorMatrix in="SourceGraphic" type="matrix" values="-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 0 0 0 1 0" /></filter></defs> - <image x="0" - y="0" - width="32" - height="32" - preserveAspectRatio="xMinYMin meet" - :filter="filterUrl" - :xlink:href="app.preview" - class="app-icon" /> - </svg> - {{ app.name }} - </h2> - <img v-if="screenshotLoaded" :src="app.screenshot" width="100%"> - <div v-if="app.level === 300 || app.level === 200 || hasRating" class="app-level"> - <span v-if="app.level === 300" - v-tooltip.auto="t('settings', 'This app is supported via your current Nextcloud subscription.')" - class="supported icon-checkmark-color"> - {{ t('settings', 'Supported') }}</span> - <span v-if="app.level === 200" - v-tooltip.auto="t('settings', 'Featured apps are developed by and within the community. They offer central functionality and are ready for production use.')" - class="official icon-checkmark"> - {{ t('settings', 'Featured') }}</span> - <AppScore v-if="hasRating" :score="app.appstoreData.ratingOverall" /> - </div> - - <div v-if="author" class="app-author"> - {{ t('settings', 'by') }} - <span v-for="(a, index) in author" :key="index"> - <a v-if="a['@attributes'] && a['@attributes']['homepage']" :href="a['@attributes']['homepage']">{{ a['@value'] }}</a><span v-else-if="a['@value']">{{ a['@value'] }}</span><span v-else>{{ a }}</span><span v-if="index+1 < author.length">, </span> - </span> - </div> - <div v-if="licence" class="app-licence"> - {{ licence }} - </div> - <div class="actions"> - <div class="actions-buttons"> + <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=""> + <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" class="update primary" type="button" - :value="t('settings', 'Update to {version}', {version: app.update})" - :disabled="installing || loading(app.id)" + :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 || loading(app.id)" + :disabled="installing || isLoading" @click="remove(app.id)"> <input v-if="app.active" class="enable" type="button" :value="t('settings','Disable')" - :disabled="installing || loading(app.id)" + :disabled="installing || isLoading" @click="disable(app.id)"> <input v-if="!app.active && (app.canInstall || app.isCompatible)" v-tooltip.auto="enableButtonTooltip" class="enable primary" type="button" :value="enableButtonText" - :disabled="!app.canInstall || installing || loading(app.id)" + :disabled="!app.canInstall || installing || isLoading" @click="enable(app.id)"> - <input v-else-if="!app.active" + <input v-else-if="!app.active && !app.canInstall" v-tooltip.auto="forceEnableButtonTooltip" class="enable force" type="button" :value="forceEnableButtonText" - :disabled="installing || loading(app.id)" + :disabled="installing || isLoading" @click="forceEnable(app.id)"> </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> - <ul class="app-dependencies"> + <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> @@ -147,7 +105,7 @@ </li> </ul> - <p class="documentation"> + <p class="app-details__documentation"> <a v-if="!app.internal" class="appslink" :href="appstoreUrl" @@ -182,7 +140,7 @@ rel="noreferrer noopener">{{ t('settings', 'Developer documentation') }} ↗</a> </p> - <div class="app-description" v-html="renderMarkdown" /> + <div class="app-details__description" v-html="renderMarkdown" /> </div> </template> @@ -191,25 +149,30 @@ import { Multiselect } from '@nextcloud/vue' import marked from 'marked' import dompurify from 'dompurify' -import AppScore from './AppList/AppScore' -import AppManagement from './AppManagement' +import AppManagement from '../mixins/AppManagement' import PrefixMixin from './PrefixMixin' -import SvgFilterMixin from './SvgFilterMixin' export default { name: 'AppDetails', + components: { Multiselect, - AppScore, }, - mixins: [AppManagement, PrefixMixin, SvgFilterMixin], - props: ['category', 'app'], + mixins: [AppManagement, PrefixMixin], + + props: { + app: { + type: Object, + required: true, + }, + }, + data() { return { groupCheckedAppsData: false, - screenshotLoaded: false, } }, + computed: { appstoreUrl() { return `https://apps.nextcloud.com/apps/${this.app.id}` @@ -220,9 +183,6 @@ export default { } return null }, - hasRating() { - return this.app.appstoreData && this.app.appstoreData.ratingNumOverall > 5 - }, author() { if (typeof this.app.author === 'string') { return [ @@ -309,27 +269,49 @@ export default { if (this.app.groups.length > 0) { this.groupCheckedAppsData = true } - if (this.app.screenshot) { - const image = new Image() - image.onload = (e) => { - this.screenshotLoaded = true - } - image.src = this.app.screenshot - } }, } </script> -<style scoped> - .force { - background: var(--color-main-background); - border-color: var(--color-error); - color: var(--color-error); +<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; } - .force:hover, - .force:active { - 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> diff --git a/apps/settings/src/components/AppList/AppItem.vue b/apps/settings/src/components/AppList/AppItem.vue index eeaa3ae38993dd903f923c747c984059b6cb3126..41ba0d9578ad200c9a4ec9f19e3669049a8e8ec7 100644 --- a/apps/settings/src/components/AppList/AppItem.vue +++ b/apps/settings/src/components/AppList/AppItem.vue @@ -69,38 +69,38 @@ <div v-if="app.error" class="warning"> {{ app.error }} </div> - <div v-if="loading(app.id)" class="icon icon-loading-small" /> + <div v-if="isLoading" class="icon icon-loading-small" /> <input v-if="app.update" class="update primary" type="button" :value="t('settings', 'Update to {update}', {update:app.update})" - :disabled="installing || loading(app.id)" + :disabled="installing || isLoading" @click.stop="update(app.id)"> <input v-if="app.canUnInstall" class="uninstall" type="button" :value="t('settings', 'Remove')" - :disabled="installing || loading(app.id)" + :disabled="installing || isLoading" @click.stop="remove(app.id)"> <input v-if="app.active" class="enable" type="button" :value="t('settings','Disable')" - :disabled="installing || loading(app.id)" + :disabled="installing || isLoading" @click.stop="disable(app.id)"> <input v-if="!app.active && (app.canInstall || app.isCompatible)" v-tooltip.auto="enableButtonTooltip" class="enable" type="button" :value="enableButtonText" - :disabled="!app.canInstall || installing || loading(app.id)" + :disabled="!app.canInstall || installing || isLoading" @click.stop="enable(app.id)"> <input v-else-if="!app.active" v-tooltip.auto="forceEnableButtonTooltip" class="enable force" type="button" :value="forceEnableButtonText" - :disabled="installing || loading(app.id)" + :disabled="installing || isLoading" @click.stop="forceEnable(app.id)"> </div> </div> @@ -108,7 +108,7 @@ <script> import AppScore from './AppScore' -import AppManagement from '../AppManagement' +import AppManagement from '../../mixins/AppManagement' import SvgFilterMixin from '../SvgFilterMixin' export default { diff --git a/apps/settings/src/components/AppManagement.vue b/apps/settings/src/mixins/AppManagement.js similarity index 75% rename from apps/settings/src/components/AppManagement.vue rename to apps/settings/src/mixins/AppManagement.js index 6bf1eee83cf47c4411f8fa985bf9b372fa8e5e5b..0bdb238601d13010af9f3f89fee221d15974f9c1 100644 --- a/apps/settings/src/components/AppManagement.vue +++ b/apps/settings/src/mixins/AppManagement.js @@ -1,40 +1,37 @@ -<!-- - - @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/>. - - - --> +/** + * @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net> + * + * @author Julius Härtl <jus@bitgrid.net> + * @author John Molakvoæ <skjnldsv@protonmail.com> + * + * @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/>. + * + */ -<script> export default { computed: { appGroups() { return this.app.groups.map(group => { return { id: group, name: group } }) }, - loading() { - const self = this - return function(id) { - return self.$store.getters.loading(id) - } - }, installing() { return this.$store.getters.loading('install') }, + isLoading() { + return this.app && this.$store.getters.loading(this.app.id) + }, enableButtonText() { if (this.app.needsDownload) { return t('settings', 'Download and enable') @@ -61,11 +58,19 @@ export default { return base }, }, + + data() { + return { + groupCheckedAppsData: false, + } + }, + mounted() { - if (this.app.groups.length > 0) { + if (this.app && this.app.groups && this.app.groups.length > 0) { this.groupCheckedAppsData = true } }, + methods: { asyncFindGroup(query) { return this.$store.dispatch('getGroups', { search: query, limit: 5, offset: 0 }) @@ -135,4 +140,3 @@ export default { }, }, } -</script> diff --git a/apps/settings/src/views/Apps.vue b/apps/settings/src/views/Apps.vue index 2c7dd9dae0efdb5f312acf3f3e283e0101b37a16..313c58afbd96077768d60a0d12de9614663219e9 100644 --- a/apps/settings/src/views/Apps.vue +++ b/apps/settings/src/views/Apps.vue @@ -22,9 +22,10 @@ <template> <Content app-name="settings" - :class="{ 'with-app-sidebar': currentApp}" + :class="{ 'with-app-sidebar': app}" :content-class="{ 'icon-loading': loadingList }" :navigation-class="{ 'icon-loading': loading }"> + <!-- Categories & filters --> <AppNavigation> <template #list> <AppNavigationItem @@ -86,11 +87,39 @@ :title="t('settings', 'Developer documentation') + ' ↗'" /> </template> </AppNavigation> + + <!-- Apps list --> <AppContent class="app-settings-content" :class="{ 'icon-loading': loadingList }"> - <AppList :category="category" :app="currentApp" :search="searchQuery" /> + <AppList :category="category" :app="app" :search="searchQuery" /> </AppContent> - <AppSidebar v-if="id && currentApp" @close="hideAppDetails"> - <AppDetails :category="category" :app="currentApp" /> + + <!-- Selected app details --> + <AppSidebar + v-if="id && app" + v-bind="appSidebar" + :class="{'app-sidebar--without-background': !appSidebar.background}" + @close="hideAppDetails"> + <template v-if="!appSidebar.background" #header> + <div class="app-sidebar-header__figure--default-app-icon icon-settings-dark" /> + </template> + + <template #primary-actions> + <!-- Featured/Supported badges --> + <div v-if="app.level === 300 || app.level === 200 || hasRating" class="app-level"> + <span v-if="app.level === 300" + v-tooltip.auto="t('settings', 'This app is supported via your current Nextcloud subscription.')" + class="supported icon-checkmark-color"> + {{ t('settings', 'Supported') }}</span> + <span v-if="app.level === 200" + v-tooltip.auto="t('settings', 'Featured apps are developed by and within the community. They offer central functionality and are ready for production use.')" + class="official icon-checkmark"> + {{ t('settings', 'Featured') }}</span> + <AppScore v-if="hasRating" :score="app.appstoreData.ratingOverall" /> + </div> + </template> + + <!-- Tab content --> + <AppDetails :app="app" /> </AppSidebar> </Content> </template> @@ -108,11 +137,14 @@ import VueLocalStorage from 'vue-localstorage' import AppList from '../components/AppList' import AppDetails from '../components/AppDetails' +import AppManagement from '../mixins/AppManagement' +import AppScore from '../components/AppList/AppScore' Vue.use(VueLocalStorage) export default { name: 'Apps', + components: { AppContent, AppDetails, @@ -121,9 +153,13 @@ export default { AppNavigationCounter, AppNavigationItem, AppNavigationSpacer, + AppScore, AppSidebar, Content, }, + + mixins: [AppManagement], + props: { category: { type: String, @@ -134,11 +170,14 @@ export default { default: '', }, }, + data() { return { searchQuery: '', + screenshotLoaded: false, } }, + computed: { loading() { return this.$store.getters.loading('categories') @@ -146,7 +185,7 @@ export default { loadingList() { return this.$store.getters.loading('list') }, - currentApp() { + app() { return this.apps.find(app => app.id === this.id) }, categories() { @@ -161,12 +200,53 @@ export default { settings() { return this.$store.getters.getServerData }, + + hasRating() { + return this.app.appstoreData && this.app.appstoreData.ratingNumOverall > 5 + }, + + // sidebar app binding + appSidebar() { + const author = Array.isArray(this.app.author) + ? this.app.author[0]['@value'] + ? this.app.author.map(author => author['@value']).join(', ') + : this.app.author.join(', ') + : this.app.author['@value'] + ? this.app.author['@value'] + : this.app.author + const license = t('settings', '{license}-licensed', { license: ('' + this.app.licence).toUpperCase() }) + + const subtitle = t('settings', 'by {author}\n{license}', { author, license }) + + return { + subtitle, + background: this.app.screenshot && this.screenshotLoaded + ? this.app.screenshot + : this.app.preview, + compact: !(this.app.screenshot && this.screenshotLoaded), + title: this.app.name, + + } + }, }, + watch: { category(val, old) { 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() { this.$store.dispatch('getCategories') this.$store.dispatch('getAllApps') @@ -179,6 +259,7 @@ export default { */ this.appSearch = new OCA.Search(this.setSearch, this.resetSearch) }, + methods: { setSearch(query) { this.searchQuery = query @@ -195,3 +276,54 @@ export default { }, } </script> + +<style lang="scss" scoped> +.app-sidebar::v-deep { + &:not(.app-sidebar--without-background) { + // with full screenshot, let's fill the figure + :not(.app-sidebar-header--compact) .app-sidebar-header__figure { + background-size: cover; + } + // revert sidebar app icon so it is black + .app-sidebar-header--compact .app-sidebar-header__figure { + background-size: 32px; + + filter: invert(1); + } + } + + // default icon slot styling + &.app-sidebar--without-background { + .app-sidebar-header__figure { + display: flex; + align-items: center; + justify-content: center; + &--default-app-icon { + width: 32px; + height: 32px; + background-size: 32px; + } + } + } + + // TODO: migrate to components + .app-sidebar-header__desc { + // allow multi line subtitle for the license + .app-sidebar-header__subtitle { + overflow: visible !important; + height: auto; + white-space: normal !important; + line-height: 16px; + } + } + + .app-sidebar-header__action { + // align with tab content + margin: 0 20px; + input { + margin: 3px; + } + } +} + +</style>