-
Notifications
You must be signed in to change notification settings - Fork 5
feat: Cross-device settings sync via cloud backend #961
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
32ab560
ba982dd
d379b84
a7832e9
c0bee33
fce1d3f
09af1fb
078ed1b
43ee3d0
332b5e1
4293661
6cc1ca5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| // ==UserScript== | ||
| // @name XMOJ | ||
| // @version 3.4.0 | ||
| // @version 3.4.1 | ||
| // @description XMOJ增强脚本 | ||
| // @author @XMOJ-Script-dev, @langningchen and the community | ||
| // @namespace https://github/langningchen | ||
|
|
@@ -532,6 +532,33 @@ let RequestAPI = (Action, Data, CallBack) => { | |
| } | ||
| } | ||
| }; | ||
| let SyncSettingsToCloud = (CallBack) => { | ||
| if (!CurrentUsername) { | ||
| if (CallBack) CallBack({ Success: false, Message: "用户未登录" }); | ||
| return; | ||
| } | ||
| if (!UtilityEnabled("CloudSync")) { | ||
| if (CallBack) CallBack({ Success: false, Message: "云同步已禁用" }); | ||
| return; | ||
| } | ||
| let Settings = {}; | ||
| for (let i = 0; i < localStorage.length; i++) { | ||
| let key = localStorage.key(i); | ||
| if (key && key.startsWith("UserScript-Setting-")) { | ||
| Settings[key.replace("UserScript-Setting-", "")] = localStorage.getItem(key); | ||
| } | ||
| } | ||
| RequestAPI("SetUserSettings", {"Settings": JSON.stringify(Settings)}, (Response) => { | ||
| if (UtilityEnabled("DebugMode")) { | ||
| if (Response.Success) { | ||
| console.log("设置已同步到云端"); | ||
| } else { | ||
| console.error("设置云端同步失败:", Response.Message); | ||
| } | ||
| } | ||
| if (CallBack) CallBack(Response); | ||
| }); | ||
| }; | ||
|
|
||
| unsafeWindow.GetContestProblemList = async function(RefreshList) { | ||
| try { | ||
|
|
@@ -2127,6 +2154,7 @@ async function main() { | |
| Select.addEventListener("change", () => { | ||
| localStorage.setItem("UserScript-Setting-Theme", Select.value); | ||
| initTheme(); | ||
| SyncSettingsToCloud(); | ||
| }); | ||
|
Comment on lines
2154
to
2158
|
||
| Row.appendChild(Select); | ||
| } else if (Data[i].Children == undefined) { | ||
|
|
@@ -2144,7 +2172,11 @@ async function main() { | |
| CheckBox.checked = true; | ||
| } | ||
| CheckBox.addEventListener("change", () => { | ||
| return localStorage.setItem("UserScript-Setting-" + Data[i].ID, CheckBox.checked); | ||
| localStorage.setItem("UserScript-Setting-" + Data[i].ID, CheckBox.checked); | ||
| // Don't sync when disabling CloudSync itself (it's already off) | ||
| if (Data[i].ID !== "CloudSync" || CheckBox.checked) { | ||
| SyncSettingsToCloud(); | ||
| } | ||
| }); | ||
|
|
||
| Row.appendChild(CheckBox); | ||
|
|
@@ -2225,6 +2257,8 @@ async function main() { | |
| "ID": "BBSPopup", "Type": "A", "Name": "讨论提醒" | ||
| }, {"ID": "MessagePopup", "Type": "A", "Name": "短消息提醒"}, { | ||
| "ID": "ImageEnlarger", "Type": "A", "Name": "图片放大功能" | ||
| }, { | ||
| "ID": "CloudSync", "Type": "A", "Name": "将设置同步至云端(跨设备同步)" | ||
| }, { | ||
| "ID": "DebugMode", "Type": "A", "Name": "调试模式(仅供开发者使用)" | ||
| }, { | ||
|
|
@@ -2236,6 +2270,88 @@ async function main() { | |
| UtilitiesCardBody.appendChild(UtilitiesCardFooter); | ||
| UtilitiesCard.appendChild(UtilitiesCardBody); | ||
| Container.appendChild(UtilitiesCard); | ||
| let SyncCard = document.createElement("div"); | ||
| SyncCard.className = "card mb-3"; | ||
| let SyncCardHeader = document.createElement("div"); | ||
| SyncCardHeader.className = "card-header"; | ||
| SyncCardHeader.innerText = "设置云同步"; | ||
| SyncCard.appendChild(SyncCardHeader); | ||
| let SyncCardBody = document.createElement("div"); | ||
| SyncCardBody.className = "card-body"; | ||
| let SyncStatusText = document.createElement("p"); | ||
| SyncStatusText.className = "card-text mb-2"; | ||
| SyncStatusText.id = "UserScript-SyncStatus"; | ||
| SyncStatusText.innerText = "正在从云端加载设置…"; | ||
| SyncCardBody.appendChild(SyncStatusText); | ||
| let SyncButtonGroup = document.createElement("div"); | ||
| SyncButtonGroup.className = "d-flex gap-2"; | ||
| let ApplyCloudSettings = (cloudSettings) => { | ||
| for (let key in cloudSettings) { | ||
| const rawValue = cloudSettings[key]; | ||
| localStorage.setItem("UserScript-Setting-" + key, String(rawValue)); | ||
| if (key === "Theme") { | ||
| let themeSelect = document.getElementById("UserScript-Setting-Theme"); | ||
| if (themeSelect) themeSelect.value = String(rawValue); | ||
| initTheme(); | ||
| } else { | ||
| let checkbox = document.getElementById(key); | ||
| if (checkbox) { | ||
| const normalizedChecked = (typeof rawValue === "boolean") ? rawValue : (String(rawValue).toLowerCase() === "true"); | ||
| checkbox.checked = normalizedChecked; | ||
| } | ||
| } | ||
| } | ||
| }; | ||
| let UploadBtn = document.createElement("button"); | ||
| UploadBtn.className = "btn btn-sm btn-primary"; | ||
| UploadBtn.innerText = "上传设置到云端"; | ||
| UploadBtn.addEventListener("click", () => { | ||
| SyncStatusText.innerText = "正在上传…"; | ||
| SyncSettingsToCloud((Response) => { | ||
| SyncStatusText.innerText = Response.Success ? "上传成功" : ("上传失败: " + Response.Message); | ||
| }); | ||
| }); | ||
| SyncButtonGroup.appendChild(UploadBtn); | ||
| let DownloadBtn = document.createElement("button"); | ||
| DownloadBtn.className = "btn btn-sm btn-secondary"; | ||
| DownloadBtn.innerText = "从云端下载设置"; | ||
| DownloadBtn.addEventListener("click", () => { | ||
| SyncStatusText.innerText = "正在下载…"; | ||
| RequestAPI("GetUserSettings", {}, (Response) => { | ||
| if (Response.Success) { | ||
| ApplyCloudSettings(Response.Data.Settings); | ||
| SyncStatusText.innerText = "下载成功,设置已应用(部分设置需刷新页面后生效)"; | ||
| } else { | ||
| SyncStatusText.innerText = "下载失败: " + Response.Message; | ||
| } | ||
| }); | ||
| }); | ||
| SyncButtonGroup.appendChild(DownloadBtn); | ||
| SyncCardBody.appendChild(SyncButtonGroup); | ||
| SyncCard.appendChild(SyncCardBody); | ||
| Container.appendChild(SyncCard); | ||
| if (UtilityEnabled("CloudSync")) { | ||
| RequestAPI("GetUserSettings", {}, (Response) => { | ||
| let SyncStatusEl = document.getElementById("UserScript-SyncStatus"); | ||
| if (Response.Success) { | ||
| const cloudSettings = (Response.Data && Response.Data.Settings) || {}; | ||
| if (Object.keys(cloudSettings).length === 0) { | ||
sourcery-ai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if (SyncStatusEl) SyncStatusEl.innerText = "正在上传本地设置至云端…"; | ||
| SyncSettingsToCloud((Resp) => { | ||
| if (SyncStatusEl) SyncStatusEl.innerText = Resp.Success ? "已将本地设置上传至云端" : ("上传失败: " + Resp.Message); | ||
| }); | ||
| } else { | ||
| ApplyCloudSettings(cloudSettings); | ||
| if (SyncStatusEl) SyncStatusEl.innerText = "已从云端加载设置"; | ||
| } | ||
| } else { | ||
| if (SyncStatusEl) SyncStatusEl.innerText = "云端设置加载失败: " + Response.Message; | ||
| } | ||
| }); | ||
| } else { | ||
| let SyncStatusEl = document.getElementById("UserScript-SyncStatus"); | ||
| if (SyncStatusEl) SyncStatusEl.innerText = "云同步已禁用"; | ||
PythonSmall-Q marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| let FeedbackCard = document.createElement("div"); | ||
| FeedbackCard.className = "card mb-3"; | ||
| let FeedbackCardHeader = document.createElement("div"); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SyncSettingsToCloudserializesSettingswithJSON.stringifybefore passing it intoRequestAPI, which already JSON-stringifies the full request body. This makesData.Settingsa string (unlike other API calls in this file that pass plain objects) and can easily get out of sync with the client-side assumption thatGetUserSettingsreturns an object. Prefer sendingSettingsas an object, or consistently JSON.parse on read + document the expected wire format.