码桶

发现社区成员的开源项目

飞扬 / LB_Down 公开
main
LB_Down/functions.php
functions.php75.3 KB
<?php
if (!defined('__TYPECHO_ROOT_DIR__')) exit;

define('LB_THEME_VERSION', '1.2.11');

function themeConfig(\Typecho\Widget\Helper\Form $form): void
{

    $cssUrl = lb_theme_url('admin/option.css') . '?v=' . LB_THEME_VERSION;
    $jsUrl = lb_theme_url('admin/option.js') . '?v=' . LB_THEME_VERSION;

    $ver = LB_THEME_VERSION;
    $lbSiteUrl = addslashes(\Typecho\Widget::widget('\Widget\Options')->siteUrl);

    echo <<<HTML
<link rel="stylesheet" href="{$cssUrl}">
<script src="{$jsUrl}" defer></script>

<div class="lb-wrap">
    <div class="lb-topbar">
        <div class="lb-brand">
            <span class="lb-logo"></span>
            <div class="lb-brand-text">
                <h2>LB Down 下载站主题</h2>
                <p>专业下载站主题 · 作者:小野 · <a href="https://lb5.net" target="_blank" rel="noopener">lb5.net</a> · PHP 8.0+</p>
            </div>
        </div>
        <div class="lb-topbar-actions">
            <span class="lb-version">v{$ver}</span>
        </div>
    </div>

    <div class="lb-tabs">
        <button type="button" class="lb-tab active" data-tab="basic">
            <span class="lb-tab-icon" data-icon="basic"></span>
            <span>基础设置</span>
        </button>
        <button type="button" class="lb-tab" data-tab="display">
            <span class="lb-tab-icon" data-icon="display"></span>
            <span>显示设置</span>
        </button>
        <button type="button" class="lb-tab" data-tab="beian">
            <span class="lb-tab-icon" data-icon="beian"></span>
            <span>备案信息</span>
        </button>
        <button type="button" class="lb-tab" data-tab="function">
            <span class="lb-tab-icon" data-icon="function"></span>
            <span>功能开关</span>
        </button>
        <button type="button" class="lb-tab" data-tab="mail">
            <span class="lb-tab-icon" data-icon="mail"></span>
            <span>邮件通知</span>
        </button>
        <button type="button" class="lb-tab" data-tab="other">
            <span class="lb-tab-icon" data-icon="other"></span>
            <span>其他设置</span>
        </button>
        <button type="button" class="lb-tab-save">
            <span class="lb-tab-icon" data-icon="save"></span>
            <span>保存设置</span>
        </button>
    </div>

    <div class="lb-panels">
        <div class="lb-panel active" data-panel="basic"></div>
        <div class="lb-panel" data-panel="beian"></div>
        <div class="lb-panel" data-panel="display"></div>
        <div class="lb-panel" data-panel="function"></div>
        <div class="lb-panel" data-panel="mail"></div>
        <div class="lb-panel" data-panel="other"></div>
    </div>
</div>
<script>window.LB_SITE_URL = '{$lbSiteUrl}';</script>
HTML;

    $GLOBALS['LB_FIELD_TABS'] = [
        'logoUrl'           => 'basic',
        'siteSubtitle'      => 'basic',
        'footerPowered'     => 'basic',
        'footerAbout'       => 'basic',
        'icp'               => 'beian',
        'icpUrl'            => 'beian',
        'govIc'             => 'beian',
        'govIcUrl'          => 'beian',
        'pageSize'          => 'display',
        'sidebarNum'        => 'display',
        'commentInfoEnable' => 'display',
        'commentInfoOptions' => 'display',
        'recEnable'         => 'display',
        'recNum'            => 'display',
        'recExcludeCats'    => 'display',
        'filterEnable'      => 'display',
        'filterCats'        => 'display',
        'listStyle'         => 'display',
        'newestCats'        => 'display',
        'enableDownloadCount' => 'function',
        'enableRating'      => 'function',
        'analyticsCode'     => 'other',
        'floatBarEnable'    => 'other',
        'floatQQ'           => 'other',
        'floatQQImg'        => 'other',
        'floatWxImg'        => 'other',
        'floatTodayUrl'     => 'other',
        'floatVipUrl'       => 'other',
        'floatAskUrl'       => 'other',
        'floatReportUrl'    => 'other',
        'lb_mail_switch'    => 'mail',
        'lb_mail_host'      => 'mail',
        'lb_mail_port'      => 'mail',
        'lb_mail_user'      => 'mail',
        'lb_mail_pass'      => 'mail',
        'lb_mail_from_name' => 'mail',
        'lb_mail_admin'     => 'mail',
        'lb_notify_feishu'   => 'mail',
        'lb_feishu_webhook'  => 'mail',
        'lb_notify_wecom'    => 'mail',
        'lb_wecom_webhook'   => 'mail',
        'lb_notify_dingtalk' => 'mail',
        'lb_dingtalk_webhook' => 'mail',
        'lb_notify_pushplus' => 'mail',
        'lb_pushplus_token'  => 'mail',
    ];

    $logoUrl = new \Typecho\Widget\Helper\Form\Element\Text(
        'logoUrl',
        null,
        null,
        _t('站点 Logo'),
        _t('填写图片 URL,留空则显示文字标题')
    );
    $form->addInput($logoUrl);

    $themeMode = new \Typecho\Widget\Helper\Form\Element\Select(
        'themeMode',
        [
            'light' => _t('浅色(默认)'),
            'dark'  => _t('深色'),
            'auto'  => _t('跟随系统')
        ],
        'light',
        _t('默认主题模式'),
        _t('首次访问时的主题;访客可用右上角按钮随时切换(记忆在本地)')
    );
    $form->addInput($themeMode);

    $siteSubtitle = new \Typecho\Widget\Helper\Form\Element\Text(
        'siteSubtitle',
        null,
        '专注于优质资源分享',
        _t('站点副标题'),
        _t('显示在 Logo 下方的描述文字')
    );
    $form->addInput($siteSubtitle);

    $pageSize = new \Typecho\Widget\Helper\Form\Element\Number(
        'pageSize',
        null,
        '20',
        _t('列表每页显示数量'),
        _t('首页、分类页、搜索页每页显示的资源数量')
    );
    $form->addInput($pageSize);

    $commentInfoEnable = new \Typecho\Widget\Helper\Form\Element\Select(
        'commentInfoEnable',
        [
            '1' => _t('开启'),
            '0' => _t('关闭')
        ],
        '1',
        _t('评论信息显示'),
        _t('控制评论区是否显示访客的 IP 属地、浏览器(UA)、操作系统等信息')
    );
    $form->addInput($commentInfoEnable);

    $commentInfoOptions = new \Typecho\Widget\Helper\Form\Element\Checkbox(
        'commentInfoOptions',
        [
            'ip'     => _t('IP 属地'),
            'ua'     => _t('浏览器(UA)'),
            'system' => _t('操作系统')
        ],
        ['ip', 'ua', 'system'],
        _t('评论信息组合'),
        _t('自由选择要显示的信息,可多选,如只选“IP 属地 + 操作系统”。注意:移动端仅显示 IP 属地,浏览器(UA)/操作系统只在电脑端显示')
    );
    $form->addInput($commentInfoOptions);

    $sidebarNum = new \Typecho\Widget\Helper\Form\Element\Number(
        'sidebarNum',
        null,
        '8',
        _t('侧边栏显示数量'),
        _t('热门下载、最新更新每个区块显示的数量')
    );
    $form->addInput($sidebarNum);

    $recEnable = new \Typecho\Widget\Helper\Form\Element\Select(
        'recEnable',
        [
            '1' => _t('开启'),
            '0' => _t('关闭')
        ],
        '1',
        _t('首页轮播'),
        _t('开启后首页顶部显示推荐轮播区域')
    );
    $form->addInput($recEnable);

    $recNum = new \Typecho\Widget\Helper\Form\Element\Number(
        'recNum',
        null,
        '6',
        _t('轮播显示数量'),
        _t('首页轮播最多显示的文章数量')
    );
    $form->addInput($recNum);

    $recExcludeCats = new \Typecho\Widget\Helper\Form\Element\Text(
        'recExcludeCats',
        null,
        null,
        _t('轮播排除分类'),
        _t('填写分类 ID,多个用英文逗号分隔,这些分类下的文章不会出现在轮播中。例如: 1,3,5')
    );
    $form->addInput($recExcludeCats);

    $filterEnable = new \Typecho\Widget\Helper\Form\Element\Select(
        'filterEnable',
        [
            '1' => _t('开启'),
            '0' => _t('关闭')
        ],
        '1',
        _t('筛选栏'),
        _t('开启后首页列表上方显示分类/授权/语言/排序筛选栏')
    );
    $form->addInput($filterEnable);

    $filterCats = new \Typecho\Widget\Helper\Form\Element\Text(
        'filterCats',
        null,
        '',
        _t('分类页筛选栏展示'),
        _t('填写分类 ID,多个用英文逗号分隔,例如:1,2,3,4。留空则所有分类页都显示筛选栏,不在列表中的分类页不显示筛选栏')
    );
    $form->addInput($filterCats);

    $listStyle = new \Typecho\Widget\Helper\Form\Element\Select(
        'listStyle',
        [
            'list' => _t('列表模式'),
            'card' => _t('卡片模式')
        ],
        'list',
        _t('列表布局样式'),
        _t('列表模式为传统横向列表,卡片模式为网格卡片布局带3D倾斜效果')
    );
    $form->addInput($listStyle);

    $paginationType = new \Typecho\Widget\Helper\Form\Element\Select(
        'paginationType',
        [
            'pager' => _t('传统分页符'),
            'scroll' => _t('滚动自动加载')
        ],
        'pager',
        _t('列表翻页方式'),
        _t('传统分页符:每页独立 URL,SEO 友好;滚动自动加载:滚动到列表底部自动加载下一页,无需翻页')
    );
    $form->addInput($paginationType);

    $floatBarEnable = new \Typecho\Widget\Helper\Form\Element\Select(
        'floatBarEnable',
        [
            '0' => _t('关闭'),
            '1' => _t('开启')
        ],
        '0',
        _t('悬浮侧栏'),
        _t('在页面右侧显示圆形悬浮按钮条(客服/二维码/链接等)')
    );
    $form->addInput($floatBarEnable);

    $floatQQ = new \Typecho\Widget\Helper\Form\Element\Text(
        'floatQQ',
        null,
        null,
        _t('QQ 客服号'),
        _t('填写 QQ 号,点击按钮会打开 QQ 临时会话。留空则不显示 QQ 按钮')
    );
    $form->addInput($floatQQ);

    $floatQQImg = new \Typecho\Widget\Helper\Form\Element\Text(
        'floatQQImg',
        null,
        null,
        _t('QQ 二维码图片 URL'),
        _t('填写二维码图片地址,鼠标悬停 QQ 按钮时展开显示。留空则不显示二维码')
    );
    $form->addInput($floatQQImg);

    $floatWxImg = new \Typecho\Widget\Helper\Form\Element\Text(
        'floatWxImg',
        null,
        null,
        _t('微信二维码图片 URL'),
        _t('填写微信二维码图片地址,将显示微信客服按钮,悬停展开二维码')
    );
    $form->addInput($floatWxImg);

    $floatTodayUrl = new \Typecho\Widget\Helper\Form\Element\Text(
        'floatTodayUrl',
        null,
        null,
        _t('悬浮按钮:今日更新链接'),
        _t('填写链接地址(如 /newest/ ),留空不显示该按钮')
    );
    $form->addInput($floatTodayUrl);

    $floatVipUrl = new \Typecho\Widget\Helper\Form\Element\Text(
        'floatVipUrl',
        null,
        null,
        _t('悬浮按钮:VIP 链接'),
        _t('填写链接地址(如 /vip.html ),留空不显示该按钮')
    );
    $form->addInput($floatVipUrl);

    $floatAskUrl = new \Typecho\Widget\Helper\Form\Element\Text(
        'floatAskUrl',
        null,
        null,
        _t('悬浮按钮:求软件链接'),
        _t('填写链接地址,留空不显示该按钮')
    );
    $form->addInput($floatAskUrl);

    $floatReportUrl = new \Typecho\Widget\Helper\Form\Element\Text(
        'floatReportUrl',
        null,
        null,
        _t('悬浮按钮:内容举报链接'),
        _t('填写链接地址,留空不显示该按钮')
    );
    $form->addInput($floatReportUrl);

    $lbMailSwitch = new \Typecho\Widget\Helper\Form\Element\Select(
        'lb_mail_switch',
        [
            '0' => _t('关闭'),
            '1' => _t('开启')
        ],
        '0',
        _t('邮件通知'),
        _t('开启后,“求软件 / 内容举报”提交的内容通过 SMTP 邮件发送到管理员邮箱')
    );
    $form->addInput($lbMailSwitch);

    $lbMailHost = new \Typecho\Widget\Helper\Form\Element\Text(
        'lb_mail_host',
        null,
        null,
        _t('SMTP 服务器'),
        _t('如:smtp.qq.com / smtp.163.com')
    );
    $form->addInput($lbMailHost);

    $lbMailPort = new \Typecho\Widget\Helper\Form\Element\Number(
        'lb_mail_port',
        null,
        '465',
        _t('SMTP 端口'),
        _t('465(SSL) / 587(TLS)')
    );
    $form->addInput($lbMailPort);

    $lbMailUser = new \Typecho\Widget\Helper\Form\Element\Text(
        'lb_mail_user',
        null,
        null,
        _t('SMTP 账号'),
        _t('发信邮箱地址')
    );
    $form->addInput($lbMailUser);

    $lbMailPass = new \Typecho\Widget\Helper\Form\Element\Password(
        'lb_mail_pass',
        null,
        null,
        _t('SMTP 密码 / 授权码'),
        _t('QQ/163 等邮箱需使用授权码')
    );
    $form->addInput($lbMailPass);

    $lbMailFromName = new \Typecho\Widget\Helper\Form\Element\Text(
        'lb_mail_from_name',
        null,
        null,
        _t('发件人名称'),
        _t('收件人看到的发件人名字,留空用站点名')
    );
    $form->addInput($lbMailFromName);

    $lbMailAdmin = new \Typecho\Widget\Helper\Form\Element\Text(
        'lb_mail_admin',
        null,
        null,
        _t('管理员接收邮箱'),
        _t('求软件 / 内容举报的邮件发送到这个邮箱')
    );
    $form->addInput($lbMailAdmin);

    $lbNotifyFeishu = new \Typecho\Widget\Helper\Form\Element\Select(
        'lb_notify_feishu',
        [
            '0' => _t('关闭'),
            '1' => _t('开启')
        ],
        '0',
        _t('飞书通知'),
        _t('开启后,通过飞书群机器人推送通知')
    );
    $form->addInput($lbNotifyFeishu);

    $lbFeishuWebhook = new \Typecho\Widget\Helper\Form\Element\Text(
        'lb_feishu_webhook',
        null,
        null,
        _t('飞书群机器人 Webhook'),
        _t('在飞书群中添加“自定义机器人”后获得的 Webhook 地址,形如 https://open.feishu.cn/open-apis/bot/v2/hook/xxxx')
    );
    $form->addInput($lbFeishuWebhook);

    $lbNotifyWecom = new \Typecho\Widget\Helper\Form\Element\Select(
        'lb_notify_wecom',
        [
            '0' => _t('关闭'),
            '1' => _t('开启')
        ],
        '0',
        _t('企业微信通知'),
        _t('开启后,通过企业微信群机器人推送通知')
    );
    $form->addInput($lbNotifyWecom);

    $lbWecomWebhook = new \Typecho\Widget\Helper\Form\Element\Text(
        'lb_wecom_webhook',
        null,
        null,
        _t('企业微信机器人 Webhook'),
        _t('在企业微信群中添加机器人后获得的 Webhook 地址,形如 https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxx')
    );
    $form->addInput($lbWecomWebhook);

    $lbNotifyDingtalk = new \Typecho\Widget\Helper\Form\Element\Select(
        'lb_notify_dingtalk',
        [
            '0' => _t('关闭'),
            '1' => _t('开启')
        ],
        '0',
        _t('钉钉通知'),
        _t('开启后,通过钉钉群机器人推送通知')
    );
    $form->addInput($lbNotifyDingtalk);

    $lbDingtalkWebhook = new \Typecho\Widget\Helper\Form\Element\Text(
        'lb_dingtalk_webhook',
        null,
        null,
        _t('钉钉群机器人 Webhook'),
        _t('在钉钉群中添加自定义机器人后获得的 Webhook 地址,形如 https://oapi.dingtalk.com/robot/send?access_token=xxxx')
    );
    $form->addInput($lbDingtalkWebhook);

    $lbNotifyPushplus = new \Typecho\Widget\Helper\Form\Element\Select(
        'lb_notify_pushplus',
        [
            '0' => _t('关闭'),
            '1' => _t('开启')
        ],
        '0',
        _t('PushPlus 通知'),
        _t('开启后,通过 PushPlus 推送到微信(免费版每天 200 条)')
    );
    $form->addInput($lbNotifyPushplus);

    $lbPushplusToken = new \Typecho\Widget\Helper\Form\Element\Text(
        'lb_pushplus_token',
        null,
        null,
        _t('PushPlus Token'),
        _t('在 pushplus.plus 网站登录后获取的 token')
    );
    $form->addInput($lbPushplusToken);

    $newestCats = new \Typecho\Widget\Helper\Form\Element\Text(
        'newestCats',
        null,
        '',
        _t('最新更新分类展示'),
        _t('填写分类 ID,多个用英文逗号分隔,例如:1,2,3,4。留空则不显示分类标签栏')
    );
    $form->addInput($newestCats);

    $rankCats = new \Typecho\Widget\Helper\Form\Element\Text(
        'downloadRankCats',
        null,
        '',
        _t('下载榜单分类'),
        _t('填写分类 ID,多个用英文逗号分隔,例如:1,2。留空则不显示下载榜单区块')
    );
    $form->addInput($rankCats);

    $newestCols = new \Typecho\Widget\Helper\Form\Element\Text(
        'newestCols',
        null,
        '4',
        _t('最新更新每排卡片数'),
        _t('每排显示几张卡片,默认 4')
    );
    $form->addInput($newestCols);

    $newestRows = new \Typecho\Widget\Helper\Form\Element\Text(
        'newestRows',
        null,
        '3',
        _t('最新更新展示排数'),
        _t('展示几排卡片,默认 3(共 4×3=12 张),超出部分通过“更多”按钮查看')
    );
    $form->addInput($newestRows);

    $articleCatIds = new \Typecho\Widget\Helper\Form\Element\Text(
        'articleCatIds',
        null,
        '',
        _t('文章卡片分类'),
        _t('填写使用文章卡片(大配图 + 标题 + 日期)的分类 ID,多个用英文逗号分隔,例如:5,6。留空则全部使用软件卡片')
    );
    $form->addInput($articleCatIds);

    $enableDownloadCount = new \Typecho\Widget\Helper\Form\Element\Select(
        'enableDownloadCount',
        [
            '1' => _t('开启'),
            '0' => _t('关闭')
        ],
        '1',
        _t('下载统计功能'),
        _t('开启后会记录每个资源的下载次数')
    );
    $form->addInput($enableDownloadCount);

    $enableRating = new \Typecho\Widget\Helper\Form\Element\Select(
        'enableRating',
        [
            '1' => _t('开启'),
            '0' => _t('关闭')
        ],
        '1',
        _t('资源评分功能'),
        _t('开启后用户可以对资源进行 1-5 星评分')
    );
    $form->addInput($enableRating);

    
    $footerPowered = new \Typecho\Widget\Helper\Form\Element\Textarea(
        'footerPowered',
        null,
        _t('Powered by <a href="https://typecho.org" target="_blank" rel="nofollow noopener">Typecho</a> &nbsp;|&nbsp; Theme <strong>LB Down</strong>'),
        _t('页脚版权行'),
        _t('页脚最底部“Powered by … | Theme …”一行,支持 HTML,留空则显示默认内容')
    );
    $form->addInput($footerPowered);

    $footerAbout = new \Typecho\Widget\Helper\Form\Element\Textarea(
        'footerAbout',
        null,
        null,
        _t('关于本站内容'),
        _t('显示在页脚“关于本站”区域,支持 HTML 代码。留空则显示站点描述')
    );
    $form->addInput($footerAbout);

    $icp = new \Typecho\Widget\Helper\Form\Element\Text(
        'icp',
        null,
        null,
        _t('ICP 备案号'),
        _t('填写后会显示在页脚')
    );
    $form->addInput($icp);

    $icpUrl = new \Typecho\Widget\Helper\Form\Element\Text(
        'icpUrl',
        null,
        'https://beian.miit.gov.cn/',
        _t('ICP 备案号跳转地址'),
        _t('点击 ICP 备案号跳转的链接,留空默认工信部备案查询官网')
    );
    $form->addInput($icpUrl);

    $govIc = new \Typecho\Widget\Helper\Form\Element\Text(
        'govIc',
        null,
        null,
        _t('公安备案号'),
        _t('填写后会显示在页脚(如:粤公网安备xxxxxxxxxxxx号)')
    );
    $form->addInput($govIc);

    $govIcUrl = new \Typecho\Widget\Helper\Form\Element\Text(
        'govIcUrl',
        null,
        'http://www.beian.gov.cn/portal/registerSystemInfo?recordcode=',
        _t('公安备案号跳转地址'),
        _t('点击公安备案号跳转的链接,留空默认公安部备案查询(recordcode= 结尾时自动拼接备案号)')
    );
    $form->addInput($govIcUrl);

    $analyticsCode = new \Typecho\Widget\Helper\Form\Element\Textarea(
        'analyticsCode',
        null,
        null,
        _t('第三方统计代码'),
        _t('百度统计、Google Analytics 等,会输出在页脚')
    );
    $form->addInput($analyticsCode);
}

