Everyone has the same buttons; the view is personal
The host's rights and the co-host are gone: anyone in a session may start a new page, put an image on the page, load a kept page, hide, expand and shrink, and move or remove images and Hides; the page left is kept in every notebook. Fit and Normal scale act on your own screen only. A session goes on without whoever started it, and a newcomer to an empty room starts page 1 with New. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
5d63a0f758
commit
60ad747e32
10 files changed
+215
-354
No files matched your search
+32
-82
@@ -230,7 +230,7 @@ const CONNECTION_GRACE_MS = 1500;
|
||||
* The notice line between the toolbar and the page. It says nothing
|
||||
* while all is well. A STANDING notice, kept by its source until that
|
||||
* source clears it, is a state: not connected, the page not being
|
||||
* saved, no page from the host yet. A PASSING notice is a report of
|
||||
* saved, no page from the others yet. A PASSING notice is a report of
|
||||
* one event, kept for a few seconds: a page kept or exported, a file
|
||||
* that was not an image. Standing notices come before a passing one.
|
||||
*/
|
||||
@@ -470,7 +470,6 @@ async function enterSession(invitationText, name, notebook) {
|
||||
input('invitation').value = config.site ? formatInvitationLink(config.site, plainInvitation) : plainInvitation;
|
||||
showInvitationCode(config.site ? input('invitation').value : '');
|
||||
|
||||
const isHost = identity.id === invitation.hostId;
|
||||
let tool = /** @type {Tool} */ ('pen');
|
||||
/** The Shape tool's current shape, remembered in this browser (Andy, 2026-10-14). */
|
||||
let shape = shapeNamed(localStorage.getItem(SHAPE_KEY));
|
||||
@@ -501,7 +500,7 @@ async function enterSession(invitationText, name, notebook) {
|
||||
function showPageNumber() {
|
||||
const waiting = session.page.number === 0;
|
||||
element('page_number').textContent = waiting ? 'No page yet' : `Page ${session.page.number}`;
|
||||
standingNotice('page', waiting ? 'Waiting for the host’s page' : '', false);
|
||||
standingNotice('page', waiting ? 'Waiting for the others’ page; New starts one' : '', false);
|
||||
}
|
||||
/** @type {ReturnType<typeof setTimeout> | undefined} */
|
||||
let connectionNoticeTimer;
|
||||
@@ -568,18 +567,6 @@ async function enterSession(invitationText, name, notebook) {
|
||||
textEditor?.layout();
|
||||
writer.changed();
|
||||
},
|
||||
onViewCommand: (command) => {
|
||||
log(`The host's view: ${command === 'fit' ? 'Fit' : 'Normal scale'}`);
|
||||
if (command === 'fit') renderer.fit();
|
||||
else renderer.sheetScale();
|
||||
},
|
||||
onPermissionsChanged: () => {
|
||||
log(`May start pages: ${session.mayMakePages() ? 'yes' : 'no'}` +
|
||||
(session.isHost ? `; guests allowed: ${session.permitted.size}` : ''));
|
||||
showHostTools();
|
||||
showParticipants(session, renderer);
|
||||
refreshArrangeCues();
|
||||
},
|
||||
onParticipantsChanged: () => {
|
||||
syncLayers();
|
||||
showParticipants(session, renderer);
|
||||
@@ -846,12 +833,11 @@ async function enterSession(invitationText, name, notebook) {
|
||||
/**
|
||||
* With the Arrange tool chosen, mark everything a press can move or
|
||||
* resize with a blue box and corners (Andy, 2026-10-14): the images
|
||||
* and Hides for the host and co-hosts, one's own text blocks and
|
||||
* shapes; free text, which has no width to resize, gets the box without
|
||||
* the corners. The one being held is left out, the green grip standing
|
||||
* in its place until the pointer lifts. Called whenever the images,
|
||||
* the Hides, one's own text or shapes, the right, or the hold may have
|
||||
* changed.
|
||||
* and Hides, one's own text blocks and shapes; free text, which has
|
||||
* no width to resize, gets the box without the corners. The one being
|
||||
* held is left out, the green grip standing in its place until the
|
||||
* pointer lifts. Called whenever the images, the Hides, one's own
|
||||
* text or shapes, or the hold may have changed.
|
||||
*/
|
||||
function refreshArrangeCues() {
|
||||
if (tool !== 'arrange') {
|
||||
@@ -861,16 +847,14 @@ async function enterSession(invitationText, name, notebook) {
|
||||
/** @type {{rect: import('./page_model.js').Rectangle, kind: 'image' | 'text' | 'hide' | 'shape' | 'stroke', corners: boolean}[]} */
|
||||
const cues = [];
|
||||
const page = session.page;
|
||||
if (session.mayMakePages()) {
|
||||
page.images.forEach((im, index) => {
|
||||
if (held?.kind === 'image' && held.index === index) return;
|
||||
cues.push({ kind: 'image', rect: { left: im.x, top: im.y, width: im.width, height: im.height }, corners: true });
|
||||
});
|
||||
page.hides.forEach((h, index) => {
|
||||
if (held?.kind === 'hide' && held.index === index) return;
|
||||
cues.push({ kind: 'hide', rect: { left: h.x, top: h.y, width: h.width, height: h.height }, corners: true });
|
||||
});
|
||||
}
|
||||
page.images.forEach((im, index) => {
|
||||
if (held?.kind === 'image' && held.index === index) return;
|
||||
cues.push({ kind: 'image', rect: { left: im.x, top: im.y, width: im.width, height: im.height }, corners: true });
|
||||
});
|
||||
page.hides.forEach((h, index) => {
|
||||
if (held?.kind === 'hide' && held.index === index) return;
|
||||
cues.push({ kind: 'hide', rect: { left: h.x, top: h.y, width: h.width, height: h.height }, corners: true });
|
||||
});
|
||||
for (const block of session.ownLayer.texts.values()) {
|
||||
if (held?.kind === 'text' && held.id === block.id) continue;
|
||||
cues.push({ kind: 'text', rect: textBounds(block), corners: block.height !== null });
|
||||
@@ -1110,13 +1094,13 @@ async function enterSession(invitationText, name, notebook) {
|
||||
if (rects.length) return rects;
|
||||
const page = session.page;
|
||||
const hide = page.hideAt(x, y, 0);
|
||||
if (hide && session.mayMakePages()) {
|
||||
if (hide) {
|
||||
const h = page.hides[hide.index];
|
||||
return [{ left: h.x, top: h.y, width: h.width, height: h.height }];
|
||||
}
|
||||
const found = page.imageAt(x, y, 0);
|
||||
const image = found && page.images[found.index];
|
||||
if (image && (session.mayMakePages() || image.by === session.identity.id)) {
|
||||
if (image) {
|
||||
return [{ left: image.x, top: image.y, width: image.width, height: image.height }];
|
||||
}
|
||||
return [];
|
||||
@@ -1143,15 +1127,15 @@ async function enterSession(invitationText, name, notebook) {
|
||||
zoomTo: (pixelsUp) => renderer.zoomDragTo(pixelsUp),
|
||||
endZoom: () => renderer.endZoomDrag(),
|
||||
// A hold moves what is under the pen, or resizes it from a corner: a
|
||||
// Hide rectangle or an image (for the host and co-hosts), or a
|
||||
// text block of our own. The dot drawn meanwhile goes.
|
||||
// Hide rectangle, an image, or a text block of our own. The dot
|
||||
// drawn meanwhile goes.
|
||||
beginHold: (clientX, clientY) => {
|
||||
const [x, y] = renderer.pageFromClient(clientX, clientY);
|
||||
const reach = renderer.cornerReach();
|
||||
const page = session.page;
|
||||
/** @type {Held | null} */
|
||||
let found = null;
|
||||
const hide = session.mayMakePages() ? page.hideAt(x, y, reach) : null;
|
||||
const hide = page.hideAt(x, y, reach);
|
||||
if (hide) {
|
||||
const h = page.hides[hide.index];
|
||||
found = hold('hide', hide.index, h.id, { left: h.x, top: h.y, width: h.width, height: h.height }, hide.corner, x, y);
|
||||
@@ -1190,7 +1174,7 @@ async function enterSession(invitationText, name, notebook) {
|
||||
const stroke = shapes[shapeAt.index];
|
||||
const box = /** @type {import('./page_model.js').Rectangle} */ (strokeBox(stroke));
|
||||
if (session.beginShapeEdit(stroke.id)) found = hold('shape', shapeAt.index, stroke.id, box, shapeAt.corner, x, y);
|
||||
} else if (session.mayMakePages()) {
|
||||
} else {
|
||||
const image = page.imageAt(x, y, reach);
|
||||
if (image) {
|
||||
const index = session.raiseImage(image.index);
|
||||
@@ -1277,8 +1261,8 @@ async function enterSession(invitationText, name, notebook) {
|
||||
lastEmptyTap = 0;
|
||||
return;
|
||||
}
|
||||
// A tap of the eraser on a Hide rectangle or an image removes it
|
||||
// (the right to make pages, or having added the image).
|
||||
// A tap of the eraser on a Hide rectangle or an image removes it,
|
||||
// for everyone (by anyone since 2026-10-18).
|
||||
let took = erased.strokes + erased.texts > 0;
|
||||
if (!took) {
|
||||
const hide = session.page.hideAt(x, y, 0);
|
||||
@@ -1458,8 +1442,8 @@ async function enterSession(invitationText, name, notebook) {
|
||||
textEditor?.layout();
|
||||
viewChanged();
|
||||
};
|
||||
// The host's Fit and Normal scale ("A4" until 2026-10-14) reach everyone (Andy, 2026-10-08); a guest's
|
||||
// are the guest's own.
|
||||
// Fit and Normal scale ("A4" until 2026-10-14) are each person's own
|
||||
// (2026-10-18; the host's reached everyone from 2026-10-08 until then).
|
||||
// The page and view commands live in the Page menu (Andy, 2026-10-14:
|
||||
// seldom used, and the one row is precious).
|
||||
attachPopup(element('page_menu_button'), element('page_menu'));
|
||||
@@ -1505,14 +1489,8 @@ async function enterSession(invitationText, name, notebook) {
|
||||
log(`The manual could not be copied: ${error}`);
|
||||
}
|
||||
});
|
||||
element('zoom_fit').addEventListener('click', () => {
|
||||
renderer.fit();
|
||||
session.sendViewCommand('fit');
|
||||
});
|
||||
element('zoom_sheet').addEventListener('click', () => {
|
||||
renderer.sheetScale();
|
||||
session.sendViewCommand('sheet');
|
||||
});
|
||||
element('zoom_fit').addEventListener('click', () => renderer.fit());
|
||||
element('zoom_sheet').addEventListener('click', () => renderer.sheetScale());
|
||||
// "Expand" makes the sheet itself fill the window at the present zoom,
|
||||
// in both directions; nothing on it moves (Andy, 2026-10-08 for the
|
||||
// width, both ways 2026-10-11).
|
||||
@@ -1541,21 +1519,9 @@ async function enterSession(invitationText, name, notebook) {
|
||||
element('desk').addEventListener('scroll', viewChanged, { passive: true });
|
||||
window.addEventListener('pagehide', saveView);
|
||||
|
||||
// The host drives: the host starts new pages, and may let a guest do so.
|
||||
function showHostTools() {
|
||||
// New and Image are the host's; Notebooks, in the same group so that
|
||||
// nothing moves for the host, is everyone's (the chooser reserves
|
||||
// loading).
|
||||
for (const id of ['new_page', 'load_image']) element(id).hidden = !session.mayMakePages();
|
||||
// The Hide tool is for the host and co-hosts; losing the right
|
||||
// while it is chosen puts the pen back in hand.
|
||||
const hideTool = /** @type {HTMLElement} */ (document.querySelector('[data-tool=hide]'));
|
||||
hideTool.hidden = !session.mayMakePages();
|
||||
if (hideTool.hidden && tool === 'hide') /** @type {HTMLElement} */ (document.querySelector('[data-tool=pen]')).click();
|
||||
element('zoom_expand').hidden = !session.mayMakePages();
|
||||
element('zoom_shrink').hidden = !session.mayMakePages();
|
||||
}
|
||||
showHostTools();
|
||||
// Everyone has every button (Andy and his teacher, 2026-10-18; until
|
||||
// then New, Image, Hide, Expand and Shrink were the host's and the
|
||||
// co-hosts').
|
||||
element('new_page').addEventListener('click', () => {
|
||||
log('New page asked for');
|
||||
session.newPage();
|
||||
@@ -1567,7 +1533,7 @@ async function enterSession(invitationText, name, notebook) {
|
||||
* @param {Blob} file
|
||||
*/
|
||||
async function loadImage(file) {
|
||||
if (!session.mayMakePages() || session.page.number === 0) return;
|
||||
if (session.page.number === 0) return;
|
||||
try {
|
||||
if (!session.addImage(await prepareImage(file))) {
|
||||
throw new Error('The page has no room for another image.');
|
||||
@@ -1588,7 +1554,7 @@ async function enterSession(invitationText, name, notebook) {
|
||||
// A deleted notebook leaves the toolbar's list.
|
||||
showNotebooks(sessionPicker).catch(() => {});
|
||||
});
|
||||
element('load_notebook_page').addEventListener('click', () => { chooser.open(writer.notebook, session.mayMakePages()); });
|
||||
element('load_notebook_page').addEventListener('click', () => { chooser.open(writer.notebook); });
|
||||
|
||||
// Image files are chosen, and pictures of the page exported, through a
|
||||
// chooser of our own, which begins where the notebooks are.
|
||||
@@ -2056,20 +2022,6 @@ function showParticipants(session, renderer) {
|
||||
const present = participant.self || participant.peer !== null;
|
||||
item.classList.toggle('absent', !present);
|
||||
|
||||
if (session.isHost && !participant.self) {
|
||||
// A checkbox with its label to the left, like Extend (Andy, 2026-10-09).
|
||||
const allowLine = document.createElement('label');
|
||||
allowLine.className = 'allow_line';
|
||||
allowLine.setAttribute('data-help', 'Make this participant a co-host: they may then start pages, put images on the page and arrange them, load notebook pages, expand, shrink and hide, as you can.');
|
||||
const allow = document.createElement('input');
|
||||
allow.type = 'checkbox';
|
||||
allow.className = 'allow';
|
||||
allow.checked = session.permitted.has(participant.id);
|
||||
allow.addEventListener('change', () => session.setPermitted(participant.id, allow.checked));
|
||||
allowLine.append('Co-host ', allow);
|
||||
item.append(allowLine);
|
||||
}
|
||||
|
||||
const label = document.createElement('label');
|
||||
const visible = document.createElement('input');
|
||||
visible.type = 'checkbox';
|
||||
@@ -2082,8 +2034,6 @@ function showParticipants(session, renderer) {
|
||||
|
||||
const roles = [];
|
||||
if (participant.self) roles.push('you');
|
||||
if (participant.host) roles.push('host');
|
||||
else if (!session.isHost && session.permitted.has(participant.id)) roles.push('co-host');
|
||||
if (!present) roles.push('away');
|
||||
if (roles.length) {
|
||||
const role = document.createElement('span');
|
||||
|
||||
Reference in new issue
Block a user