fuera Karvan: se usan el Telefono y las Salas del dev
El dev ya trae Telefono (llamadas uno a uno, buzon de voz) y Salas (hasta 50, con video, grabacion y emision). La rama quita su modulo propio de salas y llamadas: modelo, vista, JS de cliente, CSS, rutas, credenciales TURN, pruebas y textos. - el router propio pasa a llamarse fork_routes.js, sin Karvan - el audio, la camara y la emision de Android se enchufan desde main.js con setAudioFactory/setVideoFactory/setStreamFactory, que exporta el propio dev; SSB_server.js vuelve a tener su linea original - Clear-SNH.css y modules_view.js vuelven a ser identicos a los del dev - el workflow acampada y la config de serie cambian Karvan por Telefono y Salas - README y guia: las llamadas son las del dev; antes de hacer un modulo propio, mirar si el dev ya lo tiene
This commit is contained in:
parent
3937ddab1e
commit
096a7d110b
30 changed files with 266 additions and 1918 deletions
|
|
@ -469,34 +469,6 @@ label.welcome-ux-option:has(input:checked) { background: var(--a26-neon) !import
|
|||
/* el 📌 va en la esquina: el texto deja su hueco (el padding general de la burbuja lo pisaba) */
|
||||
.chat-bubble-row .chat-message.chat-has-pin-toggle { position: relative !important; padding-right: 44px !important; }
|
||||
.chat-day-chip { background: var(--a26-fg) !important; color: var(--a26-bg) !important; border: 0 !important; }
|
||||
|
||||
/* ---------- Karvan (karvan.css trae su paleta amarilla sobre negro) ---------- */
|
||||
.karvan-intro h2, .karvan-room-title, .karvan-back { color: var(--a26-fg) !important; }
|
||||
.karvan-sub, .karvan-ttl-lbl, .karvan-ttl-opt, .karvan-empty, .karvan-expire, .karvan-live,
|
||||
.karvan-room-item .kr-meta, .kc-status { color: var(--a26-muted) !important; }
|
||||
.karvan-room-item {
|
||||
background: var(--a26-bg) !important; color: var(--a26-fg) !important;
|
||||
border: var(--a26-bd) !important; border-radius: var(--a26-radius) !important;
|
||||
box-shadow: 0 4px 0 var(--a26-fg) !important;
|
||||
font-family: var(--a26-tit) !important; font-size: 18px !important; text-transform: uppercase;
|
||||
}
|
||||
.karvan-room-item:active { background: var(--a26-neon) !important; transform: translateY(3px); box-shadow: 0 1px 0 var(--a26-fg) !important; }
|
||||
.karvan-room-item .kr-meta { font-family: var(--a26-txt) !important; text-transform: none; }
|
||||
.karvan-room-head { border-bottom: var(--a26-bd) !important; padding-bottom: 8px !important; }
|
||||
.karvan-room-title { font-family: var(--a26-tit) !important; font-weight: 400 !important; font-size: 20px !important; text-transform: uppercase; }
|
||||
.karvan-msg {
|
||||
background: var(--a26-bg) !important; border: var(--a26-bd-fino) !important;
|
||||
border-radius: 14px 14px 14px 4px !important;
|
||||
}
|
||||
.karvan-msg-self { background: #e5fcff !important; border-color: var(--a26-fg) !important; border-radius: 14px 14px 4px 14px !important; }
|
||||
.karvan-msg .km-from { color: var(--a26-muted) !important; font-weight: 700; }
|
||||
.karvan-msg .km-text { color: var(--a26-fg) !important; }
|
||||
.karvan-ttl, .karvan-compose input, .karvan-compose textarea, .karvan-invite input {
|
||||
background: var(--a26-bg) !important; color: var(--a26-fg) !important; border: var(--a26-bd-fino) !important;
|
||||
}
|
||||
.kc-btn { background: var(--a26-bg) !important; color: var(--a26-fg) !important; border: var(--a26-bd-fino) !important; }
|
||||
.kc-btn.on { background: var(--a26-neon) !important; }
|
||||
.kc-btn-stop { background: var(--a26-bg) !important; color: #c4003b !important; border-color: #c4003b !important; }
|
||||
.chat-reply-cancel,
|
||||
.chat-message-meta .chat-reply-btn { color: var(--a26-fg) !important; }
|
||||
|
||||
|
|
@ -532,5 +504,4 @@ body { font-size: 18px; }
|
|||
.oasis-mobile-topbar .omt-menus .oasis-menu-btn { font-size: 22px !important; min-height: 48px !important; }
|
||||
.tags-header p { font-size: 16px !important; }
|
||||
.card-footer .date-link, .card-footer a.user-link, a.user-link, .date-link { font-size: 15px !important; }
|
||||
.karvan-room-item { font-size: 21px !important; min-height: 60px !important; }
|
||||
input, textarea, select { font-size: 18px !important; min-height: 50px; }
|
||||
|
|
|
|||
|
|
@ -1,103 +0,0 @@
|
|||
/* Estilos del modulo Karvan (salas efimeras y llamadas).
|
||||
*
|
||||
* Viven aparte del tema para que el modulo se vea igual en cualquier tema y
|
||||
* tambien en escritorio, donde OasisMobile.css no se carga. Mismo patron que
|
||||
* highlight.css, que tampoco pertenece a ningun tema concreto.
|
||||
*/
|
||||
|
||||
/* --- Reply a mensaje en chats (server-side ?replyTo=, sin JS) --- */
|
||||
.chat-message-meta .chat-reply-btn { margin-left: auto; padding: 0 4px; color: #FFD700; text-decoration: none; font-size: 15px; opacity: .85; }
|
||||
.chat-message-self .chat-message-meta .chat-reply-btn { margin-left: 8px; }
|
||||
.chat-quote {
|
||||
display: block; border-left: 3px solid #FFD700; padding: 3px 8px; margin: 0 0 5px;
|
||||
background: rgba(255,215,0,.08); border-radius: 6px; font-size: 12px; line-height: 1.3;
|
||||
}
|
||||
.chat-quote-author, .chat-quote-author a { color: #FFD700; font-weight: 700; margin-right: 6px; }
|
||||
.chat-quote-text { color: #FFDD44; }
|
||||
.chat-replying-to {
|
||||
display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
|
||||
border-left: 3px solid #FFD700; background: rgba(255,215,0,.10);
|
||||
padding: 6px 10px; margin-bottom: 8px; border-radius: 8px; font-size: 12px;
|
||||
}
|
||||
.chat-replying-label { color: #FFD700; font-weight: 700; }
|
||||
.chat-reply-cancel { margin-left: auto; color: #FFD700; text-decoration: none; font-weight: 700; font-size: 14px; }
|
||||
/* ==========================================================================
|
||||
FORK_IA_UX · Módulo KARVAN (mensajes temporales + WebRTC) — chat estilo burbujas
|
||||
========================================================================== */
|
||||
.karvan-intro h2 { color: #FFD700; margin: 0 0 6px; }
|
||||
.karvan-sub { color: #FFDD44; font-size: 13px; }
|
||||
.karvan-create input[type="text"] { width: 100%; box-sizing: border-box; padding: 10px 12px;
|
||||
background: #161616; border: 1px solid #333; border-radius: 10px; color: #FFD700; }
|
||||
.karvan-ttl { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; margin: 10px 0; }
|
||||
.karvan-ttl-lbl { color: #FFDD44; font-size: 13px; }
|
||||
.karvan-ttl-opt { display: inline-flex; align-items: center; gap: 4px; color: #FFD700; font-size: 13px; white-space: nowrap; }
|
||||
.karvan-create button, .karvan-compose button { padding: 10px 16px; border: 1px solid #FFD700;
|
||||
border-radius: 10px; background: #FFD700; color: #121212; font-weight: 700; cursor: pointer; }
|
||||
.karvan-rooms { list-style: none; margin: 8px 0 0; padding: 0; }
|
||||
.karvan-rooms li { margin: 0 0 8px; }
|
||||
.karvan-room-item { display: flex; justify-content: space-between; align-items: center; gap: 10px;
|
||||
padding: 12px 14px; background: #161616; border: 1px solid #333; border-radius: 12px;
|
||||
color: #FFD700; text-decoration: none; min-height: 48px; box-sizing: border-box; }
|
||||
.karvan-room-item .kr-title { font-weight: 600; }
|
||||
.karvan-room-item .kr-meta { color: #FFDD44; font-size: 12px; white-space: nowrap; }
|
||||
.karvan-empty { color: #FFDD44; }
|
||||
/* sala */
|
||||
.karvan-room-head { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
|
||||
.karvan-back { color: #FFD700; text-decoration: none; font-weight: 600; }
|
||||
.karvan-room-title { flex: 1 1 auto; color: #FFD700; font-weight: 700; text-align: center;
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.karvan-expire { color: #FFDD44; font-size: 12px; white-space: nowrap; }
|
||||
.karvan-live { display: inline-flex; align-items: center; gap: 6px; color: #FFDD44; font-size: 12px; margin-bottom: 8px; }
|
||||
.karvan-live .kl-dot { width: 8px; height: 8px; border-radius: 50%; background: #777; }
|
||||
.karvan-live.on .kl-dot { background: #35d07f; }
|
||||
.karvan-live.on .kl-txt { color: #35d07f; }
|
||||
.karvan-msgs { list-style: none; margin: 0; padding: 6px 0; display: flex; flex-direction: column; gap: 6px;
|
||||
max-height: 58vh; overflow-y: auto; }
|
||||
.karvan-msg { max-width: 82%; align-self: flex-start; background: #1c1c1c; border: 1px solid #333;
|
||||
border-radius: 14px; padding: 7px 11px; box-sizing: border-box; }
|
||||
.karvan-msg-self { align-self: flex-end; background: #2a2412; border-color: #FFD700; }
|
||||
.karvan-msg-live { border-left: 2px solid #35d07f; }
|
||||
.karvan-msg .km-from { display: block; font-size: 10px; color: #FFDD44; margin-bottom: 2px; }
|
||||
.karvan-msg .km-text { color: #FFD700; word-break: break-word; }
|
||||
.karvan-compose { display: flex; gap: 8px; margin-top: 10px; }
|
||||
.karvan-compose input[type="text"] { flex: 1 1 auto; min-width: 0; padding: 10px 12px;
|
||||
background: #161616; border: 1px solid #333; border-radius: 10px; color: #FFD700; }
|
||||
/* Fase 2: invitar a un contacto por feed id */
|
||||
.karvan-invite { display: flex; gap: 8px; margin-top: 8px; }
|
||||
.karvan-invite input[type="text"] { flex: 1 1 auto; min-width: 0; padding: 9px 12px;
|
||||
background: #161616; border: 1px solid #333; border-radius: 10px; color: #FFD700; font-family: monospace; font-size: 12px; }
|
||||
.karvan-invite button { flex: 0 0 auto; padding: 9px 14px; border: 1px solid #FFD700; background: #161616; color: #FFD700; border-radius: 10px; cursor: pointer; }
|
||||
.karvan-invite-msg { font-size: 12px; margin: 6px 0 0; }
|
||||
.karvan-invite-msg.ok { color: #35d07f; }
|
||||
.karvan-invite-msg.err { color: #ff8a8a; }
|
||||
/* --- Karvan videollamada (Fase 1): media P2P sobre el mismo WebRTC del chat --- */
|
||||
/* el div{background:#222;padding:20px} de style.css y el div{border} del tema
|
||||
convierten cada contenedor del panel en una caja: aqui se neutralizan */
|
||||
.karvan-call, .karvan-videos, .karvan-call-bar, .kc-status {
|
||||
background-color: transparent; box-shadow: none; border: 0; border-radius: 0; padding: 0; margin: 0;
|
||||
}
|
||||
.karvan-call { margin: 10px 0; }
|
||||
.karvan-videos { display: none; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 6px; margin-bottom: 8px; }
|
||||
.karvan-call.active .karvan-videos { display: grid; }
|
||||
.kc-video { width: 100%; aspect-ratio: 4 / 3; background: #000; border-radius: 10px; border: 1px solid #333; object-fit: cover; display: block; }
|
||||
.kc-local { transform: scaleX(-1); }
|
||||
.kc-remote { display: contents; }
|
||||
.karvan-call-bar { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
||||
.kc-btn { min-height: 44px; min-width: 44px; padding: 8px 12px; border-radius: 999px;
|
||||
border: 1px solid #333; background: #161616; color: #FFD700; line-height: 1; cursor: pointer; }
|
||||
.kc-btn.on { background: #FFD700; color: #121212; border-color: #FFD700; }
|
||||
.kc-btn-start { font-weight: 600; }
|
||||
.kc-btn-stop { background: #241414; border-color: #5a2a2a; }
|
||||
.kc-status { font-size: 12px; color: #FFDD44; margin-top: 6px; min-height: 14px; }
|
||||
|
||||
/* ==========================================================================
|
||||
Selector de identidades (ajustes)
|
||||
========================================================================== */
|
||||
.identity-list { list-style: none; margin: 10px 0; padding: 0; }
|
||||
.identity-row { display: flex; align-items: center; justify-content: space-between;
|
||||
gap: 12px; flex-wrap: wrap; padding: 8px 0; border-bottom: 1px solid #2a2a2a; }
|
||||
.identity-info { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
||||
.identity-name { color: #FFD700; font-weight: 700; }
|
||||
.identity-feed { color: #FFDD44; font-size: 12px; font-family: monospace; word-break: break-all; }
|
||||
.identity-warn { color: #ff8a8a; font-size: 12px; }
|
||||
.identity-active { color: #35d07f; font-size: 13px; font-weight: 600; white-space: nowrap; }
|
||||
|
|
@ -775,76 +775,7 @@ button, input[type="submit"], input[type="button"],
|
|||
.header .launcher { width: 100%; max-width: 440px; box-sizing: border-box; margin: 4px auto 10px; padding: 0 14px; }
|
||||
|
||||
/* holgura bajo la topbar fija */
|
||||
body { padding-top: 74px; }
|
||||
|
||||
/* ==========================================================================
|
||||
FORK_IA_UX · Módulo KARVAN (mensajes temporales + WebRTC) — chat estilo burbujas
|
||||
========================================================================== */
|
||||
.karvan-intro h2 { color: #FFD700; margin: 0 0 6px; }
|
||||
.karvan-sub { color: #FFDD44; font-size: 13px; }
|
||||
.karvan-create input[type="text"] { width: 100%; box-sizing: border-box; padding: 10px 12px;
|
||||
background: #161616; border: 1px solid #333; border-radius: 10px; color: #FFD700; }
|
||||
.karvan-ttl { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; margin: 10px 0; }
|
||||
.karvan-ttl-lbl { color: #FFDD44; font-size: 13px; }
|
||||
.karvan-ttl-opt { display: inline-flex; align-items: center; gap: 4px; color: #FFD700; font-size: 13px; }
|
||||
.karvan-create button, .karvan-compose button { padding: 10px 16px; border: 1px solid #FFD700;
|
||||
border-radius: 10px; background: #FFD700; color: #121212; font-weight: 700; cursor: pointer; }
|
||||
.karvan-rooms { list-style: none; margin: 8px 0 0; padding: 0; }
|
||||
.karvan-rooms li { margin: 0 0 8px; }
|
||||
.karvan-room-item { display: flex; justify-content: space-between; align-items: center; gap: 10px;
|
||||
padding: 12px 14px; background: #161616; border: 1px solid #333; border-radius: 12px;
|
||||
color: #FFD700; text-decoration: none; min-height: 48px; box-sizing: border-box; }
|
||||
.karvan-room-item .kr-title { font-weight: 600; }
|
||||
.karvan-room-item .kr-meta { color: #FFDD44; font-size: 12px; white-space: nowrap; }
|
||||
.karvan-empty { color: #FFDD44; }
|
||||
|
||||
/* sala */
|
||||
.karvan-room-head { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
|
||||
.karvan-back { color: #FFD700; text-decoration: none; font-weight: 600; }
|
||||
.karvan-room-title { flex: 1 1 auto; color: #FFD700; font-weight: 700; text-align: center;
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.karvan-expire { color: #FFDD44; font-size: 12px; white-space: nowrap; }
|
||||
.karvan-live { display: inline-flex; align-items: center; gap: 6px; color: #FFDD44; font-size: 12px; margin-bottom: 8px; }
|
||||
.karvan-live .kl-dot { width: 8px; height: 8px; border-radius: 50%; background: #777; }
|
||||
.karvan-live.on .kl-dot { background: #35d07f; }
|
||||
.karvan-live.on .kl-txt { color: #35d07f; }
|
||||
.karvan-msgs { list-style: none; margin: 0; padding: 6px 0; display: flex; flex-direction: column; gap: 6px;
|
||||
max-height: 58vh; overflow-y: auto; }
|
||||
.karvan-msg { max-width: 82%; align-self: flex-start; background: #1c1c1c; border: 1px solid #333;
|
||||
border-radius: 14px; padding: 7px 11px; box-sizing: border-box; }
|
||||
.karvan-msg-self { align-self: flex-end; background: #2a2412; border-color: #FFD700; }
|
||||
.karvan-msg-live { border-left: 2px solid #35d07f; } /* recibido al instante por WebRTC (P2P) */
|
||||
.karvan-msg .km-from { display: block; font-size: 10px; color: #FFDD44; margin-bottom: 2px; }
|
||||
.karvan-msg .km-text { color: #FFD700; word-break: break-word; }
|
||||
.karvan-compose { display: flex; gap: 8px; margin-top: 10px; }
|
||||
.karvan-compose input[type="text"] { flex: 1 1 auto; min-width: 0; padding: 10px 12px;
|
||||
background: #161616; border: 1px solid #333; border-radius: 10px; color: #FFD700; }
|
||||
/* Fase 2: invitar a un contacto por feed id */
|
||||
.karvan-invite { display: flex; gap: 8px; margin-top: 8px; }
|
||||
.karvan-invite input[type="text"] { flex: 1 1 auto; min-width: 0; padding: 9px 12px;
|
||||
background: #161616; border: 1px solid #333; border-radius: 10px; color: #FFD700; font-family: monospace; font-size: 12px; }
|
||||
.karvan-invite button { flex: 0 0 auto; padding: 9px 14px; border: 1px solid #FFD700; background: #161616; color: #FFD700; border-radius: 10px; cursor: pointer; }
|
||||
.karvan-invite-msg { font-size: 12px; margin: 6px 0 0; }
|
||||
.karvan-invite-msg.ok { color: #35d07f; }
|
||||
.karvan-invite-msg.err { color: #ff8a8a; }
|
||||
|
||||
/* --- Karvan videollamada (Fase 1): media P2P sobre el mismo WebRTC del chat --- */
|
||||
.karvan-call, .karvan-videos, .karvan-call-bar, .kc-status {
|
||||
background-color: transparent; box-shadow: none; border-radius: 0; padding: 0; margin: 0; /* anular div{#222} */
|
||||
}
|
||||
.karvan-call { margin: 10px 0; }
|
||||
.karvan-videos { display: none; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 6px; margin-bottom: 8px; }
|
||||
.karvan-call.active .karvan-videos { display: grid; }
|
||||
.kc-video { width: 100%; aspect-ratio: 4 / 3; background: #000; border-radius: 10px; border: 1px solid #333; object-fit: cover; display: block; }
|
||||
.kc-local { transform: scaleX(-1); } /* espejo, como cámara frontal */
|
||||
.kc-remote { display: contents; } /* los vídeos remotos entran en la misma rejilla */
|
||||
.karvan-call-bar { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
||||
.kc-btn { min-height: 44px; min-width: 44px; padding: 8px 12px; border-radius: 999px;
|
||||
border: 1px solid #333; background: #161616; color: #FFD700; line-height: 1; cursor: pointer; }
|
||||
.kc-btn.on { background: #FFD700; color: #121212; border-color: #FFD700; }
|
||||
.kc-btn-start { font-weight: 600; }
|
||||
.kc-btn-stop { background: #241414; border-color: #5a2a2a; }
|
||||
.kc-status { font-size: 12px; color: #FFDD44; margin-top: 6px; min-height: 14px; }
|
||||
body { padding-top: 74px; } /* espejo { display: contents; }
|
||||
|
||||
/* ==========================================================================
|
||||
1.3.x: upstream trae en mobile.css su propio chrome movil (cabecera fija, cajones
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/* 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
|
||||
* Va la ultima, detras del skin. 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.
|
||||
*
|
||||
|
|
@ -31,7 +31,7 @@
|
|||
/* 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 .update-btn, .main-column .tribe-action-btn, .lang-chip,
|
||||
.main-column table th, .card-chips-row > span, .pm-exposition-text {
|
||||
font-family: var(--ux-text) !important;
|
||||
font-weight: 700 !important;
|
||||
|
|
@ -59,9 +59,7 @@
|
|||
}
|
||||
|
||||
/* 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, .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"],
|
||||
el final. Antes unos centrados,
|
||||
.main-column form[action$="/tribes/create"][method="post"] > button[type="submit"],
|
||||
.main-column form[action*="/tribes/update/"] > button[type="submit"] {
|
||||
display: block !important;
|
||||
|
|
@ -69,7 +67,7 @@
|
|||
margin: 8px 0 0 !important;
|
||||
}
|
||||
|
||||
/* ---------- iconos SVG (lanzador, barra, modulos, Karvan) ---------- */
|
||||
/* ---------- iconos SVG (lanzador, barra, modulos) ---------- */
|
||||
.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; }
|
||||
|
|
@ -263,22 +261,6 @@
|
|||
}
|
||||
.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;
|
||||
|
|
@ -407,15 +389,7 @@ body .main-content > .main-column { padding-left: 0 !important; padding-right: 0
|
|||
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; }
|
||||
|
||||
/* llamada solo con voz: sin el video propio vacio */
|
||||
.karvan-call:not(.has-cam) .kc-local { display: none !important; }
|
||||
.karvan-call:not(.has-cam) .karvan-videos:not(:has(.kc-remote video)) { display: none !important; }
|
||||
.karvan-call.active .kc-btn-start { opacity: .45; pointer-events: none; }
|
||||
/* la barra de confirmar borrado con su marco rojo (Acampada quita el borde a los div) */
|
||||
.main-column div.confirm-bar { border: 2px solid var(--ux-danger) !important; }
|
||||
.main-column .content-action-confirm[open] > summary { opacity: 1 !important; }
|
||||
|
|
|
|||
|
|
@ -364,19 +364,6 @@ button.tribe-action-btn { background: transparent !important; border: 1px solid
|
|||
.chat-topic-active { background: #FFF3E0 !important; }
|
||||
.chat-topic-title { color: #2D2D2D !important; }
|
||||
.chat-workspace-empty { border-color: #DDD !important; }
|
||||
|
||||
/* Karvan: su hoja (karvan.css) esta pensada para fondo oscuro; aqui se traduce
|
||||
a la paleta clara, igual que este tema hace con el resto de la interfaz */
|
||||
.karvan-back, .karvan-room-title, .kr-title { color: #B25000 !important; }
|
||||
.karvan-sub, .karvan-expire, .karvan-live, .kr-meta,
|
||||
.karvan-ttl-lbl, .karvan-ttl-opt { color: #6B5300 !important; }
|
||||
.karvan-room-item, .karvan-create input[type="text"] {
|
||||
background-color: #FFFFFF !important; border: 1px solid #E0E0E0 !important; color: #2C2C2C !important;
|
||||
}
|
||||
.karvan-msg { background-color: #FFF8E1 !important; border: 1px solid #E6CE86 !important; }
|
||||
.karvan-msg-self { background-color: #FFEFC2 !important; border-color: #D9A400 !important; }
|
||||
.karvan-msg .km-from { color: #8A6D00 !important; }
|
||||
.karvan-msg .km-text { color: #2C2C2C !important; }
|
||||
.map-viewer, .map-stage div, .map-hero, .map-toolbar, .map-toolbar div, .map-place-results, .map-markers-list, .map-markers-list div, .map-detail-grid, .map-card-thumb .map-viewer { background: transparent !important; border: 0 !important; }
|
||||
.map-pick-cell, .map-pick-cell:hover { border: 0 !important; padding: 0 !important; border-radius: 0 !important; box-shadow: none !important; }
|
||||
.map-pick-cell { background: transparent !important; }
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
// ficheros oasis_*.js (upstream los reescribe entero en cada release).
|
||||
//
|
||||
// Contiene tres grupos:
|
||||
// 1. Claves que anade la rama: el modulo Karvan, el selector de identidades,
|
||||
// 1. Claves que anade la rama: el selector de identidades,
|
||||
// la barra inferior (bb*) y el filtro Personal/Social de la topbar.
|
||||
// 2. Claves que upstream tenia en 0.9.1 y elimino en 0.9.5, pero que el
|
||||
// codigo de la rama sigue usando (menuBlogs, spreadHint, chatShareUrl...).
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
|
||||
module.exports = {
|
||||
en: {
|
||||
// usabilidad (19/10/2026): votaciones, borrar, modulos, Karvan, pie
|
||||
// usabilidad (19/10/2026): votaciones, borrar, modulos, pie
|
||||
workflow_acampada: "Camp",
|
||||
tribeSectionsMore: "More",
|
||||
tribeSectionsLabel: "Sections",
|
||||
|
|
@ -48,43 +48,11 @@ module.exports = {
|
|||
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)
|
||||
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.",
|
||||
// fotos que no se pueden limpiar (blobHandler.js)
|
||||
imageMetadataUnsafe: "This image could not be cleaned of its metadata (location, camera…), so it has not been uploaded. Save it as JPEG or PNG and try again.",
|
||||
// Karvan
|
||||
karvanActive: "Active rooms",
|
||||
karvanBlurb: "Rooms that live only in memory and destroy themselves.",
|
||||
karvanCall: "Call",
|
||||
karvanCreate: "Create ephemeral room",
|
||||
karvanEphemeral: "Ephemeral rooms",
|
||||
karvanInvite: "Invite",
|
||||
karvanInviteBad: "✗ That is not a valid @feed-id.",
|
||||
karvanInviteFail: "✗ Could not send the invitation.",
|
||||
karvanInvitePlaceholder: "Invite a contact by @feed-id…",
|
||||
karvanInviteSent: "✓ Invitation sent to their inbox.",
|
||||
karvanNone: "No active rooms. Create one — it disappears on its own.",
|
||||
karvanRelay: "server relay",
|
||||
karvanRoomName: "Room name (optional)",
|
||||
karvanRooms: "Rooms",
|
||||
karvanSay: "Temporary message…",
|
||||
karvanSend: "Send",
|
||||
karvanTitle: "Karvan",
|
||||
karvanTtl: "Self-destruct in:",
|
||||
modulesKarvanDescription: "Ephemeral rooms: messages live only in memory and self-destruct.",
|
||||
modulesKarvanLabel: "Karvan",
|
||||
// Identidades
|
||||
identitiesActive: "In use",
|
||||
identitiesBackup: "Export or import an identity",
|
||||
|
|
@ -127,7 +95,7 @@ module.exports = {
|
|||
spreadMore: "more"
|
||||
},
|
||||
es: {
|
||||
// usabilidad (19/10/2026): votaciones, borrar, modulos, Karvan, pie
|
||||
// usabilidad (19/10/2026): votaciones, borrar, modulos, pie
|
||||
workflow_acampada: "Acampada",
|
||||
tribeSectionsMore: "Más",
|
||||
tribeSectionsLabel: "Secciones",
|
||||
|
|
@ -158,43 +126,11 @@ module.exports = {
|
|||
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)
|
||||
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á.",
|
||||
// fotos que no se pueden limpiar (blobHandler.js)
|
||||
imageMetadataUnsafe: "No se han podido quitar los metadatos de esta imagen (ubicación, cámara…), así que no se ha subido. Guárdala como JPEG o PNG y vuelve a intentarlo.",
|
||||
// Karvan
|
||||
karvanActive: "Salas activas",
|
||||
karvanBlurb: "Salas que viven solo en memoria y se destruyen solas.",
|
||||
karvanCall: "Llamar",
|
||||
karvanCreate: "Crear sala efímera",
|
||||
karvanEphemeral: "Salas efímeras",
|
||||
karvanInvite: "Invitar",
|
||||
karvanInviteBad: "✗ Eso no es un @feed-id válido.",
|
||||
karvanInviteFail: "✗ No se ha podido enviar la invitación.",
|
||||
karvanInvitePlaceholder: "Invitar a un contacto por @feed-id…",
|
||||
karvanInviteSent: "✓ Invitación enviada a su bandeja.",
|
||||
karvanNone: "No hay salas activas. Crea una: desaparece sola.",
|
||||
karvanRelay: "a través del servidor",
|
||||
karvanRoomName: "Nombre de la sala (opcional)",
|
||||
karvanRooms: "Salas",
|
||||
karvanSay: "Mensaje temporal…",
|
||||
karvanSend: "Enviar",
|
||||
karvanTitle: "Karvan",
|
||||
karvanTtl: "Se autodestruye en:",
|
||||
modulesKarvanDescription: "Salas efímeras: los mensajes viven solo en memoria y se autodestruyen.",
|
||||
modulesKarvanLabel: "Karvan",
|
||||
// Identidades
|
||||
identitiesActive: "En uso",
|
||||
identitiesBackup: "Exportar o importar una identidad",
|
||||
|
|
@ -243,7 +179,7 @@ module.exports = {
|
|||
totalspreads: "Réplicas totales"
|
||||
},
|
||||
fr: {
|
||||
// usabilidad (19/10/2026): votaciones, borrar, modulos, Karvan, pie
|
||||
// usabilidad (19/10/2026): votaciones, borrar, modulos, pie
|
||||
workflow_acampada: "Campement",
|
||||
tribeSectionsMore: "Plus",
|
||||
tribeSectionsLabel: "Sections",
|
||||
|
|
@ -274,43 +210,11 @@ module.exports = {
|
|||
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)
|
||||
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.",
|
||||
// fotos que no se pueden limpiar (blobHandler.js)
|
||||
imageMetadataUnsafe: "Impossible de retirer les métadonnées de cette image (position, appareil…), elle n'a donc pas été envoyée. Enregistrez-la en JPEG ou PNG et réessayez.",
|
||||
// Karvan
|
||||
karvanActive: "Salons actifs",
|
||||
karvanBlurb: "Des salons qui ne vivent qu'en mémoire et se détruisent d'eux-mêmes.",
|
||||
karvanCall: "Appeler",
|
||||
karvanCreate: "Créer un salon éphémère",
|
||||
karvanEphemeral: "Salons éphémères",
|
||||
karvanInvite: "Inviter",
|
||||
karvanInviteBad: "✗ Ce n'est pas un @feed-id valide.",
|
||||
karvanInviteFail: "✗ Impossible d'envoyer l'invitation.",
|
||||
karvanInvitePlaceholder: "Inviter un contact par @feed-id…",
|
||||
karvanInviteSent: "✓ Invitation envoyée dans sa boîte de réception.",
|
||||
karvanNone: "Aucun salon actif. Créez-en un : il disparaîtra tout seul.",
|
||||
karvanRelay: "relais par le serveur",
|
||||
karvanRoomName: "Nom du salon (facultatif)",
|
||||
karvanRooms: "Salons",
|
||||
karvanSay: "Message temporaire…",
|
||||
karvanSend: "Envoyer",
|
||||
karvanTitle: "Karvan",
|
||||
karvanTtl: "Autodestruction dans :",
|
||||
modulesKarvanDescription: "Salons éphémères : les messages ne vivent qu'en mémoire et s'autodétruisent.",
|
||||
modulesKarvanLabel: "Karvan",
|
||||
// Identidades
|
||||
identitiesActive: "En cours d'utilisation",
|
||||
identitiesBackup: "Exporter ou importer une identité",
|
||||
|
|
@ -353,7 +257,7 @@ module.exports = {
|
|||
spreadMore: "plus"
|
||||
},
|
||||
eu: {
|
||||
// usabilidad (19/10/2026): votaciones, borrar, modulos, Karvan, pie
|
||||
// usabilidad (19/10/2026): votaciones, borrar, modulos, pie
|
||||
workflow_acampada: "Kanpaldia",
|
||||
tribeSectionsMore: "Gehiago",
|
||||
tribeSectionsLabel: "Atalak",
|
||||
|
|
@ -384,43 +288,11 @@ module.exports = {
|
|||
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)
|
||||
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.",
|
||||
// fotos que no se pueden limpiar (blobHandler.js)
|
||||
imageMetadataUnsafe: "Ezin izan dira irudi honen metadatuak kendu (kokapena, kamera…), beraz ez da igo. Gorde JPEG edo PNG gisa eta saiatu berriro.",
|
||||
// Karvan
|
||||
karvanActive: "Gela aktiboak",
|
||||
karvanBlurb: "Memorian bakarrik bizi diren eta beren kabuz suntsitzen diren gelak.",
|
||||
karvanCall: "Deitu",
|
||||
karvanCreate: "Sortu gela iragankorra",
|
||||
karvanEphemeral: "Gela iragankorrak",
|
||||
karvanInvite: "Gonbidatu",
|
||||
karvanInviteBad: "✗ Hori ez da @feed-id baliozkoa.",
|
||||
karvanInviteFail: "✗ Ezin izan da gonbidapena bidali.",
|
||||
karvanInvitePlaceholder: "Gonbidatu kontaktu bat @feed-id bidez…",
|
||||
karvanInviteSent: "✓ Gonbidapena bere sarrera-ontzira bidali da.",
|
||||
karvanNone: "Ez dago gela aktiborik. Sortu bat: bere kabuz desagertuko da.",
|
||||
karvanRelay: "zerbitzariaren bidez",
|
||||
karvanRoomName: "Gelaren izena (aukerakoa)",
|
||||
karvanRooms: "Gelak",
|
||||
karvanSay: "Aldi baterako mezua…",
|
||||
karvanSend: "Bidali",
|
||||
karvanTitle: "Karvan",
|
||||
karvanTtl: "Bere burua suntsituko du:",
|
||||
modulesKarvanDescription: "Gela iragankorrak: mezuak memorian bakarrik bizi dira eta beren burua suntsitzen dute.",
|
||||
modulesKarvanLabel: "Karvan",
|
||||
// Identidades
|
||||
identitiesActive: "Erabiltzen",
|
||||
identitiesBackup: "Esportatu edo inportatu nortasun bat",
|
||||
|
|
@ -463,7 +335,7 @@ module.exports = {
|
|||
spreadMore: "gehiago"
|
||||
},
|
||||
de: {
|
||||
// usabilidad (19/10/2026): votaciones, borrar, modulos, Karvan, pie
|
||||
// usabilidad (19/10/2026): votaciones, borrar, modulos, pie
|
||||
workflow_acampada: "Camp",
|
||||
tribeSectionsMore: "Mehr",
|
||||
tribeSectionsLabel: "Bereiche",
|
||||
|
|
@ -494,43 +366,11 @@ module.exports = {
|
|||
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)
|
||||
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.",
|
||||
// fotos que no se pueden limpiar (blobHandler.js)
|
||||
imageMetadataUnsafe: "Die Metadaten dieses Bildes (Standort, Kamera…) konnten nicht entfernt werden, daher wurde es nicht hochgeladen. Speichere es als JPEG oder PNG und versuche es erneut.",
|
||||
// Karvan
|
||||
karvanActive: "Aktive Räume",
|
||||
karvanBlurb: "Räume, die nur im Speicher leben und sich von selbst zerstören.",
|
||||
karvanCall: "Anrufen",
|
||||
karvanCreate: "Flüchtigen Raum erstellen",
|
||||
karvanEphemeral: "Flüchtige Räume",
|
||||
karvanInvite: "Einladen",
|
||||
karvanInviteBad: "✗ Das ist keine gültige @feed-id.",
|
||||
karvanInviteFail: "✗ Einladung konnte nicht gesendet werden.",
|
||||
karvanInvitePlaceholder: "Kontakt per @feed-id einladen…",
|
||||
karvanInviteSent: "✓ Einladung an den Posteingang gesendet.",
|
||||
karvanNone: "Keine aktiven Räume. Erstelle einen — er verschwindet von selbst.",
|
||||
karvanRelay: "über den Server",
|
||||
karvanRoomName: "Raumname (optional)",
|
||||
karvanRooms: "Räume",
|
||||
karvanSay: "Temporäre Nachricht…",
|
||||
karvanSend: "Senden",
|
||||
karvanTitle: "Karvan",
|
||||
karvanTtl: "Selbstzerstörung in:",
|
||||
modulesKarvanDescription: "Flüchtige Räume: Nachrichten leben nur im Speicher und zerstören sich selbst.",
|
||||
modulesKarvanLabel: "Karvan",
|
||||
// Identidades
|
||||
identitiesActive: "In Benutzung",
|
||||
identitiesBackup: "Identität exportieren oder importieren",
|
||||
|
|
@ -573,7 +413,7 @@ module.exports = {
|
|||
spreadMore: "mehr"
|
||||
},
|
||||
it: {
|
||||
// usabilidad (19/10/2026): votaciones, borrar, modulos, Karvan, pie
|
||||
// usabilidad (19/10/2026): votaciones, borrar, modulos, pie
|
||||
workflow_acampada: "Accampata",
|
||||
tribeSectionsMore: "Altro",
|
||||
tribeSectionsLabel: "Sezioni",
|
||||
|
|
@ -604,43 +444,11 @@ module.exports = {
|
|||
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)
|
||||
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'è.",
|
||||
// fotos que no se pueden limpiar (blobHandler.js)
|
||||
imageMetadataUnsafe: "Non è stato possibile rimuovere i metadati di questa immagine (posizione, fotocamera…), quindi non è stata caricata. Salvala come JPEG o PNG e riprova.",
|
||||
// Karvan
|
||||
karvanActive: "Stanze attive",
|
||||
karvanBlurb: "Stanze che vivono solo in memoria e si distruggono da sole.",
|
||||
karvanCall: "Chiama",
|
||||
karvanCreate: "Crea stanza effimera",
|
||||
karvanEphemeral: "Stanze effimere",
|
||||
karvanInvite: "Invita",
|
||||
karvanInviteBad: "✗ Non è un @feed-id valido.",
|
||||
karvanInviteFail: "✗ Impossibile inviare l'invito.",
|
||||
karvanInvitePlaceholder: "Invita un contatto con @feed-id…",
|
||||
karvanInviteSent: "✓ Invito inviato alla sua casella.",
|
||||
karvanNone: "Nessuna stanza attiva. Creane una: sparisce da sola.",
|
||||
karvanRelay: "tramite il server",
|
||||
karvanRoomName: "Nome della stanza (facoltativo)",
|
||||
karvanRooms: "Stanze",
|
||||
karvanSay: "Messaggio temporaneo…",
|
||||
karvanSend: "Invia",
|
||||
karvanTitle: "Karvan",
|
||||
karvanTtl: "Autodistruzione tra:",
|
||||
modulesKarvanDescription: "Stanze effimere: i messaggi vivono solo in memoria e si autodistruggono.",
|
||||
modulesKarvanLabel: "Karvan",
|
||||
// Identidades
|
||||
identitiesActive: "In uso",
|
||||
identitiesBackup: "Esporta o importa un'identità",
|
||||
|
|
@ -683,7 +491,7 @@ module.exports = {
|
|||
spreadMore: "altro"
|
||||
},
|
||||
pt: {
|
||||
// usabilidad (19/10/2026): votaciones, borrar, modulos, Karvan, pie
|
||||
// usabilidad (19/10/2026): votaciones, borrar, modulos, pie
|
||||
workflow_acampada: "Acampada",
|
||||
tribeSectionsMore: "Mais",
|
||||
tribeSectionsLabel: "Secções",
|
||||
|
|
@ -714,43 +522,11 @@ module.exports = {
|
|||
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)
|
||||
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á.",
|
||||
// fotos que no se pueden limpiar (blobHandler.js)
|
||||
imageMetadataUnsafe: "Não foi possível remover os metadados desta imagem (localização, câmara…), por isso não foi enviada. Guarda-a como JPEG ou PNG e tenta de novo.",
|
||||
// Karvan
|
||||
karvanActive: "Salas ativas",
|
||||
karvanBlurb: "Salas que vivem apenas em memória e destroem-se sozinhas.",
|
||||
karvanCall: "Chamar",
|
||||
karvanCreate: "Criar sala efémera",
|
||||
karvanEphemeral: "Salas efémeras",
|
||||
karvanInvite: "Convidar",
|
||||
karvanInviteBad: "✗ Isso não é um @feed-id válido.",
|
||||
karvanInviteFail: "✗ Não foi possível enviar o convite.",
|
||||
karvanInvitePlaceholder: "Convidar um contacto por @feed-id…",
|
||||
karvanInviteSent: "✓ Convite enviado para a caixa de entrada.",
|
||||
karvanNone: "Não há salas ativas. Cria uma: desaparece sozinha.",
|
||||
karvanRelay: "através do servidor",
|
||||
karvanRoomName: "Nome da sala (opcional)",
|
||||
karvanRooms: "Salas",
|
||||
karvanSay: "Mensagem temporária…",
|
||||
karvanSend: "Enviar",
|
||||
karvanTitle: "Karvan",
|
||||
karvanTtl: "Autodestrói-se em:",
|
||||
modulesKarvanDescription: "Salas efémeras: as mensagens vivem apenas em memória e autodestroem-se.",
|
||||
modulesKarvanLabel: "Karvan",
|
||||
// Identidades
|
||||
identitiesActive: "Em uso",
|
||||
identitiesBackup: "Exportar ou importar uma identidade",
|
||||
|
|
@ -793,7 +569,7 @@ module.exports = {
|
|||
spreadMore: "mais"
|
||||
},
|
||||
zh: {
|
||||
// usabilidad (19/10/2026): votaciones, borrar, modulos, Karvan, pie
|
||||
// usabilidad (19/10/2026): votaciones, borrar, modulos, pie
|
||||
workflow_acampada: "营地",
|
||||
tribeSectionsMore: "更多",
|
||||
tribeSectionsLabel: "板块",
|
||||
|
|
@ -824,43 +600,11 @@ module.exports = {
|
|||
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)
|
||||
skinIntroMobile: "选择 Oasis 在手机上的外观。点一下就会生效。",
|
||||
workflowsDescriptionMobile: "工作流决定启用哪些模块。在手机上,你的外观保持不变。",
|
||||
// fotos que no se pueden limpiar (blobHandler.js)
|
||||
imageMetadataUnsafe: "无法清除这张图片的元数据(位置、相机等),因此没有上传。请另存为 JPEG 或 PNG 后重试。",
|
||||
// Karvan
|
||||
karvanActive: "活跃房间",
|
||||
karvanBlurb: "只存在于内存中、会自行销毁的房间。",
|
||||
karvanCall: "通话",
|
||||
karvanCreate: "创建临时房间",
|
||||
karvanEphemeral: "临时房间",
|
||||
karvanInvite: "邀请",
|
||||
karvanInviteBad: "✗ 这不是有效的 @feed-id。",
|
||||
karvanInviteFail: "✗ 无法发送邀请。",
|
||||
karvanInvitePlaceholder: "用 @feed-id 邀请联系人…",
|
||||
karvanInviteSent: "✓ 邀请已发送至对方收件箱。",
|
||||
karvanNone: "没有活跃房间。创建一个吧——它会自行消失。",
|
||||
karvanRelay: "经服务器中转",
|
||||
karvanRoomName: "房间名称(可选)",
|
||||
karvanRooms: "房间",
|
||||
karvanSay: "临时消息…",
|
||||
karvanSend: "发送",
|
||||
karvanTitle: "Karvan",
|
||||
karvanTtl: "自动销毁于:",
|
||||
modulesKarvanDescription: "临时房间:消息只存在于内存中,并会自动销毁。",
|
||||
modulesKarvanLabel: "Karvan",
|
||||
// Identidades
|
||||
identitiesActive: "使用中",
|
||||
identitiesBackup: "导出或导入身份",
|
||||
|
|
@ -903,7 +647,7 @@ module.exports = {
|
|||
spreadMore: "更多"
|
||||
},
|
||||
ar: {
|
||||
// usabilidad (19/10/2026): votaciones, borrar, modulos, Karvan, pie
|
||||
// usabilidad (19/10/2026): votaciones, borrar, modulos, pie
|
||||
workflow_acampada: "مخيم",
|
||||
tribeSectionsMore: "المزيد",
|
||||
tribeSectionsLabel: "الأقسام",
|
||||
|
|
@ -934,43 +678,11 @@ module.exports = {
|
|||
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)
|
||||
skinIntroMobile: "اختر مظهر Oasis على هاتفك. يُطبَّق بمجرد اللمس.",
|
||||
workflowsDescriptionMobile: "يحدد سير العمل الوحدات المفعّلة. على الهاتف يبقى مظهرك كما هو.",
|
||||
// fotos que no se pueden limpiar (blobHandler.js)
|
||||
imageMetadataUnsafe: "تعذّر إزالة البيانات الوصفية من هذه الصورة (الموقع، الكاميرا…)، لذلك لم تُرفع. احفظها بصيغة JPEG أو PNG وحاول مجددًا.",
|
||||
// Karvan
|
||||
karvanActive: "الغرف النشطة",
|
||||
karvanBlurb: "غرف تعيش في الذاكرة فقط وتدمّر نفسها بنفسها.",
|
||||
karvanCall: "اتصال",
|
||||
karvanCreate: "إنشاء غرفة مؤقتة",
|
||||
karvanEphemeral: "غرف مؤقتة",
|
||||
karvanInvite: "دعوة",
|
||||
karvanInviteBad: "✗ هذا ليس @feed-id صالحًا.",
|
||||
karvanInviteFail: "✗ تعذّر إرسال الدعوة.",
|
||||
karvanInvitePlaceholder: "ادعُ جهة اتصال عبر @feed-id…",
|
||||
karvanInviteSent: "✓ تم إرسال الدعوة إلى صندوق الوارد.",
|
||||
karvanNone: "لا توجد غرف نشطة. أنشئ واحدة، وستختفي وحدها.",
|
||||
karvanRelay: "عبر الخادم",
|
||||
karvanRoomName: "اسم الغرفة (اختياري)",
|
||||
karvanRooms: "الغرف",
|
||||
karvanSay: "رسالة مؤقتة…",
|
||||
karvanSend: "إرسال",
|
||||
karvanTitle: "Karvan",
|
||||
karvanTtl: "التدمير الذاتي خلال:",
|
||||
modulesKarvanDescription: "غرف مؤقتة: الرسائل تعيش في الذاكرة فقط وتدمّر نفسها.",
|
||||
modulesKarvanLabel: "Karvan",
|
||||
// Identidades
|
||||
identitiesActive: "قيد الاستخدام",
|
||||
identitiesBackup: "تصدير هوية أو استيرادها",
|
||||
|
|
@ -1013,7 +725,7 @@ module.exports = {
|
|||
spreadMore: "المزيد"
|
||||
},
|
||||
hi: {
|
||||
// usabilidad (19/10/2026): votaciones, borrar, modulos, Karvan, pie
|
||||
// usabilidad (19/10/2026): votaciones, borrar, modulos, pie
|
||||
workflow_acampada: "शिविर",
|
||||
tribeSectionsMore: "और",
|
||||
tribeSectionsLabel: "खंड",
|
||||
|
|
@ -1044,43 +756,11 @@ module.exports = {
|
|||
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)
|
||||
skinIntroMobile: "चुनें कि आपके फ़ोन पर Oasis कैसा दिखे। छूते ही लागू हो जाता है।",
|
||||
workflowsDescriptionMobile: "वर्कफ़्लो तय करता है कि कौन-से मॉड्यूल चालू हैं। फ़ोन पर आपकी स्किन वैसी ही रहती है।",
|
||||
// fotos que no se pueden limpiar (blobHandler.js)
|
||||
imageMetadataUnsafe: "इस छवि का मेटाडेटा (स्थान, कैमरा…) हटाया नहीं जा सका, इसलिए इसे अपलोड नहीं किया गया। इसे JPEG या PNG के रूप में सहेजें और फिर कोशिश करें।",
|
||||
// Karvan
|
||||
karvanActive: "सक्रिय कक्ष",
|
||||
karvanBlurb: "ऐसे कक्ष जो केवल मेमोरी में रहते हैं और स्वयं नष्ट हो जाते हैं।",
|
||||
karvanCall: "कॉल",
|
||||
karvanCreate: "अस्थायी कक्ष बनाएँ",
|
||||
karvanEphemeral: "अस्थायी कक्ष",
|
||||
karvanInvite: "आमंत्रित करें",
|
||||
karvanInviteBad: "✗ यह मान्य @feed-id नहीं है।",
|
||||
karvanInviteFail: "✗ आमंत्रण नहीं भेजा जा सका।",
|
||||
karvanInvitePlaceholder: "@feed-id से संपर्क आमंत्रित करें…",
|
||||
karvanInviteSent: "✓ आमंत्रण उनके इनबॉक्स में भेजा गया।",
|
||||
karvanNone: "कोई सक्रिय कक्ष नहीं। एक बनाएँ — यह स्वयं गायब हो जाएगा।",
|
||||
karvanRelay: "सर्वर के माध्यम से",
|
||||
karvanRoomName: "कक्ष का नाम (वैकल्पिक)",
|
||||
karvanRooms: "कक्ष",
|
||||
karvanSay: "अस्थायी संदेश…",
|
||||
karvanSend: "भेजें",
|
||||
karvanTitle: "Karvan",
|
||||
karvanTtl: "स्वतः नष्ट होगा:",
|
||||
modulesKarvanDescription: "अस्थायी कक्ष: संदेश केवल मेमोरी में रहते हैं और स्वयं नष्ट हो जाते हैं।",
|
||||
modulesKarvanLabel: "Karvan",
|
||||
// Identidades
|
||||
identitiesActive: "उपयोग में",
|
||||
identitiesBackup: "पहचान निर्यात या आयात करें",
|
||||
|
|
@ -1123,7 +803,7 @@ module.exports = {
|
|||
spreadMore: "और"
|
||||
},
|
||||
ru: {
|
||||
// usabilidad (19/10/2026): votaciones, borrar, modulos, Karvan, pie
|
||||
// usabilidad (19/10/2026): votaciones, borrar, modulos, pie
|
||||
workflow_acampada: "Лагерь",
|
||||
tribeSectionsMore: "Ещё",
|
||||
tribeSectionsLabel: "Разделы",
|
||||
|
|
@ -1154,43 +834,11 @@ module.exports = {
|
|||
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)
|
||||
skinIntroMobile: "Выберите, как Oasis выглядит на телефоне. Применяется сразу по нажатию.",
|
||||
workflowsDescriptionMobile: "Рабочий процесс задаёт, какие модули включены. На телефоне ваше оформление не меняется.",
|
||||
// fotos que no se pueden limpiar (blobHandler.js)
|
||||
imageMetadataUnsafe: "Не удалось удалить метаданные этого изображения (местоположение, камера…), поэтому оно не загружено. Сохраните его как JPEG или PNG и попробуйте снова.",
|
||||
// Karvan
|
||||
karvanActive: "Активные комнаты",
|
||||
karvanBlurb: "Комнаты, которые живут только в памяти и уничтожаются сами.",
|
||||
karvanCall: "Позвонить",
|
||||
karvanCreate: "Создать эфемерную комнату",
|
||||
karvanEphemeral: "Эфемерные комнаты",
|
||||
karvanInvite: "Пригласить",
|
||||
karvanInviteBad: "✗ Это неверный @feed-id.",
|
||||
karvanInviteFail: "✗ Не удалось отправить приглашение.",
|
||||
karvanInvitePlaceholder: "Пригласить контакт по @feed-id…",
|
||||
karvanInviteSent: "✓ Приглашение отправлено во входящие.",
|
||||
karvanNone: "Активных комнат нет. Создайте — она исчезнет сама.",
|
||||
karvanRelay: "через сервер",
|
||||
karvanRoomName: "Название комнаты (необязательно)",
|
||||
karvanRooms: "Комнаты",
|
||||
karvanSay: "Временное сообщение…",
|
||||
karvanSend: "Отправить",
|
||||
karvanTitle: "Karvan",
|
||||
karvanTtl: "Самоуничтожение через:",
|
||||
modulesKarvanDescription: "Эфемерные комнаты: сообщения живут только в памяти и самоуничтожаются.",
|
||||
modulesKarvanLabel: "Karvan",
|
||||
// Identidades
|
||||
identitiesActive: "Используется",
|
||||
identitiesBackup: "Экспортировать или импортировать личность",
|
||||
|
|
|
|||
|
|
@ -1,317 +0,0 @@
|
|||
/* KARVAN client — mensajes temporales + WebRTC (data-channel).
|
||||
* Capa fiable: relay por servidor (RAM, autodestrucción) con polling.
|
||||
* Capa turbo: data-channel WebRTC P2P (instantáneo) con "perfect negotiation".
|
||||
* Todo degrada: si no hay WebRTC o no conecta, el chat sigue por el relay.
|
||||
* Sin cámara/mic (solo data-channel) => no necesita permisos del wrapper. */
|
||||
(function () {
|
||||
"use strict";
|
||||
var root = document.querySelector(".karvan-room");
|
||||
if (!root) return;
|
||||
var roomId = root.getAttribute("data-room");
|
||||
var selfId = root.getAttribute("data-self") || "";
|
||||
var peerId = "p" + Math.random().toString(16).slice(2, 12);
|
||||
var fromLabel = selfId || peerId;
|
||||
|
||||
var listEl = document.getElementById("karvan-msgs");
|
||||
var formEl = document.getElementById("karvan-form");
|
||||
var textEl = document.getElementById("karvan-text");
|
||||
var liveTxt = document.getElementById("karvan-live-txt");
|
||||
var liveWrap = document.getElementById("karvan-live");
|
||||
|
||||
var seen = Object.create(null);
|
||||
var lastSeq = 0;
|
||||
var lastSig = 0;
|
||||
var peers = Object.create(null);
|
||||
|
||||
// --- Fase 1: videollamada (media sobre el MISMO WebRTC del chat) ---
|
||||
var callRoot = document.getElementById("karvan-call");
|
||||
var localVid = document.getElementById("karvan-local");
|
||||
var remoteEl = document.getElementById("karvan-remote");
|
||||
var statusEl = document.getElementById("kc-status");
|
||||
var btnStart = document.getElementById("kc-start");
|
||||
var btnMic = document.getElementById("kc-mic");
|
||||
var btnCam = document.getElementById("kc-cam");
|
||||
var btnStop = document.getElementById("kc-stop");
|
||||
var localStream = (window.MediaStream ? new MediaStream() : null);
|
||||
if (localVid && localStream) localVid.srcObject = localStream;
|
||||
var micActive = false, camActive = false;
|
||||
|
||||
function esc(s) { return String(s == null ? "" : s); }
|
||||
function shortId(id) { id = esc(id || "?"); return id.charAt(0) === "@" ? id.slice(1, 7) : id.slice(0, 6); }
|
||||
|
||||
function showMsg(m, live) {
|
||||
var key = m.mid ? "m" + m.mid : "s" + (m.seq || Math.random());
|
||||
if (seen[key]) return;
|
||||
seen[key] = 1;
|
||||
var li = document.createElement("li");
|
||||
li.className = "karvan-msg" + (m.from === fromLabel ? " karvan-msg-self" : "") + (live ? " karvan-msg-live" : "");
|
||||
var f = document.createElement("span"); f.className = "km-from"; f.textContent = shortId(m.from);
|
||||
var t = document.createElement("span"); t.className = "km-text"; t.textContent = esc(m.text);
|
||||
li.appendChild(f); li.appendChild(t);
|
||||
listEl.appendChild(li);
|
||||
listEl.scrollTop = listEl.scrollHeight;
|
||||
}
|
||||
|
||||
// --- relay por servidor (siempre activo) ---
|
||||
function pollMsgs() {
|
||||
fetch("/karvan/" + roomId + "/msgs?since=" + lastSeq, { headers: { Accept: "application/json" } })
|
||||
.then(function (r) { return r.ok ? r.json() : null; })
|
||||
.then(function (d) {
|
||||
if (!d || !d.messages) return;
|
||||
d.messages.forEach(function (m) { if (m.seq > lastSeq) lastSeq = m.seq; showMsg(m, false); });
|
||||
})
|
||||
.catch(function () {});
|
||||
}
|
||||
|
||||
function sendMessage(text) {
|
||||
if (!text) return;
|
||||
var mid = peerId + "-" + Date.now().toString(36) + Math.random().toString(16).slice(2, 6);
|
||||
var m = { mid: mid, from: fromLabel, text: text };
|
||||
showMsg(m, false); // optimista
|
||||
broadcastDC({ kind: "chat", mid: mid, from: fromLabel, text: text }); // instantáneo a los pares
|
||||
fetch("/karvan/" + roomId + "/msg", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ from: fromLabel, text: text, mid: mid })
|
||||
}).catch(function () {});
|
||||
}
|
||||
|
||||
if (formEl) {
|
||||
formEl.addEventListener("submit", function (e) {
|
||||
e.preventDefault();
|
||||
var v = (textEl.value || "").trim();
|
||||
if (!v) return;
|
||||
textEl.value = "";
|
||||
sendMessage(v);
|
||||
});
|
||||
}
|
||||
|
||||
// --- señalización (mailbox RAM del servidor) ---
|
||||
function sendSig(to, payload) {
|
||||
fetch("/karvan/" + roomId + "/signal", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ from: peerId, to: to || "", payload: payload })
|
||||
}).catch(function () {});
|
||||
}
|
||||
function announce() { sendSig("", { kind: "hello" }); }
|
||||
|
||||
function pollSignals() {
|
||||
fetch("/karvan/" + roomId + "/signal?for=" + peerId + "&since=" + lastSig, { headers: { Accept: "application/json" } })
|
||||
.then(function (r) { return r.ok ? r.json() : null; })
|
||||
.then(function (d) {
|
||||
if (!d) return;
|
||||
(d.signals || []).forEach(function (s) {
|
||||
if (s.seq > lastSig) lastSig = s.seq;
|
||||
onSignal(s.from, s.payload);
|
||||
});
|
||||
(d.members || []).forEach(function (mid2) { if (mid2 && mid2 !== peerId) ensurePeer(mid2); });
|
||||
})
|
||||
.catch(function () {});
|
||||
}
|
||||
|
||||
// --- WebRTC (data-channel, perfect negotiation) ---
|
||||
var RTC = window.RTCPeerConnection || window.webkitRTCPeerConnection;
|
||||
// Sin servidores ICE por defecto: solo candidatos host (LAN y NAT amable).
|
||||
// Un STUN de terceros aprende la IP publica y el momento de cada llamada, asi que
|
||||
// no se pone ninguno de fabrica. Si el nodo ofrece TURN propio, /karvan/ice lo da.
|
||||
var rtcConfig = { iceServers: [] };
|
||||
|
||||
function setLive(on) {
|
||||
if (!liveTxt) return;
|
||||
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" : "");
|
||||
}
|
||||
|
||||
function broadcastDC(obj) {
|
||||
var s = JSON.stringify(obj);
|
||||
for (var k in peers) {
|
||||
var dc = peers[k] && peers[k].dc;
|
||||
if (dc && dc.readyState === "open") { try { dc.send(s); } catch (e) {} }
|
||||
}
|
||||
}
|
||||
|
||||
function attachDC(peer, dc) {
|
||||
peer.dc = dc;
|
||||
dc.onopen = function () { setLive(true); };
|
||||
dc.onmessage = function (e) {
|
||||
try { var m = JSON.parse(e.data); if (m && m.kind === "chat") showMsg(m, true); } catch (x) {}
|
||||
};
|
||||
dc.onclose = function () {
|
||||
var any = false;
|
||||
for (var k in peers) if (peers[k].dc && peers[k].dc.readyState === "open") any = true;
|
||||
if (!any) setLive(false);
|
||||
};
|
||||
}
|
||||
|
||||
function ensurePeer(remoteId) {
|
||||
if (peers[remoteId] || !RTC) return peers[remoteId];
|
||||
var polite = peerId > remoteId; // el id mayor es "polite"
|
||||
var pc = new RTC(rtcConfig);
|
||||
var peer = { pc: pc, dc: null, polite: polite, makingOffer: false, ignoreOffer: false, vid: null, remoteStream: null };
|
||||
peers[remoteId] = peer;
|
||||
|
||||
if (!polite) { attachDC(peer, pc.createDataChannel("chat")); } // el impolite inicia el canal
|
||||
pc.ondatachannel = function (e) { attachDC(peer, e.channel); };
|
||||
// media: publicar los tracks locales que ya tengamos (si la llamada está activa) + recibir los del par
|
||||
if (localStream) { localStream.getTracks().forEach(function (t) { try { pc.addTrack(t, localStream); } catch (e) {} }); }
|
||||
pc.ontrack = function (e) { attachRemote(peer, remoteId, e); };
|
||||
pc.onicecandidate = function (e) { if (e.candidate) sendSig(remoteId, { kind: "ice", candidate: e.candidate }); };
|
||||
pc.onnegotiationneeded = function () {
|
||||
peer.makingOffer = true;
|
||||
Promise.resolve()
|
||||
.then(function () { return pc.setLocalDescription(); })
|
||||
.then(function () { sendSig(remoteId, { kind: "desc", desc: pc.localDescription }); })
|
||||
.catch(function () {})
|
||||
.then(function () { peer.makingOffer = false; });
|
||||
};
|
||||
pc.onconnectionstatechange = function () {
|
||||
var st = pc.connectionState;
|
||||
if (st === "failed" || st === "closed") { try { pc.close(); } catch (e) {} removeRemoteVideo(peer); delete peers[remoteId]; }
|
||||
};
|
||||
return peer;
|
||||
}
|
||||
|
||||
// --- media / videollamada (Fase 1) ---
|
||||
function setCallActive(on) { if (callRoot) callRoot.className = "karvan-call" + (on ? " active" : "") + (camActive ? " has-cam" : ""); }
|
||||
function setStatus(msg) { if (statusEl) statusEl.textContent = msg || ""; }
|
||||
// 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() {
|
||||
if (btnMic) btnMic.className = "kc-btn" + (micActive ? " on" : "");
|
||||
if (btnCam) btnCam.className = "kc-btn" + (camActive ? " on" : "");
|
||||
// sin camara no se ensena el video propio: era un recuadro gris con el icono de play
|
||||
if (callRoot) callRoot.classList.toggle("has-cam", !!camActive);
|
||||
}
|
||||
|
||||
function attachRemote(peer, remoteId, e) {
|
||||
if (!remoteEl) return;
|
||||
if (!peer.vid) {
|
||||
peer.vid = document.createElement("video");
|
||||
peer.vid.autoplay = true; peer.vid.playsInline = true; peer.vid.className = "kc-video kc-remote-vid";
|
||||
peer.vid.setAttribute("data-peer", remoteId);
|
||||
remoteEl.appendChild(peer.vid);
|
||||
}
|
||||
if (e.streams && e.streams[0]) peer.vid.srcObject = e.streams[0];
|
||||
else { if (!peer.remoteStream) peer.remoteStream = new MediaStream(); peer.remoteStream.addTrack(e.track); peer.vid.srcObject = peer.remoteStream; }
|
||||
setCallActive(true);
|
||||
}
|
||||
function removeRemoteVideo(peer) {
|
||||
if (peer && peer.vid && peer.vid.parentNode) { try { peer.vid.parentNode.removeChild(peer.vid); } catch (_) {} peer.vid = null; }
|
||||
}
|
||||
|
||||
function addLocalTrack(track) {
|
||||
if (!localStream) return;
|
||||
localStream.addTrack(track);
|
||||
for (var k in peers) {
|
||||
var pc = peers[k].pc, senders = pc.getSenders(), existing = null;
|
||||
for (var s = 0; s < senders.length; s++) if (senders[s].track && senders[s].track.kind === track.kind) { existing = senders[s]; break; }
|
||||
if (existing) { try { existing.replaceTrack(track); } catch (_) {} } // swap (cambio de cámara) — sin renegociar
|
||||
else { try { pc.addTrack(track, localStream); } catch (_) {} } // kind nuevo — dispara onnegotiationneeded
|
||||
}
|
||||
}
|
||||
function removeLocalKind(kind) {
|
||||
if (!localStream) return;
|
||||
localStream.getTracks().filter(function (t) { return t.kind === kind; }).forEach(function (t) { t.stop(); localStream.removeTrack(t); });
|
||||
for (var k in peers) {
|
||||
var senders = peers[k].pc.getSenders();
|
||||
for (var s = 0; s < senders.length; s++) if (senders[s].track && senders[s].track.kind === kind) { try { senders[s].replaceTrack(null); } catch (_) {} }
|
||||
}
|
||||
}
|
||||
function onMediaErr(kind, x) {
|
||||
var name = (x && x.name) || "", msg = (x && x.message) || String(x);
|
||||
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 === "NotReadableError") setStatus("⚠ Dispositivo ocupado.");
|
||||
else setStatus("⚠ " + (name || "error") + ": " + msg);
|
||||
}
|
||||
function getMedia(kind) {
|
||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { setStatus("⚠ Cámara/micro no disponibles aquí."); return; }
|
||||
var c = (kind === "audio") ? { audio: true, video: false } : { audio: false, video: true };
|
||||
setStatus((kind === "audio" ? "🎤" : "🎥") + " pidiendo permiso…");
|
||||
navigator.mediaDevices.getUserMedia(c).then(function (s) {
|
||||
s.getTracks().forEach(addLocalTrack);
|
||||
if (kind === "audio") micActive = true; else camActive = true;
|
||||
setCallActive(true); updateBtns(); setStatus(statusText());
|
||||
}).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() {
|
||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { setStatus("⚠ Cámara/micro no disponibles aquí."); return; }
|
||||
if (micActive) return;
|
||||
getMedia("audio");
|
||||
}
|
||||
function hangUp() {
|
||||
if (localStream) localStream.getTracks().forEach(function (t) { t.stop(); localStream.removeTrack(t); });
|
||||
for (var k in peers) { var sn = peers[k].pc.getSenders(); for (var s = 0; s < sn.length; s++) { try { sn[s].replaceTrack(null); } catch (_) {} } }
|
||||
micActive = false; camActive = false; setCallActive(false); updateBtns(); setStatus("");
|
||||
}
|
||||
if (btnStart) btnStart.addEventListener("click", startCall);
|
||||
if (btnMic) btnMic.addEventListener("click", function () { if (micActive) { removeLocalKind("audio"); micActive = false; updateBtns(); setStatus(statusText()); } else getMedia("audio"); });
|
||||
if (btnCam) btnCam.addEventListener("click", function () { if (camActive) { removeLocalKind("video"); camActive = false; updateBtns(); setStatus(statusText()); } else getMedia("video"); });
|
||||
if (btnStop) btnStop.addEventListener("click", hangUp);
|
||||
|
||||
function onSignal(fromRemote, payload) {
|
||||
if (!RTC || !payload) return;
|
||||
var peer = ensurePeer(fromRemote);
|
||||
if (!peer) return;
|
||||
var pc = peer.pc;
|
||||
if (payload.kind === "desc") {
|
||||
var desc = payload.desc;
|
||||
if (!desc) return; // señal malformada (sin desc) — no reventar el lote de sondeo
|
||||
var collision = desc.type === "offer" && (peer.makingOffer || pc.signalingState !== "stable");
|
||||
peer.ignoreOffer = !peer.polite && collision;
|
||||
if (peer.ignoreOffer) return;
|
||||
Promise.resolve()
|
||||
.then(function () { return pc.setRemoteDescription(desc); })
|
||||
.then(function () {
|
||||
if (desc.type === "offer") {
|
||||
return pc.setLocalDescription().then(function () {
|
||||
sendSig(fromRemote, { kind: "desc", desc: pc.localDescription });
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(function () {});
|
||||
} else if (payload.kind === "ice") {
|
||||
Promise.resolve().then(function () { return pc.addIceCandidate(payload.candidate); }).catch(function () {});
|
||||
}
|
||||
}
|
||||
|
||||
// --- arranque ---
|
||||
showMsgInitialSeq();
|
||||
function showMsgInitialSeq() {
|
||||
// registrar los mensajes ya renderizados por el servidor para no duplicarlos
|
||||
var items = listEl ? listEl.querySelectorAll(".karvan-msg") : [];
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
var seq = parseInt(items[i].getAttribute("data-seq"), 10) || 0;
|
||||
var mid = items[i].getAttribute("data-mid") || "";
|
||||
if (seq > lastSeq) lastSeq = seq;
|
||||
seen[mid ? "m" + mid : "s" + seq] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
pollMsgs();
|
||||
setInterval(pollMsgs, 2000);
|
||||
if (RTC) {
|
||||
setLive(false);
|
||||
// La configuracion ICE tiene que estar resuelta antes de la primera
|
||||
// RTCPeerConnection: no se puede cambiar de forma fiable una ya creada.
|
||||
var iceReady = fetch("/karvan/ice", { headers: { accept: "application/json" } })
|
||||
.then(function (r) { return r.ok ? r.json() : null; })
|
||||
.then(function (d) {
|
||||
if (d && Array.isArray(d.iceServers) && d.iceServers.length) {
|
||||
rtcConfig = { iceServers: d.iceServers };
|
||||
if (d.relayOnly) rtcConfig.iceTransportPolicy = "relay";
|
||||
}
|
||||
})
|
||||
.catch(function () {});
|
||||
var iceTimeout = new Promise(function (res) { setTimeout(res, 2000); });
|
||||
Promise.race([iceReady, iceTimeout]).then(function () {
|
||||
announce();
|
||||
setInterval(announce, 8000);
|
||||
pollSignals();
|
||||
setInterval(pollSignals, 2000);
|
||||
});
|
||||
}
|
||||
})();
|
||||
Loading…
Add table
Add a link
Reference in a new issue