Apps.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. <!--
  2. - @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
  3. -
  4. - @author Julius Härtl <jus@bitgrid.net>
  5. -
  6. - @license GNU AGPL version 3 or any later version
  7. -
  8. - This program is free software: you can redistribute it and/or modify
  9. - it under the terms of the GNU Affero General Public License as
  10. - published by the Free Software Foundation, either version 3 of the
  11. - License, or (at your option) any later version.
  12. -
  13. - This program is distributed in the hope that it will be useful,
  14. - but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. - GNU Affero General Public License for more details.
  17. -
  18. - You should have received a copy of the GNU Affero General Public License
  19. - along with this program. If not, see <http://www.gnu.org/licenses/>.
  20. -
  21. -->
  22. <template>
  23. <NcContent app-name="settings"
  24. :class="{ 'with-app-sidebar': app}"
  25. :content-class="{ 'icon-loading': loadingList }"
  26. :navigation-class="{ 'icon-loading': loading }">
  27. <!-- Categories & filters -->
  28. <NcAppNavigation>
  29. <template #list>
  30. <NcAppNavigationItem id="app-category-your-apps"
  31. :to="{ name: 'apps' }"
  32. :exact="true"
  33. icon="icon-category-installed"
  34. :name="t('settings', 'Your apps')" />
  35. <NcAppNavigationItem id="app-category-enabled"
  36. :to="{ name: 'apps-category', params: { category: 'enabled' } }"
  37. icon="icon-category-enabled"
  38. :name="$options.APPS_SECTION_ENUM.enabled" />
  39. <NcAppNavigationItem id="app-category-disabled"
  40. :to="{ name: 'apps-category', params: { category: 'disabled' } }"
  41. icon="icon-category-disabled"
  42. :name="$options.APPS_SECTION_ENUM.disabled" />
  43. <NcAppNavigationItem v-if="updateCount > 0"
  44. id="app-category-updates"
  45. :to="{ name: 'apps-category', params: { category: 'updates' } }"
  46. icon="icon-download"
  47. :name="$options.APPS_SECTION_ENUM.updates">
  48. <template #counter>
  49. <NcCounterBubble>{{ updateCount }}</NcCounterBubble>
  50. </template>
  51. </NcAppNavigationItem>
  52. <NcAppNavigationItem v-if="isSubscribed"
  53. id="app-category-supported"
  54. :to="{ name: 'apps-category', params: { category: 'supported' } }"
  55. :name="$options.APPS_SECTION_ENUM.supported">
  56. <template #icon>
  57. <IconStarShooting :size="20" />
  58. </template>
  59. </NcAppNavigationItem>
  60. <NcAppNavigationItem id="app-category-your-bundles"
  61. :to="{ name: 'apps-category', params: { category: 'app-bundles' } }"
  62. icon="icon-category-app-bundles"
  63. :name="$options.APPS_SECTION_ENUM['app-bundles']" />
  64. <NcAppNavigationSpacer />
  65. <!-- App store categories -->
  66. <template v-if="settings.appstoreEnabled">
  67. <NcAppNavigationItem id="app-category-featured"
  68. :to="{ name: 'apps-category', params: { category: 'featured' } }"
  69. icon="icon-favorite"
  70. :name="$options.APPS_SECTION_ENUM.featured" />
  71. <NcAppNavigationItem v-for="cat in categories"
  72. :key="'icon-category-' + cat.ident"
  73. :icon="'icon-category-' + cat.ident"
  74. :to="{
  75. name: 'apps-category',
  76. params: { category: cat.ident },
  77. }"
  78. :name="cat.displayName" />
  79. </template>
  80. <NcAppNavigationItem id="app-developer-docs"
  81. :name="t('settings', 'Developer documentation') + ' ↗'"
  82. @click="openDeveloperDocumentation" />
  83. </template>
  84. </NcAppNavigation>
  85. <!-- Apps list -->
  86. <NcAppContent class="app-settings-content" :class="{ 'icon-loading': loadingList }">
  87. <AppList :category="category" :app="app" :search="searchQuery" />
  88. </NcAppContent>
  89. <!-- Selected app details -->
  90. <NcAppSidebar v-if="id && app"
  91. v-bind="appSidebar"
  92. :class="{'app-sidebar--without-background': !appSidebar.background}"
  93. @close="hideAppDetails">
  94. <template v-if="!appSidebar.background" #header>
  95. <div class="app-sidebar-header__figure--default-app-icon icon-settings-dark" />
  96. </template>
  97. <template #description>
  98. <!-- Featured/Supported badges -->
  99. <div v-if="app.level === 300 || app.level === 200 || hasRating" class="app-level">
  100. <span v-if="app.level === 300"
  101. :title="t('settings', 'This app is supported via your current Nextcloud subscription.')"
  102. class="supported icon-checkmark-color">
  103. {{ t('settings', 'Supported') }}</span>
  104. <span v-if="app.level === 200"
  105. :title="t('settings', 'Featured apps are developed by and within the community. They offer central functionality and are ready for production use.')"
  106. class="official icon-checkmark">
  107. {{ t('settings', 'Featured') }}</span>
  108. <AppScore v-if="hasRating" :score="app.appstoreData.ratingOverall" />
  109. </div>
  110. <div class="app-version">
  111. <p>{{ app.version }}</p>
  112. </div>
  113. </template>
  114. <!-- Tab content -->
  115. <NcAppSidebarTab id="desc"
  116. icon="icon-category-office"
  117. :name="t('settings', 'Details')"
  118. :order="0">
  119. <AppDetails :app="app" />
  120. </NcAppSidebarTab>
  121. <NcAppSidebarTab v-if="app.appstoreData && app.releases[0].translations.en.changelog"
  122. id="desca"
  123. icon="icon-category-organization"
  124. :name="t('settings', 'Changelog')"
  125. :order="1">
  126. <div v-for="release in app.releases" :key="release.version" class="app-sidebar-tabs__release">
  127. <h2>{{ release.version }}</h2>
  128. <Markdown v-if="changelog(release)" :text="changelog(release)" />
  129. </div>
  130. </NcAppSidebarTab>
  131. </NcAppSidebar>
  132. </NcContent>
  133. </template>
  134. <script>
  135. import { subscribe, unsubscribe } from '@nextcloud/event-bus'
  136. import Vue from 'vue'
  137. import VueLocalStorage from 'vue-localstorage'
  138. import NcAppContent from '@nextcloud/vue/dist/Components/NcAppContent.js'
  139. import NcAppNavigation from '@nextcloud/vue/dist/Components/NcAppNavigation.js'
  140. import NcAppNavigationItem from '@nextcloud/vue/dist/Components/NcAppNavigationItem.js'
  141. import NcAppNavigationSpacer from '@nextcloud/vue/dist/Components/NcAppNavigationSpacer.js'
  142. import NcAppSidebar from '@nextcloud/vue/dist/Components/NcAppSidebar.js'
  143. import NcAppSidebarTab from '@nextcloud/vue/dist/Components/NcAppSidebarTab.js'
  144. import NcCounterBubble from '@nextcloud/vue/dist/Components/NcCounterBubble.js'
  145. import NcContent from '@nextcloud/vue/dist/Components/NcContent.js'
  146. import IconStarShooting from 'vue-material-design-icons/StarShooting.vue'
  147. import AppList from '../components/AppList.vue'
  148. import AppDetails from '../components/AppDetails.vue'
  149. import AppManagement from '../mixins/AppManagement.js'
  150. import AppScore from '../components/AppList/AppScore.vue'
  151. import Markdown from '../components/Markdown.vue'
  152. import { APPS_SECTION_ENUM } from './../constants/AppsConstants.js'
  153. Vue.use(VueLocalStorage)
  154. export default {
  155. name: 'Apps',
  156. APPS_SECTION_ENUM,
  157. components: {
  158. NcAppContent,
  159. AppDetails,
  160. AppList,
  161. IconStarShooting,
  162. NcAppNavigation,
  163. NcAppNavigationItem,
  164. NcAppNavigationSpacer,
  165. NcCounterBubble,
  166. AppScore,
  167. NcAppSidebar,
  168. NcAppSidebarTab,
  169. NcContent,
  170. Markdown,
  171. },
  172. mixins: [AppManagement],
  173. props: {
  174. category: {
  175. type: String,
  176. default: 'installed',
  177. },
  178. id: {
  179. type: String,
  180. default: '',
  181. },
  182. },
  183. data() {
  184. return {
  185. searchQuery: '',
  186. screenshotLoaded: false,
  187. }
  188. },
  189. computed: {
  190. loading() {
  191. return this.$store.getters.loading('categories')
  192. },
  193. loadingList() {
  194. return this.$store.getters.loading('list')
  195. },
  196. app() {
  197. return this.apps.find(app => app.id === this.id)
  198. },
  199. categories() {
  200. return this.$store.getters.getCategories
  201. },
  202. apps() {
  203. return this.$store.getters.getAllApps
  204. },
  205. updateCount() {
  206. return this.$store.getters.getUpdateCount
  207. },
  208. settings() {
  209. return this.$store.getters.getServerData
  210. },
  211. hasRating() {
  212. return this.app.appstoreData && this.app.appstoreData.ratingNumOverall > 5
  213. },
  214. // sidebar app binding
  215. appSidebar() {
  216. const authorName = (xmlNode) => {
  217. if (xmlNode['@value']) {
  218. // Complex node (with email or homepage attribute)
  219. return xmlNode['@value']
  220. }
  221. // Simple text node
  222. return xmlNode
  223. }
  224. const author = Array.isArray(this.app.author)
  225. ? this.app.author.map(authorName).join(', ')
  226. : authorName(this.app.author)
  227. const license = t('settings', '{license}-licensed', { license: ('' + this.app.licence).toUpperCase() })
  228. const subname = t('settings', 'by {author}\n{license}', { author, license })
  229. return {
  230. background: this.app.screenshot && this.screenshotLoaded
  231. ? this.app.screenshot
  232. : this.app.preview,
  233. compact: !(this.app.screenshot && this.screenshotLoaded),
  234. name: this.app.name,
  235. subname,
  236. }
  237. },
  238. changelog() {
  239. return (release) => release.translations.en.changelog
  240. },
  241. /**
  242. * Check if the current instance has a support subscription from the Nextcloud GmbH
  243. */
  244. isSubscribed() {
  245. // For customers of the Nextcloud GmbH the app level will be set to `300` for apps that are supported in their subscription
  246. return this.apps.some(app => app.level === 300)
  247. },
  248. },
  249. watch: {
  250. category() {
  251. this.searchQuery = ''
  252. },
  253. app() {
  254. this.screenshotLoaded = false
  255. if (this.app?.releases && this.app?.screenshot) {
  256. const image = new Image()
  257. image.onload = (e) => {
  258. this.screenshotLoaded = true
  259. }
  260. image.src = this.app.screenshot
  261. }
  262. },
  263. },
  264. beforeMount() {
  265. this.$store.dispatch('getCategories', { shouldRefetchCategories: true })
  266. this.$store.dispatch('getAllApps')
  267. this.$store.dispatch('getGroups', { offset: 0, limit: 5 })
  268. this.$store.commit('setUpdateCount', this.$store.getters.getServerData.updateCount)
  269. },
  270. mounted() {
  271. subscribe('nextcloud:unified-search.search', this.setSearch)
  272. subscribe('nextcloud:unified-search.reset', this.resetSearch)
  273. },
  274. beforeDestroy() {
  275. unsubscribe('nextcloud:unified-search.search', this.setSearch)
  276. unsubscribe('nextcloud:unified-search.reset', this.resetSearch)
  277. },
  278. methods: {
  279. setSearch({ query }) {
  280. this.searchQuery = query
  281. },
  282. resetSearch() {
  283. this.searchQuery = ''
  284. },
  285. hideAppDetails() {
  286. this.$router.push({
  287. name: 'apps-category',
  288. params: { category: this.category },
  289. })
  290. },
  291. openDeveloperDocumentation() {
  292. window.open(this.settings.developerDocumentation)
  293. },
  294. },
  295. }
  296. </script>
  297. <style lang="scss" scoped>
  298. .app-sidebar::v-deep {
  299. &:not(.app-sidebar--without-background) {
  300. // with full screenshot, let's fill the figure
  301. :not(.app-sidebar-header--compact) .app-sidebar-header__figure {
  302. background-size: cover;
  303. }
  304. // revert sidebar app icon so it is black
  305. .app-sidebar-header--compact .app-sidebar-header__figure {
  306. background-size: 32px;
  307. filter: var(--background-invert-if-bright);
  308. }
  309. }
  310. .app-sidebar-header__description {
  311. .app-version {
  312. padding-left: 10px;
  313. }
  314. }
  315. // default icon slot styling
  316. &.app-sidebar--without-background {
  317. .app-sidebar-header__figure {
  318. display: flex;
  319. align-items: center;
  320. justify-content: center;
  321. &--default-app-icon {
  322. width: 32px;
  323. height: 32px;
  324. background-size: 32px;
  325. }
  326. }
  327. }
  328. // TODO: migrate to components
  329. .app-sidebar-header__desc {
  330. // allow multi line subtitle for the license
  331. .app-sidebar-header__subtitle {
  332. overflow: visible !important;
  333. height: auto;
  334. white-space: normal !important;
  335. line-height: 16px;
  336. }
  337. }
  338. .app-sidebar-header__action {
  339. // align with tab content
  340. margin: 0 20px;
  341. input {
  342. margin: 3px;
  343. }
  344. }
  345. }
  346. // Align the appNavigation toggle with the apps header toolbar
  347. .app-navigation::v-deep button.app-navigation-toggle {
  348. top: 8px;
  349. right: -8px;
  350. }
  351. .app-sidebar-tabs__release {
  352. h2 {
  353. border-bottom: 1px solid var(--color-border);
  354. }
  355. // Overwrite changelog heading styles
  356. ::v-deep {
  357. h3 {
  358. font-size: 20px;
  359. }
  360. h4 {
  361. font-size: 17px;
  362. }
  363. }
  364. }
  365. </style>