番茄小说_我坚信

https://fanqienovel.com

zpccool (13551) 16小时前 下载:394

小说 免费 小说 免费小说 API
番茄小说书源(精简优化版)— 搜索、正文、详情均已稳定
二维码导入(APP尚未完成该功能)
// @name        番茄小说_我坚信
// @uuid        fanqiexiaoshuowojianxin
// @version     1.0.0
// @updateUrl   http://aliyun.18638642193.cn/api/sources/user:9344bc4c-5d88-44ab-a66f-e064534551fc/download
// @author      AI
// @url         https://fanqienovel.com
// @logo        https://fanqienovel.com/favicon.ico
// @enabled     true
// @tags        免费,小说,免费小说,API
// @description 番茄小说书源(精简优化版)— 搜索、正文、详情均已稳定

// ─── 内置测试 ─────────────────────────────────────────────────────────────
async function TEST(type) {
  if (type === "__list__") return ["search", "explore", "bookInfo", "chapterList", "chapterContent"];

  if (type === "search") {
    var results = await search("斗破苍稹", 1);
    if (!results || results.length < 1) return { passed: false, message: "搜索结果为空" };
    return { passed: true, message: '搜索"斗破苍稹"返回 ' + results.length + " 条结果 ✓" };
  }

  if (type === "explore") {
    var books = await explore(1, "玄幻");
    if (!books || books.length < 1) return { passed: false, message: "发现页 [玄幻] 返回为空" };
    return { passed: true, message: "发现页 [玄幻]: " + books.length + " 条结果 ✓" };
  }

  if (type === "bookInfo") {
    var r = await bookInfo("https://reading.snssdk.com/reading/bookapi/detail/v/?book_id=7589673170848713752");
    return { passed: !!r.name, message: "bookInfo name=" + r.name };
  }

  if (type === "chapterList") {
    var r = await chapterList("https://fanqienovel.com/api/reader/directory/detail?bookId=7589673170848713752");
    return { passed: r.length > 0, message: "chapterList cnt=" + r.length + " first=" + (r[0] ? r[0].name : "N/A") };
  }

  if (type === "chapterContent") {
    var r = await chapterContent("7589673176875942424");
    return { passed: r.length > 10, message: "chapterContent len=" + r.length + " first=" + r.substring(0, 30) };
  }

  return { passed: false, message: "未知测试类型: " + type };
}

// ─── 配置 ────────────────────────────────────────────────────────────────
var SIGN_HOST = "https://sg.mgz.la";
var SIGN_USER = "fq0329";
var SIGN_AUTH = "1337b73b7ddf1ed88d0d31a6bd6b2ee6db15f2b8";
var API_HOST = "https://reading.snssdk.com";
var WEB_HOST = "https://fanqienovel.com";
var NOVEL_HOST = "https://novel.snssdk.com";
var BOOK_HOST = "https://fq-book.netsite.cc";

var FQ_HEADERS = {
  "User-Agent": "Mozilla/5.0 (Linux; Android 10.0; wv) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/4.0 Chrome/58.0.3029.110 Mobile Safari/537.36 T7/10.3 SearchCraft/2.6.2 (Baidu; P1 7.0)",
  "Accept": "application/json, text/plain, */*",
  "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
  "Accept-Encoding": "gzip, deflate, br",
  "Connection": "keep-alive",
  "Cache-Control": "no-cache",
  "Pragma": "no-cache",
  "Sec-Fetch-Dest": "empty",
  "Sec-Fetch-Mode": "cors",
  "Sec-Fetch-Site": "same-site",
  "Upgrade-Insecure-Requests": "1",
};

// ─── JSON / HTTP 工具 ─────────────────────────────────────────────────────
function jsonStringifyAscii(value) {
  return JSON.stringify(value).replace(/[\u007f-\uffff]/g, function (ch) {
    var hex = ch.charCodeAt(0).toString(16);
    return "\\u" + ("0000" + hex).slice(-4);
  });
}

