Tieba Post Backup Tool

Automatically backup Tieba posts in one single click

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Tieba Post Backup Tool
// @namespace    https://github.com/ZXPrism/TiebaPostBackupTool
// @version      2.1.0
// @description  Automatically backup Tieba posts in one single click
// @author       ZXPrism
// @license      MIT
// @match        https://tieba.baidu.com/p/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @connect      gss0.bdstatic.com
// @connect      gss1.bdstatic.com
// @connect      gss2.bdstatic.com
// @connect      gss3.bdstatic.com
// @connect      gss4.bdstatic.com
// @connect      gsp0.baidu.com
// @connect      himg.bdimg.com
// @connect      tb0.bdstatic.com
// @connect      tb1.bdstatic.com
// @connect      tb2.bdstatic.com
// @connect      tiebapic.baidu.com
// @connect      imgsa.baidu.com
// @connect      static.tieba.baidu.com
// @connect      tieba.baidu.com
// @run-at       document-idle
// ==/UserScript==
"use strict";
(() => {
  // src/image_handler.ts
  var ImageHandler = class {
    constructor() {
      this.image_map = {};
      this.image_counter = 1;
    }
    /**
     * Extract all image URLs from post content and avatars.
     * Returns mapped image URLs.
     */
    process_images(post_html, avatar_urls) {
      this.image_map = {};
      this.image_counter = 1;
      const img_tags = post_html.match(/<img[^>]+>/g) || [];
      img_tags.forEach((img_tag) => {
        const src_match = img_tag.match(/src=["']([^"']+)["']/);
        if (src_match) {
          this._add_to_image_map(src_match[1]);
        }
      });
      avatar_urls.forEach((url) => {
        if (url) {
          this._add_to_image_map(url);
        }
      });
      return this.image_map;
    }
    /**
     * Add image URL to map with deduplication.
     */
    _add_to_image_map(original_url) {
      if (!this.image_map[original_url]) {
        const ext = this._get_extension(original_url);
        const new_name = `image_${this.image_counter++}.${ext}`;
        this.image_map[original_url] = new_name;
      }
    }
    /**
     * Get file extension from URL.
     */
    _get_extension(url) {
      if (url.includes("static.tieba.baidu.com")) {
        return "png";
      }
      const match = url.match(/\.([a-z]{3,4})(?:\?|$)/i);
      return match ? match[1] : "jpg";
    }
    /**
     * Replace image URLs in HTML content with new names.
     */
    replace_image_urls(html_content) {
      return html_content.replace(/src=["']([^"']+)["']/g, (match, url) => {
        const new_name = this.image_map[url];
        if (new_name) {
          return `src="./images/${new_name}"`;
        }
        return match;
      });
    }
    /**
     * Download all images as blobs with batching to prevent IP ban.
     * @param on_progress Callback for progress updates (current, total, filename)
     */
    async download_images(on_progress) {
      const blobs = {};
      const entries = Object.entries(this.image_map);
      const total = entries.length;
      let current = 0;
      const BATCH_SIZE = 5;
      const BATCH_DELAY = 1e3;
      for (let i = 0; i < entries.length; i += BATCH_SIZE) {
        const batch = entries.slice(i, Math.min(i + BATCH_SIZE, entries.length));
        await Promise.all(batch.map(async ([original_url, new_name]) => {
          try {
            const blob = await this._fetch_image(original_url);
            blobs[new_name] = blob;
            current++;
            if (on_progress) {
              on_progress(current, total, new_name);
            }
          } catch {
            blobs[new_name] = this._create_placeholder(new_name);
            current++;
            if (on_progress) {
              on_progress(current, total, new_name);
            }
          }
        }));
        if (i + BATCH_SIZE < entries.length) {
          await new Promise((resolve) => setTimeout(resolve, BATCH_DELAY));
        }
      }
      return blobs;
    }
    /**
     * Fetch single image as blob using GM_xmlhttpRequest to bypass CORS.
     */
    async _fetch_image(url) {
      return new Promise((resolve, reject) => {
        if (typeof GM_xmlhttpRequest !== "undefined") {
          GM_xmlhttpRequest({
            method: "GET",
            url,
            responseType: "blob",
            onload: (response) => {
              if (response.status === 200) {
                resolve(response.response);
              } else {
                reject(new Error(`HTTP ${response.status}`));
              }
            },
            onerror: (_error) => {
              reject(new Error("GM_xmlhttpRequest error"));
            }
          });
        } else {
          fetch(url).then((response) => {
            if (!response.ok) {
              throw new Error(`HTTP ${response.status}`);
            }
            return response.blob();
          }).then((blob) => resolve(blob)).catch((error) => reject(error));
        }
      });
    }
    /**
     * Create placeholder image for failed downloads.
     */
    _create_placeholder(_filename) {
      const svg = `
            <svg width="100" height="100" xmlns="http://www.w3.org/2000/svg">
                <rect width="100" height="100" fill="#ccc"/>
                <text x="50" y="50" text-anchor="middle" dy=".3em" font-size="12">Failed</text>
            </svg>
        `;
      return new Blob([svg], { type: "image/svg+xml" });
    }
    /**
     * Replace avatar URLs in floor list with mapped filenames.
     */
    replace_avatar_urls(floor_list) {
      floor_list.forEach((floor) => {
        if (floor.comment.avatar_url && this.image_map[floor.comment.avatar_url]) {
          floor.comment.avatar_url = this.image_map[floor.comment.avatar_url];
        }
        if (floor.sub_comment_list) {
          floor.sub_comment_list.forEach((sub) => {
            if (sub.avatar_url && this.image_map[sub.avatar_url]) {
              sub.avatar_url = this.image_map[sub.avatar_url];
            }
          });
        }
      });
    }
    /**
     * Create viewer HTML file.
     */
    create_viewer_html(post) {
      const json_data = JSON.stringify(post, null, 2);
      return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>${post.title} - Tieba Backup</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
            background: #f5f5f5;
        }
        .header {
            background: white;
            padding: 20px;
            border-radius: 8px;
            margin-bottom: 20px;
        }
        .header h1 {
            margin: 0 0 10px 0;
            color: #333;
        }
        .header .meta {
            color: #666;
            font-size: 14px;
        }
        .floor {
            background: white;
            padding: 15px;
            border-radius: 8px;
            margin-bottom: 15px;
        }
        .floor .author {
            font-weight: bold;
            color: #0055cc;
        }
        .floor .date {
            color: #999;
            font-size: 12px;
            margin-left: 10px;
        }
        .floor .ip {
            color: #999;
            font-size: 12px;
            margin-left: 10px;
        }
        .floor .content {
            margin: 10px 0;
            line-height: 1.6;
        }
        .floor .content img {
            max-width: 100%;
            height: auto;
        }
        .floor .avatar {
            width: 50px;
            height: 50px;
            border-radius: 4px;
            margin-right: 10px;
            vertical-align: middle;
            object-fit: cover;
        }
        .floor .avatar[alt] {
            display: inline-block;
        }
        .floor .floor-index {
            color: #999;
            font-size: 12px;
            margin-bottom: 5px;
        }
        .floor .author-line {
            display: flex;
            align-items: center;
            margin-bottom: 10px;
        }
        .sub-comments {
            margin-left: 20px;
            padding-left: 15px;
            border-left: 2px solid #e0e0e0;
            margin-top: 10px;
        }
        .sub-comment {
            margin: 8px 0;
            padding: 8px;
            background: #f9f9f9;
            border-radius: 4px;
            display: flex;
            align-items: center;
            gap: 8px;
        }
        .sub-comment .content {
            flex: 1;
        }
        .sub-comment .author {
            font-size: 12px;
        }
        .sub-comment .date {
            font-size: 11px;
        }
        .sub-comment .sub-avatar {
            width: 30px;
            height: 30px;
            border-radius: 3px;
            margin-right: 8px;
            vertical-align: middle;
            object-fit: cover;
        }
        .author-link {
            color: #0055cc;
            text-decoration: none;
            font-weight: bold;
        }
        .author-link:hover {
            text-decoration: underline;
        }
        .reply-to {
            color: #666;
            font-size: 12px;
        }
        .reply-to a {
            color: #0055cc;
            text-decoration: none;
        }
        .reply-to a:hover {
            text-decoration: underline;
        }
    </style>
</head>
<body>
    <div class="header">
        <h1>${post.title}</h1>
        <div class="meta">
            \u8D34\u5427: <a class="author-link" href="${post.tieba_url}" target="_blank">${post.tieba_name}</a> |
            \u697C\u5C42: ${post.floor_list.length} |
            \u56DE\u590D: ${post.comment_cnt}
        </div>
    </div>
    <div id="content"></div>

    <script>
        try {
            const post = ${json_data};

            function renderFloor(floor) {
            let html = '<div class="floor">';

            // Floor index
            html += '<div class="floor-index">' + floor.index + '\u697C</div>';

            // Author with avatar
            html += '<div class="author-line">';
            if (floor.comment.avatar_url) {
                html += '<img class="avatar" src="./images/' + floor.comment.avatar_url + '" alt="">';
            }
            // Author as link if homepage exists
            if (floor.comment.homepage_url) {
                html += '<a class="author-link" href="' + floor.comment.homepage_url + '" target="_blank">' + floor.comment.author + '</a>';
            } else {
                html += '<span class="author">' + floor.comment.author + '</span>';
            }
            html += '<span class="date">' + floor.comment.date + '</span>';
            if (floor.comment.ip_location) {
                html += '<span class="ip">\u6765\u81EA: ' + floor.comment.ip_location + '</span>';
            }
            html += '</div>';

            // Content
            html += '<div class="content">' + floor.comment.content + '</div>';

            // Sub-comments
            if (floor.sub_comment_list && floor.sub_comment_list.length > 0) {
                html += '<div class="sub-comments">';
                floor.sub_comment_list.forEach(sub => {
                    html += renderSubComment(sub);
                });
                html += '</div>';
            }

            html += '</div>';
            return html;
        }

        function renderSubComment(comment) {
            let html = '<div class="sub-comment">';

            // Avatar
            if (comment.avatar_url) {
                html += '<img class="sub-avatar" src="./images/' + comment.avatar_url + '" alt="">';
            }

            // Debug logging
            if (!comment.author || comment.author === 'Unknown') {
            }

            // Author with fallback for missing names, as link if homepage exists
            const author_name = comment.author || '\u672A\u77E5\u7528\u6237';
            if (comment.homepage_url) {
                html += '<a class="author-link" href="' + comment.homepage_url + '" target="_blank">' + author_name + '</a>';
            } else {
                html += '<span class="author">' + author_name + '</span>';
            }
            html += '<span class="date">' + comment.date + '</span>';

            // Content with reply-to filtering
            let content = comment.content;
            // Remove the original "\u56DE\u590D <a>username</a>" pattern since we'll render it separately
            // Use string methods instead of regex to avoid escaping issues
            const replyIndex = content.indexOf('\u56DE\u590D');
            if (replyIndex !== -1) {
                // Find the closing </a> and the colon after it
                const anchorEnd = content.indexOf('</a>', replyIndex);
                if (anchorEnd !== -1) {
                    const colonIndex = content.indexOf(':', anchorEnd);
                    if (colonIndex !== -1 && colonIndex < anchorEnd + 10) {
                        // Remove everything from "\u56DE\u590D" to the colon
                        content = content.substring(0, replyIndex) + content.substring(colonIndex + 1);
                    }
                }
            }
            html += '<div class="content">' + content + '</div>';

            // Reply-to information
            if (comment.reply_to_author) {
                html += '<div class="reply-to">\u56DE\u590D ';
                if (comment.reply_to_homepage_url) {
                    html += '<a href="' + comment.reply_to_homepage_url + '" target="_blank">' + comment.reply_to_author + '</a>';
                } else {
                    html += comment.reply_to_author;
                }
                html += '</div>';
            }

            html += '</div>';
            return html;
        }

        // Render all floors
        const contentDiv = document.getElementById('content');
        if (!contentDiv) {
        } else {
            post.floor_list.forEach((floor, index) => {
                try {
                    contentDiv.innerHTML += renderFloor(floor);
                } catch (e) {
                }
            });
        }
        } catch (error) {
            document.getElementById('content').innerHTML = '<p style="color: red; padding: 20px;">Error rendering post: ' + error.message + '</p>';
        }
    </script>
</body>
</html>`;
    }
    /**
     * Create tar archive from files.
     */
    async create_tar(files) {
      const chunks = [];
      const encoder = new TextEncoder();
      for (const [filename, content] of Object.entries(files)) {
        let file_data;
        let file_size;
        if (typeof content === "string") {
          file_data = encoder.encode(content);
          file_size = file_data.length;
        } else {
          file_data = await this._blob_to_uint8array(content);
          file_size = file_data.length;
        }
        const header = this._create_tar_header(filename, file_size);
        chunks.push(header);
        chunks.push(file_data);
        const padding_needed = (512 - file_size % 512) % 512;
        if (padding_needed > 0) {
          chunks.push(new Uint8Array(padding_needed));
        }
      }
      chunks.push(new Uint8Array(1024));
      const total_size = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
      const combined = new Uint8Array(total_size);
      let offset = 0;
      for (const chunk of chunks) {
        combined.set(chunk, offset);
        offset += chunk.length;
      }
      return new Blob([combined], { type: "application/x-tar" });
    }
    /**
     * Convert blob to Uint8Array.
     */
    async _blob_to_uint8array(blob) {
      const buffer = await blob.arrayBuffer();
      return new Uint8Array(buffer);
    }
    /**
     * Create tar header for a file.
     */
    _create_tar_header(filename, file_size) {
      const header = new Uint8Array(512);
      const encoder = new TextEncoder();
      const name_bytes = encoder.encode(filename);
      header.set(name_bytes.subarray(0, Math.min(100, name_bytes.length)), 0);
      encoder.encode("0000644 ").forEach((byte, i) => header[100 + i] = byte);
      encoder.encode("0000000 ").forEach((byte, i) => header[108 + i] = byte);
      encoder.encode("0000000 ").forEach((byte, i) => header[116 + i] = byte);
      const size_octal = file_size.toString(8).padStart(11, "0") + "\0";
      encoder.encode(size_octal).forEach((byte, i) => header[124 + i] = byte);
      const timestamp = Math.floor(Date.now() / 1e3).toString(8).padStart(11, "0") + "\0";
      encoder.encode(timestamp).forEach((byte, i) => header[136 + i] = byte);
      header[156] = 48;
      encoder.encode("        ").forEach((byte, i) => header[148 + i] = byte);
      let checksum = 0;
      for (let i = 0; i < 512; i++) {
        checksum += header[i];
      }
      const checksum_str = checksum.toString(8).padStart(6, "0") + "\0 ";
      encoder.encode(checksum_str).forEach((byte, i) => header[148 + i] = byte);
      return header;
    }
    /**
     * Download blob as file.
     */
    download_blob(blob, filename) {
      const url = URL.createObjectURL(blob);
      const a = document.createElement("a");
      a.href = url;
      a.download = filename;
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      URL.revokeObjectURL(url);
    }
  };

  // src/parser.ts
  var Parser = class {
    constructor() {
      this.image_handler = new ImageHandler();
      this.base_url = window.location.href.split("?")[0];
      this.current_page = this._get_current_page_number();
    }
    /**
     * Get current page number from URL query parameter.
     */
    _get_current_page_number() {
      const url_params = new URLSearchParams(window.location.search);
      const pn = url_params.get("pn");
      return pn ? parseInt(pn) : 1;
    }
    /**
     * Check if reset mode is active (don't auto-save state).
     */
    is_reset_mode() {
      return GM_getValue("reset_mode", false);
    }
    /**
     * Reset parsing state (clear all stored data).
     */
    reset_parsing() {
      GM_deleteValue("parse_state");
    }
    /**
     * Save parsing state to GM storage (respects reset mode).
     */
    _save_state(state) {
      if (!this.is_reset_mode()) {
        GM_setValue("parse_state", state);
      }
    }
    /**
     * Load parsing state from GM storage.
     */
    _load_state() {
      const state = GM_getValue("parse_state", void 0);
      return state || null;
    }
    /**
     * Clear parsing state from GM storage.
     */
    _clear_state() {
      GM_deleteValue("parse_state");
    }
    /**
     * Check if state is stale (older than 30 minutes).
     */
    _is_state_stale(state) {
      const THIRTY_MINUTES = 30 * 60 * 1e3;
      return Date.now() - state.timestamp > THIRTY_MINUTES;
    }
    /**
     * Check if parsing is in progress for this post.
     */
    is_parsing_in_progress() {
      const state = this._load_state();
      if (!state || !state.active) {
        return false;
      }
      if (this._is_state_stale(state)) {
        this._clear_state();
        return false;
      }
      if (state.post_id !== this._parse_post_id()) {
        return false;
      }
      return true;
    }
    /**
     * Cancel ongoing parsing (user manually aborted).
     */
    cancel_parsing() {
      const state = this._load_state();
      if (state && state.active) {
        this._clear_state();
      }
    }
    /**
     * Get the image handler instance (for accessing image_map and downloading).
     */
    get_image_handler() {
      return this.image_handler;
    }
    /**
     * Parse the complete post from current page DOM.
     * Automatically loads all content before parsing.
     * Handles multi-page posts with state persistence.
     */
    async parse_post() {
      const post_id = this._parse_post_id();
      const existing_state = this._load_state();
      if (existing_state && existing_state.active && existing_state.post_id === post_id) {
        if (this._is_state_stale(existing_state)) {
          this._clear_state();
        } else {
          return await this._resume_parse(existing_state);
        }
      }
      return await this._start_parse();
    }
    /**
     * Start a new parsing session.
     */
    async _start_parse() {
      const post_id = this._parse_post_id();
      const page_cnt = this._parse_page_count();
      if (this.current_page !== 1) {
        const state2 = {
          active: true,
          post_id,
          current_page: 1,
          total_pages: page_cnt,
          collected_floors: [],
          timestamp: Date.now()
        };
        this._save_state(state2);
        this._navigate_to_page(1);
        throw new Error("NAVIGATING");
      }
      const state = {
        active: true,
        post_id,
        current_page: this.current_page,
        total_pages: page_cnt,
        collected_floors: [],
        timestamp: Date.now()
      };
      this._save_state(state);
      const page_floors = await this._parse_current_page();
      state.collected_floors = state.collected_floors.concat(page_floors);
      state.current_page = this.current_page + 1;
      this._save_state(state);
      if (state.current_page <= state.total_pages) {
        this._navigate_to_page(state.current_page);
        throw new Error("NAVIGATING");
      } else {
        return await this._finalize_parse(state);
      }
    }
    /**
     * Resume an existing parsing session.
     */
    async _resume_parse(state) {
      const page_floors = await this._parse_current_page();
      state.collected_floors = state.collected_floors.concat(page_floors);
      state.current_page = this.current_page + 1;
      this._save_state(state);
      if (state.current_page <= state.total_pages) {
        this._navigate_to_page(state.current_page);
        throw new Error("NAVIGATING");
      } else {
        return await this._finalize_parse(state);
      }
    }
    /**
     * Finalize parsing and create the complete post.
     */
    async _finalize_parse(state) {
      const title = this._parse_title();
      const tieba_name = this._parse_tieba_name();
      const tieba_url = this._parse_tieba_url();
      const post_id = state.post_id;
      const page_cnt = state.total_pages;
      const comment_cnt = this._parse_comment_count();
      const avatar_urls = this._extract_all_avatar_urls(state.collected_floors);
      const all_html = this._collect_all_html(state.collected_floors);
      const image_map = this.image_handler.process_images(all_html, avatar_urls);
      this._replace_image_urls_in_floors(state.collected_floors);
      this.image_handler.replace_avatar_urls(state.collected_floors);
      const post = {
        title,
        tieba_name,
        tieba_url,
        id: post_id,
        page_cnt,
        comment_cnt,
        floor_list: state.collected_floors,
        image_map
      };
      this._clear_state();
      return post;
    }
    /**
     * Navigate to a specific page.
     */
    _navigate_to_page(page_number) {
      const page_url = `${this.base_url}?pn=${page_number}`;
      window.location.href = page_url;
    }
    /**
     * Parse the current page with full loading process.
     */
    async _parse_current_page() {
      const original_zoom = this._save_original_zoom();
      this._set_zoom(0.3);
      await new Promise((resolve) => setTimeout(resolve, 500));
      await this._scroll_to_load_floors();
      await this._expand_all_sub_comments();
      const floor_list = await this._parse_floors();
      this._set_zoom(original_zoom);
      return floor_list;
    }
    /**
     * Extract all avatar URLs from floor list.
     */
    _extract_all_avatar_urls(floor_list) {
      const avatar_urls = [];
      floor_list.forEach((floor) => {
        if (floor.comment.avatar_url) {
          avatar_urls.push(floor.comment.avatar_url);
        }
        if (floor.sub_comment_list) {
          floor.sub_comment_list.forEach((sub) => {
            if (sub.avatar_url) {
              avatar_urls.push(sub.avatar_url);
            }
          });
        }
      });
      return avatar_urls;
    }
    /**
     * Collect all HTML content from floor list.
     */
    _collect_all_html(floor_list) {
      let all_html = "";
      floor_list.forEach((floor) => {
        all_html += floor.comment.content;
        if (floor.sub_comment_list) {
          floor.sub_comment_list.forEach((sub) => {
            all_html += sub.content;
          });
        }
      });
      return all_html;
    }
    /**
     * Replace image URLs in all floor content.
     */
    _replace_image_urls_in_floors(floor_list) {
      floor_list.forEach((floor) => {
        floor.comment.content = this.image_handler.replace_image_urls(floor.comment.content);
        if (floor.sub_comment_list) {
          floor.sub_comment_list.forEach((sub) => {
            sub.content = this.image_handler.replace_image_urls(sub.content);
          });
        }
      });
    }
    /**
     * Save original zoom level.
     */
    _save_original_zoom() {
      const current_zoom = document.body.style.zoom || "1";
      return parseFloat(current_zoom);
    }
    /**
     * Set zoom level.
     */
    _set_zoom(level) {
      document.body.style.zoom = level.toString();
    }
    /**
     * Stage 1: Scroll to load all lazy-loaded floors.
     */
    async _scroll_to_load_floors() {
      return new Promise((resolve) => {
        window.scrollTo(0, 0);
        const scroll_task = setInterval(() => {
          window.scrollBy(0, 100);
          const max_scroll = document.documentElement.scrollHeight - window.innerHeight;
          if (window.scrollY + window.innerHeight >= max_scroll - 50) {
            setTimeout(() => {
              clearInterval(scroll_task);
              window.scrollTo(0, document.documentElement.scrollHeight);
              setTimeout(() => {
                resolve();
              }, 1e3);
            }, 500);
          }
        }, 100);
      });
    }
    /**
     * Stage 2: Expand all sub-comment sections.
     */
    async _expand_all_sub_comments() {
      const expand_buttons = document.querySelectorAll(".j_lzl_m");
      expand_buttons.forEach((button) => {
        try {
          button.click();
        } catch {
        }
      });
      await this._wait_for_dom_update();
    }
    /**
     * Parse post title from DOM.
     */
    _parse_title() {
      const title_element = document.querySelector(".core_title_txt");
      if (!title_element) {
        throw new Error("Post title not found");
      }
      return title_element.textContent?.trim() || "";
    }
    /**
     * Parse tieba name from DOM.
     */
    _parse_tieba_name() {
      const tieba_element = document.querySelector(".card_title_fname");
      if (!tieba_element) {
        throw new Error("Tieba name not found");
      }
      return tieba_element.textContent?.trim() || "";
    }
    /**
     * Parse tieba URL from DOM.
     */
    _parse_tieba_url() {
      const tieba_link = document.querySelector(".card_title_fname");
      if (tieba_link && tieba_link.tagName === "A") {
        const href = tieba_link.getAttribute("href");
        if (href) {
          if (href.startsWith("/")) {
            return "https://tieba.baidu.com" + href;
          }
          return href;
        }
      }
      const tieba_name = this._parse_tieba_name();
      return `https://tieba.baidu.com/f?kw=${encodeURIComponent(tieba_name)}&fr=index`;
    }
    /**
     * Parse post ID from URL.
     * URL format: tieba.baidu.com/p/{id}
     */
    _parse_post_id() {
      const url_match = window.location.href.match(/\/p\/(\d+)/);
      if (!url_match) {
        throw new Error("Post ID not found in URL");
      }
      return parseInt(url_match[1]);
    }
    /**
     * Parse total page count from DOM.
     */
    _parse_page_count() {
      const posts_num_element = document.querySelector(".l_posts_num");
      if (!posts_num_element) {
        throw new Error("Page count not found");
      }
      const text = posts_num_element.textContent || "";
      const page_match = text.match(/共(\d+)页/);
      if (!page_match) {
        throw new Error("Page count format not recognized");
      }
      return parseInt(page_match[1]);
    }
    /**
     * Parse total comment count from DOM.
     */
    _parse_comment_count() {
      const posts_num_element = document.querySelector(".l_posts_num");
      if (!posts_num_element) {
        throw new Error("Comment count not found");
      }
      const text = posts_num_element.textContent || "";
      const comment_match = text.match(/(\d+)回复贴/);
      if (!comment_match) {
        throw new Error("Comment count format not recognized");
      }
      return parseInt(comment_match[1]);
    }
    /**
     * Parse all floors from DOM.
     */
    async _parse_floors() {
      const floors = [];
      const floor_elements = Array.from(document.querySelectorAll(".l_post.l_post_bright"));
      for (let i = 0; i < floor_elements.length; i++) {
        const floor_element = floor_elements[i];
        const data_field = floor_element.getAttribute("data-field");
        if (data_field === "{}") {
          continue;
        }
        try {
          const floor = await this._parse_floor(floor_element);
          floors.push(floor);
        } catch {
        }
      }
      return floors;
    }
    /**
     * Parse single floor from DOM element.
     */
    async _parse_floor(floor_element) {
      const comment = this._parse_main_comment(floor_element);
      const index = this._parse_floor_index(floor_element);
      const sub_comments = await this._parse_sub_comments(floor_element);
      return {
        comment,
        index,
        sub_comment_list: sub_comments.length > 0 ? sub_comments : void 0
      };
    }
    /**
     * Parse main comment from floor element.
     */
    _parse_main_comment(floor_element) {
      const author = this._parse_author_name(floor_element);
      const date = this._parse_date(floor_element);
      const content = this._parse_content(floor_element);
      const ip_location = this._parse_ip_location(floor_element);
      const avatar_url = this._parse_avatar_url(floor_element);
      const homepage_url = this._parse_homepage_url(floor_element);
      return {
        author,
        date,
        content,
        ip_location,
        avatar_url,
        homepage_url
      };
    }
    /**
     * Parse author name from floor element.
     */
    _parse_author_name(floor_element) {
      const author_element = floor_element.querySelector(".p_author_name");
      if (!author_element) {
        throw new Error("Author name not found");
      }
      return author_element.textContent?.trim() || "";
    }
    /**
     * Parse homepage URL from floor element.
     */
    _parse_homepage_url(floor_element) {
      const author_link = floor_element.querySelector(".p_author_name");
      if (author_link && author_link.tagName === "A") {
        const href = author_link.getAttribute("href");
        if (href) {
          if (href.startsWith("/")) {
            return "https://tieba.baidu.com" + href;
          }
          return href;
        }
      }
      const home_link = floor_element.querySelector('a[href*="/home/main"]');
      if (home_link) {
        const href = home_link.getAttribute("href");
        if (href) {
          if (href.startsWith("/")) {
            return "https://tieba.baidu.com" + href;
          }
          return href;
        }
      }
      return void 0;
    }
    /**
     * Parse avatar URL from floor element.
     */
    _parse_avatar_url(floor_element) {
      const avatar_img = floor_element.querySelector(".p_author_face img");
      if (avatar_img) {
        return avatar_img.getAttribute("src") || void 0;
      }
      const fallback_avatar = floor_element.querySelector('img[src*="static.tieba.baidu.com"]');
      if (fallback_avatar) {
        return fallback_avatar.getAttribute("src") || void 0;
      }
      return void 0;
    }
    /**
     * Parse date from floor element.
     * Handles both old and new Tieba DOM formats.
     */
    _parse_date(floor_element) {
      const new_format_tail = floor_element.querySelector(".core_reply_tail");
      if (new_format_tail) {
        const text = new_format_tail.textContent || "";
        const date_match = text.match(/(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2})/);
        if (date_match) {
          return date_match[1];
        }
      }
      const old_format_date = floor_element.querySelector(".tail-info");
      if (old_format_date) {
        const text = old_format_date.textContent || "";
        const date_match = text.match(/(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2})/);
        if (date_match) {
          return date_match[1];
        }
      }
      throw new Error("Date pattern not found in either old or new format");
    }
    /**
     * Parse content from floor element.
     */
    _parse_content(floor_element) {
      const content_element = floor_element.querySelector(".d_post_content");
      if (!content_element) {
        throw new Error("Content element not found");
      }
      let content = content_element.innerHTML.trim();
      content = content.replace(/点击展开,查看完整图片。/g, "");
      content = content.replace(/点击展开,查看完整图片<\/a>/g, "");
      content = content.replace(/\s*点击展开,查看完整图片。?\s*/g, "");
      content = content.replace(/\s*点击展开,查看完整图片<\/a>\s*/g, "");
      return content;
    }
    /**
     * Parse IP location from floor element.
     * Returns undefined if IP location not present.
     * Handles both old and new Tieba DOM formats.
     */
    _parse_ip_location(floor_element) {
      const ip_element = floor_element.querySelector(".ip-location span");
      if (ip_element) {
        const text = ip_element.textContent || "";
        const ip_match = text.match(/IP属地:(.+)/);
        if (ip_match) {
          return ip_match[1].trim();
        }
      }
      const ip_spans = floor_element.querySelectorAll("span");
      for (const span of Array.from(ip_spans)) {
        const text = span.textContent || "";
        const ip_match = text.match(/IP属地:(.+)/);
        if (ip_match) {
          return ip_match[1].trim();
        }
      }
      return void 0;
    }
    /**
     * Parse floor index from floor element.
     * Handles both old and new Tieba DOM formats.
     */
    _parse_floor_index(floor_element) {
      const new_format_tail = floor_element.querySelector(".p_tail");
      if (new_format_tail) {
        const floor_span = new_format_tail.querySelector("span");
        if (floor_span) {
          const text = floor_span.textContent || "";
          const floor_match = text.match(/(\d+)楼/);
          if (floor_match) {
            return parseInt(floor_match[1]);
          }
        }
      }
      const tail_info_spans = Array.from(floor_element.querySelectorAll(".tail-info"));
      for (const span of tail_info_spans) {
        const text = span.textContent || "";
        const floor_match = text.match(/(\d+)楼/);
        if (floor_match) {
          return parseInt(floor_match[1]);
        }
      }
      throw new Error("Floor index pattern not found in either old or new format");
    }
    /**
     * Parse sub-comments from floor element.
     * Handles pagination by clicking through all pages.
     */
    async _parse_sub_comments(floor_element) {
      const all_sub_comments = [];
      try {
        const pager = floor_element.querySelector(".j_pager");
        if (!pager) {
          return this._parse_current_sub_comments(floor_element);
        }
        const pager_html = pager;
        if (pager_html.style.display === "none") {
          return this._parse_current_sub_comments(floor_element);
        }
        const total_pages = this._get_sub_comment_page_count(pager);
        if (total_pages === 0) {
          return this._parse_current_sub_comments(floor_element);
        }
        const first_page_comments = this._parse_current_sub_comments(floor_element);
        all_sub_comments.push(...first_page_comments);
        for (let page = 2; page <= total_pages; page++) {
          const page_link = this._find_page_button(floor_element, page);
          if (!page_link) {
            break;
          }
          page_link.click();
          await this._wait_for_dom_update();
          const page_comments = this._parse_current_sub_comments(floor_element);
          all_sub_comments.push(...page_comments);
        }
      } catch {
        return this._parse_current_sub_comments(floor_element);
      }
      return all_sub_comments;
    }
    /**
     * Get total page count from sub-comment pager.
     */
    _get_sub_comment_page_count(pager) {
      const last_page_links = Array.from(pager.querySelectorAll("a"));
      for (const link of last_page_links) {
        const text = link.textContent?.trim();
        if (text === "\u5C3E\u9875" || text === "Last") {
          const href = link.getAttribute("href");
          if (href) {
            const match = href.match(/#(\d+)/);
            if (match) {
              return parseInt(match[1]);
            }
          }
        }
      }
      const page_numbers = [];
      const all_links = Array.from(pager.querySelectorAll("a"));
      for (const link of all_links) {
        const text = link.textContent?.trim();
        if (text && /^\d+$/.test(text)) {
          page_numbers.push(parseInt(text));
        }
      }
      if (page_numbers.length > 0) {
        return Math.max(...page_numbers);
      }
      return 0;
    }
    /**
     * Find specific page button for sub-comment pagination.
     */
    _find_page_button(floor_element, page_num) {
      const pager = floor_element.querySelector(".j_pager");
      if (!pager) {
        return null;
      }
      const page_links = Array.from(pager.querySelectorAll("a"));
      for (const link of page_links) {
        const href = link.getAttribute("href");
        if (href === `#${page_num}`) {
          return link;
        }
      }
      for (const link of page_links) {
        const text = link.textContent?.trim();
        if (text === page_num.toString()) {
          return link;
        }
      }
      return null;
    }
    /**
     * Parse sub-comments currently visible in DOM.
     */
    _parse_current_sub_comments(floor_element) {
      const sub_comments = [];
      const sub_comment_elements = floor_element.querySelectorAll(".lzl_single_post, .j_lzl_s_p");
      sub_comment_elements.forEach((sub_element) => {
        try {
          const comment = this._parse_sub_comment(sub_element);
          sub_comments.push(comment);
        } catch {
        }
      });
      return sub_comments;
    }
    /**
     * Wait for DOM to update after pagination click.
     */
    async _wait_for_dom_update() {
      return new Promise((resolve) => {
        setTimeout(resolve, 1e3);
      });
    }
    /**
     * Parse single sub-comment from DOM element.
     */
    _parse_sub_comment(sub_element) {
      const author = this._parse_sub_author(sub_element);
      const date = this._parse_sub_date(sub_element);
      const content = this._parse_sub_content(sub_element);
      const avatar_url = this._parse_sub_avatar_url(sub_element);
      const homepage_url = this._parse_sub_homepage_url(sub_element);
      const reply_info = this._parse_reply_info(content);
      return {
        author,
        date,
        content,
        avatar_url,
        homepage_url,
        reply_to_author: reply_info.author,
        reply_to_homepage_url: reply_info.homepage_url
      };
    }
    /**
     * Parse sub-comment avatar URL.
     */
    _parse_sub_avatar_url(sub_element) {
      const avatar_link = sub_element.querySelector(".lzl_p_p img");
      if (avatar_link) {
        const src = avatar_link.getAttribute("src");
        if (src) {
          if (src.startsWith("//")) {
            return "https:" + src;
          }
          return src;
        }
      }
      return void 0;
    }
    /**
     * Parse sub-comment homepage URL.
     */
    _parse_sub_homepage_url(sub_element) {
      const user_card = sub_element.querySelector(".at.j_user_card");
      if (user_card && user_card.tagName === "A") {
        const href = user_card.getAttribute("href");
        if (href && href.includes("/home/main")) {
          if (href.startsWith("/")) {
            return "https://tieba.baidu.com" + href;
          }
          return href;
        }
      }
      const home_link = sub_element.querySelector('a[href*="/home/main"]');
      if (home_link) {
        const href = home_link.getAttribute("href");
        if (href) {
          if (href.startsWith("/")) {
            return "https://tieba.baidu.com" + href;
          }
          return href;
        }
      }
      return void 0;
    }
    /**
     * Parse reply-to information from sub-comment content.
     * Returns the author and homepage URL if this is a reply to another user.
     */
    _parse_reply_info(content) {
      const reply_pattern = /回复\s+<a[^>]+href="([^"]+)"[^>]*>([^<]+)<\/a>\s*:/;
      const match = content.match(reply_pattern);
      if (match) {
        let href = match[1];
        const author = match[2];
        if (href.startsWith("/")) {
          href = "https://tieba.baidu.com" + href;
        }
        return { author, homepage_url: href };
      }
      return {};
    }
    /**
     * Parse sub-comment author name.
     */
    _parse_sub_author(sub_element) {
      const user_card = sub_element.querySelector(".at.j_user_card, .j_user_card");
      if (user_card) {
        const data_field = user_card.getAttribute("data-field");
        if (data_field) {
          try {
            const parsed = JSON.parse(data_field.replace(/'/g, '"'));
            if (parsed.showname) {
              return parsed.showname;
            }
          } catch {
          }
        }
        const text = user_card.textContent?.trim();
        if (text) {
          return text;
        }
      }
      const author_span = sub_element.querySelector("[data-lzl-author]");
      if (author_span) {
        const username = author_span.getAttribute("data-lzl-author");
        if (username) {
          return username;
        }
      }
      const any_user = sub_element.querySelector("[username]");
      if (any_user) {
        const username = any_user.getAttribute("username");
        if (username) {
          return username;
        }
      }
      return "Unknown";
    }
    /**
     * Parse sub-comment date.
     * Format: "2026-3-28 11:02" (no zero-padding)
     */
    _parse_sub_date(sub_element) {
      const time_element = sub_element.querySelector(".lzl_time");
      if (!time_element) {
        throw new Error("Sub-comment date not found");
      }
      const text = (time_element.textContent || "").replace(/\u00a0/, " ");
      return text.trim();
    }
    /**
     * Parse sub-comment content.
     */
    _parse_sub_content(sub_element) {
      const content_element = sub_element.querySelector(".lzl_content_main");
      if (!content_element) {
        throw new Error("Sub-comment content not found");
      }
      return content_element.innerHTML.trim();
    }
  };

  // src/main.ts
  var DownloadModal = class {
    constructor() {
      this.modal = document.createElement("div");
      this.modal.id = "tieba-download-modal";
      this.modal.style.cssText = `
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.7);
            display: none;
            justify-content: center;
            align-items: center;
            z-index: 10000;
        `;
      const content = document.createElement("div");
      content.style.cssText = `
            background: white;
            padding: 30px;
            border-radius: 12px;
            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
            max-width: 500px;
            width: 90%;
        `;
      const title = document.createElement("h2");
      title.textContent = "\u5907\u4EFD\u5B8C\u6210";
      title.style.cssText = `
            margin: 0 0 20px 0;
            color: #333;
            text-align: center;
        `;
      this.summary_text = document.createElement("div");
      this.summary_text.style.cssText = `
            margin-bottom: 20px;
            color: #666;
            line-height: 1.6;
        `;
      const progress_container = document.createElement("div");
      progress_container.style.cssText = `
            margin-bottom: 15px;
        `;
      const progress_bg = document.createElement("div");
      progress_bg.style.cssText = `
            width: 100%;
            height: 24px;
            background: #e0e0e0;
            border-radius: 12px;
            overflow: hidden;
            position: relative;
        `;
      this.progress_bar = document.createElement("div");
      this.progress_bar.style.cssText = `
            height: 100%;
            background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
            width: 0%;
            transition: width 0.3s ease;
        `;
      const progress_overlay = document.createElement("div");
      progress_overlay.style.cssText = `
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            display: flex;
            align-items: center;
            justify-content: center;
            color: white;
            font-weight: bold;
            font-size: 12px;
            text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
        `;
      progress_overlay.id = "tieba-progress-overlay";
      progress_overlay.textContent = "0%";
      this.progress_text = document.createElement("div");
      this.progress_text.style.cssText = `
            text-align: center;
            color: #999;
            font-size: 14px;
            margin-top: 10px;
        `;
      progress_bg.appendChild(this.progress_bar);
      progress_bg.appendChild(progress_overlay);
      progress_container.appendChild(progress_bg);
      content.appendChild(title);
      content.appendChild(this.summary_text);
      content.appendChild(progress_container);
      content.appendChild(this.progress_text);
      this.modal.appendChild(content);
      document.body.appendChild(this.modal);
    }
    /**
     * Show modal with summary info
     */
    show(post, image_count) {
      this.summary_text.innerHTML = `
            <div><strong>\u8D34\u5B50\u6807\u9898:</strong> ${post.title}</div>
            <div><strong>\u8D34\u5427:</strong> ${post.tieba_name}</div>
            <div><strong>\u697C\u5C42:</strong> ${post.floor_list.length}</div>
            <div><strong>\u56DE\u590D:</strong> ${post.comment_cnt}</div>
            <div><strong>\u56FE\u7247:</strong> ${image_count}</div>
        `;
      this.modal.style.display = "flex";
      this.progress_text.textContent = "\u6B63\u5728\u4E0B\u8F7D\u56FE\u7247...";
    }
    /**
     * Update progress bar
     */
    update_progress(current, total, filename) {
      const percentage = Math.round(current / total * 100);
      this.progress_bar.style.width = `${percentage}%`;
      const overlay = document.getElementById("tieba-progress-overlay");
      if (overlay) {
        overlay.textContent = `${percentage}%`;
      }
      this.progress_text.textContent = `\u6B63\u5728\u4E0B\u8F7D: ${filename} (${current}/${total})`;
    }
    /**
     * Hide modal
     */
    hide() {
      this.modal.style.display = "none";
    }
    /**
     * Show complete message
     */
    show_complete(filename) {
      this.progress_text.textContent = `\u2705 \u5907\u4EFD\u5B8C\u6210! \u6B63\u5728\u4E0B\u8F7D: ${filename}`;
      this.progress_bar.style.width = "100%";
      const overlay = document.getElementById("tieba-progress-overlay");
      if (overlay) {
        overlay.textContent = "100%";
      }
    }
  };
  var download_modal = null;
  async function backup_post() {
    const backup_button = document.getElementById("tieba-backup-btn");
    try {
      const parser = new Parser();
      if (backup_button) {
        backup_button.textContent = "\u5907\u4EFD\u4E2D...";
        backup_button.disabled = true;
      }
      const post = await parser.parse_post();
      if (!download_modal) {
        download_modal = new DownloadModal();
      }
      const image_handler = parser.get_image_handler();
      const image_map = image_handler.image_map || {};
      const image_count = Object.keys(image_map).length;
      download_modal.show(post, image_count);
      const image_blobs = await image_handler.download_images((current, total, filename2) => {
        if (download_modal) {
          download_modal.update_progress(current, total, filename2);
        }
      });
      const viewer_html = image_handler.create_viewer_html(post);
      const json_data = JSON.stringify(post, null, 2);
      const files = {
        "post.json": json_data,
        "viewer.html": viewer_html
      };
      for (const [name, blob] of Object.entries(image_blobs)) {
        files[`images/${name}`] = blob;
      }
      const tar_blob = await image_handler.create_tar(files);
      const filename = `${post.tieba_name}_${post.id}.tar`;
      if (download_modal) {
        download_modal.show_complete(filename);
      }
      await new Promise((resolve) => setTimeout(resolve, 500));
      image_handler.download_blob(tar_blob, filename);
      setTimeout(() => {
        if (download_modal) {
          download_modal.hide();
        }
      }, 2e3);
      if (backup_button) {
        backup_button.textContent = "\u5907\u4EFD\u672C\u8D34";
        backup_button.disabled = false;
      }
      return post;
    } catch (error) {
      if (error instanceof Error && error.message === "NAVIGATING") {
        return void 0;
      }
      if (error instanceof Error) {
        const error_info = {
          message: error.message,
          stack: error.stack,
          timestamp: (/* @__PURE__ */ new Date()).toISOString(),
          url: window.location.href,
          user_agent: navigator.userAgent
        };
        window.GM_setValue("last_error", error_info);
      }
      if (download_modal) {
        download_modal.hide();
      }
      if (backup_button) {
        backup_button.textContent = "\u5907\u4EFD\u5931\u8D25";
        setTimeout(() => {
          backup_button.textContent = "\u5907\u4EFD\u672C\u8D34";
          backup_button.disabled = false;
        }, 2e3);
      }
      show_view_error_button();
      throw error;
    }
  }
  function inject_backup_button() {
    if (document.getElementById("tieba-backup-btn")) {
      return;
    }
    const container = document.createElement("div");
    container.id = "tieba-backup-container";
    container.style.cssText = `
        position: fixed;
        top: 100px;
        right: 20px;
        z-index: 9999;
        display: flex;
        flex-direction: column;
        gap: 10px;
    `;
    const backup_button = document.createElement("button");
    backup_button.id = "tieba-backup-btn";
    backup_button.textContent = "\u5907\u4EFD\u672C\u8D34";
    backup_button.style.cssText = `
        padding: 10px 20px;
        background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
        color: white;
        border: none;
        border-radius: 8px;
        cursor: pointer;
        font-size: 14px;
        font-weight: bold;
        box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
        transition: all 0.3s ease;
    `;
    backup_button.addEventListener("mouseenter", () => {
      backup_button.style.transform = "translateY(-2px)";
      backup_button.style.boxShadow = "0 6px 12px rgba(0, 0, 0, 0.15)";
    });
    backup_button.addEventListener("mouseleave", () => {
      backup_button.style.transform = "translateY(0)";
      backup_button.style.boxShadow = "0 4px 6px rgba(0, 0, 0, 0.1)";
    });
    backup_button.addEventListener("click", () => {
      backup_post();
    });
    const reset_button = document.createElement("button");
    reset_button.id = "tieba-reset-btn";
    reset_button.textContent = "\u91CD\u7F6E\u72B6\u6001";
    reset_button.style.cssText = `
        padding: 8px 16px;
        background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
        color: white;
        border: none;
        border-radius: 6px;
        cursor: pointer;
        font-size: 12px;
        font-weight: bold;
        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
        transition: all 0.3s ease;
        opacity: 0.8;
    `;
    reset_button.addEventListener("mouseenter", () => {
      reset_button.style.transform = "translateY(-2px)";
      reset_button.style.boxShadow = "0 4px 8px rgba(0, 0, 0, 0.15)";
      reset_button.style.opacity = "1";
    });
    reset_button.addEventListener("mouseleave", () => {
      reset_button.style.transform = "translateY(0)";
      reset_button.style.boxShadow = "0 2px 4px rgba(0, 0, 0, 0.1)";
      reset_button.style.opacity = "0.8";
    });
    reset_button.addEventListener("click", () => {
      const parser = new Parser();
      parser.reset_parsing();
      reset_button.textContent = "\u5DF2\u91CD\u7F6E";
      setTimeout(() => {
        reset_button.textContent = "\u91CD\u7F6E\u72B6\u6001";
      }, 1e3);
    });
    container.appendChild(backup_button);
    container.appendChild(reset_button);
    document.body.appendChild(container);
  }
  function auto_resume_if_needed() {
    const parser = new Parser();
    if (parser.is_parsing_in_progress()) {
      const reset_button = document.getElementById("tieba-reset-btn");
      if (reset_button) {
        reset_button.style.opacity = "1";
        reset_button.style.fontWeight = "bold";
      }
      setTimeout(() => {
        if (parser.is_parsing_in_progress()) {
          backup_post();
        }
      }, 2e3);
      return true;
    }
    return false;
  }
  function show_view_error_button() {
    if (document.getElementById("tieba-view-error-btn")) {
      return;
    }
    const container = document.getElementById("tieba-backup-container");
    if (!container) {
      return;
    }
    const error_button = document.createElement("button");
    error_button.id = "tieba-view-error-btn";
    error_button.textContent = "\u67E5\u770B\u9519\u8BEF";
    error_button.style.cssText = `
        padding: 8px 16px;
        background: #ff4444;
        color: white;
        border: none;
        border-radius: 4px;
        cursor: pointer;
        font-size: 12px;
        font-weight: bold;
        opacity: 0.9;
    `;
    error_button.addEventListener("click", () => {
      const error_data = window.GM_getValue("last_error", void 0);
      if (!error_data) {
        alert("\u6CA1\u6709\u9519\u8BEF\u4FE1\u606F");
        return;
      }
      const error_text = `
\u9519\u8BEF\u4FE1\u606F: ${error_data.message}

\u65F6\u95F4: ${error_data.timestamp}

\u9875\u9762: ${error_data.url}

\u6D4F\u89C8\u5668: ${error_data.user_agent}

\u5806\u6808:
${error_data.stack}
        `.trim();
      alert(error_text);
    });
    const reset_button = document.getElementById("tieba-reset-btn");
    if (reset_button) {
      container.insertBefore(error_button, reset_button);
    } else {
      container.appendChild(error_button);
    }
    setTimeout(() => {
      error_button.remove();
    }, 3e4);
  }
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", () => {
      inject_backup_button();
      auto_resume_if_needed();
    });
  } else {
    inject_backup_button();
    auto_resume_if_needed();
  }
  window.backup_post = backup_post;
})();