logo

pleroma-fe

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

interface.js (23427B)


  1. import { getResourcesIndex, applyTheme, tryLoadCache } from '../services/style_setter/style_setter.js'
  2. import { CURRENT_VERSION, generatePreset } from 'src/services/theme_data/theme_data.service.js'
  3. import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.js'
  4. import { deserialize } from '../services/theme_data/iss_deserializer.js'
  5. // helper for debugging
  6. // eslint-disable-next-line no-unused-vars
  7. const toValue = (x) => JSON.parse(JSON.stringify(x === undefined ? 'null' : x))
  8. const defaultState = {
  9. localFonts: null,
  10. themeApplied: false,
  11. themeChangeInProgress: false,
  12. themeVersion: 'v3',
  13. styleNameUsed: null,
  14. styleDataUsed: null,
  15. useStylePalette: false, // hack for applying styles from appearance tab
  16. paletteNameUsed: null,
  17. paletteDataUsed: null,
  18. themeNameUsed: null,
  19. themeDataUsed: null,
  20. temporaryChangesTimeoutId: null, // used for temporary options that revert after a timeout
  21. temporaryChangesConfirm: () => {}, // used for applying temporary options
  22. temporaryChangesRevert: () => {}, // used for reverting temporary options
  23. settingsModalState: 'hidden',
  24. settingsModalLoadedUser: false,
  25. settingsModalLoadedAdmin: false,
  26. settingsModalTargetTab: null,
  27. settingsModalMode: 'user',
  28. settings: {
  29. currentSaveStateNotice: null,
  30. noticeClearTimeout: null,
  31. notificationPermission: null
  32. },
  33. browserSupport: {
  34. cssFilter: window.CSS && window.CSS.supports && (
  35. window.CSS.supports('filter', 'drop-shadow(0 0)') ||
  36. window.CSS.supports('-webkit-filter', 'drop-shadow(0 0)')
  37. ),
  38. localFonts: typeof window.queryLocalFonts === 'function'
  39. },
  40. layoutType: 'normal',
  41. globalNotices: [],
  42. layoutHeight: 0,
  43. lastTimeline: null
  44. }
  45. const interfaceMod = {
  46. state: defaultState,
  47. mutations: {
  48. settingsSaved (state, { success, error }) {
  49. if (success) {
  50. if (state.noticeClearTimeout) {
  51. clearTimeout(state.noticeClearTimeout)
  52. }
  53. state.settings.currentSaveStateNotice = { error: false, data: success }
  54. state.settings.noticeClearTimeout = setTimeout(() => delete state.settings.currentSaveStateNotice, 2000)
  55. } else {
  56. state.settings.currentSaveStateNotice = { error: true, errorData: error }
  57. }
  58. },
  59. setTemporaryChanges (state, { timeoutId, confirm, revert }) {
  60. state.temporaryChangesTimeoutId = timeoutId
  61. state.temporaryChangesConfirm = confirm
  62. state.temporaryChangesRevert = revert
  63. },
  64. clearTemporaryChanges (state) {
  65. clearTimeout(state.temporaryChangesTimeoutId)
  66. state.temporaryChangesTimeoutId = null
  67. state.temporaryChangesConfirm = () => {}
  68. state.temporaryChangesRevert = () => {}
  69. },
  70. setThemeApplied (state) {
  71. state.themeApplied = true
  72. },
  73. setNotificationPermission (state, permission) {
  74. state.notificationPermission = permission
  75. },
  76. setLayoutType (state, value) {
  77. state.layoutType = value
  78. },
  79. closeSettingsModal (state) {
  80. state.settingsModalState = 'hidden'
  81. },
  82. togglePeekSettingsModal (state) {
  83. switch (state.settingsModalState) {
  84. case 'minimized':
  85. state.settingsModalState = 'visible'
  86. return
  87. case 'visible':
  88. state.settingsModalState = 'minimized'
  89. return
  90. default:
  91. throw new Error('Illegal minimization state of settings modal')
  92. }
  93. },
  94. openSettingsModal (state, value) {
  95. state.settingsModalMode = value
  96. state.settingsModalState = 'visible'
  97. if (value === 'user') {
  98. if (!state.settingsModalLoadedUser) {
  99. state.settingsModalLoadedUser = true
  100. }
  101. } else if (value === 'admin') {
  102. if (!state.settingsModalLoadedAdmin) {
  103. state.settingsModalLoadedAdmin = true
  104. }
  105. }
  106. },
  107. setSettingsModalTargetTab (state, value) {
  108. state.settingsModalTargetTab = value
  109. },
  110. pushGlobalNotice (state, notice) {
  111. state.globalNotices.push(notice)
  112. },
  113. removeGlobalNotice (state, notice) {
  114. state.globalNotices = state.globalNotices.filter(n => n !== notice)
  115. },
  116. setLayoutHeight (state, value) {
  117. state.layoutHeight = value
  118. },
  119. setLayoutWidth (state, value) {
  120. state.layoutWidth = value
  121. },
  122. setLastTimeline (state, value) {
  123. state.lastTimeline = value
  124. },
  125. setFontsList (state, value) {
  126. // Set is used here so that we filter out duplicate fonts (possibly same font but with different weight)
  127. state.localFonts = [...(new Set(value.map(font => font.family))).values()]
  128. }
  129. },
  130. actions: {
  131. setPageTitle ({ rootState }, option = '') {
  132. document.title = `${option} ${rootState.instance.name}`
  133. },
  134. settingsSaved ({ commit, dispatch }, { success, error }) {
  135. commit('settingsSaved', { success, error })
  136. },
  137. setNotificationPermission ({ commit }, permission) {
  138. commit('setNotificationPermission', permission)
  139. },
  140. closeSettingsModal ({ commit }) {
  141. commit('closeSettingsModal')
  142. },
  143. openSettingsModal ({ commit }, value = 'user') {
  144. commit('openSettingsModal', value)
  145. },
  146. togglePeekSettingsModal ({ commit }) {
  147. commit('togglePeekSettingsModal')
  148. },
  149. clearSettingsModalTargetTab ({ commit }) {
  150. commit('setSettingsModalTargetTab', null)
  151. },
  152. openSettingsModalTab ({ commit }, value) {
  153. commit('setSettingsModalTargetTab', value)
  154. commit('openSettingsModal', 'user')
  155. },
  156. pushGlobalNotice (
  157. { commit, dispatch, state },
  158. {
  159. messageKey,
  160. messageArgs = {},
  161. level = 'error',
  162. timeout = 0
  163. }) {
  164. const notice = {
  165. messageKey,
  166. messageArgs,
  167. level
  168. }
  169. commit('pushGlobalNotice', notice)
  170. // Adding a new element to array wraps it in a Proxy, which breaks the comparison
  171. // TODO: Generate UUID or something instead or relying on !== operator?
  172. const newNotice = state.globalNotices[state.globalNotices.length - 1]
  173. if (timeout) {
  174. setTimeout(() => dispatch('removeGlobalNotice', newNotice), timeout)
  175. }
  176. return newNotice
  177. },
  178. removeGlobalNotice ({ commit }, notice) {
  179. commit('removeGlobalNotice', notice)
  180. },
  181. setLayoutHeight ({ commit }, value) {
  182. commit('setLayoutHeight', value)
  183. },
  184. // value is optional, assuming it was cached prior
  185. setLayoutWidth ({ commit, state, rootGetters, rootState }, value) {
  186. let width = value
  187. if (value !== undefined) {
  188. commit('setLayoutWidth', value)
  189. } else {
  190. width = state.layoutWidth
  191. }
  192. const mobileLayout = width <= 800
  193. const normalOrMobile = mobileLayout ? 'mobile' : 'normal'
  194. const { thirdColumnMode } = rootGetters.mergedConfig
  195. if (thirdColumnMode === 'none' || !rootState.users.currentUser) {
  196. commit('setLayoutType', normalOrMobile)
  197. } else {
  198. const wideLayout = width >= 1300
  199. commit('setLayoutType', wideLayout ? 'wide' : normalOrMobile)
  200. }
  201. },
  202. queryLocalFonts ({ commit, dispatch, state }) {
  203. if (state.localFonts !== null) return
  204. commit('setFontsList', [])
  205. if (!state.browserSupport.localFonts) {
  206. return
  207. }
  208. window
  209. .queryLocalFonts()
  210. .then((fonts) => {
  211. commit('setFontsList', fonts)
  212. })
  213. .catch((e) => {
  214. dispatch('pushGlobalNotice', {
  215. messageKey: 'settings.style.themes3.font.font_list_unavailable',
  216. messageArgs: {
  217. error: e
  218. },
  219. level: 'error'
  220. })
  221. })
  222. },
  223. setLastTimeline ({ commit }, value) {
  224. commit('setLastTimeline', value)
  225. },
  226. async fetchPalettesIndex ({ commit, state }) {
  227. try {
  228. const value = await getResourcesIndex('/static/palettes/index.json')
  229. commit('setInstanceOption', { name: 'palettesIndex', value })
  230. return value
  231. } catch (e) {
  232. console.error('Could not fetch palettes index', e)
  233. commit('setInstanceOption', { name: 'palettesIndex', value: { _error: e } })
  234. return Promise.resolve({})
  235. }
  236. },
  237. setPalette ({ dispatch, commit }, value) {
  238. dispatch('resetThemeV3Palette')
  239. dispatch('resetThemeV2')
  240. commit('setOption', { name: 'palette', value })
  241. dispatch('applyTheme', { recompile: true })
  242. },
  243. setPaletteCustom ({ dispatch, commit }, value) {
  244. dispatch('resetThemeV3Palette')
  245. dispatch('resetThemeV2')
  246. commit('setOption', { name: 'paletteCustomData', value })
  247. dispatch('applyTheme', { recompile: true })
  248. },
  249. async fetchStylesIndex ({ commit, state }) {
  250. try {
  251. const value = await getResourcesIndex(
  252. '/static/styles/index.json',
  253. deserialize
  254. )
  255. commit('setInstanceOption', { name: 'stylesIndex', value })
  256. return value
  257. } catch (e) {
  258. console.error('Could not fetch styles index', e)
  259. commit('setInstanceOption', { name: 'stylesIndex', value: { _error: e } })
  260. return Promise.resolve({})
  261. }
  262. },
  263. setStyle ({ dispatch, commit, state }, value) {
  264. dispatch('resetThemeV3')
  265. dispatch('resetThemeV2')
  266. dispatch('resetThemeV3Palette')
  267. commit('setOption', { name: 'style', value })
  268. state.useStylePalette = true
  269. dispatch('applyTheme', { recompile: true }).then(() => {
  270. state.useStylePalette = false
  271. })
  272. },
  273. setStyleCustom ({ dispatch, commit, state }, value) {
  274. dispatch('resetThemeV3')
  275. dispatch('resetThemeV2')
  276. dispatch('resetThemeV3Palette')
  277. commit('setOption', { name: 'styleCustomData', value })
  278. state.useStylePalette = true
  279. dispatch('applyTheme', { recompile: true }).then(() => {
  280. state.useStylePalette = false
  281. })
  282. },
  283. async fetchThemesIndex ({ commit, state }) {
  284. try {
  285. const value = await getResourcesIndex('/static/styles.json')
  286. commit('setInstanceOption', { name: 'themesIndex', value })
  287. return value
  288. } catch (e) {
  289. console.error('Could not fetch themes index', e)
  290. commit('setInstanceOption', { name: 'themesIndex', value: { _error: e } })
  291. return Promise.resolve({})
  292. }
  293. },
  294. setTheme ({ dispatch, commit }, value) {
  295. dispatch('resetThemeV3')
  296. dispatch('resetThemeV3Palette')
  297. dispatch('resetThemeV2')
  298. commit('setOption', { name: 'theme', value })
  299. dispatch('applyTheme', { recompile: true })
  300. },
  301. setThemeCustom ({ dispatch, commit }, value) {
  302. dispatch('resetThemeV3')
  303. dispatch('resetThemeV3Palette')
  304. dispatch('resetThemeV2')
  305. commit('setOption', { name: 'customTheme', value })
  306. commit('setOption', { name: 'customThemeSource', value })
  307. dispatch('applyTheme', { recompile: true })
  308. },
  309. resetThemeV3 ({ dispatch, commit }) {
  310. commit('setOption', { name: 'style', value: null })
  311. commit('setOption', { name: 'styleCustomData', value: null })
  312. },
  313. resetThemeV3Palette ({ dispatch, commit }) {
  314. commit('setOption', { name: 'palette', value: null })
  315. commit('setOption', { name: 'paletteCustomData', value: null })
  316. },
  317. resetThemeV2 ({ dispatch, commit }) {
  318. commit('setOption', { name: 'theme', value: null })
  319. commit('setOption', { name: 'customTheme', value: null })
  320. commit('setOption', { name: 'customThemeSource', value: null })
  321. },
  322. async getThemeData ({ dispatch, commit, rootState, state }) {
  323. const getData = async (resource, index, customData, name) => {
  324. const capitalizedResource = resource[0].toUpperCase() + resource.slice(1)
  325. const result = {}
  326. if (customData) {
  327. result.nameUsed = 'custom' // custom data overrides name
  328. result.dataUsed = customData
  329. } else {
  330. result.nameUsed = name
  331. if (result.nameUsed == null) {
  332. result.dataUsed = null
  333. return result
  334. }
  335. let fetchFunc = index[result.nameUsed]
  336. // Fallbacks
  337. if (!fetchFunc) {
  338. if (resource === 'style' || resource === 'palette') {
  339. return result
  340. }
  341. const newName = Object.keys(index)[0]
  342. fetchFunc = index[newName]
  343. console.warn(`${capitalizedResource} with id '${state.styleNameUsed}' not found, trying back to '${newName}'`)
  344. if (!fetchFunc) {
  345. console.warn(`${capitalizedResource} doesn't have a fallback, defaulting to stock.`)
  346. fetchFunc = () => Promise.resolve(null)
  347. }
  348. }
  349. result.dataUsed = await fetchFunc()
  350. }
  351. return result
  352. }
  353. const {
  354. style: instanceStyleName,
  355. palette: instancePaletteName
  356. } = rootState.instance
  357. let {
  358. theme: instanceThemeV2Name,
  359. themesIndex,
  360. stylesIndex,
  361. palettesIndex
  362. } = rootState.instance
  363. const {
  364. style: userStyleName,
  365. styleCustomData: userStyleCustomData,
  366. palette: userPaletteName,
  367. paletteCustomData: userPaletteCustomData
  368. } = rootState.config
  369. let {
  370. theme: userThemeV2Name,
  371. customTheme: userThemeV2Snapshot,
  372. customThemeSource: userThemeV2Source
  373. } = rootState.config
  374. let majorVersionUsed
  375. console.debug(
  376. `User V3 palette: ${userPaletteName}, style: ${userStyleName} , custom: ${!!userStyleCustomData}`
  377. )
  378. console.debug(
  379. `User V2 name: ${userThemeV2Name}, source: ${!!userThemeV2Source}, snapshot: ${!!userThemeV2Snapshot}`
  380. )
  381. console.debug(`Instance V3 palette: ${instancePaletteName}, style: ${instanceStyleName}`)
  382. console.debug('Instance V2 theme: ' + instanceThemeV2Name)
  383. if (userPaletteName || userPaletteCustomData ||
  384. userStyleName || userStyleCustomData ||
  385. (
  386. // User V2 overrides instance V3
  387. (instancePaletteName ||
  388. instanceStyleName) &&
  389. instanceThemeV2Name == null &&
  390. userThemeV2Name == null
  391. )
  392. ) {
  393. // Palette and/or style overrides V2 themes
  394. instanceThemeV2Name = null
  395. userThemeV2Name = null
  396. userThemeV2Source = null
  397. userThemeV2Snapshot = null
  398. majorVersionUsed = 'v3'
  399. } else if (
  400. (userThemeV2Name ||
  401. userThemeV2Snapshot ||
  402. userThemeV2Source ||
  403. instanceThemeV2Name)
  404. ) {
  405. majorVersionUsed = 'v2'
  406. } else {
  407. // if all fails fallback to v3
  408. majorVersionUsed = 'v3'
  409. }
  410. if (majorVersionUsed === 'v3') {
  411. const result = await Promise.all([
  412. dispatch('fetchPalettesIndex'),
  413. dispatch('fetchStylesIndex')
  414. ])
  415. palettesIndex = result[0]
  416. stylesIndex = result[1]
  417. } else {
  418. // Promise.all just to be uniform with v3
  419. const result = await Promise.all([
  420. dispatch('fetchThemesIndex')
  421. ])
  422. themesIndex = result[0]
  423. }
  424. state.themeVersion = majorVersionUsed
  425. console.debug('Version used', majorVersionUsed)
  426. if (majorVersionUsed === 'v3') {
  427. state.themeDataUsed = null
  428. state.themeNameUsed = null
  429. const style = await getData(
  430. 'style',
  431. stylesIndex,
  432. userStyleCustomData,
  433. userStyleName || instanceStyleName
  434. )
  435. state.styleNameUsed = style.nameUsed
  436. state.styleDataUsed = style.dataUsed
  437. let firstStylePaletteName = null
  438. style
  439. .dataUsed
  440. ?.filter(x => x.component === '@palette')
  441. .map(x => {
  442. const cleanDirectives = Object.fromEntries(
  443. Object
  444. .entries(x.directives)
  445. .filter(([k, v]) => k)
  446. )
  447. return { name: x.variant, ...cleanDirectives }
  448. })
  449. .forEach(palette => {
  450. const key = 'style.' + palette.name.toLowerCase().replace(/ /g, '_')
  451. if (!firstStylePaletteName) firstStylePaletteName = key
  452. palettesIndex[key] = () => Promise.resolve(palette)
  453. })
  454. const palette = await getData(
  455. 'palette',
  456. palettesIndex,
  457. userPaletteCustomData,
  458. state.useStylePalette ? firstStylePaletteName : (userPaletteName || instancePaletteName)
  459. )
  460. if (state.useStylePalette) {
  461. commit('setOption', { name: 'palette', value: firstStylePaletteName })
  462. }
  463. state.paletteNameUsed = palette.nameUsed
  464. state.paletteDataUsed = palette.dataUsed
  465. if (state.paletteDataUsed) {
  466. state.paletteDataUsed.link = state.paletteDataUsed.link || state.paletteDataUsed.accent
  467. state.paletteDataUsed.accent = state.paletteDataUsed.accent || state.paletteDataUsed.link
  468. }
  469. if (Array.isArray(state.paletteDataUsed)) {
  470. const [
  471. name,
  472. bg,
  473. fg,
  474. text,
  475. link,
  476. cRed = '#FF0000',
  477. cGreen = '#00FF00',
  478. cBlue = '#0000FF',
  479. cOrange = '#E3FF00'
  480. ] = palette.dataUsed
  481. state.paletteDataUsed = {
  482. name,
  483. bg,
  484. fg,
  485. text,
  486. link,
  487. accent: link,
  488. cRed,
  489. cBlue,
  490. cGreen,
  491. cOrange
  492. }
  493. }
  494. console.debug('Palette data used', palette.dataUsed)
  495. } else {
  496. state.styleNameUsed = null
  497. state.styleDataUsed = null
  498. state.paletteNameUsed = null
  499. state.paletteDataUsed = null
  500. const theme = await getData(
  501. 'theme',
  502. themesIndex,
  503. userThemeV2Source || userThemeV2Snapshot,
  504. userThemeV2Name || instanceThemeV2Name
  505. )
  506. state.themeNameUsed = theme.nameUsed
  507. state.themeDataUsed = theme.dataUsed
  508. }
  509. },
  510. async applyTheme (
  511. { dispatch, commit, rootState, state },
  512. { recompile = false } = {}
  513. ) {
  514. const {
  515. forceThemeRecompilation,
  516. themeDebug,
  517. theme3hacks
  518. } = rootState.config
  519. state.themeChangeInProgress = true
  520. // If we're not not forced to recompile try using
  521. // cache (tryLoadCache return true if load successful)
  522. const forceRecompile = forceThemeRecompilation || recompile
  523. if (!forceRecompile && !themeDebug && await tryLoadCache()) {
  524. state.themeChangeInProgress = false
  525. return commit('setThemeApplied')
  526. }
  527. window.splashUpdate('splash.theme')
  528. await dispatch('getThemeData')
  529. try {
  530. const paletteIss = (() => {
  531. if (!state.paletteDataUsed) return null
  532. const result = {
  533. component: 'Root',
  534. directives: {}
  535. }
  536. Object
  537. .entries(state.paletteDataUsed)
  538. .filter(([k]) => k !== 'name')
  539. .forEach(([k, v]) => {
  540. let issRootDirectiveName
  541. switch (k) {
  542. case 'background':
  543. issRootDirectiveName = 'bg'
  544. break
  545. case 'foreground':
  546. issRootDirectiveName = 'fg'
  547. break
  548. default:
  549. issRootDirectiveName = k
  550. }
  551. result.directives['--' + issRootDirectiveName] = 'color | ' + v
  552. })
  553. return result
  554. })()
  555. const theme2ruleset = state.themeDataUsed && convertTheme2To3(normalizeThemeData(state.themeDataUsed))
  556. const hacks = []
  557. Object.entries(theme3hacks).forEach(([key, value]) => {
  558. switch (key) {
  559. case 'fonts': {
  560. Object.entries(theme3hacks.fonts).forEach(([fontKey, font]) => {
  561. if (!font?.family) return
  562. switch (fontKey) {
  563. case 'interface':
  564. hacks.push({
  565. component: 'Root',
  566. directives: {
  567. '--font': 'generic | ' + font.family
  568. }
  569. })
  570. break
  571. case 'input':
  572. hacks.push({
  573. component: 'Input',
  574. directives: {
  575. '--font': 'generic | ' + font.family
  576. }
  577. })
  578. break
  579. case 'post':
  580. hacks.push({
  581. component: 'RichContent',
  582. directives: {
  583. '--font': 'generic | ' + font.family
  584. }
  585. })
  586. break
  587. case 'monospace':
  588. hacks.push({
  589. component: 'Root',
  590. directives: {
  591. '--monoFont': 'generic | ' + font.family
  592. }
  593. })
  594. break
  595. }
  596. })
  597. break
  598. }
  599. case 'underlay': {
  600. if (value !== 'none') {
  601. const newRule = {
  602. component: 'Underlay',
  603. directives: {}
  604. }
  605. if (value === 'opaque') {
  606. newRule.directives.opacity = 1
  607. newRule.directives.background = '--wallpaper'
  608. }
  609. if (value === 'transparent') {
  610. newRule.directives.opacity = 0
  611. }
  612. hacks.push(newRule)
  613. }
  614. break
  615. }
  616. }
  617. })
  618. const rulesetArray = [
  619. theme2ruleset,
  620. state.styleDataUsed,
  621. paletteIss,
  622. hacks
  623. ].filter(x => x)
  624. return applyTheme(
  625. rulesetArray.flat(),
  626. () => commit('setThemeApplied'),
  627. () => {
  628. state.themeChangeInProgress = false
  629. },
  630. themeDebug
  631. )
  632. } catch (e) {
  633. window.splashError(e)
  634. }
  635. }
  636. }
  637. }
  638. export default interfaceMod
  639. export const normalizeThemeData = (input) => {
  640. let themeData, themeSource
  641. if (input.themeFileVerison === 1) {
  642. // this might not be even used at all, some leftover of unimplemented code in V2 editor
  643. return generatePreset(input).theme
  644. } else if (
  645. Object.prototype.hasOwnProperty.call(input, '_pleroma_theme_version') ||
  646. Object.prototype.hasOwnProperty.call(input, 'source') ||
  647. Object.prototype.hasOwnProperty.call(input, 'theme')
  648. ) {
  649. // We got passed a full theme file
  650. themeData = input.theme
  651. themeSource = input.source
  652. } else if (
  653. Object.prototype.hasOwnProperty.call(input, 'themeEngineVersion') ||
  654. Object.prototype.hasOwnProperty.call(input, 'colors')
  655. ) {
  656. // We got passed a source/snapshot
  657. themeData = input
  658. themeSource = input
  659. }
  660. // New theme presets don't have 'theme' property, they use 'source'
  661. let out // shout, shout let it all out
  662. if (themeSource && themeSource.themeEngineVersion === CURRENT_VERSION) {
  663. // There are some themes in wild that have completely broken source
  664. out = { ...(themeData || {}), ...themeSource }
  665. } else {
  666. out = themeData
  667. }
  668. // generatePreset here basically creates/updates "snapshot",
  669. // while also fixing the 2.2 -> 2.3 colors/shadows/etc
  670. return generatePreset(out).theme
  671. }