第47批:2FA绑定状态双向同步(pull_state/push_state);图片壁纸下卡片/侧栏半透明毛玻璃自适应;壁纸源改四级fallback(必应主域/中国镜像/NASA APOD/picsum兜底)+转发白名单扩充

This commit is contained in:
ypsdf1
2026-10-22 23:24:32 +08:00
parent 3e2bd64a70
commit d0cdadd29d
4 changed files with 204 additions and 30 deletions
+10 -1
View File
@@ -52,6 +52,12 @@ if (is_file($ossTxt)) {
.si.active { color:var(--accent); border-right:3px solid var(--accent); background:rgba(88,166,255,0.05); } .si.active { color:var(--accent); border-right:3px solid var(--accent); background:rgba(88,166,255,0.05); }
.content { flex:1; padding:20px; overflow-y:auto; max-height:calc(100vh - 52px - 44px); } .content { flex:1; padding:20px; overflow-y:auto; max-height:calc(100vh - 52px - 44px); }
.card { background:var(--card); border:1px solid var(--border); border-radius:8px; padding:16px; margin-bottom:12px; } .card { background:var(--card); border:1px solid var(--border); border-radius:8px; padding:16px; margin-bottom:12px; }
/* 第47批:图片壁纸模式下卡片/侧栏自适应半透明+毛玻璃;纯色/默认模式不受影响 */
body.bg-img-mode .card, body.bg-img-mode .sidebar {
background: rgba(22,27,34,0.55);
backdrop-filter: blur(14px) saturate(1.4);
-webkit-backdrop-filter: blur(14px) saturate(1.4);
}
.card h2 { font-size:15px; color:var(--accent); margin-bottom:10px; } .card h2 { font-size:15px; color:var(--accent); margin-bottom:10px; }
.stats { display:grid; grid-template-columns:repeat(auto-fill,minmax(150px,1fr)); gap:10px; margin-bottom:16px; } .stats { display:grid; grid-template-columns:repeat(auto-fill,minmax(150px,1fr)); gap:10px; margin-bottom:16px; }
.stat { background:var(--card); border:1px solid var(--border); border-radius:8px; padding:14px; text-align:center; } .stat { background:var(--card); border:1px solid var(--border); border-radius:8px; padding:14px; text-align:center; }
@@ -2140,6 +2146,7 @@ function applyAdminBgImage(url, dim, silent) {
b.style.backgroundPosition = 'center center'; b.style.backgroundPosition = 'center center';
b.style.backgroundRepeat = 'no-repeat'; b.style.backgroundRepeat = 'no-repeat';
b.style.backgroundAttachment = 'fixed'; b.style.backgroundAttachment = 'fixed';
b.classList.add('bg-img-mode');
document.documentElement.style.setProperty('--bg', 'rgba(13,17,23,' + Math.min(0.9, dim + 0.25) + ')'); document.documentElement.style.setProperty('--bg', 'rgba(13,17,23,' + Math.min(0.9, dim + 0.25) + ')');
localStorage.setItem('sdf1_admin_bgimg', JSON.stringify({ type: 'img', url: url, dim: dim })); localStorage.setItem('sdf1_admin_bgimg', JSON.stringify({ type: 'img', url: url, dim: dim }));
if (!silent) toast('背景图片已应用', 'ok'); if (!silent) toast('背景图片已应用', 'ok');
@@ -2227,6 +2234,7 @@ function resetBg() {
b.style.backgroundPosition = ''; b.style.backgroundPosition = '';
b.style.backgroundRepeat = ''; b.style.backgroundRepeat = '';
b.style.backgroundAttachment = ''; b.style.backgroundAttachment = '';
b.classList.remove('bg-img-mode');
document.documentElement.style.removeProperty('--bg'); document.documentElement.style.removeProperty('--bg');
b.style.background = ''; b.style.background = '';
const ov = document.getElementById('themePickerOverlay'); const ov = document.getElementById('themePickerOverlay');
@@ -2283,7 +2291,7 @@ function showThemePicker() {
<div style="color:var(--dim);font-size:11px;margin-top:6px">支持任意直链 jpg/png/webp;对方站点防盗链可能导致加载失败</div> <div style="color:var(--dim);font-size:11px;margin-top:6px">支持任意直链 jpg/png/webp;对方站点防盗链可能导致加载失败</div>
</div> </div>
<div id="wpPaneApi" style="display:none"> <div id="wpPaneApi" style="display:none">
<button class="btn btn-blue" onclick="wpFetchBing()">获取必应每日壁纸</button> <button class="btn btn-blue" onclick="wpFetchBing()">获取每日壁纸(多源)</button>
<div id="wpApiInfo" style="margin-top:10px;color:var(--dim);font-size:12px">点击获取后自动预览,再点应用生效</div> <div id="wpApiInfo" style="margin-top:10px;color:var(--dim);font-size:12px">点击获取后自动预览,再点应用生效</div>
</div> </div>
<div id="wpDimRow" style="display:none;margin-top:14px"> <div id="wpDimRow" style="display:none;margin-top:14px">
@@ -2315,6 +2323,7 @@ function setTheme(color) {
b.style.backgroundPosition = ''; b.style.backgroundPosition = '';
b.style.backgroundRepeat = ''; b.style.backgroundRepeat = '';
b.style.backgroundAttachment = ''; b.style.backgroundAttachment = '';
b.classList.remove('bg-img-mode');
document.documentElement.style.setProperty('--bg', color); document.documentElement.style.setProperty('--bg', color);
b.style.background = color; b.style.background = color;
localStorage.removeItem('sdf1_admin_bgimg'); localStorage.removeItem('sdf1_admin_bgimg');
+106 -5
View File
@@ -1,14 +1,23 @@
<?php <?php
/** /**
* 玩家 2FA (TOTP) 二维码展示中转 —— PHP 只做显示器 * 玩家 2FA (TOTP) 二维码展示中转 + 绑定状态双向同步(第47批)—— PHP 只做显示器与镜像
* *
* 2FA 的全部逻辑(生成密钥 / 动态码校验 / 绑定 / 解绑)都在 Java 端本地完成, * 2FA 的动态码校验逻辑在 Java 端与 PHP 端各自本地完成(同一 Base32 密钥),
* 这里只负责暂存 otpauth 载荷供玩家浏览器渲染二维码,10 分钟自动过期。 * 这里负责:
* 1) 暂存 otpauth 载荷供玩家浏览器渲染二维码,10 分钟自动过期
* 2) 以 web_account_2fa 为持久镜像,供 Java 端拉取/推送绑定状态
* (修复:网页端绑定后游戏内 /2fa 显示"未绑定")
* *
* Java -> PHP(secret 校验): * Java -> PHP(secret 校验):
* push_qr 存一条二维码载荷(token -> otpauth URI) * push_qr 存一条二维码载荷(token -> otpauth URI)
* pull_state 拉取该玩家网页端绑定状态(bound/secret/bound_at/updated_at)
* push_state 推送 Java 端绑定/解绑结果(网页端无记录或记录更旧时落库)
* 玩家浏览器(持有随机 token 即可读): * 玩家浏览器(持有随机 token 即可读):
* get 按 t 取回 otpauth URI * get 按 t 取回 otpauth URI
*
* 同步语义见 Java 端 TwoFactorManager.applyWebState():
* 单向导入(网页绑定游戏未绑定)/ 解绑传播(网页解绑且更晚)/
* 密钥冲突取时间新者;恢复代码两端格式不同(网页16位明文 vs 游戏6位哈希),各自维护。
*/ */
while (ob_get_level() > 0) { @ob_end_clean(); } while (ob_get_level() > 0) { @ob_end_clean(); }
@@ -117,6 +126,98 @@ try {
break; break;
} }
// ===== Java -> PHP:拉取网页端 2FA 绑定状态(第47批双向同步) =====
case 'pull_state': {
twofaRequireSecret();
$player = (string) getParam('player', '');
if ($player === '' || strlen($player) > 32) error('玩家名不合法', 400);
$db = getDB();
$stmt = $db->prepare("SELECT totp_secret, bound_at, updated_at
FROM web_account_2fa WHERE player_name = :p");
$stmt->bindValue(':p', $player, SQLITE3_TEXT);
$row = $stmt->execute()->fetchArray(SQLITE3_ASSOC);
if (!$row) {
success(['exists' => false, 'bound' => false,
'secret' => '', 'bound_at' => 0, 'updated_at' => 0]);
}
$bound = ((int) $row['bound_at'] > 0 && (string) $row['totp_secret'] !== '');
success([
'exists' => true,
'bound' => $bound,
// 明文密钥仅回给持 SECRET_KEY 的 Java 服务端,绝不出现在浏览器接口
'secret' => $bound ? (string) $row['totp_secret'] : '',
'bound_at' => (int) $row['bound_at'],
'updated_at' => (int) $row['updated_at'],
]);
break;
}
// ===== Java -> PHP:推送 Java 端绑定/解绑结果(第47批双向同步) =====
case 'push_state': {
twofaRequireSecret();
$player = (string) getParam('player', '');
$bound = (string) getParam('bound', '') === '1';
// ★ TOTP 密钥参数名必须是 twofa_secret:'secret' 已被通信密钥占用
$secret = (string) getParam('twofa_secret', '');
$boundAt = (int) getParam('bound_at', '0');
if ($player === '' || strlen($player) > 32) error('玩家名不合法', 400);
if ($bound && !preg_match('/^[A-Z2-7]{16,64}$/', $secret)) {
error('密钥格式错误', 400);
}
$db = getDB();
$stmt = $db->prepare("SELECT bound_at, recovery_codes
FROM web_account_2fa WHERE player_name = :p");
$stmt->bindValue(':p', $player, SQLITE3_TEXT);
$row = $stmt->execute()->fetchArray(SQLITE3_ASSOC);
$existingAt = $row ? (int) $row['bound_at'] : -1; // -1 = 无记录
$now = time();
if ($bound) {
// 仅当网页端无记录、未绑定、或绑定时间更旧时覆盖;
// 网页端已绑定且不比我们旧 → 不动(网页端用户主动操作优先)
$webNewer = ($existingAt > 0 && $boundAt > 0 && $existingAt >= $boundAt);
if ($webNewer) {
success(['applied' => false, 'reason' => 'web_side_newer']);
}
// 游戏端恢复代码是 SHA-256 哈希且格式与网页端(16位明文 JSON)不兼容,
// 推送时清空网页端恢复代码列表,避免网页端用旧代码通过第二层验证
$stmt = $db->prepare("INSERT INTO web_account_2fa
(player_name, totp_secret, recovery_codes, bound_at, updated_at)
VALUES (:p, :s, '', :b, :u)
ON CONFLICT(player_name) DO UPDATE SET
totp_secret = :s, recovery_codes = '',
bound_at = :b, updated_at = :u");
$stmt->bindValue(':p', $player, SQLITE3_TEXT);
$stmt->bindValue(':s', $secret, SQLITE3_TEXT);
$stmt->bindValue(':b', $boundAt > 0 ? $boundAt : $now, SQLITE3_INTEGER);
$stmt->bindValue(':u', $now, SQLITE3_INTEGER);
$stmt->execute();
success(['applied' => true, 'action' => 'bound']);
} else {
// 解绑:仅当网页端记录不比我们新时清空(防覆盖网页端更晚的重新绑定)
$webNewer = ($existingAt > 0 && $boundAt > 0 && $existingAt > $boundAt);
if ($webNewer) {
success(['applied' => false, 'reason' => 'web_side_newer']);
}
if ($existingAt === 0) {
success(['applied' => false, 'reason' => 'already_unbound']);
}
if ($existingAt > 0) {
$stmt = $db->prepare("UPDATE web_account_2fa SET
totp_secret = '', recovery_codes = '', bound_at = 0, updated_at = :u
WHERE player_name = :p");
$stmt->bindValue(':p', $player, SQLITE3_TEXT);
$stmt->bindValue(':u', $now, SQLITE3_INTEGER);
$stmt->execute();
}
success(['applied' => true, 'action' => 'unbound']);
}
break;
}
default: default:
error('未知操作', 400); error('未知操作', 400);
} }
+75 -23
View File
@@ -1,10 +1,14 @@
<?php <?php
/** /**
* 自定义背景壁纸源代理 —— 第46批 * 自定义背景壁纸源代理 —— 第46批创建,第47批改多源 fallback
* *
* 用户端(player.php)与管理员端(admin.php)的自定义背景系统共用: * 用户端(player.php)与管理员端(admin.php)的自定义背景系统共用:
* action=bing 返回必应每日壁纸元数据(图片URL/标题/版权/日期),6小时文件缓存 * action=bing 每日壁纸元数据(图片URL/标题/版权/日期),6小时文件缓存
* action=image 按 u 参数转发图片二进制(仅白名单 bing.com 域,防SSRF/防盗链),浏览器缓存6小时 * 第47批:单一必应源 → 四级 fallback 链(用户侧必应取不到图的对策):
* 1) www.bing.com HPImageArchive 2) cn.bing.com 镜像
* 3) NASA APOD (api.nasa.gov) 4) picsum.photos 兜底(永不失败)
* action=image 按 u 参数转发图片二进制(白名单域,防SSRF/防盗链),浏览器缓存6小时
* 第47批:白名单随多源扩充 NASA / picsum / unsplash 域
* *
* 自定义图片URL模式为浏览器直连(前端 background-image),不经过本代理。 * 自定义图片URL模式为浏览器直连(前端 background-image),不经过本代理。
* 自包含轻量实现,不依赖 core.php/DB。 * 自包含轻量实现,不依赖 core.php/DB。
@@ -17,7 +21,7 @@ error_reporting(E_ALL);
ini_set('display_errors', '0'); ini_set('display_errors', '0');
ini_set('log_errors', '1'); ini_set('log_errors', '1');
/** 必应结果缓存时长(秒)—— 每日换图,6小时足够 */ /** 壁纸元数据缓存时长(秒)—— 每日换图,6小时足够 */
define('WPJ_BING_TTL', 21600); define('WPJ_BING_TTL', 21600);
function wpjOut($ok, $msg, $data = null) { function wpjOut($ok, $msg, $data = null) {
@@ -74,41 +78,89 @@ function wpjFetch($url, $timeout, $referer) {
$act = isset($_GET['action']) ? $_GET['action'] : ''; $act = isset($_GET['action']) ? $_GET['action'] : '';
/* ===== 必应每日壁纸元数据 ===== */ /* ===== 每日壁纸元数据(第47批:多源 fallback 链) ===== */
if ($act === 'bing') { if ($act === 'bing') {
$cached = wpjCacheGet('bing', WPJ_BING_TTL); $cached = wpjCacheGet('bing', WPJ_BING_TTL);
if ($cached !== null) { wpjOut(true, 'ok', $cached); } if ($cached !== null) { wpjOut(true, 'ok', $cached); }
$api = 'https://www.bing.com/HPImageArchive.aspx?format=js&idx=0&n=1&mkt=zh-CN'; $data = null;
$body = wpjFetch($api, 6, 'https://www.bing.com/');
$j = json_decode($body, true); // 源1/2:必应官方 HPImageArchive(主域失败换中国镜像)
if (!is_array($j) || empty($j['images'][0]['urlbase'])) { foreach (array('https://www.bing.com', 'https://cn.bing.com') as $base) {
wpjOut(false, '壁纸源暂不可用,请稍后重试或改用图片URL'); $body = wpjFetch($base . '/HPImageArchive.aspx?format=js&idx=0&n=1&mkt=zh-CN', 6, $base . '/');
$j = json_decode($body, true);
if (!is_array($j) || empty($j['images'][0]['urlbase'])) { continue; }
$img = $j['images'][0];
$direct = $base . $img['urlbase'] . '_1920x1080.jpg';
$data = array(
// 前端优先用代理地址(防防盗链),direct 作兜底直连
'url' => 'wallpaper.php?action=image&u=' . rawurlencode($direct),
'direct' => $direct,
'source' => 'bing',
'title' => isset($img['title']) ? $img['title'] : '',
'copyright' => isset($img['copyright']) ? $img['copyright'] : '',
'date' => isset($img['startdate']) ? $img['startdate'] : '',
);
break;
} }
$img = $j['images'][0];
$direct = 'https://www.bing.com' . $img['urlbase'] . '_1920x1080.jpg'; // 源3:NASA 每日天文图 APOD(DEMO_KEY 限速,但有6小时缓存,服务器低频调用足够)
$data = array( if ($data === null) {
// 前端优先用代理地址(防防盗链),direct 作兜底直连 $body = wpjFetch('https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY&hd=true', 8, 'https://api.nasa.gov/');
'url' => 'wallpaper.php?action=image&u=' . rawurlencode($direct), $j = json_decode($body, true);
'direct' => $direct, if (is_array($j) && !empty($j['hdurl']) && preg_match('#^https?://#i', $j['hdurl'])) {
'title' => isset($img['title']) ? $img['title'] : '', $direct = $j['hdurl'];
'copyright' => isset($img['copyright']) ? $img['copyright'] : '', $data = array(
'date' => isset($img['startdate']) ? $img['startdate'] : '', 'url' => 'wallpaper.php?action=image&u=' . rawurlencode($direct),
); 'direct' => $direct,
'source' => 'nasa',
'title' => isset($j['title']) ? $j['title'] : 'NASA 每日天文图',
'copyright' => isset($j['copyright']) ? $j['copyright'] : 'NASA APOD',
'date' => isset($j['date']) ? $j['date'] : date('Y-m-d'),
);
}
}
// 源4:picsum 固定尺寸摄影图(无 key、永不应答失败的最后兜底;302 跳转浏览器直接可用)
if ($data === null) {
$direct = 'https://picsum.photos/1920/1080';
$data = array(
'url' => $direct,
'direct' => $direct,
'source' => 'picsum',
'title' => '随机摄影图',
'copyright' => 'Photos from Unsplash (picsum.photos)',
'date' => date('Y-m-d'),
);
}
wpjCacheSet('bing', $data); wpjCacheSet('bing', $data);
wpjOut(true, 'ok', $data); wpjOut(true, 'ok', $data);
} }
/* ===== 图片转发(仅 bing 白名单,防 SSRF) ===== */ /* ===== 图片转发(白名单域,防 SSRF;第47批随多源扩充) ===== */
if ($act === 'image') { if ($act === 'image') {
$u = isset($_GET['u']) ? (string)$_GET['u'] : ''; $u = isset($_GET['u']) ? (string)$_GET['u'] : '';
$host = @parse_url($u, PHP_URL_HOST); $host = @parse_url($u, PHP_URL_HOST);
if ($u === '' || !is_string($host) || $host === '') { wpjOut(false, '参数无效'); } if ($u === '' || !is_string($host) || $host === '') { wpjOut(false, '参数无效'); }
$host = strtolower($host); $host = strtolower($host);
$allow = array('www.bing.com', 'bing.com', 'th.bing.com', 'cn.bing.com'); $allow = array(
// 必应(源1/2)
'www.bing.com', 'bing.com', 'th.bing.com', 'cn.bing.com',
// NASA APOD(源3,hdurl 指向 apod.nasa.gov / www.nasa.gov 等)
'apod.nasa.gov', 'www.nasa.gov', 'images-api.nasa.gov',
// picsum 兜底链(源4,302 目标域)
'picsum.photos', 'fastly.picsum.photos', 'images.unsplash.com',
);
if (!in_array($host, $allow, true)) { wpjOut(false, '不支持的图片源'); } if (!in_array($host, $allow, true)) { wpjOut(false, '不支持的图片源'); }
$bin = wpjFetch($u, 10, 'https://www.bing.com/'); $referer = 'https://www.bing.com/';
if ($host === 'apod.nasa.gov' || $host === 'www.nasa.gov' || $host === 'images-api.nasa.gov') {
$referer = 'https://apod.nasa.gov/';
} elseif ($host === 'picsum.photos' || $host === 'fastly.picsum.photos' || $host === 'images.unsplash.com') {
$referer = 'https://picsum.photos/';
}
$bin = wpjFetch($u, 10, $referer);
if (!is_string($bin) || strlen($bin) < 1000) { wpjOut(false, '图片下载失败'); } if (!is_string($bin) || strlen($bin) < 1000) { wpjOut(false, '图片下载失败'); }
// 粗校验图片魔数,防返回HTML错误页被当图片缓存 // 粗校验图片魔数,防返回HTML错误页被当图片缓存
$head = substr($bin, 0, 3); $head = substr($bin, 0, 3);
+13 -1
View File
@@ -91,6 +91,15 @@ if ($currentVersion !== $BUILD_VERSION) {
background: var(--card); border: 1px solid var(--border); border-radius: 8px; background: var(--card); border: 1px solid var(--border); border-radius: 8px;
padding: 20px; margin-bottom: 16px; padding: 20px; margin-bottom: 16px;
} }
/* 第47批:图片壁纸模式下卡片/侧栏自适应半透明+毛玻璃;纯色/默认模式不受影响 */
body.bg-img-mode .card, body.bg-img-mode .sidebar {
background: rgba(22,27,34,0.55);
backdrop-filter: blur(14px) saturate(1.4);
-webkit-backdrop-filter: blur(14px) saturate(1.4);
}
body.bg-img-mode.light-theme .card, body.bg-img-mode.light-theme .sidebar {
background: rgba(255,255,255,0.55);
}
.card h2 { font-size: 16px; margin-bottom: 12px; color: var(--accent); } .card h2 { font-size: 16px; margin-bottom: 12px; color: var(--accent); }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 12px; } .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 12px; }
.item-card { .item-card {
@@ -2711,6 +2720,7 @@ if ($currentVersion !== $BUILD_VERSION) {
b.style.backgroundPosition = 'center center'; b.style.backgroundPosition = 'center center';
b.style.backgroundRepeat = 'no-repeat'; b.style.backgroundRepeat = 'no-repeat';
b.style.backgroundAttachment = 'fixed'; b.style.backgroundAttachment = 'fixed';
b.classList.add('bg-img-mode');
document.documentElement.style.setProperty('--bg', 'rgba(13,17,23,' + Math.min(0.9, dim + 0.25) + ')'); document.documentElement.style.setProperty('--bg', 'rgba(13,17,23,' + Math.min(0.9, dim + 0.25) + ')');
localStorage.setItem('sdf1_player_bgimg', JSON.stringify({ type: 'img', url: url, dim: dim })); localStorage.setItem('sdf1_player_bgimg', JSON.stringify({ type: 'img', url: url, dim: dim }));
if (!silent) toast('背景图片已应用', 'success'); if (!silent) toast('背景图片已应用', 'success');
@@ -2798,6 +2808,7 @@ if ($currentVersion !== $BUILD_VERSION) {
b.style.backgroundPosition = ''; b.style.backgroundPosition = '';
b.style.backgroundRepeat = ''; b.style.backgroundRepeat = '';
b.style.backgroundAttachment = ''; b.style.backgroundAttachment = '';
b.classList.remove('bg-img-mode');
document.documentElement.style.removeProperty('--bg'); document.documentElement.style.removeProperty('--bg');
b.style.background = ''; b.style.background = '';
const ov = document.getElementById('themePickerOverlay'); const ov = document.getElementById('themePickerOverlay');
@@ -2854,7 +2865,7 @@ if ($currentVersion !== $BUILD_VERSION) {
<div style="color:var(--dim);font-size:11px;margin-top:6px">支持任意直链 jpg/png/webp;对方站点防盗链可能导致加载失败</div> <div style="color:var(--dim);font-size:11px;margin-top:6px">支持任意直链 jpg/png/webp;对方站点防盗链可能导致加载失败</div>
</div> </div>
<div id="wpPaneApi" style="display:none"> <div id="wpPaneApi" style="display:none">
<button class="btn btn-blue" onclick="wpFetchBing()">获取必应每日壁纸</button> <button class="btn btn-blue" onclick="wpFetchBing()">获取每日壁纸(多源)</button>
<div id="wpApiInfo" style="margin-top:10px;color:var(--dim);font-size:12px">点击获取后自动预览,再点应用生效</div> <div id="wpApiInfo" style="margin-top:10px;color:var(--dim);font-size:12px">点击获取后自动预览,再点应用生效</div>
</div> </div>
<div id="wpDimRow" style="display:none;margin-top:14px"> <div id="wpDimRow" style="display:none;margin-top:14px">
@@ -2886,6 +2897,7 @@ if ($currentVersion !== $BUILD_VERSION) {
b.style.backgroundPosition = ''; b.style.backgroundPosition = '';
b.style.backgroundRepeat = ''; b.style.backgroundRepeat = '';
b.style.backgroundAttachment = ''; b.style.backgroundAttachment = '';
b.classList.remove('bg-img-mode');
document.documentElement.style.setProperty('--bg', color); document.documentElement.style.setProperty('--bg', color);
b.style.background = color; b.style.background = color;
localStorage.removeItem('sdf1_player_bgimg'); localStorage.removeItem('sdf1_player_bgimg');