v1 feat: Vue Routes (#2872)

This commit is contained in:
Arc 2025-01-16 11:23:34 +00:00 committed by GitHub
parent 5abfbdd07a
commit 6a08d20fe8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 1919 additions and 1597 deletions

2
.gitignore vendored
View File

@ -39,6 +39,8 @@ lnbits/static/bundle-components.js
lnbits/static/bundle.css
lnbits/static/bundle.min.js.old
lnbits/static/bundle.min.css.old
lnbits/static/bundle-components.min.js.old
lnbits/upgrades
docker
# Nix

View File

@ -1,7 +1,13 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
<div class="row q-col-gutter-md justify-center">
<div class="col q-mb-md">
{% if not ajax %} {% extends "base.html" %} {% endif %} {% from "macros.jinja"
import window_vars with context %} {% block scripts %} {{ window_vars(user) }}
{% endblock %} {% block page %}
<div class="row q-col-gutter-md q-mb-md">
<div class="col-12">
<q-card>
<div class="q-pa-sm">
<div class="row items-center justify-between q-gutter-xs">
<div class="col">
<q-btn
:label="$t('save')"
color="primary"
@ -41,7 +47,11 @@
/>
</q-btn>
<q-btn :label="$t('download_backup')" flat @click="downloadBackup"></q-btn>
<q-btn
:label="$t('download_backup')"
flat
@click="downloadBackup"
></q-btn>
<q-btn
flat
@ -56,6 +66,11 @@
</q-tooltip>
</q-btn>
</div>
<div></div>
</div>
</div>
</q-card>
</div>
</div>
<div class="row q-col-gutter-md justify-center">
@ -213,7 +228,4 @@
</div>
</q-card>
</q-dialog>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script src="{{ static_url_for('static', 'js/admin.js') }}"></script>
{% endblock %}

View File

@ -1,12 +1,39 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
{% if not ajax %} {% extends "base.html" %} {% endif %}
<!---->
{% from "macros.jinja" import window_vars with context %}
<!---->
{% block scripts %} {{ window_vars(user) }} {% endblock %} {% block page %}
<div class="row q-col-gutter-md q-mb-md">
<div class="col-12">
<q-card>
<div class="q-pa-sm q-pl-lg">
<div class="row items-center justify-between q-gutter-xs">
<div class="col">
<!-- Optional: Add content here if needed -->
</div>
<div>
<q-btn
v-if="g.user.admin"
flat
round
icon="settings"
to="/admin#audit"
>
<q-tooltip v-text="$t('admin_settings')"></q-tooltip>
</q-btn>
</div>
</div>
</div>
</q-card>
</div>
</div>
<div class="row q-col-gutter-md justify-center q-mb-lg">
<div class="col-lg-3 col-md-6 col-sm-12 text-center">
<q-card class="q-pt-sm">
<strong v-text="$t('components')"></strong>
<div style="width: 250px" class="q-pa-sm">
<canvas ref="componentUseChart"></canvas>
<canvas v-if="chartsReady" ref="componentUseChart"></canvas>
</div>
</q-card>
</div>
@ -14,7 +41,7 @@
<q-card class="q-pt-sm">
<strong v-text="$t('long_running_endpoints')"></strong>
<div style="width: 250px; height: 250px" class="q-pa-sm">
<canvas ref="longDurationChart"></canvas>
<canvas v-if="chartsReady" ref="longDurationChart"></canvas>
</div>
</q-card>
</div>
@ -22,7 +49,7 @@
<q-card class="q-pt-sm">
<strong v-text="$t('http_request_methods')"></strong>
<div style="width: 250px; height: 250px" class="q-pa-sm">
<canvas ref="requestMethodChart"></canvas>
<canvas v-if="chartsReady" ref="requestMethodChart"></canvas>
</div>
</q-card>
</div>
@ -30,7 +57,7 @@
<q-card class="q-pt-sm">
<strong v-text="$t('http_response_codes')"></strong>
<div style="width: 250px; height: 250px" class="q-pa-sm">
<canvas ref="responseCodeChart"></canvas>
<canvas v-if="chartsReady" ref="responseCodeChart"></canvas>
</div>
</q-card>
</div>
@ -83,11 +110,6 @@
<span v-else v-text="col.label"></span>
</q-th>
<q-th>
<q-btn flat round icon="settings" tag="a" href="/admin#audit"
><q-tooltip v-text="$t('admin_settings')"></q-tooltip
></q-btn>
</q-th>
</q-tr>
</template>
<template v-slot:body="props">
@ -176,6 +198,4 @@
</q-card>
</q-dialog>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script src="{{ static_url_for('static', 'js/audit.js') }}"></script>
{% endblock %}

View File

@ -1,10 +1,8 @@
{% extends "base.html" %}
{% if not ajax %} {% extends "base.html" %} {% endif %}
<!---->
{% from "macros.jinja" import window_vars with context %}
<!---->
{% block scripts %} {{ window_vars(user) }}
<script src="{{ static_url_for('static', 'js/account.js') }}"></script>
{% endblock %} {% block page %}
{% block scripts %} {{ window_vars(user) }}{% endblock %} {% block page %}
<div class="row q-col-gutter-md">
<div v-if="user" class="col-12 col-md-6 q-gutter-y-md">
@ -292,8 +290,7 @@
flat
round
icon="settings"
tag="a"
href="/admin#site_customisation"
to="/admin#site_customisation"
><q-tooltip v-text="$t('admin_settings')"></q-tooltip
></q-btn>
<div class="row q-mb-md">

View File

@ -1,10 +1,14 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {{ window_vars(user, extensions) }}{% block page %}
{% if not ajax %} {% extends "base.html" %} {% endif %}
<!---->
{% from "macros.jinja" import window_vars with context %}
<!---->
{% block scripts %} {{ window_vars(user, wallet, extensions, extension_data)
}}{% endblock %} {% block page %}
<div class="row q-col-gutter-md q-mb-md">
<div class="col-12">
<q-card>
<div class="q-pa-xs">
<div>
<div class="q-gutter-y-md">
<q-tabs
:model-value="tab"
@ -54,8 +58,7 @@
flat
round
icon="settings"
tag="a"
href="/admin#extensions"
to="/admin#extensions"
><q-tooltip v-text="$t('admin_settings')"></q-tooltip
></q-btn>
</q-tabs>
@ -66,6 +69,7 @@
</div>
<div class="row q-col-gutter-md">
<div
v-if="filteredExtensions"
class="col-12 col-sm-6 col-md-6 col-lg-4"
v-for="extension in filteredExtensions"
:key="extension.id + extension.hash"
@ -114,6 +118,7 @@
></q-tooltip>
</q-badge>
<div
v-if="extension.name"
class="text-h5"
style="cursor: pointer"
@click="showExtensionDetails(extension.id, extension.details_link)"
@ -186,7 +191,7 @@
<div class="col-10">
<div v-if="!extension.inProgress">
<q-btn
v-if="user.extensions.includes(extension.id) && extension.isActive && extension.isInstalled"
v-if="g.user.extensions.includes(extension.id) && extension.isActive && extension.isInstalled"
flat
color="primary"
type="a"
@ -194,20 +199,20 @@
:label="$t('open')"
></q-btn>
<q-btn
v-if="user.extensions.includes(extension.id) && extension.isActive && extension.isInstalled"
v-if="g.user.extensions.includes(extension.id) && extension.isActive && extension.isInstalled"
flat
color="grey-5"
@click="disableExtension(extension)"
:label="$t('disable')"
></q-btn>
<q-badge
v-if="extension.isAdminOnly && !user.admin"
v-if="extension.isAdminOnly && !g.user.admin"
v-text="$t('admin_only')"
>
</q-badge>
<q-btn
v-else-if="extension.isInstalled && extension.isActive && !user.extensions.includes(extension.id)"
v-else-if="extension.isInstalled && extension.isActive && !g.user.extensions.includes(extension.id)"
flat
color="primary"
@click="enableExtensionForUser(extension)"
@ -980,674 +985,4 @@
</div>
</q-card>
</q-dialog>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
window.app = Vue.createApp({
el: '#vue',
data: function () {
return {
slide: 0,
fullscreen: false,
autoplay: true,
searchTerm: '',
tab: 'all',
manageExtensionTab: 'releases',
filteredExtensions: null,
updatableExtensions: [],
showUninstallDialog: false,
showManageExtensionDialog: false,
showExtensionDetailsDialog: false,
showDropDbDialog: false,
showPayToEnableDialog: false,
showUpdateAllDialog: false,
dropDbExtensionId: '',
selectedExtension: null,
selectedImage: null,
selectedExtensionDetails: null,
selectedExtensionRepos: null,
selectedRelease: null,
uninstallAndDropDb: false,
maxStars: 5,
paylinkWebsocket: null,
user: null
}
},
watch: {
searchTerm(term) {
this.filterExtensions(term, this.tab)
}
},
methods: {
handleTabChanged: function (tab) {
this.filterExtensions(this.searchTerm, tab)
},
filterExtensions: function (term, tab) {
// Filter the extensions list
function extensionNameContains(searchTerm) {
return function (extension) {
return (
extension.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
extension.shortDescription
?.toLowerCase()
.includes(searchTerm.toLowerCase())
)
}
}
this.filteredExtensions = this.extensions
.filter(e => (tab === 'all' ? !e.isInstalled : true))
.filter(e => (tab === 'installed' ? e.isInstalled : true))
.filter(e =>
tab === 'installed'
? e.isActive
? true
: !!this.g.user.admin
: true
)
.filter(e => (tab === 'featured' ? e.isFeatured : true))
.filter(extensionNameContains(term))
.map(e => ({
...e,
details_link:
e.installedRelease?.details_link || e.latestRelease?.details_link
}))
this.tab = tab
},
installExtension: async function (release) {
// no longer required to check if the invoice was paid
// the install logic has been triggered one way or another
this.unsubscribeFromPaylinkWs()
const extension = this.selectedExtension
extension.inProgress = true
this.showManageExtensionDialog = false
release.payment_hash =
release.payment_hash || this.getPaylinkHash(release.pay_link)
LNbits.api
.request(
'POST',
`/api/v1/extension`,
this.g.user.wallets[0].adminkey,
{
ext_id: extension.id,
archive: release.archive,
source_repo: release.source_repo,
payment_hash: release.payment_hash,
version: release.version
}
)
.then(response => {
extension.isAvailable = true
extension.isInstalled = true
extension.installedRelease = release
this.toggleExtension(extension)
extension.inProgress = false
this.filteredExtensions = this.extensions.concat([])
this.handleTabChanged('installed')
this.tab = 'installed'
window.location.reload()
})
.catch(err => {
console.warn(err)
extension.inProgress = false
LNbits.utils.notifyApiError(err)
})
},
uninstallExtension: async function () {
const extension = this.selectedExtension
this.showManageExtensionDialog = false
this.showUninstallDialog = false
extension.inProgress = true
LNbits.api
.request(
'DELETE',
`/api/v1/extension/${extension.id}`,
this.g.user.wallets[0].adminkey
)
.then(response => {
extension.isAvailable = false
extension.isInstalled = false
extension.inProgress = false
extension.installedRelease = null
this.filteredExtensions = this.extensions.concat([])
this.handleTabChanged('installed')
this.tab = 'installed'
Quasar.Notify.create({
type: 'positive',
message: 'Extension uninstalled!'
})
if (this.uninstallAndDropDb) {
this.showDropDb()
} else {
setTimeout(() => {
window.location.reload()
}, 300)
}
})
.catch(err => {
LNbits.utils.notifyApiError(err)
extension.inProgress = false
})
},
dropExtensionDb: async function () {
const extension = this.selectedExtension
this.showManageExtensionDialog = false
this.showDropDbDialog = false
this.dropDbExtensionId = ''
extension.inProgress = true
LNbits.api
.request(
'DELETE',
`/api/v1/extension/${extension.id}/db`,
this.g.user.wallets[0].adminkey
)
.then(response => {
extension.installedRelease = null
extension.inProgress = false
extension.hasDatabaseTables = false
Quasar.Notify.create({
type: 'positive',
message: 'Extension DB deleted!'
})
setTimeout(() => {
window.location.reload()
}, 300)
})
.catch(err => {
LNbits.utils.notifyApiError(err)
extension.inProgress = false
})
},
toggleExtension(extension) {
const action = extension.isActive ? 'activate' : 'deactivate'
LNbits.api
.request(
'PUT',
`/api/v1/extension/${extension.id}/${action}`,
this.g.user.wallets[0].adminkey
)
.then(response => {
Quasar.Notify.create({
timeout: 2000,
type: 'positive',
message: `Extension '${extension.id}' ${action}d!`
})
})
.catch(err => {
LNbits.utils.notifyApiError(err)
extension.isActive = false
extension.inProgress = false
})
},
enableExtensionForUser: function (extension) {
if (extension.isPaymentRequired) {
this.showPayToEnable(extension)
return
}
this.enableExtension(extension)
},
enableExtension: function (extension) {
LNbits.api
.request(
'PUT',
`/api/v1/extension/${extension.id}/enable`,
this.g.user.wallets[0].adminkey
)
.then(response => {
Quasar.Notify.create({
type: 'positive',
message: 'Extension enabled!'
})
setTimeout(() => {
window.location.reload()
}, 300)
})
.catch(err => {
console.warn(err)
LNbits.utils.notifyApiError(err)
})
},
disableExtension: function (extension) {
LNbits.api
.request(
'PUT',
`/api/v1/extension/${extension.id}/disable`,
this.g.user.wallets[0].adminkey
)
.then(response => {
Quasar.Notify.create({
type: 'positive',
message: 'Extension disabled!'
})
setTimeout(() => {
window.location.reload()
}, 300)
})
.catch(err => {
console.warn(error)
LNbits.utils.notifyApiError(err)
})
},
showPayToEnable: function (extension) {
this.selectedExtension = extension
this.selectedExtension.payToEnable.paidAmount =
extension.payToEnable.amount
this.selectedExtension.payToEnable.showQRCode = false
this.showPayToEnableDialog = true
},
updatePayToInstallData: function (extension) {
LNbits.api
.request(
'PUT',
`/api/v1/extension/${extension.id}/sell`,
this.g.user.wallets[0].adminkey,
{
required: extension.payToEnable.required,
amount: extension.payToEnable.amount,
wallet: extension.payToEnable.wallet
}
)
.then(response => {
Quasar.Notify.create({
type: 'positive',
message: 'Payment info updated!'
})
this.showManageExtensionDialog = false
})
.catch(err => {
LNbits.utils.notifyApiError(err)
extension.inProgress = false
})
},
showUninstall: function () {
this.showManageExtensionDialog = false
this.showUninstallDialog = true
this.uninstallAndDropDb = false
},
showDropDb: function () {
this.showDropDbDialog = true
},
showManageExtension: async function (extension) {
this.selectedExtension = extension
this.selectedRelease = null
this.selectedExtensionRepos = null
this.manageExtensionTab = 'releases'
this.showManageExtensionDialog = true
try {
const {data} = await LNbits.api.request(
'GET',
`/api/v1/extension/${extension.id}/releases`,
this.g.user.wallets[0].adminkey
)
this.selectedExtensionRepos = data.reduce((repos, release) => {
repos[release.source_repo] = repos[release.source_repo] || {
releases: [],
isInstalled: false,
repo: release.repo
}
release.inProgress = false
release.error = null
release.loaded = false
release.isInstalled = this.isInstalledVersion(
this.selectedExtension,
release
)
if (release.isInstalled) {
repos[release.source_repo].isInstalled = true
}
if (release.pay_link) {
release.requiresPayment = true
release.paidAmount = release.cost_sats
release.payment_hash = this.getPaylinkHash(release.pay_link)
}
repos[release.source_repo].releases.push(release)
return repos
}, {})
} catch (error) {
LNbits.utils.notifyApiError(error)
extension.inProgress = false
}
},
showExtensionDetails: async function (extId, detailsLink) {
if (!detailsLink) {
return
}
this.selectedExtensionDetails = null
this.showExtensionDetailsDialog = true
this.slide = 0
this.fullscreen = false
try {
const {data} = await LNbits.api.request(
'GET',
`/api/v1/extension/${extId}/details?details_link=${detailsLink}`,
this.g.user.wallets[0].inkey
)
this.selectedExtensionDetails = data
this.selectedExtensionDetails.description_md =
LNbits.utils.convertMarkdown(data.description_md)
} catch (error) {
console.warn(error)
}
},
async payAndInstall(release) {
try {
this.selectedExtension.inProgress = true
this.showManageExtensionDialog = false
const paymentInfo = await this.requestPaymentForInstall(
this.selectedExtension.id,
release
)
this.rememberPaylinkHash(release.pay_link, paymentInfo.payment_hash)
const wallet = this.g.user.wallets.find(w => w.id === release.wallet)
const {data} = await LNbits.api.payInvoice(
wallet,
paymentInfo.payment_request
)
release.payment_hash = data.payment_hash
await this.installExtension(release)
} catch (err) {
console.warn(err)
LNbits.utils.notifyApiError(err)
} finally {
this.selectedExtension.inProgress = false
}
},
async payAndEnable(extension) {
try {
const paymentInfo = await this.requestPaymentForEnable(
extension.id,
extension.payToEnable.paidAmount
)
const wallet = this.g.user.wallets.find(
w => w.id === extension.payToEnable.paymentWallet
)
const {data} = await LNbits.api.payInvoice(
wallet,
paymentInfo.payment_request
)
this.enableExtension(extension)
this.showPayToEnableDialog = false
} catch (err) {
console.warn(err)
LNbits.utils.notifyApiError(err)
}
},
async showInstallQRCode(release) {
this.selectedRelease = release
try {
const data = await this.requestPaymentForInstall(
this.selectedExtension.id,
release
)
this.selectedRelease.paymentRequest = data.payment_request
this.selectedRelease.payment_hash = data.payment_hash
this.selectedRelease = _.clone(this.selectedRelease)
this.rememberPaylinkHash(
this.selectedRelease.pay_link,
this.selectedRelease.payment_hash
)
this.subscribeToPaylinkWs(
this.selectedRelease.pay_link,
data.payment_hash
)
} catch (err) {
console.warn(err)
LNbits.utils.notifyApiError(err)
}
},
async showEnableQRCode(extension) {
try {
extension.payToEnable.showQRCode = true
this.selectedExtension = _.clone(extension)
const data = await this.requestPaymentForEnable(
extension.id,
extension.payToEnable.paidAmount
)
extension.payToEnable.paymentRequest = data.payment_request
this.selectedExtension = _.clone(extension)
const url = new URL(window.location)
url.protocol = url.protocol === 'https:' ? 'wss' : 'ws'
url.pathname = `/api/v1/ws/${data.payment_hash}`
const ws = new WebSocket(url)
ws.addEventListener('message', async ({data}) => {
const payment = JSON.parse(data)
if (payment.pending === false) {
Quasar.Notify.create({
type: 'positive',
message: 'Invoice Paid!'
})
this.enableExtension(extension)
ws.close()
}
})
} catch (err) {
console.warn(err)
LNbits.utils.notifyApiError(err)
}
},
async requestPaymentForInstall(extId, release) {
const {data} = await LNbits.api.request(
'PUT',
`/api/v1/extension/${extId}/invoice/install`,
this.g.user.wallets[0].adminkey,
{
ext_id: extId,
archive: release.archive,
source_repo: release.source_repo,
cost_sats: release.paidAmount,
version: release.version
}
)
return data
},
async requestPaymentForEnable(extId, amount) {
const {data} = await LNbits.api.request(
'PUT',
`/api/v1/extension/${extId}/invoice/enable`,
this.g.user.wallets[0].adminkey,
{
amount
}
)
return data
},
clearHangingInvoice(release) {
this.forgetPaylinkHash(release.pay_link)
release.payment_hash = null
},
rememberPaylinkHash(pay_link, payment_hash) {
this.$q.localStorage.set(
`lnbits.extensions.paylink.${pay_link}`,
payment_hash
)
},
getPaylinkHash(pay_link) {
return this.$q.localStorage.getItem(
`lnbits.extensions.paylink.${pay_link}`
)
},
forgetPaylinkHash(pay_link) {
this.$q.localStorage.remove(`lnbits.extensions.paylink.${pay_link}`)
},
subscribeToPaylinkWs(pay_link, payment_hash) {
const url = new URL(`${pay_link}/${payment_hash}`)
url.protocol = url.protocol === 'https:' ? 'wss' : 'ws'
this.paylinkWebsocket = new WebSocket(url)
this.paylinkWebsocket.addEventListener('message', async ({data}) => {
const resp = JSON.parse(data)
if (resp.paid) {
Quasar.Notify.create({
type: 'positive',
message: 'Invoice Paid!'
})
this.installExtension(this.selectedRelease)
} else {
Quasar.Notify.create({
type: 'warning',
message: 'Invoice tracking lost!'
})
}
})
},
unsubscribeFromPaylinkWs() {
try {
this.paylinkWebsocket && this.paylinkWebsocket.close()
} catch (error) {
console.warn(error)
}
},
hasNewVersion: function (extension) {
if (extension.installedRelease && extension.latestRelease) {
return (
extension.installedRelease.version !==
extension.latestRelease.version
)
}
},
isInstalledVersion: function (extension, release) {
if (extension.installedRelease) {
return (
extension.installedRelease.source_repo === release.source_repo &&
extension.installedRelease.version === release.version
)
}
},
getReleaseIcon: function (release) {
if (!release.is_version_compatible) return 'block'
if (release.isInstalled) return 'download_done'
return 'download'
},
getReleaseIconColor: function (release) {
if (!release.is_version_compatible) return 'text-red'
if (release.isInstalled) return 'text-green'
return ''
},
getGitHubReleaseDetails: async function (release) {
if (!release.is_github_release || release.loaded) {
return
}
const [org, repo] = release.source_repo.split('/')
release.inProgress = true
try {
const {data} = await LNbits.api.request(
'GET',
`/api/v1/extension/release/${org}/${repo}/${release.version}`,
this.g.user.wallets[0].adminkey
)
release.loaded = true
release.is_version_compatible = data.is_version_compatible
release.min_lnbits_version = data.min_lnbits_version
release.warning = data.warning
} catch (error) {
console.warn(error)
release.error = error
LNbits.utils.notifyApiError(error)
} finally {
release.inProgress = false
}
},
selectAllUpdatableExtensionss: async function () {
this.updatableExtensions.forEach(e => (e.selectedForUpdate = true))
},
updateSelectedExtensions: async function () {
let count = 0
for (const ext of this.updatableExtensions) {
try {
if (!ext.selectedForUpdate) {
continue
}
ext.inProgress = true
await LNbits.api.request(
'POST',
`/api/v1/extension`,
this.g.user.wallets[0].adminkey,
{
ext_id: ext.id,
archive: ext.latestRelease.archive,
source_repo: ext.latestRelease.source_repo,
payment_hash: ext.latestRelease.payment_hash,
version: ext.latestRelease.version
}
)
count++
ext.isAvailable = true
ext.isInstalled = true
ext.isUpgraded = true
ext.inProgress = false
ext.installedRelease = ext.latestRelease
this.toggleExtension(ext)
} catch (err) {
console.warn(err)
Quasar.Notify.create({
type: 'negative',
message: `Failed to update ${ext.code}!`
})
} finally {
ext.inProgress = false
}
}
Quasar.Notify.create({
type: 'positive',
message: `${count} extensions updated!`
})
this.showUpdateAllDialog = false
setTimeout(() => {
window.location.reload()
}, 2000)
}
},
created: function () {
this.extensions = JSON.parse('{{extensions | tojson | safe}}').map(e => ({
...e,
inProgress: false,
selectedForUpdate: false
}))
this.filteredExtensions = this.extensions.concat([])
for (let i = 0; i < this.filteredExtensions.length; i++) {
if (this.filteredExtensions[i].isInstalled != false) {
this.handleTabChanged('installed')
this.tab = 'installed'
}
}
if (window.user) {
this.user = LNbits.map.user(window.user)
}
this.updatableExtensions = this.extensions.filter(ext =>
this.hasNewVersion(ext)
)
},
mixins: [windowMixin]
})
</script>
{% endblock %}

View File

@ -1,14 +1,9 @@
{% extends "base.html" %}
{% if not ajax %} {% extends "base.html" %} {% endif %}
<!---->
{% from "macros.jinja" import window_vars with context %}
<!---->
{% block scripts %} {{ window_vars(user, wallet) }}
<script src="{{ static_url_for('static', 'js/wallet.js') }}"></script>
{% endblock %}
<!---->
{% block title %}{{ wallet_name }} - {{ SITE_TITLE }} {% endblock %}
<!---->
{% block page %}
{% block scripts %} {{ window_vars(user, wallet) }}{% endblock %} {% block page
%}
<div class="row q-col-gutter-md">
{% if HIDE_API and AD_SPACE %}
<div class="col-12 col-md-8 q-gutter-y-md">
@ -25,68 +20,6 @@
} : ''"
>
{% endif %}
<q-scroll-area
v-if="!mobileSimple"
style="
height: 115px;
width: 100%;
overflow-x: auto;
overflow-y: hidden;
"
>
<div class="row no-wrap q-gutter-md q-pr-md">
<q-card
v-for="wallet in g.user.wallets"
:key="wallet.id"
class="wallet-list-card"
bordered
tag="a"
:href="wallet.url"
:style="
g.wallet && g.wallet.id === wallet.id
? `border: 1px solid ${primaryColor}; width: 250px; text-decoration: none;`
: 'width: 250px; text-decoration: none;'
"
:class="{
'active-wallet-card': g.wallet && g.wallet.id === wallet.id
}"
>
<q-card-section>
<div class="row items-center">
<q-avatar
size="lg"
:color="
g.wallet && g.wallet.id === wallet.id
? $q.dark.isActive
? 'primary'
: 'primary'
: 'grey-5'
"
>
<q-icon
name="flash_on"
:size="$q.dark.isActive ? '21px' : '20px'"
:color="$q.dark.isActive ? 'black' : 'grey-3'"
></q-icon>
</q-avatar>
<div
class="text-h6 q-pl-md"
:class="{
'text-bold': g.wallet && g.wallet.id === wallet.id
}"
v-text="wallet.name"
></div>
</div>
<div class="row items-center q-pt-sm">
<h6 class="q-my-none text-no-wrap">
<strong v-text="wallet.fsat"></strong>
<small> {{LNBITS_DENOMINATION}}</small>
</h6>
</div>
</q-card-section>
</q-card>
</div>
</q-scroll-area>
<q-card
:style="$q.screen.lt.md ? {
background: $q.screen.lt.md ? 'none !important': ''
@ -101,7 +34,6 @@
<small> {{LNBITS_DENOMINATION}}</small>
<lnbits-update-balance
:wallet_id="this.g.wallet.id"
@credit-value="handleBalanceUpdate"
class="q-ml-md"
></lnbits-update-balance>
</h3>
@ -117,7 +49,7 @@
</div>
<div class="col">
<q-btn
@click="mobileSimple = !mobileSimple"
@click="simpleMobile()"
color="primary"
class="float-right lt-md"
size="sm"
@ -176,11 +108,15 @@
<q-card-section>
<payment-list
:update="updatePayments"
:wallet="this.g.wallet"
:mobile-simple="mobileSimple"
/>
</q-card-section>
</q-card>
<div id="hiddenQrCodeContainer" style="display: none">
<lnbits-qrcode
:value="'lightning:' + this.receive.paymentReq"
></lnbits-qrcode>
</div>
</div>
{% if HIDE_API %}
<div class="col-12 col-md-4 q-gutter-y-md">
@ -192,8 +128,8 @@
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-mt-none q-mb-sm">
{{ SITE_TITLE }} <span v-text="$t('wallet')"></span>
<strong><em>{{wallet_name}}</em></strong>
{{ SITE_TITLE }} Wallet:
<strong><em v-text="g.wallet.name"></em></strong>
</h6>
</q-card-section>
<q-card-section class="q-pa-none">
@ -228,7 +164,9 @@
<q-card>
<q-card-section class="text-center">
<p v-text="$t('export_to_phone_desc')"></p>
<lnbits-qrcode :value="exportUrl"></lnbits-qrcode>
<lnbits-qrcode
:value="`${baseUrl}/wallet?usr=${g.user.id}&wal=${g.wallet.id}`"
></lnbits-qrcode>
</q-card-section>
<span v-text="exportWalletQR"></span>
<q-card-actions class="flex-center q-pb-md">
@ -236,7 +174,7 @@
outline
color="grey"
:label="$t('copy_wallet_url')"
@click="copyText(exportUrl)"
@click="copyText(`${baseUrl}/wallet?usr=${g.user.id}&wal=${g.wallet.id}`)"
></q-btn>
</q-card-actions>
</q-card>
@ -295,6 +233,17 @@
:label="$t('update_currency')"
@click="updateWallet({ currency: update.currency || '' })"
></q-btn>
<q-btn
v-if="g.user.admin"
class="absolute-top-right"
flat
round
icon="settings"
to="/admin#exchange_providers"
><q-tooltip
v-text="$t('exchange_providers')"
></q-tooltip
></q-btn>
</q-card-section>
</q-card>
</q-expansion-item>
@ -445,9 +394,7 @@
>
<div class="text-center q-mb-lg">
<a :href="'lightning:' + receive.paymentReq">
<lnbits-qrcode
:value="'lightning:' + receive.paymentReq.toUpperCase()"
></lnbits-qrcode>
<div v-html="invoiceQrCode"></div>
</a>
</div>
<div class="text-center">

