Instructions downloader #54
@ -2,6 +2,7 @@ from datetime import datetime, timezone
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from shutil import copyfileobj
|
from shutil import copyfileobj
|
||||||
|
import traceback
|
||||||
from typing import Tuple, TYPE_CHECKING
|
from typing import Tuple, TYPE_CHECKING
|
||||||
|
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
@ -12,14 +13,16 @@ from werkzeug.datastructures import FileStorage
|
|||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
|
|
||||||
from .exceptions import ErrorException, DownloadException
|
from .exceptions import ErrorException, DownloadException
|
||||||
from .parser import parse_set
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .rebrickable_set import RebrickableSet
|
from .rebrickable_set import RebrickableSet
|
||||||
|
from .socket import BrickSocket
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class BrickInstructions(object):
|
class BrickInstructions(object):
|
||||||
|
socket: 'BrickSocket'
|
||||||
|
|
||||||
allowed: bool
|
allowed: bool
|
||||||
rebrickable: 'RebrickableSet | None'
|
rebrickable: 'RebrickableSet | None'
|
||||||
extension: str
|
extension: str
|
||||||
@ -29,9 +32,22 @@ class BrickInstructions(object):
|
|||||||
name: str
|
name: str
|
||||||
size: int
|
size: int
|
||||||
|
|
||||||
def __init__(self, file: os.DirEntry | str, /):
|
def __init__(
|
||||||
|
self,
|
||||||
|
file: os.DirEntry | str,
|
||||||
|
/,
|
||||||
|
*,
|
||||||
|
socket: 'BrickSocket | None' = None,
|
||||||
|
):
|
||||||
|
# Save the socket
|
||||||
|
if socket is not None:
|
||||||
|
self.socket = socket
|
||||||
|
|
||||||
if isinstance(file, str):
|
if isinstance(file, str):
|
||||||
self.filename = file
|
self.filename = file
|
||||||
|
|
||||||
|
if self.filename == '':
|
||||||
|
raise ErrorException('An instruction filename cannot be empty')
|
||||||
else:
|
else:
|
||||||
self.filename = file.name
|
self.filename = file.name
|
||||||
|
|
||||||
@ -73,31 +89,87 @@ class BrickInstructions(object):
|
|||||||
|
|
||||||
# Download an instruction file
|
# Download an instruction file
|
||||||
def download(self, path: str, /) -> None:
|
def download(self, path: str, /) -> None:
|
||||||
target = self.path(filename=secure_filename(self.filename))
|
try:
|
||||||
|
# Just to make sure that the progress is initiated
|
||||||
|
self.socket.progress(
|
||||||
|
message='Downloading {file}'.format(
|
||||||
|
file=self.filename,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if os.path.isfile(target):
|
target = self.path(filename=secure_filename(self.filename))
|
||||||
raise ErrorException('Cannot download {target} as it already exists'.format( # noqa: E501
|
|
||||||
target=self.filename
|
|
||||||
))
|
|
||||||
|
|
||||||
url = current_app.config['REBRICKABLE_LINK_INSTRUCTIONS_PATTERN'].format( # noqa: E501
|
# Skipping rather than failing here
|
||||||
path=path
|
if os.path.isfile(target):
|
||||||
)
|
self.socket.complete(
|
||||||
|
message='File {file} already exists, skipped'.format(
|
||||||
|
file=self.filename,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
response = requests.get(url, stream=True)
|
else:
|
||||||
if response.ok:
|
url = current_app.config['REBRICKABLE_LINK_INSTRUCTIONS_PATTERN'].format( # noqa: E501
|
||||||
with open(target, 'wb') as f:
|
path=path
|
||||||
copyfileobj(response.raw, f)
|
)
|
||||||
else:
|
trimmed_url = current_app.config['REBRICKABLE_LINK_INSTRUCTIONS_PATTERN'].format( # noqa: E501
|
||||||
raise DownloadException('Failed to download {file}. Status code: {code}'.format( # noqa: E501
|
path=path.partition('/')[0]
|
||||||
file=self.filename,
|
)
|
||||||
code=response.status_code
|
|
||||||
))
|
|
||||||
|
|
||||||
# Info
|
# Request the file
|
||||||
logger.info('The instruction file {file} has been downloaded'.format(
|
self.socket.progress(
|
||||||
file=self.filename
|
message='Requesting {url}'.format(
|
||||||
))
|
url=trimmed_url,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
response = requests.get(url, stream=True)
|
||||||
|
if response.ok:
|
||||||
|
|
||||||
|
# Store the content header as size
|
||||||
|
try:
|
||||||
|
self.size = int(
|
||||||
|
response.headers.get('Content-length', 0)
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
self.size = 0
|
||||||
|
|
||||||
|
# Downloading the file
|
||||||
|
self.socket.progress(
|
||||||
|
message='Downloading {url} ({size})'.format(
|
||||||
|
url=trimmed_url,
|
||||||
|
size=self.human_size(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(target, 'wb') as f:
|
||||||
|
copyfileobj(response.raw, f)
|
||||||
|
else:
|
||||||
|
raise DownloadException('failed to download: {code}'.format( # noqa: E501
|
||||||
|
code=response.status_code
|
||||||
|
))
|
||||||
|
|
||||||
|
# Info
|
||||||
|
logger.info('The instruction file {file} has been downloaded'.format( # noqa: E501
|
||||||
|
file=self.filename
|
||||||
|
))
|
||||||
|
|
||||||
|
# Complete
|
||||||
|
self.socket.complete(
|
||||||
|
message='File {file} downloaded ({size})'.format( # noqa: E501
|
||||||
|
file=self.filename,
|
||||||
|
size=self.human_size()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.socket.fail(
|
||||||
|
message='Error while downloading instruction {file}: {error}'.format( # noqa: E501
|
||||||
|
file=self.filename,
|
||||||
|
error=e,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.debug(traceback.format_exc())
|
||||||
|
|
||||||
# Display the size in a human format
|
# Display the size in a human format
|
||||||
def human_size(self) -> str:
|
def human_size(self) -> str:
|
||||||
@ -175,36 +247,9 @@ class BrickInstructions(object):
|
|||||||
else:
|
else:
|
||||||
return 'file-line'
|
return 'file-line'
|
||||||
|
|
||||||
# Download selected instructions for a set
|
|
||||||
@staticmethod
|
|
||||||
def download_instructions(form: dict[str, str], /) -> None:
|
|
||||||
selected_instructions: list[Tuple[str, str]] = []
|
|
||||||
|
|
||||||
# Get the list of instructions
|
|
||||||
for key in form:
|
|
||||||
if key.startswith('instruction-') and form.get(key) == 'on':
|
|
||||||
_, _, index = key.partition('-')
|
|
||||||
alt_text = form.get(f'instruction-alt-text-{index}', '')
|
|
||||||
href_text = form.get(f'instruction-href-text-{index}', '').removeprefix('/instructions/') # Remove the /instructions/ part # noqa: E501
|
|
||||||
selected_instructions.append((href_text, alt_text))
|
|
||||||
|
|
||||||
# Raise if nothing selected
|
|
||||||
if not len(selected_instructions):
|
|
||||||
raise ErrorException('No instruction was selected to download')
|
|
||||||
|
|
||||||
# Loop over selected instructions and download them
|
|
||||||
for href, filename in selected_instructions:
|
|
||||||
BrickInstructions(f"{filename}.pdf").download(href)
|
|
||||||
|
|
||||||
# Find the instructions for a set
|
# Find the instructions for a set
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def find_instructions(form: dict[str, str], /) -> list[Tuple[str, str]]:
|
def find_instructions(set: str, /) -> list[Tuple[str, str]]:
|
||||||
# Grab the set ID
|
|
||||||
set: str = form.get('add-set', '')
|
|
||||||
|
|
||||||
# Parse it
|
|
||||||
set = parse_set(set)
|
|
||||||
|
|
||||||
response = requests.get(
|
response = requests.get(
|
||||||
current_app.config['REBRICKABLE_LINK_INSTRUCTIONS_PATTERN'].format(
|
current_app.config['REBRICKABLE_LINK_INSTRUCTIONS_PATTERN'].format(
|
||||||
path=set,
|
path=set,
|
||||||
|
@ -5,6 +5,8 @@ from flask import copy_current_request_context, Flask, request
|
|||||||
from flask_socketio import SocketIO
|
from flask_socketio import SocketIO
|
||||||
|
|
||||||
from .configuration_list import BrickConfigurationList
|
from .configuration_list import BrickConfigurationList
|
||||||
|
from .instructions import BrickInstructions
|
||||||
|
from .instructions_list import BrickInstructionsList
|
||||||
from .login import LoginManager
|
from .login import LoginManager
|
||||||
from .set import BrickSet
|
from .set import BrickSet
|
||||||
from .sql import close as sql_close
|
from .sql import close as sql_close
|
||||||
@ -13,10 +15,10 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
# Messages valid through the socket
|
# Messages valid through the socket
|
||||||
MESSAGES: Final[dict[str, str]] = {
|
MESSAGES: Final[dict[str, str]] = {
|
||||||
'ADD_SET': 'add_set',
|
|
||||||
'COMPLETE': 'complete',
|
'COMPLETE': 'complete',
|
||||||
'CONNECT': 'connect',
|
'CONNECT': 'connect',
|
||||||
'DISCONNECT': 'disconnect',
|
'DISCONNECT': 'disconnect',
|
||||||
|
'DOWNLOAD_INSTRUCTIONS': 'download_instructions',
|
||||||
'FAIL': 'fail',
|
'FAIL': 'fail',
|
||||||
'IMPORT_SET': 'import_set',
|
'IMPORT_SET': 'import_set',
|
||||||
'LOAD_SET': 'load_set',
|
'LOAD_SET': 'load_set',
|
||||||
@ -84,6 +86,41 @@ class BrickSocket(object):
|
|||||||
def disconnect() -> None:
|
def disconnect() -> None:
|
||||||
self.disconnected()
|
self.disconnected()
|
||||||
|
|
||||||
|
@self.socket.on(MESSAGES['DOWNLOAD_INSTRUCTIONS'], namespace=self.namespace) # noqa: E501
|
||||||
|
def download_instructions(data: dict[str, Any], /) -> None:
|
||||||
|
# Needs to be authenticated
|
||||||
|
if LoginManager.is_not_authenticated():
|
||||||
|
self.fail(message='You need to be authenticated')
|
||||||
|
return
|
||||||
|
|
||||||
|
instructions = BrickInstructions(
|
||||||
|
'{name}.pdf'.format(name=data.get('alt', '')),
|
||||||
|
socket=self
|
||||||
|
)
|
||||||
|
|
||||||
|
path = data.get('href', '').removeprefix('/instructions/')
|
||||||
|
|
||||||
|
# Update the progress
|
||||||
|
try:
|
||||||
|
self.progress_total = int(data.get('total', 0))
|
||||||
|
self.progress_count = int(data.get('current', 0))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Start it in a thread if requested
|
||||||
|
if self.threaded:
|
||||||
|
@copy_current_request_context
|
||||||
|
def do_download() -> None:
|
||||||
|
instructions.download(path)
|
||||||
|
|
||||||
|
BrickInstructionsList(force=True)
|
||||||
|
|
||||||
|
self.socket.start_background_task(do_download)
|
||||||
|
else:
|
||||||
|
instructions.download(path)
|
||||||
|
|
||||||
|
BrickInstructionsList(force=True)
|
||||||
|
|
||||||
@self.socket.on(MESSAGES['IMPORT_SET'], namespace=self.namespace)
|
@self.socket.on(MESSAGES['IMPORT_SET'], namespace=self.namespace)
|
||||||
def import_set(data: dict[str, Any], /) -> None:
|
def import_set(data: dict[str, Any], /) -> None:
|
||||||
# Needs to be authenticated
|
# Needs to be authenticated
|
||||||
|
@ -14,6 +14,7 @@ from .exceptions import exception_handler
|
|||||||
from ..instructions import BrickInstructions
|
from ..instructions import BrickInstructions
|
||||||
from ..instructions_list import BrickInstructionsList
|
from ..instructions_list import BrickInstructionsList
|
||||||
from ..parser import parse_set
|
from ..parser import parse_set
|
||||||
|
from ..socket import MESSAGES
|
||||||
from .upload import upload_helper
|
from .upload import upload_helper
|
||||||
|
|
||||||
instructions_page = Blueprint(
|
instructions_page = Blueprint(
|
||||||
@ -149,24 +150,22 @@ def download() -> str:
|
|||||||
|
|
||||||
|
|
||||||
# Show search results
|
# Show search results
|
||||||
@instructions_page.route('/download/select', methods=['POST'])
|
|
||||||
@login_required
|
|
||||||
@exception_handler(__file__, post_redirect='instructions.download')
|
|
||||||
def select_download() -> str:
|
|
||||||
return render_template(
|
|
||||||
'instructions.html',
|
|
||||||
download=True,
|
|
||||||
instructions=BrickInstructions.find_instructions(request.form)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Download files
|
|
||||||
@instructions_page.route('/download', methods=['POST'])
|
@instructions_page.route('/download', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@exception_handler(__file__, post_redirect='instructions.download')
|
@exception_handler(__file__, post_redirect='instructions.download')
|
||||||
def do_download() -> Response:
|
def do_download() -> str:
|
||||||
BrickInstructions.download_instructions(request.form)
|
# Grab the set number
|
||||||
|
try:
|
||||||
|
set = parse_set(request.form.get('download-set', ''))
|
||||||
|
except Exception:
|
||||||
|
set = ''
|
||||||
|
|
||||||
BrickInstructionsList(force=True)
|
return render_template(
|
||||||
|
'instructions.html',
|
||||||
return redirect(url_for('instructions.list'))
|
download=True,
|
||||||
|
instructions=BrickInstructions.find_instructions(set),
|
||||||
|
set=set,
|
||||||
|
path=current_app.config['SOCKET_PATH'],
|
||||||
|
namespace=current_app.config['SOCKET_NAMESPACE'],
|
||||||
|
messages=MESSAGES
|
||||||
|
)
|
||||||
|
108
static/scripts/socket/instructions.js
Normal file
108
static/scripts/socket/instructions.js
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
// Instructions Socket class
|
||||||
|
class BrickInstructionsSocket extends BrickSocket {
|
||||||
|
constructor(id, path, namespace, messages) {
|
||||||
|
super(id, path, namespace, messages, true);
|
||||||
|
|
||||||
|
// Listeners
|
||||||
|
this.download_listener = undefined;
|
||||||
|
|
||||||
|
// Form elements (built based on the initial id)
|
||||||
|
this.html_button = document.getElementById(id);
|
||||||
|
this.html_files = document.getElementById(`${id}-files`);
|
||||||
|
|
||||||
|
if (this.html_button) {
|
||||||
|
this.download_listener = ((bricksocket) => (e) => {
|
||||||
|
if (!bricksocket.disabled && bricksocket.socket !== undefined && bricksocket.socket.connected) {
|
||||||
|
bricksocket.toggle(false);
|
||||||
|
|
||||||
|
bricksocket.download_instructions();
|
||||||
|
}
|
||||||
|
})(this);
|
||||||
|
|
||||||
|
this.html_button.addEventListener("click", this.download_listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.html_card_dismiss && this.html_card) {
|
||||||
|
this.html_card_dismiss.addEventListener("click", ((card) => (e) => {
|
||||||
|
card.classList.add("d-none");
|
||||||
|
})(this.html_card));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup the socket
|
||||||
|
this.setup();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upon receiving a complete message
|
||||||
|
complete(data) {
|
||||||
|
super.complete(data);
|
||||||
|
|
||||||
|
// Uncheck current file
|
||||||
|
this.file.checked = false;
|
||||||
|
|
||||||
|
// Download the next file
|
||||||
|
this.download_instructions(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the list of checkboxes describing files
|
||||||
|
get_files(checked=false) {
|
||||||
|
let files = [];
|
||||||
|
|
||||||
|
if (this.html_files) {
|
||||||
|
files = [...this.html_files.querySelectorAll('input[type="checkbox"]')];
|
||||||
|
|
||||||
|
if (checked) {
|
||||||
|
files = files.filter(file => file.checked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download an instructions file
|
||||||
|
download_instructions(from_complete=false) {
|
||||||
|
if (this.html_files) {
|
||||||
|
if (!from_complete) {
|
||||||
|
this.total = this.get_files(true).length;
|
||||||
|
this.current = 0;
|
||||||
|
this.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the next checkbox
|
||||||
|
this.file = this.get_files(true).shift();
|
||||||
|
|
||||||
|
// Abort if nothing left to process
|
||||||
|
if (this.file === undefined) {
|
||||||
|
// Settle the form
|
||||||
|
this.spinner(false);
|
||||||
|
this.toggle(true);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.spinner(true);
|
||||||
|
|
||||||
|
this.current++;
|
||||||
|
this.socket.emit(this.messages.DOWNLOAD_INSTRUCTIONS, {
|
||||||
|
alt: this.file.dataset.downloadAlt,
|
||||||
|
href: this.file.dataset.downloadHref,
|
||||||
|
total: this.total,
|
||||||
|
current: this.current,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.fail("Could not find the list of files to download");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle clicking on the button, or sending events
|
||||||
|
toggle(enabled) {
|
||||||
|
super.toggle(enabled);
|
||||||
|
|
||||||
|
if (this.html_files) {
|
||||||
|
this.get_files().forEach(el => el.disabled != enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.html_button) {
|
||||||
|
this.html_button.disabled = !enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
244
static/scripts/socket/set.js
Normal file
244
static/scripts/socket/set.js
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
// Set Socket class
|
||||||
|
class BrickSetSocket extends BrickSocket {
|
||||||
|
constructor(id, path, namespace, messages, bulk=false) {
|
||||||
|
super(id, path, namespace, messages, bulk);
|
||||||
|
|
||||||
|
// Listeners
|
||||||
|
this.add_listener = undefined;
|
||||||
|
this.confirm_listener = undefined;
|
||||||
|
|
||||||
|
// Form elements (built based on the initial id)
|
||||||
|
this.html_button = document.getElementById(id);
|
||||||
|
this.html_input = document.getElementById(`${id}-set`);
|
||||||
|
this.html_no_confim = document.getElementById(`${id}-no-confirm`);
|
||||||
|
|
||||||
|
// Card elements
|
||||||
|
this.html_card = document.getElementById(`${id}-card`);
|
||||||
|
this.html_card_set = document.getElementById(`${id}-card-set`);
|
||||||
|
this.html_card_name = document.getElementById(`${id}-card-name`);
|
||||||
|
this.html_card_image_container = document.getElementById(`${id}-card-image-container`);
|
||||||
|
this.html_card_image = document.getElementById(`${id}-card-image`);
|
||||||
|
this.html_card_footer = document.getElementById(`${id}-card-footer`);
|
||||||
|
this.html_card_confirm = document.getElementById(`${id}-card-confirm`);
|
||||||
|
this.html_card_dismiss = document.getElementById(`${id}-card-dismiss`);
|
||||||
|
|
||||||
|
if (this.html_button) {
|
||||||
|
this.add_listener = ((bricksocket) => (e) => {
|
||||||
|
if (!bricksocket.disabled && bricksocket.socket !== undefined && bricksocket.socket.connected) {
|
||||||
|
bricksocket.toggle(false);
|
||||||
|
|
||||||
|
// Split and save the list if bulk
|
||||||
|
if (bricksocket.bulk) {
|
||||||
|
bricksocket.read_set_list()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bricksocket.bulk || (bricksocket.html_no_confim && bricksocket.html_no_confim.checked)) {
|
||||||
|
bricksocket.import_set(true);
|
||||||
|
} else {
|
||||||
|
bricksocket.load_set();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})(this);
|
||||||
|
|
||||||
|
this.html_button.addEventListener("click", this.add_listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.html_card_dismiss && this.html_card) {
|
||||||
|
this.html_card_dismiss.addEventListener("click", ((card) => (e) => {
|
||||||
|
card.classList.add("d-none");
|
||||||
|
})(this.html_card));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup the socket
|
||||||
|
this.setup();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear form
|
||||||
|
clear() {
|
||||||
|
super.clear();
|
||||||
|
|
||||||
|
if (this.html_card) {
|
||||||
|
this.html_card.classList.add("d-none");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.html_card_footer) {
|
||||||
|
this.html_card_footer.classList.add("d-none");
|
||||||
|
|
||||||
|
if (this.html_card_confirm) {
|
||||||
|
this.html_card_footer.classList.add("d-none");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upon receiving a complete message
|
||||||
|
complete(data) {
|
||||||
|
super.complete(data);
|
||||||
|
|
||||||
|
if (this.bulk) {
|
||||||
|
// Import the next set
|
||||||
|
this.import_set(true, undefined, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upon receiving a fail message
|
||||||
|
fail(data) {
|
||||||
|
super.fail(data);
|
||||||
|
|
||||||
|
if (this.bulk && this.html_input) {
|
||||||
|
if (this.set_list_last_set !== undefined) {
|
||||||
|
this.set_list.unshift(this.set_list_last_set);
|
||||||
|
this.set_list_last_set = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.html_input.value = this.set_list.join(', ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import a set
|
||||||
|
import_set(no_confirm, set, from_complete=false) {
|
||||||
|
if (this.html_input) {
|
||||||
|
if (!this.bulk || !from_complete) {
|
||||||
|
// Reset the progress
|
||||||
|
if (no_confirm) {
|
||||||
|
this.clear();
|
||||||
|
} else {
|
||||||
|
this.clear_status();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grab from the list if bulk
|
||||||
|
if (this.bulk) {
|
||||||
|
set = this.set_list.shift()
|
||||||
|
|
||||||
|
// Abort if nothing left to process
|
||||||
|
if (set === undefined) {
|
||||||
|
// Clear the input
|
||||||
|
this.html_input.value = "";
|
||||||
|
|
||||||
|
// Settle the form
|
||||||
|
this.spinner(false);
|
||||||
|
this.toggle(true);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the pulled set
|
||||||
|
this.set_list_last_set = set;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.spinner(true);
|
||||||
|
|
||||||
|
this.socket.emit(this.messages.IMPORT_SET, {
|
||||||
|
set: (set !== undefined) ? set : this.html_input.value,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.fail("Could not find the input field for the set number");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load a set
|
||||||
|
load_set() {
|
||||||
|
if (this.html_input) {
|
||||||
|
// Reset the progress
|
||||||
|
this.clear()
|
||||||
|
this.spinner(true);
|
||||||
|
|
||||||
|
this.socket.emit(this.messages.LOAD_SET, {
|
||||||
|
set: this.html_input.value
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.fail("Could not find the input field for the set number");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bulk: read the input as a list
|
||||||
|
read_set_list() {
|
||||||
|
this.set_list = [];
|
||||||
|
|
||||||
|
if (this.html_input) {
|
||||||
|
const value = this.html_input.value;
|
||||||
|
this.set_list = value.split(",").map((el) => el.trim())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set is loaded
|
||||||
|
set_loaded(data) {
|
||||||
|
if (this.html_card) {
|
||||||
|
this.html_card.classList.remove("d-none");
|
||||||
|
|
||||||
|
if (this.html_card_set) {
|
||||||
|
this.html_card_set.textContent = data["set"];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.html_card_name) {
|
||||||
|
this.html_card_name.textContent = data["name"];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.html_card_image_container) {
|
||||||
|
this.html_card_image_container.setAttribute("style", `background-image: url(${data["image"]})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.html_card_image) {
|
||||||
|
this.html_card_image.setAttribute("src", data["image"]);
|
||||||
|
this.html_card_image.setAttribute("alt", data["set"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.html_card_footer) {
|
||||||
|
this.html_card_footer.classList.add("d-none");
|
||||||
|
|
||||||
|
if (!data.download) {
|
||||||
|
this.html_card_footer.classList.remove("d-none");
|
||||||
|
|
||||||
|
if (this.html_card_confirm) {
|
||||||
|
if (this.confirm_listener !== undefined) {
|
||||||
|
this.html_card_confirm.removeEventListener("click", this.confirm_listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.confirm_listener = ((bricksocket, set) => (e) => {
|
||||||
|
if (!bricksocket.disabled) {
|
||||||
|
bricksocket.toggle(false);
|
||||||
|
bricksocket.import_set(false, set);
|
||||||
|
}
|
||||||
|
})(this, data["set"]);
|
||||||
|
|
||||||
|
this.html_card_confirm.addEventListener("click", this.confirm_listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup the actual socket
|
||||||
|
setup() {
|
||||||
|
super.setup();
|
||||||
|
|
||||||
|
if (this.socket !== undefined) {
|
||||||
|
// Set loaded
|
||||||
|
this.socket.on(this.messages.SET_LOADED, ((bricksocket) => (data) => {
|
||||||
|
bricksocket.set_loaded(data);
|
||||||
|
})(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Toggle clicking on the button, or sending events
|
||||||
|
toggle(enabled) {
|
||||||
|
super.toggle(enabled);
|
||||||
|
|
||||||
|
if (this.html_button) {
|
||||||
|
this.html_button.disabled = !enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.html_input) {
|
||||||
|
this.html_input.disabled = !enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.html_card_confirm) {
|
||||||
|
this.html_card_confirm.disabled = !enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.html_card_dismiss) {
|
||||||
|
this.html_card_dismiss.disabled = !enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,22 +5,17 @@ class BrickSocket {
|
|||||||
this.path = path;
|
this.path = path;
|
||||||
this.namespace = namespace;
|
this.namespace = namespace;
|
||||||
this.messages = messages;
|
this.messages = messages;
|
||||||
this.bulk = bulk;
|
|
||||||
|
|
||||||
this.disabled = false;
|
this.disabled = false;
|
||||||
this.socket = undefined;
|
this.socket = undefined;
|
||||||
|
|
||||||
// Listeners
|
// Bulk mode
|
||||||
this.add_listener = undefined;
|
this.bulk = bulk;
|
||||||
this.confirm_listener = undefined;
|
|
||||||
|
|
||||||
// Form elements (built based on the initial id)
|
// Form elements (built based on the initial id)
|
||||||
this.html_button = document.getElementById(id);
|
|
||||||
this.html_complete = document.getElementById(`${id}-complete`);
|
this.html_complete = document.getElementById(`${id}-complete`);
|
||||||
this.html_count = document.getElementById(`${id}-count`);
|
this.html_count = document.getElementById(`${id}-count`);
|
||||||
this.html_fail = document.getElementById(`${id}-fail`);
|
this.html_fail = document.getElementById(`${id}-fail`);
|
||||||
this.html_input = document.getElementById(`${id}-set`);
|
|
||||||
this.html_no_confim = document.getElementById(`${id}-no-confirm`);
|
|
||||||
this.html_progress = document.getElementById(`${id}-progress`);
|
this.html_progress = document.getElementById(`${id}-progress`);
|
||||||
this.html_progress_bar = document.getElementById(`${id}-progress-bar`);
|
this.html_progress_bar = document.getElementById(`${id}-progress-bar`);
|
||||||
this.html_progress_message = document.getElementById(`${id}-progress-message`);
|
this.html_progress_message = document.getElementById(`${id}-progress-message`);
|
||||||
@ -28,50 +23,10 @@ class BrickSocket {
|
|||||||
this.html_status = document.getElementById(`${id}-status`);
|
this.html_status = document.getElementById(`${id}-status`);
|
||||||
this.html_status_icon = document.getElementById(`${id}-status-icon`);
|
this.html_status_icon = document.getElementById(`${id}-status-icon`);
|
||||||
|
|
||||||
// Card elements
|
|
||||||
this.html_card = document.getElementById(`${id}-card`);
|
|
||||||
this.html_card_set = document.getElementById(`${id}-card-set`);
|
|
||||||
this.html_card_name = document.getElementById(`${id}-card-name`);
|
|
||||||
this.html_card_image_container = document.getElementById(`${id}-card-image-container`);
|
|
||||||
this.html_card_image = document.getElementById(`${id}-card-image`);
|
|
||||||
this.html_card_footer = document.getElementById(`${id}-card-footer`);
|
|
||||||
this.html_card_confirm = document.getElementById(`${id}-card-confirm`);
|
|
||||||
this.html_card_dismiss = document.getElementById(`${id}-card-dismiss`);
|
|
||||||
|
|
||||||
if (this.html_button) {
|
|
||||||
this.add_listener = ((bricksocket) => (e) => {
|
|
||||||
if (!bricksocket.disabled && bricksocket.socket !== undefined && bricksocket.socket.connected) {
|
|
||||||
bricksocket.toggle(false);
|
|
||||||
|
|
||||||
// Split and save the list if bulk
|
|
||||||
if (bricksocket.bulk) {
|
|
||||||
bricksocket.read_set_list()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bricksocket.bulk || (bricksocket.html_no_confim && bricksocket.html_no_confim.checked)) {
|
|
||||||
bricksocket.import_set(true);
|
|
||||||
} else {
|
|
||||||
bricksocket.load_set();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})(this);
|
|
||||||
|
|
||||||
this.html_button.addEventListener("click", this.add_listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.html_card_dismiss && this.html_card) {
|
|
||||||
this.html_card_dismiss.addEventListener("click", ((card) => (e) => {
|
|
||||||
card.classList.add("d-none");
|
|
||||||
})(this.html_card));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Socket status
|
// Socket status
|
||||||
window.setInterval(((bricksocket) => () => {
|
window.setInterval(((bricksocket) => () => {
|
||||||
bricksocket.status();
|
bricksocket.status();
|
||||||
})(this), 500);
|
})(this), 1000);
|
||||||
|
|
||||||
// Setup the socket
|
|
||||||
this.setup();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear form
|
// Clear form
|
||||||
@ -88,19 +43,9 @@ class BrickSocket {
|
|||||||
this.html_progress_bar.textContent = "";
|
this.html_progress_bar.textContent = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.progress_message("");
|
||||||
|
|
||||||
this.spinner(false);
|
this.spinner(false);
|
||||||
|
|
||||||
if (this.html_card) {
|
|
||||||
this.html_card.classList.add("d-none");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.html_card_footer) {
|
|
||||||
this.html_card_footer.classList.add("d-none");
|
|
||||||
|
|
||||||
if (this.html_card_confirm) {
|
|
||||||
this.html_card_footer.classList.add("d-none");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear status message
|
// Clear status message
|
||||||
@ -141,9 +86,6 @@ class BrickSocket {
|
|||||||
|
|
||||||
this.html_complete.append(success)
|
this.html_complete.append(success)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Import the next set
|
|
||||||
this.import_set(true, undefined, true);
|
|
||||||
} else {
|
} else {
|
||||||
this.spinner(false);
|
this.spinner(false);
|
||||||
|
|
||||||
@ -188,73 +130,8 @@ class BrickSocket {
|
|||||||
if (this.html_progress_bar) {
|
if (this.html_progress_bar) {
|
||||||
this.html_progress_bar.classList.remove("progress-bar-animated");
|
this.html_progress_bar.classList.remove("progress-bar-animated");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.bulk && this.html_input) {
|
|
||||||
if (this.set_list_last_set !== undefined) {
|
|
||||||
this.set_list.unshift(this.set_list_last_set);
|
|
||||||
this.set_list_last_set = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.html_input.value = this.set_list.join(', ');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Import a set
|
|
||||||
import_set(no_confirm, set, from_complete=false) {
|
|
||||||
if (this.html_input) {
|
|
||||||
if (!this.bulk || !from_complete) {
|
|
||||||
// Reset the progress
|
|
||||||
if (no_confirm) {
|
|
||||||
this.clear();
|
|
||||||
} else {
|
|
||||||
this.clear_status();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Grab from the list if bulk
|
|
||||||
if (this.bulk) {
|
|
||||||
set = this.set_list.shift()
|
|
||||||
|
|
||||||
// Abort if nothing left to process
|
|
||||||
if (set === undefined) {
|
|
||||||
// Clear the input
|
|
||||||
this.html_input.value = "";
|
|
||||||
|
|
||||||
// Settle the form
|
|
||||||
this.spinner(false);
|
|
||||||
this.toggle(true);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save the pulled set
|
|
||||||
this.set_list_last_set = set;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.spinner(true);
|
|
||||||
|
|
||||||
this.socket.emit(this.messages.IMPORT_SET, {
|
|
||||||
set: (set !== undefined) ? set : this.html_input.value,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.fail("Could not find the input field for the set number");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load a set
|
|
||||||
load_set() {
|
|
||||||
if (this.html_input) {
|
|
||||||
// Reset the progress
|
|
||||||
this.clear()
|
|
||||||
this.spinner(true);
|
|
||||||
|
|
||||||
this.socket.emit(this.messages.LOAD_SET, {
|
|
||||||
set: this.html_input.value
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.fail("Could not find the input field for the set number");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the progress
|
// Update the progress
|
||||||
progress(data={}) {
|
progress(data={}) {
|
||||||
@ -262,7 +139,7 @@ class BrickSocket {
|
|||||||
let count = data["count"]
|
let count = data["count"]
|
||||||
|
|
||||||
// Fix the total if bogus
|
// Fix the total if bogus
|
||||||
if (!total || isNaN(total) || total <= 1) {
|
if (!total || isNaN(total) || total <= 0) {
|
||||||
total = 0;
|
total = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -304,63 +181,6 @@ class BrickSocket {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bulk: read the input as a list
|
|
||||||
read_set_list() {
|
|
||||||
this.set_list = [];
|
|
||||||
|
|
||||||
if (this.html_input) {
|
|
||||||
const value = this.html_input.value;
|
|
||||||
this.set_list = value.split(",").map((el) => el.trim())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set is loaded
|
|
||||||
set_loaded(data) {
|
|
||||||
if (this.html_card) {
|
|
||||||
this.html_card.classList.remove("d-none");
|
|
||||||
|
|
||||||
if (this.html_card_set) {
|
|
||||||
this.html_card_set.textContent = data["set"];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.html_card_name) {
|
|
||||||
this.html_card_name.textContent = data["name"];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.html_card_image_container) {
|
|
||||||
this.html_card_image_container.setAttribute("style", `background-image: url(${data["image"]})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.html_card_image) {
|
|
||||||
this.html_card_image.setAttribute("src", data["image"]);
|
|
||||||
this.html_card_image.setAttribute("alt", data["set"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.html_card_footer) {
|
|
||||||
this.html_card_footer.classList.add("d-none");
|
|
||||||
|
|
||||||
if (!data.download) {
|
|
||||||
this.html_card_footer.classList.remove("d-none");
|
|
||||||
|
|
||||||
if (this.html_card_confirm) {
|
|
||||||
if (this.confirm_listener !== undefined) {
|
|
||||||
this.html_card_confirm.removeEventListener("click", this.confirm_listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.confirm_listener = ((bricksocket, set) => (e) => {
|
|
||||||
if (!bricksocket.disabled) {
|
|
||||||
bricksocket.toggle(false);
|
|
||||||
bricksocket.import_set(false, set);
|
|
||||||
}
|
|
||||||
})(this, data["set"]);
|
|
||||||
|
|
||||||
this.html_card_confirm.addEventListener("click", this.confirm_listener);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup the actual socket
|
// Setup the actual socket
|
||||||
setup() {
|
setup() {
|
||||||
if (this.socket === undefined) {
|
if (this.socket === undefined) {
|
||||||
@ -387,11 +207,6 @@ class BrickSocket {
|
|||||||
this.socket.on(this.messages.PROGRESS, ((bricksocket) => (data) => {
|
this.socket.on(this.messages.PROGRESS, ((bricksocket) => (data) => {
|
||||||
bricksocket.progress(data);
|
bricksocket.progress(data);
|
||||||
})(this));
|
})(this));
|
||||||
|
|
||||||
// Set loaded
|
|
||||||
this.socket.on(this.messages.SET_LOADED, ((bricksocket) => (data) => {
|
|
||||||
bricksocket.set_loaded(data);
|
|
||||||
})(this));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -434,21 +249,5 @@ class BrickSocket {
|
|||||||
// Toggle clicking on the button, or sending events
|
// Toggle clicking on the button, or sending events
|
||||||
toggle(enabled) {
|
toggle(enabled) {
|
||||||
this.disabled = !enabled;
|
this.disabled = !enabled;
|
||||||
|
|
||||||
if (this.html_button) {
|
|
||||||
this.html_button.disabled = !enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.html_input) {
|
|
||||||
this.html_input.disabled = !enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.html_card_confirm) {
|
|
||||||
this.html_card_confirm.disabled = !enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.html_card_dismiss) {
|
|
||||||
this.html_card_dismiss.disabled = !enabled;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -82,7 +82,9 @@
|
|||||||
<script src="{{ url_for('static', filename='scripts/changer.js') }}"></script>
|
<script src="{{ url_for('static', filename='scripts/changer.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='scripts/grid.js') }}"></script>
|
<script src="{{ url_for('static', filename='scripts/grid.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='scripts/set.js') }}"></script>
|
<script src="{{ url_for('static', filename='scripts/set.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='scripts/socket.js') }}"></script>
|
<script src="{{ url_for('static', filename='scripts/socket/socket.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='scripts/socket/instructions.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='scripts/socket/set.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='scripts/table.js') }}"></script>
|
<script src="{{ url_for('static', filename='scripts/table.js') }}"></script>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
@ -1,50 +1,63 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
{% if error %}<div class="alert alert-danger" role="alert"><strong>Error:</strong> {{ error }}.</div>{% endif %}
|
{% if error %}<div class="alert alert-danger" role="alert"><strong>Error:</strong> {{ error }}.</div>{% endif %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<form method="POST" action="{{ url_for('instructions.select_download') }}">
|
<form method="POST" action="{{ url_for('instructions.do_download') }}">
|
||||||
<div class="card mb-3">
|
<div class="card mb-3">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h5 class="mb-0"><i class="ri-add-circle-line"></i> Download instructions from Rebrickable</h5>
|
<h5 class="mb-0"><i class="ri-download-line"></i> Download instructions from Rebrickable</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div id="add-fail" class="alert alert-danger d-none" role="alert"></div>
|
<div class="mb-3">
|
||||||
<div id="add-complete" class="alert alert-success d-none" role="alert"></div>
|
<label for="download-set" class="form-label">Set number (only one)</label>
|
||||||
<div class="mb-3">
|
<input type="text" class="form-control" id="download-set" name="download-set" placeholder="107-1 or 1642-1 or ..." value="{{ set }}">
|
||||||
<label for="add-set" class="form-label">Set number (only one)</label>
|
|
||||||
<input type="text" class="form-control" id="add-set" name="add-set" placeholder="107-1 or 1642-1 or ..." value="{{ set }}">
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-primary">Search</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{% if instructions %}
|
|
||||||
<div class="card mb-3">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5 class="mb-0"><i class="ri-add-circle-line"></i> Select instructions to download</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<form method="POST" action="{{ url_for('instructions.do_download') }}">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Available Instructions</label>
|
|
||||||
<div class="form-check">
|
|
||||||
{% for alt_text, href in instructions %}
|
|
||||||
<div class="form-check">
|
|
||||||
<input class="form-check-input" type="checkbox" name="instruction-{{ loop.index }}" id="instruction-{{ loop.index }}">
|
|
||||||
<label class="form-check-label" for="instruction-{{ loop.index }}">
|
|
||||||
{{ alt_text }}
|
|
||||||
</label>
|
|
||||||
<input type="hidden" name="instruction-alt-text-{{ loop.index }}" value="{{ alt_text }}">
|
|
||||||
<input type="hidden" name="instruction-href-text-{{ loop.index }}" value="{{ href }}">
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-primary">Download Selected</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
</div>
|
||||||
|
<div class="card-footer text-end">
|
||||||
|
<button type="submit" class="btn btn-primary"><i class="ri-search-line"></i> Search</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
|
{% if instructions %}
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0"><i class="ri-checkbox-line"></i> Select instructions to download</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<div id="download-fail" class="alert alert-danger d-none" role="alert"></div>
|
||||||
|
<div id="download-complete"></div>
|
||||||
|
<h5 class="border-bottom">Available Instructions</h5>
|
||||||
|
<div id="download-files">
|
||||||
|
{% for alt_text, href in instructions %}
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="instruction-{{ loop.index }}" data-download-href="{{ href }}" data-download-alt="{{ alt_text }}" autocomplete="off">
|
||||||
|
<label class="form-check-label" for="instruction-{{ loop.index }}">{{ alt_text }}</label>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<div class="mb-3">
|
||||||
|
<p>
|
||||||
|
Progress <span id="download-count"></span>
|
||||||
|
<span id="download-spinner" class="d-none">
|
||||||
|
<span class="spinner-border spinner-border-sm" aria-hidden="true"></span>
|
||||||
|
<span class="visually-hidden" role="status">Loading...</span>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<div id="download-progress" class="progress" role="progressbar" aria-label="Download an instructions file progress" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
|
||||||
|
<div id="download-progress-bar" class="progress-bar" style="width: 0%"></div>
|
||||||
|
</div>
|
||||||
|
<p id="download-progress-message" class="text-center d-none"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer text-end">
|
||||||
|
<span id="download-status-icon" class="me-1"></span><span id="download-status" class="me-1"></span><button id="download" type="button" class="btn btn-primary"><i class="ri-download-line"></i> Download selected files</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% include 'instructions/socket.html' %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
10
templates/instructions/socket.html
Normal file
10
templates/instructions/socket.html
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<script type="text/javascript">
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
new BrickInstructionsSocket('download', '{{ path }}', '{{ namespace }}', {
|
||||||
|
COMPLETE: '{{ messages['COMPLETE'] }}',
|
||||||
|
DOWNLOAD_INSTRUCTIONS: '{{ messages['DOWNLOAD_INSTRUCTIONS'] }}',
|
||||||
|
FAIL: '{{ messages['FAIL'] }}',
|
||||||
|
PROGRESS: '{{ messages['PROGRESS'] }}',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
@ -1,6 +1,6 @@
|
|||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
new BrickSocket('add', '{{ path }}', '{{ namespace }}', {
|
new BrickSetSocket('add', '{{ path }}', '{{ namespace }}', {
|
||||||
COMPLETE: '{{ messages['COMPLETE'] }}',
|
COMPLETE: '{{ messages['COMPLETE'] }}',
|
||||||
FAIL: '{{ messages['FAIL'] }}',
|
FAIL: '{{ messages['FAIL'] }}',
|
||||||
IMPORT_SET: '{{ messages['IMPORT_SET'] }}',
|
IMPORT_SET: '{{ messages['IMPORT_SET'] }}',
|
||||||
|
Loading…
Reference in New Issue
Block a user