function safeDecodeUrlValue(value) {
  if (value === undefined || value === null) return "";
  try {
    return decodeURIComponent(String(value).replace(/\+/g, "%20"));
  } catch (e) {
    return String(value);
  }
}

var JSON_HEADERS = {
  "Content-Type": "application/json; charset=utf-8",
};

// ─── 设备管理 ─────────────────────────────────────────────────────────────
function randomHex(n) {
  var chars = "0123456789abcdef";
  var result = "";
  for (var i = 0; i < n; i++) {
    result += chars.charAt(Math.floor(Math.random() * 16));
  }
  return result;
}

function formatTimestamp(ts) {
  var d = new Date(ts);
  var pad = function (n) {
    return n < 10 ? "0" + n : "" + n;
  };
  return d.getFullYear() + "-" + pad(d.getMonth() + 1) + "-" + pad(d.getDate()) + " " + pad(d.getHours()) + ":" + pad(d.getMinutes()) + ":" + pad(d.getSeconds());
}

function loadDevice() {
  var raw = legado.config.read("booksource", "fanqie_device");
  if (!raw) return null;
  try {
    var dev = JSON.parse(raw);
    if (dev && dev.time && Date.now() - dev.time < 30 * 24 * 60 * 60 * 1000) {
      return dev;
    }
  } catch (e) {}
  return null;
}

function saveDevice(dev) {
  legado.config.write("booksource", "fanqie_device", JSON.stringify(dev));
}

function formatVersion(code, a, b, c) {
  var s = String(code);
  if (!a) a = 1;
  if (!b) b = 2;
  if (!c) c = 3;
  var parts = [];
  var idx = 0;
  var lens = [a, b, c];
  for (var i = 0; i < lens.length; i++) {
    if (idx + lens[i] <= s.length) {
      parts.push(parseInt(s.substring(idx, idx + lens[i]), 10));
      idx += lens[i];
    }
  }
  if (idx < s.length) {
    parts.push(parseInt(s.substring(idx), 10));
  }
  return parts.join(".");
}

async function registerDevice() {
  legado.log("[registerDevice] 开始设备注册...");
  var oaid = randomHex(16);
  var udid = randomHex(16);
  var regDevice = {
    oaid: oaid,
    openudid: udid,
    device_brand: "Xiaomi",
    device_model: "MI 9",
    os_api: 30,
    os_version: "11",
    rom_version: "V12.5.7.0.RFACNXM",
    version: "63932",
    version_str: formatVersion("63932", 1, 2, 3),
    aid: "1967",
    channel: "oppo_1967_64",
    display_name: "番茄免费小说",
    package: "com.dragon.read",
    app_name: "novelapp",
  };
  var buildBody = jsonStringifyAscii({
    user: SIGN_USER,
    auth: SIGN_AUTH,
    device: regDevice,
  });
  var buildResp = await legado.http.post(SIGN_HOST + "/api/device/build-register", buildBody, JSON_HEADERS);
  var buildData = JSON.parse(buildResp);
  if (!buildData || !buildData.data) {
    legado.log("[registerDevice] build-register 失败: " + buildResp);
    return null;
  }
  var regUrl = buildData.data.url;
  var regBody = buildData.data.options.body;
  var regHeaders = buildData.data.options.headers;
  var device = buildData.data.device;
  var regResp = await legado.http.postBinary(regUrl, regBody, regHeaders);
  var regResult = JSON.parse(regResp);
  if (!regResult || !regResult.device_id_str) {
    legado.log("[registerDevice] device_register 失败: " + regResp);
    return null;
  }
  device.iid = regResult.install_id_str;
  device.device_id = regResult.device_id_str;
  device.device_token = regResult.device_token || "";
  device.klink_egdi = regResult.klink_egdi || "";
  device.time = Date.now();
  legado.log("[registerDevice] 设备ID: " + device.device_id);
  var actBody = jsonStringifyAscii({
    user: SIGN_USER,
    auth: SIGN_AUTH,
    device: device,
  });
  var actResp = await legado.http.post(SIGN_HOST + "/api/device/build-activate", actBody, JSON_HEADERS);
  var actData = JSON.parse(actResp);
  if (actData && actData.data && actData.data.url) {
    var actHeaders = actData.data.options && actData.data.options.headers ? actData.data.options.headers : {};
    await legado.http.get(actData.data.url, actHeaders);
  }
  saveDevice(device);
  legado.log("[registerDevice] 注册完成");
  return device;
}

