Added BookPopupViewer

This commit is contained in:
2025-08-29 20:38:16 +02:00
parent 92b10ccf29
commit 4d39d5b8f8

View File

@@ -0,0 +1,338 @@
#@Name BookPopupViewer
#@Hook Books
import clr
clr.AddReference('System.Windows.Forms')
clr.AddReference('System.Drawing')
clr.AddReference('System')
clr.AddReference('System.IO')
clr.AddReference('System.Data')
import System
import System.Windows.Forms as WinForms
import System.Drawing as Drawing
import System.IO
import System.Data
# ---------------- Helpers ----------------
COLUMNS = [
("Series", "Series"),
("Issue", "Issue"),
("Vol", "Vol"),
("Title", "Title"),
("Publisher", "Publisher"),
("Writer", "Writer"),
("Characters", "Characters"),
("Published", "Published"),
("FileSizeMB", "MB"),
("FilePath", "File Path"),
("CV_Volume", "CV Volume")
]
def _safe_text(value):
if value is None:
return ""
try:
text = str(value)
except Exception:
try:
text = repr(value)
except Exception:
text = ""
return text.strip()
def _published_text(year, month):
y = _safe_text(year)
m = _safe_text(month)
if y and m:
try:
mi = int(m)
if 1 <= mi <= 12:
m = "{:02d}".format(mi)
except Exception:
pass
return y + "-" + m
return y or ""
def _file_size_mb(path):
try:
if path and System.IO.File.Exists(path):
fi = System.IO.FileInfo(path)
sz = fi.Length / (1024.0 * 1024.0)
return "{:.2f}".format(sz)
except Exception:
pass
return ""
def _book_row(book):
series = _safe_text(getattr(book, "Series", None))
number = _safe_text(getattr(book, "Number", None))
volume = _safe_text(getattr(book, "Volume", None))
title = _safe_text(getattr(book, "Title", None))
publisher = _safe_text(getattr(book, "Publisher", None))
writer = _safe_text(getattr(book, "Writer", None))
characters = _safe_text(getattr(book, "Characters", None))
year = getattr(book, "Year", None)
month = getattr(book, "Month", None)
published = _published_text(year, month)
filepath = _safe_text(getattr(book, "FilePath", None))
filesize = _file_size_mb(filepath)
try:
comicvine_volume = _safe_text(book.GetCustomValue("comicvine_volume"))
except Exception:
comicvine_volume = ""
return {
"Series": series,
"Issue": number,
"Vol": volume,
"Title": title,
"Publisher": publisher,
"Writer": writer,
"Characters": characters,
"Published": published,
"FileSizeMB": filesize,
"FilePath": filepath,
"CV_Volume": comicvine_volume
}
def _rows_to_datatable(rows):
# Build a System.Data.DataTable from list of dict rows.
dt = System.Data.DataTable("Books")
# define columns with ordered headers
for prop, header in COLUMNS:
col = System.Data.DataColumn(prop, System.String) # store as string
dt.Columns.Add(col)
# add rows
for r in rows:
dr = dt.NewRow()
for prop, header in COLUMNS:
dr[prop] = _safe_text(r.get(prop, ""))
dt.Rows.Add(dr)
return dt
# --------------- Main UI -----------------
def BookPopupViewer(books):
if not books:
WinForms.MessageBox.Show("No books selected.", "BookPopupViewer")
return
# Build rows
all_rows = []
for b in books:
try:
all_rows.append(_book_row(b))
except Exception:
all_rows.append({
"Series":"(error reading book)","Issue":"","Vol":"","Title":"",
"Publisher":"","Writer":"","Characters":"","Published":"",
"FileSizeMB":"","FilePath":"","CV_Volume":""
})
# --- Form (the window) ---
form = WinForms.Form()
form.Text = "Selected Books - Viewer" # title bar
form.StartPosition = WinForms.FormStartPosition.CenterScreen
form.FormBorderStyle = WinForms.FormBorderStyle.FixedDialog # simple layout for beginners
form.MaximizeBox = False
form.MinimizeBox = False
form.ClientSize = Drawing.Size(980, 520) # content area (not counting frame)
# --- Filter label + textbox ---
lblFilter = WinForms.Label(); lblFilter.Parent = form
lblFilter.Text = "Filter:"
lblFilter.AutoSize = True
lblFilter.Left = 10
lblFilter.Top = 14
txtFilter = WinForms.TextBox(); txtFilter.Parent = form
txtFilter.Left = 60
txtFilter.Top = 10
txtFilter.Width = 540
# --- Action buttons (top-right) ---
btnCopy = WinForms.Button(); btnCopy.Parent = form
btnCopy.Text = "Copy Selected"
btnCopy.Width = 120
btnCopy.Top = 8
btnCopy.Left = form.ClientSize.Width - 350
btnExport = WinForms.Button(); btnExport.Parent = form
btnExport.Text = "Export CSV"
btnExport.Width = 120
btnExport.Top = 8
btnExport.Left = form.ClientSize.Width - 230
btnClose = WinForms.Button(); btnClose.Parent = form
btnClose.Text = "Close"
btnClose.Width = 80
btnClose.Top = 8
btnClose.Left = form.ClientSize.Width - 110
btnClose.Click += (lambda *_: form.Close())
# --- Data grid (the table) ---
grid = WinForms.DataGridView(); grid.Parent = form
grid.Left = 10
grid.Top = 40
grid.Width = form.ClientSize.Width - 20
grid.Height = form.ClientSize.Height - 66
# Make it resize with the window edges (even if we keep dialog fixed)
grid.Anchor = (WinForms.AnchorStyles.Top | WinForms.AnchorStyles.Left |
WinForms.AnchorStyles.Right | WinForms.AnchorStyles.Bottom)
# Read-only, select whole rows, auto-size columns to fit content
grid.ReadOnly = True
grid.AllowUserToAddRows = False
grid.AllowUserToDeleteRows = False
grid.SelectionMode = WinForms.DataGridViewSelectionMode.FullRowSelect
grid.MultiSelect = True
grid.AutoSizeColumnsMode = WinForms.DataGridViewAutoSizeColumnsMode.DisplayedCells
grid.AutoGenerateColumns = True # We'll bind a DataTable, so this is fine
# --- Status bar (footer) ---
status = WinForms.StatusStrip(); status.Parent = form
lblStatus = WinForms.ToolStripStatusLabel()
status.Items.Add(lblStatus)
# all_rows already built from books with: _book_row(book)
view_rows = list(all_rows) # start by showing everything
def bind_table():
# Build a DataTable from view_rows and bind it to the grid.
dt = _rows_to_datatable(view_rows)
grid.DataSource = dt
# Apply friendly column headers (second item in COLUMNS tuples)
for i, (prop, header) in enumerate(COLUMNS):
grid.Columns[i].HeaderText = header
def refresh_grid():
# Rebind and update the status line.
bind_table()
selected = grid.SelectedRows.Count if grid.SelectedRows is not None else 0
lblStatus.Text = "Showing {0} of {1} - Selected: {2}".format(
len(view_rows), len(all_rows), selected
)
def filter_rows():
# Live filter: keep rows where joined text contains the filter string.
text = txtFilter.Text.strip().lower()
if not text:
# No filter? Show everything.
del view_rows[:]
view_rows.extend(all_rows)
else:
# Simple 'contains' match across all column values
filtered = []
for r in all_rows:
haystack = " ".join([_safe_text(r.get(prop, "")) for prop, _ in COLUMNS]).lower()
if text in haystack:
filtered.append(r)
del view_rows[:]
view_rows.extend(filtered)
refresh_grid()
# Wire the TextChanged event so filtering happens as you type
txtFilter.TextChanged += (lambda *_: filter_rows())
def on_selection_changed(sender, args):
selected = grid.SelectedRows.Count if grid.SelectedRows is not None else 0
lblStatus.Text = "Showing {0} of {1} - Selected: {2}".format(len(view_rows), len(all_rows), selected)
grid.SelectionChanged += on_selection_changed
_sort_state = {"prop": None, "asc": True}
def on_column_header_click(sender, args):
col_index = args.ColumnIndex
if col_index < 0 or col_index >= len(COLUMNS):
return
prop = COLUMNS[col_index][0]
# Toggle or reset sort direction
if _sort_state["prop"] == prop:
_sort_state["asc"] = not _sort_state["asc"]
else:
_sort_state["prop"] = prop
_sort_state["asc"] = True
asc = _sort_state["asc"]
# Sort view_rows directly, then refresh
try:
view_rows.sort(key=lambda r: r.get(prop, ""), reverse=(not asc))
except Exception:
view_rows.sort(key=lambda r: _safe_text(r.get(prop, "")), reverse=(not asc))
refresh_grid()
grid.ColumnHeaderMouseClick += on_column_header_click
def copy_selected(sender, args):
if grid.SelectedRows is None or grid.SelectedRows.Count == 0:
WinForms.MessageBox.Show("No rows selected to copy.", "BookPopupViewer")
return
headers = [hdr for _, hdr in COLUMNS]
lines = ["\t".join(headers)]
indices = sorted([row.Index for row in grid.SelectedRows]) # normalize order
for i in indices:
# read values from grid cells to match what's visible
values = []
for c in range(len(COLUMNS)):
cell = grid.Rows[i].Cells[c].Value
values.append(_safe_text(cell))
lines.append("\t".join(values))
try:
WinForms.Clipboard.SetText("\r\n".join(lines))
WinForms.MessageBox.Show("Copied to clipboard.", "BookPopupViewer")
except Exception:
WinForms.MessageBox.Show("Failed to access clipboard.", "BookPopupViewer")
btnCopy.Click += copy_selected
def export_csv(sender, args):
if len(view_rows) == 0:
WinForms.MessageBox.Show("Nothing to export.", "BookPopupViewer")
return
sfd = WinForms.SaveFileDialog()
sfd.Title = "Export to CSV"
sfd.Filter = "CSV files (*.csv)|*.csv|All files (*.*)|*.*"
sfd.FileName = "ComicRackSelection.csv"
if sfd.ShowDialog() == WinForms.DialogResult.OK:
path = sfd.FileName
try:
def csv_escape(t):
t = _safe_text(t)
need = ("," in t) or ("\n" in t) or ("\r" in t) or ('"' in t)
t = t.replace('"', '""')
return '"' + t + '"' if need else t
sw = System.IO.StreamWriter(path, False, System.Text.Encoding.UTF8)
try:
# header
sw.WriteLine(",".join([csv_escape(h) for _, h in COLUMNS]))
# rows
for r in view_rows:
row = [csv_escape(r.get(prop, "")) for prop, _ in COLUMNS]
sw.WriteLine(",".join(row))
finally:
sw.Close()
WinForms.MessageBox.Show("Exported {0} row(s).".format(len(view_rows)), "BookPopupViewer")
except Exception:
WinForms.MessageBox.Show("Failed to write CSV file.", "BookPopupViewer")
btnExport.Click += export_csv
# Show window
filter_rows() # initial bind
form.ShowDialog()