#@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()