added new files

This commit is contained in:
Xavier Fiechter 2024-07-30 22:43:13 +02:00
parent 7fc66c3309
commit 06ef65f183
30 changed files with 961 additions and 373 deletions

View file

@ -2,7 +2,16 @@ from django.contrib import admin
from .models import OutputStat, HistoricalPrice
class OutputStatAdmin(admin.ModelAdmin):
list_display = ('type_ref_hash', 'value', 'confirmed_at_block_height', 'confirmed_at_block_time', 'get_spent_status', 'spent', 'network', 'user')
list_display = ('type_ref_hash',
'value',
'confirmed_at_block_height',
'confirmed_at_block_time',
'get_spent_status',
'spent',
'network',
'user',
'next_enc_input_attrs',
'last_error')
list_filter = ('network', 'spent')
search_fields = ('type_ref_hash',)
ordering = ('-confirmed_at_block_time',)

View file

@ -88,7 +88,8 @@ def checkup_label(label_id, loop):
if utxo_resp:
txid, index, address, value, blocktime, utxo_data = utxo_resp
if utxo_data:
output.next_input_attributes = utxo_data
#output.next_input_attributes = utxo_data
output.set_next_input_attributes(utxo_data)
if blocktime:
HistoricalPrice.get_or_create_from_api(timestamp=blocktime)
@ -98,9 +99,6 @@ def checkup_label(label_id, loop):
conn.last_error = None # reset error if needed
unspents = loop.run_until_complete(interact_addr(conn, server_info, "blockchain.scripthash.listunspent", address))
utxo_value = 0
utxo_height = 0

View file

@ -0,0 +1,22 @@
# Generated by Django 3.2.25 on 2024-07-01 09:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('finances', '0011_alter_outputstat_network'),
]
operations = [
migrations.RemoveField(
model_name='outputstat',
name='next_input_attributes',
),
migrations.AddField(
model_name='outputstat',
name='next_enc_input_attrs',
field=models.TextField(default=None, null=True),
),
]

View file

@ -7,7 +7,10 @@ from pymempool import MempoolAPI
from labelbase.receivers import compute_type_ref_hash
from django.conf import settings
from jsonfield import JSONField
import json
from django.contrib.auth.models import User
from shared.encryption import get_fernet_key, cipher_suite
import logging
@ -42,7 +45,18 @@ class OutputStat(models.Model):
confirmed_at_block_time = models.IntegerField(default=0)
last_error = JSONField(default={})
next_input_attributes = JSONField(default={}) # will be used for fee estimation
next_enc_input_attrs = models.TextField(default=None, null=True) # will be used for fee estimation
def set_next_input_attributes(self, data):
json_data = json.dumps(data).encode('utf-8')
encrypted_data = cipher_suite.encrypt(json_data)
self.next_enc_input_attrs = encrypted_data.decode('utf-8')
def next_input_attributes(self):
encrypted_data = self.next_enc_input_attrs.encode('utf-8')
decrypted_data = cipher_suite.decrypt(encrypted_data)
return json.loads(decrypted_data.decode('utf-8'))
MAINNET = 'mainnet'
TESTNET = 'testnet'

View file

@ -1,15 +1,10 @@
from django.contrib.auth.signals import user_logged_in
from django.contrib import messages
from django.dispatch import receiver
from finances.models import HistoricalPrice
from django.contrib import messages
@receiver(user_logged_in)
def perform_tasks_on_login(sender, user, request, **kwargs):
""" """
if user.profile.update_utxo_on_login:
from finances.tasks import check_all_outputs
from labelbase.models import Label
@ -17,4 +12,7 @@ def perform_tasks_on_login(sender, user, request, **kwargs):
if Label.objects.filter(labelbase__user_id=user.id).exists():
messages.info(request, "<strong>Sync in progress:</strong> We are checking your unspent transaction outputs now.")
# Store nearest price information.
HistoricalPrice.get_or_create_from_api(-1)
try:
HistoricalPrice.get_or_create_from_api(-1)
except Exception as ex:
logger.error(ex, exc_info=True)

View file

@ -1,8 +1,6 @@
import logging
from background_task import background
from background_task.management.commands.remove_completed import _remove_completed_task
from labelbase.models import Label
from finances.electrum import checkup_label

View file

@ -8,3 +8,8 @@ class UploadFileForm(forms.Form):
choices=IMPORTER_CHOICES
)
file = forms.FileField()
passphrase = forms.CharField(
widget=forms.PasswordInput(),
required=False,
max_length=100
)

View file

@ -2,15 +2,18 @@ from django.db import models
from django.contrib.auth.models import User
from labelbase.models import Labelbase
from uuid_upload_path import upload_to
from django.conf import settings
IMPORTER_CHOICES = (
IMPORTER_CHOICES = [
("BIP-0329", "BIP-329 .jsonl"),
# TODO: ("BIP-0329-7z-enc" , "BIP-329 (encrypted) .7z"),
("csv-bluewallet", "BlueWallet .csv"),
("csv-bitbox", "BitBox .csv"),
("pocket-accointing", "Pocket Accointing .csv")
)
("pocket-accointing", "Pocket Accointing .csv"),
]
if settings.SELF_HOSTED:
IMPORTER_CHOICES.append(("samourai", "Samourai .txt, (v2)"))
class UploadedData(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)

View file

@ -1,7 +1,5 @@
import csv
import json
from labelbase.models import Label
from labelbase.models import Labelbase
import json
def validate_csv_format(csv_file_path):

153
django/importer/samourai.py Normal file
View file

