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


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