Apps.vue 11 KB

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