@ -0,0 +1,153 @@
import json
from Crypto.Cipher import AES
from Crypto.Hash import SHA256
from Crypto.Protocol.KDF import PBKDF2
import hashlib
import re
import base64
from labelbase.serializers import LabelSerializer
from labelbase.models import Label
import logging
logger = logging.getLogger('labelbase')
DefaultPBKDF2Iterations = 5000
DefaultPBKDF2HMACSHA256Iterations = 15000
DefaultSamouraiImportLabel = "Imported form samourai.txt"
def decrypt_v1(payload, password, iterations=DefaultPBKDF2Iterations):
# V1 uses PBKDF2 for key derivation and AES for decryption
AESBlockSize = 16
cipherdata = base64.b64decode(payload)
iv = cipherdata[:AESBlockSize]
input_data = cipherdata[AESBlockSize:]
key = PBKDF2(password, iv, dkLen=32, count=iterations)
cipher = AES.new(key, AES.MODE_CBC, iv)
decrypted = cipher.decrypt(input_data)
return decrypted.rstrip(b"\x00").decode('utf-8')
def decrypt_v2(payload, password, iterations=DefaultPBKDF2HMACSHA256Iterations):
# V2 uses SHA256 for key derivation and AES for decryption
encrypted_bytes = base64.b64decode(payload.replace("\n", ""))
salt = encrypted_bytes[8:16]
cipher_text = encrypted_bytes[16:]
key_iv = PBKDF2(password, salt, dkLen=48, count=iterations, hmac_hash_module=SHA256)
key = key_iv[:32]
iv = key_iv[32:]
cipher = AES.new(key, AES.MODE_CBC, iv)
decrypted = cipher.decrypt(cipher_text)
pad_len = decrypted[-1]
decrypted = decrypted[:-pad_len]
return decrypted.decode('utf-8')
def import_samourai_labels(labelbase, content, passphrase):
content = content.decode('utf-8')
pattern = re.compile(r'\{.*?\}')
match = pattern.search(content)
imported_lables = 0
payload = None
if match:
json_content = match.group(0)
try:
logger.info(f"json_content {json_content}")
data = json.loads(json_content)
logger.info(f"data: {data}")
version = data.get("version", 1)
payload = data.get("payload", "")
logger.info(f"version: {version}, payload {payload}, passphrase {passphrase}")
if payload:
if version in [1, "1"]:
decrypted_data = decrypt_v1(payload, passphrase)
elif version in [2, "2"]:
decrypted_data = decrypt_v2(payload, passphrase)
else:
logger.error(f"Unsupported backup version: {version}")
raise ValueError(f"Unsupported backup version: {version}")
logger.info(decrypted_data)
samourai_data = json.loads(decrypted_data)
logger.info(samourai_data)
"""
DOC/KB: If the labelbase where you import your samourai.txt into, labelbase will set the fingerprint,
"""
labels = Label.objects.filter(labelbase__id=labelbase.id)
if labels.count() == 0:
if not labelbase.fingerprint:
labelbase.fingerprint = samourai_data.get('wallet').get('fingerprint')
if samourai_data.get('wallet').get('testnet'):
labelbase.network == labelbase.TESTNET
else:
labelbase.network == labelbase.MAINNET
labelbase.save()
xpub = samourai_data.get('wallet', {}).get('accounts')[0].get('xpub')
ypub = samourai_data.get('wallet', {}).get('bip49_accounts')[0].get('ypub')
zpub = samourai_data.get('wallet', {}).get('bip84_accounts')[0].get('zpub')
for pub in [xpub, ypub, zpub]:
if pub:
_data = {
"type": Label.TYPE_XPUB,
"ref": pub,
"label": DefaultSamouraiImportLabel,
}
_data["labelbase"] = labelbase.id
serializer = LabelSerializer(data=_data)
if serializer.is_valid():
serializer.save()
imported_lables += 1
utxo_notes = samourai_data.get('meta', {}).get('utxo_notes')
logger.info(utxo_notes)
for note in utxo_notes:
_data = {
"type": Label.TYPE_TX,
"ref": note[0],
"label": note[1],
}
_data["labelbase"] = labelbase.id
serializer = LabelSerializer(data=_data)
if serializer.is_valid():
serializer.save()
imported_lables += 1
blocked_utxos = samourai_data.get('meta', {}).get('blocked_utxos',{}).get('blocked')
logger.info(blocked_utxos)
for utxo in blocked_utxos:
_data = {
"type": Label.TYPE_OUTPUT,
"ref": utxo.get('id','').replace("-", ":"),
"label": DefaultSamouraiImportLabel,
"spendable": False
}
_data["labelbase"] = labelbase.id
serializer = LabelSerializer(data=_data)
if serializer.is_valid():
serializer.save()
imported_lables += 1
return imported_lables
else:
print("No payload found in the JSON content.")
logger.error("No payload found in the JSON content.")
return imported_lables
except json.JSONDecodeError as e:
print(f"JSONDecodeError: {e}")
logger.error(f"JSONDecodeError: {e}")
return imported_lables
except Exception as ex:
print(f"An error occurred: {ex}")
logger.error(f"An error occurred: {ex}")
logger.error(ex, exc_info=True)
return imported_lables
else:
print("No JSON found in file.")
logger.error("No JSON found in file.")
return imported_lables

View file

