mirror of
https://github.com/lnbits/lnbits-legend.git
synced 2025-02-22 06:21:53 +01:00
Initial commit 🎵
This commit is contained in:
parent
bcecf6d431
commit
c0b18d78cc
12 changed files with 770 additions and 0 deletions
5
lnbits/extensions/jukebox/README.md
Normal file
5
lnbits/extensions/jukebox/README.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# Jukebox
|
||||
|
||||
To use this extension you need a Spotify client ID and client secret. You get these by creating an app in the Spotify developers dashboard here https://developer.spotify.com/dashboard/applications
|
||||
|
||||
Select the playlists you want people to be able to pay for, share the frontend page, profit :)
|
14
lnbits/extensions/jukebox/__init__.py
Normal file
14
lnbits/extensions/jukebox/__init__.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
from quart import Blueprint
|
||||
|
||||
from lnbits.db import Database
|
||||
|
||||
db = Database("ext_jukebox")
|
||||
|
||||
jukebox_ext: Blueprint = Blueprint(
|
||||
"jukebox", __name__, static_folder="static", template_folder="templates"
|
||||
)
|
||||
|
||||
|
||||
from .views_api import * # noqa
|
||||
from .views import * # noqa
|
||||
from .lnurl import * # noqa
|
6
lnbits/extensions/jukebox/config.json
Normal file
6
lnbits/extensions/jukebox/config.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "Jukebox",
|
||||
"short_description": "Spotify jukebox middleware",
|
||||
"icon": "audiotrack",
|
||||
"contributors": ["benarc"]
|
||||
}
|
34
lnbits/extensions/jukebox/crud.py
Normal file
34
lnbits/extensions/jukebox/crud.py
Normal file
|
@ -0,0 +1,34 @@
|
|||
from typing import List, Optional
|
||||
|
||||
from . import db
|
||||
from .wordlists import animals
|
||||
from .models import Shop, Item
|
||||
|
||||
|
||||
async def create_update_jukebox(wallet_id: str) -> int:
|
||||
juke_id = urlsafe_short_hash()
|
||||
result = await db.execute(
|
||||
"""
|
||||
INSERT INTO jukebox (id, wallet, user, secret, token, playlists)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(juke_id, wallet_id, "", "", "", ""),
|
||||
)
|
||||
return result._result_proxy.lastrowid
|
||||
|
||||
|
||||
async def get_jukebox(id: str) -> Optional[Jukebox]:
|
||||
row = await db.fetchone("SELECT * FROM jukebox WHERE id = ?", (id,))
|
||||
return Shop(**dict(row)) if row else None
|
||||
|
||||
async def get_jukeboxs(id: str) -> Optional[Jukebox]:
|
||||
row = await db.fetchone("SELECT * FROM jukebox WHERE id = ?", (id,))
|
||||
return Shop(**dict(row)) if row else None
|
||||
|
||||
async def delete_jukebox(shop: int, item_id: int):
|
||||
await db.execute(
|
||||
"""
|
||||
DELETE FROM jukebox WHERE id = ?
|
||||
""",
|
||||
(shop, item_id),
|
||||
)
|
16
lnbits/extensions/jukebox/migrations.py
Normal file
16
lnbits/extensions/jukebox/migrations.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
async def m001_initial(db):
|
||||
"""
|
||||
Initial jukebox table.
|
||||
"""
|
||||
await db.execute(
|
||||
"""
|
||||
CREATE TABLE jukebox (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
wallet TEXT NOT NULL,
|
||||
user TEXT NOT NULL,
|
||||
secret TEXT NOT NULL,
|
||||
token TEXT NOT NULL,
|
||||
playlists TEXT NOT NULL
|
||||
);
|
||||
"""
|
||||
)
|
18
lnbits/extensions/jukebox/models.py
Normal file
18
lnbits/extensions/jukebox/models.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
import json
|
||||
import base64
|
||||
import hashlib
|
||||
from collections import OrderedDict
|
||||
from quart import url_for
|
||||
from typing import NamedTuple, Optional, List, Dict
|
||||
|
||||
class Jukebox(NamedTuple):
|
||||
id: int
|
||||
wallet: str
|
||||
user: str
|
||||
secret: str
|
||||
token: str
|
||||
playlists: str
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row) -> "Charges":
|
||||
return cls(**dict(row))
|
216
lnbits/extensions/jukebox/static/js/index.js
Normal file
216
lnbits/extensions/jukebox/static/js/index.js
Normal file
|
@ -0,0 +1,216 @@
|
|||
/* globals Quasar, Vue, _, VueQrcode, windowMixin, LNbits, LOCALE */
|
||||
|
||||
Vue.component(VueQrcode.name, VueQrcode)
|
||||
|
||||
const pica = window.pica()
|
||||
|
||||
const defaultItemData = {
|
||||
unit: 'sat'
|
||||
}
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data() {
|
||||
return {
|
||||
selectedWallet: null,
|
||||
confirmationMethod: 'wordlist',
|
||||
wordlistTainted: false,
|
||||
jukebox: {
|
||||
method: null,
|
||||
wordlist: [],
|
||||
items: []
|
||||
},
|
||||
itemDialog: {
|
||||
show: false,
|
||||
data: {...defaultItemData},
|
||||
units: ['sat']
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
printItems() {
|
||||
return this.jukebox.items.filter(({enabled}) => enabled)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openNewDialog() {
|
||||
this.itemDialog.show = true
|
||||
this.itemDialog.data = {...defaultItemData}
|
||||
},
|
||||
openUpdateDialog(itemId) {
|
||||
this.itemDialog.show = true
|
||||
let item = this.jukebox.items.find(item => item.id === itemId)
|
||||
this.itemDialog.data = item
|
||||
},
|
||||
imageAdded(file) {
|
||||
let blobURL = URL.createObjectURL(file)
|
||||
let image = new Image()
|
||||
image.src = blobURL
|
||||
image.onload = async () => {
|
||||
let canvas = document.createElement('canvas')
|
||||
canvas.setAttribute('width', 100)
|
||||
canvas.setAttribute('height', 100)
|
||||
await pica.resize(image, canvas, {
|
||||
quality: 0,
|
||||
alpha: true,
|
||||
unsharpAmount: 95,
|
||||
unsharpRadius: 0.9,
|
||||
unsharpThreshold: 70
|
||||
})
|
||||
this.itemDialog.data.image = canvas.toDataURL()
|
||||
this.itemDialog = {...this.itemDialog}
|
||||
}
|
||||
},
|
||||
imageCleared() {
|
||||
this.itemDialog.data.image = null
|
||||
this.itemDialog = {...this.itemDialog}
|
||||
},
|
||||
disabledAddItemButton() {
|
||||
return (
|
||||
!this.itemDialog.data.name ||
|
||||
this.itemDialog.data.name.length === 0 ||
|
||||
!this.itemDialog.data.price ||
|
||||
!this.itemDialog.data.description ||
|
||||
!this.itemDialog.data.unit ||
|
||||
this.itemDialog.data.unit.length === 0
|
||||
)
|
||||
},
|
||||
changedWallet(wallet) {
|
||||
this.selectedWallet = wallet
|
||||
this.loadShop()
|
||||
},
|
||||
loadShop() {
|
||||
LNbits.api
|
||||
.request('GET', '/jukebox/api/v1/jukebox', this.selectedWallet.inkey)
|
||||
.then(response => {
|
||||
this.jukebox = response.data
|
||||
this.confirmationMethod = response.data.method
|
||||
this.wordlistTainted = false
|
||||
})
|
||||
.catch(err => {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
})
|
||||
},
|
||||
async setMethod() {
|
||||
try {
|
||||
await LNbits.api.request(
|
||||
'PUT',
|
||||
'/jukebox/api/v1/jukebox/method',
|
||||
this.selectedWallet.inkey,
|
||||
{method: this.confirmationMethod, wordlist: this.jukebox.wordlist}
|
||||
)
|
||||
} catch (err) {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
return
|
||||
}
|
||||
|
||||
this.$q.notify({
|
||||
message:
|
||||
`Method set to ${this.confirmationMethod}.` +
|
||||
(this.confirmationMethod === 'wordlist' ? ' Counter reset.' : ''),
|
||||
timeout: 700
|
||||
})
|
||||
this.loadShop()
|
||||
},
|
||||
async sendItem() {
|
||||
let {id, name, image, description, price, unit} = this.itemDialog.data
|
||||
const data = {
|
||||
name,
|
||||
description,
|
||||
image,
|
||||
price,
|
||||
unit
|
||||
}
|
||||
|
||||
try {
|
||||
if (id) {
|
||||
await LNbits.api.request(
|
||||
'PUT',
|
||||
'/jukebox/api/v1/jukebox/items/' + id,
|
||||
this.selectedWallet.inkey,
|
||||
data
|
||||
)
|
||||
} else {
|
||||
await LNbits.api.request(
|
||||
'POST',
|
||||
'/jukebox/api/v1/jukebox/items',
|
||||
this.selectedWallet.inkey,
|
||||
data
|
||||
)
|
||||
this.$q.notify({
|
||||
message: `Item '${this.itemDialog.data.name}' added.`,
|
||||
timeout: 700
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
return
|
||||
}
|
||||
|
||||
this.loadShop()
|
||||
this.itemDialog.show = false
|
||||
this.itemDialog.data = {...defaultItemData}
|
||||
},
|
||||
toggleItem(itemId) {
|
||||
let item = this.jukebox.items.find(item => item.id === itemId)
|
||||
item.enabled = !item.enabled
|
||||
|
||||
LNbits.api
|
||||
.request(
|
||||
'PUT',
|
||||
'/jukebox/api/v1/jukebox/items/' + itemId,
|
||||
this.selectedWallet.inkey,
|
||||
item
|
||||
)
|
||||
.then(response => {
|
||||
this.$q.notify({
|
||||
message: `Item ${item.enabled ? 'enabled' : 'disabled'}.`,
|
||||
timeout: 700
|
||||
})
|
||||
this.jukebox.items = this.jukebox.items
|
||||
})
|
||||
.catch(err => {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
})
|
||||
},
|
||||
deleteItem(itemId) {
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this item?')
|
||||
.onOk(() => {
|
||||
LNbits.api
|
||||
.request(
|
||||
'DELETE',
|
||||
'/jukebox/api/v1/jukebox/items/' + itemId,
|
||||
this.selectedWallet.inkey
|
||||
)
|
||||
.then(response => {
|
||||
this.$q.notify({
|
||||
message: `Item deleted.`,
|
||||
timeout: 700
|
||||
})
|
||||
this.jukebox.items.splice(
|
||||
this.jukebox.items.findIndex(item => item.id === itemId),
|
||||
1
|
||||
)
|
||||
})
|
||||
.catch(err => {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.selectedWallet = this.g.user.wallets[0]
|
||||
this.loadShop()
|
||||
|
||||
LNbits.api
|
||||
.request('GET', '/jukebox/api/v1/currencies')
|
||||
.then(response => {
|
||||
this.itemDialog = {...this.itemDialog, units: ['sat', ...response.data]}
|
||||
})
|
||||
.catch(err => {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
})
|
||||
}
|
||||
})
|
146
lnbits/extensions/jukebox/templates/jukebox/_api_docs.html
Normal file
146
lnbits/extensions/jukebox/templates/jukebox/_api_docs.html
Normal file
|
@ -0,0 +1,146 @@
|
|||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="swap_vertical_circle"
|
||||
label="How to use"
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<ol>
|
||||
<li>Register items.</li>
|
||||
<li>
|
||||
Print QR codes and paste them on your store, your menu, somewhere,
|
||||
somehow.
|
||||
</li>
|
||||
<li>
|
||||
Clients scan the QR codes and get information about the items plus the
|
||||
price on their phones directly (they must have internet)
|
||||
</li>
|
||||
<li>
|
||||
Once they decide to pay, they'll get an invoice on their phones
|
||||
automatically
|
||||
</li>
|
||||
<li>
|
||||
When the payment is confirmed, a confirmation code will be issued for
|
||||
them.
|
||||
</li>
|
||||
</ol>
|
||||
<p>
|
||||
The confirmation codes are words from a predefined sequential word list.
|
||||
Each new payment bumps the words sequence by 1. So you can check the
|
||||
confirmation codes manually by just looking at them.
|
||||
</p>
|
||||
<p>
|
||||
For example, if your wordlist is
|
||||
<code>[apple, banana, coconut]</code> the first purchase will be
|
||||
<code>apple</code>, the second <code>banana</code> and so on. When it
|
||||
gets to the end it starts from the beginning again.
|
||||
</p>
|
||||
<p>Powered by LNURL-pay.</p>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
|
||||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="swap_vertical_circle"
|
||||
label="API info"
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="Create item (a shop will be created automatically based on the wallet you use)"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code><span class="text-blue">POST</span></code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <invoice_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Returns 201 OK</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.url_root }}/jukebox/api/v1/jukebox/items -H
|
||||
"Content-Type: application/json" -H "X-Api-Key: {{
|
||||
g.user.wallets[0].inkey }}" -d '{"name": <string>,
|
||||
"description": <string>, "image": <data-uri string>,
|
||||
"price": <integer>, "unit": <"sat" or "USD">}'
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="Get the shop data along with items"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code><span class="text-blue">GET</span></code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <invoice_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 200 OK (application/json)
|
||||
</h5>
|
||||
<code
|
||||
>{"id": <integer>, "wallet": <string>, "wordlist":
|
||||
<string>, "items": [{"id": <integer>, "name":
|
||||
<string>, "description": <string>, "image":
|
||||
<string>, "enabled": <boolean>, "price": <integer>,
|
||||
"unit": <string>, "lnurl": <string>}, ...]}<</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.url_root }}/jukebox/api/v1/jukebox -H
|
||||
"X-Api-Key: {{ g.user.wallets[0].inkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="Update item (all fields must be sent again)"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code><span class="text-blue">PUT</span></code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <invoice_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Returns 200 OK</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.url_root
|
||||
}}/jukebox/api/v1/jukebox/items/<item_id> -H "Content-Type:
|
||||
application/json" -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" -d
|
||||
'{"name": <string>, "description": <string>, "image":
|
||||
<data-uri string>, "price": <integer>, "unit": <"sat"
|
||||
or "USD">}'
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item group="api" dense expand-separator label="Delete item">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code><span class="text-blue">DELETE</span></code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <invoice_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Returns 200 OK</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.url_root
|
||||
}}/jukebox/api/v1/jukebox/items/<item_id> -H "X-Api-Key: {{
|
||||
g.user.wallets[0].inkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
</q-expansion-item>
|
211
lnbits/extensions/jukebox/templates/jukebox/index.html
Normal file
211
lnbits/extensions/jukebox/templates/jukebox/index.html
Normal file
|
@ -0,0 +1,211 @@
|
|||
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||
%} {% block page %}
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-7 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="row items-center no-wrap q-mb-md">
|
||||
<div class="col">
|
||||
<h5 class="text-subtitle1 q-my-none">Items</h5>
|
||||
</div>
|
||||
<div class="col q-ml-lg">
|
||||
<q-btn unelevated color="deep-purple" @click="openNewDialog()"
|
||||
>Add jukebox</q-btn
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{% raw %}
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
selection="multiple"
|
||||
:data="jukebox.items"
|
||||
row-key="id"
|
||||
no-data-label="No items for sale yet"
|
||||
:pagination="{rowsPerPage: 0}"
|
||||
:binary-state-sort="true"
|
||||
>
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th auto-width></q-th>
|
||||
<q-th auto-width>Name</q-th>
|
||||
<q-th auto-width>Description</q-th>
|
||||
<q-th auto-width>Image</q-th>
|
||||
<q-th auto-width>Price</q-th>
|
||||
<q-th auto-width></q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
:icon="props.row.enabled ? 'done' : 'block'"
|
||||
:color="props.row.enabled ? 'green' : ($q.dark.isActive ? 'grey-7' : 'grey-5')"
|
||||
type="a"
|
||||
@click="toggleItem(props.row.id)"
|
||||
target="_blank"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
<q-td auto-width class="text-center">{{ props.row.name }}</q-td>
|
||||
<q-td auto-width> {{ props.row.description }} </q-td>
|
||||
<q-td class="text-center" auto-width>
|
||||
<img
|
||||
v-if="props.row.image"
|
||||
:src="props.row.image"
|
||||
style="height: 1.5em"
|
||||
/>
|
||||
</q-td>
|
||||
<q-td class="text-center" auto-width>
|
||||
{{ props.row.price }} {{ props.row.unit }}
|
||||
</q-td>
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="openUpdateDialog(props.row.id)"
|
||||
icon="edit"
|
||||
color="light-blue"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="delete"
|
||||
color="negative"
|
||||
type="a"
|
||||
@click="deleteItem(props.row.id)"
|
||||
target="_blank"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
{% endraw %}
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-5 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h6 class="text-subtitle1 q-my-none">LNbits jukebox extension</h6>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-separator></q-separator>
|
||||
<q-list> {% include "jukebox/_api_docs.html" %} </q-list>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<q-dialog v-model="itemDialog.show">
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<q-card-section>
|
||||
<h5
|
||||
class="q-ma-none"
|
||||
v-if="itemDialog.data.id"
|
||||
v-text="itemDialog.data.name"
|
||||
></h5>
|
||||
<h5 class="q-ma-none q-mb-xl" v-else>Adding a new item</h5>
|
||||
|
||||
<q-responsive v-if="itemDialog.data.id" :ratio="1">
|
||||
<qrcode
|
||||
:value="itemDialog.data.lnurl"
|
||||
:options="{width: 800}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
</q-responsive>
|
||||
|
||||
<div v-if="itemDialog.data.id" class="row q-gutter-sm justify-center">
|
||||
<q-btn
|
||||
outline
|
||||
color="grey"
|
||||
@click="copyText(itemDialog.data.lnurl, 'LNURL copied to clipboard!')"
|
||||
class="q-mb-lg"
|
||||
>Copy LNURL</q-btn
|
||||
>
|
||||
</div>
|
||||
<q-form @submit="sendItem" class="q-gutter-md">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="itemDialog.data.name"
|
||||
type="text"
|
||||
label="Item name"
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="itemDialog.data.description"
|
||||
type="text"
|
||||
label="Brief description"
|
||||
></q-input>
|
||||
<q-file
|
||||
filled
|
||||
dense
|
||||
capture="environment"
|
||||
accept="image/jpeg, image/png"
|
||||
:max-file-size="3*1024**2"
|
||||
label="Small image (optional)"
|
||||
clearable
|
||||
@input="imageAdded"
|
||||
@clear="imageCleared"
|
||||
>
|
||||
<template v-if="itemDialog.data.image" v-slot:before>
|
||||
<img style="height: 1em" :src="itemDialog.data.image" />
|
||||
</template>
|
||||
<template v-if="itemDialog.data.image" v-slot:append>
|
||||
<q-icon
|
||||
name="cancel"
|
||||
@click.stop.prevent="imageCleared"
|
||||
class="cursor-pointer"
|
||||
/>
|
||||
</template>
|
||||
</q-file>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.number="itemDialog.data.price"
|
||||
type="number"
|
||||
min="1"
|
||||
:label="`Item price (${itemDialog.data.unit})`"
|
||||
></q-input>
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
v-model="itemDialog.data.unit"
|
||||
type="text"
|
||||
label="Unit"
|
||||
:options="itemDialog.units"
|
||||
></q-select>
|
||||
|
||||
<div class="row q-mt-lg">
|
||||
<div class="col q-ml-lg">
|
||||
<q-btn
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
:disable="disabledAddItemButton()"
|
||||
type="submit"
|
||||
>
|
||||
{% raw %}{{ itemDialog.data.id ? 'Update' : 'Add' }}{% endraw %}
|
||||
Item
|
||||
</q-btn>
|
||||
</div>
|
||||
<div class="col q-ml-lg">
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||
>Cancel</q-btn
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||
<script src="https://cdn.jsdelivr.net/npm/pica@6.1.1/dist/pica.min.js"></script>
|
||||
<script src="/jukebox/static/js/index.js"></script>
|
||||
{% endblock %}
|
25
lnbits/extensions/jukebox/templates/jukebox/jukebox.html
Normal file
25
lnbits/extensions/jukebox/templates/jukebox/jukebox.html
Normal file
|
@ -0,0 +1,25 @@
|
|||
{% extends "print.html" %} {% block page %} {% raw %}
|
||||
<div class="row justify-center">
|
||||
<div v-for="item in items" class="q-my-sm q-mx-lg">
|
||||
<div class="text-center q-ma-none q-mb-sm">{{ item.name }}</div>
|
||||
<qrcode :value="item.lnurl" :options="{margin: 0, width: 250}"></qrcode>
|
||||
<div class="text-center q-ma-none q-mt-sm">{{ item.price }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endraw %} {% endblock %} {% block scripts %}
|
||||
<script>
|
||||
Vue.component(VueQrcode.name, VueQrcode)
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
created: function () {
|
||||
window.print()
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
items: JSON.parse('{{items | tojson}}')
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
25
lnbits/extensions/jukebox/views.py
Normal file
25
lnbits/extensions/jukebox/views.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
import time
|
||||
from datetime import datetime
|
||||
from quart import g, render_template, request
|
||||
from http import HTTPStatus
|
||||
|
||||
from lnbits.decorators import check_user_exists, validate_uuids
|
||||
from lnbits.core.models import Payment
|
||||
from lnbits.core.crud import get_standalone_payment
|
||||
|
||||
from . import jukebox_ext
|
||||
from .crud import get_jukebox
|
||||
|
||||
|
||||
@jukebox_ext.route("/")
|
||||
@validate_uuids(["usr"], required=True)
|
||||
@check_user_exists()
|
||||
async def index():
|
||||
return await render_template("jukebox/index.html", user=g.user)
|
||||
|
||||
|
||||
@jukebox_ext.route("/<juke_id>")
|
||||
async def print_qr_codes(juke_id):
|
||||
jukebox = await get_jukebox(juke_id)
|
||||
|
||||
return await render_template("jukebox/jukebox.html", jukebox=jukebox)
|
54
lnbits/extensions/jukebox/views_api.py
Normal file
54
lnbits/extensions/jukebox/views_api.py
Normal file
|
@ -0,0 +1,54 @@
|
|||
from quart import g, jsonify
|
||||
from http import HTTPStatus
|
||||
from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl # type: ignore
|
||||
|
||||
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
||||
|
||||
from . import jukebox_ext
|
||||
from .crud import (
|
||||
create_update_jukebox,
|
||||
get_jukebox,
|
||||
get_jukeboxs,
|
||||
delete_jukebox,
|
||||
)
|
||||
from .models import Jukebox
|
||||
|
||||
|
||||
@jukebox_ext.route("/api/v1/jukebox", methods=["GET"])
|
||||
@api_check_wallet_key("invoice")
|
||||
async def api_get_jukeboxs():
|
||||
jukebox = await get_jukeboxs(g.wallet.id)
|
||||
return (
|
||||
jsonify(
|
||||
{
|
||||
jukebox._asdict()
|
||||
}
|
||||
),
|
||||
HTTPStatus.OK,
|
||||
)
|
||||
|
||||
#websocket get spotify crap
|
||||
|
||||
@jukebox_ext.route("/api/v1/jukebox/items", methods=["POST"])
|
||||
@jukebox_ext.route("/api/v1/jukebox/items/<item_id>", methods=["PUT"])
|
||||
@api_check_wallet_key("admin")
|
||||
@api_validate_post_request(
|
||||
|
||||
schema={
|
||||
"wallet": {"type": "string", "empty": False},
|
||||
"user": {"type": "string", "empty": False},
|
||||
"secret": {"type": "string", "required": False},
|
||||
"token": {"type": "string", "required": True},
|
||||
"playlists": {"type": "string", "required": True},
|
||||
}
|
||||
)
|
||||
async def api_create_update_jukebox(item_id=None):
|
||||
jukebox = await create_update_jukebox(g.wallet.id, **g.data)
|
||||
return jsonify(jukebox._asdict()), HTTPStatus.CREATED
|
||||
|
||||
|
||||
@jukebox_ext.route("/api/v1/jukebox/<juke_id>", methods=["DELETE"])
|
||||
@api_check_wallet_key("admin")
|
||||
async def api_delete_item(juke_id):
|
||||
shop = await delete_jukebox(juke_id)
|
||||
return "", HTTPStatus.NO_CONTENT
|
Loading…
Add table
Reference in a new issue