group code popup
This commit is contained in:
@@ -595,23 +595,23 @@
|
||||
.current-location-button img { width: 24px; height: 24px; vertical-align: middle; }
|
||||
|
||||
/* Shared drafts and vertically scrolling Wichtel-code wheels. */
|
||||
.group-pair { display: inline-flex; align-items: center; justify-content: center; width: 4.5rem; }
|
||||
.group-pair img { width: 1.8rem; height: 2.3rem; object-fit: contain; pointer-events: none; }
|
||||
.group-share-pictures { display: flex; align-items: center; justify-content: center; gap: 1rem; margin: 1rem 0; }
|
||||
.group-share-pictures img { width: min(34vw, 9rem); height: 10rem; object-fit: contain; }
|
||||
.group-dialog { width: min(35rem, calc(100% - 2rem)); max-height: 90dvh; box-sizing: border-box; overflow-y: auto; padding: 1.25rem; border: 1px solid #c8d3ca; border-radius: 1.25rem; background: #f8faf5; color: #1f3022; }
|
||||
.group-dialog::backdrop { background: rgba(20, 40, 25, .5); }
|
||||
.group-dialog-header { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
|
||||
.group-dialog-header h2 { margin: 0; font-size: 1.2rem; }
|
||||
.group-dialog button { min-width: 44px; min-height: 44px; border: 1px solid #c8d3ca; border-radius: .85rem; background: white; color: #24462b; touch-action: manipulation; }
|
||||
.group-dialog button:focus-visible, .group-wheel-viewport:focus-visible { outline: 3px solid #1e6fba; outline-offset: 2px; }
|
||||
.group-wheels { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: .8rem; padding: 0; border: 0; margin: 1rem 0; }
|
||||
.group-wheels { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: .3rem; padding: 0; border: 0; margin: 1rem 0; }
|
||||
.group-code-fieldset { border: 0; padding: 0; min-width: 0; }
|
||||
.group-code-fieldset legend { margin-bottom: .5rem; font-weight: 600; }
|
||||
.group-wheel { display: grid; gap: .3rem; min-width: 0; }
|
||||
.group-wheel-viewport { height: 156px; padding: 52px 0; box-sizing: border-box; overflow-y: auto; overscroll-behavior: contain; scroll-snap-type: y mandatory; scrollbar-width: none; border-radius: .85rem; background: linear-gradient(to bottom, #edf2eb 0 33.33%, #dcebdc 33.33% 66.66%, #edf2eb 66.66%); }
|
||||
.group-wheel-viewport::-webkit-scrollbar { display: none; }
|
||||
.group-wheel-option { height: 52px; display: grid; place-items: center; scroll-snap-align: center; cursor: pointer; }
|
||||
.group-wheel-option img { width: 48px; height: 48px; object-fit: contain; pointer-events: none; }
|
||||
.group-code { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: .75rem; padding: 1rem; border: 1px solid #c8d3ca; border-radius: 1rem; background: #e7f1e9; }
|
||||
.group-wheel-option img { width: 100%; max-width: 48px; height: 48px; object-fit: contain; pointer-events: none; }
|
||||
.group-code { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: .3rem; padding: .6rem; border: 1px solid #c8d3ca; border-radius: 1rem; background: #e7f1e9; }
|
||||
.group-code img { width: 100%; height: 64px; object-fit: contain; }
|
||||
.group-actions { max-width: min(18rem, 52vw); flex-wrap: wrap; justify-content: flex-end; }
|
||||
.group-actions .editor-secondary-button { font-size: .85rem; }
|
||||
|
||||
+14
-2
@@ -336,7 +336,7 @@
|
||||
<h3>5. Deine eigenen Strecken</h3>
|
||||
<p>Neue Strecken bleiben zuerst auf deinem Gerät. Gespeichert werden der Weg, die Orte der Stationen, Texte sowie ausgewählte Bilder und Tonaufnahmen.</p>
|
||||
<p>Deine Entwürfe bleiben erhalten, wenn alter Fortschritt gelöscht wird oder du „Meinen Fortschritt merken“ ausschaltest. Sie bleiben auf diesem Gerät, bis du sie löschst oder die gespeicherten Daten von Wegwichtel in deinem Internetbrowser entfernst. Nur wenn du „Zur Prüfung senden“ oder „Gemeinsam bearbeiten“ wählst, wird deine Strecke an die Website gesendet.</p>
|
||||
<p>Änderungen an gemeinsamen Stationen werden vor dem Senden zusätzlich auf diesem Gerät gesichert. Dazu gehören deine Eingaben, neu ausgewählte Bilder und Tonaufnahmen sowie vorgemerkte Entfernungen. Noch nicht gesendete Änderungen bleiben bis zum erfolgreichen Senden oder bewussten Verwerfen erhalten. Das Löschen der Website-Daten im Internetbrowser kann diese Sicherungen entfernen. Auf der Website wird während der Bearbeitung eine Sperre mit deiner Gerätekennung und deinem Gruppenzugang gespeichert, damit sich Änderungen nicht unbemerkt überschreiben.</p>
|
||||
<p>Änderungen an gemeinsamen Stationen werden vor dem Senden zusätzlich auf diesem Gerät gesichert. Dazu gehören deine Eingaben, neu ausgewählte Bilder und Tonaufnahmen sowie vorgemerkte Entfernungen. Noch nicht gesendete Änderungen bleiben bis zum erfolgreichen Senden oder bewussten Verwerfen erhalten. Das Löschen der Website-Daten im Internetbrowser kann diese Sicherungen entfernen. Auf der Website wird während der Bearbeitung eine Sperre mit deiner Gerätekennung und deinem Gruppenzugang gespeichert, damit sich Änderungen nicht unbemerkt überschreiben. Nach 60 Minuten ohne gemeldete Aktivität wird diese Sperre entfernt.</p>
|
||||
<p>Beim ersten Start wird eine zufällige Gerätekennung gespeichert. Sie wird beim Einsenden mit deiner Strecke verknüpft. Dafür werden keine Angaben über dein Gerät oder deinen Internetanbieter gesammelt. Beim Löschen von Entwürfen oder gespeicherten App-Daten bleibt die Kennung erhalten. Wenn du alle Website-Daten im Internetbrowser löschst, geht auch die Kennung verloren.</p>
|
||||
<h3>6. Werbung und gespeicherte Einstellungen</h3>
|
||||
<p>Wegwichtel verwendet keine Werbe- oder Tracking-Cookies. Für die Gruppenbearbeitung wird nach dem Öffnen einer Gruppe oder dem Beitritt ein geschütztes Zugangscookie für höchstens 30 Tage gespeichert. Es merkt sich, welche gemeinsamen Entwürfe du bearbeiten darfst. Beim Öffnen einer Gruppe oder beim Beitritt werden außerdem deine Gerätekennung und deine Gruppenmitgliedschaft auf der Website gespeichert. Gemeinsame Entwürfe und ihre Bilder und Tonaufnahmen werden auf der Website gespeichert und sind für die Gruppenmitglieder zugänglich. Es zählt keine Besuche für Werbung und verfolgt nicht, welche anderen Seiten du besuchst. Auf deinem Gerät wird gespeichert, ob du „Meinen Fortschritt merken“ eingeschaltet hast.</p>
|
||||
@@ -579,13 +579,25 @@
|
||||
<script src="js/user-messages.js?v=0.12.14"></script>
|
||||
<script src="js/local-routes.js?v=0.12.14"></script>
|
||||
<script src="js/route-recorder.js?v=0.12.14"></script>
|
||||
<dialog id="group-share-dialog" class="group-dialog" aria-labelledby="group-share-title">
|
||||
<header class="group-dialog-header">
|
||||
<h2 id="group-share-title" data-i18n="groups.share">Gemeinsam bearbeiten</h2>
|
||||
<button id="group-share-close" type="button" data-i18n-aria-label="info.close">×</button>
|
||||
</header>
|
||||
<p id="group-share-route"></p>
|
||||
<div id="group-share-pictures" class="group-share-pictures"></div>
|
||||
<p data-i18n="groups.share-help">Mit diesen beiden Wichteln öffnest du einen gemeinsamen Entwurf. Andere können mit einem Wichtelcode mitmachen. Das Original bleibt erhalten.</p>
|
||||
<div class="editor-form-actions">
|
||||
<button id="group-share-start" type="button" data-i18n="groups.share-start">Gemeinsamen Entwurf öffnen</button>
|
||||
</div>
|
||||
</dialog>
|
||||
<dialog id="group-dialog" class="group-dialog" aria-labelledby="group-title">
|
||||
<header class="group-dialog-header">
|
||||
<h2 id="group-title" data-i18n="groups.share">Gemeinsam bearbeiten</h2>
|
||||
<button id="group-close" type="button" aria-label="Schließen" data-i18n-aria-label="info.close">×</button>
|
||||
</header>
|
||||
<section id="group-entry">
|
||||
<p data-i18n="groups.wheel-help">Schiebe die Wichtel nach oben oder unten. Die Wichtel in der Mitte ergeben den Code. Lies von links nach rechts, zuerst die obere Reihe.</p>
|
||||
<p data-i18n="groups.wheel-help">Schiebe die Wichtel nach oben oder unten. Die fünf Wichtel in der Mitte ergeben den Code. Lies von links nach rechts. Ein Wichtel darf mehrmals vorkommen.</p>
|
||||
<fieldset class="group-code-fieldset"><legend data-i18n="groups.code-label">Gruppencode</legend><div id="group-wheels" class="group-wheels"></div></fieldset>
|
||||
<button id="group-join" type="button" data-i18n="groups.join">Mitmachen</button>
|
||||
</section>
|
||||
|
||||
@@ -1652,6 +1652,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
if (shareInProgress) return;
|
||||
shareInProgress = true;
|
||||
try {
|
||||
if (ns.Groups.confirmShare && !await ns.Groups.confirmShare(routeName(route))) return;
|
||||
let sourceId = routeId(route);
|
||||
if (routeSource(route) === 'local') {
|
||||
if (!globalThis.confirm(ns.I18n.t('groups.upload-confirm'))) return;
|
||||
@@ -1712,6 +1713,14 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
}
|
||||
|
||||
function bind() {
|
||||
for (const eventName of ['input', 'change', 'pointerdown', 'keydown']) {
|
||||
byId(ids.poiForm)?.addEventListener(eventName, () => {
|
||||
if (state.currentSource === 'group') ns.GroupPoints?.noteActivity?.();
|
||||
});
|
||||
}
|
||||
document.addEventListener('wegwichtel:point-lock-lost', () => {
|
||||
if (state.currentSource === 'group') setStatus(ns.I18n.t('point-locks.lost'));
|
||||
});
|
||||
for (const eventName of ['cancel', 'close']) {
|
||||
byId(ids.dialog)?.addEventListener(eventName, event => {
|
||||
if (pointSaveInProgress && eventName === 'cancel') { event.preventDefault(); return; }
|
||||
|
||||
@@ -5,6 +5,9 @@ globalThis.Wegwichtel ??= {};
|
||||
let database;
|
||||
let current = null;
|
||||
let generation = 0;
|
||||
let activity = 0;
|
||||
let sentActivity = 0;
|
||||
let touching = false;
|
||||
const key = (routeId, poiId) => `${routeId}:${poiId}`;
|
||||
async function store(mode, operation) {
|
||||
if (!database) database = new Promise((resolve, reject) => {
|
||||
@@ -45,6 +48,7 @@ globalThis.Wegwichtel ??= {};
|
||||
method: 'POST', credentials: 'same-origin', cache: 'no-store',
|
||||
headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body)
|
||||
});
|
||||
if (response.status === 204) return {};
|
||||
const payload = await response.json();
|
||||
if (!response.ok) {
|
||||
const error = new Error(t(response.status === 423 ? 'lost' : 'unavailable'));
|
||||
@@ -158,6 +162,23 @@ globalThis.Wegwichtel ??= {};
|
||||
if (savingGeneration !== generation) return false;
|
||||
return remote;
|
||||
}
|
||||
function noteActivity() { activity += 1; }
|
||||
async function keepActive() {
|
||||
if (!current?.token || activity === sentActivity || touching) return;
|
||||
const editing = current;
|
||||
const observed = activity;
|
||||
touching = true;
|
||||
try {
|
||||
await request(editing.routeId, editing.poiId, { heartbeat: true, token: editing.token });
|
||||
if (editing === current) sentActivity = observed;
|
||||
} catch (error) {
|
||||
if (error.status === 423) {
|
||||
sentActivity = observed;
|
||||
document.dispatchEvent(new Event('wegwichtel:point-lock-lost'));
|
||||
}
|
||||
} finally { touching = false; }
|
||||
}
|
||||
globalThis.setInterval?.(() => { void keepActive(); }, 30_000);
|
||||
function headersFor(path) {
|
||||
const match = /^\/routes\/(\d+)\/pois\/(\d+)(?:\/|$)/.exec(path);
|
||||
if (!match || !current || String(current.routeId) !== match[1] || String(current.poiId) !== match[2]) return {};
|
||||
@@ -167,7 +188,7 @@ globalThis.Wegwichtel ??= {};
|
||||
const version = response.headers?.get('X-Poi-Version');
|
||||
if (current && String(current.poiId) === response.headers?.get('X-Poi-Id') && version) current.version = version;
|
||||
}
|
||||
ns.GroupPoints = Object.freeze({ begin, release, remember, read, list, remove, prepare, headersFor, observe,
|
||||
ns.GroupPoints = Object.freeze({ begin, release, remember, read, list, remove, prepare, headersFor, observe, noteActivity, keepActive,
|
||||
acceptNew: async (routeId, poi) => { current = { ...await acquire(routeId, poi.id), routeId, poiId: poi.id }; },
|
||||
changed: () => document.dispatchEvent(new Event('wegwichtel:point-drafts-changed')) });
|
||||
}(globalThis.Wegwichtel));
|
||||
|
||||
+44
-8
@@ -8,7 +8,7 @@ globalThis.Wegwichtel ??= {};
|
||||
entry: 'group-entry', invitation: 'group-invitation', code: 'group-code', join: 'group-join',
|
||||
edit: 'group-edit', expiry: 'group-expiry'
|
||||
};
|
||||
const codeLength = 8;
|
||||
const codeLength = 5;
|
||||
const wheelStep = 52;
|
||||
const revisions = new Map();
|
||||
let selection = [];
|
||||
@@ -43,6 +43,7 @@ globalThis.Wegwichtel ??= {};
|
||||
function errorMessage(error) {
|
||||
if (error.reason === 'group-limit') return t('error.limit');
|
||||
if (error.reason === 'group-full') return t('error.full');
|
||||
if (error.reason === 'group-editing') return t('code-wait');
|
||||
if (error.status === 423) return ns.I18n.t('point-locks.busy');
|
||||
if (error.status === 429) return t('error.rate');
|
||||
if (error.status === 404) return t('error.code');
|
||||
@@ -167,6 +168,19 @@ globalThis.Wegwichtel ??= {};
|
||||
if (!element(ids.dialog).open) element(ids.dialog).showModal();
|
||||
}
|
||||
|
||||
let invitationPolling = false;
|
||||
async function refreshInvitation() {
|
||||
const group = currentGroup;
|
||||
if (!group || invitationPolling || !element(ids.dialog).open || element(ids.invitation).hidden) return;
|
||||
invitationPolling = true;
|
||||
try {
|
||||
const payload = await request(`/${group.id}/invitation`);
|
||||
if (currentGroup?.id !== group.id || !element(ids.dialog).open || element(ids.invitation).hidden) return;
|
||||
if (payload.code) showInvitation(payload);
|
||||
element(ids.status).textContent = payload.busy && payload.expired ? t('code-wait') : '';
|
||||
} catch { /* Keep the invitation visible; expiry still applies on the server. */ }
|
||||
finally { invitationPolling = false; }
|
||||
}
|
||||
async function edit(group) {
|
||||
element(ids.dialog).close();
|
||||
await ns.EditorUi.openGroup(group.route);
|
||||
@@ -244,18 +258,34 @@ globalThis.Wegwichtel ??= {};
|
||||
function shareButton(action) {
|
||||
const result = document.createElement('button');
|
||||
result.type = 'button';
|
||||
result.className = 'editor-icon-button group-pair';
|
||||
result.className = 'editor-icon-button';
|
||||
result.dataset.editorAction = action;
|
||||
result.title = t('share');
|
||||
result.setAttribute('aria-label', t('share'));
|
||||
for (const value of ['🐦', '🪨']) {
|
||||
const image = picture(ns.GroupSymbols.get(value));
|
||||
image.alt = '';
|
||||
result.appendChild(image);
|
||||
}
|
||||
result.setAttribute('aria-haspopup', 'dialog');
|
||||
result.textContent = '👥';
|
||||
return result;
|
||||
}
|
||||
|
||||
let shareDecision = null;
|
||||
function confirmShare(name) {
|
||||
if (shareDecision) return Promise.resolve(false);
|
||||
const dialog = element('group-share-dialog');
|
||||
element('group-share-route').textContent = name;
|
||||
element('group-share-pictures').replaceChildren(...['🐦', '🪨'].map(value => picture(ns.GroupSymbols.get(value))));
|
||||
return new Promise(resolve => {
|
||||
shareDecision = resolve;
|
||||
dialog.showModal();
|
||||
});
|
||||
}
|
||||
|
||||
function finishShare(accepted) {
|
||||
const resolve = shareDecision;
|
||||
shareDecision = null;
|
||||
element('group-share-dialog').close();
|
||||
resolve?.(accepted);
|
||||
}
|
||||
|
||||
function headersFor(path) {
|
||||
const id = /^\/routes\/(\d+)(?:\/|$)/.exec(path)?.[1];
|
||||
return id && revisions.has(id) ? { 'If-Match': String(revisions.get(id)) } : {};
|
||||
@@ -268,6 +298,12 @@ globalThis.Wegwichtel ??= {};
|
||||
}
|
||||
|
||||
function init() {
|
||||
globalThis.setInterval?.(() => { void refreshInvitation(); }, 30_000);
|
||||
activate(element('group-share-start'), () => finishShare(true));
|
||||
activate(element('group-share-close'), () => finishShare(false));
|
||||
element('group-share-dialog').addEventListener('close', () => {
|
||||
const resolve = shareDecision; shareDecision = null; resolve?.(false);
|
||||
});
|
||||
document.addEventListener('wegwichtel:point-drafts-changed', () => { void renderPending(); void ns.StorageUsage?.refresh(); });
|
||||
element(ids.dialog).addEventListener('cancel', () => { dialogGeneration += 1; });
|
||||
element(ids.dialog).addEventListener('close', () => { dialogGeneration += 1; });
|
||||
@@ -292,7 +328,7 @@ globalThis.Wegwichtel ??= {};
|
||||
activate(element(ids.edit), () => guarded(() => edit(currentGroup)));
|
||||
}
|
||||
|
||||
ns.Groups = Object.freeze({ share, shareButton, refresh, headersFor, observe });
|
||||
ns.Groups = Object.freeze({ share, shareButton, confirmShare, refresh, refreshInvitation, headersFor, observe });
|
||||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init, { once: true });
|
||||
else init();
|
||||
}(globalThis.Wegwichtel));
|
||||
|
||||
@@ -134,12 +134,15 @@ globalThis.Wegwichtel ??= {};
|
||||
"ready": "Alles ist bereit."
|
||||
},
|
||||
"groups": {
|
||||
"code-wait": "In der Gruppe wird gerade eine Station bearbeitet. Ein neuer Code erscheint, sobald die Stationen wieder frei sind. Ein abgelaufener Code erlaubt keinen neuen Beitritt.",
|
||||
"share-help": "Mit diesen beiden Wichteln öffnest du einen gemeinsamen Entwurf. Andere können mit einem Wichtelcode mitmachen. Das Original bleibt erhalten.",
|
||||
"share-start": "Gemeinsamen Entwurf öffnen",
|
||||
"title": "Gemeinsame Entwürfe",
|
||||
"upload-confirm": "Dein Entwurf mit Bildern und Ton wird dafür an die Website gesendet und als gemeinsamer Entwurf kopiert. Fortfahren?",
|
||||
"share": "Gemeinsam bearbeiten",
|
||||
"join-open": "Gruppencode eingeben",
|
||||
"online": "Gemeinsam bearbeiten braucht eine Internetverbindung.",
|
||||
"wheel-help": "Schiebe die Wichtel nach oben oder unten. Die Wichtel in der Mitte ergeben den Code. Lies von links nach rechts, zuerst die obere Reihe.",
|
||||
"wheel-help": "Schiebe die Wichtel nach oben oder unten. Die fünf Wichtel in der Mitte ergeben den Code. Lies von links nach rechts. Ein Wichtel darf mehrmals vorkommen.",
|
||||
"code-label": "Gruppencode",
|
||||
"join": "Mitmachen",
|
||||
"invite-help": "Zeige diesen Code den anderen. Damit können sie deinen gemeinsamen Entwurf öffnen und bearbeiten. Das Original bleibt erhalten.",
|
||||
|
||||
@@ -134,12 +134,15 @@ globalThis.Wegwichtel ??= {};
|
||||
"ready": "Everything is ready."
|
||||
},
|
||||
"groups": {
|
||||
"code-wait": "Someone in the group is editing a stop. A new code will appear once the stops are free. An expired code cannot be used to join.",
|
||||
"share-help": "These two gnomes open a shared draft. Others can join using a gnome code. The original stays unchanged.",
|
||||
"share-start": "Open a shared draft",
|
||||
"title": "Shared drafts",
|
||||
"upload-confirm": "Your draft, including pictures and recordings, will be sent to the website and copied into a shared draft. Continue?",
|
||||
"share": "Edit together",
|
||||
"join-open": "Enter group code",
|
||||
"online": "You need an internet connection to edit together.",
|
||||
"wheel-help": "Slide the gnomes up or down. The gnomes in the middle make the code. Read from left to right, starting with the top row.",
|
||||
"wheel-help": "Slide the gnomes up or down. The gnomes in the middle make the code. Read the five gnomes from left to right. The same gnome can appear more than once.",
|
||||
"code-label": "Group code",
|
||||
"join": "Join in",
|
||||
"invite-help": "Show this code to the others so they can open and edit your shared draft. The original stays as it is.",
|
||||
|
||||
Reference in New Issue
Block a user