162 lines
7.3 KiB
Python
162 lines
7.3 KiB
Python
from pathlib import Path
|
|
folder=Path(__file__).resolve().parent;p=folder/'payload/assets/ui/console-app.js'
|
|
s=p.read_text(encoding='utf-8')
|
|
helper='''// Keep in-progress values separate from background snapshots. They are cleared
|
|
// only when the backend echoes the same value, never just because focus moved.
|
|
const fieldDrafts = new Map();
|
|
const pageScroll = new Map();
|
|
const pendingEdits = new Map();
|
|
const fieldSources = {
|
|
"ai-name":["ai","name"], "auto-poll":["automation","poll"],
|
|
"auto-batch":["automation","batch"], "auto-delay":["automation","delay"],
|
|
"auto-idle":["automation","idle"], "auto-context":["automation","context"],
|
|
"auto-draft-autosend":["automation","draftAutosend"],
|
|
"sys-language":["system","language"], "sys-scale":["system","scale"],
|
|
"sys-retention":["system","retention"], "sys-server-url":["system","serverUrl"],
|
|
"archive-search":[null,"archiveQuery"], "event-search":[null,"eventQuery"],
|
|
"wechat-db-search":["wechatDb","query"], "archive-date":[null,"archiveDate"]
|
|
};
|
|
function recordDraft(element){
|
|
if (!element || !fieldSources[element.id]) return;
|
|
fieldDrafts.set(element.id, {view:STATE.view,value:element.value});
|
|
}
|
|
function acknowledgeDrafts(data){
|
|
for (const [id,draft] of fieldDrafts){
|
|
const [section,key]=fieldSources[id];
|
|
const source=section ? data[section] : data;
|
|
if (source && source[key] != null && String(source[key]) === draft.value) fieldDrafts.delete(id);
|
|
}
|
|
}
|
|
function restoreDrafts(view){
|
|
for (const [id,draft] of fieldDrafts){
|
|
const element=document.getElementById(id);
|
|
if (draft.view===view && element && element.value!==draft.value) element.value=draft.value;
|
|
}
|
|
if (view==='ai' && fieldDrafts.has('ai-name')) markAiDirty();
|
|
}
|
|
function cancelEdit(key){
|
|
const edit=pendingEdits.get(key);
|
|
if (edit) clearTimeout(edit.timer);
|
|
pendingEdits.delete(key);
|
|
}
|
|
function sendEdit(key){
|
|
const edit=pendingEdits.get(key);
|
|
if (!edit) return;
|
|
cancelEdit(key);
|
|
callBridge(edit.method,edit.payload);
|
|
}
|
|
function scheduleEdit(key,method,payload,delay){
|
|
cancelEdit(key);
|
|
pendingEdits.set(key,{method,payload,timer:setTimeout(()=>sendEdit(key),delay)});
|
|
}
|
|
function flushEdits(){
|
|
for (const key of [...pendingEdits.keys()]) sendEdit(key);
|
|
}
|
|
let composing=false, pointerDown=false, deferredRefresh=0;
|
|
document.addEventListener('pointerdown',()=>{pointerDown=true;},true);
|
|
document.addEventListener('pointerup',()=>{pointerDown=false;},true);
|
|
document.addEventListener('pointercancel',()=>{pointerDown=false;},true);
|
|
document.addEventListener('compositionstart',()=>{composing=true;});
|
|
document.addEventListener('compositionend',event=>{
|
|
composing=false;
|
|
event.target.dispatchEvent(new Event('input',{bubbles:true}));
|
|
});
|
|
document.addEventListener('focusout',()=>{
|
|
clearTimeout(deferredRefresh);
|
|
deferredRefresh=setTimeout(()=>{
|
|
const active=document.activeElement;
|
|
if (!pointerDown && !composing && window.__deferredView===STATE.view
|
|
&& !(active && /^(INPUT|TEXTAREA|SELECT)$/.test(active.tagName))){
|
|
window.__deferredView=null;
|
|
render(STATE.view);
|
|
}
|
|
},100);
|
|
});
|
|
|
|
'''
|
|
assert s.count('const _render = render;')==1;s=s.replace('const _render = render;',helper+'const _render = render;')
|
|
s=s.replace(''' const sameView = STATE.view === name;
|
|
STATE.view = name;''',''' const sameView = STATE.view === name;
|
|
const scrollHost = document.getElementById("main");
|
|
if (scrollHost) pageScroll.set(STATE.view,scrollHost.scrollTop);
|
|
STATE.view = name;''',1)
|
|
s=s.replace(''' main.innerHTML = html;
|
|
normalizeReferenceView();''',''' main.innerHTML = html;
|
|
restoreDrafts(name);
|
|
normalizeReferenceView();
|
|
main.scrollTop = pageScroll.get(name) || 0;''',1)
|
|
point=''' const prev = STATE.view;
|
|
Object.assign(STATE, data);'''
|
|
new=''' if (window.__pendingNavigation && data.view && data.view!==window.__pendingNavigation) return;
|
|
if (data.view===window.__pendingNavigation) window.__pendingNavigation=null;
|
|
acknowledgeDrafts(data);
|
|
const prev = STATE.view;
|
|
const previousScale=(STATE.system || {}).scale;
|
|
Object.assign(STATE, data);
|
|
if (data.system && data.system.scale!==previousScale && typeof fit==='function') fit();'''
|
|
assert s.count(point)==1;s=s.replace(point,new)
|
|
s=s.replace(''' if (typing && (STATE.view || prev) === prev && ["ai","automation","system","archive","logs"].includes(prev)) {
|
|
return;
|
|
}''',''' if ((typing || composing) && (STATE.view || prev)===prev) {
|
|
window.__deferredView=prev;
|
|
return;
|
|
}''')
|
|
old=''' if (name !== STATE.view) render(name);
|
|
callBridge("navigate", name);'''
|
|
new=''' flushEdits();
|
|
window.__pendingNavigation=name;
|
|
if (name !== STATE.view) render(name);
|
|
const serial=window.__navigationSerial=(window.__navigationSerial || 0)+1;
|
|
requestAnimationFrame(()=>requestAnimationFrame(()=>{
|
|
if (serial===window.__navigationSerial) callBridge("navigate",name);
|
|
}));'''
|
|
assert s.count(old)==1;s=s.replace(old,new)
|
|
# Separate debounces, capture values now (before a navigation replaces the DOM).
|
|
s=s.replace('let searchTimer = 0;\n','')
|
|
s=s.replace(''' const id = e.target.id || "";
|
|
const value = e.target.value || "";''',''' const id = e.target.id || "";
|
|
const value = e.target.value || "";
|
|
recordDraft(e.target);
|
|
if (e.isComposing || composing) return;''',1)
|
|
old=''' clearTimeout(searchTimer);
|
|
searchTimer = setTimeout(() => {
|
|
if (id === "archive-search") callBridge("setArchiveQuery", value);
|
|
else if (id === "event-search") callBridge("setEventQuery", value);
|
|
else callBridge("setWeChatDatabaseQuery", value);
|
|
}, 280);'''
|
|
new=''' const method={"archive-search":"setArchiveQuery","event-search":"setEventQuery","wechat-db-search":"setWeChatDatabaseQuery"}[id];
|
|
scheduleEdit(id,method,value,280);'''
|
|
assert old in s;s=s.replace(old,new)
|
|
s=s.replace('id === "auto-context") {','id === "auto-context" || id === "auto-draft-autosend") {')
|
|
old=''' clearTimeout(searchTimer);
|
|
searchTimer = setTimeout(() => {
|
|
callBridge("saveAutomationSettings", collectAutomationPayload());
|
|
}, 400);'''
|
|
new=''' scheduleEdit("automation","saveAutomationSettings",collectAutomationPayload(),400);'''
|
|
assert old in s;s=s.replace(old,new)
|
|
old=''' clearTimeout(searchTimer);
|
|
searchTimer = setTimeout(() => {
|
|
callBridge("setSystemField", collectSystemPayload());
|
|
}, 280);'''
|
|
new=''' if (id === "sys-retention") scheduleEdit("system","setSystemField",collectSystemPayload(),280);'''
|
|
assert old in s;s=s.replace(old,new)
|
|
old=''' if (id === "sys-language" || id === "sys-scale") {
|
|
callBridge("setSystemField", collectSystemPayload());
|
|
}'''
|
|
new=''' if (id === "sys-language" || id === "sys-scale") {
|
|
recordDraft(e.target);
|
|
cancelEdit("system");
|
|
callBridge("setSystemField",collectSystemPayload());
|
|
}'''
|
|
assert old in s;s=s.replace(old,new)
|
|
# Cancel the pending autosave when clicking the explicit Save button.
|
|
point='''document.addEventListener("click", e => {
|
|
const nav ='''
|
|
new='''document.addEventListener("click", e => {
|
|
const saveAction=e.target.closest('[data-action="save-system"],[data-action="save-automation"]');
|
|
if (saveAction) cancelEdit(saveAction.dataset.action==='save-system'?'system':'automation');
|
|
const nav ='''
|
|
assert point in s;s=s.replace(point,new)
|
|
p.write_text(s,encoding='utf-8',newline='\n')
|
|
print('Prepared independent saves, draft protection and navigation coalescing')
|