Merge branch 'themes3-grand-finale-maybe' into shigusegubu-themes3

This commit is contained in:
Henry Jameson 2024-11-26 02:02:02 +02:00
commit eb5f47ebf9
23 changed files with 290 additions and 110 deletions

1
changelog.d/custom.add Normal file
View file

@ -0,0 +1 @@
Added support for fetching /{resource}.custom.ext to allow adding instance-specific themes without altering sourcetree

View file

@ -0,0 +1 @@
Fix whitespaces for multiple status mute reasons, display bot status reason

1
changelog.d/tabs.change Normal file
View file

@ -0,0 +1 @@
Tabs now have indentation for better visibility of which tab is currently active

1
changelog.d/themes3.add Normal file
View file

@ -0,0 +1 @@
UI for making v3 themes and palettes, support for bundling v3 themes

View file

@ -26,6 +26,7 @@
overflow: hidden;
display: flex;
flex-wrap: nowrap;
gap: 1ex;
& .status-username,
& .mute-thread,

View file

@ -64,6 +64,9 @@ const SettingsModalContent = {
},
bodyLock () {
return this.$store.state.interface.settingsModalState === 'visible'
},
expertLevel () {
return this.$store.state.config.expertLevel
}
},
methods: {

View file

@ -28,7 +28,8 @@
<StyleTab />
</div>
<div
:label="$t('settings.theme')"
v-if="expertLevel > 0"
:label="$t('settings.theme_old')"
icon="paint-brush"
data-tab-name="theme"
>

View file

@ -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,

View file

@ -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)"
>
<!-- eslint-disable vue/no-v-text-v-html-on-component -->
<div v-if="style.ready || noIntersectionObserver">

View file

@ -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,

View file

@ -21,7 +21,7 @@
<div class="style-actions">
<button
class="btn button-default button-new"
@click="clearTheme"
@click="clearStyle"
>
<FAIcon icon="arrows-rotate" />
{{ $t('settings.style.themes3.editor.reset_style') }}

View file

@ -138,7 +138,6 @@ export default {
})
},
mounted () {
this.loadThemeFromLocalStorage()
if (typeof this.shadowSelected === 'undefined') {
this.shadowSelected = this.shadowsAvailable[0]
}
@ -296,6 +295,9 @@ export default {
return {}
}
},
themeDataUsed () {
return this.$store.state.interface.themeDataUsed
},
shadowsAvailable () {
return Object.keys(DEFAULT_SHADOWS).sort()
},
@ -478,15 +480,11 @@ export default {
this.dismissWarning()
},
loadThemeFromLocalStorage (confirmLoadSource = false, forceSnapshot = false) {
const {
customTheme: theme,
customThemeSource: source
} = this.$store.getters.mergedConfig
if (theme || source) {
const theme = this.themeDataUsed?.source
if (theme) {
this.loadTheme(
{
theme,
source: forceSnapshot ? theme : source
theme
},
'localStorage',
confirmLoadSource
@ -705,6 +703,9 @@ export default {
}
},
watch: {
themeDataUsed () {
this.loadThemeFromLocalStorage()
},
currentRadii () {
try {
this.previewTheme.radii = generateRadii({ radii: this.currentRadii }).theme.radii

View file

@ -958,6 +958,7 @@
:separate-inset="shadowSelected === 'avatar' || shadowSelected === 'avatarStatus'"
:fallback="currentShadowFallback"
:static-vars="previewTheme.colors"
:compact="true"
/>
</div>
<div

View file

@ -281,6 +281,7 @@
overflow: hidden;
display: flex;
flex-wrap: nowrap;
gap: 1ex;
& .status-username,
& .mute-thread,

View file

@ -36,17 +36,24 @@
>
{{ $t('status.sensitive_muted') }}
</small>
<small
v-if="muteBotStatuses && botStatus"
class="mute-thread"
>
{{ $t('status.bot_muted') }}
</small>
<small
v-if="showReasonMutedThread"
class="mute-thread"
>
{{ $t('status.thread_muted') }}
</small>
<small
v-if="showReasonMutedThread && muteWordHits.length > 0"
class="mute-thread"
>
{{ $t('status.thread_muted_and_words') }}
<span>
{{ $t('status.thread_muted') }}
</span>
<span
v-if="muteWordHits.length > 0"
>
{{ $t('status.thread_muted_and_words') }}
</span>
</small>
<small
class="mute-words"

View file

@ -701,6 +701,7 @@
"use_websockets": "Use websockets (Realtime updates)",
"text": "Text",
"theme": "Theme",
"theme_old": "Theme (old)",
"theme_help": "Use hex color codes (#rrggbb) to customize your color theme.",
"theme_help_v2_1": "You can also override certain component's colors and opacity by toggling the checkbox, use \"Clear all\" button to clear all overrides.",
"theme_help_v2_2": "Icons underneath some entries are background/text contrast indicators, hover over for detailed info. Please keep in mind that when using transparency contrast indicators show the worst possible case.",
@ -1228,6 +1229,7 @@
"thread_muted": "Thread muted",
"thread_muted_and_words": ", has words:",
"sensitive_muted": "Muting sensitive content",
"bot_muted": "Muting bot content",
"show_full_subject": "Show full subject",
"hide_full_subject": "Hide full subject",
"show_content": "Show content",

View file

@ -321,7 +321,6 @@ const interfaceMod = {
commit('setOption', { name: 'customThemeSource', value: null })
},
async getThemeData ({ dispatch, commit, rootState, state }) {
console.log('GET THEME DATA CALLED')
const getData = async (resource, index, customData, name) => {
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',

View file

@ -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))
}

View file

@ -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

1
static/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
*.custom.*

View file

@ -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
}

View file

@ -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
}

View file

@ -1,3 +1,4 @@
{
"RedmondDX": "/static/styles/Redmond DX.piss"
"RedmondDX": "/static/styles/Redmond DX.piss",
"BreezyDX": "/static/styles/Breezy DX.piss"
}