async function getDevice() {
  var dev = loadDevice();
  if (dev) return dev;
  return await registerDevice();
}

// ─── 签名请求(仅用于详情页备用) ─────────────────────────────────────
async function signedApiGet(path, params) {
  var device = await getDevice();
  if (!device) {
    legado.log("[signedApiGet] 无法获取设备信息");
    return null;
  }

  var fullUrl = API_HOST + path + (params ? "?" + params : "");

  var paramsObj = {};
  if (params) {
    var pairs = params.split("&");
    for (var i = 0; i < pairs.length; i++) {
      var pair = pairs[i];
      if (!pair) continue;
      var eqIdx = pair.indexOf("=");
      if (eqIdx > 0) {
        var key = pair.substring(0, eqIdx);
        var val = pair.substring(eqIdx + 1);
        paramsObj[safeDecodeUrlValue(key)] = safeDecodeUrlValue(val);
      } else {
        paramsObj[safeDecodeUrlValue(pair)] = "";
      }
    }
  }

  var signBody = jsonStringifyAscii({
    user: SIGN_USER,
    auth: SIGN_AUTH,
    url: fullUrl,
    params: paramsObj,
    device: device,
    body: null,
    cookie: "",
    header: null,
  });

  var signResp = await legado.http.post(SIGN_HOST + "/api/sign", signBody, JSON_HEADERS);
  var signData = JSON.parse(signResp);

  if (!signData || !signData.data) {
    legado.log("[signedApiGet] 签名失败");
    return null;
  }

  var signedUrl = signData.data.url;
  var signedHeaders = signData.data.options.headers || {};

  var resp = await legado.http.get(signedUrl, signedHeaders);
  return JSON.parse(resp);
}

// ─── 搜索(仅 fq-book)───────────────────────────────────────────────
async function search(keyword, page) {
  legado.log("[search] keyword=" + keyword + " page=" + page);

  var url = BOOK_HOST + "/search?query=" + encodeURIComponent(keyword) + "&page=" + (page || 1);
  var resp = await legado.http.get(url, FQ_HEADERS);
  if (!resp || resp.length === 0) return [];

  var data = JSON.parse(resp);
  if (!data || !data.data) return [];

  return parseSearchResults(data.data);
}

