diff --git a/.env.sample b/.env.sample
index 27de81a..7cbc5c0 100644
--- a/.env.sample
+++ b/.env.sample
@@ -178,6 +178,14 @@
# Default: https://rebrickable.com/parts/{number}/_/{color}
# BK_REBRICKABLE_LINK_PART_PATTERN=
+# Optional: Pattern of the link to Rebrickable for instructions. Will be passed to Python .format()
+# Default: https://rebrickable.com/instructions/{path}
+# BK_REBRICKABLE_LINK_INSTRUCTIONS_PATTERN=
+
+# Optional: User-Agent to use when querying Rebrickable outside of the Rebrick python library
+# Default: 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
+# BK_REBRICKABLE_USER_AGENT=
+
# Optional: Display Rebrickable links wherever applicable
# Default: false
# Legacy name: LINKS
diff --git a/bricktracker/config.py b/bricktracker/config.py
index 8cfdbba..08db61b 100644
--- a/bricktracker/config.py
+++ b/bricktracker/config.py
@@ -43,6 +43,8 @@ CONFIG: Final[list[dict[str, Any]]] = [
{'n': 'REBRICKABLE_IMAGE_NIL_MINIFIGURE', 'd': 'https://rebrickable.com/static/img/nil_mf.jpg'}, # noqa: E501
{'n': 'REBRICKABLE_LINK_MINIFIGURE_PATTERN', 'd': 'https://rebrickable.com/minifigs/{number}'}, # noqa: E501
{'n': 'REBRICKABLE_LINK_PART_PATTERN', 'd': 'https://rebrickable.com/parts/{number}/_/{color}'}, # noqa: E501
+ {'n': 'REBRICKABLE_LINK_INSTRUCTIONS_PATTERN', 'd': 'https://rebrickable.com/instructions/{path}'}, # noqa: E501
+ {'n': 'REBRICKABLE_USER_AGENT', 'd': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'}, # noqa: E501
{'n': 'REBRICKABLE_LINKS', 'e': 'LINKS', 'c': bool},
{'n': 'REBRICKABLE_PAGE_SIZE', 'd': 100, 'c': int},
{'n': 'RETIRED_SETS_FILE_URL', 'd': 'https://docs.google.com/spreadsheets/d/1rlYfEXtNKxUOZt2Mfv0H17DvK7bj6Pe0CuYwq6ay8WA/gviz/tq?tqx=out:csv&sheet=Sorted%20by%20Retirement%20Date'}, # noqa: E501
diff --git a/bricktracker/instructions.py b/bricktracker/instructions.py
index 8f4b4fc..cc5cee7 100644
--- a/bricktracker/instructions.py
+++ b/bricktracker/instructions.py
@@ -1,21 +1,28 @@
from datetime import datetime, timezone
import logging
import os
-from typing import TYPE_CHECKING
+from shutil import copyfileobj
+import traceback
+from typing import Tuple, TYPE_CHECKING
+from bs4 import BeautifulSoup
from flask import current_app, g, url_for
import humanize
+import requests
from werkzeug.datastructures import FileStorage
from werkzeug.utils import secure_filename
-from .exceptions import ErrorException
+from .exceptions import ErrorException, DownloadException
if TYPE_CHECKING:
from .rebrickable_set import RebrickableSet
+ from .socket import BrickSocket
logger = logging.getLogger(__name__)
class BrickInstructions(object):
+ socket: 'BrickSocket'
+
allowed: bool
rebrickable: 'RebrickableSet | None'
extension: str
@@ -25,9 +32,22 @@ class BrickInstructions(object):
name: str
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):
self.filename = file
+
+ if self.filename == '':
+ raise ErrorException('An instruction filename cannot be empty')
else:
self.filename = file.name
@@ -67,6 +87,90 @@ class BrickInstructions(object):
def delete(self, /) -> None:
os.remove(self.path())
+ # Download an instruction file
+ def download(self, path: str, /) -> None:
+ try:
+ # Just to make sure that the progress is initiated
+ self.socket.progress(
+ message='Downloading {file}'.format(
+ file=self.filename,
+ )
+ )
+
+ target = self.path(filename=secure_filename(self.filename))
+
+ # Skipping rather than failing here
+ if os.path.isfile(target):
+ self.socket.complete(
+ message='File {file} already exists, skipped'.format(
+ file=self.filename,
+ )
+ )
+
+ else:
+ url = current_app.config['REBRICKABLE_LINK_INSTRUCTIONS_PATTERN'].format( # noqa: E501
+ path=path
+ )
+ trimmed_url = current_app.config['REBRICKABLE_LINK_INSTRUCTIONS_PATTERN'].format( # noqa: E501
+ path=path.partition('/')[0]
+ )
+
+ # Request the file
+ self.socket.progress(
+ 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
def human_size(self) -> str:
return humanize.naturalsize(self.size)
@@ -142,3 +246,44 @@ class BrickInstructions(object):
return 'file-image-line'
else:
return 'file-line'
+
+ # Find the instructions for a set
+ @staticmethod
+ def find_instructions(set: str, /) -> list[Tuple[str, str]]:
+ response = requests.get(
+ current_app.config['REBRICKABLE_LINK_INSTRUCTIONS_PATTERN'].format(
+ path=set,
+ ),
+ headers={
+ 'User-Agent': current_app.config['REBRICKABLE_USER_AGENT']
+ }
+ )
+
+ if not response.ok:
+ raise ErrorException('Failed to load the Rebrickable instructions page. Status code: {code}'.format( # noqa: E501
+ code=response.status_code
+ ))
+
+ # Parse the HTML content
+ soup = BeautifulSoup(response.content, 'html.parser')
+
+ # Collect all tags with "LEGO Building Instructions" in the
+ # alt attribute
+ found_tags: list[Tuple[str, str]] = []
+ for a_tag in soup.find_all('a', href=True):
+ img_tag = a_tag.find('img', alt=True)
+ if img_tag and "LEGO Building Instructions" in img_tag['alt']:
+ found_tags.append(
+ (
+ img_tag['alt'].removeprefix('LEGO Building Instructions for '), # noqa: E501
+ a_tag['href']
+ )
+ ) # Save alt and href
+
+ # Raise an error if nothing found
+ if not len(found_tags):
+ raise ErrorException('No instruction found for set {set}'.format(
+ set=set
+ ))
+
+ return found_tags
diff --git a/bricktracker/parser.py b/bricktracker/parser.py
new file mode 100644
index 0000000..d3602e2
--- /dev/null
+++ b/bricktracker/parser.py
@@ -0,0 +1,37 @@
+from .exceptions import ErrorException
+
+
+# Make sense of string supposed to contain a set ID
+def parse_set(set: str, /) -> str:
+ number, _, version = set.partition('-')
+
+ # Making sure both are integers
+ if version == '':
+ version = 1
+
+ try:
+ number = int(number)
+ except Exception:
+ raise ErrorException('Number "{number}" is not a number'.format(
+ number=number,
+ ))
+
+ try:
+ version = int(version)
+ except Exception:
+ raise ErrorException('Version "{version}" is not a number'.format(
+ version=version,
+ ))
+
+ # Make sure both are positive
+ if number < 0:
+ raise ErrorException('Number "{number}" should be positive'.format(
+ number=number,
+ ))
+
+ if version < 0:
+ raise ErrorException('Version "{version}" should be positive'.format( # noqa: E501
+ version=version,
+ ))
+
+ return '{number}-{version}'.format(number=number, version=version)
diff --git a/bricktracker/rebrickable_set.py b/bricktracker/rebrickable_set.py
index 37e26b3..5a1c41f 100644
--- a/bricktracker/rebrickable_set.py
+++ b/bricktracker/rebrickable_set.py
@@ -7,6 +7,7 @@ from flask import current_app
from .exceptions import ErrorException, NotFoundException
from .instructions import BrickInstructions
+from .parser import parse_set
from .rebrickable import Rebrickable
from .rebrickable_image import RebrickableImage
from .record import BrickRecord
@@ -98,7 +99,7 @@ class RebrickableSet(BrickRecord):
try:
self.socket.auto_progress(message='Parsing set number')
- set = RebrickableSet.parse_number(str(data['set']))
+ set = parse_set(str(data['set']))
self.socket.auto_progress(
message='Set {set}: loading from Rebrickable'.format(
@@ -187,39 +188,3 @@ class RebrickableSet(BrickRecord):
'url': str(data['set_url']),
'last_modified': str(data['last_modified_dt']),
}
-
- # Make sense of the number from the data
- @staticmethod
- def parse_number(set: str, /) -> str:
- number, _, version = set.partition('-')
-
- # Making sure both are integers
- if version == '':
- version = 1
-
- try:
- number = int(number)
- except Exception:
- raise ErrorException('Number "{number}" is not a number'.format(
- number=number,
- ))
-
- try:
- version = int(version)
- except Exception:
- raise ErrorException('Version "{version}" is not a number'.format(
- version=version,
- ))
-
- # Make sure both are positive
- if number < 0:
- raise ErrorException('Number "{number}" should be positive'.format(
- number=number,
- ))
-
- if version < 0:
- raise ErrorException('Version "{version}" should be positive'.format( # noqa: E501
- version=version,
- ))
-
- return '{number}-{version}'.format(number=number, version=version)
diff --git a/bricktracker/socket.py b/bricktracker/socket.py
index 4351592..c7215ae 100644
--- a/bricktracker/socket.py
+++ b/bricktracker/socket.py
@@ -5,6 +5,8 @@ from flask import copy_current_request_context, Flask, request
from flask_socketio import SocketIO
from .configuration_list import BrickConfigurationList
+from .instructions import BrickInstructions
+from .instructions_list import BrickInstructionsList
from .login import LoginManager
from .set import BrickSet
from .sql import close as sql_close
@@ -13,10 +15,10 @@ logger = logging.getLogger(__name__)
# Messages valid through the socket
MESSAGES: Final[dict[str, str]] = {
- 'ADD_SET': 'add_set',
'COMPLETE': 'complete',
'CONNECT': 'connect',
'DISCONNECT': 'disconnect',
+ 'DOWNLOAD_INSTRUCTIONS': 'download_instructions',
'FAIL': 'fail',
'IMPORT_SET': 'import_set',
'LOAD_SET': 'load_set',
@@ -84,6 +86,41 @@ class BrickSocket(object):
def disconnect() -> None:
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)
def import_set(data: dict[str, Any], /) -> None:
# Needs to be authenticated
diff --git a/bricktracker/views/instructions.py b/bricktracker/views/instructions.py
index 047e961..2c2138a 100644
--- a/bricktracker/views/instructions.py
+++ b/bricktracker/views/instructions.py
@@ -13,6 +13,8 @@ from werkzeug.utils import secure_filename
from .exceptions import exception_handler
from ..instructions import BrickInstructions
from ..instructions_list import BrickInstructionsList
+from ..parser import parse_set
+from ..socket import MESSAGES
from .upload import upload_helper
instructions_page = Blueprint(
@@ -126,3 +128,44 @@ def do_upload() -> Response:
BrickInstructionsList(force=True)
return redirect(url_for('instructions.list'))
+
+
+# Download instructions from Rebrickable
+@instructions_page.route('/download/', methods=['GET'])
+@login_required
+@exception_handler(__file__)
+def download() -> str:
+ # Grab the set number
+ try:
+ set = parse_set(request.args.get('set', ''))
+ except Exception:
+ set = ''
+
+ return render_template(
+ 'instructions.html',
+ download=True,
+ error=request.args.get('error'),
+ set=set
+ )
+
+
+# Show search results
+@instructions_page.route('/download', methods=['POST'])
+@login_required
+@exception_handler(__file__, post_redirect='instructions.download')
+def do_download() -> str:
+ # Grab the set number
+ try:
+ set = parse_set(request.form.get('download-set', ''))
+ except Exception:
+ set = ''
+
+ return render_template(
+ 'instructions.html',
+ download=True,
+ instructions=BrickInstructions.find_instructions(set),
+ set=set,
+ path=current_app.config['SOCKET_PATH'],
+ namespace=current_app.config['SOCKET_NAMESPACE'],
+ messages=MESSAGES
+ )
diff --git a/bricktracker/wish_list.py b/bricktracker/wish_list.py
index dfba800..880021b 100644
--- a/bricktracker/wish_list.py
+++ b/bricktracker/wish_list.py
@@ -4,9 +4,9 @@ from typing import Self
from flask import current_app
from .exceptions import NotFoundException
+from .parser import parse_set
from .rebrickable import Rebrickable
from .rebrickable_image import RebrickableImage
-from .rebrickable_set import RebrickableSet
from .record_list import BrickRecordList
from .wish import BrickWish
@@ -34,7 +34,7 @@ class BrickWishList(BrickRecordList[BrickWish]):
@staticmethod
def add(set: str, /) -> None:
try:
- set = RebrickableSet.parse_number(set)
+ set = parse_set(set)
BrickWish().select_specific(set)
except NotFoundException:
logger.debug('rebrick.lego.get_set("{set}")'.format(
diff --git a/requirements.txt b/requirements.txt
index aedd691..d2ca909 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -7,4 +7,5 @@ humanize
jinja2
rebrick
requests
-tzdata
\ No newline at end of file
+tzdata
+bs4
diff --git a/static/scripts/socket/instructions.js b/static/scripts/socket/instructions.js
new file mode 100644
index 0000000..271c0c3
--- /dev/null
+++ b/static/scripts/socket/instructions.js
@@ -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;
+ }
+ }
+}
diff --git a/static/scripts/socket/set.js b/static/scripts/socket/set.js
new file mode 100644
index 0000000..60a2244
--- /dev/null
+++ b/static/scripts/socket/set.js
@@ -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;
+ }
+ }
+}
diff --git a/static/scripts/socket.js b/static/scripts/socket/socket.js
similarity index 54%
rename from static/scripts/socket.js
rename to static/scripts/socket/socket.js
index 5a24d06..cb08889 100644
--- a/static/scripts/socket.js
+++ b/static/scripts/socket/socket.js
@@ -5,22 +5,17 @@ class BrickSocket {
this.path = path;
this.namespace = namespace;
this.messages = messages;
- this.bulk = bulk;
this.disabled = false;
this.socket = undefined;
- // Listeners
- this.add_listener = undefined;
- this.confirm_listener = undefined;
+ // Bulk mode
+ this.bulk = bulk;
// Form elements (built based on the initial id)
- this.html_button = document.getElementById(id);
this.html_complete = document.getElementById(`${id}-complete`);
this.html_count = document.getElementById(`${id}-count`);
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_bar = document.getElementById(`${id}-progress-bar`);
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_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
window.setInterval(((bricksocket) => () => {
bricksocket.status();
- })(this), 500);
-
- // Setup the socket
- this.setup();
+ })(this), 1000);
}
// Clear form
@@ -88,19 +43,9 @@ class BrickSocket {
this.html_progress_bar.textContent = "";
}
+ this.progress_message("");
+
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
@@ -141,9 +86,6 @@ class BrickSocket {
this.html_complete.append(success)
}
-
- // Import the next set
- this.import_set(true, undefined, true);
} else {
this.spinner(false);
@@ -188,73 +130,8 @@ class BrickSocket {
if (this.html_progress_bar) {
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
progress(data={}) {
@@ -262,7 +139,7 @@ class BrickSocket {
let count = data["count"]
// Fix the total if bogus
- if (!total || isNaN(total) || total <= 1) {
+ if (!total || isNaN(total) || 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() {
if (this.socket === undefined) {
@@ -387,11 +207,6 @@ class BrickSocket {
this.socket.on(this.messages.PROGRESS, ((bricksocket) => (data) => {
bricksocket.progress(data);
})(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(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;
- }
}
}
diff --git a/templates/base.html b/templates/base.html
index 1132f2b..6d89d3d 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -82,7 +82,9 @@
-
+
+
+
diff --git a/templates/set/card.html b/templates/set/card.html
index 582ea43..9fcea3d 100644
--- a/templates/set/card.html
+++ b/templates/set/card.html
@@ -50,6 +50,9 @@
Upload an instructions file
{% endif %}
{% endif %}
+ {% if g.login.is_authenticated() %}
+ Download instructions from Rebrickable
+ {% endif %}
{{ accordion.footer() }}
{{ accordion.table(item.parts(), 'Parts', 'parts-inventory', 'set-details', 'part/table.html', icon='shapes-line')}}
diff --git a/templates/set/socket.html b/templates/set/socket.html
index e2718eb..a566a95 100644
--- a/templates/set/socket.html
+++ b/templates/set/socket.html
@@ -1,6 +1,6 @@