function themeFields(object $form): void
{

    $thumb = new \Typecho\Widget\Helper\Form\Element\Text(
        'thumb',
        null,
        null,
        _t('资源缩略图'),
        _t('资源封面图 URL,列表页和详情页展示')
    );
    $form->addItem($thumb);

    $version = new \Typecho\Widget\Helper\Form\Element\Text(
        'version',
        null,
        null,
        _t('版本号'),
        _t('如: v1.0.0、v2026.15 中文版')
    );
    $form->addItem($version);

    $fileSize = new \Typecho\Widget\Helper\Form\Element\Text(
        'fileSize',
        null,
        null,
        _t('文件大小'),
        _t('如: 65.67 MB、1.2 GB')
    );
    $form->addItem($fileSize);

    $systemReq = new \Typecho\Widget\Helper\Form\Element\Text(
        'systemReq',
        null,
        null,
        _t('系统要求'),
        _t('如: macOS 10.15+ / Windows 10+')
    );
    $form->addItem($systemReq);

    $language = new \Typecho\Widget\Helper\Form\Element\Select(
        'language',
        [
            '' => _t('未设置'),
            'zh-CN' => _t('简体中文'),
            'zh-TW' => _t('繁体中文'),
            'en' => _t('英文'),
            'multi' => _t('多语言')
        ],
        '',
        _t('资源语言'),
        _t('选择资源的语言类型')
    );
    $form->addItem($language);

    $license = new \Typecho\Widget\Helper\Form\Element\Select(
        'license',
        [
            '' => _t('未设置'),
            'free' => _t('免费'),
            'vip' => _t('VIP免费'),
            'paid' => _t('付费'),
            'trial' => _t('试用'),
            'open' => _t('开源')
        ],
        '',
        _t('授权类型'),
        _t('选择资源的授权方式')
    );
    $form->addItem($license);

    $unzipPassword = new \Typecho\Widget\Helper\Form\Element\Text(
        'unzipPassword',
        null,
        null,
        _t('解压密码'),
        _t('如有解压密码请填写,详情页会显示复制按钮')
    );
    $form->addItem($unzipPassword);

    $downloadLinks = new \Typecho\Widget\Helper\Form\Element\Textarea(
        'downloadLinks',
        null,
        null,
        _t('下载链接'),
        _t('每行一个链接,格式: 线路名称|链接地址|下载方式(可选: local/cloud)。例如: 百度网盘|https://pan.baidu.com/xxx|cloud')
    );
    $form->addItem($downloadLinks);

    $changelog = new \Typecho\Widget\Helper\Form\Element\Textarea(
        'changelog',
        null,
        null,
        _t('更新日志'),
        _t('每行一条记录,格式: 版本号|日期|更新内容。例如: v1.2.0|2026-07-15|修复了已知问题')
    );
    $form->addItem($changelog);

    $downloadCount = new \Typecho\Widget\Helper\Form\Element\Number(
        'downloadCount',
        null,
        '0',
        _t('下载次数'),
        _t('由程序自动维护,可不填')
    );
    $form->addItem($downloadCount);

    $recommend = new \Typecho\Widget\Helper\Form\Element\Select(
        'recommend',
        [
            '0' => _t('不推荐'),
            '1' => _t('推荐到轮播')
        ],
        '0',
        _t('首页轮播推荐'),
        _t('设为推荐后,该文章将出现在首页顶部轮播中')
    );
    $form->addItem($recommend);

    $noticeTitle = new \Typecho\Widget\Helper\Form\Element\Text(
        'notice_title',
        null,
        null,
        _t('温馨提示标题'),
        _t('选填,默认“特别提醒”。留空则不显示标题')
    );
    $form->addItem($noticeTitle);

    $noticeContent = new \Typecho\Widget\Helper\Form\Element\Textarea(
        'notice_content',
        null,
        null,
        _t('温馨提示内容'),
        _t('填写后详情页会弹出提示(如“不支持 macOS 13.0,新系统用户可以使用 MacDroid”),支持 HTML。留空则不弹窗')
    );
    $form->addItem($noticeContent);
}

