logo

pleroma-fe

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

interface.js (13544B)


  1. import { getPreset, 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. const defaultState = {
  5. localFonts: null,
  6. themeApplied: false,
  7. temporaryChangesTimeoutId: null, // used for temporary options that revert after a timeout
  8. temporaryChangesConfirm: () => {}, // used for applying temporary options
  9. temporaryChangesRevert: () => {}, // used for reverting temporary options
  10. settingsModalState: 'hidden',
  11. settingsModalLoadedUser: false,
  12. settingsModalLoadedAdmin: false,
  13. settingsModalTargetTab: null,
  14. settingsModalMode: 'user',
  15. settings: {
  16. currentSaveStateNotice: null,
  17. noticeClearTimeout: null,
  18. notificationPermission: null
  19. },
  20. browserSupport: {
  21. cssFilter: window.CSS && window.CSS.supports && (
  22. window.CSS.supports('filter', 'drop-shadow(0 0)') ||
  23. window.CSS.supports('-webkit-filter', 'drop-shadow(0 0)')
  24. ),
  25. localFonts: typeof window.queryLocalFonts === 'function'
  26. },
  27. layoutType: 'normal',
  28. globalNotices: [],
  29. layoutHeight: 0,
  30. lastTimeline: null
  31. }
  32. const interfaceMod = {
  33. state: defaultState,
  34. mutations: {
  35. settingsSaved (state, { success, error }) {
  36. if (success) {
  37. if (state.noticeClearTimeout) {
  38. clearTimeout(state.noticeClearTimeout)
  39. }
  40. state.settings.currentSaveStateNotice = { error: false, data: success }
  41. state.settings.noticeClearTimeout = setTimeout(() => delete state.settings.currentSaveStateNotice, 2000)
  42. } else {
  43. state.settings.currentSaveStateNotice = { error: true, errorData: error }
  44. }
  45. },
  46. setTemporaryChanges (state, { timeoutId, confirm, revert }) {
  47. state.temporaryChangesTimeoutId = timeoutId
  48. state.temporaryChangesConfirm = confirm
  49. state.temporaryChangesRevert = revert
  50. },
  51. clearTemporaryChanges (state) {
  52. clearTimeout(state.temporaryChangesTimeoutId)
  53. state.temporaryChangesTimeoutId = null
  54. state.temporaryChangesConfirm = () => {}
  55. state.temporaryChangesRevert = () => {}
  56. },
  57. setThemeApplied (state) {
  58. state.themeApplied = true
  59. },
  60. setNotificationPermission (state, permission) {
  61. state.notificationPermission = permission
  62. },
  63. setLayoutType (state, value) {
  64. state.layoutType = value
  65. },
  66. closeSettingsModal (state) {
  67. state.settingsModalState = 'hidden'
  68. },
  69. togglePeekSettingsModal (state) {
  70. switch (state.settingsModalState) {
  71. case 'minimized':
  72. state.settingsModalState = 'visible'
  73. return
  74. case 'visible':
  75. state.settingsModalState = 'minimized'
  76. return
  77. default:
  78. throw new Error('Illegal minimization state of settings modal')
  79. }
  80. },
  81. openSettingsModal (state, value) {
  82. state.settingsModalMode = value
  83. state.settingsModalState = 'visible'
  84. if (value === 'user') {
  85. if (!state.settingsModalLoadedUser) {
  86. state.settingsModalLoadedUser = true
  87. }
  88. } else if (value === 'admin') {
  89. if (!state.settingsModalLoadedAdmin) {
  90. state.settingsModalLoadedAdmin = true
  91. }
  92. }
  93. },
  94. setSettingsModalTargetTab (state, value) {
  95. state.settingsModalTargetTab = value
  96. },
  97. pushGlobalNotice (state, notice) {
  98. state.globalNotices.push(notice)
  99. },
  100. removeGlobalNotice (state, notice) {
  101. state.globalNotices = state.globalNotices.filter(n => n !== notice)
  102. },
  103. setLayoutHeight (state, value) {
  104. state.layoutHeight = value
  105. },
  106. setLayoutWidth (state, value) {
  107. state.layoutWidth = value
  108. },
  109. setLastTimeline (state, value) {
  110. state.lastTimeline = value
  111. },
  112. setFontsList (state, value) {
  113. // Set is used here so that we filter out duplicate fonts (possibly same font but with different weight)
  114. state.localFonts = [...(new Set(value.map(font => font.family))).values()]
  115. }
  116. },
  117. actions: {
  118. setPageTitle ({ rootState }, option = '') {
  119. document.title = `${option} ${rootState.instance.name}`
  120. },
  121. settingsSaved ({ commit, dispatch }, { success, error }) {
  122. commit('settingsSaved', { success, error })
  123. },
  124. setNotificationPermission ({ commit }, permission) {
  125. commit('setNotificationPermission', permission)
  126. },
  127. closeSettingsModal ({ commit }) {
  128. commit('closeSettingsModal')
  129. },
  130. openSettingsModal ({ commit }, value = 'user') {
  131. commit('openSettingsModal', value)
  132. },
  133. togglePeekSettingsModal ({ commit }) {
  134. commit('togglePeekSettingsModal')
  135. },
  136. clearSettingsModalTargetTab ({ commit }) {
  137. commit('setSettingsModalTargetTab', null)
  138. },
  139. openSettingsModalTab ({ commit }, value) {
  140. commit('setSettingsModalTargetTab', value)
  141. commit('openSettingsModal', 'user')
  142. },
  143. pushGlobalNotice (
  144. { commit, dispatch, state },
  145. {
  146. messageKey,
  147. messageArgs = {},
  148. level = 'error',
  149. timeout = 0
  150. }) {
  151. const notice = {
  152. messageKey,
  153. messageArgs,
  154. level
  155. }
  156. commit('pushGlobalNotice', notice)
  157. // Adding a new element to array wraps it in a Proxy, which breaks the comparison
  158. // TODO: Generate UUID or something instead or relying on !== operator?
  159. const newNotice = state.globalNotices[state.globalNotices.length - 1]
  160. if (timeout) {
  161. setTimeout(() => dispatch('removeGlobalNotice', newNotice), timeout)
  162. }
  163. return newNotice
  164. },
  165. removeGlobalNotice ({ commit }, notice) {
  166. commit('removeGlobalNotice', notice)
  167. },
  168. setLayoutHeight ({ commit }, value) {
  169. commit('setLayoutHeight', value)
  170. },
  171. // value is optional, assuming it was cached prior
  172. setLayoutWidth ({ commit, state, rootGetters, rootState }, value) {
  173. let width = value
  174. if (value !== undefined) {
  175. commit('setLayoutWidth', value)
  176. } else {
  177. width = state.layoutWidth
  178. }
  179. const mobileLayout = width <= 800
  180. const normalOrMobile = mobileLayout ? 'mobile' : 'normal'
  181. const { thirdColumnMode } = rootGetters.mergedConfig
  182. if (thirdColumnMode === 'none' || !rootState.users.currentUser) {
  183. commit('setLayoutType', normalOrMobile)
  184. } else {
  185. const wideLayout = width >= 1300
  186. commit('setLayoutType', wideLayout ? 'wide' : normalOrMobile)
  187. }
  188. },
  189. queryLocalFonts ({ commit, dispatch, state }) {
  190. if (state.localFonts !== null) return
  191. commit('setFontsList', [])
  192. if (!state.browserSupport.localFonts) {
  193. return
  194. }
  195. window
  196. .queryLocalFonts()
  197. .then((fonts) => {
  198. commit('setFontsList', fonts)
  199. })
  200. .catch((e) => {
  201. dispatch('pushGlobalNotice', {
  202. messageKey: 'settings.style.themes3.font.font_list_unavailable',
  203. messageArgs: {
  204. error: e
  205. },
  206. level: 'error'
  207. })
  208. })
  209. },
  210. setLastTimeline ({ commit }, value) {
  211. commit('setLastTimeline', value)
  212. },
  213. setTheme ({ commit, rootState }, { themeName, themeData, recompile, saveData } = {}) {
  214. const {
  215. theme: instanceThemeName
  216. } = rootState.instance
  217. const {
  218. theme: userThemeName,
  219. customTheme: userThemeSnapshot,
  220. customThemeSource: userThemeSource,
  221. forceThemeRecompilation,
  222. themeDebug,
  223. theme3hacks
  224. } = rootState.config
  225. const actualThemeName = userThemeName || instanceThemeName
  226. const forceRecompile = forceThemeRecompilation || recompile
  227. let promise = null
  228. if (themeData) {
  229. promise = Promise.resolve(normalizeThemeData(themeData))
  230. } else if (themeName) {
  231. promise = getPreset(themeName).then(themeData => normalizeThemeData(themeData))
  232. } else if (userThemeSource || userThemeSnapshot) {
  233. promise = Promise.resolve(normalizeThemeData({
  234. _pleroma_theme_version: 2,
  235. theme: userThemeSnapshot,
  236. source: userThemeSource
  237. }))
  238. } else if (actualThemeName && actualThemeName !== 'custom') {
  239. promise = getPreset(actualThemeName).then(themeData => {
  240. const realThemeData = normalizeThemeData(themeData)
  241. if (actualThemeName === instanceThemeName) {
  242. // This sole line is the reason why this whole block is above the recompilation check
  243. commit('setInstanceOption', { name: 'themeData', value: { theme: realThemeData } })
  244. }
  245. return realThemeData
  246. })
  247. } else {
  248. throw new Error('Cannot load any theme!')
  249. }
  250. // If we're not not forced to recompile try using
  251. // cache (tryLoadCache return true if load successful)
  252. if (!forceRecompile && !themeDebug && tryLoadCache()) {
  253. commit('setThemeApplied')
  254. return
  255. }
  256. promise
  257. .then(realThemeData => {
  258. const theme2ruleset = convertTheme2To3(realThemeData)
  259. if (saveData) {
  260. commit('setOption', { name: 'theme', value: themeName || actualThemeName })
  261. commit('setOption', { name: 'customTheme', value: realThemeData })
  262. commit('setOption', { name: 'customThemeSource', value: realThemeData })
  263. }
  264. const hacks = []
  265. Object.entries(theme3hacks).forEach(([key, value]) => {
  266. switch (key) {
  267. case 'fonts': {
  268. Object.entries(theme3hacks.fonts).forEach(([fontKey, font]) => {
  269. if (!font?.family) return
  270. switch (fontKey) {
  271. case 'interface':
  272. hacks.push({
  273. component: 'Root',
  274. directives: {
  275. '--font': 'generic | ' + font.family
  276. }
  277. })
  278. break
  279. case 'input':
  280. hacks.push({
  281. component: 'Input',
  282. directives: {
  283. '--font': 'generic | ' + font.family
  284. }
  285. })
  286. break
  287. case 'post':
  288. hacks.push({
  289. component: 'RichContent',
  290. directives: {
  291. '--font': 'generic | ' + font.family
  292. }
  293. })
  294. break
  295. case 'monospace':
  296. hacks.push({
  297. component: 'Root',
  298. directives: {
  299. '--monoFont': 'generic | ' + font.family
  300. }
  301. })
  302. break
  303. }
  304. })
  305. break
  306. }
  307. case 'underlay': {
  308. if (value !== 'none') {
  309. const newRule = {
  310. component: 'Underlay',
  311. directives: {}
  312. }
  313. if (value === 'opaque') {
  314. newRule.directives.opacity = 1
  315. newRule.directives.background = '--wallpaper'
  316. }
  317. if (value === 'transparent') {
  318. newRule.directives.opacity = 0
  319. }
  320. hacks.push(newRule)
  321. }
  322. break
  323. }
  324. }
  325. })
  326. const ruleset = [
  327. ...theme2ruleset,
  328. ...hacks
  329. ]
  330. applyTheme(
  331. ruleset,
  332. () => commit('setThemeApplied'),
  333. themeDebug
  334. )
  335. })
  336. return promise
  337. }
  338. }
  339. }
  340. export default interfaceMod
  341. export const normalizeThemeData = (input) => {
  342. if (Array.isArray(input)) {
  343. const themeData = { colors: {} }
  344. themeData.colors.bg = input[1]
  345. themeData.colors.fg = input[2]
  346. themeData.colors.text = input[3]
  347. themeData.colors.link = input[4]
  348. themeData.colors.cRed = input[5]
  349. themeData.colors.cGreen = input[6]
  350. themeData.colors.cBlue = input[7]
  351. themeData.colors.cOrange = input[8]
  352. return generatePreset(themeData).theme
  353. }
  354. let themeData, themeSource
  355. if (input.themeFileVerison === 1) {
  356. // this might not be even used at all, some leftover of unimplemented code in V2 editor
  357. return generatePreset(input).theme
  358. } else if (
  359. Object.prototype.hasOwnProperty.call(input, '_pleroma_theme_version') ||
  360. Object.prototype.hasOwnProperty.call(input, 'source') ||
  361. Object.prototype.hasOwnProperty.call(input, 'theme')
  362. ) {
  363. // We got passed a full theme file
  364. themeData = input.theme
  365. themeSource = input.source
  366. } else if (Object.prototype.hasOwnProperty.call(input, 'themeEngineVersion')) {
  367. // We got passed a source/snapshot
  368. themeData = input
  369. themeSource = input
  370. }
  371. // New theme presets don't have 'theme' property, they use 'source'
  372. let out // shout, shout let it all out
  373. if (themeSource && themeSource.themeEngineVersion === CURRENT_VERSION) {
  374. // There are some themes in wild that have completely broken source
  375. out = { ...(themeData || {}), ...themeSource }
  376. } else {
  377. out = themeData
  378. }
  379. // generatePreset here basically creates/updates "snapshot",
  380. // while also fixing the 2.2 -> 2.3 colors/shadows/etc
  381. return generatePreset(out).theme
  382. }