Feat: Adds wallet icon/color select (#2917)

This commit is contained in:
Arc 2025-02-04 13:03:35 +00:00 committed by GitHub
parent dd9b217fdf
commit 960c58db87
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 205 additions and 63 deletions

View file

@ -161,4 +161,4 @@ async def get_wallet_for_key(
async def get_total_balance(conn: Optional[Connection] = None): async def get_total_balance(conn: Optional[Connection] = None):
result = await (conn or db).execute("SELECT SUM(balance) as balance FROM balances") result = await (conn or db).execute("SELECT SUM(balance) as balance FROM balances")
row = result.mappings().first() row = result.mappings().first()
return row.get("balance", 0) return row.get("balance", 0) or 0

View file

@ -692,3 +692,10 @@ async def m030_add_user_api_tokens_column(db: Connection):
ALTER TABLE accounts ADD COLUMN access_control_list TEXT ALTER TABLE accounts ADD COLUMN access_control_list TEXT
""" """
) )
async def m031_add_color_and_icon_to_wallets(db: Connection):
"""
Adds icon and color columns to wallets.
"""
await db.execute("ALTER TABLE wallets ADD COLUMN extra TEXT")

View file

@ -23,6 +23,11 @@ class BaseWallet(BaseModel):
balance_msat: int balance_msat: int
class WalletExtra(BaseModel):
icon: str = "flash_on"
color: str = "primary"
class Wallet(BaseModel): class Wallet(BaseModel):
id: str id: str
user: str user: str
@ -34,6 +39,7 @@ class Wallet(BaseModel):
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
currency: Optional[str] = None currency: Optional[str] = None
balance_msat: int = Field(default=0, no_database=True) balance_msat: int = Field(default=0, no_database=True)
extra: WalletExtra = WalletExtra()
@property @property
def balance(self) -> int: def balance(self) -> int:

View file

@ -206,11 +206,29 @@
{% else %} {% else %}
<div v-if="!mobileSimple" class="col-12 col-md-5 q-gutter-y-md"> <div v-if="!mobileSimple" class="col-12 col-md-5 q-gutter-y-md">
<q-card> <q-card>
<q-card-section> <q-card-section class="q-pb-xs">
<h6 class="text-subtitle1 q-mt-none q-mb-sm"> <div class="row items-center">
{{ SITE_TITLE }} Wallet: <q-avatar
<strong><em v-text="g.wallet.name"></em></strong> size="lg"
</h6> :icon="g.wallet.extra.icon"
:text-color="$q.dark.isActive ? 'black' : 'grey-3'"
:color="g.wallet.extra.color"
>
</q-avatar>
<q-btn
@click="icon.show = true"
round
color="grey-5"
text-color="black"
size="xs"
icon="add"
style="position: relative; left: -20px; bottom: -10px"
></q-btn>
<div class="text-subtitle1 q-mt-none q-mb-none">
{{ SITE_TITLE }} Wallet:
<strong><em v-text="g.wallet.name"></em></strong>
</div>
</div>
</q-card-section> </q-card-section>
<q-card-section class="q-pa-none"> <q-card-section class="q-pa-none">
<q-separator></q-separator> <q-separator></q-separator>
@ -388,6 +406,50 @@
</div> </div>
</div> </div>
<q-dialog v-model="icon.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="setIcon" class="q-gutter-md">
<div class="q-gutter-sm q-pa-sm flex flex-wrap justify-center">
<!-- Loop through all icons -->
<q-btn
v-for="(thisIcon, index) in icon.options"
:key="index"
@click="setSelectedIcon(thisIcon)"
round
text-color="black"
:color="icon.data.icon === thisIcon ? icon.data.color || 'primary' : 'grey-5'"
size="md"
:icon="thisIcon"
class="q-mb-sm"
></q-btn>
</div>
<div class="q-pa-sm flex justify-between items-center">
<div class="flex q-pl-lg">
<!-- Color options -->
<q-btn
v-for="(color, index) in icon.colorOptions"
:key="'color-' + index"
@click="setSelectedColor(color)"
round
:color="color"
size="xs"
style="width: 24px; height: 24px; min-width: 24px; padding: 0"
class="q-mr-xs"
></q-btn>
</div>
<q-btn
unelevated
color="primary"
:disable="!icon.data.icon"
type="submit"
>
Save Icon
</q-btn>
</div>
</q-form>
</q-card>
</q-dialog>
<q-dialog <q-dialog
v-model="receive.show" v-model="receive.show"
position="top" position="top"