function lb_parse_download_links(?string $rawLinks): array
{
    $links = [];
    if (empty($rawLinks)) return $links;

    $lines = explode("\n", trim($rawLinks));
    foreach ($lines as $line) {
        $line = trim($line);
        if (empty($line)) continue;

        $parts = explode('|', $line);
        $name = trim($parts[0] ?? '下载');
        $url = trim($parts[1] ?? '');
        $type = trim($parts[2] ?? 'cloud');

        if (!empty($url)) {
            $links[] = [
                'name' => $name,
                'url' => $url,
                'type' => $type
            ];
        }
    }
    return $links;
}

function lb_parse_changelog(?string $rawLog): array
{
    $log = [];
    if (empty($rawLog)) return $log;

    $lines = explode("\n", trim($rawLog));
    foreach ($lines as $line) {
        $line = trim($line);
        if (empty($line)) continue;

        $parts = explode('|', $line);
        if (count($parts) >= 3) {
            $log[] = [
                'version' => trim($parts[0]),
                'date' => trim($parts[1]),
                'content' => trim($parts[2])
            ];
        }
    }
    return $log;
}

function lb_get_thumb(object $archive): string
{
    $thumb = $archive->fields->thumb ?? '';
    if (!empty($thumb)) return $thumb;

    $content = $archive->content;
    if (preg_match('/<img[^>]+src=["\']([^"\']+)["\']/i', $content, $matches)) {
        return $matches[1];
    }

    return lb_theme_url('assets/img/placeholder.svg');
}

function lb_theme_url(string $path = ''): string
{
    static $themeUrl = null;
    if ($themeUrl === null) {
        $options = \Typecho\Widget::widget('\Widget\Options');
        $themeUrl = $options->themeUrl;
    }
    return rtrim($themeUrl, '/') . '/' . ltrim($path, '/');
}

function lb_format_size(string $sizeStr): string
{
    return htmlspecialchars($sizeStr);
}

function lb_get_license_label(?string $license): array
{
    $map = [
        'free' => ['text' => '免费', 'class' => 'tag-free'],
        'vip' => ['text' => 'VIP免费', 'class' => 'tag-vip'],
        'paid' => ['text' => '付费', 'class' => 'tag-paid'],
        'trial' => ['text' => '试用', 'class' => 'tag-trial'],
        'open' => ['text' => '开源', 'class' => 'tag-open'],
    ];
    return $map[$license] ?? [];
}

function lb_get_language_label(?string $lang): string
{
    $map = [
        'zh-CN' => '简体中文',
        'zh-TW' => '繁体中文',
        'en' => '英文',
        'multi' => '多语言',
    ];
    return $map[$lang] ?? '';
}

