feat: refactor create lnbits-theme vue component (#3515)

This commit is contained in:
dni ⚡ 2025-11-13 11:50:33 +01:00 committed by GitHub
parent 9d0ec97d39
commit b7d178c08e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 221 additions and 278 deletions

View file

@ -97,11 +97,11 @@ def template_renderer(additional_folders: list | None = None) -> Jinja2Templates
"LNBITS_THEME_OPTIONS": settings.lnbits_theme_options,
"LNBITS_VERSION": settings.version,
"USE_CUSTOM_LOGO": settings.lnbits_custom_logo,
"USE_DEFAULT_REACTION": settings.lnbits_default_reaction,
"USE_DEFAULT_THEME": settings.lnbits_default_theme,
"USE_DEFAULT_BORDER": settings.lnbits_default_border,
"USE_DEFAULT_GRADIENT": settings.lnbits_default_gradient,
"USE_DEFAULT_BGIMAGE": settings.lnbits_default_bgimage,
"LNBITS_DEFAULT_REACTION": settings.lnbits_default_reaction,
"LNBITS_DEFAULT_THEME": settings.lnbits_default_theme,
"LNBITS_DEFAULT_BORDER": settings.lnbits_default_border,
"LNBITS_DEFAULT_GRADIENT": settings.lnbits_default_gradient,
"LNBITS_DEFAULT_BGIMAGE": settings.lnbits_default_bgimage,
"VOIDWALLET": settings.lnbits_backend_wallet_class == "VoidWallet",
"WEBPUSH_PUBKEY": settings.lnbits_webpush_pubkey,
"LNBITS_DENOMINATION": settings.lnbits_denomination,

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -185,6 +185,11 @@ body[data-theme=salvador] [data-theme=salvador] .q-stepper--dark {
}
body.gradient-bg {
background-image: linear-gradient(to bottom right, #fff, var(--q-primary));
background-attachment: fixed;
}
body.gradient-bg.body--dark {
background-image: linear-gradient(to bottom right, var(--q-dark-page), #0a0a0a);
background-attachment: fixed;
}

View file

@ -0,0 +1,107 @@
window.app.component('lnbits-theme', {
mixins: [window.windowMixin],
watch: {
'g.reactionChoice'(val) {
this.$q.localStorage.set('lnbits.reactions', val)
},
'g.themeChoice'(val) {
document.body.setAttribute('data-theme', val)
this.$q.localStorage.set('lnbits.theme', val)
},
'g.darkChoice'(val) {
this.$q.dark.set(val)
this.$q.localStorage.set('lnbits.darkMode', val)
},
'g.borderChoice'(val) {
document.body.classList.forEach(cls => {
if (cls.endsWith('-border')) {
document.body.classList.remove(cls)
}
})
this.$q.localStorage.setItem('lnbits.border', val)
document.body.classList.add(val)
},
'g.gradientChoice'(val) {
this.$q.localStorage.set('lnbits.gradientBg', val)
if (val === true) {
document.body.classList.add('gradient-bg')
} else {
document.body.classList.remove('gradient-bg')
}
},
'g.bgimageChoice'(val) {
this.$q.localStorage.set('lnbits.backgroundImage', val)
if (val === '') {
document.body.classList.remove('bg-image')
} else {
document.body.classList.add('bg-image')
document.body.style.setProperty('--background', `url(${val})`)
}
}
},
methods: {
async checkUrlParams() {
const params = new URLSearchParams(window.location.search)
if (params.length === 0) {
return
}
if (params.has('theme')) {
const theme = params.get('theme').trim().toLowerCase()
this.g.themeChoice = theme
params.delete('theme')
}
if (params.has('border')) {
const border = params.get('border').trim().toLowerCase()
this.g.borderChoice = border
params.delete('border')
}
if (params.has('gradient')) {
const gradient = params.get('gradient').toLowerCase()
this.g.gradientChoice = gradient === '1' || gradient === 'true'
params.delete('gradient')
}
if (params.has('dark')) {
const dark = params.get('dark').trim().toLowerCase()
this.g.darkChoice = dark === '1' || dark === 'true'
params.delete('dark')
}
if (params.has('usr')) {
try {
await LNbits.api.loginUsr(params.get('usr'))
window.location.href = '/wallet'
} catch (e) {
LNbits.utils.notifyApiError(e)
}
params.delete('usr')
}
// cleanup url
const cleanParams = params.size ? `?${params.toString()}` : ''
const url = window.location.pathname + cleanParams
// TODO state gets overridden somewhere else
window.history.replaceState(null, null, url)
}
},
created() {
// TODO: fix Chart global import each chart has to take care of its own config
// else there is no reactivity on theme change
Chart.defaults.color = this.$q.dark.isActive ? '#fff' : '#000'
this.$q.dark.set(this.g.darkChoice)
document.body.setAttribute('data-theme', this.g.themeChoice)
document.body.classList.add(this.g.borderChoice)
if (this.g.gradientChoice === true) {
document.body.classList.add('gradient-bg')
}
if (this.g.bgimageChoice !== '') {
document.body.classList.add('bg-image')
document.body.style.setProperty(
'--background',
`url(${this.g.bgimageChoice})`
)
}
this.checkUrlParams()
}
})

View file

@ -1,4 +1,12 @@
const localStore = (key, defaultValue) => {
const value = Quasar.LocalStorage.getItem(key)
return value !== null && value !== undefined && value !== 'undefined'
? value
: defaultValue
}
window.g = Vue.reactive({
isUserAuthorized: !!Quasar.Cookies.get('is_lnbits_user_authorized'),
offline: !navigator.onLine,
visibleDrawer: false,
extensions: [],
@ -12,9 +20,26 @@ window.g = Vue.reactive({
walletEventListeners: [],
updatePayments: false,
updatePaymentsHash: '',
walletFlip: Quasar.LocalStorage.getItem('lnbits.walletFlip') ?? false,
locale:
Quasar.LocalStorage.getItem('lnbits.lang') ?? navigator.languages[1] ?? 'en'
walletFlip: localStore('lnbits.walletFlip', false),
locale: localStore('lnbits.lang', navigator.languages[1] ?? 'en'),
darkChoice: localStore('lnbits.darkMode', true),
themeChoice: localStore('lnbits.theme', WINDOW_SETTINGS.LNBITS_DEFAULT_THEME),
borderChoice: localStore(
'lnbits.border',
WINDOW_SETTINGS.LNBITS_DEFAULT_REACTION
),
gradientChoice: localStore(
'lnbits.gradientBg',
WINDOW_SETTINGS.LNBITS_DEFAULT_GRADIENT
),
reactionChoice: localStore(
'lnbits.reactions',
WINDOW_SETTINGS.LNBITS_DEFAULT_REACTION
),
bgimageChoice: localStore(
'lnbits.backgroundImage',
WINDOW_SETTINGS.LNBITS_DEFAULT_BGIMAGE
)
})
window.dateFormat = 'YYYY-MM-DD HH:mm'
@ -22,3 +47,5 @@ window.dateFormat = 'YYYY-MM-DD HH:mm'
const websocketPrefix =
window.location.protocol === 'http:' ? 'ws://' : 'wss://'
const websocketUrl = `${websocketPrefix}${window.location.host}/api/v1/ws`
const _access_cookies_for_safari_refresh_do_not_delete = document.cookie

View file

@ -6,6 +6,40 @@ window.PageAccount = {
user: null,
hasUsername: false,
showUserId: false,
themeOptions: [
{
name: 'bitcoin',
color: 'deep-orange'
},
{
name: 'mint',
color: 'green'
},
{
name: 'autumn',
color: 'brown'
},
{
name: 'monochrome',
color: 'grey'
},
{
name: 'salvador',
color: 'blue-10'
},
{
name: 'freedom',
color: 'pink-13'
},
{
name: 'cyber',
color: 'light-green-9'
},
{
name: 'flamingo',
color: 'pink-3'
}
],
reactionOptions: [
'None',
'confettiBothSides',
@ -542,5 +576,10 @@ window.PageAccount = {
}
await this.getApiACLs()
await this.getUserAssets()
// filter out themes that are not allowed
this.themeOptions = this.themeOptions.filter(theme =>
this.allowedThemes.includes(theme.name)
)
}
}

View file

@ -4,7 +4,6 @@ window.PageHome = {
data() {
return {
lnurl: '',
isUserAuthorized: false,
authAction: 'login',
authMethod: 'username-password',
usr: '',
@ -136,21 +135,14 @@ window.PageHome = {
}
},
created() {
this.isUserAuthorized = !!this.$q.cookies.get('is_lnbits_user_authorized')
const _access_cookies_for_safari_refresh_do_not_delete = document.cookie
if (this.isUserAuthorized) {
if (this.g.isUserAuthorized) {
window.location.href = '/wallet'
}
const urlParams = new URLSearchParams(window.location.search)
this.reset_key = urlParams.get('reset_key')
if (this.reset_key) {
this.authAction = 'reset'
}
// check if lightning parameters are present in the URL
if (urlParams.has('lightning')) {
this.lnurl = urlParams.get('lightning')

View file

@ -9,28 +9,9 @@ window.windowMixin = {
mobileSimple: true,
addWalletDialog: {show: false, walletType: 'lightning'},
walletTypes: [{label: 'Lightning Wallet', value: 'lightning'}],
isUserAuthorized: false,
isSatsDenomination: WINDOW_SETTINGS['LNBITS_DENOMINATION'] == 'sats',
allowedThemes: WINDOW_SETTINGS['LNBITS_THEME_OPTIONS'],
walletEventListeners: [],
darkChoice: this.$q.localStorage.has('lnbits.darkMode')
? this.$q.localStorage.getItem('lnbits.darkMode')
: true,
borderChoice: this.$q.localStorage.has('lnbits.border')
? this.$q.localStorage.getItem('lnbits.border')
: USE_DEFAULT_BORDER,
gradientChoice: this.$q.localStorage.has('lnbits.gradientBg')
? this.$q.localStorage.getItem('lnbits.gradientBg')
: USE_DEFAULT_GRADIENT,
themeChoice: this.$q.localStorage.has('lnbits.theme')
? this.$q.localStorage.getItem('lnbits.theme')
: USE_DEFAULT_THEME,
reactionChoice: this.$q.localStorage.has('lnbits.reactions')
? this.$q.localStorage.getItem('lnbits.reactions')
: USE_DEFAULT_REACTION,
bgimageChoice: this.$q.localStorage.has('lnbits.backgroundImage')
? this.$q.localStorage.getItem('lnbits.backgroundImage')
: USE_DEFAULT_BGIMAGE,
...WINDOW_SETTINGS
}
},
@ -131,56 +112,6 @@ window.windowMixin = {
return LNbits.utils.formatSat(amount) + ' sats'
}
},
changeTheme(newValue) {
document.body.setAttribute('data-theme', newValue)
this.$q.localStorage.set('lnbits.theme', newValue)
this.themeChoice = newValue
},
applyGradient() {
if (this.gradientChoice) {
document.body.classList.add('gradient-bg')
this.$q.localStorage.set('lnbits.gradientBg', true)
// Ensure dark mode is enabled when gradient background is applied
if (!this.$q.dark.isActive) {
this.toggleDarkMode()
}
} else {
document.body.classList.remove('gradient-bg')
this.$q.localStorage.set('lnbits.gradientBg', false)
}
},
applyBackgroundImage() {
if (this.bgimageChoice == 'null') this.bgimageChoice = ''
if (this.bgimageChoice == '') {
document.body.classList.remove('bg-image')
} else {
document.body.classList.add('bg-image')
document.body.style.setProperty(
'--background',
`url(${this.bgimageChoice})`
)
}
this.$q.localStorage.set('lnbits.backgroundImage', this.bgimageChoice)
},
applyBorder() {
// Remove any existing border classes
document.body.classList.forEach(cls => {
if (cls.endsWith('-border')) {
document.body.classList.remove(cls)
}
})
this.$q.localStorage.setItem('lnbits.border', this.borderChoice)
document.body.classList.add(this.borderChoice)
},
toggleDarkMode() {
this.$q.dark.toggle()
this.darkChoice = this.$q.dark.isActive
this.$q.localStorage.set('lnbits.darkMode', this.$q.dark.isActive)
if (!this.$q.dark.isActive) {
this.gradientChoice = false
this.applyGradient()
}
},
copyText(text, message, position) {
Quasar.copyToClipboard(text).then(() => {
Quasar.Notify.create({
@ -189,77 +120,6 @@ window.windowMixin = {
})
})
},
async checkUsrInUrl() {
try {
const params = new URLSearchParams(window.location.search)
const usr = params.get('usr')
if (!usr) {
return
}
if (!this.isUserAuthorized) {
await LNbits.api.loginUsr(usr)
}
params.delete('usr')
const cleanQueryPrams = params.size ? `?${params.toString()}` : ''
window.history.replaceState(
{},
document.title,
window.location.pathname + cleanQueryPrams
)
} finally {
this.isUserAuthorized = !!this.$q.cookies.get(
'is_lnbits_user_authorized'
)
}
},
themeParams() {
const url = new URL(window.location.href)
const params = new URLSearchParams(window.location.search)
const fields = ['theme', 'dark', 'gradient']
const toBoolean = value =>
value.trim().toLowerCase() === 'true' || value === '1'
// Check if any of the relevant parameters ('theme', 'dark', 'gradient') are present in the URL.
if (fields.some(param => params.has(param))) {
const theme = params.get('theme')
const darkMode = params.get('dark')
const gradient = params.get('gradient')
const border = params.get('border')
if (theme && this.allowedThemes.includes(theme.trim().toLowerCase())) {
const normalizedTheme = theme.trim().toLowerCase()
document.body.setAttribute('data-theme', normalizedTheme)
this.$q.localStorage.set('lnbits.theme', normalizedTheme)
}
if (darkMode) {
const isDark = toBoolean(darkMode)
this.$q.localStorage.set('lnbits.darkMode', isDark)
if (!isDark) {
this.$q.localStorage.set('lnbits.gradientBg', false)
}
}
if (gradient) {
const isGradient = toBoolean(gradient)
this.$q.localStorage.set('lnbits.gradientBg', isGradient)
if (isGradient) {
this.$q.localStorage.set('lnbits.darkMode', true)
}
}
if (border) {
this.$q.localStorage.set('lnbits.border', border)
}
// Remove processed parameters
fields.forEach(param => params.delete(param))
window.history.replaceState(null, null, url.pathname)
}
},
refreshRoute() {
const path = window.location.pathname
console.log(path)
@ -270,19 +130,6 @@ window.windowMixin = {
}
},
async created() {
this.$q.dark.set(
this.$q.localStorage.has('lnbits.darkMode')
? this.$q.localStorage.getItem('lnbits.darkMode')
: true
)
Chart.defaults.color = this.$q.dark.isActive ? '#fff' : '#000'
this.changeTheme(this.themeChoice)
this.applyBorder()
if (this.$q.dark.isActive) {
this.applyGradient()
}
this.applyBackgroundImage()
addEventListener('offline', event => {
console.log('offline', event)
this.g.offline = true
@ -296,6 +143,7 @@ window.windowMixin = {
if (window.user) {
this.g.user = Vue.reactive(window.LNbits.map.user(window.user))
}
if (this.g.user?.extra?.wallet_invite_requests?.length) {
this.walletTypes.push({
label: `Lightning Wallet (Share Invite: ${this.g.user.extra.wallet_invite_requests.length})`,
@ -308,8 +156,6 @@ window.windowMixin = {
if (window.extensions) {
this.g.extensions = Vue.reactive(window.extensions)
}
await this.checkUsrInUrl()
this.themeParams()
if (
this.$q.screen.gt.sm ||
this.$q.localStorage.getItem('lnbits.mobileSimple') == false

View file

@ -23,6 +23,11 @@
}
body.gradient-bg {
background-image: linear-gradient(to bottom right, #fff, var(--q-primary));
background-attachment: fixed;
}
body.gradient-bg.body--dark {
background-image: linear-gradient(
to bottom right,
var(--q-dark-page),

View file

@ -75,6 +75,7 @@
"js/components/lnbits-header.js",
"js/components/lnbits-header-wallets.js",
"js/components/lnbits-drawer.js",
"js/components/lnbits-theme.js",
"js/components/lnbits-manage-extension-list.js",
"js/components/lnbits-language-dropdown.js",
"js/components/extension-settings.js",

View file

@ -33,6 +33,7 @@
<body data-theme="bitcoin">
<div id="vue">
<q-layout view="hHh lpR lfr" v-cloak>
<lnbits-theme></lnbits-theme>
<lnbits-header></lnbits-header>
{% block drawer %}
<lnbits-drawer></lnbits-drawer>

View file

@ -73,7 +73,7 @@
<lnbits-language-dropdown></lnbits-language-dropdown>
<q-btn-dropdown
v-if="g.user || isUserAuthorized"
v-if="g.user || g.isUserAuthorized"
flat
rounded
size="sm"

View file

@ -391,93 +391,17 @@
</div>
<div class="col-8">
<q-btn
v-if="allowedThemes.includes('classic')"
v-for="theme in themeOptions"
:key="theme.name"
@click="g.themeChoice = theme.name"
:color="theme.color"
dense
flat
@click="changeTheme('classic')"
icon="circle"
color="deep-purple"
size="md"
><q-tooltip>classic</q-tooltip>
</q-btn>
<q-btn
v-if="allowedThemes.includes('bitcoin')"
dense
flat
@click="changeTheme('bitcoin')"
icon="circle"
color="deep-orange"
size="md"
><q-tooltip>bitcoin</q-tooltip>
</q-btn>
<q-btn
v-if="allowedThemes.includes('mint')"
dense
flat
@click="changeTheme('mint')"
icon="circle"
color="green"
size="md"
><q-tooltip>mint</q-tooltip> </q-btn
><q-btn
v-if="allowedThemes.includes('autumn')"
dense
flat
@click="changeTheme('autumn')"
icon="circle"
color="brown"
size="md"
><q-tooltip>autumn</q-tooltip>
</q-btn>
<q-btn
v-if="allowedThemes.includes('monochrome')"
dense
flat
@click="changeTheme('monochrome')"
icon="circle"
color="grey"
size="md"
><q-tooltip>monochrome</q-tooltip>
</q-btn>
<q-btn
v-if="allowedThemes.includes('salvador')"
dense
flat
@click="changeTheme('salvador')"
icon="circle"
color="blue-10"
size="md"
><q-tooltip>elSalvador</q-tooltip>
</q-btn>
<q-btn
v-if="allowedThemes.includes('freedom')"
dense
flat
@click="changeTheme('freedom')"
icon="circle"
color="pink-13"
size="md"
><q-tooltip>freedom</q-tooltip>
</q-btn>
<q-btn
v-if="allowedThemes.includes('cyber')"
dense
flat
@click="changeTheme('cyber')"
icon="circle"
color="light-green-9"
size="md"
><q-tooltip>cyber</q-tooltip>
</q-btn>
<q-btn
v-if="allowedThemes.includes('flamingo')"
dense
flat
@click="changeTheme('flamingo')"
icon="circle"
color="pink-3"
size="md"
><q-tooltip>flamingo</q-tooltip>
><q-tooltip
><span v-text="theme.name"></span
></q-tooltip>
</q-btn>
</div>
</div>
@ -487,9 +411,8 @@
</div>
<div class="col-8">
<q-input
v-model="bgimageChoice"
v-model="g.bgimageChoice"
:label="$t('background_image')"
@update:model-value="applyBackgroundImage"
>
<q-tooltip
><span v-text="$t('background_image')"></span
@ -507,8 +430,7 @@
flat
round
icon="gradient"
v-model="gradientChoice"
@update:model-value="applyGradient"
v-model="g.gradientChoice"
>
<q-tooltip
><span v-text="$t('toggle_gradient')"></span
@ -526,8 +448,7 @@
dense
flat
round
v-model="darkChoice"
@click="toggleDarkMode"
v-model="g.darkChoice"
:icon="$q.dark.isActive ? 'brightness_3' : 'wb_sunny'"
size="sm"
>
@ -543,10 +464,9 @@
</div>
<div class="col-8">
<q-select
v-model="borderChoice"
v-model="g.borderChoice"
:options="borderOptions"
label="Borders"
@update:model-value="applyBorder"
>
<q-tooltip
><span v-text="$t('border_choices')"></span
@ -571,10 +491,9 @@
</div>
<div class="col-8">
<q-select
v-model="reactionChoice"
v-model="g.reactionChoice"
:options="reactionOptions"
label="Reactions"
@update:model-value="reactionChoiceFunc"
>
<q-tooltip
><span v-text="$t('payment_reactions')"></span

View file

@ -127,6 +127,7 @@
"js/components/lnbits-header.js",
"js/components/lnbits-header-wallets.js",
"js/components/lnbits-drawer.js",
"js/components/lnbits-theme.js",
"js/components/lnbits-manage-extension-list.js",
"js/components/lnbits-language-dropdown.js",
"js/components/extension-settings.js",