View file

@ -59,6 +59,8 @@ async def api_update_wallet_name(
@wallet_router.patch("") @wallet_router.patch("")
async def api_update_wallet( async def api_update_wallet(
name: Optional[str] = Body(None), name: Optional[str] = Body(None),
icon: Optional[str] = Body(None),
color: Optional[str] = Body(None),
currency: Optional[str] = Body(None), currency: Optional[str] = Body(None),
key_info: WalletTypeInfo = Depends(require_admin_key), key_info: WalletTypeInfo = Depends(require_admin_key),
) -> Wallet: ) -> Wallet:
@ -66,6 +68,8 @@ async def api_update_wallet(
if not wallet: if not wallet:
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Wallet not found") raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Wallet not found")
wallet.name = name or wallet.name wallet.name = name or wallet.name
wallet.extra.icon = icon or wallet.extra.icon
wallet.extra.color = color or wallet.extra.color
wallet.currency = currency if currency is not None else wallet.currency wallet.currency = currency if currency is not None else wallet.currency
await update_wallet(wallet) await update_wallet(wallet)
return wallet return wallet

File diff suppressed because one or more lines are too long

View file

@ -216,7 +216,8 @@ window.LNbits = {
name: data.name, name: data.name,
adminkey: data.adminkey, adminkey: data.adminkey,
inkey: data.inkey, inkey: data.inkey,
currency: data.currency currency: data.currency,
extra: data.extra
} }
newWallet.msat = data.balance_msat newWallet.msat = data.balance_msat
newWallet.sat = Math.floor(data.balance_msat / 1000) newWallet.sat = Math.floor(data.balance_msat / 1000)

View file

