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 (8163B)


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