@ -2,9 +2,10 @@ from background_task import background
import json
import decimal
from labelbase.models import Labelbase
from labelbase.serializers import LabelSerializer
import logging
logger = logging.getLogger('labelbase')
from .models import UploadedData
@ -12,93 +13,132 @@ EOLSTOP = [b"", "", None, "\n"]
@background(schedule=1)
def process_uploaded_data(uploaded_data_id, loop=None):
imported_lables = 0
uploaded_data = UploadedData.objects.get(pk=uploaded_data_id)
labelbase = uploaded_data.labelbase
fp = uploaded_data.file.open()
def process_uploaded_data(uploaded_data_id, passphrase=None, loop=None):
try:
uploaded_data = UploadedData.objects.get(pk=uploaded_data_id)
imported_lables = 0
labelbase = uploaded_data.labelbase
fp = uploaded_data.file.open()
# BIP-0329
if uploaded_data.import_type == "BIP-0329":
while True:
buf = fp.readline()
if buf in EOLSTOP:
break
data = json.loads(buf)
data["labelbase"] = labelbase.id
serializer = LabelSerializer(data=data)
if serializer.is_valid():
serializer.save()
imported_lables += 1
elif uploaded_data.import_type == "BIP-0329-7z-enc":
# TODO: Implementation needed.
pass
# Bitbox App
elif uploaded_data.import_type == "csv-bitbox":
while True:
buf = fp.readline()
if buf in EOLSTOP:
break
try:
buf = str(buf.decode("utf-8"))
sbuf = buf.split(",")
# Time,Type,Amount,Unit,Fee,Address,Transaction ID,Note
for elem in [("tx", 6), ("addr", 5)]:
data = {
"type": elem[0],
"ref": sbuf[elem[1]],
"label": " ".join(sbuf[7:]),
}
data["labelbase"] = labelbase.id
serializer = LabelSerializer(data=data)
if serializer.is_valid():
serializer.save()
imported_lables += 1
else:
messages.add_message(
request,
messages.ERROR,
'Could not process line "{}".'.format(buf),
)
except Exception as ex:
messages.add_message(
request,
messages.ERROR,
'Could not process line "{}", {}.'.format(buf, ex),
)
# Pocket Accointing
elif uploaded_data.import_type == "pocket-accointing":
fp.close()
csv_file_path = fp.name
mempool_api = labelbase.get_mempool_api()
from .pocket import validate_csv_format, parse_csv_to_json
if validate_csv_format(csv_file_path):
for item in parse_csv_to_json(csv_file_path):
label = "Got {} {} for {:.2f} {} with reference: {} #Pocket".format(item[0].get('outSellAmount'), item[1].get(
'inBuyAsset'), decimal.Decimal(item[2].get('inBuyAmount')), item[2].get('inBuyAsset'), item[1].get('operationId'))
txid = item[0].get('operationId')
tx = mempool_api.get_transaction(txid)
potential_utxos = []
vouts = tx.get("vout", [])
for i in range(len(vouts)):
if vouts[i].get('value', 0) == decimal.Decimal(item[0].get('outSellAmount'))*100000000:
potential_utxos.append("{}:{}".format(txid, i ))
data = {}
if len(potential_utxos) == 1:
# label UTXO/output of tx
data = {
"type": "output",
"ref": potential_utxos[0],
"label": label,
}
if len(potential_utxos) > 1:
# mark tx, add warning tag
# BIP-0329
if uploaded_data.import_type == "BIP-0329":
while True:
buf = fp.readline()
if buf in EOLSTOP:
break
data = json.loads(buf)
data["labelbase"] = labelbase.id
serializer = LabelSerializer(data=data)
if serializer.is_valid():
serializer.save()
imported_lables += 1
elif uploaded_data.import_type == "BIP-0329-7z-enc":
# TODO: Implementation needed.
pass
elif uploaded_data.import_type == "samourai":
buf = fp.read()
logger.info(buf)
print(buf)
from .samourai import import_samourai_labels
import_samourai_labels(labelbase, buf, passphrase)
# Bitbox App
elif uploaded_data.import_type == "csv-bitbox":
while True:
buf = fp.readline()
if buf in EOLSTOP:
break
try:
buf = str(buf.decode("utf-8"))
sbuf = buf.split(",")
# Time,Type,Amount,Unit,Fee,Address,Transaction ID,Note
for elem in [("tx", 6), ("addr", 5)]:
data = {
"type": elem[0],
"ref": sbuf[elem[1]],
"label": " ".join(sbuf[7:]),
}
data["labelbase"] = labelbase.id
serializer = LabelSerializer(data=data)
if serializer.is_valid():
serializer.save()
imported_lables += 1
else:
messages.add_message(
request,
messages.ERROR,
'Could not process line "{}".'.format(buf),
)
except Exception as ex:
messages.add_message(
request,
messages.ERROR,
'Could not process line "{}", {}.'.format(buf, ex),
)
# Pocket Accointing
elif uploaded_data.import_type == "pocket-accointing":
fp.close()
csv_file_path = fp.name
mempool_api = labelbase.get_mempool_api()
from .pocket import validate_csv_format, parse_csv_to_json
if validate_csv_format(csv_file_path):
for item in parse_csv_to_json(csv_file_path):
label = "Got {} {} for {:.2f} {} with reference: {} #Pocket".format(item[0].get('outSellAmount'), item[1].get(
'inBuyAsset'), decimal.Decimal(item[2].get('inBuyAmount')), item[2].get('inBuyAsset'), item[1].get('operationId'))
txid = item[0].get('operationId')
tx = mempool_api.get_transaction(txid)
potential_utxos = []
vouts = tx.get("vout", [])
for i in range(len(vouts)):
if vouts[i].get('value', 0) == decimal.Decimal(item[0].get('outSellAmount'))*100000000:
potential_utxos.append("{}:{}".format(txid, i ))
data = {}
if len(potential_utxos) == 1:
# label UTXO/output of tx
data = {
"type": "output",
"ref": potential_utxos[0],
"label": label,
}
if len(potential_utxos) > 1:
# mark tx, add warning tag
data = {
"type": "tx",
"ref": txid,
"label": "{} {}".format(label, "#W001_UTXO_NOT_FOUND"),
}
if data:
data["labelbase"] = labelbase.id
serializer = LabelSerializer(data=data)
if serializer.is_valid():
serializer.save()
imported_lables += 1
else:
# messages.add_message(
# request,
# messages.ERROR,
# 'Could not process record "{}".'.format(item),
# )
pass
else:
print("ERROR") # TODO
# BlueWallet
elif uploaded_data.import_type == "csv-bluewallet":
header_row = True
while True:
buf = fp.readline()
if buf in EOLSTOP:
break
if header_row:
header_row = False
continue
try:
buf = str(buf.decode("utf-8"))
sbuf = buf.split(",")
data = {
"type": "tx",
"ref": txid,
"label": "{} {}".format(label, "#W001_UTXO_NOT_FOUND"),
"ref": sbuf[1],
"label": " ".join(sbuf[3:]),
}
if data:
data["labelbase"] = labelbase.id
serializer = LabelSerializer(data=data)
if serializer.is_valid():
@ -108,50 +148,20 @@ def process_uploaded_data(uploaded_data_id, loop=None):
# messages.add_message(
# request,
# messages.ERROR,
# 'Could not process record "{}".'.format(item),
# 'Could not process line "{}".'.format(buf),
# )
pass
else:
print("ERROR") # TODO
# BlueWallet
elif uploaded_data.import_type == "csv-bluewallet":
header_row = True
while True:
buf = fp.readline()
if buf in EOLSTOP:
break
if header_row:
header_row = False
continue
try:
buf = str(buf.decode("utf-8"))
sbuf = buf.split(",")
data = {
"type": "tx",
"ref": sbuf[1],
"label": " ".join(sbuf[3:]),
}
data["labelbase"] = labelbase.id
serializer = LabelSerializer(data=data)
if serializer.is_valid():
serializer.save()
imported_lables += 1
else:
except Exception as ex:
# messages.add_message(
# request,
# messages.ERROR,
# 'Could not process line "{}".'.format(buf),
# 'Could not process line "{}", {}.'.format(buf, ex),
# )
pass
except Exception as ex:
# messages.add_message(
# request,
# messages.ERROR,
# 'Could not process line "{}", {}.'.format(buf, ex),
# )
pass
# Clean up  Note: Currently we delete the upload from the file system,
# later we can store the messages.add_message messages, the state and the
# amount of importet labels in it to propagate the messages to the
# frontend/user interface.
uploaded_data.delete()
# Clean up  Note: Currently we delete the upload from the file system,
# later we can store the messages.add_message messages, the state and the
# amount of importet labels in it to propagate the messages to the
# frontend/user interface.
uploaded_data.delete()
except Exception as ex:
logger.error(ex, exc_info=True)

View file

@ -10,27 +10,4 @@ def genericlabeluploadform(labelbase_id):
form.fields["labelbase_id"].initial = labelbase_id
form.fields["import_type"].initial = "BIP-0329"
return form
@register.simple_tag
def bip0329labeluploadform(labelbase_id):
form = UploadFileForm()
form.fields["labelbase_id"].initial = labelbase_id
form.fields["import_type"].initial = "BIP-0329"
return form
@register.simple_tag
def csvBlueWalletlabeluploadform(labelbase_id):
form = UploadFileForm()
form.fields["labelbase_id"].initial = labelbase_id
form.fields["import_type"].initial = "csv-bluewallet"
return form
@register.simple_tag
def csvBitBoxLabeluploadform(labelbase_id):
form = UploadFileForm()
form.fields["labelbase_id"].initial = labelbase_id
form.fields["import_type"].initial = "csv-bitbox"
return form

View file