function lb_get_recommend_posts(?int $limit = null): array
{
    $options = \Typecho\Widget::widget('\Widget\Options');
    if ($limit === null) {
        $limit = intval($options->recNum ?? 6);
    }
    if ($limit <= 0) $limit = 6;

    $excludeCats = [];
    $excludeRaw = trim($options->recExcludeCats ?? '');
    if (!empty($excludeRaw)) {
        $excludeCats = array_filter(array_map('intval', explode(',', $excludeRaw)));
    }

    $db = \Typecho\Db::get();
    $prefix = $db->getPrefix();
    $adapter = $db->getAdapterName();
    $isMysql = (strpos($adapter, 'Mysql') !== false || strpos($adapter, 'Pdo_Mysql') !== false);
    $q = $isMysql ? '`' : '"';

    $excludeJoin = '';
    $excludeWhere = '';
    if ($excludeCats) {
        $excludeIds = implode(',', $excludeCats);
        $excludeJoin = "LEFT JOIN {$q}{$prefix}relationships{$q} r ON c.cid = r.cid AND r.mid IN ({$excludeIds})";
        $excludeWhere = "AND r.mid IS NULL";
    }

    $sql = "SELECT c.* FROM {$q}{$prefix}contents{$q} c
            INNER JOIN {$q}{$prefix}fields{$q} f ON c.cid = f.cid
            {$excludeJoin}
            WHERE c.status = 'publish'
              AND c.type = 'post'
              AND f.name = 'recommend'
              AND f.str_value = '1'
              {$excludeWhere}
            ORDER BY c.created DESC
            LIMIT {$limit}";
    $posts = $db->fetchAll($sql);

    if (count($posts) < $limit) {
        $more = $limit - count($posts);
        $existCids = array_column($posts, 'cid');
        $inClause = $existCids ? 'AND c.cid NOT IN (' . implode(',', array_map('intval', $existCids)) . ')' : '';
        $sql2 = "SELECT c.* FROM {$q}{$prefix}contents{$q} c
                 {$excludeJoin}
                 WHERE c.status = 'publish'
                   AND c.type = 'post'
                   {$inClause}
                   {$excludeWhere}
                 ORDER BY c.created DESC
                 LIMIT {$more}";
        $posts = array_merge($posts, $db->fetchAll($sql2));
    }

    if ($posts) {
        $cids = array_column($posts, 'cid');
        $inCids = implode(',', array_map('intval', $cids));
        $fieldSql = "SELECT cid, name, str_value, int_value, float_value FROM {$q}{$prefix}fields{$q} WHERE cid IN ({$inCids})";
        $fieldRows = $db->fetchAll($fieldSql);

        $fieldsMap = [];
        foreach ($fieldRows as $fr) {
            $cid = $fr['cid'];
            $name = $fr['name'];
            $val = $fr['str_value'] ?? (string)($fr['int_value'] ?? $fr['float_value'] ?? '');
            if (!isset($fieldsMap[$cid])) $fieldsMap[$cid] = [];
            $fieldsMap[$cid][$name] = $val;
        }

        foreach ($posts as &$p) {
            $cid = $p['cid'];
            $p['fields'] = (object)($fieldsMap[$cid] ?? []);
        }
        unset($p);
    }

    return $posts;
}

function lb_get_rank_posts(array $catIds, int $limit = 14): array
{
    if (!$catIds) return [];
    $limit = max(1, min(50, $limit));

    $db = \Typecho\Db::get();
    $prefix = $db->getPrefix();
    $adapter = $db->getAdapterName();
    $isMysql = (strpos($adapter, 'Mysql') !== false || strpos($adapter, 'Pdo_Mysql') !== false);
    $q = $isMysql ? '`' : '"';
    $catIn = implode(',', array_map('intval', $catIds));

    $sql = "SELECT c.* FROM {$q}{$prefix}contents{$q} c
            INNER JOIN {$q}{$prefix}relationships{$q} r ON c.cid = r.cid
            INNER JOIN {$q}{$prefix}metas{$q} m ON r.mid = m.mid
            LEFT JOIN {$q}{$prefix}fields{$q} fdc ON c.cid = fdc.cid AND fdc.name = 'downloadCount'
            WHERE c.status = 'publish' AND c.type = 'post'
              AND m.type = 'category' AND m.mid IN ({$catIn})
            GROUP BY c.cid
            ORDER BY CAST(COALESCE(fdc.str_value, '0') AS UNSIGNED) DESC, c.created DESC
            LIMIT {$limit}";
    $posts = $db->fetchAll($sql);

    if ($posts) {
        $cids = array_column($posts, 'cid');
        $inCids = implode(',', array_map('intval', $cids));
        $fieldSql = "SELECT cid, name, str_value, int_value, float_value FROM {$q}{$prefix}fields{$q} WHERE cid IN ({$inCids})";
        $fieldRows = $db->fetchAll($fieldSql);
        $fieldsMap = [];
        foreach ($fieldRows as $fr) {
            $cid = $fr['cid'];
            $name = $fr['name'];
            $val = $fr['str_value'] ?? (string)($fr['int_value'] ?? $fr['float_value'] ?? '');
            if (!isset($fieldsMap[$cid])) $fieldsMap[$cid] = [];
            $fieldsMap[$cid][$name] = $val;
        }
        foreach ($posts as &$p) {
            $p['fields'] = (object)($fieldsMap[$p['cid']] ?? []);
        }
        unset($p);
    }

    return $posts;
}

function lb_download_counter(): void
{
    if ('POST' === ($_SERVER['REQUEST_METHOD'] ?? '')) {
        $cid = intval($_POST['cid'] ?? 0);
        if ($cid > 0) {
            $db = \Typecho\Db::get();
            $current = $db->fetchObject($db->select('str_value')
                ->from('table.fields')
                ->where('cid = ?', $cid)
                ->where('name = ?', 'downloadCount'));

            $count = 1;
            if ($current) {
                $count = intval($current->str_value) + 1;
                $db->query($db->update('table.fields')
                    ->rows(['str_value' => (string)$count])
                    ->where('cid = ?', $cid)
                    ->where('name = ?', 'downloadCount'));
            } else {
                $db->query($db->insert('table.fields')
                    ->rows([
                        'cid' => $cid,
                        'name' => 'downloadCount',
                        'type' => 'str',
                        'str_value' => '1',
                        'int_value' => 0,
                        'float_value' => 0
                    ]));
            }

            header('Content-Type: application/json');
            echo json_encode(['code' => 0, 'count' => $count]);
            exit;
        }
    }

    header('Content-Type: application/json');
    echo json_encode(['code' => 1]);
    exit;
}

function lb_rating_handler(): void
{
    if ('POST' === ($_SERVER['REQUEST_METHOD'] ?? '')) {
        $cid = intval($_POST['cid'] ?? 0);
        $rating = intval($_POST['rating'] ?? 0);

        if ($cid > 0 && $rating >= 1 && $rating <= 5) {
            $db = \Typecho\Db::get();
            $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';

            $existing = $db->fetchObject($db->select()
                ->from('table.lb_ratings')
                ->where('cid = ?', $cid)
                ->where('ip = ?', $ip)
                ->where('created > ?', time() - 86400));

            if ($existing) {
                header('Content-Type: application/json');
                echo json_encode(['code' => 2, 'msg' => '您今天已经评分过了']);
                exit;
            }

            $db->query($db->insert('table.lb_ratings')
                ->rows([
                    'cid' => $cid,
                    'rating' => $rating,
                    'ip' => $ip,
                    'created' => time()
                ]));

            $result = $db->fetchObject($db->select(
                ['AVG(rating)' => 'avg'],
                ['COUNT(*)' => 'cnt']
            )->from('table.lb_ratings')->where('cid = ?', $cid));

            $avg = round(floatval($result->avg), 1);
            $cnt = intval($result->cnt);

            header('Content-Type: application/json');
            echo json_encode([
                'code' => 0,
                'avg' => $avg,
                'count' => $cnt
            ]);
            exit;
        }
    }

    header('Content-Type: application/json');
    echo json_encode(['code' => 1]);
    exit;
}

function lb_get_rating(int $cid): array
{
    lb_create_ratings_table();
    $db = \Typecho\Db::get();
    $result = $db->fetchObject($db->select(
        ['AVG(rating)' => 'avg'],
        ['COUNT(*)' => 'cnt']
    )->from('table.lb_ratings')->where('cid = ?', $cid));

    return [
        'avg' => $result ? round(floatval($result->avg), 1) : 0,
        'count' => $result ? intval($result->cnt) : 0
    ];
}

function lb_create_ratings_table(): void
{
    static $created = false;
    if ($created) return;
    $created = true;

    $db = \Typecho\Db::get();
    $prefix = $db->getPrefix();
    $adapter = strtolower($db->getAdapterName());
    $isMysql = (false !== strpos($adapter, 'mysql'));

    try {
        $db->query($db->select()->from('table.lb_ratings')->limit(1));
        return;
    } catch (\Exception $e) {

    }

    if ($isMysql) {
        $sql = "CREATE TABLE IF NOT EXISTS `{$prefix}lb_ratings` (
            `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
            `cid` INT UNSIGNED NOT NULL DEFAULT 0,
            `rating` TINYINT UNSIGNED NOT NULL DEFAULT 0,
            `ip` VARCHAR(45) NOT NULL DEFAULT '',
            `created` INT UNSIGNED NOT NULL DEFAULT 0,
            PRIMARY KEY (`id`),
            KEY `cid` (`cid`),
            KEY `ip` (`ip`),
            KEY `created` (`created`)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;";
    } else {
        $sql = "CREATE TABLE IF NOT EXISTS [{$prefix}lb_ratings] (
            [id] INTEGER PRIMARY KEY AUTOINCREMENT,
            [cid] INTEGER NOT NULL DEFAULT 0,
            [rating] INTEGER NOT NULL DEFAULT 0,
            [ip] TEXT NOT NULL DEFAULT '',
            [created] INTEGER NOT NULL DEFAULT 0
        );";
        $db->query($sql);
        try {
            $db->query("CREATE INDEX IF NOT EXISTS [{$prefix}lb_ratings_cid] ON [{$prefix}lb_ratings] ([cid])");
            $db->query("CREATE INDEX IF NOT EXISTS [{$prefix}lb_ratings_ip] ON [{$prefix}lb_ratings] ([ip])");
            $db->query("CREATE INDEX IF NOT EXISTS [{$prefix}lb_ratings_created] ON [{$prefix}lb_ratings] ([created])");
        } catch (\Exception $e) {}
        return;
    }

    $db->query($sql);
}

function lb_gradient(int $index): string
{
    $gradients = [
        'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
        'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
        'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)',
        'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)',
        'linear-gradient(135deg, #fa709a 0%, #fee140 100%)',
        'linear-gradient(135deg, #30cfd0 0%, #330867 100%)',
        'linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)',
        'linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%)',
        'linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%)',
        'linear-gradient(135deg, #a1c4fd 0%, #c2e9fb 100%)',
        'linear-gradient(135deg, #d299c2 0%, #fef9d7 100%)',
        'linear-gradient(135deg, #89f7fe 0%, #66a6ff 100%)',
    ];
    return $gradients[$index % count($gradients)];
}

function lb_resource_card(object $archive, int $index = 0): void
{
    $cid = $archive->cid;
    $thumb = lb_get_thumb($archive);
    $version = $archive->fields->version ?? '';
    $fileSize = $archive->fields->fileSize ?? '';
    $license = $archive->fields->license ?? '';
    $language = $archive->fields->language ?? '';
    $downloadCount = $archive->fields->downloadCount ?? '0';
    $licenseLabel = lb_get_license_label($license);
    $langLabel = lb_get_language_label($language);

    $catMid = 0;
    $categories = $archive->categories;
    if (!empty($categories)) {
        $firstCat = is_array($categories) ? reset($categories) : $categories;
        if (is_object($firstCat)) {
            $catMid = $firstCat->mid;
        }
    }

    if (!$catMid) {
        try {
            $db = \Typecho\Db::get();
            $prefix = $db->getPrefix();
            $adapter = $db->getAdapterName();
            $isMysql = (strpos($adapter, 'Mysql') !== false || strpos($adapter, 'Pdo_Mysql') !== false);
            $q = $isMysql ? '`' : '"';
            $row = $db->fetchRow($db->query(
                "SELECT m.mid FROM {$q}{$prefix}relationships{$q} r "
                . "INNER JOIN {$q}{$prefix}metas{$q} m ON r.mid = m.mid "
                . "WHERE r.cid = " . intval($cid) . " AND m.type = 'category' "
                . "LIMIT 1"
            ));
            if ($row) {
                $catMid = intval($row['mid']);
            }
        } catch (\Exception $e) {
            $catMid = 0;
        }
    }
    ?>
