forked from viewit/KX-Bridge-Release
fix: isolate filament profiles per printer in multi-printer bridge (#74)
Per-printer [filament_profiles_<id>] sections so configuring one printer no longer overwrites another (read-fallback to the legacy global section keeps single-printer setups unchanged). Dropdown/switch links now navigate to each printer's own bridge_url. Adds pytest coverage and a CHANGELOG entry. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
16
CHANGELOG.md
16
CHANGELOG.md
@ -1,5 +1,21 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Filament profiles not isolated between printers in a multi-printer bridge**
|
||||||
|
(issue #74). The slot→profile mapping and `visible_vendors` were stored in a
|
||||||
|
single global `[filament_profiles]` section, so configuring one printer
|
||||||
|
overwrote the other and after a restart both loaded the same mapping. Each
|
||||||
|
printer now persists to its own `[filament_profiles_<id>]` section, with a
|
||||||
|
read-fallback to the legacy global section (single-printer setups unchanged).
|
||||||
|
- **Printer dropdown showed the other printer's filament profiles** (issue #74).
|
||||||
|
The header dropdown and the printers-management "switch" link navigated within
|
||||||
|
the same port (`/printerN`), so viewing another printer pulled its profile
|
||||||
|
names cross-instance from the local origin. The links now point at each
|
||||||
|
printer's own `bridge_url`, so every printer is viewed same-origin on its own
|
||||||
|
port.
|
||||||
|
|
||||||
## [0.9.26] – 2026-06-21
|
## [0.9.26] – 2026-06-21
|
||||||
|
|
||||||
### New
|
### New
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
import pathlib
|
import pathlib
|
||||||
import configparser
|
import configparser
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
_BASE = pathlib.Path(sys.executable).parent if getattr(sys, "frozen", False) else pathlib.Path(__file__).parent
|
_BASE = pathlib.Path(sys.executable).parent if getattr(sys, "frozen", False) else pathlib.Path(__file__).parent
|
||||||
|
|
||||||
@ -182,9 +183,27 @@ def list_printers() -> list[dict]:
|
|||||||
return printers
|
return printers
|
||||||
|
|
||||||
|
|
||||||
def list_filament_profiles() -> dict[int, dict]:
|
def _filament_section(printer_id: Optional[str] = None) -> str:
|
||||||
|
"""Section name holding a printer's filament-profile mapping.
|
||||||
|
|
||||||
|
Multi-printer (one bridge, N printers): each printer keeps its own
|
||||||
|
``[filament_profiles_<id>]`` section so the mappings cannot overwrite each
|
||||||
|
other. ``printer_id is None`` (single-printer / legacy callers) maps to the
|
||||||
|
original global ``[filament_profiles]`` section — full backward compatibility.
|
||||||
|
"""
|
||||||
|
pid = str(printer_id).strip() if printer_id is not None else ""
|
||||||
|
if pid and pid != "0":
|
||||||
|
return f"filament_profiles_{pid}"
|
||||||
|
return "filament_profiles"
|
||||||
|
|
||||||
|
|
||||||
|
def list_filament_profiles(printer_id: Optional[str] = None) -> dict[int, dict]:
|
||||||
"""Liest die [filament_profiles]-Sektion aus config.ini.
|
"""Liest die [filament_profiles]-Sektion aus config.ini.
|
||||||
|
|
||||||
|
With ``printer_id`` set, reads the per-printer ``[filament_profiles_<id>]``
|
||||||
|
section and falls back to the legacy global ``[filament_profiles]`` while
|
||||||
|
that printer has no own section yet.
|
||||||
|
|
||||||
Format pro AMS-Slot — primärer Selector ist (vendor, name), die `id` wird
|
Format pro AMS-Slot — primärer Selector ist (vendor, name), die `id` wird
|
||||||
aus der orca_filaments.json beim Speichern nachgeschlagen und mitgeführt
|
aus der orca_filaments.json beim Speichern nachgeschlagen und mitgeführt
|
||||||
(als Hint für OrcaSlicer; das Orca-Datenmodell hat ~136 Profile mit
|
(als Hint für OrcaSlicer; das Orca-Datenmodell hat ~136 Profile mit
|
||||||
@ -208,10 +227,13 @@ def list_filament_profiles() -> dict[int, dict]:
|
|||||||
return {}
|
return {}
|
||||||
cfg = configparser.ConfigParser()
|
cfg = configparser.ConfigParser()
|
||||||
cfg.read(path, encoding="utf-8")
|
cfg.read(path, encoding="utf-8")
|
||||||
if not cfg.has_section("filament_profiles"):
|
section = _filament_section(printer_id)
|
||||||
|
if not cfg.has_section(section):
|
||||||
|
section = "filament_profiles" # fallback: legacy global section
|
||||||
|
if not cfg.has_section(section):
|
||||||
return {}
|
return {}
|
||||||
result: dict[int, dict] = {}
|
result: dict[int, dict] = {}
|
||||||
for key, value in cfg.items("filament_profiles"):
|
for key, value in cfg.items(section):
|
||||||
# Erwartet: slot_<idx>_id oder slot_<idx>_vendor oder slot_<idx>_name
|
# Erwartet: slot_<idx>_id oder slot_<idx>_vendor oder slot_<idx>_name
|
||||||
if not key.startswith("slot_"):
|
if not key.startswith("slot_"):
|
||||||
continue
|
continue
|
||||||
@ -231,74 +253,97 @@ def list_filament_profiles() -> dict[int, dict]:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def save_filament_profiles(profiles: dict[int, dict]) -> bool:
|
def save_filament_profiles(profiles: dict[int, dict], printer_id: Optional[str] = None) -> bool:
|
||||||
"""Schreibt die übergebenen Slot-Profile in die [filament_profiles]-
|
"""Schreibt die übergebenen Slot-Profile in die [filament_profiles]-
|
||||||
Sektion der config.ini. Existierende Einträge werden komplett ersetzt.
|
Sektion der config.ini. Existierende Einträge werden komplett ersetzt.
|
||||||
|
|
||||||
profiles: {slot_index: {"id": "OGFL01", "vendor": "Polymaker", "name": "PolyTerra PLA"}}
|
profiles: {slot_index: {"id": "OGFL01", "vendor": "Polymaker", "name": "PolyTerra PLA"}}
|
||||||
Mindestens vendor+name müssen gesetzt sein; id ist optional (Hint).
|
Mindestens vendor+name müssen gesetzt sein; id ist optional (Hint).
|
||||||
|
|
||||||
|
With ``printer_id`` set, writes the per-printer ``[filament_profiles_<id>]``
|
||||||
|
section only — other printers and the legacy global section are untouched.
|
||||||
"""
|
"""
|
||||||
path = _find_config_file()
|
path = _find_config_file()
|
||||||
if not path:
|
if not path:
|
||||||
return False
|
return False
|
||||||
cfg = configparser.ConfigParser()
|
cfg = configparser.ConfigParser()
|
||||||
cfg.read(path, encoding="utf-8")
|
cfg.read(path, encoding="utf-8")
|
||||||
|
section = _filament_section(printer_id)
|
||||||
# visible_vendors (Issue #41) ist kein Slot-Mapping — beim Ersetzen der
|
# visible_vendors (Issue #41) ist kein Slot-Mapping — beim Ersetzen der
|
||||||
# Sektion erhalten, sonst geht der Vendor-Filter beim Slot-Save verloren.
|
# Sektion erhalten, sonst geht der Vendor-Filter beim Slot-Save verloren.
|
||||||
|
# First save of a per-printer section inherits the legacy global filter.
|
||||||
preserved_vendors = None
|
preserved_vendors = None
|
||||||
if cfg.has_option("filament_profiles", "visible_vendors"):
|
if cfg.has_option(section, "visible_vendors"):
|
||||||
|
preserved_vendors = cfg.get(section, "visible_vendors")
|
||||||
|
elif cfg.has_option("filament_profiles", "visible_vendors"):
|
||||||
preserved_vendors = cfg.get("filament_profiles", "visible_vendors")
|
preserved_vendors = cfg.get("filament_profiles", "visible_vendors")
|
||||||
if cfg.has_section("filament_profiles"):
|
if cfg.has_section(section):
|
||||||
cfg.remove_section("filament_profiles")
|
cfg.remove_section(section)
|
||||||
if profiles or preserved_vendors:
|
if profiles or preserved_vendors:
|
||||||
cfg["filament_profiles"] = {}
|
cfg[section] = {}
|
||||||
if preserved_vendors:
|
if preserved_vendors:
|
||||||
cfg["filament_profiles"]["visible_vendors"] = preserved_vendors
|
cfg[section]["visible_vendors"] = preserved_vendors
|
||||||
for slot_idx in sorted(profiles.keys()):
|
for slot_idx in sorted(profiles.keys()):
|
||||||
entry = profiles[slot_idx] or {}
|
entry = profiles[slot_idx] or {}
|
||||||
if entry.get("vendor"):
|
if entry.get("vendor"):
|
||||||
cfg["filament_profiles"][f"slot_{slot_idx}_vendor"] = entry["vendor"]
|
cfg[section][f"slot_{slot_idx}_vendor"] = entry["vendor"]
|
||||||
if entry.get("name"):
|
if entry.get("name"):
|
||||||
cfg["filament_profiles"][f"slot_{slot_idx}_name"] = entry["name"]
|
cfg[section][f"slot_{slot_idx}_name"] = entry["name"]
|
||||||
if entry.get("id"):
|
if entry.get("id"):
|
||||||
cfg["filament_profiles"][f"slot_{slot_idx}_id"] = entry["id"]
|
cfg[section][f"slot_{slot_idx}_id"] = entry["id"]
|
||||||
with open(path, "w", encoding="utf-8") as f:
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
cfg.write(f)
|
cfg.write(f)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def list_visible_vendors() -> list[str]:
|
def list_visible_vendors(printer_id: Optional[str] = None) -> list[str]:
|
||||||
"""Liest [filament_profiles] visible_vendors (komma-separiert) aus config.ini.
|
"""Liest [filament_profiles] visible_vendors (komma-separiert) aus config.ini.
|
||||||
|
|
||||||
Vendor-Sichtbarkeitsfilter für das Slot-Profil-Dropdown (Issue #41 Option A).
|
Vendor-Sichtbarkeitsfilter für das Slot-Profil-Dropdown (Issue #41 Option A).
|
||||||
Leere Liste = keine Einschränkung (rückwärtskompatibel: alle Vendoren).
|
Leere Liste = keine Einschränkung (rückwärtskompatibel: alle Vendoren).
|
||||||
|
|
||||||
|
With ``printer_id`` set, reads the per-printer section and falls back to the
|
||||||
|
legacy global ``[filament_profiles]`` filter.
|
||||||
"""
|
"""
|
||||||
path = _find_config_file()
|
path = _find_config_file()
|
||||||
if not path:
|
if not path:
|
||||||
return []
|
return []
|
||||||
cfg = configparser.ConfigParser()
|
cfg = configparser.ConfigParser()
|
||||||
cfg.read(path, encoding="utf-8")
|
cfg.read(path, encoding="utf-8")
|
||||||
if not cfg.has_option("filament_profiles", "visible_vendors"):
|
section = _filament_section(printer_id)
|
||||||
|
if not cfg.has_option(section, "visible_vendors"):
|
||||||
|
section = "filament_profiles" # fallback: legacy global section
|
||||||
|
if not cfg.has_option(section, "visible_vendors"):
|
||||||
return []
|
return []
|
||||||
raw = cfg.get("filament_profiles", "visible_vendors")
|
raw = cfg.get(section, "visible_vendors")
|
||||||
return [v.strip() for v in raw.split(",") if v.strip()]
|
return [v.strip() for v in raw.split(",") if v.strip()]
|
||||||
|
|
||||||
|
|
||||||
def save_visible_vendors(vendors: list[str]) -> bool:
|
def save_visible_vendors(vendors: list[str], printer_id: Optional[str] = None) -> bool:
|
||||||
"""Schreibt visible_vendors in [filament_profiles], ohne die Slot-Mappings
|
"""Schreibt visible_vendors in [filament_profiles], ohne die Slot-Mappings
|
||||||
(slot_N_*) zu verlieren. Leere Liste entfernt den Key wieder."""
|
(slot_N_*) zu verlieren. Leere Liste entfernt den Key wieder.
|
||||||
|
|
||||||
|
With ``printer_id`` set, writes the per-printer section. When that section is
|
||||||
|
created here for the first time, the slot mappings are seeded from the legacy
|
||||||
|
global section so they are not orphaned by the read-fallback in
|
||||||
|
``list_filament_profiles``."""
|
||||||
path = _find_config_file()
|
path = _find_config_file()
|
||||||
if not path:
|
if not path:
|
||||||
return False
|
return False
|
||||||
cfg = configparser.ConfigParser()
|
cfg = configparser.ConfigParser()
|
||||||
cfg.read(path, encoding="utf-8")
|
cfg.read(path, encoding="utf-8")
|
||||||
if not cfg.has_section("filament_profiles"):
|
section = _filament_section(printer_id)
|
||||||
cfg.add_section("filament_profiles")
|
if not cfg.has_section(section):
|
||||||
|
cfg.add_section(section)
|
||||||
|
if section != "filament_profiles" and cfg.has_section("filament_profiles"):
|
||||||
|
for key, value in cfg.items("filament_profiles"):
|
||||||
|
if key.startswith("slot_"):
|
||||||
|
cfg[section][key] = value
|
||||||
clean = [v.strip() for v in (vendors or []) if v and v.strip()]
|
clean = [v.strip() for v in (vendors or []) if v and v.strip()]
|
||||||
if clean:
|
if clean:
|
||||||
cfg["filament_profiles"]["visible_vendors"] = ", ".join(clean)
|
cfg[section]["visible_vendors"] = ", ".join(clean)
|
||||||
elif cfg.has_option("filament_profiles", "visible_vendors"):
|
elif cfg.has_option(section, "visible_vendors"):
|
||||||
cfg.remove_option("filament_profiles", "visible_vendors")
|
cfg.remove_option(section, "visible_vendors")
|
||||||
with open(path, "w", encoding="utf-8") as f:
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
cfg.write(f)
|
cfg.write(f)
|
||||||
return True
|
return True
|
||||||
|
|||||||
@ -828,14 +828,14 @@ class KobraXBridge:
|
|||||||
# Marke ("PolyTerra PLA — Polymaker") statt nur "Generic PLA" anzeigt.
|
# Marke ("PolyTerra PLA — Polymaker") statt nur "Generic PLA" anzeigt.
|
||||||
try:
|
try:
|
||||||
import config_loader as _cl
|
import config_loader as _cl
|
||||||
self._filament_profiles: dict[int, dict] = _cl.list_filament_profiles()
|
self._filament_profiles: dict[int, dict] = _cl.list_filament_profiles(self._printer_id)
|
||||||
except Exception:
|
except Exception:
|
||||||
self._filament_profiles = {}
|
self._filament_profiles = {}
|
||||||
# Vendor-Sichtbarkeitsfilter fürs Slot-Profil-Dropdown (Issue #41 Option A).
|
# Vendor-Sichtbarkeitsfilter fürs Slot-Profil-Dropdown (Issue #41 Option A).
|
||||||
# Leere Liste = alle Vendoren sichtbar (rückwärtskompatibel).
|
# Leere Liste = alle Vendoren sichtbar (rückwärtskompatibel).
|
||||||
try:
|
try:
|
||||||
import config_loader as _cl
|
import config_loader as _cl
|
||||||
self._visible_vendors: list[str] = _cl.list_visible_vendors()
|
self._visible_vendors: list[str] = _cl.list_visible_vendors(self._printer_id)
|
||||||
except Exception:
|
except Exception:
|
||||||
self._visible_vendors = []
|
self._visible_vendors = []
|
||||||
self._last_state: dict = {}
|
self._last_state: dict = {}
|
||||||
@ -2601,7 +2601,7 @@ class KobraXBridge:
|
|||||||
# Persistieren in config.ini
|
# Persistieren in config.ini
|
||||||
try:
|
try:
|
||||||
import config_loader as _cl
|
import config_loader as _cl
|
||||||
_cl.save_filament_profiles(self._filament_profiles)
|
_cl.save_filament_profiles(self._filament_profiles, self._printer_id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.warning(f"save_filament_profiles failed: {e}")
|
log.warning(f"save_filament_profiles failed: {e}")
|
||||||
return self._json_cors({"error": str(e)}, status=500)
|
return self._json_cors({"error": str(e)}, status=500)
|
||||||
@ -2631,7 +2631,7 @@ class KobraXBridge:
|
|||||||
self._visible_vendors = [str(v).strip() for v in vendors if str(v).strip()]
|
self._visible_vendors = [str(v).strip() for v in vendors if str(v).strip()]
|
||||||
try:
|
try:
|
||||||
import config_loader as _cl
|
import config_loader as _cl
|
||||||
_cl.save_visible_vendors(self._visible_vendors)
|
_cl.save_visible_vendors(self._visible_vendors, self._printer_id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.warning(f"save_visible_vendors failed: {e}")
|
log.warning(f"save_visible_vendors failed: {e}")
|
||||||
return self._json_cors({"error": str(e)}, status=500)
|
return self._json_cors({"error": str(e)}, status=500)
|
||||||
|
|||||||
67
tests/test_filament_profiles_per_printer.py
Normal file
67
tests/test_filament_profiles_per_printer.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
"""Per-printer filament-profile isolation (config_loader).
|
||||||
|
|
||||||
|
Regression test for the multi-printer bug (issue #74): the slot->profile mapping
|
||||||
|
and ``visible_vendors`` lived in a single global ``[filament_profiles]`` section,
|
||||||
|
so configuring one printer overwrote the other and after a restart both loaded
|
||||||
|
the same map. Each printer now uses its own ``[filament_profiles_<id>]`` section,
|
||||||
|
with a read-fallback to the legacy global section for backward compatibility.
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent)) # repo root
|
||||||
|
import config_loader # noqa: E402
|
||||||
|
|
||||||
|
BASE_INI = (
|
||||||
|
"[printer_1]\nname = K1\n\n"
|
||||||
|
"[printer_2]\nname = K2\n\n"
|
||||||
|
"[filament_profiles]\n"
|
||||||
|
"visible_vendors = Anycubic, SUNLU\n"
|
||||||
|
"slot_0_vendor = Anycubic\nslot_0_name = Anycubic PLA+\nslot_0_id = GFPLA+\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _use_ini(monkeypatch, tmp_path, text=BASE_INI):
|
||||||
|
path = tmp_path / "config.ini"
|
||||||
|
path.write_text(text, encoding="utf-8")
|
||||||
|
monkeypatch.setattr(config_loader, "_find_config_file", lambda: path)
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def test_legacy_global_still_works(tmp_path, monkeypatch):
|
||||||
|
"""No printer_id -> original global section (single-printer back-compat)."""
|
||||||
|
_use_ini(monkeypatch, tmp_path)
|
||||||
|
assert config_loader.list_filament_profiles()[0]["name"] == "Anycubic PLA+"
|
||||||
|
assert config_loader.list_visible_vendors() == ["Anycubic", "SUNLU"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_falls_back_to_global_until_first_save(tmp_path, monkeypatch):
|
||||||
|
"""Before any per-printer save, both printers see the global mapping."""
|
||||||
|
_use_ini(monkeypatch, tmp_path)
|
||||||
|
assert config_loader.list_filament_profiles("1")[0]["name"] == "Anycubic PLA+"
|
||||||
|
assert config_loader.list_filament_profiles("2")[0]["name"] == "Anycubic PLA+"
|
||||||
|
|
||||||
|
|
||||||
|
def test_saving_one_printer_does_not_touch_the_other(tmp_path, monkeypatch):
|
||||||
|
"""Core regression: configuring printer 1 must not change printer 2."""
|
||||||
|
_use_ini(monkeypatch, tmp_path)
|
||||||
|
config_loader.save_filament_profiles(
|
||||||
|
{0: {"vendor": "KINGROON", "name": "KINGROON PLA Basic", "id": "Pc0b8a01"}}, "1")
|
||||||
|
assert config_loader.list_filament_profiles("1")[0]["name"] == "KINGROON PLA Basic"
|
||||||
|
assert config_loader.list_filament_profiles("2")[0]["name"] == "Anycubic PLA+"
|
||||||
|
# legacy global section preserved untouched
|
||||||
|
assert config_loader.list_filament_profiles()[0]["name"] == "Anycubic PLA+"
|
||||||
|
|
||||||
|
|
||||||
|
def test_visible_vendors_isolated_per_printer(tmp_path, monkeypatch):
|
||||||
|
_use_ini(monkeypatch, tmp_path)
|
||||||
|
config_loader.save_visible_vendors(["KINGROON"], "1")
|
||||||
|
assert config_loader.list_visible_vendors("1") == ["KINGROON"]
|
||||||
|
assert config_loader.list_visible_vendors("2") == ["Anycubic", "SUNLU"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_visible_vendors_keeps_slot_fallback(tmp_path, monkeypatch):
|
||||||
|
"""Creating a per-printer section only for vendors must not orphan slots."""
|
||||||
|
_use_ini(monkeypatch, tmp_path)
|
||||||
|
config_loader.save_visible_vendors(["KINGROON"], "1")
|
||||||
|
assert config_loader.list_filament_profiles("1")[0]["name"] == "Anycubic PLA+"
|
||||||
@ -292,7 +292,7 @@ function renderPrinterDropdown(){
|
|||||||
menu.innerHTML=_printers.map(function(p){
|
menu.innerHTML=_printers.map(function(p){
|
||||||
var active=_activePrinter&&String(p.id)===String(_activePrinter.id);
|
var active=_activePrinter&&String(p.id)===String(_activePrinter.id);
|
||||||
var num=p.id;
|
var num=p.id;
|
||||||
return '<a href="/printer'+num+'" style="display:block;padding:10px 14px;color:'+(active?'var(--accent)':'var(--txt)')+';text-decoration:none;font-size:13px;border-bottom:1px solid var(--border)" '+(active?'style="font-weight:600"':'')+'>'+
|
return '<a href="'+((p.bridge_url||'').replace(/\/+$/,''))+'/printer'+num+'" style="display:block;padding:10px 14px;color:'+(active?'var(--accent)':'var(--txt)')+';text-decoration:none;font-size:13px;border-bottom:1px solid var(--border)" '+(active?'style="font-weight:600"':'')+'>'+
|
||||||
(active?'▶ ':'')+p.name+'</a>';
|
(active?'▶ ':'')+p.name+'</a>';
|
||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
@ -3243,7 +3243,7 @@ function loadPrinterTab(){
|
|||||||
'<div style="font-size:12px;color:var(--txt2);display:flex;gap:12px">'+
|
'<div style="font-size:12px;color:var(--txt2);display:flex;gap:12px">'+
|
||||||
'<span>🌡 '+nt+'°C</span><span>🛏 '+bt+'°C</span>'+
|
'<span>🌡 '+nt+'°C</span><span>🛏 '+bt+'°C</span>'+
|
||||||
'</div>'+
|
'</div>'+
|
||||||
(!isActive?'<a href="/printer'+printerNum+'" style="display:block;text-align:center;padding:7px;background:var(--accent);color:#fff;border-radius:7px;font-size:13px;font-weight:600;text-decoration:none;margin-top:4px">'+T.printers_switch+'</a>':'<div style="text-align:center;padding:7px;font-size:12px;color:var(--txt2)">'+T.printers_current+'</div>')+
|
(!isActive?'<a href="'+url+'/printer'+printerNum+'" style="display:block;text-align:center;padding:7px;background:var(--accent);color:#fff;border-radius:7px;font-size:13px;font-weight:600;text-decoration:none;margin-top:4px">'+T.printers_switch+'</a>':'<div style="text-align:center;padding:7px;font-size:12px;color:var(--txt2)">'+T.printers_current+'</div>')+
|
||||||
'</div>';
|
'</div>';
|
||||||
}).join('');
|
}).join('');
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user