You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

appDetails.vue 9.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  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. <div id="app-details-view" style="padding: 20px;">
  24. <a class="close icon-close" href="#" v-on:click="hideAppDetails"><span class="hidden-visually">Close</span></a>
  25. <h2>
  26. <div v-if="!app.preview" class="icon-settings-dark"></div>
  27. <svg v-if="app.previewAsIcon && app.preview" width="32" height="32" viewBox="0 0 32 32">
  28. <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"></feColorMatrix></filter></defs>
  29. <image x="0" y="0" width="32" height="32" preserveAspectRatio="xMinYMin meet" :filter="filterUrl" :xlink:href="app.preview" class="app-icon"></image>
  30. </svg>
  31. {{ app.name }}</h2>
  32. <img v-if="app.screenshot" :src="app.screenshot" width="100%" />
  33. <div class="app-level" v-if="app.level === 200 || hasRating">
  34. <span class="official icon-checkmark" v-if="app.level === 200"
  35. v-tooltip.auto="t('settings', 'Official apps are developed by and within the community. They offer central functionality and are ready for production use.')">
  36. {{ t('settings', 'Official') }}</span>
  37. <app-score v-if="hasRating" :score="app.appstoreData.ratingOverall"></app-score>
  38. </div>
  39. <div class="app-author" v-if="author">
  40. {{ t('settings', 'by') }}
  41. <span v-for="(a, index) in author">
  42. <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>
  43. </span>
  44. </div>
  45. <div class="app-licence" v-if="licence">{{ licence }}</div>
  46. <div class="actions">
  47. <div class="actions-buttons">
  48. <input v-if="app.update" class="update primary" type="button" :value="t('settings', 'Update to {version}', {version: app.update})" v-on:click="update(app.id)" :disabled="installing || loading(app.id)"/>
  49. <input v-if="app.canUnInstall" class="uninstall" type="button" :value="t('settings', 'Remove')" v-on:click="remove(app.id)" :disabled="installing || loading(app.id)"/>
  50. <input v-if="app.active" class="enable" type="button" :value="t('settings','Disable')" v-on:click="disable(app.id)" :disabled="installing || loading(app.id)" />
  51. <input v-if="!app.active && (app.canInstall || app.isCompatible)" class="enable primary" type="button" :value="enableButtonText" v-on:click="enable(app.id)" v-tooltip.auto="enableButtonTooltip" :disabled="!app.canInstall || installing || loading(app.id)" />
  52. <input v-else-if="!app.active" class="enable force" type="button" :value="forceEnableButtonText" v-on:click="forceEnable(app.id)" v-tooltip.auto="forceEnableButtonTooltip" :disabled="installing || loading(app.id)" />
  53. </div>
  54. <div class="app-groups">
  55. <div class="groups-enable" v-if="app.active && canLimitToGroups(app)">
  56. <input type="checkbox" :value="app.id" v-model="groupCheckedAppsData" v-on:change="setGroupLimit" class="groups-enable__checkbox checkbox" :id="prefix('groups_enable', app.id)">
  57. <label :for="prefix('groups_enable', app.id)">{{ t('settings', 'Limit to groups') }}</label>
  58. <input type="hidden" class="group_select" :title="t('settings', 'All')" value="">
  59. <multiselect v-if="isLimitedToGroups(app)" :options="groups" :value="appGroups" @select="addGroupLimitation" @remove="removeGroupLimitation" :options-limit="5"
  60. :placeholder="t('settings', 'Limit app usage to groups')"
  61. label="name" track-by="id" class="multiselect-vue"
  62. :multiple="true" :close-on-select="false"
  63. @search-change="asyncFindGroup">
  64. <span slot="noResult">{{t('settings', 'No results')}}</span>
  65. </multiselect>
  66. </div>
  67. </div>
  68. </div>
  69. <p class="documentation">
  70. <a class="appslink" :href="appstoreUrl" v-if="!app.internal" target="_blank" rel="noreferrer noopener">{{ t('settings', 'View in store')}} ↗</a>
  71. <a class="appslink" v-if="app.website" :href="app.website" target="_blank" rel="noreferrer noopener">{{ t('settings', 'Visit website') }} ↗</a>
  72. <a class="appslink" v-if="app.bugs" :href="app.bugs" target="_blank" rel="noreferrer noopener">{{ t('settings', 'Report a bug') }} ↗</a>
  73. <a class="appslink" v-if="app.documentation && app.documentation.user" :href="app.documentation.user" target="_blank" rel="noreferrer noopener">{{ t('settings', 'User documentation') }} ↗</a>
  74. <a class="appslink" v-if="app.documentation && app.documentation.admin" :href="app.documentation.admin" target="_blank" rel="noreferrer noopener">{{ t('settings', 'Admin documentation') }} ↗</a>
  75. <a class="appslink" v-if="app.documentation && app.documentation.developer" :href="app.documentation.developer" target="_blank" rel="noreferrer noopener">{{ t('settings', 'Developer documentation') }} ↗</a>
  76. </p>
  77. <ul class="app-dependencies">
  78. <li v-if="app.missingMinOwnCloudVersion">{{ t('settings', 'This app has no minimum Nextcloud version assigned. This will be an error in the future.') }}</li>
  79. <li v-if="app.missingMaxOwnCloudVersion">{{ t('settings', 'This app has no maximum Nextcloud version assigned. This will be an error in the future.') }}</li>
  80. <li v-if="!app.canInstall">
  81. {{ t('settings', 'This app cannot be installed because the following dependencies are not fulfilled:') }}
  82. <ul class="missing-dependencies">
  83. <li v-for="dep in app.missingDependencies">{{ dep }}</li>
  84. </ul>
  85. </li>
  86. </ul>
  87. <div class="app-description" v-html="renderMarkdown"></div>
  88. </div>
  89. </template>
  90. <script>
  91. import Multiselect from 'vue-multiselect';
  92. import marked from 'marked';
  93. import dompurify from 'dompurify'
  94. import AppScore from './appList/appScore';
  95. import AppManagement from './appManagement';
  96. import prefix from './prefixMixin';
  97. import SvgFilterMixin from './svgFilterMixin';
  98. export default {
  99. mixins: [AppManagement, prefix, SvgFilterMixin],
  100. name: 'appDetails',
  101. props: ['category', 'app'],
  102. components: {
  103. Multiselect,
  104. AppScore
  105. },
  106. data() {
  107. return {
  108. groupCheckedAppsData: false,
  109. }
  110. },
  111. mounted() {
  112. if (this.app.groups.length > 0) {
  113. this.groupCheckedAppsData = true;
  114. }
  115. },
  116. methods: {
  117. hideAppDetails() {
  118. this.$router.push({
  119. name: 'apps-category',
  120. params: {category: this.category}
  121. });
  122. },
  123. },
  124. computed: {
  125. appstoreUrl() {
  126. return `https://apps.nextcloud.com/apps/${this.app.id}`;
  127. },
  128. licence() {
  129. if (this.app.licence) {
  130. return t('settings', '{license}-licensed', { license: ('' + this.app.licence).toUpperCase() } );
  131. }
  132. return null;
  133. },
  134. hasRating() {
  135. return this.app.appstoreData && this.app.appstoreData.ratingNumOverall > 5;
  136. },
  137. author() {
  138. if (typeof this.app.author === 'string') {
  139. return [
  140. {
  141. '@value': this.app.author
  142. }
  143. ]
  144. }
  145. if (this.app.author['@value']) {
  146. return [this.app.author];
  147. }
  148. return this.app.author;
  149. },
  150. appGroups() {
  151. return this.app.groups.map(group => {return {id: group, name: group}});
  152. },
  153. groups() {
  154. return this.$store.getters.getGroups
  155. .filter(group => group.id !== 'disabled')
  156. .sort((a, b) => a.name.localeCompare(b.name));
  157. },
  158. renderMarkdown() {
  159. var renderer = new marked.Renderer();
  160. renderer.link = function(href, title, text) {
  161. try {
  162. var prot = decodeURIComponent(unescape(href))
  163. .replace(/[^\w:]/g, '')
  164. .toLowerCase();
  165. } catch (e) {
  166. return '';
  167. }
  168. if (prot.indexOf('http:') !== 0 && prot.indexOf('https:') !== 0) {
  169. return '';
  170. }
  171. var out = '<a href="' + href + '" rel="noreferrer noopener"';
  172. if (title) {
  173. out += ' title="' + title + '"';
  174. }
  175. out += '>' + text + '</a>';
  176. return out;
  177. };
  178. renderer.image = function(href, title, text) {
  179. if (text) {
  180. return text;
  181. }
  182. return title;
  183. };
  184. renderer.blockquote = function(quote) {
  185. return quote;
  186. };
  187. return dompurify.sanitize(
  188. marked(this.app.description.trim(), {
  189. renderer: renderer,
  190. gfm: false,
  191. highlight: false,
  192. tables: false,
  193. breaks: false,
  194. pedantic: false,
  195. sanitize: true,
  196. smartLists: true,
  197. smartypants: false
  198. }),
  199. {
  200. SAFE_FOR_JQUERY: true,
  201. ALLOWED_TAGS: [
  202. 'strong',
  203. 'p',
  204. 'a',
  205. 'ul',
  206. 'ol',
  207. 'li',
  208. 'em',
  209. 'del',
  210. 'blockquote'
  211. ]
  212. }
  213. );
  214. }
  215. }
  216. }
  217. </script>
  218. <style scoped>
  219. .force {
  220. background: var(--color-main-background);
  221. border-color: var(--color-error);
  222. color: var(--color-error);
  223. }
  224. .force:hover,
  225. .force:active {
  226. background: var(--color-error);
  227. border-color: var(--color-error) !important;
  228. color: var(--color-main-background);
  229. }
  230. </style>