View File

@ -1,5 +1,8 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
{% if not ajax %} {% extends "base.html" %} {% endif %}
<!---->
{% from "macros.jinja" import window_vars with context %}
<!---->
{% block scripts %} {{ window_vars(user) }}{% endblock %} {% block page %}
<q-dialog v-model="nodeInfoDialog.show" position="top">
<lnbits-node-qrcode :info="nodeInfoDialog.data"></lnbits-node-qrcode>
@ -40,364 +43,4 @@
</q-card>
</div>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
window.app = Vue.createApp({
el: '#vue',
config: {
globalProperties: {
LNbits,
msg: 'hello'
}
},
mixins: [window.windowMixin],
data() {
return {
isSuperUser: false,
wallet: {},
tab: 'dashboard',
payments: 1000,
info: {},
channel_stats: {},
channels: {
data: [],
filter: ''
},
activeBalance: {},
ranks: {},
peers: {
data: [],
filter: ''
},
connectPeerDialog: {
show: false,
data: {}
},
setFeeDialog: {
show: false,
data: {
fee_ppm: 0,
fee_base_msat: 0
}
},
openChannelDialog: {
show: false,
data: {}
},
closeChannelDialog: {
show: false,
data: {}
},
nodeInfoDialog: {
show: false,
data: {}
},
transactionDetailsDialog: {
show: false,
data: {}
},
states: [
{label: 'Active', value: 'active', color: 'green'},
{label: 'Pending', value: 'pending', color: 'orange'},
{label: 'Inactive', value: 'inactive', color: 'grey'},
{label: 'Closed', value: 'closed', color: 'red'}
],
stateFilters: [
{label: 'Active', value: 'active'},
{label: 'Pending', value: 'pending'}
],
paymentsTable: {
data: [],
columns: [
{
name: 'pending',
label: ''
},
{
name: 'date',
align: 'left',
label: this.$t('date'),
field: 'date',
sortable: true
},
{
name: 'sat',
align: 'right',
label: this.$t('amount') + ' (' + LNBITS_DENOMINATION + ')',
field: row => this.formatMsat(row.amount),
sortable: true
},
{
name: 'fee',
align: 'right',
label: this.$t('fee') + ' (m' + LNBITS_DENOMINATION + ')',
field: 'fee'
},
{
name: 'destination',
align: 'right',
label: 'Destination',
field: 'destination'
},
{
name: 'memo',
align: 'left',
label: this.$t('memo'),
field: 'memo'
}
],
pagination: {
rowsPerPage: 10,
page: 1,
rowsNumber: 10
},
filter: null
},
invoiceTable: {
data: [],
columns: [
{
name: 'pending',
label: ''
},
{
name: 'paid_at',
field: 'paid_at',
align: 'left',
label: 'Paid at',
sortable: true
},
{
name: 'expiry',
label: this.$t('expiry'),
field: 'expiry',
align: 'left',
sortable: true
},
{
name: 'amount',
label: this.$t('amount') + ' (' + LNBITS_DENOMINATION + ')',
field: row => this.formatMsat(row.amount),
sortable: true
},
{
name: 'memo',
align: 'left',
label: this.$t('memo'),
field: 'memo'
}
],
pagination: {
rowsPerPage: 10,
page: 1,
rowsNumber: 10
},
filter: null
}
}
},
created() {
this.getInfo()
this.get1MLStats()
},
watch: {
tab(val) {
if (val === 'transactions' && !this.paymentsTable.data.length) {
this.getPayments()
this.getInvoices()
} else if (val === 'channels' && !this.channels.data.length) {
this.getChannels()
this.getPeers()
}
}
},
computed: {
checkChanges() {
return !_.isEqual(this.settings, this.formData)
},
filteredChannels() {
return this.stateFilters
? this.channels.data.filter(channel => {
return this.stateFilters.find(({value}) => value == channel.state)
})
: this.channels.data
},
totalBalance() {
return this.filteredChannels.reduce(
(balance, channel) => {
balance.local_msat += channel.balance.local_msat
balance.remote_msat += channel.balance.remote_msat
balance.total_msat += channel.balance.total_msat
return balance
},
{local_msat: 0, remote_msat: 0, total_msat: 0}
)
}
},
methods: {
formatMsat(msat) {
return LNbits.utils.formatMsat(msat)
},
api(method, url, options) {
const params = new URLSearchParams(options?.query)
return LNbits.api
.request(method, `/node/api/v1${url}?${params}`, {}, options?.data)
.catch(error => {
LNbits.utils.notifyApiError(error)
})
},
getChannel(channel_id) {
return this.api('GET', `/channels/${channel_id}`).then(response => {
this.setFeeDialog.data.fee_ppm = response.data.fee_ppm
this.setFeeDialog.data.fee_base_msat = response.data.fee_base_msat
})
},
getChannels() {
return this.api('GET', '/channels').then(response => {
this.channels.data = response.data
})
},
getInfo() {
return this.api('GET', '/info').then(response => {
this.info = response.data
this.channel_stats = response.data.channel_stats
})
},
get1MLStats() {
return this.api('GET', '/rank').then(response => {
this.ranks = response.data
})
},
getPayments(props) {
if (props) {
this.paymentsTable.pagination = props.pagination
}
let pagination = this.paymentsTable.pagination
const query = {
limit: pagination.rowsPerPage,
offset: (pagination.page - 1) * pagination.rowsPerPage ?? 0
}
return this.api('GET', '/payments', {query}).then(response => {
this.paymentsTable.data = response.data.data
this.paymentsTable.pagination.rowsNumber = response.data.total
})
},
getInvoices(props) {
if (props) {
this.invoiceTable.pagination = props.pagination
}
let pagination = this.invoiceTable.pagination
const query = {
limit: pagination.rowsPerPage,
offset: (pagination.page - 1) * pagination.rowsPerPage ?? 0
}
return this.api('GET', '/invoices', {query}).then(response => {
this.invoiceTable.data = response.data.data
this.invoiceTable.pagination.rowsNumber = response.data.total
})
},
getPeers() {
return this.api('GET', '/peers').then(response => {
this.peers.data = response.data
console.log('peers', this.peers)
})
},
connectPeer() {
this.api('POST', '/peers', {data: this.connectPeerDialog.data}).then(
() => {
this.connectPeerDialog.show = false
this.getPeers()
}
)
},
disconnectPeer(id) {
LNbits.utils
.confirmDialog('Do you really wanna disconnect this peer?')
.onOk(() => {
this.api('DELETE', `/peers/${id}`).then(response => {
Quasar.Notify.create({
message: 'Disconnected',
icon: null
})
this.needsRestart = true
this.getPeers()
})
})
},
setChannelFee(channel_id) {
this.api('PUT', `/channels/${channel_id}`, {
data: this.setFeeDialog.data
})
.then(response => {
this.setFeeDialog.show = false
this.getChannels()
})
.catch(LNbits.utils.notifyApiError)
},
openChannel() {
this.api('POST', '/channels', {data: this.openChannelDialog.data})
.then(response => {
this.openChannelDialog.show = false
this.getChannels()
})
.catch(error => {
console.log(error)
})
},
showCloseChannelDialog(channel) {
this.closeChannelDialog.show = true
this.closeChannelDialog.data = {
force: false,
short_id: channel.short_id,
...channel.point
}
},
closeChannel() {
this.api('DELETE', '/channels', {
query: this.closeChannelDialog.data
}).then(response => {
this.closeChannelDialog.show = false
this.getChannels()
})
},
showSetFeeDialog(channel_id) {
this.setFeeDialog.show = true
this.setFeeDialog.channel_id = channel_id
this.getChannel(channel_id)
},
showOpenChannelDialog(peer_id) {
this.openChannelDialog.show = true
this.openChannelDialog.data = {peer_id, funding_amount: 0}
},
showNodeInfoDialog(node) {
this.nodeInfoDialog.show = true
this.nodeInfoDialog.data = node
},
showTransactionDetailsDialog(details) {
this.transactionDetailsDialog.show = true
this.transactionDetailsDialog.data = details
console.log('details', details)
},
shortenNodeId(nodeId) {
return nodeId
? nodeId.substring(0, 5) + '...' + nodeId.substring(nodeId.length - 5)
: '...'
}
}
})
</script>
<script src="{{ static_url_for('static', 'js/node.js') }}"></script>
{% endblock %}

