@linkshell/host 0.4.7 → 0.4.9
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/dist/host/src/computer-preview-capture.d.ts +11 -0
- package/dist/host/src/computer-preview-capture.js +87 -0
- package/dist/host/src/computer-preview-capture.js.map +1 -0
- package/dist/host/src/computer-preview.d.ts +73 -0
- package/dist/host/src/computer-preview.js +335 -0
- package/dist/host/src/computer-preview.js.map +1 -0
- package/dist/host/src/drivers/acp/connection.d.ts +2 -0
- package/dist/host/src/drivers/acp/connection.js +14 -1
- package/dist/host/src/drivers/acp/connection.js.map +1 -1
- package/dist/host/src/drivers/acp/driver.d.ts +4 -0
- package/dist/host/src/drivers/acp/driver.js +45 -1
- package/dist/host/src/drivers/acp/driver.js.map +1 -1
- package/dist/host/src/drivers/acp/mapper.js +11 -1
- package/dist/host/src/drivers/acp/mapper.js.map +1 -1
- package/dist/host/src/drivers/claude/activity.d.ts +1 -0
- package/dist/host/src/drivers/claude/activity.js +1 -0
- package/dist/host/src/drivers/claude/activity.js.map +1 -1
- package/dist/host/src/drivers/claude/driver.d.ts +6 -0
- package/dist/host/src/drivers/claude/driver.js +30 -2
- package/dist/host/src/drivers/claude/driver.js.map +1 -1
- package/dist/host/src/drivers/claude/tasks.d.ts +33 -0
- package/dist/host/src/drivers/claude/tasks.js +138 -0
- package/dist/host/src/drivers/claude/tasks.js.map +1 -0
- package/dist/host/src/drivers/claude/transcript.d.ts +31 -3
- package/dist/host/src/drivers/claude/transcript.js +20 -1
- package/dist/host/src/drivers/claude/transcript.js.map +1 -1
- package/dist/host/src/drivers/codex/app-server.js +0 -3
- package/dist/host/src/drivers/codex/app-server.js.map +1 -1
- package/dist/host/src/drivers/codex/commands.js +7 -1
- package/dist/host/src/drivers/codex/commands.js.map +1 -1
- package/dist/host/src/drivers/codex/computer-preview.d.ts +3 -0
- package/dist/host/src/drivers/codex/computer-preview.js +44 -0
- package/dist/host/src/drivers/codex/computer-preview.js.map +1 -0
- package/dist/host/src/drivers/codex/driver.d.ts +16 -1
- package/dist/host/src/drivers/codex/driver.js +262 -6
- package/dist/host/src/drivers/codex/driver.js.map +1 -1
- package/dist/host/src/drivers/codex/mapper.js +38 -3
- package/dist/host/src/drivers/codex/mapper.js.map +1 -1
- package/dist/host/src/drivers/registry.js +1 -1
- package/dist/host/src/drivers/registry.js.map +1 -1
- package/dist/host/src/drivers/types.d.ts +23 -1
- package/dist/host/src/hub.d.ts +18 -1
- package/dist/host/src/hub.js +102 -1
- package/dist/host/src/hub.js.map +1 -1
- package/dist/host/src/input.d.ts +9 -0
- package/dist/host/src/input.js +15 -0
- package/dist/host/src/input.js.map +1 -1
- package/dist/host/src/rpc/server.d.ts +1 -0
- package/dist/host/src/rpc/server.js +20 -2
- package/dist/host/src/rpc/server.js.map +1 -1
- package/dist/host/src/screen-viewer.js +641 -109
- package/dist/host/src/screen-viewer.js.map +1 -1
- package/dist/host/src/store.d.ts +10 -0
- package/dist/host/src/store.js +29 -0
- package/dist/host/src/store.js.map +1 -1
- package/dist/host/src/task-output.d.ts +6 -0
- package/dist/host/src/task-output.js +22 -0
- package/dist/host/src/task-output.js.map +1 -0
- package/dist/host/src/terminals.d.ts +37 -2
- package/dist/host/src/terminals.js +54 -14
- package/dist/host/src/terminals.js.map +1 -1
- package/dist/host/tsconfig.tsbuildinfo +1 -1
- package/dist/wire/src/computer-preview.d.ts +35 -0
- package/dist/wire/src/computer-preview.js +51 -0
- package/dist/wire/src/computer-preview.js.map +1 -0
- package/dist/wire/src/goal.d.ts +66 -0
- package/dist/wire/src/goal.js +19 -0
- package/dist/wire/src/goal.js.map +1 -0
- package/dist/wire/src/index.d.ts +2 -0
- package/dist/wire/src/index.js +2 -0
- package/dist/wire/src/index.js.map +1 -1
- package/dist/wire/src/model.d.ts +78 -0
- package/dist/wire/src/model.js +31 -0
- package/dist/wire/src/model.js.map +1 -1
- package/dist/wire/src/rpc.d.ts +1289 -60
- package/dist/wire/src/rpc.js +53 -3
- package/dist/wire/src/rpc.js.map +1 -1
- package/dist/wire/src/updates.d.ts +334 -22
- package/dist/wire/src/updates.js +11 -1
- package/dist/wire/src/updates.js.map +1 -1
- package/package.json +8 -5
- package/scripts/postinstall.mjs +23 -0
|
@@ -30,13 +30,16 @@ const ICONS = {
|
|
|
30
30
|
down: icon('<path d="m6 9.5 6 6 6-6"/>'),
|
|
31
31
|
quick: icon('<path d="M13 2.8 5.2 13.6H11l-1 7.6 7.8-10.8H12Z"/>'),
|
|
32
32
|
compose: icon('<path d="M4 5h16v10.5H9.5L5.5 19v-3.5H4Z"/><path d="M8 9h8M8 12h5"/>'),
|
|
33
|
+
info: icon('<circle cx="12" cy="12" r="8.5"/><path d="M12 11v6M12 7.2v.6"/>'),
|
|
33
34
|
close: icon('<path d="m6.5 6.5 11 11M17.5 6.5l-11 11"/>'),
|
|
34
35
|
};
|
|
35
36
|
const STYLE = String.raw `
|
|
36
37
|
* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; -webkit-touch-callout: none; -webkit-user-select: none; user-select: none; }
|
|
37
|
-
html, body { margin: 0; height: 100%; background: #000; overflow: hidden; overscroll-behavior: none; }
|
|
38
|
+
html, body { margin: 0; height: 100%; -webkit-text-size-adjust: 100%; text-size-adjust: 100%; background: #000; overflow: hidden; overscroll-behavior: none; }
|
|
38
39
|
body { position: fixed; inset: 0; color: #fff; font: 15px/1.45 -apple-system, system-ui, "PingFang SC", "Noto Sans CJK SC", sans-serif; }
|
|
39
40
|
#stage { position: absolute; inset: 0; overflow: hidden; touch-action: none; }
|
|
41
|
+
#picture { position: absolute; inset: 0; transform-origin: 0 0; will-change: transform; }
|
|
42
|
+
#stage.presenting #pointer { opacity: 0 !important; }
|
|
40
43
|
canvas, video { position: absolute; left: 0; top: 0; transform-origin: 0 0; will-change: transform; }
|
|
41
44
|
video { object-fit: fill; pointer-events: none; }
|
|
42
45
|
#pointer { position: absolute; left: -9px; top: -9px; width: 18px; height: 18px; border-radius: 50%; pointer-events: none;
|
|
@@ -50,51 +53,62 @@ const STYLE = String.raw `
|
|
|
50
53
|
color: rgba(255,255,255,0.72); text-align: center; pointer-events: none; }
|
|
51
54
|
.glass { background: rgba(38,38,42,0.74); -webkit-backdrop-filter: blur(22px) saturate(1.6); backdrop-filter: blur(22px) saturate(1.6);
|
|
52
55
|
border: 0.5px solid rgba(255,255,255,0.16); box-shadow: 0 8px 28px rgba(0,0,0,0.38); }
|
|
53
|
-
#
|
|
56
|
+
#control-shell { position: absolute; pointer-events: none; z-index: 1; will-change: width, height, transform, border-radius, opacity; }
|
|
57
|
+
#bar { position: absolute; z-index: 2; width: max-content; height: max-content; display: flex; gap: 2px; padding: 4px; border-radius: 26px; overflow: auto; scrollbar-width: none; touch-action: pan-x pan-y; }
|
|
54
58
|
#bar.side { flex-direction: column; }
|
|
55
|
-
#
|
|
56
|
-
|
|
57
|
-
.
|
|
59
|
+
#orb { position: absolute; width: 44px; height: 44px; display: flex; align-items: center; justify-content: center; opacity: 0; pointer-events: none; touch-action: none; z-index: 3; }
|
|
60
|
+
.orb-core { width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: #fff; }
|
|
61
|
+
.orb-core svg { width: 18px; height: 18px; }
|
|
62
|
+
.gone { display: none !important; }
|
|
63
|
+
.tool { flex: none; width: 44px; height: 44px; border-radius: 22px; display: flex; align-items: center; justify-content: center; color: rgba(255,255,255,0.92); }
|
|
58
64
|
.tool.on { background: #fff; color: #111; }
|
|
59
65
|
.tool:active { background: rgba(255,255,255,0.18); }
|
|
60
66
|
.tool.on:active { background: rgba(255,255,255,0.82); }
|
|
61
|
-
#menu { position: absolute; width: 248px; padding: 6px; border-radius: 20px; }
|
|
62
|
-
.
|
|
67
|
+
#menu, #connection { position: absolute; z-index: 6; width: 248px; padding: 6px; border-radius: 20px; overflow-y: auto; overscroll-behavior: contain; touch-action: pan-y; }
|
|
68
|
+
#menu.horizontal { display: flex; gap: 4px; width: 468px; }
|
|
69
|
+
#menu.horizontal .choice { flex: 1; min-width: 0; gap: 8px; padding: 9px 8px; }
|
|
70
|
+
#menu.horizontal .choice span { font-size: calc(11px * var(--text-scale, 1)); white-space: nowrap; }
|
|
71
|
+
.choice { display: flex; align-items: center; min-height: 52px; gap: 10px; padding: 8px 10px; border-radius: 12px; transition: background-color 160ms ease-out; }
|
|
72
|
+
.choice i { display: flex; flex: none; }
|
|
73
|
+
.choice i svg { width: 20px; height: 20px; }
|
|
74
|
+
#connectionhead { display: flex; align-items: center; padding-left: 10px; font-size: calc(13px * var(--text-scale, 1)); color: rgba(255,255,255,0.7); }
|
|
75
|
+
#connectionhead span { flex: 1; }
|
|
63
76
|
.choice:active { background: rgba(255,255,255,0.12); }
|
|
64
77
|
.choice.on { background: rgba(255,255,255,0.16); }
|
|
65
|
-
.choice b { display: block; font-weight: 600; font-size: 15px; }
|
|
66
|
-
.choice span { display: block; font-size: 12.5px; color: rgba(255,255,255,0.62); }
|
|
67
|
-
#status {
|
|
78
|
+
.choice b { display: block; font-weight: 600; font-size: calc(15px * var(--text-scale, 1)); }
|
|
79
|
+
.choice span { display: block; font-size: calc(12.5px * var(--text-scale, 1)); color: rgba(255,255,255,0.62); }
|
|
80
|
+
#status { padding: 2px 10px 10px; font-size: calc(12px * var(--text-scale, 1)); line-height: 1.5;
|
|
68
81
|
color: rgba(255,255,255,0.5); white-space: pre-line; font-variant-numeric: tabular-nums; }
|
|
69
82
|
#toast { position: absolute; left: 50%; max-width: min(86vw, 420px); width: max-content; transform: translateX(-50%); padding: 10px 16px; border-radius: 18px;
|
|
70
|
-
font-size: 13.5px; text-align: center; color: rgba(255,255,255,0.94); pointer-events: none; opacity: 0; transition: opacity 0.3s; }
|
|
83
|
+
font-size: calc(13.5px * var(--text-scale, 1)); text-align: center; color: rgba(255,255,255,0.94); pointer-events: none; opacity: 0; transition: opacity 0.3s; }
|
|
71
84
|
#toast.on { opacity: 1; }
|
|
72
|
-
#keys { position: absolute; left: 0; right: 0; height:
|
|
85
|
+
#keys { position: absolute; z-index: 4; left: 0; right: 0; height: 56px; display: flex; align-items: center; gap: 4px; padding: 0 5px; overflow-x: auto; overscroll-behavior: contain; touch-action: pan-x;
|
|
73
86
|
background: rgba(28,28,30,0.96); border-top: 0.5px solid rgba(255,255,255,0.14); }
|
|
74
|
-
.key { flex: 1
|
|
75
|
-
background: rgba(255,255,255,0.13); color: #fff; font-size: 15px; font-variant-numeric: tabular-nums; }
|
|
76
|
-
.key.word { font-size: 12.5px; }
|
|
87
|
+
.key { flex: 1 0 44px; min-width: 44px; height: calc(44px * var(--text-scale, 1)); border-radius: 8px; display: flex; align-items: center; justify-content: center;
|
|
88
|
+
background: rgba(255,255,255,0.13); color: #fff; font-size: calc(15px * var(--text-scale, 1)); font-variant-numeric: tabular-nums; }
|
|
89
|
+
.key.word { font-size: calc(12.5px * var(--text-scale, 1)); }
|
|
77
90
|
.key svg { width: 20px; height: 20px; }
|
|
78
91
|
.key:active { background: rgba(255,255,255,0.3); }
|
|
79
92
|
.key.on { background: #fff; color: #111; }
|
|
80
|
-
#sheet { position: absolute; display: flex; flex-direction: column; border-radius: 24px; overflow: hidden; }
|
|
93
|
+
#sheet { position: absolute; z-index: 4; display: flex; flex-direction: column; border-radius: 24px; overflow: hidden; }
|
|
81
94
|
#sheethead { display: flex; align-items: center; gap: 2px; padding: 8px 6px 4px 10px; }
|
|
82
|
-
#compose { flex: 1; min-width: 0; height:
|
|
83
|
-
background: rgba(255,255,255,0.1); color: rgba(255,255,255,0.62); font-size: 14.5px; white-space: nowrap; }
|
|
95
|
+
#compose { flex: 1; min-width: 0; min-height: 44px; margin-right: 4px; border-radius: 20px; display: flex; align-items: center; gap: 8px; padding: 0 14px;
|
|
96
|
+
background: rgba(255,255,255,0.1); color: rgba(255,255,255,0.62); font-size: calc(14.5px * var(--text-scale, 1)); white-space: nowrap; }
|
|
84
97
|
#compose:active { background: rgba(255,255,255,0.2); }
|
|
85
98
|
#compose svg { width: 19px; height: 19px; flex: none; }
|
|
86
99
|
#actions, #maker { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch; touch-action: pan-y; padding: 0 10px 12px; }
|
|
87
|
-
.group { display: flex; align-items: center; justify-content: space-between; padding: 12px 4px 7px; font-size: 12.5px; color: rgba(255,255,255,0.55); }
|
|
88
|
-
.group i { font-style: normal; padding: 4px 8px; margin: -4px -4px -4px 0; border-radius: 10px; color: rgba(255,255,255,0.86); }
|
|
100
|
+
.group { display: flex; align-items: center; justify-content: space-between; padding: 12px 4px 7px; font-size: calc(12.5px * var(--text-scale, 1)); color: rgba(255,255,255,0.55); }
|
|
101
|
+
.group i { display: inline-flex; align-items: center; min-width: 44px; min-height: 44px; font-style: normal; padding: 4px 8px; margin: -4px -4px -4px 0; border-radius: 10px; color: rgba(255,255,255,0.86); }
|
|
89
102
|
.group i:active { background: rgba(255,255,255,0.16); }
|
|
90
103
|
.tiles { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 6px; }
|
|
91
|
-
.tiles.six { grid-template-columns: repeat(
|
|
104
|
+
.tiles.six { grid-template-columns: repeat(auto-fit, minmax(calc(44px * var(--text-scale, 1)), 1fr)); margin-bottom: 6px; }
|
|
92
105
|
#sheet.narrow .tiles { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
93
|
-
#sheet.
|
|
106
|
+
#sheet.folded .tiles:not(.six) { grid-template-columns: repeat(auto-fit, minmax(64px, 1fr)); }
|
|
107
|
+
#sheet.narrow .tiles.six { grid-template-columns: repeat(auto-fit, minmax(calc(44px * var(--text-scale, 1)), 1fr)); }
|
|
94
108
|
.tile { position: relative; min-height: 52px; padding: 6px 3px; border-radius: 12px; display: flex; flex-direction: column; align-items: center; justify-content: center;
|
|
95
109
|
background: rgba(255,255,255,0.1); text-align: center; }
|
|
96
|
-
.tile b { max-width: 100%; overflow: hidden;
|
|
97
|
-
.tile span { font-size: 11.5px; line-height: 1.3; color: rgba(255,255,255,0.55); font-variant-numeric: tabular-nums; }
|
|
110
|
+
.tile b { max-width: 100%; overflow: hidden; white-space: normal; overflow-wrap: anywhere; font-weight: 500; font-size: calc(13.5px * var(--text-scale, 1)); line-height: 1.3; }
|
|
111
|
+
.tile span { font-size: calc(11.5px * var(--text-scale, 1)); line-height: 1.3; color: rgba(255,255,255,0.55); font-variant-numeric: tabular-nums; }
|
|
98
112
|
.tiles.six .tile { min-height: 44px; }
|
|
99
113
|
.tile:active, .tile.hit { background: rgba(255,255,255,0.3); }
|
|
100
114
|
.tile.on { background: #fff; color: #111; }
|
|
@@ -103,38 +117,151 @@ const STYLE = String.raw `
|
|
|
103
117
|
.tile.sure span { color: rgba(255,255,255,0.86); }
|
|
104
118
|
.tile.add { background: transparent; border: 1px dashed rgba(255,255,255,0.3); color: rgba(255,255,255,0.78); }
|
|
105
119
|
.tile.loose::after { content: "×"; position: absolute; top: -5px; right: -4px; width: 19px; height: 19px; border-radius: 10px; background: #d9392f;
|
|
106
|
-
font-size: 14px; line-height: 18px; text-align: center; }
|
|
120
|
+
font-size: calc(14px * var(--text-scale, 1)); line-height: 18px; text-align: center; }
|
|
107
121
|
.field { display: flex; align-items: center; gap: 12px; height: 46px; margin-top: 6px; padding: 0 12px; border-radius: 12px; background: rgba(255,255,255,0.1); }
|
|
108
|
-
.field span { flex: none; font-size: 14px; color: rgba(255,255,255,0.62); }
|
|
122
|
+
.field span { flex: none; font-size: calc(14px * var(--text-scale, 1)); color: rgba(255,255,255,0.62); }
|
|
109
123
|
.field select, .field input { flex: 1; min-width: 0; height: 100%; padding: 0; border: 0; outline: 0; border-radius: 0; background: transparent; color: #fff;
|
|
110
|
-
font: inherit; font-size: 16px; text-align: right; text-align-last: right; -webkit-appearance: none; appearance: none; -webkit-user-select: text; user-select: text; }
|
|
124
|
+
font: inherit; font-size: calc(16px * var(--text-scale, 1)); text-align: right; text-align-last: right; -webkit-appearance: none; appearance: none; -webkit-user-select: text; user-select: text; }
|
|
111
125
|
.field input::placeholder { color: rgba(255,255,255,0.36); }
|
|
112
126
|
.ends { display: flex; justify-content: flex-end; gap: 8px; margin-top: 12px; }
|
|
113
|
-
.soft { flex: none; height:
|
|
114
|
-
font-size: 14px; white-space: nowrap; }
|
|
127
|
+
.soft { flex: none; min-height: 44px; padding: 0 13px; border-radius: 19px; display: flex; align-items: center; justify-content: center; background: rgba(255,255,255,0.13);
|
|
128
|
+
font-size: calc(14px * var(--text-scale, 1)); white-space: nowrap; }
|
|
115
129
|
.soft:active { background: rgba(255,255,255,0.3); }
|
|
116
130
|
.soft.strong { padding: 0 17px; background: #fff; color: #111; font-weight: 600; }
|
|
117
131
|
.soft.strong:active { background: rgba(255,255,255,0.82); }
|
|
118
132
|
.soft svg { width: 20px; height: 20px; }
|
|
119
133
|
.spring { flex: 1; }
|
|
120
|
-
#composer { position: absolute; left: 0; right: 0; display: flex; flex-direction: column; gap: 8px; background: rgba(28,28,30,0.96); border-top: 0.5px solid rgba(255,255,255,0.14); }
|
|
134
|
+
#composer { position: absolute; z-index: 4; left: 0; right: 0; overflow-y: auto; overscroll-behavior: contain; display: flex; flex-direction: column; gap: 8px; background: rgba(28,28,30,0.96); border-top: 0.5px solid rgba(255,255,255,0.14); }
|
|
121
135
|
#composer.wide { flex-direction: row; align-items: flex-end; }
|
|
122
136
|
#wordsbox { position: relative; flex: none; }
|
|
123
137
|
#composer.wide #wordsbox { flex: 1; min-width: 0; }
|
|
124
138
|
#words { display: block; width: 100%; height: 66px; margin: 0; padding: 9px 12px; border: 0; outline: 0; resize: none; border-radius: 12px; background: rgba(255,255,255,0.12);
|
|
125
|
-
color: #fff; font: inherit; font-size: 16px; line-height: 1.4; -webkit-user-select: text; user-select: text; }
|
|
139
|
+
color: #fff; font: inherit; font-size: calc(16px * var(--text-scale, 1)); line-height: 1.4; -webkit-user-select: text; user-select: text; }
|
|
126
140
|
#composer.wide #words { height: 44px; }
|
|
127
|
-
#wordshint { position: absolute; left: 12px; right: 12px; top: 9px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-size: 16px; line-height: 1.4;
|
|
141
|
+
#wordshint { position: absolute; left: 12px; right: 12px; top: 9px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-size: calc(16px * var(--text-scale, 1)); line-height: 1.4;
|
|
128
142
|
color: rgba(255,255,255,0.36); pointer-events: none; }
|
|
129
|
-
#sendrow { display: flex; align-items: center; gap: 6px; }
|
|
143
|
+
#sendrow { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
|
|
130
144
|
#typing, #shortcut { position: absolute; left: 0; top: 0; width: 2px; height: 2px; padding: 0; border: 0; outline: 0; resize: none; opacity: 0;
|
|
131
|
-
background: transparent; color: transparent; caret-color: transparent; font-size: 16px; -webkit-user-select: text; user-select: text; }
|
|
145
|
+
background: transparent; color: transparent; caret-color: transparent; font-size: calc(16px * var(--text-scale, 1)); -webkit-user-select: text; user-select: text; }
|
|
132
146
|
`;
|
|
133
147
|
/**
|
|
134
148
|
* The parts of the page's script that only work things out, with nothing of the page in them:
|
|
135
149
|
* exported so that they can be tried without a browser.
|
|
136
150
|
*/
|
|
137
151
|
export const VIEWER_LOGIC = String.raw `
|
|
152
|
+
/** All controls share the same usable rectangle, including asymmetric system chrome. */
|
|
153
|
+
function usableViewport(v, inset) {
|
|
154
|
+
const left = Math.max(0, inset.left || 0), right = Math.max(0, inset.right || 0);
|
|
155
|
+
const top = Math.max(0, inset.top || 0), bottom = Math.max(0, inset.bottom || 0);
|
|
156
|
+
return { x: v.x + left, y: v.y + top, w: Math.max(1, v.w - left - right), h: Math.max(1, v.h - top - bottom) };
|
|
157
|
+
}
|
|
158
|
+
function containedBox(safe, width, height, x, y, gap = 8) {
|
|
159
|
+
const w = Math.min(width, Math.max(1, safe.w - gap * 2)), h = Math.min(height, Math.max(1, safe.h - gap * 2));
|
|
160
|
+
return { x: Math.max(safe.x + gap, Math.min(x, safe.x + safe.w - gap - w)), y: Math.max(safe.y + gap, Math.min(y, safe.y + safe.h - gap - h)), w, h };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Try each edge of the toolbar, preferring the black outside the fitted picture. */
|
|
164
|
+
function anchoredPanel(safe, picture, anchor, width, height) {
|
|
165
|
+
const overlap = (a, b) => Math.max(0, Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x)) * Math.max(0, Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y));
|
|
166
|
+
const candidates = [
|
|
167
|
+
[anchor.x - width - 10, anchor.y + (anchor.h - height) / 2],
|
|
168
|
+
[anchor.x + anchor.w + 10, anchor.y + (anchor.h - height) / 2],
|
|
169
|
+
[anchor.x + (anchor.w - width) / 2, anchor.y - height - 10],
|
|
170
|
+
[anchor.x + (anchor.w - width) / 2, anchor.y + anchor.h + 10],
|
|
171
|
+
].map(([x, y]) => containedBox(safe, width, height, x, y));
|
|
172
|
+
const score = (box) => overlap(box, picture) + overlap(box, anchor) * 100;
|
|
173
|
+
return candidates.reduce((best, box) => score(box) < score(best) ? box : best);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** FLIP keeps the live media at its visible position while its destination changes. */
|
|
177
|
+
function presentationFlip(from, to, reduceMotion) {
|
|
178
|
+
if (reduceMotion || !from || !to || ![from.x, from.y, from.w, from.h, to.x, to.y, to.w, to.h].every(Number.isFinite) || Math.min(from.w, from.h, to.w, to.h) <= 0) return null;
|
|
179
|
+
const sx = from.w / to.w, sy = from.h / to.h;
|
|
180
|
+
return { x: from.x - to.x * sx, y: from.y - to.y * sy, sx, sy };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Exact damped-spring integration keeps momentum when the destination changes mid-flight. */
|
|
184
|
+
function presentationSpring(current, velocity, target, seconds, frequency = 32) {
|
|
185
|
+
const damping = 0.8;
|
|
186
|
+
const decay = frequency * damping, oscillation = frequency * Math.sqrt(1 - damping * damping);
|
|
187
|
+
const dt = Math.max(0, Math.min(seconds, 0.064)), envelope = Math.exp(-decay * dt);
|
|
188
|
+
const cosine = Math.cos(oscillation * dt), sine = Math.sin(oscillation * dt);
|
|
189
|
+
const next = {}, speed = {};
|
|
190
|
+
let settled = true;
|
|
191
|
+
for (const key of ["x", "y", "w", "h"]) {
|
|
192
|
+
const displacement = current[key] - target[key], v = velocity[key];
|
|
193
|
+
next[key] = target[key] + envelope * (displacement * cosine + (v + decay * displacement) / oscillation * sine);
|
|
194
|
+
speed[key] = envelope * (v * cosine - (decay * v + frequency * frequency * displacement) / oscillation * sine);
|
|
195
|
+
if (Math.abs(next[key] - target[key]) > 0.05 || Math.abs(speed[key]) > 0.4) settled = false;
|
|
196
|
+
}
|
|
197
|
+
return { current: next, velocity: speed, settled };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** The same panel enters from its available edge in both narrow and wide layouts. */
|
|
201
|
+
function sheetPresentation(progress, width, height, dock) {
|
|
202
|
+
const remaining = 1 - progress;
|
|
203
|
+
const x = dock === "left" ? -(width + 24) * remaining : dock === "right" ? (width + 24) * remaining : 0;
|
|
204
|
+
const y = dock === "bottom" ? (height + 24) * remaining : dock === "top" ? -(height + 24) * remaining : 0;
|
|
205
|
+
return { x, y, opacity: Math.max(0, Math.min(1, progress)) };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function popoverPresentation(progress, dx, dy) {
|
|
209
|
+
return { x: dx * (1 - progress), y: dy * (1 - progress), scale: 0.94 + 0.06 * progress, opacity: Math.max(0, Math.min(1, progress)) };
|
|
210
|
+
}
|
|
211
|
+
function controlCanCollapse(state) {
|
|
212
|
+
return !!state.fullscreen && !state.menu && !state.sheet && !state.keyboard && !state.pressed && !state.presenting;
|
|
213
|
+
}
|
|
214
|
+
function orbBounds(safe, anchor, point) {
|
|
215
|
+
const x = point ? safe.x + 8 + Math.max(0, Math.min(1, point.u)) * Math.max(0, safe.w - 60) : anchor.x + anchor.w / 2 - 22;
|
|
216
|
+
const y = point ? safe.y + 8 + Math.max(0, Math.min(1, point.v)) * Math.max(0, safe.h - 60) : anchor.y + anchor.h / 2 - 22;
|
|
217
|
+
return containedBox(safe, 44, 44, x, y);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** One glass surface changes shape; its contents can fade without swapping backgrounds. */
|
|
221
|
+
function controlsMorph(progress, anchor, ball) {
|
|
222
|
+
const p = Math.max(0, Math.min(1, progress)), phase = Math.min(1, p / 0.3);
|
|
223
|
+
const x = ball.x + 6 + (anchor.x - ball.x - 6) * progress;
|
|
224
|
+
const y = ball.y + 6 + (anchor.y - ball.y - 6) * progress;
|
|
225
|
+
const w = Math.max(1, 32 + (anchor.w - 32) * progress), h = Math.max(1, 32 + (anchor.h - 32) * progress);
|
|
226
|
+
return { x, y, w, h, radius: Math.min(w, h) / 2, shellOpacity: 0.3 + 0.7 * phase * phase * (3 - 2 * phase),
|
|
227
|
+
dx: x + w / 2 - anchor.x - anchor.w / 2, dy: y + h / 2 - anchor.y - anchor.h / 2,
|
|
228
|
+
contentScale: Math.max(0.01, Math.min(w / Math.max(1, anchor.w), h / Math.max(1, anchor.h))),
|
|
229
|
+
barOpacity: p * p, orbOpacity: 1 - p };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Keep controls in one usable pane while the live desktop remains continuous across a fold. */
|
|
233
|
+
function interactionViewport(viewport, insets, divisions, preferred) {
|
|
234
|
+
const safe = usableViewport(viewport, insets);
|
|
235
|
+
let regions = [safe];
|
|
236
|
+
for (const reported of Array.isArray(divisions) ? divisions : []) {
|
|
237
|
+
if (!reported.active || ![reported.x, reported.y, reported.width, reported.height].every(Number.isFinite)) continue;
|
|
238
|
+
if (reported.width < 0 || reported.height < 0) continue;
|
|
239
|
+
const division = { ...reported, x: viewport.x + reported.x, y: viewport.y + reported.y };
|
|
240
|
+
const vertical = division.height > division.width;
|
|
241
|
+
const next = [];
|
|
242
|
+
for (const region of regions) {
|
|
243
|
+
const right = region.x + region.w, bottom = region.y + region.h;
|
|
244
|
+
if (division.x >= right || division.y >= bottom || division.x + division.width <= region.x || division.y + division.height <= region.y) { next.push(region); continue; }
|
|
245
|
+
if (vertical) {
|
|
246
|
+
if (division.x > region.x) next.push({ x: region.x, y: region.y, w: division.x - region.x, h: region.h });
|
|
247
|
+
if (division.x + division.width < right) next.push({ x: division.x + division.width, y: region.y, w: right - division.x - division.width, h: region.h });
|
|
248
|
+
} else {
|
|
249
|
+
if (division.y > region.y) next.push({ x: region.x, y: region.y, w: region.w, h: division.y - region.y });
|
|
250
|
+
if (division.y + division.height < bottom) next.push({ x: region.x, y: division.y + division.height, w: region.w, h: bottom - division.y - division.height });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (next.length) regions = next;
|
|
254
|
+
}
|
|
255
|
+
const usable = regions.filter(region => region.w >= Math.min(180, safe.w) && region.h >= Math.min(80, safe.h));
|
|
256
|
+
const candidates = usable.length ? usable : regions;
|
|
257
|
+
if (preferred && Number.isFinite(preferred.x) && Number.isFinite(preferred.y)) {
|
|
258
|
+
const distance = region => Math.hypot(Math.max(region.x - preferred.x, 0, preferred.x - region.x - region.w), Math.max(region.y - preferred.y, 0, preferred.y - region.y - region.h));
|
|
259
|
+
return candidates.reduce((best, region) => distance(region) <= distance(best) ? region : best);
|
|
260
|
+
}
|
|
261
|
+
// The outer right edge and the lower tabletop pane are the natural default reach areas.
|
|
262
|
+
return candidates[candidates.length - 1] || safe;
|
|
263
|
+
}
|
|
264
|
+
|
|
138
265
|
/** Undoes the Gray code the time on a clock strip is written in. */
|
|
139
266
|
function fromGray(gray) {
|
|
140
267
|
let n = gray;
|
|
@@ -282,8 +409,8 @@ function cleanShortcuts(list) {
|
|
|
282
409
|
`;
|
|
283
410
|
const BODY = String.raw `
|
|
284
411
|
const $ = (id) => document.getElementById(id);
|
|
285
|
-
const stage = $("stage"), canvas = $("screen"), ctx = canvas.getContext("2d"), video = $("video"), pointer = $("pointer");
|
|
286
|
-
const note = $("note"), bar = $("bar"), menu = $("menu"), toast = $("toast"), keys = $("keys"), typing = $("typing"), shortcut = $("shortcut");
|
|
412
|
+
const stage = $("stage"), picture = $("picture"), canvas = $("screen"), ctx = canvas.getContext("2d"), video = $("video"), pointer = $("pointer");
|
|
413
|
+
const note = $("note"), bar = $("bar"), controlShell = $("control-shell"), orb = $("orb"), menu = $("menu"), connection = $("connection"), toast = $("toast"), keys = $("keys"), typing = $("typing"), shortcut = $("shortcut");
|
|
287
414
|
const sheet = $("sheet"), actions = $("actions"), maker = $("maker"), naming = $("makername"), composer = $("composer"), words = $("words");
|
|
288
415
|
const query = new URLSearchParams(location.search);
|
|
289
416
|
const app = window.ReactNativeWebView;
|
|
@@ -295,7 +422,7 @@ const android = /Android/i.test(navigator.userAgent);
|
|
|
295
422
|
const MODES = ["view", "trackpad", "touch"];
|
|
296
423
|
const HINTS = {
|
|
297
424
|
view: "只看:双指缩放,单指移动画面",
|
|
298
|
-
trackpad: "
|
|
425
|
+
trackpad: "触控板:滑动指针 · 轻点点击 · 双指轻点右键 · 双指滑动滚动 · 轻点两下并按住拖拽",
|
|
299
426
|
touch: "点按:点哪里就点哪里 · 长按右键 · 双指滑动滚动 · 轻点两下并按住拖拽",
|
|
300
427
|
};
|
|
301
428
|
let stored = null;
|
|
@@ -658,54 +785,407 @@ function viewport() {
|
|
|
658
785
|
// What a browser's window has been seen to do: a keyboard coming up makes it shorter, on a phone (see keyboardIsUp).
|
|
659
786
|
const seen = { w: 0, h: 0, follows: false };
|
|
660
787
|
|
|
788
|
+
const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)");
|
|
789
|
+
let sheetTransition = null, sheetDock = "bottom";
|
|
790
|
+
const presentation = { active: false, target: null, geometry: "", current: null, velocity: { x: 0, y: 0, w: 0, h: 0 }, destination: null, waiting: false, frame: 0, time: null, timer: 0 };
|
|
791
|
+
function visiblePicture() {
|
|
792
|
+
if (!content.w) return null;
|
|
793
|
+
const rect = (live ? video : canvas).getBoundingClientRect();
|
|
794
|
+
return { x: rect.left, y: rect.top, w: rect.width, h: rect.height };
|
|
795
|
+
}
|
|
796
|
+
function finishPresentation() {
|
|
797
|
+
clearTimeout(presentation.timer);
|
|
798
|
+
cancelAnimationFrame(presentation.frame);
|
|
799
|
+
picture.style.transform = "";
|
|
800
|
+
sheetTransition = null;
|
|
801
|
+
sheet.style.transform = sheet.style.opacity = sheet.style.pointerEvents = "";
|
|
802
|
+
sheet.classList.toggle("gone", !sheetOpen);
|
|
803
|
+
presentation.frame = 0;
|
|
804
|
+
presentation.current = null;
|
|
805
|
+
presentation.time = null;
|
|
806
|
+
presentation.active = false;
|
|
807
|
+
stage.classList.remove("presenting");
|
|
808
|
+
armControlsIdle();
|
|
809
|
+
}
|
|
810
|
+
function drawPresentation() {
|
|
811
|
+
if (sheetTransition) {
|
|
812
|
+
const panel = sheetPresentation(sheetTransition.progress, sheet.offsetWidth, sheet.offsetHeight, sheetDock);
|
|
813
|
+
sheet.style.transform = "translate(" + panel.x + "px," + panel.y + "px)";
|
|
814
|
+
sheet.style.opacity = String(panel.opacity);
|
|
815
|
+
sheet.style.pointerEvents = sheetTransition.target ? "auto" : "none";
|
|
816
|
+
}
|
|
817
|
+
if (!content.w) return;
|
|
818
|
+
const flip = presentationFlip(presentation.current, shown, reducedMotion.matches);
|
|
819
|
+
if (!flip) return finishPresentation();
|
|
820
|
+
picture.style.transform = "translate(" + flip.x + "px," + flip.y + "px) scale(" + flip.sx + "," + flip.sy + ")";
|
|
821
|
+
}
|
|
822
|
+
function stepPresentation(time) {
|
|
823
|
+
presentation.frame = 0;
|
|
824
|
+
if (!presentation.active) return;
|
|
825
|
+
if (presentation.time === null) presentation.time = time;
|
|
826
|
+
const seconds = (time - presentation.time) / 1000;
|
|
827
|
+
const next = presentationSpring(presentation.current, presentation.velocity, presentation.destination, seconds);
|
|
828
|
+
let sheetSettled = true;
|
|
829
|
+
if (sheetTransition) {
|
|
830
|
+
const panel = presentationSpring({ x: sheetTransition.progress, y: 0, w: 1, h: 1 }, { x: sheetTransition.velocity, y: 0, w: 0, h: 0 }, { x: sheetTransition.target, y: 0, w: 1, h: 1 }, seconds);
|
|
831
|
+
sheetTransition.progress = panel.current.x;
|
|
832
|
+
sheetTransition.velocity = panel.velocity.x;
|
|
833
|
+
sheetSettled = Math.abs(panel.current.x - sheetTransition.target) < 0.001 && Math.abs(panel.velocity.x) < 0.01;
|
|
834
|
+
}
|
|
835
|
+
presentation.time = time;
|
|
836
|
+
presentation.current = next.current;
|
|
837
|
+
presentation.velocity = next.velocity;
|
|
838
|
+
drawPresentation();
|
|
839
|
+
if (!presentation.active) return;
|
|
840
|
+
if (next.settled && sheetSettled && !presentation.waiting) return finishPresentation();
|
|
841
|
+
presentation.frame = requestAnimationFrame(stepPresentation);
|
|
842
|
+
}
|
|
843
|
+
function beginPresentation(full) {
|
|
844
|
+
if ((presentation.active && presentation.target === "fullscreen:" + full) || (!presentation.active && full === (app ? chrome.fullscreen : !!document.fullscreenElement))) return;
|
|
845
|
+
beginLayoutTransition("fullscreen:" + full);
|
|
846
|
+
}
|
|
847
|
+
function beginLayoutTransition(target) {
|
|
848
|
+
if (reducedMotion.matches) return finishPresentation();
|
|
849
|
+
// A held mouse button must be released before the displayed coordinate system moves.
|
|
850
|
+
wake();
|
|
851
|
+
closeMenu();
|
|
852
|
+
endOne();
|
|
853
|
+
if (mouseHeld && mouseHeld !== "pan") act({ t: "up", b: mouseHeld });
|
|
854
|
+
mouseHeld = null;
|
|
855
|
+
gesture = null;
|
|
856
|
+
touches.clear();
|
|
857
|
+
lastTap = null;
|
|
858
|
+
if (!presentation.active) {
|
|
859
|
+
presentation.current = visiblePicture() || { ...shown };
|
|
860
|
+
presentation.velocity = { x: 0, y: 0, w: 0, h: 0 };
|
|
861
|
+
presentation.destination = { ...shown };
|
|
862
|
+
presentation.time = null;
|
|
863
|
+
}
|
|
864
|
+
presentation.active = true;
|
|
865
|
+
presentation.target = target;
|
|
866
|
+
presentation.waiting = !sheetTransition;
|
|
867
|
+
presentation.geometry = JSON.stringify(shown);
|
|
868
|
+
stage.classList.add("presenting");
|
|
869
|
+
if (!presentation.frame) presentation.frame = requestAnimationFrame(stepPresentation);
|
|
870
|
+
clearTimeout(presentation.timer);
|
|
871
|
+
presentation.timer = setTimeout(finishPresentation, 350);
|
|
872
|
+
}
|
|
873
|
+
function beginSheetTransition(open) {
|
|
874
|
+
if (!reducedMotion.matches) {
|
|
875
|
+
sheetTransition = sheetTransition ? { ...sheetTransition, target: open ? 1 : 0 } : { progress: open ? 0 : 1, velocity: 0, target: open ? 1 : 0 };
|
|
876
|
+
}
|
|
877
|
+
beginLayoutTransition("shortcuts:" + open);
|
|
878
|
+
}
|
|
879
|
+
function animatePresentation() {
|
|
880
|
+
if (!presentation.active) return;
|
|
881
|
+
const geometry = JSON.stringify(shown);
|
|
882
|
+
if (geometry !== presentation.geometry) {
|
|
883
|
+
presentation.geometry = geometry;
|
|
884
|
+
presentation.destination = { ...shown };
|
|
885
|
+
presentation.waiting = false;
|
|
886
|
+
clearTimeout(presentation.timer);
|
|
887
|
+
presentation.timer = setTimeout(finishPresentation, 800);
|
|
888
|
+
}
|
|
889
|
+
// Layout may change while the status bar moves. Rebase the same live spring without resetting it.
|
|
890
|
+
drawPresentation();
|
|
891
|
+
if (presentation.active && !presentation.frame) presentation.frame = requestAnimationFrame(stepPresentation);
|
|
892
|
+
}
|
|
893
|
+
reducedMotion.addEventListener("change", () => { if (reducedMotion.matches) finishPresentation(); });
|
|
894
|
+
window.linkshellPresent = beginPresentation;
|
|
895
|
+
document.addEventListener("visibilitychange", () => { if (document.hidden) finishPresentation(); });
|
|
896
|
+
|
|
897
|
+
// Scalar springs drive controls without touching the live video or remote pointer coordinates.
|
|
898
|
+
const controlSprings = new Map(), popoverMotions = new Map(), pressedControls = new Set();
|
|
899
|
+
let controlFrame = 0;
|
|
900
|
+
const controlsVisibility = {};
|
|
901
|
+
const controls = { value: 1, target: 1, visibility: 1, visibilityTarget: 1, timer: 0, point: null, preferred: null, safe: null, anchor: null, orb: null, drag: null };
|
|
902
|
+
function advanceControls(time) {
|
|
903
|
+
controlFrame = 0;
|
|
904
|
+
for (const [key, motion] of [...controlSprings]) {
|
|
905
|
+
if (motion.time === null) motion.time = time;
|
|
906
|
+
// A changing glass silhouette needs more time to read than a button press.
|
|
907
|
+
const frequency = key === controls ? 17 : key === menu || key === connection ? 22 : 32;
|
|
908
|
+
const next = presentationSpring({ x: motion.value, y: 0, w: 1, h: 1 }, { x: motion.velocity, y: 0, w: 0, h: 0 }, { x: motion.target, y: 0, w: 1, h: 1 }, (time - motion.time) / 1000, frequency);
|
|
909
|
+
motion.time = time;
|
|
910
|
+
motion.value = next.current.x;
|
|
911
|
+
motion.velocity = next.velocity.x;
|
|
912
|
+
motion.render(motion.value);
|
|
913
|
+
if (Math.abs(motion.value - motion.target) < 0.001 && Math.abs(motion.velocity) < 0.01) {
|
|
914
|
+
controlSprings.delete(key);
|
|
915
|
+
motion.render(motion.target);
|
|
916
|
+
motion.done?.(motion.target);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
if (controlSprings.size) controlFrame = requestAnimationFrame(advanceControls);
|
|
920
|
+
}
|
|
921
|
+
function controlSpring(key, target, initial, render, done) {
|
|
922
|
+
const motion = controlSprings.get(key) || { value: initial, velocity: 0, time: null };
|
|
923
|
+
Object.assign(motion, { target, render, done });
|
|
924
|
+
if (reducedMotion.matches) {
|
|
925
|
+
controlSprings.delete(key);
|
|
926
|
+
render(target);
|
|
927
|
+
done?.(target);
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
controlSprings.set(key, motion);
|
|
931
|
+
render(motion.value);
|
|
932
|
+
if (!controlFrame) controlFrame = requestAnimationFrame(advanceControls);
|
|
933
|
+
}
|
|
934
|
+
function pressFeedback(element, down) {
|
|
935
|
+
if (down) {
|
|
936
|
+
pressedControls.add(element);
|
|
937
|
+
const v = viewport(), rect = element.getBoundingClientRect();
|
|
938
|
+
controls.preferred = { u: (rect.left + rect.width / 2 - v.x) / Math.max(1, v.w), v: (rect.top + rect.height / 2 - v.y) / Math.max(1, v.h) };
|
|
939
|
+
} else pressedControls.delete(element);
|
|
940
|
+
const pending = controlSprings.get(element);
|
|
941
|
+
// Even a tap released before the first animation frame gets a visible press, without delaying its action.
|
|
942
|
+
if (!down && !reducedMotion.matches && pending?.target === 0.92 && pending.value > 0.97) pending.velocity = Math.min(pending.velocity, -6);
|
|
943
|
+
controlSpring(element, down ? 0.92 : 1, 1, (scale) => { element.style.scale = reducedMotion.matches || scale === 1 ? "" : String(scale); });
|
|
944
|
+
if (down) clearTimeout(controls.timer); else armControlsIdle();
|
|
945
|
+
}
|
|
946
|
+
function popoverOpen(panel) {
|
|
947
|
+
return popoverMotions.has(panel) ? popoverMotions.get(panel).target === 1 : !panel.classList.contains("gone");
|
|
948
|
+
}
|
|
949
|
+
function drawPopover(panel) {
|
|
950
|
+
const motion = popoverMotions.get(panel);
|
|
951
|
+
if (!motion) return;
|
|
952
|
+
const trigger = $(panel === menu ? "mode" : "info").getBoundingClientRect();
|
|
953
|
+
const x = parseFloat(panel.style.left) || 0, y = parseFloat(panel.style.top) || 0;
|
|
954
|
+
const width = panel.offsetWidth, height = panel.offsetHeight;
|
|
955
|
+
const ax = trigger.left + trigger.width / 2, ay = trigger.top + trigger.height / 2;
|
|
956
|
+
const dx = ax - x - width / 2, dy = ay - y - height / 2, distance = Math.max(1, Math.hypot(dx, dy));
|
|
957
|
+
const frame = popoverPresentation(motion.value, dx / distance * 10, dy / distance * 10);
|
|
958
|
+
panel.style.transformOrigin = clamp(ax - x, 0, width) + "px " + clamp(ay - y, 0, height) + "px";
|
|
959
|
+
panel.style.transform = "translate(" + frame.x + "px," + frame.y + "px) scale(" + frame.scale + ")";
|
|
960
|
+
panel.style.opacity = String(frame.opacity);
|
|
961
|
+
}
|
|
962
|
+
function presentPopover(panel, open) {
|
|
963
|
+
if (!open && panel.classList.contains("gone")) return;
|
|
964
|
+
const existing = popoverMotions.get(panel);
|
|
965
|
+
const motion = existing || { value: open ? 0 : 1, target: open ? 1 : 0 };
|
|
966
|
+
motion.target = open ? 1 : 0;
|
|
967
|
+
popoverMotions.set(panel, motion);
|
|
968
|
+
panel.style.opacity = String(Math.max(0, Math.min(1, motion.value)));
|
|
969
|
+
panel.style.pointerEvents = open ? "auto" : "none";
|
|
970
|
+
panel.classList.remove("gone");
|
|
971
|
+
// Measure at its final location before revealing it.
|
|
972
|
+
layout();
|
|
973
|
+
controlSpring(panel, motion.target, motion.value, (value) => { motion.value = value; drawPopover(panel); }, (target) => {
|
|
974
|
+
if (target === 0) panel.classList.add("gone");
|
|
975
|
+
panel.style.transform = panel.style.transformOrigin = panel.style.opacity = panel.style.pointerEvents = "";
|
|
976
|
+
popoverMotions.delete(panel);
|
|
977
|
+
armControlsIdle();
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
function immersiveControls() { return app ? chrome.fullscreen : !!document.fullscreenElement; }
|
|
981
|
+
function controlsMayCollapse() {
|
|
982
|
+
if (document.hidden) return false;
|
|
983
|
+
return controlCanCollapse({ fullscreen: immersiveControls(), menu: !menu.classList.contains("gone") || !connection.classList.contains("gone"), sheet: sheetOpen || !!sheetTransition || composerOpen, keyboard: keysOpen || chrome.keyboard === true, pressed: pressedControls.size > 0 || !!controls.drag, presenting: presentation.active });
|
|
984
|
+
}
|
|
985
|
+
function renderControls(value) {
|
|
986
|
+
controls.value = value;
|
|
987
|
+
if (!controls.safe || !controls.anchor || !controls.orb) return;
|
|
988
|
+
const visible = Math.max(0, Math.min(1, value)), anchor = controls.anchor, ball = controls.orb;
|
|
989
|
+
const groupOpacity = Math.max(0, Math.min(1, controls.visibility));
|
|
990
|
+
const hiddenByPanel = keysOpen || sheetOpen || composerOpen;
|
|
991
|
+
const morph = controlsMorph(value, anchor, ball);
|
|
992
|
+
controlShell.style.left = morph.x + "px";
|
|
993
|
+
controlShell.style.top = morph.y + "px";
|
|
994
|
+
controlShell.style.width = morph.w + "px";
|
|
995
|
+
controlShell.style.height = morph.h + "px";
|
|
996
|
+
controlShell.style.borderRadius = morph.radius + "px";
|
|
997
|
+
controlShell.style.opacity = String(Math.max(morph.shellOpacity, controls.drag ? 0.7 : 0) * groupOpacity);
|
|
998
|
+
bar.style.transformOrigin = "50% 50%";
|
|
999
|
+
bar.style.transform = "translate(" + morph.dx + "px," + morph.dy + "px) scale(" + morph.contentScale + ")";
|
|
1000
|
+
bar.style.opacity = String(morph.barOpacity * groupOpacity);
|
|
1001
|
+
bar.style.pointerEvents = visible > 0.6 && !hiddenByPanel ? "auto" : "none";
|
|
1002
|
+
orb.style.left = ball.x + "px";
|
|
1003
|
+
orb.style.top = ball.y + "px";
|
|
1004
|
+
orb.style.opacity = String(morph.orbOpacity * (controls.drag ? 0.7 : 0.3) * groupOpacity);
|
|
1005
|
+
orb.style.transform = "scale(" + (1 - 0.2 * visible) + ")";
|
|
1006
|
+
orb.style.pointerEvents = immersiveControls() && visible < 0.5 && !hiddenByPanel ? "auto" : "none";
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function expandControls(open) {
|
|
1010
|
+
const target = open ? 1 : 0;
|
|
1011
|
+
if (controls.target === target && (controlSprings.has(controls) || controls.value === target)) return;
|
|
1012
|
+
controls.target = target;
|
|
1013
|
+
controlSpring(controls, target, controls.value, renderControls);
|
|
1014
|
+
}
|
|
1015
|
+
function armControlsIdle() {
|
|
1016
|
+
clearTimeout(controls.timer);
|
|
1017
|
+
if (!controlsMayCollapse() || controls.target === 0) return;
|
|
1018
|
+
controls.timer = setTimeout(() => { if (controlsMayCollapse()) expandControls(false); }, 3000);
|
|
1019
|
+
}
|
|
1020
|
+
function layoutControls(safe, anchor) {
|
|
1021
|
+
if (controls.drag && controls.safe && ["x", "y", "w", "h"].some((key) => controls.safe[key] !== safe[key])) {
|
|
1022
|
+
const id = controls.drag.id;
|
|
1023
|
+
controls.drag = null;
|
|
1024
|
+
pressFeedback(orb, false);
|
|
1025
|
+
try { orb.releasePointerCapture(id); } catch {}
|
|
1026
|
+
}
|
|
1027
|
+
controls.safe = safe;
|
|
1028
|
+
const fullscreen = immersiveControls();
|
|
1029
|
+
const v = viewport();
|
|
1030
|
+
const ball = fullscreen && controls.point && controls.preferred
|
|
1031
|
+
? containedBox(safe, 44, 44, v.x + controls.preferred.u * v.w - 22, v.y + controls.preferred.v * v.h - 22)
|
|
1032
|
+
: orbBounds(safe, anchor, fullscreen ? controls.point : null);
|
|
1033
|
+
if (fullscreen && controls.point) anchor = containedBox(safe, anchor.w, anchor.h, ball.x + 22 - anchor.w / 2, ball.y + 22 - anchor.h / 2);
|
|
1034
|
+
controls.anchor = anchor;
|
|
1035
|
+
controls.orb = ball;
|
|
1036
|
+
const visibility = keysOpen || sheetOpen || composerOpen ? 0 : 1;
|
|
1037
|
+
if (controls.visibilityTarget !== visibility) {
|
|
1038
|
+
controls.visibilityTarget = visibility;
|
|
1039
|
+
controlSpring(controlsVisibility, visibility, controls.visibility, (value) => { controls.visibility = value; renderControls(controls.value); });
|
|
1040
|
+
}
|
|
1041
|
+
if (!fullscreen || keysOpen || sheetOpen || composerOpen || !menu.classList.contains("gone") || !connection.classList.contains("gone")) expandControls(true);
|
|
1042
|
+
renderControls(controls.value);
|
|
1043
|
+
return anchor;
|
|
1044
|
+
}
|
|
1045
|
+
reducedMotion.addEventListener("change", () => {
|
|
1046
|
+
if (!reducedMotion.matches) return;
|
|
1047
|
+
for (const [key, motion] of [...controlSprings]) {
|
|
1048
|
+
controlSprings.delete(key);
|
|
1049
|
+
motion.render(motion.target);
|
|
1050
|
+
motion.done?.(motion.target);
|
|
1051
|
+
}
|
|
1052
|
+
cancelAnimationFrame(controlFrame);
|
|
1053
|
+
controlFrame = 0;
|
|
1054
|
+
});
|
|
1055
|
+
orb.addEventListener("pointerdown", (event) => {
|
|
1056
|
+
event.preventDefault(); event.stopPropagation();
|
|
1057
|
+
if (!controls.orb) return;
|
|
1058
|
+
try { orb.setPointerCapture(event.pointerId); } catch {}
|
|
1059
|
+
clearTimeout(controls.timer);
|
|
1060
|
+
controls.drag = { id: event.pointerId, x: event.clientX, y: event.clientY, origin: { ...controls.orb }, moved: false };
|
|
1061
|
+
pressFeedback(orb, true);
|
|
1062
|
+
renderControls(controls.value);
|
|
1063
|
+
});
|
|
1064
|
+
orb.addEventListener("pointermove", (event) => {
|
|
1065
|
+
event.preventDefault(); event.stopPropagation();
|
|
1066
|
+
const drag = controls.drag;
|
|
1067
|
+
if (!drag || drag.id !== event.pointerId || !controls.safe) return;
|
|
1068
|
+
const dx = event.clientX - drag.x, dy = event.clientY - drag.y;
|
|
1069
|
+
if (Math.hypot(dx, dy) > 6) drag.moved = true;
|
|
1070
|
+
if (!drag.moved) return;
|
|
1071
|
+
const v = viewport();
|
|
1072
|
+
const preferred = { x: drag.origin.x + dx + 22, y: drag.origin.y + dy + 22 };
|
|
1073
|
+
const safe = interactionViewport(v, chrome.insets || { top: 0, right: 0, bottom: 0, left: 0 }, chrome.divisions, preferred);
|
|
1074
|
+
const point = containedBox(safe, 44, 44, preferred.x - 22, preferred.y - 22);
|
|
1075
|
+
controls.point = { u: (point.x - safe.x - 8) / Math.max(1, safe.w - 60), v: (point.y - safe.y - 8) / Math.max(1, safe.h - 60) };
|
|
1076
|
+
controls.preferred = { u: (point.x + 22 - v.x) / Math.max(1, v.w), v: (point.y + 22 - v.y) / Math.max(1, v.h) };
|
|
1077
|
+
controls.safe = safe;
|
|
1078
|
+
controls.orb = point;
|
|
1079
|
+
renderControls(controls.value);
|
|
1080
|
+
});
|
|
1081
|
+
function releaseOrb(event, cancelled) {
|
|
1082
|
+
event.preventDefault(); event.stopPropagation();
|
|
1083
|
+
const drag = controls.drag;
|
|
1084
|
+
if (!drag || drag.id !== event.pointerId) return;
|
|
1085
|
+
controls.drag = null;
|
|
1086
|
+
try { orb.releasePointerCapture(event.pointerId); } catch {}
|
|
1087
|
+
pressFeedback(orb, false);
|
|
1088
|
+
layout();
|
|
1089
|
+
if (!cancelled && !drag.moved) wake();
|
|
1090
|
+
else renderControls(controls.value);
|
|
1091
|
+
}
|
|
1092
|
+
orb.addEventListener("pointerup", (event) => releaseOrb(event, false));
|
|
1093
|
+
orb.addEventListener("pointercancel", (event) => releaseOrb(event, true));
|
|
1094
|
+
orb.addEventListener("lostpointercapture", (event) => releaseOrb(event, true));
|
|
1095
|
+
document.addEventListener("visibilitychange", () => {
|
|
1096
|
+
if (!document.hidden) {
|
|
1097
|
+
layout();
|
|
1098
|
+
armControlsIdle();
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
const captured = controls.drag?.id;
|
|
1102
|
+
controls.drag = null;
|
|
1103
|
+
if (captured !== undefined) { try { orb.releasePointerCapture(captured); } catch {} }
|
|
1104
|
+
for (const element of [...pressedControls]) pressFeedback(element, false);
|
|
1105
|
+
clearTimeout(controls.timer);
|
|
1106
|
+
});
|
|
1107
|
+
for (const name of ["touchstart", "touchend", "mousedown", "click"]) orb.addEventListener(name, (event) => { event.preventDefault(); event.stopPropagation(); }, { passive: false });
|
|
1108
|
+
|
|
661
1109
|
function layout() {
|
|
662
|
-
const v = viewport(), inset = chrome.insets;
|
|
1110
|
+
const v = viewport(), inset = chrome.insets, screenSafe = usableViewport(v, inset);
|
|
1111
|
+
const preferred = controls.preferred ? { x: v.x + controls.preferred.u * v.w, y: v.y + controls.preferred.v * v.h } : null;
|
|
1112
|
+
const safe = interactionViewport(v, inset, chrome.divisions, preferred);
|
|
1113
|
+
const divided = safe.x !== screenSafe.x || safe.y !== screenSafe.y || safe.w !== screenSafe.w || safe.h !== screenSafe.h;
|
|
1114
|
+
const horizontalDivision = divided && safe.h < screenSafe.h;
|
|
1115
|
+
const controlInset = { top: safe.y - v.y, right: v.x + v.w - safe.x - safe.w, bottom: v.y + v.h - safe.y - safe.h, left: safe.x - v.x };
|
|
1116
|
+
// The app keeps its authored type sizes while window geometry remains adaptive.
|
|
1117
|
+
const fontScale = 1;
|
|
1118
|
+
document.documentElement.style.setProperty("--text-scale", String(fontScale));
|
|
663
1119
|
if (v.w !== seen.w) Object.assign(seen, { w: v.w, h: v.h });
|
|
664
1120
|
else if (v.h > seen.h) seen.h = v.h;
|
|
665
1121
|
if (seen.h - v.h > 120 && fieldFocused()) seen.follows = true;
|
|
666
1122
|
// A phone lying down has its room beside the picture, not under it.
|
|
667
|
-
const wide =
|
|
1123
|
+
const wide = !horizontalDivision && (divided || safe.w > safe.h && safe.w >= 560);
|
|
668
1124
|
// Beside the picture, things take the side without the camera.
|
|
669
|
-
const onLeft = chrome.clear ? chrome.clear === "left" :
|
|
1125
|
+
const onLeft = chrome.clear ? chrome.clear === "left" : controlInset.left < controlInset.right;
|
|
670
1126
|
// Under the picture: the box text is written in, or the key bar (above the keyboard, or above the phone's
|
|
671
1127
|
// bottom edge when the keys are a real keyboard's), or the shortcuts. Each has the picture make room for it.
|
|
672
|
-
let under = 0, aside = 0;
|
|
1128
|
+
let under = 0, aside = 0, over = 0;
|
|
673
1129
|
composer.classList.toggle("gone", !composerOpen);
|
|
674
|
-
|
|
1130
|
+
const composerWide = wide && safe.w >= 560;
|
|
1131
|
+
composer.classList.toggle("wide", composerWide);
|
|
675
1132
|
keys.classList.toggle("gone", !keysOpen || composerOpen);
|
|
676
|
-
sheet.classList.toggle("gone", !sheetOpen);
|
|
1133
|
+
sheet.classList.toggle("gone", !sheetOpen && !sheetTransition);
|
|
677
1134
|
sheet.classList.toggle("narrow", wide);
|
|
1135
|
+
sheet.classList.toggle("folded", divided);
|
|
678
1136
|
if (composerOpen) {
|
|
679
|
-
composer.style.
|
|
680
|
-
|
|
681
|
-
composer.style.
|
|
1137
|
+
composer.style.maxHeight = safe.h + "px";
|
|
1138
|
+
composer.style.left = divided ? safe.x + "px" : "0px";
|
|
1139
|
+
composer.style.right = divided ? "auto" : "0px";
|
|
1140
|
+
composer.style.width = divided ? safe.w + "px" : "";
|
|
1141
|
+
composer.style.padding = divided ? "8px" : "8px " + (8 + inset.right) + "px " + (8 + inset.bottom) + "px " + (8 + inset.left) + "px";
|
|
1142
|
+
words.style.height = "";
|
|
1143
|
+
words.style.height = clamp(words.scrollHeight, (composerWide ? 44 : 66) * fontScale, (composerWide ? 66 : 132) * fontScale) + "px";
|
|
1144
|
+
const height = composer.offsetHeight, top = divided ? safe.y + safe.h - height : v.y + v.h - height;
|
|
1145
|
+
composer.style.top = top + "px";
|
|
1146
|
+
if (horizontalDivision && safe.y + safe.h / 2 < v.y + v.h / 2) over = top + height - v.y + 8;
|
|
1147
|
+
else under = v.y + v.h - top;
|
|
682
1148
|
} else if (keysOpen) {
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
keys.style.
|
|
686
|
-
|
|
687
|
-
keys.style.
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
1149
|
+
const height = divided ? Math.min(56, safe.h) : 44 * fontScale + 12 + inset.bottom;
|
|
1150
|
+
const top = divided ? safe.y + safe.h - height : v.y + v.h - height;
|
|
1151
|
+
keys.style.left = divided ? safe.x + "px" : "0px";
|
|
1152
|
+
keys.style.right = divided ? "auto" : "0px";
|
|
1153
|
+
keys.style.width = divided ? safe.w + "px" : "";
|
|
1154
|
+
keys.style.top = top + "px";
|
|
1155
|
+
keys.style.height = height + "px";
|
|
1156
|
+
keys.style.padding = divided ? "0 5px" : "0 " + (5 + inset.right) + "px " + inset.bottom + "px " + (5 + inset.left) + "px";
|
|
1157
|
+
if (horizontalDivision && safe.y + safe.h / 2 < v.y + v.h / 2) over = top + height - v.y + 8;
|
|
1158
|
+
else under = v.y + v.h - top;
|
|
1159
|
+
} else if ((sheetOpen || sheetTransition) && wide) {
|
|
1160
|
+
sheetDock = onLeft ? "left" : "right";
|
|
1161
|
+
const edge = chrome.clear ? 12 : Math.max(12, onLeft ? controlInset.left : controlInset.right), top = Math.max(controlInset.top, 12);
|
|
1162
|
+
const width = Math.min(320, Math.max(180, Math.round(safe.w * 0.42)), Math.max(1, safe.w - 24));
|
|
692
1163
|
sheet.style.width = width + "px";
|
|
693
|
-
sheet.style.height = Math.max(
|
|
1164
|
+
sheet.style.height = Math.max(1, v.h - top - Math.max(controlInset.bottom, 12)) + "px";
|
|
694
1165
|
sheet.style.left = (onLeft ? v.x + edge : v.x + v.w - edge - width) + "px";
|
|
695
1166
|
sheet.style.top = v.y + top + "px";
|
|
696
|
-
aside = edge + width + 8;
|
|
697
|
-
} else if (sheetOpen) {
|
|
698
|
-
const
|
|
699
|
-
|
|
1167
|
+
aside = sheetOpen ? edge + width + 8 : 0;
|
|
1168
|
+
} else if (sheetOpen || sheetTransition) {
|
|
1169
|
+
const upper = horizontalDivision && safe.y + safe.h / 2 < v.y + v.h / 2;
|
|
1170
|
+
sheetDock = upper ? "top" : "bottom";
|
|
1171
|
+
const edge = divided ? 8 : Math.max(inset.bottom, 8), side = divided ? 8 : Math.max(inset.left, inset.right, 8);
|
|
1172
|
+
const most = Math.max(1, divided ? safe.h - 16 : v.h - inset.top - edge - 8);
|
|
700
1173
|
const height = document.activeElement === naming ? most : Math.min(most, clamp(Math.round(v.h * 0.5), 260, 400));
|
|
701
|
-
|
|
1174
|
+
const top = divided ? upper ? safe.y + 8 : safe.y + safe.h - edge - height : v.y + v.h - edge - height;
|
|
1175
|
+
sheet.style.width = Math.max(1, (divided ? safe.w : v.w) - side * 2) + "px";
|
|
702
1176
|
sheet.style.height = height + "px";
|
|
703
|
-
sheet.style.left = v.x + side + "px";
|
|
704
|
-
sheet.style.top =
|
|
705
|
-
|
|
1177
|
+
sheet.style.left = (divided ? safe.x + side : v.x + side) + "px";
|
|
1178
|
+
sheet.style.top = top + "px";
|
|
1179
|
+
if (sheetOpen) {
|
|
1180
|
+
if (upper) over = top + height - v.y + 8;
|
|
1181
|
+
else under = v.y + v.h - top + 8;
|
|
1182
|
+
}
|
|
706
1183
|
}
|
|
707
|
-
|
|
708
|
-
|
|
1184
|
+
// Immersive video uses the entire surface; controls still stay inside the system's safe rectangle.
|
|
1185
|
+
const pictureInsets = chrome.fullscreen || document.fullscreenElement ? { top: 0, right: 0, bottom: 0, left: 0 } : inset;
|
|
1186
|
+
const left = Math.max(pictureInsets.left, onLeft ? aside : 0), right = Math.max(pictureInsets.right, onLeft ? 0 : aside);
|
|
1187
|
+
const top = Math.max(pictureInsets.top, over);
|
|
1188
|
+
area = { x: v.x + left, y: v.y + top, w: Math.max(1, v.w - left - right), h: Math.max(1, v.h - top - Math.max(under, pictureInsets.bottom)) };
|
|
709
1189
|
if (content.w) {
|
|
710
1190
|
fit = Math.min(area.w / content.w, area.h / content.h);
|
|
711
1191
|
const w = content.w * fit * zoom, h = content.h * fit * zoom;
|
|
@@ -723,8 +1203,13 @@ function layout() {
|
|
|
723
1203
|
}
|
|
724
1204
|
// The toolbar stands in the black beside the picture when there is some, under it otherwise; whatever opens takes its place.
|
|
725
1205
|
const beside = content.w ? area.w - content.w * fit > area.h - content.h * fit + inset.bottom : v.w > v.h;
|
|
1206
|
+
if (bar.classList.contains("side") !== beside) {
|
|
1207
|
+
bar.scrollLeft = 0;
|
|
1208
|
+
bar.scrollTop = 0;
|
|
1209
|
+
}
|
|
726
1210
|
bar.classList.toggle("side", beside);
|
|
727
|
-
bar.
|
|
1211
|
+
bar.style.maxWidth = Math.max(1, safe.w - 16) + "px";
|
|
1212
|
+
bar.style.maxHeight = Math.max(1, safe.h - 16) + "px";
|
|
728
1213
|
const size = { w: bar.offsetWidth, h: bar.offsetHeight };
|
|
729
1214
|
// Beside the picture it stands in the middle of the black there.
|
|
730
1215
|
if (beside) {
|
|
@@ -734,16 +1219,28 @@ function layout() {
|
|
|
734
1219
|
bar.style.left = (onLeft ? v.x + gap : v.x + v.w - gap - size.w) + "px";
|
|
735
1220
|
bar.style.top = area.y + (area.h - size.h) / 2 + "px";
|
|
736
1221
|
} else {
|
|
737
|
-
bar.style.left =
|
|
1222
|
+
bar.style.left = (safe.w >= 600 ? safe.x + safe.w - size.w - 16 : safe.x + (safe.w - size.w) / 2) + "px";
|
|
738
1223
|
bar.style.top = v.y + v.h - Math.max(inset.bottom, 8) - 8 - size.h + "px";
|
|
739
1224
|
}
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
1225
|
+
const boundedBar = layoutControls(safe, containedBox(safe, size.w, size.h, parseFloat(bar.style.left), parseFloat(bar.style.top)));
|
|
1226
|
+
bar.style.left = boundedBar.x + "px";
|
|
1227
|
+
bar.style.top = boundedBar.y + "px";
|
|
1228
|
+
const horizontalMenu = fontScale <= 1.15 && !beside && safe.w >= 468 + size.w + 32;
|
|
1229
|
+
menu.classList.toggle("horizontal", horizontalMenu);
|
|
1230
|
+
for (const panel of [menu, connection]) {
|
|
1231
|
+
if (panel.classList.contains("gone")) continue;
|
|
1232
|
+
panel.style.width = Math.min(panel === menu && horizontalMenu ? 468 : 232 * fontScale, Math.max(1, safe.w - 16)) + "px";
|
|
1233
|
+
panel.style.maxHeight = Math.max(1, safe.h - 16) + "px";
|
|
1234
|
+
const placed = anchoredPanel(safe, content.w ? shown : { x: 0, y: 0, w: 0, h: 0 }, boundedBar, panel.offsetWidth, panel.offsetHeight);
|
|
1235
|
+
panel.style.left = placed.x + "px";
|
|
1236
|
+
panel.style.top = placed.y + "px";
|
|
1237
|
+
drawPopover(panel);
|
|
744
1238
|
}
|
|
1239
|
+
toast.style.left = safe.x + safe.w / 2 + "px";
|
|
1240
|
+
toast.style.maxWidth = Math.max(1, Math.min(420, safe.w - 24)) + "px";
|
|
745
1241
|
toast.style.top = area.y + 14 + "px";
|
|
746
1242
|
place();
|
|
1243
|
+
animatePresentation();
|
|
747
1244
|
}
|
|
748
1245
|
|
|
749
1246
|
function place() {
|
|
@@ -753,11 +1250,18 @@ function place() {
|
|
|
753
1250
|
pointer.style.transform = "translate(" + (shown.x + cursor.x * shown.w) + "px," + (shown.y + cursor.y * shown.h) + "px)" + size;
|
|
754
1251
|
}
|
|
755
1252
|
|
|
756
|
-
|
|
1253
|
+
let pendingLayout = 0;
|
|
1254
|
+
function requestLayout() {
|
|
1255
|
+
if (pendingLayout) return;
|
|
1256
|
+
pendingLayout = requestAnimationFrame(() => { pendingLayout = 0; layout(); });
|
|
1257
|
+
}
|
|
1258
|
+
addEventListener("resize", requestLayout);
|
|
757
1259
|
if (window.visualViewport) {
|
|
758
|
-
visualViewport.addEventListener("resize",
|
|
759
|
-
visualViewport.addEventListener("scroll",
|
|
1260
|
+
visualViewport.addEventListener("resize", requestLayout);
|
|
1261
|
+
visualViewport.addEventListener("scroll", requestLayout);
|
|
760
1262
|
}
|
|
1263
|
+
// A resized native WebView can change its frame before the window reports a new viewport.
|
|
1264
|
+
if (typeof ResizeObserver !== "undefined") new ResizeObserver(requestLayout).observe(stage);
|
|
761
1265
|
|
|
762
1266
|
function zoomTo(next, at) {
|
|
763
1267
|
// The point of the picture under the fingers stays under them.
|
|
@@ -920,7 +1424,8 @@ function endOne() {
|
|
|
920
1424
|
}
|
|
921
1425
|
|
|
922
1426
|
stage.addEventListener("pointerdown", (event) => {
|
|
923
|
-
|
|
1427
|
+
if (presentation.active) { event.preventDefault(); return; }
|
|
1428
|
+
if (!immersiveControls() || controls.target > 0) wake();
|
|
924
1429
|
// A touch that closes the menu, or the shortcuts, does nothing else.
|
|
925
1430
|
const closing = [closeMenu(), closeSheet()].some(Boolean);
|
|
926
1431
|
if (event.pointerType === "mouse") return closing ? undefined : mouseDown(event);
|
|
@@ -952,6 +1457,7 @@ stage.addEventListener("pointerdown", (event) => {
|
|
|
952
1457
|
});
|
|
953
1458
|
|
|
954
1459
|
stage.addEventListener("pointermove", (event) => {
|
|
1460
|
+
if (presentation.active) return;
|
|
955
1461
|
if (event.pointerType === "mouse") return mouseMove(event);
|
|
956
1462
|
const touch = touches.get(event.pointerId);
|
|
957
1463
|
if (!touch || !gesture) return;
|
|
@@ -1083,6 +1589,7 @@ function mouseUp(event) {
|
|
|
1083
1589
|
}
|
|
1084
1590
|
stage.addEventListener("wheel", (event) => {
|
|
1085
1591
|
event.preventDefault();
|
|
1592
|
+
if (presentation.active) return;
|
|
1086
1593
|
if (controlling() && !event.ctrlKey) return act({ t: "scroll", dx: -event.deltaX, dy: -event.deltaY });
|
|
1087
1594
|
zoomTo(zoom * Math.exp(-event.deltaY / 300), { x: event.clientX, y: event.clientY });
|
|
1088
1595
|
}, { passive: false });
|
|
@@ -1248,6 +1755,7 @@ function dropKeys() {
|
|
|
1248
1755
|
mods.clear();
|
|
1249
1756
|
drawKeys();
|
|
1250
1757
|
refresh();
|
|
1758
|
+
armControlsIdle();
|
|
1251
1759
|
}
|
|
1252
1760
|
|
|
1253
1761
|
function raiseKeys() {
|
|
@@ -1284,22 +1792,26 @@ document.addEventListener("visibilitychange", () => { if (!document.hidden) sett
|
|
|
1284
1792
|
|
|
1285
1793
|
/** A control that works on touch-up and never takes the focus (the keyboard stays as it is). */
|
|
1286
1794
|
function tappable(element, action) {
|
|
1287
|
-
element.addEventListener("pointerdown", (event) => { event.preventDefault(); event.stopPropagation(); wake(); });
|
|
1795
|
+
element.addEventListener("pointerdown", (event) => { event.preventDefault(); event.stopPropagation(); wake(); pressFeedback(element, true); });
|
|
1796
|
+
element.addEventListener("pointercancel", () => pressFeedback(element, false));
|
|
1797
|
+
element.addEventListener("pointerleave", () => pressFeedback(element, false));
|
|
1288
1798
|
element.addEventListener("touchstart", (event) => event.preventDefault(), { passive: false });
|
|
1289
1799
|
element.addEventListener("mousedown", (event) => event.preventDefault());
|
|
1290
1800
|
element.addEventListener("pointerup", (event) => {
|
|
1291
1801
|
event.preventDefault();
|
|
1292
1802
|
event.stopPropagation();
|
|
1293
|
-
|
|
1294
|
-
|
|
1803
|
+
pressFeedback(element, false);
|
|
1804
|
+
const box = element.getBoundingClientRect(), cx = box.left + box.width / 2, cy = box.top + box.height / 2;
|
|
1805
|
+
if (Math.abs(event.clientX - cx) <= element.offsetWidth / 2 && Math.abs(event.clientY - cy) <= element.offsetHeight / 2) action();
|
|
1295
1806
|
});
|
|
1296
1807
|
}
|
|
1297
1808
|
|
|
1298
1809
|
/** The same inside something that scrolls: the touch is left to the browser, and counts if it stayed where it landed. */
|
|
1299
1810
|
function pressable(element, action) {
|
|
1300
1811
|
let from = null;
|
|
1301
|
-
element.addEventListener("pointerdown", (event) => { event.stopPropagation(); wake(); from = { id: event.pointerId, x: event.clientX, y: event.clientY }; });
|
|
1302
|
-
element.addEventListener("pointercancel", () => { from = null; });
|
|
1812
|
+
element.addEventListener("pointerdown", (event) => { event.stopPropagation(); wake(); pressFeedback(element, true); from = { id: event.pointerId, x: event.clientX, y: event.clientY }; });
|
|
1813
|
+
element.addEventListener("pointercancel", () => { from = null; pressFeedback(element, false); });
|
|
1814
|
+
element.addEventListener("pointerleave", () => pressFeedback(element, false));
|
|
1303
1815
|
// The mouse's events a browser makes up after a touch would land on whatever the action has left under the
|
|
1304
1816
|
// finger, and take the focus from the field it gave it to. A touch that scrolled has none to stop.
|
|
1305
1817
|
element.addEventListener("touchend", (event) => { if (event.cancelable) event.preventDefault(); });
|
|
@@ -1307,15 +1819,15 @@ function pressable(element, action) {
|
|
|
1307
1819
|
element.addEventListener("pointerup", (event) => {
|
|
1308
1820
|
const was = from;
|
|
1309
1821
|
from = null;
|
|
1822
|
+
pressFeedback(element, false);
|
|
1823
|
+
event.stopPropagation();
|
|
1310
1824
|
if (was && was.id === event.pointerId && Math.hypot(event.clientX - was.x, event.clientY - was.y) < SLOP) action();
|
|
1311
1825
|
});
|
|
1312
1826
|
}
|
|
1313
1827
|
|
|
1314
|
-
let idle = 0;
|
|
1315
1828
|
function wake() {
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
idle = setTimeout(() => { if (menu.classList.contains("gone")) bar.classList.add("idle"); }, 4500);
|
|
1829
|
+
expandControls(true);
|
|
1830
|
+
armControlsIdle();
|
|
1319
1831
|
}
|
|
1320
1832
|
|
|
1321
1833
|
let toastTimer = 0;
|
|
@@ -1358,8 +1870,9 @@ function setMode(next) {
|
|
|
1358
1870
|
}
|
|
1359
1871
|
|
|
1360
1872
|
function closeMenu() {
|
|
1361
|
-
if (menu
|
|
1362
|
-
menu
|
|
1873
|
+
if (!popoverOpen(menu) && !popoverOpen(connection)) return false;
|
|
1874
|
+
presentPopover(menu, false);
|
|
1875
|
+
presentPopover(connection, false);
|
|
1363
1876
|
clearInterval(statusTimer);
|
|
1364
1877
|
wake();
|
|
1365
1878
|
return true;
|
|
@@ -1376,9 +1889,13 @@ function refresh() {
|
|
|
1376
1889
|
const full = app ? chrome.fullscreen : !!document.fullscreenElement;
|
|
1377
1890
|
$("full").classList.toggle("gone", !app && !document.fullscreenEnabled);
|
|
1378
1891
|
$("full").innerHTML = full ? ICON.shrink : ICON.expand;
|
|
1379
|
-
for (const choice of choices)
|
|
1892
|
+
for (const choice of choices) {
|
|
1893
|
+
choice.classList.toggle("on", choice.dataset.mode === mode);
|
|
1894
|
+
choice.setAttribute("aria-pressed", String(choice.dataset.mode === mode));
|
|
1895
|
+
}
|
|
1380
1896
|
dress();
|
|
1381
1897
|
layout();
|
|
1898
|
+
armControlsIdle();
|
|
1382
1899
|
}
|
|
1383
1900
|
|
|
1384
1901
|
function drawKeys() {
|
|
@@ -1418,7 +1935,7 @@ async function vitals(pc, before) {
|
|
|
1418
1935
|
};
|
|
1419
1936
|
}
|
|
1420
1937
|
|
|
1421
|
-
/**
|
|
1938
|
+
/** Connection details are requested separately from the control mode. */
|
|
1422
1939
|
let statusTimer = 0, statusWas = null;
|
|
1423
1940
|
async function status() {
|
|
1424
1941
|
const line = $("status");
|
|
@@ -1438,25 +1955,33 @@ async function status() {
|
|
|
1438
1955
|
|
|
1439
1956
|
const ICON = JSON.parse($("icons").textContent);
|
|
1440
1957
|
const choices = menu.querySelectorAll(".choice");
|
|
1441
|
-
|
|
1958
|
+
pressable($("mode"), () => {
|
|
1442
1959
|
closeSheet();
|
|
1443
|
-
menu
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1960
|
+
const wasOpen = popoverOpen(menu);
|
|
1961
|
+
closeMenu();
|
|
1962
|
+
if (!wasOpen) presentPopover(menu, true);
|
|
1963
|
+
});
|
|
1964
|
+
pressable($("info"), () => {
|
|
1965
|
+
closeSheet();
|
|
1966
|
+
const wasOpen = popoverOpen(connection);
|
|
1967
|
+
closeMenu();
|
|
1968
|
+
if (!wasOpen) presentPopover(connection, true);
|
|
1969
|
+
if (!wasOpen) {
|
|
1447
1970
|
statusWas = null;
|
|
1448
1971
|
status();
|
|
1449
1972
|
statusTimer = setInterval(status, 1500);
|
|
1450
1973
|
}
|
|
1451
1974
|
layout();
|
|
1452
1975
|
});
|
|
1976
|
+
tappable($("closeinfo"), closeMenu);
|
|
1453
1977
|
// On a phone the button is there only while the key bar is not, which is while the page knows of no keyboard:
|
|
1454
1978
|
// there it only ever raises one, whatever state the field was left in. With a mouse it gives the keys and takes them.
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1979
|
+
pressable($("keyboard"), () => (coarse ? raiseKeys() : toggleKeys()));
|
|
1980
|
+
pressable($("quick"), openSheet);
|
|
1981
|
+
pressable($("fit"), () => zoomTo(1, { x: area.x + area.w / 2, y: area.y + area.h / 2 }));
|
|
1982
|
+
pressable($("rotate"), () => tellApp({ type: "landscape", on: !chrome.landscape }));
|
|
1983
|
+
pressable($("full"), () => {
|
|
1984
|
+
beginPresentation(app ? !chrome.fullscreen : !document.fullscreenElement);
|
|
1460
1985
|
if (app) return tellApp({ type: "fullscreen", on: !chrome.fullscreen });
|
|
1461
1986
|
if (document.fullscreenElement) document.exitFullscreen();
|
|
1462
1987
|
else document.documentElement.requestFullscreen().catch(() => {});
|
|
@@ -1464,10 +1989,10 @@ tappable($("full"), () => {
|
|
|
1464
1989
|
document.addEventListener("fullscreenchange", refresh);
|
|
1465
1990
|
for (const choice of choices) {
|
|
1466
1991
|
choice.querySelector("i").innerHTML = ICON[choice.dataset.mode];
|
|
1467
|
-
|
|
1992
|
+
pressable(choice, () => { closeMenu(); setMode(choice.dataset.mode); });
|
|
1468
1993
|
}
|
|
1469
1994
|
for (const key of keys.children) {
|
|
1470
|
-
|
|
1995
|
+
pressable(key, () => {
|
|
1471
1996
|
const mod = key.dataset.mod;
|
|
1472
1997
|
if (mod) {
|
|
1473
1998
|
if (!mods.delete(mod)) mods.add(mod);
|
|
@@ -1632,6 +2157,7 @@ tappable($("makersave"), saveMaker);
|
|
|
1632
2157
|
|
|
1633
2158
|
function openSheet() {
|
|
1634
2159
|
if (mode === "view" || sheetOpen) return;
|
|
2160
|
+
beginSheetTransition(true);
|
|
1635
2161
|
closeMenu();
|
|
1636
2162
|
closeComposer();
|
|
1637
2163
|
back = keysOpen && fieldFocused();
|
|
@@ -1646,6 +2172,7 @@ function openSheet() {
|
|
|
1646
2172
|
/** True when there was a sheet to close. toKeys: the keyboard it took the place of comes back. */
|
|
1647
2173
|
function closeSheet(toKeys) {
|
|
1648
2174
|
if (!sheetOpen) return false;
|
|
2175
|
+
beginSheetTransition(false);
|
|
1649
2176
|
sheetOpen = false;
|
|
1650
2177
|
disarm();
|
|
1651
2178
|
if (document.activeElement === naming) naming.blur();
|
|
@@ -1672,12 +2199,9 @@ words.addEventListener("compositionend", () => { writing = false; });
|
|
|
1672
2199
|
const unfinished = () => writing && (hint("先在键盘上选好字,再发送", 2500), true);
|
|
1673
2200
|
|
|
1674
2201
|
function grow() {
|
|
1675
|
-
const wide = composer.classList.contains("wide");
|
|
1676
2202
|
// What to write there, said by the page: a phone's browser has been seen to draw a text box's own
|
|
1677
2203
|
// placeholder cut to the width of what an input method last wrote in it.
|
|
1678
2204
|
$("wordshint").classList.toggle("gone", words.value !== "");
|
|
1679
|
-
words.style.height = "";
|
|
1680
|
-
words.style.height = clamp(words.scrollHeight, wide ? 44 : 66, wide ? 66 : 132) + "px";
|
|
1681
2205
|
layout();
|
|
1682
2206
|
}
|
|
1683
2207
|
|
|
@@ -1740,6 +2264,7 @@ $("composeclose").innerHTML = ICON.down;
|
|
|
1740
2264
|
|
|
1741
2265
|
/** The app tells the page what it did with the phone's screen, whether the keyboard is on it, and what shortcuts it keeps for the user. */
|
|
1742
2266
|
window.linkshellChrome = (next) => {
|
|
2267
|
+
if (typeof next.fullscreen === "boolean" && next.fullscreen !== chrome.fullscreen) beginPresentation(next.fullscreen);
|
|
1743
2268
|
const was = chrome.keyboard;
|
|
1744
2269
|
Object.assign(chrome, next);
|
|
1745
2270
|
if (chrome.keyboard === true) wentDown = false;
|
|
@@ -1752,6 +2277,7 @@ window.linkshellChrome = (next) => {
|
|
|
1752
2277
|
drawMine();
|
|
1753
2278
|
}
|
|
1754
2279
|
refresh();
|
|
2280
|
+
armControlsIdle();
|
|
1755
2281
|
};
|
|
1756
2282
|
|
|
1757
2283
|
// ---- Measuring (?measure=1) ------------------------------------------------
|
|
@@ -1919,23 +2445,29 @@ export function viewerPage() {
|
|
|
1919
2445
|
<title>屏幕</title>
|
|
1920
2446
|
<style>${STYLE}</style></head>
|
|
1921
2447
|
<body>
|
|
1922
|
-
<div id="stage"><canvas id="screen"></canvas><video id="video" class="gone" autoplay playsinline muted></video><div id="pointer"></div></div>
|
|
2448
|
+
<div id="stage"><div id="picture"><canvas id="screen"></canvas><video id="video" class="gone" autoplay playsinline muted></video><div id="pointer"></div></div></div>
|
|
1923
2449
|
<div id="note">正在连接电脑屏幕…</div>
|
|
1924
2450
|
<div id="toast" class="glass"></div>
|
|
1925
2451
|
<div id="menu" class="glass gone">
|
|
1926
|
-
<div class="choice" data-mode="view"><i></i><div><b>只看</b><span
|
|
1927
|
-
<div class="choice" data-mode="trackpad"><i></i><div><b>触控板</b><span
|
|
1928
|
-
<div class="choice" data-mode="touch"><i></i><div><b>点按</b><span
|
|
2452
|
+
<div class="choice" role="button" aria-label="只看,不操作电脑" data-mode="view"><i></i><div><b>只看</b><span>不操作电脑</span></div></div>
|
|
2453
|
+
<div class="choice" role="button" aria-label="触控板,滑动指针,轻点点击" data-mode="trackpad"><i></i><div><b>触控板</b><span>滑动指针 · 轻点点击</span></div></div>
|
|
2454
|
+
<div class="choice" role="button" aria-label="点按,点按画面直接操作" data-mode="touch"><i></i><div><b>点按</b><span>点按画面直接操作</span></div></div>
|
|
2455
|
+
</div>
|
|
2456
|
+
<div id="connection" class="glass gone">
|
|
2457
|
+
<div id="connectionhead"><span>连接信息</span><div class="tool" id="closeinfo" role="button" aria-label="关闭连接信息">${ICONS.close}</div></div>
|
|
1929
2458
|
<div id="status"></div>
|
|
1930
2459
|
</div>
|
|
1931
|
-
<div id="
|
|
2460
|
+
<div id="control-shell" class="glass" aria-hidden="true"></div>
|
|
2461
|
+
<div id="bar">
|
|
1932
2462
|
<div class="tool" id="mode" role="button" aria-label="控制方式"></div>
|
|
1933
2463
|
<div class="tool" id="keyboard" role="button" aria-label="键盘">${ICONS.keyboard}</div>
|
|
1934
2464
|
<div class="tool" id="quick" role="button" aria-label="快捷操作">${ICONS.quick}</div>
|
|
1935
2465
|
<div class="tool" id="fit" role="button" aria-label="还原缩放">${ICONS.fit}</div>
|
|
1936
2466
|
<div class="tool" id="rotate" role="button" aria-label="横屏">${ICONS.rotate}</div>
|
|
2467
|
+
<div class="tool" id="info" role="button" aria-label="连接信息">${ICONS.info}</div>
|
|
1937
2468
|
<div class="tool" id="full" role="button" aria-label="全屏"></div>
|
|
1938
2469
|
</div>
|
|
2470
|
+
<div id="orb" role="button" aria-label="展开屏幕控制"><div class="orb-core"><svg viewBox="0 0 24 24" fill="currentColor"><circle cx="5" cy="12" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="19" cy="12" r="2"/></svg></div></div>
|
|
1939
2471
|
<div id="sheet" class="glass gone">
|
|
1940
2472
|
<div id="sheethead">
|
|
1941
2473
|
<div id="compose" role="button">发送文字…</div>
|