Added BookPopupViewer
This commit is contained in:
338
BookPopupViewer/BookPopupViewer.py
Normal file
338
BookPopupViewer/BookPopupViewer.py
Normal 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()
|
||||
Reference in New Issue
Block a user