布局优化与功能增强脚本,包含主题布局、内容过滤、图片灯箱、可配置图床上传与自动签到。
// ==UserScript==
// @name LINUX.SB-Enhance-Script
// @namespace https://linux.sb/
// @version 1.7.0
// @description 布局优化与功能增强脚本,包含主题布局、内容过滤、图片灯箱、可配置图床上传与自动签到。
// @author COMCOM
// @match https://linux.sb/*
// @match http://linux.sb/*
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @grant GM_xmlhttpRequest
// @connect *
// @run-at document-start
// @license MIT
// ==/UserScript==
(function () {
'use strict';
var STORAGE_KEY = 'linux-sb-wide-layout-settings-v2';
var SETTINGS_VERSION = 3;
var DEFAULTS = {
version: SETTINGS_VERSION,
maxWidth: 1100,
headerHeight: 68,
fontSize: 15,
radius: 6,
sidebarWidth: 270,
shellPadding: 28,
columnGap: 28,
theme: 'neutral',
accent: '#b8b8b8',
textPalette: 'neutral',
textColor: '#eeeeee',
homePersonalized: false,
sidebarSwap: false,
autoCheckin: false,
autoCheckinLastDate: '',
imageLightbox: false,
imageUpload: false,
imageUploadProvider: 'imgur',
imageUploadHost: 'https://imgur.com/',
imageUploadEndpoint: 'https://api.imgur.com/3/image',
imageUploadFileField: 'image',
imageUploadResponsePath: 'data.link',
imageUploadAuthMode: 'imgur-client-id',
imageUploadToken: '',
imageUploadSettingsCollapsed: false,
panelLeft: null,
panelTop: null,
toggleLeft: null,
toggleTop: null,
titleFilters: [],
userFilters: []
};
var NODEIMAGE_PRESET = {
provider: 'nodeimage',
host: 'https://nodeimage.com/',
endpoint: 'https://api.nodeimage.com/api/upload',
field: 'image',
responsePath: 'data.url',
authMode: 'nodeimage-api-key',
token: ''
};
var TEXT_PALETTES = {
neutral: { label: '中性银白', color: '#eeeeee' },
mist: { label: '雾灰', color: '#d6d6d6' },
silver: { label: '柔银', color: '#c7c7c7' },
warm: { label: '暖白', color: '#eee9e1' },
custom: { label: '自定义颜色', color: '#eeeeee' }
};
var HOME_LOGO_SVG = [
'<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="LINUX SB logo">',
'<circle fill="#EFEFEF" cx="256" cy="256" r="256"/>',
'<path d="M279.69124,269.784594 C289.092146,253.471257 283.562201,232.595717 267.248865,223.194811 C250.935529,213.793906 230.059989,219.32385 220.659083,235.637186 L212.087669,250.429788 C202.686764,266.743124 208.216708,287.618664 224.530044,297.01957 C240.84338,306.420476 261.718921,300.890531 271.119826,284.577195 L279.69124,269.784594 Z" fill="#737373"/>',
'<path d="M298.6313,68.356366 C308.032206,52.04303 302.502261,31.1674897 286.188925,21.7665841 C269.875589,12.3656785 249.000048,17.895623 239.599143,34.208959 L145.866584,196.651078 C136.465678,212.964414 141.995623,233.839955 158.308959,243.24086 C174.622295,252.641766 195.497836,247.111821 204.898741,230.798485 L298.6313,68.356366 Z" fill="#1E1E20"/>',
'<path d="M355.419725,307.249829 C364.82523,290.910346 359.29258,270.001346 342.971263,260.585373 C326.649946,251.1694 305.764193,256.708207 296.358689,273.047691 L202.580275,435.750171 C193.17477,452.089654 198.70742,472.998654 215.028737,482.414627 C231.350054,491.8306 252.235807,486.291793 261.641311,469.952309 L355.419725,307.249829 Z" fill="#FEB005"/>',
'</svg>'
].join('');
var RANGE_DEFINITIONS = [
{ key: 'maxWidth', label: '页面最大宽度', min: 960, max: 2200, step: 20, unit: 'px' },
{ key: 'headerHeight', label: '顶部栏高度', min: 48, max: 100, step: 2, unit: 'px' },
{ key: 'fontSize', label: '正文基准字号', min: 13, max: 22, step: 1, unit: 'px' },
{ key: 'radius', label: '全局圆角', min: 0, max: 14, step: 1, unit: 'px' },
{ key: 'sidebarWidth', label: '侧栏宽度', min: 220, max: 420, step: 10, unit: 'px' },
{ key: 'shellPadding', label: '内容内边距', min: 12, max: 56, step: 2, unit: 'px' },
{ key: 'columnGap', label: '正文与侧栏间距', min: 12, max: 56, step: 2, unit: 'px' }
];
/*
* 默认中性深灰严格使用中性灰阶作为界面底色。
* 彩色只用于成功、警告、错误等语义反馈,避免背景偏蓝或偏紫。
*/
var THEMES = {
neutral: {
label: '中性深灰(默认)',
accent: '#b8b8b8',
textColor: '#eeeeee',
vars: {
'--bg': '#121212',
'--panel': '#1b1b1b',
'--line': '#343434',
'--line-soft': '#272727',
'--text': '#eeeeee',
'--text-muted': '#b6b6b6',
'--text-subtle': '#898989',
'--text-disabled': '#666666',
'--success': '#9fbea9',
'--success-soft': '#223128',
'--danger': '#e28b8b',
'--danger-soft': '#3a2424',
'--warning': '#d8b276',
'--warning-soft': '#382e20',
'--info': '#9bb4d0',
'--info-soft': '#25303b',
'--inverse': '#090909',
'--inverse-border': '#3a3a3a',
'--inverse-text': '#f5f5f5',
'--color-dark-rgb': '0,0,0',
'--backdrop': 'rgba(0,0,0,.68)',
'--shadow-base': 'rgba(0,0,0,.28)',
'--shadow-medium': 'rgba(0,0,0,.48)'
}
},
graphite: {
label: '石墨青',
accent: '#7faea6',
textColor: '#ecf0f2',
vars: {
'--bg': '#111416',
'--panel': '#191d20',
'--line': '#30363b',
'--line-soft': '#24292d',
'--text': '#ecf0f2',
'--text-muted': '#a9b1b7',
'--text-subtle': '#77818a',
'--text-disabled': '#5f686f',
'--success': '#8bb59b',
'--success-soft': '#203329',
'--danger': '#df8888',
'--danger-soft': '#382326',
'--warning': '#d2ab70',
'--warning-soft': '#362d20',
'--info': '#8fafd0',
'--info-soft': '#24303c',
'--inverse': '#090b0c',
'--inverse-border': '#343b40',
'--inverse-text': '#f4f7f8',
'--color-dark-rgb': '3,5,6',
'--backdrop': 'rgba(3,5,6,.68)',
'--shadow-base': 'rgba(0,0,0,.30)',
'--shadow-medium': 'rgba(0,0,0,.50)'
}
},
warm: {
label: '暖石墨',
accent: '#b8a27f',
textColor: '#eee9e1',
vars: {
'--bg': '#151310',
'--panel': '#1e1b17',
'--line': '#37322b',
'--line-soft': '#29251f',
'--text': '#eee9e1',
'--text-muted': '#b8afa3',
'--text-subtle': '#8b8277',
'--text-disabled': '#685f55',
'--success': '#a3b68e',
'--success-soft': '#293022',
'--danger': '#df8e84',
'--danger-soft': '#3a2521',
'--warning': '#d4ad70',
'--warning-soft': '#392f20',
'--info': '#99adc2',
'--info-soft': '#283039',
'--inverse': '#0b0a08',
'--inverse-border': '#3b362f',
'--inverse-text': '#f7f3ec',
'--color-dark-rgb': '5,4,3',
'--backdrop': 'rgba(5,4,3,.68)',
'--shadow-base': 'rgba(0,0,0,.28)',
'--shadow-medium': 'rgba(0,0,0,.48)'
}
},
original: {
label: '跟随原站主题',
accent: '#5eaaa0',
textColor: '#c8c9d9',
vars: null
}
};
var THEME_VARIABLES = [
'--bg', '--panel', '--line', '--line-soft',
'--text', '--text-muted', '--text-subtle', '--text-disabled',
'--brand', '--brand-hover', '--brand-soft',
'--success', '--success-soft', '--danger', '--danger-soft',
'--warning', '--warning-soft', '--info', '--info-soft',
'--inverse', '--inverse-border', '--inverse-text',
'--color-dark-rgb', '--backdrop', '--shadow-base', '--shadow-medium',
'--focus-ring', '--swal2-background', '--swal2-color',
'--swal2-validation-message-background', '--swal2-validation-message-color'
];
var BASE_CSS = [
'@media (min-width: 721px) {',
' :root {',
' --font-size-sm: calc(var(--lsb-base-font-size) - 2px) !important;',
' --font-size-md: var(--lsb-base-font-size) !important;',
' --font-size-lg: calc(var(--lsb-base-font-size) + 1px) !important;',
' }',
' body { font-size: var(--lsb-base-font-size) !important; }',
' .top .bar {',
' width: min(var(--lsb-wide-max), calc(100vw - 48px)) !important;',
' max-width: none !important;',
' min-height: var(--lsb-header-height) !important;',
' padding: 0 20px !important;',
' grid-template-rows: var(--lsb-header-height) !important;',
' column-gap: 16px !important;',
' }',
' .top .brand { font-size: var(--lsb-base-font-size) !important; }',
' .top .brand::before { width: 28px !important; height: 28px !important; flex-basis: 28px !important; }',
' .top .search-form { width: 280px !important; height: 36px !important; }',
' main.wrap, .footer, .forum-more-panel {',
' width: min(var(--lsb-wide-max), calc(100vw - 48px)) !important;',
' max-width: none !important;',
' }',
' main.wrap { padding: 18px 0 28px !important; }',
' .home-shell { padding: var(--lsb-shell-padding) !important; }',
' .home-shell .forum-layout, .forum-layout { gap: var(--lsb-column-gap) !important; }',
' .home-shell .forum-layout {',
' display: grid !important; grid-template-columns: minmax(0, 1fr) var(--lsb-sidebar-width) !important;',
' align-items: start !important;',
' }',
' .home-shell .forum-main { grid-column: 1 !important; grid-row: 1 !important; }',
' .home-shell .sidebar { grid-column: 2 !important; grid-row: 1 !important; }',
' .home-shell .forum-layout.lsb-home-sidebar-swapped {',
' grid-template-columns: var(--lsb-sidebar-width) minmax(0, 1fr) !important;',
' }',
' .home-shell .forum-layout.lsb-home-sidebar-swapped > .forum-main { grid-column: 2 !important; grid-row: 1 !important; }',
' .home-shell .forum-layout.lsb-home-sidebar-swapped > .sidebar { grid-column: 1 !important; grid-row: 1 !important; }',
' .home-shell .forum-layout.lsb-home-personalized-layout { row-gap: 0 !important; }',
' .home-shell .forum-layout.lsb-home-personalized-layout > #lsb-home-personalization {',
' grid-column: 1 / -1 !important; grid-row: 1 !important; min-width: 0; width: 100%; box-sizing: border-box;',
' }',
' .home-shell .forum-layout.lsb-home-personalized-layout > .forum-main { grid-column: 1 !important; grid-row: 2 !important; }',
' .home-shell .forum-layout.lsb-home-personalized-layout > .sidebar { grid-column: 2 !important; grid-row: 2 !important; }',
' .home-shell .forum-layout.lsb-home-personalized-layout.lsb-home-sidebar-swapped > .forum-main { grid-column: 2 !important; grid-row: 2 !important; }',
' .home-shell .forum-layout.lsb-home-personalized-layout.lsb-home-sidebar-swapped > .sidebar { grid-column: 1 !important; grid-row: 2 !important; }',
' .sidebar { width: var(--lsb-sidebar-width) !important; }',
' .post-item { min-height: 64px !important; padding-top: 13px !important; padding-bottom: 13px !important; }',
' .post-title, .post-content, .notification-content { font-size: var(--lsb-base-font-size) !important; }',
' .post-content { line-height: 1.75 !important; }',
' .post-meta, .topic-pages, .post-content-stats, .profile-exit small {',
' font-size: calc(var(--lsb-base-font-size) - 2px) !important;',
' }',
' h2 { font-size: calc(var(--lsb-base-font-size) + 3px) !important; }',
' h3 { font-size: calc(var(--lsb-base-font-size) + 1px) !important; }',
'}',
'@media (min-width: 721px) and (max-width: 1180px) {',
' .top .bar, main.wrap, .footer, .forum-more-panel { width: calc(100vw - 24px) !important; }',
'}',
'#lsb-home-personalization {',
' display: grid; justify-items: center; gap: 0; padding: 42px 20px 38px;',
' margin: -4px 0 20px; border-bottom: 1px solid var(--line-soft); text-align: center;',
'}',
'#lsb-home-personalization .lsb-home-logo {',
' display: block; width: clamp(64px, 7vw, 96px); height: auto; margin: 0 0 14px;',
' filter: drop-shadow(0 12px 24px rgba(0,0,0,.24));',
'}',
'#lsb-home-personalization .lsb-home-tagline {',
' margin: 0; color: var(--text); font-size: clamp(18px, 2.1vw, 28px);',
' font-weight: 700; letter-spacing: .08em; line-height: 1.25;',
'}',
'#lsb-home-personalization .lsb-home-search-form {',
' grid-column: auto; grid-row: auto; justify-self: center; display: flex;',
' width: min(720px, 100%); height: 46px; margin: 24px auto 0;',
' border: 1px solid var(--line); border-radius: var(--radius);',
' background: var(--panel); box-shadow: 0 10px 28px var(--shadow-base);',
'}',
'#lsb-home-personalization .lsb-home-search-form .search-input { font-size: var(--font-size-md); }',
'#lsb-home-personalization .lsb-home-search-form .search-field { min-width: 76px; }',
'#lsb-home-personalization[hidden] { display: none !important; }',
'/* 主题首帖与评论建立清晰层级:首帖突出,评论缩进显示。 */',
'.topic-post-list { display: grid; gap: 0; }',
'.topic-post-list > .post-entry { box-sizing: border-box; }',
'.topic-post-list > .post-entry:not([data-floor]) {',
' position: relative; margin: 0 0 14px; padding: 18px 20px !important;',
' border: 1px solid var(--line); border-left: 4px solid var(--brand);',
' border-radius: var(--radius); background: var(--panel) !important;',
' box-shadow: 0 10px 24px var(--shadow-base);',
'}',
'.topic-post-list > .post-entry:not([data-floor]) .post-head::before {',
' content: "楼主"; display: inline-flex; align-items: center; flex: 0 0 auto;',
' min-height: 20px; margin-right: 8px; padding: 0 7px; border-radius: 999px;',
' background: var(--brand-soft); color: var(--brand-hover); font-size: 11px; font-weight: 700;',
'}',
'.topic-post-list > .post-entry:not([data-floor]) .post-content {',
' margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--line-soft);',
'}',
'.topic-post-list > .post-entry[data-floor] {',
' position: relative; grid-template-columns: 36px minmax(0, 1fr);',
' grid-template-areas: "avatar body" ". content"; column-gap: 12px; row-gap: 7px;',
' margin: 0 0 0 clamp(18px, 3vw, 42px); padding: 14px 0 !important; border: 0;',
' border-radius: 0; background: transparent !important; box-shadow: none;',
' transition: color .18s ease;',
'}',
'.topic-post-list > .post-entry[data-floor]::after {',
' content: ""; position: absolute; right: 0; bottom: 0; left: 0; height: 1px;',
' background: linear-gradient(90deg, var(--brand) 0, var(--brand) 28px, var(--line) 28px, var(--line-soft) 72%, transparent 100%);',
' opacity: .42; pointer-events: none; transition: opacity .18s ease;',
'}',
'.topic-post-list > .post-entry[data-floor]:hover {',
' background: transparent !important; box-shadow: none;',
'}',
'.topic-post-list > .post-entry[data-floor]:hover::after { opacity: .72; }',
'.topic-post-list > .post-entry[data-floor]:last-child::after { display: none; }',
'.topic-post-list > .post-entry[data-floor] .post-head::before {',
' content: "评论"; display: inline-flex; align-items: center; flex: 0 0 auto;',
' min-height: 19px; margin-right: 7px; padding: 0 6px; border: 1px solid var(--line);',
' border-radius: 999px; color: var(--text-subtle); font-size: 10px; font-weight: 700;',
'}',
'.topic-post-list > .post-entry[data-floor] .post-author { color: var(--text); font-weight: 700; }',
'.topic-post-list > .post-entry[data-floor] .post-meta { margin-top: 5px; }',
'.topic-post-list > .post-entry[data-floor] .post-content {',
' width: auto; margin: 0; padding: 0; border: 0; border-radius: 0; background: transparent;',
'}',
'.topic-post-list > .post-entry[data-floor] .post-head.has-floor { min-height: 30px; padding-right: 176px !important; }',
'.topic-post-list > .post-entry[data-floor] .post-ops { gap: 6px; }',
'.topic-post-list > .post-entry[data-floor] .icon-action { width: 30px; height: 30px; opacity: .7; }',
'.topic-post-list > .post-entry[data-floor] .icon-action::before { width: 18px; height: 18px; background-size: 18px 18px; }',
'.topic-post-list > .post-entry[data-floor] .icon-quote::before { -webkit-mask-size: 18px 18px; mask-size: 18px 18px; }',
'.topic-post-list > .post-entry[data-floor] .content-report-icon { width: 18px; height: 18px; }',
'.topic-post-list > .post-entry[data-floor] .like-coin-form .like-coin-action {',
' min-width: 30px; height: 30px; min-height: 30px; padding: 0 4px; gap: 3px; font-size: 13px; opacity: .7;',
'}',
'.topic-post-list > .post-entry[data-floor] .like-coin-icon { width: 18px; height: 18px; flex-basis: 18px; }',
'.topic-post-list > .post-entry[data-floor] .post-floor {',
' order: initial; min-width: 30px; height: 30px; padding: 0 3px; border: 0; border-radius: 0;',
' background: transparent; color: var(--text-muted); font-size: 14px; font-weight: 700;',
' font-variant-numeric: tabular-nums;',
'}',
'/* 发帖编辑器插件的浅色回退值改为跟随当前深色主题。 */',
'.topic-type-group .topic-type-choice,',
'.topic-type-panels > .topic-type-panel,',
'.lottery-prize-row, .lottery-card, .lottery-condition {',
' background: var(--card-bg) !important; color: var(--text) !important;',
'}',
'.lottery-rule-note { background: var(--brand-soft) !important; color: var(--text-muted) !important; }',
'.lottery-title-status, .lottery-result { background: var(--warning-soft) !important; color: var(--warning) !important; }',
'.lottery-title-status.drawn {',
' background: var(--success-soft) !important; color: var(--success) !important;',
'}',
'.topic-type-group .topic-type-choice strong, .topic-type-group-title,',
'.lottery-prize-head, .lottery-prize-row label, .lottery-card strong { color: var(--text) !important; }',
'.topic-type-group .topic-type-choice small, .lottery-prize-row small, .lottery-card header span,',
'.lottery-card header em, .lottery-prizes span, .virtual-card-tip { color: var(--text-muted) !important; }',
'@media (max-width: 720px) {',
' .topic-post-list { gap: 0; }',
' .topic-post-list > .post-entry:not([data-floor]) { padding: 14px !important; }',
' .topic-post-list > .post-entry[data-floor] {',
' grid-template-columns: 32px minmax(0, 1fr); column-gap: 9px;',
' margin-left: 8px; padding: 12px 0 !important;',
' }',
' .topic-post-list > .post-entry[data-floor] .post-head.has-floor { padding-right: 168px !important; }',
'}',
'/* 每个标签独立圆角;四角统一封顶 12px,避免站点的首尾伪类把单个标签压成胶囊。 */',
'.topic-toolbar > .tab-bar {',
' display: inline-flex !important; flex: 0 0 auto !important; align-items: stretch !important;',
' gap: 5px !important; width: max-content !important; max-width: 100% !important;',
' border-radius: 0 !important; overflow: visible !important;',
' isolation: auto !important; transform: none !important;',
'}',
'.topic-toolbar > .tab-bar > a.tab,',
'.topic-toolbar > .tab-bar > a.tab:first-child,',
'.topic-toolbar > .tab-bar > a.tab:last-child,',
'.topic-toolbar > .tab-bar > a.tab:not(:has(~ .tab)),',
'.topic-toolbar > .tab-bar > a.tab.active {',
' display: inline-flex !important; align-items: center !important; justify-content: center !important;',
' min-height: 27px !important; line-height: 1.1 !important; box-sizing: border-box !important;',
' border-radius: var(--lsb-tab-radius) !important;',
' border-top-left-radius: var(--lsb-tab-radius) !important;',
' border-top-right-radius: var(--lsb-tab-radius) !important;',
' border-bottom-right-radius: var(--lsb-tab-radius) !important;',
' border-bottom-left-radius: var(--lsb-tab-radius) !important;',
' margin-left: 0 !important; overflow: hidden !important; background-clip: padding-box !important;',
'}',
'.topic-toolbar > .tab-bar > a.tab + a.tab { margin-left: 0 !important; }',
'/* 搜索框内部控件统一字号、留白和圆角。 */',
'.search-form {',
' box-sizing: border-box; border-radius: var(--lsb-search-radius) !important; overflow: hidden;',
'}',
'.search-field {',
' flex: 0 0 82px !important; height: calc(100% - 8px) !important;',
' margin: 4px 0 4px 4px !important; padding: 0 28px 0 12px !important;',
' border: 1px solid var(--line-soft) !important;',
' border-radius: var(--lsb-search-radius) !important;',
' background-color: var(--panel) !important; color: var(--text) !important;',
' font-size: var(--lsb-base-font-size) !important; line-height: 1 !important;',
' appearance: none; -webkit-appearance: none;',
' background-image: url("data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 width=%2714%27 height=%2714%27 viewBox=%270 0 14 14%27 fill=%27none%27%3E%3Cpath d=%27M3.5 5.5 7 9l3.5-3.5%27 stroke=%27%237f7f7f%27 stroke-width=%271.4%27 stroke-linecap=%27round%27 stroke-linejoin=%27round%27/%3E%3C/svg%3E");',
' background-repeat: no-repeat; background-position: right 9px center; background-size: 14px;',
'}',
'.search-input { padding: 0 14px !important; font-size: var(--lsb-base-font-size) !important; }',
'.search-btn { width: 48px !important; padding: 0 12px !important; display: inline-flex; align-items: center; justify-content: center; border-radius: 0 var(--lsb-search-radius) var(--lsb-search-radius) 0 !important; }',
'.search-btn svg { width: 19px !important; height: 19px !important; }',
'.search-field option { background: var(--panel); color: var(--text); }',
'.search-form:has(.lsb-search-select) { overflow: visible !important; }',
'.lsb-search-select { position: relative; flex: 0 0 88px; height: 100%; z-index: 3; }',
'.lsb-search-native {',
' position: absolute !important; width: 1px !important; height: 1px !important;',
' opacity: 0 !important; pointer-events: none !important; clip: rect(0 0 0 0) !important;',
'}',
'.lsb-search-select-trigger {',
' position: relative; display: flex; align-items: center; width: 100%; height: 100%;',
' padding: 0 26px 0 12px; border: 0; border-right: 1px solid var(--line-soft);',
' border-radius: var(--lsb-search-radius) 0 0 var(--lsb-search-radius);',
' background: var(--panel); color: var(--text); font: inherit; font-size: var(--lsb-base-font-size);',
' text-align: left; cursor: pointer;',
'}',
'.lsb-search-select-trigger::after {',
' content: ""; position: absolute; right: 10px; top: 50%; width: 7px; height: 7px;',
' border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor;',
' opacity: .72; transform: translateY(-65%) rotate(45deg);',
'}',
'.lsb-search-select-trigger[aria-expanded="true"]::after { transform: translateY(-30%) rotate(225deg); }',
'.lsb-search-options {',
' position: absolute; top: calc(100% + 6px); left: 0; width: 132px; padding: 5px;',
' border: 1px solid var(--line); border-radius: var(--lsb-search-radius);',
' background: var(--panel); box-shadow: 0 14px 34px var(--shadow-medium);',
'}',
'.lsb-search-options[hidden] { display: none !important; }',
'.lsb-search-option {',
' display: block; width: 100%; min-height: 32px; padding: 6px 9px; border: 0;',
' border-radius: min(var(--radius-sm), 8px); background: transparent; color: var(--text-muted);',
' font: inherit; font-size: var(--lsb-base-font-size); text-align: left; cursor: pointer;',
'}',
'.lsb-search-option:hover, .lsb-search-option[aria-selected="true"] {',
' background: var(--brand-soft); color: var(--text);',
'}',
'html, body, .top, .card, .box, .home-shell, .main-panel, .sidebar-card {',
' transition: background-color .18s ease, border-color .18s ease, color .18s ease;',
'}',
'/* 自定义面板:保留原有交互结构,仅对排版和控件做轻量优化。 */',
'#lsb-layout-toggle {',
' --lsb-ui-accent: var(--brand, #b8b8b8); --lsb-ui-text: var(--text, #eeeeee);',
' --lsb-ui-panel: var(--panel, #1b1b1b); --lsb-ui-line: var(--line, #343434);',
' position: fixed; left: 22px; bottom: 22px; z-index: 2147483646;',
' display: inline-flex; align-items: center; justify-content: center;',
' width: 44px; height: 44px; padding: 0; border: 1px solid var(--lsb-ui-line);',
' border-radius: 12px; background: var(--lsb-ui-panel); color: var(--lsb-ui-text);',
' box-shadow: 0 6px 18px var(--shadow-base, rgba(0,0,0,.28));',
' cursor: pointer; touch-action: none; transition: transform .18s ease, border-color .18s ease, background .18s ease;',
'}',
'#lsb-layout-toggle.lsb-toggle-dragging { cursor: grabbing; transform: none !important; transition: none !important; }',
'#lsb-layout-toggle:hover { transform: scale(1.05); border-color: var(--lsb-ui-accent); background: var(--brand-soft, var(--lsb-ui-panel)); }',
'#lsb-layout-toggle[aria-expanded="true"] { border-color: var(--lsb-ui-accent); background: var(--brand-soft, var(--lsb-ui-panel)); }',
'#lsb-layout-toggle:focus-visible, #lsb-layout-panel :focus-visible { outline: 2px solid var(--lsb-ui-accent); outline-offset: 2px; }',
'#lsb-layout-toggle svg { width: 20px; height: 20px; }',
'#lsb-layout-panel {',
' --lsb-ui-accent: var(--brand, #b8b8b8); --lsb-ui-text: var(--text, #eeeeee);',
' --lsb-ui-panel: var(--panel, #1b1b1b); --lsb-ui-line: var(--line, #343434);',
' --lsb-ui-line-soft: var(--line-soft, #272727); --lsb-ui-muted: var(--text-muted, #b6b6b6);',
' position: fixed; right: 22px; bottom: 76px; z-index: 2147483647;',
' width: min(360px, calc(100vw - 24px)); max-height: calc(100vh - 163px);',
' overflow: auto; color: var(--lsb-ui-text); background: var(--lsb-ui-panel);',
' border: 1px solid var(--lsb-ui-line); border-radius: 14px;',
' box-shadow: 0 18px 46px var(--shadow-medium, rgba(0,0,0,.48));',
' font: 13px/1.45 -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif;',
' scrollbar-color: var(--lsb-ui-muted) transparent;',
'}',
'#lsb-layout-panel[hidden] { display: none !important; }',
'/* 侧栏对调:切换按钮与面板位置联动 */',
'.lsb-toggle-left { left: 22px !important; right: auto !important; }',
'.lsb-toggle-right { right: 22px !important; left: auto !important; }',
'.lsb-panel-left { left: 22px !important; right: auto !important; }',
'.lsb-panel-right { right: 22px !important; left: auto !important; }',
'.lsb-panel-head {',
' position: sticky; top: 0; z-index: 2; display: flex; align-items: center;',
' justify-content: space-between; gap: 7px; padding: 15px 16px 13px;',
' border-bottom: 1px solid var(--lsb-ui-line); background: var(--lsb-ui-panel);',
' cursor: move; user-select: none; touch-action: none;',
'}',
'.lsb-panel-head button { cursor: pointer; }',
'#lsb-layout-panel.lsb-dragging { transition: none !important; user-select: none; }',
'.lsb-panel-title { display: grid; gap: 2px; }',
'.lsb-panel-title strong { color: var(--lsb-ui-text); font-size: 15px; letter-spacing: .01em; }',
'.lsb-panel-title span { color: var(--lsb-ui-muted); font-size: 11px; }',
'.lsb-icon-button {',
' display: inline-flex; align-items: center; justify-content: center;',
' width: 22px; height: 22px; padding: 0; border: 0;',
' border-radius: 0; background: transparent; color: var(--lsb-ui-muted); font-size: 16px; line-height: 1; cursor: pointer;',
'}',
'.lsb-panel-credit { color: var(--lsb-ui-muted); font-size: 10px; line-height: 1; opacity: .72; white-space: nowrap; }',
'.lsb-panel-credit a { color: inherit; text-decoration: none; }',
'.lsb-panel-body { display: grid; gap: 17px; padding: 15px 16px 17px; }',
'.lsb-section { display: grid; gap: 11px; }',
'.lsb-section-title {',
' display: flex; align-items: center; gap: 8px; margin: 0;',
' color: var(--lsb-ui-muted); font-size: 10px; line-height: 1.2; font-weight: 700;',
' letter-spacing: .1em; text-transform: uppercase;',
'}',
'.lsb-section-title::after { content: ""; height: 1px; flex: 1; background: var(--lsb-ui-line-soft); }',
'.lsb-range-row { display: grid; gap: 7px; }',
'.lsb-range-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }',
'.lsb-range-label, .lsb-field > span { color: var(--lsb-ui-muted); font-size: 12px; }',
'.lsb-range-value { min-width: 54px; color: var(--lsb-ui-text); font-size: 12px; font-variant-numeric: tabular-nums; text-align: right; }',
'.lsb-range { width: 100%; height: 18px; margin: 0; accent-color: var(--lsb-ui-accent); cursor: pointer; }',
'.lsb-field { display: grid; gap: 7px; }',
'.lsb-select, .lsb-textarea {',
' width: 100%; border: 1px solid var(--lsb-ui-line); border-radius: 8px;',
' background: var(--bg-soft, var(--lsb-ui-panel)); color: var(--lsb-ui-text); font: inherit; box-sizing: border-box;',
'}',
'.lsb-select { height: 38px; padding: 0 10px; cursor: pointer; }',
'.lsb-select option { background: var(--lsb-ui-panel); color: var(--lsb-ui-text); }',
'.lsb-select:focus, .lsb-textarea:focus { outline: none; border-color: var(--lsb-ui-accent); box-shadow: 0 0 0 2px var(--focus-ring, rgba(184,184,184,.34)); }',
'.lsb-color-line {',
' display: grid; grid-template-columns: 42px 1fr; align-items: center; gap: 10px;',
' padding: 7px 9px; border: 1px solid var(--lsb-ui-line); border-radius: 9px; background: var(--bg-soft, var(--lsb-ui-panel));',
'}',
'.lsb-color-input { width: 42px; height: 30px; padding: 2px; border: 0; border-radius: 7px; background: transparent; cursor: pointer; }',
'.lsb-color-value { color: var(--lsb-ui-muted); font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }',
'.lsb-check-line { display: grid; grid-template-columns: 34px minmax(0, 1fr); align-items: center; gap: 10px; min-height: 26px; color: var(--lsb-ui-muted); cursor: pointer; }',
'.lsb-check-line input { position: relative; width: 34px; height: 19px; margin: 0; border: 1px solid var(--lsb-ui-line); border-radius: 999px; background: var(--lsb-ui-line-soft); cursor: pointer; appearance: none; -webkit-appearance: none; transition: background-color .16s ease, border-color .16s ease; }',
'.lsb-check-line input::after { content: ""; position: absolute; top: 3px; left: 3px; width: 11px; height: 11px; border-radius: 50%; background: var(--lsb-ui-muted); transition: left .16s ease, background-color .16s ease; }',
'.lsb-check-line input:checked { border-color: var(--lsb-ui-accent); background: var(--lsb-ui-accent); }',
'.lsb-check-line input:checked::after { left: 18px; background: var(--inverse, #111111); }',
'.lsb-check-line input:focus-visible { outline: 2px solid var(--lsb-ui-accent); outline-offset: 2px; }',
'.lsb-check-line input:checked + span { color: var(--lsb-ui-text); }',
'.lsb-check-line span { line-height: 1.45; }',
'.lsb-theme-note { margin: 0; color: var(--lsb-ui-muted); font-size: 11px; line-height: 1.55; }',
'.lsb-filter-section { margin-top: 3px; }',
'.lsb-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 9px; }',
'.lsb-button {',
' min-height: 38px; padding: 0 12px; border: 1px solid var(--lsb-ui-line); border-radius: 9px;',
' background: transparent; color: var(--lsb-ui-text); font: inherit; font-weight: 600; cursor: pointer;',
'}',
'.lsb-button:hover { border-color: var(--lsb-ui-accent); background: var(--brand-soft, transparent); }',
'.lsb-button-primary { border-color: var(--lsb-ui-accent); background: var(--brand-soft, transparent); }',
'.lsb-status { min-height: 16px; margin: 0; color: var(--lsb-ui-muted); font-size: 11px; text-align: center; }',
'.lsb-input { width: 100%; height: 38px; padding: 0 10px; border: 1px solid var(--lsb-ui-line); border-radius: 8px; background: var(--bg-soft, var(--lsb-ui-panel)); color: var(--lsb-ui-text); font: inherit; box-sizing: border-box; }',
'.lsb-input:focus { outline: none; border-color: var(--lsb-ui-accent); box-shadow: 0 0 0 2px var(--focus-ring, rgba(184,184,184,.34)); }',
'.lsb-check-line-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; }',
'.lsb-upload-settings-toggle { display: inline-flex; align-items: center; justify-content: center; min-height: 24px; padding: 0 8px; border: 1px solid var(--lsb-ui-line); border-radius: 6px; background: transparent; color: var(--lsb-ui-muted); font: 10px/1 inherit; cursor: pointer; }',
'.lsb-upload-settings-toggle:hover { border-color: var(--lsb-ui-accent); color: var(--lsb-ui-text); }',
'.lsb-upload-settings-toggle[hidden], .lsb-upload-settings[hidden] { display: none !important; }',
'.lsb-upload-settings { display: grid; gap: 10px; margin: 2px 0 2px 44px; padding: 10px; border-left: 1px solid var(--lsb-ui-line-soft); }',
'.lsb-upload-hint { margin: 0; color: var(--lsb-ui-muted); font-size: 10px; line-height: 1.5; }',
'.lsb-image-upload-button { display: inline-flex; align-items: center; justify-content: center; min-height: 28px; margin: 7px 0 0; padding: 0 9px; border: 1px dashed var(--line, #343434); border-radius: 7px; background: transparent; color: var(--text-muted, #b6b6b6); font: 12px/1 inherit; cursor: pointer; }',
'.lsb-image-upload-button:hover, .lsb-image-upload-button:focus-visible { border-color: var(--brand, #b8b8b8); color: var(--text, #eeeeee); outline: none; }',
'.lsb-image-upload-button[disabled] { opacity: .65; cursor: wait; }',
'.lsb-image-upload-drop-target { outline: 2px dashed var(--brand, #b8b8b8); outline-offset: 3px; }',
'.lsb-image-upload-hint { margin: 5px 0 0; color: var(--text-muted, #b6b6b6); font-size: 11px; }',
'#lsb-image-lightbox { position: fixed; inset: 0; z-index: 2147483645; display: flex; align-items: center; justify-content: center; padding: 24px; background: var(--backdrop, rgba(0,0,0,.68)); cursor: zoom-out; }',
'#lsb-image-lightbox[hidden] { display: none !important; }',
'#lsb-image-lightbox .lsb-lightbox-image { max-width: min(1200px, 100%); max-height: calc(100vh - 48px); border: 1px solid var(--line, #343434); border-radius: var(--radius, 6px); background: var(--panel, #1b1b1b); box-shadow: 0 20px 52px var(--shadow-medium, rgba(0,0,0,.48)); cursor: default; object-fit: contain; }',
'#lsb-image-lightbox .lsb-lightbox-close { position: absolute; top: 14px; right: 16px; width: 30px; height: 30px; padding: 0; border: 1px solid var(--line, #343434); border-radius: 8px; background: var(--panel, #1b1b1b); color: var(--text, #eeeeee); font-size: 20px; line-height: 1; cursor: pointer; }',
'.post-entry .post-content img[data-lsb-lightbox-image], .post-item .post-content img[data-lsb-lightbox-image] { cursor: zoom-in; }',
'body.lsb-lightbox-open { overflow: hidden !important; }',
'@media (max-width: 520px) {',
' #lsb-layout-toggle { right: 12px; bottom: 12px; }',
' #lsb-layout-panel { right: 12px; bottom: 66px; max-height: calc(100vh - 82px); }',
'}',
'@media (prefers-reduced-motion: reduce) {',
' #lsb-layout-toggle, html, body, .top, .card, .box, .home-shell, .main-panel, .sidebar-card { transition-duration: .01ms !important; }',
'}',
'.lsb-textarea { padding: 8px 10px; font-size: 12px; line-height: 1.5; resize: vertical; }'
].join('\n');
var panel = null;
var toggleButton = null;
var statusElement = null;
var saveTimer = 0;
var suppressToggleClick = false;
var searchDropdownEventsBound = false;
var searchDropdownId = 0;
var homeObserver = null;
var imageLightbox = null;
var imageLightboxImage = null;
var imageLightboxEventsBound = false;
var imageUploadBusyEditors = new WeakSet();
var autoCheckinInFlight = false;
var autoCheckinStartTimer = 0;
var settings = loadSettings();
addStyle(buildStartupCss(settings) + '\n' + BASE_CSS);
applySettings();
startHomeObserver();
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', ensureInterface, { once: true });
} else {
ensureInterface();
}
if (typeof GM_registerMenuCommand === 'function') {
GM_registerMenuCommand('打开 LINUX SB 布局设置', function () {
ensureInterface();
setPanelOpen(true);
});
}
function addStyle(cssText) {
if (typeof GM_addStyle === 'function') {
GM_addStyle(cssText);
return;
}
var style = document.createElement('style');
style.id = 'linux-sb-wide-layout-style';
style.textContent = cssText;
(document.head || document.documentElement).appendChild(style);
}
function buildStartupCss(currentSettings) {
var variables = {
'--lsb-wide-max': currentSettings.maxWidth + 'px',
'--lsb-header-height': currentSettings.headerHeight + 'px',
'--lsb-base-font-size': currentSettings.fontSize + 'px',
'--lsb-radius': currentSettings.radius + 'px',
'--radius': currentSettings.radius + 'px',
'--radius-sm': Math.max(0, currentSettings.radius - 2) + 'px',
'--lsb-tab-radius': Math.min(currentSettings.radius, 12) + 'px',
'--lsb-search-radius': Math.min(currentSettings.radius, 12) + 'px',
'--lsb-sidebar-width': currentSettings.sidebarWidth + 'px',
'--lsb-shell-padding': currentSettings.shellPadding + 'px',
'--lsb-column-gap': currentSettings.columnGap + 'px',
'--bg-soft': 'var(--bg)',
'--card-bg': 'var(--panel)'
};
var theme = THEMES[currentSettings.theme] || THEMES.neutral;
if (theme.vars) {
Object.keys(theme.vars).forEach(function (name) {
variables[name] = theme.vars[name];
});
}
var accentRgb = hexToRgb(currentSettings.accent);
var background = theme.vars && theme.vars['--bg'] ? theme.vars['--bg'] : '#1a1b2e';
variables['--brand'] = currentSettings.accent;
variables['--brand-hover'] = mixHex(currentSettings.accent, '#ffffff', 0.18);
variables['--brand-soft'] = 'rgba(' + accentRgb.r + ',' + accentRgb.g + ',' + accentRgb.b + ',.16)';
variables['--focus-ring'] = 'rgba(' + accentRgb.r + ',' + accentRgb.g + ',' + accentRgb.b + ',.34)';
variables['--text'] = currentSettings.textColor;
variables['--text-muted'] = mixHex(currentSettings.textColor, background, 0.34);
variables['--text-subtle'] = mixHex(currentSettings.textColor, background, 0.55);
variables['--text-disabled'] = mixHex(currentSettings.textColor, background, 0.72);
if (currentSettings.theme !== 'original') {
variables['--swal2-background'] = 'var(--panel)';
variables['--swal2-color'] = 'var(--text)';
variables['--swal2-validation-message-background'] = 'var(--line-soft)';
variables['--swal2-validation-message-color'] = 'var(--text-muted)';
}
return [
':root:not([data-lsb-ready]) {',
Object.keys(variables).map(function (name) {
return ' ' + name + ': ' + variables[name] + ' !important;';
}).join('\n'),
'}'
].join('\n');
}
function startHomeObserver() {
if (homeObserver || typeof MutationObserver !== 'function') {
return;
}
homeObserver = new MutationObserver(function () {
applyHomePersonalization();
enhanceSearchFields(document);
enforceRadiusOverrides();
scheduleFilter();
updateImageLightboxTargets();
updateImageUploadTargets();
});
homeObserver.observe(document, { childList: true, subtree: true });
}
function normalizeSettings(value) {
var source = value && typeof value === 'object' ? value : {};
var result = Object.assign({}, DEFAULTS, source);
RANGE_DEFINITIONS.forEach(function (definition) {
var number = Number(result[definition.key]);
if (!Number.isFinite(number)) {
number = DEFAULTS[definition.key];
}
result[definition.key] = Math.min(definition.max, Math.max(definition.min, number));
});
if (!Object.prototype.hasOwnProperty.call(THEMES, result.theme)) {
result.theme = DEFAULTS.theme;
}
if (!Object.prototype.hasOwnProperty.call(TEXT_PALETTES, result.textPalette)) {
result.textPalette = DEFAULTS.textPalette;
}
result.homePersonalized = result.homePersonalized === true || result.homePersonalized === 'true';
result.autoCheckin = result.autoCheckin === true || result.autoCheckin === 'true';
result.autoCheckinLastDate = /^\d{4}-\d{2}-\d{2}$/.test(String(result.autoCheckinLastDate || '')) ? String(result.autoCheckinLastDate) : '';
result.imageLightbox = result.imageLightbox === true || result.imageLightbox === 'true';
result.imageUpload = result.imageUpload === true || result.imageUpload === 'true';
result.imageUploadProvider = ['imgur', 'nodeimage', 'custom'].indexOf(result.imageUploadProvider) >= 0 ? result.imageUploadProvider : 'imgur';
result.imageUploadHost = normalizeUploadText(result.imageUploadHost, DEFAULTS.imageUploadHost, 512);
result.imageUploadEndpoint = normalizeUploadText(result.imageUploadEndpoint, DEFAULTS.imageUploadEndpoint, 1024);
result.imageUploadFileField = normalizeUploadField(result.imageUploadFileField, DEFAULTS.imageUploadFileField);
result.imageUploadResponsePath = normalizeUploadResponsePath(result.imageUploadResponsePath, DEFAULTS.imageUploadResponsePath);
result.imageUploadAuthMode = ['none', 'imgur-client-id', 'nodeimage-api-key', 'bearer'].indexOf(result.imageUploadAuthMode) >= 0 ? result.imageUploadAuthMode : DEFAULTS.imageUploadAuthMode;
result.imageUploadToken = normalizeUploadText(result.imageUploadToken, '', 512);
result.imageUploadSettingsCollapsed = result.imageUploadSettingsCollapsed === true || result.imageUploadSettingsCollapsed === 'true';
if (!/^#[0-9a-f]{6}$/i.test(String(result.accent))) {
result.accent = THEMES[result.theme].accent;
}
result.accent = String(result.accent).toLowerCase();
if (!/^#[0-9a-f]{6}$/i.test(String(result.textColor))) {
result.textColor = THEMES[result.theme].textColor || DEFAULTS.textColor;
}
result.textColor = String(result.textColor).toLowerCase();
['panelLeft', 'panelTop', 'toggleLeft', 'toggleTop'].forEach(function (key) {
var rawPosition = result[key];
var position = Number(rawPosition);
result[key] = rawPosition !== null && rawPosition !== undefined && rawPosition !== '' &&
Number.isFinite(position) && position >= 0 ? Math.round(position) : null;
});
// 设置迁移:v1 升级到 v2,maxWidth 从 1600 调整为 1100
if ((result.version || 0) < 2 && result.maxWidth === 1600) {
result.maxWidth = 1100;
}
result.version = SETTINGS_VERSION;
if (!Array.isArray(result.titleFilters)) {
result.titleFilters = [];
}
if (!Array.isArray(result.userFilters)) {
result.userFilters = [];
}
return result;
}
function loadSettings() {
try {
var stored = null;
if (typeof GM_getValue === 'function') {
stored = GM_getValue(STORAGE_KEY, null);
} else {
var raw = localStorage.getItem(STORAGE_KEY);
stored = raw ? JSON.parse(raw) : null;
}
return normalizeSettings(stored);
} catch (error) {
return normalizeSettings(null);
}
}
function persistSettings() {
try {
if (typeof GM_setValue === 'function') {
GM_setValue(STORAGE_KEY, settings);
} else {
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
}
showStatus('设置已保存');
} catch (error) {
showStatus('设置已应用,但保存失败');
}
}
function scheduleSave() {
window.clearTimeout(saveTimer);
saveTimer = window.setTimeout(persistSettings, 140);
}
function setRootVariable(name, value) {
if (document.documentElement) {
document.documentElement.style.setProperty(name, value, 'important');
}
}
function clearThemeVariables() {
if (!document.documentElement) {
return;
}
THEME_VARIABLES.forEach(function (name) {
document.documentElement.style.removeProperty(name);
});
}
function applySettings() {
if (!document.documentElement) {
return;
}
setRootVariable('--lsb-wide-max', settings.maxWidth + 'px');
setRootVariable('--lsb-header-height', settings.headerHeight + 'px');
setRootVariable('--lsb-base-font-size', settings.fontSize + 'px');
setRootVariable('--lsb-radius', settings.radius + 'px');
setRootVariable('--radius', settings.radius + 'px');
setRootVariable('--radius-sm', Math.max(0, settings.radius - 2) + 'px');
setRootVariable('--lsb-tab-radius', Math.min(settings.radius, 12) + 'px');
setRootVariable('--lsb-search-radius', Math.min(settings.radius, 12) + 'px');
setRootVariable('--lsb-sidebar-width', settings.sidebarWidth + 'px');
setRootVariable('--lsb-shell-padding', settings.shellPadding + 'px');
setRootVariable('--lsb-column-gap', settings.columnGap + 'px');
setRootVariable('--bg-soft', 'var(--bg)');
setRootVariable('--card-bg', 'var(--panel)');
applyTheme();
document.documentElement.setAttribute('data-lsb-ready', '');
applyHomePersonalization();
applySidebarSwap();
enhanceSearchFields(document);
enforceRadiusOverrides();
applyFilters();
applyAutoCheckin();
applyImageLightbox();
applyImageUpload();
if (panel) {
panel.style.setProperty('--lsb-ui-accent', settings.accent);
panel.style.setProperty('--lsb-ui-text', settings.textColor);
}
if (toggleButton) {
toggleButton.style.setProperty('--lsb-ui-accent', settings.accent);
toggleButton.style.setProperty('--lsb-ui-text', settings.textColor);
}
}
function normalizeUploadText(value, fallback, maxLength) {
var normalized = value === null || value === undefined ? '' : String(value).trim();
if (!normalized) {
return fallback;
}
return normalized.slice(0, maxLength);
}
function normalizeUploadField(value, fallback) {
var normalized = normalizeUploadText(value, fallback, 64);
return /^[A-Za-z0-9_-]+$/.test(normalized) ? normalized : fallback;
}
function normalizeUploadResponsePath(value, fallback) {
var normalized = normalizeUploadText(value, fallback, 120);
return /^(?:[A-Za-z_$][A-Za-z0-9_$]*)(?:\.[A-Za-z_$][A-Za-z0-9_$]*)*$/.test(normalized) ? normalized : fallback;
}
function syncImageUploadControls() {
if (!panel) {
return;
}
var settingsBlock = panel.querySelector('[data-lsb-upload-settings]');
var settingsToggle = panel.querySelector('[data-lsb-upload-settings-toggle]');
var provider = panel.querySelector('[data-lsb-upload-provider]');
var host = panel.querySelector('[data-lsb-upload-host]');
var endpoint = panel.querySelector('[data-lsb-upload-endpoint]');
var field = panel.querySelector('[data-lsb-upload-file-field]');
var responsePath = panel.querySelector('[data-lsb-upload-response-path]');
var authMode = panel.querySelector('[data-lsb-upload-auth-mode]');
var token = panel.querySelector('[data-lsb-upload-token]');
if (!settingsBlock || !settingsToggle || !provider || !host || !endpoint || !field || !responsePath || !authMode || !token) {
return;
}
var expanded = settings.imageUpload && !settings.imageUploadSettingsCollapsed;
settingsBlock.hidden = !expanded;
settingsToggle.style.display = settings.imageUpload ? '' : 'none';
settingsToggle.setAttribute('aria-expanded', String(expanded));
settingsToggle.textContent = expanded ? '收起配置' : '展开配置';
provider.value = settings.imageUploadProvider;
host.value = settings.imageUploadHost;
endpoint.value = settings.imageUploadEndpoint;
field.value = settings.imageUploadFileField;
responsePath.value = settings.imageUploadResponsePath;
authMode.value = settings.imageUploadAuthMode;
token.value = settings.imageUploadToken;
}
function applyImageUpload() {
updateImageUploadTargets();
}
function updateImageUploadTargets() {
if (!document.querySelectorAll) {
return;
}
document.querySelectorAll('[data-lsb-image-upload-button]').forEach(function (button) {
if (!settings.imageUpload || !button.previousElementSibling || !isImageUploadEditor(button.previousElementSibling)) {
button.remove();
}
});
if (!settings.imageUpload) {
document.querySelectorAll('.lsb-image-upload-drop-target').forEach(function (editor) {
editor.classList.remove('lsb-image-upload-drop-target');
});
return;
}
getImageUploadEditors().forEach(function (editor) {
bindImageUploadEditor(editor);
});
}
function getImageUploadEditors() {
var result = [];
document.querySelectorAll('textarea, [contenteditable="true"], input[data-quick-reply-action]').forEach(function (editor) {
if (isImageUploadEditor(editor)) {
result.push(editor);
}
});
return result;
}
function isImageUploadEditor(editor) {
if (!editor || !editor.isConnected || editor.closest('#lsb-layout-panel') || editor.readOnly || editor.disabled) {
return false;
}
if (editor.matches('input[data-quick-reply-action]')) {
return true;
}
if (editor.matches('[contenteditable="true"]')) {
return true;
}
if (!editor.matches('textarea')) {
return false;
}
return !editor.classList.contains('lsb-textarea');
}
function bindImageUploadEditor(editor) {
if (editor.getAttribute('data-lsb-image-upload-bound') === '1') {
ensureImageUploadButton(editor);
return;
}
editor.setAttribute('data-lsb-image-upload-bound', '1');
editor.addEventListener('dragenter', function (event) {
if (settings.imageUpload && containsImageFiles(event.dataTransfer)) {
event.preventDefault();
editor.classList.add('lsb-image-upload-drop-target');
}
});
editor.addEventListener('dragover', function (event) {
if (settings.imageUpload && containsImageFiles(event.dataTransfer)) {
event.preventDefault();
editor.classList.add('lsb-image-upload-drop-target');
if (event.dataTransfer) {
event.dataTransfer.dropEffect = 'copy';
}
}
});
editor.addEventListener('dragleave', function () {
editor.classList.remove('lsb-image-upload-drop-target');
});
editor.addEventListener('drop', function (event) {
editor.classList.remove('lsb-image-upload-drop-target');
if (!settings.imageUpload || !containsImageFiles(event.dataTransfer)) {
return;
}
event.preventDefault();
event.stopPropagation();
uploadImageFiles(editor, Array.from(event.dataTransfer.files || []));
});
editor.addEventListener('paste', function (event) {
if (!settings.imageUpload) {
return;
}
var files = getClipboardImageFiles(event.clipboardData);
if (!files.length) {
return;
}
event.preventDefault();
event.stopPropagation();
uploadImageFiles(editor, files);
});
ensureImageUploadButton(editor);
}
function ensureImageUploadButton(editor) {
var button = editor.nextElementSibling;
if (button && button.hasAttribute('data-lsb-image-upload-button')) {
return;
}
button = document.createElement('button');
button.type = 'button';
button.className = 'lsb-image-upload-button';
button.setAttribute('data-lsb-image-upload-button', '1');
button.textContent = '上传图片';
button.addEventListener('click', function () {
selectImagesForEditor(editor);
});
editor.insertAdjacentElement('afterend', button);
}
function containsImageFiles(dataTransfer) {
if (!dataTransfer || !dataTransfer.files || !dataTransfer.files.length) {
return false;
}
return Array.from(dataTransfer.files).some(function (file) {
return file && /^image\//i.test(file.type || '');
});
}
function getClipboardImageFiles(clipboardData) {
if (!clipboardData) {
return [];
}
var files = Array.from(clipboardData.items || []).map(function (item) {
if (!item || item.kind !== 'file' || !/^image\//i.test(item.type || '')) {
return null;
}
return item.getAsFile ? item.getAsFile() : null;
}).filter(Boolean);
if (!files.length && clipboardData.files) {
files = Array.from(clipboardData.files).filter(function (file) {
return file && /^image\//i.test(file.type || '');
});
}
return files;
}
function selectImagesForEditor(editor) {
if (!settings.imageUpload || imageUploadBusyEditors.has(editor)) {
return;
}
var picker = document.createElement('input');
picker.type = 'file';
picker.accept = 'image/*';
picker.multiple = true;
picker.hidden = true;
picker.addEventListener('change', function () {
uploadImageFiles(editor, Array.from(picker.files || []));
picker.remove();
}, { once: true });
document.body.appendChild(picker);
picker.click();
}
function uploadImageFiles(editor, files) {
var validFiles = files.filter(function (file) {
return file && /^image\//i.test(file.type || '') && file.size > 0;
});
var oversized = validFiles.filter(function (file) {
return file.size > 10 * 1024 * 1024;
});
validFiles = validFiles.filter(function (file) {
return file.size <= 10 * 1024 * 1024;
}).slice(0, 6);
if (!validFiles.length) {
showStatus(oversized.length ? '图片超过 10 MB,未开始上传' : '请选择 PNG、JPG、GIF、WebP 等图片文件');
return;
}
if (imageUploadBusyEditors.has(editor)) {
return;
}
imageUploadBusyEditors.add(editor);
setImageUploadButtonState(editor, '上传中…', true);
var uploaded = 0;
var chain = Promise.resolve();
validFiles.forEach(function (file) {
chain = chain.then(function () {
return uploadOneImage(file).then(function (url) {
insertMarkdownImage(editor, url, file.name);
uploaded += 1;
setImageUploadButtonState(editor, '已上传 ' + uploaded + '/' + validFiles.length, true);
});
});
});
chain.then(function () {
showStatus('已插入 ' + uploaded + ' 张图片');
}).catch(function (error) {
showStatus('图片上传失败:' + getUploadErrorMessage(error));
}).finally(function () {
imageUploadBusyEditors.delete(editor);
setImageUploadButtonState(editor, '上传图片', false);
});
}
function getImageUploadConfig() {
var endpoint = settings.imageUploadEndpoint;
var host = settings.imageUploadHost;
if (!isSafeHttpsUrl(endpoint) || !isSafeHttpsUrl(host)) {
throw new Error('请填写有效的 HTTPS 图床主页和上传接口');
}
if (settings.imageUploadAuthMode === 'imgur-client-id' && !settings.imageUploadToken) {
throw new Error('Imgur 上传需要填写 Client ID');
}
if (settings.imageUploadAuthMode === 'nodeimage-api-key' && !settings.imageUploadToken) {
throw new Error('Nodeimage 上传需要填写 API 密钥');
}
return {
provider: settings.imageUploadProvider,
endpoint: endpoint,
field: settings.imageUploadFileField,
responsePath: settings.imageUploadResponsePath,
authMode: settings.imageUploadAuthMode,
token: settings.imageUploadToken
};
}
function isSafeHttpsUrl(value) {
try {
var url = new URL(String(value));
return url.protocol === 'https:' && !url.username && !url.password;
} catch (error) {
return false;
}
}
function uploadOneImage(file) {
var config;
try {
config = getImageUploadConfig();
} catch (error) {
return Promise.reject(error);
}
var data = new FormData();
data.append(config.field, file, file.name);
var headers = {};
if (config.authMode === 'imgur-client-id') {
headers.Authorization = 'Client-ID ' + config.token;
} else if (config.authMode === 'nodeimage-api-key') {
headers['X-API-Key'] = config.token;
} else if (config.authMode === 'bearer' && config.token) {
headers.Authorization = 'Bearer ' + config.token;
}
if (typeof GM_xmlhttpRequest === 'function') {
return new Promise(function (resolve, reject) {
GM_xmlhttpRequest({
method: 'POST',
url: config.endpoint,
headers: headers,
data: data,
responseType: 'json',
timeout: 60000,
onload: function (response) {
if (response.status < 200 || response.status >= 300) {
reject(new Error('图床返回 HTTP ' + response.status));
return;
}
try {
resolve(resolveUploadedImageUrl(parseUploadResponse(response.response, response.responseText), config));
} catch (error) {
reject(error);
}
},
onerror: function () {
reject(new Error('网络或跨域请求失败'));
},
ontimeout: function () {
reject(new Error('请求超时'));
}
});
});
}
return fetch(config.endpoint, {
method: 'POST',
headers: headers,
body: data,
credentials: 'omit'
}).then(function (response) {
if (!response.ok) {
throw new Error('图床返回 HTTP ' + response.status);
}
return response.text();
}).then(function (body) {
return resolveUploadedImageUrl(parseUploadResponse(null, body), config);
});
}
function parseUploadResponse(response, responseText) {
if (response && typeof response === 'object') {
return response;
}
if (typeof responseText === 'string') {
try {
return JSON.parse(responseText);
} catch (error) {
return responseText;
}
}
return response;
}
function resolveUploadedImageUrl(payload, config) {
if (config.provider !== 'nodeimage') {
return extractImageUrl(payload, config.responsePath);
}
// Nodeimage 的公开接口说明未公开固定返回字段,因此优先使用预设字段,再兼容常见图片直链字段。
var paths = [config.responsePath, 'data.url', 'data.link', 'url', 'link', 'image.url', 'image.link', 'data.image.url', 'data.image.link'];
for (var index = 0; index < paths.length; index += 1) {
try {
return extractImageUrl(payload, paths[index]);
} catch (error) {
// 尝试下一个候选字段。
}
}
var fallbackUrl = findFirstSafeHttpsUrl(payload, 0);
if (fallbackUrl) {
return fallbackUrl;
}
throw new Error('未从 Nodeimage 返回结果中取得 HTTPS 图片直链');
}
function findFirstSafeHttpsUrl(value, depth) {
if (depth > 5 || value === null || value === undefined) {
return '';
}
if (typeof value === 'string') {
var candidate = value.trim();
return isSafeHttpsUrl(candidate) ? candidate : '';
}
if (Array.isArray(value)) {
for (var index = 0; index < value.length; index += 1) {
var arrayUrl = findFirstSafeHttpsUrl(value[index], depth + 1);
if (arrayUrl) {
return arrayUrl;
}
}
return '';
}
if (typeof value === 'object') {
var keys = Object.keys(value);
for (var keyIndex = 0; keyIndex < keys.length; keyIndex += 1) {
var objectUrl = findFirstSafeHttpsUrl(value[keys[keyIndex]], depth + 1);
if (objectUrl) {
return objectUrl;
}
}
}
return '';
}
function extractImageUrl(payload, path) {
var value = payload;
if (typeof payload === 'string') {
value = payload.trim();
} else {
path.split('.').forEach(function (segment) {
if (value && typeof value === 'object' && Object.prototype.hasOwnProperty.call(value, segment)) {
value = value[segment];
} else {
value = null;
}
});
}
if (typeof value !== 'string' || !isSafeHttpsUrl(value)) {
throw new Error('未从返回结果中取得 HTTPS 图片直链,请检查返回字段');
}
return value;
}
function insertMarkdownImage(editor, url, filename) {
var alt = String(filename || '图片').replace(/[\[\]\n\r]/g, '').slice(0, 80) || '图片';
var markdown = '';
var prefix = getEditorText(editor) ? '\n' : '';
var text = prefix + markdown + '\n';
if (editor.matches('[contenteditable="true"]')) {
editor.focus();
var selection = window.getSelection();
if (selection && selection.rangeCount && editor.contains(selection.getRangeAt(0).commonAncestorContainer)) {
selection.getRangeAt(0).deleteContents();
selection.getRangeAt(0).insertNode(document.createTextNode(text));
selection.collapseToEnd();
} else {
editor.appendChild(document.createTextNode(text));
}
} else {
var start = Number.isFinite(editor.selectionStart) ? editor.selectionStart : editor.value.length;
var end = Number.isFinite(editor.selectionEnd) ? editor.selectionEnd : start;
editor.focus();
editor.setRangeText(text, start, end, 'end');
}
editor.dispatchEvent(new Event('input', { bubbles: true }));
editor.dispatchEvent(new Event('change', { bubbles: true }));
}
function getEditorText(editor) {
return editor.matches('[contenteditable="true"]') ? editor.textContent.trim() : String(editor.value || '').trim();
}
function setImageUploadButtonState(editor, text, disabled) {
var button = editor.nextElementSibling;
if (!button || !button.hasAttribute('data-lsb-image-upload-button')) {
return;
}
button.textContent = text;
button.disabled = disabled;
}
function getUploadErrorMessage(error) {
var message = error && error.message ? String(error.message) : '未知错误';
return message.slice(0, 120);
}
function getAutoCheckinDate() {
var now = new Date();
var month = String(now.getMonth() + 1).padStart(2, '0');
var day = String(now.getDate()).padStart(2, '0');
return now.getFullYear() + '-' + month + '-' + day;
}
function applyAutoCheckin() {
window.clearTimeout(autoCheckinStartTimer);
autoCheckinStartTimer = 0;
if (!settings.autoCheckin || autoCheckinInFlight || settings.autoCheckinLastDate === getAutoCheckinDate()) {
return;
}
var scheduleAttempt = function () {
autoCheckinStartTimer = window.setTimeout(function () {
autoCheckinStartTimer = 0;
performAutoCheckin();
}, 900);
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', scheduleAttempt, { once: true });
} else {
scheduleAttempt();
}
}
function hasCompletedDailyCheckin(html) {
// 与参考脚本一致:站点不同页面可能只显示“已签到”。
return /今天已签到|已签到|签到成功|已完成签到/.test(String(html || ''));
}
function isDailyCheckinLoginPage(html) {
var source = String(html || '');
return /用户名或邮箱|忘记密码|<title[^>]*>[^<]*登录|name=["'](?:username|password)["']/i.test(source);
}
function hasDailyCheckinFailure(html) {
// 仅识别明确的签到失败结果,普通页面中的 CSRF 隐藏字段不视为异常。
return /签到失败|请求失败|(?:csrf|token)[^\n<]{0,32}(?:失效|错误|无效)|error\s*(?:message|:)/i.test(String(html || ''));
}
function extractDailyCheckinCsrf(html) {
var source = String(html || '');
try {
var documentNode = new DOMParser().parseFromString(source, 'text/html');
var input = documentNode.querySelector('input[name="_csrf"], input[name="csrf_token"], input[name="csrf"]');
if (input && input.value) {
return { name: input.name, value: input.value };
}
} catch (error) {
// DOM 解析不可用时,继续使用正则回退。
}
var match = source.match(/<input\b[^>]*\bname=["'](_csrf|csrf_token|csrf)["'][^>]*\bvalue=["']([^"']+)["'][^>]*>/i) ||
source.match(/<input\b[^>]*\bvalue=["']([^"']+)["'][^>]*\bname=["'](_csrf|csrf_token|csrf)["'][^>]*>/i);
if (!match) {
return null;
}
return match[1] === '_csrf' || match[1] === 'csrf_token' || match[1] === 'csrf' ?
{ name: match[1], value: match[2] } : { name: match[2], value: match[1] };
}
function fetchDailyCheckin(options) {
return fetch('/daily_checkin', Object.assign({
// 与参考脚本一致,显式携带当前登录会话的 Cookie。
credentials: 'include',
headers: { 'X-Requested-With': 'XMLHttpRequest' }
}, options || {}));
}
function performAutoCheckin() {
var today = getAutoCheckinDate();
if (!settings.autoCheckin || autoCheckinInFlight || settings.autoCheckinLastDate === today) {
return;
}
autoCheckinInFlight = true;
fetchDailyCheckin({ method: 'GET' }).then(function (response) {
if (!response.ok || /\/login(?:[?#]|$)/.test(response.url || '')) {
throw new Error('未检测到登录状态');
}
return response.text();
}).then(function (html) {
if (isDailyCheckinLoginPage(html)) {
throw new Error('未检测到登录状态');
}
if (hasCompletedDailyCheckin(html)) {
return { completed: true };
}
if (!settings.autoCheckin) {
return { cancelled: true };
}
var csrf = extractDailyCheckinCsrf(html);
if (!csrf || !csrf.name || !csrf.value) {
throw new Error('未找到签到凭据');
}
var formData = new FormData();
formData.append(csrf.name, csrf.value);
return fetchDailyCheckin({ method: 'POST', body: formData }).then(function (response) {
if (!response.ok || /\/login(?:[?#]|$)/.test(response.url || '')) {
throw new Error('签到请求未获授权');
}
return response.text();
}).then(function (resultHtml) {
if (isDailyCheckinLoginPage(resultHtml)) {
throw new Error('签到请求未获授权');
}
if (hasCompletedDailyCheckin(resultHtml)) {
return { completed: true };
}
// 与参考脚本相同:POST 后再次读取状态;不因通用页面文案提前中止。
return fetchDailyCheckin({ method: 'GET' }).then(function (verifyResponse) {
if (!verifyResponse.ok || /\/login(?:[?#]|$)/.test(verifyResponse.url || '')) {
throw new Error('无法验证签到结果');
}
return verifyResponse.text();
}).then(function (verifyHtml) {
if (isDailyCheckinLoginPage(verifyHtml)) {
throw new Error('无法验证登录状态');
}
if (hasCompletedDailyCheckin(verifyHtml)) {
return { completed: true };
}
if (hasDailyCheckinFailure(verifyHtml)) {
throw new Error('签到状态验证失败');
}
// 参考脚本同样将成功的 POST 作为最终回退判断;兼容不显示固定成功文字的站内版本。
return { completed: true };
});
});
}).then(function (result) {
if (!result || !result.completed) {
return;
}
settings.autoCheckinLastDate = today;
persistSettings();
showStatus('今日自动签到已完成');
}).catch(function (error) {
if (settings.autoCheckin) {
console.warn('[LINUX.SB 自动签到]', error && error.message ? error.message : error);
showStatus('自动签到未完成,请检查登录状态');
}
}).finally(function () {
autoCheckinInFlight = false;
});
}
function applyImageLightbox() {
ensureImageLightboxEvents();
updateImageLightboxTargets();
if (!settings.imageLightbox) {
closeImageLightbox();
}
}
function updateImageLightboxTargets() {
if (!document.querySelectorAll) {
return;
}
document.querySelectorAll('.post-entry .post-content img, .post-item .post-content img').forEach(function (image) {
if (settings.imageLightbox && image.getAttribute('src')) {
image.setAttribute('data-lsb-lightbox-image', '1');
} else {
image.removeAttribute('data-lsb-lightbox-image');
}
});
}
function ensureImageLightboxEvents() {
if (imageLightboxEventsBound) {
return;
}
imageLightboxEventsBound = true;
document.addEventListener('click', function (event) {
if (!settings.imageLightbox || event.defaultPrevented) {
return;
}
var target = event.target;
if (!target || !target.matches || !target.matches('.post-entry .post-content img, .post-item .post-content img')) {
return;
}
var source = target.currentSrc || target.src;
if (!source || target.closest('#lsb-image-lightbox')) {
return;
}
event.preventDefault();
event.stopPropagation();
openImageLightbox(source, target.alt || '帖子图片');
}, true);
document.addEventListener('keydown', function (event) {
if (event.key === 'Escape' && imageLightbox && !imageLightbox.hidden) {
closeImageLightbox();
}
});
}
function ensureImageLightbox() {
if (imageLightbox && document.body && document.body.contains(imageLightbox)) {
return imageLightbox;
}
if (!document.body) {
return null;
}
imageLightbox = document.createElement('div');
imageLightbox.id = 'lsb-image-lightbox';
imageLightbox.hidden = true;
imageLightbox.setAttribute('role', 'dialog');
imageLightbox.setAttribute('aria-modal', 'true');
imageLightbox.setAttribute('aria-label', '图片预览');
var closeButton = document.createElement('button');
closeButton.type = 'button';
closeButton.className = 'lsb-lightbox-close';
closeButton.setAttribute('aria-label', '关闭图片预览');
closeButton.textContent = '×';
imageLightboxImage = document.createElement('img');
imageLightboxImage.className = 'lsb-lightbox-image';
imageLightboxImage.alt = '';
closeButton.addEventListener('click', closeImageLightbox);
imageLightbox.addEventListener('click', function (event) {
if (event.target === imageLightbox) {
closeImageLightbox();
}
});
imageLightbox.appendChild(closeButton);
imageLightbox.appendChild(imageLightboxImage);
document.body.appendChild(imageLightbox);
return imageLightbox;
}
function openImageLightbox(source, altText) {
var overlay = ensureImageLightbox();
if (!overlay || !imageLightboxImage) {
return;
}
imageLightboxImage.src = source;
imageLightboxImage.alt = altText || '帖子图片';
overlay.hidden = false;
document.body.classList.add('lsb-lightbox-open');
window.requestAnimationFrame(function () {
var closeButton = overlay.querySelector('.lsb-lightbox-close');
if (closeButton) {
closeButton.focus();
}
});
}
function closeImageLightbox() {
if (!imageLightbox || imageLightbox.hidden) {
return;
}
imageLightbox.hidden = true;
document.body.classList.remove('lsb-lightbox-open');
if (imageLightboxImage) {
imageLightboxImage.removeAttribute('src');
imageLightboxImage.alt = '';
}
}
function applyTheme() {
clearThemeVariables();
var theme = THEMES[settings.theme] || THEMES.neutral;
if (theme.vars) {
Object.keys(theme.vars).forEach(function (name) {
setRootVariable(name, theme.vars[name]);
});
}
var accentRgb = hexToRgb(settings.accent);
setRootVariable('--brand', settings.accent);
setRootVariable('--brand-hover', mixHex(settings.accent, '#ffffff', 0.18));
setRootVariable('--brand-soft', 'rgba(' + accentRgb.r + ',' + accentRgb.g + ',' + accentRgb.b + ',.16)');
setRootVariable('--focus-ring', 'rgba(' + accentRgb.r + ',' + accentRgb.g + ',' + accentRgb.b + ',.34)');
var background = theme.vars && theme.vars['--bg'] ? theme.vars['--bg'] : '#1a1b2e';
setRootVariable('--text', settings.textColor);
setRootVariable('--text-muted', mixHex(settings.textColor, background, 0.34));
setRootVariable('--text-subtle', mixHex(settings.textColor, background, 0.55));
setRootVariable('--text-disabled', mixHex(settings.textColor, background, 0.72));
if (settings.theme !== 'original') {
setRootVariable('--swal2-background', 'var(--panel)');
setRootVariable('--swal2-color', 'var(--text)');
setRootVariable('--swal2-validation-message-background', 'var(--line-soft)');
setRootVariable('--swal2-validation-message-color', 'var(--text-muted)');
}
}
function hexToRgb(hex) {
var normalized = String(hex).replace('#', '');
return {
r: parseInt(normalized.slice(0, 2), 16),
g: parseInt(normalized.slice(2, 4), 16),
b: parseInt(normalized.slice(4, 6), 16)
};
}
function mixHex(first, second, weight) {
var a = hexToRgb(first);
var b = hexToRgb(second);
var mix = function (left, right) {
return Math.round(left + (right - left) * weight);
};
var toHex = function (number) {
return number.toString(16).padStart(2, '0');
};
return '#' + toHex(mix(a.r, b.r)) + toHex(mix(a.g, b.g)) + toHex(mix(a.b, b.b));
}
function findTextPalette(color) {
var normalized = String(color).toLowerCase();
var key = Object.keys(TEXT_PALETTES).find(function (paletteKey) {
return paletteKey !== 'custom' && TEXT_PALETTES[paletteKey].color === normalized;
});
return key || 'custom';
}
function ensureInterface() {
if (panel && document.body.contains(panel)) {
return;
}
if (!document.body) {
return;
}
toggleButton = document.createElement('button');
toggleButton.id = 'lsb-layout-toggle';
toggleButton.type = 'button';
toggleButton.setAttribute('aria-label', '打开布局与主题设置');
toggleButton.setAttribute('aria-controls', 'lsb-layout-panel');
toggleButton.setAttribute('aria-expanded', 'false');
toggleButton.title = '布局与主题设置';
toggleButton.innerHTML = [
'<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">',
'<path d="M4 7h10M18 7h2M4 17h2M10 17h10M14 4v6M7 14v6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>',
'</svg>'
].join('');
panel = document.createElement('section');
panel.id = 'lsb-layout-panel';
panel.hidden = true;
panel.setAttribute('role', 'dialog');
panel.setAttribute('aria-modal', 'false');
panel.setAttribute('aria-label', '布局与主题设置');
panel.innerHTML = [
'<div class="lsb-panel-head">',
' <button class="lsb-icon-button" type="button" data-lsb-close aria-label="关闭设置面板">×</button>',
' <span class="lsb-panel-credit">Design 1 @nmxyh | Design 2 <a href="https://com.com.ee" target="_blank" rel="noopener noreferrer">COMCOM</a></span>',
'</div>',
'<div class="lsb-panel-body">',
' <section class="lsb-section" aria-labelledby="lsb-layout-title">',
' <h2 class="lsb-section-title" id="lsb-layout-title">布局参数</h2>',
' <div data-lsb-ranges></div>',
' </section>',
' <section class="lsb-section" aria-labelledby="lsb-home-title">',
' <h2 class="lsb-section-title" id="lsb-home-title">首页设置</h2>',
' <label class="lsb-check-line"><input type="checkbox" data-lsb-home-personalized><span>启用首页个性化头图与搜索</span></label>',
' <label class="lsb-check-line"><input type="checkbox" data-lsb-home-sidebar-swap><span>侧栏位置对调</span></label>',
' <label class="lsb-check-line"><input type="checkbox" data-lsb-auto-checkin><span>自动签到功能</span></label>',
' <label class="lsb-check-line"><input type="checkbox" data-lsb-image-lightbox><span>图片灯箱功能</span></label>',
' <div class="lsb-check-line-row">',
' <label class="lsb-check-line"><input type="checkbox" data-lsb-image-upload><span>拖拽图片上传</span></label>',
' <button class="lsb-upload-settings-toggle" type="button" data-lsb-upload-settings-toggle aria-expanded="false">展开图床配置</button>',
' </div>',
' <div class="lsb-upload-settings" data-lsb-upload-settings>',
' <label class="lsb-field"><span>图床类型</span><select class="lsb-select" data-lsb-upload-provider><option value="imgur">Imgur(默认)</option><option value="nodeimage">Nodeimage</option><option value="custom">自定义接口</option></select></label>',
' <label class="lsb-field"><span>图床主页</span><input class="lsb-input" type="url" inputmode="url" data-lsb-upload-host placeholder="https://imgur.com/"></label>',
' <label class="lsb-field"><span>上传接口(HTTPS)</span><input class="lsb-input" type="url" inputmode="url" data-lsb-upload-endpoint placeholder="https://api.imgur.com/3/image"></label>',
' <label class="lsb-field"><span>文件字段名</span><input class="lsb-input" type="text" data-lsb-upload-file-field placeholder="image"></label>',
' <label class="lsb-field"><span>返回图片地址字段</span><input class="lsb-input" type="text" data-lsb-upload-response-path placeholder="data.link"></label>',
' <label class="lsb-field"><span>认证方式</span><select class="lsb-select" data-lsb-upload-auth-mode><option value="imgur-client-id">Imgur Client ID</option><option value="nodeimage-api-key">Nodeimage API Key</option><option value="none">不使用认证</option><option value="bearer">Bearer Token</option></select></label>',
' <label class="lsb-field"><span>API 密钥 / Token</span><input class="lsb-input" type="password" autocomplete="off" data-lsb-upload-token placeholder="Imgur 需填写 Client ID"></label>',
' <p class="lsb-upload-hint">启用后可将图片拖入发帖或回复编辑器,或点击编辑器下方的“上传图片”。默认使用 Imgur;其匿名上传需要填写 Client ID。自定义接口应返回可访问的 HTTPS 图片直链。</p>',
' </div>',
' <div class="lsb-filter-section">',
' <label class="lsb-field"><span>过滤标题关键字(每行一个,最多 10 个)</span>',
' <textarea class="lsb-textarea" data-lsb-title-filters rows="3" placeholder="输入要屏蔽的关键字,每行一个"></textarea>',
' </label>',
' <label class="lsb-field"><span>过滤用户名(每行一个,最多 10 个)</span>',
' <textarea class="lsb-textarea" data-lsb-user-filters rows="3" placeholder="输入要屏蔽的用户名,每行一个"></textarea>',
' </label>',
' </div>',
' </section>',
' <section class="lsb-section" aria-labelledby="lsb-theme-title">',
' <h2 class="lsb-section-title" id="lsb-theme-title">主题颜色</h2>',
' <label class="lsb-field"><span>配色方案</span><select class="lsb-select" data-lsb-theme></select></label>',
' <label class="lsb-field"><span>强调色</span>',
' <span class="lsb-color-line">',
' <input class="lsb-color-input" type="color" data-lsb-accent aria-label="选择强调色">',
' <output class="lsb-color-value" data-lsb-accent-value></output>',
' </span>',
' </label>',
' <label class="lsb-field"><span>文字色板</span><select class="lsb-select" data-lsb-text-palette></select></label>',
' <label class="lsb-field"><span>文字颜色</span>',
' <span class="lsb-color-line">',
' <input class="lsb-color-input" type="color" data-lsb-text-color aria-label="选择文字颜色">',
' <output class="lsb-color-value" data-lsb-text-color-value></output>',
' </span>',
' </label>',
' <p class="lsb-theme-note">中性深灰使用纯灰阶构建背景层级;强调色负责交互状态,文字色负责内容层级。</p>',
' </section>',
' <div class="lsb-actions">',
' <button class="lsb-button" type="button" data-lsb-reset>恢复默认</button>',
' <button class="lsb-button lsb-button-primary" type="button" data-lsb-done>完成</button>',
' </div>',
' <p class="lsb-status" data-lsb-status aria-live="polite"></p>',
'</div>'
].join('');
document.body.appendChild(toggleButton);
document.body.appendChild(panel);
// 初始设置侧栏位置类
toggleButton.classList.add(settings.sidebarSwap ? 'lsb-toggle-left' : 'lsb-toggle-right');
panel.classList.add(settings.sidebarSwap ? 'lsb-panel-left' : 'lsb-panel-right');
statusElement = panel.querySelector('[data-lsb-status]');
buildRangeControls();
buildThemeControls();
bindInterfaceEvents();
bindDragEvents();
syncInterface();
applySettings();
restoreTogglePosition();
}
function isHomePage() {
var path = window.location.pathname.replace(/\/+$/, '') || '/';
if (path === '/') {
return true;
}
if (path !== '/index.php') {
return false;
}
var query = new URLSearchParams(window.location.search);
return !query.has('a') && !query.has('do') && !query.has('id');
}
function applyHomePersonalization() {
var existing = document.getElementById('lsb-home-personalization');
if (!settings.homePersonalized || !isHomePage()) {
if (existing) {
existing.remove();
}
document.querySelectorAll('.lsb-home-personalized-layout').forEach(function (layout) {
layout.classList.remove('lsb-home-personalized-layout');
});
return;
}
var forumLayout = document.querySelector('main.wrap .home-shell .forum-layout, main.wrap .forum-layout');
var forumMain = forumLayout ? forumLayout.querySelector('.forum-main') : null;
if (!forumLayout || !forumMain) {
return;
}
if (existing) {
if (existing.parentElement !== forumLayout) {
forumLayout.insertBefore(existing, forumLayout.firstChild);
}
forumLayout.classList.add('lsb-home-personalized-layout');
return;
}
var hero = document.createElement('section');
hero.id = 'lsb-home-personalization';
hero.setAttribute('aria-label', 'LINUX SB 首页欢迎区');
var logo = document.createElement('div');
logo.className = 'lsb-home-logo';
logo.innerHTML = HOME_LOGO_SVG;
var tagline = document.createElement('h1');
tagline.className = 'lsb-home-tagline';
tagline.textContent = 'Here IS The New Ideal Community';
var sourceSearch = document.querySelector('.top .search-form, .search-form');
if (!sourceSearch) {
return;
}
var search = sourceSearch.cloneNode(true);
stripSearchEnhancement(search);
search.className = 'search-form lsb-home-search-form';
search.removeAttribute('id');
search.setAttribute('aria-label', '首页搜索');
var searchInput = search.querySelector('.search-input');
if (searchInput) {
searchInput.setAttribute('aria-label', '搜索关键词');
}
hero.appendChild(logo);
hero.appendChild(tagline);
hero.appendChild(search);
forumLayout.insertBefore(hero, forumLayout.firstChild);
forumLayout.classList.add('lsb-home-personalized-layout');
}
function applySidebarSwap() {
if (!isHomePage()) {
return;
}
var forumLayout = document.querySelector('main.wrap .home-shell .forum-layout, main.wrap .forum-layout');
if (forumLayout) {
if (settings.sidebarSwap) {
forumLayout.classList.add('lsb-home-sidebar-swapped');
} else {
forumLayout.classList.remove('lsb-home-sidebar-swapped');
}
}
// 切换按钮与面板位置对调联动
if (toggleButton) {
if (settings.sidebarSwap) {
toggleButton.classList.add('lsb-toggle-left');
toggleButton.classList.remove('lsb-toggle-right');
} else {
toggleButton.classList.add('lsb-toggle-right');
toggleButton.classList.remove('lsb-toggle-left');
}
}
if (panel) {
if (settings.sidebarSwap) {
panel.classList.add('lsb-panel-left');
panel.classList.remove('lsb-panel-right');
} else {
panel.classList.add('lsb-panel-right');
panel.classList.remove('lsb-panel-left');
}
}
}
var filterDebounceTimer = 0;
function scheduleFilter() {
window.clearTimeout(filterDebounceTimer);
filterDebounceTimer = window.setTimeout(applyFilters, 200);
}
function applyFilters() {
var titleKeywords = (settings.titleFilters || []).filter(function(k) { return k.trim(); });
var usernames = (settings.userFilters || []).filter(function(u) { return u.trim(); });
var hasFilters = titleKeywords.length > 0 || usernames.length > 0;
if (!hasFilters) {
document.querySelectorAll('.post-item[data-lsb-filtered]').forEach(function(item) {
item.style.display = '';
item.removeAttribute('data-lsb-filtered');
});
return;
}
document.querySelectorAll('.post-item').forEach(function(item) {
var shouldHide = false;
// 标题关键字过滤
if (!shouldHide && titleKeywords.length > 0) {
var titleEl = item.querySelector('.post-title');
if (titleEl) {
var title = titleEl.textContent.toLowerCase();
shouldHide = titleKeywords.some(function(kw) {
return title.includes(kw.toLowerCase());
});
}
}
// 用户名过滤 — 多选择器回退,兼容首页列表与帖子详情页
if (!shouldHide && usernames.length > 0) {
var matchedAuthor = '';
// 选择器1:帖子详情页评论作者
var authorEl = item.querySelector('.post-author');
// 选择器2:用户资料链接(首页列表通用)
if (!authorEl) {
authorEl = item.querySelector('.post-meta a[href*="/user/"]');
}
// 选择器3:回退到 .post-meta 内第一个链接
if (!authorEl) {
var meta = item.querySelector('.post-meta');
if (meta) {
authorEl = meta.querySelector('a');
}
}
if (authorEl) {
matchedAuthor = authorEl.textContent.trim();
}
if (matchedAuthor) {
var lower = matchedAuthor.toLowerCase();
shouldHide = usernames.some(function(u) {
return lower === u.toLowerCase();
});
}
}
if (shouldHide) {
item.style.display = 'none';
item.setAttribute('data-lsb-filtered', '1');
} else {
item.style.display = '';
item.removeAttribute('data-lsb-filtered');
}
});
}
function enforceRadiusOverrides() {
var tabRadius = Math.min(settings.radius, 12) + 'px';
var searchRadius = Math.min(settings.radius, 12) + 'px';
document.querySelectorAll('.topic-toolbar > .tab-bar').forEach(function (tabBar) {
tabBar.style.setProperty('display', 'inline-flex', 'important');
tabBar.style.setProperty('flex', '0 0 auto', 'important');
tabBar.style.setProperty('gap', '5px', 'important');
tabBar.style.setProperty('width', 'max-content', 'important');
tabBar.style.setProperty('border-radius', '0px', 'important');
tabBar.style.setProperty('overflow', 'visible', 'important');
tabBar.style.setProperty('isolation', 'auto', 'important');
tabBar.style.setProperty('transform', 'none', 'important');
tabBar.querySelectorAll(':scope > a.tab').forEach(function (tab) {
tab.style.setProperty('display', 'inline-flex', 'important');
tab.style.setProperty('align-items', 'center', 'important');
tab.style.setProperty('justify-content', 'center', 'important');
tab.style.setProperty('min-height', '27px', 'important');
tab.style.setProperty('line-height', '1.1', 'important');
tab.style.setProperty('box-sizing', 'border-box', 'important');
tab.style.setProperty('border-radius', tabRadius, 'important');
tab.style.setProperty('border-top-left-radius', tabRadius, 'important');
tab.style.setProperty('border-top-right-radius', tabRadius, 'important');
tab.style.setProperty('border-bottom-right-radius', tabRadius, 'important');
tab.style.setProperty('border-bottom-left-radius', tabRadius, 'important');
tab.style.setProperty('margin-left', '0px', 'important');
tab.style.setProperty('overflow', 'hidden', 'important');
tab.style.setProperty('background-clip', 'padding-box', 'important');
});
});
document.querySelectorAll('.search-form').forEach(function (form) {
form.style.setProperty('border-radius', searchRadius, 'important');
});
document.querySelectorAll('.search-field').forEach(function (field) {
field.style.setProperty('border-radius', searchRadius, 'important');
});
document.querySelectorAll('.lsb-search-select-trigger').forEach(function (trigger) {
trigger.style.setProperty('border-top-left-radius', searchRadius, 'important');
trigger.style.setProperty('border-bottom-left-radius', searchRadius, 'important');
});
document.querySelectorAll('.search-btn').forEach(function (button) {
button.style.setProperty('border-top-right-radius', searchRadius, 'important');
button.style.setProperty('border-bottom-right-radius', searchRadius, 'important');
});
}
function stripSearchEnhancement(form) {
form.querySelectorAll('.lsb-search-select').forEach(function (wrapper) {
var select = wrapper.querySelector('.search-field');
if (select) {
select.classList.remove('lsb-search-native');
select.removeAttribute('data-lsb-search-enhanced');
wrapper.replaceWith(select);
}
});
}
function enhanceSearchFields(root) {
if (!root || !root.querySelectorAll) {
return;
}
root.querySelectorAll('.search-field').forEach(function (select) {
if (select.getAttribute('data-lsb-search-enhanced') === '1') {
return;
}
select.setAttribute('data-lsb-search-enhanced', '1');
var wrapper = document.createElement('div');
wrapper.className = 'lsb-search-select';
var trigger = document.createElement('button');
trigger.type = 'button';
trigger.className = 'lsb-search-select-trigger';
trigger.setAttribute('aria-haspopup', 'listbox');
trigger.setAttribute('aria-expanded', 'false');
trigger.setAttribute('aria-label', '选择搜索范围');
var menu = document.createElement('div');
menu.className = 'lsb-search-options';
menu.setAttribute('role', 'listbox');
menu.hidden = true;
menu.id = 'lsb-search-options-' + (++searchDropdownId);
trigger.setAttribute('aria-controls', menu.id);
Array.from(select.options).forEach(function (option) {
var item = document.createElement('button');
item.type = 'button';
item.className = 'lsb-search-option';
item.setAttribute('role', 'option');
item.dataset.value = option.value;
item.textContent = option.textContent;
item.addEventListener('click', function () {
select.value = option.value;
select.dispatchEvent(new Event('change', { bubbles: true }));
closeSearchDropdowns();
trigger.focus();
});
menu.appendChild(item);
});
var sync = function () {
var selected = select.options[select.selectedIndex] || select.options[0];
trigger.textContent = selected ? selected.textContent : '';
menu.querySelectorAll('.lsb-search-option').forEach(function (item) {
var active = selected && item.dataset.value === selected.value;
item.setAttribute('aria-selected', String(Boolean(active)));
});
};
trigger.addEventListener('click', function () {
var nextOpen = menu.hidden;
closeSearchDropdowns();
menu.hidden = !nextOpen;
trigger.setAttribute('aria-expanded', String(nextOpen));
});
trigger.addEventListener('keydown', function (event) {
if (event.key === 'ArrowDown' || event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
trigger.click();
}
if (event.key === 'Escape') {
closeSearchDropdowns();
}
});
select.addEventListener('change', sync);
wrapper.appendChild(trigger);
wrapper.appendChild(menu);
select.classList.add('lsb-search-native');
select.parentNode.insertBefore(wrapper, select);
wrapper.appendChild(select);
sync();
});
if (!searchDropdownEventsBound) {
searchDropdownEventsBound = true;
document.addEventListener('pointerdown', function (event) {
if (!event.target.closest || !event.target.closest('.lsb-search-select')) {
closeSearchDropdowns();
}
});
document.addEventListener('keydown', function (event) {
if (event.key === 'Escape') {
closeSearchDropdowns();
}
});
}
}
function closeSearchDropdowns() {
document.querySelectorAll('.lsb-search-options').forEach(function (menu) {
menu.hidden = true;
var trigger = menu.parentElement && menu.parentElement.querySelector('.lsb-search-select-trigger');
if (trigger) {
trigger.setAttribute('aria-expanded', 'false');
}
});
}
function buildRangeControls() {
var container = panel.querySelector('[data-lsb-ranges]');
container.className = 'lsb-section';
RANGE_DEFINITIONS.forEach(function (definition) {
var label = document.createElement('label');
label.className = 'lsb-range-row';
var head = document.createElement('span');
head.className = 'lsb-range-head';
var name = document.createElement('span');
name.className = 'lsb-range-label';
name.textContent = definition.label;
var output = document.createElement('output');
output.className = 'lsb-range-value';
output.dataset.valueFor = definition.key;
var input = document.createElement('input');
input.className = 'lsb-range';
input.type = 'range';
input.min = String(definition.min);
input.max = String(definition.max);
input.step = String(definition.step);
input.dataset.settingKey = definition.key;
input.setAttribute('aria-label', definition.label);
head.appendChild(name);
head.appendChild(output);
label.appendChild(head);
label.appendChild(input);
container.appendChild(label);
input.addEventListener('input', function () {
settings[definition.key] = Number(input.value);
output.value = input.value + definition.unit;
output.textContent = output.value;
applySettings();
scheduleSave();
});
});
}
function buildThemeControls() {
var select = panel.querySelector('[data-lsb-theme]');
Object.keys(THEMES).forEach(function (key) {
var option = document.createElement('option');
option.value = key;
option.textContent = THEMES[key].label;
select.appendChild(option);
});
select.addEventListener('change', function () {
settings.theme = select.value;
settings.accent = THEMES[settings.theme].accent;
settings.textColor = THEMES[settings.theme].textColor || DEFAULTS.textColor;
settings.textPalette = findTextPalette(settings.textColor);
applySettings();
syncInterface();
persistSettings();
});
var accentInput = panel.querySelector('[data-lsb-accent]');
accentInput.addEventListener('input', function () {
settings.accent = accentInput.value.toLowerCase();
applySettings();
syncAccentControl();
scheduleSave();
});
var textPalette = panel.querySelector('[data-lsb-text-palette]');
Object.keys(TEXT_PALETTES).forEach(function (key) {
var option = document.createElement('option');
option.value = key;
option.textContent = TEXT_PALETTES[key].label;
textPalette.appendChild(option);
});
textPalette.addEventListener('change', function () {
settings.textPalette = textPalette.value;
if (settings.textPalette !== 'custom') {
settings.textColor = TEXT_PALETTES[settings.textPalette].color;
}
applySettings();
syncTextControl();
persistSettings();
});
var textInput = panel.querySelector('[data-lsb-text-color]');
textInput.addEventListener('input', function () {
settings.textPalette = 'custom';
settings.textColor = textInput.value.toLowerCase();
applySettings();
syncTextControl();
scheduleSave();
});
}
function bindInterfaceEvents() {
panel.querySelector('[data-lsb-home-personalized]').addEventListener('change', function (event) {
settings.homePersonalized = event.target.checked;
applySettings();
syncInterface();
persistSettings();
});
panel.querySelector('[data-lsb-home-sidebar-swap]').addEventListener('change', function (event) {
settings.sidebarSwap = event.target.checked;
applySidebarSwap();
syncInterface();
persistSettings();
});
panel.querySelector('[data-lsb-auto-checkin]').addEventListener('change', function (event) {
settings.autoCheckin = event.target.checked;
applyAutoCheckin();
syncInterface();
persistSettings();
});
panel.querySelector('[data-lsb-image-lightbox]').addEventListener('change', function (event) {
settings.imageLightbox = event.target.checked;
applyImageLightbox();
syncInterface();
persistSettings();
});
panel.querySelector('[data-lsb-image-upload]').addEventListener('change', function (event) {
settings.imageUpload = event.target.checked;
applyImageUpload();
syncInterface();
persistSettings();
});
panel.querySelector('[data-lsb-upload-settings-toggle]').addEventListener('click', function () {
settings.imageUploadSettingsCollapsed = !settings.imageUploadSettingsCollapsed;
syncImageUploadControls();
persistSettings();
});
panel.querySelector('[data-lsb-upload-provider]').addEventListener('change', function (event) {
settings.imageUploadProvider = event.target.value;
if (settings.imageUploadProvider === 'imgur') {
settings.imageUploadHost = DEFAULTS.imageUploadHost;
settings.imageUploadEndpoint = DEFAULTS.imageUploadEndpoint;
settings.imageUploadFileField = DEFAULTS.imageUploadFileField;
settings.imageUploadResponsePath = DEFAULTS.imageUploadResponsePath;
settings.imageUploadAuthMode = DEFAULTS.imageUploadAuthMode;
} else if (settings.imageUploadProvider === 'nodeimage') {
settings.imageUploadHost = NODEIMAGE_PRESET.host;
settings.imageUploadEndpoint = NODEIMAGE_PRESET.endpoint;
settings.imageUploadFileField = NODEIMAGE_PRESET.field;
settings.imageUploadResponsePath = NODEIMAGE_PRESET.responsePath;
settings.imageUploadAuthMode = NODEIMAGE_PRESET.authMode;
settings.imageUploadToken = NODEIMAGE_PRESET.token;
}
syncInterface();
persistSettings();
});
[
['[data-lsb-upload-host]', 'imageUploadHost'],
['[data-lsb-upload-endpoint]', 'imageUploadEndpoint'],
['[data-lsb-upload-file-field]', 'imageUploadFileField'],
['[data-lsb-upload-response-path]', 'imageUploadResponsePath'],
['[data-lsb-upload-auth-mode]', 'imageUploadAuthMode'],
['[data-lsb-upload-token]', 'imageUploadToken']
].forEach(function (item) {
var control = panel.querySelector(item[0]);
control.addEventListener('change', function () {
settings[item[1]] = control.value.trim();
syncImageUploadControls();
persistSettings();
});
});
var parseFilterTextarea = function (value, maxItems) {
var items = String(value).split(/[\n,]+/).map(function(s) { return s.trim(); }).filter(Boolean);
if (maxItems > 0) {
items = items.slice(0, maxItems);
}
return items;
};
var titleFiltersTextarea = panel.querySelector('[data-lsb-title-filters]');
titleFiltersTextarea.addEventListener('blur', function () {
var value = titleFiltersTextarea.value;
settings.titleFilters = parseFilterTextarea(value, 10);
titleFiltersTextarea.value = settings.titleFilters.join('\n');
applyFilters();
persistSettings();
});
var userFiltersTextarea = panel.querySelector('[data-lsb-user-filters]');
userFiltersTextarea.addEventListener('blur', function () {
var value = userFiltersTextarea.value;
settings.userFilters = parseFilterTextarea(value, 10);
userFiltersTextarea.value = settings.userFilters.join('\n');
applyFilters();
persistSettings();
});
toggleButton.addEventListener('click', function () {
if (suppressToggleClick) {
suppressToggleClick = false;
return;
}
setPanelOpen(panel.hidden);
});
panel.querySelector('[data-lsb-close]').addEventListener('click', function () {
setPanelOpen(false);
});
panel.querySelector('[data-lsb-done]').addEventListener('click', function () {
persistSettings();
setPanelOpen(false);
});
panel.querySelector('[data-lsb-reset]').addEventListener('click', function () {
var previousLeft = settings.panelLeft;
var previousTop = settings.panelTop;
var previousToggleLeft = settings.toggleLeft;
var previousToggleTop = settings.toggleTop;
settings = normalizeSettings(DEFAULTS);
settings.panelLeft = previousLeft;
settings.panelTop = previousTop;
settings.toggleLeft = previousToggleLeft;
settings.toggleTop = previousToggleTop;
applySettings();
syncInterface();
persistSettings();
showStatus('已恢复默认中性深灰方案');
});
document.addEventListener('keydown', function (event) {
if (event.key === 'Escape' && panel && !panel.hidden) {
setPanelOpen(false);
}
});
document.addEventListener('pointerdown', function (event) {
if (!panel || panel.hidden) {
return;
}
if (!panel.contains(event.target) && !toggleButton.contains(event.target)) {
setPanelOpen(false);
}
});
}
function bindDragEvents() {
var head = panel.querySelector('.lsb-panel-head');
var dragState = null;
head.addEventListener('pointerdown', function (event) {
var target = event.target;
if (event.button !== 0 || (target && target.closest && target.closest('button, a'))) {
return;
}
var rect = panel.getBoundingClientRect();
dragState = {
pointerId: event.pointerId,
offsetX: event.clientX - rect.left,
offsetY: event.clientY - rect.top
};
panel.classList.add('lsb-dragging');
if (head.setPointerCapture) {
head.setPointerCapture(event.pointerId);
}
event.preventDefault();
});
head.addEventListener('pointermove', function (event) {
if (!dragState || event.pointerId !== dragState.pointerId) {
return;
}
setPanelPosition(
event.clientX - dragState.offsetX,
event.clientY - dragState.offsetY,
false
);
});
var finishDrag = function (event) {
if (!dragState || event.pointerId !== dragState.pointerId) {
return;
}
if (head.releasePointerCapture) {
try {
head.releasePointerCapture(event.pointerId);
} catch (error) {
// 某些浏览器在 pointercancel 后会抛出无害异常。
}
}
dragState = null;
panel.classList.remove('lsb-dragging');
persistSettings();
};
head.addEventListener('pointerup', finishDrag);
head.addEventListener('pointercancel', finishDrag);
var toggleDragState = null;
toggleButton.addEventListener('pointerdown', function (event) {
if (event.button !== 0) {
return;
}
var rect = toggleButton.getBoundingClientRect();
toggleDragState = {
pointerId: event.pointerId,
startX: event.clientX,
startY: event.clientY,
offsetX: event.clientX - rect.left,
offsetY: event.clientY - rect.top,
moved: false
};
toggleButton.classList.add('lsb-toggle-dragging');
if (toggleButton.setPointerCapture) {
toggleButton.setPointerCapture(event.pointerId);
}
event.preventDefault();
});
toggleButton.addEventListener('pointermove', function (event) {
if (!toggleDragState || event.pointerId !== toggleDragState.pointerId) {
return;
}
if (Math.abs(event.clientX - toggleDragState.startX) > 4 ||
Math.abs(event.clientY - toggleDragState.startY) > 4) {
toggleDragState.moved = true;
}
if (toggleDragState.moved) {
setTogglePosition(
event.clientX - toggleDragState.offsetX,
event.clientY - toggleDragState.offsetY,
false
);
}
});
var finishToggleDrag = function (event) {
if (!toggleDragState || event.pointerId !== toggleDragState.pointerId) {
return;
}
if (toggleButton.releasePointerCapture) {
try {
toggleButton.releasePointerCapture(event.pointerId);
} catch (error) {
// 某些浏览器在 pointercancel 后会抛出无害异常。
}
}
var moved = toggleDragState.moved;
suppressToggleClick = true;
toggleDragState = null;
toggleButton.classList.remove('lsb-toggle-dragging');
if (moved) {
persistSettings();
} else if (event.type === 'pointerup') {
setPanelOpen(panel.hidden);
}
window.setTimeout(function () {
suppressToggleClick = false;
}, 0);
};
toggleButton.addEventListener('pointerup', finishToggleDrag);
toggleButton.addEventListener('pointercancel', finishToggleDrag);
window.addEventListener('resize', function () {
if (panel && !panel.hidden && settings.panelLeft !== null && settings.panelTop !== null) {
setPanelPosition(settings.panelLeft, settings.panelTop, false);
scheduleSave();
}
if (settings.toggleLeft !== null && settings.toggleTop !== null) {
setTogglePosition(settings.toggleLeft, settings.toggleTop, false);
scheduleSave();
}
});
}
function setPanelPosition(left, top, persist) {
if (!panel) {
return;
}
var margin = 8;
var maxLeft = Math.max(margin, window.innerWidth - panel.offsetWidth - margin);
var maxTop = Math.max(margin, window.innerHeight - panel.offsetHeight - margin);
var nextLeft = Math.round(Math.min(maxLeft, Math.max(margin, left)));
var nextTop = Math.round(Math.min(maxTop, Math.max(margin, top)));
panel.style.left = nextLeft + 'px';
panel.style.top = nextTop + 'px';
panel.style.right = 'auto';
panel.style.bottom = 'auto';
settings.panelLeft = nextLeft;
settings.panelTop = nextTop;
if (persist) {
persistSettings();
}
}
function restorePanelPosition() {
if (!panel || settings.panelLeft === null || settings.panelTop === null) {
return;
}
setPanelPosition(settings.panelLeft, settings.panelTop, false);
}
function setTogglePosition(left, top, persist) {
if (!toggleButton) {
return;
}
var margin = 8;
var maxLeft = Math.max(margin, window.innerWidth - toggleButton.offsetWidth - margin);
var maxTop = Math.max(margin, window.innerHeight - toggleButton.offsetHeight - margin);
var nextLeft = Math.round(Math.min(maxLeft, Math.max(margin, left)));
var nextTop = Math.round(Math.min(maxTop, Math.max(margin, top)));
toggleButton.style.left = nextLeft + 'px';
toggleButton.style.top = nextTop + 'px';
toggleButton.style.right = 'auto';
toggleButton.style.bottom = 'auto';
settings.toggleLeft = nextLeft;
settings.toggleTop = nextTop;
if (persist) {
persistSettings();
}
}
function restoreTogglePosition() {
if (!toggleButton || settings.toggleLeft === null || settings.toggleTop === null) {
return;
}
setTogglePosition(settings.toggleLeft, settings.toggleTop, false);
}
function syncInterface() {
if (!panel) {
return;
}
RANGE_DEFINITIONS.forEach(function (definition) {
var input = panel.querySelector('[data-setting-key="' + definition.key + '"]');
var output = panel.querySelector('[data-value-for="' + definition.key + '"]');
input.value = String(settings[definition.key]);
output.value = settings[definition.key] + definition.unit;
output.textContent = output.value;
});
panel.querySelector('[data-lsb-theme]').value = settings.theme;
panel.querySelector('[data-lsb-home-personalized]').checked = settings.homePersonalized;
panel.querySelector('[data-lsb-home-sidebar-swap]').checked = settings.sidebarSwap;
panel.querySelector('[data-lsb-auto-checkin]').checked = settings.autoCheckin;
panel.querySelector('[data-lsb-image-lightbox]').checked = settings.imageLightbox;
panel.querySelector('[data-lsb-image-upload]').checked = settings.imageUpload;
syncImageUploadControls();
var titleFiltersTextarea = panel.querySelector('[data-lsb-title-filters]');
if (titleFiltersTextarea) {
titleFiltersTextarea.value = (settings.titleFilters || []).join('\n');
}
var userFiltersTextarea = panel.querySelector('[data-lsb-user-filters]');
if (userFiltersTextarea) {
userFiltersTextarea.value = (settings.userFilters || []).join('\n');
}
syncAccentControl();
syncTextControl();
}
function syncAccentControl() {
if (!panel) {
return;
}
panel.querySelector('[data-lsb-accent]').value = settings.accent;
panel.querySelector('[data-lsb-accent-value]').value = settings.accent.toUpperCase();
panel.querySelector('[data-lsb-accent-value]').textContent = settings.accent.toUpperCase();
panel.style.setProperty('--lsb-ui-accent', settings.accent);
toggleButton.style.setProperty('--lsb-ui-accent', settings.accent);
}
function syncTextControl() {
if (!panel) {
return;
}
panel.querySelector('[data-lsb-text-palette]').value = settings.textPalette;
panel.querySelector('[data-lsb-text-color]').value = settings.textColor;
panel.querySelector('[data-lsb-text-color-value]').value = settings.textColor.toUpperCase();
panel.querySelector('[data-lsb-text-color-value]').textContent = settings.textColor.toUpperCase();
panel.style.setProperty('--lsb-ui-text', settings.textColor);
toggleButton.style.setProperty('--lsb-ui-text', settings.textColor);
}
function setPanelOpen(open) {
if (!panel || !toggleButton) {
return;
}
panel.hidden = !open;
toggleButton.setAttribute('aria-expanded', String(open));
toggleButton.setAttribute('aria-label', open ? '关闭布局与主题设置' : '打开布局与主题设置');
if (open) {
restorePanelPosition();
syncInterface();
window.requestAnimationFrame(function () {
panel.querySelector('[data-lsb-close]').focus();
});
} else {
toggleButton.focus();
}
}
function showStatus(message) {
if (!statusElement) {
return;
}
statusElement.textContent = message;
window.setTimeout(function () {
if (statusElement && statusElement.textContent === message) {
statusElement.textContent = '';
}
}, 1600);
}
})();