View File

@ -1,5 +1,8 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
{% if not ajax %} {% extends "base.html" %} {% endif %}
<!---->
{% from "macros.jinja" import window_vars with context %}
<!---->
{% block scripts %} {{ window_vars(user) }}{% endblock %} {% block page %}
<div class="row q-col-gutter-md justify-center">
<div class="col">
@ -11,13 +14,35 @@
{%include "users/_createWalletDialog.html" %}
</div>
<div v-else>
<div class="row q-col-gutter-md q-mb-md">
<div class="col-12">
<q-card>
<div class="q-pa-sm">
<div class="row items-center justify-between q-gutter-xs">
<div class="col">
<q-btn
@click="showAccountPage()"
:label="$t('create_account')"
color="primary"
class="q-mb-md"
>
</q-btn>
</div>
<div>
<q-btn
v-if="g.user.admin"
flat
round
icon="settings"
to="/admin#users"
>
<q-tooltip v-text="$t('admin_settings')"></q-tooltip>
</q-btn>
</div>
</div>
</div>
</q-card>
</div>
</div>
<q-card class="q-pa-md">
<q-table
@ -148,6 +173,4 @@
</div>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script src="{{ static_url_for('static', 'js/users.js') }}"></script>
{% endblock %}

View File

@ -107,7 +107,7 @@ async def extensions(request: Request, user: User = Depends(check_user_exists)):
inactive_extensions = [e.id for e in await get_installed_extensions(active=False)]
db_versions = await get_db_versions()
extensions = [
extension_data = [
{
"id": ext.id,
"name": ext.name,
@ -152,7 +152,8 @@ async def extensions(request: Request, user: User = Depends(check_user_exists)):
"core/extensions.html",
{
"user": user.json(),
"extensions": extensions,
"extension_data": extension_data,
"ajax": _is_ajax_request(request),
},
)
@ -182,11 +183,7 @@ async def wallet(
return template_renderer().TemplateResponse(
request, "error.html", {"err": "Wallet not found"}, HTTPStatus.NOT_FOUND
)
resp = template_renderer().TemplateResponse(
request,
"core/wallet.html",
{
context = {
"user": user.json(),
"wallet": wallet.json(),
"wallet_name": wallet.name,
@ -194,10 +191,13 @@ async def wallet(
"service_fee": settings.lnbits_service_fee,
"service_fee_max": settings.lnbits_service_fee_max,
"web_manifest": f"/manifest/{user.id}.webmanifest",
},
}
return template_renderer().TemplateResponse(
request,
"core/wallet.html",
{**context, "ajax": _is_ajax_request(request)},
)
resp.set_cookie("lnbits_last_active_wallet", wallet.id)
return resp
@generic_router.get(
@ -209,11 +209,13 @@ async def account(
request: Request,
user: User = Depends(check_user_exists),
):
return template_renderer().TemplateResponse(
request,
"core/account.html",
{
"user": user.json(),
"ajax": _is_ajax_request(request),
},
)
@ -327,6 +329,7 @@ async def node(request: Request, user: User = Depends(check_admin)):
"settings": settings.dict(),
"balance": balance,
"wallets": user.wallets[0].json(),
"ajax": _is_ajax_request(request),
},
)
@ -365,6 +368,7 @@ async def admin_index(request: Request, user: User = Depends(check_admin)):
"settings": settings.dict(),
"balance": balance,
"currencies": list(currencies.keys()),
"ajax": _is_ajax_request(request),
},
)
@ -381,6 +385,7 @@ async def users_index(request: Request, user: User = Depends(check_admin)):
"user": user.json(),
"settings": settings.dict(),
"currencies": list(currencies.keys()),
"ajax": _is_ajax_request(request),
},
)
@ -395,6 +400,7 @@ async def audit_index(request: Request, user: User = Depends(check_admin)):
{
"request": request,
"user": user.json(),
"ajax": _is_ajax_request(request),
},
)
@ -458,3 +464,7 @@ async def lnurlwallet(request: Request):
return RedirectResponse(
f"/wallet?usr={account.id}&wal={wallet.id}",
)
def _is_ajax_request(request: Request):
return request.headers.get("X-Requested-With", None) == "XMLHttpRequest"

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,4 @@
window.app = Vue.createApp({
el: '#vue',
window.AccountPageLogic = {
mixins: [window.windowMixin],
data() {
return {
@ -10,7 +9,8 @@ window.app = Vue.createApp({
'None',
'confettiBothSides',
'confettiFireworks',
'confettiStars'
'confettiStars',
'confettiTop'
],
borderOptions: ['retro-border', 'hard-border', 'no-border'],
tab: 'user',
@ -39,58 +39,12 @@ window.app = Vue.createApp({
this.toggleGradient()
}
},
applyGradient() {
darkBgColor = this.$q.localStorage.getItem('lnbits.darkBgColor')
primaryColor = this.$q.localStorage.getItem('lnbits.primaryColor')
if (this.gradientChoice) {
if (!this.$q.dark.isActive) {
this.toggleDarkMode()
}
const gradientStyle = `linear-gradient(to bottom right, ${LNbits.utils.hexDarken(String(primaryColor), -70)}, #0a0a0a)`
document.body.style.setProperty(
'background-image',
gradientStyle,
'important'
)
const gradientStyleCards = `background-color: ${LNbits.utils.hexAlpha(String(darkBgColor), 0.4)} !important`
const style = document.createElement('style')
style.innerHTML =
`body[data-theme="${this.$q.localStorage.getItem('lnbits.theme')}"] .q-card:not(.q-dialog .q-card, .lnbits__dialog-card, .q-dialog-plugin--dark), body.body${this.$q.dark.isActive ? '--dark' : ''} .q-header, body.body${this.$q.dark.isActive ? '--dark' : ''} .q-drawer { ${gradientStyleCards} }` +
`body[data-theme="${this.$q.localStorage.getItem('lnbits.theme')}"].body--dark{background: ${LNbits.utils.hexDarken(String(primaryColor), -88)} !important; }` +
`[data-theme="${this.$q.localStorage.getItem('lnbits.theme')}"] .q-card--dark{background: ${String(darkBgColor)} !important;} }`
document.head.appendChild(style)
this.$q.localStorage.set('lnbits.gradientBg', true)
} else {
this.$q.localStorage.set('lnbits.gradientBg', false)
}
},
applyBorder() {
if (this.borderChoice) {
this.$q.localStorage.setItem('lnbits.border', this.borderChoice)
}
let borderStyle = this.$q.localStorage.getItem('lnbits.border')
this.borderChoice = borderStyle
let borderStyleCSS
if (borderStyle == 'hard-border') {
borderStyleCSS = `box-shadow: 0 0 0 1px rgba(0,0,0,.12), 0 0 0 1px #ffffff47; border: none;`
}
if (borderStyle == 'no-border') {
borderStyleCSS = `box-shadow: none; border: none;`
}
if (borderStyle == 'retro-border') {
borderStyleCSS = `border: none; border-color: rgba(255, 255, 255, 0.28); box-shadow: 0 1px 5px rgba(255, 255, 255, 0.2), 0 2px 2px rgba(255, 255, 255, 0.14), 0 3px 1px -2px rgba(255, 255, 255, 0.12);`
}
let style = document.createElement('style')
style.innerHTML = `body[data-theme="${this.$q.localStorage.getItem('lnbits.theme')}"] .q-card.q-card--dark, .q-date--dark { ${borderStyleCSS} }`
document.head.appendChild(style)
},
toggleGradient() {
this.gradientChoice = !this.gradientChoice
this.applyGradient()
if (!this.gradientChoice) {
window.location.reload()
}
this.gradientChoice = this.$q.localStorage.getItem('lnbits.gradientBg')
},
reactionChoiceFunc() {
this.$q.localStorage.set('lnbits.reactions', this.reactionChoice)
@ -208,15 +162,9 @@ window.app = Vue.createApp({
} catch (e) {
LNbits.utils.notifyApiError(e)
}
if (this.$q.localStorage.getItem('lnbits.gradientBg')) {
this.applyGradient()
}
if (this.$q.localStorage.getItem('lnbits.border')) {
this.applyBorder()
}
const hash = window.location.hash.replace('#', '')
if (hash) {
this.tab = hash
}
}
})
}

View File

@ -1,5 +1,4 @@
window.app = Vue.createApp({
el: '#vue',
window.AdminPageLogic = {
mixins: [windowMixin],
data() {
return {
@ -35,7 +34,10 @@ window.app = Vue.createApp({
}
]
},
formData: {},
formData: {
lnbits_exchange_rate_providers: []
},
chartReady: false,
formAddAdmin: '',
formAddUser: '',
formAddExtensionsManifest: '',
@ -118,11 +120,14 @@ window.app = Vue.createApp({
}
}
},
created() {
this.getSettings()
this.getAudit()
async created() {
await this.getSettings()
await this.getAudit()
this.balance = +'{{ balance|safe }}'
const hash = window.location.hash.replace('#', '')
if (hash === 'exchange_providers') {
this.showExchangeProvidersTab(hash)
}
if (hash) {
this.tab = hash
}
@ -387,8 +392,8 @@ window.app = Vue.createApp({
})
}
},
getAudit() {
LNbits.api
async getAudit() {
await LNbits.api
.request('GET', '/admin/api/v1/audit', this.g.user.wallets[0].adminkey)
.then(response => {
this.auditData = response.data
@ -405,8 +410,8 @@ window.app = Vue.createApp({
LNbits.utils.notifyApiError(error)
})
},
getSettings() {
LNbits.api
async getSettings() {
await LNbits.api
.request(
'GET',
'/admin/api/v1/settings',
@ -513,4 +518,4 @@ window.app = Vue.createApp({
)
}
}
})
}

View File

@ -1,8 +1,8 @@
window.app = Vue.createApp({
el: '#vue',
window.AuditPageLogic = {
mixins: [window.windowMixin],
data() {
return {
chartsReady: false,
auditEntries: [],
searchData: {
user_id: '',
@ -96,12 +96,13 @@ window.app = Vue.createApp({
}
},
async created() {
async created() {},
async mounted() {
this.chartsReady = true // Allow the DOM to render the canvas elements
await this.$nextTick() // Wait for DOM updates before initializing charts
this.initCharts() // Initialize charts after DOM is ready
await this.fetchAudit()
},
mounted() {
this.initCharts()
},
methods: {
formatDate(dateString) {
@ -208,7 +209,11 @@ window.app = Vue.createApp({
}
return `${value.substring(0, 5)}...${value.substring(valueLength - 5, valueLength)}`
},
initCharts() {
async initCharts() {
if (!this.chartsReady) {
console.warn('Charts are not ready yet. Initialization delayed.')
return
}
this.responseCodeChart = new Chart(
this.$refs.responseCodeChart.getContext('2d'),
{
@ -383,4 +388,4 @@ window.app = Vue.createApp({
)
}
}
})
}

View File

@ -1,16 +1,5 @@
window.LOCALE = 'en'
window.dateFormat = 'YYYY-MM-DD HH:mm'
window.i18n = new VueI18n.createI18n({
locale: window.LOCALE,
fallbackLocale: window.LOCALE,
messages: window.localisation
})
const websocketPrefix =
window.location.protocol === 'http:' ? 'ws://' : 'wss://'
const websocketUrl = `${websocketPrefix}${window.location.host}/api/v1/ws`
window.LNbits = {
g: window.g,
api: {
request(method, url, apiKey, data) {
return axios({
@ -448,39 +437,115 @@ window.LNbits = {
}
}
window.windowMixin = {
i18n: window.i18n,
data() {
return {
toggleSubs: true,
reactionChoice: 'confettiBothSides',
borderChoice: '',
gradientChoice:
this.$q.localStorage.getItem('lnbits.gradientBg') || false,
isUserAuthorized: false,
g: {
if (!window.g) {
window.g = Vue.reactive({
offline: !navigator.onLine,
visibleDrawer: false,
extensions: [],
user: null,
wallet: null,
wallet: {},
wallets: [],
payments: [],
allowedThemes: null,
langs: []
})
}
window.windowMixin = {
inject: ['g'],
i18n: window.i18n,
data() {
return {
toggleSubs: true,
updatePayments: false,
updatePaymentsHash: '',
mobileSimple: true,
walletFlip: true,
reactionChoice: 'confettiTop',
borderChoice: '',
gradientChoice:
this.$q.localStorage.getItem('lnbits.gradientBg') || false,
isUserAuthorized: false,
eventListeners: []
}
},
methods: {
flipWallets(smallScreen) {
this.walletFlip = !this.walletFlip
if (this.walletFlip && smallScreen) {
this.g.visibleDrawer = false
}
this.$q.localStorage.set('lnbits.walletFlip', this.walletFlip)
},
simpleMobile() {
this.$q.localStorage.set('lnbits.mobileSimple', !this.mobileSimple)
this.refreshRoute()
},
paymentEvents() {
this.g.user.wallets.forEach(wallet => {
if (!this.eventListeners.includes(wallet.id)) {
this.eventListeners.push(wallet.id)
LNbits.events.onInvoicePaid(wallet, data => {
const walletIndex = this.g.user.wallets.findIndex(
w => w.id === wallet.id
)
if (walletIndex !== -1) {
//needed for balance being deducted
let satBalance = data.wallet_balance
if (data.payment.amount < 0) {
satBalance = data.wallet_balance += data.payment.amount / 1000
}
//update the wallet
Object.assign(this.g.user.wallets[walletIndex], {
sat: satBalance,
msat: data.wallet_balance * 1000,
fsat: data.wallet_balance.toLocaleString()
})
//update the current wallet
if (this.g.wallet.id === data.payment.wallet_id) {
Object.assign(this.g.wallet, this.g.user.wallets[walletIndex])
this.updatePayments = !this.updatePayments
//if on the wallet page and payment is incoming trigger the eventReaction
if (
data.payment.amount > 0 &&
window.location.pathname === '/wallet'
) {
eventReaction(data.wallet_balance * 1000)
}
}
}
this.updatePaymentsHash = data.payment.payment_hash
})
}
})
},
selectWallet(wallet) {
Object.assign(this.g.wallet, wallet)
// this.wallet = this.g.wallet
this.updatePayments = !this.updatePayments
this.balance = parseInt(wallet.balance_msat / 1000)
const currentPath = this.$route.path
if (currentPath !== '/wallet') {
this.$router.push({
path: '/wallet',
query: {wal: this.g.wallet.id}
})
} else {
const url = new URL(window.location.href)
url.searchParams.set('wal', this.g.wallet.id)
window.history.replaceState({}, '', url.toString())
}
},
changeColor(newValue) {
document.body.setAttribute('data-theme', newValue)
this.$q.localStorage.set('lnbits.theme', newValue)
},
applyGradient() {
if (this.$q.localStorage.getItem('lnbits.gradientBg')) {
this.setColors()
darkBgColor = this.$q.localStorage.getItem('lnbits.darkBgColor')
primaryColor = this.$q.localStorage.getItem('lnbits.primaryColor')
if (this.gradientChoice) {
this.$q.localStorage.set('lnbits.gradientBg', true)
const gradientStyle = `linear-gradient(to bottom right, ${LNbits.utils.hexDarken(String(primaryColor), -70)}, #0a0a0a)`
document.body.style.setProperty(
'background-image',
@ -494,6 +559,11 @@ window.windowMixin = {
`body[data-theme="${this.$q.localStorage.getItem('lnbits.theme')}"].body--dark{background: ${LNbits.utils.hexDarken(String(primaryColor), -88)} !important; }` +
`[data-theme="${this.$q.localStorage.getItem('lnbits.theme')}"] .q-card--dark{background: ${String(darkBgColor)} !important;} }`
document.head.appendChild(style)
} else {
this.$q.localStorage.set('lnbits.gradientBg', false)
}
if (!this.$q.dark.isActive) {
this.toggleDarkMode()
}
},
applyBorder() {
@ -502,7 +572,7 @@ window.windowMixin = {
}
let borderStyle = this.$q.localStorage.getItem('lnbits.border')
if (!borderStyle) {
this.$q.localStorage.set('lnbits.border', 'retro-border')
this.$q.localStorage.set('lnbits.border', 'hard-border')
borderStyle = 'hard-border'
}
this.borderChoice = borderStyle
@ -533,6 +603,14 @@ window.windowMixin = {
'lnbits.darkBgColor',
LNbits.utils.getPaletteColor('dark')
)
document.documentElement.style.setProperty(
'--q-primary',
LNbits.utils.getPaletteColor('primary')
)
document.documentElement.style.setProperty(
'--q-secondary',
LNbits.utils.getPaletteColor('secondary')
)
},
copyText(text, message, position) {
Quasar.copyToClipboard(text).then(() => {
@ -576,6 +654,7 @@ window.windowMixin = {
)
.onOk(async () => {
try {
this.$q.localStorage.remove('lnbits.disclaimerShown')
await LNbits.api.logout()
window.location = '/'
} catch (e) {
@ -632,6 +711,14 @@ window.windowMixin = {
}
this.setColors()
},
refreshRoute() {
const path = window.location.pathname
console.log(path)
this.$router.push('/temp').then(() => {
this.$router.replace({path})
})
}
},
async created() {
@ -644,7 +731,7 @@ window.windowMixin = {
this.$q.dark.set(true)
}
this.reactionChoice =
this.$q.localStorage.getItem('lnbits.reactions') || 'confettiBothSides'
this.$q.localStorage.getItem('lnbits.reactions') || 'confettiTop'
this.g.allowedThemes = window.allowedThemes ?? ['bitcoin']
@ -683,23 +770,24 @@ window.windowMixin = {
this.$q.localStorage.getItem('lnbits.theme')
)
}
this.applyGradient()
this.applyBorder()
if (window.user) {
this.g.user = Object.freeze(window.LNbits.map.user(window.user))
this.g.user = Vue.reactive(window.LNbits.map.user(window.user))
}
if (window.wallet) {
this.g.wallet = Object.freeze(window.LNbits.map.wallet(window.wallet))
this.g.wallet = Vue.reactive(window.LNbits.map.wallet(window.wallet))
}
if (window.extensions) {
const extensions = Object.freeze(window.extensions)
this.g.extensions = extensions
this.g.extensions = Vue.reactive(window.extensions)
}
await this.checkUsrInUrl()
this.themeParams()
this.walletFlip = this.$q.localStorage.getItem('lnbits.walletFlip')
this.mobileSimple = this.$q.localStorage.getItem('lnbits.mobileSimple')
},
mounted() {
this.paymentEvents()
}
}

View File

@ -22,11 +22,11 @@ window.app.component('lnbits-fsat', {
})
window.app.component('lnbits-wallet-list', {
mixins: [window.windowMixin],
template: '#lnbits-wallet-list',
props: ['balance'],
data() {
return {
user: null,
activeWallet: null,
balance: 0,
showForm: false,
@ -34,67 +34,60 @@ window.app.component('lnbits-wallet-list', {
LNBITS_DENOMINATION: LNBITS_DENOMINATION
}
},
computed: {
wallets() {
const bal = this.balance
return this.user.wallets.map(obj => {
obj.live_fsat =
bal.length && bal[0] === obj.id
? LNbits.utils.formatSat(bal[1])
: obj.fsat
return obj
})
}
},
methods: {
createWallet() {
LNbits.api.createWallet(this.user.wallets[0], this.walletName)
LNbits.api.createWallet(this.g.user.wallets[0], this.walletName)
}
},
created() {
if (window.user) {
this.user = LNbits.map.user(window.user)
}
if (window.wallet) {
this.activeWallet = LNbits.map.wallet(window.wallet)
}
document.addEventListener('updateWalletBalance', this.updateWalletBalance)
}
})
window.app.component('lnbits-extension-list', {
mixins: [window.windowMixin],
template: '#lnbits-extension-list',
data() {
return {
extensions: [],
user: null,
userExtensions: [],
searchTerm: ''
}
},
watch: {
searchTerm(term) {
this.userExtensions = this.updateUserExtensions(term)
'g.user.extensions': {
handler(newExtensions) {
this.loadExtensions()
},
deep: true
}
},
computed: {
userExtensions() {
return this.updateUserExtensions(this.searchTerm)
}
},
methods: {
async loadExtensions() {
try {
const {data} = await LNbits.api.request('GET', '/api/v1/extension')
this.extensions = data
.map(extension => LNbits.map.extension(extension))
.sort((a, b) => a.name.localeCompare(b.name))
} catch (error) {
LNbits.utils.notifyApiError(error)
}
},
updateUserExtensions(filterBy) {
if (!this.user) return []
const path = window.location.pathname
const userExtensions = this.user.extensions
const userExtensions = this.g.user.extensions
return this.extensions
.filter(o => {
return userExtensions.indexOf(o.code) !== -1
})
.filter(o => userExtensions.includes(o.code))
.filter(o => {
if (!filterBy) return true
return (
`${o.code} ${o.name} ${o.short_description} ${o.url}`
return `${o.code} ${o.name} ${o.short_description} ${o.url}`
.toLocaleLowerCase()
.indexOf(filterBy.toLocaleLowerCase()) !== -1
)
.includes(filterBy.toLocaleLowerCase())
})
.map(obj => {
obj.isActive = path.startsWith(obj.url)
@ -103,27 +96,12 @@ window.app.component('lnbits-extension-list', {
}
},
async created() {
if (window.user) {
this.user = LNbits.map.user(window.user)
}
try {
const {data} = await LNbits.api.request('GET', '/api/v1/extension')
this.extensions = data
.map(data => {
return LNbits.map.extension(data)
})
.sort((a, b) => {
return a.name.localeCompare(b.name)
})
this.userExtensions = this.updateUserExtensions()
} catch (error) {
LNbits.utils.notifyApiError(error)
}
await this.loadExtensions()
}
})
window.app.component('lnbits-manage', {
mixins: [window.windowMixin],
template: '#lnbits-manage',
props: ['showAdmin', 'showNode', 'showExtensions', 'showUsers', 'showAudit'],
methods: {
@ -133,18 +111,13 @@ window.app.component('lnbits-manage', {
},
data() {
return {
extensions: [],
user: null
}
},
created() {
if (window.user) {
this.user = LNbits.map.user(window.user)
extensions: []
}
}
})
window.app.component('lnbits-payment-details', {
mixins: [window.windowMixin],
template: '#lnbits-payment-details',
props: ['payment'],
mixins: [window.windowMixin],
@ -196,6 +169,7 @@ window.app.component('lnbits-payment-details', {
})
window.app.component('lnbits-lnurlpay-success-action', {
mixins: [window.windowMixin],
template: '#lnbits-lnurlpay-success-action',
props: ['payment', 'success_action'],
data() {
@ -214,8 +188,8 @@ window.app.component('lnbits-lnurlpay-success-action', {
})
window.app.component('lnbits-qrcode', {
template: '#lnbits-qrcode',
mixins: [window.windowMixin],
template: '#lnbits-qrcode',
components: {
QrcodeVue
},
@ -484,7 +458,7 @@ window.app.component('lnbits-dynamic-chips', {
window.app.component('lnbits-update-balance', {
template: '#lnbits-update-balance',
mixins: [window.windowMixin],
props: ['wallet_id', 'credit-value'],
props: ['wallet_id'],
computed: {
denomination() {
return LNBITS_DENOMINATION
@ -498,20 +472,15 @@ window.app.component('lnbits-update-balance', {
credit: 0
}
},
watch: {
credit(val) {
this.updateBalance(val)
}
},
methods: {
updateBalance(credit) {
updateBalance(scope) {
LNbits.api
.updateBalance(credit, this.wallet_id)
.updateBalance(scope.value, this.wallet_id)
.then(res => {
if (res.data.success !== true) {
throw new Error(res.data)
}
credit = parseInt(credit)
credit = parseInt(scope.value)
Quasar.Notify.create({
type: 'positive',
message: this.$t('credit_ok', {
@ -519,8 +488,9 @@ window.app.component('lnbits-update-balance', {
}),
icon: null
})
this.$emit('credit-value', credit)
return credit
this.credit = 0
scope.value = 0
scope.set()
})
.catch(LNbits.utils.notifyApiError)
}

View File

@ -111,7 +111,7 @@ window.app.component('payment-chart', {
.request(
'GET',
'/api/v1/payments/history?group=' + this.paymentsChart.group.value,
this.wallet.adminkey
this.g.wallet.adminkey
)
.then(response => {
this.$nextTick(() => {

View File

@ -1,7 +1,7 @@
window.app.component('payment-list', {
name: 'payment-list',
template: '#payment-list',
props: ['update', 'wallet', 'mobileSimple', 'lazy'],
props: ['update', 'lazy'],
mixins: [window.windowMixin],
data() {
return {
@ -32,7 +32,7 @@ window.app.component('payment-list', {
descending: true,
rowsNumber: 10
},
search: null,
search: '',
filter: {
'status[ne]': 'failed'
},
@ -116,6 +116,9 @@ window.app.component('payment-list', {
}
},
computed: {
wallet() {
return this.g.wallet
},
filteredPayments() {
const q = this.paymentsTable.search
if (!q || q === '') return this.payments
@ -136,7 +139,7 @@ window.app.component('payment-list', {
fetchPayments(props) {
const params = LNbits.utils.prepareFilterQuery(this.paymentsTable, props)
return LNbits.api
.getPayments(this.wallet, params)
.getPayments(this.g.wallet, params)
.then(response => {
this.paymentsTable.loading = false
this.paymentsTable.pagination.rowsNumber = response.data.total
@ -162,7 +165,7 @@ window.app.component('payment-list', {
direction: pagination.descending ? 'desc' : 'asc'
}
const params = new URLSearchParams(query)
LNbits.api.getPayments(this.wallet, params).then(response => {
LNbits.api.getPayments(this.g.wallet, params).then(response => {
let payments = response.data.data.map(LNbits.map.payment)
let columns = this.paymentsCSV.columns
@ -190,7 +193,7 @@ window.app.component('payment-list', {
LNbits.utils.exportCSV(
columns,
payments,
this.wallet.name + '-payments'
this.g.wallet.name + '-payments'
)
})
},
@ -233,6 +236,12 @@ window.app.component('payment-list', {
},
update() {
this.fetchPayments()
},
'g.wallet': {
handler(newWallet) {
this.fetchPayments()
},
deep: true
}
},
created() {

View File

@ -16,23 +16,53 @@ function eventReaction(amount) {
console.log(e)
}
}
function confettiBothSides() {
function confettiTop() {
document.getElementById('vue').disabled = true
var end = Date.now() + 2 * 1000
var colors = ['#FFD700', '#ffffff']
var end = Date.now() + 200
var colors = [
localStorage.getItem('lnbits.primaryColor') || '#FFD700',
localStorage.getItem('lnbits.secondaryColor') || 'E89400',
'#ffffff'
]
function frame() {
confetti({
particleCount: 2,
particleCount: 3,
angle: 270,
spread: 1000,
origin: {y: 0},
colors: colors,
zIndex: 999999
})
if (Date.now() < end) {
requestAnimationFrame(frame)
} else {
document.getElementById('vue').disabled = false
}
}
frame()
}
function confettiBothSides() {
document.getElementById('vue').disabled = true
var end = Date.now() + 200
var colors = [
localStorage.getItem('lnbits.primaryColor') || '#FFD700',
localStorage.getItem('lnbits.secondaryColor') || 'E89400',
'#ffffff'
]
function frame() {
confetti({
particleCount: 3,
angle: 60,
spread: 55,
spread: 1000,
origin: {x: 0},
colors: colors,
zIndex: 999999
})
confetti({
particleCount: 2,
particleCount: 3,
angle: 120,
spread: 55,
spread: 1000,
origin: {x: 1},
colors: colors,
zIndex: 999999
@ -46,9 +76,19 @@ function confettiBothSides() {
frame()
}
function confettiFireworks() {
var duration = 3 * 1000
var duration = 1000
var animationEnd = Date.now() + duration
var defaults = {startVelocity: 30, spread: 360, ticks: 60, zIndex: 0}
var defaults = {
startVelocity: 30,
spread: 1000,
ticks: 60,
zIndex: 0,
colors: [
localStorage.getItem('lnbits.primaryColor') || 'FFE400',
localStorage.getItem('lnbits.secondaryColor') || 'E89400',
'FFFFFF'
]
}
function randomInRange(min, max) {
return Math.random() * (max - min) + min
@ -80,16 +120,20 @@ function confettiStars() {
var defaults = {
spread: 360,
ticks: 50,
gravity: 0,
gravity: 4,
decay: 0.94,
startVelocity: 30,
colors: ['FFE400', 'FFBD00', 'E89400', 'FFCA6C', 'FDFFB8']
colors: [
localStorage.getItem('lnbits.primaryColor') || 'FFE400',
localStorage.getItem('lnbits.secondaryColor') || 'E89400',
'FFFFFF'
]
}
function shoot() {
confetti({
...defaults,
particleCount: 40,
particleCount: 20,
scalar: 1.2,
shapes: ['star']
})

View File

@ -0,0 +1,650 @@
window.ExtensionsPageLogic = {
data: function () {
return {
slide: 0,
fullscreen: false,
autoplay: true,
searchTerm: '',
tab: 'all',
manageExtensionTab: 'releases',
filteredExtensions: null,
updatableExtensions: [],
showUninstallDialog: false,
showManageExtensionDialog: false,
showExtensionDetailsDialog: false,
showDropDbDialog: false,
showPayToEnableDialog: false,
showUpdateAllDialog: false,
dropDbExtensionId: '',
selectedExtension: null,
selectedImage: null,
selectedExtensionDetails: null,
selectedExtensionRepos: null,
selectedRelease: null,
uninstallAndDropDb: false,
maxStars: 5,
paylinkWebsocket: null,
user: null
}
},
watch: {
searchTerm(term) {
this.filterExtensions(term, this.tab)
}
},
methods: {
handleTabChanged: function (tab) {
this.filterExtensions(this.searchTerm, tab)
},
filterExtensions: function (term, tab) {
// Filter the extensions list
function extensionNameContains(searchTerm) {
return function (extension) {
return (
extension.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
extension.shortDescription
?.toLowerCase()
.includes(searchTerm.toLowerCase())
)
}
}
this.filteredExtensions = this.extensions
.filter(e => (tab === 'all' ? !e.isInstalled : true))
.filter(e => (tab === 'installed' ? e.isInstalled : true))
.filter(e =>
tab === 'installed' ? (e.isActive ? true : !!this.g.user.admin) : true
)
.filter(e => (tab === 'featured' ? e.isFeatured : true))
.filter(extensionNameContains(term))
.map(e => ({
...e,
details_link:
e.installedRelease?.details_link || e.latestRelease?.details_link
}))
this.tab = tab
},
installExtension: async function (release) {
// no longer required to check if the invoice was paid
// the install logic has been triggered one way or another
this.unsubscribeFromPaylinkWs()
const extension = this.selectedExtension
extension.inProgress = true
this.showManageExtensionDialog = false
release.payment_hash =
release.payment_hash || this.getPaylinkHash(release.pay_link)
LNbits.api
.request('POST', `/api/v1/extension`, this.g.user.wallets[0].adminkey, {
ext_id: extension.id,
archive: release.archive,
source_repo: release.source_repo,
payment_hash: release.payment_hash,
version: release.version
})
.then(response => {
extension.isAvailable = true
extension.isInstalled = true
extension.installedRelease = release
this.toggleExtension(extension)
extension.inProgress = false
this.filteredExtensions = this.extensions.concat([])
this.handleTabChanged('installed')
this.tab = 'installed'
this.refreshRoute()
})
.catch(err => {
console.warn(err)
extension.inProgress = false
LNbits.utils.notifyApiError(err)
})
},
uninstallExtension: async function () {
const extension = this.selectedExtension
this.showManageExtensionDialog = false
this.showUninstallDialog = false
extension.inProgress = true
LNbits.api
.request(
'DELETE',
`/api/v1/extension/${extension.id}`,
this.g.user.wallets[0].adminkey
)
.then(response => {
extension.isAvailable = false
extension.isInstalled = false
extension.inProgress = false
extension.installedRelease = null
this.filteredExtensions = this.extensions.concat([])
this.handleTabChanged('installed')
this.tab = 'installed'
Quasar.Notify.create({
type: 'positive',
message: 'Extension uninstalled!'
})
if (this.uninstallAndDropDb) {
this.showDropDb()
} else {
setTimeout(() => {
this.refreshRoute()
}, 300)
}
})
.catch(err => {
LNbits.utils.notifyApiError(err)
extension.inProgress = false
})
},
dropExtensionDb: async function () {
const extension = this.selectedExtension
this.showManageExtensionDialog = false
this.showDropDbDialog = false
this.dropDbExtensionId = ''
extension.inProgress = true
LNbits.api
.request(
'DELETE',
`/api/v1/extension/${extension.id}/db`,
this.g.user.wallets[0].adminkey
)
.then(response => {
extension.installedRelease = null
extension.inProgress = false
extension.hasDatabaseTables = false
Quasar.Notify.create({
type: 'positive',
message: 'Extension DB deleted!'
})
setTimeout(() => {
this.refreshRoute()
}, 300)
})
.catch(err => {
LNbits.utils.notifyApiError(err)
extension.inProgress = false
})
},
toggleExtension(extension) {
const action = extension.isActive ? 'activate' : 'deactivate'
LNbits.api
.request(
'PUT',
`/api/v1/extension/${extension.id}/${action}`,
this.g.user.wallets[0].adminkey
)
.then(response => {
Quasar.Notify.create({
timeout: 2000,
type: 'positive',
message: `Extension '${extension.id}' ${action}d!`
})
})
.catch(err => {
LNbits.utils.notifyApiError(err)
extension.isActive = false
extension.inProgress = false
})
},
enableExtensionForUser: function (extension) {
if (extension.isPaymentRequired) {
this.showPayToEnable(extension)
return
}
this.enableExtension(extension)
},
enableExtension: function (extension) {
LNbits.api
.request(
'PUT',
`/api/v1/extension/${extension.id}/enable`,
this.g.user.wallets[0].adminkey
)
.then(response => {
Quasar.Notify.create({
type: 'positive',
message: 'Extension enabled!'
})
setTimeout(() => {
this.refreshRoute()
}, 300)
})
.catch(err => {
console.warn(err)
LNbits.utils.notifyApiError(err)
})
},
disableExtension: function (extension) {
LNbits.api
.request(
'PUT',
`/api/v1/extension/${extension.id}/disable`,
this.g.user.wallets[0].adminkey
)
.then(response => {
Quasar.Notify.create({
type: 'positive',
message: 'Extension disabled!'
})
setTimeout(() => {
this.refreshRoute()
}, 300)
})
.catch(err => {
console.warn(error)
LNbits.utils.notifyApiError(err)
})
},
showPayToEnable: function (extension) {
this.selectedExtension = extension
this.selectedExtension.payToEnable.paidAmount =
extension.payToEnable.amount
this.selectedExtension.payToEnable.showQRCode = false
this.showPayToEnableDialog = true
},
updatePayToInstallData: function (extension) {
LNbits.api
.request(
'PUT',
`/api/v1/extension/${extension.id}/sell`,
this.g.user.wallets[0].adminkey,
{
required: extension.payToEnable.required,
amount: extension.payToEnable.amount,
wallet: extension.payToEnable.wallet
}
)
.then(response => {
Quasar.Notify.create({
type: 'positive',
message: 'Payment info updated!'
})
this.showManageExtensionDialog = false
})
.catch(err => {
LNbits.utils.notifyApiError(err)
extension.inProgress = false
})
},
showUninstall: function () {
this.showManageExtensionDialog = false
this.showUninstallDialog = true
this.uninstallAndDropDb = false
},
showDropDb: function () {
this.showDropDbDialog = true
},
showManageExtension: async function (extension) {
this.selectedExtension = extension
this.selectedRelease = null
this.selectedExtensionRepos = null
this.manageExtensionTab = 'releases'
this.showManageExtensionDialog = true
try {
const {data} = await LNbits.api.request(
'GET',
`/api/v1/extension/${extension.id}/releases`,
this.g.user.wallets[0].adminkey
)
this.selectedExtensionRepos = data.reduce((repos, release) => {
repos[release.source_repo] = repos[release.source_repo] || {
releases: [],
isInstalled: false,
repo: release.repo
}
release.inProgress = false
release.error = null
release.loaded = false
release.isInstalled = this.isInstalledVersion(
this.selectedExtension,
release
)
if (release.isInstalled) {
repos[release.source_repo].isInstalled = true
}
if (release.pay_link) {
release.requiresPayment = true
release.paidAmount = release.cost_sats
release.payment_hash = this.getPaylinkHash(release.pay_link)
}
repos[release.source_repo].releases.push(release)
return repos
}, {})
} catch (error) {
LNbits.utils.notifyApiError(error)
extension.inProgress = false
}
},
showExtensionDetails: async function (extId, detailsLink) {
if (!detailsLink) {
return
}
this.selectedExtensionDetails = null
this.showExtensionDetailsDialog = true
this.slide = 0
this.fullscreen = false
try {
const {data} = await LNbits.api.request(
'GET',
`/api/v1/extension/${extId}/details?details_link=${detailsLink}`,
this.g.user.wallets[0].inkey
)
this.selectedExtensionDetails = data
this.selectedExtensionDetails.description_md =
LNbits.utils.convertMarkdown(data.description_md)
} catch (error) {
console.warn(error)
}
},
async payAndInstall(release) {
try {
this.selectedExtension.inProgress = true
this.showManageExtensionDialog = false
const paymentInfo = await this.requestPaymentForInstall(
this.selectedExtension.id,
release
)
this.rememberPaylinkHash(release.pay_link, paymentInfo.payment_hash)
const wallet = this.g.user.wallets.find(w => w.id === release.wallet)
const {data} = await LNbits.api.payInvoice(
wallet,
paymentInfo.payment_request
)
release.payment_hash = data.payment_hash
await this.installExtension(release)
} catch (err) {
console.warn(err)
LNbits.utils.notifyApiError(err)
} finally {
this.selectedExtension.inProgress = false
}
},
async payAndEnable(extension) {
try {
const paymentInfo = await this.requestPaymentForEnable(
extension.id,
extension.payToEnable.paidAmount
)
const wallet = this.g.user.wallets.find(
w => w.id === extension.payToEnable.paymentWallet
)
const {data} = await LNbits.api.payInvoice(
wallet,
paymentInfo.payment_request
)
this.enableExtension(extension)
this.showPayToEnableDialog = false
} catch (err) {
console.warn(err)
LNbits.utils.notifyApiError(err)
}
},
async showInstallQRCode(release) {
this.selectedRelease = release
try {
const data = await this.requestPaymentForInstall(
this.selectedExtension.id,
release
)
this.selectedRelease.paymentRequest = data.payment_request
this.selectedRelease.payment_hash = data.payment_hash
this.selectedRelease = _.clone(this.selectedRelease)
this.rememberPaylinkHash(
this.selectedRelease.pay_link,
this.selectedRelease.payment_hash
)
this.subscribeToPaylinkWs(
this.selectedRelease.pay_link,
data.payment_hash
)
} catch (err) {
console.warn(err)
LNbits.utils.notifyApiError(err)
}
},
async showEnableQRCode(extension) {
try {
extension.payToEnable.showQRCode = true
this.selectedExtension = _.clone(extension)
const data = await this.requestPaymentForEnable(
extension.id,
extension.payToEnable.paidAmount
)
extension.payToEnable.paymentRequest = data.payment_request
this.selectedExtension = _.clone(extension)
const url = new URL(window.location)
url.protocol = url.protocol === 'https:' ? 'wss' : 'ws'
url.pathname = `/api/v1/ws/${data.payment_hash}`
const ws = new WebSocket(url)
ws.addEventListener('message', async ({data}) => {
const payment = JSON.parse(data)
if (payment.pending === false) {
Quasar.Notify.create({
type: 'positive',
message: 'Invoice Paid!'
})
this.enableExtension(extension)
ws.close()
}
})
} catch (err) {
console.warn(err)
LNbits.utils.notifyApiError(err)
}
},
async requestPaymentForInstall(extId, release) {
const {data} = await LNbits.api.request(
'PUT',
`/api/v1/extension/${extId}/invoice/install`,
this.g.user.wallets[0].adminkey,
{
ext_id: extId,
archive: release.archive,
source_repo: release.source_repo,
cost_sats: release.paidAmount,
version: release.version
}
)
return data
},
async requestPaymentForEnable(extId, amount) {
const {data} = await LNbits.api.request(
'PUT',
`/api/v1/extension/${extId}/invoice/enable`,
this.g.user.wallets[0].adminkey,
{
amount
}
)
return data
},
clearHangingInvoice(release) {
this.forgetPaylinkHash(release.pay_link)
release.payment_hash = null
},
rememberPaylinkHash(pay_link, payment_hash) {
this.$q.localStorage.set(
`lnbits.extensions.paylink.${pay_link}`,
payment_hash
)
},
getPaylinkHash(pay_link) {
return this.$q.localStorage.getItem(
`lnbits.extensions.paylink.${pay_link}`
)
},
forgetPaylinkHash(pay_link) {
this.$q.localStorage.remove(`lnbits.extensions.paylink.${pay_link}`)
},
subscribeToPaylinkWs(pay_link, payment_hash) {
const url = new URL(`${pay_link}/${payment_hash}`)
url.protocol = url.protocol === 'https:' ? 'wss' : 'ws'
this.paylinkWebsocket = new WebSocket(url)
this.paylinkWebsocket.addEventListener('message', async ({data}) => {
const resp = JSON.parse(data)
if (resp.paid) {
Quasar.Notify.create({
type: 'positive',
message: 'Invoice Paid!'
})
this.installExtension(this.selectedRelease)
} else {
Quasar.Notify.create({
type: 'warning',
message: 'Invoice tracking lost!'
})
}
})
},
unsubscribeFromPaylinkWs() {
try {
this.paylinkWebsocket && this.paylinkWebsocket.close()
} catch (error) {
console.warn(error)
}
},
hasNewVersion: function (extension) {
if (extension.installedRelease && extension.latestRelease) {
return (
extension.installedRelease.version !== extension.latestRelease.version
)
}
},
isInstalledVersion: function (extension, release) {
if (extension.installedRelease) {
return (
extension.installedRelease.source_repo === release.source_repo &&
extension.installedRelease.version === release.version
)
}
},
getReleaseIcon: function (release) {
if (!release.is_version_compatible) return 'block'
if (release.isInstalled) return 'download_done'
return 'download'
},
getReleaseIconColor: function (release) {
if (!release.is_version_compatible) return 'text-red'
if (release.isInstalled) return 'text-green'
return ''
},
getGitHubReleaseDetails: async function (release) {
if (!release.is_github_release || release.loaded) {
return
}
const [org, repo] = release.source_repo.split('/')
release.inProgress = true
try {
const {data} = await LNbits.api.request(
'GET',
`/api/v1/extension/release/${org}/${repo}/${release.version}`,
this.g.user.wallets[0].adminkey
)
release.loaded = true
release.is_version_compatible = data.is_version_compatible
release.min_lnbits_version = data.min_lnbits_version
release.warning = data.warning
} catch (error) {
console.warn(error)
release.error = error
LNbits.utils.notifyApiError(error)
} finally {
release.inProgress = false
}
},
selectAllUpdatableExtensionss: async function () {
this.updatableExtensions.forEach(e => (e.selectedForUpdate = true))
},
updateSelectedExtensions: async function () {
let count = 0
for (const ext of this.updatableExtensions) {
try {
if (!ext.selectedForUpdate) {
continue
}
ext.inProgress = true
await LNbits.api.request(
'POST',
`/api/v1/extension`,
this.g.user.wallets[0].adminkey,
{
ext_id: ext.id,
archive: ext.latestRelease.archive,
source_repo: ext.latestRelease.source_repo,
payment_hash: ext.latestRelease.payment_hash,
version: ext.latestRelease.version
}
)
count++
ext.isAvailable = true
ext.isInstalled = true
ext.isUpgraded = true
ext.inProgress = false
ext.installedRelease = ext.latestRelease
this.toggleExtension(ext)
} catch (err) {
console.warn(err)
Quasar.Notify.create({
type: 'negative',
message: `Failed to update ${ext.code}!`
})
} finally {
ext.inProgress = false
}
}
Quasar.Notify.create({
type: 'positive',
message: `${count} extensions updated!`
})
this.showUpdateAllDialog = false
setTimeout(() => {
this.refreshRoute()
}, 2000)
}
},
created: function () {
this.extensions = window.extension_data.map(e => ({
...e,
inProgress: false,
selectedForUpdate: false
}))
this.filteredExtensions = this.extensions.concat([])
for (let i = 0; i < this.filteredExtensions.length; i++) {
if (this.filteredExtensions[i].isInstalled != false) {
this.handleTabChanged('installed')
this.tab = 'installed'
}
}
this.updatableExtensions = this.extensions.filter(ext =>
this.hasNewVersion(ext)
)
},
mixins: [windowMixin]
}

View File

@ -1,4 +1,206 @@
const DynamicComponent = {
props: {
fetchUrl: {
type: String,
required: true
},
scripts: {
type: Array,
default: () => []
}
},
async mounted() {
await this.loadDynamicContent()
},
methods: {
async loadScript(src) {
return new Promise((resolve, reject) => {
const existingScript = document.querySelector(`script[src="${src}"]`)
if (existingScript) {
existingScript.remove()
}
const script = document.createElement('script')
script.src = src
script.async = true
script.onload = resolve
script.onerror = () =>
reject(new Error(`Failed to load script: ${src}`))
document.head.appendChild(script)
})
},
async loadDynamicContent() {
this.$q.loading.show()
try {
const cleanUrl = this.fetchUrl.split('#')[0]
//grab page content, need to be before loading scripts
const response = await fetch(cleanUrl, {
credentials: 'include',
headers: {
Accept: 'text/html',
'X-Requested-With': 'XMLHttpRequest'
}
})
const html = await response.text()
// load window variables
const parser = new DOMParser()
const htmlDocument = parser.parseFromString(html, 'text/html')
const inlineScript = htmlDocument.querySelector('#window-vars-script')
if (inlineScript) {
new Function(inlineScript.innerHTML)() // Execute the script
}
//load scripts defined in the route
await this.loadScript('/static/js/base.js')
for (const script of this.scripts) {
await this.loadScript(script)
}
//housecleaning, remove old component
//const previousRouteName =
// this.$router.currentRoute.value.meta.previousRouteName
//if (
// previousRouteName &&
// window.app._context.components[previousRouteName]
//) {
// delete window.app._context.components[previousRouteName]
//}
//load component logic
const logicKey = `${this.$route.name}PageLogic`
const componentLogic = window[logicKey]
if (!componentLogic) {
throw new Error(
`Component logic '${logicKey}' not found. Ensure it is defined in the script.`
)
}
//Add mixins
componentLogic.mixins = componentLogic.mixins || []
if (window.windowMixin) {
componentLogic.mixins.push(window.windowMixin)
}
//Build component
window.app.component(this.$route.name, {
...componentLogic,
template: html // Use the fetched HTML as the template
})
delete window[logicKey] //dont need this anymore
this.$forceUpdate()
} catch (error) {
console.error('Error loading dynamic content:', error)
} finally {
this.$q.loading.hide()
}
}
},
watch: {
$route(to, from) {
const validRouteNames = routes.map(route => route.name)
if (validRouteNames.includes(to.name)) {
this.$router.currentRoute.value.meta.previousRouteName = from.name
this.loadDynamicContent()
} else {
console.log(
`Route '${to.name}' is not valid. Leave this one to Fastapi.`
)
}
}
},
template: `
<component :is="$route.name"></component>
`
}
const routes = [
{
path: '/wallet',
name: 'Wallet',
component: DynamicComponent,
props: route => ({
fetchUrl: `/wallet${route.query.wal ? `?wal=${route.query.wal}` : ''}`,
scripts: ['/static/js/wallet.js']
})
},
{
path: '/admin',
name: 'Admin',
component: DynamicComponent,
props: {
fetchUrl: '/admin',
scripts: ['/static/js/admin.js']
}
},
{
path: '/users',
name: 'Users',
component: DynamicComponent,
props: {
fetchUrl: '/users',
scripts: ['/static/js/users.js']
}
},
{
path: '/audit',
name: 'Audit',
component: DynamicComponent,
props: {
fetchUrl: '/audit',
scripts: ['/static/js/audit.js']
}
},
{
path: '/extensions',
name: 'Extensions',
component: DynamicComponent,
props: {
fetchUrl: '/extensions',
scripts: ['/static/js/extensions.js']
}
},
{
path: '/account',
name: 'Account',
component: DynamicComponent,
props: {
fetchUrl: '/account',
scripts: ['/static/js/account.js']
}
},
{
path: '/node',
name: 'Node',
component: DynamicComponent,
props: {
fetchUrl: '/node',
scripts: ['/static/js/node.js']
}
}
]
window.router = VueRouter.createRouter({
history: VueRouter.createWebHistory(),
routes
})
window.app.mixin({
computed: {
isVueRoute() {
const currentPath = window.location.pathname
const matchedRoute = window.router.resolve(currentPath)
const isVueRoute = matchedRoute?.matched?.length > 0
return isVueRoute
}
}
})
window.app.use(VueQrcodeReader)
window.app.use(Quasar)
window.app.use(window.i18n)
window.app.provide('g', g)
window.app.use(window.router)
window.app.component('DynamicComponent', DynamicComponent)
window.app.mount('#vue')

View File

@ -1,8 +1,362 @@
window.NodePageLogic = {
mixins: [window.windowMixin],
config: {
globalProperties: {
LNbits,
msg: 'hello'
}
},
data() {
return {
isSuperUser: false,
wallet: {},
tab: 'dashboard',
payments: 1000,
info: {},
channel_stats: {},
channels: {
data: [],
filter: ''
},
activeBalance: {},
ranks: {},
peers: {
data: [],
filter: ''
},
connectPeerDialog: {
show: false,
data: {}
},
setFeeDialog: {
show: false,
data: {
fee_ppm: 0,
fee_base_msat: 0
}
},
openChannelDialog: {
show: false,
data: {}
},
closeChannelDialog: {
show: false,
data: {}
},
nodeInfoDialog: {
show: false,
data: {}
},
transactionDetailsDialog: {
show: false,
data: {}
},
states: [
{label: 'Active', value: 'active', color: 'green'},
{label: 'Pending', value: 'pending', color: 'orange'},
{label: 'Inactive', value: 'inactive', color: 'grey'},
{label: 'Closed', value: 'closed', color: 'red'}
],
stateFilters: [
{label: 'Active', value: 'active'},
{label: 'Pending', value: 'pending'}
],
paymentsTable: {
data: [],
columns: [
{
name: 'pending',
label: ''
},
{
name: 'date',
align: 'left',
label: this.$t('date'),
field: 'date',
sortable: true
},
{
name: 'sat',
align: 'right',
label: this.$t('amount') + ' (' + LNBITS_DENOMINATION + ')',
field: row => this.formatMsat(row.amount),
sortable: true
},
{
name: 'fee',
align: 'right',
label: this.$t('fee') + ' (m' + LNBITS_DENOMINATION + ')',
field: 'fee'
},
{
name: 'destination',
align: 'right',
label: 'Destination',
field: 'destination'
},
{
name: 'memo',
align: 'left',
label: this.$t('memo'),
field: 'memo'
}
],
pagination: {
rowsPerPage: 10,
page: 1,
rowsNumber: 10
},
filter: null
},
invoiceTable: {
data: [],
columns: [
{
name: 'pending',
label: ''
},
{
name: 'paid_at',
field: 'paid_at',
align: 'left',
label: 'Paid at',
sortable: true
},
{
name: 'expiry',
label: this.$t('expiry'),
field: 'expiry',
align: 'left',
sortable: true
},
{
name: 'amount',
label: this.$t('amount') + ' (' + LNBITS_DENOMINATION + ')',
field: row => this.formatMsat(row.amount),
sortable: true
},
{
name: 'memo',
align: 'left',
label: this.$t('memo'),
field: 'memo'
}
],
pagination: {
rowsPerPage: 10,
page: 1,
rowsNumber: 10
},
filter: null
}
}
},
created() {
this.getInfo()
this.get1MLStats()
},
watch: {
tab(val) {
if (val === 'transactions' && !this.paymentsTable.data.length) {
this.getPayments()
this.getInvoices()
} else if (val === 'channels' && !this.channels.data.length) {
this.getChannels()
this.getPeers()
}
}
},
computed: {
checkChanges() {
return !_.isEqual(this.settings, this.formData)
},
filteredChannels() {
return this.stateFilters
? this.channels.data.filter(channel => {
return this.stateFilters.find(({value}) => value == channel.state)
})
: this.channels.data
},
totalBalance() {
return this.filteredChannels.reduce(
(balance, channel) => {
balance.local_msat += channel.balance.local_msat
balance.remote_msat += channel.balance.remote_msat
balance.total_msat += channel.balance.total_msat
return balance
},
{local_msat: 0, remote_msat: 0, total_msat: 0}
)
}
},
methods: {
formatMsat(msat) {
return LNbits.utils.formatMsat(msat)
},
api(method, url, options) {
const params = new URLSearchParams(options?.query)
return LNbits.api
.request(method, `/node/api/v1${url}?${params}`, {}, options?.data)
.catch(error => {
LNbits.utils.notifyApiError(error)
})
},
getChannel(channel_id) {
return this.api('GET', `/channels/${channel_id}`).then(response => {
this.setFeeDialog.data.fee_ppm = response.data.fee_ppm
this.setFeeDialog.data.fee_base_msat = response.data.fee_base_msat
})
},
getChannels() {
return this.api('GET', '/channels').then(response => {
this.channels.data = response.data
})
},
getInfo() {
return this.api('GET', '/info').then(response => {
this.info = response.data
this.channel_stats = response.data.channel_stats
})
},
get1MLStats() {
return this.api('GET', '/rank').then(response => {
this.ranks = response.data
})
},
getPayments(props) {
if (props) {
this.paymentsTable.pagination = props.pagination
}
let pagination = this.paymentsTable.pagination
const query = {
limit: pagination.rowsPerPage,
offset: (pagination.page - 1) * pagination.rowsPerPage ?? 0
}
return this.api('GET', '/payments', {query}).then(response => {
this.paymentsTable.data = response.data.data
this.paymentsTable.pagination.rowsNumber = response.data.total
})
},
getInvoices(props) {
if (props) {
this.invoiceTable.pagination = props.pagination
}
let pagination = this.invoiceTable.pagination
const query = {
limit: pagination.rowsPerPage,
offset: (pagination.page - 1) * pagination.rowsPerPage ?? 0
}
return this.api('GET', '/invoices', {query}).then(response => {
this.invoiceTable.data = response.data.data
this.invoiceTable.pagination.rowsNumber = response.data.total
})
},
getPeers() {
return this.api('GET', '/peers').then(response => {
this.peers.data = response.data
console.log('peers', this.peers)
})
},
connectPeer() {
this.api('POST', '/peers', {data: this.connectPeerDialog.data}).then(
() => {
this.connectPeerDialog.show = false
this.getPeers()
}
)
},
disconnectPeer(id) {
LNbits.utils
.confirmDialog('Do you really wanna disconnect this peer?')
.onOk(() => {
this.api('DELETE', `/peers/${id}`).then(response => {
Quasar.Notify.create({
message: 'Disconnected',
icon: null
})
this.needsRestart = true
this.getPeers()
})
})
},
setChannelFee(channel_id) {
this.api('PUT', `/channels/${channel_id}`, {
data: this.setFeeDialog.data
})
.then(response => {
this.setFeeDialog.show = false
this.getChannels()
})
.catch(LNbits.utils.notifyApiError)
},
openChannel() {
this.api('POST', '/channels', {data: this.openChannelDialog.data})
.then(response => {
this.openChannelDialog.show = false
this.getChannels()
})
.catch(error => {
console.log(error)
})
},
showCloseChannelDialog(channel) {
this.closeChannelDialog.show = true
this.closeChannelDialog.data = {
force: false,
short_id: channel.short_id,
...channel.point
}
},
closeChannel() {
this.api('DELETE', '/channels', {
query: this.closeChannelDialog.data
}).then(response => {
this.closeChannelDialog.show = false
this.getChannels()
})
},
showSetFeeDialog(channel_id) {
this.setFeeDialog.show = true
this.setFeeDialog.channel_id = channel_id
this.getChannel(channel_id)
},
showOpenChannelDialog(peer_id) {
this.openChannelDialog.show = true
this.openChannelDialog.data = {peer_id, funding_amount: 0}
},
showNodeInfoDialog(node) {
this.nodeInfoDialog.show = true
this.nodeInfoDialog.data = node
},
showTransactionDetailsDialog(details) {
this.transactionDetailsDialog.show = true
this.transactionDetailsDialog.data = details
console.log('details', details)
},
shortenNodeId(nodeId) {
return nodeId
? nodeId.substring(0, 5) + '...' + nodeId.substring(nodeId.length - 5)
: '...'
}
}
}
window.app.component('lnbits-node-ranks', {
props: ['ranks'],
data() {
return {
user: {},
stats: [
{label: 'Capacity', key: 'capacity'},
{label: 'Channels', key: 'channelcount'},
@ -57,12 +411,7 @@ window.app.component('lnbits-channel-stats', {
</div>
</div>
</q-card>
`,
created() {
if (window.user) {
this.user = LNbits.map.user(window.user)
}
}
`
})
window.app.component('lnbits-stat', {

View File

@ -1,5 +1,4 @@
window.app = Vue.createApp({
el: '#vue',
window.UsersPageLogic = {
mixins: [window.windowMixin],
data() {
return {
@ -403,4 +402,4 @@ window.app = Vue.createApp({
return `${value.substring(0, 5)}...${value.substring(valueLength - 5, valueLength)}`
}
}
})
}

View File

@ -1,29 +1,9 @@
window.app = Vue.createApp({
el: '#vue',
window.WalletPageLogic = {
mixins: [window.windowMixin],
data() {
return {
updatePayments: false,
origin: window.location.origin,
wallet: LNbits.map.wallet(window.wallet),
user: LNbits.map.user(window.user),
exportUrl: `${window.location.origin}/wallet?usr=${window.user.id}&wal=${window.wallet.id}`,
baseUrl: `${window.location.protocol}//${window.location.host}/`,
receive: {
show: false,
status: 'pending',
paymentReq: null,
paymentHash: null,
amountMsat: null,
minMax: [0, 2100000000000000],
lnurl: null,
units: ['sat'],
unit: 'sat',
data: {
amount: null,
memo: ''
}
},
parse: {
show: false,
invoice: null,
@ -44,13 +24,27 @@ window.app = Vue.createApp({
camera: 'auto'
}
},
receive: {
show: false,
status: 'pending',
paymentReq: null,
paymentHash: null,
amountMsat: null,
minMax: [0, 2100000000000000],
lnurl: null,
units: ['sat'],
unit: 'sat',
data: {
amount: null,
memo: ''
}
},
invoiceQrCode: '',
disclaimerDialog: {
show: false,
location: window.location
},
balance: parseInt(wallet.balance_msat / 1000),
fiatBalance: 0,
mobileSimple: false,
update: {
name: null,
currency: null
@ -65,9 +59,9 @@ window.app = Vue.createApp({
computed: {
formattedBalance() {
if (LNBITS_DENOMINATION != 'sats') {
return this.balance / 100
return this.g.wallet.sat / 100
} else {
return LNbits.utils.formatSat(this.balance || this.g.wallet.sat)
return LNbits.utils.formatSat(this.g.wallet.sat)
}
},
formattedFiatBalance() {
@ -80,7 +74,7 @@ window.app = Vue.createApp({
},
canPay() {
if (!this.parse.invoice) return false
return this.parse.invoice.sat <= this.balance
return this.parse.invoice.sat <= this.g.wallet.sat
},
formattedAmount() {
if (this.receive.unit != 'sat') {
@ -94,6 +88,9 @@ window.app = Vue.createApp({
},
formattedSatAmount() {
return LNbits.utils.formatMsat(this.receive.amountMsat) + ' sat'
},
wallet() {
return this.g.wallet
}
},
methods: {
@ -144,21 +141,16 @@ window.app = Vue.createApp({
clearInterval(this.parse.paymentChecker)
}, 10000)
},
onPaymentReceived(paymentHash) {
this.updatePayments = !this.updatePayments
if (this.receive.paymentHash === paymentHash) {
this.receive.show = false
this.receive.paymentHash = null
}
},
handleBalanceUpdate(value) {
this.balance = this.balance + value
this.g.wallet.sat = this.g.wallet.sat + value
},
createInvoice() {
console.log('Creating invoice...')
this.receive.status = 'loading'
if (LNBITS_DENOMINATION != 'sats') {
this.receive.data.amount = this.receive.data.amount * 100
}
LNbits.api
.createInvoice(
this.g.wallet,
@ -172,7 +164,6 @@ window.app = Vue.createApp({
this.receive.paymentReq = response.data.bolt11
this.receive.amountMsat = response.data.amount
this.receive.paymentHash = response.data.payment_hash
this.readNfcTag()
// TODO: lnurl_callback and lnurl_response
@ -200,9 +191,12 @@ window.app = Vue.createApp({
})
}
}
// Hack as rendering in dialog causes reactivity issues. Does speed up, as only rendering lnbits-qrcode once.
this.$nextTick(() => {
this.invoiceQrCode = document.getElementById(
'hiddenQrCodeContainer'
).innerHTML
})
.then(() => {
this.updatePayments = !this.updatePayments
})
.catch(err => {
LNbits.utils.notifyApiError(err)
@ -367,22 +361,16 @@ window.app = Vue.createApp({
LNbits.api
.payInvoice(this.g.wallet, this.parse.data.request)
.then(response => {
.then(_ => {
clearInterval(this.parse.paymentChecker)
setTimeout(() => {
clearInterval(this.parse.paymentChecker)
}, 40000)
this.parse.paymentChecker = setInterval(() => {
LNbits.api
.getPayment(this.g.wallet, response.data.payment_hash)
.then(res => {
if (res.data.paid) {
if (!this.parse.show) {
dismissPaymentMsg()
clearInterval(this.parse.paymentChecker)
this.updatePayments = !this.updatePayments
this.parse.show = false
}
})
}, 2000)
})
.catch(err => {
@ -397,7 +385,6 @@ window.app = Vue.createApp({
timeout: 0,
message: 'Processing payment...'
})
LNbits.api
.payLnurl(
this.g.wallet,
@ -509,13 +496,13 @@ window.app = Vue.createApp({
updateWallet(data) {
LNbits.api
.request('PATCH', '/api/v1/wallet', this.g.wallet.adminkey, data)
.then(_ => {
.then(response => {
this.refreshRoute()
Quasar.Notify.create({
message: `Wallet updated.`,
message: 'Wallet and user updated.',
type: 'positive',
timeout: 3500
})
window.location.reload()
})
.catch(err => {
LNbits.utils.notifyApiError(err)
@ -543,7 +530,7 @@ window.app = Vue.createApp({
if (!this.g.wallet.currency) return 0
LNbits.api
.request('POST', `/api/v1/conversion`, null, {
amount: this.balance || this.g.wallet.sat,
amount: this.g.wallet.sat,
to: this.g.wallet.currency
})
.then(response => {
@ -652,6 +639,12 @@ window.app = Vue.createApp({
dismissPaymentMsg()
LNbits.utils.notifyApiError(err)
})
},
createdTasks() {
this.update.name = this.g.wallet.name
this.update.currency = this.g.wallet.currency
this.receive.units = ['sat', ...window.currencies]
this.updateFiatBalance()
}
},
created() {
@ -665,19 +658,27 @@ window.app = Vue.createApp({
if (this.$q.screen.lt.md) {
this.mobileSimple = true
}
this.update.name = this.g.wallet.name
this.update.currency = this.g.wallet.currency
this.receive.units = ['sat', ...window.currencies]
this.updateFiatBalance()
this.createdTasks()
},
watch: {
updatePayments() {
this.parse.show = false
if (this.receive.paymentHash === this.updatePaymentsHash) {
this.receive.show = false
this.receive.paymentHash = null
}
this.updateFiatBalance()
},
'$q.screen.gt.sm'(value) {
if (value == true) {
this.mobileSimple = false
}
},
'g.wallet': {
handler(newWallet) {
this.createdTasks()
},
deep: true
}
},
mounted() {
@ -685,18 +686,11 @@ window.app = Vue.createApp({
if (!this.$q.localStorage.getItem('lnbits.disclaimerShown')) {
this.disclaimerDialog.show = true
this.$q.localStorage.set('lnbits.disclaimerShown', true)
// Turn on payment reactions by default
this.$q.localStorage.set('lnbits.reactions', 'confettiTop')
}
}
// listen to incoming payments
LNbits.events.onInvoicePaid(this.g.wallet, data => {
console.log('Payment received:', data.payment.payment_hash)
console.log('Wallet balance:', data.wallet_balance)
console.log('Wallet ID:', this.g.wallet)
this.onPaymentReceived(data.payment.payment_hash)
this.balance = data.wallet_balance
eventReaction(data.payment.amount)
})
}
})
if (navigator.serviceWorker != null) {
navigator.serviceWorker.register('/service-worker.js').then(registration => {

View File

@ -4431,12 +4431,12 @@
// options
if (options) {
this.deep = !!options.deep;
this.user = !!options.user;
this.g.user = !!options.user;
this.lazy = !!options.lazy;
this.sync = !!options.sync;
this.before = options.before;
} else {
this.deep = this.user = this.lazy = this.sync = false;
this.deep = this.g.user = this.lazy = this.sync = false;
}
this.cb = cb;
this.id = ++uid$2; // uid for batching
@ -4477,7 +4477,7 @@
try {
value = this.getter.call(vm, vm);
} catch (e) {
if (this.user) {
if (this.g.user) {
handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
} else {
throw e
@ -4562,7 +4562,7 @@
// set new value
var oldValue = this.value;
this.value = value;
if (this.user) {
if (this.g.user) {
try {
this.cb.call(this.vm, value, oldValue);
} catch (e) {

View File

@ -115,7 +115,7 @@
</div>
</template>
<q-list>
<q-item tag="a" href="/account" clickable v-close-popup
<q-item to="/account" clickable v-close-popup
><q-item-section>
<q-icon name="person" />
</q-item-section>
@ -128,7 +128,7 @@
<q-item-label> </q-item-label>
</q-item-section>
</q-item>
<q-item tag="a" href="/account#theme" clickable v-close-popup
<q-item to="/account#theme" clickable v-close-popup
><q-item-section>
<q-icon
:name="$q.dark.isActive ? 'dark_mode' : 'light_mode'"
@ -170,8 +170,22 @@
show-if-above
:elevated="$q.screen.lt.md"
>
<lnbits-wallet-list :balance="balance"></lnbits-wallet-list>
<q-item-label
:style="$q.dark.isActive ? 'color:rgba(255, 255, 255, 0.64)' : ''"
class="q-item__label q-item__label--header"
header
v-text="$t('wallets')"
></q-item-label>
<q-btn
flat
:icon=" walletFlip ? 'keyboard_arrow_right' : 'keyboard_arrow_down'"
class="absolute-top-right"
@click="flipWallets($q.screen.lt.md)"
></q-btn>
<lnbits-wallet-list
v-if="!walletFlip"
:balance="balance"
></lnbits-wallet-list>
<lnbits-manage
:show-admin="'{{LNBITS_ADMIN_UI}}' == 'True'"
:show-users="'{{LNBITS_ADMIN_UI}}' == 'True'"
@ -184,7 +198,73 @@
{% endblock %} {% block page_container %}
<q-page-container>
<q-page class="q-px-md q-py-lg" :class="{'q-px-lg': $q.screen.gt.xs}">
{% block page %}{% endblock %}
<q-scroll-area
v-if="walletFlip"
style="
height: 130px;
width: 100%;
overflow-x: auto;
overflow-y: hidden;
"
>
<div class="row no-wrap q-gutter-md q-pr-md">
<q-card
v-for="wallet in g.user.wallets"
:key="wallet.id"
clickable
@click="selectWallet(wallet)"
class="wallet-list-card"
style="text-decoration: none"
:style="
g.wallet && g.wallet.id === wallet.id
? `border: 1px solid ${primaryColor}; width: 250px; text-decoration: none; cursor: pointer;`
: 'width: 250px; text-decoration: none; border: 0px; cursor: pointer;'
"
:class="{
'active-wallet-card': g.wallet && g.wallet.id === wallet.id
}"
>
<q-card-section>
<div class="row items-center">
<q-avatar
size="lg"
:color="
g.wallet && g.wallet.id === wallet.id
? $q.dark.isActive
? 'primary'
: 'primary'
: 'grey-5'
"
>
<q-icon
name="flash_on"
:size="$q.dark.isActive ? '21px' : '20px'"
:color="$q.dark.isActive ? 'black' : 'grey-3'"
></q-icon>
</q-avatar>
<div
class="text-h6 q-pl-md"
:class="{
'text-bold': g.wallet && g.wallet.id === wallet.id
}"
v-text="wallet.name"
></div>
</div>
<div class="row items-center q-pt-sm">
<h6 class="q-my-none text-no-wrap">
<strong v-text="wallet.fsat"></strong>
<small> {{LNBITS_DENOMINATION}}</small>
</h6>
</div>
</q-card-section>
</q-card>
</div>
</q-scroll-area>
<router-view v-if="isVueRoute"></router-view>
<!-- FastAPI Content -->
<div v-else>{% block page %}{% endblock %}</div>
</q-page>
</q-page-container>
{% endblock %} {% block footer %}
@ -268,6 +348,16 @@
{ value: 'kr', label: '한국어', display: '🇰🇷 KR' },
{ value: 'fi', label: 'Suomi', display: '🇫🇮 FI' }
]
window.LOCALE = 'en'
window.dateFormat = 'YYYY-MM-DD HH:mm'
window.i18n = new VueI18n.createI18n({
locale: window.LOCALE,
fallbackLocale: window.LOCALE,
messages: window.localisation
})
const websocketPrefix =
window.location.protocol === 'http:' ? 'ws://' : 'wss://'
const websocketUrl = `${'http:' ? 'ws://' : 'wss://'}${window.location.host}/api/v1/ws`
</script>
{% block scripts %}{% endblock %} {% for url in INCLUDED_COMPONENTS %}
<script src="{{ static_url_for('static', url) }}"></script>

View File

@ -1,23 +1,21 @@
<template id="lnbits-wallet-list">
<q-list
v-if="user && user.wallets.length"
v-if="g.user && g.user.wallets.length"
dense
class="lnbits-drawer__q-list"
>
<q-item-label header v-text="$t('wallets')"></q-item-label>
<q-item
v-for="wallet in wallets"
:key="wallet.id"
v-for="walletRec in g.user.wallets"
:key="walletRec.id"
clickable
:active="activeWallet && activeWallet.id === wallet.id"
tag="a"
:href="wallet.url"
:active="g.wallet && g.wallet.id === walletRec.id"
@click="selectWallet(walletRec)"
>
<q-item-section side>
<q-avatar
size="md"
:color="
activeWallet && activeWallet.id === wallet.id
g.wallet && g.wallet.id === walletRec.id
? $q.dark.isActive
? 'primary'
: 'primary'
@ -33,27 +31,23 @@
</q-item-section>
<q-item-section>
<q-item-label lines="1"
><span v-text="wallet.name"></span
><span v-text="walletRec.name"></span
></q-item-label>
<q-item-label v-if="LNBITS_DENOMINATION != 'sats'" caption>
<span
v-text="
parseFloat(String(wallet.live_fsat).replaceAll(',', '')) / 100
parseFloat(String(walletRec.fsat).replaceAll(',', '')) / 100
"
></span
>&nbsp;
<span v-text="LNBITS_DENOMINATION"></span>
</q-item-label>
<q-item-label v-else caption>
<span v-text="wallet.live_fsat"></span>&nbsp;
<span v-text="walletRec.fsat"></span>&nbsp;
<span v-text="LNBITS_DENOMINATION"></span>
</q-item-label>
</q-item-section>
<q-item-section
side
v-show="activeWallet && activeWallet.id === wallet.id"
>
<q-icon name="chevron_right" color="grey-5" size="md"></q-icon>
<q-item-section side v-show="g.wallet && g.wallet.id === walletRec.id">
</q-item-section>
</q-item>
<q-item clickable @click="showForm = !showForm">
@ -96,7 +90,9 @@
<template id="lnbits-extension-list">
<q-list
v-if="user && (userExtensions.length > 0 || !!searchTerm)"
v-if="
(g.user && userExtensions && userExtensions.length > 0) || !!searchTerm
"
dense
class="lnbits-drawer__q-list"
>
@ -138,16 +134,10 @@
</template>
<template id="lnbits-manage">
<q-list v-if="user" dense class="lnbits-drawer__q-list">
<q-list v-if="g.user" dense class="lnbits-drawer__q-list">
<q-item-label header v-text="$t('manage')"></q-item-label>
<div v-if="user.admin">
<q-item
v-if="showAdmin"
clickable
tag="a"
href="/admin"
:active="isActive('/admin')"
>
<div v-if="g.user.admin">
<q-item v-if="showAdmin" to="/admin">
<q-item-section side>
<q-icon
name="admin_panel_settings"
@ -159,13 +149,7 @@
<q-item-label lines="1" v-text="$t('server')"></q-item-label>
</q-item-section>
</q-item>
<q-item
v-if="showNode"
clickable
tag="a"
href="/node"
:active="isActive('/node')"
>
<q-item v-if="showNode" to="/node">
<q-item-section side>
<q-icon
name="developer_board"
@ -177,13 +161,7 @@
<q-item-label lines="1" v-text="$t('node')"></q-item-label>
</q-item-section>
</q-item>
<q-item
v-if="showUsers"
clickable
tag="a"
href="/users"
:active="isActive('/users')"
>
<q-item v-if="showUsers" to="/users">
<q-item-section side>
<q-icon
name="groups"
@ -195,13 +173,7 @@
<q-item-label lines="1" v-text="$t('users')"></q-item-label>
</q-item-section>
</q-item>
<q-item
v-if="showAudit"
clickable
tag="a"
href="/audit"
:active="isActive('/audit')"
>
<q-item v-if="showAudit" to="/audit">
<q-item-section side>
<q-icon
name="playlist_add_check_circle"
@ -214,13 +186,7 @@
</q-item-section>
</q-item>
</div>
<q-item
v-if="showExtensions"
clickable
tag="a"
href="/extensions"
:active="isActive('/extensions')"
>
<q-item v-if="showExtensions" to="/extensions">
<q-item-section side>
<q-icon
name="extension"
@ -236,7 +202,7 @@
</template>
<template id="lnbits-payment-details">
<div class="q-py-md" style="text-align: left">
<div v-if="payment" class="q-py-md" style="text-align: left">
<div v-if="payment.tag" class="row justify-center q-mb-md">
<q-badge v-if="hasTag" color="yellow" text-color="black">
#<span v-text="payment.tag"></span>
@ -514,7 +480,7 @@
v-model="scope.value"
dense
autofocus
@keyup.enter="scope.set"
@keyup.enter="updateBalance(scope)"
>
<template v-slot:append>
<q-icon name="edit" />
@ -568,9 +534,8 @@
<template id="payment-list">
<div class="row items-center no-wrap">
<div class="col">
<div class="col" v-if="!mobileSimple || $q.screen.gt.sm">
<q-input
v-if="!mobileSimple"
:label="$t('search_by_tag_memo_amount')"
dense
class="q-pr-xl"

View File

@ -1,6 +1,9 @@
{% macro window_vars(user, wallet) -%}
<script>
window.extensions = {{ EXTENSIONS | tojson | safe }};
{% macro window_vars(user, wallet, extensions, extension_data) -%}
<script id="window-vars-script">
window.extensions = JSON.parse('{{ EXTENSIONS | tojson | safe }}');
{% if extension_data %}
window.extension_data = {{ extension_data | tojson | safe }};
{% endif %}
{% if currencies %}
window.currencies = {{ currencies | tojson | safe }};
{% endif %}
@ -11,4 +14,16 @@
window.wallet = JSON.parse({{ wallet | tojson | safe }});
{% endif %}
</script>
<script>
//Needed for Vue to create the app on first load (although called on every page, its only loaded once)
window.app = Vue.createApp({
el: '#vue',
mixins: [window.windowMixin],
data() {
return {
updatePayments: false
}
}
})
</script>
{%- endmacro %}