logo

pleroma-fe

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

appearance_tab.js (13136B)


  1. import BooleanSetting from '../helpers/boolean_setting.vue'
  2. import ChoiceSetting from '../helpers/choice_setting.vue'
  3. import IntegerSetting from '../helpers/integer_setting.vue'
  4. import FloatSetting from '../helpers/float_setting.vue'
  5. import UnitSetting, { defaultHorizontalUnits } from '../helpers/unit_setting.vue'
  6. import PaletteEditor from 'src/components/palette_editor/palette_editor.vue'
  7. import FontControl from 'src/components/font_control/font_control.vue'
  8. import { normalizeThemeData } from 'src/modules/interface'
  9. import { newImporter } from 'src/services/export_import/export_import.js'
  10. import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.js'
  11. import { init } from 'src/services/theme_data/theme_data_3.service.js'
  12. import {
  13. getCssRules,
  14. getScopedVersion
  15. } from 'src/services/theme_data/css_utils.js'
  16. import { deserialize } from 'src/services/theme_data/iss_deserializer.js'
  17. import SharedComputedObject from '../helpers/shared_computed_object.js'
  18. import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue'
  19. import { library } from '@fortawesome/fontawesome-svg-core'
  20. import {
  21. faGlobe
  22. } from '@fortawesome/free-solid-svg-icons'
  23. import Preview from './theme_tab/theme_preview.vue'
  24. // helper for debugging
  25. // eslint-disable-next-line no-unused-vars
  26. const toValue = (x) => JSON.parse(JSON.stringify(x === undefined ? 'null' : x))
  27. library.add(
  28. faGlobe
  29. )
  30. const AppearanceTab = {
  31. data () {
  32. return {
  33. availableThemesV3: [],
  34. availableThemesV2: [],
  35. bundledPalettes: [],
  36. compilationCache: {},
  37. fileImporter: newImporter({
  38. accept: '.json, .iss',
  39. validator: this.importValidator,
  40. onImport: this.onImport,
  41. parser: this.importParser,
  42. onImportFailure: this.onImportFailure
  43. }),
  44. palettesKeys: [
  45. 'bg',
  46. 'fg',
  47. 'link',
  48. 'text',
  49. 'cRed',
  50. 'cGreen',
  51. 'cBlue',
  52. 'cOrange'
  53. ],
  54. userPalette: {},
  55. intersectionObserver: null,
  56. thirdColumnModeOptions: ['none', 'notifications', 'postform'].map(mode => ({
  57. key: mode,
  58. value: mode,
  59. label: this.$t(`settings.third_column_mode_${mode}`)
  60. })),
  61. forcedRoundnessOptions: ['disabled', 'sharp', 'nonsharp', 'round'].map((mode, i) => ({
  62. key: mode,
  63. value: i - 1,
  64. label: this.$t(`settings.style.themes3.hacks.forced_roundness_mode_${mode}`)
  65. })),
  66. underlayOverrideModes: ['none', 'opaque', 'transparent'].map((mode, i) => ({
  67. key: mode,
  68. value: mode,
  69. label: this.$t(`settings.style.themes3.hacks.underlay_override_mode_${mode}`)
  70. }))
  71. }
  72. },
  73. components: {
  74. BooleanSetting,
  75. ChoiceSetting,
  76. IntegerSetting,
  77. FloatSetting,
  78. UnitSetting,
  79. ProfileSettingIndicator,
  80. FontControl,
  81. Preview,
  82. PaletteEditor
  83. },
  84. mounted () {
  85. this.$store.dispatch('getThemeData')
  86. const updateIndex = (resource) => {
  87. const capitalizedResource = resource[0].toUpperCase() + resource.slice(1)
  88. const currentIndex = this.$store.state.instance[`${resource}sIndex`]
  89. let promise
  90. if (currentIndex) {
  91. promise = Promise.resolve(currentIndex)
  92. } else {
  93. promise = this.$store.dispatch(`fetch${capitalizedResource}sIndex`)
  94. }
  95. return promise.then(index => {
  96. return Object
  97. .entries(index)
  98. .map(([k, func]) => [k, func()])
  99. })
  100. }
  101. updateIndex('style').then(styles => {
  102. styles.forEach(([key, stylePromise]) => stylePromise.then(data => {
  103. const meta = data.find(x => x.component === '@meta')
  104. this.availableThemesV3.push({ key, data, name: meta.directives.name, version: 'v3' })
  105. }))
  106. })
  107. updateIndex('theme').then(themes => {
  108. themes.forEach(([key, themePromise]) => themePromise.then(data => {
  109. if (!data) {
  110. console.warn(`Theme with key ${key} is empty or malformed`)
  111. } else if (Array.isArray(data)) {
  112. console.warn(`Theme with key ${key} is a v1 theme and should be moved to static/palettes/index.json`)
  113. } else if (!data.source && !data.theme) {
  114. console.warn(`Theme with key ${key} is malformed`)
  115. } else {
  116. this.availableThemesV2.push({ key, data, name: data.name, version: 'v2' })
  117. }
  118. }))
  119. })
  120. this.userPalette = this.$store.state.interface.paletteDataUsed || {}
  121. updateIndex('palette').then(bundledPalettes => {
  122. bundledPalettes.forEach(([key, palettePromise]) => palettePromise.then(v => {
  123. let palette
  124. if (Array.isArray(v)) {
  125. const [
  126. name,
  127. bg,
  128. fg,
  129. text,
  130. link,
  131. cRed = '#FF0000',
  132. cGreen = '#00FF00',
  133. cBlue = '#0000FF',
  134. cOrange = '#E3FF00'
  135. ] = v
  136. palette = { key, name, bg, fg, text, link, cRed, cBlue, cGreen, cOrange }
  137. } else {
  138. palette = { key, ...v }
  139. }
  140. if (!palette.key.startsWith('style.')) {
  141. this.bundledPalettes.push(palette)
  142. }
  143. }))
  144. })
  145. if (window.IntersectionObserver) {
  146. this.intersectionObserver = new IntersectionObserver((entries, observer) => {
  147. entries.forEach(({ target, isIntersecting }) => {
  148. if (!isIntersecting) return
  149. const theme = this.availableStyles.find(x => x.key === target.dataset.themeKey)
  150. this.$nextTick(() => {
  151. if (theme) theme.ready = true
  152. })
  153. observer.unobserve(target)
  154. })
  155. }, {
  156. root: this.$refs.themeList
  157. })
  158. }
  159. },
  160. updated () {
  161. this.$nextTick(() => {
  162. this.$refs.themeList.querySelectorAll('.theme-preview').forEach(node => {
  163. this.intersectionObserver.observe(node)
  164. })
  165. })
  166. },
  167. watch: {
  168. paletteDataUsed () {
  169. this.userPalette = this.paletteDataUsed || {}
  170. }
  171. },
  172. computed: {
  173. switchInProgress () {
  174. return this.$store.state.interface.themeChangeInProgress
  175. },
  176. paletteDataUsed () {
  177. return this.$store.state.interface.paletteDataUsed
  178. },
  179. availableStyles () {
  180. return [
  181. ...this.availableThemesV3,
  182. ...this.availableThemesV2
  183. ]
  184. },
  185. availablePalettes () {
  186. return [
  187. ...this.bundledPalettes,
  188. ...this.stylePalettes
  189. ]
  190. },
  191. stylePalettes () {
  192. const ruleset = this.$store.state.interface.styleDataUsed || []
  193. if (!ruleset && ruleset.length === 0) return
  194. const meta = ruleset.find(x => x.component === '@meta')
  195. const result = ruleset.filter(x => x.component.startsWith('@palette'))
  196. .map(x => {
  197. const { variant, directives } = x
  198. const {
  199. bg,
  200. fg,
  201. text,
  202. link,
  203. accent,
  204. cRed,
  205. cBlue,
  206. cGreen,
  207. cOrange,
  208. wallpaper
  209. } = directives
  210. const result = {
  211. name: `${meta.directives.name || this.$t('settings.style.themes3.palette.imported')}: ${variant}`,
  212. key: `style.${variant.toLowerCase().replace(/ /g, '_')}`,
  213. bg,
  214. fg,
  215. text,
  216. link,
  217. accent,
  218. cRed,
  219. cBlue,
  220. cGreen,
  221. cOrange,
  222. wallpaper
  223. }
  224. return Object.fromEntries(Object.entries(result).filter(([k, v]) => v))
  225. })
  226. return result
  227. },
  228. noIntersectionObserver () {
  229. return !window.IntersectionObserver
  230. },
  231. horizontalUnits () {
  232. return defaultHorizontalUnits
  233. },
  234. fontsOverride () {
  235. return this.$store.getters.mergedConfig.fontsOverride
  236. },
  237. columns () {
  238. const mode = this.$store.getters.mergedConfig.thirdColumnMode
  239. const notif = mode === 'none' ? [] : ['notifs']
  240. if (this.$store.getters.mergedConfig.sidebarRight || mode === 'postform') {
  241. return [...notif, 'content', 'sidebar']
  242. } else {
  243. return ['sidebar', 'content', ...notif]
  244. }
  245. },
  246. instanceWallpaperUsed () {
  247. return this.$store.state.instance.background &&
  248. !this.$store.state.users.currentUser.background_image
  249. },
  250. language: {
  251. get: function () { return this.$store.getters.mergedConfig.interfaceLanguage },
  252. set: function (val) {
  253. this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
  254. }
  255. },
  256. customThemeVersion () {
  257. const { themeVersion } = this.$store.state.interface
  258. return themeVersion
  259. },
  260. isCustomThemeUsed () {
  261. const { customTheme, customThemeSource } = this.mergedConfig
  262. return customTheme != null || customThemeSource != null
  263. },
  264. isCustomStyleUsed (name) {
  265. const { styleCustomData } = this.mergedConfig
  266. return styleCustomData != null
  267. },
  268. ...SharedComputedObject()
  269. },
  270. methods: {
  271. updateFont (key, value) {
  272. this.$store.dispatch('setOption', {
  273. name: 'theme3hacks',
  274. value: {
  275. ...this.mergedConfig.theme3hacks,
  276. fonts: {
  277. ...this.mergedConfig.theme3hacks.fonts,
  278. [key]: value
  279. }
  280. }
  281. })
  282. },
  283. importFile () {
  284. this.fileImporter.importData()
  285. },
  286. importParser (file, filename) {
  287. if (filename.endsWith('.json')) {
  288. return JSON.parse(file)
  289. } else if (filename.endsWith('.iss')) {
  290. return deserialize(file)
  291. }
  292. },
  293. onImport (parsed, filename) {
  294. if (filename.endsWith('.json')) {
  295. this.$store.dispatch('setThemeCustom', parsed.source || parsed.theme)
  296. } else if (filename.endsWith('.iss')) {
  297. this.$store.dispatch('setStyleCustom', parsed)
  298. }
  299. },
  300. onImportFailure (result) {
  301. console.error('Failure importing theme:', result)
  302. this.$store.dispatch('pushGlobalNotice', { messageKey: 'settings.invalid_theme_imported', level: 'error' })
  303. },
  304. importValidator (parsed, filename) {
  305. if (filename.endsWith('.json')) {
  306. const version = parsed._pleroma_theme_version
  307. return version >= 1 || version <= 2
  308. } else if (filename.endsWith('.iss')) {
  309. if (!Array.isArray(parsed)) return false
  310. if (parsed.length < 1) return false
  311. if (parsed.find(x => x.component === '@meta') == null) return false
  312. return true
  313. }
  314. },
  315. isThemeActive (key) {
  316. return key === (this.mergedConfig.theme || this.$store.state.instance.theme)
  317. },
  318. isStyleActive (key) {
  319. return key === (this.mergedConfig.style || this.$store.state.instance.style)
  320. },
  321. isPaletteActive (key) {
  322. return key === (this.mergedConfig.palette || this.$store.state.instance.palette)
  323. },
  324. setStyle (name) {
  325. this.$store.dispatch('setStyle', name)
  326. },
  327. setTheme (name) {
  328. this.$store.dispatch('setTheme', name)
  329. },
  330. setPalette (name, data) {
  331. this.$store.dispatch('setPalette', name)
  332. this.userPalette = data
  333. },
  334. setPaletteCustom (data) {
  335. this.$store.dispatch('setPaletteCustom', data)
  336. this.userPalette = data
  337. },
  338. resetTheming (name) {
  339. this.$store.dispatch('setStyle', 'stock')
  340. },
  341. previewTheme (key, version, input) {
  342. let theme3
  343. if (this.compilationCache[key]) {
  344. theme3 = this.compilationCache[key]
  345. } else if (input) {
  346. if (version === 'v2') {
  347. const style = normalizeThemeData(input)
  348. const theme2 = convertTheme2To3(style)
  349. theme3 = init({
  350. inputRuleset: theme2,
  351. ultimateBackgroundColor: '#000000',
  352. liteMode: true,
  353. debug: true,
  354. onlyNormalState: true
  355. })
  356. } else if (version === 'v3') {
  357. const palette = input.find(x => x.component === '@palette')
  358. let paletteRule
  359. if (palette) {
  360. const { directives } = palette
  361. directives.link = directives.link || directives.accent
  362. directives.accent = directives.accent || directives.link
  363. paletteRule = {
  364. component: 'Root',
  365. directives: Object.fromEntries(
  366. Object
  367. .entries(directives)
  368. .filter(([k, v]) => k && k !== 'name')
  369. .map(([k, v]) => ['--' + k, 'color | ' + v])
  370. )
  371. }
  372. } else {
  373. paletteRule = null
  374. }
  375. theme3 = init({
  376. inputRuleset: [...input, paletteRule].filter(x => x),
  377. ultimateBackgroundColor: '#000000',
  378. liteMode: true,
  379. debug: true,
  380. onlyNormalState: true
  381. })
  382. }
  383. } else {
  384. theme3 = init({
  385. inputRuleset: [],
  386. ultimateBackgroundColor: '#000000',
  387. liteMode: true,
  388. debug: true,
  389. onlyNormalState: true
  390. })
  391. }
  392. if (!this.compilationCache[key]) {
  393. this.compilationCache[key] = theme3
  394. }
  395. return getScopedVersion(
  396. getCssRules(theme3.eager),
  397. '#theme-preview-' + key
  398. ).join('\n')
  399. }
  400. }
  401. }
  402. export default AppearanceTab