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.

BackgroundSettings.vue 8.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. <!--
  2. - @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net>
  3. - @copyright Copyright (c) 2022 Greta Doci <gretadoci@gmail.com>
  4. -
  5. - @author Julius Härtl <jus@bitgrid.net>
  6. - @author Greta Doci <gretadoci@gmail.com>
  7. - @author Christopher Ng <chrng8@gmail.com>
  8. -
  9. - @license GNU AGPL version 3 or any later version
  10. -
  11. - This program is free software: you can redistribute it and/or modify
  12. - it under the terms of the GNU Affero General Public License as
  13. - published by the Free Software Foundation, either version 3 of the
  14. - License, or (at your option) any later version.
  15. -
  16. - This program is distributed in the hope that it will be useful,
  17. - but WITHOUT ANY WARRANTY; without even the implied warranty of
  18. - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  19. - GNU Affero General Public License for more details.
  20. -
  21. - You should have received a copy of the GNU Affero General Public License
  22. - along with this program. If not, see <http://www.gnu.org/licenses/>.
  23. -
  24. -->
  25. <template>
  26. <div class="background-selector">
  27. <!-- Custom background -->
  28. <button class="background background__filepicker"
  29. :class="{ 'background--active': backgroundImage === 'custom' }"
  30. tabindex="0"
  31. @click="pickFile">
  32. {{ t('theming', 'Custom background') }}
  33. </button>
  34. <!-- Default background -->
  35. <button class="background background__default"
  36. :class="{ 'icon-loading': loading === 'default', 'background--active': backgroundImage === 'default' }"
  37. :data-color-bright="invertTextColor(Theming.defaultColor)"
  38. :style="{ '--border-color': Theming.defaultColor }"
  39. tabindex="0"
  40. @click="setDefault">
  41. {{ t('theming', 'Default background') }}
  42. <Check :size="44" />
  43. </button>
  44. <!-- Custom color picker -->
  45. <NcColorPicker v-model="Theming.color" @input="debouncePickColor">
  46. <button class="background background__color"
  47. :data-color="Theming.color"
  48. :data-color-bright="invertTextColor(Theming.color)"
  49. :style="{ backgroundColor: Theming.color, '--border-color': Theming.color}"
  50. tabindex="0">
  51. {{ t('theming', 'Change color') }}
  52. </button>
  53. </NcColorPicker>
  54. <!-- Background set selection -->
  55. <button v-for="shippedBackground in shippedBackgrounds"
  56. :key="shippedBackground.name"
  57. v-tooltip="shippedBackground.details.attribution"
  58. :class="{ 'icon-loading': loading === shippedBackground.name, 'background--active': backgroundImage === shippedBackground.name }"
  59. :data-color-bright="shippedBackground.details.theming === 'dark'"
  60. :style="{ backgroundImage: 'url(' + shippedBackground.preview + ')', '--border-color': shippedBackground.details.primary_color }"
  61. class="background background__shipped"
  62. tabindex="0"
  63. @click="setShipped(shippedBackground.name)">
  64. <Check :size="44" />
  65. </button>
  66. <!-- Remove background -->
  67. <button class="background background__delete"
  68. tabindex="0"
  69. @click="removeBackground">
  70. {{ t('theming', 'Remove background') }}
  71. <Close :size="24" />
  72. </button>
  73. </div>
  74. </template>
  75. <script>
  76. import { generateFilePath, generateUrl } from '@nextcloud/router'
  77. import { loadState } from '@nextcloud/initial-state'
  78. import axios from '@nextcloud/axios'
  79. import Check from 'vue-material-design-icons/Check.vue'
  80. import Close from 'vue-material-design-icons/Close.vue'
  81. import debounce from 'debounce'
  82. import NcColorPicker from '@nextcloud/vue/dist/Components/NcColorPicker'
  83. import Tooltip from '@nextcloud/vue/dist/Directives/Tooltip'
  84. const backgroundColor = loadState('theming', 'backgroundColor')
  85. const backgroundImage = loadState('theming', 'backgroundImage')
  86. const shippedBackgroundList = loadState('theming', 'shippedBackgrounds')
  87. const themingDefaultBackground = loadState('theming', 'themingDefaultBackground')
  88. const defaultShippedBackground = loadState('theming', 'defaultShippedBackground')
  89. const prefixWithBaseUrl = (url) => generateFilePath('theming', '', 'img/background/') + url
  90. export default {
  91. name: 'BackgroundSettings',
  92. directives: {
  93. Tooltip,
  94. },
  95. components: {
  96. Check,
  97. Close,
  98. NcColorPicker,
  99. },
  100. data() {
  101. return {
  102. loading: false,
  103. Theming: loadState('theming', 'data', {}),
  104. // User background image and color settings
  105. backgroundImage,
  106. backgroundColor,
  107. }
  108. },
  109. computed: {
  110. shippedBackgrounds() {
  111. return Object.keys(shippedBackgroundList)
  112. .map(fileName => {
  113. return {
  114. name: fileName,
  115. url: prefixWithBaseUrl(fileName),
  116. preview: prefixWithBaseUrl('preview/' + fileName),
  117. details: shippedBackgroundList[fileName],
  118. }
  119. })
  120. .filter(background => {
  121. // If the admin did not changed the global background
  122. // let's hide the default background to not show it twice
  123. if (!this.isGlobalBackgroundDeleted && !this.isGlobalBackgroundDefault) {
  124. return background.name !== defaultShippedBackground
  125. }
  126. return true
  127. })
  128. },
  129. isGlobalBackgroundDefault() {
  130. return !!themingDefaultBackground
  131. },
  132. isGlobalBackgroundDeleted() {
  133. return themingDefaultBackground === 'backgroundColor'
  134. },
  135. },
  136. methods: {
  137. /**
  138. * Do we need to invert the text if color is too bright?
  139. *
  140. * @param {string} color the hex color
  141. */
  142. invertTextColor(color) {
  143. return this.calculateLuma(color) > 0.6
  144. },
  145. /**
  146. * Calculate luminance of provided hex color
  147. *
  148. * @param {string} color the hex color
  149. */
  150. calculateLuma(color) {
  151. const [red, green, blue] = this.hexToRGB(color)
  152. return (0.2126 * red + 0.7152 * green + 0.0722 * blue) / 255
  153. },
  154. /**
  155. * Convert hex color to RGB
  156. *
  157. * @param {string} hex the hex color
  158. */
  159. hexToRGB(hex) {
  160. const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
  161. return result
  162. ? [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)]
  163. : null
  164. },
  165. /**
  166. * Update local state
  167. *
  168. * @param {object} data destructuring object
  169. * @param {string} data.backgroundColor background color value
  170. * @param {string} data.backgroundImage background image value
  171. * @param {string} data.version cache buster number
  172. * @see https://github.com/nextcloud/server/blob/c78bd45c64d9695724fc44fe8453a88824b85f2f/apps/theming/lib/Controller/UserThemeController.php#L187-L191
  173. */
  174. async update(data) {
  175. // Update state
  176. this.backgroundImage = data.backgroundImage
  177. this.backgroundColor = data.backgroundColor
  178. this.Theming.color = data.backgroundColor
  179. // Notify parent and reload style
  180. this.$emit('update:background')
  181. this.loading = false
  182. },
  183. async setDefault() {
  184. this.loading = 'default'
  185. const result = await axios.post(generateUrl('/apps/theming/background/default'))
  186. this.update(result.data)
  187. },
  188. async setShipped(shipped) {
  189. this.loading = shipped
  190. const result = await axios.post(generateUrl('/apps/theming/background/shipped'), { value: shipped })
  191. this.update(result.data)
  192. },
  193. async setFile(path) {
  194. this.loading = 'custom'
  195. const result = await axios.post(generateUrl('/apps/theming/background/custom'), { value: path })
  196. this.update(result.data)
  197. },
  198. async removeBackground() {
  199. this.loading = 'remove'
  200. const result = await axios.delete(generateUrl('/apps/theming/background/custom'))
  201. this.update(result.data)
  202. },
  203. async pickColor(event) {
  204. this.loading = 'color'
  205. const color = event?.target?.dataset?.color || this.Theming?.color || '#0082c9'
  206. const result = await axios.post(generateUrl('/apps/theming/background/color'), { value: color })
  207. this.update(result.data)
  208. },
  209. debouncePickColor: debounce(function() {
  210. this.pickColor(...arguments)
  211. }, 200),
  212. pickFile() {
  213. window.OC.dialogs.filepicker(t('theming', 'Select a background from your files'), (path, type) => {
  214. if (type === OC.dialogs.FILEPICKER_TYPE_CHOOSE) {
  215. this.setFile(path)
  216. }
  217. }, false, ['image/png', 'image/gif', 'image/jpeg', 'image/svg'], true, OC.dialogs.FILEPICKER_TYPE_CHOOSE)
  218. },
  219. },
  220. }
  221. </script>
  222. <style scoped lang="scss">
  223. .background-selector {
  224. display: flex;
  225. flex-wrap: wrap;
  226. justify-content: center;
  227. .background {
  228. overflow: hidden;
  229. width: 176px;
  230. height: 96px;
  231. margin: 8px;
  232. text-align: center;
  233. border: 2px solid var(--color-main-background);
  234. border-radius: var(--border-radius-large);
  235. background-position: center center;
  236. background-size: cover;
  237. &__default {
  238. background-color: var(--color-primary-default);
  239. background-image: var(--image-background-default);
  240. }
  241. &__filepicker, &__default, &__color {
  242. border-color: var(--color-border);
  243. }
  244. &__color {
  245. color: var(--color-primary-text);
  246. background-color: var(--color-primary-default);
  247. }
  248. // Text and svg icon dark on bright background
  249. &[data-color-bright] {
  250. color: black;
  251. }
  252. &--active,
  253. &:hover,
  254. &:focus {
  255. // Use theme color primary, see inline css variable in template
  256. border: 2px solid var(--border-color, var(--color-primary)) !important;
  257. }
  258. // Icon
  259. span {
  260. margin: 4px;
  261. }
  262. &__default,
  263. &__shipped {
  264. color: white;
  265. span {
  266. display: none;
  267. }
  268. }
  269. &--active:not(.icon-loading) {
  270. span {
  271. display: block;
  272. }
  273. }
  274. }
  275. }
  276. </style>