movil: usabilidad tras la prueba en el Pixel
Lo que salio de recorrer la app como alguien de la acampada: - borrar (tribus y todo lo que usa renderContentActions) pide un segundo toque; la X va la ultima de la fila - Karvan: Llamar empieza solo con voz (si se negaba la camara no arrancaba nada); botones con icono y nombre; textos traducidos; ayuda para invitar - votaciones: tipo Si/No, semaforo o termometro (el servidor ya aceptaba options); resultados en barras; aviso a quien la crea de que no puede votar - workflow activistas con tribus, parlamento, votaciones y eventos - seccion apagada: /modules dice cual era y la activa de un toque - Modulos en el movil: agrupados como el lanzador, interruptores y Guardar fijo - tribus en filas compactas; secciones de una tribu en tira de fichas + Mas - lanzador y barra con iconos SVG en vez de letras vai - Ajustes: idioma y aspecto primero; idiomas de un toque - aviso de bienvenida solo en la portada y en una linea; Ahora no al final - pie de una linea con el estado de sincronizacion en claro - mobile_ux.css para los dos skins: un margen de 14px (eran 74 por lado en las fichas), Anton solo en titulos y boton principal, buscador en una linea, la barra de abajo se esconde con el teclado
This commit is contained in:
parent
2e3ff1e79f
commit
4dd573662c
14 changed files with 1279 additions and 54 deletions
|
|
@ -370,7 +370,21 @@ const refreshWalletReady = async (force = false) => {
|
||||||
} catch (_) { return ready; }
|
} catch (_) { return ready; }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// en el movil, al abrir una seccion apagada se avisa en /modules de cual era y se
|
||||||
|
// ofrece activarla (antes era un salto mudo a la tabla). La app es de una persona,
|
||||||
|
// asi que basta con recordar el ultimo bloqueo unos segundos
|
||||||
|
let lastBlockedMod = null;
|
||||||
const checkMod = (ctx, mod) => {
|
const checkMod = (ctx, mod) => {
|
||||||
|
const ok = checkModRaw(ctx, mod);
|
||||||
|
if (!ok && process.env.OASIS_MOBILE === '1' && ctx.method === 'GET') lastBlockedMod = { mod, path: ctx.path, at: Date.now() };
|
||||||
|
return ok;
|
||||||
|
};
|
||||||
|
const takeBlockedMod = () => {
|
||||||
|
const b = lastBlockedMod && Date.now() - lastBlockedMod.at < 15000 ? lastBlockedMod : null;
|
||||||
|
lastBlockedMod = null;
|
||||||
|
return b;
|
||||||
|
};
|
||||||
|
const checkModRaw = (ctx, mod) => {
|
||||||
const cfg = getConfig();
|
const cfg = getConfig();
|
||||||
const serverValue = cfg.modules?.[mod];
|
const serverValue = cfg.modules?.[mod];
|
||||||
if (serverValue === 'off') return false;
|
if (serverValue === 'off') return false;
|
||||||
|
|
@ -4677,7 +4691,18 @@ router
|
||||||
.get("/modules", async (ctx) => {
|
.get("/modules", async (ctx) => {
|
||||||
const modules = ['blogs', 'polls', 'karvan', 'fediverse', 'invites', 'wallet', 'backup', 'dev', 'cipher', 'bookmarks', 'calendars', 'chats', 'videos', 'docs', 'audios', 'tags', 'images', 'maps', 'trending', 'events', 'tasks', 'market', 'tribes', 'larp', 'votes', 'reports', 'opinions', 'pads', 'transfers', 'feed', 'pixelia', 'melody', 'agenda', 'favorites', 'ai', 'forum', 'games', 'housing', 'jobs', 'projects', 'industry', 'shops', 'banking', 'school', 'parliament', 'courts'];
|
const modules = ['blogs', 'polls', 'karvan', 'fediverse', 'invites', 'wallet', 'backup', 'dev', 'cipher', 'bookmarks', 'calendars', 'chats', 'videos', 'docs', 'audios', 'tags', 'images', 'maps', 'trending', 'events', 'tasks', 'market', 'tribes', 'larp', 'votes', 'reports', 'opinions', 'pads', 'transfers', 'feed', 'pixelia', 'melody', 'agenda', 'favorites', 'ai', 'forum', 'games', 'housing', 'jobs', 'projects', 'industry', 'shops', 'banking', 'school', 'parliament', 'courts'];
|
||||||
const cfg = getConfig().modules;
|
const cfg = getConfig().modules;
|
||||||
ctx.body = modulesView(modules.reduce((acc, m) => { acc[`${m}Mod`] = cfg[`${m}Mod`]; return acc; }, {}));
|
ctx.body = modulesView(modules.reduce((acc, m) => { acc[`${m}Mod`] = cfg[`${m}Mod`]; return acc; }, {}), { blocked: takeBlockedMod(), saved: ctx.query.saved === '1' });
|
||||||
|
})
|
||||||
|
// activar un solo modulo desde el aviso de /modules y volver a donde se queria ir
|
||||||
|
.post("/modules/enable", koaBody(), async (ctx) => {
|
||||||
|
if (!isLoopbackRequest(ctx)) { ctx.status = 403; ctx.body = ''; return; }
|
||||||
|
const mod = String(ctx.request.body.mod || '').replace(/Mod$/, '');
|
||||||
|
if (!workflowsModel.ALL_MODULES.includes(mod)) { ctx.redirect('/modules'); return; }
|
||||||
|
const cfg = getConfig();
|
||||||
|
cfg.modules[`${mod}Mod`] = 'on';
|
||||||
|
saveConfig(cfg);
|
||||||
|
const back = String(ctx.request.body.returnTo || '');
|
||||||
|
ctx.redirect(isLocalPath(back) ? back : `/${mod}`);
|
||||||
})
|
})
|
||||||
.get('/ai', async (ctx) => {
|
.get('/ai', async (ctx) => {
|
||||||
if (!checkMod(ctx, 'aiMod')) return ctx.redirect('/modules');
|
if (!checkMod(ctx, 'aiMod')) return ctx.redirect('/modules');
|
||||||
|
|
@ -15198,7 +15223,7 @@ router
|
||||||
MOBILE_UNAVAILABLE_MODS.forEach(mod => cfg.modules[`${mod}Mod`] = 'off');
|
MOBILE_UNAVAILABLE_MODS.forEach(mod => cfg.modules[`${mod}Mod`] = 'off');
|
||||||
}
|
}
|
||||||
saveConfig(cfg);
|
saveConfig(cfg);
|
||||||
ctx.redirect(`/modules`);
|
ctx.redirect(process.env.OASIS_MOBILE === '1' ? `/modules?saved=1` : `/modules`);
|
||||||
})
|
})
|
||||||
.post("/settings/ai", koaBody(), async (ctx) => {
|
.post("/settings/ai", koaBody(), async (ctx) => {
|
||||||
const aiPrompt = String(ctx.request.body.ai_prompt || "").trim();
|
const aiPrompt = String(ctx.request.body.ai_prompt || "").trim();
|
||||||
|
|
|
||||||
413
src/client/assets/styles/mobile_ux.css
Normal file
413
src/client/assets/styles/mobile_ux.css
Normal file
|
|
@ -0,0 +1,413 @@
|
||||||
|
/* Piezas de usabilidad de la rama movil, para los dos skins (Acampada y OasisMobile).
|
||||||
|
*
|
||||||
|
* Va la ultima, detras de karvan.css. Los colores salen de las variables de
|
||||||
|
* Acampada (--a26-*) si el tema las define y, si no, de la paleta oscura con oro
|
||||||
|
* de OasisMobile; asi cada pieza nueva se ve bien en los dos sin duplicarla.
|
||||||
|
*
|
||||||
|
* Letra: Anton (--ux-title) solo para titulos y para el boton principal de cada
|
||||||
|
* pantalla; todo lo demas (fichas, botones secundarios, etiquetas) en la letra del
|
||||||
|
* sistema en negrita. Antes habia Anton en botones, fichas, cabeceras de tabla y
|
||||||
|
* pestañas, y la pantalla parecia un cartel.
|
||||||
|
*
|
||||||
|
* Espacios: rejilla de 4/8. Zonas tactiles de 44px como minimo. */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--ux-bg: var(--a26-bg, #121212);
|
||||||
|
--ux-fg: var(--a26-fg, #f2f2f2);
|
||||||
|
--ux-accent: var(--a26-neon, #ffd700);
|
||||||
|
--ux-on-accent: #000;
|
||||||
|
--ux-soft: var(--a26-soft, #1e1e1e);
|
||||||
|
--ux-muted: var(--a26-muted, #a8a8a8);
|
||||||
|
--ux-line: var(--a26-fg, #3a3a3a);
|
||||||
|
--ux-radius: var(--a26-radius, 12px);
|
||||||
|
--ux-title: var(--a26-tit, inherit);
|
||||||
|
--ux-text: var(--a26-txt, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif);
|
||||||
|
--ux-danger: #c4003b;
|
||||||
|
--ux-ok: #0a8f3c;
|
||||||
|
--ux-warn: #c98a00;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- letra unificada ---------- */
|
||||||
|
/* fichas de filtro, pestañas y botones secundarios: letra del sistema */
|
||||||
|
.main-column :is(.tribe-mode-buttons, .mode-buttons, .mode-buttons-row, .filter-group, .filters) .filter-btn:not(.create-button),
|
||||||
|
.main-column .filter-box__button, .main-column .doc-export-actions .filter-btn,
|
||||||
|
.main-column .update-btn, .main-column .tribe-action-btn, .lang-chip, .kc-btn,
|
||||||
|
.main-column table th, .card-chips-row > span, .pm-exposition-text {
|
||||||
|
font-family: var(--ux-text) !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
letter-spacing: .02em !important;
|
||||||
|
}
|
||||||
|
.main-column :is(.tribe-mode-buttons, .mode-buttons, .mode-buttons-row, .filter-group, .filters) .filter-btn:not(.create-button) {
|
||||||
|
font-size: 15px !important;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
/* los formularios de cada modulo (crear tribu, votacion, sala...): etiquetas como
|
||||||
|
texto, no como titulos; el h2 de la pregunta de una votacion era Anton de 28px */
|
||||||
|
.main-column .vote-form h2, .main-column form > label:not(.module-switch):not(.vote-kind-option), .main-column form label[for]:not(.module-switch) {
|
||||||
|
font-family: var(--ux-text) !important;
|
||||||
|
font-size: 15px !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
text-transform: none !important;
|
||||||
|
letter-spacing: 0 !important;
|
||||||
|
color: var(--ux-fg) !important;
|
||||||
|
}
|
||||||
|
.form-hint {
|
||||||
|
margin: 6px 0 14px !important;
|
||||||
|
font-size: 14px !important;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: var(--ux-muted) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* el boton de enviar de los formularios: a todo lo ancho, abajo, que se vea que es
|
||||||
|
el final. Antes unos centrados, otros a la izquierda y otros enteros */
|
||||||
|
.main-column :is(.vote-form, .tribe-form, .karvan-create, .karvan-compose, .karvan-invite) form > button[type="submit"],
|
||||||
|
.main-column :is(.vote-form, .karvan-create, .karvan-compose, .karvan-invite) > form > button[type="submit"],
|
||||||
|
.main-column form[action$="/tribes/create"][method="post"] > button[type="submit"],
|
||||||
|
.main-column form[action*="/tribes/update/"] > button[type="submit"] {
|
||||||
|
display: block !important;
|
||||||
|
width: 100% !important;
|
||||||
|
margin: 8px 0 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- iconos SVG (lanzador, barra, modulos, Karvan) ---------- */
|
||||||
|
.has-svg { display: inline-flex !important; align-items: center; justify-content: center; }
|
||||||
|
.has-svg svg { width: 100%; height: 100%; display: block; }
|
||||||
|
.ln-ico.has-svg { padding: 15px !important; }
|
||||||
|
.ln-ico.has-svg svg { width: 30px; height: 30px; }
|
||||||
|
.oasis-bottombar .bb-ico.has-svg {
|
||||||
|
width: 56px !important; height: 30px !important; padding: 3px 0 !important;
|
||||||
|
border-radius: 16px !important; font-size: 0 !important;
|
||||||
|
}
|
||||||
|
.oasis-bottombar .bb-ico.has-svg svg { width: 24px; height: 24px; }
|
||||||
|
.bb-manage-ico.has-svg, .bb-pick-ico.has-svg { width: 24px; height: 24px; }
|
||||||
|
|
||||||
|
/* ---------- barra de abajo: la pestaña actual con pastilla, como en Android ---------- */
|
||||||
|
.oasis-bottombar a.bb-item.current .bb-ico.has-svg { background: var(--ux-accent) !important; color: var(--ux-on-accent) !important; }
|
||||||
|
.oasis-bottombar .bb-lbl { font-family: var(--ux-text) !important; }
|
||||||
|
/* con el teclado abierto la ventana encoge: la barra se esconde y el formulario
|
||||||
|
recupera ese tercio de pantalla */
|
||||||
|
@media (max-height: 520px) {
|
||||||
|
.oasis-bottombar { display: none !important; }
|
||||||
|
body { padding-bottom: 0 !important; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- aviso de bienvenida: una linea, no un bloque ---------- */
|
||||||
|
.welcome-banner {
|
||||||
|
display: flex !important; align-items: center !important; gap: 10px !important;
|
||||||
|
padding: 8px 8px 8px 14px !important; min-height: 0 !important;
|
||||||
|
}
|
||||||
|
.welcome-banner .update-banner-icon { display: none !important; }
|
||||||
|
.welcome-banner .update-banner-text { flex: 1 1 auto; font-size: 15px !important; line-height: 1.3 !important; margin: 0 !important; }
|
||||||
|
.welcome-banner .welcome-banner-start {
|
||||||
|
flex: 0 0 auto; font-size: 16px !important; min-height: 40px !important;
|
||||||
|
padding: 6px 14px !important; margin: 0 !important; width: auto !important;
|
||||||
|
}
|
||||||
|
.welcome-banner .welcome-banner-close { margin: 0 !important; flex: 0 0 auto; }
|
||||||
|
.welcome-banner .welcome-banner-close-btn { min-height: 40px !important; width: 36px !important; margin: 0 !important; }
|
||||||
|
|
||||||
|
/* ---------- pie de una linea ---------- */
|
||||||
|
.oasis-footer.oasis-footer-mobile {
|
||||||
|
display: flex !important; flex-direction: column; align-items: center; gap: 4px;
|
||||||
|
padding: 14px 16px 18px !important; margin-top: 24px !important; text-align: center;
|
||||||
|
font-size: 13px !important;
|
||||||
|
}
|
||||||
|
.oasis-footer-mobile .footer-sync {
|
||||||
|
display: inline-flex; align-items: center; gap: 8px; min-height: 36px;
|
||||||
|
font-weight: 700; text-decoration: none !important; color: var(--ux-fg) !important;
|
||||||
|
}
|
||||||
|
.oasis-footer-mobile .footer-sync-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--ux-muted); }
|
||||||
|
.oasis-footer-mobile .footer-sync.is-on .footer-sync-dot { background: var(--ux-ok); }
|
||||||
|
.oasis-footer-mobile .footer-version { color: var(--ux-muted) !important; }
|
||||||
|
|
||||||
|
/* ---------- acciones de tarjeta: borrar con confirmacion ---------- */
|
||||||
|
.content-action-confirm { display: inline-flex; flex-wrap: wrap; justify-content: flex-end; }
|
||||||
|
.content-action-confirm > summary { list-style: none; cursor: pointer; }
|
||||||
|
.content-action-confirm > summary::-webkit-details-marker { display: none; }
|
||||||
|
.content-action-confirm[open] { flex-basis: 100%; order: 99; }
|
||||||
|
.content-action-confirm[open] > summary { background: var(--ux-danger) !important; color: #fff !important; border-color: var(--ux-danger) !important; }
|
||||||
|
.content-action-confirm .confirm-bar {
|
||||||
|
flex-basis: 100%; display: flex; align-items: center; justify-content: flex-end; gap: 10px;
|
||||||
|
margin-top: 8px; padding: 8px 8px 8px 12px; border: 2px solid var(--ux-danger); border-radius: var(--ux-radius);
|
||||||
|
}
|
||||||
|
.content-action-confirm .confirm-question { flex: 1 1 auto; font-weight: 700; font-size: 15px; text-align: left; }
|
||||||
|
.content-action-confirm form { margin: 0 !important; width: auto !important; }
|
||||||
|
.main-column .content-action-confirm .confirm-yes, .content-action-confirm .confirm-yes {
|
||||||
|
background: var(--ux-danger) !important; color: #fff !important; border-color: var(--ux-danger) !important;
|
||||||
|
box-shadow: none !important; min-height: 44px !important; margin: 0 !important; width: auto !important;
|
||||||
|
font-family: var(--ux-text) !important; font-weight: 700 !important; font-size: 15px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- lista de tribus: una fila por tribu ---------- */
|
||||||
|
.main-column .tribe-list { display: flex !important; flex-direction: column; gap: 10px; padding: 0 !important; }
|
||||||
|
.main-column a.tribe-row {
|
||||||
|
display: flex !important; flex-wrap: nowrap !important; align-items: center; gap: 12px; width: auto !important;
|
||||||
|
padding: 10px 12px !important; min-height: 72px;
|
||||||
|
border: 2px solid var(--ux-line) !important; border-radius: var(--ux-radius) !important;
|
||||||
|
background: var(--ux-bg) !important; color: var(--ux-fg) !important; text-decoration: none !important;
|
||||||
|
}
|
||||||
|
.main-column a.tribe-row:active { background: var(--ux-accent) !important; color: var(--ux-on-accent) !important; }
|
||||||
|
.tribe-row .tribe-row-thumb {
|
||||||
|
flex: 0 0 52px; width: 52px !important; height: 52px !important; border-radius: 10px !important;
|
||||||
|
object-fit: cover; border: 2px solid var(--ux-line) !important; margin: 0 !important;
|
||||||
|
}
|
||||||
|
.tribe-row .tribe-row-icon { padding: 11px; background: var(--ux-soft); }
|
||||||
|
.tribe-row .tribe-row-text { flex: 1 1 0; min-width: 0; overflow: hidden; display: flex; flex-direction: column; gap: 2px; }
|
||||||
|
.tribe-row .tribe-row-text > span { display: block; max-width: 100%; margin: 0 !important; padding: 0 !important; line-height: 1.3; }
|
||||||
|
.tribe-row .tribe-row-title {
|
||||||
|
font-family: var(--ux-title) !important; font-size: 21px; line-height: 1.1; text-transform: uppercase;
|
||||||
|
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||||
|
}
|
||||||
|
.tribe-row .tribe-row-facts { font-size: 13px; font-weight: 700; color: var(--ux-muted); }
|
||||||
|
.tribe-row .tribe-row-desc { font-size: 14px; color: var(--ux-fg); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.tribe-row .tribe-row-go { flex: 0 0 auto; font-size: 28px; line-height: 1; color: var(--ux-muted); }
|
||||||
|
|
||||||
|
/* ---------- votaciones ---------- */
|
||||||
|
.vote-bars { display: flex; flex-direction: column; gap: 10px; margin: 8px 0 4px; }
|
||||||
|
.vote-bar { display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 4px 10px; }
|
||||||
|
.vote-bar-label { font-weight: 700; font-size: 15px; }
|
||||||
|
.vote-bar-count { font-weight: 700; font-size: 15px; min-width: 2ch; text-align: right; }
|
||||||
|
.vote-bar-meter {
|
||||||
|
grid-column: 1 / -1; width: 100%; height: 12px; appearance: none; -webkit-appearance: none;
|
||||||
|
border: 2px solid var(--ux-line); border-radius: 8px; overflow: hidden; background: var(--ux-soft);
|
||||||
|
}
|
||||||
|
.vote-bar-meter::-webkit-progress-bar { background: var(--ux-soft); }
|
||||||
|
.vote-bar-meter::-webkit-progress-value { background: var(--ux-fg); }
|
||||||
|
.vote-bar-meter::-moz-progress-bar { background: var(--ux-fg); }
|
||||||
|
.vote-bar-yes .vote-bar-meter::-webkit-progress-value, .vote-bar-green .vote-bar-meter::-webkit-progress-value { background: var(--ux-ok); }
|
||||||
|
.vote-bar-no .vote-bar-meter::-webkit-progress-value, .vote-bar-red .vote-bar-meter::-webkit-progress-value { background: var(--ux-danger); }
|
||||||
|
.vote-bar-yellow .vote-bar-meter::-webkit-progress-value { background: var(--ux-warn); }
|
||||||
|
.vote-own-note {
|
||||||
|
padding: 12px 14px !important; border-left: 4px solid var(--ux-accent) !important;
|
||||||
|
background: var(--ux-soft) !important; border-radius: 0 10px 10px 0; font-size: 15px !important;
|
||||||
|
}
|
||||||
|
/* botones de votar: rejilla de dos columnas, cada opcion un boton grande */
|
||||||
|
.main-column .vote-buttons-row-single { display: grid !important; grid-template-columns: 1fr 1fr; gap: 10px !important; }
|
||||||
|
.main-column .vote-buttons-row-single form { margin: 0 !important; width: 100% !important; }
|
||||||
|
.main-column .vote-buttons-row-single button {
|
||||||
|
width: 100% !important; min-height: 52px !important; margin: 0 !important;
|
||||||
|
font-family: var(--ux-text) !important; font-weight: 700 !important; font-size: 16px !important;
|
||||||
|
}
|
||||||
|
.main-column .vote-buttons-row-single button[value="YES"], .main-column .vote-buttons-row-single button[value="GREEN"] { border-color: var(--ux-ok) !important; box-shadow: 0 4px 0 var(--ux-ok) !important; }
|
||||||
|
.main-column .vote-buttons-row-single button[value="NO"], .main-column .vote-buttons-row-single button[value="RED"] { border-color: var(--ux-danger) !important; box-shadow: 0 4px 0 var(--ux-danger) !important; }
|
||||||
|
.main-column .vote-buttons-row-single button[value="YELLOW"] { border-color: var(--ux-warn) !important; box-shadow: 0 4px 0 var(--ux-warn) !important; }
|
||||||
|
/* tipo de votacion: tarjetas de radio */
|
||||||
|
.vote-kind { display: flex; flex-direction: column; gap: 10px; margin: 4px 0 18px; }
|
||||||
|
.vote-kind-title { font-weight: 700; font-size: 15px; }
|
||||||
|
.vote-kind-option {
|
||||||
|
display: grid !important; grid-template-columns: auto 1fr; gap: 2px 12px; align-items: center;
|
||||||
|
padding: 12px 14px; border: 2px solid var(--ux-line); border-radius: var(--ux-radius); cursor: pointer;
|
||||||
|
}
|
||||||
|
.main-column .vote-kind-option input[type="radio"] { grid-row: 1 / span 2; width: 22px !important; height: 22px !important; min-height: 0 !important; margin: 0 !important; accent-color: var(--ux-fg); }
|
||||||
|
.vote-kind-option:has(input:checked) { background: var(--ux-accent); color: var(--ux-on-accent); }
|
||||||
|
.vote-kind-option:has(input:checked) .vote-kind-hint { color: var(--ux-on-accent); }
|
||||||
|
.vote-kind-name { font-weight: 700; font-size: 17px; }
|
||||||
|
.vote-kind-hint { font-size: 14px; color: var(--ux-muted); }
|
||||||
|
|
||||||
|
/* ---------- modulos: interruptores agrupados ---------- */
|
||||||
|
.module-switches { display: flex; flex-direction: column; gap: 22px; padding-bottom: 8px; }
|
||||||
|
.module-group { display: flex; flex-direction: column; gap: 8px; }
|
||||||
|
.module-group-title {
|
||||||
|
font-family: var(--ux-title) !important; font-weight: 400 !important; font-size: 20px !important;
|
||||||
|
text-transform: uppercase; margin: 0 0 4px !important; padding-bottom: 4px; border-bottom: 3px solid var(--ux-line);
|
||||||
|
}
|
||||||
|
.main-column label.module-switch {
|
||||||
|
display: grid !important; grid-template-columns: 40px minmax(0, 1fr) 52px; align-items: center; gap: 12px;
|
||||||
|
padding: 10px 12px; min-height: 64px; cursor: pointer;
|
||||||
|
border: 2px solid var(--ux-line); border-radius: var(--ux-radius);
|
||||||
|
font-family: var(--ux-text) !important; text-transform: none !important; letter-spacing: 0 !important;
|
||||||
|
}
|
||||||
|
.module-switch-ico { width: 40px; height: 40px; padding: 8px; border-radius: 10px; background: var(--ux-soft); color: var(--ux-fg); }
|
||||||
|
.module-switch-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
||||||
|
.module-switch-name { font-weight: 700; font-size: 17px; }
|
||||||
|
.module-switch-desc { font-size: 13px; color: var(--ux-muted); line-height: 1.3; }
|
||||||
|
.module-switch:has(.switch-input:checked) { border-color: var(--ux-fg); }
|
||||||
|
/* el interruptor: la casilla de siempre con forma de pastilla */
|
||||||
|
.main-column label.module-switch input.switch-input[type="checkbox"] {
|
||||||
|
appearance: none; -webkit-appearance: none; flex: 0 0 auto;
|
||||||
|
width: 52px !important; height: 30px !important; min-height: 0 !important; margin: 0 !important; padding: 0 !important;
|
||||||
|
border: 2px solid var(--ux-line) !important; border-radius: 16px !important; background: var(--ux-soft) !important;
|
||||||
|
position: relative; cursor: pointer; transition: background .15s ease-out;
|
||||||
|
}
|
||||||
|
.main-column label.module-switch input.switch-input[type="checkbox"]::after {
|
||||||
|
content: ""; position: absolute; top: 3px; left: 3px; width: 20px; height: 20px; border-radius: 50%;
|
||||||
|
background: var(--ux-muted); transition: transform .15s ease-out, background .15s ease-out;
|
||||||
|
}
|
||||||
|
.main-column label.module-switch input.switch-input[type="checkbox"]:checked { background: var(--ux-accent) !important; }
|
||||||
|
.main-column label.module-switch input.switch-input[type="checkbox"]:checked::after { transform: translateX(22px); background: var(--ux-on-accent); }
|
||||||
|
/* Guardar siempre a la vista, encima de la barra de abajo */
|
||||||
|
.module-save-bar {
|
||||||
|
position: sticky; bottom: calc(76px + env(safe-area-inset-bottom, 0px)); z-index: 5;
|
||||||
|
padding: 10px 0; background: var(--ux-bg);
|
||||||
|
}
|
||||||
|
.module-save-bar .create-button { width: 100% !important; margin: 0 !important; }
|
||||||
|
.module-saved {
|
||||||
|
padding: 10px 14px !important; margin: 0 0 14px !important; font-weight: 700;
|
||||||
|
border-left: 4px solid var(--ux-ok) !important; background: var(--ux-soft) !important; border-radius: 0 10px 10px 0;
|
||||||
|
}
|
||||||
|
/* la seccion apagada a la que se queria entrar */
|
||||||
|
.main-column div.module-blocked {
|
||||||
|
display: grid; grid-template-columns: 48px 1fr; gap: 12px; align-items: center;
|
||||||
|
padding: 14px; margin: 0 0 20px; border: 3px solid var(--ux-line); border-radius: var(--ux-radius); background: var(--ux-soft);
|
||||||
|
}
|
||||||
|
.module-blocked-ico { width: 48px; height: 48px; padding: 10px; border-radius: 12px; background: var(--ux-bg); }
|
||||||
|
.module-blocked-text { margin: 0 !important; font-weight: 700; font-size: 17px; }
|
||||||
|
.module-blocked form { grid-column: 1 / -1; margin: 0 !important; }
|
||||||
|
.module-blocked .create-button { width: 100% !important; margin: 0 !important; }
|
||||||
|
|
||||||
|
/* ---------- Ajustes: idiomas como fichas de un toque ---------- */
|
||||||
|
.lang-pick { display: grid !important; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 12px; }
|
||||||
|
.main-column .lang-pick .lang-chip, .lang-pick .lang-chip {
|
||||||
|
width: 100% !important; min-height: 48px !important; margin: 0 !important; padding: 6px 10px !important;
|
||||||
|
font-size: 16px !important; text-transform: none !important; box-shadow: none !important;
|
||||||
|
border: 2px solid var(--ux-line) !important; background: var(--ux-bg) !important; color: var(--ux-fg) !important;
|
||||||
|
}
|
||||||
|
.main-column .lang-pick .lang-chip.is-current, .lang-pick .lang-chip.is-current { background: var(--ux-accent) !important; color: var(--ux-on-accent) !important; }
|
||||||
|
|
||||||
|
/* ---------- Karvan: barra de llamada con nombre en cada boton ---------- */
|
||||||
|
.karvan-call-bar { display: grid !important; grid-template-columns: repeat(4, 1fr); gap: 8px !important; }
|
||||||
|
.main-column .karvan-call-bar .kc-btn, .karvan-call-bar .kc-btn {
|
||||||
|
display: flex !important; flex-direction: column; align-items: center; justify-content: center; gap: 4px;
|
||||||
|
width: 100% !important; min-height: 64px !important; padding: 8px 4px !important; margin: 0 !important;
|
||||||
|
font-size: 13px !important; text-transform: none !important; box-shadow: none !important;
|
||||||
|
}
|
||||||
|
.kc-btn .kc-ico { width: 24px; height: 24px; }
|
||||||
|
.kc-btn .kc-lbl { line-height: 1.1; }
|
||||||
|
.main-column .karvan-call-bar .kc-btn-start { background: var(--ux-ok) !important; color: #fff !important; border-color: var(--ux-ok) !important; }
|
||||||
|
.main-column .karvan-call-bar .kc-btn.on { background: var(--ux-accent) !important; color: var(--ux-on-accent) !important; }
|
||||||
|
.main-column .karvan-call-bar .kc-btn-stop { color: var(--ux-danger) !important; border-color: var(--ux-danger) !important; }
|
||||||
|
.kc-status:not(:empty) { margin-top: 10px; padding: 8px 12px; border-radius: 10px; background: var(--ux-soft); font-size: 14px; font-weight: 700; }
|
||||||
|
.karvan-invite { margin-top: 24px !important; padding-top: 16px !important; border-top: 2px solid var(--ux-line) !important; }
|
||||||
|
.karvan-invite-lbl { display: block; margin-bottom: 6px; font-weight: 700; }
|
||||||
|
|
||||||
|
/* ---------- foco visible para teclado y lectores ---------- */
|
||||||
|
a:focus-visible, button:focus-visible, summary:focus-visible, label.module-switch:focus-within {
|
||||||
|
outline: 3px solid var(--ux-accent) !important; outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.main-column label.module-switch input.switch-input[type="checkbox"], .main-column label.module-switch input.switch-input[type="checkbox"]::after { transition: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- buscador de cada modulo: campo y lupa en la misma linea ---------- */
|
||||||
|
/* solo cuando el unico control es el boton: los de Transferencias llevan rangos y
|
||||||
|
desplegables y siguen en columna */
|
||||||
|
.main-column form.filter-box:not(:has(.filter-box__select, .filter-box__number, .transfer-range)) {
|
||||||
|
display: flex !important; flex-direction: row !important; flex-wrap: nowrap !important;
|
||||||
|
align-items: stretch !important; gap: 8px !important; width: 100% !important; margin: 0 0 12px !important;
|
||||||
|
}
|
||||||
|
.main-column form.filter-box:not(:has(.filter-box__select, .filter-box__number, .transfer-range)) .filter-box__input {
|
||||||
|
flex: 1 1 auto !important; min-width: 0 !important; width: 0 !important; height: 48px !important; margin: 0 !important;
|
||||||
|
}
|
||||||
|
.main-column form.filter-box:not(:has(.filter-box__select, .filter-box__number, .transfer-range)) .filter-box__controls {
|
||||||
|
flex: 0 0 auto !important; width: auto !important; margin: 0 !important;
|
||||||
|
}
|
||||||
|
.main-column form.filter-box .filter-box__button { min-height: 48px !important; margin: 0 !important; }
|
||||||
|
/* el recuento (TOTAL: 12 · PUBLICA: 12) como una linea discreta, no un bloque */
|
||||||
|
.main-column .activity-toolbar-row { display: flex !important; flex-direction: column !important; align-items: stretch !important; gap: 6px !important; }
|
||||||
|
.main-column .module-stats, .main-column [class*="module-stats"] { font-size: 13px !important; color: var(--ux-muted) !important; text-align: left !important; margin: 0 !important; }
|
||||||
|
|
||||||
|
/* el boton de crear de cada modulo a todo lo ancho, tambien cuando va solo en su
|
||||||
|
formulario (en Votaciones salia estrecho y en Tribus entero) */
|
||||||
|
.main-column .filters > form:has(.create-button), .main-column .mode-buttons > form:has(.create-button) { width: 100% !important; }
|
||||||
|
|
||||||
|
/* jerga fuera del movil: la tasa ECO (microgramos de CO2) no se entendia y su ficha
|
||||||
|
salia ilegible; sigue en Estadisticas */
|
||||||
|
.main-column .eco-tax-chip, .main-column [class*="eco-tax-chip"] { display: none !important; }
|
||||||
|
/* etiquetas (#asamblea) sin caja negra detras */
|
||||||
|
.main-column .card-tags, .main-column .tribe-side .card-tags { background: transparent !important; padding: 0 !important; margin: 8px 0 !important; }
|
||||||
|
|
||||||
|
/* acciones secundarias de una ficha (actualizar, PDF, compartir): una rejilla de
|
||||||
|
dos columnas, sin los huecos de 80px que dejaban los formularios sueltos */
|
||||||
|
.main-column :is(.tribe-side-actions, .doc-export-actions) {
|
||||||
|
display: grid !important; grid-template-columns: 1fr 1fr; gap: 8px !important; margin: 12px 0 !important; padding: 0 !important;
|
||||||
|
}
|
||||||
|
.main-column :is(.tribe-side-actions, .doc-export-actions) form { margin: 0 !important; width: 100% !important; padding: 0 !important; }
|
||||||
|
.main-column :is(.tribe-side-actions, .doc-export-actions) :is(button, .filter-btn, a) {
|
||||||
|
width: 100% !important; min-height: 44px !important; margin: 0 !important; padding: 6px 8px !important;
|
||||||
|
font-family: var(--ux-text) !important; font-weight: 700 !important; font-size: 14px !important; box-shadow: none !important;
|
||||||
|
}
|
||||||
|
.main-column .tribe-side-actions + .doc-export-actions { margin-top: 0 !important; }
|
||||||
|
/* resultado de la votacion: ficha de color legible en los dos skins */
|
||||||
|
.main-column .vote-outcome {
|
||||||
|
display: inline-block; padding: 2px 10px !important; border-radius: 8px;
|
||||||
|
font-family: var(--ux-text) !important; font-size: 15px !important; font-weight: 700 !important;
|
||||||
|
background: var(--ux-soft) !important; color: var(--ux-fg) !important; vertical-align: middle;
|
||||||
|
}
|
||||||
|
.main-column .vote-outcome-yes { background: var(--ux-ok) !important; color: #fff !important; }
|
||||||
|
.main-column .vote-outcome-no { background: var(--ux-danger) !important; color: #fff !important; }
|
||||||
|
.main-column .job-section-title:has(.vote-outcome) { display: flex !important; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||||||
|
|
||||||
|
/* ---------- margenes: uno solo, de 14px ---------- */
|
||||||
|
/* se sumaban capa a capa (contenido 20 + columna 10 + ficha 24 + lateral 20 = 74px
|
||||||
|
por lado) y el texto de una votacion o una tribu se quedaba en 264 de 412px */
|
||||||
|
body .main-content { padding-left: 14px !important; padding-right: 14px !important; }
|
||||||
|
body .main-content > .main-column { padding-left: 0 !important; padding-right: 0 !important; }
|
||||||
|
.main-column :is(.tribe-details, .tribe-side, .tribe-main, .job-section, .vote-form, .tribe-section, .tribe-content) {
|
||||||
|
padding-left: 0 !important; padding-right: 0 !important; margin-left: 0 !important; margin-right: 0 !important;
|
||||||
|
width: 100% !important; max-width: 100% !important; box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.main-column .tribe-details { display: flex !important; flex-direction: column !important; gap: 18px; }
|
||||||
|
/* dentro de una ficha, un solo ritmo: 10px entre bloques (antes hueco de 16 + los
|
||||||
|
margenes de cada bloque + 20 de relleno, unos 70px entre secciones) */
|
||||||
|
.main-column .tribe-side { gap: 10px !important; padding-top: 0 !important; padding-bottom: 0 !important; }
|
||||||
|
.main-column .tribe-side > *, .main-column .tribe-main > * { margin-top: 0 !important; margin-bottom: 0 !important; }
|
||||||
|
.main-column .tribe-main { display: flex !important; flex-direction: column; gap: 14px !important; }
|
||||||
|
.main-column .tribe-card-members { text-align: left !important; padding: 0 !important; font-weight: 700; }
|
||||||
|
.main-column :is(.tribe-side-actions, .doc-export-actions) > :only-child { grid-column: 1 / -1; }
|
||||||
|
/* la imagen generica de tribu no aporta nada y ocupaba 300px: solo se ve la propia */
|
||||||
|
.main-column img[src*="default-tribe"] { display: none !important; }
|
||||||
|
/* etiquetas del lanzador largas (Configuracion): se parten con guion */
|
||||||
|
.ln-lbl { hyphens: auto; -webkit-hyphens: auto; font-size: 12.5px !important; }
|
||||||
|
|
||||||
|
/* ---------- secciones de una tribu: tira de fichas deslizable + "Mas" ---------- */
|
||||||
|
.main-column .tribe-tabs { margin: 4px 0 12px !important; }
|
||||||
|
.tribe-tabs-strip {
|
||||||
|
display: flex; gap: 8px; overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch;
|
||||||
|
margin: 0; padding: 4px 0 8px; scroll-snap-type: x proximity;
|
||||||
|
}
|
||||||
|
.tribe-tabs-strip::-webkit-scrollbar { display: none; }
|
||||||
|
.main-column a.tribe-tab, .tribe-tabs-more > summary {
|
||||||
|
flex: 0 0 auto; scroll-snap-align: start; display: inline-flex; align-items: center; justify-content: center;
|
||||||
|
min-height: 40px; padding: 4px 14px; border: 2px solid var(--ux-line) !important; border-radius: 20px !important;
|
||||||
|
background: var(--ux-bg) !important; color: var(--ux-fg) !important; text-decoration: none !important;
|
||||||
|
font-family: var(--ux-text) !important; font-weight: 700 !important; font-size: 14px !important; white-space: nowrap;
|
||||||
|
text-transform: uppercase; letter-spacing: .02em;
|
||||||
|
}
|
||||||
|
.main-column a.tribe-tab.is-current { background: var(--ux-accent) !important; color: var(--ux-on-accent) !important; }
|
||||||
|
.tribe-tabs-more > summary { list-style: none; cursor: pointer; margin-top: 2px; }
|
||||||
|
.tribe-tabs-more > summary::-webkit-details-marker { display: none; }
|
||||||
|
.tribe-tabs-more > summary::after { content: " ▾"; }
|
||||||
|
.tribe-tabs-more[open] > summary::after { content: " ▴"; }
|
||||||
|
.tribe-tabs-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 10px; }
|
||||||
|
.tribe-tabs-grid .tribe-tab { width: 100%; }
|
||||||
|
/* filas etiqueta / valor de las fichas (plazo, estado, creado el): en dos columnas */
|
||||||
|
.main-column table.tribe-info-table tr { display: grid !important; grid-template-columns: minmax(96px, 38%) 1fr; align-items: center; }
|
||||||
|
.main-column table.tribe-info-table td { display: block !important; border: 0 !important; padding: 8px 10px !important; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
.main-column table.tribe-info-table tr + tr { border-top: 1px solid #ccc !important; }
|
||||||
|
|
||||||
|
/* bienvenida: "Ahora no" al final, como enlace discreto */
|
||||||
|
.main-column .welcome-skip-end { text-align: center; margin: 8px 0 0 !important; }
|
||||||
|
.main-column .welcome-skip-end .welcome-skip-btn {
|
||||||
|
background: transparent !important; border: 0 !important; box-shadow: none !important;
|
||||||
|
font-family: var(--ux-text) !important; font-weight: 700 !important; font-size: 15px !important;
|
||||||
|
text-transform: none !important; text-decoration: underline; color: var(--ux-muted) !important; width: auto !important;
|
||||||
|
}
|
||||||
|
/* buzon y otros: los botones de filtro sueltos (Mas recientes, Marcar como leido)
|
||||||
|
en la letra de texto, como el resto de fichas */
|
||||||
|
.main-column .inbox-filters button, .main-column [class*="inbox"] .filter-btn:not(.create-button),
|
||||||
|
.main-column form[action^="/inbox"] button:not(.create-button) {
|
||||||
|
font-family: var(--ux-text) !important; font-weight: 700 !important; font-size: 15px !important; letter-spacing: .02em !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- regla general de letra para botones ---------- */
|
||||||
|
/* todo boton en letra del sistema; Anton solo en el principal de cada pantalla
|
||||||
|
(el negro, .create-button) y en las tarjetas de aspecto */
|
||||||
|
.main-column :is(button, input[type="submit"], input[type="button"], .filter-btn, a.filter-btn):not(.create-button):not(.welcome-banner-start):not(.skin-card):not(.workflow-card):not(.btn-singleview) {
|
||||||
|
font-family: var(--ux-text) !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
letter-spacing: .02em !important;
|
||||||
|
}
|
||||||
|
.main-column :is(button, input[type="submit"]):not(.create-button):not(.filter-btn):not(.btn-singleview):not(.chat-react-btn):not(.lang-chip):not(.kc-btn):not(.confirm-yes):not(.welcome-skip-btn) {
|
||||||
|
font-size: 16px !important;
|
||||||
|
}
|
||||||
|
.main-column form.welcome-skip-end { display: flex !important; justify-content: center !important; }
|
||||||
|
|
@ -17,6 +17,47 @@
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
en: {
|
en: {
|
||||||
|
// usabilidad (19/10/2026): votaciones, borrar, modulos, Karvan, pie
|
||||||
|
tribeSectionsMore: "More",
|
||||||
|
tribeSectionsLabel: "Sections",
|
||||||
|
voteYes: "Yes",
|
||||||
|
voteNo: "No",
|
||||||
|
voteAbstention: "Abstain",
|
||||||
|
voteGreen: "Green · agree",
|
||||||
|
voteYellow: "Yellow · doubts",
|
||||||
|
voteRed: "Red · against",
|
||||||
|
voteOwnNote: "You created this vote, so you cannot vote in it: whoever proposes stays neutral.",
|
||||||
|
voteKindLabel: "Type of vote",
|
||||||
|
voteKind_classic: "Yes / No",
|
||||||
|
voteKindHint_classic: "Yes, no, abstain and the usual options",
|
||||||
|
voteKind_traffic: "Traffic light",
|
||||||
|
voteKindHint_traffic: "Green agree · yellow doubts · red against, like in the assembly",
|
||||||
|
voteKind_scale: "Thermometer",
|
||||||
|
voteKindHint_scale: "From 1 (cold) to 5 (hot)",
|
||||||
|
deleteConfirmQuestion: "Delete it for good?",
|
||||||
|
deleteConfirmYes: "Yes, delete",
|
||||||
|
welcomeBannerTextMobile: "Add your name and photo so people know it is you.",
|
||||||
|
welcomeBannerActionMobile: "Set up",
|
||||||
|
footerPeersNone: "Not syncing with anyone yet",
|
||||||
|
footerPeersSome: "Syncing with {n}",
|
||||||
|
tribeMapUrlLabel: "Map link (optional)",
|
||||||
|
tribeHintPrivacy: "Private: only members see it, and it is encrypted. Public: the whole network sees it.",
|
||||||
|
tribeHintMode: "Open: any member can invite people. Strict: only whoever created it can invite.",
|
||||||
|
moduleBlockedText: "{name} is turned off.",
|
||||||
|
moduleBlockedAction: "Turn on {name}",
|
||||||
|
modulesOnCount: "{on} of {total} sections on",
|
||||||
|
modulesSaved: "Saved.",
|
||||||
|
karvanVoiceOn: "Voice on",
|
||||||
|
karvanCamOn: "Camera on",
|
||||||
|
karvanNoCam: "No camera permission: the call goes on with voice only.",
|
||||||
|
karvanNoMic: "No microphone permission. You can allow it in the phone settings; the chat still works.",
|
||||||
|
karvanMic: "Mic",
|
||||||
|
karvanCam: "Camera",
|
||||||
|
karvanHangUp: "Hang up",
|
||||||
|
karvanInviteLabel: "Invite someone",
|
||||||
|
karvanInviteHint: "Paste their @id: it is on their profile, under their name.",
|
||||||
|
karvanRelayHuman: "Through your node",
|
||||||
|
karvanP2pHuman: "Direct and encrypted",
|
||||||
// skins y workflows en el movil (settings_view.js)
|
// skins y workflows en el movil (settings_view.js)
|
||||||
skinIntroMobile: "Pick how Oasis looks on your phone. It applies as soon as you tap it.",
|
skinIntroMobile: "Pick how Oasis looks on your phone. It applies as soon as you tap it.",
|
||||||
workflowsDescriptionMobile: "A workflow sets which modules are on. On the phone your skin stays as it is.",
|
workflowsDescriptionMobile: "A workflow sets which modules are on. On the phone your skin stays as it is.",
|
||||||
|
|
@ -85,6 +126,47 @@ module.exports = {
|
||||||
spreadMore: "more"
|
spreadMore: "more"
|
||||||
},
|
},
|
||||||
es: {
|
es: {
|
||||||
|
// usabilidad (19/10/2026): votaciones, borrar, modulos, Karvan, pie
|
||||||
|
tribeSectionsMore: "Más",
|
||||||
|
tribeSectionsLabel: "Secciones",
|
||||||
|
voteYes: "Sí",
|
||||||
|
voteNo: "No",
|
||||||
|
voteAbstention: "Abstención",
|
||||||
|
voteGreen: "Verde · de acuerdo",
|
||||||
|
voteYellow: "Amarillo · dudas",
|
||||||
|
voteRed: "Rojo · en contra",
|
||||||
|
voteOwnNote: "Has creado esta votación, así que no puedes votar en ella: quien propone se queda neutral.",
|
||||||
|
voteKindLabel: "Tipo de votación",
|
||||||
|
voteKind_classic: "Sí / No",
|
||||||
|
voteKindHint_classic: "Sí, no, abstención y las opciones de siempre",
|
||||||
|
voteKind_traffic: "Semáforo",
|
||||||
|
voteKindHint_traffic: "Verde de acuerdo · amarillo dudas · rojo en contra, como en asamblea",
|
||||||
|
voteKind_scale: "Termómetro",
|
||||||
|
voteKindHint_scale: "Del 1 (frío) al 5 (caliente)",
|
||||||
|
deleteConfirmQuestion: "¿Borrar para siempre?",
|
||||||
|
deleteConfirmYes: "Sí, borrar",
|
||||||
|
welcomeBannerTextMobile: "Pon tu nombre y tu foto para que te reconozcan.",
|
||||||
|
welcomeBannerActionMobile: "Ponerlos",
|
||||||
|
footerPeersNone: "Aún no sincronizas con nadie",
|
||||||
|
footerPeersSome: "Sincronizando con {n}",
|
||||||
|
tribeMapUrlLabel: "Enlace del mapa (opcional)",
|
||||||
|
tribeHintPrivacy: "Privada: solo la ven sus miembros y va cifrada. Pública: la ve toda la red.",
|
||||||
|
tribeHintMode: "Abierta: cualquier miembro puede invitar. Estricta: solo invita quien la creó.",
|
||||||
|
moduleBlockedText: "{name} está desactivado.",
|
||||||
|
moduleBlockedAction: "Activar {name}",
|
||||||
|
modulesOnCount: "{on} de {total} secciones activas",
|
||||||
|
modulesSaved: "Guardado.",
|
||||||
|
karvanVoiceOn: "Voz activa",
|
||||||
|
karvanCamOn: "Cámara activa",
|
||||||
|
karvanNoCam: "Sin permiso de cámara: la llamada sigue solo con voz.",
|
||||||
|
karvanNoMic: "Sin permiso de micrófono. Puedes darlo en los ajustes del móvil; el chat sigue funcionando.",
|
||||||
|
karvanMic: "Micro",
|
||||||
|
karvanCam: "Cámara",
|
||||||
|
karvanHangUp: "Colgar",
|
||||||
|
karvanInviteLabel: "Invitar a alguien",
|
||||||
|
karvanInviteHint: "Pega su @id: está en su perfil, debajo del nombre.",
|
||||||
|
karvanRelayHuman: "A través de tu nodo",
|
||||||
|
karvanP2pHuman: "Directo y cifrado",
|
||||||
// skins y workflows en el movil (settings_view.js)
|
// skins y workflows en el movil (settings_view.js)
|
||||||
skinIntroMobile: "Elige cómo se ve Oasis en tu móvil. Se aplica al tocarlo.",
|
skinIntroMobile: "Elige cómo se ve Oasis en tu móvil. Se aplica al tocarlo.",
|
||||||
workflowsDescriptionMobile: "Un workflow fija qué módulos están activos. En el móvil tu skin se queda como está.",
|
workflowsDescriptionMobile: "Un workflow fija qué módulos están activos. En el móvil tu skin se queda como está.",
|
||||||
|
|
@ -159,6 +241,47 @@ module.exports = {
|
||||||
totalspreads: "Réplicas totales"
|
totalspreads: "Réplicas totales"
|
||||||
},
|
},
|
||||||
fr: {
|
fr: {
|
||||||
|
// usabilidad (19/10/2026): votaciones, borrar, modulos, Karvan, pie
|
||||||
|
tribeSectionsMore: "Plus",
|
||||||
|
tribeSectionsLabel: "Sections",
|
||||||
|
voteYes: "Oui",
|
||||||
|
voteNo: "Non",
|
||||||
|
voteAbstention: "Abstention",
|
||||||
|
voteGreen: "Vert · d'accord",
|
||||||
|
voteYellow: "Jaune · doutes",
|
||||||
|
voteRed: "Rouge · contre",
|
||||||
|
voteOwnNote: "Vous avez créé ce vote, vous ne pouvez donc pas y voter : qui propose reste neutre.",
|
||||||
|
voteKindLabel: "Type de vote",
|
||||||
|
voteKind_classic: "Oui / Non",
|
||||||
|
voteKindHint_classic: "Oui, non, abstention et les options habituelles",
|
||||||
|
voteKind_traffic: "Feu tricolore",
|
||||||
|
voteKindHint_traffic: "Vert d'accord · jaune doutes · rouge contre, comme en assemblée",
|
||||||
|
voteKind_scale: "Thermomètre",
|
||||||
|
voteKindHint_scale: "De 1 (froid) à 5 (chaud)",
|
||||||
|
deleteConfirmQuestion: "Supprimer définitivement ?",
|
||||||
|
deleteConfirmYes: "Oui, supprimer",
|
||||||
|
welcomeBannerTextMobile: "Ajoutez votre nom et votre photo pour qu'on vous reconnaisse.",
|
||||||
|
welcomeBannerActionMobile: "Ajouter",
|
||||||
|
footerPeersNone: "Aucune synchronisation pour l'instant",
|
||||||
|
footerPeersSome: "Synchronisé avec {n}",
|
||||||
|
tribeMapUrlLabel: "Lien de la carte (facultatif)",
|
||||||
|
tribeHintPrivacy: "Privée : seuls ses membres la voient, et elle est chiffrée. Publique : tout le réseau la voit.",
|
||||||
|
tribeHintMode: "Ouverte : tout membre peut inviter. Stricte : seule la personne qui l'a créée invite.",
|
||||||
|
moduleBlockedText: "{name} est désactivé.",
|
||||||
|
moduleBlockedAction: "Activer {name}",
|
||||||
|
modulesOnCount: "{on} sections actives sur {total}",
|
||||||
|
modulesSaved: "Enregistré.",
|
||||||
|
karvanVoiceOn: "Voix active",
|
||||||
|
karvanCamOn: "Caméra active",
|
||||||
|
karvanNoCam: "Pas d'accès à la caméra : l'appel continue en voix seule.",
|
||||||
|
karvanNoMic: "Pas d'accès au micro. Vous pouvez l'autoriser dans les réglages du téléphone ; le chat fonctionne toujours.",
|
||||||
|
karvanMic: "Micro",
|
||||||
|
karvanCam: "Caméra",
|
||||||
|
karvanHangUp: "Raccrocher",
|
||||||
|
karvanInviteLabel: "Inviter quelqu'un",
|
||||||
|
karvanInviteHint: "Collez son @id : il est sur son profil, sous son nom.",
|
||||||
|
karvanRelayHuman: "Via votre nœud",
|
||||||
|
karvanP2pHuman: "Direct et chiffré",
|
||||||
// skins y workflows en el movil (settings_view.js)
|
// skins y workflows en el movil (settings_view.js)
|
||||||
skinIntroMobile: "Choisis l'apparence d'Oasis sur ton téléphone. Elle s'applique dès que tu touches.",
|
skinIntroMobile: "Choisis l'apparence d'Oasis sur ton téléphone. Elle s'applique dès que tu touches.",
|
||||||
workflowsDescriptionMobile: "Un workflow fixe les modules actifs. Sur le téléphone, ton thème ne change pas.",
|
workflowsDescriptionMobile: "Un workflow fixe les modules actifs. Sur le téléphone, ton thème ne change pas.",
|
||||||
|
|
@ -227,6 +350,47 @@ module.exports = {
|
||||||
spreadMore: "plus"
|
spreadMore: "plus"
|
||||||
},
|
},
|
||||||
eu: {
|
eu: {
|
||||||
|
// usabilidad (19/10/2026): votaciones, borrar, modulos, Karvan, pie
|
||||||
|
tribeSectionsMore: "Gehiago",
|
||||||
|
tribeSectionsLabel: "Atalak",
|
||||||
|
voteYes: "Bai",
|
||||||
|
voteNo: "Ez",
|
||||||
|
voteAbstention: "Abstentzioa",
|
||||||
|
voteGreen: "Berdea · ados",
|
||||||
|
voteYellow: "Horia · zalantzak",
|
||||||
|
voteRed: "Gorria · aurka",
|
||||||
|
voteOwnNote: "Bozketa hau zuk sortu duzu, beraz ezin duzu bozkatu: proposatzen duena neutral geratzen da.",
|
||||||
|
voteKindLabel: "Bozketa mota",
|
||||||
|
voteKind_classic: "Bai / Ez",
|
||||||
|
voteKindHint_classic: "Bai, ez, abstentzioa eta ohiko aukerak",
|
||||||
|
voteKind_traffic: "Semaforoa",
|
||||||
|
voteKindHint_traffic: "Berdea ados · horia zalantzak · gorria aurka, batzarrean bezala",
|
||||||
|
voteKind_scale: "Termometroa",
|
||||||
|
voteKindHint_scale: "1etik (hotza) 5era (beroa)",
|
||||||
|
deleteConfirmQuestion: "Betiko ezabatu?",
|
||||||
|
deleteConfirmYes: "Bai, ezabatu",
|
||||||
|
welcomeBannerTextMobile: "Jarri zure izena eta argazkia ezagutu zaitzaten.",
|
||||||
|
welcomeBannerActionMobile: "Jarri",
|
||||||
|
footerPeersNone: "Oraindik ez zaude inorekin sinkronizatzen",
|
||||||
|
footerPeersSome: "{n}-rekin sinkronizatzen",
|
||||||
|
tribeMapUrlLabel: "Maparen esteka (aukerakoa)",
|
||||||
|
tribeHintPrivacy: "Pribatua: kideek bakarrik ikusten dute, eta zifratuta dago. Publikoa: sare osoak ikusten du.",
|
||||||
|
tribeHintMode: "Irekia: edozein kidek gonbida dezake. Zorrotza: sortu zuenak bakarrik gonbidatzen du.",
|
||||||
|
moduleBlockedText: "{name} desaktibatuta dago.",
|
||||||
|
moduleBlockedAction: "Aktibatu {name}",
|
||||||
|
modulesOnCount: "{total} ataletatik {on} aktibo",
|
||||||
|
modulesSaved: "Gordeta.",
|
||||||
|
karvanVoiceOn: "Ahotsa piztuta",
|
||||||
|
karvanCamOn: "Kamera piztuta",
|
||||||
|
karvanNoCam: "Kamerarako baimenik ez: deiak ahotsarekin bakarrik jarraitzen du.",
|
||||||
|
karvanNoMic: "Mikrofonorako baimenik ez. Mugikorraren ezarpenetan eman dezakezu; txatak funtzionatzen jarraitzen du.",
|
||||||
|
karvanMic: "Mikroa",
|
||||||
|
karvanCam: "Kamera",
|
||||||
|
karvanHangUp: "Eskegi",
|
||||||
|
karvanInviteLabel: "Norbait gonbidatu",
|
||||||
|
karvanInviteHint: "Itsatsi bere @id-a: bere profilean dago, izenaren azpian.",
|
||||||
|
karvanRelayHuman: "Zure nodoaren bidez",
|
||||||
|
karvanP2pHuman: "Zuzena eta zifratua",
|
||||||
// skins y workflows en el movil (settings_view.js)
|
// skins y workflows en el movil (settings_view.js)
|
||||||
skinIntroMobile: "Aukeratu nolakoa den Oasis zure mugikorrean. Ukitu bezain laster aplikatzen da.",
|
skinIntroMobile: "Aukeratu nolakoa den Oasis zure mugikorrean. Ukitu bezain laster aplikatzen da.",
|
||||||
workflowsDescriptionMobile: "Workflow batek zein modulu dauden aktibo finkatzen du. Mugikorrean zure itxura ez da aldatzen.",
|
workflowsDescriptionMobile: "Workflow batek zein modulu dauden aktibo finkatzen du. Mugikorrean zure itxura ez da aldatzen.",
|
||||||
|
|
@ -295,6 +459,47 @@ module.exports = {
|
||||||
spreadMore: "gehiago"
|
spreadMore: "gehiago"
|
||||||
},
|
},
|
||||||
de: {
|
de: {
|
||||||
|
// usabilidad (19/10/2026): votaciones, borrar, modulos, Karvan, pie
|
||||||
|
tribeSectionsMore: "Mehr",
|
||||||
|
tribeSectionsLabel: "Bereiche",
|
||||||
|
voteYes: "Ja",
|
||||||
|
voteNo: "Nein",
|
||||||
|
voteAbstention: "Enthaltung",
|
||||||
|
voteGreen: "Grün · einverstanden",
|
||||||
|
voteYellow: "Gelb · Zweifel",
|
||||||
|
voteRed: "Rot · dagegen",
|
||||||
|
voteOwnNote: "Du hast diese Abstimmung erstellt und kannst daher nicht abstimmen: wer vorschlägt, bleibt neutral.",
|
||||||
|
voteKindLabel: "Art der Abstimmung",
|
||||||
|
voteKind_classic: "Ja / Nein",
|
||||||
|
voteKindHint_classic: "Ja, nein, Enthaltung und die üblichen Optionen",
|
||||||
|
voteKind_traffic: "Ampel",
|
||||||
|
voteKindHint_traffic: "Grün einverstanden · gelb Zweifel · rot dagegen, wie im Plenum",
|
||||||
|
voteKind_scale: "Thermometer",
|
||||||
|
voteKindHint_scale: "Von 1 (kalt) bis 5 (heiß)",
|
||||||
|
deleteConfirmQuestion: "Endgültig löschen?",
|
||||||
|
deleteConfirmYes: "Ja, löschen",
|
||||||
|
welcomeBannerTextMobile: "Füge deinen Namen und dein Foto hinzu, damit man dich erkennt.",
|
||||||
|
welcomeBannerActionMobile: "Einrichten",
|
||||||
|
footerPeersNone: "Noch mit niemandem synchronisiert",
|
||||||
|
footerPeersSome: "Synchronisiert mit {n}",
|
||||||
|
tribeMapUrlLabel: "Kartenlink (optional)",
|
||||||
|
tribeHintPrivacy: "Privat: nur Mitglieder sehen sie, und sie ist verschlüsselt. Öffentlich: das ganze Netz sieht sie.",
|
||||||
|
tribeHintMode: "Offen: jedes Mitglied kann einladen. Streng: nur wer sie erstellt hat, lädt ein.",
|
||||||
|
moduleBlockedText: "{name} ist ausgeschaltet.",
|
||||||
|
moduleBlockedAction: "{name} einschalten",
|
||||||
|
modulesOnCount: "{on} von {total} Bereichen aktiv",
|
||||||
|
modulesSaved: "Gespeichert.",
|
||||||
|
karvanVoiceOn: "Stimme an",
|
||||||
|
karvanCamOn: "Kamera an",
|
||||||
|
karvanNoCam: "Keine Kameraberechtigung: der Anruf läuft nur mit Stimme weiter.",
|
||||||
|
karvanNoMic: "Keine Mikrofonberechtigung. Du kannst sie in den Telefoneinstellungen erlauben; der Chat funktioniert weiter.",
|
||||||
|
karvanMic: "Mikro",
|
||||||
|
karvanCam: "Kamera",
|
||||||
|
karvanHangUp: "Auflegen",
|
||||||
|
karvanInviteLabel: "Jemanden einladen",
|
||||||
|
karvanInviteHint: "Füge die @id ein: sie steht im Profil, unter dem Namen.",
|
||||||
|
karvanRelayHuman: "Über deinen Knoten",
|
||||||
|
karvanP2pHuman: "Direkt und verschlüsselt",
|
||||||
// skins y workflows en el movil (settings_view.js)
|
// skins y workflows en el movil (settings_view.js)
|
||||||
skinIntroMobile: "Wähle, wie Oasis auf deinem Handy aussieht. Es wird beim Antippen übernommen.",
|
skinIntroMobile: "Wähle, wie Oasis auf deinem Handy aussieht. Es wird beim Antippen übernommen.",
|
||||||
workflowsDescriptionMobile: "Ein Workflow legt fest, welche Module aktiv sind. Auf dem Handy bleibt dein Skin, wie er ist.",
|
workflowsDescriptionMobile: "Ein Workflow legt fest, welche Module aktiv sind. Auf dem Handy bleibt dein Skin, wie er ist.",
|
||||||
|
|
@ -363,6 +568,47 @@ module.exports = {
|
||||||
spreadMore: "mehr"
|
spreadMore: "mehr"
|
||||||
},
|
},
|
||||||
it: {
|
it: {
|
||||||
|
// usabilidad (19/10/2026): votaciones, borrar, modulos, Karvan, pie
|
||||||
|
tribeSectionsMore: "Altro",
|
||||||
|
tribeSectionsLabel: "Sezioni",
|
||||||
|
voteYes: "Sì",
|
||||||
|
voteNo: "No",
|
||||||
|
voteAbstention: "Astensione",
|
||||||
|
voteGreen: "Verde · d'accordo",
|
||||||
|
voteYellow: "Giallo · dubbi",
|
||||||
|
voteRed: "Rosso · contrario",
|
||||||
|
voteOwnNote: "Hai creato tu questa votazione, quindi non puoi votare: chi propone resta neutrale.",
|
||||||
|
voteKindLabel: "Tipo di votazione",
|
||||||
|
voteKind_classic: "Sì / No",
|
||||||
|
voteKindHint_classic: "Sì, no, astensione e le opzioni di sempre",
|
||||||
|
voteKind_traffic: "Semaforo",
|
||||||
|
voteKindHint_traffic: "Verde d'accordo · giallo dubbi · rosso contrario, come in assemblea",
|
||||||
|
voteKind_scale: "Termometro",
|
||||||
|
voteKindHint_scale: "Da 1 (freddo) a 5 (caldo)",
|
||||||
|
deleteConfirmQuestion: "Eliminare per sempre?",
|
||||||
|
deleteConfirmYes: "Sì, elimina",
|
||||||
|
welcomeBannerTextMobile: "Aggiungi nome e foto perché ti riconoscano.",
|
||||||
|
welcomeBannerActionMobile: "Aggiungi",
|
||||||
|
footerPeersNone: "Non sincronizzi ancora con nessuno",
|
||||||
|
footerPeersSome: "In sincronia con {n}",
|
||||||
|
tribeMapUrlLabel: "Link della mappa (facoltativo)",
|
||||||
|
tribeHintPrivacy: "Privata: la vedono solo i membri ed è cifrata. Pubblica: la vede tutta la rete.",
|
||||||
|
tribeHintMode: "Aperta: qualsiasi membro può invitare. Rigida: invita solo chi l'ha creata.",
|
||||||
|
moduleBlockedText: "{name} è disattivato.",
|
||||||
|
moduleBlockedAction: "Attiva {name}",
|
||||||
|
modulesOnCount: "{on} sezioni attive su {total}",
|
||||||
|
modulesSaved: "Salvato.",
|
||||||
|
karvanVoiceOn: "Voce attiva",
|
||||||
|
karvanCamOn: "Fotocamera attiva",
|
||||||
|
karvanNoCam: "Nessun permesso per la fotocamera: la chiamata continua solo in voce.",
|
||||||
|
karvanNoMic: "Nessun permesso per il microfono. Puoi darlo nelle impostazioni del telefono; la chat continua a funzionare.",
|
||||||
|
karvanMic: "Micro",
|
||||||
|
karvanCam: "Fotocamera",
|
||||||
|
karvanHangUp: "Riaggancia",
|
||||||
|
karvanInviteLabel: "Invita qualcuno",
|
||||||
|
karvanInviteHint: "Incolla il suo @id: è nel suo profilo, sotto il nome.",
|
||||||
|
karvanRelayHuman: "Tramite il tuo nodo",
|
||||||
|
karvanP2pHuman: "Diretto e cifrato",
|
||||||
// skins y workflows en el movil (settings_view.js)
|
// skins y workflows en el movil (settings_view.js)
|
||||||
skinIntroMobile: "Scegli come appare Oasis sul tuo telefono. Si applica appena lo tocchi.",
|
skinIntroMobile: "Scegli come appare Oasis sul tuo telefono. Si applica appena lo tocchi.",
|
||||||
workflowsDescriptionMobile: "Un workflow stabilisce quali moduli sono attivi. Sul telefono la tua skin resta com'è.",
|
workflowsDescriptionMobile: "Un workflow stabilisce quali moduli sono attivi. Sul telefono la tua skin resta com'è.",
|
||||||
|
|
@ -431,6 +677,47 @@ module.exports = {
|
||||||
spreadMore: "altro"
|
spreadMore: "altro"
|
||||||
},
|
},
|
||||||
pt: {
|
pt: {
|
||||||
|
// usabilidad (19/10/2026): votaciones, borrar, modulos, Karvan, pie
|
||||||
|
tribeSectionsMore: "Mais",
|
||||||
|
tribeSectionsLabel: "Secções",
|
||||||
|
voteYes: "Sim",
|
||||||
|
voteNo: "Não",
|
||||||
|
voteAbstention: "Abstenção",
|
||||||
|
voteGreen: "Verde · de acordo",
|
||||||
|
voteYellow: "Amarelo · dúvidas",
|
||||||
|
voteRed: "Vermelho · contra",
|
||||||
|
voteOwnNote: "Criaste esta votação, por isso não podes votar nela: quem propõe fica neutro.",
|
||||||
|
voteKindLabel: "Tipo de votação",
|
||||||
|
voteKind_classic: "Sim / Não",
|
||||||
|
voteKindHint_classic: "Sim, não, abstenção e as opções de sempre",
|
||||||
|
voteKind_traffic: "Semáforo",
|
||||||
|
voteKindHint_traffic: "Verde de acordo · amarelo dúvidas · vermelho contra, como em assembleia",
|
||||||
|
voteKind_scale: "Termómetro",
|
||||||
|
voteKindHint_scale: "De 1 (frio) a 5 (quente)",
|
||||||
|
deleteConfirmQuestion: "Apagar para sempre?",
|
||||||
|
deleteConfirmYes: "Sim, apagar",
|
||||||
|
welcomeBannerTextMobile: "Põe o teu nome e a tua foto para que te reconheçam.",
|
||||||
|
welcomeBannerActionMobile: "Pôr",
|
||||||
|
footerPeersNone: "Ainda não sincronizas com ninguém",
|
||||||
|
footerPeersSome: "A sincronizar com {n}",
|
||||||
|
tribeMapUrlLabel: "Ligação do mapa (opcional)",
|
||||||
|
tribeHintPrivacy: "Privada: só os membros a veem e vai cifrada. Pública: toda a rede a vê.",
|
||||||
|
tribeHintMode: "Aberta: qualquer membro pode convidar. Estrita: só convida quem a criou.",
|
||||||
|
moduleBlockedText: "{name} está desativado.",
|
||||||
|
moduleBlockedAction: "Ativar {name}",
|
||||||
|
modulesOnCount: "{on} de {total} secções ativas",
|
||||||
|
modulesSaved: "Guardado.",
|
||||||
|
karvanVoiceOn: "Voz ativa",
|
||||||
|
karvanCamOn: "Câmara ativa",
|
||||||
|
karvanNoCam: "Sem permissão de câmara: a chamada continua só com voz.",
|
||||||
|
karvanNoMic: "Sem permissão de microfone. Podes dá-la nas definições do telemóvel; o chat continua a funcionar.",
|
||||||
|
karvanMic: "Micro",
|
||||||
|
karvanCam: "Câmara",
|
||||||
|
karvanHangUp: "Desligar",
|
||||||
|
karvanInviteLabel: "Convidar alguém",
|
||||||
|
karvanInviteHint: "Cola o @id da pessoa: está no perfil, por baixo do nome.",
|
||||||
|
karvanRelayHuman: "Através do teu nó",
|
||||||
|
karvanP2pHuman: "Direto e cifrado",
|
||||||
// skins y workflows en el movil (settings_view.js)
|
// skins y workflows en el movil (settings_view.js)
|
||||||
skinIntroMobile: "Escolhe como o Oasis aparece no teu telemóvel. Aplica-se ao tocar.",
|
skinIntroMobile: "Escolhe como o Oasis aparece no teu telemóvel. Aplica-se ao tocar.",
|
||||||
workflowsDescriptionMobile: "Um workflow define que módulos estão ativos. No telemóvel o teu tema fica como está.",
|
workflowsDescriptionMobile: "Um workflow define que módulos estão ativos. No telemóvel o teu tema fica como está.",
|
||||||
|
|
@ -499,6 +786,47 @@ module.exports = {
|
||||||
spreadMore: "mais"
|
spreadMore: "mais"
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
|
// usabilidad (19/10/2026): votaciones, borrar, modulos, Karvan, pie
|
||||||
|
tribeSectionsMore: "更多",
|
||||||
|
tribeSectionsLabel: "板块",
|
||||||
|
voteYes: "赞成",
|
||||||
|
voteNo: "反对",
|
||||||
|
voteAbstention: "弃权",
|
||||||
|
voteGreen: "绿 · 同意",
|
||||||
|
voteYellow: "黄 · 有疑问",
|
||||||
|
voteRed: "红 · 反对",
|
||||||
|
voteOwnNote: "这是你发起的投票,所以你不能投票:发起人保持中立。",
|
||||||
|
voteKindLabel: "投票类型",
|
||||||
|
voteKind_classic: "是 / 否",
|
||||||
|
voteKindHint_classic: "赞成、反对、弃权及常用选项",
|
||||||
|
voteKind_traffic: "红绿灯",
|
||||||
|
voteKindHint_traffic: "绿同意 · 黄有疑问 · 红反对,和集会上一样",
|
||||||
|
voteKind_scale: "温度计",
|
||||||
|
voteKindHint_scale: "从 1(冷)到 5(热)",
|
||||||
|
deleteConfirmQuestion: "永久删除?",
|
||||||
|
deleteConfirmYes: "是,删除",
|
||||||
|
welcomeBannerTextMobile: "添加你的名字和照片,让大家认出你。",
|
||||||
|
welcomeBannerActionMobile: "设置",
|
||||||
|
footerPeersNone: "尚未与任何人同步",
|
||||||
|
footerPeersSome: "正在与 {n} 个节点同步",
|
||||||
|
tribeMapUrlLabel: "地图链接(可选)",
|
||||||
|
tribeHintPrivacy: "私密:只有成员能看到,并且加密。公开:整个网络都能看到。",
|
||||||
|
tribeHintMode: "开放:任何成员都可以邀请。严格:只有创建者可以邀请。",
|
||||||
|
moduleBlockedText: "{name} 已关闭。",
|
||||||
|
moduleBlockedAction: "开启 {name}",
|
||||||
|
modulesOnCount: "已开启 {on}/{total} 个板块",
|
||||||
|
modulesSaved: "已保存。",
|
||||||
|
karvanVoiceOn: "语音已开",
|
||||||
|
karvanCamOn: "摄像头已开",
|
||||||
|
karvanNoCam: "没有摄像头权限:通话仅以语音继续。",
|
||||||
|
karvanNoMic: "没有麦克风权限。可以在手机设置中允许;聊天仍可使用。",
|
||||||
|
karvanMic: "麦克风",
|
||||||
|
karvanCam: "摄像头",
|
||||||
|
karvanHangUp: "挂断",
|
||||||
|
karvanInviteLabel: "邀请某人",
|
||||||
|
karvanInviteHint: "粘贴对方的 @id:在其个人资料中,名字下方。",
|
||||||
|
karvanRelayHuman: "通过你的节点",
|
||||||
|
karvanP2pHuman: "直连且加密",
|
||||||
// skins y workflows en el movil (settings_view.js)
|
// skins y workflows en el movil (settings_view.js)
|
||||||
skinIntroMobile: "选择 Oasis 在手机上的外观。点一下就会生效。",
|
skinIntroMobile: "选择 Oasis 在手机上的外观。点一下就会生效。",
|
||||||
workflowsDescriptionMobile: "工作流决定启用哪些模块。在手机上,你的外观保持不变。",
|
workflowsDescriptionMobile: "工作流决定启用哪些模块。在手机上,你的外观保持不变。",
|
||||||
|
|
@ -567,6 +895,47 @@ module.exports = {
|
||||||
spreadMore: "更多"
|
spreadMore: "更多"
|
||||||
},
|
},
|
||||||
ar: {
|
ar: {
|
||||||
|
// usabilidad (19/10/2026): votaciones, borrar, modulos, Karvan, pie
|
||||||
|
tribeSectionsMore: "المزيد",
|
||||||
|
tribeSectionsLabel: "الأقسام",
|
||||||
|
voteYes: "نعم",
|
||||||
|
voteNo: "لا",
|
||||||
|
voteAbstention: "امتناع",
|
||||||
|
voteGreen: "أخضر · موافق",
|
||||||
|
voteYellow: "أصفر · شكوك",
|
||||||
|
voteRed: "أحمر · ضد",
|
||||||
|
voteOwnNote: "أنت من أنشأ هذا التصويت، لذا لا يمكنك التصويت فيه: من يقترح يبقى محايدًا.",
|
||||||
|
voteKindLabel: "نوع التصويت",
|
||||||
|
voteKind_classic: "نعم / لا",
|
||||||
|
voteKindHint_classic: "نعم، لا، امتناع والخيارات المعتادة",
|
||||||
|
voteKind_traffic: "إشارة المرور",
|
||||||
|
voteKindHint_traffic: "أخضر موافق · أصفر شكوك · أحمر ضد، كما في الجمعية",
|
||||||
|
voteKind_scale: "مقياس الحرارة",
|
||||||
|
voteKindHint_scale: "من 1 (بارد) إلى 5 (ساخن)",
|
||||||
|
deleteConfirmQuestion: "حذف نهائيًا؟",
|
||||||
|
deleteConfirmYes: "نعم، احذف",
|
||||||
|
welcomeBannerTextMobile: "أضف اسمك وصورتك ليتعرّف عليك الآخرون.",
|
||||||
|
welcomeBannerActionMobile: "إضافة",
|
||||||
|
footerPeersNone: "لا مزامنة مع أحد بعد",
|
||||||
|
footerPeersSome: "مزامنة مع {n}",
|
||||||
|
tribeMapUrlLabel: "رابط الخريطة (اختياري)",
|
||||||
|
tribeHintPrivacy: "خاصة: يراها أعضاؤها فقط وهي مشفّرة. عامة: تراها الشبكة كلها.",
|
||||||
|
tribeHintMode: "مفتوحة: يمكن لأي عضو أن يدعو. صارمة: لا يدعو إلا من أنشأها.",
|
||||||
|
moduleBlockedText: "{name} مُعطّل.",
|
||||||
|
moduleBlockedAction: "تفعيل {name}",
|
||||||
|
modulesOnCount: "{on} من {total} أقسام مفعّلة",
|
||||||
|
modulesSaved: "تم الحفظ.",
|
||||||
|
karvanVoiceOn: "الصوت مفعّل",
|
||||||
|
karvanCamOn: "الكاميرا مفعّلة",
|
||||||
|
karvanNoCam: "لا إذن للكاميرا: تستمر المكالمة بالصوت فقط.",
|
||||||
|
karvanNoMic: "لا إذن للميكروفون. يمكنك السماح به من إعدادات الهاتف؛ الدردشة تعمل.",
|
||||||
|
karvanMic: "ميكروفون",
|
||||||
|
karvanCam: "كاميرا",
|
||||||
|
karvanHangUp: "إنهاء",
|
||||||
|
karvanInviteLabel: "ادعُ شخصًا",
|
||||||
|
karvanInviteHint: "الصق معرّفه @id: تجده في ملفه تحت اسمه.",
|
||||||
|
karvanRelayHuman: "عبر عقدتك",
|
||||||
|
karvanP2pHuman: "مباشر ومشفّر",
|
||||||
// skins y workflows en el movil (settings_view.js)
|
// skins y workflows en el movil (settings_view.js)
|
||||||
skinIntroMobile: "اختر مظهر Oasis على هاتفك. يُطبَّق بمجرد اللمس.",
|
skinIntroMobile: "اختر مظهر Oasis على هاتفك. يُطبَّق بمجرد اللمس.",
|
||||||
workflowsDescriptionMobile: "يحدد سير العمل الوحدات المفعّلة. على الهاتف يبقى مظهرك كما هو.",
|
workflowsDescriptionMobile: "يحدد سير العمل الوحدات المفعّلة. على الهاتف يبقى مظهرك كما هو.",
|
||||||
|
|
@ -635,6 +1004,47 @@ module.exports = {
|
||||||
spreadMore: "المزيد"
|
spreadMore: "المزيد"
|
||||||
},
|
},
|
||||||
hi: {
|
hi: {
|
||||||
|
// usabilidad (19/10/2026): votaciones, borrar, modulos, Karvan, pie
|
||||||
|
tribeSectionsMore: "और",
|
||||||
|
tribeSectionsLabel: "खंड",
|
||||||
|
voteYes: "हाँ",
|
||||||
|
voteNo: "नहीं",
|
||||||
|
voteAbstention: "तटस्थ",
|
||||||
|
voteGreen: "हरा · सहमत",
|
||||||
|
voteYellow: "पीला · संदेह",
|
||||||
|
voteRed: "लाल · विरोध",
|
||||||
|
voteOwnNote: "यह मतदान आपने बनाया है, इसलिए आप इसमें वोट नहीं दे सकते: प्रस्ताव रखने वाला तटस्थ रहता है।",
|
||||||
|
voteKindLabel: "मतदान का प्रकार",
|
||||||
|
voteKind_classic: "हाँ / नहीं",
|
||||||
|
voteKindHint_classic: "हाँ, नहीं, तटस्थ और सामान्य विकल्प",
|
||||||
|
voteKind_traffic: "ट्रैफ़िक लाइट",
|
||||||
|
voteKindHint_traffic: "हरा सहमत · पीला संदेह · लाल विरोध, सभा की तरह",
|
||||||
|
voteKind_scale: "थर्मामीटर",
|
||||||
|
voteKindHint_scale: "1 (ठंडा) से 5 (गरम) तक",
|
||||||
|
deleteConfirmQuestion: "हमेशा के लिए हटाएँ?",
|
||||||
|
deleteConfirmYes: "हाँ, हटाएँ",
|
||||||
|
welcomeBannerTextMobile: "अपना नाम और फ़ोटो जोड़ें ताकि लोग आपको पहचानें।",
|
||||||
|
welcomeBannerActionMobile: "जोड़ें",
|
||||||
|
footerPeersNone: "अभी किसी से सिंक नहीं",
|
||||||
|
footerPeersSome: "{n} से सिंक हो रहा है",
|
||||||
|
tribeMapUrlLabel: "नक्शे का लिंक (वैकल्पिक)",
|
||||||
|
tribeHintPrivacy: "निजी: केवल सदस्य देखते हैं, और यह एन्क्रिप्टेड है। सार्वजनिक: पूरा नेटवर्क देखता है।",
|
||||||
|
tribeHintMode: "खुला: कोई भी सदस्य आमंत्रित कर सकता है। सख़्त: केवल बनाने वाला आमंत्रित करता है।",
|
||||||
|
moduleBlockedText: "{name} बंद है।",
|
||||||
|
moduleBlockedAction: "{name} चालू करें",
|
||||||
|
modulesOnCount: "{total} में से {on} खंड चालू",
|
||||||
|
modulesSaved: "सहेजा गया।",
|
||||||
|
karvanVoiceOn: "आवाज़ चालू",
|
||||||
|
karvanCamOn: "कैमरा चालू",
|
||||||
|
karvanNoCam: "कैमरे की अनुमति नहीं: कॉल केवल आवाज़ से जारी है।",
|
||||||
|
karvanNoMic: "माइक्रोफ़ोन की अनुमति नहीं। फ़ोन की सेटिंग में दे सकते हैं; चैट चलती रहेगी।",
|
||||||
|
karvanMic: "माइक",
|
||||||
|
karvanCam: "कैमरा",
|
||||||
|
karvanHangUp: "कॉल काटें",
|
||||||
|
karvanInviteLabel: "किसी को आमंत्रित करें",
|
||||||
|
karvanInviteHint: "उनका @id चिपकाएँ: यह उनकी प्रोफ़ाइल में नाम के नीचे है।",
|
||||||
|
karvanRelayHuman: "आपके नोड के ज़रिए",
|
||||||
|
karvanP2pHuman: "सीधा और एन्क्रिप्टेड",
|
||||||
// skins y workflows en el movil (settings_view.js)
|
// skins y workflows en el movil (settings_view.js)
|
||||||
skinIntroMobile: "चुनें कि आपके फ़ोन पर Oasis कैसा दिखे। छूते ही लागू हो जाता है।",
|
skinIntroMobile: "चुनें कि आपके फ़ोन पर Oasis कैसा दिखे। छूते ही लागू हो जाता है।",
|
||||||
workflowsDescriptionMobile: "वर्कफ़्लो तय करता है कि कौन-से मॉड्यूल चालू हैं। फ़ोन पर आपकी स्किन वैसी ही रहती है।",
|
workflowsDescriptionMobile: "वर्कफ़्लो तय करता है कि कौन-से मॉड्यूल चालू हैं। फ़ोन पर आपकी स्किन वैसी ही रहती है।",
|
||||||
|
|
@ -703,6 +1113,47 @@ module.exports = {
|
||||||
spreadMore: "और"
|
spreadMore: "और"
|
||||||
},
|
},
|
||||||
ru: {
|
ru: {
|
||||||
|
// usabilidad (19/10/2026): votaciones, borrar, modulos, Karvan, pie
|
||||||
|
tribeSectionsMore: "Ещё",
|
||||||
|
tribeSectionsLabel: "Разделы",
|
||||||
|
voteYes: "Да",
|
||||||
|
voteNo: "Нет",
|
||||||
|
voteAbstention: "Воздержаться",
|
||||||
|
voteGreen: "Зелёный · за",
|
||||||
|
voteYellow: "Жёлтый · сомнения",
|
||||||
|
voteRed: "Красный · против",
|
||||||
|
voteOwnNote: "Вы создали это голосование, поэтому не можете в нём голосовать: кто предлагает, остаётся нейтральным.",
|
||||||
|
voteKindLabel: "Тип голосования",
|
||||||
|
voteKind_classic: "Да / Нет",
|
||||||
|
voteKindHint_classic: "Да, нет, воздержаться и обычные варианты",
|
||||||
|
voteKind_traffic: "Светофор",
|
||||||
|
voteKindHint_traffic: "Зелёный за · жёлтый сомнения · красный против, как на собрании",
|
||||||
|
voteKind_scale: "Термометр",
|
||||||
|
voteKindHint_scale: "От 1 (холодно) до 5 (горячо)",
|
||||||
|
deleteConfirmQuestion: "Удалить навсегда?",
|
||||||
|
deleteConfirmYes: "Да, удалить",
|
||||||
|
welcomeBannerTextMobile: "Добавьте имя и фото, чтобы вас узнавали.",
|
||||||
|
welcomeBannerActionMobile: "Добавить",
|
||||||
|
footerPeersNone: "Пока ни с кем не синхронизировано",
|
||||||
|
footerPeersSome: "Синхронизация с {n}",
|
||||||
|
tribeMapUrlLabel: "Ссылка на карту (необязательно)",
|
||||||
|
tribeHintPrivacy: "Закрытое: видят только участники, и оно зашифровано. Открытое: видит вся сеть.",
|
||||||
|
tribeHintMode: "Открытое: приглашать может любой участник. Строгое: приглашает только создатель.",
|
||||||
|
moduleBlockedText: "{name} выключен.",
|
||||||
|
moduleBlockedAction: "Включить {name}",
|
||||||
|
modulesOnCount: "Включено {on} из {total} разделов",
|
||||||
|
modulesSaved: "Сохранено.",
|
||||||
|
karvanVoiceOn: "Голос включён",
|
||||||
|
karvanCamOn: "Камера включена",
|
||||||
|
karvanNoCam: "Нет доступа к камере: звонок продолжается только голосом.",
|
||||||
|
karvanNoMic: "Нет доступа к микрофону. Его можно разрешить в настройках телефона; чат продолжает работать.",
|
||||||
|
karvanMic: "Микрофон",
|
||||||
|
karvanCam: "Камера",
|
||||||
|
karvanHangUp: "Завершить",
|
||||||
|
karvanInviteLabel: "Пригласить кого-нибудь",
|
||||||
|
karvanInviteHint: "Вставьте его @id: он в профиле, под именем.",
|
||||||
|
karvanRelayHuman: "Через ваш узел",
|
||||||
|
karvanP2pHuman: "Напрямую и зашифровано",
|
||||||
// skins y workflows en el movil (settings_view.js)
|
// skins y workflows en el movil (settings_view.js)
|
||||||
skinIntroMobile: "Выберите, как Oasis выглядит на телефоне. Применяется сразу по нажатию.",
|
skinIntroMobile: "Выберите, как Oasis выглядит на телефоне. Применяется сразу по нажатию.",
|
||||||
workflowsDescriptionMobile: "Рабочий процесс задаёт, какие модули включены. На телефоне ваше оформление не меняется.",
|
workflowsDescriptionMobile: "Рабочий процесс задаёт, какие модули включены. На телефоне ваше оформление не меняется.",
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,7 @@
|
||||||
|
|
||||||
function setLive(on) {
|
function setLive(on) {
|
||||||
if (!liveTxt) return;
|
if (!liveTxt) return;
|
||||||
liveTxt.textContent = on ? "🔒 P2P (WebRTC)" : "server relay";
|
liveTxt.textContent = on ? (liveWrap && liveWrap.getAttribute("data-txt-p2p") || "🔒 P2P (WebRTC)") : (liveWrap && liveWrap.getAttribute("data-txt-relay") || "server relay");
|
||||||
if (liveWrap) liveWrap.className = "karvan-live" + (on ? " on" : "");
|
if (liveWrap) liveWrap.className = "karvan-live" + (on ? " on" : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -173,7 +173,9 @@
|
||||||
// --- media / videollamada (Fase 1) ---
|
// --- media / videollamada (Fase 1) ---
|
||||||
function setCallActive(on) { if (callRoot) callRoot.className = "karvan-call" + (on ? " active" : ""); }
|
function setCallActive(on) { if (callRoot) callRoot.className = "karvan-call" + (on ? " active" : ""); }
|
||||||
function setStatus(msg) { if (statusEl) statusEl.textContent = msg || ""; }
|
function setStatus(msg) { if (statusEl) statusEl.textContent = msg || ""; }
|
||||||
function statusText() { var p = []; if (micActive) p.push("mic ON"); if (camActive) p.push("cam ON"); return p.length ? p.join(" · ") : ""; }
|
// textos traducidos: los pone la vista en data-txt-* de #karvan-call
|
||||||
|
function txt(k, def) { return (callRoot && callRoot.getAttribute("data-txt-" + k)) || def; }
|
||||||
|
function statusText() { var p = []; if (micActive) p.push(txt("mic", "Voice on")); if (camActive) p.push(txt("cam", "Camera on")); return p.length ? p.join(" · ") : ""; }
|
||||||
function updateBtns() {
|
function updateBtns() {
|
||||||
if (btnMic) btnMic.className = "kc-btn" + (micActive ? " on" : "");
|
if (btnMic) btnMic.className = "kc-btn" + (micActive ? " on" : "");
|
||||||
if (btnCam) btnCam.className = "kc-btn" + (camActive ? " on" : "");
|
if (btnCam) btnCam.className = "kc-btn" + (camActive ? " on" : "");
|
||||||
|
|
@ -215,7 +217,7 @@
|
||||||
}
|
}
|
||||||
function onMediaErr(kind, x) {
|
function onMediaErr(kind, x) {
|
||||||
var name = (x && x.name) || "", msg = (x && x.message) || String(x);
|
var name = (x && x.name) || "", msg = (x && x.message) || String(x);
|
||||||
if (name === "NotAllowedError" || /denied|permission/i.test(msg)) setStatus("⛔ Cámara/micro denegados (en el móvil, pendiente del wrapper). El chat sigue.");
|
if (name === "NotAllowedError" || /denied|permission/i.test(msg)) setStatus(kind === "video" ? txt("nocam", "No camera permission: the call goes on with voice only.") : txt("nomic", "No microphone permission. You can allow it in the phone settings; the chat still works."));
|
||||||
else if (name === "NotFoundError") setStatus("⚠ No hay " + (kind === "audio" ? "micrófono" : "cámara") + ".");
|
else if (name === "NotFoundError") setStatus("⚠ No hay " + (kind === "audio" ? "micrófono" : "cámara") + ".");
|
||||||
else if (name === "NotReadableError") setStatus("⚠ Dispositivo ocupado.");
|
else if (name === "NotReadableError") setStatus("⚠ Dispositivo ocupado.");
|
||||||
else setStatus("⚠ " + (name || "error") + ": " + msg);
|
else setStatus("⚠ " + (name || "error") + ": " + msg);
|
||||||
|
|
@ -230,16 +232,13 @@
|
||||||
setCallActive(true); updateBtns(); setStatus(statusText());
|
setCallActive(true); updateBtns(); setStatus(statusText());
|
||||||
}).catch(function (x) { onMediaErr(kind, x); });
|
}).catch(function (x) { onMediaErr(kind, x); });
|
||||||
}
|
}
|
||||||
|
// Llamar empieza solo con voz: pedir la camara de entrada hacia que, si se
|
||||||
|
// negaba, no arrancara nada aunque el micro estuviera permitido. El video se
|
||||||
|
// anade despues con su boton
|
||||||
function startCall() {
|
function startCall() {
|
||||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { setStatus("⚠ Cámara/micro no disponibles aquí."); return; }
|
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { setStatus("⚠ Cámara/micro no disponibles aquí."); return; }
|
||||||
setStatus("📞 pidiendo cámara y micro…");
|
if (micActive) return;
|
||||||
navigator.mediaDevices.getUserMedia({ audio: true, video: true }).then(function (s) {
|
getMedia("audio");
|
||||||
s.getTracks().forEach(addLocalTrack);
|
|
||||||
micActive = true; camActive = true; setCallActive(true); updateBtns(); setStatus(statusText());
|
|
||||||
}).catch(function (x) {
|
|
||||||
if (x && x.name === "NotFoundError") { getMedia("audio"); return; } // sin cámara → intenta solo audio
|
|
||||||
onMediaErr("call", x);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
function hangUp() {
|
function hangUp() {
|
||||||
if (localStream) localStream.getTracks().forEach(function (t) { t.stop(); localStream.removeTrack(t); });
|
if (localStream) localStream.getTracks().forEach(function (t) { t.stop(); localStream.removeTrack(t); });
|
||||||
|
|
|
||||||
|
|
@ -72,11 +72,13 @@ const WORKFLOWS = [
|
||||||
'tags', 'trending', 'tribes', 'larp', 'polls', 'opinions', 'invites']
|
'tags', 'trending', 'tribes', 'larp', 'polls', 'opinions', 'invites']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// lo justo para organizarse: hablar, escribir juntos y compartir lo que pasa
|
// lo justo para organizarse: hablar, escribir juntos, compartir lo que pasa
|
||||||
|
// y decidir (tribus = comisiones, parlamento y votaciones para la asamblea)
|
||||||
key: 'activists',
|
key: 'activists',
|
||||||
theme: 'Purple-SNH',
|
theme: 'Purple-SNH',
|
||||||
homePage: 'activity',
|
homePage: 'activity',
|
||||||
modules: ['chats', 'karvan', 'pads', 'forum', 'images', 'videos', 'wiki', 'blogs', 'feed', 'invites', 'backup']
|
modules: ['chats', 'karvan', 'pads', 'forum', 'images', 'videos', 'wiki', 'blogs', 'feed', 'invites', 'backup',
|
||||||
|
'tribes', 'parliament', 'votes', 'events']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'mobile',
|
key: 'mobile',
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
const { a, button, div, form, h2, h3, img, input, label, nav, p, section, span } = require("../server/node_modules/hyperaxe");
|
const { a, button, div, form, h2, h3, img, input, label, nav, p, section, span } = require("../server/node_modules/hyperaxe");
|
||||||
const { getConfig } = require("../configs/config-manager.js");
|
const { getConfig } = require("../configs/config-manager.js");
|
||||||
const sharedState = require("../configs/shared-state");
|
const sharedState = require("../configs/shared-state");
|
||||||
|
const { iconNode, iconKeyOf } = require("./mobile_icons");
|
||||||
|
|
||||||
// con pocos modulos activos (el workflow activistas) todo cabe en una pantalla:
|
// con pocos modulos activos (el workflow activistas) todo cabe en una pantalla:
|
||||||
// sobran el filtro Personal/Social y los accesos de usuario avanzado
|
// sobran el filtro Personal/Social y los accesos de usuario avanzado
|
||||||
|
|
@ -96,7 +97,7 @@ module.exports = ({ i18n }) => {
|
||||||
const modOn = (m) => !m || cfg.modules[m] === 'on' || cfg.modules[m] === undefined;
|
const modOn = (m) => !m || cfg.modules[m] === 'on' || cfg.modules[m] === undefined;
|
||||||
const bbLink = (href, ico, label, badge) => a(
|
const bbLink = (href, ico, label, badge) => a(
|
||||||
{ class: "bb-item", href },
|
{ class: "bb-item", href },
|
||||||
span({ class: "bb-ico" }, ico),
|
iconNode("bb-ico", iconKeyOf(href), ico),
|
||||||
(badge && badge > 0) ? span({ class: "bb-badge" }, String(badge)) : "",
|
(badge && badge > 0) ? span({ class: "bb-badge" }, String(badge)) : "",
|
||||||
span({ class: "bb-lbl" }, label)
|
span({ class: "bb-lbl" }, label)
|
||||||
);
|
);
|
||||||
|
|
@ -211,7 +212,7 @@ module.exports = ({ i18n }) => {
|
||||||
// Personal/Social de la topbar (lo pone el middleware desde ?hive=) parte el lanzador en dos
|
// Personal/Social de la topbar (lo pone el middleware desde ?hive=) parte el lanzador en dos
|
||||||
const hiveFilter = compact ? "" : ((typeof global !== "undefined" && global.__OASIS_HIVE_FILTER__) || "");
|
const hiveFilter = compact ? "" : ((typeof global !== "undefined" && global.__OASIS_HIVE_FILTER__) || "");
|
||||||
const app = (m) => a({ class: 'ln-app', href: m.href },
|
const app = (m) => a({ class: 'ln-app', href: m.href },
|
||||||
span({ class: 'ln-ico', 'aria-hidden': 'true' }, m.glyph),
|
iconNode('ln-ico', iconKeyOf(m.href), m.glyph),
|
||||||
span({ class: 'ln-lbl' }, i18n[m.labelKey] || m.href.slice(1)));
|
span({ class: 'ln-lbl' }, i18n[m.labelKey] || m.href.slice(1)));
|
||||||
const groups = GROUPS
|
const groups = GROUPS
|
||||||
.filter(g => !hiveFilter || g.side === hiveFilter)
|
.filter(g => !hiveFilter || g.side === hiveFilter)
|
||||||
|
|
@ -243,14 +244,14 @@ module.exports = ({ i18n }) => {
|
||||||
);
|
);
|
||||||
const pinnedRow = (m) => actionForm('remove', m.key, 'bb-manage-form',
|
const pinnedRow = (m) => actionForm('remove', m.key, 'bb-manage-form',
|
||||||
button({ type: 'submit', class: 'bb-manage-btn', 'aria-label': i18n.bbRemove || 'Remove' },
|
button({ type: 'submit', class: 'bb-manage-btn', 'aria-label': i18n.bbRemove || 'Remove' },
|
||||||
span({ class: 'bb-manage-ico' }, m.glyph),
|
iconNode('bb-manage-ico', iconKeyOf(m.href), m.glyph),
|
||||||
span({ class: 'bb-manage-lbl' }, i18n[m.labelKey] || m.key),
|
span({ class: 'bb-manage-lbl' }, i18n[m.labelKey] || m.key),
|
||||||
span({ class: 'bb-manage-x' }, '✕')
|
span({ class: 'bb-manage-x' }, '✕')
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
const addBtn = (m) => actionForm('add', m.key, 'bb-pick-form',
|
const addBtn = (m) => actionForm('add', m.key, 'bb-pick-form',
|
||||||
button({ type: 'submit', class: 'bb-pick-btn' },
|
button({ type: 'submit', class: 'bb-pick-btn' },
|
||||||
span({ class: 'bb-pick-ico' }, m.glyph),
|
iconNode('bb-pick-ico', iconKeyOf(m.href), m.glyph),
|
||||||
span({ class: 'bb-pick-lbl' }, i18n[m.labelKey] || m.key)
|
span({ class: 'bb-pick-lbl' }, i18n[m.labelKey] || m.key)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
@ -279,5 +280,5 @@ module.exports = ({ i18n }) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return { renderMobileTopbar, renderBottomBar, renderHiveNav, PINNABLE_MODULES, bottomBarPickerView };
|
return { renderMobileTopbar, renderBottomBar, renderHiveNav, PINNABLE_MODULES, bottomBarPickerView, GROUPS };
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,11 @@
|
||||||
const { div, section, h2, h3, p, a, form, input, button, span, ul, li, script, label, video } =
|
const { div, section, h2, h3, p, a, form, input, button, span, ul, li, script, label, video } =
|
||||||
require("../server/node_modules/hyperaxe");
|
require("../server/node_modules/hyperaxe");
|
||||||
const { template, i18n } = require("./main_views");
|
const { template, i18n } = require("./main_views");
|
||||||
|
const { iconNode } = require("./mobile_icons");
|
||||||
|
|
||||||
|
// boton de la barra de llamada: icono y su nombre debajo (antes 🎤 🎥 ⛔ sin texto)
|
||||||
|
const callBtn = (id, cls, icon, text) =>
|
||||||
|
button({ type: "button", class: "kc-btn " + cls, id }, iconNode("kc-ico", icon), span({ class: "kc-lbl" }, text));
|
||||||
|
|
||||||
const fmtExpires = (expiresAt) => {
|
const fmtExpires = (expiresAt) => {
|
||||||
const mins = Math.max(0, Math.round((expiresAt - Date.now()) / 60000));
|
const mins = Math.max(0, Math.round((expiresAt - Date.now()) / 60000));
|
||||||
|
|
@ -77,23 +82,27 @@ exports.karvanRoomView = (room, messages, params = {}) =>
|
||||||
span({ class: "karvan-room-title" }, room.title || "room " + room.id.slice(0, 6)),
|
span({ class: "karvan-room-title" }, room.title || "room " + room.id.slice(0, 6)),
|
||||||
span({ class: "karvan-expire", id: "karvan-expire" }, "⏳ " + fmtExpires(room.expiresAt))
|
span({ class: "karvan-expire", id: "karvan-expire" }, "⏳ " + fmtExpires(room.expiresAt))
|
||||||
),
|
),
|
||||||
div({ class: "karvan-live", id: "karvan-live" },
|
div({ class: "karvan-live", id: "karvan-live",
|
||||||
|
"data-txt-relay": i18n.karvanRelayHuman || "server relay", "data-txt-p2p": i18n.karvanP2pHuman || "🔒 P2P (WebRTC)" },
|
||||||
span({ class: "kl-dot" }),
|
span({ class: "kl-dot" }),
|
||||||
span({ class: "kl-txt", id: "karvan-live-txt" }, i18n.karvanRelay || "server relay")
|
span({ class: "kl-txt", id: "karvan-live-txt" }, i18n.karvanRelayHuman || i18n.karvanRelay || "server relay")
|
||||||
),
|
),
|
||||||
// Fase 1: panel de videollamada (media P2P sobre el mismo WebRTC del chat). La galería
|
// Fase 1: panel de videollamada (media P2P sobre el mismo WebRTC del chat). La galería
|
||||||
// aparece al iniciar; los botones cablean cámara/micro. En el móvil actual getUserMedia
|
// aparece al iniciar; los botones cablean cámara/micro. En el móvil actual getUserMedia
|
||||||
// fallará hasta que el wrapper conceda permisos (Fases 2/3) → se avisa en #kc-status.
|
// fallará hasta que el wrapper conceda permisos (Fases 2/3) → se avisa en #kc-status.
|
||||||
div({ class: "karvan-call", id: "karvan-call" },
|
div({ class: "karvan-call", id: "karvan-call",
|
||||||
|
"data-txt-mic": i18n.karvanVoiceOn || "Voice on", "data-txt-cam": i18n.karvanCamOn || "Camera on",
|
||||||
|
"data-txt-nocam": i18n.karvanNoCam || "No camera permission: the call goes on with voice only.",
|
||||||
|
"data-txt-nomic": i18n.karvanNoMic || "No microphone permission. You can allow it in the phone settings; the chat still works." },
|
||||||
div({ class: "karvan-videos", id: "karvan-videos" },
|
div({ class: "karvan-videos", id: "karvan-videos" },
|
||||||
video({ id: "karvan-local", class: "kc-video kc-local", muted: "muted", autoplay: "autoplay", playsinline: "playsinline" }),
|
video({ id: "karvan-local", class: "kc-video kc-local", muted: "muted", autoplay: "autoplay", playsinline: "playsinline" }),
|
||||||
div({ class: "kc-remote", id: "karvan-remote" })
|
div({ class: "kc-remote", id: "karvan-remote" })
|
||||||
),
|
),
|
||||||
div({ class: "karvan-call-bar" },
|
div({ class: "karvan-call-bar" },
|
||||||
button({ type: "button", class: "kc-btn kc-btn-start", id: "kc-start" }, "📞 " + (i18n.karvanCall || "Call")),
|
callBtn("kc-start", "kc-btn-start", "phone", i18n.karvanCall || "Call"),
|
||||||
button({ type: "button", class: "kc-btn", id: "kc-mic", "aria-label": "mic" }, "🎤"),
|
callBtn("kc-mic", "", "mic", i18n.karvanMic || "Mic"),
|
||||||
button({ type: "button", class: "kc-btn", id: "kc-cam", "aria-label": "camera" }, "🎥"),
|
callBtn("kc-cam", "", "camera", i18n.karvanCam || "Camera"),
|
||||||
button({ type: "button", class: "kc-btn kc-btn-stop", id: "kc-stop", "aria-label": "hang up" }, "⛔")
|
callBtn("kc-stop", "kc-btn-stop", "hangup", i18n.karvanHangUp || "Hang up")
|
||||||
),
|
),
|
||||||
div({ class: "kc-status", id: "kc-status" }, "")
|
div({ class: "kc-status", id: "kc-status" }, "")
|
||||||
),
|
),
|
||||||
|
|
@ -105,7 +114,9 @@ exports.karvanRoomView = (room, messages, params = {}) =>
|
||||||
),
|
),
|
||||||
// Fase 2: invitar a un contacto por su feed id → le llega al inbox un enlace a esta sala
|
// Fase 2: invitar a un contacto por su feed id → le llega al inbox un enlace a esta sala
|
||||||
form({ class: "karvan-invite", method: "POST", action: "/karvan/" + room.id + "/invite" },
|
form({ class: "karvan-invite", method: "POST", action: "/karvan/" + room.id + "/invite" },
|
||||||
input({ type: "text", name: "to", placeholder: i18n.karvanInvitePlaceholder || "Invite a contact by @feed-id…", autocomplete: "off", spellcheck: "false" }),
|
label({ class: "karvan-invite-lbl", for: "karvan-invite-to" }, i18n.karvanInviteLabel || "Invite someone"),
|
||||||
|
input({ type: "text", name: "to", id: "karvan-invite-to", placeholder: i18n.karvanInvitePlaceholder || "Invite a contact by @feed-id…", autocomplete: "off", spellcheck: "false" }),
|
||||||
|
p({ class: "form-hint" }, i18n.karvanInviteHint || "Paste their @id: it is on their profile, under their name."),
|
||||||
button({ type: "submit" }, i18n.karvanInvite || "Invite")
|
button({ type: "submit" }, i18n.karvanInvite || "Invite")
|
||||||
),
|
),
|
||||||
params.invite === "ok"
|
params.invite === "ok"
|
||||||
|
|
|
||||||
|
|
@ -386,12 +386,22 @@ const renderContentActions = (msgId, viewHref, opts = {}) => {
|
||||||
? a({ href: `/phone?filter=create&to=${encodeURIComponent(o.author)}`, class: 'btn-singleview btn-call', title: i18n.phoneCallTooltip }, renderIcon('call'))
|
? a({ href: `/phone?filter=create&to=${encodeURIComponent(o.author)}`, class: 'btn-singleview btn-call', title: i18n.phoneCallTooltip }, renderIcon('call'))
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const deleteBtn = o.deleteAction
|
// en el movil borrar pide un segundo toque: la X abre "¿Borrar? Si, borrar" y
|
||||||
? form({ method: 'POST', action: o.deleteAction, class: 'content-action-form content-action-delete' },
|
// solo ese boton envia. Sin JS, con <details>
|
||||||
|
const deleteForm = (cls, label) => form({ method: 'POST', action: o.deleteAction, class: cls },
|
||||||
o.returnTo && o.returnTo !== viewHref ? input({ type: 'hidden', name: 'returnTo', value: o.returnTo }) : null,
|
o.returnTo && o.returnTo !== viewHref ? input({ type: 'hidden', name: 'returnTo', value: o.returnTo }) : null,
|
||||||
button({ type: 'submit', class: 'btn-singleview btn-delete', title: i18n.chatDelete }, renderIcon('close'))
|
label
|
||||||
|
);
|
||||||
|
const deleteBtn = !o.deleteAction ? null
|
||||||
|
: process.env.OASIS_MOBILE === '1'
|
||||||
|
? details({ class: 'content-action-confirm' },
|
||||||
|
summary({ class: 'btn-singleview btn-delete', title: i18n.chatDelete, 'aria-label': i18n.chatDelete }, renderIcon('close')),
|
||||||
|
div({ class: 'confirm-bar' },
|
||||||
|
span({ class: 'confirm-question' }, i18n.deleteConfirmQuestion || 'Delete it for good?'),
|
||||||
|
deleteForm('content-action-form', button({ type: 'submit', class: 'confirm-yes' }, i18n.deleteConfirmYes || 'Yes, delete'))
|
||||||
)
|
)
|
||||||
: null;
|
)
|
||||||
|
: deleteForm('content-action-form content-action-delete', button({ type: 'submit', class: 'btn-singleview btn-delete', title: i18n.chatDelete }, renderIcon('close')));
|
||||||
|
|
||||||
const hideBtn = blockId && o.hide !== false && !(o.author && String(o.author) === String(myId))
|
const hideBtn = blockId && o.hide !== false && !(o.author && String(o.author) === String(myId))
|
||||||
? form({ method: 'POST', action: `/hide/${encodeURIComponent(blockId)}`, class: 'content-action-form' },
|
? form({ method: 'POST', action: `/hide/${encodeURIComponent(blockId)}`, class: 'content-action-form' },
|
||||||
|
|
@ -403,6 +413,8 @@ const renderContentActions = (msgId, viewHref, opts = {}) => {
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (!pinBtn && !spreadBtn && !chainBtn && !contentBtn && !reportBtn && !pmBtn && !callBtn && !deleteBtn && !donateBtn && !torrentBtn && !hideBtn) return null;
|
if (!pinBtn && !spreadBtn && !chainBtn && !contentBtn && !reportBtn && !pmBtn && !callBtn && !deleteBtn && !donateBtn && !torrentBtn && !hideBtn) return null;
|
||||||
|
// en el movil lo que mas se usa (abrir, enlace) va primero y borrar al final, lejos del pulgar
|
||||||
|
if (process.env.OASIS_MOBILE === '1') return div({ class: 'content-actions' }, contentBtn, linkBtn, pinBtn, spreadBtn, pmBtn, callBtn, donateBtn, reportBtn, hideBtn, deleteBtn);
|
||||||
return div({ class: 'content-actions' }, deleteBtn, spreadBtn, pinBtn, reportBtn, hideBtn, donateBtn, pmBtn, callBtn, torrentBtn, linkBtn, chainBtn, contentBtn);
|
return div({ class: 'content-actions' }, deleteBtn, spreadBtn, pinBtn, reportBtn, hideBtn, donateBtn, pmBtn, callBtn, torrentBtn, linkBtn, chainBtn, contentBtn);
|
||||||
};
|
};
|
||||||
exports.renderContentActions = renderContentActions;
|
exports.renderContentActions = renderContentActions;
|
||||||
|
|
@ -972,6 +984,22 @@ const renderFooter = () => {
|
||||||
const lastSyncLabel = lastSyncTs ? moment(lastSyncTs).fromNow() : '–';
|
const lastSyncLabel = lastSyncTs ? moment(lastSyncTs).fromNow() : '–';
|
||||||
const lastActivity = sharedState.getLastActivity ? sharedState.getLastActivity() : null;
|
const lastActivity = sharedState.getLastActivity ? sharedState.getLastActivity() : null;
|
||||||
|
|
||||||
|
// en el movil, una linea: con cuanta gente estas sincronizando, dicho en claro, y la
|
||||||
|
// version. La clave, HcT/HcH y la licencia estan en Perfil, Estadisticas y Ajustes
|
||||||
|
if (process.env.OASIS_MOBILE === '1') {
|
||||||
|
const n = Number(syncedPeers) || 0;
|
||||||
|
const peersText = n > 0
|
||||||
|
? String(i18n.footerPeersSome || '{n} peers in sync').replace('{n}', String(n))
|
||||||
|
: (i18n.footerPeersNone || 'Not syncing with anyone yet');
|
||||||
|
return div({ class: "oasis-footer oasis-footer-mobile" },
|
||||||
|
a({ href: "/peers", class: n > 0 ? "footer-sync is-on" : "footer-sync" },
|
||||||
|
span({ class: "footer-sync-dot", "aria-hidden": "true" }),
|
||||||
|
span(peersText)
|
||||||
|
),
|
||||||
|
span({ class: "footer-version" }, `Oasis ${pkgVersion} · GPLv3`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return div(
|
return div(
|
||||||
{ class: "oasis-footer" },
|
{ class: "oasis-footer" },
|
||||||
div(
|
div(
|
||||||
|
|
@ -1880,6 +1908,7 @@ const renderMobileTopbar = forkNav.renderMobileTopbar;
|
||||||
const renderBottomBar = forkNav.renderBottomBar;
|
const renderBottomBar = forkNav.renderBottomBar;
|
||||||
const renderHiveNav = forkNav.renderHiveNav;
|
const renderHiveNav = forkNav.renderHiveNav;
|
||||||
exports.PINNABLE_MODULES = forkNav.PINNABLE_MODULES;
|
exports.PINNABLE_MODULES = forkNav.PINNABLE_MODULES;
|
||||||
|
exports.LAUNCHER_GROUPS = forkNav.GROUPS;
|
||||||
exports.renderHiveNav = renderHiveNav;
|
exports.renderHiveNav = renderHiveNav;
|
||||||
// se resuelve al llamarse, cuando template ya esta definido mas abajo
|
// se resuelve al llamarse, cuando template ya esta definido mas abajo
|
||||||
exports.bottomBarPickerView = () => forkNav.bottomBarPickerView(template);
|
exports.bottomBarPickerView = () => forkNav.bottomBarPickerView(template);
|
||||||
|
|
@ -2019,6 +2048,8 @@ const template = (titlePrefix, ...elements) => {
|
||||||
// el modulo Karvan trae su propia hoja: asi se ve igual en cualquier tema
|
// el modulo Karvan trae su propia hoja: asi se ve igual en cualquier tema
|
||||||
// y tambien en escritorio, donde no se carga OasisMobile.css
|
// y tambien en escritorio, donde no se carga OasisMobile.css
|
||||||
link({ rel: "stylesheet", href: `/assets/styles/karvan.css?v=${assetVersion()}` }),
|
link({ rel: "stylesheet", href: `/assets/styles/karvan.css?v=${assetVersion()}` }),
|
||||||
|
// movil: piezas de usabilidad de la rama para los dos skins, la ultima para mandar
|
||||||
|
process.env.OASIS_MOBILE === '1' ? link({ rel: "stylesheet", href: `/assets/styles/mobile_ux.css?v=${assetVersion()}` }) : null,
|
||||||
link({ rel: "icon", href: "/assets/images/favicon.svg" }),
|
link({ rel: "icon", href: "/assets/images/favicon.svg" }),
|
||||||
meta({ charset: "utf-8" }),
|
meta({ charset: "utf-8" }),
|
||||||
meta({ name: "description", content: i18n.oasisDescription }),
|
meta({ name: "description", content: i18n.oasisDescription }),
|
||||||
|
|
@ -2188,11 +2219,15 @@ const template = (titlePrefix, ...elements) => {
|
||||||
let here = '';
|
let here = '';
|
||||||
try { here = (require('../models/typed_log').requestScope.getStore() || {}).path || ''; } catch (_) {}
|
try { here = (require('../models/typed_log').requestScope.getStore() || {}).path || ''; } catch (_) {}
|
||||||
if (here === '/welcome' || here.startsWith('/welcome/')) return null;
|
if (here === '/welcome' || here.startsWith('/welcome/')) return null;
|
||||||
|
// en el movil solo en la portada: en cada pagina se comia una quinta parte
|
||||||
|
// de la pantalla. Y el texto dice para que es, no solo "Empezar"
|
||||||
|
const mobile = process.env.OASIS_MOBILE === '1';
|
||||||
|
if (mobile && !['', '/', '/activity'].includes(here)) return null;
|
||||||
return div(
|
return div(
|
||||||
{ class: "update-banner welcome-banner" },
|
{ class: "update-banner welcome-banner" },
|
||||||
span({ class: "update-banner-icon" }, "🌴"),
|
span({ class: "update-banner-icon" }, "🌴"),
|
||||||
span({ class: "update-banner-text" }, i18n.welcomeBannerText),
|
span({ class: "update-banner-text" }, mobile ? (i18n.welcomeBannerTextMobile || i18n.welcomeBannerText) : i18n.welcomeBannerText),
|
||||||
a({ href: "/welcome", class: "create-button welcome-banner-start" }, i18n.welcomeBannerAction),
|
a({ href: "/welcome", class: "create-button welcome-banner-start" }, mobile ? (i18n.welcomeBannerActionMobile || i18n.welcomeBannerAction) : i18n.welcomeBannerAction),
|
||||||
form(
|
form(
|
||||||
{ method: "POST", action: "/welcome/dismiss", class: "welcome-banner-close" },
|
{ method: "POST", action: "/welcome/dismiss", class: "welcome-banner-close" },
|
||||||
button({ type: "submit", class: "welcome-banner-close-btn" }, "✕")
|
button({ type: "submit", class: "welcome-banner-close-btn" }, "✕")
|
||||||
|
|
@ -2408,6 +2443,8 @@ exports.ainavHomeView = ({ recentTags = [] } = {}) => {
|
||||||
// el modulo Karvan trae su propia hoja: asi se ve igual en cualquier tema
|
// el modulo Karvan trae su propia hoja: asi se ve igual en cualquier tema
|
||||||
// y tambien en escritorio, donde no se carga OasisMobile.css
|
// y tambien en escritorio, donde no se carga OasisMobile.css
|
||||||
link({ rel: "stylesheet", href: `/assets/styles/karvan.css?v=${assetVersion()}` }),
|
link({ rel: "stylesheet", href: `/assets/styles/karvan.css?v=${assetVersion()}` }),
|
||||||
|
// movil: piezas de usabilidad de la rama para los dos skins, la ultima para mandar
|
||||||
|
process.env.OASIS_MOBILE === '1' ? link({ rel: "stylesheet", href: `/assets/styles/mobile_ux.css?v=${assetVersion()}` }) : null,
|
||||||
link({ rel: "icon", href: "/assets/images/favicon.svg" }),
|
link({ rel: "icon", href: "/assets/images/favicon.svg" }),
|
||||||
meta({ charset: "utf-8" }),
|
meta({ charset: "utf-8" }),
|
||||||
meta({ name: "description", content: i18n.oasisDescription }),
|
meta({ name: "description", content: i18n.oasisDescription }),
|
||||||
|
|
|
||||||
107
src/views/mobile_icons.js
Normal file
107
src/views/mobile_icons.js
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
"use strict";
|
||||||
|
// Iconos del lanzador y de la barra de abajo (rama movil).
|
||||||
|
//
|
||||||
|
// Antes eran letras del alfabeto vai (ꔙ ꕿ ꖒ...): no decian nada de lo que abren,
|
||||||
|
// dependian de que el movil trajera la fuente y Karvan y Nodos llevaban casi el
|
||||||
|
// mismo reloj de arena. Estos son SVG de trazo en rejilla de 24, el mismo estilo
|
||||||
|
// que los ACTION_ICONS de las tarjetas (main_views.js), y se pintan con
|
||||||
|
// currentColor, asi que cada tema les pone su color.
|
||||||
|
|
||||||
|
const RAW_HTML = Symbol.for('oasis.rawHtml');
|
||||||
|
const { span } = require("../server/node_modules/hyperaxe");
|
||||||
|
|
||||||
|
const ICONS = {
|
||||||
|
activity: '<polyline points="3 12 7 12 10 5 14 19 17 12 21 12"/>',
|
||||||
|
feed: '<path d="M4 6h16M4 12h16M4 18h10"/>',
|
||||||
|
chats: '<path d="M21 11.5a8.5 8.5 0 0 1-12.3 7.6L3 20.5l1.4-5A8.5 8.5 0 1 1 21 11.5z"/>',
|
||||||
|
karvan: '<path d="M21 11.5a8.5 8.5 0 0 1-12.3 7.6L3 20.5l1.4-5A8.5 8.5 0 1 1 21 11.5z"/><path d="M12 7.5v4l2.5 2"/>',
|
||||||
|
rooms: '<path d="M6 21V4a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v17M3 21h18"/><circle cx="14.5" cy="12" r="1"/>',
|
||||||
|
forum: '<path d="M14 9a2 2 0 0 1-2 2H6l-3 3V4a2 2 0 0 1 2-2h7a2 2 0 0 1 2 2z"/><path d="M18 9h1a2 2 0 0 1 2 2v10l-3-3h-6a2 2 0 0 1-2-2v-1"/>',
|
||||||
|
pads: '<path d="M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><path d="M14 3v6h6M8 13h8M8 17h5"/>',
|
||||||
|
wiki: '<path d="M2 4h6a4 4 0 0 1 4 4v13a3 3 0 0 0-3-3H2z"/><path d="M22 4h-6a4 4 0 0 0-4 4v13a3 3 0 0 1 3-3h7z"/>',
|
||||||
|
blogs: '<path d="M16.5 3.5l4 4L8 20H4v-4z"/><path d="M13.5 6.5l4 4"/>',
|
||||||
|
opinions: '<polygon points="12 3 14.8 8.8 21 9.6 16.5 14 17.6 20.3 12 17.3 6.4 20.3 7.5 14 3 9.6 9.2 8.8"/>',
|
||||||
|
mentions: '<circle cx="12" cy="12" r="4"/><path d="M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-4 8"/>',
|
||||||
|
tags: '<path d="M4 9h16M4 15h16M10 3L8 21M16 3l-2 18"/>',
|
||||||
|
trending: '<polyline points="3 17 9 11 13 15 21 7"/><polyline points="15 7 21 7 21 13"/>',
|
||||||
|
maps: '<path d="M12 21s-7-6.2-7-11a7 7 0 0 1 14 0c0 4.8-7 11-7 11z"/><circle cx="12" cy="10" r="2.5"/>',
|
||||||
|
graphos: '<circle cx="5" cy="6" r="2"/><circle cx="19" cy="6" r="2"/><circle cx="12" cy="18" r="2"/><path d="M7 6h10M6 8l5 8M18 8l-5 8"/>',
|
||||||
|
inhabitants: '<circle cx="9" cy="8" r="3.5"/><path d="M2 21a7 7 0 0 1 14 0M16 4.5a3.5 3.5 0 0 1 0 7M22 21a7 7 0 0 0-4-6.3"/>',
|
||||||
|
fediverse: '<circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18"/>',
|
||||||
|
images: '<rect x="3" y="4" width="18" height="16" rx="2"/><circle cx="9" cy="10" r="2"/><path d="M21 16l-5-5-9 9"/>',
|
||||||
|
videos: '<rect x="2" y="6" width="14" height="12" rx="2"/><path d="M16 10l6-3v10l-6-3z"/>',
|
||||||
|
audios: '<path d="M9 18V5l11-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="17" cy="16" r="3"/>',
|
||||||
|
podcasts: '<rect x="9" y="2" width="6" height="12" rx="3"/><path d="M5 11a7 7 0 0 0 14 0M12 18v4M8 22h8"/>',
|
||||||
|
documents: '<path d="M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><path d="M14 3v6h6"/>',
|
||||||
|
files: '<path d="M3 6a2 2 0 0 1 2-2h4l2 3h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>',
|
||||||
|
bookmarks: '<path d="M6 3h12v18l-6-4-6 4z"/>',
|
||||||
|
torrents: '<line x1="7" y1="20" x2="7" y2="4"/><polyline points="3 8 7 4 11 8"/><line x1="17" y1="4" x2="17" y2="20"/><polyline points="13 16 17 20 21 16"/>',
|
||||||
|
games: '<rect x="2" y="7" width="20" height="11" rx="5"/><path d="M7 10.5v4M5 12.5h4"/><circle cx="15.5" cy="11.5" r="1"/><circle cx="18" cy="14" r="1"/>',
|
||||||
|
pixelia: '<rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M3 15h18M9 3v18M15 3v18"/>',
|
||||||
|
melody: '<circle cx="7" cy="17" r="3"/><path d="M10 17V4l8 3"/>',
|
||||||
|
events: '<rect x="3" y="5" width="18" height="16" rx="2"/><path d="M3 10h18M8 3v4M16 3v4"/><circle cx="12" cy="15.5" r="1.5"/>',
|
||||||
|
calendars: '<rect x="3" y="5" width="18" height="16" rx="2"/><path d="M3 10h18M8 3v4M16 3v4M7.5 14h2M14.5 14h2M7.5 17.5h2"/>',
|
||||||
|
tasks: '<path d="M3.5 6l2 2 3.5-3.5M3.5 14l2 2 3.5-3.5M12 7h9M12 15h9"/>',
|
||||||
|
campaigns: '<path d="M3 10v4h4l8 5V5L7 10z"/><path d="M19 9a4 4 0 0 1 0 6"/>',
|
||||||
|
mailing: '<rect x="3" y="5" width="18" height="14" rx="2"/><polyline points="3 7 12 13 21 7"/>',
|
||||||
|
votes: '<path d="M4 12h16v8H4z"/><path d="M8 12V4h8v8"/><path d="M10 8l1.5 1.5L14 7"/>',
|
||||||
|
polls: '<path d="M4 20V10M10 20V4M16 20v-7M2 20h20"/>',
|
||||||
|
reports: '<line x1="5" y1="21" x2="5" y2="3"/><path d="M5 4h12l-2.5 4.5L17 13H5"/>',
|
||||||
|
tribes: '<path d="M2 20h20M5 20l7-14 7 14M10 20l2-5 2 5"/>',
|
||||||
|
emergencies: '<path d="M12 3l10 18H2z"/><path d="M12 10v4M12 17.5v.5"/>',
|
||||||
|
school: '<path d="M2 9l10-5 10 5-10 5z"/><path d="M6 11v5c3 2.5 9 2.5 12 0v-5M22 9v6"/>',
|
||||||
|
larp: '<path d="M12 3l2 7 7 2-7 2-2 7-2-7-7-2 7-2z"/>',
|
||||||
|
parliament: '<path d="M3 21h18M4 10h16M12 3l9 5H3z"/><path d="M6 10v8M10 10v8M14 10v8M18 10v8"/>',
|
||||||
|
courts: '<path d="M12 3v18M7 21h10M5 7h14M5 7l-3 7a3 3 0 0 0 6 0zM19 7l-3 7a3 3 0 0 0 6 0z"/>',
|
||||||
|
market: '<path d="M6 7h12l1 14H5z"/><path d="M9 10V6a3 3 0 0 1 6 0v4"/>',
|
||||||
|
shops: '<path d="M3 9l2-5h14l2 5"/><path d="M3 9a3 3 0 0 0 6 0 3 3 0 0 0 6 0 3 3 0 0 0 6 0"/><path d="M5 12v9h14v-9M10 21v-5h4v5"/>',
|
||||||
|
jobs: '<rect x="3" y="7" width="18" height="13" rx="2"/><path d="M9 7V5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2M3 13h18"/>',
|
||||||
|
projects: '<rect x="3" y="4" width="5" height="16" rx="1"/><rect x="10" y="4" width="5" height="10" rx="1"/><rect x="17" y="4" width="4" height="13" rx="1"/>',
|
||||||
|
industry: '<path d="M2 21h20M4 21V11l5 3V11l5 3V7l6 4v10"/><path d="M8 17h1M13 17h1M18 17h1"/>',
|
||||||
|
housing: '<path d="M3 11l9-7 9 7"/><path d="M5 10v10h14V10M10 20v-6h4v6"/>',
|
||||||
|
logistics: '<path d="M2 6h12v10H2zM14 10h4l3 3v3h-7"/><circle cx="6" cy="18" r="2"/><circle cx="17" cy="18" r="2"/>',
|
||||||
|
banking: '<rect x="2" y="5" width="20" height="14" rx="2"/><path d="M2 10h20M6 15h4"/>',
|
||||||
|
transfers: '<path d="M4 8h16M16 4l4 4-4 4M20 16H4M8 12l-4 4 4 4"/>',
|
||||||
|
profile: '<circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/>',
|
||||||
|
agenda: '<rect x="5" y="4" width="14" height="17" rx="2"/><path d="M9 4V3h6v1M9 11h6M9 15h4"/>',
|
||||||
|
favorites: '<path d="M12 20s-8-5-8-11a4.5 4.5 0 0 1 8-2.8A4.5 4.5 0 0 1 20 9c0 6-8 11-8 11z"/>',
|
||||||
|
phone: '<path d="M5 3h4l2 5-2.5 1.5a11 11 0 0 0 6 6L16 13l5 2v4a2 2 0 0 1-2 2A17 17 0 0 1 3 5a2 2 0 0 1 2-2z"/>',
|
||||||
|
cv: '<rect x="3" y="5" width="18" height="14" rx="2"/><circle cx="9" cy="11" r="2"/><path d="M6 16a3 3 0 0 1 6 0M14 10h4M14 14h4"/>',
|
||||||
|
wallet: '<path d="M3 7a2 2 0 0 1 2-2h13v4"/><rect x="3" y="7" width="18" height="13" rx="2"/><path d="M16 13.5h2"/>',
|
||||||
|
logs: '<path d="M8 6h13M8 12h13M8 18h13M3.5 6h.01M3.5 12h.01M3.5 18h.01"/>',
|
||||||
|
invites: '<circle cx="9" cy="8" r="4"/><path d="M2 21a7 7 0 0 1 14 0M19 8v6M16 11h6"/>',
|
||||||
|
peers: '<circle cx="12" cy="12" r="2"/><path d="M8.5 8.5a5 5 0 0 0 0 7M15.5 8.5a5 5 0 0 1 0 7M5.5 5.5a9.2 9.2 0 0 0 0 13M18.5 5.5a9.2 9.2 0 0 1 0 13"/>',
|
||||||
|
bar: '<path d="M4 21v-7M4 10V3M12 21v-9M12 8V3M20 21v-5M20 12V3M1 14h6M9 8h6M17 16h6"/>',
|
||||||
|
backup: '<path d="M12 3v12M7 10l5 5 5-5"/><path d="M4 17v3h16v-3"/>',
|
||||||
|
cipher: '<rect x="4" y="11" width="16" height="10" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/>',
|
||||||
|
ai: '<rect x="6" y="6" width="12" height="12" rx="2"/><path d="M9 2v4M15 2v4M9 18v4M15 18v4M2 9h4M2 15h4M18 9h4M18 15h4"/>',
|
||||||
|
stats: '<path d="M21 12A9 9 0 1 1 12 3v9z"/><path d="M15 3.5A9 9 0 0 1 20.5 9H15z"/>',
|
||||||
|
blockexplorer: '<path d="M12 2.5l8.5 4.75v9.5L12 21.5l-8.5-4.75v-9.5z"/><polyline points="3.5 7.25 12 12 20.5 7.25"/><line x1="12" y1="12" x2="12" y2="21.5"/>',
|
||||||
|
modules: '<rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/>',
|
||||||
|
settings: '<circle cx="12" cy="12" r="3"/><path d="M12 2v3M12 19v3M4.9 4.9L7 7M17 17l2.1 2.1M2 12h3M19 12h3M4.9 19.1L7 17M17 7l2.1-2.1"/>',
|
||||||
|
search: '<circle cx="11" cy="11" r="7"/><path d="M21 21l-5-5"/>',
|
||||||
|
inbox: '<path d="M3 13l3-8h12l3 8v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1z"/><path d="M3 13h5l1 3h6l1-3h5"/>',
|
||||||
|
publish: '<path d="M12 5v14M5 12h14"/>',
|
||||||
|
data: '<ellipse cx="12" cy="5" rx="8" ry="3"/><path d="M4 5v14c0 1.7 3.6 3 8 3s8-1.3 8-3V5M4 12c0 1.7 3.6 3 8 3s8-1.3 8-3"/>',
|
||||||
|
mic: '<rect x="9" y="2" width="6" height="12" rx="3"/><path d="M5 11a7 7 0 0 0 14 0M12 18v4"/>',
|
||||||
|
camera: '<rect x="2" y="6" width="14" height="12" rx="2"/><path d="M16 10l6-3v10l-6-3z"/>',
|
||||||
|
hangup: '<path d="M3 15.5c5-4.7 13-4.7 18 0l-2.5 2.5-3-1.5v-2.5a10 10 0 0 0-7 0v2.5l-3 1.5z"/>'
|
||||||
|
};
|
||||||
|
|
||||||
|
// la clave sale de la ruta: /settings/bottombar es "bar"
|
||||||
|
const iconKeyOf = (href) => {
|
||||||
|
const h = String(href || '').replace(/^\//, '').split(/[?#]/)[0];
|
||||||
|
if (h === 'settings/bottombar') return 'bar';
|
||||||
|
return h.split('/')[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
const svg = (key) => ICONS[key]
|
||||||
|
? `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">${ICONS[key]}</svg>`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
// span con el SVG dentro; si no hay icono para esa clave, el caracter de antes
|
||||||
|
const iconNode = (cls, key, fallback) => ICONS[key]
|
||||||
|
? span({ class: cls + ' has-svg', 'aria-hidden': 'true', [RAW_HTML]: svg(key) })
|
||||||
|
: span({ class: cls, 'aria-hidden': 'true' }, fallback || '');
|
||||||
|
|
||||||
|
module.exports = { ICONS, iconKeyOf, svg, iconNode };
|
||||||
|
|
@ -1,9 +1,74 @@
|
||||||
const { form, button, div, h2, p, section, table, tr, td, input, a, br, ul, li, span, strong } = require("../server/node_modules/hyperaxe");
|
const { form, button, div, h2, h3, p, section, table, tr, td, input, a, br, ul, li, span, strong, label } = require("../server/node_modules/hyperaxe");
|
||||||
const { template, i18n } = require('./main_views');
|
const { template, i18n, LAUNCHER_GROUPS } = require('./main_views');
|
||||||
|
const { iconNode } = require('./mobile_icons');
|
||||||
const { getConfig } = require('../configs/config-manager.js');
|
const { getConfig } = require('../configs/config-manager.js');
|
||||||
const { PRESETS: PRESET_LIST } = require('../models/workflows_model');
|
const { PRESETS: PRESET_LIST } = require('../models/workflows_model');
|
||||||
|
|
||||||
const modulesView = () => {
|
// en el movil: los modulos agrupados como en el lanzador, cada uno una fila que se
|
||||||
|
// toca entera con un interruptor, y Guardar siempre a la vista. La tabla de 52 filas
|
||||||
|
// con casillas de 16px eran 11 pantallas
|
||||||
|
const mobileModulesView = (visibleModules, moduleStates, opts) => {
|
||||||
|
const groupOf = {};
|
||||||
|
(LAUNCHER_GROUPS || []).forEach(g => g.mods.forEach(m => { if (m.mod) groupOf[m.mod.replace(/Mod$/, '')] = g; }));
|
||||||
|
// lo que no sale en el lanzador: Industria va con la economia; lo demas, al final
|
||||||
|
const economy = (LAUNCHER_GROUPS || []).find(g => g.id === 'economy');
|
||||||
|
if (economy && !groupOf.industry) groupOf.industry = economy;
|
||||||
|
const groups = [];
|
||||||
|
const byId = {};
|
||||||
|
visibleModules.forEach(mod => {
|
||||||
|
const g = groupOf[mod.name] || { id: 'other', titleKey: 'menuTools' };
|
||||||
|
if (!byId[g.id]) { byId[g.id] = { g, mods: [] }; groups.push(byId[g.id]); }
|
||||||
|
byId[g.id].mods.push(mod);
|
||||||
|
});
|
||||||
|
const order = (LAUNCHER_GROUPS || []).map(g => g.id);
|
||||||
|
const rank = (id) => { const i = order.indexOf(id); return i === -1 ? order.length : i; };
|
||||||
|
groups.sort((x, y) => rank(x.g.id) - rank(y.g.id));
|
||||||
|
const on = visibleModules.filter(m => moduleStates[`${m.name}Mod`] === 'on').length;
|
||||||
|
|
||||||
|
const blocked = opts.blocked && visibleModules.find(m => `${m.name}Mod` === opts.blocked.mod);
|
||||||
|
const blockedBox = blocked ? div({ class: 'module-blocked' },
|
||||||
|
iconNode('module-blocked-ico', blocked.name),
|
||||||
|
p({ class: 'module-blocked-text' }, String(i18n.moduleBlockedText || '{name} is turned off.').replace('{name}', blocked.label)),
|
||||||
|
form({ method: 'POST', action: '/modules/enable' },
|
||||||
|
input({ type: 'hidden', name: 'mod', value: blocked.name }),
|
||||||
|
input({ type: 'hidden', name: 'returnTo', value: opts.blocked.path || `/${blocked.name}` }),
|
||||||
|
button({ type: 'submit', class: 'create-button' }, String(i18n.moduleBlockedAction || 'Turn on {name}').replace('{name}', blocked.label))
|
||||||
|
)
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
return template(
|
||||||
|
i18n.modules,
|
||||||
|
section(div({ class: 'tags-header module-header-line' },
|
||||||
|
h2(i18n.modulesViewTitle),
|
||||||
|
p(String(i18n.modulesOnCount || '{on} of {total} sections on').replace('{on}', on).replace('{total}', visibleModules.length))
|
||||||
|
)),
|
||||||
|
blockedBox,
|
||||||
|
opts.saved ? p({ class: 'module-saved', role: 'status' }, i18n.modulesSaved || 'Saved.') : null,
|
||||||
|
section(
|
||||||
|
form({ action: '/save-modules', method: 'post', class: 'module-switches' },
|
||||||
|
...groups.map(({ g, mods }) => div({ class: 'module-group' },
|
||||||
|
h3({ class: 'module-group-title' }, i18n[g.titleKey] || g.id),
|
||||||
|
...mods.map(mod => label({ class: 'module-switch', for: `${mod.name}Mod` },
|
||||||
|
iconNode('module-switch-ico', mod.name),
|
||||||
|
span({ class: 'module-switch-text' },
|
||||||
|
span({ class: 'module-switch-name' }, mod.label),
|
||||||
|
span({ class: 'module-switch-desc' }, mod.description)
|
||||||
|
),
|
||||||
|
input({
|
||||||
|
type: 'checkbox', role: 'switch', id: `${mod.name}Mod`, name: `${mod.name}Form`, value: 'on', class: 'switch-input',
|
||||||
|
checked: moduleStates[`${mod.name}Mod`] === 'on' ? true : undefined
|
||||||
|
})
|
||||||
|
))
|
||||||
|
)),
|
||||||
|
div({ class: 'module-save-bar' },
|
||||||
|
button({ type: 'submit', class: 'create-button' }, i18n.saveSettings)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const modulesView = (_states, opts = {}) => {
|
||||||
const config = getConfig().modules;
|
const config = getConfig().modules;
|
||||||
const modules = [
|
const modules = [
|
||||||
{ name: 'agenda', label: i18n.modulesAgendaLabel, description: i18n.modulesAgendaDescription },
|
{ name: 'agenda', label: i18n.modulesAgendaLabel, description: i18n.modulesAgendaDescription },
|
||||||
|
|
@ -76,6 +141,8 @@ const modulesView = () => {
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
|
if (isMobile) return mobileModulesView(visibleModules, moduleStates, opts);
|
||||||
|
|
||||||
const activeModulesCount = visibleModules.filter(mod => moduleStates[`${mod.name}Mod`] === 'on').length;
|
const activeModulesCount = visibleModules.filter(mod => moduleStates[`${mod.name}Mod`] === 'on').length;
|
||||||
const totalModulesCount = visibleModules.length;
|
const totalModulesCount = visibleModules.length;
|
||||||
const disabledModulesCount = totalModulesCount - activeModulesCount;
|
const disabledModulesCount = totalModulesCount - activeModulesCount;
|
||||||
|
|
|
||||||
|
|
@ -139,6 +139,15 @@ const settingsView = (params) => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// movil: primero lo que se toca (idioma, aspecto, tipo de uso, portada) y despues
|
||||||
|
// lo tecnico (identidades, red, indices...). Se reordena por id, sin tocar las secciones
|
||||||
|
const MOBILE_FIRST = ['update', 'language', 'theme', 'workflows', 'home-page'];
|
||||||
|
const mobileOrder = (parts) => {
|
||||||
|
const list = parts.filter(Boolean);
|
||||||
|
const rank = (el) => { const i = MOBILE_FIRST.indexOf(el && el.id); return i === -1 ? MOBILE_FIRST.length : i; };
|
||||||
|
return list.map((el, i) => ({ el, i })).sort((x, y) => rank(x.el) - rank(y.el) || x.i - y.i).map(x => x.el);
|
||||||
|
};
|
||||||
|
|
||||||
const filter = params.filter === 'multiverse' && modOn('fediverse') ? 'multiverse' : params.filter === 'content' ? 'content' : 'oasis';
|
const filter = params.filter === 'multiverse' && modOn('fediverse') ? 'multiverse' : params.filter === 'content' ? 'content' : 'oasis';
|
||||||
const oasisParts = [
|
const oasisParts = [
|
||||||
updatePanel,
|
updatePanel,
|
||||||
|
|
@ -147,7 +156,13 @@ const settingsView = (params) => {
|
||||||
div({ class: "tags-header" },
|
div({ class: "tags-header" },
|
||||||
h2(i18n.language),
|
h2(i18n.language),
|
||||||
p(i18n.languageDescription),
|
p(i18n.languageDescription),
|
||||||
form(
|
// movil: cada idioma es un boton que se aplica al tocarlo (antes desplegable,
|
||||||
|
// elegir y otro boton mas abajo)
|
||||||
|
isMobile ? form({ action: "/language", method: "post", class: "lang-pick" },
|
||||||
|
...[["Español", "es"], ["Català", "ca"], ["Euskara", "eu"], ["Galego", "gl"], ["English", "en"], ["Français", "fr"],
|
||||||
|
["Português", "pt"], ["Italiano", "it"], ["Deutsch", "de"], ["Русский", "ru"], ["العربية", "ar"], ["हिन्दी", "hi"], ["中文", "zh"]]
|
||||||
|
.map(([name, code]) => button({ type: "submit", name: "language", value: code, class: code === selectedLanguage ? "lang-chip is-current" : "lang-chip", lang: code }, name))
|
||||||
|
) : form(
|
||||||
{ action: "/language", method: "post" },
|
{ action: "/language", method: "post" },
|
||||||
select({ name: "language" }, [
|
select({ name: "language" }, [
|
||||||
languageOption("English", "en"),
|
languageOption("English", "en"),
|
||||||
|
|
@ -674,7 +689,7 @@ const settingsView = (params) => {
|
||||||
modOn('fediverse') ? a({ class: filter === 'multiverse' ? 'filter-btn active' : 'filter-btn', href: '/settings?filter=multiverse' }, String(i18n.multiverse).toUpperCase()) : null
|
modOn('fediverse') ? a({ class: filter === 'multiverse' ? 'filter-btn active' : 'filter-btn', href: '/settings?filter=multiverse' }, String(i18n.multiverse).toUpperCase()) : null
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
...(filter === 'multiverse' ? multiverseParts : filter === 'content' ? contentParts : oasisParts)
|
...(filter === 'multiverse' ? multiverseParts : filter === 'content' ? contentParts : (isMobile ? mobileOrder(oasisParts) : oasisParts))
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
const { div, h2, h3, p, section, button, form, a, input, img, label, select, option, br, textarea, h1, span, nav, ul, li, video, audio, table, tr, td, thead, tbody, th } = require("../server/node_modules/hyperaxe");
|
const { div, h2, h3, p, section, button, form, a, input, img, label, select, option, br, textarea, h1, span, nav, ul, li, video, audio, table, tr, td, thead, tbody, th, details, summary } = require("../server/node_modules/hyperaxe");
|
||||||
const { TEXT_CAP } = require('../backend/long_text');
|
const { TEXT_CAP } = require('../backend/long_text');
|
||||||
const moment = require("../server/node_modules/moment");
|
const moment = require("../server/node_modules/moment");
|
||||||
const { template, i18n, userLink, renderStateChip, renderPrivacyChip, renderLifespanChip, renderModeChip, renderInviteQrCard, renderContentActions, renderSubscriptionBox, renderModuleStats, renderModuleStatsBy, moduleIsEmpty, renderTorrentDownload, torrentDownloadHref, contentDeleteAction, clearnetSlugFor, renderEngagement, renderOpinionsVoting, renderVotesSummary, paged, CLEARNET_HUB_CSS } = require('./main_views');
|
const { template, i18n, userLink, renderStateChip, renderPrivacyChip, renderLifespanChip, renderModeChip, renderInviteQrCard, renderContentActions, renderSubscriptionBox, renderModuleStats, renderModuleStatsBy, moduleIsEmpty, renderTorrentDownload, torrentDownloadHref, contentDeleteAction, clearnetSlugFor, renderEngagement, renderOpinionsVoting, renderVotesSummary, paged, CLEARNET_HUB_CSS } = require('./main_views');
|
||||||
const { renderTribeWikiSection } = require('./wiki_view');
|
const { renderTribeWikiSection } = require('./wiki_view');
|
||||||
|
const { iconNode } = require('./mobile_icons');
|
||||||
const { renderEncryptedChip: renderTribeEncryptedChip, renderLicenseSelect, renderReachChip, renderClearnetPage, escapeHtml, renderRichText, blobUrl: cnBlobUrl, paginateClearnet, renderClearnetPager, CLEARNET_PAGER_CSS } = require('./clearnet_view');
|
const { renderEncryptedChip: renderTribeEncryptedChip, renderLicenseSelect, renderReachChip, renderClearnetPage, escapeHtml, renderRichText, blobUrl: cnBlobUrl, paginateClearnet, renderClearnetPager, CLEARNET_PAGER_CSS } = require('./clearnet_view');
|
||||||
const { renderPollCard } = require('./polls_view');
|
const { renderPollCard } = require('./polls_view');
|
||||||
const { renderFeedCard } = require('./feed_view');
|
const { renderFeedCard } = require('./feed_view');
|
||||||
|
|
@ -286,7 +287,7 @@ exports.tribesView = async (tribes, filter, tribeId, query = {}, allTribes = nul
|
||||||
br,
|
br,
|
||||||
input({ type: 'text', name: 'location', id: 'location', placeholder: i18n.tribeLocationPlaceholder, value: tribeToEdit.location || '' }),
|
input({ type: 'text', name: 'location', id: 'location', placeholder: i18n.tribeLocationPlaceholder, value: tribeToEdit.location || '' }),
|
||||||
br,
|
br,
|
||||||
label(i18n.mapLocationTitle || 'Map Location'),
|
label(process.env.OASIS_MOBILE === '1' ? (i18n.tribeMapUrlLabel || 'Map link (optional)') : (i18n.mapLocationTitle || 'Map Location')),
|
||||||
br,
|
br,
|
||||||
input({ type: 'text', name: 'mapUrl', placeholder: i18n.mapUrlPlaceholder || '/maps/MAP_ID', value: tribeToEdit.mapUrl || '' }),
|
input({ type: 'text', name: 'mapUrl', placeholder: i18n.mapUrlPlaceholder || '/maps/MAP_ID', value: tribeToEdit.mapUrl || '' }),
|
||||||
br,
|
br,
|
||||||
|
|
@ -304,6 +305,7 @@ exports.tribesView = async (tribes, filter, tribeId, query = {}, allTribes = nul
|
||||||
option({ value: 'true', selected: tribeToEdit.isAnonymous === true ? 'selected' : undefined }, i18n.tribePrivate),
|
option({ value: 'true', selected: tribeToEdit.isAnonymous === true ? 'selected' : undefined }, i18n.tribePrivate),
|
||||||
option({ value: 'false', selected: tribeToEdit.isAnonymous === false ? 'selected' : undefined }, i18n.tribePublic)
|
option({ value: 'false', selected: tribeToEdit.isAnonymous === false ? 'selected' : undefined }, i18n.tribePublic)
|
||||||
),
|
),
|
||||||
|
!isSubEdit && process.env.OASIS_MOBILE === '1' ? p({ class: 'form-hint' }, i18n.tribeHintPrivacy || 'Private: only members see it, and it is encrypted. Public: the whole network sees it.') : null,
|
||||||
isSubEdit ? null : br(),
|
isSubEdit ? null : br(),
|
||||||
isSubEdit ? null : br(),
|
isSubEdit ? null : br(),
|
||||||
label({ for: 'inviteMode' }, i18n.tribeModeLabel),
|
label({ for: 'inviteMode' }, i18n.tribeModeLabel),
|
||||||
|
|
@ -312,6 +314,7 @@ exports.tribesView = async (tribes, filter, tribeId, query = {}, allTribes = nul
|
||||||
option({ value: 'strict', selected: tribeToEdit.inviteMode === 'strict' ? 'selected' : undefined }, i18n.tribeStrict),
|
option({ value: 'strict', selected: tribeToEdit.inviteMode === 'strict' ? 'selected' : undefined }, i18n.tribeStrict),
|
||||||
option({ value: 'open', selected: tribeToEdit.inviteMode === 'open' ? 'selected' : undefined }, i18n.tribeOpen)
|
option({ value: 'open', selected: tribeToEdit.inviteMode === 'open' ? 'selected' : undefined }, i18n.tribeOpen)
|
||||||
),
|
),
|
||||||
|
process.env.OASIS_MOBILE === '1' ? p({ class: 'form-hint' }, i18n.tribeHintMode || 'Open: any member can invite people. Strict: only whoever created it can invite.') : null,
|
||||||
br(), br(),
|
br(), br(),
|
||||||
button({ type: 'submit' }, isEdit ? i18n.tribeUpdateButton : i18n.tribeCreateButton)
|
button({ type: 'submit' }, isEdit ? i18n.tribeUpdateButton : i18n.tribeCreateButton)
|
||||||
)
|
)
|
||||||
|
|
@ -321,7 +324,32 @@ exports.tribesView = async (tribes, filter, tribeId, query = {}, allTribes = nul
|
||||||
|
|
||||||
const shownTribes = filter === 'create' || filter === 'edit' ? [] : paged(sorted);
|
const shownTribes = filter === 'create' || filter === 'edit' ? [] : paged(sorted);
|
||||||
|
|
||||||
|
// en el movil, una fila por tribu: con 12 comisiones la lista de tarjetas con la
|
||||||
|
// imagen generica eran 10 pantallas. La imagen solo sale si la tribu tiene una
|
||||||
|
// propia, y borrar/compartir estan dentro de la tribu
|
||||||
|
const tribeRow = (t) => {
|
||||||
|
const href = `/tribe/${encodeURIComponent(t.id)}`;
|
||||||
|
const members = t.memberCount != null ? t.memberCount : (t.members || []).length;
|
||||||
|
const facts = [
|
||||||
|
t.isAnonymous ? i18n.tribePrivate : i18n.tribePublic,
|
||||||
|
t.inviteMode === 'open' ? i18n.tribeOpen : i18n.tribeStrict,
|
||||||
|
`${members} ${String(i18n.tribeMembersCount || '').toLowerCase()}`
|
||||||
|
].filter(Boolean).join(' · ');
|
||||||
|
return a({ href, class: 'tribe-row' + (String(t.author) === String(userId) ? ' own-content' : '') },
|
||||||
|
t.image
|
||||||
|
? renderMediaBlob(t.image, null, { class: 'tribe-row-thumb' })
|
||||||
|
: iconNode('tribe-row-thumb tribe-row-icon', 'tribes'),
|
||||||
|
span({ class: 'tribe-row-text' },
|
||||||
|
span({ class: 'tribe-row-title' }, t.title),
|
||||||
|
span({ class: 'tribe-row-facts' }, facts),
|
||||||
|
t.description ? span({ class: 'tribe-row-desc' }, String(t.description).slice(0, 90)) : null
|
||||||
|
),
|
||||||
|
span({ class: 'tribe-row-go', 'aria-hidden': 'true' }, '›')
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const tribeCards = shownTribes.map(t => {
|
const tribeCards = shownTribes.map(t => {
|
||||||
|
if (process.env.OASIS_MOBILE === '1') return tribeRow(t);
|
||||||
const parentTribe = t.parentTribeId ? allT.find(p => p.id === t.parentTribeId) : null;
|
const parentTribe = t.parentTribeId ? allT.find(p => p.id === t.parentTribeId) : null;
|
||||||
|
|
||||||
const isOwn = String(t.author) === String(userId);
|
const isOwn = String(t.author) === String(userId);
|
||||||
|
|
@ -376,7 +404,7 @@ exports.tribesView = async (tribes, filter, tribeId, query = {}, allTribes = nul
|
||||||
: filter === 'gallery'
|
: filter === 'gallery'
|
||||||
? renderGallery(shownTribes)
|
? renderGallery(shownTribes)
|
||||||
: tribeCards.length > 0
|
: tribeCards.length > 0
|
||||||
? div({ class: 'tribe-grid' }, tribeCards)
|
? div({ class: process.env.OASIS_MOBILE === '1' ? 'tribe-list' : 'tribe-grid' }, tribeCards)
|
||||||
: div({ class: "no-content-box" }, p(i18n.noTribes))
|
: div({ class: "no-content-box" }, p(i18n.noTribes))
|
||||||
),
|
),
|
||||||
...renderLightbox(shownTribes)
|
...renderLightbox(shownTribes)
|
||||||
|
|
@ -409,6 +437,23 @@ const renderSectionNav = (tribe, section) => {
|
||||||
{ items: [{ key: 'images', label: i18n.tribeSectionImages || 'IMAGES' }, { key: 'audios', label: i18n.tribeSectionAudios || 'AUDIOS' }, { key: 'videos', label: i18n.tribeSectionVideos || 'VIDEOS' }, { key: 'documents', label: i18n.tribeSectionDocuments || 'DOCUMENTS' }, { key: 'files', label: i18n.tribeSectionFiles || 'FILES' }, { key: 'bookmarks', label: i18n.tribeSectionBookmarks || 'BOOKMARKS' }] },
|
{ items: [{ key: 'images', label: i18n.tribeSectionImages || 'IMAGES' }, { key: 'audios', label: i18n.tribeSectionAudios || 'AUDIOS' }, { key: 'videos', label: i18n.tribeSectionVideos || 'VIDEOS' }, { key: 'documents', label: i18n.tribeSectionDocuments || 'DOCUMENTS' }, { key: 'files', label: i18n.tribeSectionFiles || 'FILES' }, { key: 'bookmarks', label: i18n.tribeSectionBookmarks || 'BOOKMARKS' }] },
|
||||||
{ items: [{ key: 'tags', label: i18n.tribeSectionTags || 'TAGS' }, { key: 'search', label: i18n.tribeSectionSearch }] },
|
{ items: [{ key: 'tags', label: i18n.tribeSectionTags || 'TAGS' }, { key: 'search', label: i18n.tribeSectionSearch }] },
|
||||||
];
|
];
|
||||||
|
// movil: 25 botones a todo lo ancho eran varias pantallas antes del contenido.
|
||||||
|
// Una tira de fichas con lo que mas usa una comision y el resto en "Mas"
|
||||||
|
if (process.env.OASIS_MOBILE === '1') {
|
||||||
|
const all = sections.flatMap(g => g.items);
|
||||||
|
const MAIN = ['activity', 'inhabitants', 'governance', 'votations', 'forum', 'chats', 'events', 'pads'];
|
||||||
|
const tab = (s) => a({ href: `/tribe/${encodeURIComponent(tribe.id)}?section=${encodeURIComponent(s.key)}`, class: section === s.key ? 'tribe-tab is-current' : 'tribe-tab', ...(section === s.key ? { 'aria-current': 'page' } : {}) }, s.label);
|
||||||
|
const main = all.filter(s => MAIN.includes(s.key));
|
||||||
|
const rest = all.filter(s => !MAIN.includes(s.key));
|
||||||
|
const currentRest = rest.find(s => s.key === section);
|
||||||
|
return nav({ class: 'tribe-tabs', 'aria-label': i18n.tribeSectionsLabel || 'Sections' },
|
||||||
|
div({ class: 'tribe-tabs-strip' }, ...(currentRest ? [tab(currentRest)] : []), ...main.map(tab)),
|
||||||
|
details({ class: 'tribe-tabs-more' },
|
||||||
|
summary(i18n.tribeSectionsMore || 'More'),
|
||||||
|
div({ class: 'tribe-tabs-grid' }, ...rest.filter(s => s !== currentRest).map(tab))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
return div({ class: 'tribe-section-nav no-border' },
|
return div({ class: 'tribe-section-nav no-border' },
|
||||||
sections.map(g =>
|
sections.map(g =>
|
||||||
div({ class: 'tribe-section-group no-border' },
|
div({ class: 'tribe-section-group no-border' },
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
const { div, h2, p, section, button, form, a, textarea, br, input, table, tr, th, td, label, span } = require("../server/node_modules/hyperaxe");
|
const { div, h2, p, section, button, form, a, textarea, br, input, table, tr, th, td, label, span, progress } = require("../server/node_modules/hyperaxe");
|
||||||
const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view");
|
const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view");
|
||||||
const { template, i18n, renderOpinionsVoting, renderEngagement, renderOpenClosedChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions, renderSpreadEditWarning, renderDocumentActions, renderModuleStatsBy, moduleIsEmpty, contentDeleteAction, paged, renderCreatedRows } = require("./main_views");
|
const { template, i18n, renderOpinionsVoting, renderEngagement, renderOpenClosedChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions, renderSpreadEditWarning, renderDocumentActions, renderModuleStatsBy, moduleIsEmpty, contentDeleteAction, paged, renderCreatedRows } = require("./main_views");
|
||||||
const moment = require("../server/node_modules/moment");
|
const moment = require("../server/node_modules/moment");
|
||||||
|
|
@ -7,6 +7,15 @@ const { renderTribeOriginChip } = require("./clearnet_view");
|
||||||
|
|
||||||
const userId = config.keys.id;
|
const userId = config.keys.id;
|
||||||
const VOTE_QUORUM = 2;
|
const VOTE_QUORUM = 2;
|
||||||
|
const isMobile = process.env.OASIS_MOBILE === "1";
|
||||||
|
|
||||||
|
// tipos de votacion en el movil: el servidor ya guarda las opciones que se le
|
||||||
|
// pasen (campo options), aqui solo se ofrecen las que usan las asambleas
|
||||||
|
const VOTE_KINDS = [
|
||||||
|
{ key: "classic", options: null },
|
||||||
|
{ key: "traffic", options: ["GREEN", "YELLOW", "RED"] },
|
||||||
|
{ key: "scale", options: ["1", "2", "3", "4", "5"] }
|
||||||
|
];
|
||||||
|
|
||||||
const safeArray = (v) => Array.isArray(v) ? v : [];
|
const safeArray = (v) => Array.isArray(v) ? v : [];
|
||||||
|
|
||||||
|
|
@ -21,7 +30,7 @@ const computeVoteOutcome = (baseCounts, voteOptions, totalVotesNum) => {
|
||||||
const topOpts = opts.filter((o) => (baseCounts[o] || 0) === maxCount);
|
const topOpts = opts.filter((o) => (baseCounts[o] || 0) === maxCount);
|
||||||
if (maxCount === 0 || topOpts.length > 1) return noResult;
|
if (maxCount === 0 || topOpts.length > 1) return noResult;
|
||||||
const winner = topOpts[0];
|
const winner = topOpts[0];
|
||||||
const variant = winner === "YES" ? "yes" : winner === "NO" ? "no" : "pending";
|
const variant = (winner === "YES" || winner === "GREEN") ? "yes" : (winner === "NO" || winner === "RED") ? "no" : "pending";
|
||||||
return { text: voteLabel(winner), variant };
|
return { text: voteLabel(winner), variant };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -157,6 +166,22 @@ exports.renderVoteOptionsBlock = (v, voteOptions, action, returnTo, canVote) =>
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// en el movil los resultados son barras, una por opcion: la tabla de 6 columnas
|
||||||
|
// no cabia y partia las palabras por silabas
|
||||||
|
const renderVoteBars = (voteOptions, baseCounts) => {
|
||||||
|
const total = voteOptions.reduce((s, o) => s + (Number(baseCounts[o]) || 0), 0);
|
||||||
|
return div({ class: "vote-bars" },
|
||||||
|
...voteOptions.map((opt) => {
|
||||||
|
const n = Number(baseCounts[opt]) || 0;
|
||||||
|
return div({ class: `vote-bar vote-bar-${String(opt).toLowerCase()}` },
|
||||||
|
span({ class: "vote-bar-label" }, voteLabel(opt)),
|
||||||
|
span({ class: "vote-bar-count" }, String(n)),
|
||||||
|
progress({ class: "vote-bar-meter", max: String(Math.max(total, 1)), value: String(n) })
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const renderVoteDetail = (v, voteOptionsDefault, firstRow, secondRow, mode, activeFilter, params = {}) => {
|
const renderVoteDetail = (v, voteOptionsDefault, firstRow, secondRow, mode, activeFilter, params = {}) => {
|
||||||
const voteOptions = Array.isArray(v.options) && v.options.length ? v.options : voteOptionsDefault;
|
const voteOptions = Array.isArray(v.options) && v.options.length ? v.options : voteOptionsDefault;
|
||||||
const baseCounts = voteOptions.reduce((acc, opt) => {
|
const baseCounts = voteOptions.reduce((acc, opt) => {
|
||||||
|
|
@ -212,7 +237,12 @@ const renderVoteDetail = (v, voteOptionsDefault, firstRow, secondRow, mode, acti
|
||||||
|
|
||||||
const voteButtonsNode = renderVoteButtons(v, voteOptions, firstRow, secondRow, returnTo);
|
const voteButtonsNode = renderVoteButtons(v, voteOptions, firstRow, secondRow, returnTo);
|
||||||
|
|
||||||
|
const ownOpenNote = isMobile && !voteButtonsNode && isVoteAuthor(v) && normalizeStatus(v.status) === "OPEN"
|
||||||
|
? p({ class: "vote-own-note" }, i18n.voteOwnNote || "You created this vote, so you cannot vote in it.")
|
||||||
|
: null;
|
||||||
|
|
||||||
const voteMain = div({ class: "tribe-main" },
|
const voteMain = div({ class: "tribe-main" },
|
||||||
|
ownOpenNote,
|
||||||
voteButtonsNode
|
voteButtonsNode
|
||||||
? div({ class: "job-section" },
|
? div({ class: "job-section" },
|
||||||
h2({ class: "job-section-title" }, i18n.voteCastTitle || "Cast Vote"),
|
h2({ class: "job-section-title" }, i18n.voteCastTitle || "Cast Vote"),
|
||||||
|
|
@ -224,7 +254,7 @@ const renderVoteDetail = (v, voteOptionsDefault, firstRow, secondRow, mode, acti
|
||||||
`${i18n.voteResults || "Results"}: `,
|
`${i18n.voteResults || "Results"}: `,
|
||||||
span({ class: `vote-outcome vote-outcome-${outcome.variant}` }, outcome.text)
|
span({ class: `vote-outcome vote-outcome-${outcome.variant}` }, outcome.text)
|
||||||
),
|
),
|
||||||
div({ class: "vote-table" },
|
isMobile ? renderVoteBars(voteOptions, baseCounts) : div({ class: "vote-table" },
|
||||||
table(
|
table(
|
||||||
tr(...voteOptions.map((opt) => th(voteLabel(opt)))),
|
tr(...voteOptions.map((opt) => th(voteLabel(opt)))),
|
||||||
tr(...voteOptions.map((opt) => td(baseCounts[opt])))
|
tr(...voteOptions.map((opt) => td(baseCounts[opt])))
|
||||||
|
|
@ -247,6 +277,22 @@ const renderCommentsSection = (voteId, comments, activeFilter) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderVoteKindPicker = (current) => {
|
||||||
|
const cur = String(current || "");
|
||||||
|
return div({ class: "vote-kind" },
|
||||||
|
label({ class: "vote-kind-title" }, i18n.voteKindLabel || "Type of vote"),
|
||||||
|
...VOTE_KINDS.map((k, i) => {
|
||||||
|
const value = k.options ? k.options.join(",") : "";
|
||||||
|
const checked = cur ? cur === value : i === 0;
|
||||||
|
return label({ class: "vote-kind-option" },
|
||||||
|
input(Object.assign({ type: "radio", name: "options", value }, checked ? { checked: "checked" } : {})),
|
||||||
|
span({ class: "vote-kind-name" }, i18n["voteKind_" + k.key] || k.key),
|
||||||
|
span({ class: "vote-kind-hint" }, i18n["voteKindHint_" + k.key] || "")
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
exports.voteView = async (votes, mode, voteId, comments = [], activeFilterParam, params = {}) => {
|
exports.voteView = async (votes, mode, voteId, comments = [], activeFilterParam, params = {}) => {
|
||||||
const list = Array.isArray(votes) ? votes : [votes];
|
const list = Array.isArray(votes) ? votes : [votes];
|
||||||
|
|
||||||
|
|
@ -351,6 +397,7 @@ exports.voteView = async (votes, mode, voteId, comments = [], activeFilterParam,
|
||||||
}), br(), br(),
|
}), br(), br(),
|
||||||
label(i18n.voteTagsLabel), br(),
|
label(i18n.voteTagsLabel), br(),
|
||||||
input({ type: "text", name: "tags", id: "tags", value: tagsValue }), br(), br(),
|
input({ type: "text", name: "tags", id: "tags", value: tagsValue }), br(), br(),
|
||||||
|
isMobile && mode === "create" ? renderVoteKindPicker(draft.options) : null,
|
||||||
button({ type: "submit" }, mode === "edit" ? i18n.voteUpdateButton : i18n.voteCreateButton)
|
button({ type: "submit" }, mode === "edit" ? i18n.voteUpdateButton : i18n.voteCreateButton)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,15 @@ exports.welcomeView = async (status, profile = {}) => {
|
||||||
const steps = status.steps || {}
|
const steps = status.steps || {}
|
||||||
const usable = status.usable || []
|
const usable = status.usable || []
|
||||||
const opened = usable.find(key => !steps[key])
|
const opened = usable.find(key => !steps[key])
|
||||||
|
// en el movil "Ahora no" va al final y discreto: arriba y a todo lo ancho era lo
|
||||||
|
// primero que se tocaba y la gente se saltaba la bienvenida entera
|
||||||
|
const mobile = process.env.OASIS_MOBILE === "1"
|
||||||
|
const skipForm = status.pending
|
||||||
|
? form({ method: "POST", action: "/welcome/dismiss", class: mobile ? "welcome-action welcome-skip-end" : "welcome-action" },
|
||||||
|
input({ type: "hidden", name: "returnTo", value: "/" }),
|
||||||
|
button({ type: "submit", class: mobile ? "welcome-skip-btn" : undefined }, i18n.welcomeSkip)
|
||||||
|
)
|
||||||
|
: null
|
||||||
const html = template(
|
const html = template(
|
||||||
i18n.welcomeTitle,
|
i18n.welcomeTitle,
|
||||||
section(
|
section(
|
||||||
|
|
@ -111,12 +120,7 @@ exports.welcomeView = async (status, profile = {}) => {
|
||||||
h2(i18n.welcomeTitle),
|
h2(i18n.welcomeTitle),
|
||||||
p(i18n.welcomeDescription)
|
p(i18n.welcomeDescription)
|
||||||
),
|
),
|
||||||
status.pending
|
mobile ? null : skipForm
|
||||||
? form({ method: "POST", action: "/welcome/dismiss", class: "welcome-action" },
|
|
||||||
input({ type: "hidden", name: "returnTo", value: "/" }),
|
|
||||||
button({ type: "submit" }, i18n.welcomeSkip)
|
|
||||||
)
|
|
||||||
: null
|
|
||||||
),
|
),
|
||||||
status.complete
|
status.complete
|
||||||
? section(
|
? section(
|
||||||
|
|
@ -135,7 +139,8 @@ exports.welcomeView = async (status, profile = {}) => {
|
||||||
),
|
),
|
||||||
stepAction(key, profile || {}, status)
|
stepAction(key, profile || {}, status)
|
||||||
)
|
)
|
||||||
))
|
)),
|
||||||
|
mobile && skipForm ? section(skipForm) : null
|
||||||
)
|
)
|
||||||
return status.bootstrap && status.bootstrap.running ? html.replace('</head>', '<meta http-equiv="refresh" content="5"></head>') : html
|
return status.bootstrap && status.bootstrap.running ? html.replace('</head>', '<meta http-equiv="refresh" content="5"></head>') : html
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue