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


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