@ -30,7 +30,7 @@ def upload_labels(request):
file=request.FILES["file"],
)
# Schedule the background task to process the uploaded data
process_uploaded_data(uploaded_data.id)
process_uploaded_data(uploaded_data.id, passphrase=form.cleaned_data.get("passphrase", None))
messages.add_message(
request,
messages.INFO,

View file

@ -10,6 +10,9 @@ from labelbase.models import Labelbase, Label
from labelbase.serializers import LabelbaseSerializer, LabelSerializer
import logging
logger = logging.getLogger('labelbase')
class LabelbaseAPIView(APIView):
"""
Labelbase
@ -90,7 +93,7 @@ class LabelAPIView(APIView):
"spendable": request.data.get("spendable", "null"),
}
#logger.debug(f"data: {data}")
serializer = LabelSerializer(data=data)
if serializer.is_valid():
serializer.save()

View file

@ -130,7 +130,7 @@ class Label(models.Model):
TYPE_PUBKEY = "pubkey"
TYPE_INPUT = "input"
TYPE_OUTPUT = "output"
TYPE_XPUT = "xpub"
TYPE_XPUB = "xpub"
TYPE_CHOICES = [
(TYPE_TX, "tx"),
@ -138,7 +138,7 @@ class Label(models.Model):
(TYPE_PUBKEY, "pubkey"),
(TYPE_INPUT, "input"),
(TYPE_OUTPUT, "output"),
(TYPE_XPUT, "xpub"),
(TYPE_XPUB, "xpub"),
]
type = models.CharField(

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

View file

@ -178,6 +178,9 @@ DATABASES = {
"PASSWORD": proj_config.get("database", "password"),
'HOST': 'localhost',
'PORT': 3306,
'OPTIONS': {
'charset': 'utf8mb4',
},
}
}

View file

@ -37,6 +37,7 @@ from .views import (
AboutView,
EncryptionView,
InteroperationalView,
CloudView,
ExportLabelsView,
# StatsAndKPIView,
TreeMapsView,
@ -262,6 +263,11 @@ urlpatterns = [
InteroperationalView.as_view(),
name="interoperational"
),
path(
"cloud",
CloudView.as_view(),
name="cloud"
),
path(
"outputstat/<int:output_stats_id>/update/<int:label_id>/",
login_required(OutputStatUpdateRedirectView.as_view()),

View file

@ -101,6 +101,22 @@ class BitcoinAddressDatatableView(BaseDatatableView):
pub = key.derive(f"m/0/{idx}").key
sc = script.p2wpkh(pub)
address = sc.address(NETWORKS["main"])
elif derivation == 'm/44' and xpub.startswith("tpub"):
# BIP 44 - Legacy Addresses (P2PKH)
pub = key.derive(f"m/0/{idx}").key
sc = script.p2pkh(pub)
address = sc.address(NETWORKS["test"])
elif derivation == 'm/49' and xpub.startswith("upub"):
# BIP 49 - SegWit Addresses (P2SH-P2WPKH)
pub = key.derive(f"m/0/{idx}").key
witness_script = script.p2wpkh(pub)
sc = script.p2sh(witness_script)
address = sc.address(NETWORKS["test"])
elif derivation == 'm/84' and xpub.startswith("vpub"):
# BIP 84 - Native SegWit Addresses (P2WPKH)
pub = key.derive(f"m/0/{idx}").key
sc = script.p2wpkh(pub)
address = sc.address(NETWORKS["test"])
else:
continue
@ -171,6 +187,10 @@ class InteroperationalView(TemplateView):
template_name = "interoperational.html"
class CloudView(TemplateView):
template_name = "cloud.html"
class HomeView(TemplateView):
template_name = "home.html"

View file

@ -1,11 +1,12 @@
gunicorn
asgiref==3.4.1
certifi==2023.07.22
certifi==2024.07.04
cffi==1.15.1
charset-normalizer==2.0.12
coreapi==2.3.3
coreschema==0.0.4
cryptography==42.0.4
pycryptodome
Django==3.2.25
django-appconf==1.0.5
django-bootstrap-form==3.4

View file

@ -184,7 +184,7 @@
{% endif %}
{% endwith %}
<li><a class="nav-link" style="font-size: .875rem;" href="{% url "labelbase_fix_and_manage" labelbase_id=labelbase.id %}"><span data-feather="git-merge" class="align-text-bottom"></span> Fix & Manage</a></li>
<!--li><a class="nav-link" style="font-size: .875rem;" href="{% url "labelbase_health" labelbase_id=labelbase.id %}"><span data-feather="activity" class="align-text-bottom"></span>UTXOs Health</a></li-->
{% comment %}<li><a class="nav-link" style="font-size: .875rem;" href="{% url "labelbase_health" labelbase_id=labelbase.id %}"><span data-feather="activity" class="align-text-bottom"></span>UTXOs Health</a></li>{% endcomment %}
<li><a class="nav-link" style="font-size: .875rem;" href="{% url "labelbase_tree_maps" pk=labelbase.id %}unspent-outputs/"><span data-feather="grid" class="align-text-bottom"></span> Tree Map</a></li>
{% comment %}<li><a class="nav-link" style="font-size: .875rem;" href="{% url "labelbase_stats_and_kpi" labelbase_id=labelbase.id %}"><span data-feather="pie-chart" class="align-text-bottom"></span> Stats & KPIs</a></li>{% endcomment %}
{% if request.user.profile.use_fiatfinances %}

View file

@ -8,30 +8,35 @@
{% endif %}
</div>
<div class="col float-end" >
<div class="btn-group float-end" role="group" style="padding-top: 2em;">
<button type="button" class="rounded-start btn btn-sm btn-outline-secondary "
<!-- <button type="button" class="rounded-start btn btn-sm btn-outline-secondary "
data-bs-toggle="modal" data-bs-target="#addLabelModal">New Label</button>
<!--button type="button" class="rounded-start d-md-none btn btn-sm btn-outline-secondary " data-bs-toggle="modal" data-bs-target="#addLabelModal">New</button-->
{% comment %}
<div class="btn-group" role="group">
<button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle " data-bs-toggle="dropdown" aria-expanded="false">
Import
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#importBip329LabelsModal">BIP-0329 Labels</a></li>
<li><a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#importBlueWalletCSVLabelsModal">BlueWallet CSV History</a></li>
<li><a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#importBitboxCSVLabelsModal">BitBox App CSV History</a></li>
</ul>
</div>
<a href="{% url 'export_labels' labelbase.id %}" class="btn btn-sm btn-outline-secondary">Export</a>
{% endcomment %}
-->
<button type="button" class="rounded-start btn btn-sm btn-outline-secondary d-md-none"
data-bs-toggle="modal" data-bs-target="#addLabelModal"> New </button>
<button type="button" class="rounded-start btn btn-sm btn-outline-secondary d-none d-md-inline"
style="border-right-width: 0;"
data-bs-toggle="modal" data-bs-target="#addLabelModal"> New Label </button>
<div class="btn-group" role="group">
<!--button type="button" class="d-md-none btn btn-sm btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
Labels
</button-->
<button type="button" class="{#d-none d-md-block #} btn btn-sm btn-outline-secondary dropdown-toggle rounded-end me-2" data-bs-toggle="dropdown" aria-expanded="false">
<!-- <button type="button" class="{#d-none d-md-block #} btn btn-sm btn-outline-secondary dropdown-toggle rounded-end me-2" data-bs-toggle="dropdown" aria-expanded="false">
Label Actions
</button>
-->
<button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle rounded-end me-2 d-md-none"
data-bs-toggle="dropdown" aria-expanded="false"> Actions </button>
<button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle rounded-end me-2 d-none d-md-inline"
data-bs-toggle="dropdown" aria-expanded="false"> Label Actions </button>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" href="{% url "labelbase_actions" labelbase_id=labelbase.id action="update-spent-outputs" %}">

View file

@ -0,0 +1,35 @@
<div class="modal" tabindex="-1" id="connectApiKeyLabelbaseModal">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">API Connect</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>
API Key: <strong><tt>{{ api_token }}</tt></strong>
<br>
Labelbase ID: <strong><tt>{{labelbase.id }}</tt></strong>
<br>
Base Endpoint: <strong><tt>https://labelbase.space/api/v0/</tt></strong><br>
<small>NOTE: Replace "https://labelbase.space" with your own host. </small>
</p>
<center>
<div style="padding:1.5em;" id="qrcode"></div>
<div class="alert alert-warning" role="alert">
API keys work like passwords. Keep them secret!<br>
Whoever knows the key can access your labelbases.<br>
</div>
</center>
<p>
Our API reference can be found here: <br>
<a href="https://labelbase.space/api-reference/">https://labelbase.space/api-reference/</a>
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">OK</button>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,26 @@
<div class="modal" tabindex="-1" id="deleteLabelbaseModal">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Delete labelbase and labels?</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="alert alert-warning" role="alert">
<strong>Warning:</strong>
<br>
<br>
This action will permanently delete the labelbase and its labels and cannot be undone.
<br><br>
Are you sure you want to proceed?
</div>
</div>
<form method="post" action="{% url 'del_labelbase' labelbase.pk %}">{% csrf_token %}
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">CANCEL</button>
<button type="submit" class="btn btn-danger" data-bs-dismiss="modal">DELETE</button>
</div>
</form>
</div>
</div>
</div>

View file

@ -0,0 +1,24 @@
{% load i18n %}
{% load labelbase_tags %}
{% load bootstrap %}
<div class="modal" tabindex="-1" id="editLabelbaseModal">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<form action="{% url 'edit_labelbase' labelbase.id %}" method="post">
{% csrf_token %}
<div class="modal-header">
<h5 class="modal-title">Edit labelbase</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
{% labelbaseform_edit labelbase as edit_form %}
{{ edit_form|bootstrap }}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">OK</button>
</div>
</form>
</div>
</div>
</div>

View file

@ -18,9 +18,19 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="alert alert-warning" style="display:none;" id="samourai-warning" role="alert">
<strong>Warning:</strong>
<br>
<br>
Importing a samourai.txt backup file will expose the seed and should only be done on your personal instance of Labelbase that you control!
</div>
{% genericlabeluploadform labelbase.id as impform %}
{{ impform|bootstrap }}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">OK</button>
@ -31,11 +41,34 @@
</div>
{% addtoblock "js" %}
$(document).ready(function () {
var form = $('#importLabelbaseModal form');
form.on('submit', function () {
// Disable the OK button on form submission
$('#importLabelbaseModal button[type="submit"]').prop('disabled', true);
var form = $('#importLabelbaseModal form');
var importTypeField = $('#id_import_type');
var passphraseFieldGroup = $('#id_passphrase').closest('.form-group');
function togglePassphraseField() {
if (importTypeField.val() === 'samourai') {
passphraseFieldGroup.show();
$('#id_passphrase').prop('disabled', false);
$('#samourai-warning').show();
} else {
passphraseFieldGroup.hide();
$('#id_passphrase').prop('disabled', true);
$('#id_passphrase').val('')
$('#samourai-warning').hide();
}
}
importTypeField.on('change', togglePassphraseField);
// Initial call to set the correct state on page load
togglePassphraseField();
form.on('submit', function () {
// Disable the OK button on form submission
$('#importLabelbaseModal button[type="submit"]').prop('disabled', true);
});
});
});
{% endaddtoblock %}

View file

@ -0,0 +1,51 @@
{% extends "_base.html" %}
{% load i18n %}
{% block title %}Interoperability{% endblock %}
{% block nav_home %}active{% endblock %}
{% block content %}
<div class="p-3 pb-md-4 mx-auto text-center">
<h1 style="padding-top: 1.6em; padding-bottom: 0.9em; text-transform: uppercase;font-weight:800;">L<span style="height:1.1em; width:1em" data-feather="tag" class="align-text-bottom"></span>belbase</h1>
<h2 class="display-6 fw-normal" style="padding-bottom: 0.9em;">All your labels in one place.</h2>
</div>
<main class="lb-header mx-auto text-center">
<!--
"Use your labels here, use your labels there Dave doesn't care."
"Use this wallet today, use that wallet tomorrow Dave doesn't care."
-->
<!-- Centered "Getting Started" CTA Block -->
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="mb-8">
<img src="/static/Cloud.png"
class="card-img-top"
alt="..."
style="padding: 0.25em;">
<div class="card-body">
<p style="font-weight: 700;">Cloud</p>
<p><i>"Use your labels here, use your labels there Dave doesn't care."</i></p>
<p>
Be like Dave with Labelbase, your ultimate cloud-based platform for managing, merging, and synchronizing wallet labels across all your devices and wallet applications.
Labelbase seamlessly synchronizes your labels across various wallets and systems, ensuring you have consistent and organized data wherever you go. Simplify your financial management and stay in sync with Labelbase.
</p>
<p>
<br><br>
<a href="{{ reg_url }}" type="button" class="w-100 btn btn-lg btn-primary">Get started</a>
</p>
</div>
</div>
</div>
</div>
</main>
{% endblock %}

View file

@ -7,22 +7,106 @@
{% block content %}
<div class=" p-3 pb-md-4 mx-auto text-left">
<h2 class="display-8 fw-normal">Support Labelbase: Make a difference with your donation</h2>
<br>
<p class="fs-5 text-muted">Every contribution helps us build a better Bitcoin labeling experience.</p>
<p class="fs-5 text-muted">
Choose the amount you'd like to donate to Labelbase.
</p> <p class="fs-5 text-muted">
Your generosity ensures that our project continues to thrive and evolve, directly supporting our mission to make Bitcoin transactions more organized and transparent.
</p> <p class="fs-5 text-muted">
Thank you for your support!
<div class="p-3 pb-md-4 mx-auto text-center" >
<h2 class="display-8 fw-bold text-muted" style="padding-top: 2rem;padding-bottom: 0.9em;">Support Labelbase: Keep it Going</h2>
<p class="lb-header mx-auto text-center fs-5 text-muted">
Every contribution helps us continue building a better Bitcoin labeling experience.
</p>
<p class="lb-header mx-auto text-center fs-5 text-muted">
Choose the amount you'd like to donate to Labelbase.
</p>
<p class="lb-header mx-auto text-center fs-5 text-muted">
As a free and open-source software, your generosity is crucial to our mission. Your support ensures our project keeps thriving and evolving, directly enhancing Bitcoin label management.
</p>
<style> .btcpay-form { display: inline-flex; align-items: center; justify-content: center; } .btcpay-form--inline { flex-direction: row; } .btcpay-form--block { flex-direction: column; } .btcpay-form--inline .submit { margin-left: 15px; } .btcpay-form--block select { margin-bottom: 10px; } .btcpay-form .btcpay-custom-container{ text-align: center; }.btcpay-custom { display: flex; align-items: center; justify-content: center; } .btcpay-form .plus-minus { cursor:pointer; font-size:25px; line-height: 25px; background: #DFE0E1; height: 30px; width: 45px; border:none; border-radius: 60px; margin: auto 5px; display: inline-flex; justify-content: center; } .btcpay-form select { -moz-appearance: none; -webkit-appearance: none; appearance: none; color: currentColor; background: transparent; border:1px solid transparent; display: block; padding: 1px; margin-left: auto; margin-right: auto; font-size: 11px; cursor: pointer; } .btcpay-form select:hover { border-color: #ccc; } .btcpay-form option { color: #000; background: rgba(0,0,0,.1); } .btcpay-input-price { -moz-appearance: textfield; border: none; box-shadow: none; text-align: center; font-size: 25px; margin: auto; border-radius: 5px; line-height: 35px; background: #fff; }.btcpay-input-price::-webkit-outer-spin-button, .btcpay-input-price::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; } </style>
<style> input[type=range].btcpay-input-range { -webkit-appearance:none; width:100%; background: transparent; } input[type=range].btcpay-input-range:focus { outline:0; } input[type=range].btcpay-input-range::-webkit-slider-runnable-track { width:100%; height:3.1px; cursor:pointer; box-shadow:0 0 1.7px #020,0 0 0 #003c00; background:#f3f3f3; border-radius:1px; border:0; } input[type=range].btcpay-input-range::-webkit-slider-thumb { box-shadow:none; border:2.5px solid #cedc21; height:22px; width:22px; border-radius:50%; background:#0f3723; cursor:pointer; -webkit-appearance:none; margin-top:-9.45px } input[type=range].btcpay-input-range:focus::-webkit-slider-runnable-track { background:#fff; } input[type=range].btcpay-input-range::-moz-range-track { width:100%; height:3.1px; cursor:pointer; box-shadow:0 0 1.7px #020,0 0 0 #003c00; background:#f3f3f3; border-radius:1px; border:0; } input[type=range].btcpay-input-range::-moz-range-thumb { box-shadow:none; border:2.5px solid #cedc21; height:22px; width:22px; border-radius:50%; background:#0f3723; cursor:pointer; } input[type=range].btcpay-input-range::-ms-track { width:100%; height:3.1px; cursor:pointer; background:0 0; border-color:transparent; color:transparent; } input[type=range].btcpay-input-range::-ms-fill-lower { background:#e6e6e6; border:0; border-radius:2px; box-shadow:0 0 1.7px #020,0 0 0 #003c00; } input[type=range].btcpay-input-range::-ms-fill-upper { background:#f3f3f3; border:0; border-radius:2px; box-shadow:0 0 1.7px #020,0 0 0 #003c00; } input[type=range].btcpay-input-range::-ms-thumb { box-shadow:none; border:2.5px solid #cedc21; height:22px; width:22px; border-radius:50%; background:#0f3723; cursor:pointer; height:3.1px; } input[type=range].btcpay-input-range:focus::-ms-fill-lower { background:#f3f3f3; } input[type=range].btcpay-input-range:focus::-ms-fill-upper { background:#fff; } </style>
<form method="POST" action="https://pay.seedor.io/api/v1/invoices" class="btcpay-form btcpay-form--block">
<input type="hidden" name="storeId" value="6BNs4QPhiLFM9yh7uByokgYf1okjCNq6dwAMaVWxgKZN" />
<input type="hidden" name="checkoutDesc" value="Thank you for supporting Labelbase!" />
<input type="hidden" name="browserRedirect" value="https://labelbase.space/thanks" />
<!--input type="hidden" name="notifyEmail" value="xavier@labelbase.space" /-->
<input type="hidden" name="notifyEmail" value="xavierfiechter@gmail.com" /-->
<div class="btcpay-custom-container ">
<input class="btcpay-input-price" type="number" name="price" min="1" max="1000" step="1" value="50" data-price="50" style="width:209px;" />
<select name="currency">
<option value="USD" selected>USD</option>
<option value="CHF">CHF</option>
<option value="GBP">GBP</option>
<option value="EUR">EUR</option>
<option value="BTC">BTC</option>
</select>
<input type="range" class="btcpay-input-range" min="1" max="1000" step="1" value="50" style="width:209px;margin-bottom:15px;" />
</div>
<input type="hidden" name="defaultPaymentMethod" value="BTC_LightningLike" />
<button type="submit" class="submit" name="submit" style="min-width:209px;min-height:57px;border-radius:4px;border-style:none;background-color:#0f3b21;cursor:pointer;" title="Pay with BTCPay Server, a Self-Hosted Bitcoin Payment Processor"><span style="color:#fff">Donate with</span>
<img src="https://pay.seedor.io/img/paybutton/logo.svg" style="height:57px;display:inline-block;padding:5% 0 5% 5px;vertical-align:middle;">
</button></form>
<script>
function handleSliderChange(event) {
event.preventDefault();
const root = event.target.closest('.btcpay-form');
const el = root.querySelector('.btcpay-input-price');
const price = parseInt(el.value);
const min = parseInt(event.target.getAttribute('min')) || 1;
const max = parseInt(event.target.getAttribute('max'));
if (price < min) {
el.value = min;
} else if (price > max) {
el.value = max;
}
root.querySelector('.btcpay-input-range').value = el.value;
}
function handleSliderInput(event) {
event.target.closest('.btcpay-form').querySelector('.btcpay-input-price').value = event.target.value;
}
document.querySelectorAll(".btcpay-form .btcpay-input-range").forEach(function(el) {
if (!el.dataset.initialized) {
el.addEventListener('input', handleSliderInput);
el.dataset.initialized = true;
}
});
document.querySelectorAll(".btcpay-form .btcpay-input-price").forEach(function(el) {
if (!el.dataset.initialized) {
el.addEventListener('change', handleSliderChange);
el.dataset.initialized = true;
}
});
function handlePriceInput(event) {
event.preventDefault();
const root = event.target.closest('.btcpay-form');
const price = parseInt(event.target.dataset.price);
if (isNaN(event.target.value)) root.querySelector('.btcpay-input-price').value = price;
const min = parseInt(event.target.getAttribute('min')) || 1;
const max = parseInt(event.target.getAttribute('max'));
if (event.target.value < min) {
event.target.value = min;
} else if (event.target.value > max) {
event.target.value = max;
}
}
document.querySelectorAll(".btcpay-form .btcpay-input-price").forEach(function(el) {
if (!el.dataset.initialized) {
el.addEventListener('input', handlePriceInput);
el.dataset.initialized = true;
}
});
</script>
<p class="lb-header mx-auto text-center fs-5 text-muted" style="padding-bottom:2rem;"><br>
Thank you for keeping Labelbase going.
</p>
<div style="display: inline-block; border-top: 1px solid black; ">
<small>
<a href="https://www.seedor.io/en/pages/about-us">Seedor</a> powers our <a href="https://btcpayserver.org/">BTCPay Server</a> instance, providing secure and private donations.
</small>
</div>
</div>

View file

@ -64,10 +64,13 @@
<li class="nav-item">
{% switch object.ref|slice:":4" %}
{% case "xpub" %}
{% case "tpub" %}
<a class="nav-link {% if action == "derive-addresses" %}active{% endif %}" href="{% url 'edit_label' object.id %}derive-addresses/?derivation=m/44&address_count={{ address_count }}&offset={{ offset }}">Derive Addresses</a>
{% case "ypub" %}
{% case "upub" %}
<a class="nav-link {% if action == "derive-addresses" %}active{% endif %}" href="{% url 'edit_label' object.id %}derive-addresses/?derivation=m/49&address_count={{ address_count }}&offset={{ offset }}">Derive Addresses</a>
{% case "zpub" %}
{% case "vpub" %}
<a class="nav-link {% if action == "derive-addresses" %}active{% endif %}" href="{% url 'edit_label' object.id %}derive-addresses/?derivation=m/84&address_count={{ address_count }}&offset={{ offset }}">Derive Addresses</a>
{% else %}
<a class="nav-link {% if action == "derive-addresses" %}active{% endif %}" href="{% url 'edit_label' object.id %}derive-addresses/">Derive Addresses</a>

View file

@ -9,23 +9,19 @@
{% include "_labelbase_header_info_menu.html" %}
{% if labelbase %}
{% if request.GET.tag %}
Hashtag filter is active:
<span class="badge badge-hashtag badge-hashtag-nohover">
<tt style="pointer-events: none;">{{ request.GET.tag }}</tt>
<button onclick="window.location='{% url 'labelbase' labelbase.id %}'";
type="button"
class="btn-close"
style="padding-left: 0.4rem; margin-right: -0.1rem; font-size: .6rem; font-weight: bolder !important;"
></button>
<tt style="pointer-events: none;">{{ request.GET.tag }}</tt>
<button onclick="window.location='{% url 'labelbase' labelbase.id %}'";
type="button"
class="btn-close"
style="padding-left: 0.4rem; margin-right: -0.1rem; font-size: .6rem; font-weight: bolder !important;"></button>
</span>
{% else %}
{% endif %}
<div class="table-responsive" style="padding-top:1em" >
<div class="table-responsive d-none d-md-block" style="padding-top:1em">
{% if label_list %}
<table id="bip329labels" class="table table-striped table-sm">
<thead>
@ -38,188 +34,301 @@
<th scope="col">spendable</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<div id="datatable-info-regular"></div>
{% else %}
<p>There are no labels in this labelbase.</p>
<p>You can create new labels by using the 'New Label' button on the top right, or <a href="#" data-bs-toggle="modal" data-bs-target="#importLabelbaseModal">import your existing labels</a>.</p>
{% endif %}
</div>
{% endif %}
{% if labelbase %}
<div class="d-md-none" style="padding-top:0.5em;">
<div id="datatable-info-mobile"></div>
<div class="input-group mb-3" style="padding-top:0.5em;">
<input type="text" id="mobileSearch" class="form-control" placeholder="Search">
</div>
<div id="mobileLabels" class="row">
<!-- Cards will be injected here -->
</div>
<div id="mobilePagination" class="mt-3">
<!-- DataTable pagination will be injected here -->
</div>
</div>
{% include "_modal_add_label.html" %}
{% include "_modal_edit_labelbase.html" %}
{% include "_modal_connect_api_key.html" %}
{% include "_modal_delete_labelbase.html" %}
<!-- modal -->
<div class="modal" tabindex="-1" id="editLabelbaseModal">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<form action="{% url 'edit_labelbase' labelbase.id %}" method="post">
{% csrf_token %}
<div class="modal-header">
<h5 class="modal-title">Edit labelbase</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
{% labelbaseform_edit labelbase as edit_form %}
{{ edit_form|bootstrap }}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">OK</button>
</div>
</form>
</div>
</div>
</div>
<style>
@media (max-width: 767.98px) {
.pagination {
display: flex;
justify-content: center;
width: 100%;
}
.pagination .page-item {
flex: 1;
text-align: center;
}
<div class="modal" tabindex="-1" id="connectApiKeyLabelbaseModal">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">API Connect</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>
API Key: <strong><tt>{{ api_token }}</tt></strong>
<br>
Labelbase ID: <strong><tt>{{labelbase.id }}</tt></strong>
<br>
Base Endpoint: <strong><tt>https://labelbase.space/api/</tt></strong>
</p>
<center>
<div style="padding:1.5em;" id="qrcode"></div>
.pagination .page-link {
display: block;
width: 100%;
padding: 0.5rem;
font-size: 1rem;
}
<div class="alert alert-warning" role="alert">
API keys work like passwords. Keep them secret!<br>
Whoever knows the key can access your labelbases.<br>
</div>
</center>
<p>
Our API reference can be found here: <br>
<a href="https://labelbase.space/api-reference/">https://labelbase.space/api-reference/</a>
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">OK</button>
</div>
</div>
</div>
</div>
.pagination .page-item:first-child .page-link,
.pagination .page-item:last-child .page-link {
font-size: 1rem;
font-weight: bold;
}
.card {
/* margin-bottom: 1rem;*/
<div class="modal" tabindex="-1" id="deleteLabelbaseModal">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Delete labelbase and labels?</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="alert alert-warning" role="alert">
<strong>Warning:</strong>
<br>
<br>
This action will permanently delete the labelbase and its labels and cannot be undone.
<br><br>
Are you sure you want to proceed?
</div>
</div>
<form method="post" action="{% url 'del_labelbase' labelbase.pk %}">{% csrf_token %}
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">CANCEL</button>
<button type="submit" class="btn btn-danger" data-bs-dismiss="modal">DELETE</button>
</div>
</form>
</div>
</div>
</div>
}
.card .card-body {
padding: 1rem;
padding-bottom: 0 !important;
}
.card-title {
margin-bottom: 0.75rem;
font-size: 1.25rem;
font-weight: bold;
}
<script>
.card-text {
margin-bottom: 0.5rem;
}
.card-text strong {
display: inline-block;
min-width: 80px;
font-weight: bold;
}
.td-key {
width: 6rem;
}
}
</style>
<script>
{% addtoblock "js" %}
$(document).ready(function () {
const dt_table = $('#bip329labels').dataTable({
order: [[0, "asc"]],
columns: [
{
data: 'id',
orderable: true,
searchable: true
const dt_table = $('#bip329labels').DataTable({
order: [[0, "asc"]],
columns: [
{ data: 'id', orderable: true, searchable: true },
{ data: 'type', orderable: true, searchable: true },
{ data: 'ref', orderable: true, searchable: true },
{ data: 'label', orderable: true, searchable: true },
{ data: 'origin', orderable: true, searchable: true },
{ data: 'spendable', orderable: true, searchable: true }
],
searching: true,
processing: false,
serverSide: true,
stateSave: true,
responsive: true,
ajax: {
url: "{% url 'labelbase_label_data' labelbase.pk %}?tag={{ request.GET.tag }}",
type: 'GET',
dataSrc: 'data'
},
{
data: 'type',
orderable: true,
searchable: true
},
{
data: 'ref',
orderable: true,
searchable: true,
},
{
data: 'label',
orderable: true,
searchable: true,
},
{
data: 'origin',
orderable: true,
searchable: true,
},
{
data: 'spendable',
orderable: true,
searchable: true,
drawCallback: function(settings) {
let isMobile = $(window).width() < 768;
if (isMobile) {
renderMobileView(settings.json.data, settings._iDisplayStart, settings._iDisplayLength, settings._iRecordsDisplay, settings.fnRecordsTotal());
}
}
],
searching: true,
processing: false,
serverSide: true,
stateSave: true,
responsive: true,
ajax: {
url: "{% url 'labelbase_label_data' labelbase.pk %}?tag={{ request.GET.tag }}",
type: 'GET'
},
});
});
function createCard(data) {
let originText = '';
if (data.origin) {
originText = `<p class="card-text"><strong>Origin:</strong> ${data.origin}</p>`;
}
let spendableText = '';
if (data.type === '<tt>output</tt>' && (data.spendable === "<tt>true</tt>" || data.spendable === "<tt>false</tt>")) {
spendableText = `<tr><td> <strong>Spendable:</strong></td><td> ${data.spendable === "<tt>true</tt>" ? 'true' : 'false'}</td></tr>`;
}
return `
<div class="col-12 mb-3">
<div class="card">
<div class="card-body position-relative">
<strong>
<span class="card-title" style="max-width: 88%; font-size: 1.2em; display: inline-block;">
${data.label}
</span>
</strong>
<button type="button" class="btn btn-link btn-sm position-absolute top-0 end-0 m-2">
#${data.id}
</button>
<table class="table table-borderless mt-3">
<tbody>
<tr>
<td class="td-key"><strong>Type:</strong></td>
<td>${data.type}</td>
</tr>
<tr>
<td><strong>Ref:</strong></td>
<td>${data.ref}</td>
</tr>
${originText ? `<tr><td colspan="2">${originText}</td></tr>` : ''}
${spendableText ? `${spendableText}` : ''}
</tbody>
</table>
</div>
</div>
</div>
`;
}
var qrcode = new QRCode("qrcode", {
text: JSON.stringify({
api_key: '{{ api_token }}',
api_base: 'https://labelbase.space/api/',
labelbase_id: {{labelbase.id }},
name: '{{labelbase.name }}',
fingerprint: '{{  labelbase.fingerprint }}',
function renderMobileView(data, start, length, totalRecords, totalRecordsAll) {
$('#mobileLabels').empty();
data.forEach(function(item) {
$('#mobileLabels').append(createCard(item));
});
renderMobilePagination(start, length, totalRecords);
}
function renderMobilePagination(start, length, totalRecords) {
const totalPages = Math.ceil(totalRecords / length);
const currentPage = Math.ceil(start / length) + 1;
let paginationHtml = '<nav><ul class="pagination">';
if (currentPage > 1) {
paginationHtml += `<li class="page-item"><a class="page-link" href="#">Previous</a></li>`;
}
for (let i = 1; i <= totalPages; i++) {
paginationHtml += `<li class="page-item ${i === currentPage ? 'active' : ''}"><a class="page-link" href="#">${i}</a></li>`;
}
if (currentPage < totalPages) {
paginationHtml += `<li class="page-item"><a class="page-link" href="#">Next</a></li>`;
}
paginationHtml += '</ul></nav>';
$('#mobilePagination').html(paginationHtml);
$('.page-link').click(function (e) {
e.preventDefault();
let page = $(this).text();
if (page === 'Previous') {
page = currentPage - 1;
} else if (page === 'Next') {
page = currentPage + 1;
} else {
page = parseInt(page);
}
const newStart = (page - 1) * length;
dt_table.page(page - 1).draw(false);
});
}
function updateInfo(start, end, totalFiltered, total, isMobile) {
let info = `Showing ${start} to ${end} of ${totalFiltered} entries`;
if (totalFiltered !== total) {
info += ` (filtered from ${total} total entries)`;
}
if (isMobile) {
$('#datatable-info-mobile').html(info);
$('#datatable-info-regular').empty(); // Clear regular info
} else {
$('#datatable-info-regular').html(info);
$('#datatable-info-mobile').empty(); // Clear mobile info
}
}
$('#mobileSearch').on('keyup', function() {
$('#bip329labels_filter input').val(this.value).trigger('keyup');
update_showing();
});
$('#bip329labels_filter input').on('keyup', function() {
$('#mobileSearch').val(this.value);
});
/*
$(window).resize(function() {
let isMobile = $(window).width() < 768;
if (isMobile) {
dt_table.ajax.reload(function(json) {
renderMobileView(json.data, dt_table.page.info().start, dt_table.page.info().length, dt_table.page.info().recordsDisplay, dt_table.page.info().recordsTotal);
updateInfo(dt_table.page.info().start + 1, Math.min(dt_table.page.info().start + dt_table.page.info().length, dt_table.page.info().recordsDisplay), dt_table.page.info().recordsDisplay, dt_table.page.info().recordsTotal, true);
});
}
});
if ($(window).width() < 768) {
dt_table.ajax.reload(function(json) {
renderMobileView(json.data, dt_table.page.info().start, dt_table.page.info().length, dt_table.page.info().recordsDisplay, dt_table.page.info().recordsTotal);
updateInfo(dt_table.page.info().start + 1, Math.min(dt_table.page.info().start + dt_table.page.info().length, dt_table.page.info().recordsDisplay), dt_table.page.info().recordsDisplay, dt_table.page.info().recordsTotal, true);
});
} */
function update_showing() {
if ($(window).width() < 768) {
dt_table.ajax.reload(function(json) {
let start = dt_table.page.info().start;
let length = json.data.length;
let recordsDisplay = dt_table.page.info().recordsDisplay;
let recordsTotal = dt_table.page.info().recordsTotal;
renderMobileView(json.data, start, length, recordsDisplay, recordsTotal);
updateInfo(Math.min(recordsDisplay, start +1), Math.min(start + length, recordsDisplay), length, recordsTotal, true);
});
}
}
// run once
update_showing();
$(window).resize(function() {
let isMobile = $(window).width() < 768;
if (isMobile) {
update_showing();
/*dt_table.ajax.reload(function(json) {
let start = dt_table.page.info().start;
let length = json.data.length;
let recordsDisplay = dt_table.page.info().recordsDisplay;
let recordsTotal = dt_table.page.info().recordsTotal;
renderMobileView(json.data, start, length, recordsDisplay, recordsTotal);
updateInfo(start + 1, Math.min(start + length, recordsDisplay), length, recordsTotal, true);
});*/
}
});
var qrcode = new QRCode("qrcode", {
text: JSON.stringify({
api_key: '{{ api_token }}',
api_base: 'https://labelbase.space/api/',
labelbase_id: {{ labelbase.id }},
name: '{{ labelbase.name }}',
fingerprint: '{{ labelbase.fingerprint }}'
}),
width: 200,
height: 200,
colorDark : "#000000",
colorLight : "#ffffff",
correctLevel : QRCode.CorrectLevel.H
});
width: 200,
height: 200,
colorDark : "#000000",
colorLight : "#ffffff",
correctLevel : QRCode.CorrectLevel.H
});
});
function removeHashtag() {
alert("Hashtag removed!"); // Example alert
}
{% endaddtoblock %}
</script>
</script>
{% endif %}
<!-- end modal -->
{% endblock %}