<article class="resource-card" data-cat-mid="<?php echo $catMid; ?>" data-tilt data-tilt-max="10" data-tilt-scale="1" data-tilt-speed="300" data-tilt-perspective="500" itemscope itemtype="http://schema.org/SoftwareApplication">
    <div class="resource-card-inner" style="--card-gradient: <?php echo lb_gradient($index); ?>;">
    <a class="card-thumb" href="<?php $archive->permalink(); ?>" itemprop="url">
        <img src="<?php echo htmlspecialchars($thumb); ?>" alt="<?php $archive->title(); ?>"
             loading="lazy" itemprop="image"
             onerror="this.src='<?php echo lb_theme_url('assets/img/placeholder.svg'); ?>'">
    </a>
    <div class="card-info">
        <h3 class="card-title" itemprop="name">
            <a href="<?php $archive->permalink(); ?>"><?php $archive->title(); ?></a>
        </h3>
        <?php if ($version): ?>
            <div class="card-version" itemprop="softwareVersion"><?php echo htmlspecialchars($version); ?></div>
        <?php endif; ?>
        <div class="card-meta">
            <?php if ($fileSize): ?>
                <span class="meta-item" itemprop="fileSize">
                    <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                        <path d="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48"/>
                    </svg>
                    <?php echo htmlspecialchars($fileSize); ?>
                </span>
            <?php endif; ?>
            <span class="meta-item">
                <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                    <rect x="3" y="4" width="18" height="18" rx="2" ry="2"/>
                    <line x1="16" y1="2" x2="16" y2="6"/>
                    <line x1="8" y1="2" x2="8" y2="6"/>
                    <line x1="3" y1="10" x2="21" y2="10"/>
                </svg>
                <?php $archive->date('Y-m-d'); ?>
            </span>
            <span class="meta-item download-count">
                <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                    <path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/>
                    <polyline points="7 10 12 15 17 10"/>
                    <line x1="12" y1="15" x2="12" y2="3"/>
                </svg>
                <?php echo intval($downloadCount); ?> 次下载
            </span>
            <?php if ($langLabel): ?>
                <span class="meta-item lang-tag"><?php echo $langLabel; ?></span>
            <?php endif; ?>
        </div>
    </div>
    </div>
</article>
<?php
}

function lb_resource_card_grid(object $archive, int $index = 0): void
{
    $cid = $archive->cid;
    $thumb = lb_get_thumb($archive);
    $version = $archive->fields->version ?? '';
    $fileSize = $archive->fields->fileSize ?? '';
    $systemReq = $archive->fields->systemReq ?? '';
    $permalink = $archive->permalink;
    $title = $archive->title;

    ob_start();
    $archive->excerpt(80);
    $excerpt = trim(ob_get_clean());
    if (!$excerpt) {
        $excerpt = mb_substr(strip_tags($archive->content), 0, 80) . '...';
    }

    $gradient = lb_gradient($index);

    $catMid = 0;
    $categories = $archive->categories;
    if (!empty($categories)) {
        $firstCat = is_array($categories) ? reset($categories) : $categories;
        if (is_object($firstCat)) {
            $catMid = $firstCat->mid;
        }
    }

    if (!$catMid) {
        try {
            $db = \Typecho\Db::get();
            $prefix = $db->getPrefix();
            $adapter = $db->getAdapterName();
            $isMysql = (strpos($adapter, 'Mysql') !== false || strpos($adapter, 'Pdo_Mysql') !== false);
            $q = $isMysql ? '`' : '"';
            $row = $db->fetchRow($db->query(
                "SELECT m.mid FROM {$q}{$prefix}relationships{$q} r "
                . "INNER JOIN {$q}{$prefix}metas{$q} m ON r.mid = m.mid "
                . "WHERE r.cid = " . intval($cid) . " AND m.type = 'category' "
                . "LIMIT 1"
            ));
            if ($row) {
                $catMid = intval($row['mid']);
            }
        } catch (\Exception $e) {
            $catMid = 0;
        }
    }

    $isArticleCard = false;
    $lbOpts = \Typecho\Widget::widget('\Widget\Options');
    $articleRaw = trim($lbOpts->articleCatIds ?? '');
    if (!empty($articleRaw)) {
        $articleCats = array_filter(array_map('intval', explode(',', $articleRaw)));
        if (!empty($articleCats) && in_array($catMid, $articleCats)) {
            $isArticleCard = true;
        }
    }
    ?>
<li class="grid-card<?php if ($isArticleCard) echo ' grid-card-article'; ?>" data-cat-mid="<?php echo $catMid; ?>" data-tilt data-tilt-max="10" data-tilt-scale="1.08" data-tilt-speed="300" data-tilt-perspective="500">
    <a href="<?php echo htmlspecialchars($permalink); ?>" title="<?php echo htmlspecialchars($title); ?>">
        <div class="grid-card-inner" style="--card-gradient: <?php echo $gradient; ?>;">
            <?php if ($isArticleCard): ?>
            
            <div class="grid-card-img">
                <img src="<?php echo htmlspecialchars($thumb); ?>" alt="<?php echo htmlspecialchars($title); ?>"
                     onerror="this.src='<?php echo lb_theme_url('assets/img/placeholder.svg'); ?>'">
            </div>
            <div class="grid-card-con">
                <p class="grid-card-title"><?php echo htmlspecialchars($title); ?></p>
                <p class="grid-card-info">
                    <span><?php $archive->date('Y-m-d'); ?></span>
                    <?php if ($fileSize): ?>
                        <span class="sep">|</span>
                        <span><?php echo htmlspecialchars($fileSize); ?></span>
                    <?php endif; ?>
                </p>
            </div>
            <?php else: ?>
            <div class="grid-card-img">
                <img src="<?php echo htmlspecialchars($thumb); ?>" alt="<?php echo htmlspecialchars($title); ?>"
                     onerror="this.src='<?php echo lb_theme_url('assets/img/placeholder.svg'); ?>'">
            </div>
            <div class="grid-card-con">
                <p class="grid-card-title"><?php echo htmlspecialchars($title); ?></p>
                <?php if ($version): ?>
                    <p class="grid-card-version"><?php echo htmlspecialchars($version); ?></p>
                <?php endif; ?>
                <p class="grid-card-info">
                    <?php if ($fileSize): ?>
                        <span><?php echo htmlspecialchars($fileSize); ?></span>
                        <span class="sep">|</span>
                    <?php endif; ?>
                    <span><?php $archive->date('Y-m-d'); ?></span>
                </p>
            </div>
            <?php if ($systemReq): ?>
            <div class="grid-card-arm">
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="12" height="12">
                    <rect x="2" y="4" width="20" height="14" rx="2" ry="2"/>
                    <line x1="8" y1="2" x2="8" y2="4"/>
                    <line x1="16" y1="2" x2="16" y2="4"/>
                </svg>
                <span><?php echo htmlspecialchars($systemReq); ?></span>
            </div>
            <?php endif; ?>
            <?php endif; ?>
        </div>
    </a>
</li>
<?php
}

$lb_action = $_GET['lb_action'] ?? $_POST['lb_action'] ?? '';
if ('lb_download' === $lb_action) {
    lb_download_counter();
    exit;
}
if ('lb_rating' === $lb_action) {
    lb_create_ratings_table();
    lb_rating_handler();
    exit;
}
if ('lb_more_posts' === $lb_action) {
    lb_more_posts_handler();
    exit;
}
if ('lb_test_notify' === $lb_action) {
    lb_test_notify_handler();
    exit;
}
if ('lb_live_search' === $lb_action) {
    lb_live_search_handler();
    exit;
}

