[feat] ui support for high number of wallets and payments (#3174)

This commit is contained in:
Vlad Stan 2025-05-27 11:41:25 +03:00 committed by GitHub
parent 375b95c004
commit beee24bd92
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 545 additions and 74 deletions

View File

@ -123,12 +123,8 @@ async def get_payments_paginated(
values["wallet_id"] = wallet_id
clause.append("wallet_id = :wallet_id")
elif user_id:
wallet_ids = await get_wallets_ids(user_id=user_id, conn=conn) or [
"no-wallets-for-user"
]
# wallet ids are safe to use in sql queries
wallet_ids_str = [f"'{w}'" for w in wallet_ids]
clause.append(f""" wallet_id IN ({", ".join(wallet_ids_str)}) """)
only_user_wallets = await _only_user_wallets_statement(user_id, conn=conn)
clause.append(only_user_wallets)
if complete and pending:
clause.append(
@ -366,12 +362,19 @@ async def get_payments_history(
async def get_payment_count_stats(
field: PaymentCountField,
filters: Optional[Filters[PaymentFilters]] = None,
user_id: Optional[str] = None,
conn: Optional[Connection] = None,
) -> list[PaymentCountStat]:
if not filters:
filters = Filters()
clause = filters.where()
extra_stmts = []
if user_id:
only_user_wallets = await _only_user_wallets_statement(user_id, conn=conn)
extra_stmts.append(only_user_wallets)
clause = filters.where(extra_stmts)
data = await (conn or db).fetchall(
query=f"""
SELECT {field} as field, count(*) as total
@ -389,18 +392,26 @@ async def get_payment_count_stats(
async def get_daily_stats(
filters: Optional[Filters[PaymentFilters]] = None,
user_id: Optional[str] = None,
conn: Optional[Connection] = None,
) -> Tuple[list[PaymentDailyStats], list[PaymentDailyStats]]:
if not filters:
filters = Filters()
in_clause = filters.where(
["(apipayments.status = 'success' AND apipayments.amount > 0)"]
)
out_clause = filters.where(
["(apipayments.status IN ('success', 'pending') AND apipayments.amount < 0)"]
)
in_where_stmts = ["(apipayments.status = 'success' AND apipayments.amount > 0)"]
out_where_stmts = [
"(apipayments.status IN ('success', 'pending') AND apipayments.amount < 0)"
]
if user_id:
only_user_wallets = await _only_user_wallets_statement(user_id, conn=conn)
in_where_stmts.append(only_user_wallets)
out_where_stmts.append(only_user_wallets)
in_clause = filters.where(in_where_stmts)
out_clause = filters.where(out_where_stmts)
date_trunc = db.datetime_grouping("day")
query = """
SELECT {date_trunc} date,
@ -431,6 +442,7 @@ async def get_daily_stats(
async def get_wallets_stats(
filters: Optional[Filters[PaymentFilters]] = None,
user_id: Optional[str] = None,
conn: Optional[Connection] = None,
) -> list[PaymentWalletStats]:
@ -449,6 +461,10 @@ async def get_wallets_stats(
)
""",
]
if user_id:
only_user_wallets = await _only_user_wallets_statement(user_id, conn=conn)
where_stmts.append(only_user_wallets)
clauses = filters.where(where_stmts)
data = await (conn or db).fetchall(
@ -524,3 +540,14 @@ async def mark_webhook_sent(payment_hash: str, status: str) -> None:
""",
{"status": status, "hash": payment_hash},
)
async def _only_user_wallets_statement(
user_id: str, conn: Optional[Connection] = None
) -> str:
wallet_ids = await get_wallets_ids(user_id=user_id, conn=conn) or [
"no-wallets-for-user"
]
# wallet ids are safe to use in sql queries
wallet_ids_str = [f"'{w}'" for w in wallet_ids]
return f""" wallet_id IN ({", ".join(wallet_ids_str)}) """

View File

@ -4,7 +4,8 @@ from typing import Optional
from uuid import uuid4
from lnbits.core.db import db
from lnbits.db import Connection
from lnbits.core.models.wallets import WalletsFilters
from lnbits.db import Connection, Filters, Page
from lnbits.settings import settings
from ..models import Wallet
@ -135,6 +136,29 @@ async def get_wallets(
)
async def get_wallets_paginated(
user_id: str,
deleted: Optional[bool] = None,
filters: Optional[Filters[WalletsFilters]] = None,
conn: Optional[Connection] = None,
) -> Page[Wallet]:
if deleted is None:
deleted = False
where: list[str] = [""" "user" = :user AND deleted = :deleted """]
return await (conn or db).fetch_page(
"""
SELECT *, COALESCE((
SELECT balance FROM balances WHERE wallet_id = wallets.id
), 0) AS balance_msat FROM wallets
""",
where=where,
values={"user": user_id, "deleted": deleted},
filters=filters,
model=Wallet,
)
async def get_wallets_ids(
user_id: str, deleted: Optional[bool] = None, conn: Optional[Connection] = None
) -> list[str]:

View File

@ -27,6 +27,9 @@ class UserExtra(BaseModel):
# - "google | github | ...": the user was created using an SSO provider
provider: str | None = "lnbits" # auth provider
# how many wallets are shown in the user interface
visible_wallet_count: int | None = 10
class EndpointAccess(BaseModel):
path: str

View File

@ -9,6 +9,7 @@ from enum import Enum
from ecdsa import SECP256k1, SigningKey
from pydantic import BaseModel, Field
from lnbits.db import FilterModel
from lnbits.helpers import url_for
from lnbits.lnurl import encode as lnurl_encode
from lnbits.settings import settings
@ -25,6 +26,7 @@ class BaseWallet(BaseModel):
class WalletExtra(BaseModel):
icon: str = "flash_on"
color: str = "primary"
pinned: bool = False
class Wallet(BaseModel):
@ -83,3 +85,13 @@ class KeyType(Enum):
class WalletTypeInfo:
key_type: KeyType
wallet: Wallet
class WalletsFilters(FilterModel):
__search_fields__ = ["id", "name", "currency"]
__sort_fields__ = ["id", "name", "currency", "created_at", "updated_at"]
id: str | None
name: str | None
currency: str | None

View File

@ -399,8 +399,9 @@ async def check_transaction_status(
async def get_payments_daily_stats(
filters: Filters[PaymentFilters],
user_id: Optional[str] = None,
) -> list[PaymentDailyStats]:
data_in, data_out = await get_daily_stats(filters)
data_in, data_out = await get_daily_stats(filters, user_id=user_id)
balance_total: float = 0
_none = PaymentDailyStats(date=datetime.now(timezone.utc))

View File

@ -335,6 +335,22 @@
</q-btn-dropdown>
</div>
</div>
<div class="row q-mb-md">
<div class="col-4">
<span v-text="$t('visible_wallet_count')"></span>
</div>
<div class="col-8">
<q-input
v-model="user.extra.visible_wallet_count"
:label="$t('visible_wallet_count')"
filled
dense
type="number"
class="q-mb-md"
></q-input>
</div>
</div>
<div class="row q-mb-md">
<div class="col-4">
<span v-text="$t('color_scheme')"></span>

View File

@ -183,7 +183,7 @@
<span v-text="$t('existing_account_question')"></span>
<span
class="text-secondary cursor-pointer"
class="text-secondary cursor-pointer q-ml-sm"
@click="showLogin('username-password')"
v-text="$t('login')"
></span>

View File

@ -242,6 +242,23 @@
{{ SITE_TITLE }} Wallet:
<strong><em v-text="g.wallet.name"></em></strong>
</div>
<q-space></q-space>
<div class="float-right">
<q-btn
@click="updateWallet({ pinned: !g.wallet.extra.pinned })"
round
class="float-right"
:color="g.wallet.extra.pinned ? 'primary' : 'grey-5'"
text-color="black"
size="sm"
icon="push_pin"
style="transform: rotate(30deg)"
>
<q-tooltip
><span v-text="$t('pin_wallet')"></span
></q-tooltip>
</q-btn>
</div>
</div>
</q-card-section>
<q-card-section class="q-pa-none">

View File

@ -0,0 +1,165 @@
{% 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">
<q-btn
@click="showAddWalletDialog.show = true"
:label="$t('add_wallet')"
color="primary"
>
</q-btn>
</div>
<div class="float-left">
<q-input
:label="$t('search_wallets')"
dense
class="float-right q-pr-xl"
v-model="walletsTable.search"
>
<template v-slot:before>
<q-icon name="search"> </q-icon>
</template>
<template v-slot:append>
<q-icon
v-if="walletsTable.search !== ''"
name="close"
@click="walletsTable.search = ''"
class="cursor-pointer"
>
</q-icon>
</template>
</q-input>
</div>
</div>
</div>
</q-card>
</div>
</div>
<div>
<div>
<div>
<q-table
grid
grid-header
flat
bordered
:rows="wallets"
:columns="walletsTable.columns"
v-model:pagination="walletsTable.pagination"
:loading="walletsTable.loading"
@request="getUserWallets"
row-key="id"
:filter="filter"
hide-header
>
<template v-slot:item="props">
<div class="q-pa-xs col-xs-12 col-sm-6 col-md-4">
<q-card
class="q-ma-sm cursor-pointer wallet-list-card"
style="text-decoration: none"
@click="goToWallet(props.row.id)"
>
<q-card-section>
<div class="row items-center">
<q-avatar
size="lg"
:text-color="$q.dark.isActive ? 'black' : 'grey-3'"
:color="props.row.extra.color"
:icon="props.row.extra.icon"
>
</q-avatar>
<div
class="text-h6 q-pl-md ellipsis"
class="text-bold"
v-text="props.row.name"
></div>
<q-space> </q-space>
<q-btn
v-if="props.row.extra.pinned"
round
color="primary"
text-color="black"
size="xs"
icon="push_pin"
class="float-right"
style="transform: rotate(30deg)"
></q-btn>
</div>
<div class="row items-center q-pt-sm">
<h6 class="q-my-none ellipsis full-width">
<strong
v-text="formatBalance(props.row.balance_msat / 1000)"
></strong>
</h6>
</div>
</q-card-section>
<q-separator />
<q-card-section class="text-left">
<small>
<strong>
<span v-text="$t('currency')"></span>
</strong>
<span v-text="props.row.currency || 'sat'"></span>
</small>
<br />
<small>
<strong>
<span v-text="$t('id')"></span>
:
</strong>
<span v-text="props.row.id"></span>
</small>
</q-card-section>
</q-card>
</div>
</template>
</q-table>
</div>
</div>
</div>
<q-dialog
v-model="showAddWalletDialog.show"
persistent
@hide="showAddWalletDialog = {show: false}"
>
<q-card style="min-width: 350px">
<q-card-section>
<div class="text-h6">
<span v-text="$t('wallet_name')"></span>
</div>
</q-card-section>
<q-card-section class="q-pt-none">
<q-input
dense
v-model="showAddWalletDialog.name"
autofocus
@keyup.enter="submitAddWallet()"
></q-input>
</q-card-section>
<q-card-actions align="right" class="text-primary">
<q-btn flat :label="$t('cancel')" v-close-popup></q-btn>
<q-btn
flat
:label="$t('add_wallet')"
v-close-popup
@click="submitAddWallet()"
></q-btn>
</q-card-actions>
</q-card>
</q-dialog>
{% endblock %}

View File

@ -216,6 +216,25 @@ async def account(
)
@generic_router.get(
"/wallets",
response_class=HTMLResponse,
description="show wallets page",
)
async def wallets(
request: Request,
user: User = Depends(check_user_exists),
):
return template_renderer().TemplateResponse(
request,
"core/wallets.html",
{
"user": user.json(),
"ajax": _is_ajax_request(request),
},
)
@generic_router.get("/service-worker.js")
async def service_worker(request: Request):
return template_renderer().TemplateResponse(
@ -402,7 +421,7 @@ async def audit_index(request: Request, user: User = Depends(check_admin)):
@generic_router.get("/payments", response_class=HTMLResponse)
async def payments_index(request: Request, user: User = Depends(check_admin)):
async def payments_index(request: Request, user: User = Depends(check_user_exists)):
return template_renderer().TemplateResponse(
"payments/index.html",
{

View File

@ -44,7 +44,6 @@ from lnbits.core.services.payments import (
from lnbits.db import Filters, Page
from lnbits.decorators import (
WalletTypeInfo,
check_admin,
check_user_exists,
parse_filters,
require_admin_key,
@ -116,30 +115,44 @@ async def api_payments_history(
@payment_router.get(
"/stats/count",
name="Get payments history for all users",
dependencies=[Depends(check_admin)],
response_model=List[PaymentCountStat],
openapi_extra=generate_filter_params_openapi(PaymentFilters),
)
async def api_payments_counting_stats(
count_by: PaymentCountField = Query("tag"),
filters: Filters[PaymentFilters] = Depends(parse_filters(PaymentFilters)),
user: User = Depends(check_user_exists),
):
return await get_payment_count_stats(count_by, filters)
if user.admin:
# admin user can see payments from all wallets
for_user_id = None
else:
# regular user can only see payments from their wallets
for_user_id = user.id
return await get_payment_count_stats(count_by, filters=filters, user_id=for_user_id)
@payment_router.get(
"/stats/wallets",
name="Get payments history for all users",
dependencies=[Depends(check_admin)],
response_model=List[PaymentWalletStats],
openapi_extra=generate_filter_params_openapi(PaymentFilters),
)
async def api_payments_wallets_stats(
filters: Filters[PaymentFilters] = Depends(parse_filters(PaymentFilters)),
user: User = Depends(check_user_exists),
):
return await get_wallets_stats(filters)
if user.admin:
# admin user can see payments from all wallets
for_user_id = None
else:
# regular user can only see payments from their wallets
for_user_id = user.id
return await get_wallets_stats(filters, user_id=for_user_id)
@payment_router.get(
@ -153,22 +166,13 @@ async def api_payments_daily_stats(
filters: Filters[PaymentFilters] = Depends(parse_filters(PaymentFilters)),
):
if not user.admin:
exc = HTTPException(
status_code=HTTPStatus.FORBIDDEN,
detail="Missing wallet id.",
)
wallet_filter = next(
(f for f in filters.filters if f.field == "wallet_id"), None
)
if not wallet_filter:
raise exc
wallet_id = list((wallet_filter.values or {}).values())
if len(wallet_id) == 0:
raise exc
if not user.get_wallet(wallet_id[0]):
raise exc
return await get_payments_daily_stats(filters)
if user.admin:
# admin user can see payments from all wallets
for_user_id = None
else:
# regular user can only see payments from their wallets
for_user_id = user.id
return await get_payments_daily_stats(filters, user_id=for_user_id)
@payment_router.get(

View File

@ -9,13 +9,18 @@ from fastapi import (
HTTPException,
)
from lnbits.core.crud.wallets import get_wallets_paginated
from lnbits.core.models import CreateWallet, KeyType, User, Wallet
from lnbits.core.models.wallets import WalletsFilters
from lnbits.db import Filters, Page
from lnbits.decorators import (
WalletTypeInfo,
check_user_exists,
parse_filters,
require_admin_key,
require_invoice_key,
)
from lnbits.helpers import generate_filter_params_openapi
from ..crud import (
create_wallet,
@ -38,6 +43,26 @@ async def api_wallet(key_info: WalletTypeInfo = Depends(require_invoice_key)):
return res
@wallet_router.get(
"/paginated",
name="Wallet List",
summary="get paginated list of user wallets",
response_description="list of user wallets",
response_model=Page[Wallet],
openapi_extra=generate_filter_params_openapi(WalletsFilters),
)
async def api_wallets_paginated(
user: User = Depends(check_user_exists),
filters: Filters = Depends(parse_filters(WalletsFilters)),
):
page = await get_wallets_paginated(
user_id=user.id,
filters=filters,
)
return page
@wallet_router.put("/{new_name}")
async def api_update_wallet_name(
new_name: str, key_info: WalletTypeInfo = Depends(require_admin_key)
@ -74,6 +99,7 @@ async def api_update_wallet(
icon: Optional[str] = Body(None),
color: Optional[str] = Body(None),
currency: Optional[str] = Body(None),
pinned: Optional[bool] = Body(None),
key_info: WalletTypeInfo = Depends(require_admin_key),
) -> Wallet:
wallet = await get_wallet(key_info.wallet.id)
@ -82,6 +108,7 @@ async def api_update_wallet(
wallet.name = name or wallet.name
wallet.extra.icon = icon or wallet.extra.icon
wallet.extra.color = color or wallet.extra.color
wallet.extra.pinned = pinned if pinned is not None else wallet.extra.pinned
wallet.currency = currency if currency is not None else wallet.currency
await update_wallet(wallet)
return wallet

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -49,8 +49,11 @@ window.localisation.en = {
'This QR code contains your wallet URL with full access. You can scan it from your phone to open your wallet from there.',
access_wallet_on_mobile: 'Mobile Access',
wallet: 'Wallet: ',
wallet_name: 'Wallet name',
wallets: 'Wallets',
add_wallet: 'Add a new wallet',
add_wallet: 'Add wallet',
add_new_wallet: 'Add a new wallet',
pin_wallet: 'Pin wallet',
delete_wallet: 'Delete wallet',
delete_wallet_desc:
'This whole wallet will be deleted, the funds will be UNRECOVERABLE.',
@ -125,6 +128,7 @@ window.localisation.en = {
no_extensions: "You don't have any extensions installed :(",
created: 'Created',
search_extensions: 'Search extensions',
search_wallets: 'Search wallets',
extension_sources: 'Extension Sources',
ext_sources_hint: 'Repositories from where the extensions can be downloaded',
ext_sources_label:
@ -264,6 +268,7 @@ window.localisation.en = {
notification_source_label:
'Source URL (only use the official LNbits status source, and sources you can trust)',
more: 'more',
more_count: '{count} more',
less: 'less',
releases: 'Releases',
watchdog: 'Watchdog',
@ -330,6 +335,7 @@ window.localisation.en = {
username: 'Username',
pubkey: 'Public Key',
user_id: 'User ID',
id: 'ID',
email: 'Email',
first_name: 'First Name',
last_name: 'Last Name',
@ -356,6 +362,7 @@ window.localisation.en = {
gradient_background: 'Gradient Background',
language: 'Language',
color_scheme: 'Color Scheme',
visible_wallet_count: 'Visible Wallet Count',
admin_settings: 'Admin Settings',
extension_cost: 'This release requires a payment of minimum {cost} sats.',
extension_paid_sats: 'You have already paid {paid_sats} sats.',

View File

@ -197,7 +197,8 @@ window.LNbits = {
email: data.email,
extensions: data.extensions,
wallets: data.wallets,
super_user: data.super_user
super_user: data.super_user,
extra: data.extra ?? {}
}
const mapWallet = this.wallet
obj.wallets = obj.wallets
@ -205,6 +206,9 @@ window.LNbits = {
return mapWallet(obj)
})
.sort((a, b) => {
if (a.extra.pinned !== b.extra.pinned) {
return a.extra.pinned ? -1 : 1
}
return a.name.localeCompare(b.name)
})
obj.walletOptions = obj.wallets.map(obj => {
@ -213,6 +217,10 @@ window.LNbits = {
value: obj.id
}
})
obj.hiddenWalletsCount = Math.max(
0,
data.wallets.length - data.extra.visible_wallet_count
)
return obj
},
wallet(data) {
@ -508,6 +516,9 @@ window.windowMixin = {
}
this.$q.localStorage.set('lnbits.walletFlip', this.walletFlip)
},
goToWallets() {
window.location = '/wallets'
},
submitAddWallet() {
if (
this.showAddWalletDialog.name &&

View File

@ -103,14 +103,7 @@ window.app.component('lnbits-extension-list', {
window.app.component('lnbits-manage', {
mixins: [window.windowMixin],
template: '#lnbits-manage',
props: [
'showAdmin',
'showNode',
'showExtensions',
'showUsers',
'showAudit',
'showPayments'
],
props: ['showAdmin', 'showNode', 'showExtensions', 'showUsers', 'showAudit'],
methods: {
isActive(path) {
return window.location.pathname === path

View File

@ -193,6 +193,15 @@ const routes = [
scripts: ['/static/js/account.js']
}
},
{
path: '/wallets',
name: 'Wallets',
component: DynamicComponent,
props: {
fetchUrl: '/wallets',
scripts: ['/static/js/wallets.js']
}
},
{
path: '/node',
name: 'Node',

View File

@ -662,7 +662,7 @@ window.WalletPageLogic = {
}
}
Quasar.Notify.create({
message: 'Wallet and user updated.',
message: 'Wallet updated.',
type: 'positive',
timeout: 3500
})

View File

@ -0,0 +1,89 @@
window.WalletsPageLogic = {
mixins: [window.windowMixin],
data() {
return {
user: null,
tab: 'wallets',
wallets: [],
showAddWalletDialog: {show: false},
walletsTable: {
columns: [
{
name: 'name',
align: 'left',
label: 'Name',
field: 'name',
sortable: true
},
{
name: 'currency',
align: 'center',
label: 'Currency',
field: 'currency',
sortable: true
},
{
name: 'updated_at',
align: 'right',
label: 'Last Updated',
field: 'updated_at',
sortable: true
}
],
pagination: {
sortBy: 'updated_at',
rowsPerPage: 12,
page: 1,
descending: true,
rowsNumber: 10
},
search: '',
hideEmpty: true,
loading: false
}
}
},
watch: {
'walletsTable.search': {
handler() {
const props = {}
if (this.walletsTable.search) {
props['search'] = this.walletsTable.search
}
this.getUserWallets()
}
}
},
methods: {
async getUserWallets(props) {
try {
this.walletsTable.loading = true
const params = LNbits.utils.prepareFilterQuery(this.walletsTable, props)
const {data} = await LNbits.api.request(
'GET',
`/api/v1/wallet/paginated?${params}`,
null
)
this.wallets = data.data
this.walletsTable.pagination.rowsNumber = data.total
} catch (e) {
LNbits.utils.notifyApiError(e)
} finally {
this.walletsTable.loading = false
}
},
goToWallet(walletId) {
window.location = `/wallet?wal=${walletId}`
},
formattedFiatAmount(amount, currency) {
return LNbits.utils.formatCurrency(Number(amount).toFixed(2), currency)
},
formattedSatAmount(amount) {
return LNbits.utils.formatMsat(amount) + ' sat'
}
},
async created() {
await this.getUserWallets()
}
}

View File

@ -179,12 +179,12 @@
>
<q-scroll-area style="height: 100%">
<q-item>
<q-item-section>
<q-item-section class="cursor-pointer" @click="goToWallets()">
<q-item-label
:style="$q.dark.isActive ? 'color:rgba(255, 255, 255, 0.64)' : ''"
class="q-item__label q-item__label--header q-pa-none"
header
v-text="$t('wallets')"
v-text="$t('wallets') + ' (' + g.user.wallets.length + ')'"
></q-item-label>
</q-item-section>
<q-item-section side>
@ -211,7 +211,6 @@
:show-admin="'{{LNBITS_ADMIN_UI}}' == 'True'"
:show-users="'{{LNBITS_ADMIN_UI}}' == 'True'"
:show-audit="'{{LNBITS_AUDIT_ENABLED}}' == 'True'"
:show-payments="'{{LNBITS_ADMIN_UI}}' == 'True'"
:show-node="'{{LNBITS_NODE_UI}}' == 'True'"
:show-extensions="'{{LNBITS_EXTENSIONS_DEACTIVATE_ALL}}' == 'False'"
></lnbits-manage>
@ -248,7 +247,7 @@
@click="showAddWalletDialog.show = true"
>
<q-tooltip
><span v-text="$t('add_wallet')"></span
><span v-text="$t('add_new_wallet')"></span
></q-tooltip>
</q-btn>
<q-dialog
@ -258,7 +257,9 @@
>
<q-card style="min-width: 350px">
<q-card-section>
<div class="text-h6">Wallet name</div>
<div class="text-h6">
<span v-text="$t('wallet_name')"></span>
</div>
</q-card-section>
<q-card-section class="q-pt-none">
@ -271,10 +272,14 @@
</q-card-section>
<q-card-actions align="right" class="text-primary">
<q-btn flat label="Cancel" v-close-popup></q-btn>
<q-btn
flat
label="Add wallet"
:label="$t('cancel')"
v-close-popup
></q-btn>
<q-btn
flat
:label="$t('add_wallet')"
v-close-popup
@click="submitAddWallet()"
></q-btn>
@ -285,7 +290,7 @@
</q-card-section>
</q-card>
<q-card
v-for="wallet in g.user.wallets"
v-for="wallet in g.user.wallets.slice(0, g.user.extra.visible_wallet_count || 10)"
:key="wallet.id"
clickable
@click="selectWallet(wallet)"
@ -331,6 +336,29 @@
</div>
</q-card-section>
</q-card>
<q-card
v-if="g.user.hiddenWalletsCount"
class="wallet-list-card"
>
<q-card-section
class="flex flex-center column full-height text-center"
>
<div>
<q-btn
round
color="primary"
icon="more_horiz"
@click="goToWallets()"
>
<q-tooltip
><span
v-text="$t('more_count', {count: g.user.hiddenWalletsCount})"
></span
></q-tooltip>
</q-btn>
</div>
</q-card-section>
</q-card>
</div>
</q-scroll-area>

View File

@ -5,7 +5,10 @@
class="lnbits-drawer__q-list"
>
<q-item
v-for="walletRec in g.user.wallets"
v-for="walletRec in g.user.wallets.slice(
0,
g.user.extra.visible_wallet_count || 10
)"
:key="walletRec.id"
clickable
:active="g.wallet && g.wallet.id === walletRec.id"
@ -43,6 +46,22 @@
<q-item-section side v-show="g.wallet && g.wallet.id === walletRec.id">
</q-item-section>
</q-item>
<q-item
v-if="g.user.hiddenWalletsCount > 0"
clickable
@click="goToWallets()"
>
<q-item-section side>
<q-icon name="more_horiz" color="grey-5" size="md"></q-icon>
</q-item-section>
<q-item-section>
<q-item-label
lines="1"
class="text-caption"
v-text="$t('more_count', {count: g.user.hiddenWalletsCount})"
></q-item-label>
</q-item-section>
</q-item>
<q-item clickable @click="showForm = !showForm">
<q-item-section side>
<q-icon
@ -55,7 +74,7 @@
<q-item-label
lines="1"
class="text-caption"
v-text="$t('add_wallet')"
v-text="$t('add_new_wallet')"
></q-item-label>
</q-item-section>
</q-item>
@ -178,19 +197,19 @@
<q-item-label lines="1" v-text="$t('api_watch')"></q-item-label>
</q-item-section>
</q-item>
<q-item v-if="showPayments" to="/payments">
<q-item-section side>
<q-icon
name="query_stats"
:color="isActive('/payments') ? 'primary' : 'grey-5'"
size="md"
></q-icon>
</q-item-section>
<q-item-section>
<q-item-label lines="1" v-text="$t('payments')"></q-item-label>
</q-item-section>
</q-item>
</div>
<q-item to="/payments">
<q-item-section side>
<q-icon
name="query_stats"
:color="isActive('/payments') ? 'primary' : 'grey-5'"
size="md"
></q-icon>
</q-item-section>
<q-item-section>
<q-item-label lines="1" v-text="$t('payments')"></q-item-label>
</q-item-section>
</q-item>
<q-item v-if="showExtensions" to="/extensions">
<q-item-section side>
<q-icon
@ -1185,7 +1204,7 @@
color="primary"
:disable="walletName == ''"
type="submit"
:label="$t('add_wallet')"
:label="$t('add_new_wallet')"
class="full-width q-mb-sm"
></q-btn>
<q-btn