mirror of
https://github.com/Labelbase/Labelbase.git
synced 2026-08-13 12:33:23 +02:00
added new files
This commit is contained in:
parent
7fc66c3309
commit
06ef65f183
30 changed files with 961 additions and 373 deletions
|
|
@ -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',)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
22
django/finances/migrations/0012_auto_20240701_0932.py
Normal file
22
django/finances/migrations/0012_auto_20240701_0932.py
Normal 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),
|
||||
),
|
||||
]
|
||||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
153
django/importer/samourai.py
Normal 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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
BIN
django/labelbase/static/Cloud.png
Normal file
BIN
django/labelbase/static/Cloud.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 89 KiB |
|
|
@ -178,6 +178,9 @@ DATABASES = {
|
|||
"PASSWORD": proj_config.get("database", "password"),
|
||||
'HOST': 'localhost',
|
||||
'PORT': 3306,
|
||||
'OPTIONS': {
|
||||
'charset': 'utf8mb4',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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" %}">
|
||||
|
|
|
|||
35
django/templates/_modal_connect_api_key.html
Normal file
35
django/templates/_modal_connect_api_key.html
Normal 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>
|
||||
26
django/templates/_modal_delete_labelbase.html
Normal file
26
django/templates/_modal_delete_labelbase.html
Normal 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>
|
||||
24
django/templates/_modal_edit_labelbase.html
Normal file
24
django/templates/_modal_edit_labelbase.html
Normal 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>
|
||||
|
|
@ -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 %}
|
||||
|
|
|
|||
51
django/templates/cloud.html
Normal file
51
django/templates/cloud.html
Normal 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 %}
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue