logo

pleroma-fe

My custom branche(s) on git.pleroma.social/pleroma/pleroma-fe git clone https://anongit.hacktivis.me/git/pleroma-fe.git/

style_setter.js (8494B)


  1. import { hex2rgb } from '../color_convert/color_convert.js'
  2. import { init, getEngineChecksum } from '../theme_data/theme_data_3.service.js'
  3. import { getCssRules } from '../theme_data/css_utils.js'
  4. import { defaultState } from '../../modules/config.js'
  5. import { chunk } from 'lodash'
  6. // On platforms where this is not supported, it will return undefined
  7. // Otherwise it will return an array
  8. const supportsAdoptedStyleSheets = !!document.adoptedStyleSheets
  9. const createStyleSheet = (id) => {
  10. if (supportsAdoptedStyleSheets) {
  11. return {
  12. el: null,
  13. sheet: new CSSStyleSheet(),
  14. rules: []
  15. }
  16. }
  17. const el = document.getElementById(id)
  18. // Clear all rules in it
  19. for (let i = el.sheet.cssRules.length - 1; i >= 0; --i) {
  20. el.sheet.deleteRule(i)
  21. }
  22. return {
  23. el,
  24. sheet: el.sheet,
  25. rules: []
  26. }
  27. }
  28. const EAGER_STYLE_ID = 'pleroma-eager-styles'
  29. const LAZY_STYLE_ID = 'pleroma-lazy-styles'
  30. const adoptStyleSheets = (styles) => {
  31. if (supportsAdoptedStyleSheets) {
  32. document.adoptedStyleSheets = styles.map(s => s.sheet)
  33. }
  34. // Some older browsers do not support document.adoptedStyleSheets.
  35. // In this case, we use the <style> elements.
  36. // Since the <style> elements we need are already in the DOM, there
  37. // is nothing to do here.
  38. }
  39. export const generateTheme = async (inputRuleset, callbacks, debug) => {
  40. const {
  41. onNewRule = (rule, isLazy) => {},
  42. onLazyFinished = () => {},
  43. onEagerFinished = () => {}
  44. } = callbacks
  45. // Assuming that "worst case scenario background" is panel background since it's the most likely one
  46. const themes3 = init({
  47. inputRuleset,
  48. ultimateBackgroundColor: inputRuleset[0].directives['--bg'].split('|')[1].trim(),
  49. debug
  50. })
  51. getCssRules(themes3.eager, debug).forEach(rule => {
  52. // Hacks to support multiple selectors on same component
  53. if (rule.match(/::-webkit-scrollbar-button/)) {
  54. const parts = rule.split(/[{}]/g)
  55. const newRule = [
  56. parts[0],
  57. ', ',
  58. parts[0].replace(/button/, 'thumb'),
  59. ', ',
  60. parts[0].replace(/scrollbar-button/, 'resizer'),
  61. ' {',
  62. parts[1],
  63. '}'
  64. ].join('')
  65. onNewRule(newRule, false)
  66. } else {
  67. onNewRule(rule, false)
  68. }
  69. })
  70. onEagerFinished()
  71. // Optimization - instead of processing all lazy rules in one go, process them in small chunks
  72. // so that UI can do other things and be somewhat responsive while less important rules are being
  73. // processed
  74. let counter = 0
  75. const chunks = chunk(themes3.lazy, 200)
  76. // let t0 = performance.now()
  77. const processChunk = () => {
  78. const chunk = chunks[counter]
  79. Promise.all(chunk.map(x => x())).then(result => {
  80. getCssRules(result.filter(x => x), debug).forEach(rule => {
  81. if (rule.match(/\.modal-view/)) {
  82. const parts = rule.split(/[{}]/g)
  83. const newRule = [
  84. parts[0],
  85. ', ',
  86. parts[0].replace(/\.modal-view/, '#modal'),
  87. ', ',
  88. parts[0].replace(/\.modal-view/, '.shout-panel'),
  89. ' {',
  90. parts[1],
  91. '}'
  92. ].join('')
  93. onNewRule(newRule, true)
  94. } else {
  95. onNewRule(rule, true)
  96. }
  97. })
  98. // const t1 = performance.now()
  99. // console.debug('Chunk ' + counter + ' took ' + (t1 - t0) + 'ms')
  100. // t0 = t1
  101. counter += 1
  102. if (counter < chunks.length) {
  103. setTimeout(processChunk, 0)
  104. } else {
  105. onLazyFinished()
  106. }
  107. })
  108. }
  109. return { lazyProcessFunc: processChunk }
  110. }
  111. export const tryLoadCache = () => {
  112. const json = localStorage.getItem('pleroma-fe-theme-cache')
  113. if (!json) return null
  114. let cache
  115. try {
  116. cache = JSON.parse(json)
  117. } catch (e) {
  118. console.error('Failed to decode theme cache:', e)
  119. return false
  120. }
  121. if (cache.engineChecksum === getEngineChecksum()) {
  122. const eagerStyles = createStyleSheet(EAGER_STYLE_ID)
  123. const lazyStyles = createStyleSheet(LAZY_STYLE_ID)
  124. cache.data[0].forEach(rule => eagerStyles.sheet.insertRule(rule, 'index-max'))
  125. cache.data[1].forEach(rule => lazyStyles.sheet.insertRule(rule, 'index-max'))
  126. adoptStyleSheets([eagerStyles, lazyStyles])
  127. return true
  128. } else {
  129. console.warn('Engine checksum doesn\'t match, cache not usable, clearing')
  130. localStorage.removeItem('pleroma-fe-theme-cache')
  131. }
  132. }
  133. export const applyTheme = async (input, onFinish = (data) => {}, debug) => {
  134. const eagerStyles = createStyleSheet(EAGER_STYLE_ID)
  135. const lazyStyles = createStyleSheet(LAZY_STYLE_ID)
  136. const { lazyProcessFunc } = await generateTheme(
  137. input,
  138. {
  139. onNewRule (rule, isLazy) {
  140. if (isLazy) {
  141. lazyStyles.sheet.insertRule(rule, 'index-max')
  142. lazyStyles.rules.push(rule)
  143. } else {
  144. eagerStyles.sheet.insertRule(rule, 'index-max')
  145. eagerStyles.rules.push(rule)
  146. }
  147. },
  148. onEagerFinished () {
  149. adoptStyleSheets([eagerStyles])
  150. },
  151. onLazyFinished () {
  152. adoptStyleSheets([eagerStyles, lazyStyles])
  153. const cache = { engineChecksum: getEngineChecksum(), data: [eagerStyles.rules, lazyStyles.rules] }
  154. onFinish(cache)
  155. localStorage.setItem('pleroma-fe-theme-cache', JSON.stringify(cache))
  156. }
  157. },
  158. debug
  159. )
  160. setTimeout(lazyProcessFunc, 0)
  161. return Promise.resolve()
  162. }
  163. const extractStyleConfig = ({
  164. sidebarColumnWidth,
  165. contentColumnWidth,
  166. notifsColumnWidth,
  167. emojiReactionsScale,
  168. emojiSize,
  169. navbarSize,
  170. panelHeaderSize,
  171. textSize,
  172. forcedRoundness
  173. }) => {
  174. const result = {
  175. sidebarColumnWidth,
  176. contentColumnWidth,
  177. notifsColumnWidth,
  178. emojiReactionsScale,
  179. emojiSize,
  180. navbarSize,
  181. panelHeaderSize,
  182. textSize
  183. }
  184. switch (forcedRoundness) {
  185. case 'disable':
  186. break
  187. case '0':
  188. result.forcedRoundness = '0'
  189. break
  190. case '1':
  191. result.forcedRoundness = '1px'
  192. break
  193. case '2':
  194. result.forcedRoundness = '0.4rem'
  195. break
  196. default:
  197. }
  198. return result
  199. }
  200. const defaultStyleConfig = extractStyleConfig(defaultState)
  201. export const applyConfig = (input) => {
  202. const config = extractStyleConfig(input)
  203. if (config === defaultStyleConfig) {
  204. return
  205. }
  206. const head = document.head
  207. const body = document.body
  208. body.classList.add('hidden')
  209. const rules = Object
  210. .entries(config)
  211. .filter(([k, v]) => v)
  212. .map(([k, v]) => `--${k}: ${v}`).join(';')
  213. document.getElementById('style-config')?.remove()
  214. const styleEl = document.createElement('style')
  215. styleEl.id = 'style-config'
  216. head.appendChild(styleEl)
  217. const styleSheet = styleEl.sheet
  218. styleSheet.toString()
  219. styleSheet.insertRule(`:root { ${rules} }`, 'index-max')
  220. if (Object.prototype.hasOwnProperty.call(config, 'forcedRoundness')) {
  221. styleSheet.insertRule(` * {
  222. --roundness: var(--forcedRoundness) !important;
  223. }`, 'index-max')
  224. }
  225. body.classList.remove('hidden')
  226. }
  227. export const getThemes = () => {
  228. const cache = 'no-store'
  229. return window.fetch('/static/styles.json', { cache })
  230. .then((data) => data.json())
  231. .then((themes) => {
  232. return Object.entries(themes).map(([k, v]) => {
  233. let promise = null
  234. if (typeof v === 'object') {
  235. promise = Promise.resolve(v)
  236. } else if (typeof v === 'string') {
  237. promise = window.fetch(v, { cache })
  238. .then((data) => data.json())
  239. .catch((e) => {
  240. console.error(e)
  241. return null
  242. })
  243. }
  244. return [k, promise]
  245. })
  246. })
  247. .then((promises) => {
  248. return promises
  249. .reduce((acc, [k, v]) => {
  250. acc[k] = v
  251. return acc
  252. }, {})
  253. })
  254. }
  255. export const getPreset = (val) => {
  256. return getThemes()
  257. .then((themes) => themes[val] ? themes[val] : themes['pleroma-dark'])
  258. .then((theme) => {
  259. const isV1 = Array.isArray(theme)
  260. const data = isV1 ? {} : theme.theme
  261. if (isV1) {
  262. const bg = hex2rgb(theme[1])
  263. const fg = hex2rgb(theme[2])
  264. const text = hex2rgb(theme[3])
  265. const link = hex2rgb(theme[4])
  266. const cRed = hex2rgb(theme[5] || '#FF0000')
  267. const cGreen = hex2rgb(theme[6] || '#00FF00')
  268. const cBlue = hex2rgb(theme[7] || '#0000FF')
  269. const cOrange = hex2rgb(theme[8] || '#E3FF00')
  270. data.colors = { bg, fg, text, link, cRed, cBlue, cGreen, cOrange }
  271. }
  272. return { theme: data, source: theme.source }
  273. })
  274. }