// 遍历所有 search_tabs 提取结果
function parseSearchResults(data) {
  var books = [];
  var seenIds = {};

  if (data.search_tabs && Array.isArray(data.search_tabs)) {
    for (var t = 0; t < data.search_tabs.length; t++) {
      var tab = data.search_tabs[t];
      if (tab && tab.data && Array.isArray(tab.data)) {
        for (var i = 0; i < tab.data.length; i++) {
          var item = tab.data[i];
          var bookList = [];
          if (item.book_data && Array.isArray(item.book_data)) {
            bookList = item.book_data;
          } else if (item.book_info) {
            bookList = [item.book_info];
          }
          for (var j = 0; j < bookList.length; j++) {
            var b = bookList[j];
            if (!b || !b.book_id) continue;
            if (seenIds[b.book_id]) continue;
            seenIds[b.book_id] = true;
            books.push({
              name: (b.book_name || b.title || "").replace(/<\/?em>/g, "").replace(/《|》/g, ""),
              author: (b.author || "").replace(/<em>|<\/em>/g, ""),
              bookUrl: BOOK_HOST + "/info?book_id=" + b.book_id,
              coverUrl: b.thumb_url || "",
              intro: b.abstract || "",
              kind: (b.category || "") + " " + (b.tags || ""),
            });
          }
        }
      }
    }
  }

  // 旧格式兼容
  if (books.length === 0) {
    var items = data.ret_data || data.book_data || data.book_info || [];
    if (!Array.isArray(items)) {
      if (typeof items === 'object' && items !== null) items = [items];
      else items = [];
    }
    for (var k = 0; k < items.length; k++) {
      var item2 = items[k];
      var b2 = item2;
      if (item2.book_data && Array.isArray(item2.book_data) && item2.book_data.length > 0) {
        b2 = item2.book_data[0];
      } else if (item2.book_info) {
        b2 = item2.book_info;
      }
      if (!b2 || !b2.book_id) continue;
      if (seenIds[b2.book_id]) continue;
      seenIds[b2.book_id] = true;
      books.push({
        name: (b2.book_name || b2.title || "").replace(/<\/?em>/g, "").replace(/《|》/g, ""),
        author: (b2.author || "").replace(/<em>|<\/em>/g, ""),
        bookUrl: BOOK_HOST + "/info?book_id=" + b2.book_id,
        coverUrl: b2.thumb_url || "",
        intro: b2.abstract || "",
        kind: (b2.category || "") + " " + (b2.tags || ""),
      });
    }
  }
  return books;
}

// ─── 书籍详情 ────────────────────────────────────────────────────
async function bookInfo(bookUrl) {
  legado.log("[bookInfo] url=" + bookUrl);
  var m = bookUrl.match(/book_id=(\d+)/);
  if (!m) {
    return { name: "", author: "", coverUrl: "", intro: "", lastChapter: "", kind: "", tocUrl: bookUrl };
  }
  var bookId = m[1];

  // 方案1: fq-book
  try {
    var url = BOOK_HOST + "/info?book_id=" + bookId;
    var resp = await legado.http.get(url, FQ_HEADERS);
    var data = JSON.parse(resp);
    if (data && data.data && data.data.data) {
      return parseBookInfoDetail(data.data.data, bookId);
    }
  } catch (e) {
    legado.log("[bookInfo] fq-book 请求失败: " + e.message);
  }

  // 方案2: 签名接口
  try {
    var params = "book_id=" + bookId;
    var signedData = await signedApiGet("/reading/bookapi/detail/v/", params);
    if (signedData && signedData.data) {
      return parseBookInfoDetail(signedData.data, bookId);
    }
  } catch (e) {
    legado.log("[bookInfo] signedApi 请求失败: " + e.message);
  }

  return { name: "", author: "", coverUrl: "", intro: "", lastChapter: "", kind: "", tocUrl: bookUrl };
}

function parseBookInfoDetail(d, bookId) {
  var status = "";
  if (d.creation_status === 0) status = "连载";
  else if (d.creation_status === 1) status = "完结";
  else if (d.creation_status === 4) status = "断更";

  var kindParts = [];
  if (status) kindParts.push(status);
  if (d.category) kindParts.push(d.category);
  if (d.tags) kindParts.push(d.tags);
  if (d.score && d.score > 0) kindParts.push(d.score + "分");

  return {
    name: (d.book_name || d.name || "").replace(/<\/?em>/g, "").replace(/《|》/g, ""),
    author: (d.author || "").replace(/<em>|<\/em>/g, ""),
    coverUrl: d.thumb_url || d.cover || "",
    intro: (d.abstract || d.intro || "").trim(),
    lastChapter: d.last_chapter_title || "",
    kind: kindParts.join(","),
    tocUrl: WEB_HOST + "/api/reader/directory/detail?bookId=" + bookId,
    wordCount: d.word_number || "",
  };
}

