diff --git a/changelog.d/custom.add b/changelog.d/custom.add new file mode 100644 index 000000000..97848d7e4 --- /dev/null +++ b/changelog.d/custom.add @@ -0,0 +1 @@ +Added support for fetching /{resource}.custom.ext to allow adding instance-specific themes without altering sourcetree diff --git a/changelog.d/multiple-status-mute-reasons.fix b/changelog.d/multiple-status-mute-reasons.fix new file mode 100644 index 000000000..952ccea82 --- /dev/null +++ b/changelog.d/multiple-status-mute-reasons.fix @@ -0,0 +1 @@ +Fix whitespaces for multiple status mute reasons, display bot status reason diff --git a/changelog.d/tabs.change b/changelog.d/tabs.change new file mode 100644 index 000000000..e716ad427 --- /dev/null +++ b/changelog.d/tabs.change @@ -0,0 +1 @@ +Tabs now have indentation for better visibility of which tab is currently active diff --git a/changelog.d/themes3.add b/changelog.d/themes3.add new file mode 100644 index 000000000..040957ced --- /dev/null +++ b/changelog.d/themes3.add @@ -0,0 +1 @@ +UI for making v3 themes and palettes, support for bundling v3 themes diff --git a/src/components/notification/notification.scss b/src/components/notification/notification.scss index 2dbced099..0ca6aea77 100644 --- a/src/components/notification/notification.scss +++ b/src/components/notification/notification.scss @@ -26,6 +26,7 @@ overflow: hidden; display: flex; flex-wrap: nowrap; + gap: 1ex; & .status-username, & .mute-thread, diff --git a/src/components/settings_modal/settings_modal_user_content.js b/src/components/settings_modal/settings_modal_user_content.js index a6d850ec5..f6b49819a 100644 --- a/src/components/settings_modal/settings_modal_user_content.js +++ b/src/components/settings_modal/settings_modal_user_content.js @@ -64,6 +64,9 @@ const SettingsModalContent = { }, bodyLock () { return this.$store.state.interface.settingsModalState === 'visible' + }, + expertLevel () { + return this.$store.state.config.expertLevel } }, methods: { diff --git a/src/components/settings_modal/settings_modal_user_content.vue b/src/components/settings_modal/settings_modal_user_content.vue index 695102bd8..06605e7dc 100644 --- a/src/components/settings_modal/settings_modal_user_content.vue +++ b/src/components/settings_modal/settings_modal_user_content.vue @@ -28,7 +28,8 @@
diff --git a/src/components/settings_modal/tabs/appearance_tab.js b/src/components/settings_modal/tabs/appearance_tab.js index 711b1961b..d314dc78b 100644 --- a/src/components/settings_modal/tabs/appearance_tab.js +++ b/src/components/settings_modal/tabs/appearance_tab.js @@ -34,7 +34,8 @@ library.add( const AppearanceTab = { data () { return { - availableStyles: [], + availableThemesV3: [], + availableThemesV2: [], bundledPalettes: [], compilationCache: {}, fileImporter: newImporter({ @@ -108,13 +109,13 @@ const AppearanceTab = { updateIndex('style').then(styles => { styles.forEach(([key, stylePromise]) => stylePromise.then(data => { const meta = data.find(x => x.component === '@meta') - this.availableStyles.push({ key, data, name: meta.directives.name, version: 'v3' }) + this.availableThemesV3.push({ key, data, name: meta.directives.name, version: 'v3' }) })) }) updateIndex('theme').then(themes => { themes.forEach(([key, themePromise]) => themePromise.then(data => { - this.availableStyles.push({ key, data, name: data.name, version: 'v2' }) + this.availableThemesV2.push({ key, data, name: data.name, version: 'v2' }) })) }) @@ -169,6 +170,12 @@ const AppearanceTab = { }) }, computed: { + availableStyles () { + return [ + ...this.availableThemesV3, + ...this.availableThemesV2 + ] + }, availablePalettes () { return [ ...this.bundledPalettes, diff --git a/src/components/settings_modal/tabs/appearance_tab.vue b/src/components/settings_modal/tabs/appearance_tab.vue index 547b72c07..5c1c46fe3 100644 --- a/src/components/settings_modal/tabs/appearance_tab.vue +++ b/src/components/settings_modal/tabs/appearance_tab.vue @@ -55,7 +55,7 @@ :data-theme-key="style.key" class="button-default theme-preview" :class="{ toggled: isThemeActive(style.key) }" - @click="style.version == 'v2' ? setTheme(style.key) : setStyle(style.key)" + @click="style.version === 'v2' ? setTheme(style.key) : setStyle(style.key)" >
diff --git a/src/components/settings_modal/tabs/style_tab/style_tab.js b/src/components/settings_modal/tabs/style_tab/style_tab.js index cea734962..533a553fa 100644 --- a/src/components/settings_modal/tabs/style_tab/style_tab.js +++ b/src/components/settings_modal/tabs/style_tab/style_tab.js @@ -82,7 +82,12 @@ export default { const exports = {} const store = useStore() // All rules that are made by editor - const allEditedRules = reactive({}) + const allEditedRules = ref(store.state.interface.styleDataUsed || {}) + const styleDataUsed = computed(() => store.state.interface.styleDataUsed) + + watch([styleDataUsed], (value) => { + onImport(store.state.interface.styleDataUsed) + }, { once: true }) exports.isActive = computed(() => { const tabSwitcher = getCurrentInstance().parent.ctx @@ -171,6 +176,8 @@ export default { exports.selectedPalette = selectedPalette provide('selectedPalette', selectedPalette) + watch([selectedPalette], () => updateOverallPreview()) + exports.getNewPalette = () => ({ name: 'new palette', bg: '#121a24', @@ -291,7 +298,7 @@ export default { } if (hasChildren) { - acc._children = acc._children ?? {} + output._children = output._children ?? {} const { component: cComponent, variant: cVariant = 'normal', @@ -300,7 +307,7 @@ export default { } = child const cPath = `${cComponent}.${cVariant}.${normalizeStates(cState)}` - set(output._children, cPath, directives) + set(output._children, cPath, { directives }) } else { output.directives = parent.directives } @@ -338,7 +345,7 @@ export default { // Templates for directives const isElementPresent = (component, directive, defaultValue = '') => computed({ get () { - return get(allEditedRules, getPath(component, directive)) != null + return get(allEditedRules.value, getPath(component, directive)) != null }, set (value) { if (value) { @@ -346,10 +353,11 @@ export default { editorFriendlyFallbackStructure.value, getPath(component, directive) ) - set(allEditedRules, getPath(component, directive), fallback ?? defaultValue) + set(allEditedRules.value, getPath(component, directive), fallback ?? defaultValue) } else { - unset(allEditedRules, getPath(component, directive)) + unset(allEditedRules.value, getPath(component, directive)) } + exports.updateOverallPreview() } }) @@ -357,7 +365,7 @@ export default { get () { let usedRule const fallback = editorFriendlyFallbackStructure.value - const real = allEditedRules + const real = allEditedRules.value const path = getPath(component, directive) usedRule = get(real, path) // get real @@ -369,10 +377,11 @@ export default { }, set (value) { if (value) { - set(allEditedRules, getPath(component, directive), value) + set(allEditedRules.value, getPath(component, directive), value) } else { - unset(allEditedRules, getPath(component, directive)) + unset(allEditedRules.value, getPath(component, directive)) } + exports.updateOverallPreview() } }) @@ -520,7 +529,7 @@ export default { } componentsMap.values().forEach(({ name }) => { - convert(name, allEditedRules[name]) + convert(name, allEditedRules.value[name]) }) return resultRules @@ -540,8 +549,8 @@ export default { const [valType, valVal] = value.split('|') return { name: name.substring(2), - valType: valType.trim(), - value: valVal.trim() + valType: valType?.trim(), + value: valVal?.trim() } }) @@ -599,6 +608,33 @@ export default { getExportedObject: () => exportStyleData.value }) + const onImport = parsed => { + const editorComponents = parsed.filter(x => x.component.startsWith('@')) + const rootComponent = parsed.find(x => x.component === 'Root') + const rules = parsed.filter(x => !x.component.startsWith('@') && x.component !== 'Root') + const metaIn = editorComponents.find(x => x.component === '@meta').directives + const palettesIn = editorComponents.filter(x => x.component === '@palette') + + exports.name.value = metaIn.name + exports.license.value = metaIn.license + exports.author.value = metaIn.author + exports.website.value = metaIn.website + + const newVirtualDirectives = Object + .entries(rootComponent.directives) + .map(([name, value]) => { + const [valType, valVal] = value.split('|').map(x => x.trim()) + return { name: name.substring(2), valType, value: valVal } + }) + virtualDirectives.value = newVirtualDirectives + + onPalettesUpdate(palettesIn.map(x => ({ name: x.variant, ...x.directives }))) + + allEditedRules.value = rulesToEditorFriendly(rules) + + exports.updateOverallPreview() + } + const styleImporter = newImporter({ accept: '.piss', parser (string) { return deserialize(string) }, @@ -606,39 +642,7 @@ export default { console.error('Failure importing style:', result) this.$store.dispatch('pushGlobalNotice', { messageKey: 'settings.invalid_theme_imported', level: 'error' }) }, - onImport (parsed, filename) { - const editorComponents = parsed.filter(x => x.component.startsWith('@')) - const rootComponent = parsed.find(x => x.component === 'Root') - const rules = parsed.filter(x => !x.component.startsWith('@') && x.component !== 'Root') - const metaIn = editorComponents.find(x => x.component === '@meta').directives - const palettesIn = editorComponents.filter(x => x.component === '@palette') - - exports.name.value = metaIn.name - exports.license.value = metaIn.license - exports.author.value = metaIn.author - exports.website.value = metaIn.website - - const newVirtualDirectives = Object - .entries(rootComponent.directives) - .map(([name, value]) => { - const [valType, valVal] = value.split('|').map(x => x.trim()) - return { name: name.substring(2), valType, value: valVal } - }) - virtualDirectives.value = newVirtualDirectives - - onPalettesUpdate(palettesIn.map(x => ({ name: x.variant, ...x.directives }))) - - Object.keys(allEditedRules).forEach((k) => delete allEditedRules[k]) - - rules.forEach(rule => { - rulesToEditorFriendly( - [rule], - allEditedRules - ) - }) - - exports.updateOverallPreview() - } + onImport }) // Raw format @@ -659,6 +663,10 @@ export default { ].join('\n\n') }) + exports.clearStyle = () => { + onImport(store.state.interface.styleDataUsed) + } + exports.exportStyle = () => { styleExporter.exportData() } @@ -691,16 +699,26 @@ export default { const updateOverallPreview = throttle(() => { try { overallPreviewRules.value = init({ - inputRuleset: exportRules.value, + inputRuleset: [ + ...exportRules.value, + { + component: 'Root', + directives: Object.fromEntries( + Object + .entries(selectedPalette.value) + .filter(([k, v]) => k && v && k !== 'name') + .map(([k, v]) => [`--${k}`, `color | ${v}`]) + ) + } + ], ultimateBackgroundColor: '#000000', - liteMode: true, debug: true }).eager } catch (e) { console.error('Could not compile preview theme', e) return null } - }, 1000) + }, 5000) // // Apart from "hover" we can't really show how component looks like in // certain states, so we have to fake them. @@ -803,7 +821,7 @@ export default { watch( [ - allEditedRules, + allEditedRules.value, palettes, selectedPalette, selectedState, diff --git a/src/components/settings_modal/tabs/style_tab/style_tab.vue b/src/components/settings_modal/tabs/style_tab/style_tab.vue index eabbb582f..68a0193fd 100644 --- a/src/components/settings_modal/tabs/style_tab/style_tab.vue +++ b/src/components/settings_modal/tabs/style_tab/style_tab.vue @@ -21,7 +21,7 @@
{{ $t('status.sensitive_muted') }} + + {{ $t('status.bot_muted') }} + - {{ $t('status.thread_muted') }} - - - {{ $t('status.thread_muted_and_words') }} + + {{ $t('status.thread_muted') }} + + + {{ $t('status.thread_muted_and_words') }} + { const capitalizedResource = resource[0].toUpperCase() + resource.slice(1) const result = {} @@ -449,6 +448,8 @@ const interfaceMod = { ) state.paletteNameUsed = palette.nameUsed state.paletteDataUsed = palette.dataUsed + state.paletteDataUsed.link = state.paletteDataUsed.link || state.paletteDataUsed.accent + state.paletteDataUsed.accent = state.paletteDataUsed.accent || state.paletteDataUsed.link if (Array.isArray(state.paletteDataUsed)) { const [ name, @@ -461,7 +462,18 @@ const interfaceMod = { cBlue = '#0000FF', cOrange = '#E3FF00' ] = palette.dataUsed - state.paletteDataUsed = { name, bg, fg, text, link, cRed, cBlue, cGreen, cOrange } + state.paletteDataUsed = { + name, + bg, + fg, + text, + link, + accent: link, + cRed, + cBlue, + cGreen, + cOrange + } } console.debug('Palette data used', palette.dataUsed) @@ -473,12 +485,6 @@ const interfaceMod = { ) state.styleNameUsed = style.nameUsed state.styleDataUsed = style.dataUsed - - console.log( - 'GOT THEME DATA', - state.styleDataUsed, - state.paletteDataUsed - ) } else { const theme = await getData( 'theme', diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js index 7ea947647..f57fd80e9 100644 --- a/src/services/style_setter/style_setter.js +++ b/src/services/style_setter/style_setter.js @@ -228,35 +228,56 @@ export const applyConfig = (input, i18n) => { export const getResourcesIndex = async (url, parser = JSON.parse) => { const cache = 'no-store' + const customUrl = url.replace(/\.(\w+)$/, '.custom.$1') + let builtin + let custom + + const resourceTransform = (resources) => { + return Object + .entries(resources) + .map(([k, v]) => { + if (typeof v === 'object') { + return [k, () => Promise.resolve(v)] + } else if (typeof v === 'string') { + return [ + k, + () => window + .fetch(v, { cache }) + .then(data => data.text()) + .then(text => parser(text)) + .catch(e => { + console.error(e) + return null + }) + ] + } else { + console.error(`Unknown resource format - ${k} is a ${typeof v}`) + return [k, null] + } + }) + } try { - const data = await window.fetch(url, { cache }) - const resources = await data.json() - return Object.fromEntries( - Object - .entries(resources) - .map(([k, v]) => { - if (typeof v === 'object') { - return [k, () => Promise.resolve(v)] - } else if (typeof v === 'string') { - return [ - k, - () => window - .fetch(v, { cache }) - .then(data => data.text()) - .then(text => parser(text)) - .catch(e => { - console.error(e) - return null - }) - ] - } else { - console.error(`Unknown resource format - ${k} is a ${typeof v}`) - return [k, null] - } - }) - ) + const builtinData = await window.fetch(url, { cache }) + const builtinResources = await builtinData.json() + builtin = resourceTransform(builtinResources) } catch (e) { - return Promise.reject(e) + builtin = [] + console.warn(`Builtin resources at ${url} unavailable`) } + + try { + const customData = await window.fetch(customUrl, { cache }) + const customResources = await customData.json() + custom = resourceTransform(customResources) + } catch (e) { + custom = [] + console.warn(`Custom resources at ${customUrl} unavailable`) + } + + const total = [...custom, ...builtin] + if (total.length === 0) { + return Promise.reject(new Error(`Resource at ${url} and ${customUrl} completely unavailable. Panicking`)) + } + return Promise.resolve(Object.fromEntries(total)) } diff --git a/src/services/theme_data/iss_deserializer.js b/src/services/theme_data/iss_deserializer.js index 196007e65..55a596a5f 100644 --- a/src/services/theme_data/iss_deserializer.js +++ b/src/services/theme_data/iss_deserializer.js @@ -15,7 +15,7 @@ export const deserializeShadow = string => { // spread (optional) '(?:(-?[0-9]+(?:\\.[0-9]+)?)\\s+)?', // either hex, variable or function - '(#[0-9a-f]{6}|--[a-z0-9\\-_]+|\\$[a-z0-9\\-()_]+)', + '(#[0-9a-f]{6}|--[a-z0-9\\-_]+|\\$[a-z0-9\\-()_ ]+)', // opacity (optional) '(?:\\s+\\/\\s+([0-9]+(?:\\.[0-9]+)?)\\s*)?', // name diff --git a/static/.gitignore b/static/.gitignore new file mode 100644 index 000000000..3332292a1 --- /dev/null +++ b/static/.gitignore @@ -0,0 +1 @@ +*.custom.* diff --git a/static/styles/Breezy DX.piss b/static/styles/Breezy DX.piss new file mode 100644 index 000000000..06f0cd5f8 --- /dev/null +++ b/static/styles/Breezy DX.piss @@ -0,0 +1,63 @@ +@meta { + name: Breezy DX; + author: HJ; + license: WTFPL; + website: ebin.club; +} + +@palette.Dark { + bg: #121a24; + fg: #182230; + text: #b9b9ba; + link: #d8a070; + accent: #d8a070; + cRed: #FF0000; + cBlue: #0095ff; + cGreen: #0fa00f; + cOrange: #ffa500; +} + +@palette.Light { + bg: #f2f6f9; + fg: #d6dfed; + text: #304055; + underlay: #5d6086; + accent: #f55b1b; + cBlue: #0095ff; + cRed: #d31014; + cGreen: #0fa00f; + cOrange: #ffa500; + border: #d8e6f9; +} + +Root { + --badgeNotification: color | --cRed; + --buttonDefaultHoverGlow: shadow | inset 0 0 0 1 --accent / 1; + --buttonDefaultFocusGlow: shadow | inset 0 0 0 1 --accent / 1; + --buttonDefaultShadow: shadow | inset 0 0 0 1 --text / 0.35, 0 5 5 -5 #000000 / 0.35; + --buttonDefaultBevel: shadow | inset 0 14 14 -14 #FFFFFF / 0.1; + --buttonPressedBevel: shadow | inset 0 -20 20 -20 #000000 / 0.05, inset 0 20 0 80 --accent / 0.2; + --defaultInputBevel: shadow | inset 0 0 0 1 --text / 0.35; + --defaultInputHoverGlow: shadow | 0 0 0 1 --accent / 1; + --defaultInputFocusGlow: shadow | 0 0 0 1 --link / 1; +} + +Button:disabled { + shadow: --buttonDefaultBevel, --buttonDefaultShadow +} + +Button:hover { + shadow: --buttonDefaultHoverGlow, --buttonDefaultBevel, --buttonDefaultShadow +} + +Input { + shadow: --defaultInputBevel +} + +PanelHeader { + shadow: inset 0 30 30 -30 #ffffff / 0.25 +} + +Tab:hover { + shadow: --buttonDefaultHoverGlow, --buttonDefaultBevel, --buttonDefaultShadow +} diff --git a/static/styles/Redmond DX.piss b/static/styles/Redmond DX.piss index c9ce0c04d..7777b7d33 100644 --- a/static/styles/Redmond DX.piss +++ b/static/styles/Redmond DX.piss @@ -48,8 +48,9 @@ Root { --bevelDark: color | $brightness(--bg -20); --bevelExtraDark: color | #404040; --buttonDefaultBevel: shadow | $borderSide(--bevelExtraDark bottom-right 1 1), $borderSide(--bevelLight top-left 1 1), $borderSide(--bevelDark bottom-right 1 2); - --buttonPressedBevel: shadow | inset 0 0 0 1 #000000 / 1 #Outer , inset 0 0 0 2 --bevelExtraDark / 1 #inner; - --defaultInputBevel: shadow | $borderSide(--bevelLight bottom-right 1), $borderSide(--bevelDark top-left 1 1), $borderSide(--bg bottom-right 1 2), $borderSide(--bevelExtraDark top-left 1 2); + --buttonPressedFocusedBevel: shadow | inset 0 0 0 1 #000000 / 1 #Outer , inset 0 0 0 2 --bevelExtraDark / 1 #inner; + --buttonPressedBevel: shadow | $borderSide(--bevelDark top-left 1 1), $borderSide(--bevelLight bottom-right 1 1), $borderSide(--bevelExtraDark top-left 1 2); + --defaultInputBevel: shadow | $borderSide(--bevelDark top-left 1 1), $borderSide(--bevelLight bottom-right 1 1), $borderSide(--bevelExtraDark top-left 1 2), $borderSide(--bg bottom-right 1 2); } Button:toggled { @@ -80,12 +81,44 @@ Button:pressed:hover { shadow: --buttonPressedBevel } +Button:hover:pressed:focused { + shadow: --buttonPressedFocusedBevel +} + +Button:pressed:focused { + shadow: --buttonPressedFocusedBevel +} + +Button:toggled:pressed { + shadow: --buttonPressedFocusedBevel +} + Input { background: $mod(--bg -80); shadow: --defaultInputBevel; roundness: 0 } +Input:focused { + shadow: inset 0 0 0 1 #000000 / 1, --defaultInputBevel +} + +Input:focused:hover { + shadow: --defaultInputBevel +} + +Input:focused:hover:disabled { + shadow: --defaultInputBevel +} + +Input:hover { + shadow: --defaultInputBevel +} + +Input:disabled { + shadow: --defaultInputBevel +} + Panel { shadow: --buttonDefaultBevel; roundness: 0 @@ -106,7 +139,8 @@ Tab:active { } Tab:active:hover { - background: --bg + background: --bg; + shadow: --defaultButtonBevel } Tab:active:hover:disabled { @@ -125,3 +159,11 @@ Tab { background: --bg; shadow: --buttonDefaultBevel } + +Tab:hover:active { + shadow: --buttonDefaultBevel +} + +TopBar Link { + textColor: #ffffff +} diff --git a/static/styles/index.json b/static/styles/index.json index 7a136cbc7..52365cd3c 100644 --- a/static/styles/index.json +++ b/static/styles/index.json @@ -1,3 +1,4 @@ { - "RedmondDX": "/static/styles/Redmond DX.piss" + "RedmondDX": "/static/styles/Redmond DX.piss", + "BreezyDX": "/static/styles/Breezy DX.piss" }