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.

AdminTheming.vue 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. <!--
  2. - SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
  3. - SPDX-License-Identifier: AGPL-3.0-or-later
  4. -->
  5. <template>
  6. <section>
  7. <NcSettingsSection :name="t('theming', 'Theming')"
  8. :description="t('theming', 'Theming makes it possible to easily customize the look and feel of your instance and supported clients. This will be visible for all users.')"
  9. :doc-url="docUrl"
  10. data-admin-theming-settings>
  11. <div class="admin-theming">
  12. <NcNoteCard v-if="!isThemable"
  13. type="error"
  14. :show-alert="true">
  15. <p>{{ notThemableErrorMessage }}</p>
  16. </NcNoteCard>
  17. <!-- Name, web link, slogan... fields -->
  18. <TextField v-for="field in textFields"
  19. :key="field.name"
  20. :data-admin-theming-setting-field="field.name"
  21. :default-value="field.defaultValue"
  22. :display-name="field.displayName"
  23. :maxlength="field.maxlength"
  24. :name="field.name"
  25. :placeholder="field.placeholder"
  26. :type="field.type"
  27. :value.sync="field.value"
  28. @update:theming="refreshStyles" />
  29. <!-- Primary color picker -->
  30. <ColorPickerField :name="primaryColorPickerField.name"
  31. :description="primaryColorPickerField.description"
  32. :default-value="primaryColorPickerField.defaultValue"
  33. :display-name="primaryColorPickerField.displayName"
  34. :value.sync="primaryColorPickerField.value"
  35. data-admin-theming-setting-primary-color
  36. @update:theming="refreshStyles" />
  37. <!-- Background color picker -->
  38. <ColorPickerField name="background_color"
  39. :description="t('theming', 'Instead of a background image you can also configure a plain background color. If you use a background image changing this color will influence the color of the app menu icons.')"
  40. :default-value.sync="defaultBackgroundColor"
  41. :display-name="t('theming', 'Background color')"
  42. :value.sync="backgroundColor"
  43. data-admin-theming-setting-background-color
  44. @update:theming="refreshStyles" />
  45. <!-- Default background picker -->
  46. <FileInputField :aria-label="t('theming', 'Upload new logo')"
  47. data-admin-theming-setting-file="logo"
  48. :display-name="t('theming', 'Logo')"
  49. mime-name="logoMime"
  50. :mime-value.sync="logoMime"
  51. name="logo"
  52. @update:theming="refreshStyles" />
  53. <FileInputField :aria-label="t('theming', 'Upload new background and login image')"
  54. data-admin-theming-setting-file="background"
  55. :display-name="t('theming', 'Background and login image')"
  56. mime-name="backgroundMime"
  57. :mime-value.sync="backgroundMime"
  58. name="background"
  59. @uploaded="backgroundURL = $event"
  60. @update:theming="refreshStyles" />
  61. <div class="admin-theming__preview" data-admin-theming-preview>
  62. <div class="admin-theming__preview-logo" data-admin-theming-preview-logo />
  63. </div>
  64. </div>
  65. </NcSettingsSection>
  66. <NcSettingsSection :name="t('theming', 'Advanced options')">
  67. <div class="admin-theming-advanced">
  68. <TextField v-for="field in advancedTextFields"
  69. :key="field.name"
  70. :name="field.name"
  71. :value.sync="field.value"
  72. :default-value="field.defaultValue"
  73. :type="field.type"
  74. :display-name="field.displayName"
  75. :placeholder="field.placeholder"
  76. :maxlength="field.maxlength"
  77. @update:theming="refreshStyles" />
  78. <FileInputField v-for="field in advancedFileInputFields"
  79. :key="field.name"
  80. :name="field.name"
  81. :mime-name="field.mimeName"
  82. :mime-value.sync="field.mimeValue"
  83. :default-mime-value="field.defaultMimeValue"
  84. :display-name="field.displayName"
  85. :aria-label="field.ariaLabel"
  86. @update:theming="refreshStyles" />
  87. <CheckboxField :name="userThemingField.name"
  88. :value="userThemingField.value"
  89. :default-value="userThemingField.defaultValue"
  90. :display-name="userThemingField.displayName"
  91. :label="userThemingField.label"
  92. :description="userThemingField.description"
  93. data-admin-theming-setting-disable-user-theming
  94. @update:theming="refreshStyles" />
  95. <a v-if="!canThemeIcons"
  96. :href="docUrlIcons"
  97. rel="noreferrer noopener">
  98. <em>{{ t('theming', 'Install the ImageMagick PHP extension with support for SVG images to automatically generate favicons based on the uploaded logo and color.') }}</em>
  99. </a>
  100. </div>
  101. </NcSettingsSection>
  102. <AppMenuSection :default-apps.sync="defaultApps" />
  103. </section>
  104. </template>
  105. <script>
  106. import { loadState } from '@nextcloud/initial-state'
  107. import { refreshStyles } from './helpers/refreshStyles.js'
  108. import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
  109. import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js'
  110. import CheckboxField from './components/admin/CheckboxField.vue'
  111. import ColorPickerField from './components/admin/ColorPickerField.vue'
  112. import FileInputField from './components/admin/FileInputField.vue'
  113. import TextField from './components/admin/TextField.vue'
  114. import AppMenuSection from './components/admin/AppMenuSection.vue'
  115. const {
  116. defaultBackgroundURL,
  117. backgroundMime,
  118. backgroundURL,
  119. backgroundColor,
  120. canThemeIcons,
  121. docUrl,
  122. docUrlIcons,
  123. faviconMime,
  124. isThemable,
  125. legalNoticeUrl,
  126. logoheaderMime,
  127. logoMime,
  128. name,
  129. notThemableErrorMessage,
  130. primaryColor,
  131. privacyPolicyUrl,
  132. slogan,
  133. url,
  134. userThemingDisabled,
  135. defaultApps,
  136. } = loadState('theming', 'adminThemingParameters')
  137. const textFields = [
  138. {
  139. name: 'name',
  140. value: name,
  141. defaultValue: 'Nextcloud',
  142. type: 'text',
  143. displayName: t('theming', 'Name'),
  144. placeholder: t('theming', 'Name'),
  145. maxlength: 250,
  146. },
  147. {
  148. name: 'url',
  149. value: url,
  150. defaultValue: 'https://nextcloud.com',
  151. type: 'url',
  152. displayName: t('theming', 'Web link'),
  153. placeholder: 'https://…',
  154. maxlength: 500,
  155. },
  156. {
  157. name: 'slogan',
  158. value: slogan,
  159. defaultValue: t('theming', 'a safe home for all your data'),
  160. type: 'text',
  161. displayName: t('theming', 'Slogan'),
  162. placeholder: t('theming', 'Slogan'),
  163. maxlength: 500,
  164. },
  165. ]
  166. const primaryColorPickerField = {
  167. name: 'primary_color',
  168. value: primaryColor,
  169. defaultValue: '#0082c9',
  170. displayName: t('theming', 'Primary color'),
  171. description: t('theming', 'The primary color is used for highlighting elements like important buttons. It might get slightly adjusted depending on the current color schema.'),
  172. }
  173. const advancedTextFields = [
  174. {
  175. name: 'imprintUrl',
  176. value: legalNoticeUrl,
  177. defaultValue: '',
  178. type: 'url',
  179. displayName: t('theming', 'Legal notice link'),
  180. placeholder: 'https://…',
  181. maxlength: 500,
  182. },
  183. {
  184. name: 'privacyUrl',
  185. value: privacyPolicyUrl,
  186. defaultValue: '',
  187. type: 'url',
  188. displayName: t('theming', 'Privacy policy link'),
  189. placeholder: 'https://…',
  190. maxlength: 500,
  191. },
  192. ]
  193. const advancedFileInputFields = [
  194. {
  195. name: 'logoheader',
  196. mimeName: 'logoheaderMime',
  197. mimeValue: logoheaderMime,
  198. defaultMimeValue: '',
  199. displayName: t('theming', 'Header logo'),
  200. ariaLabel: t('theming', 'Upload new header logo'),
  201. },
  202. {
  203. name: 'favicon',
  204. mimeName: 'faviconMime',
  205. mimeValue: faviconMime,
  206. defaultMimeValue: '',
  207. displayName: t('theming', 'Favicon'),
  208. ariaLabel: t('theming', 'Upload new favicon'),
  209. },
  210. ]
  211. const userThemingField = {
  212. name: 'disable-user-theming',
  213. value: userThemingDisabled,
  214. defaultValue: false,
  215. displayName: t('theming', 'User settings'),
  216. label: t('theming', 'Disable user theming'),
  217. description: t('theming', 'Although you can select and customize your instance, users can change their background and colors. If you want to enforce your customization, you can toggle this on.'),
  218. }
  219. export default {
  220. name: 'AdminTheming',
  221. components: {
  222. AppMenuSection,
  223. CheckboxField,
  224. ColorPickerField,
  225. FileInputField,
  226. NcNoteCard,
  227. NcSettingsSection,
  228. TextField,
  229. },
  230. data() {
  231. return {
  232. backgroundMime,
  233. backgroundURL,
  234. backgroundColor,
  235. defaultBackgroundColor: '#0069c3',
  236. logoMime,
  237. textFields,
  238. primaryColorPickerField,
  239. advancedTextFields,
  240. advancedFileInputFields,
  241. userThemingField,
  242. defaultApps,
  243. canThemeIcons,
  244. docUrl,
  245. docUrlIcons,
  246. isThemable,
  247. notThemableErrorMessage,
  248. }
  249. },
  250. computed: {
  251. cssBackgroundImage() {
  252. if (this.backgroundURL) {
  253. return `url('${this.backgroundURL}')`
  254. }
  255. return 'unset'
  256. },
  257. },
  258. watch: {
  259. backgroundMime() {
  260. if (this.backgroundMime === '') {
  261. // Reset URL to default value for preview
  262. this.backgroundURL = defaultBackgroundURL
  263. } else if (this.backgroundMime === 'backgroundColor') {
  264. // Reset URL to empty image when only color is configured
  265. this.backgroundURL = ''
  266. }
  267. },
  268. async backgroundURL() {
  269. // When the background is changed we need to emulate the background color change
  270. if (this.backgroundURL !== '') {
  271. const color = await this.calculateDefaultBackground()
  272. this.defaultBackgroundColor = color
  273. this.backgroundColor = color
  274. }
  275. },
  276. },
  277. async mounted() {
  278. if (this.backgroundURL) {
  279. this.defaultBackgroundColor = await this.calculateDefaultBackground()
  280. }
  281. },
  282. methods: {
  283. refreshStyles,
  284. /**
  285. * Same as on server - if a user uploads an image the mean color will be set as the background color
  286. */
  287. calculateDefaultBackground() {
  288. const toHex = (num) => `00${num.toString(16)}`.slice(-2)
  289. return new Promise((resolve, reject) => {
  290. const img = new Image()
  291. img.src = this.backgroundURL
  292. img.onload = () => {
  293. const context = document.createElement('canvas').getContext('2d')
  294. context.imageSmoothingEnabled = true
  295. context.drawImage(img, 0, 0, 1, 1)
  296. resolve('#' + [...context.getImageData(0, 0, 1, 1).data.slice(0, 3)].map(toHex).join(''))
  297. }
  298. img.onerror = reject
  299. })
  300. },
  301. },
  302. }
  303. </script>
  304. <style lang="scss" scoped>
  305. .admin-theming,
  306. .admin-theming-advanced {
  307. display: flex;
  308. flex-direction: column;
  309. gap: 8px 0;
  310. }
  311. .admin-theming {
  312. &__preview {
  313. width: 230px;
  314. height: 140px;
  315. background-size: cover;
  316. background-position: center;
  317. text-align: center;
  318. margin-top: 10px;
  319. background-color: v-bind('backgroundColor');
  320. background-image: v-bind('cssBackgroundImage');
  321. &-logo {
  322. width: 20%;
  323. height: 20%;
  324. margin-top: 20px;
  325. display: inline-block;
  326. background-size: contain;
  327. background-position: center;
  328. background-repeat: no-repeat;
  329. background-image: var(--image-logo, url('../../../core/img/logo/logo.svg'));
  330. }
  331. }
  332. }
  333. </style>