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
16 changed files with 221 additions and 278 deletions
+5 -5
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
+1 -1
View File
File diff suppressed because one or more lines are too long
+10 -10
View File
File diff suppressed because one or more lines are too long
+5
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;
}
+107
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()
}
})
+30 -3
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
+39
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)
)
}
}
+1 -9
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')
+1 -155
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
+5
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),
+1
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",
+1
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>
@@ -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"
+12 -93
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
+1
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",