ispyx 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +183 -2
- package/config.example.json +15 -0
- package/package.json +47 -4
- package/src/core/RESIDENT.md +45 -0
- package/src/core/auth.js +129 -0
- package/src/core/guard.js +287 -0
- package/src/core/image.js +117 -0
- package/src/core/ipc.js +139 -0
- package/src/core/launcher.js +70 -0
- package/src/core/mode.js +17 -0
- package/src/core/motion.js +57 -0
- package/src/core/panel/README.md +70 -0
- package/src/core/panel/index.js +144 -0
- package/src/core/panel/page.html +434 -0
- package/src/core/resident.js +271 -0
- package/src/core/server.js +832 -0
- package/src/effects/appicon.js +46 -0
- package/src/effects/scan.js +850 -0
- package/src/macos-entry.js +16 -0
- package/src/platform/CONTRACT.md +131 -0
- package/src/platform/darwin/PROTOCOL.md +82 -0
- package/src/platform/darwin/hotkey.js +41 -0
- package/src/platform/darwin/index.js +118 -0
- package/src/platform/darwin/keys.js +49 -0
- package/src/platform/darwin/layer.js +87 -0
- package/src/platform/darwin/overlay.js +83 -0
- package/src/platform/darwin/panel.js +25 -0
- package/src/platform/darwin/rpc-worker.js +132 -0
- package/src/platform/darwin/rpc.js +146 -0
- package/src/platform/fake/index.js +162 -0
- package/src/platform/index.js +13 -0
- package/src/platform/win32/auth-dialog.ps1 +140 -0
- package/src/platform/win32/autostart.js +54 -0
- package/src/platform/win32/dialog.js +41 -0
- package/src/platform/win32/hotkey.js +77 -0
- package/src/platform/win32/index.js +31 -0
- package/src/platform/win32/keys.js +31 -0
- package/src/platform/win32/layer.js +120 -0
- package/src/platform/win32/overlay.js +265 -0
- package/src/platform/win32/pump.js +37 -0
- package/src/platform/win32/tray-worker.js +172 -0
- package/src/platform/win32/tray.js +103 -0
- package/src/platform/win32/win32.js +577 -0
- package/src/server.js +14 -0
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<meta name="color-scheme" content="light dark">
|
|
7
|
+
<title>Ispyx</title>
|
|
8
|
+
<link rel="icon" href="icon.png">
|
|
9
|
+
<style>
|
|
10
|
+
:root{--bg:#f5f5f7;--card:#fff;--fg:#1d1d1f;--mute:#6e6e73;--line:#e3e3e8;--fill:#f0f0f3;--acc:#2fd16b;--accfg:#0b3d1e;--ok:#1f9d4c;--warn:#c66a00;--warnbg:#fff3e0;--okbg:#e6f8ed;--danger:#d93a3a}
|
|
11
|
+
@media (prefers-color-scheme:dark){:root{--bg:#1c1c1e;--card:#2a2a2d;--fg:#f5f5f7;--mute:#98989f;--line:#3c3c41;--fill:#363639;--ok:#3ddc7d;--warn:#ffb255;--warnbg:#3b2d17;--okbg:#17342266;--danger:#ff6b6b}}
|
|
12
|
+
*{box-sizing:border-box}
|
|
13
|
+
html{-webkit-text-size-adjust:100%}
|
|
14
|
+
body{margin:0;background:var(--bg);color:var(--fg);font:13px/1.4 -apple-system,BlinkMacSystemFont,"SF Pro Text","Segoe UI Variable Text","Segoe UI",system-ui,sans-serif;-webkit-font-smoothing:antialiased;overflow-x:hidden}
|
|
15
|
+
main{max-width:480px;margin:0 auto;padding:22px 20px 26px}
|
|
16
|
+
header{display:flex;align-items:center;gap:14px;margin-bottom:18px}
|
|
17
|
+
header img{width:44px;height:44px;flex:none;border-radius:10px}
|
|
18
|
+
h1{font-size:17px;line-height:1.25;margin:0;font-weight:700}
|
|
19
|
+
.sub{color:var(--mute);font-size:12px;margin-top:2px;overflow-wrap:anywhere}
|
|
20
|
+
.dot{display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--mute);margin-right:6px;vertical-align:1px}
|
|
21
|
+
.run .dot{background:var(--acc);box-shadow:0 0 0 3px rgba(47,209,107,.22)}
|
|
22
|
+
.warn .dot{background:var(--warn)}
|
|
23
|
+
section{margin-bottom:18px}
|
|
24
|
+
h2{font-size:11px;letter-spacing:.05em;text-transform:uppercase;color:var(--mute);margin:0 0 6px 4px;font-weight:600}
|
|
25
|
+
.card{background:var(--card);border:1px solid var(--line);border-radius:10px;overflow:hidden}
|
|
26
|
+
.item{padding:11px 14px;border-top:1px solid var(--line)}
|
|
27
|
+
.item:first-child{border-top:0}
|
|
28
|
+
.line{display:flex;align-items:center;justify-content:space-between;gap:12px;min-height:22px}
|
|
29
|
+
.line b{font-weight:600}
|
|
30
|
+
.d{color:var(--mute);font-size:12px;margin-top:3px}
|
|
31
|
+
.badge{font-size:12px;font-weight:600;white-space:nowrap;padding:1px 9px;border-radius:999px}
|
|
32
|
+
.badge.ok{color:var(--ok);background:var(--okbg)}
|
|
33
|
+
.badge.no{color:var(--warn);background:var(--warnbg)}
|
|
34
|
+
.btns{display:flex;flex-wrap:wrap;gap:8px;margin-top:9px}
|
|
35
|
+
button{font:inherit;color:var(--fg);background:var(--card);border:1px solid var(--line);border-radius:7px;padding:4px 12px;cursor:pointer;box-shadow:0 1px 0 rgba(0,0,0,.04)}
|
|
36
|
+
button:hover{background:var(--fill)}
|
|
37
|
+
button:active{filter:brightness(.95)}
|
|
38
|
+
button:focus-visible,.sw input:focus-visible+i{outline:2px solid var(--acc);outline-offset:2px}
|
|
39
|
+
button.pri{background:var(--acc);border-color:var(--acc);color:var(--accfg);font-weight:600}
|
|
40
|
+
button.pri:hover{filter:brightness(.96);background:var(--acc)}
|
|
41
|
+
button.danger{color:var(--danger)}
|
|
42
|
+
.lbl{margin:0 0 5px}
|
|
43
|
+
.code{display:flex;gap:8px;align-items:flex-start}
|
|
44
|
+
pre{flex:1;min-width:0;margin:0;background:var(--fill);border-radius:7px;padding:7px 9px;font:11px/1.45 ui-monospace,"SF Mono",Menlo,"Cascadia Mono",Consolas,monospace;white-space:pre-wrap;overflow-wrap:anywhere;user-select:text;-webkit-user-select:text}
|
|
45
|
+
.code button{flex:none;min-width:58px}
|
|
46
|
+
.note{font-size:12px;line-height:1.5}
|
|
47
|
+
.sw{position:relative;width:38px;height:22px;flex:none}
|
|
48
|
+
.sw input{position:absolute;inset:0;opacity:0;margin:0;cursor:pointer;z-index:1;width:100%;height:100%}
|
|
49
|
+
.sw i{position:absolute;inset:0;border-radius:11px;background:var(--line);transition:background .15s}
|
|
50
|
+
.sw i::after{content:"";position:absolute;left:2px;top:2px;width:18px;height:18px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.3);transition:transform .15s}
|
|
51
|
+
.sw input:checked+i{background:var(--acc)}
|
|
52
|
+
.sw input:checked+i::after{transform:translateX(16px)}
|
|
53
|
+
.v{color:var(--mute);text-align:right;overflow-wrap:anywhere}
|
|
54
|
+
.tags{display:flex;flex-wrap:wrap;gap:6px;margin-top:7px}
|
|
55
|
+
.tags span{background:var(--fill);border-radius:999px;padding:1px 9px;font-size:12px}
|
|
56
|
+
.tags span button{border:0;background:none;box-shadow:none;padding:0 0 0 5px;color:var(--mute);font-size:13px;line-height:1}
|
|
57
|
+
.tags span button:hover{color:var(--danger);background:none}
|
|
58
|
+
.add{display:flex;gap:8px;margin-top:9px}
|
|
59
|
+
input[type=text],input[type=password]{font:inherit;color:var(--fg);background:var(--card);border:1px solid var(--line);border-radius:7px;padding:4px 9px;min-width:0}
|
|
60
|
+
.add input{flex:1}
|
|
61
|
+
input[type=text]:focus-visible,input[type=password]:focus-visible{outline:2px solid var(--acc);outline-offset:1px}
|
|
62
|
+
dialog{border:1px solid var(--line);border-radius:12px;background:var(--card);color:var(--fg);padding:16px 18px;width:min(340px,calc(100vw - 32px));box-shadow:0 12px 40px rgba(0,0,0,.35)}
|
|
63
|
+
dialog::backdrop{background:rgba(0,0,0,.35)}
|
|
64
|
+
dialog h3{margin:0 0 10px;font-size:14px}
|
|
65
|
+
dialog label{display:block;font-size:12px;color:var(--mute);margin:8px 0 3px}
|
|
66
|
+
dialog input{width:100%}
|
|
67
|
+
.err{color:var(--danger);font-size:12px;margin:8px 0 0}
|
|
68
|
+
.btns.end{justify-content:flex-end}
|
|
69
|
+
ul{list-style:none;margin:0;padding:0;border-top:1px solid var(--line)}
|
|
70
|
+
ul.open{max-height:320px;overflow-y:auto;overscroll-behavior:contain}
|
|
71
|
+
li{padding:6px 14px;border-top:1px solid var(--line);display:flex;gap:10px;align-items:baseline}
|
|
72
|
+
li:first-child{border-top:0}
|
|
73
|
+
#rClients[hidden]+ul{border-top:0}
|
|
74
|
+
li .t{color:var(--mute);font-size:11px;flex:none;width:62px;font-variant-numeric:tabular-nums}
|
|
75
|
+
li .m{min-width:0;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
76
|
+
li .tool{font-weight:600}
|
|
77
|
+
li .n{color:var(--mute);font-weight:400}
|
|
78
|
+
li .s{color:var(--mute);font-size:12px;margin-left:6px}
|
|
79
|
+
.more{display:flex;justify-content:space-between;padding:7px 14px;border-top:1px solid var(--line)}
|
|
80
|
+
.empty{color:var(--mute);padding:11px 14px}
|
|
81
|
+
footer{display:flex;justify-content:space-between;align-items:center;gap:10px;padding-top:2px}
|
|
82
|
+
button.link{border:0;background:none;box-shadow:none;color:var(--mute);padding:0;text-decoration:underline}
|
|
83
|
+
button.link:hover{background:none;color:var(--fg)}
|
|
84
|
+
[hidden]{display:none!important}
|
|
85
|
+
#off{text-align:center;color:var(--mute);padding:40px 0}
|
|
86
|
+
</style>
|
|
87
|
+
</head>
|
|
88
|
+
<body>
|
|
89
|
+
<main>
|
|
90
|
+
<header>
|
|
91
|
+
<img src="icon.png" alt="" width="44" height="44">
|
|
92
|
+
<div>
|
|
93
|
+
<h1 id="head">Ispyx</h1>
|
|
94
|
+
<div class="sub" id="state"><span class="dot"></span><span id="stateTxt">Connecting</span></div>
|
|
95
|
+
<div class="sub" id="meta"></div>
|
|
96
|
+
</div>
|
|
97
|
+
</header>
|
|
98
|
+
<div id="off" hidden>Ispyx has quit. You can close this window.</div>
|
|
99
|
+
<div id="app" hidden>
|
|
100
|
+
|
|
101
|
+
<section id="sPerm" hidden>
|
|
102
|
+
<h2>Permissions</h2>
|
|
103
|
+
<div class="card" id="perms"></div>
|
|
104
|
+
</section>
|
|
105
|
+
|
|
106
|
+
<section id="sConn" hidden>
|
|
107
|
+
<h2>Connect to your AI app</h2>
|
|
108
|
+
<div class="card">
|
|
109
|
+
<div class="item">
|
|
110
|
+
<p class="lbl">Claude Code: run once in a terminal.</p>
|
|
111
|
+
<div class="code"><pre id="cmd"></pre><button type="button" id="cmdCopy">Copy</button></div>
|
|
112
|
+
</div>
|
|
113
|
+
<div class="item">
|
|
114
|
+
<p class="lbl">Claude Desktop: add to <span id="cfgName">claude_desktop_config.json</span>, then restart it.</p>
|
|
115
|
+
<div class="code"><pre id="cfg"></pre><button type="button" id="cfgCopy">Copy</button></div>
|
|
116
|
+
<div class="btns" id="cfgBtns" hidden><button type="button" id="cfgShow">Show Config File</button></div>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
</section>
|
|
120
|
+
|
|
121
|
+
<section>
|
|
122
|
+
<div class="card"><div class="item note" id="usage"></div></div>
|
|
123
|
+
</section>
|
|
124
|
+
|
|
125
|
+
<section id="sGen" hidden>
|
|
126
|
+
<h2>General</h2>
|
|
127
|
+
<div class="card">
|
|
128
|
+
<div class="item line" id="rAuto" hidden><label for="auto" id="autoLbl">Open at login</label><span class="sw"><input type="checkbox" id="auto"><i></i></span></div>
|
|
129
|
+
<div class="item line" id="rFx" hidden><label for="fx">Show effects</label><span class="sw"><input type="checkbox" id="fx"><i></i></span></div>
|
|
130
|
+
</div>
|
|
131
|
+
</section>
|
|
132
|
+
|
|
133
|
+
<section id="sAct" hidden>
|
|
134
|
+
<h2 id="hAct">Activity</h2>
|
|
135
|
+
<div class="card">
|
|
136
|
+
<div class="item line" id="rClients" hidden><span>AI clients</span><span class="v" id="clients"></span></div>
|
|
137
|
+
<ul id="recent" hidden></ul>
|
|
138
|
+
<div class="more" id="more" hidden><button type="button" class="link" id="showAll"></button><button type="button" class="link" id="clearAct">Clear</button></div>
|
|
139
|
+
</div>
|
|
140
|
+
</section>
|
|
141
|
+
|
|
142
|
+
<section id="sSafe" hidden>
|
|
143
|
+
<h2>Safety</h2>
|
|
144
|
+
<div class="card">
|
|
145
|
+
<div class="item line" id="rAuth" hidden><label for="authOn">Ask for a password</label><span class="sw"><input type="checkbox" id="authOn"><i></i></span></div>
|
|
146
|
+
<div class="item line" id="rPw" hidden><span id="pwTxt">Password</span><button type="button" id="pwBtn">Set…</button></div>
|
|
147
|
+
<div class="item" id="rDeny" hidden>
|
|
148
|
+
<span>Never touched</span>
|
|
149
|
+
<div class="tags" id="deny"></div>
|
|
150
|
+
<form class="add" id="denyForm"><input id="denyIn" placeholder="program.exe" maxlength="80" autocomplete="off" spellcheck="false"><button type="submit">Add</button></form>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
</section>
|
|
154
|
+
|
|
155
|
+
<dialog id="dlg">
|
|
156
|
+
<form id="dlgForm">
|
|
157
|
+
<h3 id="dlgT"></h3>
|
|
158
|
+
<div id="dlgFields"></div>
|
|
159
|
+
<p class="err" id="dlgErr" hidden></p>
|
|
160
|
+
<div class="btns end"><button type="button" id="dlgCancel">Cancel</button><button type="submit" class="pri" id="dlgOk">OK</button></div>
|
|
161
|
+
</form>
|
|
162
|
+
</dialog>
|
|
163
|
+
|
|
164
|
+
<footer>
|
|
165
|
+
<button type="button" class="link" id="log" hidden>Open log</button>
|
|
166
|
+
<span></span>
|
|
167
|
+
<button type="button" id="end"></button>
|
|
168
|
+
</footer>
|
|
169
|
+
</div>
|
|
170
|
+
</main>
|
|
171
|
+
<script>
|
|
172
|
+
(function(){
|
|
173
|
+
"use strict";
|
|
174
|
+
// Host bridge. macOS injects window.ispyx before this script; on Windows the panel server answers these routes.
|
|
175
|
+
if(!window.ispyx){
|
|
176
|
+
var call=function(url,opt){return fetch(url,opt).then(function(r){
|
|
177
|
+
if(!r.ok)return r.json().catch(function(){return {}}).then(function(b){throw new Error(b.error||String(r.status))});
|
|
178
|
+
return r.json()})};
|
|
179
|
+
window.ispyx={
|
|
180
|
+
getState:function(){return call("api/state",{cache:"no-store"})},
|
|
181
|
+
action:function(name,value){return call("api/action",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:name,value:value})})}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
var host=window.ispyx;
|
|
185
|
+
var $=function(i){return document.getElementById(i)};
|
|
186
|
+
var busy={},last=null,timer=null,reachable=true;
|
|
187
|
+
|
|
188
|
+
function fmt(ms){var s=Math.floor(ms/1000),d=Math.floor(s/86400),h=Math.floor(s%86400/3600),m=Math.floor(s%3600/60);
|
|
189
|
+
return d?d+"d "+h+"h":h?h+"h "+m+"m":m?m+"m "+(s%60)+"s":s+"s"}
|
|
190
|
+
function el(tag,cls,text){var e=document.createElement(tag);if(cls)e.className=cls;if(text!=null)e.textContent=text;return e}
|
|
191
|
+
function show(id,on){$(id).hidden=!on}
|
|
192
|
+
function act(name,value){return Promise.resolve().then(function(){return host.action(name,value)})}
|
|
193
|
+
|
|
194
|
+
function copyText(text,btn){
|
|
195
|
+
var done=function(){btn.textContent="Copied";setTimeout(function(){btn.textContent="Copy"},1500)};
|
|
196
|
+
var viaHost=function(){return act("copy",text).then(done,function(){})};
|
|
197
|
+
try{
|
|
198
|
+
if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(text).then(done,viaHost);return}
|
|
199
|
+
}catch(e){}
|
|
200
|
+
viaHost();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function renderPerms(list){
|
|
204
|
+
var box=$("perms");box.textContent="";
|
|
205
|
+
list.forEach(function(p){
|
|
206
|
+
var item=el("div","item"),line=el("div","line");
|
|
207
|
+
line.appendChild(el("b",null,p.title||p.id));
|
|
208
|
+
line.appendChild(el("span","badge "+(p.granted?"ok":"no"),p.granted?"✓ Allowed":"Not allowed"));
|
|
209
|
+
item.appendChild(line);
|
|
210
|
+
if(p.detail)item.appendChild(el("div","d",p.detail));
|
|
211
|
+
if(!p.granted){
|
|
212
|
+
var b=el("div","btns"),allow=el("button","pri","Allow"),open=el("button",null,"Open Settings…");
|
|
213
|
+
allow.type=open.type="button";
|
|
214
|
+
allow.addEventListener("click",function(){act("requestPermission",p.id)});
|
|
215
|
+
open.addEventListener("click",function(){act("openPermissionSettings",p.id)});
|
|
216
|
+
b.appendChild(allow);b.appendChild(open);item.appendChild(b);
|
|
217
|
+
}
|
|
218
|
+
box.appendChild(item);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
var COLLAPSED=6,expanded=false;
|
|
223
|
+
// newest first; consecutive calls of the same tool become one row ("move x40")
|
|
224
|
+
function groupRecent(r){
|
|
225
|
+
var out=[];
|
|
226
|
+
r.forEach(function(x){
|
|
227
|
+
var g=out[out.length-1];
|
|
228
|
+
if(g&&g.tool===x.tool)g.n++;else out.push({tool:x.tool,time:x.time,summary:x.summary,n:1});
|
|
229
|
+
});
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
232
|
+
function renderRecent(r,total){
|
|
233
|
+
var ul=$("recent"),groups=groupRecent(r),shown=expanded?groups:groups.slice(0,COLLAPSED);
|
|
234
|
+
var oldTop=ul.scrollTop,oldH=ul.scrollHeight;
|
|
235
|
+
ul.textContent="";ul.className=expanded?"open":"";
|
|
236
|
+
if(!groups.length){ul.appendChild(el("li","empty","No activity yet."))}
|
|
237
|
+
shown.forEach(function(x){
|
|
238
|
+
var li=el("li"),m=el("div","m");
|
|
239
|
+
li.appendChild(el("span","t",x.time?new Date(x.time).toLocaleTimeString([], {hour:"2-digit",minute:"2-digit",second:"2-digit"}):""));
|
|
240
|
+
m.appendChild(el("span","tool",x.tool));
|
|
241
|
+
if(x.n>1)m.appendChild(el("span","n"," ×"+x.n));
|
|
242
|
+
if(x.summary&&x.n===1)m.appendChild(el("span","s",x.summary));
|
|
243
|
+
li.appendChild(m);ul.appendChild(li);
|
|
244
|
+
});
|
|
245
|
+
if(expanded&&oldTop>0)ul.scrollTop=oldTop+(ul.scrollHeight-oldH); // keep the row under the cursor still while new ones arrive on top
|
|
246
|
+
var more=groups.length>COLLAPSED;
|
|
247
|
+
show("more",groups.length>0);
|
|
248
|
+
$("showAll").hidden=!more&&!expanded;
|
|
249
|
+
$("showAll").textContent=expanded?"Show less":"Show all ("+r.length+")";
|
|
250
|
+
$("hAct").textContent=typeof total==="number"&&total>r.length?"Activity · "+total+" actions":"Activity";
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function setText(id,t){if($(id).textContent!==t)$(id).textContent=t}
|
|
254
|
+
|
|
255
|
+
function render(s){
|
|
256
|
+
last=s;reachable=true;fails=0;
|
|
257
|
+
var mac=s.platform==="darwin";
|
|
258
|
+
$("denyIn").placeholder=mac?"App name, e.g. KeePassXC":"program.exe";
|
|
259
|
+
var perms=Array.isArray(s.permissions)?s.permissions:null;
|
|
260
|
+
var missing=perms?perms.filter(function(p){return !p.granted}).length:0;
|
|
261
|
+
$("app").hidden=false;
|
|
262
|
+
|
|
263
|
+
setText("head",missing?"Permission needed":"Ispyx is running");
|
|
264
|
+
$("state").className="sub "+(missing?"warn":"run");
|
|
265
|
+
var n=s.clients;
|
|
266
|
+
setText("stateTxt",missing?"Allow the items below."
|
|
267
|
+
:typeof n==="number"?(n===0?"No AI client connected":n===1?"1 AI client connected":n+" AI clients connected"):"Ready");
|
|
268
|
+
var meta=["v"+s.version];
|
|
269
|
+
if(typeof s.uptimeMs==="number")meta.push("up "+fmt(s.uptimeMs));
|
|
270
|
+
if(s.pid!=null)meta.push("pid "+s.pid);
|
|
271
|
+
setText("meta",meta.join(" · "));
|
|
272
|
+
|
|
273
|
+
show("sPerm",!!(perms&&perms.length));
|
|
274
|
+
if(perms&&perms.length)renderPerms(perms);
|
|
275
|
+
|
|
276
|
+
var c=s.connect;
|
|
277
|
+
show("sConn",!!c);
|
|
278
|
+
if(c){
|
|
279
|
+
setText("cmd",c.claudeCodeCommand||"");setText("cfg",c.desktopConfigJson||"");
|
|
280
|
+
show("cfgBtns",!!c.desktopConfigPath);
|
|
281
|
+
if(c.desktopConfigPath)setText("cfgName",c.desktopConfigPath.split(/[\\/]/).pop());
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
var stop=s.stopLabel||"the stop key";
|
|
285
|
+
setText("usage","Press "+stop+" to stop the assistant."
|
|
286
|
+
+(mac?" Press twice to disconnect it.":"")
|
|
287
|
+
+" Ispyx lives in the "+(mac?"menu bar":"tray")+".");
|
|
288
|
+
|
|
289
|
+
var hasAuto=typeof s.autostart==="boolean",hasFx=typeof s.effectsEnabled==="boolean";
|
|
290
|
+
show("sGen",hasAuto||hasFx);show("rAuto",hasAuto);show("rFx",hasFx);
|
|
291
|
+
if(hasAuto){
|
|
292
|
+
setText("autoLbl",s.autostartLabel||(mac?"Open at login":"Open at Windows start"));
|
|
293
|
+
if(!busy.auto)$("auto").checked=s.autostart;
|
|
294
|
+
}
|
|
295
|
+
if(hasFx&&!busy.fx)$("fx").checked=s.effectsEnabled;
|
|
296
|
+
|
|
297
|
+
var hasClients=typeof s.clients==="number",hasRecent=Array.isArray(s.recent);
|
|
298
|
+
show("sAct",hasClients||hasRecent);show("rClients",hasClients);show("recent",hasRecent);
|
|
299
|
+
if(hasClients)setText("clients",String(s.clients));
|
|
300
|
+
show("more",false);
|
|
301
|
+
if(hasRecent)renderRecent(s.recent,s.recentTotal);
|
|
302
|
+
|
|
303
|
+
var hasPw=typeof s.passwordSet==="boolean",hasDeny=Array.isArray(s.denylist),hasAuth=typeof s.authRequired==="boolean";
|
|
304
|
+
show("sSafe",hasPw||hasDeny||hasAuth);show("rPw",hasPw);show("rAuth",hasAuth);show("rDeny",hasDeny);
|
|
305
|
+
if(hasPw){setText("pwTxt",s.passwordSet?"Password is set":"No password yet");setText("pwBtn",s.passwordSet?"Change…":"Set…")}
|
|
306
|
+
if(hasAuth&&!busy.authOn)$("authOn").checked=s.authRequired;
|
|
307
|
+
if(hasDeny){
|
|
308
|
+
var d=$("deny"),sig=s.denylist.join("\n")+"|"+!!s.passwordSet;
|
|
309
|
+
if(d.dataset.sig!==sig){ // rebuild only on change so a click is never lost to the 1 s refresh
|
|
310
|
+
d.dataset.sig=sig;d.textContent="";
|
|
311
|
+
if(!s.denylist.length)d.appendChild(el("span","v","none"));
|
|
312
|
+
s.denylist.forEach(function(x){
|
|
313
|
+
var t=el("span",null,x),b=el("button",null,"×");
|
|
314
|
+
b.type="button";b.title="Remove "+x;b.setAttribute("aria-label","Remove "+x);
|
|
315
|
+
b.addEventListener("click",function(){removeDeny(x)});
|
|
316
|
+
t.appendChild(b);d.appendChild(t);
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
show("log",!mac);
|
|
322
|
+
setText("end",mac?"Close":"Quit Ispyx");
|
|
323
|
+
$("end").className=mac?"pri":"danger";
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
var fails=0;
|
|
327
|
+
// Ispyx quit (from the tray, or from this window): say so and close the window; if the browser refuses, the note stays.
|
|
328
|
+
function showOff(){
|
|
329
|
+
clearInterval(timer);
|
|
330
|
+
$("app").hidden=true;$("off").hidden=false;$("state").hidden=true;$("meta").hidden=true;
|
|
331
|
+
var d=$("dlg");if(d.open)d.close();
|
|
332
|
+
setTimeout(function(){try{window.close()}catch(e){}},700);
|
|
333
|
+
}
|
|
334
|
+
function gone(){
|
|
335
|
+
if(!last)setText("stateTxt","Not reachable");
|
|
336
|
+
else{$("state").className="sub";setText("stateTxt","Not reachable")}
|
|
337
|
+
reachable=false;
|
|
338
|
+
if(++fails>=3&&!(last&&last.platform==="darwin"))showOff(); // three misses in a row: it is gone for good
|
|
339
|
+
}
|
|
340
|
+
function poll(){
|
|
341
|
+
Promise.resolve().then(function(){return host.getState()}).then(render,gone);
|
|
342
|
+
}
|
|
343
|
+
function toggle(id,key,name){
|
|
344
|
+
$(id).addEventListener("change",function(){
|
|
345
|
+
busy[key]=true;var v=$(id).checked;
|
|
346
|
+
act(name,v).catch(function(){$(id).checked=!v}).then(function(){busy[key]=false;poll()});
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
toggle("auto","auto","setAutostart");toggle("fx","fx","setEffects");
|
|
350
|
+
|
|
351
|
+
// Small modal for passwords. fn(values) returns a promise; a rejection shows its message and keeps the dialog open.
|
|
352
|
+
var dlg=$("dlg"),dlgRun=null;
|
|
353
|
+
function ask(title,fields,okText,fn){
|
|
354
|
+
$("dlgT").textContent=title;$("dlgOk").textContent=okText||"OK";
|
|
355
|
+
var box=$("dlgFields");box.textContent="";$("dlgErr").hidden=true;
|
|
356
|
+
fields.forEach(function(f){
|
|
357
|
+
var l=el("label",null,f.label),i=document.createElement("input");
|
|
358
|
+
i.type="password";i.id="f_"+f.id;i.autocomplete=f.id==="current"?"current-password":"new-password";
|
|
359
|
+
l.htmlFor=i.id;box.appendChild(l);box.appendChild(i);
|
|
360
|
+
});
|
|
361
|
+
dlgRun=function(){
|
|
362
|
+
var v={};fields.forEach(function(f){v[f.id]=$("f_"+f.id).value});
|
|
363
|
+
return fn(v);
|
|
364
|
+
};
|
|
365
|
+
if(typeof dlg.showModal==="function")dlg.showModal();else dlg.setAttribute("open","");
|
|
366
|
+
var first=box.querySelector("input");if(first)first.focus();
|
|
367
|
+
}
|
|
368
|
+
$("dlgForm").addEventListener("submit",function(e){
|
|
369
|
+
e.preventDefault();if(!dlgRun)return;
|
|
370
|
+
$("dlgOk").disabled=true;
|
|
371
|
+
Promise.resolve().then(dlgRun).then(function(){dlg.close();dlgRun=null;poll()},function(err){
|
|
372
|
+
$("dlgErr").textContent=err&&err.message?err.message:"Something went wrong.";$("dlgErr").hidden=false;
|
|
373
|
+
}).then(function(){$("dlgOk").disabled=false});
|
|
374
|
+
});
|
|
375
|
+
$("dlgCancel").addEventListener("click",function(){dlg.close();dlgRun=null;poll()});
|
|
376
|
+
dlg.addEventListener("cancel",function(){dlgRun=null});
|
|
377
|
+
|
|
378
|
+
function askNewPassword(current){
|
|
379
|
+
var fields=[];if(current)fields.push({id:"current",label:"Current password"});
|
|
380
|
+
fields.push({id:"next",label:"New password (6+ characters)"},{id:"again",label:"Repeat new password"});
|
|
381
|
+
return fields;
|
|
382
|
+
}
|
|
383
|
+
function newPasswordFn(v){
|
|
384
|
+
if(v.next!==v.again)return Promise.reject(new Error("The two passwords do not match."));
|
|
385
|
+
return act("setPassword",{current:v.current,next:v.next});
|
|
386
|
+
}
|
|
387
|
+
$("pwBtn").addEventListener("click",function(){
|
|
388
|
+
var has=last&&last.passwordSet;
|
|
389
|
+
ask(has?"Change password":"Set a password",askNewPassword(has),"Save",newPasswordFn);
|
|
390
|
+
});
|
|
391
|
+
$("authOn").addEventListener("change",function(){
|
|
392
|
+
var want=$("authOn").checked,box=$("authOn");
|
|
393
|
+
busy.authOn=true;
|
|
394
|
+
var done=function(){busy.authOn=false;poll()};
|
|
395
|
+
if(want&&!(last&&last.passwordSet)){ // a password must exist first; turn it on right after it is set
|
|
396
|
+
box.checked=false;
|
|
397
|
+
ask("Set a password",askNewPassword(false),"Save",function(v){return newPasswordFn(v).then(function(){return act("setAuthRequired",{on:true})})});
|
|
398
|
+
done();return;
|
|
399
|
+
}
|
|
400
|
+
if(!want&&last&&last.passwordSet){
|
|
401
|
+
box.checked=true;
|
|
402
|
+
ask("Turn off password",[{id:"current",label:"Password"}],"Turn off",function(v){return act("setAuthRequired",{on:false,current:v.current})});
|
|
403
|
+
done();return;
|
|
404
|
+
}
|
|
405
|
+
act("setAuthRequired",{on:want,current:undefined}).catch(function(){box.checked=!want}).then(done);
|
|
406
|
+
});
|
|
407
|
+
function removeDeny(name){
|
|
408
|
+
if(last&&last.passwordSet){
|
|
409
|
+
ask("Remove "+name,[{id:"current",label:"Password"}],"Remove",function(v){return act("removeDeny",{name:name,current:v.current})});
|
|
410
|
+
}else act("removeDeny",{name:name}).then(poll,function(){});
|
|
411
|
+
}
|
|
412
|
+
$("denyForm").addEventListener("submit",function(e){
|
|
413
|
+
e.preventDefault();var v=$("denyIn").value.trim();if(!v)return;
|
|
414
|
+
act("addDeny",v).then(function(){$("denyIn").value="";poll()},function(err){
|
|
415
|
+
$("denyIn").setCustomValidity(err.message);$("denyIn").reportValidity();setTimeout(function(){$("denyIn").setCustomValidity("")},2500);
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
$("cmdCopy").addEventListener("click",function(){copyText($("cmd").textContent,this)});
|
|
419
|
+
$("cfgCopy").addEventListener("click",function(){copyText($("cfg").textContent,this)});
|
|
420
|
+
$("cfgShow").addEventListener("click",function(){act("showConfigFile")});
|
|
421
|
+
$("log").addEventListener("click",function(){act("openLog")});
|
|
422
|
+
$("showAll").addEventListener("click",function(){expanded=!expanded;if(last&&last.recent)renderRecent(last.recent,last.recentTotal)});
|
|
423
|
+
$("clearAct").addEventListener("click",function(){act("clearRecent").then(poll)});
|
|
424
|
+
$("end").addEventListener("click",function(){
|
|
425
|
+
if(last&&last.platform==="darwin"){act("close");return}
|
|
426
|
+
if(!confirm("Quit Ispyx? AI tools lose access until it is reopened."))return;
|
|
427
|
+
act("quit").then(showOff,showOff); // either way the app is going away
|
|
428
|
+
});
|
|
429
|
+
poll();timer=setInterval(poll,1000);
|
|
430
|
+
document.addEventListener("visibilitychange",function(){if(!document.hidden)poll()});
|
|
431
|
+
})();
|
|
432
|
+
</script>
|
|
433
|
+
</body>
|
|
434
|
+
</html>
|