function lb_more_posts_handler(): void
{
    header('Content-Type: text/html; charset=UTF-8');

    $catMid = isset($_GET['cat']) ? intval($_GET['cat']) : 0;
    $page = max(1, isset($_GET['page']) ? intval($_GET['page']) : 1);
    $license = isset($_GET['license']) ? trim($_GET['license']) : '';
    $lang = isset($_GET['lang']) ? trim($_GET['lang']) : '';
    $sort = isset($_GET['sort']) ? trim($_GET['sort']) : 'date';
    $view = isset($_GET['view']) && in_array($_GET['view'], ['list', 'card'], true) ? $_GET['view'] : '';

    $options = \Typecho\Widget::widget('\Widget\Options');
    $pageSize = intval($options->pageSize ?: 20);
    if ($pageSize <= 0) $pageSize = 20;
    $offset = ($page - 1) * $pageSize;

    $db = \Typecho\Db::get();
    $prefix = $db->getPrefix();
    $adapter = $db->getAdapterName();
    $isMysql = (strpos($adapter, 'Mysql') !== false || strpos($adapter, 'Pdo_Mysql') !== false);
    $q = $isMysql ? '`' : '"';

    $sql = "SELECT c.* FROM {$q}{$prefix}contents{$q} c";
    $joins = '';
    $where = " WHERE c.status = 'publish' AND c.type = 'post'";
    $orderby = '';
    $groupby = '';

    if ($catMid > 0) {
        $joins .= " INNER JOIN {$q}{$prefix}relationships{$q} r ON c.cid = r.cid";
        $joins .= " INNER JOIN {$q}{$prefix}metas{$q} m ON r.mid = m.mid";
        $where .= " AND m.type = 'category' AND m.mid = {$catMid}";
        $groupby = " GROUP BY c.cid";
    }

    if ($license) {
        $joins .= " INNER JOIN {$q}{$prefix}fields{$q} fl ON c.cid = fl.cid AND fl.name = 'license'";
        $where .= " AND fl.str_value = '{$license}'";
    }
    if ($lang) {
        $joins .= " INNER JOIN {$q}{$prefix}fields{$q} flang ON c.cid = flang.cid AND flang.name = 'language'";
        $where .= " AND flang.str_value = '{$lang}'";
    }

    switch ($sort) {
        case 'hot':
            $joins .= " LEFT JOIN {$q}{$prefix}fields{$q} fdc ON c.cid = fdc.cid AND fdc.name = 'downloadCount'";
            $orderby = " ORDER BY CAST(COALESCE(fdc.str_value, '0') AS UNSIGNED) DESC, c.created DESC";
            break;
        case 'rating':
            $joins .= " LEFT JOIN {$q}{$prefix}lb_ratings{$q} frt ON c.cid = frt.cid";
            $orderby = " ORDER BY AVG(frt.rating) DESC, c.created DESC";
            break;
        default:
            $orderby = " ORDER BY c.created DESC";
    }

    $fullSql = $sql . $joins . $where . $groupby . $orderby . " LIMIT {$pageSize} OFFSET {$offset}";
    $posts = $db->fetchAll($fullSql);

    if (!$posts) {
        echo '';
        return;
    }

    $widget = \Widget\Contents\Post\Recent::alloc();
    $isCard = ($view === 'card');
    foreach ($posts as $i => $post) {
        $widget->reset();
        $widget->push($post);
        if ($isCard) {
            lb_resource_card_grid($widget, $offset + $i);
        } else {
            lb_resource_card($widget, $offset + $i);
        }
    }
}

function lb_send_mail(string $to, string $subject, string $body): bool
{
    $options = \Typecho\Widget::widget('\Widget\Options');
    if (($options->lb_mail_switch ?? '0') !== '1') {
        return false;
    }
    $host = trim($options->lb_mail_host ?? '');
    $port = intval($options->lb_mail_port ?? 465);
    $user = trim($options->lb_mail_user ?? '');
    $pass = trim($options->lb_mail_pass ?? '');
    if (!$host || !$user || !$pass || !$to) {
        return false;
    }

    require_once __DIR__ . '/phpmailer.php';
    require_once __DIR__ . '/smtp.php';

    try {
        $mail = new PHPMailer();
        $mail->isSMTP();
        $mail->CharSet = 'UTF-8';
        $mail->SMTPAuth = true;
        $mail->Host = $host;
        $mail->Port = $port;
        $mail->Username = $user;
        $mail->Password = $pass;
        if ($port == 465) {
            $mail->SMTPSecure = 'ssl';
        } elseif ($port == 587) {
            $mail->SMTPSecure = 'tls';
        }
        $mail->setFrom($user, trim($options->lb_mail_from_name ?? '') ?: $options->title);
        $mail->addAddress($to);
        $mail->isHTML(true);
        $mail->Subject = $subject;
        $mail->Body = $body;
        $mail->send();
        return true;
    } catch (\Exception $e) {
        return false;
    }
}

function lb_send_feishu(string $text): bool
{
    $options = \Typecho\Widget::widget('\Widget\Options');
    $webhook = trim($options->lb_feishu_webhook ?? '');
    if (!$webhook || !$text) {
        return false;
    }

    $payload = json_encode([
        'msg_type' => 'text',
        'content'  => ['text' => $text],
    ], JSON_UNESCAPED_UNICODE);

    $context = stream_context_create([
        'http' => [
            'method'  => 'POST',
            'header'  => "Content-Type: application/json\r\nContent-Length: " . strlen($payload) . "\r\n",
            'content' => $payload,
            'timeout' => 10,
        ],
    ]);
    $resp = @file_get_contents($webhook, false, $context);
    if ($resp === false) {
        return false;
    }
    $data = json_decode($resp, true);
    return is_array($data) && isset($data['code']) && intval($data['code']) === 0;
}

function lb_notify(string $subject, string $mailBody, string $plainText): bool
{
    $options = \Typecho\Widget::widget('\Widget\Options');
    $ok = false;

    if (($options->lb_mail_switch ?? '0') === '1') {
        $adminMail = trim($options->lb_mail_admin ?? '');
        if ($adminMail && lb_send_mail($adminMail, $subject, $mailBody)) {
            $ok = true;
        }
    }

    if (($options->lb_notify_feishu ?? '0') === '1') {
        if (lb_send_feishu($plainText)) {
            $ok = true;
        }
    }

    if (($options->lb_notify_wecom ?? '0') === '1') {
        if (lb_send_wecom($plainText)) {
            $ok = true;
        }
    }

    if (($options->lb_notify_dingtalk ?? '0') === '1') {
        if (lb_send_dingtalk($plainText)) {
            $ok = true;
        }
    }

    if (($options->lb_notify_pushplus ?? '0') === '1') {
        if (lb_send_pushplus($subject, $plainText)) {
            $ok = true;
        }
    }

    return $ok;
}

function lb_test_notify_handler(): void
{

    ob_start();

    $result = ['ok' => false, 'msg' => '未知错误'];
    $user = \Typecho\Widget::widget('Widget_User');
    if (!$user->hasLogin() || !$user->pass('administrator', false)) {
        $result = ['ok' => false, 'msg' => '无权限操作'];
    } else {
        $channel = $_GET['channel'] ?? 'mail';
        $lbOpts = \Typecho\Widget::widget('\Widget\Options');
        $siteTitle = trim($lbOpts->title ?? '本站');
        $channelNames = [
            'mail'     => '邮件',
            'feishu'   => '飞书',
            'wecom'    => '企业微信',
            'dingtalk' => '钉钉',
            'pushplus' => 'PushPlus',
        ];
        $channelName = $channelNames[$channel] ?? '通知';
        $testText = '【测试】' . $siteTitle . '的' . $channelName . '测试消息,收到即配置正确。' . date('Y-m-d H:i');

        switch ($channel) {
            case 'feishu':
                $ok = lb_send_feishu($testText);
                $result = ['ok' => $ok, 'msg' => $ok ? '飞书测试消息发送成功,请查看飞书群' : '发送失败,请检查飞书 Webhook 配置'];
                break;

            case 'wecom':
                $ok = lb_send_wecom($testText);
                $result = ['ok' => $ok, 'msg' => $ok ? '企业微信测试消息发送成功,请查看企业微信群' : '发送失败,请检查企业微信 Webhook 配置'];
                break;

            case 'dingtalk':
                $ok = lb_send_dingtalk($testText);
                $result = ['ok' => $ok, 'msg' => $ok ? '钉钉测试消息发送成功,请查看钉钉群' : '发送失败,请检查钉钉 Webhook 配置'];
                break;

            case 'pushplus':
                $ok = lb_send_pushplus('【测试】' . $siteTitle . ' 通知测试', $testText);
                $result = ['ok' => $ok, 'msg' => $ok ? 'PushPlus 测试消息发送成功,请查看微信' : '发送失败,请检查 PushPlus Token 配置'];
                break;

            case 'mail':
            default:
                $adminMail = trim($lbOpts->lb_mail_admin ?? '');
                if (!$adminMail) {
                    $result = ['ok' => false, 'msg' => '未配置管理员接收邮箱'];
                } else {
                    $body = '<h3>这是一条测试消息</h3>'
                          . '<p>如果你收到这封邮件,说明 SMTP 配置正确。</p>'
                          . '<p>时间:' . date('Y-m-d H:i') . '</p>';
                    $ok = lb_send_mail($adminMail, '【测试】' . $siteTitle . ' 邮件通知测试', $body);
                    $result = ['ok' => $ok, 'msg' => $ok ? '测试邮件发送成功,请查看收件箱' : '发送失败,请检查 SMTP 配置'];
                }
                break;
        }
    }

    while (ob_get_level() > 0) {
        ob_end_clean();
    }
    header('Content-Type: application/json; charset=UTF-8');
    echo json_encode($result, JSON_UNESCAPED_UNICODE);
    exit;
}

