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


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