@ -44,6 +44,62 @@ window.WalletPageLogic = {
show: false, show: false,
location: window.location location: window.location
}, },
icon: {
show: false,
data: {},
colorOptions: [
'primary',
'purple',
'orange',
'green',
'brown',
'blue',
'red',
'pink'
],
options: [
'home',
'star',
'bolt',
'paid',
'savings',
'store',
'videocam',
'music_note',
'flight',
'train',
'directions_car',
'school',
'construction',
'science',
'sports_esports',
'sports_tennis',
'theaters',
'water',
'headset_mic',
'videogame_asset',
'person',
'group',
'pets',
'sunny',
'elderly',
'verified',
'snooze',
'mail',
'forum',
'shopping_cart',
'shopping_bag',
'attach_money',
'print_connect',
'dark_mode',
'light_mode',
'android',
'network_wifi',
'shield',
'fitness_center',
'lunch_dining'
]
},
update: { update: {
name: null, name: null,
currency: null currency: null
@ -149,6 +205,16 @@ window.WalletPageLogic = {
handleBalanceUpdate(value) { handleBalanceUpdate(value) {
this.g.wallet.sat = this.g.wallet.sat + value this.g.wallet.sat = this.g.wallet.sat + value
}, },
setSelectedIcon(selectedIcon) {
this.icon.data.icon = selectedIcon
},
setSelectedColor(selectedColor) {
this.icon.data.color = selectedColor
},
setIcon() {
this.updateWallet(this.icon.data)
this.icon.show = false
},
createInvoice() { createInvoice() {
this.receive.status = 'loading' this.receive.status = 'loading'
if (LNBITS_DENOMINATION != 'sats') { if (LNBITS_DENOMINATION != 'sats') {

View file

@ -29,6 +29,7 @@
<link async="async" rel="manifest" href="{{ web_manifest }}" /> <link async="async" rel="manifest" href="{{ web_manifest }}" />
{% endif %} {% block head_scripts %}{% endblock %} {% endif %} {% block head_scripts %}{% endblock %}
</head> </head>
<body data-theme="bitcoin"> <body data-theme="bitcoin">
<div id="vue"> <div id="vue">
<q-layout view="hHh lpR lfr" v-cloak> <q-layout view="hHh lpR lfr" v-cloak>
@ -228,19 +229,14 @@
<div class="row items-center"> <div class="row items-center">
<q-avatar <q-avatar
size="lg" size="lg"
:color=" :text-color="$q.dark.isActive ? 'black' : 'grey-3'"
g.wallet && g.wallet.id === wallet.id :class="g.wallet && g.wallet.id === wallet.id
? $q.dark.isActive ? ''
? 'primary' : 'disabled'
: 'primary' "
: 'grey-5' :color="g.wallet && g.wallet.id === wallet.id ? wallet.extra.color : wallet.extra.color"
" :icon="g.wallet && g.wallet.id === wallet.id ? wallet.extra.icon : wallet.extra.icon"
> >
<q-icon
name="flash_on"
:size="$q.dark.isActive ? '21px' : '20px'"
:color="$q.dark.isActive ? 'black' : 'grey-3'"
></q-icon>
</q-avatar> </q-avatar>
<div <div
class="text-h6 q-pl-md" class="text-h6 q-pl-md"
@ -321,39 +317,39 @@
<script src="{{ static_url_for('static', url) }}"></script> <script src="{{ static_url_for('static', url) }}"></script>
{% endfor %} {% endfor %}
<script type="text/javascript"> <script type="text/javascript">
const SITE_DESCRIPTION = {{ SITE_DESCRIPTION | tojson}} const SITE_DESCRIPTION = {{ SITE_DESCRIPTION | tojson}}
const themes = {{ LNBITS_THEME_OPTIONS | tojson }} const themes = {{ LNBITS_THEME_OPTIONS | tojson }}
const LNBITS_DENOMINATION = {{ LNBITS_DENOMINATION | tojson }} const LNBITS_DENOMINATION = {{ LNBITS_DENOMINATION | tojson }}
const LNBITS_VERSION = {{ LNBITS_VERSION | tojson }} const LNBITS_VERSION = {{ LNBITS_VERSION | tojson }}
const LNBITS_QR_LOGO = {{ LNBITS_QR_LOGO | tojson }} const LNBITS_QR_LOGO = {{ LNBITS_QR_LOGO | tojson }}
const USE_DEFAULT_REACTION = {{ USE_DEFAULT_REACTION | tojson }} const USE_DEFAULT_REACTION = {{ USE_DEFAULT_REACTION | tojson }}
const USE_DEFAULT_THEME = {{ USE_DEFAULT_THEME | tojson }} const USE_DEFAULT_THEME = {{ USE_DEFAULT_THEME | tojson }}
const USE_DEFAULT_BORDER = {{ USE_DEFAULT_BORDER | tojson }} const USE_DEFAULT_BORDER = {{ USE_DEFAULT_BORDER | tojson }}
const USE_DEFAULT_GRADIENT = {{ USE_DEFAULT_GRADIENT | lower | tojson }} const USE_DEFAULT_GRADIENT = {{ USE_DEFAULT_GRADIENT | lower | tojson }}
const USE_DEFAULT_BGIMAGE = "{{ USE_DEFAULT_BGIMAGE or None | tojson }}" const USE_DEFAULT_BGIMAGE = "{{ USE_DEFAULT_BGIMAGE or None | tojson }}"
if (themes && themes.length) { if (themes && themes.length) {
window.allowedThemes = themes.map(str => str.trim()) window.allowedThemes = themes.map(str => str.trim())
} }
window.langs = [ window.langs = [
{ value: 'en', label: 'English', display: '🇬🇧 EN' }, { value: 'en', label: 'English', display: '🇬🇧 EN' },
{ value: 'de', label: 'Deutsch', display: '🇩🇪 DE' }, { value: 'de', label: 'Deutsch', display: '🇩🇪 DE' },
{ value: 'es', label: 'Español', display: '🇪🇸 ES' }, { value: 'es', label: 'Español', display: '🇪🇸 ES' },
{ value: 'jp', label: '日本語', display: '🇯🇵 JP' }, { value: 'jp', label: '日本語', display: '🇯🇵 JP' },
{ value: 'cn', label: '中文', display: '🇨🇳 CN' }, { value: 'cn', label: '中文', display: '🇨🇳 CN' },
{ value: 'fr', label: 'Français', display: '🇫🇷 FR' }, { value: 'fr', label: 'Français', display: '🇫🇷 FR' },
{ value: 'it', label: 'Italiano', display: '🇮🇹 IT' }, { value: 'it', label: 'Italiano', display: '🇮🇹 IT' },
{ value: 'pi', label: 'Pirate', display: '🏴‍☠️ PI' }, { value: 'pi', label: 'Pirate', display: '🏴‍☠️ PI' },
{ value: 'nl', label: 'Nederlands', display: '🇳🇱 NL' }, { value: 'nl', label: 'Nederlands', display: '🇳🇱 NL' },
{ value: 'we', label: 'Cymraeg', display: '🏴󠁧󠁢󠁷󠁬󠁳󠁿 CY' }, { value: 'we', label: 'Cymraeg', display: '🏴󠁧󠁢󠁷󠁬󠁳󠁿 CY' },
{ value: 'pl', label: 'Polski', display: '🇵🇱 PL' }, { value: 'pl', label: 'Polski', display: '🇵🇱 PL' },
{ value: 'pt', label: 'Português', display: '🇵🇹 PT' }, { value: 'pt', label: 'Português', display: '🇵🇹 PT' },
{ value: 'br', label: 'Português do Brasil', display: '🇧🇷 BR' }, { value: 'br', label: 'Português do Brasil', display: '🇧🇷 BR' },
{ value: 'cs', label: 'Česky', display: '🇨🇿 CS' }, { value: 'cs', label: 'Česky', display: '🇨🇿 CS' },
{ value: 'sk', label: 'Slovensky', display: '🇸🇰 SK' }, { value: 'sk', label: 'Slovensky', display: '🇸🇰 SK' },
{ value: 'kr', label: '한국어', display: '🇰🇷 KR' }, { value: 'kr', label: '한국어', display: '🇰🇷 KR' },
{ value: 'fi', label: 'Suomi', display: '🇫🇮 FI' } { value: 'fi', label: 'Suomi', display: '🇫🇮 FI' }
] ]
window.LOCALE = 'en' window.LOCALE = 'en'
window.dateFormat = 'YYYY-MM-DD HH:mm' window.dateFormat = 'YYYY-MM-DD HH:mm'
window.i18n = new VueI18n.createI18n({ window.i18n = new VueI18n.createI18n({
locale: window.LOCALE, locale: window.LOCALE,

View file

@ -13,20 +13,20 @@
> >
<q-item-section side> <q-item-section side>
<q-avatar <q-avatar
size="md" size="lg"
:text-color="$q.dark.isActive ? 'black' : 'grey-3'"
:class="g.wallet && g.wallet.id === walletRec.id ? '' : 'disabled'"
:color=" :color="
g.wallet && g.wallet.id === walletRec.id g.wallet && g.wallet.id === walletRec.id
? $q.dark.isActive ? walletRec.extra.color
? 'primary' : walletRec.extra.color
: 'primary' "
: 'grey-5' :icon="
g.wallet && g.wallet.id === walletRec.id
? walletRec.extra.icon
: walletRec.extra.icon
" "
> >
<q-icon
name="flash_on"
:size="$q.dark.isActive ? '21px' : '20px'"
:color="$q.dark.isActive ? 'blue-grey-10' : 'grey-3'"
></q-icon>
</q-avatar> </q-avatar>
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>