Release v0.6.53: synchronize chat language profiles

This commit is contained in:
agrynco committed 2026-11-06 12:39:00 +02:00
1 parent ede4ce47c0
commit 727dc96908
11 files changed
+115 -22

No files matched your search

+7 -3
View File
@@ -219,8 +219,7 @@ Bytes sharedSyncSettings(const Settings &s)
return a.at("layoutId").get<std::string>() < b.at("layoutId").get<std::string>();
});
for (auto key : {"startWithSystem", "autostartAsAdministrator", "tesseractPath", "useTesseract",
"historyEnabled", "maxItems", "maxTotalMegabytes", "maxImageMegabytes", "writeLog", "preventScreenLock",
"chatCycles"}) j.erase(key); // Chat names and matching rules stay on this PC.
"historyEnabled", "maxItems", "maxTotalMegabytes", "maxImageMegabytes", "writeLog", "preventScreenLock"}) j.erase(key);
return bytes(canonicalProfile(j));
}
Settings restoreSyncSettings(const Bytes &shared, const Settings &local, const std::vector<Layout> &layouts, bool keepLocalLayouts)
@@ -230,6 +229,9 @@ Settings restoreSyncSettings(const Bytes &shared, const Settings &local, const s
if (!j.contains("autoCorrectReplace")) j["autoCorrectReplace"] = false;
if (!j.contains("autoCorrectTypos")) j["autoCorrectTypos"] = false;
if (!j.contains("autoCorrectReplacements")) j["autoCorrectReplacements"] = Json::object();
// Profiles uploaded before v0.6.53 do not participate in chat synchronization.
// An explicit empty object from a newer client does mean remove all profiles.
if (!j.contains("chatCycles")) j["chatCycles"] = original.at("chatCycles");
auto allowed = parse(sharedSyncSettings(local));
if (!j.is_object() || j.value("format", "") != "kswitcher-settings" || j.value("schemaVersion", 0) != 1)
throw std::runtime_error("Unsupported synchronized settings format.");
@@ -295,7 +297,9 @@ SyncResult synchronize(SyncProvider &provider, const Bytes &base, const Bytes &l
if (action == SyncAction::Automatic) merged = mergeSyncProfile(mergeBase, mergeLocal, remote.settings, choices);
else
{
auto selected = bytes(normalizeProfile(parse(action == SyncAction::Download ? remote.settings : mergeLocal)));
auto profile = parse(action == SyncAction::Download ? remote.settings : mergeLocal);
if (!profile.contains("chatCycles")) profile["chatCycles"] = parse(local).value("chatCycles", Json::object());
auto selected = bytes(normalizeProfile(profile));
merged = {selected, selected, selected, {}};
}
if (validate) { validate(merged.local); validate(merged.remote); }
+17 -1
View File
@@ -8,6 +8,7 @@ Json normalizeProfile(Json j)
if (!j.contains("autoCorrectTypos")) j["autoCorrectTypos"] = false;
if (!j.contains("autoCorrectReplacements")) j["autoCorrectReplacements"] = Json::object();
if (!j.contains("snippetTombstones")) j["snippetTombstones"] = Json::array();
if (!j.contains("chatCycles")) j["chatCycles"] = Json::object();
for (auto &sn : j.at("snippets"))
if (!sn.contains("id")) sn["id"] = utf8(snippetId({wide(sn.at("trigger").get<std::string>()), L""}));
return j;
@@ -53,6 +54,10 @@ Json flatProfile(Json j)
for (auto it = replacements.begin(); it != replacements.end();)
if (it.value().is_null()) it = replacements.erase(it);
else ++it;
auto &chats = j["chatCycles"];
for (auto it = chats.begin(); it != chats.end();)
if (it.value().is_null()) it = chats.erase(it);
else ++it;
Json snippets = Json::array(), deleted = Json::array();
for (const auto &[id, item] : j.at("snippets").items())
{
@@ -94,6 +99,7 @@ std::wstring fieldLabel(const std::string &key)
{"translitHotkey", L"Transliteration shortcut"}, {"undoHotkey", L"Undo shortcut"},
{"clipboardHotkey", L"Clipboard shortcut"}, {"screenshotHotkey", L"Screenshot shortcut"},
{"layouts", L"Layout cycle order"}, {"layoutHotkeys", L"Layout shortcuts"},
{"chatCycles", L"Chat languages"},
{"convertLastWord", L"Convert recent word"}, {"switchSystemLayout", L"Switch system layout"},
{"autoCorrectMinLength", L"Minimum word length"}, {"autoCorrectMaxRank", L"Maximum word rank"},
{"autoCorrectLayout", L"Automatic layout correction"}, {"autoCorrectLanguages", L"Dictionary languages"},
@@ -141,6 +147,11 @@ MergeNode mergeNode(const Json &b, const Json &l, const Json &r, const std::stri
auto item = get(l); if (item.is_null() || tombstone(item)) item = get(r);
childLabel = L"Snippet: " + wide(item.is_object() ? item.value("trigger", key) : key);
}
if (path == "/chatCycles") {
auto name = key;
if (auto separator = name.find('\n'); separator != name.npos) name.replace(separator, 1, " / ");
childLabel = L"Chat languages: " + wide(name);
}
auto child = mergeNode(get(b), get(l), get(r), path + "/" + pointerKey(key), childLabel, choices, conflicts, forced);
// Keep nulls in the ancestor so deletion conflicts survive restarts.
out.local[key] = child.local; out.remote[key] = child.remote; out.base[key] = child.base;
@@ -181,7 +192,12 @@ void collisionPaths(const Json &j, std::set<std::string> &paths)
}
SyncMerge mergeSyncProfile(const Bytes &base, const Bytes &local, const Bytes &remote, const std::vector<SyncChoice> &choices)
{
auto l = treeProfile(parse(local)), r = treeProfile(parse(remote));
auto localProfile = parse(local), remoteProfile = parse(remote);
// Absence in an older client is not a deletion. Preserve the participating
// client's chat profiles until both copies understand this field.
if (!remoteProfile.contains("chatCycles")) remoteProfile["chatCycles"] = localProfile.value("chatCycles", Json::object());
if (!localProfile.contains("chatCycles")) localProfile["chatCycles"] = remoteProfile.at("chatCycles");
auto l = treeProfile(localProfile), r = treeProfile(remoteProfile);
// With no common history, union independent entries; differing scalar values
// remain on their respective devices until the user resolves the conflict.
auto b = base.empty() ? Json::object() : treeProfile(parse(base));
+1 -1
View File
@@ -2,7 +2,7 @@
#include "platform.h"
namespace ks
{
inline constexpr wchar_t AppVersion[] = L"0.6.52";
inline constexpr wchar_t AppVersion[] = L"0.6.53";
inline constexpr UINT UpdateMessage = WM_APP + 7;
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;