|
|
|
|
@@ -2940,6 +2940,257 @@ class Silo_Auth:
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Start panel
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SiloStartPanel:
|
|
|
|
|
"""Content widget for the Silo Start Panel dock."""
|
|
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
from PySide import QtCore, QtGui
|
|
|
|
|
|
|
|
|
|
self.widget = QtGui.QWidget()
|
|
|
|
|
self._build_ui()
|
|
|
|
|
self._refresh()
|
|
|
|
|
|
|
|
|
|
self._timer = QtCore.QTimer(self.widget)
|
|
|
|
|
self._timer.timeout.connect(self._refresh)
|
|
|
|
|
self._timer.start(60000) # Refresh every 60 seconds
|
|
|
|
|
|
|
|
|
|
def _build_ui(self):
|
|
|
|
|
from PySide import QtCore, QtGui
|
|
|
|
|
|
|
|
|
|
layout = QtGui.QVBoxLayout(self.widget)
|
|
|
|
|
layout.setContentsMargins(8, 8, 8, 8)
|
|
|
|
|
layout.setSpacing(6)
|
|
|
|
|
|
|
|
|
|
# Connection status badge
|
|
|
|
|
status_row = QtGui.QHBoxLayout()
|
|
|
|
|
status_row.setSpacing(6)
|
|
|
|
|
self._conn_dot = QtGui.QLabel("\u2b24")
|
|
|
|
|
self._conn_dot.setFixedWidth(16)
|
|
|
|
|
self._conn_dot.setAlignment(QtCore.Qt.AlignCenter)
|
|
|
|
|
self._conn_label = QtGui.QLabel("Checking...")
|
|
|
|
|
self._conn_label.setStyleSheet("font-weight: bold;")
|
|
|
|
|
status_row.addWidget(self._conn_dot)
|
|
|
|
|
status_row.addWidget(self._conn_label)
|
|
|
|
|
status_row.addStretch()
|
|
|
|
|
layout.addLayout(status_row)
|
|
|
|
|
|
|
|
|
|
layout.addSpacing(8)
|
|
|
|
|
|
|
|
|
|
# My Checkouts section
|
|
|
|
|
checkout_header = QtGui.QLabel("My Checkouts")
|
|
|
|
|
checkout_header.setStyleSheet("font-weight: bold; font-size: 12px;")
|
|
|
|
|
layout.addWidget(checkout_header)
|
|
|
|
|
|
|
|
|
|
self._checkout_list = QtGui.QListWidget()
|
|
|
|
|
self._checkout_list.setMaximumHeight(160)
|
|
|
|
|
self._checkout_list.setAlternatingRowColors(True)
|
|
|
|
|
self._checkout_list.itemDoubleClicked.connect(self._on_checkout_clicked)
|
|
|
|
|
layout.addWidget(self._checkout_list)
|
|
|
|
|
|
|
|
|
|
layout.addSpacing(8)
|
|
|
|
|
|
|
|
|
|
# Recent Silo Activity section
|
|
|
|
|
activity_header = QtGui.QLabel("Recent Silo Activity")
|
|
|
|
|
activity_header.setStyleSheet("font-weight: bold; font-size: 12px;")
|
|
|
|
|
layout.addWidget(activity_header)
|
|
|
|
|
|
|
|
|
|
self._activity_list = QtGui.QListWidget()
|
|
|
|
|
self._activity_list.setMaximumHeight(200)
|
|
|
|
|
self._activity_list.setAlternatingRowColors(True)
|
|
|
|
|
self._activity_list.itemDoubleClicked.connect(self._on_activity_clicked)
|
|
|
|
|
layout.addWidget(self._activity_list)
|
|
|
|
|
|
|
|
|
|
layout.addSpacing(8)
|
|
|
|
|
|
|
|
|
|
# Local Recent Files section
|
|
|
|
|
local_header = QtGui.QLabel("Local Recent Files")
|
|
|
|
|
local_header.setStyleSheet("font-weight: bold; font-size: 12px;")
|
|
|
|
|
layout.addWidget(local_header)
|
|
|
|
|
|
|
|
|
|
self._local_list = QtGui.QListWidget()
|
|
|
|
|
self._local_list.setMaximumHeight(160)
|
|
|
|
|
self._local_list.setAlternatingRowColors(True)
|
|
|
|
|
self._local_list.itemDoubleClicked.connect(self._on_local_clicked)
|
|
|
|
|
layout.addWidget(self._local_list)
|
|
|
|
|
|
|
|
|
|
layout.addStretch()
|
|
|
|
|
|
|
|
|
|
def _refresh(self):
|
|
|
|
|
self._refresh_connection()
|
|
|
|
|
self._refresh_checkouts()
|
|
|
|
|
self._refresh_activity()
|
|
|
|
|
self._refresh_local()
|
|
|
|
|
|
|
|
|
|
def _refresh_connection(self):
|
|
|
|
|
try:
|
|
|
|
|
reachable, _ = _client.check_connection()
|
|
|
|
|
except Exception:
|
|
|
|
|
reachable = False
|
|
|
|
|
|
|
|
|
|
if reachable:
|
|
|
|
|
api_url = _get_api_url()
|
|
|
|
|
parsed = urllib.parse.urlparse(api_url)
|
|
|
|
|
hostname = parsed.hostname or api_url
|
|
|
|
|
self._conn_dot.setStyleSheet("color: #4CAF50; font-size: 10px;")
|
|
|
|
|
self._conn_label.setText(f"Connected to {hostname}")
|
|
|
|
|
else:
|
|
|
|
|
self._conn_dot.setStyleSheet("color: #888; font-size: 10px;")
|
|
|
|
|
self._conn_label.setText("Disconnected")
|
|
|
|
|
|
|
|
|
|
def _refresh_checkouts(self):
|
|
|
|
|
self._checkout_list.clear()
|
|
|
|
|
try:
|
|
|
|
|
local_files = search_local_files()
|
|
|
|
|
for item in local_files[:15]:
|
|
|
|
|
pn = item.get("part_number", "")
|
|
|
|
|
desc = item.get("description", "")
|
|
|
|
|
modified = (item.get("modified") or "")[:10]
|
|
|
|
|
label = f"{pn} {desc}"
|
|
|
|
|
if modified:
|
|
|
|
|
label += f" ({modified})"
|
|
|
|
|
list_item = self._checkout_list.addItem(label)
|
|
|
|
|
except Exception:
|
|
|
|
|
self._checkout_list.addItem("(Unable to scan local files)")
|
|
|
|
|
|
|
|
|
|
if self._checkout_list.count() == 0:
|
|
|
|
|
self._checkout_list.addItem("(No local checkouts)")
|
|
|
|
|
|
|
|
|
|
def _refresh_activity(self):
|
|
|
|
|
self._activity_list.clear()
|
|
|
|
|
try:
|
|
|
|
|
reachable, _ = _client.check_connection()
|
|
|
|
|
except Exception:
|
|
|
|
|
reachable = False
|
|
|
|
|
|
|
|
|
|
if not reachable:
|
|
|
|
|
self._activity_list.addItem("(Not connected)")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
items = _client.list_items()
|
|
|
|
|
if isinstance(items, list):
|
|
|
|
|
# Collect local part numbers for badge comparison
|
|
|
|
|
local_pns = set()
|
|
|
|
|
try:
|
|
|
|
|
for lf in search_local_files():
|
|
|
|
|
local_pns.add(lf.get("part_number", ""))
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
for item in items[:10]:
|
|
|
|
|
pn = item.get("part_number", "")
|
|
|
|
|
desc = item.get("description", "")
|
|
|
|
|
updated = (item.get("updated_at") or "")[:10]
|
|
|
|
|
badge = "\u2713 " if pn in local_pns else ""
|
|
|
|
|
label = f"{badge}{pn} {desc}"
|
|
|
|
|
if updated:
|
|
|
|
|
label += f" ({updated})"
|
|
|
|
|
self._activity_list.addItem(label)
|
|
|
|
|
|
|
|
|
|
if self._activity_list.count() == 0:
|
|
|
|
|
self._activity_list.addItem("(No items in database)")
|
|
|
|
|
except Exception:
|
|
|
|
|
self._activity_list.addItem("(Unable to fetch activity)")
|
|
|
|
|
|
|
|
|
|
def _refresh_local(self):
|
|
|
|
|
from PySide import QtGui
|
|
|
|
|
|
|
|
|
|
self._local_list.clear()
|
|
|
|
|
try:
|
|
|
|
|
param = FreeCAD.ParamGet("User parameter:BaseApp/RecentFiles")
|
|
|
|
|
count = param.GetInt("RecentFiles", 0)
|
|
|
|
|
for i in range(min(count, 10)):
|
|
|
|
|
path = param.GetString(f"MRU{i}", "")
|
|
|
|
|
if path:
|
|
|
|
|
name = Path(path).name
|
|
|
|
|
item = QtGui.QListWidgetItem(name)
|
|
|
|
|
item.setToolTip(path)
|
|
|
|
|
item.setData(256, path) # Qt.UserRole = 256
|
|
|
|
|
self._local_list.addItem(item)
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
if self._local_list.count() == 0:
|
|
|
|
|
self._local_list.addItem("(No recent files)")
|
|
|
|
|
|
|
|
|
|
def _on_checkout_clicked(self, item):
|
|
|
|
|
"""Open a local checkout file."""
|
|
|
|
|
text = item.text()
|
|
|
|
|
if text.startswith("("):
|
|
|
|
|
return
|
|
|
|
|
pn = text.split()[0] if text else ""
|
|
|
|
|
if not pn:
|
|
|
|
|
return
|
|
|
|
|
local_path = find_file_by_part_number(pn)
|
|
|
|
|
if local_path and local_path.exists():
|
|
|
|
|
FreeCAD.openDocument(str(local_path))
|
|
|
|
|
|
|
|
|
|
def _on_activity_clicked(self, item):
|
|
|
|
|
"""Open/checkout a remote item."""
|
|
|
|
|
text = item.text()
|
|
|
|
|
if text.startswith("("):
|
|
|
|
|
return
|
|
|
|
|
# Strip badge if present
|
|
|
|
|
text = text.lstrip("\u2713 ")
|
|
|
|
|
pn = text.split()[0] if text else ""
|
|
|
|
|
if not pn:
|
|
|
|
|
return
|
|
|
|
|
local_path = find_file_by_part_number(pn)
|
|
|
|
|
if local_path and local_path.exists():
|
|
|
|
|
FreeCAD.openDocument(str(local_path))
|
|
|
|
|
else:
|
|
|
|
|
_sync.open_item(pn)
|
|
|
|
|
|
|
|
|
|
def _on_local_clicked(self, item):
|
|
|
|
|
"""Open a local recent file."""
|
|
|
|
|
path = item.data(256) # Qt.UserRole
|
|
|
|
|
if path and Path(path).exists():
|
|
|
|
|
FreeCAD.openDocument(path)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Silo_StartPanel:
|
|
|
|
|
"""Show the Silo Start Panel."""
|
|
|
|
|
|
|
|
|
|
def GetResources(self):
|
|
|
|
|
return {
|
|
|
|
|
"MenuText": "Start Panel",
|
|
|
|
|
"ToolTip": "Show Silo start panel with checkouts and recent activity",
|
|
|
|
|
"Pixmap": _icon("open"),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def Activated(self):
|
|
|
|
|
from PySide import QtCore, QtGui
|
|
|
|
|
|
|
|
|
|
mw = FreeCADGui.getMainWindow()
|
|
|
|
|
if mw is None:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Reuse existing panel if it exists
|
|
|
|
|
panel = mw.findChild(QtGui.QDockWidget, "SiloStartPanel")
|
|
|
|
|
if panel:
|
|
|
|
|
panel.show()
|
|
|
|
|
panel.raise_()
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Create new dock widget
|
|
|
|
|
content = SiloStartPanel()
|
|
|
|
|
dock = QtGui.QDockWidget("Silo", mw)
|
|
|
|
|
dock.setObjectName("SiloStartPanel")
|
|
|
|
|
dock.setWidget(content.widget)
|
|
|
|
|
dock.setAllowedAreas(
|
|
|
|
|
QtCore.Qt.LeftDockWidgetArea | QtCore.Qt.RightDockWidgetArea
|
|
|
|
|
)
|
|
|
|
|
mw.addDockWidget(QtCore.Qt.LeftDockWidgetArea, dock)
|
|
|
|
|
|
|
|
|
|
def IsActive(self):
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Register commands
|
|
|
|
|
FreeCADGui.addCommand("Silo_Open", Silo_Open())
|
|
|
|
|
FreeCADGui.addCommand("Silo_New", Silo_New())
|
|
|
|
|
@@ -2955,3 +3206,4 @@ FreeCADGui.addCommand("Silo_SetStatus", Silo_SetStatus())
|
|
|
|
|
FreeCADGui.addCommand("Silo_Settings", Silo_Settings())
|
|
|
|
|
|
|
|
|
|
FreeCADGui.addCommand("Silo_Auth", Silo_Auth())
|
|
|
|
|
FreeCADGui.addCommand("Silo_StartPanel", Silo_StartPanel())
|
|
|
|
|
|