// ─── 章节列表 ─────────────────────────────────────────────────────────
async function chapterList(tocUrl) {
  legado.log("[chapterList] url=" + tocUrl);
  var bookIdInUrl = tocUrl.match(/book_id=(\d+)/);
  if (bookIdInUrl) {
    tocUrl = WEB_HOST + "/api/reader/directory/detail?bookId=" + bookIdInUrl[1];
  }
  var resp = await legado.http.get(tocUrl, {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
  });
  var data = JSON.parse(resp);
  if (!data || !data.data) return [];
  var chapters = [];
  var volumeList = data.data.chapterListWithVolume;
  if (volumeList && volumeList.length > 0) {
    for (var vi = 0; vi < volumeList.length; vi++) {
      var vol = volumeList[vi];
      for (var ci = 0; ci < vol.length; ci++) {
        var ch = vol[ci];
        chapters.push({
          name: ch.title || "第" + (chapters.length + 1) + "章",
          url: ch.itemId || "",
        });
      }
    }
  } else {
    var ids = data.data.allItemIds || [];
    for (var i = 0; i < ids.length; i++) {
      chapters.push({
        name: "第" + (i + 1) + "章",
        url: ids[i],
      });
    }
  }
  return chapters;
}

// ─── 正文内容(仅 fq-book /content)───────────────────────────────────
async function chapterContent(chapterUrl) {
  legado.log("[content] chapterUrl=" + chapterUrl);

  var itemId = "";
  if (/^\d+$/.test(chapterUrl)) {
    itemId = chapterUrl;
  } else if (chapterUrl.match(/itemId=(\d+)/)) {
    itemId = chapterUrl.match(/itemId=(\d+)/)[1];
  } else if (chapterUrl.match(/item_id=(\d+)/)) {
    itemId = chapterUrl.match(/item_id=(\d+)/)[1];
  } else {
    legado.log("[content] 无法提取itemId");
    return "";
  }

  var url = BOOK_HOST + "/content?item_id=" + itemId;
  try {
    var resp = await legado.http.get(url, FQ_HEADERS);
    if (resp && resp.length > 0) {
      var data = JSON.parse(resp);
      var content = deepFindContent(data);
      if (content) {
        content = content.replace(/##收听有声版[\s\S]*$/, "");
        content = formatContent(content);
        legado.log("[content] 成功 length=" + content.length);
        return content;
      }
    }
  } catch (e) {
    legado.log("[content] 请求失败: " + e.message);
  }
  return "";
}

// 递归查找content字段
function deepFindContent(obj) {
  if (typeof obj !== 'object' || obj === null) return null;
  if (obj.content && typeof obj.content === 'string') return obj.content;
  for (var key in obj) {
    var val = obj[key];
    if (typeof val === 'object') {
      var found = deepFindContent(val);
      if (found) return found;
    }
  }
  return null;
}

function extractContent(html) {
  var text = html
    .replace(/<\?xml[^?]*\?>\s*/g, "")
    .replace(/<!DOCTYPE[^>]*>\s*/g, "")
    .replace(/<\/?html[^>]*>/g, "")
    .replace(/<head[^>]*>[\s\S]*?<\/head>/g, "")
    .replace(/<\/?body[^>]*>/g, "");
  text = text.replace(/<h1[^>]*>[\s\S]*?<\/h1>/g, "");
  text = text.replace(/<tt_keyword_ad[\s\S]*?<\/tt_keyword_ad>/g, "");
  var paragraphs = [];
  var pRegex = /<p[^>]*>([\s\S]*?)<\/p>/g;
  var match;
  while ((match = pRegex.exec(text)) !== null) {
    var pText = match[1]
      .replace(/<[^>]+>/g, "")
      .replace(/&nbsp;/g, " ")
      .replace(/&lt;/g, "<")
      .replace(/&gt;/g, ">")
      .replace(/&amp;/g, "&")
      .replace(/&quot;/g, '"')
      .trim();
    if (pText) paragraphs.push(pText);
  }
  if (paragraphs.length === 0) {
    text = text.replace(/<[^>]+>/g, "").trim();
    if (text) paragraphs.push(text);
  }
  return paragraphs.join("\n\n");
}

function formatContent(content) {
  if (!content) return "";
  if (/<[^>]+>/.test(content)) {
    return extractContent(content);
  }
  content = content
    .replace(/&nbsp;/g, " ")
    .replace(/&lt;/g, "<")
    .replace(/&gt;/g, ">")
    .replace(/&amp;/g, "&")
    .replace(/&quot;/g, '"')
    .replace(/&#39;/g, "'")
    .replace(/&apos;/g, "'");
  var paragraphs = content
    .split(/\n{2,}|\r\n{2,}/)
    .map(function(p) { return p.trim(); })
    .filter(function(p) { return p.length > 0; });
  if (paragraphs.length === 0 && content.trim()) {
    paragraphs.push(content.trim());
  }
  return paragraphs.join("\n\n");
}

// ─── 设置管理 ────────────────────────────────────────────────────────
var DEFAULT_SETTINGS = {
  gender: "",
  algo: "204",
  limit: "30",
  contentProxy: "0",
};

function loadSettings() {
  var raw = legado.config.read("booksource", "fanqie_settings");
  if (!raw) return DEFAULT_SETTINGS;
  try {
    var s = JSON.parse(raw);
    return {
      gender: s.gender || DEFAULT_SETTINGS.gender,
      algo: s.algo || DEFAULT_SETTINGS.algo,
      limit: s.limit || DEFAULT_SETTINGS.limit,
      contentProxy: s.contentProxy || DEFAULT_SETTINGS.contentProxy,
    };
  } catch (e) {
    return DEFAULT_SETTINGS;
  }
}

// ─── 发现页 ──────────────────────────────────────────────────────────
var CATEGORY_MAP = {
  "都市": { category_id: 1, gender: 1 },
  "都市生活": { category_id: 2, gender: 1 },
  "玄幻": { category_id: 7, gender: 1 },
  "科幻": { category_id: 8, gender: 1 },
  "悬疑": { category_id: 10, gender: 1 },
  "乡村": { category_id: 11, gender: 1 },
  "仙侠": { category_id: 12, gender: 1 },
  "历史": { category_id: 13, gender: 1 },
  "游戏": { category_id: 14, gender: 1 },
  "奇幻": { category_id: 15, gender: 1 },
  "军事": { category_id: 16, gender: 1 },
  "灵异": { category_id: 17, gender: 1 },
  "同人": { category_id: 18, gender: 1 },
  "末世": { category_id: 19, gender: 1 },
  "轻小说": { category_id: 20, gender: 1 },
  "其他": { category_id: 21, gender: 1 },
  "古代言情": { category_id: 22, gender: 2 },
  "现代言情": { category_id: 23, gender: 2 },
  "青春校园": { category_id: 24, gender: 2 },
  "纯爱": { category_id: 25, gender: 2 },
  "幻想言情": { category_id: 26, gender: 2 },
  "悬疑推理": { category_id: 27, gender: 2 },
  "武侠": { category_id: 28, gender: 2 },
  "短篇": { category_id: 29, gender: 1 },
  "全本": { category_id: 30, gender: 1 },
};

async function explore(page, category) {
  if (!category || category === "GETALL") {
    return Object.keys(CATEGORY_MAP).concat(["⚙ 设置"]);
  }
  if (category === "⚙ 设置") {
    return buildSettingsHtml();
  }
  var catInfo = CATEGORY_MAP[category];
  if (!catInfo) return [];
  var limit = 100;
  var offset = ((page || 1) - 1) * limit;
  var url = NOVEL_HOST + "/api/novel/channel/homepage/new_category/book_list/v1/?" +
    "parent_enterfrom=novel_channel_category.tab.&aid=1967" +
    "&offset=" + offset +
    "&limit=" + limit +
    "&category_id=" + catInfo.category_id +
    "&gender=" + catInfo.gender;
  var resp = await legado.http.get(url);
  var data = JSON.parse(resp);
  if (!data || !data.data || !data.data.data) return [];
  var items = data.data.data;
  var books = [];
  for (var i = 0; i < items.length; i++) {
    var b = items[i];
    if (!b || !b.book_id) continue;
    books.push({
      name: b.book_name || "",
      author: b.author || "",
      bookUrl: BOOK_HOST + "/info?book_id=" + b.book_id,
      coverUrl: b.thumb_url || "",
      lastChapter: "",
      kind: (b.category || "") + " " + (b.creation_status === 0 ? "连载" : "完结"),
    });
  }
  return books;
}

// ─── 设置页(保持不变)───────────────────────────────────────────────
function buildSettingsHtml() {
  var defaultsJson = JSON.stringify(DEFAULT_SETTINGS);
  var content = html`
<div class="settings-root">
  <div class="card mb-sm">
    <label class="card-title">阅读偏好</label>
    <div class="flex flex-wrap gap-sm">
      <button class="pref-btn" data-key="gender" data-val="">不限</button>
      <button class="pref-btn" data-key="gender" data-val="1">男生</button>
      <button class="pref-btn" data-key="gender" data-val="0">女生</button>
    </div>
    <p class="text-sm text-secondary mt-sm">影响混合分类(玄幻/仙侠/都市等)的推荐内容</p>
  </div>
  <div class="card mb-sm">
    <label class="card-title">推荐榜单</label>
    <div class="flex flex-wrap gap-sm">
      <button class="pref-btn" data-key="algo" data-val="101">推荐榜</button>
      <button class="pref-btn" data-key="algo" data-val="100">完本榜</button>
      <button class="pref-btn" data-key="algo" data-val="200">巅峰榜</button>
      <button class="pref-btn" data-key="algo" data-val="103">热搜榜</button>
      <button class="pref-btn" data-key="algo" data-val="204">新书榜</button>
      <button class="pref-btn" data-key="algo" data-val="601">短篇榜</button>
      <button class="pref-btn" data-key="algo" data-val="156">抖音榜</button>
    </div>
    <p class="text-sm text-secondary mt-sm">决定各分类下展示的排行方式</p>
  </div>
  <div class="card mb-sm">
    <label class="card-title">每页数量</label>
    <div class="flex flex-wrap gap-sm">
      <button class="pref-btn" data-key="limit" data-val="20">20</button>
      <button class="pref-btn" data-key="limit" data-val="30">30</button>
      <button class="pref-btn" data-key="limit" data-val="50">50</button>
    </div>
  </div>
  <div class="card mb-sm">
    <label class="card-title">正文代理服务器</label>
    <div class="flex flex-wrap gap-sm">
      <button class="pref-btn" data-key="contentProxy" data-val="0">gofq 优先</button>
      <button class="pref-btn" data-key="contentProxy" data-val="1">pyfq 优先</button>
    </div>
    <p class="text-sm text-secondary mt-sm">章节正文获取服务器的优先顺序</p>
  </div>
  <div class="card mb-sm">
    <label class="card-title">设备与缓存</label>
    <div class="flex gap-sm">
      <button onclick="resetDevice()" style="flex:1;">重新注册设备</button>
      <button onclick="showDeviceInfo()" style="flex:1;">查看设备信息</button>
    </div>
  </div>
  <div id="device-info" class="card mb-sm" style="display:none; grid-column:1/-1;">
    <pre id="device-info-content" class="text-sm" style="white-space:pre-wrap; word-break:break-all;"></pre>
  </div>
</div>
<style>
  .settings-root {
    max-width: 960px;
    margin: 0 auto;
    display: grid;
    grid-template-columns: 1fr;
    gap: 0;
  }
  @media (min-width: 560px) {
    .settings-root {
      grid-template-columns: 1fr 1fr;
      gap: 8px;
    }
    .settings-root > h2 {
      grid-column: 1 / -1;
    }
  }
  .card-title {
    font-weight: 600;
    display: block;
    margin-bottom: 6px;
  }
  .pref-btn {
    transition: all 0.15s;
  }
  .pref-btn.active {
    background: var(--primary) !important;
    border-color: var(--primary) !important;
    color: #fff !important;
  }
</style>
<script>
var currentSettings = {};
var DEFAULTS = ${defaultsJson};
async function init() {
  try {
    var raw = await legado.callSource("getSettings");
    currentSettings = typeof raw === "string" ? JSON.parse(raw) : raw;
  } catch(e) {
    currentSettings = Object.assign({}, DEFAULTS);
  }
  updateUI();
}
function updateUI() {
  document.querySelectorAll(".pref-btn").forEach(function(btn) {
    var key = btn.getAttribute("data-key");
    var val = btn.getAttribute("data-val");
    var current = currentSettings[key] !== undefined ? String(currentSettings[key]) : (DEFAULTS[key] || "");
    btn.classList.toggle("active", val === current);
  });
}
document.addEventListener("click", async function(e) {
  var btn = e.target.closest(".pref-btn");
  if (!btn) return;
  var key = btn.getAttribute("data-key");
  var val = btn.getAttribute("data-val");
  currentSettings[key] = val;
  updateUI();
  try {
    await legado.callSource("saveSettings", JSON.stringify(currentSettings));
    legado.toast("已保存: " + btn.textContent.trim(), "success");
  } catch(e) {
    legado.toast("保存失败: " + e.message, "error");
  }
});
async function resetDevice() {
  var btn = document.querySelector("[onclick*=resetDevice]");
  if (!btn._confirm) {
    btn._confirm = true;
    btn.textContent = "⚠ 确定重置?再次点击确认";
    btn.style.borderColor = "var(--primary)";
    setTimeout(function() {
      btn._confirm = false;
      btn.textContent = "重新注册设备";
      btn.style.borderColor = "";
    }, 3000);
    return;
  }
  btn._confirm = false;
  btn.textContent = "重新注册设备";
  btn.style.borderColor = "";
  try {
    await legado.callSource("resetDeviceInfo");
    legado.toast("设备信息已清除,下次请求时将自动重新注册", "success");
  } catch(e) {
    legado.toast("清除失败: " + e.message, "error");
  }
}
async function showDeviceInfo() {
  var el = document.getElementById("device-info");
  var content = document.getElementById("device-info-content");
  if (el.style.display !== "none") {
    el.style.display = "none";
    return;
  }
  try {
    var raw = await legado.callSource("getDeviceInfo");
    content.textContent = typeof raw === "string" ? raw : JSON.stringify(raw, null, 2);
  } catch(e) {
    content.textContent = "无法获取设备信息: " + e.message;
  }
  el.style.display = "block";
}
init();
</script>`;

  return {
    type: "html",
    html: content,
    title: "番茄小说设置",
  };
}

// ─── 设置页回调函数 ─────────────────────────────────────────────────
function getSettings() {
  var s = loadSettings();
  return JSON.stringify(s);
}

function saveSettings(settingsJson) {
  legado.config.write("booksource", "fanqie_settings", settingsJson);
  return "ok";
}

function resetDeviceInfo() {
  legado.config.write("booksource", "fanqie_device", "");
  return "ok";
}

function getDeviceInfo() {
  var raw = legado.config.read("booksource", "fanqie_device");
  if (!raw) return JSON.stringify({ status: "未注册" });
  try {
    var dev = JSON.parse(raw);
    return JSON.stringify(
      {
        device_id: dev.device_id || "无",
        iid: dev.iid || "无",
        device_brand: dev.device_brand || "无",
        device_model: dev.device_model || "无",
        version: dev.version_str || dev.version || "无",
        registered: dev.time ? formatTimestamp(dev.time) : "无",
        expires: dev.time ? formatTimestamp(dev.time + 30 * 24 * 60 * 60 * 1000) : "无",
      },
      null,
      2
    );
  } catch (e) {
    return JSON.stringify({ error: "解析失败", detail: String(e), raw: raw.substring(0, 200) });
  }
}
广告