function lb_send_wecom(string $text): bool
{
    $options = \Typecho\Widget::widget('\Widget\Options');
    $webhook = trim($options->lb_wecom_webhook ?? '');
    if (!$webhook || !$text) {
        return false;
    }

    $payload = json_encode([
        'msgtype' => 'text',
        'text'    => ['content' => $text],
    ], JSON_UNESCAPED_UNICODE);

    $context = stream_context_create([
        'http' => [
            'method'  => 'POST',
            'header'  => "Content-Type: application/json\r\nContent-Length: " . strlen($payload) . "\r\n",
            'content' => $payload,
            'timeout' => 10,
        ],
    ]);
    $resp = @file_get_contents($webhook, false, $context);
    if ($resp === false) {
        return false;
    }
    $data = json_decode($resp, true);
    return is_array($data) && isset($data['errcode']) && intval($data['errcode']) === 0;
}

function lb_send_dingtalk(string $text): bool
{
    $options = \Typecho\Widget::widget('\Widget\Options');
    $webhook = trim($options->lb_dingtalk_webhook ?? '');
    if (!$webhook || !$text) {
        return false;
    }

    $payload = json_encode([
        'msgtype' => 'text',
        'text'    => ['content' => $text],
    ], JSON_UNESCAPED_UNICODE);

    $context = stream_context_create([
        'http' => [
            'method'  => 'POST',
            'header'  => "Content-Type: application/json\r\nContent-Length: " . strlen($payload) . "\r\n",
            'content' => $payload,
            'timeout' => 10,
        ],
    ]);
    $resp = @file_get_contents($webhook, false, $context);
    if ($resp === false) {
        return false;
    }
    $data = json_decode($resp, true);
    return is_array($data) && isset($data['errcode']) && intval($data['errcode']) === 0;
}

function lb_send_pushplus(string $title, string $content): bool
{
    $options = \Typecho\Widget::widget('\Widget\Options');
    $token = trim($options->lb_pushplus_token ?? '');
    if (!$token) {
        return false;
    }

    $payload = http_build_query([
        'token'   => $token,
        'title'   => $title,
        'content' => $content,
        'template' => 'txt',
    ]);

    $context = stream_context_create([
        'http' => [
            'method'  => 'POST',
            'header'  => "Content-Type: application/x-www-form-urlencoded\r\nContent-Length: " . strlen($payload) . "\r\n",
            'content' => $payload,
            'timeout' => 10,
        ],
    ]);
    $resp = @file_get_contents('http://www.pushplus.plus/send', false, $context);
    if ($resp === false) {
        return false;
    }
    $data = json_decode($resp, true);
    return is_array($data) && isset($data['code']) && intval($data['code']) === 200;
}

function lb_avatar_url(?string $mail, int $size = 100): string
{
    $mail = strtolower(trim($mail ?? ''));
    if ($mail !== '' && preg_match('/^(\d+)@qq\.com$/', $mail, $m)) {
        return 'http://q1.qlogo.cn/g?b=qq&nk=' . $m[1] . '&s=100';
    }
    return 'https://weavatar.com/avatar/' . md5($mail) . '?s=' . $size . '&d=mm';
}

function lb_get_favicon(string $host): string
{
    $host = trim($host);
    if ($host === '') {
        return '';
    }
    $host = preg_replace('/[^a-zA-Z0-9.\-]/', '', $host);
    if ($host === '') {
        return '';
    }

    $uploadDir = __TYPECHO_ROOT_DIR__ . '/usr/uploads/favicon/';
    $cacheFile = $uploadDir . $host . '.ico';
    $failFile  = $uploadDir . $host . '.fail';
    $cacheUrl  = \Typecho\Widget::widget('\Widget\Options')->siteUrl . 'usr/uploads/favicon/' . $host . '.ico';

    if (file_exists($cacheFile) && filesize($cacheFile) > 100) {
        return $cacheUrl;
    }

    if (file_exists($failFile) && (time() - filemtime($failFile)) < 86400) {
        return '';
    }

    if (!is_dir($uploadDir)) {
        @mkdir($uploadDir, 0755, true);
    }

    $content = @file_get_contents('http://' . $host . '/favicon.ico');
    if ($content === false || strlen($content) < 100) {
        $content = @file_get_contents('https://' . $host . '/favicon.ico');
    }

    if ($content !== false && strlen($content) >= 100) {
        @file_put_contents($cacheFile, $content);
        return $cacheUrl;
    }

    @file_put_contents($failFile, (string)time());
    return '';
}

function threadedComments($comments, $options)
{
    $commentClass = 'comment-item';
    if ($comments->levels > 0) {
        $commentClass .= ' comment-child';
    }
    ?>
    <li class="<?php echo $commentClass; ?>" id="<?php $comments->theId(); ?>">
        <div class="comment-body">
            <div class="comment-avatar">
                <img src="<?php echo htmlspecialchars(lb_avatar_url($comments->mail, 48)); ?>" alt="" loading="lazy">
            </div>
            <div class="comment-content">
                <div class="comment-meta">
                    <div class="comment-meta-top">
                        <span class="comment-author">
                            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                                <path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/>
                                <circle cx="12" cy="7" r="4"/>
                            </svg>
                            <?php $comments->author(); ?>
                            <?php

                            $lbCfg = \Typecho\Widget::widget('\Widget\Options');
                            $lbInfoSel = [];
                            if (($lbCfg->commentInfoEnable ?? '1') === '1') {
                                $lbRaw = $lbCfg->commentInfoOptions ?? ['ip', 'ua', 'system'];
                                if (is_array($lbRaw)) {
                                    $lbInfoSel = array_values(array_filter(array_map('trim', $lbRaw)));
                                } else {
                                    $lbInfoSel = array_filter(array_map('trim', explode(',', (string)$lbRaw)));
                                }
                            }
                            $lbShowIp = in_array('ip', $lbInfoSel);
                            $lbShowUa = in_array('ua', $lbInfoSel);
                            $lbShowOs = in_array('system', $lbInfoSel);
                            $lbIsBlogger = ($comments->authorId > 0 && $comments->authorId == $comments->ownerId);
                            ?>
                            <?php if ($lbIsBlogger): ?>
                            <span class="comment-blogger" data-tip="博主">
                                <span class="comment-blogger-icon">
                                    <svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M275.099846 3.625725a156.71042 156.71042 0 0 1 131.071193 69.789109A412.945151 412.945151 0 0 0 155.253199 257.024781a153.244595 153.244595 0 0 1-21.464483-164.469141A156.119655 156.119655 0 0 1 275.099846 3.625725z m324.094006 299.242467c-44.898185 31.586267-40.053907 108.622101 10.909471 172.070325 51.002763 63.448225 128.7869 89.284374 173.803238 57.698107 44.976954-31.586267 39.89637-108.622101-11.106393-172.070326-50.963379-63.448225-128.747515-89.284374-173.606316-57.698106z m-463.947915-8.940253a394.631418 394.631418 0 0 0 68.765116 441.616975 336.657621 336.657621 0 0 0 110.118707 74.003236c6.971034 3.032597 29.144436 14.414681 91.489898 13.548225 27.372139 0 72.821706-5.631965 88.536071-38.951145 2.678137-9.964246 3.662747-20.282952 2.914443-30.562274a78.374902 78.374902 0 0 1-75.460458 13.390687c-45.449566-13.233149-44.543726-59.391635-42.377586-72.073402 13.705762-79.16259 92.080664-80.659196 110.86701-83.652409a124.37585 124.37585 0 0 1 121.776482 81.05304c72.821706 195.70095-88.299764 299.990769-129.377666 313.657147-115.632519 38.990529-286.324392 8.270718-355.995347-69.513419C44.780033 860.15707 0.078769 746.218079 0.078769 625.071747c0-133.119181 47.497554-247.215709 135.167168-331.695189v0.551381z m746.215716 0a469.85557 469.85557 0 0 1 22.724783 650.629842c-142.886505 159.624864-364.147913 56.240885-218.465117-112.087925 22.724783-26.111839 79.950277-53.641516 127.290294-96.885558A394.631418 394.631418 0 0 0 881.501037 293.809786v0.118153z m-463.475302 8.940253c-45.016338-31.586267-122.642938-5.750118-173.763853 57.698106-51.160301 63.448225-55.807657 141.03544-11.067009 173.054935 44.701263 32.019495 122.642938 5.789503 173.606316-57.658722 51.002763-63.487609 55.92581-140.484059 11.067009-172.10971l0.157537-0.984609zM745.231107 0.002363c61.518391 0 117.286663 35.839779 142.453277 91.371745a153.441517 153.441517 0 0 1-25.914917 166.083901A412.196848 412.196848 0 0 0 612.336235 73.848012A156.198423 156.198423 0 0 1 745.231107 0.002363z" fill="#f59e0b"/></svg>
                                </span>
                                博主
                            </span>
                            <?php endif; ?>
                            <?php

                            $lbRegion = '';
                            if (preg_match('/LBRegion\/([^ ]+)/', $comments->agent, $rm)) {
                                $lbRegion = $rm[1];
                            } else {
                                $lbIpRaw = $comments->ip;
                                if ($lbIpRaw === '' || $lbIpRaw === '127.0.0.1' || $lbIpRaw === '::1'
                                    || preg_match('/^(10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)/', $lbIpRaw)) {
                                    $lbRegion = '本地';
                                } else {
                                    $lbRegion = '未知';
                                }
                            }
                            $lbIpTip = 'IP: ' . $comments->ip
                                . ($lbRegion !== '' && $lbRegion !== '未知' ? ' · ' . $lbRegion : '');
                            ?>
                            <?php if (!$lbIsBlogger && $lbShowIp): ?>
                            <span class="comment-browser comment-ip-badge" data-tip="<?php echo htmlspecialchars($lbIpTip); ?>">
                                <span class="comment-browser-icon">
                                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                                        <circle cx="12" cy="12" r="10"/>
                                        <line x1="2" y1="12" x2="22" y2="12"/>
                                        <path d="M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z"/>
                                    </svg>
                                </span>
                                <span class="comment-browser-text"><?php echo htmlspecialchars($lbRegion); ?></span>
                            </span>
                            <?php endif; ?>
                            <?php
                            $lbIcons = lb_browser_icons();

                            if (!$lbIsBlogger && $lbShowUa):
                                $lbBadge = lb_browser_badge($comments->agent);
                                if ($lbBadge):
                                    $lbIconSvg = $lbIcons[$lbBadge['key']] ?? ($lbIcons['default'] ?? '');
                                    $lbBadgeText = $lbBadge['name'] . ($lbBadge['version'] !== '' ? ' ' . $lbBadge['version'] : '');
                            ?>
                            <span class="comment-browser" data-tip="<?php echo htmlspecialchars($lbBadgeText); ?>">
                                <?php if ($lbIconSvg): ?>
                                <span class="comment-browser-icon"><?php echo $lbIconSvg; ?></span>
                                <?php endif; ?>
                                <span class="comment-browser-text"><?php echo htmlspecialchars($lbBadgeText); ?></span>
                            </span>
                                <?php endif; ?>
                            <?php endif; ?>
                            <?php

                            if (!$lbIsBlogger && $lbShowOs):
                                $lbOs = lb_os_badge($comments->agent);
                                $lbOsIcon = $lbIcons[$lbOs['key']] ?? ($lbIcons['default'] ?? '');
                            ?>
                            <span class="comment-browser comment-os" data-tip="<?php echo htmlspecialchars($lbOs['name']); ?>">
                                <?php if ($lbOsIcon): ?>
                                <span class="comment-browser-icon"><?php echo $lbOsIcon; ?></span>
                                <?php endif; ?>
                                <span class="comment-browser-text"><?php echo htmlspecialchars($lbOs['name']); ?></span>
                            </span>
                            <?php endif; ?>
                        </span>
                        <?php $comments->reply(); ?>
                    </div>
                    <span class="comment-date">
                        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                            <circle cx="12" cy="12" r="10"/>
                            <polyline points="12 6 12 12 16 14"/>
                        </svg>
                        <?php $comments->date('Y年m月d日'); ?>
                    </span>
                </div>
                <div class="comment-text">
                    <?php $comments->content(); ?>
                </div>
            </div>
        </div>
        <?php if ($comments->children): ?>
        <div class="comment-children">
            <?php $comments->threadedComments(); ?>
        </div>
        <?php endif; ?>
    </li>
    <?php
}

