{"id":653,"date":"2026-01-31T17:45:14","date_gmt":"2026-01-31T21:45:14","guid":{"rendered":"https:\/\/hiropaper.ca\/?page_id=653"},"modified":"2026-02-24T16:15:07","modified_gmt":"2026-02-24T20:15:07","slug":"xml-creation-tool","status":"publish","type":"page","link":"https:\/\/hiropaper.ca\/?page_id=653","title":{"rendered":"XML creation tool"},"content":{"rendered":"<!doctype html>\r\n<html lang=\"en\">\r\n<head>\r\n  <meta charset=\"utf-8\"\/>\r\n  <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"\/>\r\n  <title>Drive Folder \u2192 XML<\/title>\r\n  <style>\r\n    body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial; margin: 0px; min-height: 100dvh; }\r\n    label { display:block; font-weight:600; margin: 12px 0 6px; }\r\n    input, select, button { padding:10px; font-size:14px; }\r\n    input[type=\"text\"], input[type=\"search\"] { width:min(980px, 100%); }\r\n    .row { display:flex; gap:12px; flex-wrap:wrap; align-items:end; }\r\n    .muted { color:#666; font-size:13px; }\r\n    .grid { display:grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap:12px; margin-top:16px; }\r\n    .item { border:1px solid #ddd; border-radius:12px; padding:10px; }\r\n    .thumb { width:100%; aspect-ratio: 63\/88; border-radius:10px; object-fit:cover; background:#0A161D; }\r\n    .name { font-size:13px; margin-top:8px; word-break:break-word; }\r\n    .controls { display:flex; gap:8px; align-items:center; margin-top:10px; flex-wrap:wrap; }\r\n    .pill { font-size:12px; padding:4px 8px; border-radius:999px; background:#0A161D; color:#fff; }\r\n    .pill.light { background:#2b3b46; }\r\n    .pill.warn { background:#8a1f1f; }\r\n    .warn { background:#0A161D; border:1px solid #ffd2d2; color:#8a1f1f; padding:10px 12px; border-radius:10px; margin-top:10px; display:none; }\r\n    pre { white-space:pre-wrap; background:#0b1020; color:#e7e7e7; padding:12px; border-radius:10px; overflow:auto; }\r\n    .statbar { display:flex; gap:12px; flex-wrap:wrap; margin-top:10px; background:#0A161D; }\r\n    .stat { background:#0A161D; border:1px solid #e9e9e9; border-radius:10px; padding:8px 10px; font-size:13px; color:#fff; }\r\n    .btn-small { padding:8px 10px; }\r\n    #sortSelect { background:#0f3c57; color:#fff; }\r\n    a { color:#0f3c57; }\r\n\r\n    \/* Modal *\/\r\n    .modal-overlay {\r\n      position: fixed; inset: 0;\r\n      background: rgba(0,0,0,0.55);\r\n      display: none;\r\n      align-items: center;\r\n      justify-content: center;\r\n      z-index: 99999;\r\n      padding: 18px;\r\n    }\r\n    .modal {\r\n      width: min(980px, 100%);\r\n      background: #fff;\r\n      border-radius: 14px;\r\n      border: 1px solid #ddd;\r\n      overflow: hidden;\r\n      box-shadow: 0 10px 40px rgba(0,0,0,0.25);\r\n    }\r\n    .modal header {\r\n      padding: 14px 16px;\r\n      background: #0A161D;\r\n      color: #fff;\r\n      display:flex;\r\n      gap:12px;\r\n      align-items:center;\r\n      justify-content: space-between;\r\n    }\r\n    .modal header h3 {\r\n      margin: 0; font-size: 16px;\r\n    }\r\n    .modal .content {\r\n      padding: 14px 16px;\r\n      max-height: min(70vh, 680px);\r\n      background: #0A161D;\r\n      overflow: auto;\r\n    }\r\n    .modal .actions {\r\n      padding: 12px 16px;\r\n      display:flex;\r\n      gap:10px;\r\n      flex-wrap:wrap;\r\n      justify-content: flex-end;\r\n      border-top: 1px solid #eee;\r\n      background: #0A161D;\r\n    }\r\n    .modal .actions button {\r\n      padding: 10px 12px;\r\n    }\r\n    .modal-grid {\r\n      display:grid;\r\n      grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));\r\n      gap: 12px;\r\n      margin-top: 12px;\r\n    }\r\n    .modal-card {\r\n      border: 1px solid #ddd;\r\n      border-radius: 12px;\r\n      padding: 8px;\r\n      cursor: pointer;\r\n      background: #0A161D;\r\n    }\r\n    .modal-card:hover { border-color:#999; }\r\n    .modal-card img {\r\n      width: 100%;\r\n      aspect-ratio: 63\/88;\r\n      object-fit: cover;\r\n      border-radius: 10px;\r\n      background:#0A161D;\r\n    }\r\n    .modal-card .n {\r\n      font-size: 12px;\r\n      margin-top: 6px;\r\n      word-break: break-word;\r\n    }\r\n    .modal-row {\r\n      display:flex;\r\n      gap:12px;\r\n      flex-wrap:wrap;\r\n      align-items:end;\r\n    }\r\n    .modal-row input[type=\"search\"]{\r\n      width: min(520px, 100%);\r\n    }\r\n  <\/style>\r\n<\/head>\r\n<body>\r\n  <h1>Google Drive Folder \u2192 XML<\/h1>\r\n  <p>\r\n    Paste a <b>public<\/b> Google Drive folder link (shared \u201cAnyone with the link\u201d). Select files + quantities, pick a cardback, then generate an XML file.\r\n  <\/p>\r\n  <p>\r\n    Watch our YouTube tutorial on how to use this tool here:\r\n    <u><b><a href=\"https:\/\/youtu.be\/vDk4EXINFYE\" target=\"_blank\" rel=\"noopener noreferrer\">Google Drive Folder to XML File<\/a><\/b><\/u>\r\n  <\/p>\r\n\r\n  <label>Drive folder link<\/label>\r\n  <input id=\"folderUrl\" type=\"text\" placeholder=\"https:\/\/drive.google.com\/drive\/folders\/XXXXXXXXXXXX?usp=sharing\" \/>\r\n\r\n  <div class=\"row\" style=\"margin-top:12px\">\r\n    <button id=\"loadBtn\">Load folder<\/button>\r\n    <button id=\"genBtn\" disabled>Generate XML<\/button>\r\n    <button id=\"dlBtn\" disabled>Download cards.xml<\/button>\r\n  <\/div>\r\n\r\n  <div class=\"statbar\">\r\n    <div class=\"stat\"><b>Files:<\/b> <span id=\"filesCount\">0<\/span><\/div>\r\n    <div class=\"stat\"><b>Selected items:<\/b> <span id=\"selectedItems\">0<\/span><\/div>\r\n    <div class=\"stat\"><b>Total cards:<\/b> <span id=\"cardsTotal\">0<\/span><\/div>\r\n    <div class=\"stat\"><b>Auto bracket:<\/b> <span id=\"bracketAuto\">\u2014<\/span><\/div>\r\n    <div class=\"stat\"><b>Cardback:<\/b> <span id=\"cardbackLabel\">None<\/span><\/div>\r\n  <\/div>\r\n\r\n  <div id=\"warnNoBack\" class=\"warn\">\r\n    <b>No cardback selected.<\/b> Your XML will still generate, but most workflows expect a back.\r\n    Pick one image and click <b>Set as back<\/b>.\r\n  <\/div>\r\n\r\n  <div id=\"status\" class=\"muted\" style=\"margin-top:10px\"><\/div>\r\n\r\n  <div class=\"row\" style=\"margin-top:12px\">\r\n    <div>\r\n      <label>Search<\/label>\r\n      <input id=\"searchBox\" type=\"search\" placeholder=\"Filter by filename...\" disabled \/>\r\n    <\/div>\r\n    <div>\r\n      <label>Sort<\/label>\r\n      <select id=\"sortSelect\" disabled>\r\n        <option value=\"name_asc\" selected>Name (A \u2192 Z)<\/option>\r\n        <option value=\"name_desc\">Name (Z \u2192 A)<\/option>\r\n        <option value=\"modified_desc\">Modified (newest)<\/option>\r\n        <option value=\"modified_asc\">Modified (oldest)<\/option>\r\n        <option value=\"created_desc\">Created (newest)<\/option>\r\n        <option value=\"created_asc\">Created (oldest)<\/option>\r\n      <\/select>\r\n    <\/div>\r\n\r\n    <button id=\"selectAllBtn\" class=\"btn-small\" disabled>Select all (filtered)<\/button>\r\n    <button id=\"clearBtn\" class=\"btn-small\" disabled>Clear selections<\/button>\r\n  <\/div>\r\n\r\n  <div id=\"grid\" class=\"grid\"><\/div>\r\n\r\n  <h2 style=\"margin-top:22px\">XML<\/h2>\r\n  <pre id=\"xmlOut\">(nothing yet)<\/pre>\r\n\r\n  <!-- Modal -->\r\n  <div id=\"modalOverlay\" class=\"modal-overlay\" role=\"dialog\" aria-modal=\"true\">\r\n    <div class=\"modal\">\r\n      <header>\r\n        <h3 id=\"modalTitle\">Modal<\/h3>\r\n        <button id=\"modalCloseBtn\" class=\"btn-small\" title=\"Close\">\u2715<\/button>\r\n      <\/header>\r\n      <div id=\"modalContent\" class=\"content\"><\/div>\r\n      <div id=\"modalActions\" class=\"actions\"><\/div>\r\n    <\/div>\r\n  <\/div>\r\n\r\n<script>\r\n  \/\/ Using workers.dev (no Cloudflare DNS changes needed)\r\n  const WORKER_BASE = \"https:\/\/hiropaper-drive-list.hiropaperco.workers.dev\";\r\n\r\n  \/\/ Change requested:\r\n  const STOCK_DEFAULT = \"(S30) Standard Smooth\";\r\n  const FOIL_DEFAULT = \"false\";\r\n  const BRACKET_TIERS = [18, 36, 55, 90, 108, 612];\r\n\r\n  function driveThumb(fileId, w=512) {\r\n    return `https:\/\/drive.google.com\/thumbnail?id=${encodeURIComponent(fileId)}&sz=w${w}`;\r\n  }\r\n\r\n  function extractFolderId(input) {\r\n    const s = String(input || \"\").trim();\r\n    if (!s) return null;\r\n    if (\/^[a-zA-Z0-9_-]{10,}$\/.test(s) && !s.includes(\"http\")) return s;\r\n    try {\r\n      const u = new URL(s);\r\n      const parts = u.pathname.split(\"\/\").filter(Boolean);\r\n      const idx = parts.indexOf(\"folders\");\r\n      if (idx !== -1 && parts[idx+1]) return parts[idx+1];\r\n      const idParam = u.searchParams.get(\"id\");\r\n      if (idParam) return idParam;\r\n    } catch {}\r\n    return null;\r\n  }\r\n\r\n  function x(s) {\r\n    return String(s ?? \"\")\r\n      .replaceAll(\"&\",\"&amp;\")\r\n      .replaceAll(\"<\",\"&lt;\")\r\n      .replaceAll(\">\",\"&gt;\")\r\n      .replaceAll('\"',\"&quot;\")\r\n      .replaceAll(\"'\",\"&apos;\");\r\n  }\r\n\r\n  function downloadText(filename, text) {\r\n    const blob = new Blob([text], { type: \"application\/xml;charset=utf-8\" });\r\n    const a = document.createElement(\"a\");\r\n    a.href = URL.createObjectURL(blob);\r\n    a.download = filename;\r\n    document.body.appendChild(a);\r\n    a.click();\r\n    a.remove();\r\n    setTimeout(()=>URL.revokeObjectURL(a.href), 1500);\r\n  }\r\n\r\n  function stripExt(name) {\r\n    const i = name.lastIndexOf(\".\");\r\n    return (i > 0) ? name.slice(0, i) : name;\r\n  }\r\n\r\n  function parseTime(t) {\r\n    const ms = Date.parse(t || \"\");\r\n    return Number.isFinite(ms) ? ms : 0;\r\n  }\r\n\r\n  function autoBracket(totalCards) {\r\n    for (const t of BRACKET_TIERS) {\r\n      if (totalCards <= t) return t;\r\n    }\r\n    return totalCards;\r\n  }\r\n\r\n  \/\/ -------------------------\r\n  \/\/ State\r\n  \/\/ -------------------------\r\n  let allFiles = [];                 \/\/ [{id,name,modifiedTime,createdTime}]\r\n  let selections = new Map();        \/\/ id -> { selected:boolean, qty:number }\r\n  let cardbackId = \"\";               \/\/ default back\r\n  let dfcMap = new Map();            \/\/ frontId -> backId\r\n  let dfcBackToFronts = new Map();   \/\/ backId -> Set(frontId)\r\n\r\n  \/\/ -------------------------\r\n  \/\/ UI refs\r\n  \/\/ -------------------------\r\n  const statusEl = document.getElementById(\"status\");\r\n  const gridEl = document.getElementById(\"grid\");\r\n  const filesCountEl = document.getElementById(\"filesCount\");\r\n  const cardsTotalEl = document.getElementById(\"cardsTotal\");\r\n  const selectedItemsEl = document.getElementById(\"selectedItems\");\r\n  const cardbackLabelEl = document.getElementById(\"cardbackLabel\");\r\n  const bracketAutoEl = document.getElementById(\"bracketAuto\");\r\n  const searchBox = document.getElementById(\"searchBox\");\r\n  const sortSelect = document.getElementById(\"sortSelect\");\r\n  const warnNoBack = document.getElementById(\"warnNoBack\");\r\n\r\n  const genBtn = document.getElementById(\"genBtn\");\r\n  const dlBtn = document.getElementById(\"dlBtn\");\r\n  const selectAllBtn = document.getElementById(\"selectAllBtn\");\r\n  const clearBtn = document.getElementById(\"clearBtn\");\r\n\r\n  \/\/ Modal refs\r\n  const modalOverlay = document.getElementById(\"modalOverlay\");\r\n  const modalTitle = document.getElementById(\"modalTitle\");\r\n  const modalContent = document.getElementById(\"modalContent\");\r\n  const modalActions = document.getElementById(\"modalActions\");\r\n  const modalCloseBtn = document.getElementById(\"modalCloseBtn\");\r\n\r\n  modalCloseBtn.addEventListener(\"click\", () => closeModal(null));\r\n  modalOverlay.addEventListener(\"click\", (e) => {\r\n    if (e.target === modalOverlay) closeModal(null);\r\n  });\r\n  window.addEventListener(\"keydown\", (e) => {\r\n    if (e.key === \"Escape\" && modalOverlay.style.display === \"flex\") closeModal(null);\r\n  });\r\n\r\n  let modalResolve = null;\r\n  function openModal({ title, contentNode, actions }) {\r\n    modalTitle.textContent = title;\r\n    modalContent.innerHTML = \"\";\r\n    modalContent.appendChild(contentNode);\r\n    modalActions.innerHTML = \"\";\r\n\r\n    for (const a of actions) {\r\n      const btn = document.createElement(\"button\");\r\n      btn.textContent = a.label;\r\n      if (a.kind === \"danger\") btn.style.background = \"#8a1f1f\", btn.style.color=\"#fff\";\r\n      if (a.kind === \"primary\") btn.style.background = \"#0f3c57\", btn.style.color=\"#fff\";\r\n      btn.addEventListener(\"click\", () => closeModal(a.value));\r\n      modalActions.appendChild(btn);\r\n    }\r\n\r\n    modalOverlay.style.display = \"flex\";\r\n    return new Promise((resolve) => { modalResolve = resolve; });\r\n  }\r\n\r\n  function closeModal(value) {\r\n    modalOverlay.style.display = \"none\";\r\n    const r = modalResolve;\r\n    modalResolve = null;\r\n    if (r) r(value);\r\n  }\r\n\r\n  \/\/ -------------------------\r\n  \/\/ Helpers for DFC bookkeeping\r\n  \/\/ -------------------------\r\n  function setDfc(frontId, backId) {\r\n    \/\/ remove old reverse mapping\r\n    const old = dfcMap.get(frontId);\r\n    if (old) {\r\n      const set = dfcBackToFronts.get(old);\r\n      if (set) {\r\n        set.delete(frontId);\r\n        if (set.size === 0) dfcBackToFronts.delete(old);\r\n      }\r\n    }\r\n\r\n    if (!backId) {\r\n      dfcMap.delete(frontId);\r\n      return;\r\n    }\r\n\r\n    dfcMap.set(frontId, backId);\r\n    if (!dfcBackToFronts.has(backId)) dfcBackToFronts.set(backId, new Set());\r\n    dfcBackToFronts.get(backId).add(frontId);\r\n  }\r\n\r\n  function isUsedAsDfcBack(fileId) {\r\n    const set = dfcBackToFronts.get(fileId);\r\n    return !!(set && set.size > 0);\r\n  }\r\n\r\n  \/\/ -------------------------\r\n  \/\/ UI enable\/disable\r\n  \/\/ -------------------------\r\n  function setControlsEnabled(enabled) {\r\n    searchBox.disabled = !enabled;\r\n    sortSelect.disabled = !enabled;\r\n    genBtn.disabled = !enabled;\r\n    selectAllBtn.disabled = !enabled;\r\n    clearBtn.disabled = !enabled;\r\n  }\r\n\r\n  function computeStats() {\r\n    let totalCards = 0;\r\n    let selectedItems = 0;\r\n\r\n    for (const s of selections.values()) {\r\n      if (s.selected && s.qty > 0) {\r\n        totalCards += s.qty;\r\n        selectedItems += 1;\r\n      }\r\n    }\r\n    return { totalCards, selectedItems };\r\n  }\r\n\r\n  function updateStats() {\r\n    filesCountEl.textContent = String(allFiles.length);\r\n\r\n    const { totalCards, selectedItems } = computeStats();\r\n    cardsTotalEl.textContent = String(totalCards);\r\n    selectedItemsEl.textContent = String(selectedItems);\r\n\r\n    const bracket = totalCards > 0 ? autoBracket(totalCards) : \"\u2014\";\r\n    bracketAutoEl.textContent = String(bracket);\r\n\r\n    if (cardbackId) {\r\n      const f = allFiles.find(ff => ff.id === cardbackId);\r\n      cardbackLabelEl.textContent = f ? f.name : cardbackId;\r\n      warnNoBack.style.display = \"none\";\r\n    } else {\r\n      cardbackLabelEl.textContent = \"None\";\r\n      warnNoBack.style.display = (totalCards > 0) ? \"block\" : \"none\";\r\n    }\r\n  }\r\n\r\n  function getFilteredSortedFiles() {\r\n    const q = (searchBox.value || \"\").trim().toLowerCase();\r\n    let arr = allFiles;\r\n\r\n    if (q) arr = arr.filter(f => (f.name || \"\").toLowerCase().includes(q));\r\n\r\n    const sort = sortSelect.value;\r\n    arr = [...arr].sort((a,b) => {\r\n      if (sort === \"name_asc\") return a.name.localeCompare(b.name);\r\n      if (sort === \"name_desc\") return b.name.localeCompare(a.name);\r\n\r\n      if (sort === \"modified_desc\") return parseTime(b.modifiedTime) - parseTime(a.modifiedTime);\r\n      if (sort === \"modified_asc\") return parseTime(a.modifiedTime) - parseTime(b.modifiedTime);\r\n\r\n      if (sort === \"created_desc\") return parseTime(b.createdTime) - parseTime(a.createdTime);\r\n      if (sort === \"created_asc\") return parseTime(a.createdTime) - parseTime(b.createdTime);\r\n\r\n      return 0;\r\n    });\r\n\r\n    return arr;\r\n  }\r\n\r\n  \/\/ -------------------------\r\n  \/\/ Conflict prompts\r\n  \/\/ -------------------------\r\n  async function promptDefaultBackConflict(fileName) {\r\n    const wrap = document.createElement(\"div\");\r\n    wrap.innerHTML = `\r\n      <p><b>You have selected both 'use as front' and as 'default back'<\/b> for:<\/p>\r\n      <p style=\"margin-top:6px;\"><code>${x(fileName)}<\/code><\/p>\r\n      <p style=\"margin-top:10px;\">Would you like to:<\/p>\r\n    `;\r\n    return await openModal({\r\n      title: \"Front + Default Back Conflict\",\r\n      contentNode: wrap,\r\n      actions: [\r\n        { label: \"Keep as both a front and default back\", value: \"both\", kind: \"primary\" },\r\n        { label: \"Use as front only\", value: \"front_only\" },\r\n        { label: \"Use as default back only\", value: \"back_only\", kind: \"danger\" },\r\n        { label: \"Cancel\", value: null },\r\n      ],\r\n    });\r\n  }\r\n\r\n  async function promptDfcBackAlreadyUsedAsFront(fileName) {\r\n    const wrap = document.createElement(\"div\");\r\n    wrap.innerHTML = `\r\n      <p><b>This card is being used as a DFC back<\/b>:<\/p>\r\n      <p style=\"margin-top:6px;\"><code>${x(fileName)}<\/code><\/p>\r\n      <p style=\"margin-top:10px;\">Would you like to:<\/p>\r\n    `;\r\n    return await openModal({\r\n      title: \"DFC Back + Front Usage\",\r\n      contentNode: wrap,\r\n      actions: [\r\n        { label: \"Also use as its own front\", value: \"keep_front\", kind: \"primary\" },\r\n        { label: \"Do not use as a front\", value: \"remove_front\", kind: \"danger\" },\r\n        { label: \"Cancel\", value: null },\r\n      ],\r\n    });\r\n  }\r\n\r\n  \/\/ -------------------------\r\n  \/\/ DFC picker modal\r\n  \/\/ -------------------------\r\n  async function pickDfcBack(frontFile) {\r\n    const wrap = document.createElement(\"div\");\r\n\r\n    const row = document.createElement(\"div\");\r\n    row.className = \"modal-row\";\r\n\r\n    const info = document.createElement(\"div\");\r\n    info.innerHTML = `\r\n      <div class=\"muted\">Choose a back image for:<\/div>\r\n      <div style=\"font-weight:700; margin-top:4px;\">${x(frontFile.name)}<\/div>\r\n    `;\r\n\r\n    const search = document.createElement(\"input\");\r\n    search.type = \"search\";\r\n    search.placeholder = \"Search images\u2026\";\r\n    search.value = \"\";\r\n\r\n    row.appendChild(info);\r\n    row.appendChild(search);\r\n\r\n    const grid = document.createElement(\"div\");\r\n    grid.className = \"modal-grid\";\r\n\r\n    function renderPicker() {\r\n      const q = (search.value || \"\").trim().toLowerCase();\r\n      const list = allFiles.filter(f => {\r\n        if (f.id === frontFile.id) return false; \/\/ can't pick itself\r\n        if (!q) return true;\r\n        return (f.name || \"\").toLowerCase().includes(q);\r\n      });\r\n\r\n      grid.innerHTML = \"\";\r\n      for (const f of list) {\r\n        const card = document.createElement(\"div\");\r\n        card.className = \"modal-card\";\r\n        card.innerHTML = `\r\n          <img decoding=\"async\" src=\"${driveThumb(f.id, 512)}\" alt=\"${x(f.name)}\"\/>\r\n          <div class=\"n\">${x(f.name)}<\/div>\r\n        `;\r\n        card.addEventListener(\"click\", () => closeModal({ pickedId: f.id }));\r\n        grid.appendChild(card);\r\n      }\r\n\r\n      if (list.length === 0) {\r\n        const empty = document.createElement(\"div\");\r\n        empty.className = \"muted\";\r\n        empty.textContent = \"No matches.\";\r\n        grid.appendChild(empty);\r\n      }\r\n    }\r\n\r\n    search.addEventListener(\"input\", renderPicker);\r\n\r\n    wrap.appendChild(row);\r\n    wrap.appendChild(grid);\r\n    renderPicker();\r\n\r\n    const result = await openModal({\r\n      title: \"Set Dual-Faced Card Back\",\r\n      contentNode: wrap,\r\n      actions: [\r\n        { label: \"Clear DFC (no special back)\", value: { pickedId: \"\" } },\r\n        { label: \"Cancel\", value: null, kind: \"danger\" },\r\n      ],\r\n    });\r\n\r\n    \/\/ If they clicked a thumbnail, modal closes with {pickedId: ...}\r\n    if (result && typeof result === \"object\" && \"pickedId\" in result) return result.pickedId;\r\n\r\n    \/\/ If they clicked actions:\r\n    if (result && typeof result === \"object\" && \"pickedId\" in result) return result.pickedId;\r\n    if (result && result.pickedId !== undefined) return result.pickedId;\r\n\r\n    \/\/ Cancel\r\n    return null;\r\n  }\r\n\r\n  \/\/ -------------------------\r\n  \/\/ Render\r\n  \/\/ -------------------------\r\n  function render() {\r\n    const list = getFilteredSortedFiles();\r\n    gridEl.innerHTML = \"\";\r\n\r\n    for (const f of list) {\r\n      if (!selections.has(f.id)) selections.set(f.id, { selected:false, qty:0 });\r\n      const s = selections.get(f.id);\r\n\r\n      const div = document.createElement(\"div\");\r\n      div.className = \"item\";\r\n\r\n      const img = document.createElement(\"img\");\r\n      img.className = \"thumb\";\r\n      img.src = driveThumb(f.id);\r\n      img.alt = f.name;\r\n\r\n      const name = document.createElement(\"div\");\r\n      name.className = \"name\";\r\n      name.textContent = f.name;\r\n\r\n      const controls = document.createElement(\"div\");\r\n      controls.className = \"controls\";\r\n\r\n      const checkbox = document.createElement(\"input\");\r\n      checkbox.type = \"checkbox\";\r\n      checkbox.checked = !!s.selected;\r\n\r\n      const qty = document.createElement(\"input\");\r\n      qty.type = \"number\";\r\n      qty.min = \"0\";\r\n      qty.value = String(s.qty || 0);\r\n      qty.style.width = \"76px\";\r\n\r\n      \/\/ Set as default back\r\n      const backBtn = document.createElement(\"button\");\r\n      backBtn.textContent = \"Set as back\";\r\n\r\n      \/\/ DFC\r\n      const dfcBtn = document.createElement(\"button\");\r\n      dfcBtn.textContent = \"Set as DFC\";\r\n      const dfcBadge = document.createElement(\"span\");\r\n      dfcBadge.className = \"pill light\";\r\n      const dfcBackId = dfcMap.get(f.id) || \"\";\r\n      if (dfcBackId) {\r\n        const bf = allFiles.find(ff => ff.id === dfcBackId);\r\n        dfcBadge.textContent = bf ? `DFC \u2192 ${bf.name}` : `DFC \u2192 ${dfcBackId}`;\r\n      } else {\r\n        dfcBadge.textContent = \"\";\r\n        dfcBadge.style.display = \"none\";\r\n      }\r\n\r\n      const badge = document.createElement(\"span\");\r\n      badge.className = \"pill\";\r\n      badge.dataset.badgeFor = f.id;\r\n      badge.textContent = (f.id === cardbackId) ? \"Cardback\" : \"\";\r\n\r\n      const usedAsDfcBackBadge = document.createElement(\"span\");\r\n      usedAsDfcBackBadge.className = \"pill warn\";\r\n      if (isUsedAsDfcBack(f.id)) {\r\n        usedAsDfcBackBadge.textContent = \"Used as DFC back\";\r\n      } else {\r\n        usedAsDfcBackBadge.textContent = \"\";\r\n        usedAsDfcBackBadge.style.display = \"none\";\r\n      }\r\n\r\n      \/\/ Events (async because of modals)\r\n      checkbox.addEventListener(\"change\", async () => {\r\n        const wantSelected = checkbox.checked;\r\n\r\n        \/\/ If this file is the DEFAULT back and they want to also use as front, ask.\r\n        if (wantSelected && cardbackId === f.id) {\r\n          const choice = await promptDefaultBackConflict(f.name);\r\n          if (choice === \"both\") {\r\n            \/\/ keep both; ensure qty >= 1\r\n            s.selected = true;\r\n            if (s.qty < 1) s.qty = 1, qty.value = \"1\";\r\n          } else if (choice === \"front_only\") {\r\n            \/\/ unset default back, keep as front\r\n            cardbackId = \"\";\r\n            s.selected = true;\r\n            if (s.qty < 1) s.qty = 1, qty.value = \"1\";\r\n          } else if (choice === \"back_only\") {\r\n            \/\/ keep as back, remove from front\r\n            cardbackId = f.id;\r\n            s.selected = false;\r\n            s.qty = 0;\r\n            checkbox.checked = false;\r\n            qty.value = \"0\";\r\n          } else {\r\n            \/\/ cancel\r\n            checkbox.checked = !!s.selected;\r\n          }\r\n          render();\r\n          updateStats();\r\n          return;\r\n        }\r\n\r\n        \/\/ If this file is used as a DFC back and they want to use as front, ask.\r\n        if (wantSelected && isUsedAsDfcBack(f.id)) {\r\n          const choice = await promptDfcBackAlreadyUsedAsFront(f.name);\r\n          if (choice === \"keep_front\") {\r\n            s.selected = true;\r\n            if (s.qty < 1) s.qty = 1, qty.value = \"1\";\r\n          } else if (choice === \"remove_front\") {\r\n            s.selected = false;\r\n            s.qty = 0;\r\n            checkbox.checked = false;\r\n            qty.value = \"0\";\r\n          } else {\r\n            checkbox.checked = !!s.selected;\r\n          }\r\n          updateStats();\r\n          return;\r\n        }\r\n\r\n        s.selected = wantSelected;\r\n        if (s.selected && s.qty < 1) {\r\n          s.qty = 1;\r\n          qty.value = \"1\";\r\n        }\r\n        updateStats();\r\n      });\r\n\r\n      qty.addEventListener(\"input\", async () => {\r\n        const n = Math.max(0, parseInt(qty.value || \"0\", 10));\r\n\r\n        \/\/ If increasing qty > 0 on a file that is the DEFAULT back, ask conflict.\r\n        if (n > 0 && cardbackId === f.id) {\r\n          const choice = await promptDefaultBackConflict(f.name);\r\n          if (choice === \"both\") {\r\n            s.qty = n;\r\n            s.selected = true;\r\n            checkbox.checked = true;\r\n          } else if (choice === \"front_only\") {\r\n            cardbackId = \"\";\r\n            s.qty = n;\r\n            s.selected = true;\r\n            checkbox.checked = true;\r\n          } else if (choice === \"back_only\") {\r\n            \/\/ remove from front\r\n            s.qty = 0;\r\n            s.selected = false;\r\n            checkbox.checked = false;\r\n            qty.value = \"0\";\r\n          } else {\r\n            \/\/ cancel -> revert UI\r\n            qty.value = String(s.qty || 0);\r\n          }\r\n          render();\r\n          updateStats();\r\n          return;\r\n        }\r\n\r\n        \/\/ If increasing qty on a file used as DFC back, ask.\r\n        if (n > 0 && isUsedAsDfcBack(f.id)) {\r\n          const choice = await promptDfcBackAlreadyUsedAsFront(f.name);\r\n          if (choice === \"keep_front\") {\r\n            s.qty = n;\r\n            s.selected = true;\r\n            checkbox.checked = true;\r\n          } else if (choice === \"remove_front\") {\r\n            s.qty = 0;\r\n            s.selected = false;\r\n            checkbox.checked = false;\r\n            qty.value = \"0\";\r\n          } else {\r\n            qty.value = String(s.qty || 0);\r\n          }\r\n          updateStats();\r\n          return;\r\n        }\r\n\r\n        s.qty = n;\r\n        s.selected = n > 0;\r\n        checkbox.checked = s.selected;\r\n        updateStats();\r\n      });\r\n\r\n      backBtn.addEventListener(\"click\", async () => {\r\n        \/\/ If already set to back, toggle off\r\n        if (cardbackId === f.id) {\r\n          cardbackId = \"\";\r\n          render();\r\n          updateStats();\r\n          return;\r\n        }\r\n\r\n        \/\/ If this file is selected as front (or has qty), show 3-option popup\r\n        const currentlyFront = (s.selected && s.qty > 0);\r\n        if (currentlyFront) {\r\n          const choice = await promptDefaultBackConflict(f.name);\r\n          if (choice === \"both\") {\r\n            cardbackId = f.id;\r\n          } else if (choice === \"front_only\") {\r\n            \/\/ do NOT set as back\r\n            \/\/ leave cardbackId unchanged\r\n          } else if (choice === \"back_only\") {\r\n            cardbackId = f.id;\r\n            s.selected = false;\r\n            s.qty = 0;\r\n          } else {\r\n            \/\/ cancel\r\n          }\r\n        } else {\r\n          \/\/ no conflict, just set as back\r\n          cardbackId = f.id;\r\n        }\r\n\r\n        render();\r\n        updateStats();\r\n      });\r\n\r\n      dfcBtn.addEventListener(\"click\", async () => {\r\n        const picked = await (async () => {\r\n          \/\/ build a picker modal that returns pickedId by clicking a card\r\n          \/\/ we implement it by opening modal + rendering cards; clicking card closes with {pickedId:...}\r\n          const wrap = document.createElement(\"div\");\r\n\r\n          const row = document.createElement(\"div\");\r\n          row.className = \"modal-row\";\r\n\r\n          const info = document.createElement(\"div\");\r\n          info.innerHTML = `\r\n            <div class=\"muted\">Choose a back image for:<\/div>\r\n            <div style=\"font-weight:700; margin-top:4px;\">${x(f.name)}<\/div>\r\n          `;\r\n\r\n          const search = document.createElement(\"input\");\r\n          search.type = \"search\";\r\n          search.placeholder = \"Search images\u2026\";\r\n          search.value = \"\";\r\n\r\n          row.appendChild(info);\r\n          row.appendChild(search);\r\n\r\n          const grid = document.createElement(\"div\");\r\n          grid.className = \"modal-grid\";\r\n\r\n          function renderPicker() {\r\n            const q = (search.value || \"\").trim().toLowerCase();\r\n            const list = allFiles.filter(ff => {\r\n              if (ff.id === f.id) return false; \/\/ can't pick itself\r\n              if (!q) return true;\r\n              return (ff.name || \"\").toLowerCase().includes(q);\r\n            });\r\n\r\n            grid.innerHTML = \"\";\r\n            for (const ff of list) {\r\n              const card = document.createElement(\"div\");\r\n              card.className = \"modal-card\";\r\n              card.innerHTML = `\r\n                <img decoding=\"async\" src=\"${driveThumb(ff.id, 512)}\" alt=\"${x(ff.name)}\"\/>\r\n                <div class=\"n\">${x(ff.name)}<\/div>\r\n              `;\r\n              card.addEventListener(\"click\", () => closeModal({ pickedId: ff.id }));\r\n              grid.appendChild(card);\r\n            }\r\n\r\n            if (list.length === 0) {\r\n              const empty = document.createElement(\"div\");\r\n              empty.className = \"muted\";\r\n              empty.textContent = \"No matches.\";\r\n              grid.appendChild(empty);\r\n            }\r\n          }\r\n\r\n          search.addEventListener(\"input\", renderPicker);\r\n\r\n          wrap.appendChild(row);\r\n          wrap.appendChild(grid);\r\n          renderPicker();\r\n\r\n          const res = await openModal({\r\n            title: \"Set Dual-Faced Card Back\",\r\n            contentNode: wrap,\r\n            actions: [\r\n              { label: \"Clear DFC (no special back)\", value: { pickedId: \"\" } },\r\n              { label: \"Cancel\", value: null, kind: \"danger\" },\r\n            ],\r\n          });\r\n\r\n          if (res && typeof res === \"object\" && \"pickedId\" in res) return res.pickedId;\r\n          return null;\r\n        })();\r\n\r\n        if (picked === null) return; \/\/ cancelled\r\n\r\n        if (!picked) {\r\n          \/\/ Clear DFC\r\n          setDfc(f.id, \"\");\r\n          render();\r\n          updateStats();\r\n          return;\r\n        }\r\n\r\n        \/\/ Set DFC mapping\r\n        setDfc(f.id, picked);\r\n\r\n        \/\/ If chosen DFC back is currently used as a front, ask if it should stay a front\r\n        const backSel = selections.get(picked);\r\n        if (backSel && backSel.selected && backSel.qty > 0) {\r\n          const backFile = allFiles.find(ff => ff.id === picked);\r\n          const choice = await promptDfcBackAlreadyUsedAsFront(backFile ? backFile.name : picked);\r\n          if (choice === \"remove_front\") {\r\n            backSel.selected = false;\r\n            backSel.qty = 0;\r\n          } else if (choice === null) {\r\n            \/\/ cancel -> revert dfc mapping\r\n            setDfc(f.id, \"\");\r\n          }\r\n        }\r\n\r\n        render();\r\n        updateStats();\r\n      });\r\n\r\n      \/\/ Build\r\n      controls.appendChild(checkbox);\r\n      controls.appendChild(document.createTextNode(\" Use\"));\r\n      controls.appendChild(document.createTextNode(\" Qty:\"));\r\n      controls.appendChild(qty);\r\n      controls.appendChild(backBtn);\r\n      controls.appendChild(badge);\r\n      controls.appendChild(dfcBtn);\r\n      controls.appendChild(dfcBadge);\r\n      controls.appendChild(usedAsDfcBackBadge);\r\n\r\n      div.appendChild(img);\r\n      div.appendChild(name);\r\n      div.appendChild(controls);\r\n      gridEl.appendChild(div);\r\n    }\r\n\r\n    updateStats();\r\n  }\r\n\r\n  \/\/ -------------------------\r\n  \/\/ Load folder\r\n  \/\/ -------------------------\r\n  async function loadFolder() {\r\n    const input = document.getElementById(\"folderUrl\").value.trim();\r\n    const folderId = extractFolderId(input);\r\n\r\n    allFiles = [];\r\n    selections.clear();\r\n    cardbackId = \"\";\r\n    dfcMap.clear();\r\n    dfcBackToFronts.clear();\r\n\r\n    setControlsEnabled(false);\r\n    dlBtn.disabled = true;\r\n    document.getElementById(\"xmlOut\").textContent = \"(nothing yet)\";\r\n    gridEl.innerHTML = \"\";\r\n    updateStats();\r\n\r\n    if (!folderId) {\r\n      statusEl.textContent = \"Could not detect folder ID. Paste a Drive folder link shared as \u201cAnyone with the link\u201d.\";\r\n      return;\r\n    }\r\n\r\n    statusEl.textContent = \"Loading folder files...\";\r\n    try {\r\n      const res = await fetch(`${WORKER_BASE}\/api\/drive-list?folderId=${encodeURIComponent(folderId)}`);\r\n      const data = await res.json();\r\n      if (!res.ok || data.error) throw new Error(data.error || \"Failed to load folder\");\r\n\r\n      allFiles = Array.isArray(data.files) ? data.files : [];\r\n      for (const f of allFiles) selections.set(f.id, { selected:false, qty:0 });\r\n\r\n      statusEl.textContent = `Loaded ${allFiles.length} image files from folder.`;\r\n      setControlsEnabled(allFiles.length > 0);\r\n      render();\r\n    } catch (e) {\r\n      statusEl.textContent = `Error: ${e.message}`;\r\n    }\r\n  }\r\n\r\n  \/\/ -------------------------\r\n  \/\/ Build XML (with DFC support)\r\n  \/\/ -------------------------\r\n  function buildXml() {\r\n    const { totalCards } = computeStats();\r\n    const bracket = autoBracket(totalCards);\r\n\r\n    \/\/ Expand selected files into sequential slots based on qty\r\n    const chosen = [];\r\n    for (const f of allFiles) {\r\n      const s = selections.get(f.id);\r\n      if (!s || !s.selected || s.qty <= 0) continue;\r\n      chosen.push({ file: f, qty: s.qty });\r\n    }\r\n\r\n    let slot = 0;\r\n\r\n    \/\/ Track per-slot back overrides\r\n    \/\/ For each slot, if front has dfcMap(frontId) => backId\r\n    const backOverrides = []; \/\/ [{slot, backId, backName}]\r\n\r\n    let xml = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n`;\r\n    xml += `<order>\\n`;\r\n    xml += `    <details>\\n`;\r\n    xml += `        <quantity>${x(totalCards)}<\/quantity>\\n`;\r\n    xml += `        <bracket>${x(bracket)}<\/bracket>\\n`;\r\n    xml += `        <stock>${x(STOCK_DEFAULT)}<\/stock>\\n`;\r\n    xml += `        <foil>${x(FOIL_DEFAULT)}<\/foil>\\n`;\r\n    xml += `    <\/details>\\n`;\r\n    xml += `    <fronts>\\n`;\r\n\r\n    for (const item of chosen) {\r\n      const query = stripExt(item.file.name);\r\n      const dfcBackId = dfcMap.get(item.file.id) || \"\";\r\n\r\n      for (let i = 0; i < item.qty; i++) {\r\n        xml += `        <card>\\n`;\r\n        xml += `            <id>${x(item.file.id)}<\/id>\\n`;\r\n        xml += `            <slots>${x(slot)}<\/slots>\\n`;\r\n        xml += `            <name>${x(item.file.name)}<\/name>\\n`;\r\n        xml += `            <query>${x(query)}<\/query>\\n`;\r\n        xml += `        <\/card>\\n`;\r\n\r\n        if (dfcBackId) {\r\n          const bf = allFiles.find(ff => ff.id === dfcBackId);\r\n          backOverrides.push({\r\n            slot: slot,\r\n            backId: dfcBackId,\r\n            backName: bf ? bf.name : dfcBackId,\r\n            backQuery: bf ? stripExt(bf.name) : dfcBackId\r\n          });\r\n        }\r\n\r\n        slot++;\r\n      }\r\n    }\r\n\r\n    xml += `    <\/fronts>\\n`;\r\n\r\n    \/\/ DFC overrides: generate <backs> only if needed\r\n    if (backOverrides.length > 0) {\r\n      xml += `    <backs>\\n`;\r\n      for (const b of backOverrides) {\r\n        xml += `        <card>\\n`;\r\n        xml += `            <id>${x(b.backId)}<\/id>\\n`;\r\n        xml += `            <slots>${x(b.slot)}<\/slots>\\n`;\r\n        xml += `            <name>${x(b.backName)}<\/name>\\n`;\r\n        xml += `            <query>${x(b.backQuery)}<\/query>\\n`;\r\n        xml += `        <\/card>\\n`;\r\n      }\r\n      xml += `    <\/backs>\\n`;\r\n    }\r\n\r\n    \/\/ Default back\r\n    if (cardbackId) xml += `    <cardback>${x(cardbackId)}<\/cardback>\\n`;\r\n\r\n    xml += `<\/order>\\n`;\r\n\r\n    return { xml, count: totalCards };\r\n  }\r\n\r\n  \/\/ -------------------------\r\n  \/\/ Bulk actions\r\n  \/\/ -------------------------\r\n  function selectAllFiltered() {\r\n    const list = getFilteredSortedFiles();\r\n    for (const f of list) {\r\n      const s = selections.get(f.id);\r\n      if (!s) continue;\r\n\r\n      \/\/ If file is default back or used as dfc back, don't auto-prompt; just set qty (user can adjust)\r\n      s.selected = true;\r\n      if (s.qty < 1) s.qty = 1;\r\n    }\r\n    render();\r\n  }\r\n\r\n  function clearSelections() {\r\n    for (const f of allFiles) {\r\n      const s = selections.get(f.id);\r\n      if (!s) continue;\r\n      s.selected = false;\r\n      s.qty = 0;\r\n    }\r\n    cardbackId = \"\";\r\n    dfcMap.clear();\r\n    dfcBackToFronts.clear();\r\n    render();\r\n  }\r\n\r\n  \/\/ -------------------------\r\n  \/\/ Events\r\n  \/\/ -------------------------\r\n  document.getElementById(\"loadBtn\").addEventListener(\"click\", loadFolder);\r\n  searchBox.addEventListener(\"input\", () => render());\r\n  sortSelect.addEventListener(\"change\", () => render());\r\n\r\n  selectAllBtn.addEventListener(\"click\", selectAllFiltered);\r\n  clearBtn.addEventListener(\"click\", clearSelections);\r\n\r\n  genBtn.addEventListener(\"click\", () => {\r\n    const { xml, count } = buildXml();\r\n    document.getElementById(\"xmlOut\").textContent = xml;\r\n    dlBtn.disabled = (count === 0);\r\n    statusEl.textContent = `Generated XML with ${count} total cards.`;\r\n    updateStats();\r\n  });\r\n\r\n  dlBtn.addEventListener(\"click\", () => {\r\n    const xml = document.getElementById(\"xmlOut\").textContent;\r\n    if (!xml || xml.startsWith(\"(nothing\")) return;\r\n    downloadText(\"cards.xml\", xml);\r\n  });\r\n\r\n  \/\/ Initialize\r\n  setControlsEnabled(false);\r\n  updateStats();\r\n<\/script>\r\n<\/body>\r\n<\/html>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-653","page","type-page","status-publish","hentry"],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/hiropaper.ca\/index.php?rest_route=\/wp\/v2\/pages\/653","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/hiropaper.ca\/index.php?rest_route=\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/hiropaper.ca\/index.php?rest_route=\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/hiropaper.ca\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/hiropaper.ca\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=653"}],"version-history":[{"count":3,"href":"https:\/\/hiropaper.ca\/index.php?rest_route=\/wp\/v2\/pages\/653\/revisions"}],"predecessor-version":[{"id":841,"href":"https:\/\/hiropaper.ca\/index.php?rest_route=\/wp\/v2\/pages\/653\/revisions\/841"}],"wp:attachment":[{"href":"https:\/\/hiropaper.ca\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=653"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}