function lb_browser_icons(): array
{
    static $icons = null;
    if ($icons === null) {
        $file = __DIR__ . '/browser-icons.json';
        $icons = file_exists($file) ? (json_decode(file_get_contents($file), true) ?: []) : [];
    }
    return $icons;
}

function lb_browser_badge(?string $agent): array
{
    $agent = $agent ?? '';

    $rules = [
        ['Edg/',       'edge',      'Edge',              '/Edg\/([\d.]+)/'],
        ['OPR/',       'opera',     'Opera',             '/OPR\/([\d.]+)/'],
        ['QQBrowser',  'qq',        'QQ浏览器',           '/QQBrowser\/([\d.]+)/'],
        ['UCBrowser',  'uc',        'UC浏览器',           '/UCBrowser\/([\d.]+)/'],
        ['LBBROWSER',  'liebao',    '猎豹浏览器',         '/LBBROWSER/i'],
        ['MetaSr',     'sogou',     '搜狗浏览器',         '/MetaSr/i'],
        ['Sogou',      'sogou',     '搜狗浏览器',         '/Sogou\/([\d.]+)/'],
        ['TheWorld',   'world',     '世界之窗',           '/TheWorld\/([\d.]+)/'],
        ['TorBrowser', 'tor',       'Tor Browser',       '/TorBrowser\/([\d.]+)/'],
        ['DuckDuckGo', 'duckduckgo','DuckDuckGo',        '/DuckDuckGo\/([\d.]+)/'],
        ['Chromium',   'chromium',  'Chromium',          '/Chromium\/([\d.]+)/'],
        ['Chrome/',    'chrome',    'Chrome',            '/Chrome\/([\d.]+)/'],
        ['Firefox/',   'firefox',   'Firefox',           '/Firefox\/([\d.]+)/'],
        ['MSIE',       'ie',        'Internet Explorer', '/MSIE ([\d.]+)/'],
        ['Trident',    'ie',        'Internet Explorer', '/rv:([\d.]+)/'],
        ['Safari/',    'safari',    'Safari',            '/Version\/([\d.]+)/'],
    ];

    foreach ($rules as $rule) {
        if (stripos($agent, $rule[0]) !== false) {
            $version = '';
            if ($rule[3] !== '' && preg_match($rule[3], $agent, $m)) {
                $version = $m[1];
            }
            return ['key' => $rule[1], 'name' => $rule[2], 'version' => $version];
        }
    }

    return ['key' => 'default', 'name' => '浏览器', 'version' => ''];
}

function lb_os_badge(?string $agent): array
{
    $agent = $agent ?? '';

    if (preg_match('/LBWin\/(10|11)/', $agent, $wm)) {
        return ['key' => 'os_windows', 'name' => 'Windows ' . $wm[1]];
    }

    if (stripos($agent, 'Windows') !== false) {
        $ver = '';
        if (preg_match('/Windows NT ([\d.]+)/', $agent, $m)) {
            $map = [
                '10.0' => '10/11', '6.3' => '8.1', '6.2' => '8',
                '6.1' => '7', '6.0' => 'Vista', '5.1' => 'XP',
            ];
            $ver = $map[$m[1]] ?? $m[1];
        }
        return ['key' => 'os_windows', 'name' => 'Windows' . ($ver !== '' ? ' ' . $ver : '')];
    }

    if (stripos($agent, 'Mac OS X') !== false || stripos($agent, 'Macintosh') !== false) {
        $ver = '';
        if (preg_match('/Mac OS X ([\d_]+)/', $agent, $m)) {
            $ver = str_replace('_', '.', $m[1]);
        }
        return ['key' => 'os_macos', 'name' => 'macOS' . ($ver !== '' ? ' ' . $ver : '')];
    }

    if (preg_match('/iPhone|iPad|iPod/', $agent)) {
        $ver = '';
        if (preg_match('/OS ([\d_]+)/', $agent, $m)) {
            $ver = str_replace('_', '.', $m[1]);
        }
        return ['key' => 'os_ios', 'name' => 'iOS' . ($ver !== '' ? ' ' . $ver : '')];
    }

    if (stripos($agent, 'Android') !== false) {
        $ver = '';
        if (preg_match('/Android ([\d.]+)/', $agent, $m)) {
            $ver = $m[1];
        }
        return ['key' => 'os_android', 'name' => 'Android' . ($ver !== '' ? ' ' . $ver : '')];
    }

    if (stripos($agent, 'CrOS') !== false || stripos($agent, 'Chrome OS') !== false) {
        return ['key' => 'default', 'name' => 'ChromeOS'];
    }

    if (stripos($agent, 'Linux') !== false) {
        return ['key' => 'os_ubuntu', 'name' => 'Linux'];
    }

    return ['key' => 'default', 'name' => '未知系统'];
}

function lb_ip_region(string $ip): string
{
    $ip = trim($ip);

    if ($ip === '' || $ip === '127.0.0.1' || $ip === '::1'
        || preg_match('/^(10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)/', $ip)) {
        return '本地';
    }

    static $searcher = null;
    if ($searcher === null) {
        $dbFile = __DIR__ . '/lib/ip2region/ip2region.xdb';
        if (!file_exists($dbFile)) {
            return '未知';
        }
        try {
            require_once __DIR__ . '/lib/ip2region/Searcher.class.php';
            $handle = fopen($dbFile, 'r');
            $header = \ip2region\xdb\Util::loadHeader($handle);
            $version = \ip2region\xdb\Util::versionFromHeader($header);
            $searcher = \ip2region\xdb\Searcher::newWithFileOnly($version, $dbFile);
        } catch (\Exception $e) {
            return '未知';
        }
    }

    try {
        $region = $searcher->search($ip);
        if ($region === null) {
            return '未知';
        }
        $parts = explode('|', $region);

        $pro = $parts[1] ?? '';
        if ($pro === '0' || $pro === '') {
            $pro = $parts[0] ?? '';
        }
        $pro = str_replace(['省', '市', '自治区', '特别行政区'], '', $pro);
        return $pro !== '' ? $pro : '未知';
    } catch (\Exception $e) {
        return '未知';
    }
}

if (!function_exists('lb_attach_platform_hook')) {
    function lb_attach_platform_hook($comment, $content)
    {
        $platform = trim($_POST['lb_platform'] ?? '');
        if ($platform === 'Win10' || $platform === 'Win11') {
            $comment['agent'] .= ' LBWin/' . substr($platform, 3);
        }
        $region = lb_ip_region($comment['ip'] ?? '');
        if ($region !== '') {
            $comment['agent'] .= ' LBRegion/' . $region;
        }
        return $comment;
    }
}
\Typecho\Plugin::factory('Widget_Feedback')->comment = 'lb_attach_platform_hook';

function lb_live_search_handler(): void
{
    ob_start();
    $result = ['items' => []];

    $s = trim($_GET['kw'] ?? '');
    if ($s !== '') {
        try {
            $db = \Typecho\Db::get();
            $options = \Typecho\Widget::widget('\Widget\Options');
            $rows = $db->fetchAll($db->select('cid, title, slug')
                ->from('table.contents')
                ->where('type = ?', 'post')
                ->where('status = ?', 'publish')
                ->where('title LIKE ?', '%' . $s . '%')
                ->order('created', \Typecho\Db::SORT_DESC)
                ->limit(10));
            foreach ($rows as $r) {
                $result['items'][] = [
                    'title' => $r['title'],
                    'url'   => $options->siteUrl . intval($r['cid']) . '.html',
                ];
            }
        } catch (\Exception $e) {
            $result = ['items' => []];
        }
    }

    while (ob_get_level() > 0) {
        ob_end_clean();
    }
    header('Content-Type: application/json; charset=UTF-8');
    echo json_encode($result, JSON_UNESCAPED_UNICODE);
    exit;
}